datajunction-ui 0.0.1-a109 → 0.0.1-a110
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +3 -2
- package/src/app/icons/AlertIcon.jsx +1 -0
- package/src/app/icons/InvalidIcon.jsx +5 -3
- package/src/app/icons/NodeIcon.jsx +49 -0
- package/src/app/icons/ValidIcon.jsx +5 -3
- package/src/app/index.tsx +6 -0
- package/src/app/pages/OverviewPage/ByStatusPanel.jsx +69 -0
- package/src/app/pages/OverviewPage/DimensionNodeUsagePanel.jsx +48 -0
- package/src/app/pages/OverviewPage/GovernanceWarningsPanel.jsx +107 -0
- package/src/app/pages/OverviewPage/Loadable.jsx +16 -0
- package/src/app/pages/OverviewPage/NodesByTypePanel.jsx +63 -0
- package/src/app/pages/OverviewPage/OverviewPanel.jsx +94 -0
- package/src/app/pages/OverviewPage/TrendsPanel.jsx +66 -0
- package/src/app/pages/OverviewPage/__tests__/ByStatusPanel.test.jsx +36 -0
- package/src/app/pages/OverviewPage/__tests__/DimensionNodeUsagePanel.test.jsx +76 -0
- package/src/app/pages/OverviewPage/__tests__/GovernanceWarningsPanel.test.jsx +77 -0
- package/src/app/pages/OverviewPage/__tests__/NodesByTypePanel.test.jsx +86 -0
- package/src/app/pages/OverviewPage/__tests__/OverviewPanel.test.jsx +78 -0
- package/src/app/pages/OverviewPage/__tests__/TrendsPanel.test.jsx +120 -0
- package/src/app/pages/OverviewPage/__tests__/index.test.jsx +54 -0
- package/src/app/pages/OverviewPage/index.jsx +22 -0
- package/src/app/pages/RegisterTablePage/__tests__/__snapshots__/RegisterTablePage.test.jsx.snap +3 -2
- package/src/app/services/DJService.js +122 -0
- package/src/app/services/__tests__/DJService.test.jsx +364 -0
- package/src/styles/overview.css +72 -0
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { render, screen, waitFor } from '@testing-library/react';
|
|
2
|
+
import DJClientContext from '../../../providers/djclient';
|
|
3
|
+
import { DimensionNodeUsagePanel } from '../DimensionNodeUsagePanel';
|
|
4
|
+
|
|
5
|
+
describe('<DimensionNodeUsagePanel />', () => {
|
|
6
|
+
it('fetches dimension nodes and displays them in sorted order', async () => {
|
|
7
|
+
const mockDimensions = [
|
|
8
|
+
{ name: 'dimension_a', indegree: 2, cube_count: 5 }, // 7
|
|
9
|
+
{ name: 'dimension_b', indegree: 1, cube_count: 10 }, // 11
|
|
10
|
+
{ name: 'dimension_c', indegree: 3, cube_count: 3 }, // 6
|
|
11
|
+
];
|
|
12
|
+
|
|
13
|
+
const mockDjClient = {
|
|
14
|
+
system: {
|
|
15
|
+
dimensions: jest.fn().mockResolvedValue(mockDimensions),
|
|
16
|
+
},
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
render(
|
|
20
|
+
<DJClientContext.Provider value={{ DataJunctionAPI: mockDjClient }}>
|
|
21
|
+
<DimensionNodeUsagePanel />
|
|
22
|
+
</DJClientContext.Provider>,
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
// Wait for the API to be called
|
|
26
|
+
await waitFor(() => {
|
|
27
|
+
expect(mockDjClient.system.dimensions).toHaveBeenCalled();
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
// Check that rows are rendered
|
|
31
|
+
expect(screen.getByText('dimension_b')).toBeInTheDocument();
|
|
32
|
+
expect(screen.getByText('dimension_a')).toBeInTheDocument();
|
|
33
|
+
expect(screen.getByText('dimension_c')).toBeInTheDocument();
|
|
34
|
+
|
|
35
|
+
// Check that sorting is correct: b (11), a (7), c (6)
|
|
36
|
+
const rows = screen.getAllByRole('row').slice(1); // skip the header row
|
|
37
|
+
const names = rows.map(row => row.querySelector('a').textContent);
|
|
38
|
+
expect(names).toEqual(['dimension_b', 'dimension_a', 'dimension_c']);
|
|
39
|
+
|
|
40
|
+
// Check that links have correct hrefs
|
|
41
|
+
expect(screen.getByText('dimension_b').closest('a')).toHaveAttribute(
|
|
42
|
+
'href',
|
|
43
|
+
'/nodes/dimension_b',
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
// Check indegree and cube_count cells
|
|
47
|
+
expect(screen.getByText('1')).toBeInTheDocument(); // b indegree
|
|
48
|
+
expect(screen.getByText('10')).toBeInTheDocument(); // b cube_count
|
|
49
|
+
expect(screen.getByText('2')).toBeInTheDocument(); // a indegree
|
|
50
|
+
expect(screen.getByText('5')).toBeInTheDocument(); // a cube_count
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('handles empty dimensions gracefully', async () => {
|
|
54
|
+
const mockDjClient = {
|
|
55
|
+
system: {
|
|
56
|
+
dimensions: jest.fn().mockResolvedValue([]),
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
render(
|
|
61
|
+
<DJClientContext.Provider value={{ DataJunctionAPI: mockDjClient }}>
|
|
62
|
+
<DimensionNodeUsagePanel />
|
|
63
|
+
</DJClientContext.Provider>,
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
await waitFor(() => {
|
|
67
|
+
expect(mockDjClient.system.dimensions).toHaveBeenCalled();
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
// Table should still be there, but no rows
|
|
71
|
+
expect(screen.getByText('Dimension Node Usage')).toBeInTheDocument();
|
|
72
|
+
expect(
|
|
73
|
+
screen.queryByRole('row', { name: /dimension_/ }),
|
|
74
|
+
).not.toBeInTheDocument();
|
|
75
|
+
});
|
|
76
|
+
});
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { render, screen, waitFor } from '@testing-library/react';
|
|
2
|
+
import DJClientContext from '../../../providers/djclient';
|
|
3
|
+
import { GovernanceWarningsPanel } from '../GovernanceWarningsPanel';
|
|
4
|
+
|
|
5
|
+
describe('<GovernanceWarningsPanel />', () => {
|
|
6
|
+
it('fetches governance warnings and displays percentages and orphaned dimension count', async () => {
|
|
7
|
+
const mockNodesWithoutDescription = [
|
|
8
|
+
{ name: 'SOURCE', value: 0.1 }, // 10%
|
|
9
|
+
{ name: 'METRIC', value: 0.2 }, // 20%
|
|
10
|
+
];
|
|
11
|
+
|
|
12
|
+
const mockDimensions = [
|
|
13
|
+
{ name: 'dim_1', indegree: 0, cube_count: 1 }, // orphaned
|
|
14
|
+
{ name: 'dim_2', indegree: 1, cube_count: 0 }, // orphaned
|
|
15
|
+
{ name: 'dim_3', indegree: 2, cube_count: 1 }, // not orphaned
|
|
16
|
+
];
|
|
17
|
+
|
|
18
|
+
const mockDjClient = {
|
|
19
|
+
system: {
|
|
20
|
+
nodes_without_description: jest
|
|
21
|
+
.fn()
|
|
22
|
+
.mockResolvedValue(mockNodesWithoutDescription),
|
|
23
|
+
dimensions: jest.fn().mockResolvedValue(mockDimensions),
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
render(
|
|
28
|
+
<DJClientContext.Provider value={{ DataJunctionAPI: mockDjClient }}>
|
|
29
|
+
<GovernanceWarningsPanel />
|
|
30
|
+
</DJClientContext.Provider>,
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
// Wait for both calls to be made and data rendered
|
|
34
|
+
await waitFor(() => {
|
|
35
|
+
expect(mockDjClient.system.nodes_without_description).toHaveBeenCalled();
|
|
36
|
+
expect(mockDjClient.system.dimensions).toHaveBeenCalled();
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
// Check missing description badges
|
|
40
|
+
expect(screen.getByText('10%')).toBeInTheDocument();
|
|
41
|
+
expect(screen.getByText('20%')).toBeInTheDocument();
|
|
42
|
+
expect(screen.getByText('sources')).toBeInTheDocument();
|
|
43
|
+
expect(screen.getByText('metrics')).toBeInTheDocument();
|
|
44
|
+
|
|
45
|
+
// Check orphaned dimension count: should be 2
|
|
46
|
+
expect(screen.getByText('2')).toBeInTheDocument();
|
|
47
|
+
expect(screen.getByText(/dimension nodes/)).toBeInTheDocument();
|
|
48
|
+
|
|
49
|
+
// Should show the title
|
|
50
|
+
expect(screen.getByText('Governance Warnings')).toBeInTheDocument();
|
|
51
|
+
expect(screen.getByText('Missing Description')).toBeInTheDocument();
|
|
52
|
+
expect(screen.getByText('Orphaned Dimensions')).toBeInTheDocument();
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('shows fallback if no data returned', async () => {
|
|
56
|
+
const mockDjClient = {
|
|
57
|
+
system: {
|
|
58
|
+
nodes_without_description: jest.fn().mockResolvedValue([]),
|
|
59
|
+
dimensions: jest.fn().mockResolvedValue([]),
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
render(
|
|
64
|
+
<DJClientContext.Provider value={{ DataJunctionAPI: mockDjClient }}>
|
|
65
|
+
<GovernanceWarningsPanel />
|
|
66
|
+
</DJClientContext.Provider>,
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
await waitFor(() => {
|
|
70
|
+
expect(mockDjClient.system.nodes_without_description).toHaveBeenCalled();
|
|
71
|
+
expect(mockDjClient.system.dimensions).toHaveBeenCalled();
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
// Should show fallback value for orphaned nodes
|
|
75
|
+
expect(screen.getByText('...')).toBeInTheDocument();
|
|
76
|
+
});
|
|
77
|
+
});
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { render, screen, waitFor } from '@testing-library/react';
|
|
2
|
+
import DJClientContext from '../../../providers/djclient';
|
|
3
|
+
import { NodesByTypePanel } from '../NodesByTypePanel';
|
|
4
|
+
|
|
5
|
+
describe('<NodesByTypePanel />', () => {
|
|
6
|
+
it('fetches nodes & materializations by type and renders them correctly', async () => {
|
|
7
|
+
const mockNodesByType = [
|
|
8
|
+
{ name: 'source', value: 5 },
|
|
9
|
+
{ name: 'cube', value: 2 },
|
|
10
|
+
];
|
|
11
|
+
const mockMaterializationsByType = [
|
|
12
|
+
{ name: 'transform', value: 3 },
|
|
13
|
+
{ name: 'metric', value: 1 },
|
|
14
|
+
];
|
|
15
|
+
|
|
16
|
+
const mockDjClient = {
|
|
17
|
+
system: {
|
|
18
|
+
node_counts_by_type: jest.fn().mockResolvedValue(mockNodesByType),
|
|
19
|
+
materialization_counts_by_type: jest
|
|
20
|
+
.fn()
|
|
21
|
+
.mockResolvedValue(mockMaterializationsByType),
|
|
22
|
+
},
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
render(
|
|
26
|
+
<DJClientContext.Provider value={{ DataJunctionAPI: mockDjClient }}>
|
|
27
|
+
<NodesByTypePanel />
|
|
28
|
+
</DJClientContext.Provider>,
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
// Wait for async calls to complete
|
|
32
|
+
await waitFor(() => {
|
|
33
|
+
expect(mockDjClient.system.node_counts_by_type).toHaveBeenCalled();
|
|
34
|
+
expect(
|
|
35
|
+
mockDjClient.system.materialization_counts_by_type,
|
|
36
|
+
).toHaveBeenCalled();
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
// Check that nodes by type appear
|
|
40
|
+
expect(screen.getByText('Nodes by Type')).toBeInTheDocument();
|
|
41
|
+
expect(screen.getByText('5')).toBeInTheDocument();
|
|
42
|
+
expect(screen.getByText('sources')).toBeInTheDocument();
|
|
43
|
+
expect(screen.getByText('2')).toBeInTheDocument();
|
|
44
|
+
expect(screen.getByText('cubes')).toBeInTheDocument();
|
|
45
|
+
|
|
46
|
+
// Check that materializations by type appear
|
|
47
|
+
expect(screen.getByText('Materializations by Type')).toBeInTheDocument();
|
|
48
|
+
expect(screen.getByText('3')).toBeInTheDocument();
|
|
49
|
+
expect(screen.getByText('transforms')).toBeInTheDocument();
|
|
50
|
+
expect(screen.getByText('1')).toBeInTheDocument();
|
|
51
|
+
expect(screen.getByText('metrics')).toBeInTheDocument();
|
|
52
|
+
|
|
53
|
+
// Should render an icon for each entry
|
|
54
|
+
const icons = screen.getAllByTestId('node-icon');
|
|
55
|
+
expect(icons).toHaveLength(
|
|
56
|
+
mockNodesByType.length + mockMaterializationsByType.length,
|
|
57
|
+
);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('renders nothing if no data returned', async () => {
|
|
61
|
+
const mockDjClient = {
|
|
62
|
+
system: {
|
|
63
|
+
node_counts_by_type: jest.fn().mockResolvedValue([]),
|
|
64
|
+
materialization_counts_by_type: jest.fn().mockResolvedValue([]),
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
render(
|
|
69
|
+
<DJClientContext.Provider value={{ DataJunctionAPI: mockDjClient }}>
|
|
70
|
+
<NodesByTypePanel />
|
|
71
|
+
</DJClientContext.Provider>,
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
await waitFor(() => {
|
|
75
|
+
expect(mockDjClient.system.node_counts_by_type).toHaveBeenCalled();
|
|
76
|
+
expect(
|
|
77
|
+
mockDjClient.system.materialization_counts_by_type,
|
|
78
|
+
).toHaveBeenCalled();
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
expect(screen.getByText('Nodes by Type')).toBeInTheDocument();
|
|
82
|
+
expect(screen.getByText('Materializations by Type')).toBeInTheDocument();
|
|
83
|
+
expect(screen.queryByText('sources')).not.toBeInTheDocument();
|
|
84
|
+
expect(screen.queryByText('metrics')).not.toBeInTheDocument();
|
|
85
|
+
});
|
|
86
|
+
});
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { render, screen, waitFor } from '@testing-library/react';
|
|
2
|
+
import DJClientContext from '../../../providers/djclient';
|
|
3
|
+
import { OverviewPanel } from '../OverviewPanel';
|
|
4
|
+
|
|
5
|
+
describe('<OverviewPanel />', () => {
|
|
6
|
+
it('renders Active Nodes and Valid/Invalid nodes correctly', async () => {
|
|
7
|
+
const mockNodesByActive = [
|
|
8
|
+
{ name: 'true', value: 7 },
|
|
9
|
+
{ name: 'false', value: 2 }, // Should be filtered out
|
|
10
|
+
];
|
|
11
|
+
|
|
12
|
+
const mockNodesByStatus = [
|
|
13
|
+
{ name: 'VALID', value: 5 },
|
|
14
|
+
{ name: 'INVALID', value: 3 },
|
|
15
|
+
];
|
|
16
|
+
|
|
17
|
+
const mockDjClient = {
|
|
18
|
+
system: {
|
|
19
|
+
node_counts_by_active: jest.fn().mockResolvedValue(mockNodesByActive),
|
|
20
|
+
node_counts_by_status: jest.fn().mockResolvedValue(mockNodesByStatus),
|
|
21
|
+
},
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
render(
|
|
25
|
+
<DJClientContext.Provider value={{ DataJunctionAPI: mockDjClient }}>
|
|
26
|
+
<OverviewPanel />
|
|
27
|
+
</DJClientContext.Provider>,
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
await waitFor(() => {
|
|
31
|
+
expect(mockDjClient.system.node_counts_by_active).toHaveBeenCalled();
|
|
32
|
+
expect(mockDjClient.system.node_counts_by_status).toHaveBeenCalled();
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
// Chart title
|
|
36
|
+
expect(screen.getByText('Overview')).toBeInTheDocument();
|
|
37
|
+
|
|
38
|
+
// Active Nodes section
|
|
39
|
+
expect(screen.getByText('7')).toBeInTheDocument();
|
|
40
|
+
expect(screen.getByText('Active Nodes')).toBeInTheDocument();
|
|
41
|
+
expect(screen.getAllByTestId('node-icon')).toHaveLength(1);
|
|
42
|
+
|
|
43
|
+
// Valid/Invalid status section
|
|
44
|
+
expect(screen.getByText('5')).toBeInTheDocument();
|
|
45
|
+
expect(screen.getByText('valid')).toBeInTheDocument();
|
|
46
|
+
expect(screen.getByText('3')).toBeInTheDocument();
|
|
47
|
+
expect(screen.getByText('invalid')).toBeInTheDocument();
|
|
48
|
+
|
|
49
|
+
// Should render one ValidIcon and one InvalidIcon
|
|
50
|
+
expect(screen.getAllByTestId('valid-icon')).toHaveLength(1);
|
|
51
|
+
expect(screen.getAllByTestId('invalid-icon')).toHaveLength(1);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('renders no badges if data is empty', async () => {
|
|
55
|
+
const mockDjClient = {
|
|
56
|
+
system: {
|
|
57
|
+
node_counts_by_active: jest.fn().mockResolvedValue([]),
|
|
58
|
+
node_counts_by_status: jest.fn().mockResolvedValue([]),
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
render(
|
|
63
|
+
<DJClientContext.Provider value={{ DataJunctionAPI: mockDjClient }}>
|
|
64
|
+
<OverviewPanel />
|
|
65
|
+
</DJClientContext.Provider>,
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
await waitFor(() => {
|
|
69
|
+
expect(mockDjClient.system.node_counts_by_active).toHaveBeenCalled();
|
|
70
|
+
expect(mockDjClient.system.node_counts_by_status).toHaveBeenCalled();
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
expect(screen.getByText('Overview')).toBeInTheDocument();
|
|
74
|
+
expect(screen.queryByTestId('node-icon')).not.toBeInTheDocument();
|
|
75
|
+
expect(screen.queryByTestId('valid-icon')).not.toBeInTheDocument();
|
|
76
|
+
expect(screen.queryByTestId('invalid-icon')).not.toBeInTheDocument();
|
|
77
|
+
});
|
|
78
|
+
});
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { render, screen, waitFor } from '@testing-library/react';
|
|
2
|
+
import DJClientContext from '../../../providers/djclient';
|
|
3
|
+
import { TrendsPanel } from '../TrendsPanel';
|
|
4
|
+
|
|
5
|
+
jest.mock('recharts', () => {
|
|
6
|
+
const Original = jest.requireActual('recharts');
|
|
7
|
+
return {
|
|
8
|
+
...Original,
|
|
9
|
+
ResponsiveContainer: ({ children, ...props }) => (
|
|
10
|
+
<div data-testid="responsive-container">{children}</div>
|
|
11
|
+
),
|
|
12
|
+
BarChart: ({ children, data, ...props }) => (
|
|
13
|
+
<div data-testid="barchart" data-data={JSON.stringify(data)}>
|
|
14
|
+
{children}
|
|
15
|
+
</div>
|
|
16
|
+
),
|
|
17
|
+
CartesianGrid: props => <div data-testid="cartesian-grid" />,
|
|
18
|
+
XAxis: props => <div data-testid="x-axis" />,
|
|
19
|
+
YAxis: props => <div data-testid="y-axis" />,
|
|
20
|
+
Tooltip: props => <div data-testid="tooltip" />,
|
|
21
|
+
Legend: props => <div data-testid="legend" />,
|
|
22
|
+
Bar: props => (
|
|
23
|
+
<div data-testid={`bar-${props.dataKey}`} data-fill={props.fill} />
|
|
24
|
+
),
|
|
25
|
+
};
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
describe('<TrendsPanel />', () => {
|
|
29
|
+
it('fetches and renders node trends', async () => {
|
|
30
|
+
const mockNodeTrends = [
|
|
31
|
+
{
|
|
32
|
+
date: '2024-01-01',
|
|
33
|
+
source: 2,
|
|
34
|
+
dimension: 1,
|
|
35
|
+
transform: 3,
|
|
36
|
+
metric: 5,
|
|
37
|
+
cube: 0,
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
date: '2024-01-02',
|
|
41
|
+
source: 1,
|
|
42
|
+
dimension: 2,
|
|
43
|
+
transform: 1,
|
|
44
|
+
metric: 3,
|
|
45
|
+
cube: 4,
|
|
46
|
+
},
|
|
47
|
+
];
|
|
48
|
+
|
|
49
|
+
const mockDjClient = {
|
|
50
|
+
system: {
|
|
51
|
+
node_trends: jest.fn().mockResolvedValue(mockNodeTrends),
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
render(
|
|
56
|
+
<DJClientContext.Provider value={{ DataJunctionAPI: mockDjClient }}>
|
|
57
|
+
<TrendsPanel />
|
|
58
|
+
</DJClientContext.Provider>,
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
await waitFor(() => {
|
|
62
|
+
expect(mockDjClient.system.node_trends).toHaveBeenCalled();
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
// Chart title
|
|
66
|
+
expect(screen.getByText('Trends')).toBeInTheDocument();
|
|
67
|
+
|
|
68
|
+
// The Recharts containers render
|
|
69
|
+
expect(screen.getByTestId('responsive-container')).toBeInTheDocument();
|
|
70
|
+
expect(screen.getByTestId('barchart')).toBeInTheDocument();
|
|
71
|
+
|
|
72
|
+
// Should contain grid, axes, tooltip, legend
|
|
73
|
+
expect(screen.getByTestId('cartesian-grid')).toBeInTheDocument();
|
|
74
|
+
expect(screen.getByTestId('x-axis')).toBeInTheDocument();
|
|
75
|
+
expect(screen.getByTestId('y-axis')).toBeInTheDocument();
|
|
76
|
+
expect(screen.getByTestId('tooltip')).toBeInTheDocument();
|
|
77
|
+
expect(screen.getByTestId('legend')).toBeInTheDocument();
|
|
78
|
+
|
|
79
|
+
// Should render bars with expected colors
|
|
80
|
+
const expectedBars = {
|
|
81
|
+
source: '#00C49F',
|
|
82
|
+
dimension: '#FFBB28',
|
|
83
|
+
transform: '#0088FE',
|
|
84
|
+
metric: '#FF91A3',
|
|
85
|
+
cube: '#AA46BE',
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
Object.entries(expectedBars).forEach(([key, color]) => {
|
|
89
|
+
const bar = screen.getByTestId(`bar-${key}`);
|
|
90
|
+
expect(bar).toBeInTheDocument();
|
|
91
|
+
expect(bar).toHaveAttribute('data-fill', color);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
// BarChart gets correct data
|
|
95
|
+
const barChart = screen.getByTestId('barchart');
|
|
96
|
+
expect(barChart.getAttribute('data-data')).toContain('2024-01-01');
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('renders with empty trends', async () => {
|
|
100
|
+
const mockDjClient = {
|
|
101
|
+
system: {
|
|
102
|
+
node_trends: jest.fn().mockResolvedValue([]),
|
|
103
|
+
},
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
render(
|
|
107
|
+
<DJClientContext.Provider value={{ DataJunctionAPI: mockDjClient }}>
|
|
108
|
+
<TrendsPanel />
|
|
109
|
+
</DJClientContext.Provider>,
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
await waitFor(() => {
|
|
113
|
+
expect(mockDjClient.system.node_trends).toHaveBeenCalled();
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
expect(screen.getByText('Trends')).toBeInTheDocument();
|
|
117
|
+
expect(screen.getByTestId('responsive-container')).toBeInTheDocument();
|
|
118
|
+
expect(screen.getByTestId('barchart')).toBeInTheDocument();
|
|
119
|
+
});
|
|
120
|
+
});
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { render, screen } from '@testing-library/react';
|
|
2
|
+
import { OverviewPage } from '../index';
|
|
3
|
+
|
|
4
|
+
jest.mock('../OverviewPanel', () => ({
|
|
5
|
+
OverviewPanel: () => <div data-testid="overview-panel">OverviewPanel</div>,
|
|
6
|
+
}));
|
|
7
|
+
jest.mock('../NodesByTypePanel', () => ({
|
|
8
|
+
NodesByTypePanel: () => (
|
|
9
|
+
<div data-testid="nodes-by-type-panel">NodesByTypePanel</div>
|
|
10
|
+
),
|
|
11
|
+
}));
|
|
12
|
+
jest.mock('../GovernanceWarningsPanel', () => ({
|
|
13
|
+
GovernanceWarningsPanel: () => (
|
|
14
|
+
<div data-testid="governance-warnings-panel">GovernanceWarningsPanel</div>
|
|
15
|
+
),
|
|
16
|
+
}));
|
|
17
|
+
jest.mock('../TrendsPanel', () => ({
|
|
18
|
+
TrendsPanel: () => <div data-testid="trends-panel">TrendsPanel</div>,
|
|
19
|
+
}));
|
|
20
|
+
jest.mock('../DimensionNodeUsagePanel', () => ({
|
|
21
|
+
DimensionNodeUsagePanel: () => (
|
|
22
|
+
<div data-testid="dimension-node-usage-panel">DimensionNodeUsagePanel</div>
|
|
23
|
+
),
|
|
24
|
+
}));
|
|
25
|
+
|
|
26
|
+
describe('<OverviewPage />', () => {
|
|
27
|
+
it('renders the OverviewPage with all panels', () => {
|
|
28
|
+
render(<OverviewPage />);
|
|
29
|
+
|
|
30
|
+
// Check the main containers
|
|
31
|
+
const containers = screen.getAllByClassName
|
|
32
|
+
? screen.getAllByClassName('chart-container')
|
|
33
|
+
: document.querySelectorAll('.chart-container');
|
|
34
|
+
|
|
35
|
+
expect(containers.length).toBe(2);
|
|
36
|
+
|
|
37
|
+
// Check each mocked panel appears
|
|
38
|
+
expect(screen.getByTestId('overview-panel')).toBeInTheDocument();
|
|
39
|
+
expect(screen.getByTestId('nodes-by-type-panel')).toBeInTheDocument();
|
|
40
|
+
expect(screen.getByTestId('governance-warnings-panel')).toBeInTheDocument();
|
|
41
|
+
expect(screen.getByTestId('trends-panel')).toBeInTheDocument();
|
|
42
|
+
expect(
|
|
43
|
+
screen.getByTestId('dimension-node-usage-panel'),
|
|
44
|
+
).toBeInTheDocument();
|
|
45
|
+
|
|
46
|
+
// Check panels are in the correct containers
|
|
47
|
+
expect(containers[0].innerHTML).toContain('OverviewPanel');
|
|
48
|
+
expect(containers[0].innerHTML).toContain('NodesByTypePanel');
|
|
49
|
+
expect(containers[0].innerHTML).toContain('GovernanceWarningsPanel');
|
|
50
|
+
|
|
51
|
+
expect(containers[1].innerHTML).toContain('TrendsPanel');
|
|
52
|
+
expect(containers[1].innerHTML).toContain('DimensionNodeUsagePanel');
|
|
53
|
+
});
|
|
54
|
+
});
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { OverviewPanel } from './OverviewPanel';
|
|
2
|
+
import { NodesByTypePanel } from './NodesByTypePanel';
|
|
3
|
+
import { GovernanceWarningsPanel } from './GovernanceWarningsPanel';
|
|
4
|
+
import { TrendsPanel } from './TrendsPanel';
|
|
5
|
+
import { DimensionNodeUsagePanel } from './DimensionNodeUsagePanel';
|
|
6
|
+
|
|
7
|
+
export function OverviewPage() {
|
|
8
|
+
return (
|
|
9
|
+
<div className="mid">
|
|
10
|
+
<div className="chart-container">
|
|
11
|
+
<OverviewPanel />
|
|
12
|
+
<NodesByTypePanel />
|
|
13
|
+
<GovernanceWarningsPanel />
|
|
14
|
+
</div>
|
|
15
|
+
|
|
16
|
+
<div className="chart-container">
|
|
17
|
+
<TrendsPanel />
|
|
18
|
+
<DimensionNodeUsagePanel />
|
|
19
|
+
</div>
|
|
20
|
+
</div>
|
|
21
|
+
);
|
|
22
|
+
}
|
package/src/app/pages/RegisterTablePage/__tests__/__snapshots__/RegisterTablePage.test.jsx.snap
CHANGED
|
@@ -8,10 +8,11 @@ HTMLCollection [
|
|
|
8
8
|
>
|
|
9
9
|
<svg
|
|
10
10
|
class="bi bi-check-circle-fill"
|
|
11
|
+
data-testid="valid-icon"
|
|
11
12
|
fill="currentColor"
|
|
12
|
-
height="
|
|
13
|
+
height="25px"
|
|
13
14
|
viewBox="0 0 16 16"
|
|
14
|
-
width="
|
|
15
|
+
width="25px"
|
|
15
16
|
xmlns="http://www.w3.org/2000/svg"
|
|
16
17
|
>
|
|
17
18
|
<path
|
|
@@ -88,6 +88,128 @@ export const DataJunctionAPI = {
|
|
|
88
88
|
).json();
|
|
89
89
|
},
|
|
90
90
|
|
|
91
|
+
querySystemMetric: async function ({
|
|
92
|
+
metric,
|
|
93
|
+
dimensions = [],
|
|
94
|
+
filters = [],
|
|
95
|
+
orderby = [],
|
|
96
|
+
}) {
|
|
97
|
+
const params = new URLSearchParams();
|
|
98
|
+
dimensions.forEach(d => params.append('dimensions', d));
|
|
99
|
+
filters.forEach(f => params.append('filters', f));
|
|
100
|
+
orderby.forEach(o => params.append('orderby', o));
|
|
101
|
+
|
|
102
|
+
const url = `${DJ_URL}/system/data/${metric}?${params.toString()}`;
|
|
103
|
+
const res = await fetch(url, { credentials: 'include' });
|
|
104
|
+
|
|
105
|
+
if (!res.ok) {
|
|
106
|
+
throw new Error(`Failed to fetch metric data ${metric}: ${res.status}`);
|
|
107
|
+
}
|
|
108
|
+
return await res.json();
|
|
109
|
+
},
|
|
110
|
+
|
|
111
|
+
querySystemMetricSingleDimension: async function ({
|
|
112
|
+
metric,
|
|
113
|
+
dimension,
|
|
114
|
+
filters = [],
|
|
115
|
+
orderby = [],
|
|
116
|
+
}) {
|
|
117
|
+
const results = await DataJunctionAPI.querySystemMetric({
|
|
118
|
+
metric: metric,
|
|
119
|
+
dimensions: [dimension],
|
|
120
|
+
filters: filters,
|
|
121
|
+
orderby: orderby,
|
|
122
|
+
});
|
|
123
|
+
return results.map(row => {
|
|
124
|
+
return {
|
|
125
|
+
name:
|
|
126
|
+
row.find(entry => entry.col === dimension)?.value?.toString() ??
|
|
127
|
+
'unknown',
|
|
128
|
+
value: row.find(entry => entry.col === metric)?.value ?? 0,
|
|
129
|
+
};
|
|
130
|
+
});
|
|
131
|
+
},
|
|
132
|
+
|
|
133
|
+
system: {
|
|
134
|
+
node_counts_by_active: async function () {
|
|
135
|
+
return DataJunctionAPI.querySystemMetricSingleDimension({
|
|
136
|
+
metric: 'system.dj.number_of_nodes',
|
|
137
|
+
dimension: 'system.dj.is_active.active_id',
|
|
138
|
+
});
|
|
139
|
+
},
|
|
140
|
+
node_counts_by_type: async function () {
|
|
141
|
+
return DataJunctionAPI.querySystemMetricSingleDimension({
|
|
142
|
+
metric: 'system.dj.number_of_nodes',
|
|
143
|
+
dimension: 'system.dj.node_type.type',
|
|
144
|
+
filters: ['system.dj.is_active.active_id=true'],
|
|
145
|
+
orderby: ['system.dj.node_type.type'],
|
|
146
|
+
});
|
|
147
|
+
},
|
|
148
|
+
node_counts_by_status: async function () {
|
|
149
|
+
return DataJunctionAPI.querySystemMetricSingleDimension({
|
|
150
|
+
metric: 'system.dj.number_of_nodes',
|
|
151
|
+
dimension: 'system.dj.nodes.status',
|
|
152
|
+
filters: ['system.dj.is_active.active_id=true'],
|
|
153
|
+
orderby: ['system.dj.nodes.status'],
|
|
154
|
+
});
|
|
155
|
+
},
|
|
156
|
+
nodes_without_description: async function () {
|
|
157
|
+
return DataJunctionAPI.querySystemMetricSingleDimension({
|
|
158
|
+
metric: 'system.dj.node_without_description',
|
|
159
|
+
dimension: 'system.dj.node_type.type',
|
|
160
|
+
filters: ['system.dj.is_active.active_id=true'],
|
|
161
|
+
orderby: ['system.dj.node_type.type'],
|
|
162
|
+
});
|
|
163
|
+
},
|
|
164
|
+
node_trends: async function () {
|
|
165
|
+
const results = await (
|
|
166
|
+
await fetch(
|
|
167
|
+
`${DJ_URL}/system/data/system.dj.number_of_nodes?dimensions=system.dj.nodes.created_at_week&dimensions=system.dj.node_type.type&filters=system.dj.nodes.created_at_week>=20240101&orderby=system.dj.nodes.created_at_week`,
|
|
168
|
+
{ credentials: 'include' },
|
|
169
|
+
)
|
|
170
|
+
).json();
|
|
171
|
+
const byDateint = {};
|
|
172
|
+
results.forEach(row => {
|
|
173
|
+
const dateint = row.find(
|
|
174
|
+
r => r.col === 'system.dj.nodes.created_at_week',
|
|
175
|
+
)?.value;
|
|
176
|
+
const nodeType = row.find(
|
|
177
|
+
r => r.col === 'system.dj.node_type.type',
|
|
178
|
+
)?.value;
|
|
179
|
+
const count = row.find(
|
|
180
|
+
r => r.col === 'system.dj.number_of_nodes',
|
|
181
|
+
)?.value;
|
|
182
|
+
if (!byDateint[dateint]) {
|
|
183
|
+
byDateint[dateint] = { date: dateint };
|
|
184
|
+
}
|
|
185
|
+
byDateint[dateint][nodeType] =
|
|
186
|
+
(byDateint[dateint][nodeType] || 0) + count;
|
|
187
|
+
});
|
|
188
|
+
return Object.entries(byDateint).map(([dateint, data]) => {
|
|
189
|
+
return {
|
|
190
|
+
date: dateint,
|
|
191
|
+
...data,
|
|
192
|
+
};
|
|
193
|
+
});
|
|
194
|
+
},
|
|
195
|
+
materialization_counts_by_type: async function () {
|
|
196
|
+
return DataJunctionAPI.querySystemMetricSingleDimension({
|
|
197
|
+
metric: 'system.dj.number_of_materializations',
|
|
198
|
+
dimension: 'system.dj.node_type.type',
|
|
199
|
+
filters: ['system.dj.is_active.active_id=true'],
|
|
200
|
+
orderby: ['system.dj.node_type.type'],
|
|
201
|
+
});
|
|
202
|
+
},
|
|
203
|
+
|
|
204
|
+
dimensions: async function () {
|
|
205
|
+
return await (
|
|
206
|
+
await fetch(`${DJ_URL}/system/dimensions`, {
|
|
207
|
+
credentials: 'include',
|
|
208
|
+
})
|
|
209
|
+
).json();
|
|
210
|
+
},
|
|
211
|
+
},
|
|
212
|
+
|
|
91
213
|
logout: async function () {
|
|
92
214
|
return await fetch(`${DJ_URL}/logout/`, {
|
|
93
215
|
credentials: 'include',
|