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.
Files changed (25) hide show
  1. package/package.json +3 -2
  2. package/src/app/icons/AlertIcon.jsx +1 -0
  3. package/src/app/icons/InvalidIcon.jsx +5 -3
  4. package/src/app/icons/NodeIcon.jsx +49 -0
  5. package/src/app/icons/ValidIcon.jsx +5 -3
  6. package/src/app/index.tsx +6 -0
  7. package/src/app/pages/OverviewPage/ByStatusPanel.jsx +69 -0
  8. package/src/app/pages/OverviewPage/DimensionNodeUsagePanel.jsx +48 -0
  9. package/src/app/pages/OverviewPage/GovernanceWarningsPanel.jsx +107 -0
  10. package/src/app/pages/OverviewPage/Loadable.jsx +16 -0
  11. package/src/app/pages/OverviewPage/NodesByTypePanel.jsx +63 -0
  12. package/src/app/pages/OverviewPage/OverviewPanel.jsx +94 -0
  13. package/src/app/pages/OverviewPage/TrendsPanel.jsx +66 -0
  14. package/src/app/pages/OverviewPage/__tests__/ByStatusPanel.test.jsx +36 -0
  15. package/src/app/pages/OverviewPage/__tests__/DimensionNodeUsagePanel.test.jsx +76 -0
  16. package/src/app/pages/OverviewPage/__tests__/GovernanceWarningsPanel.test.jsx +77 -0
  17. package/src/app/pages/OverviewPage/__tests__/NodesByTypePanel.test.jsx +86 -0
  18. package/src/app/pages/OverviewPage/__tests__/OverviewPanel.test.jsx +78 -0
  19. package/src/app/pages/OverviewPage/__tests__/TrendsPanel.test.jsx +120 -0
  20. package/src/app/pages/OverviewPage/__tests__/index.test.jsx +54 -0
  21. package/src/app/pages/OverviewPage/index.jsx +22 -0
  22. package/src/app/pages/RegisterTablePage/__tests__/__snapshots__/RegisterTablePage.test.jsx.snap +3 -2
  23. package/src/app/services/DJService.js +122 -0
  24. package/src/app/services/__tests__/DJService.test.jsx +364 -0
  25. 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
+ }
@@ -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="25"
13
+ height="25px"
13
14
  viewBox="0 0 16 16"
14
- width="25"
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',