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.
Files changed (29) 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/CubeBuilderPage/DimensionsSelect.jsx +8 -10
  8. package/src/app/pages/CubeBuilderPage/MetricsSelect.jsx +6 -8
  9. package/src/app/pages/CubeBuilderPage/__tests__/index.test.jsx +39 -71
  10. package/src/app/pages/CubeBuilderPage/index.jsx +31 -7
  11. package/src/app/pages/OverviewPage/ByStatusPanel.jsx +69 -0
  12. package/src/app/pages/OverviewPage/DimensionNodeUsagePanel.jsx +48 -0
  13. package/src/app/pages/OverviewPage/GovernanceWarningsPanel.jsx +107 -0
  14. package/src/app/pages/OverviewPage/Loadable.jsx +16 -0
  15. package/src/app/pages/OverviewPage/NodesByTypePanel.jsx +63 -0
  16. package/src/app/pages/OverviewPage/OverviewPanel.jsx +94 -0
  17. package/src/app/pages/OverviewPage/TrendsPanel.jsx +66 -0
  18. package/src/app/pages/OverviewPage/__tests__/ByStatusPanel.test.jsx +36 -0
  19. package/src/app/pages/OverviewPage/__tests__/DimensionNodeUsagePanel.test.jsx +76 -0
  20. package/src/app/pages/OverviewPage/__tests__/GovernanceWarningsPanel.test.jsx +77 -0
  21. package/src/app/pages/OverviewPage/__tests__/NodesByTypePanel.test.jsx +86 -0
  22. package/src/app/pages/OverviewPage/__tests__/OverviewPanel.test.jsx +78 -0
  23. package/src/app/pages/OverviewPage/__tests__/TrendsPanel.test.jsx +120 -0
  24. package/src/app/pages/OverviewPage/__tests__/index.test.jsx +54 -0
  25. package/src/app/pages/OverviewPage/index.jsx +22 -0
  26. package/src/app/pages/RegisterTablePage/__tests__/__snapshots__/RegisterTablePage.test.jsx.snap +3 -2
  27. package/src/app/services/DJService.js +175 -0
  28. package/src/app/services/__tests__/DJService.test.jsx +364 -0
  29. package/src/styles/overview.css +72 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "datajunction-ui",
3
- "version": "0.0.1a108",
3
+ "version": "0.0.1a110",
4
4
  "description": "DataJunction Metrics Platform UI",
5
5
  "module": "src/index.tsx",
