Skip to content

Commit aacaa76

Browse files
chriscollins3456shirshankacClaudeclaude
authored
feat(ui): Modernize Analytics Page with v2/Alchemy Components (#6872) (#15068)
Co-authored-by: Shirshanka Das <[email protected]> Co-authored-by: cclaude-session <[email protected]> Co-authored-by: Claude <[email protected]>
1 parent 6eb27d6 commit aacaa76

35 files changed

+79975
-5
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ smoke-test/spark-smoke-test/__pycache__/
6565
# cypress integration test generated files
6666
**/cypress/videos
6767
**/cypress/screenshots
68+
**/cypress/results
6869
**/cypress/node_modules
6970

7071
# Metadata Ingestion Generated

datahub-web-react/src/alchemy-components/components/GraphCard/components.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ export const GraphContainer = styled.div<{ $isEmpty?: boolean; $height: string }
2828
`
2929
position: relative;
3030
pointer-events: none;
31-
filter: blur(2px);
31+
filter: blur(2px);
3232
`}
3333
`;
3434

datahub-web-react/src/app/SearchRoutes.tsx

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import React from 'react';
22
import { Redirect, Route, Switch } from 'react-router-dom';
33

44
import { AnalyticsPage } from '@app/analyticsDashboard/components/AnalyticsPage';
5+
import { AnalyticsPage as AnalyticsPageV2 } from '@app/analyticsDashboardV2/components/AnalyticsPage';
56
import { ManageApplications } from '@app/applications/ManageApplications';
67
import { BrowseResultsPage } from '@app/browse/BrowseResultsPage';
78
import { BusinessAttributes } from '@app/businessAttribute/BusinessAttributes';
@@ -63,6 +64,13 @@ export const SearchRoutes = (): JSX.Element => {
6364
const showIngestV2 = config.featureFlags.showIngestionPageRedesign;
6465
const showAnalytics = (config?.analyticsConfig?.enabled && me && me?.platformPrivileges?.viewAnalytics) || false;
6566

67+
const renderAnalyticsPage = () => {
68+
if (!showAnalytics) {
69+
return <NoPageFound />;
70+
}
71+
return isThemeV2 ? <AnalyticsPageV2 /> : <AnalyticsPage />;
72+
};
73+
6674
return (
6775
<FinalSearchablePage>
6876
<Switch>
@@ -86,10 +94,7 @@ export const SearchRoutes = (): JSX.Element => {
8694
<Route path={PageRoutes.BROWSE_RESULTS} render={() => <BrowseResultsPage />} />
8795
{showTags ? <Route path={PageRoutes.MANAGE_TAGS} render={() => <ManageTags />} /> : null}
8896
<Route path={PageRoutes.MANAGE_APPLICATIONS} render={() => <ManageApplications />} />
89-
<Route
90-
path={PageRoutes.ANALYTICS}
91-
render={() => (showAnalytics ? <AnalyticsPage /> : <NoPageFound />)}
92-
/>
97+
<Route path={PageRoutes.ANALYTICS} render={renderAnalyticsPage} />
9398
<Route path={PageRoutes.POLICIES} render={() => <Redirect to="/settings/permissions/policies" />} />
9499
<Route
95100
path={PageRoutes.SETTINGS_POLICIES}
Lines changed: 229 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,229 @@
1+
/**
2+
* Unit tests for Analytics Chart Color Assignment System
3+
*
4+
* Tests cover:
5+
* - Entity color matching
6+
* - Name variation handling
7+
* - Fallback to qualitative colors
8+
* - Generated colors for large series
9+
* - User override functionality
10+
*/
11+
import { describe, expect, test } from 'vitest';
12+
13+
import { assignAnalyticsChartColors } from '@app/analyticsDashboardV2/utils/analyticsChartColors';
14+
import { DATAHUB_ENTITY_COLORS, QUALITATIVE_COLORS } from '@app/analyticsDashboardV2/utils/chartColorConstants';
15+
import { findDataHubEntityColor, getAllEntityMatches } from '@app/analyticsDashboardV2/utils/chartColorMatcher';
16+
17+
describe('DataHub Entity Color Matching', () => {
18+
test('assigns entity colors correctly for exact matches', () => {
19+
const result = assignAnalyticsChartColors(['schema', 'incidents', 'dataset']);
20+
21+
expect(result.assignments.schema.color).toBe(DATAHUB_ENTITY_COLORS.schema);
22+
expect(result.assignments.incidents.color).toBe(DATAHUB_ENTITY_COLORS.incidents);
23+
expect(result.assignments.dataset.color).toBe(DATAHUB_ENTITY_COLORS.dataset);
24+
expect(result.assignments.schema.strategy.source).toBe('datahub-entity');
25+
});
26+
27+
test('handles entity name variations - case insensitive', () => {
28+
expect(findDataHubEntityColor('DATASET')).toBe(DATAHUB_ENTITY_COLORS.dataset);
29+
expect(findDataHubEntityColor('Dataset')).toBe(DATAHUB_ENTITY_COLORS.dataset);
30+
expect(findDataHubEntityColor('DaTaSeT')).toBe(DATAHUB_ENTITY_COLORS.dataset);
31+
});
32+
33+
test('handles entity name variations - separators', () => {
34+
expect(findDataHubEntityColor('data_set')).toBe(DATAHUB_ENTITY_COLORS.dataset);
35+
expect(findDataHubEntityColor('data-set')).toBe(DATAHUB_ENTITY_COLORS.dataset);
36+
expect(findDataHubEntityColor('data set')).toBe(DATAHUB_ENTITY_COLORS.dataset);
37+
});
38+
39+
test('handles entity name variations - compound names', () => {
40+
expect(findDataHubEntityColor('data_product')).toBe(DATAHUB_ENTITY_COLORS.dataproduct);
41+
expect(findDataHubEntityColor('DataProduct')).toBe(DATAHUB_ENTITY_COLORS.dataproduct);
42+
expect(findDataHubEntityColor('DATA_PRODUCT')).toBe(DATAHUB_ENTITY_COLORS.dataproduct);
43+
});
44+
45+
test('handles partial matches for entity types', () => {
46+
// Note: partial matching works when the entity type is contained in the key
47+
const datasetMatch = findDataHubEntityColor('dataset_view');
48+
const schemaMatch = findDataHubEntityColor('schema_tab');
49+
50+
// These should match because 'dataset' is contained in 'dataset_view'
51+
expect(datasetMatch).not.toBeNull();
52+
expect(schemaMatch).not.toBeNull();
53+
});
54+
55+
test('returns null for unknown entity types', () => {
56+
expect(findDataHubEntityColor('unknown_type')).toBeNull();
57+
expect(findDataHubEntityColor('random_entity')).toBeNull();
58+
});
59+
});
60+
61+
describe('Qualitative Color Fallback', () => {
62+
test('falls back to qualitative colors for unknown entities', () => {
63+
const unknownEntities = ['unknownType1', 'unknownType2', 'unknownType3'];
64+
const result = assignAnalyticsChartColors(unknownEntities);
65+
66+
expect(result.assignments.unknownType1.strategy.type).toBe('qualitative');
67+
expect(result.assignments.unknownType1.color).toBe(QUALITATIVE_COLORS[0]);
68+
expect(result.assignments.unknownType2.color).toBe(QUALITATIVE_COLORS[1]);
69+
expect(result.assignments.unknownType3.color).toBe(QUALITATIVE_COLORS[2]);
70+
});
71+
72+
test('uses qualitative colors after entity colors are exhausted', () => {
73+
const mixedEntities = ['dataset', 'unknownType1', 'schema', 'unknownType2'];
74+
const result = assignAnalyticsChartColors(mixedEntities);
75+
76+
expect(result.assignments.dataset.strategy.source).toBe('datahub-entity');
77+
expect(result.assignments.schema.strategy.source).toBe('datahub-entity');
78+
expect(result.assignments.unknownType1.strategy.type).toBe('qualitative');
79+
expect(result.assignments.unknownType2.strategy.type).toBe('qualitative');
80+
});
81+
82+
test('tracks unused qualitative colors', () => {
83+
const result = assignAnalyticsChartColors(['unknownType1', 'unknownType2']);
84+
85+
expect(result.unusedColors.length).toBe(QUALITATIVE_COLORS.length - 2);
86+
expect(result.unusedColors).not.toContain(QUALITATIVE_COLORS[0]);
87+
expect(result.unusedColors).not.toContain(QUALITATIVE_COLORS[1]);
88+
});
89+
});
90+
91+
describe('Generated Colors for Large Series', () => {
92+
test('generates colors for large series beyond qualitative palette', () => {
93+
const manyEntities = Array.from({ length: 20 }, (_, i) => `entity${i}`);
94+
const result = assignAnalyticsChartColors(manyEntities);
95+
96+
expect(Object.keys(result.assignments)).toHaveLength(20);
97+
expect(result.generatedCount).toBeGreaterThan(0);
98+
});
99+
100+
test('generated colors have correct strategy', () => {
101+
const manyEntities = Array.from({ length: 15 }, (_, i) => `entity${i}`);
102+
const result = assignAnalyticsChartColors(manyEntities);
103+
104+
const generatedAssignments = Object.values(result.assignments).filter((a) => a.strategy.type === 'generated');
105+
106+
expect(generatedAssignments.length).toBe(result.generatedCount);
107+
generatedAssignments.forEach((assignment) => {
108+
expect(assignment.strategy.source).toBe('hsl-golden-ratio');
109+
});
110+
});
111+
112+
test('all series get unique colors', () => {
113+
const manyEntities = Array.from({ length: 25 }, (_, i) => `entity${i}`);
114+
const result = assignAnalyticsChartColors(manyEntities);
115+
116+
const colors = Object.values(result.assignments).map((a) => a.color);
117+
const uniqueColors = new Set(colors);
118+
119+
expect(uniqueColors.size).toBe(colors.length);
120+
});
121+
});
122+
123+
describe('User Override Functionality', () => {
124+
test('respects user overrides', () => {
125+
const overrides = { schema: '#FF0000', dataset: '#00FF00' };
126+
const result = assignAnalyticsChartColors(['schema', 'dataset'], overrides);
127+
128+
expect(result.assignments.schema.color).toBe('#FF0000');
129+
expect(result.assignments.schema.userOverride).toBe('#FF0000');
130+
expect(result.assignments.dataset.color).toBe('#00FF00');
131+
expect(result.assignments.dataset.userOverride).toBe('#00FF00');
132+
});
133+
134+
test('user overrides take precedence over entity colors', () => {
135+
const overrides = { dataset: '#123456' };
136+
const result = assignAnalyticsChartColors(['dataset'], overrides);
137+
138+
expect(result.assignments.dataset.color).toBe('#123456');
139+
expect(result.assignments.dataset.color).not.toBe(DATAHUB_ENTITY_COLORS.dataset);
140+
});
141+
142+
test('partial overrides work with automatic assignment', () => {
143+
const overrides = { entity1: '#AAAAAA' };
144+
const result = assignAnalyticsChartColors(['entity1', 'dataset', 'entity2'], overrides);
145+
146+
expect(result.assignments.entity1.color).toBe('#AAAAAA');
147+
expect(result.assignments.dataset.color).toBe(DATAHUB_ENTITY_COLORS.dataset);
148+
expect(result.assignments.entity2.strategy.type).toBe('qualitative');
149+
});
150+
});
151+
152+
describe('Color Assignment Priority', () => {
153+
test('follows correct priority order', () => {
154+
const overrides = { override: '#CUSTOM' };
155+
const seriesKeys = ['override', 'dataset', 'unknown', 'another'];
156+
157+
const result = assignAnalyticsChartColors(seriesKeys, overrides);
158+
159+
// Priority 1: User override
160+
expect(result.assignments.override.strategy.source).toBe('user-override');
161+
162+
// Priority 2: Entity color
163+
expect(result.assignments.dataset.strategy.source).toBe('datahub-entity');
164+
165+
// Priority 3: Qualitative
166+
expect(result.assignments.unknown.strategy.type).toBe('qualitative');
167+
expect(result.assignments.another.strategy.type).toBe('qualitative');
168+
});
169+
170+
test('does not reuse colors across different sources', () => {
171+
const seriesKeys = ['dataset', 'schema', 'unknown1', 'unknown2'];
172+
const result = assignAnalyticsChartColors(seriesKeys);
173+
174+
const colors = Object.values(result.assignments).map((a) => a.color);
175+
const uniqueColors = new Set(colors);
176+
177+
expect(uniqueColors.size).toBe(colors.length);
178+
});
179+
});
180+
181+
describe('Edge Cases', () => {
182+
test('handles empty series array', () => {
183+
const result = assignAnalyticsChartColors([]);
184+
185+
expect(Object.keys(result.assignments)).toHaveLength(0);
186+
expect(result.generatedCount).toBe(0);
187+
expect(result.unusedColors.length).toBe(QUALITATIVE_COLORS.length);
188+
});
189+
190+
test('handles single series', () => {
191+
const result = assignAnalyticsChartColors(['dataset']);
192+
193+
expect(Object.keys(result.assignments)).toHaveLength(1);
194+
expect(result.assignments.dataset.color).toBe(DATAHUB_ENTITY_COLORS.dataset);
195+
});
196+
197+
test('handles duplicate series keys', () => {
198+
const result = assignAnalyticsChartColors(['dataset', 'dataset']);
199+
200+
expect(Object.keys(result.assignments)).toHaveLength(1);
201+
expect(result.assignments.dataset.color).toBe(DATAHUB_ENTITY_COLORS.dataset);
202+
});
203+
204+
test('handles very long series names', () => {
205+
const longName = 'a'.repeat(100);
206+
const result = assignAnalyticsChartColors([longName]);
207+
208+
expect(result.assignments[longName]).toBeDefined();
209+
expect(result.assignments[longName].color).toBeTruthy();
210+
});
211+
});
212+
213+
describe('getAllEntityMatches utility', () => {
214+
test('returns all matching entities', () => {
215+
const matches = getAllEntityMatches('dataset');
216+
217+
expect(matches.length).toBeGreaterThan(0);
218+
expect(matches.some((m) => m.entity === 'dataset')).toBe(true);
219+
matches.forEach((match) => {
220+
expect(match.color).toBeTruthy();
221+
});
222+
});
223+
224+
test('returns empty array for no matches', () => {
225+
const matches = getAllEntityMatches('xyz123notfound');
226+
227+
expect(matches.length).toBe(0);
228+
});
229+
});

0 commit comments

Comments
 (0)