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 => {
+ 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 => {
+ 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 => {
+ 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 => {
+ 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 => {
+ 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 => {
+ 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 => {
+ 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 => {
+ 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:
+
+ -
+
+ GitHub
+
+
+ -
+
+ GitLab
+
+
+ - Other Git platforms: Check your platform's documentation for SSH key setup
+
>
),
- 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.
+ )}
-
);
};