From c91e2e2e2994e28ec4caf032714ae84248faf718 Mon Sep 17 00:00:00 2001 From: Anush Kumar Date: Wed, 22 Oct 2025 17:40:25 -0700 Subject: [PATCH 1/2] feat(ingest): enhance SelectTemplateStep with no results message and improve Git repository fields --- .../__tests__/common-git-info.test.tsx | 115 ++++++++ .../__tests__/constants-git-info.test.tsx | 135 +++++++++ .../__tests__/git-info-validation.test.tsx | 148 ++++++++++ .../RecipeForm/__tests__/lookml.test.tsx | 278 ++++++++++++++++++ .../source/builder/RecipeForm/common.tsx | 19 +- .../source/builder/RecipeForm/constants.ts | 10 +- .../source/builder/RecipeForm/lookml.tsx | 115 ++++++-- .../source/builder/SelectTemplateStep.tsx | 37 ++- 8 files changed, 812 insertions(+), 45 deletions(-) create mode 100644 datahub-web-react/src/app/ingestV2/source/builder/RecipeForm/__tests__/common-git-info.test.tsx create mode 100644 datahub-web-react/src/app/ingestV2/source/builder/RecipeForm/__tests__/constants-git-info.test.tsx create mode 100644 datahub-web-react/src/app/ingestV2/source/builder/RecipeForm/__tests__/git-info-validation.test.tsx create mode 100644 datahub-web-react/src/app/ingestV2/source/builder/RecipeForm/__tests__/lookml.test.tsx diff --git a/datahub-web-react/src/app/ingestV2/source/builder/RecipeForm/__tests__/common-git-info.test.tsx b/datahub-web-react/src/app/ingestV2/source/builder/RecipeForm/__tests__/common-git-info.test.tsx new file mode 100644 index 00000000000000..2ed63279af1b32 --- /dev/null +++ b/datahub-web-react/src/app/ingestV2/source/builder/RecipeForm/__tests__/common-git-info.test.tsx @@ -0,0 +1,115 @@ +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import { Form } from 'antd'; + +import { GIT_INFO_REPO } from '@app/ingestV2/source/builder/RecipeForm/common'; +import { FieldType } from '@app/ingestV2/source/builder/RecipeForm/common'; + +// Mock FormField component for testing +const MockFormField = ({ field, removeMargin }) => { + const { name, label, tooltip, type, placeholder, rules, required } = field; + + return ( +
+ + {tooltip &&
{typeof tooltip === 'string' ? tooltip : 'React tooltip'}
} + +
+ ); +}; + +describe('Common Git Info Fields', () => { + describe('GIT_INFO_REPO', () => { + it('should have correct field properties', () => { + expect(GIT_INFO_REPO.name).toBe('git_info.repo'); + expect(GIT_INFO_REPO.label).toBe('Git Repository'); + expect(GIT_INFO_REPO.type).toBe(FieldType.TEXT); + expect(GIT_INFO_REPO.fieldPath).toBe('source.config.git_info.repo'); + expect(GIT_INFO_REPO.rules).toBeNull(); + }); + + it('should not be required by default', () => { + expect(GIT_INFO_REPO.required).toBeUndefined(); + }); + + it('should render tooltip with multi-platform examples', () => { + render( +
+ + + ); + + const tooltip = screen.getByTestId('tooltip-git_info.repo'); + expect(tooltip.textContent).toContain('React tooltip'); + }); + + it('should have updated from deprecated github_info', () => { + expect(GIT_INFO_REPO.name).not.toContain('github_info'); + expect(GIT_INFO_REPO.name).toContain('git_info'); + expect(GIT_INFO_REPO.label).not.toBe('GitHub Repo'); + expect(GIT_INFO_REPO.label).toBe('Git Repository'); + }); + + it('should support multiple Git platforms in tooltip', () => { + // Test that the tooltip contains information about multiple platforms + const tooltip = GIT_INFO_REPO.tooltip; + expect(tooltip).toBeDefined(); + + // Since tooltip is a React component, we test its structure + expect(React.isValidElement(tooltip)).toBe(true); + }); + }); + + describe('Field Configuration', () => { + it('should have correct field path structure', () => { + expect(GIT_INFO_REPO.fieldPath).toBe('source.config.git_info.repo'); + expect(GIT_INFO_REPO.fieldPath).toMatch(/^source\.config\.git_info\./); + }); + + it('should be a text input field', () => { + expect(GIT_INFO_REPO.type).toBe(FieldType.TEXT); + }); + + it('should not have validation rules', () => { + expect(GIT_INFO_REPO.rules).toBeNull(); + }); + }); + + describe('Multi-Platform Support', () => { + it('should have tooltip that mentions multiple platforms', () => { + // The tooltip should be a React component that includes examples + // for different Git platforms + expect(GIT_INFO_REPO.tooltip).toBeDefined(); + expect(React.isValidElement(GIT_INFO_REPO.tooltip)).toBe(true); + }); + + it('should support flexible repository URL formats', () => { + // The field should accept various repository URL formats + // This is tested by the fact that there are no strict validation rules + expect(GIT_INFO_REPO.rules).toBeNull(); + }); + }); + + describe('Backward Compatibility', () => { + it('should replace deprecated github_info field', () => { + // Ensure the field name has been updated from github_info to git_info + expect(GIT_INFO_REPO.name).toBe('git_info.repo'); + expect(GIT_INFO_REPO.name).not.toBe('github_info.repo'); + }); + + it('should have updated label from GitHub-specific to generic', () => { + expect(GIT_INFO_REPO.label).toBe('Git Repository'); + expect(GIT_INFO_REPO.label).not.toBe('GitHub Repo'); + }); + + it('should have updated field path', () => { + expect(GIT_INFO_REPO.fieldPath).toBe('source.config.git_info.repo'); + expect(GIT_INFO_REPO.fieldPath).not.toBe('source.config.github_info.repo'); + }); + }); +}); diff --git a/datahub-web-react/src/app/ingestV2/source/builder/RecipeForm/__tests__/constants-git-info.test.tsx b/datahub-web-react/src/app/ingestV2/source/builder/RecipeForm/__tests__/constants-git-info.test.tsx new file mode 100644 index 00000000000000..84db00b7b4cca4 --- /dev/null +++ b/datahub-web-react/src/app/ingestV2/source/builder/RecipeForm/__tests__/constants-git-info.test.tsx @@ -0,0 +1,135 @@ +import { LOOKML_GIT_INFO_REPO } from '@app/ingestV2/source/builder/RecipeForm/lookml'; +import { GIT_INFO_REPO } from '@app/ingestV2/source/builder/RecipeForm/common'; +import { LOOKML } from '@app/ingestV2/source/builder/RecipeForm/lookml'; +import { SOURCE_CONFIGS } from '@app/ingestV2/source/builder/RecipeForm/constants'; + +describe('Constants Git Info Integration', () => { + describe('Field Imports', () => { + it('should import LOOKML_GIT_INFO_REPO from lookml module', () => { + expect(LOOKML_GIT_INFO_REPO).toBeDefined(); + expect(LOOKML_GIT_INFO_REPO.name).toBe('git_info.repo'); + }); + + it('should import GIT_INFO_REPO from common module', () => { + expect(GIT_INFO_REPO).toBeDefined(); + expect(GIT_INFO_REPO.name).toBe('git_info.repo'); + }); + }); + + describe('Source Configuration Integration', () => { + it('should include LOOKML_GIT_INFO_REPO in LOOKML source config', () => { + const lookmlConfig = SOURCE_CONFIGS[LOOKML]; + expect(lookmlConfig).toBeDefined(); + expect(lookmlConfig.fields).toContain(LOOKML_GIT_INFO_REPO); + }); + + it('should have correct field order in LOOKML config', () => { + const lookmlConfig = SOURCE_CONFIGS[LOOKML]; + const fields = lookmlConfig.fields; + + // LOOKML_GIT_INFO_REPO should be the first field + expect(fields[0]).toBe(LOOKML_GIT_INFO_REPO); + }); + + it('should not contain deprecated github_info references', () => { + const lookmlConfig = SOURCE_CONFIGS[LOOKML]; + const fields = lookmlConfig.fields; + + // Check that no fields have github_info in their name + const githubInfoFields = fields.filter(field => + field.name && field.name.includes('github_info') + ); + expect(githubInfoFields).toHaveLength(0); + }); + + it('should contain git_info references', () => { + const lookmlConfig = SOURCE_CONFIGS[LOOKML]; + const fields = lookmlConfig.fields; + + // Check that fields have git_info in their name + const gitInfoFields = fields.filter(field => + field.name && field.name.includes('git_info') + ); + expect(gitInfoFields.length).toBeGreaterThan(0); + }); + }); + + describe('Field Consistency', () => { + it('should have consistent field paths for git_info', () => { + const lookmlConfig = SOURCE_CONFIGS[LOOKML]; + const fields = lookmlConfig.fields; + + const gitInfoFields = fields.filter(field => + field.name && field.name.includes('git_info') + ); + + gitInfoFields.forEach(field => { + expect(field.fieldPath).toMatch(/^source\.config\.git_info\./); + }); + }); + + it('should have proper field types for git_info fields', () => { + const lookmlConfig = SOURCE_CONFIGS[LOOKML]; + const fields = lookmlConfig.fields; + + const gitInfoFields = fields.filter(field => + field.name && field.name.includes('git_info') + ); + + gitInfoFields.forEach(field => { + expect(field.type).toBeDefined(); + expect(['TEXT', 'SECRET']).toContain(field.type); + }); + }); + }); + + describe('Migration from github_info', () => { + it('should not have any github_info field references', () => { + const lookmlConfig = SOURCE_CONFIGS[LOOKML]; + const fields = lookmlConfig.fields; + + // Ensure no fields reference the old github_info structure + const oldFieldPaths = fields.filter(field => + field.fieldPath && field.fieldPath.includes('github_info') + ); + expect(oldFieldPaths).toHaveLength(0); + }); + + it('should have updated field names from github_info to git_info', () => { + const lookmlConfig = SOURCE_CONFIGS[LOOKML]; + const fields = lookmlConfig.fields; + + const gitInfoFields = fields.filter(field => + field.name && field.name.includes('git_info') + ); + + expect(gitInfoFields.length).toBeGreaterThan(0); + + gitInfoFields.forEach(field => { + expect(field.name).toMatch(/^git_info\./); + expect(field.name).not.toMatch(/^github_info\./); + }); + }); + }); + + describe('Field Validation', () => { + it('should have required fields properly marked', () => { + const lookmlConfig = SOURCE_CONFIGS[LOOKML]; + const fields = lookmlConfig.fields; + + const requiredFields = fields.filter(field => field.required === true); + expect(requiredFields.length).toBeGreaterThan(0); + }); + + it('should have validation rules for required fields', () => { + const lookmlConfig = SOURCE_CONFIGS[LOOKML]; + const fields = lookmlConfig.fields; + + const fieldsWithRules = fields.filter(field => + field.rules && field.rules.length > 0 + ); + + expect(fieldsWithRules.length).toBeGreaterThan(0); + }); + }); +}); diff --git a/datahub-web-react/src/app/ingestV2/source/builder/RecipeForm/__tests__/git-info-validation.test.tsx b/datahub-web-react/src/app/ingestV2/source/builder/RecipeForm/__tests__/git-info-validation.test.tsx new file mode 100644 index 00000000000000..2ba43d1a6444ad --- /dev/null +++ b/datahub-web-react/src/app/ingestV2/source/builder/RecipeForm/__tests__/git-info-validation.test.tsx @@ -0,0 +1,148 @@ +import { LOOKML_GIT_INFO_REPO_SSH_LOCATOR } from '@app/ingestV2/source/builder/RecipeForm/lookml'; + +describe('Git Info Validation Logic', () => { + describe('REPO_SSH_LOCATOR Validation', () => { + const createValidator = (repoValue) => { + return LOOKML_GIT_INFO_REPO_SSH_LOCATOR.rules![0]({ + getFieldValue: (fieldName) => { + if (fieldName === 'git_info.repo') return repoValue; + return undefined; + } + }); + }; + + describe('GitHub Repository Detection', () => { + it('should not require SSH locator for GitHub short format', async () => { + const validator = createValidator('datahub-project/datahub'); + await expect(validator.validator({}, '')).resolves.toBeUndefined(); + }); + + it('should not require SSH locator for GitHub full URL', async () => { + const validator = createValidator('https://github.com/datahub-project/datahub'); + await expect(validator.validator({}, '')).resolves.toBeUndefined(); + }); + + it('should not require SSH locator for GitHub URL without protocol', async () => { + const validator = createValidator('github.com/datahub-project/datahub'); + await expect(validator.validator({}, '')).resolves.toBeUndefined(); + }); + + it('should not require SSH locator for GitHub URL with trailing slash', async () => { + const validator = createValidator('https://github.com/datahub-project/datahub/'); + await expect(validator.validator({}, '')).resolves.toBeUndefined(); + }); + }); + + describe('GitLab Repository Detection', () => { + it('should not require SSH locator for GitLab full URL', async () => { + const validator = createValidator('https://gitlab.com/gitlab-org/gitlab'); + await expect(validator.validator({}, '')).resolves.toBeUndefined(); + }); + + it('should not require SSH locator for GitLab URL without protocol', async () => { + const validator = createValidator('gitlab.com/gitlab-org/gitlab'); + await expect(validator.validator({}, '')).resolves.toBeUndefined(); + }); + + it('should not require SSH locator for GitLab URL with trailing slash', async () => { + const validator = createValidator('https://gitlab.com/gitlab-org/gitlab/'); + await expect(validator.validator({}, '')).resolves.toBeUndefined(); + }); + }); + + describe('Other Git Platforms', () => { + it('should require SSH locator for Bitbucket', async () => { + const validator = createValidator('https://bitbucket.org/org/repo'); + await expect(validator.validator({}, '')).rejects.toThrow( + 'Repository SSH Locator is required for Git platforms other than GitHub and GitLab' + ); + }); + + it('should require SSH locator for custom Git server', async () => { + const validator = createValidator('https://custom-git.com/org/repo'); + await expect(validator.validator({}, '')).rejects.toThrow( + 'Repository SSH Locator is required for Git platforms other than GitHub and GitLab' + ); + }); + + it('should require SSH locator for SSH URL format', async () => { + const validator = createValidator('git@custom-server.com:org/repo'); + await expect(validator.validator({}, '')).rejects.toThrow( + 'Repository SSH Locator is required for Git platforms other than GitHub and GitLab' + ); + }); + + it('should not require SSH locator when SSH locator is provided', async () => { + const validator = createValidator('https://custom-git.com/org/repo'); + await expect(validator.validator({}, 'git@custom-git.com:org/repo.git')).resolves.toBeUndefined(); + }); + }); + + describe('Edge Cases', () => { + it('should not require SSH locator when repo is undefined', async () => { + const validator = createValidator(undefined); + await expect(validator.validator({}, '')).resolves.toBeUndefined(); + }); + + it('should not require SSH locator when repo is null', async () => { + const validator = createValidator(null); + await expect(validator.validator({}, '')).resolves.toBeUndefined(); + }); + + it('should not require SSH locator when repo is empty string', async () => { + const validator = createValidator(''); + await expect(validator.validator({}, '')).resolves.toBeUndefined(); + }); + + it('should handle case-insensitive GitHub detection', async () => { + const validator = createValidator('https://GITHUB.COM/datahub-project/datahub'); + await expect(validator.validator({}, '')).resolves.toBeUndefined(); + }); + + it('should handle case-insensitive GitLab detection', async () => { + const validator = createValidator('https://GITLAB.COM/gitlab-org/gitlab'); + await expect(validator.validator({}, '')).resolves.toBeUndefined(); + }); + }); + + describe('Complex Repository URLs', () => { + it('should handle GitHub URLs with additional path segments', async () => { + const validator = createValidator('https://github.com/datahub-project/datahub/tree/main/src'); + await expect(validator.validator({}, '')).resolves.toBeUndefined(); + }); + + it('should handle GitLab URLs with additional path segments', async () => { + const validator = createValidator('https://gitlab.com/gitlab-org/gitlab/-/tree/main/src'); + await expect(validator.validator({}, '')).resolves.toBeUndefined(); + }); + + it('should require SSH locator for non-GitHub/GitLab URLs with similar patterns', async () => { + const validator = createValidator('https://github-enterprise.company.com/org/repo'); + await expect(validator.validator({}, '')).rejects.toThrow( + 'Repository SSH Locator is required for Git platforms other than GitHub and GitLab' + ); + }); + }); + }); + + describe('Validation Error Messages', () => { + it('should provide clear error message for missing SSH locator', async () => { + const validator = createValidator('https://custom-git.com/org/repo'); + try { + await validator.validator({}, ''); + } catch (error) { + expect(error.message).toBe('Repository SSH Locator is required for Git platforms other than GitHub and GitLab'); + } + }); + }); + + // Helper function for creating validators + function createValidator(repoValue) { + return LOOKML_GIT_INFO_REPO_SSH_LOCATOR.rules![0]({ + getFieldValue: (fieldName) => { + if (fieldName === 'git_info.repo') return repoValue; + return undefined; + } + }); + } +}); diff --git a/datahub-web-react/src/app/ingestV2/source/builder/RecipeForm/__tests__/lookml.test.tsx b/datahub-web-react/src/app/ingestV2/source/builder/RecipeForm/__tests__/lookml.test.tsx new file mode 100644 index 00000000000000..5b27791b94d9db --- /dev/null +++ b/datahub-web-react/src/app/ingestV2/source/builder/RecipeForm/__tests__/lookml.test.tsx @@ -0,0 +1,278 @@ +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import { Form } from 'antd'; + +import { + LOOKML_GIT_INFO_REPO, + LOOKML_GIT_INFO_DEPLOY_KEY, + LOOKML_GIT_INFO_REPO_SSH_LOCATOR, + LOOKML_BASE_URL, + LOOKML_CLIENT_ID, + LOOKML_CLIENT_SECRET, + PROJECT_NAME, + PARSE_TABLE_NAMES_FROM_SQL, + CONNECTION_TO_PLATFORM_MAP, +} from '@app/ingestV2/source/builder/RecipeForm/lookml'; +import { FieldType } from '@app/ingestV2/source/builder/RecipeForm/common'; + +// Mock FormField component for testing +const MockFormField = ({ field, removeMargin }) => { + const { name, label, tooltip, type, placeholder, rules, required } = field; + + return ( +
+ + {tooltip &&
{typeof tooltip === 'string' ? tooltip : 'React tooltip'}
} + +
+ ); +}; + +describe('LookML Git Info Fields', () => { + describe('LOOKML_GIT_INFO_REPO', () => { + it('should have correct field properties', () => { + expect(LOOKML_GIT_INFO_REPO.name).toBe('git_info.repo'); + expect(LOOKML_GIT_INFO_REPO.label).toBe('Git Repository'); + expect(LOOKML_GIT_INFO_REPO.type).toBe(FieldType.TEXT); + expect(LOOKML_GIT_INFO_REPO.fieldPath).toBe('source.config.git_info.repo'); + expect(LOOKML_GIT_INFO_REPO.required).toBe(true); + expect(LOOKML_GIT_INFO_REPO.placeholder).toBe('datahub-project/datahub or https://github.com/datahub-project/datahub'); + }); + + it('should have validation rules', () => { + expect(LOOKML_GIT_INFO_REPO.rules).toHaveLength(1); + expect(LOOKML_GIT_INFO_REPO.rules![0].required).toBe(true); + expect(LOOKML_GIT_INFO_REPO.rules![0].message).toBe('Git Repository is required'); + }); + + it('should render tooltip with multi-platform support', () => { + render( +
+ + + ); + + const tooltip = screen.getByTestId('tooltip-git_info.repo'); + expect(tooltip.textContent).toContain('React tooltip'); + }); + }); + + describe('DEPLOY_KEY', () => { + it('should have correct field properties', () => { + expect(LOOKML_GIT_INFO_DEPLOY_KEY.name).toBe('git_info.deploy_key'); + expect(LOOKML_GIT_INFO_DEPLOY_KEY.label).toBe('Git Deploy Key'); + expect(LOOKML_GIT_INFO_DEPLOY_KEY.type).toBe(FieldType.SECRET); + expect(LOOKML_GIT_INFO_DEPLOY_KEY.fieldPath).toBe('source.config.git_info.deploy_key'); + expect(LOOKML_GIT_INFO_DEPLOY_KEY.required).toBe(true); + expect(LOOKML_GIT_INFO_DEPLOY_KEY.placeholder).toBe('-----BEGIN OPENSSH PRIVATE KEY-----\n...'); + }); + + it('should have validation rules', () => { + expect(LOOKML_GIT_INFO_DEPLOY_KEY.rules).toHaveLength(1); + expect(LOOKML_GIT_INFO_DEPLOY_KEY.rules![0].required).toBe(true); + expect(LOOKML_GIT_INFO_DEPLOY_KEY.rules![0].message).toBe('Git Deploy Key is required'); + }); + + it('should have setValueOnRecipeOverride function', () => { + expect(typeof LOOKML_GIT_INFO_DEPLOY_KEY.setValueOnRecipeOverride).toBe('function'); + + const recipe = { source: { config: {} } }; + const result = LOOKML_GIT_INFO_DEPLOY_KEY.setValueOnRecipeOverride!(recipe, 'test-key'); + expect(result.source.config.git_info.deploy_key).toBe('test-key\n'); + }); + + it('should render tooltip with multi-platform links', () => { + render( +
+ + + ); + + const tooltip = screen.getByTestId('tooltip-git_info.deploy_key'); + expect(tooltip.textContent).toContain('React tooltip'); + }); + }); + + describe('REPO_SSH_LOCATOR', () => { + it('should have correct field properties', () => { + expect(LOOKML_GIT_INFO_REPO_SSH_LOCATOR.name).toBe('git_info.repo_ssh_locator'); + expect(LOOKML_GIT_INFO_REPO_SSH_LOCATOR.label).toBe('Repository SSH Locator'); + expect(LOOKML_GIT_INFO_REPO_SSH_LOCATOR.type).toBe(FieldType.TEXT); + expect(LOOKML_GIT_INFO_REPO_SSH_LOCATOR.fieldPath).toBe('source.config.git_info.repo_ssh_locator'); + expect(LOOKML_GIT_INFO_REPO_SSH_LOCATOR.placeholder).toBe('git@your-git-server.com:org/repo.git'); + }); + + it('should have conditional validation rules', () => { + expect(LOOKML_GIT_INFO_REPO_SSH_LOCATOR.rules).toHaveLength(1); + expect(typeof LOOKML_GIT_INFO_REPO_SSH_LOCATOR.rules![0]).toBe('function'); + }); + + it('should validate GitHub repos do not require SSH locator', async () => { + const validator = LOOKML_GIT_INFO_REPO_SSH_LOCATOR.rules![0]({ + getFieldValue: (fieldName) => { + if (fieldName === 'git_info.repo') return 'datahub-project/datahub'; + return undefined; + } + }); + + const result = await validator.validator({}, ''); + expect(result).toBeUndefined(); // Should not throw error + }); + + it('should validate GitLab repos do not require SSH locator', async () => { + const validator = LOOKML_GIT_INFO_REPO_SSH_LOCATOR.rules![0]({ + getFieldValue: (fieldName) => { + if (fieldName === 'git_info.repo') return 'https://gitlab.com/gitlab-org/gitlab'; + return undefined; + } + }); + + const result = await validator.validator({}, ''); + expect(result).toBeUndefined(); // Should not throw error + }); + + it('should require SSH locator for non-GitHub/GitLab repos', async () => { + const validator = LOOKML_GIT_INFO_REPO_SSH_LOCATOR.rules![0]({ + getFieldValue: (fieldName) => { + if (fieldName === 'git_info.repo') return 'https://custom-git.com/org/repo'; + return undefined; + } + }); + + try { + await validator.validator({}, ''); + expect(true).toBe(false); // Should not reach here + } catch (error: any) { + expect(error.message).toBe('Repository SSH Locator is required for Git platforms other than GitHub and GitLab'); + } + }); + + it('should not require SSH locator when repo is not provided', async () => { + const validator = LOOKML_GIT_INFO_REPO_SSH_LOCATOR.rules![0]({ + getFieldValue: (fieldName) => { + if (fieldName === 'git_info.repo') return undefined; + return undefined; + } + }); + + const result = await validator.validator({}, ''); + expect(result).toBeUndefined(); // Should not throw error + }); + + it('should render tooltip with examples', () => { + render( +
+ + + ); + + const tooltip = screen.getByTestId('tooltip-git_info.repo_ssh_locator'); + expect(tooltip.textContent).toContain('React tooltip'); + }); + }); + + describe('Field Integration', () => { + it('should have consistent field paths for git_info', () => { + expect(LOOKML_GIT_INFO_REPO.fieldPath).toBe('source.config.git_info.repo'); + expect(LOOKML_GIT_INFO_DEPLOY_KEY.fieldPath).toBe('source.config.git_info.deploy_key'); + expect(LOOKML_GIT_INFO_REPO_SSH_LOCATOR.fieldPath).toBe('source.config.git_info.repo_ssh_locator'); + }); + + it('should have proper field types', () => { + expect(LOOKML_GIT_INFO_REPO.type).toBe(FieldType.TEXT); + expect(LOOKML_GIT_INFO_DEPLOY_KEY.type).toBe(FieldType.SECRET); + expect(LOOKML_GIT_INFO_REPO_SSH_LOCATOR.type).toBe(FieldType.TEXT); + }); + + it('should have required fields marked correctly', () => { + expect(LOOKML_GIT_INFO_REPO.required).toBe(true); + expect(LOOKML_GIT_INFO_DEPLOY_KEY.required).toBe(true); + expect(LOOKML_GIT_INFO_REPO_SSH_LOCATOR.required).toBeUndefined(); // Conditional requirement + }); + }); + + describe('Backward Compatibility', () => { + it('should use git_info instead of deprecated github_info', () => { + expect(LOOKML_GIT_INFO_REPO.name).not.toContain('github_info'); + expect(LOOKML_GIT_INFO_DEPLOY_KEY.name).not.toContain('github_info'); + expect(LOOKML_GIT_INFO_REPO_SSH_LOCATOR.name).not.toContain('github_info'); + + expect(LOOKML_GIT_INFO_REPO.name).toContain('git_info'); + expect(LOOKML_GIT_INFO_DEPLOY_KEY.name).toContain('git_info'); + expect(LOOKML_GIT_INFO_REPO_SSH_LOCATOR.name).toContain('git_info'); + }); + + it('should have updated labels from GitHub-specific to Git-generic', () => { + expect(LOOKML_GIT_INFO_REPO.label).toBe('Git Repository'); + expect(LOOKML_GIT_INFO_DEPLOY_KEY.label).toBe('Git Deploy Key'); + expect(LOOKML_GIT_INFO_REPO_SSH_LOCATOR.label).toBe('Repository SSH Locator'); + }); + }); + + describe('Multi-Platform Support', () => { + it('should support GitHub repositories', () => { + const githubRepos = [ + 'datahub-project/datahub', + 'https://github.com/datahub-project/datahub', + 'github.com/datahub-project/datahub' + ]; + + githubRepos.forEach(repo => { + const validator = LOOKML_GIT_INFO_REPO_SSH_LOCATOR.rules![0]({ + getFieldValue: (fieldName) => { + if (fieldName === 'git_info.repo') return repo; + return undefined; + } + }); + + expect(async () => { + await validator.validator({}, ''); + }).not.toThrow(); + }); + }); + + it('should support GitLab repositories', () => { + const gitlabRepos = [ + 'https://gitlab.com/gitlab-org/gitlab', + 'gitlab.com/gitlab-org/gitlab' + ]; + + gitlabRepos.forEach(repo => { + const validator = LOOKML_GIT_INFO_REPO_SSH_LOCATOR.rules![0]({ + getFieldValue: (fieldName) => { + if (fieldName === 'git_info.repo') return repo; + return undefined; + } + }); + + expect(async () => { + await validator.validator({}, ''); + }).not.toThrow(); + }); + }); + + it('should require SSH locator for other Git platforms', async () => { + const otherPlatformRepos = [ + 'https://bitbucket.org/org/repo', + 'https://custom-git.com/org/repo', + 'https://git.company.com/org/repo' + ]; + + for (const repo of otherPlatformRepos) { + const validator = LOOKML_GIT_INFO_REPO_SSH_LOCATOR.rules![0]({ + getFieldValue: (fieldName) => { + if (fieldName === 'git_info.repo') return repo; + return undefined; + } + }); + + await expect(validator.validator({}, '')).rejects.toThrow('Repository SSH Locator is required for Git platforms other than GitHub and GitLab'); + } + }); + }); +}); diff --git a/datahub-web-react/src/app/ingestV2/source/builder/RecipeForm/common.tsx b/datahub-web-react/src/app/ingestV2/source/builder/RecipeForm/common.tsx index 3a8a19927401e1..46b1e174c25462 100644 --- a/datahub-web-react/src/app/ingestV2/source/builder/RecipeForm/common.tsx +++ b/datahub-web-react/src/app/ingestV2/source/builder/RecipeForm/common.tsx @@ -411,22 +411,23 @@ export const INCLUDE_VIEWS: RecipeField = { rules: null, }; -export const GITHUB_INFO_REPO: RecipeField = { - name: 'github_info.repo', - label: 'GitHub Repo', +export const GIT_INFO_REPO: RecipeField = { + name: 'git_info.repo', + label: 'Git Repository', tooltip: (

- Name of your github repo. e.g. repo for{' '} - - https://github.com/datahub-project/datahub - {' '} - is datahub-project/datahub. + URL or name of your Git repository. Supports GitHub, GitLab, and other Git platforms. Examples:

+
    +
  • GitHub: datahub-project/datahub or https://github.com/datahub-project/datahub
  • +
  • GitLab: https://gitlab.com/gitlab-org/gitlab
  • +
  • Other platforms: https://your-git-server.com/org/repo
  • +
), type: FieldType.TEXT, - fieldPath: 'source.config.github_info.repo', + fieldPath: 'source.config.git_info.repo', rules: null, }; diff --git a/datahub-web-react/src/app/ingestV2/source/builder/RecipeForm/constants.ts b/datahub-web-react/src/app/ingestV2/source/builder/RecipeForm/constants.ts index 1197ec6b4e00fa..d0fb2033c10b1b 100644 --- a/datahub-web-react/src/app/ingestV2/source/builder/RecipeForm/constants.ts +++ b/datahub-web-react/src/app/ingestV2/source/builder/RecipeForm/constants.ts @@ -101,12 +101,13 @@ import { } from '@app/ingestV2/source/builder/RecipeForm/looker'; import { CONNECTION_TO_PLATFORM_MAP, - DEPLOY_KEY, + LOOKML_GIT_INFO_DEPLOY_KEY, + LOOKML_GIT_INFO_REPO_SSH_LOCATOR, LOOKML, LOOKML_BASE_URL, LOOKML_CLIENT_ID, LOOKML_CLIENT_SECRET, - LOOKML_GITHUB_INFO_REPO, + LOOKML_GIT_INFO_REPO, PARSE_TABLE_NAMES_FROM_SQL, PROJECT_NAME, } from '@app/ingestV2/source/builder/RecipeForm/lookml'; @@ -356,8 +357,9 @@ export const RECIPE_FIELDS: RecipeFields = { }, [LOOKML]: { fields: [ - LOOKML_GITHUB_INFO_REPO, - DEPLOY_KEY, + LOOKML_GIT_INFO_REPO, + LOOKML_GIT_INFO_REPO_SSH_LOCATOR, + LOOKML_GIT_INFO_DEPLOY_KEY, PROJECT_NAME, LOOKML_BASE_URL, LOOKML_CLIENT_ID, diff --git a/datahub-web-react/src/app/ingestV2/source/builder/RecipeForm/lookml.tsx b/datahub-web-react/src/app/ingestV2/source/builder/RecipeForm/lookml.tsx index 7e1d0deb7f1d22..cc9835183182ed 100644 --- a/datahub-web-react/src/app/ingestV2/source/builder/RecipeForm/lookml.tsx +++ b/datahub-web-react/src/app/ingestV2/source/builder/RecipeForm/lookml.tsx @@ -5,42 +5,66 @@ import { FieldType, RecipeField, setFieldValueOnRecipe } from '@app/ingestV2/sou export const LOOKML = 'lookml'; -export const LOOKML_GITHUB_INFO_REPO: RecipeField = { - name: 'github_info.repo', - label: 'GitHub Repo', - tooltip: 'The name of the GitHub repository where your LookML is defined.', +export const LOOKML_GIT_INFO_REPO: RecipeField = { + name: 'git_info.repo', + label: 'Git Repository', + tooltip: ( + <> +

+ Name of your GitHub repository or the URL of your Git repository. Supports GitHub, GitLab, and other Git platforms. Examples: +

    +
  • GitHub: datahub-project/datahub or https://github.com/datahub-project/datahub
  • +
  • GitLab: https://gitlab.com/gitlab-org/gitlab
  • +
  • Other platforms: https://your-git-server.com/org/repo (Repository SSH Locator is required)
  • +
+

+ + ), type: FieldType.TEXT, - fieldPath: 'source.config.github_info.repo', - placeholder: 'datahub-project/datahub', - rules: [{ required: true, message: 'Github Repo is required' }], + fieldPath: 'source.config.git_info.repo', + placeholder: 'datahub-project/datahub or https://github.com/datahub-project/datahub', + rules: [{ required: true, message: 'Git Repository is required' }], required: true, }; -const deployKeyFieldPath = 'source.config.github_info.deploy_key'; -export const DEPLOY_KEY: RecipeField = { - name: 'github_info.deploy_key', - label: 'GitHub Deploy Key', +const deployKeyFieldPath = 'source.config.git_info.deploy_key'; +export const LOOKML_GIT_INFO_DEPLOY_KEY: RecipeField = { + name: 'git_info.deploy_key', + label: 'Git Deploy Key', tooltip: ( <> - An SSH private key that has been provisioned for read access on the GitHub repository where the LookML is + An SSH private key that has been provisioned for read access on the Git repository where the LookML is defined.
- Learn how to generate an SSH for your GitHub repository{' '} - - here - - . + Learn how to generate SSH keys for your Git platform: +
), - type: FieldType.TEXTAREA, - fieldPath: 'source.config.github_info.deploy_key', + type: FieldType.SECRET, + fieldPath: 'source.config.git_info.deploy_key', placeholder: '-----BEGIN OPENSSH PRIVATE KEY-----\n...', - rules: [{ required: true, message: 'Github Deploy Key is required' }], + rules: [{ required: true, message: 'Git Deploy Key is required' }], setValueOnRecipeOverride: (recipe: any, value: string) => { const valueWithNewLine = `${value}\n`; return setFieldValueOnRecipe(recipe, valueWithNewLine, deployKeyFieldPath); @@ -48,6 +72,49 @@ export const DEPLOY_KEY: RecipeField = { required: true, }; +export const LOOKML_GIT_INFO_REPO_SSH_LOCATOR: RecipeField = { + name: 'git_info.repo_ssh_locator', + label: 'Repository SSH Locator', + tooltip: ( + <> + The SSH URL to clone the repository. Required for Git platforms other than GitHub and GitLab. +
+ Examples: +
    +
  • GitHub: git@github.com:datahub-project/datahub.git
  • +
  • GitLab: git@gitlab.com:gitlab-org/gitlab.git
  • +
  • Other platforms: git@your-git-server.com:org/repo.git
  • +
+
+ + ), + type: FieldType.TEXT, + fieldPath: 'source.config.git_info.repo_ssh_locator', + placeholder: 'git@your-git-server.com:org/repo.git', + rules: [ + ({ getFieldValue }) => ({ + validator(_, value) { + const repo = getFieldValue('git_info.repo'); + if (!repo) return Promise.resolve(); + + // Check if it's GitHub or GitLab (these are auto-inferred) + const isGitHub = repo.toLowerCase().includes('github.com') || + (!repo.includes('://') && repo.split('/').length === 2) || + repo.startsWith('git@github.com:'); + const isGitLab = repo.toLowerCase().includes('gitlab.com') || + repo.startsWith('git@gitlab.com:'); + + if (!isGitHub && !isGitLab && !value) { + return Promise.reject( + new Error('Repository SSH Locator is required for Git platforms other than GitHub and GitLab') + ); + } + return Promise.resolve(); + }, + }), + ], +}; + function validateApiSection(getFieldValue, fieldName) { return { validator(_, value) { diff --git a/datahub-web-react/src/app/ingestV2/source/builder/SelectTemplateStep.tsx b/datahub-web-react/src/app/ingestV2/source/builder/SelectTemplateStep.tsx index 4cbe195b48a735..3de7f5482fdfc9 100644 --- a/datahub-web-react/src/app/ingestV2/source/builder/SelectTemplateStep.tsx +++ b/datahub-web-react/src/app/ingestV2/source/builder/SelectTemplateStep.tsx @@ -1,6 +1,6 @@ import { FormOutlined, SearchOutlined } from '@ant-design/icons'; import { Input } from 'antd'; -import React, { useState } from 'react'; +import React, { useEffect, useRef, useState } from 'react'; import styled from 'styled-components'; import { ANTD_GRAY } from '@app/entity/shared/constants'; @@ -54,6 +54,17 @@ const PlatformListContainer = styled.div` padding-right: 12px; `; +const NoResultsMessage = styled.div` + grid-column: 1 / -1; + display: flex; + justify-content: center; + align-items: center; + padding: 40px 20px; + color: #666; + font-size: 16px; + text-align: center; +`; + interface SourceOptionProps { source: SourceConfig; onClick: () => void; @@ -86,12 +97,18 @@ export const SelectTemplateStep = ({ state, updateState, goTo, - cancel, ingestionSources, setSelectedSourceType, }: StepProps) => { const [searchFilter, setSearchFilter] = useState(''); + // Callback ref that focuses immediately when the element is attached + const searchInputCallbackRef = (node: any) => { + if (node) { + node.focus(); + } + }; + const onSelectTemplate = (type: string) => { const newState: SourceBuilderState = { ...state, @@ -126,6 +143,7 @@ export const SelectTemplateStep = ({
setSearchFilter(e.target.value)} @@ -134,14 +152,17 @@ export const SelectTemplateStep = ({ /> - {filteredSources.map((source) => ( - onSelectTemplate(source.name)} /> - ))} + {filteredSources.length > 0 ? ( + filteredSources.map((source) => ( + onSelectTemplate(source.name)} /> + )) + ) : ( + + Data Source with name "{searchFilter}" not found. + + )}
- ); }; From a66ad534c90d8dcb65719c2d0c927269f24c2e11 Mon Sep 17 00:00:00 2001 From: Anush Kumar Date: Thu, 23 Oct 2025 15:11:20 -0700 Subject: [PATCH 2/2] Lint fixes --- .../__tests__/common-git-info.test.tsx | 30 ++--- .../__tests__/constants-git-info.test.tsx | 107 ++++++++---------- .../__tests__/git-info-validation.test.tsx | 42 +++---- .../RecipeForm/__tests__/lookml.test.tsx | 100 ++++++++-------- .../source/builder/RecipeForm/common.tsx | 4 +- .../source/builder/RecipeForm/constants.ts | 4 +- .../source/builder/RecipeForm/lookml.tsx | 27 ++--- .../source/builder/SelectTemplateStep.tsx | 13 ++- 8 files changed, 152 insertions(+), 175 deletions(-) diff --git a/datahub-web-react/src/app/ingestV2/source/builder/RecipeForm/__tests__/common-git-info.test.tsx b/datahub-web-react/src/app/ingestV2/source/builder/RecipeForm/__tests__/common-git-info.test.tsx index 2ed63279af1b32..41f554c400b164 100644 --- a/datahub-web-react/src/app/ingestV2/source/builder/RecipeForm/__tests__/common-git-info.test.tsx +++ b/datahub-web-react/src/app/ingestV2/source/builder/RecipeForm/__tests__/common-git-info.test.tsx @@ -1,20 +1,22 @@ -import React from 'react'; import { render, screen } from '@testing-library/react'; import { Form } from 'antd'; +import React from 'react'; -import { GIT_INFO_REPO } from '@app/ingestV2/source/builder/RecipeForm/common'; -import { FieldType } from '@app/ingestV2/source/builder/RecipeForm/common'; +import { FieldType, GIT_INFO_REPO } from '@app/ingestV2/source/builder/RecipeForm/common'; // Mock FormField component for testing -const MockFormField = ({ field, removeMargin }) => { +const MockFormField = ({ field, removeMargin: _removeMargin }: { field: any; removeMargin: boolean }) => { const { name, label, tooltip, type, placeholder, rules, required } = field; - + return (
- - {tooltip &&
{typeof tooltip === 'string' ? tooltip : 'React tooltip'}
} - {label} + {tooltip && ( +
{typeof tooltip === 'string' ? tooltip : 'React tooltip'}
+ )} + { it('should render tooltip with multi-platform examples', () => { render(
- - + , + , ); - + const tooltip = screen.getByTestId('tooltip-git_info.repo'); expect(tooltip.textContent).toContain('React tooltip'); }); @@ -57,9 +59,9 @@ describe('Common Git Info Fields', () => { it('should support multiple Git platforms in tooltip', () => { // Test that the tooltip contains information about multiple platforms - const tooltip = GIT_INFO_REPO.tooltip; + const { tooltip } = GIT_INFO_REPO; expect(tooltip).toBeDefined(); - + // Since tooltip is a React component, we test its structure expect(React.isValidElement(tooltip)).toBe(true); }); diff --git a/datahub-web-react/src/app/ingestV2/source/builder/RecipeForm/__tests__/constants-git-info.test.tsx b/datahub-web-react/src/app/ingestV2/source/builder/RecipeForm/__tests__/constants-git-info.test.tsx index 84db00b7b4cca4..70b4f71d3e5cdd 100644 --- a/datahub-web-react/src/app/ingestV2/source/builder/RecipeForm/__tests__/constants-git-info.test.tsx +++ b/datahub-web-react/src/app/ingestV2/source/builder/RecipeForm/__tests__/constants-git-info.test.tsx @@ -1,7 +1,6 @@ -import { LOOKML_GIT_INFO_REPO } from '@app/ingestV2/source/builder/RecipeForm/lookml'; import { GIT_INFO_REPO } from '@app/ingestV2/source/builder/RecipeForm/common'; -import { LOOKML } from '@app/ingestV2/source/builder/RecipeForm/lookml'; -import { SOURCE_CONFIGS } from '@app/ingestV2/source/builder/RecipeForm/constants'; +import { RECIPE_FIELDS } from '@app/ingestV2/source/builder/RecipeForm/constants'; +import { LOOKML, LOOKML_GIT_INFO_REPO } from '@app/ingestV2/source/builder/RecipeForm/lookml'; describe('Constants Git Info Integration', () => { describe('Field Imports', () => { @@ -18,65 +17,57 @@ describe('Constants Git Info Integration', () => { describe('Source Configuration Integration', () => { it('should include LOOKML_GIT_INFO_REPO in LOOKML source config', () => { - const lookmlConfig = SOURCE_CONFIGS[LOOKML]; + const lookmlConfig = RECIPE_FIELDS[LOOKML]; expect(lookmlConfig).toBeDefined(); expect(lookmlConfig.fields).toContain(LOOKML_GIT_INFO_REPO); }); it('should have correct field order in LOOKML config', () => { - const lookmlConfig = SOURCE_CONFIGS[LOOKML]; - const fields = lookmlConfig.fields; - + const lookmlConfig = RECIPE_FIELDS[LOOKML]; + const { fields } = lookmlConfig; + // LOOKML_GIT_INFO_REPO should be the first field expect(fields[0]).toBe(LOOKML_GIT_INFO_REPO); }); it('should not contain deprecated github_info references', () => { - const lookmlConfig = SOURCE_CONFIGS[LOOKML]; - const fields = lookmlConfig.fields; - + const lookmlConfig = RECIPE_FIELDS[LOOKML]; + const { fields } = lookmlConfig; + // Check that no fields have github_info in their name - const githubInfoFields = fields.filter(field => - field.name && field.name.includes('github_info') - ); + const githubInfoFields = fields.filter((field) => field.name && field.name.includes('github_info')); expect(githubInfoFields).toHaveLength(0); }); it('should contain git_info references', () => { - const lookmlConfig = SOURCE_CONFIGS[LOOKML]; - const fields = lookmlConfig.fields; - + const lookmlConfig = RECIPE_FIELDS[LOOKML]; + const { fields } = lookmlConfig; + // Check that fields have git_info in their name - const gitInfoFields = fields.filter(field => - field.name && field.name.includes('git_info') - ); + const gitInfoFields = fields.filter((field) => field.name && field.name.includes('git_info')); expect(gitInfoFields.length).toBeGreaterThan(0); }); }); describe('Field Consistency', () => { it('should have consistent field paths for git_info', () => { - const lookmlConfig = SOURCE_CONFIGS[LOOKML]; - const fields = lookmlConfig.fields; - - const gitInfoFields = fields.filter(field => - field.name && field.name.includes('git_info') - ); - - gitInfoFields.forEach(field => { + const lookmlConfig = RECIPE_FIELDS[LOOKML]; + const { fields } = lookmlConfig; + + const gitInfoFields = fields.filter((field) => field.name && field.name.includes('git_info')); + + gitInfoFields.forEach((field) => { expect(field.fieldPath).toMatch(/^source\.config\.git_info\./); }); }); it('should have proper field types for git_info fields', () => { - const lookmlConfig = SOURCE_CONFIGS[LOOKML]; - const fields = lookmlConfig.fields; - - const gitInfoFields = fields.filter(field => - field.name && field.name.includes('git_info') - ); - - gitInfoFields.forEach(field => { + const lookmlConfig = RECIPE_FIELDS[LOOKML]; + const { fields } = lookmlConfig; + + const gitInfoFields = fields.filter((field) => field.name && field.name.includes('git_info')); + + gitInfoFields.forEach((field) => { expect(field.type).toBeDefined(); expect(['TEXT', 'SECRET']).toContain(field.type); }); @@ -85,27 +76,23 @@ describe('Constants Git Info Integration', () => { describe('Migration from github_info', () => { it('should not have any github_info field references', () => { - const lookmlConfig = SOURCE_CONFIGS[LOOKML]; - const fields = lookmlConfig.fields; - + const lookmlConfig = RECIPE_FIELDS[LOOKML]; + const { fields } = lookmlConfig; + // Ensure no fields reference the old github_info structure - const oldFieldPaths = fields.filter(field => - field.fieldPath && field.fieldPath.includes('github_info') - ); + const oldFieldPaths = fields.filter((field) => field.fieldPath && field.fieldPath.includes('github_info')); expect(oldFieldPaths).toHaveLength(0); }); it('should have updated field names from github_info to git_info', () => { - const lookmlConfig = SOURCE_CONFIGS[LOOKML]; - const fields = lookmlConfig.fields; - - const gitInfoFields = fields.filter(field => - field.name && field.name.includes('git_info') - ); - + const lookmlConfig = RECIPE_FIELDS[LOOKML]; + const { fields } = lookmlConfig; + + const gitInfoFields = fields.filter((field) => field.name && field.name.includes('git_info')); + expect(gitInfoFields.length).toBeGreaterThan(0); - - gitInfoFields.forEach(field => { + + gitInfoFields.forEach((field) => { expect(field.name).toMatch(/^git_info\./); expect(field.name).not.toMatch(/^github_info\./); }); @@ -114,21 +101,19 @@ describe('Constants Git Info Integration', () => { describe('Field Validation', () => { it('should have required fields properly marked', () => { - const lookmlConfig = SOURCE_CONFIGS[LOOKML]; - const fields = lookmlConfig.fields; - - const requiredFields = fields.filter(field => field.required === true); + const lookmlConfig = RECIPE_FIELDS[LOOKML]; + const { fields } = lookmlConfig; + + const requiredFields = fields.filter((field) => field.required === true); expect(requiredFields.length).toBeGreaterThan(0); }); it('should have validation rules for required fields', () => { - const lookmlConfig = SOURCE_CONFIGS[LOOKML]; - const fields = lookmlConfig.fields; - - const fieldsWithRules = fields.filter(field => - field.rules && field.rules.length > 0 - ); - + const lookmlConfig = RECIPE_FIELDS[LOOKML]; + const { fields } = lookmlConfig; + + const fieldsWithRules = fields.filter((field) => field.rules && field.rules.length > 0); + expect(fieldsWithRules.length).toBeGreaterThan(0); }); }); diff --git a/datahub-web-react/src/app/ingestV2/source/builder/RecipeForm/__tests__/git-info-validation.test.tsx b/datahub-web-react/src/app/ingestV2/source/builder/RecipeForm/__tests__/git-info-validation.test.tsx index 2ba43d1a6444ad..6444d368c7ae4c 100644 --- a/datahub-web-react/src/app/ingestV2/source/builder/RecipeForm/__tests__/git-info-validation.test.tsx +++ b/datahub-web-react/src/app/ingestV2/source/builder/RecipeForm/__tests__/git-info-validation.test.tsx @@ -1,16 +1,16 @@ import { LOOKML_GIT_INFO_REPO_SSH_LOCATOR } from '@app/ingestV2/source/builder/RecipeForm/lookml'; describe('Git Info Validation Logic', () => { - describe('REPO_SSH_LOCATOR Validation', () => { - const createValidator = (repoValue) => { - return LOOKML_GIT_INFO_REPO_SSH_LOCATOR.rules![0]({ - getFieldValue: (fieldName) => { - if (fieldName === 'git_info.repo') return repoValue; - return undefined; - } - }); - }; + const createValidator = (repoValue: string | undefined | null) => { + return LOOKML_GIT_INFO_REPO_SSH_LOCATOR.rules![0]({ + getFieldValue: (fieldName) => { + if (fieldName === 'git_info.repo') return repoValue; + return undefined; + }, + }); + }; + describe('REPO_SSH_LOCATOR Validation', () => { describe('GitHub Repository Detection', () => { it('should not require SSH locator for GitHub short format', async () => { const validator = createValidator('datahub-project/datahub'); @@ -54,21 +54,21 @@ describe('Git Info Validation Logic', () => { it('should require SSH locator for Bitbucket', async () => { const validator = createValidator('https://bitbucket.org/org/repo'); await expect(validator.validator({}, '')).rejects.toThrow( - 'Repository SSH Locator is required for Git platforms other than GitHub and GitLab' + 'Repository SSH Locator is required for Git platforms other than GitHub and GitLab', ); }); it('should require SSH locator for custom Git server', async () => { const validator = createValidator('https://custom-git.com/org/repo'); await expect(validator.validator({}, '')).rejects.toThrow( - 'Repository SSH Locator is required for Git platforms other than GitHub and GitLab' + 'Repository SSH Locator is required for Git platforms other than GitHub and GitLab', ); }); it('should require SSH locator for SSH URL format', async () => { const validator = createValidator('git@custom-server.com:org/repo'); await expect(validator.validator({}, '')).rejects.toThrow( - 'Repository SSH Locator is required for Git platforms other than GitHub and GitLab' + 'Repository SSH Locator is required for Git platforms other than GitHub and GitLab', ); }); @@ -119,7 +119,7 @@ describe('Git Info Validation Logic', () => { it('should require SSH locator for non-GitHub/GitLab URLs with similar patterns', async () => { const validator = createValidator('https://github-enterprise.company.com/org/repo'); await expect(validator.validator({}, '')).rejects.toThrow( - 'Repository SSH Locator is required for Git platforms other than GitHub and GitLab' + 'Repository SSH Locator is required for Git platforms other than GitHub and GitLab', ); }); }); @@ -130,19 +130,11 @@ describe('Git Info Validation Logic', () => { const validator = createValidator('https://custom-git.com/org/repo'); try { await validator.validator({}, ''); - } catch (error) { - expect(error.message).toBe('Repository SSH Locator is required for Git platforms other than GitHub and GitLab'); + } catch (error: any) { + expect(error.message).toBe( + 'Repository SSH Locator is required for Git platforms other than GitHub and GitLab', + ); } }); }); - - // Helper function for creating validators - function createValidator(repoValue) { - return LOOKML_GIT_INFO_REPO_SSH_LOCATOR.rules![0]({ - getFieldValue: (fieldName) => { - if (fieldName === 'git_info.repo') return repoValue; - return undefined; - } - }); - } }); diff --git a/datahub-web-react/src/app/ingestV2/source/builder/RecipeForm/__tests__/lookml.test.tsx b/datahub-web-react/src/app/ingestV2/source/builder/RecipeForm/__tests__/lookml.test.tsx index 5b27791b94d9db..e507d738bd5eb7 100644 --- a/datahub-web-react/src/app/ingestV2/source/builder/RecipeForm/__tests__/lookml.test.tsx +++ b/datahub-web-react/src/app/ingestV2/source/builder/RecipeForm/__tests__/lookml.test.tsx @@ -1,30 +1,27 @@ -import React from 'react'; import { render, screen } from '@testing-library/react'; import { Form } from 'antd'; +import React from 'react'; +import { FieldType } from '@app/ingestV2/source/builder/RecipeForm/common'; import { - LOOKML_GIT_INFO_REPO, LOOKML_GIT_INFO_DEPLOY_KEY, + LOOKML_GIT_INFO_REPO, LOOKML_GIT_INFO_REPO_SSH_LOCATOR, - LOOKML_BASE_URL, - LOOKML_CLIENT_ID, - LOOKML_CLIENT_SECRET, - PROJECT_NAME, - PARSE_TABLE_NAMES_FROM_SQL, - CONNECTION_TO_PLATFORM_MAP, } from '@app/ingestV2/source/builder/RecipeForm/lookml'; -import { FieldType } from '@app/ingestV2/source/builder/RecipeForm/common'; // Mock FormField component for testing -const MockFormField = ({ field, removeMargin }) => { +const MockFormField = ({ field, removeMargin: _removeMargin }: { field: any; removeMargin: boolean }) => { const { name, label, tooltip, type, placeholder, rules, required } = field; - + return (
- - {tooltip &&
{typeof tooltip === 'string' ? tooltip : 'React tooltip'}
} - {label} + {tooltip && ( +
{typeof tooltip === 'string' ? tooltip : 'React tooltip'}
+ )} + { expect(LOOKML_GIT_INFO_REPO.type).toBe(FieldType.TEXT); expect(LOOKML_GIT_INFO_REPO.fieldPath).toBe('source.config.git_info.repo'); expect(LOOKML_GIT_INFO_REPO.required).toBe(true); - expect(LOOKML_GIT_INFO_REPO.placeholder).toBe('datahub-project/datahub or https://github.com/datahub-project/datahub'); + expect(LOOKML_GIT_INFO_REPO.placeholder).toBe( + 'datahub-project/datahub or https://github.com/datahub-project/datahub', + ); }); it('should have validation rules', () => { @@ -54,9 +53,9 @@ describe('LookML Git Info Fields', () => { render(
- + , ); - + const tooltip = screen.getByTestId('tooltip-git_info.repo'); expect(tooltip.textContent).toContain('React tooltip'); }); @@ -80,7 +79,7 @@ describe('LookML Git Info Fields', () => { it('should have setValueOnRecipeOverride function', () => { expect(typeof LOOKML_GIT_INFO_DEPLOY_KEY.setValueOnRecipeOverride).toBe('function'); - + const recipe = { source: { config: {} } }; const result = LOOKML_GIT_INFO_DEPLOY_KEY.setValueOnRecipeOverride!(recipe, 'test-key'); expect(result.source.config.git_info.deploy_key).toBe('test-key\n'); @@ -90,9 +89,9 @@ describe('LookML Git Info Fields', () => { render(
- + , ); - + const tooltip = screen.getByTestId('tooltip-git_info.deploy_key'); expect(tooltip.textContent).toContain('React tooltip'); }); @@ -117,7 +116,7 @@ describe('LookML Git Info Fields', () => { getFieldValue: (fieldName) => { if (fieldName === 'git_info.repo') return 'datahub-project/datahub'; return undefined; - } + }, }); const result = await validator.validator({}, ''); @@ -129,7 +128,7 @@ describe('LookML Git Info Fields', () => { getFieldValue: (fieldName) => { if (fieldName === 'git_info.repo') return 'https://gitlab.com/gitlab-org/gitlab'; return undefined; - } + }, }); const result = await validator.validator({}, ''); @@ -141,14 +140,16 @@ describe('LookML Git Info Fields', () => { getFieldValue: (fieldName) => { if (fieldName === 'git_info.repo') return 'https://custom-git.com/org/repo'; return undefined; - } + }, }); try { await validator.validator({}, ''); expect(true).toBe(false); // Should not reach here } catch (error: any) { - expect(error.message).toBe('Repository SSH Locator is required for Git platforms other than GitHub and GitLab'); + expect(error.message).toBe( + 'Repository SSH Locator is required for Git platforms other than GitHub and GitLab', + ); } }); @@ -157,7 +158,7 @@ describe('LookML Git Info Fields', () => { getFieldValue: (fieldName) => { if (fieldName === 'git_info.repo') return undefined; return undefined; - } + }, }); const result = await validator.validator({}, ''); @@ -168,9 +169,9 @@ describe('LookML Git Info Fields', () => { render(
- + , ); - + const tooltip = screen.getByTestId('tooltip-git_info.repo_ssh_locator'); expect(tooltip.textContent).toContain('React tooltip'); }); @@ -201,7 +202,7 @@ describe('LookML Git Info Fields', () => { expect(LOOKML_GIT_INFO_REPO.name).not.toContain('github_info'); expect(LOOKML_GIT_INFO_DEPLOY_KEY.name).not.toContain('github_info'); expect(LOOKML_GIT_INFO_REPO_SSH_LOCATOR.name).not.toContain('github_info'); - + expect(LOOKML_GIT_INFO_REPO.name).toContain('git_info'); expect(LOOKML_GIT_INFO_DEPLOY_KEY.name).toContain('git_info'); expect(LOOKML_GIT_INFO_REPO_SSH_LOCATOR.name).toContain('git_info'); @@ -219,15 +220,15 @@ describe('LookML Git Info Fields', () => { const githubRepos = [ 'datahub-project/datahub', 'https://github.com/datahub-project/datahub', - 'github.com/datahub-project/datahub' + 'github.com/datahub-project/datahub', ]; - githubRepos.forEach(repo => { + githubRepos.forEach((repo) => { const validator = LOOKML_GIT_INFO_REPO_SSH_LOCATOR.rules![0]({ getFieldValue: (fieldName) => { if (fieldName === 'git_info.repo') return repo; return undefined; - } + }, }); expect(async () => { @@ -237,17 +238,14 @@ describe('LookML Git Info Fields', () => { }); it('should support GitLab repositories', () => { - const gitlabRepos = [ - 'https://gitlab.com/gitlab-org/gitlab', - 'gitlab.com/gitlab-org/gitlab' - ]; + const gitlabRepos = ['https://gitlab.com/gitlab-org/gitlab', 'gitlab.com/gitlab-org/gitlab']; - gitlabRepos.forEach(repo => { + gitlabRepos.forEach((repo) => { const validator = LOOKML_GIT_INFO_REPO_SSH_LOCATOR.rules![0]({ getFieldValue: (fieldName) => { if (fieldName === 'git_info.repo') return repo; return undefined; - } + }, }); expect(async () => { @@ -260,19 +258,23 @@ describe('LookML Git Info Fields', () => { const otherPlatformRepos = [ 'https://bitbucket.org/org/repo', 'https://custom-git.com/org/repo', - 'https://git.company.com/org/repo' + 'https://git.company.com/org/repo', ]; - for (const repo of otherPlatformRepos) { - const validator = LOOKML_GIT_INFO_REPO_SSH_LOCATOR.rules![0]({ - getFieldValue: (fieldName) => { - if (fieldName === 'git_info.repo') return repo; - return undefined; - } - }); - - await expect(validator.validator({}, '')).rejects.toThrow('Repository SSH Locator is required for Git platforms other than GitHub and GitLab'); - } + await Promise.all( + otherPlatformRepos.map(async (repo) => { + const validator = LOOKML_GIT_INFO_REPO_SSH_LOCATOR.rules![0]({ + getFieldValue: (fieldName) => { + if (fieldName === 'git_info.repo') return repo; + return undefined; + }, + }); + + await expect(validator.validator({}, '')).rejects.toThrow( + 'Repository SSH Locator is required for Git platforms other than GitHub and GitLab', + ); + }), + ); }); }); }); diff --git a/datahub-web-react/src/app/ingestV2/source/builder/RecipeForm/common.tsx b/datahub-web-react/src/app/ingestV2/source/builder/RecipeForm/common.tsx index 46b1e174c25462..d64858e6352a51 100644 --- a/datahub-web-react/src/app/ingestV2/source/builder/RecipeForm/common.tsx +++ b/datahub-web-react/src/app/ingestV2/source/builder/RecipeForm/common.tsx @@ -416,9 +416,7 @@ export const GIT_INFO_REPO: RecipeField = { label: 'Git Repository', tooltip: (
-

- URL or name of your Git repository. Supports GitHub, GitLab, and other Git platforms. Examples: -

+

URL or name of your Git repository. Supports GitHub, GitLab, and other Git platforms. Examples:

  • GitHub: datahub-project/datahub or https://github.com/datahub-project/datahub
  • GitLab: https://gitlab.com/gitlab-org/gitlab
  • diff --git a/datahub-web-react/src/app/ingestV2/source/builder/RecipeForm/constants.ts b/datahub-web-react/src/app/ingestV2/source/builder/RecipeForm/constants.ts index d0fb2033c10b1b..b00e2908e2295f 100644 --- a/datahub-web-react/src/app/ingestV2/source/builder/RecipeForm/constants.ts +++ b/datahub-web-react/src/app/ingestV2/source/builder/RecipeForm/constants.ts @@ -101,13 +101,13 @@ import { } from '@app/ingestV2/source/builder/RecipeForm/looker'; import { CONNECTION_TO_PLATFORM_MAP, - LOOKML_GIT_INFO_DEPLOY_KEY, - LOOKML_GIT_INFO_REPO_SSH_LOCATOR, LOOKML, LOOKML_BASE_URL, LOOKML_CLIENT_ID, LOOKML_CLIENT_SECRET, + LOOKML_GIT_INFO_DEPLOY_KEY, LOOKML_GIT_INFO_REPO, + LOOKML_GIT_INFO_REPO_SSH_LOCATOR, PARSE_TABLE_NAMES_FROM_SQL, PROJECT_NAME, } from '@app/ingestV2/source/builder/RecipeForm/lookml'; diff --git a/datahub-web-react/src/app/ingestV2/source/builder/RecipeForm/lookml.tsx b/datahub-web-react/src/app/ingestV2/source/builder/RecipeForm/lookml.tsx index cc9835183182ed..33fbb473f2d948 100644 --- a/datahub-web-react/src/app/ingestV2/source/builder/RecipeForm/lookml.tsx +++ b/datahub-web-react/src/app/ingestV2/source/builder/RecipeForm/lookml.tsx @@ -11,7 +11,8 @@ export const LOOKML_GIT_INFO_REPO: RecipeField = { tooltip: ( <>

    - Name of your GitHub repository or the URL of your Git repository. Supports GitHub, GitLab, and other Git platforms. Examples: + Name of your GitHub repository or the URL of your Git repository. Supports GitHub, GitLab, and other Git + platforms. Examples:

    • GitHub: datahub-project/datahub or https://github.com/datahub-project/datahub
    • GitLab: https://gitlab.com/gitlab-org/gitlab
    • @@ -48,15 +49,11 @@ export const LOOKML_GIT_INFO_DEPLOY_KEY: RecipeField = {
    • - + GitLab
    • -
    • Other Git platforms: Check your platform's documentation for SSH key setup
    • +
    • Other Git platforms: Check your platform's documentation for SSH key setup
@@ -96,17 +93,17 @@ export const LOOKML_GIT_INFO_REPO_SSH_LOCATOR: RecipeField = { validator(_, value) { const repo = getFieldValue('git_info.repo'); if (!repo) return Promise.resolve(); - + // Check if it's GitHub or GitLab (these are auto-inferred) - const isGitHub = repo.toLowerCase().includes('github.com') || - (!repo.includes('://') && repo.split('/').length === 2) || - repo.startsWith('git@github.com:'); - const isGitLab = repo.toLowerCase().includes('gitlab.com') || - repo.startsWith('git@gitlab.com:'); - + const isGitHub = + repo.toLowerCase().includes('github.com') || + (!repo.includes('://') && repo.split('/').length === 2) || + repo.startsWith('git@github.com:'); + const isGitLab = repo.toLowerCase().includes('gitlab.com') || repo.startsWith('git@gitlab.com:'); + if (!isGitHub && !isGitLab && !value) { return Promise.reject( - new Error('Repository SSH Locator is required for Git platforms other than GitHub and GitLab') + new Error('Repository SSH Locator is required for Git platforms other than GitHub and GitLab'), ); } return Promise.resolve(); diff --git a/datahub-web-react/src/app/ingestV2/source/builder/SelectTemplateStep.tsx b/datahub-web-react/src/app/ingestV2/source/builder/SelectTemplateStep.tsx index 3de7f5482fdfc9..b280bef76e67a1 100644 --- a/datahub-web-react/src/app/ingestV2/source/builder/SelectTemplateStep.tsx +++ b/datahub-web-react/src/app/ingestV2/source/builder/SelectTemplateStep.tsx @@ -1,6 +1,6 @@ import { FormOutlined, SearchOutlined } from '@ant-design/icons'; import { Input } from 'antd'; -import React, { useEffect, useRef, useState } from 'react'; +import React, { useState } from 'react'; import styled from 'styled-components'; import { ANTD_GRAY } from '@app/entity/shared/constants'; @@ -9,7 +9,6 @@ import { CUSTOM } from '@app/ingestV2/source/builder/constants'; import { IngestionSourceBuilderStep } from '@app/ingestV2/source/builder/steps'; import { SourceBuilderState, SourceConfig, StepProps } from '@app/ingestV2/source/builder/types'; import useGetSourceLogoUrl from '@app/ingestV2/source/builder/useGetSourceLogoUrl'; -import { Button } from '@src/alchemy-components'; const Container = styled.div` max-height: 82vh; @@ -154,12 +153,14 @@ export const SelectTemplateStep = ({ {filteredSources.length > 0 ? ( filteredSources.map((source) => ( - onSelectTemplate(source.name)} /> + onSelectTemplate(source.name)} + /> )) ) : ( - - Data Source with name "{searchFilter}" not found. - + Data Source with name "{searchFilter}" not found. )}