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 0000000000000..41f554c400b16 --- /dev/null +++ b/datahub-web-react/src/app/ingestV2/source/builder/RecipeForm/__tests__/common-git-info.test.tsx @@ -0,0 +1,117 @@ +import { render, screen } from '@testing-library/react'; +import { Form } from 'antd'; +import React from 'react'; + +import { FieldType, GIT_INFO_REPO } from '@app/ingestV2/source/builder/RecipeForm/common'; + +// Mock FormField component for testing +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'}
+ )} + +
+ ); +}; + +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; + 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 0000000000000..d036070a507f5 --- /dev/null +++ b/datahub-web-react/src/app/ingestV2/source/builder/RecipeForm/__tests__/constants-git-info.test.tsx @@ -0,0 +1,120 @@ +import { FieldType, GIT_INFO_REPO } from '@app/ingestV2/source/builder/RecipeForm/common'; +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', () => { + 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 = 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 = 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 = 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')); + expect(githubInfoFields).toHaveLength(0); + }); + + it('should contain git_info references', () => { + 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')); + expect(gitInfoFields.length).toBeGreaterThan(0); + }); + }); + + describe('Field Consistency', () => { + it('should have consistent field paths for git_info', () => { + 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 = 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([FieldType.TEXT, FieldType.SECRET]).toContain(field.type); + }); + }); + }); + + describe('Migration from github_info', () => { + it('should not have any github_info field references', () => { + 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')); + expect(oldFieldPaths).toHaveLength(0); + }); + + it('should have updated field names from github_info to 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) => { + 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 = 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 = 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 new file mode 100644 index 0000000000000..6444d368c7ae4 --- /dev/null +++ b/datahub-web-react/src/app/ingestV2/source/builder/RecipeForm/__tests__/git-info-validation.test.tsx @@ -0,0 +1,140 @@ +import { LOOKML_GIT_INFO_REPO_SSH_LOCATOR } from '@app/ingestV2/source/builder/RecipeForm/lookml'; + +describe('Git Info Validation Logic', () => { + 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'); + 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: any) { + expect(error.message).toBe( + '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/__tests__/lookml.test.tsx b/datahub-web-react/src/app/ingestV2/source/builder/RecipeForm/__tests__/lookml.test.tsx new file mode 100644 index 0000000000000..e507d738bd5eb --- /dev/null +++ b/datahub-web-react/src/app/ingestV2/source/builder/RecipeForm/__tests__/lookml.test.tsx @@ -0,0 +1,280 @@ +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_DEPLOY_KEY, + LOOKML_GIT_INFO_REPO, + LOOKML_GIT_INFO_REPO_SSH_LOCATOR, +} from '@app/ingestV2/source/builder/RecipeForm/lookml'; + +// Mock FormField component for testing +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'}
+ )} + +
+ ); +}; + +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', + ]; + + 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 3a8a19927401e..d64858e6352a5 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,21 @@ 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 1197ec6b4e00f..b00e2908e2295 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, LOOKML_BASE_URL, LOOKML_CLIENT_ID, LOOKML_CLIENT_SECRET, - LOOKML_GITHUB_INFO_REPO, + 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'; @@ -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 7e1d0deb7f1d2..561084cec1be1 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,63 @@ 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 +69,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 (which are + auto-inferred). Example: 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) + // If it starts with git@, it must be explicitly github.com or gitlab.com + const isGitHubSSH = repo.startsWith('git@github.com:'); + const isGitLabSSH = repo.startsWith('git@gitlab.com:'); + + // Check for other SSH formats (git@custom-server.com:...) - these are NOT GitHub/GitLab + const isOtherSSH = repo.startsWith('git@') && !isGitHubSSH && !isGitLabSSH; + + const isGitHub = + repo.toLowerCase().includes('github.com') || + (!repo.includes('://') && !repo.startsWith('git@') && repo.split('/').length === 2) || + isGitHubSSH; + const isGitLab = (repo.toLowerCase().includes('gitlab.com') && !isOtherSSH) || isGitLabSSH; + + 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 4cbe195b48a73..061c24cc9765f 100644 --- a/datahub-web-react/src/app/ingestV2/source/builder/SelectTemplateStep.tsx +++ b/datahub-web-react/src/app/ingestV2/source/builder/SelectTemplateStep.tsx @@ -1,5 +1,5 @@ import { FormOutlined, SearchOutlined } from '@ant-design/icons'; -import { Input } from 'antd'; +import { Input, InputRef } from 'antd'; import React, { useState } from 'react'; import styled from 'styled-components'; @@ -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; @@ -54,6 +53,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 +96,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: InputRef | null) => { + if (node) { + node.focus(); + } + }; + const onSelectTemplate = (type: string) => { const newState: SourceBuilderState = { ...state, @@ -126,6 +142,7 @@ export const SelectTemplateStep = ({
setSearchFilter(e.target.value)} @@ -134,14 +151,19 @@ 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. + )}
- ); };