datajunction-ui 0.0.1-rc.9 → 0.0.3

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 (205) hide show
  1. package/.env +2 -0
  2. package/.prettierignore +3 -1
  3. package/Makefile +9 -0
  4. package/dj-logo.svg +10 -0
  5. package/package.json +53 -14
  6. package/public/favicon.ico +0 -0
  7. package/public/index.html +1 -1
  8. package/src/__tests__/reportWebVitals.test.jsx +44 -0
  9. package/src/app/__tests__/__snapshots__/index.test.tsx.snap +5 -109
  10. package/src/app/components/AddNodeDropdown.jsx +44 -0
  11. package/src/app/components/ListGroupItem.jsx +9 -1
  12. package/src/app/components/NamespaceHeader.jsx +4 -13
  13. package/src/app/components/NodeListActions.jsx +69 -0
  14. package/src/app/components/NodeMaterializationDelete.jsx +90 -0
  15. package/src/app/components/QueryInfo.jsx +172 -0
  16. package/src/app/components/Search.jsx +94 -0
  17. package/src/app/components/Tab.jsx +8 -1
  18. package/src/app/components/ToggleSwitch.jsx +20 -0
  19. package/src/app/components/__tests__/NodeListActions.test.jsx +94 -0
  20. package/src/app/components/__tests__/QueryInfo.test.jsx +55 -0
  21. package/src/app/components/__tests__/Search.test.jsx +63 -0
  22. package/src/app/components/__tests__/Tab.test.jsx +27 -0
  23. package/src/app/components/__tests__/ToggleSwitch.test.jsx +43 -0
  24. package/src/app/components/__tests__/__snapshots__/ListGroupItem.test.tsx.snap +8 -3
  25. package/src/app/components/__tests__/__snapshots__/NamespaceHeader.test.jsx.snap +2 -18
  26. package/src/app/components/djgraph/Collapse.jsx +47 -0
  27. package/src/app/components/djgraph/DJNode.jsx +61 -83
  28. package/src/app/components/djgraph/DJNodeColumns.jsx +75 -0
  29. package/src/app/components/djgraph/DJNodeDimensions.jsx +75 -0
  30. package/src/app/components/djgraph/LayoutFlow.jsx +106 -0
  31. package/src/app/components/djgraph/__tests__/Collapse.test.jsx +51 -0
  32. package/src/app/components/djgraph/__tests__/DJNodeColumns.test.jsx +83 -0
  33. package/src/app/components/djgraph/__tests__/DJNodeDimensions.test.jsx +118 -0
  34. package/src/app/components/djgraph/__tests__/__snapshots__/DJNode.test.tsx.snap +84 -40
  35. package/src/app/components/forms/Action.jsx +8 -0
  36. package/src/app/components/forms/NodeNameField.jsx +64 -0
  37. package/src/app/components/search.css +17 -0
  38. package/src/app/constants.js +2 -0
  39. package/src/app/icons/AddItemIcon.jsx +16 -0
  40. package/src/app/icons/AlertIcon.jsx +33 -0
  41. package/src/app/icons/CollapsedIcon.jsx +15 -0
  42. package/src/app/icons/CommitIcon.jsx +45 -0
  43. package/src/app/icons/DJLogo.jsx +36 -0
  44. package/src/app/icons/DeleteIcon.jsx +21 -0
  45. package/src/app/icons/DiffIcon.jsx +63 -0
  46. package/src/app/icons/EditIcon.jsx +18 -0
  47. package/src/app/icons/ExpandedIcon.jsx +15 -0
  48. package/src/app/icons/EyeIcon.jsx +20 -0
  49. package/src/app/icons/FilterIcon.jsx +7 -0
  50. package/src/app/icons/HorizontalHierarchyIcon.jsx +15 -0
  51. package/src/app/icons/InvalidIcon.jsx +16 -0
  52. package/src/app/icons/JupyterExportIcon.jsx +25 -0
  53. package/src/app/icons/LoadingIcon.jsx +14 -0
  54. package/src/app/icons/NodeIcon.jsx +49 -0
  55. package/src/app/icons/PythonIcon.jsx +14 -0
  56. package/src/app/icons/TableIcon.jsx +14 -0
  57. package/src/app/icons/ValidIcon.jsx +16 -0
  58. package/src/app/index.tsx +118 -37
  59. package/src/app/pages/AddEditNodePage/AlertMessage.jsx +10 -0
  60. package/src/app/pages/AddEditNodePage/ColumnsSelect.jsx +84 -0
  61. package/src/app/pages/AddEditNodePage/DescriptionField.jsx +17 -0
  62. package/src/app/pages/AddEditNodePage/DisplayNameField.jsx +16 -0
  63. package/src/app/pages/AddEditNodePage/FormikSelect.jsx +51 -0
  64. package/src/app/pages/AddEditNodePage/FullNameField.jsx +38 -0
  65. package/src/app/pages/AddEditNodePage/Loadable.jsx +20 -0
  66. package/src/app/pages/AddEditNodePage/MetricMetadataFields.jsx +75 -0
  67. package/src/app/pages/AddEditNodePage/MetricQueryField.jsx +71 -0
  68. package/src/app/pages/AddEditNodePage/NamespaceField.jsx +40 -0
  69. package/src/app/pages/AddEditNodePage/NodeModeField.jsx +14 -0
  70. package/src/app/pages/AddEditNodePage/NodeQueryField.jsx +94 -0
  71. package/src/app/pages/AddEditNodePage/OwnersField.jsx +54 -0
  72. package/src/app/pages/AddEditNodePage/RequiredDimensionsSelect.jsx +54 -0
  73. package/src/app/pages/AddEditNodePage/TagsField.jsx +47 -0
  74. package/src/app/pages/AddEditNodePage/UpstreamNodeField.jsx +49 -0
  75. package/src/app/pages/AddEditNodePage/__tests__/AddEditNodePageFormFailed.test.jsx +109 -0
  76. package/src/app/pages/AddEditNodePage/__tests__/AddEditNodePageFormSuccess.test.jsx +287 -0
  77. package/src/app/pages/AddEditNodePage/__tests__/FormikSelect.test.jsx +75 -0
  78. package/src/app/pages/AddEditNodePage/__tests__/FullNameField.test.jsx +31 -0
  79. package/src/app/pages/AddEditNodePage/__tests__/NodeQueryField.test.jsx +30 -0
  80. package/src/app/pages/AddEditNodePage/__tests__/__snapshots__/AddEditNodePageFormFailed.test.jsx.snap +54 -0
  81. package/src/app/pages/AddEditNodePage/__tests__/__snapshots__/AddEditNodePageFormSuccess.test.jsx.snap +3 -0
  82. package/src/app/pages/AddEditNodePage/__tests__/__snapshots__/index.test.jsx.snap +3 -0
  83. package/src/app/pages/AddEditNodePage/__tests__/index.test.jsx +224 -0
  84. package/src/app/pages/AddEditNodePage/index.jsx +506 -0
  85. package/src/app/pages/AddEditTagPage/Loadable.jsx +16 -0
  86. package/src/app/pages/AddEditTagPage/__tests__/AddEditTagPage.test.jsx +107 -0
  87. package/src/app/pages/AddEditTagPage/index.jsx +132 -0
  88. package/src/app/pages/CubeBuilderPage/DimensionsSelect.jsx +152 -0
  89. package/src/app/pages/CubeBuilderPage/Loadable.jsx +16 -0
  90. package/src/app/pages/CubeBuilderPage/MetricsSelect.jsx +75 -0
  91. package/src/app/pages/CubeBuilderPage/__tests__/index.test.jsx +373 -0
  92. package/src/app/pages/CubeBuilderPage/index.jsx +291 -0
  93. package/src/app/pages/LoginPage/LoginForm.jsx +124 -0
  94. package/src/app/pages/LoginPage/SignupForm.jsx +156 -0
  95. package/src/app/pages/LoginPage/__tests__/index.test.jsx +97 -0
  96. package/src/app/pages/LoginPage/assets/sign-in-with-github.png +0 -0
  97. package/src/app/pages/LoginPage/assets/sign-in-with-google.png +0 -0
  98. package/src/app/pages/LoginPage/index.jsx +17 -0
  99. package/src/app/pages/NamespacePage/AddNamespacePopover.jsx +85 -0
  100. package/src/app/pages/NamespacePage/Explorer.jsx +61 -0
  101. package/src/app/pages/NamespacePage/FieldControl.jsx +21 -0
  102. package/src/app/pages/NamespacePage/NodeTypeSelect.jsx +30 -0
  103. package/src/app/pages/NamespacePage/TagSelect.jsx +44 -0
  104. package/src/app/pages/NamespacePage/UserSelect.jsx +47 -0
  105. package/src/app/pages/NamespacePage/__tests__/index.test.jsx +297 -0
  106. package/src/app/pages/NamespacePage/index.jsx +319 -42
  107. package/src/app/pages/NodePage/AddBackfillPopover.jsx +165 -0
  108. package/src/app/pages/NodePage/AddMaterializationPopover.jsx +222 -0
  109. package/src/app/pages/NodePage/AvailabilityStateBlock.jsx +67 -0
  110. package/src/app/pages/NodePage/ClientCodePopover.jsx +94 -0
  111. package/src/app/pages/NodePage/DimensionFilter.jsx +86 -0
  112. package/src/app/pages/NodePage/EditColumnDescriptionPopover.jsx +116 -0
  113. package/src/app/pages/NodePage/EditColumnPopover.jsx +116 -0
  114. package/src/app/pages/NodePage/LinkDimensionPopover.jsx +164 -0
  115. package/src/app/pages/NodePage/MaterializationConfigField.jsx +60 -0
  116. package/src/app/pages/NodePage/NodeColumnTab.jsx +256 -30
  117. package/src/app/pages/NodePage/NodeDependenciesTab.jsx +153 -0
  118. package/src/app/pages/NodePage/NodeGraphTab.jsx +119 -148
  119. package/src/app/pages/NodePage/NodeHistory.jsx +236 -0
  120. package/src/app/pages/NodePage/NodeInfoTab.jsx +325 -49
  121. package/src/app/pages/NodePage/NodeLineageTab.jsx +84 -0
  122. package/src/app/pages/NodePage/NodeMaterializationTab.jsx +585 -0
  123. package/src/app/pages/NodePage/NodeRevisionMaterializationTab.jsx +58 -0
  124. package/src/app/pages/NodePage/NodeStatus.jsx +100 -31
  125. package/src/app/pages/NodePage/NodeValidateTab.jsx +367 -0
  126. package/src/app/pages/NodePage/NodesWithDimension.jsx +42 -0
  127. package/src/app/pages/NodePage/NotebookDownload.jsx +36 -0
  128. package/src/app/pages/NodePage/PartitionColumnPopover.jsx +151 -0
  129. package/src/app/pages/NodePage/PartitionValueForm.jsx +60 -0
  130. package/src/app/pages/NodePage/RevisionDiff.jsx +209 -0
  131. package/src/app/pages/NodePage/WatchNodeButton.jsx +226 -0
  132. package/src/app/pages/NodePage/__tests__/AddBackfillPopover.test.jsx +56 -0
  133. package/src/app/pages/NodePage/__tests__/AddMaterializationPopover.test.jsx +87 -0
  134. package/src/app/pages/NodePage/__tests__/DimensionFilter.test.jsx +74 -0
  135. package/src/app/pages/NodePage/__tests__/EditColumnDescriptionPopover.test.jsx +149 -0
  136. package/src/app/pages/NodePage/__tests__/EditColumnPopover.test.jsx +148 -0
  137. package/src/app/pages/NodePage/__tests__/LinkDimensionPopover.test.jsx +161 -0
  138. package/src/app/pages/NodePage/__tests__/NodeColumnTab.test.jsx +166 -0
  139. package/src/app/pages/NodePage/__tests__/NodeDependenciesTab.test.jsx +151 -0
  140. package/src/app/pages/NodePage/__tests__/NodeGraphTab.test.jsx +595 -0
  141. package/src/app/pages/NodePage/__tests__/NodeLineageTab.test.jsx +58 -0
  142. package/src/app/pages/NodePage/__tests__/NodeMaterializationTab.test.jsx +190 -0
  143. package/src/app/pages/NodePage/__tests__/NodePage.test.jsx +872 -0
  144. package/src/app/pages/NodePage/__tests__/NodeWithDimension.test.jsx +175 -0
  145. package/src/app/pages/NodePage/__tests__/RevisionDiff.test.jsx +164 -0
  146. package/src/app/pages/NodePage/__tests__/__snapshots__/NodePage.test.jsx.snap +19 -0
  147. package/src/app/pages/NodePage/index.jsx +190 -44
  148. package/src/app/pages/NotFoundPage/__tests__/index.test.jsx +16 -0
  149. package/src/app/pages/OverviewPage/ByStatusPanel.jsx +69 -0
  150. package/src/app/pages/OverviewPage/DimensionNodeUsagePanel.jsx +48 -0
  151. package/src/app/pages/OverviewPage/GovernanceWarningsPanel.jsx +107 -0
  152. package/src/app/pages/OverviewPage/Loadable.jsx +16 -0
  153. package/src/app/pages/OverviewPage/NodesByTypePanel.jsx +63 -0
  154. package/src/app/pages/OverviewPage/OverviewPanel.jsx +94 -0
  155. package/src/app/pages/OverviewPage/TrendsPanel.jsx +66 -0
  156. package/src/app/pages/OverviewPage/__tests__/ByStatusPanel.test.jsx +36 -0
  157. package/src/app/pages/OverviewPage/__tests__/DimensionNodeUsagePanel.test.jsx +76 -0
  158. package/src/app/pages/OverviewPage/__tests__/GovernanceWarningsPanel.test.jsx +77 -0
  159. package/src/app/pages/OverviewPage/__tests__/NodesByTypePanel.test.jsx +86 -0
  160. package/src/app/pages/OverviewPage/__tests__/OverviewPanel.test.jsx +78 -0
  161. package/src/app/pages/OverviewPage/__tests__/TrendsPanel.test.jsx +120 -0
  162. package/src/app/pages/OverviewPage/__tests__/index.test.jsx +54 -0
  163. package/src/app/pages/OverviewPage/index.jsx +22 -0
  164. package/src/app/pages/RegisterTablePage/Loadable.jsx +16 -0
  165. package/src/app/pages/RegisterTablePage/__tests__/RegisterTablePage.test.jsx +110 -0
  166. package/src/app/pages/RegisterTablePage/__tests__/__snapshots__/RegisterTablePage.test.jsx.snap +38 -0
  167. package/src/app/pages/RegisterTablePage/index.jsx +142 -0
  168. package/src/app/pages/Root/__tests__/index.test.jsx +79 -0
  169. package/src/app/pages/Root/index.tsx +84 -6
  170. package/src/app/pages/SQLBuilderPage/Loadable.jsx +16 -0
  171. package/src/app/pages/SQLBuilderPage/__tests__/index.test.jsx +173 -0
  172. package/src/app/pages/SQLBuilderPage/index.jsx +390 -0
  173. package/src/app/pages/TagPage/Loadable.jsx +16 -0
  174. package/src/app/pages/TagPage/__tests__/TagPage.test.jsx +70 -0
  175. package/src/app/pages/TagPage/index.jsx +79 -0
  176. package/src/app/services/DJService.js +1314 -21
  177. package/src/app/services/__tests__/DJService.test.jsx +1559 -0
  178. package/src/index.tsx +1 -0
  179. package/src/mocks/mockNodes.jsx +1474 -0
  180. package/src/setupTests.ts +31 -1
  181. package/src/styles/dag.css +117 -5
  182. package/src/styles/index.css +1027 -30
  183. package/src/styles/loading.css +34 -0
  184. package/src/styles/login.css +81 -0
  185. package/src/styles/node-creation.scss +276 -0
  186. package/src/styles/node-list.css +4 -0
  187. package/src/styles/overview.css +72 -0
  188. package/src/styles/sorted-table.css +15 -0
  189. package/src/styles/styles.scss +44 -0
  190. package/src/styles/styles.scss.d.ts +9 -0
  191. package/src/utils/form.jsx +23 -0
  192. package/webpack.config.js +16 -6
  193. package/.babelrc +0 -4
  194. package/.env.local +0 -4
  195. package/.env.production +0 -1
  196. package/.github/pull_request_template.md +0 -11
  197. package/.github/workflows/ci.yml +0 -33
  198. package/.vscode/extensions.json +0 -7
  199. package/.vscode/launch.json +0 -15
  200. package/.vscode/settings.json +0 -25
  201. package/Dockerfile +0 -7
  202. package/src/app/pages/ListNamespacesPage/Loadable.jsx +0 -14
  203. package/src/app/pages/ListNamespacesPage/index.jsx +0 -62
  204. package/src/app/pages/NamespacePage/__tests__/__snapshots__/index.test.tsx.snap +0 -45
  205. package/src/app/pages/NamespacePage/__tests__/index.test.tsx +0 -14
