datajunction-ui 0.0.1-a108 → 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/CubeBuilderPage/DimensionsSelect.jsx +8 -10
- package/src/app/pages/CubeBuilderPage/MetricsSelect.jsx +6 -8
- package/src/app/pages/CubeBuilderPage/__tests__/index.test.jsx +39 -71
- package/src/app/pages/CubeBuilderPage/index.jsx +31 -7
- 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 +175 -0
- package/src/app/services/__tests__/DJService.test.jsx +364 -0
- package/src/styles/overview.css +72 -0
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Asynchronously loads the component for the Overview page
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import * as React from 'react';
|
|
6
|
+
import { lazyLoad } from '../../../utils/loadable';
|
|
7
|
+
|
|
8
|
+
export const OverviewPage = props => {
|
|
9
|
+
return lazyLoad(
|
|
10
|
+
() => import('./index'),
|
|
11
|
+
module => module.OverviewPage,
|
|
12
|
+
{
|
|
13
|
+
fallback: <div></div>,
|
|
14
|
+
},
|
|
15
|
+
)(props);
|
|
16
|
+
};
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { useContext, useEffect, useState } from 'react';
|
|
2
|
+
import DJClientContext from '../../providers/djclient';
|
|
3
|
+
import NodeIcon from '../../icons/NodeIcon';
|
|
4
|
+
import '../../../styles/overview.css';
|
|
5
|
+
|
|
6
|
+
const COLOR_MAPPING = {
|
|
7
|
+
source: '#00C49F',
|
|
8
|
+
dimension: '#FFBB28', //'#FF8042',
|
|
9
|
+
transform: '#0088FE',
|
|
10
|
+
metric: '#FF91A3', //'#FFBB28',
|
|
11
|
+
cube: '#AA46BE',
|
|
12
|
+
valid: '#00B368',
|
|
13
|
+
invalid: '#B34B00',
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export const NodesByTypePanel = () => {
|
|
17
|
+
const djClient = useContext(DJClientContext).DataJunctionAPI;
|
|
18
|
+
const [nodesByType, setNodesByType] = useState(null);
|
|
19
|
+
const [materializationsByType, setMaterializationsByType] = useState(null);
|
|
20
|
+
|
|
21
|
+
useEffect(() => {
|
|
22
|
+
const fetchData = async () => {
|
|
23
|
+
setNodesByType(await djClient.system.node_counts_by_type());
|
|
24
|
+
setMaterializationsByType(
|
|
25
|
+
await djClient.system.materialization_counts_by_type(),
|
|
26
|
+
);
|
|
27
|
+
};
|
|
28
|
+
fetchData().catch(console.error);
|
|
29
|
+
}, [djClient]);
|
|
30
|
+
|
|
31
|
+
return (
|
|
32
|
+
<>
|
|
33
|
+
<div className="chart-box" style={{ flex: '0 0 30%', maxWidth: '350px' }}>
|
|
34
|
+
<div className="chart-title">Nodes by Type</div>
|
|
35
|
+
<div className="horiz-box">
|
|
36
|
+
{nodesByType?.map(entry => (
|
|
37
|
+
<div className="vert-box" key={entry.name}>
|
|
38
|
+
<NodeIcon color={COLOR_MAPPING[entry.name]} />
|
|
39
|
+
<strong style={{ color: COLOR_MAPPING[entry.name] }}>
|
|
40
|
+
{entry.value}
|
|
41
|
+
</strong>
|
|
42
|
+
<span>{entry.name}s</span>
|
|
43
|
+
</div>
|
|
44
|
+
))}
|
|
45
|
+
</div>
|
|
46
|
+
</div>
|
|
47
|
+
<div className="chart-box" style={{ flex: '0 0 30%', maxWidth: '350px' }}>
|
|
48
|
+
<div className="chart-title">Materializations by Type</div>
|
|
49
|
+
<div className="horiz-box">
|
|
50
|
+
{materializationsByType?.map(entry => (
|
|
51
|
+
<div className="vert-box" key={entry.name}>
|
|
52
|
+
<NodeIcon color={COLOR_MAPPING[entry.name]} />
|
|
53
|
+
<strong style={{ color: COLOR_MAPPING[entry.name] }}>
|
|
54
|
+
{entry.value}
|
|
55
|
+
</strong>
|
|
56
|
+
<span>{entry.name}s</span>
|
|
57
|
+
</div>
|
|
58
|
+
))}
|
|
59
|
+
</div>
|
|
60
|
+
</div>
|
|
61
|
+
</>
|
|
62
|
+
);
|
|
63
|
+
};
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { useContext, useEffect, useState } from 'react';
|
|
2
|
+
import DJClientContext from '../../providers/djclient';
|
|
3
|
+
import NodeIcon from '../../icons/NodeIcon';
|
|
4
|
+
|
|
5
|
+
import ValidIcon from '../../icons/ValidIcon';
|
|
6
|
+
import InvalidIcon from '../../icons/InvalidIcon';
|
|
7
|
+
|
|
8
|
+
const COLOR_MAPPING = {
|
|
9
|
+
valid: '#00b368',
|
|
10
|
+
invalid: '#FF91A3', // '#b34b00',
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export const OverviewPanel = () => {
|
|
14
|
+
const djClient = useContext(DJClientContext).DataJunctionAPI;
|
|
15
|
+
const [nodesByActive, setNodesByActive] = useState(null);
|
|
16
|
+
const [nodesByStatus, setNodesByStatus] = useState(null);
|
|
17
|
+
|
|
18
|
+
useEffect(() => {
|
|
19
|
+
const fetchData = async () => {
|
|
20
|
+
setNodesByActive(await djClient.system.node_counts_by_active());
|
|
21
|
+
setNodesByStatus(await djClient.system.node_counts_by_status());
|
|
22
|
+
};
|
|
23
|
+
fetchData().catch(console.error);
|
|
24
|
+
}, [djClient]);
|
|
25
|
+
|
|
26
|
+
return (
|
|
27
|
+
<div className="chart-box" style={{ flex: '0 0 2%' }}>
|
|
28
|
+
<div className="chart-title">Overview</div>
|
|
29
|
+
<div className="horiz-box">
|
|
30
|
+
{nodesByActive
|
|
31
|
+
?.filter(entry => entry.name === 'true')
|
|
32
|
+
.map(entry => (
|
|
33
|
+
<div
|
|
34
|
+
className="jss316 badge"
|
|
35
|
+
style={{ color: '#000', margin: '0.2em' }}
|
|
36
|
+
>
|
|
37
|
+
<NodeIcon color="#FFBB28" style={{ marginTop: '0.75em' }} />
|
|
38
|
+
<div style={{ display: 'inline-grid', alignItems: 'center' }}>
|
|
39
|
+
<strong className="horiz-box-value">{entry.value}</strong>
|
|
40
|
+
<span className={'horiz-box-label'}>
|
|
41
|
+
{entry.name === 'true' ? 'Active Nodes' : 'Deactivated'}
|
|
42
|
+
</span>
|
|
43
|
+
</div>
|
|
44
|
+
</div>
|
|
45
|
+
))}
|
|
46
|
+
</div>
|
|
47
|
+
<div className="horiz-box">
|
|
48
|
+
{nodesByStatus?.map(entry => (
|
|
49
|
+
<div
|
|
50
|
+
className="jss316 badge"
|
|
51
|
+
style={{ color: '#000', margin: '0.2em', marginLeft: '1.2em' }}
|
|
52
|
+
>
|
|
53
|
+
↳
|
|
54
|
+
<span
|
|
55
|
+
style={{
|
|
56
|
+
color: COLOR_MAPPING[entry.name.toLowerCase()],
|
|
57
|
+
margin: '0 0.2em 0 0.4em',
|
|
58
|
+
}}
|
|
59
|
+
>
|
|
60
|
+
{entry.name === 'VALID' ? (
|
|
61
|
+
<ValidIcon
|
|
62
|
+
width={'25px'}
|
|
63
|
+
height={'25px'}
|
|
64
|
+
style={{ marginTop: '0.2em' }}
|
|
65
|
+
/>
|
|
66
|
+
) : (
|
|
67
|
+
<InvalidIcon
|
|
68
|
+
width={'25px'}
|
|
69
|
+
height={'25px'}
|
|
70
|
+
style={{ marginTop: '0.2em' }}
|
|
71
|
+
/>
|
|
72
|
+
)}
|
|
73
|
+
</span>
|
|
74
|
+
<div style={{ display: 'inline-flex', alignItems: 'center' }}>
|
|
75
|
+
<strong
|
|
76
|
+
style={{
|
|
77
|
+
color: COLOR_MAPPING[entry.name.toLowerCase()],
|
|
78
|
+
margin: '0 2px',
|
|
79
|
+
fontSize: '16px',
|
|
80
|
+
textAlign: 'left',
|
|
81
|
+
}}
|
|
82
|
+
>
|
|
83
|
+
{entry.value}
|
|
84
|
+
</strong>
|
|
85
|
+
<span style={{ fontSize: 'smaller', padding: '5px 2px' }}>
|
|
86
|
+
{entry.name.toLowerCase()}
|
|
87
|
+
</span>
|
|
88
|
+
</div>
|
|
89
|
+
</div>
|
|
90
|
+
))}
|
|
91
|
+
</div>
|
|
92
|
+
</div>
|
|
93
|
+
);
|
|
94
|
+
};
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { useContext, useEffect, useState } from 'react';
|
|
2
|
+
import DJClientContext from '../../providers/djclient';
|
|
3
|
+
import {
|
|
4
|
+
Legend,
|
|
5
|
+
Tooltip,
|
|
6
|
+
ResponsiveContainer,
|
|
7
|
+
BarChart,
|
|
8
|
+
CartesianGrid,
|
|
9
|
+
XAxis,
|
|
10
|
+
YAxis,
|
|
11
|
+
Bar,
|
|
12
|
+
} from 'recharts';
|
|
13
|
+
|
|
14
|
+
const COLOR_MAPPING = {
|
|
15
|
+
source: '#00C49F',
|
|
16
|
+
dimension: '#FFBB28',
|
|
17
|
+
transform: '#0088FE',
|
|
18
|
+
metric: '#FF91A3',
|
|
19
|
+
cube: '#AA46BE',
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export const TrendsPanel = () => {
|
|
23
|
+
const djClient = useContext(DJClientContext).DataJunctionAPI;
|
|
24
|
+
const [nodeTrends, setNodeTrends] = useState([]);
|
|
25
|
+
|
|
26
|
+
useEffect(() => {
|
|
27
|
+
const fetchData = async () => {
|
|
28
|
+
setNodeTrends(await djClient.system.node_trends());
|
|
29
|
+
};
|
|
30
|
+
fetchData().catch(console.error);
|
|
31
|
+
}, [djClient]);
|
|
32
|
+
|
|
33
|
+
return (
|
|
34
|
+
<div className="chart-box" style={{ maxWidth: '60%', flex: '1 1 20%' }}>
|
|
35
|
+
<div className="chart-title">Trends</div>
|
|
36
|
+
<ResponsiveContainer width="100%" height={400}>
|
|
37
|
+
<BarChart
|
|
38
|
+
width={1000}
|
|
39
|
+
height={400}
|
|
40
|
+
data={nodeTrends}
|
|
41
|
+
margin={{
|
|
42
|
+
top: 20,
|
|
43
|
+
right: 30,
|
|
44
|
+
left: 20,
|
|
45
|
+
bottom: 5,
|
|
46
|
+
}}
|
|
47
|
+
>
|
|
48
|
+
<CartesianGrid strokeDasharray="3 3" />
|
|
49
|
+
<XAxis dataKey="date" />
|
|
50
|
+
<YAxis />
|
|
51
|
+
<Tooltip />
|
|
52
|
+
<Legend />
|
|
53
|
+
{Object.entries(COLOR_MAPPING).map(([key, color]) => (
|
|
54
|
+
<Bar
|
|
55
|
+
key={key}
|
|
56
|
+
dataKey={key}
|
|
57
|
+
stackId="nodeCount"
|
|
58
|
+
fill={color}
|
|
59
|
+
name={key.charAt(0).toUpperCase() + key.slice(1)}
|
|
60
|
+
/>
|
|
61
|
+
))}
|
|
62
|
+
</BarChart>
|
|
63
|
+
</ResponsiveContainer>
|
|
64
|
+
</div>
|
|
65
|
+
);
|
|
66
|
+
};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { render, screen, waitFor } from '@testing-library/react';
|
|
2
|
+
import { ByStatusPanel } from '../ByStatusPanel';
|
|
3
|
+
import DJClientContext from '../../../providers/djclient';
|
|
4
|
+
|
|
5
|
+
describe('<ByStatusPanel />', () => {
|
|
6
|
+
it('fetches nodes by status and displays them correctly', async () => {
|
|
7
|
+
const mockNodeCounts = [
|
|
8
|
+
{ name: 'VALID', value: 10 },
|
|
9
|
+
{ name: 'INVALID', value: 5 },
|
|
10
|
+
];
|
|
11
|
+
|
|
12
|
+
const mockDjClient = {
|
|
13
|
+
system: {
|
|
14
|
+
node_counts_by_status: jest.fn().mockResolvedValue(mockNodeCounts),
|
|
15
|
+
},
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
render(
|
|
19
|
+
<DJClientContext.Provider value={{ DataJunctionAPI: mockDjClient }}>
|
|
20
|
+
<ByStatusPanel />
|
|
21
|
+
</DJClientContext.Provider>,
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
await waitFor(() => {
|
|
25
|
+
expect(mockDjClient.system.node_counts_by_status).toHaveBeenCalled();
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
// Check that counts are rendered
|
|
29
|
+
expect(screen.getByText('10')).toBeInTheDocument();
|
|
30
|
+
expect(screen.getByText('5')).toBeInTheDocument();
|
|
31
|
+
|
|
32
|
+
// Check that labels are rendered
|
|
33
|
+
expect(screen.getByText('valid nodes')).toBeInTheDocument();
|
|
34
|
+
expect(screen.getByText('invalid nodes')).toBeInTheDocument();
|
|
35
|
+
});
|
|
36
|
+
});
|
|
@@ -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
|
+
});
|