6
6
  "repository": {
@@ -83,6 +83,7 @@
83
83
  "react-syntax-highlighter": "^15.5.0",
84
84
  "react-test-renderer": "18.2.0",
85
85
  "reactflow": "^11.7.0",
86
+ "recharts": "3.0.2",
86
87
  "redux-injectors": "2.1.0",
87
88
  "redux-saga": "1.2.1",
88
89
  "rimraf": "3.0.2",
@@ -97,7 +98,7 @@
97
98
  "stylelint-config-recommended": "9.0.0",
98
99
  "ts-loader": "9.4.2",
99
100
  "ts-node": "10.9.1",
100
- "typescript": "4.6.4",
101
+ "typescript": "5.8.3",
101
102
  "unidiff": "1.0.4",
102
103
  "web-vitals": "2.1.4",
103
104
  "webpack": "5.81.0",
@@ -5,6 +5,7 @@ const AlertIcon = props => (
5
5
  viewBox="0 0 24 24"
6
6
  version="1.1"
7
7
  xmlns="http://www.w3.org/2000/svg"
8
+ {...props}
8
9
  >
9
10
  <title>alert_fill</title>
10
11
  <g id="page-1" stroke="none" strokeWidth="1" fill="none" fillRule="evenodd">
@@ -1,11 +1,13 @@
1
- const InvalidIcon = props => (
1
+ const InvalidIcon = ({ width = '25px', height = '25px', style = {} }) => (
2
2
  <svg
3
3
  xmlns="http://www.w3.org/2000/svg"
4
- width="25"
5
- height="25"
4
+ width={width}
5
+ height={height}
6
+ style={style}
6
7
  fill="currentColor"
7
8
  className="bi bi-x-circle-fill"
8
9
  viewBox="0 0 16 16"
10
+ data-testid="invalid-icon"
9
11
  >
10
12
  <path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0zM5.354 4.646a.5.5 0 1 0-.708.708L7.293 8l-2.647 2.646a.5.5 0 0 0 .708.708L8 8.707l2.646 2.647a.5.5 0 0 0 .708-.708L8.707 8l2.647-2.646a.5.5 0 0 0-.708-.708L8 7.293 5.354 4.646z" />
11
13
  </svg>
@@ -0,0 +1,49 @@
1
+ const NodeIcon = ({
2
+ width = '45px',
3
+ height = '45px',
4
+ color = '#cccccc',
5
+ style = {},
6
+ }) => (
7
+ <svg
8
+ width={width}
9
+ height={height}
10
+ viewBox="0 0 30 30"
11
+ fill="none"
12
+ xmlns="http://www.w3.org/2000/svg"
13
+ style={style}
14
+ data-testid="node-icon"
15
+ >
16
+ <path
17
+ opacity="0.317243"
18
+ fillRule="evenodd"
19
+ clipRule="evenodd"
20
+ d="M15 30C23.2843 30 30 23.2843 30 15C30 6.71573 23.2843 0 15 0C6.71573 0 0 6.71573 0 15C0 23.2843 6.71573 30 15 30Z"
21
+ fill={color}
22
+ ></path>
23
+ <path
24
+ fillRule="evenodd"
25
+ clipRule="evenodd"
26
+ d="M16.3333 7.6665C16.5144 7.6665 16.6787 7.73873 16.7988 7.85594C16.9229 7.97702 17 8.1461 17 8.33317V9.94265L18.7239 11.6665H20.3333C20.7015 11.6665 21 11.965 21 12.3332V20.3332C21 21.4377 20.1046 22.3332 19 22.3332H11C9.89543 22.3332 9 21.4377 9 20.3332V9.6665C9 8.56193 9.89543 7.6665 11 7.6665H16.3333ZM11 8.99984H15.6667L19.6667 12.9998V20.3332C19.6667 20.7014 19.3682 20.9998 19 20.9998H11C10.6318 20.9998 10.3333 20.7014 10.3333 20.3332V9.6665C10.3333 9.29831 10.6318 8.99984 11 8.99984ZM12.3333 14.9998H17.6667C18.0349 14.9998 18.3333 15.2983 18.3333 15.6665C18.3333 16.0347 18.0349 16.3332 17.6667 16.3332H12.3333C11.9651 16.3332 11.6667 16.0347 11.6667 15.6665C11.6667 15.2983 11.9651 14.9998 12.3333 14.9998ZM17.6667 17.6665H12.3333C11.9651 17.6665 11.6667 17.965 11.6667 18.3332C11.6667 18.7014 11.9651 18.9998 12.3333 18.9998H17.6667C18.0349 18.9998 18.3333 18.7014 18.3333 18.3332C18.3333 17.965 18.0349 17.6665 17.6667 17.6665ZM12.3333 12.3332H13.6667C14.0349 12.3332 14.3333 12.6316 14.3333 12.9998C14.3333 13.368 14.0349 13.6665 13.6667 13.6665H12.3333C11.9651 13.6665 11.6667 13.368 11.6667 12.9998C11.6667 12.6316 11.9651 12.3332 12.3333 12.3332Z"
27
+ fill={color}
28
+ ></path>
29
+ <mask
30
+ id="mask0"
31
+ type="alpha"
32
+ maskUnits="userSpaceOnUse"
33
+ x="9"
34
+ y="7"
35
+ width="12"
36
+ height="16"
37
+ >
38
+ <path
39
+ fillRule="evenodd"
40
+ clipRule="evenodd"
41
+ d="M16.3333 7.6665C16.5144 7.6665 16.6787 7.73873 16.7988 7.85594C16.9229 7.97702 17 8.1461 17 8.33317V9.94265L18.7239 11.6665H20.3333C20.7015 11.6665 21 11.965 21 12.3332V20.3332C21 21.4377 20.1046 22.3332 19 22.3332H11C9.89543 22.3332 9 21.4377 9 20.3332V9.6665C9 8.56193 9.89543 7.6665 11 7.6665H16.3333ZM11 8.99984H15.6667L19.6667 12.9998V20.3332C19.6667 20.7014 19.3682 20.9998 19 20.9998H11C10.6318 20.9998 10.3333 20.7014 10.3333 20.3332V9.6665C10.3333 9.29831 10.6318 8.99984 11 8.99984ZM12.3333 14.9998H17.6667C18.0349 14.9998 18.3333 15.2983 18.3333 15.6665C18.3333 16.0347 18.0349 16.3332 17.6667 16.3332H12.3333C11.9651 16.3332 11.6667 16.0347 11.6667 15.6665C11.6667 15.2983 11.9651 14.9998 12.3333 14.9998ZM17.6667 17.6665H12.3333C11.9651 17.6665 11.6667 17.965 11.6667 18.3332C11.6667 18.7014 11.9651 18.9998 12.3333 18.9998H17.6667C18.0349 18.9998 18.3333 18.7014 18.3333 18.3332C18.3333 17.965 18.0349 17.6665 17.6667 17.6665ZM12.3333 12.3332H13.6667C14.0349 12.3332 14.3333 12.6316 14.3333 12.9998C14.3333 13.368 14.0349 13.6665 13.6667 13.6665H12.3333C11.9651 13.6665 11.6667 13.368 11.6667 12.9998C11.6667 12.6316 11.9651 12.3332 12.3333 12.3332Z"
42
+ fill="white"
43
+ ></path>
44
+ </mask>
45
+ <g mask="url(#mask0)"></g>
46
+ </svg>
47
+ );
48
+
49
+ export default NodeIcon;
@@ -1,11 +1,13 @@
1
- const ValidIcon = props => (
1
+ const ValidIcon = ({ width = '25px', height = '25px', style = {} }) => (
2
2
  <svg
3
3
  xmlns="http://www.w3.org/2000/svg"
4
- width="25"
5
- height="25"
4
+ width={width}
5
+ height={height}
6
+ style={style}
6
7
  fill="currentColor"
7
8
  className="bi bi-check-circle-fill"
8
9
  viewBox="0 0 16 16"
10
+ data-testid="valid-icon"
9
11
  >
10
12
  <path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0zm-3.97-3.03a.75.75 0 0 0-1.08.022L7.477 9.417 5.384 7.323a.75.75 0 0 0-1.06 1.06L6.97 11.03a.75.75 0 0 0 1.079-.02l3.992-4.99a.75.75 0 0 0-.01-1.05z" />
11
13
  </svg>
package/src/app/index.tsx CHANGED
@@ -8,6 +8,7 @@ import { Helmet } from 'react-helmet-async';
8
8
  import { BrowserRouter, Routes, Route } from 'react-router-dom';
9
9
 
10
10
  import { NamespacePage } from './pages/NamespacePage/Loadable';
11
+ import { OverviewPage } from './pages/OverviewPage/Loadable';
11
12
  import { NodePage } from './pages/NodePage/Loadable';
12
13
  import RevisionDiff from './pages/NodePage/RevisionDiff';
13
14
  import { SQLBuilderPage } from './pages/SQLBuilderPage/Loadable';
@@ -116,6 +117,11 @@ export function App() {
116
117
  <Route path="tags" key="tags">
117
118
  <Route path=":name" element={<TagPage />} />
118
119
  </Route>
120
+ <Route
121
+ path="overview"
122
+ key="overview"
123
+ element={<OverviewPage />}
124
+ />
119
125
  </>
120
126
  }
121
127
  />
@@ -30,16 +30,14 @@ export const DimensionsSelect = ({ cube }) => {
30
30
  const fetchData = async () => {
31
31
  let cubeDimensions = undefined;
32
32
  if (cube) {
33
- cubeDimensions = cube?.cube_elements
34
- .filter(element => element.type === 'dimension')
35
- .map(cubeDim => {
36
- return {
37
- value: cubeDim.node_name + '.' + cubeDim.name,
38
- label:
39
- labelize(cubeDim.name) +
40
- (cubeDim.properties?.includes('primary_key') ? ' (PK)' : ''),
41
- };
42
- });
33
+ cubeDimensions = cube?.current.cubeDimensions.map(cubeDim => {
34
+ return {
35
+ value: cubeDim.name,
36
+ label:
37
+ labelize(cubeDim.attribute) +
38
+ (cubeDim.properties?.includes('primary_key') ? ' (PK)' : ''),
39
+ };
40
+ });
43
41
  setDefaultDimensions(cubeDimensions);
44
42
  setValue(cubeDimensions.map(m => m.value));
45
43
  }
@@ -24,14 +24,12 @@ export const MetricsSelect = ({ cube }) => {
24
24
  useEffect(() => {
25
25
  const fetchData = async () => {
26
26
  if (cube) {
27
- const cubeMetrics = cube?.cube_elements
28
- .filter(element => element.type === 'metric')
29
- .map(metric => {
30
- return {
31
- value: metric.node_name,
32
- label: metric.node_name,
33
- };
34
- });
27
+ const cubeMetrics = cube?.current.cubeMetrics.map(metric => {
28
+ return {
29
+ value: metric.name,
30
+ label: metric.name,
31
+ };
32
+ });
35
33
  setDefaultMetrics(cubeMetrics);
36
34
  await setValue(cubeMetrics.map(m => m.value));
37
35
  }
@@ -10,10 +10,13 @@ const mockDjClient = {
10
10
  createCube: jest.fn(),
11
11
  namespaces: jest.fn(),
12
12
  cube: jest.fn(),
13
+ getCubeForEditing: jest.fn(),
13
14
  node: jest.fn(),
14
15
  listTags: jest.fn(),
15
16
  tagsNode: jest.fn(),
16
17
  patchCube: jest.fn(),
18
+ users: jest.fn(),
19
+ whoami: jest.fn(),
17
20
  };
18
21
 
19
22
  const mockMetrics = [
@@ -23,81 +26,44 @@ const mockMetrics = [
23
26
  ];
24
27
 
25
28
  const mockCube = {
26
- node_revision_id: 102,
27
- node_id: 33,
28
- type: 'cube',
29
29
  name: 'default.repair_orders_cube',
30
- display_name: 'Default: Repair Orders Cube',
31
- version: 'v4.0',
32
- description: 'Repairs cube',
33
- availability: null,
34
- cube_elements: [
30
+ type: 'CUBE',
31
+ owners: [
35
32
  {
36
- name: 'default_DOT_total_repair_cost',
37
- display_name: 'Total Repair Cost',
38
- node_name: 'default.total_repair_cost',
39
- type: 'metric',
40
- partition: null,
41
- },
42
- {
43
- name: 'default_DOT_num_repair_orders',
44
- display_name: 'Num Repair Orders',
45
- node_name: 'default.num_repair_orders',
46
- type: 'metric',
47
- partition: null,
48
- },
49
- {
50
- name: 'country',
51
- display_name: 'Country',
52
- node_name: 'default.hard_hat',
53
- type: 'dimension',
54
- partition: null,
55
- },
56
- {
57
- name: 'state',
58
- display_name: 'State',
59
- node_name: 'default.hard_hat',
60
- type: 'dimension',
61
- partition: null,
33
+ username: 'someone@example.com',
62
34
  },
63
35
  ],
64
- query: '',
65
- columns: [
66
- {
67
- name: 'default.total_repair_cost',
68
- display_name: 'Total Repair Cost',
69
- type: 'double',
70
- attributes: [],
71
- dimension: null,
72
- partition: null,
73
- },
74
- {
75
- name: 'default.num_repair_orders',
76
- display_name: 'Num Repair Orders',
77
- type: 'bigint',
78
- attributes: [],
79
- dimension: null,
80
- partition: null,
81
- },
82
- {
83
- name: 'default.hard_hat.country',
84
- display_name: 'Country',
85
- type: 'string',
86
- attributes: [],
87
- dimension: null,
88
- partition: null,
89
- },
36
+ current: {
37
+ displayName: 'Default: Repair Orders Cube',
38
+ description: 'Repairs cube',
39
+ mode: 'DRAFT',
40
+ cubeMetrics: [
41
+ {
42
+ name: 'default.total_repair_cost',
43
+ },
44
+ {
45
+ name: 'default.num_repair_orders',
46
+ },
47
+ ],
48
+ cubeDimensions: [
49
+ {
50
+ name: 'default.hard_hat.country',
51
+ attribute: 'country',
52
+ properties: ['dimension'],
53
+ },
54
+ {
55
+ name: 'default.hard_hat.state',
56
+ attribute: 'state',
57
+ properties: ['dimension'],
58
+ },
59
+ ],
60
+ },
61
+ tags: [
90
62
  {
91
- name: 'default.hard_hat.state',
92
- display_name: 'State',
93
- type: 'string',
94
- attributes: [],
95
- dimension: null,
96
- partition: null,
63
+ name: 'repairs',
64
+ displayName: 'Repairs Domain',
97
65
  },
98
66
  ],
99
- updated_at: '2023-12-03T06:51:09.598532+00:00',
100
- materializations: [],
101
67
  };
102
68
 
103
69
  const mockCommonDimensions = [
@@ -205,11 +171,12 @@ describe('CubeBuilderPage', () => {
205
171
  mockDjClient.commonDimensions.mockResolvedValue(mockCommonDimensions);
206
172
  mockDjClient.createCube.mockResolvedValue({ status: 201, json: {} });
207
173
  mockDjClient.namespaces.mockResolvedValue(['default']);
208
- mockDjClient.cube.mockResolvedValue(mockCube);
209
- mockDjClient.node.mockResolvedValue(mockCube);
174
+ mockDjClient.getCubeForEditing.mockResolvedValue(mockCube);
210
175
  mockDjClient.listTags.mockResolvedValue([]);
211
176
  mockDjClient.tagsNode.mockResolvedValue([]);
212
177
  mockDjClient.patchCube.mockResolvedValue({ status: 201, json: {} });
178
+ mockDjClient.users.mockResolvedValue([{ username: 'dj' }]);
179
+ mockDjClient.whoami.mockResolvedValue({ username: 'dj' });
213
180
 
214
181
  window.scrollTo = jest.fn();
215
182
  });
@@ -342,7 +309,7 @@ describe('CubeBuilderPage', () => {
342
309
  );
343
310
  expect(screen.getAllByText('Edit')[0]).toBeInTheDocument();
344
311
  await waitFor(() => {
345
- expect(mockDjClient.cube).toHaveBeenCalled();
312
+ expect(mockDjClient.getCubeForEditing).toHaveBeenCalled();
346
313
  });
347
314
  await waitFor(() => {
348
315
  expect(mockDjClient.metrics).toHaveBeenCalled();
@@ -399,6 +366,7 @@ describe('CubeBuilderPage', () => {
399
366
  'default.date_dim.dateint',
400
367
  ],
401
368
  [],
369
+ [],
402
370
  );
403
371
  });
404
372
  });
@@ -12,6 +12,7 @@ import NodeNameField from '../../components/forms/NodeNameField';
12
12
  import { MetricsSelect } from './MetricsSelect';
13
13
  import { DimensionsSelect } from './DimensionsSelect';
14
14
  import { TagsField } from '../AddEditNodePage/TagsField';
15
+ import { OwnersField } from '../AddEditNodePage/OwnersField';
15
16
 
16
17
  export function CubeBuilderPage() {
17
18
  const djClient = useContext(DJClientContext).DataJunctionAPI;
@@ -30,6 +31,7 @@ export function CubeBuilderPage() {
30
31
  dimensions: [],
31
32
  filters: [],
32
33
  tags: [],
34
+ owners: [],
33
35
  };
34
36
 
35
37
  const handleSubmit = (values, { setSubmitting, setStatus }) => {
@@ -85,6 +87,7 @@ export function CubeBuilderPage() {
85
87
  values.metrics,
86
88
  values.dimensions,
87
89
  values.filters || [],
90
+ values.owners,
88
91
  );
89
92
  const tagsResponse = await djClient.tagsNode(
90
93
  values.name,
@@ -106,10 +109,15 @@ export function CubeBuilderPage() {
106
109
  }
107
110
  };
108
111
 
109
- const updateFieldsWithNodeData = (data, setFieldValue, setSelectTags) => {
110
- setFieldValue('display_name', data.display_name || '', false);
111
- setFieldValue('description', data.description || '', false);
112
- setFieldValue('mode', data.mode || 'draft', false);
112
+ const updateFieldsWithNodeData = (
113
+ data,
114
+ setFieldValue,
115
+ setSelectTags,
116
+ setSelectOwners,
117
+ ) => {
118
+ setFieldValue('display_name', data.current.displayName || '', false);
119
+ setFieldValue('description', data.current.description || '', false);
120
+ setFieldValue('mode', data.current.mode.toLowerCase() || 'draft', false);
113
121
  setFieldValue(
114
122
  'tags',
115
123
  data.tags.map(tag => tag.name),
@@ -119,10 +127,19 @@ export function CubeBuilderPage() {
119
127
  setSelectTags(
120
128
  <TagsField
121
129
  defaultValue={data.tags.map(t => {
122
- return { value: t.name, label: t.display_name };
130
+ return { value: t.name, label: t.displayName };
123
131
  })}
124
132
  />,
125
133
  );
134
+ if (data.owners) {
135
+ setSelectOwners(
136
+ <OwnersField
137
+ defaultValue={data.owners.map(owner => {
138
+ return { value: owner.username, label: owner.username };
139
+ })}
140
+ />,
141
+ );
142
+ }
126
143
  };
127
144
 
128
145
  const staticFieldsInEdit = () => (
@@ -159,14 +176,20 @@ export function CubeBuilderPage() {
159
176
  {function Render({ isSubmitting, status, setFieldValue, props }) {
160
177
  const [node, setNode] = useState([]);
161
178
  const [selectTags, setSelectTags] = useState(null);
179
+ const [selectOwners, setSelectOwners] = useState(null);
162
180
 
163
181
  // Get cube
164
182
  useEffect(() => {
165
183
  const fetchData = async () => {
166
184
  if (name) {
167
- const cube = await djClient.cube(name);
185
+ const cube = await djClient.getCubeForEditing(name);
168
186
  setNode(cube);
169
- updateFieldsWithNodeData(cube, setFieldValue, setSelectTags);
187
+ updateFieldsWithNodeData(
188
+ cube,
189
+ setFieldValue,
190
+ setSelectTags,
191
+ setSelectOwners,
192
+ );
170
193
  }
171
194
  };
172
195
  fetchData().catch(console.error);
@@ -243,6 +266,7 @@ export function CubeBuilderPage() {
243
266
  </Field>
244
267
  </div>
245
268
  {action === Action.Edit ? selectTags : <TagsField />}
269
+ {action === Action.Edit ? selectOwners : <OwnersField />}
246
270
  <button
247
271
  type="submit"
248
272
  disabled={isSubmitting}
@@ -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
+ };