@@ -0,0 +1,69 @@
1
+ import { useContext, useEffect, useState } from 'react';
2
+ import DJClientContext from '../../providers/djclient';
3
+
4
+ import ValidIcon from '../../icons/ValidIcon';
5
+ import InvalidIcon from '../../icons/InvalidIcon';
6
+
7
+ const COLOR_MAPPING = {
8
+ valid: '#00b368',
9
+ invalid: '#FF91A3', // '#b34b00',
10
+ };
11
+
12
+ export const ByStatusPanel = () => {
13
+ const djClient = useContext(DJClientContext).DataJunctionAPI;
14
+ const [nodesByStatus, setNodesByStatus] = useState(null);
15
+
16
+ useEffect(() => {
17
+ const fetchData = async () => {
18
+ setNodesByStatus(await djClient.system.node_counts_by_status());
19
+ };
20
+ fetchData().catch(console.error);
21
+ }, [djClient]);
22
+
23
+ return (
24
+ <>
25
+ <div className="chart-box" style={{ flex: '0 0 2%' }}>
26
+ <div className="horiz-box">
27
+ <div className="chart-title">Nodes By Status</div>
28
+ {nodesByStatus?.map(entry => (
29
+ <div
30
+ className="jss316 badge"
31
+ style={{ color: '#000', margin: '0.2em' }}
32
+ key={entry.name}
33
+ >
34
+ <span style={{ color: COLOR_MAPPING[entry.name.toLowerCase()] }}>
35
+ {entry.name === 'VALID' ? (
36
+ <ValidIcon
37
+ width={'45px'}
38
+ height={'45px'}
39
+ style={{ marginTop: '0.75em' }}
40
+ />
41
+ ) : (
42
+ <InvalidIcon
43
+ width={'45px'}
44
+ height={'45px'}
45
+ style={{ marginTop: '0.75em' }}
46
+ />
47
+ )}
48
+ </span>
49
+
50
+ <div style={{ display: 'inline-grid', alignItems: 'center' }}>
51
+ <strong
52
+ className="horiz-box-value"
53
+ style={{
54
+ color: COLOR_MAPPING[entry.name.toLowerCase()],
55
+ }}
56
+ >
57
+ {entry.value}
58
+ </strong>
59
+ <span className={'horiz-box-label'}>
60
+ {entry.name.toLowerCase()} nodes
61
+ </span>
62
+ </div>
63
+ </div>
64
+ ))}
65
+ </div>
66
+ </div>
67
+ </>
68
+ );
69
+ };
@@ -0,0 +1,48 @@
1
+ import { useContext, useEffect, useState } from 'react';
2
+ import DJClientContext from '../../providers/djclient';
3
+
4
+ export const DimensionNodeUsagePanel = () => {
5
+ const djClient = useContext(DJClientContext).DataJunctionAPI;
6
+ const [dimensionNodes, setDimensionNodes] = useState(null);
7
+
8
+ useEffect(() => {
9
+ const fetchData = async () => {
10
+ setDimensionNodes(await djClient.system.dimensions());
11
+ };
12
+ fetchData().catch(console.error);
13
+ }, [djClient]);
14
+
15
+ return (
16
+ <>
17
+ <div className="chart-box">
18
+ <div className="chart-title">Dimension Node Usage</div>
19
+ <table className="card-inner-table table" style={{ marginTop: '0' }}>
20
+ <thead className="fs-7 fw-bold text-gray-400 border-bottom-0">
21
+ <tr>
22
+ <th className="text-start">Dimension</th>
23
+ <th className="a">Links</th>
24
+ <th className="a">Cubes</th>
25
+ </tr>
26
+ </thead>
27
+ <tbody>
28
+ {dimensionNodes
29
+ ?.sort(
30
+ (a, b) =>
31
+ b.cube_count + b.indegree - (a.cube_count + a.indegree),
32
+ )
33
+ .slice(0, 6)
34
+ .map((dim, index) => (
35
+ <tr key={index}>
36
+ <td className="a">
37
+ <a href={`/nodes/${dim.name}`}>{dim.name}</a>
38
+ </td>
39
+ <td className="a">{dim.indegree}</td>
40
+ <td className="a">{dim.cube_count}</td>
41
+ </tr>
42
+ ))}
43
+ </tbody>
44
+ </table>
45
+ </div>
46
+ </>
47
+ );
48
+ };
@@ -0,0 +1,107 @@
1
+ import { useContext, useEffect, useState } from 'react';
2
+ import DJClientContext from '../../providers/djclient';
3
+ import '../../../styles/node-creation.scss';
4
+ const COLOR_MAPPING = {
5
+ source: '#00C49F',
6
+ dimension: '#FFBB28', //'#FF8042',
7
+ transform: '#0088FE',
8
+ metric: '#ff91a3', //'#FFBB28',
9
+ cube: '#AA46BE',
10
+ valid: '#00b368',
11
+ invalid: '#b34b00',
12
+ };
13
+
14
+ export const GovernanceWarningsPanel = () => {
15
+ const djClient = useContext(DJClientContext).DataJunctionAPI;
16
+ const [nodesWithoutDescription, setNodesWithoutDescription] = useState(null);
17
+ const [dimensionNodes, setDimensionNodes] = useState(null);
18
+
19
+ useEffect(() => {
20
+ const fetchData = async () => {
21
+ setNodesWithoutDescription(
22
+ await djClient.system.nodes_without_description(),
23
+ );
24
+ setDimensionNodes(await djClient.system.dimensions());
25
+ };
26
+ fetchData().catch(console.error);
27
+ }, [djClient]);
28
+
29
+ return (
30
+ <div className="chart-box" style={{ flex: '1 1 10%', maxWidth: '470px' }}>
31
+ <div className="chart-title">Governance Warnings</div>
32
+ <div
33
+ className="horiz-box"
34
+ style={{
35
+ padding: '5px 10px',
36
+ marginTop: '10px',
37
+ border: '1px solid #AA46BE30',
38
+ }}
39
+ >
40
+ <span style={{ color: '#FF804255', fontSize: '30px' }}>⚠</span>
41
+ <span style={{ padding: '10px 12px', fontSize: '18px' }}>
42
+ Missing Description
43
+ </span>
44
+ <div style={{ display: 'block' }}>
45
+ {nodesWithoutDescription?.map(entry => (
46
+ <div
47
+ className="jss316 badge"
48
+ style={{
49
+ margin: '5px 10px',
50
+ fontSize: '14px',
51
+ padding: '10px',
52
+ color: COLOR_MAPPING[entry.name.toLowerCase()],
53
+ backgroundColor: COLOR_MAPPING[entry.name.toLowerCase()] + '10',
54
+ }}
55
+ key={entry.name}
56
+ >
57
+ <strong>{Math.round(entry.value * 100)}%</strong>{' '}
58
+ <span>{entry.name.toLowerCase()}s</span>
59
+ </div>
60
+ ))}
61
+ </div>
62
+ </div>
63
+ <div
64
+ className="horiz-box"
65
+ style={{
66
+ padding: '5px 10px',
67
+ marginTop: '10px',
68
+ border: '1px solid #AA46BE30',
69
+ }}
70
+ >
71
+ <div
72
+ style={{ width: '100%', display: 'inline-flex', marginTop: '-10px' }}
73
+ >
74
+ <span style={{ color: '#FF804255', fontSize: '40px' }}>∅</span>
75
+ <span
76
+ style={{
77
+ padding: '10px 12px',
78
+ fontSize: '18px',
79
+ marginTop: '10px',
80
+ }}
81
+ >
82
+ Orphaned Dimensions
83
+ </span>
84
+ </div>
85
+ <div style={{ display: 'block' }}>
86
+ <div
87
+ className="jss316 badge"
88
+ style={{
89
+ margin: '5px 10px',
90
+ fontSize: '14px',
91
+ padding: '10px',
92
+ color: COLOR_MAPPING.dimension,
93
+ backgroundColor: COLOR_MAPPING.dimension + '10',
94
+ }}
95
+ >
96
+ <strong>
97
+ {dimensionNodes?.filter(
98
+ dim => dim.indegree === 0 || dim.cube_count === 0,
99
+ ).length || '...'}
100
+ </strong>
101
+ <span> dimension nodes</span>
102
+ </div>
103
+ </div>
104
+ </div>
105
+ </div>
106
+ );
107
+ };
@@ -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
+ });