datajunction-ui 0.0.1-rc.2 → 0.0.1-rc.21

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 (136) hide show
  1. package/.env +1 -0
  2. package/.prettierignore +3 -1
  3. package/dj-logo.svg +10 -0
  4. package/package.json +43 -13
  5. package/public/favicon.ico +0 -0
  6. package/src/__tests__/reportWebVitals.test.jsx +44 -0
  7. package/src/app/__tests__/__snapshots__/index.test.tsx.snap +5 -39
  8. package/src/app/components/DeleteNode.jsx +79 -0
  9. package/src/app/components/ListGroupItem.jsx +8 -1
  10. package/src/app/components/NamespaceHeader.jsx +4 -13
  11. package/src/app/components/QueryInfo.jsx +77 -0
  12. package/src/app/components/Tab.jsx +3 -2
  13. package/src/app/components/ToggleSwitch.jsx +20 -0
  14. package/src/app/components/__tests__/QueryInfo.test.jsx +55 -0
  15. package/src/app/components/__tests__/Tab.test.jsx +27 -0
  16. package/src/app/components/__tests__/ToggleSwitch.test.jsx +43 -0
  17. package/src/app/components/__tests__/__snapshots__/ListGroupItem.test.tsx.snap +3 -0
  18. package/src/app/components/__tests__/__snapshots__/NamespaceHeader.test.jsx.snap +2 -18
  19. package/src/app/components/djgraph/Collapse.jsx +46 -0
  20. package/src/app/components/djgraph/DJNode.jsx +60 -82
  21. package/src/app/components/djgraph/DJNodeColumns.jsx +71 -0
  22. package/src/app/components/djgraph/DJNodeDimensions.jsx +75 -0
  23. package/src/app/components/djgraph/LayoutFlow.jsx +104 -0
  24. package/src/app/components/djgraph/__tests__/Collapse.test.jsx +51 -0
  25. package/src/app/components/djgraph/__tests__/DJNodeColumns.test.jsx +83 -0
  26. package/src/app/components/djgraph/__tests__/DJNodeDimensions.test.jsx +118 -0
  27. package/src/app/components/djgraph/__tests__/__snapshots__/DJNode.test.tsx.snap +84 -40
  28. package/src/app/constants.js +2 -0
  29. package/src/app/icons/AlertIcon.jsx +32 -0
  30. package/src/app/icons/CollapsedIcon.jsx +15 -0
  31. package/src/app/icons/DJLogo.jsx +36 -0
  32. package/src/app/icons/DeleteIcon.jsx +21 -0
  33. package/src/app/icons/EditIcon.jsx +18 -0
  34. package/src/app/icons/ExpandedIcon.jsx +15 -0
  35. package/src/app/icons/HorizontalHierarchyIcon.jsx +15 -0
  36. package/src/app/icons/InvalidIcon.jsx +14 -0
  37. package/src/app/icons/PythonIcon.jsx +52 -0
  38. package/src/app/icons/TableIcon.jsx +14 -0
  39. package/src/app/icons/ValidIcon.jsx +14 -0
  40. package/src/app/index.tsx +79 -26
  41. package/src/app/pages/AddEditNodePage/FormikSelect.jsx +46 -0
  42. package/src/app/pages/AddEditNodePage/FullNameField.jsx +37 -0
  43. package/src/app/pages/AddEditNodePage/Loadable.jsx +16 -0
  44. package/src/app/pages/AddEditNodePage/NodeQueryField.jsx +89 -0
  45. package/src/app/pages/AddEditNodePage/__tests__/AddEditNodePageFormFailed.test.jsx +77 -0
  46. package/src/app/pages/AddEditNodePage/__tests__/AddEditNodePageFormSuccess.test.jsx +93 -0
  47. package/src/app/pages/AddEditNodePage/__tests__/FormikSelect.test.jsx +75 -0
  48. package/src/app/pages/AddEditNodePage/__tests__/FullNameField.test.jsx +31 -0
  49. package/src/app/pages/AddEditNodePage/__tests__/NodeQueryField.test.jsx +30 -0
  50. package/src/app/pages/AddEditNodePage/__tests__/__snapshots__/AddEditNodePageFormFailed.test.jsx.snap +53 -0
  51. package/src/app/pages/AddEditNodePage/__tests__/__snapshots__/AddEditNodePageFormSuccess.test.jsx.snap +53 -0
  52. package/src/app/pages/AddEditNodePage/__tests__/__snapshots__/index.test.jsx.snap +3 -0
  53. package/src/app/pages/AddEditNodePage/__tests__/index.test.jsx +178 -0
  54. package/src/app/pages/AddEditNodePage/index.jsx +357 -0
  55. package/src/app/pages/LoginPage/__tests__/index.test.jsx +70 -0
  56. package/src/app/pages/LoginPage/assets/sign-in-with-github.png +0 -0
  57. package/src/app/pages/LoginPage/assets/sign-in-with-google.png +0 -0
  58. package/src/app/pages/LoginPage/index.jsx +90 -0
  59. package/src/app/pages/NamespacePage/AddNamespacePopover.jsx +86 -0
  60. package/src/app/pages/NamespacePage/Explorer.jsx +57 -0
  61. package/src/app/pages/NamespacePage/Loadable.jsx +9 -7
  62. package/src/app/pages/NamespacePage/__tests__/index.test.jsx +95 -0
  63. package/src/app/pages/NamespacePage/index.jsx +132 -31
  64. package/src/app/pages/NodePage/ClientCodePopover.jsx +32 -0
  65. package/src/app/pages/NodePage/EditColumnPopover.jsx +102 -0
  66. package/src/app/pages/NodePage/LinkDimensionPopover.jsx +135 -0
  67. package/src/app/pages/NodePage/Loadable.jsx +9 -7
  68. package/src/app/pages/NodePage/NodeColumnTab.jsx +106 -27
  69. package/src/app/pages/NodePage/NodeGraphTab.jsx +94 -148
  70. package/src/app/pages/NodePage/NodeHistory.jsx +212 -0
  71. package/src/app/pages/NodePage/NodeInfoTab.jsx +166 -51
  72. package/src/app/pages/NodePage/NodeLineageTab.jsx +84 -0
  73. package/src/app/pages/NodePage/NodeMaterializationTab.jsx +174 -0
  74. package/src/app/pages/NodePage/NodeSQLTab.jsx +82 -0
  75. package/src/app/pages/NodePage/NodeStatus.jsx +14 -20
  76. package/src/app/pages/NodePage/NodesWithDimension.jsx +42 -0
  77. package/src/app/pages/NodePage/__tests__/ClientCodePopover.test.jsx +49 -0
  78. package/src/app/pages/NodePage/__tests__/EditColumnPopover.test.jsx +148 -0
  79. package/src/app/pages/NodePage/__tests__/LinkDimensionPopover.test.jsx +165 -0
  80. package/src/app/pages/NodePage/__tests__/NodeGraphTab.test.jsx +591 -0
  81. package/src/app/pages/NodePage/__tests__/NodeLineageTab.test.jsx +57 -0
  82. package/src/app/pages/NodePage/__tests__/NodePage.test.jsx +725 -0
  83. package/src/app/pages/NodePage/__tests__/NodeWithDimension.test.jsx +175 -0
  84. package/src/app/pages/NodePage/__tests__/__snapshots__/NodePage.test.jsx.snap +402 -0
  85. package/src/app/pages/NodePage/index.jsx +151 -41
  86. package/src/app/pages/NotFoundPage/__tests__/index.test.jsx +16 -0
  87. package/src/app/pages/RegisterTablePage/Loadable.jsx +16 -0
  88. package/src/app/pages/RegisterTablePage/index.jsx +163 -0
  89. package/src/app/pages/Root/__tests__/index.test.jsx +77 -0
  90. package/src/app/pages/Root/index.tsx +32 -4
  91. package/src/app/pages/SQLBuilderPage/Loadable.jsx +16 -0
  92. package/src/app/pages/SQLBuilderPage/__tests__/index.test.jsx +173 -0
  93. package/src/app/pages/SQLBuilderPage/index.jsx +390 -0
  94. package/src/app/providers/djclient.jsx +5 -0
  95. package/src/app/services/DJService.js +398 -22
  96. package/src/app/services/__tests__/DJService.test.jsx +609 -0
  97. package/src/mocks/mockNodes.jsx +1397 -0
  98. package/src/setupTests.ts +31 -1
  99. package/src/styles/dag.css +111 -5
  100. package/src/styles/index.css +467 -31
  101. package/src/styles/login.css +67 -0
  102. package/src/styles/node-creation.scss +197 -0
  103. package/src/styles/styles.scss +44 -0
  104. package/src/styles/styles.scss.d.ts +9 -0
  105. package/src/utils/form.jsx +23 -0
  106. package/tsconfig.json +1 -5
  107. package/webpack.config.js +29 -6
  108. package/.babelrc +0 -4
  109. package/.env.local +0 -4
  110. package/.env.production +0 -1
  111. package/.github/pull_request_template.md +0 -11
  112. package/.github/workflows/ci.yml +0 -33
  113. package/.vscode/extensions.json +0 -7
  114. package/.vscode/launch.json +0 -15
  115. package/.vscode/settings.json +0 -25
  116. package/Dockerfile +0 -7
  117. package/dist/5fa71a03d45dc2e3d61447f3013a303d.png +0 -0
  118. package/dist/index.html +0 -1
  119. package/dist/main.js +0 -23303
  120. package/dist/static/main.05a86d446163fd5f17d3.js +0 -2
  121. package/dist/static/main.05a86d446163fd5f17d3.js.LICENSE.txt +0 -98
  122. package/dist/static/main.9e53bed734dae98e5b10.js +0 -2
  123. package/dist/static/main.9e53bed734dae98e5b10.js.LICENSE.txt +0 -98
  124. package/dist/static/main.js +0 -2
  125. package/dist/static/main.js.LICENSE.txt +0 -98
  126. package/dist/static/vendor.05a86d446163fd5f17d3.js +0 -2
  127. package/dist/static/vendor.05a86d446163fd5f17d3.js.LICENSE.txt +0 -29
  128. package/dist/static/vendor.9e53bed734dae98e5b10.js +0 -2
  129. package/dist/static/vendor.9e53bed734dae98e5b10.js.LICENSE.txt +0 -29
  130. package/dist/static/vendor.js +0 -2
  131. package/dist/static/vendor.js.LICENSE.txt +0 -29
  132. package/dist/vendor.js +0 -281
  133. package/src/app/pages/ListNamespacesPage/Loadable.jsx +0 -14
  134. package/src/app/pages/ListNamespacesPage/index.jsx +0 -52
  135. package/src/app/pages/NamespacePage/__tests__/__snapshots__/index.test.tsx.snap +0 -45
  136. package/src/app/pages/NamespacePage/__tests__/index.test.tsx +0 -14
@@ -0,0 +1,53 @@
1
+ // Jest Snapshot v1, https://goo.gl/fbAQLP
2
+
3
+ exports[`AddEditNodePage Create node page renders with the selected nodeType and namespace 1`] = `HTMLCollection []`;
4
+
5
+ exports[`AddEditNodePage submission succeeded for creating a node 1`] = `
6
+ HTMLCollection [
7
+ <div
8
+ class="message alert"
9
+ >
10
+ <svg
11
+ height="2em"
12
+ version="1.1"
13
+ viewBox="0 0 24 24"
14
+ width="2em"
15
+ xmlns="http://www.w3.org/2000/svg"
16
+ >
17
+ <title>
18
+ alert_fill
19
+ </title>
20
+ <g
21
+ fill="none"
22
+ fill-rule="evenodd"
23
+ id="page-1"
24
+ stroke="none"
25
+ stroke-width="1"
26
+ >
27
+ <g
28
+ fill-rule="nonzero"
29
+ id="System"
30
+ transform="translate(-48.000000, -48.000000)"
31
+ >
32
+ <g
33
+ id="alert_fill"
34
+ transform="translate(48.000000, 48.000000)"
35
+ >
36
+ <path
37
+ d="M24,0 L24,24 L0,24 L0,0 L24,0 Z M12.5934901,23.257841 L12.5819402,23.2595131 L12.5108777,23.2950439 L12.4918791,23.2987469 L12.4918791,23.2987469 L12.4767152,23.2950439 L12.4056548,23.2595131 C12.3958229,23.2563662 12.3870493,23.2590235 12.3821421,23.2649074 L12.3780323,23.275831 L12.360941,23.7031097 L12.3658947,23.7234994 L12.3769048,23.7357139 L12.4804777,23.8096931 L12.4953491,23.8136134 L12.4953491,23.8136134 L12.5071152,23.8096931 L12.6106902,23.7357139 L12.6232938,23.7196733 L12.6232938,23.7196733 L12.6266527,23.7031097 L12.609561,23.275831 C12.6075724,23.2657013 12.6010112,23.2592993 12.5934901,23.257841 L12.5934901,23.257841 Z M12.8583906,23.1452862 L12.8445485,23.1473072 L12.6598443,23.2396597 L12.6498822,23.2499052 L12.6498822,23.2499052 L12.6471943,23.2611114 L12.6650943,23.6906389 L12.6699349,23.7034178 L12.6699349,23.7034178 L12.678386,23.7104931 L12.8793402,23.8032389 C12.8914285,23.8068999 12.9022333,23.8029875 12.9078286,23.7952264 L12.9118235,23.7811639 L12.8776777,23.1665331 C12.8752882,23.1545897 12.8674102,23.1470016 12.8583906,23.1452862 L12.8583906,23.1452862 Z M12.1430473,23.1473072 C12.1332178,23.1423925 12.1221763,23.1452606 12.1156365,23.1525954 L12.1099173,23.1665331 L12.0757714,23.7811639 C12.0751323,23.7926639 12.0828099,23.8018602 12.0926481,23.8045676 L12.108256,23.8032389 L12.3092106,23.7104931 L12.3186497,23.7024347 L12.3186497,23.7024347 L12.3225043,23.6906389 L12.340401,23.2611114 L12.337245,23.2485176 L12.337245,23.2485176 L12.3277531,23.2396597 L12.1430473,23.1473072 Z"
38
+ fill-rule="nonzero"
39
+ id="MingCute"
40
+ />
41
+ <path
42
+ d="M13.299,3.1477 L21.933,18.1022 C22.5103,19.1022 21.7887,20.3522 20.634,20.3522 L3.36601,20.3522 C2.21131,20.3522 1.48962,19.1022 2.06697,18.1022 L10.7009,3.14771 C11.2783,2.14771 12.7217,2.1477 13.299,3.1477 Z M12,15 C11.4477,15 11,15.4477 11,16 C11,16.5523 11.4477,17 12,17 C12.5523,17 13,16.5523 13,16 C13,15.4477 12.5523,15 12,15 Z M12,8 C11.48715,8 11.0644908,8.38604429 11.0067275,8.88337975 L11,9 L11,13 C11,13.5523 11.4477,14 12,14 C12.51285,14 12.9355092,13.613973 12.9932725,13.1166239 L13,13 L13,9 C13,8.44772 12.5523,8 12,8 Z"
43
+ fill="#09244B"
44
+ id="shape"
45
+ />
46
+ </g>
47
+ </g>
48
+ </g>
49
+ </svg>
50
+ Some columns in the primary key [] were not found
51
+ </div>,
52
+ ]
53
+ `;
@@ -0,0 +1,3 @@
1
+ // Jest Snapshot v1, https://goo.gl/fbAQLP
2
+
3
+ exports[`AddEditNodePage Create node page renders with the selected nodeType and namespace 1`] = `HTMLCollection []`;
@@ -0,0 +1,178 @@
1
+ import React from 'react';
2
+ import { MemoryRouter, Route, Routes } from 'react-router-dom';
3
+ import { screen, waitFor } from '@testing-library/react';
4
+ import { render } from '../../../../setupTests';
5
+ import fetchMock from 'jest-fetch-mock';
6
+ import { AddEditNodePage } from '../index.jsx';
7
+ import { mocks } from '../../../../mocks/mockNodes';
8
+ import DJClientContext from '../../../providers/djclient';
9
+ import userEvent from '@testing-library/user-event';
10
+
11
+ fetchMock.enableMocks();
12
+
13
+ export const initializeMockDJClient = () => {
14
+ return {
15
+ DataJunctionAPI: {
16
+ namespace: _ => {
17
+ return [
18
+ {
19
+ name: 'default.contractors',
20
+ display_name: 'Default: Contractors',
21
+ version: 'v1.0',
22
+ type: 'source',
23
+ status: 'valid',
24
+ mode: 'published',
25
+ updated_at: '2023-08-21T16:48:53.246914+00:00',
26
+ },
27
+ {
28
+ name: 'default.num_repair_orders',
29
+ display_name: 'Default: Num Repair Orders',
30
+ version: 'v1.0',
31
+ type: 'metric',
32
+ status: 'valid',
33
+ mode: 'published',
34
+ updated_at: '2023-08-21T16:48:56.841704+00:00',
35
+ },
36
+ ];
37
+ },
38
+ metrics: {},
39
+ namespaces: () => {
40
+ return [
41
+ {
42
+ namespace: 'default',
43
+ num_nodes: 33,
44
+ },
45
+ {
46
+ namespace: 'default123',
47
+ num_nodes: 0,
48
+ },
49
+ ];
50
+ },
51
+ createNode: jest.fn(),
52
+ patchNode: jest.fn(),
53
+ node: jest.fn(),
54
+ },
55
+ };
56
+ };
57
+
58
+ export const testElement = djClient => {
59
+ return (
60
+ <DJClientContext.Provider value={djClient}>
61
+ <AddEditNodePage />
62
+ </DJClientContext.Provider>
63
+ );
64
+ };
65
+
66
+ export const renderCreateNode = element => {
67
+ return render(
68
+ <MemoryRouter initialEntries={['/create/dimension/default']}>
69
+ <Routes>
70
+ <Route path="create/:nodeType/:initialNamespace" element={element} />
71
+ </Routes>
72
+ </MemoryRouter>,
73
+ );
74
+ };
75
+
76
+ export const renderEditNode = element => {
77
+ return render(
78
+ <MemoryRouter initialEntries={['/nodes/default.num_repair_orders/edit']}>
79
+ <Routes>
80
+ <Route path="nodes/:name/edit" element={element} />
81
+ </Routes>
82
+ </MemoryRouter>,
83
+ );
84
+ };
85
+
86
+ describe('AddEditNodePage', () => {
87
+ beforeEach(() => {
88
+ fetchMock.resetMocks();
89
+ jest.clearAllMocks();
90
+ window.scrollTo = jest.fn();
91
+ });
92
+
93
+ it('Create node page renders with the selected nodeType and namespace', async () => {
94
+ const mockDjClient = initializeMockDJClient();
95
+ const element = testElement(mockDjClient);
96
+ const { container } = renderCreateNode(element);
97
+
98
+ // The node type should be included in the page title
99
+ await waitFor(() => {
100
+ expect(
101
+ container.getElementsByClassName('node_type__metric'),
102
+ ).toMatchSnapshot();
103
+
104
+ // The namespace should be set to the one provided in params
105
+ expect(screen.getByText('default')).toBeInTheDocument();
106
+ });
107
+ });
108
+
109
+ it('Edit node page renders with the selected node', async () => {
110
+ const mockDjClient = initializeMockDJClient();
111
+ mockDjClient.DataJunctionAPI.node.mockReturnValue(mocks.mockMetricNode);
112
+
113
+ const element = testElement(mockDjClient);
114
+ renderEditNode(element);
115
+
116
+ await waitFor(() => {
117
+ // Should be an edit node page
118
+ expect(screen.getByText('Edit')).toBeInTheDocument();
119
+
120
+ // The node name should be loaded onto the page
121
+ expect(screen.getByText('default.num_repair_orders')).toBeInTheDocument();
122
+
123
+ // The node type should be loaded onto the page
124
+ expect(screen.getByText('metric')).toBeInTheDocument();
125
+
126
+ // The description should be populated
127
+ expect(screen.getByText('Number of repair orders')).toBeInTheDocument();
128
+
129
+ // The query should be populated
130
+ expect(
131
+ screen.getByText(
132
+ 'SELECT count(repair_order_id) default_DOT_num_repair_orders FROM default.repair_orders',
133
+ ),
134
+ ).toBeInTheDocument();
135
+ });
136
+ });
137
+
138
+ it('Verify edit page node not found', async () => {
139
+ const mockDjClient = initializeMockDJClient();
140
+ mockDjClient.DataJunctionAPI.node = jest.fn();
141
+ mockDjClient.DataJunctionAPI.node.mockReturnValue({
142
+ message: 'A node with name `default.num_repair_orders` does not exist.',
143
+ errors: [],
144
+ warnings: [],
145
+ });
146
+ const element = testElement(mockDjClient);
147
+ renderEditNode(element);
148
+
149
+ await waitFor(() => {
150
+ expect(mockDjClient.DataJunctionAPI.node).toBeCalledTimes(1);
151
+ expect(
152
+ screen.getByText('Node default.num_repair_orders does not exist!'),
153
+ ).toBeInTheDocument();
154
+ });
155
+ }, 60000);
156
+
157
+ it('Verify only transforms, metrics, and dimensions can be edited', async () => {
158
+ const mockDjClient = initializeMockDJClient();
159
+ mockDjClient.DataJunctionAPI.node = jest.fn();
160
+ mockDjClient.DataJunctionAPI.node.mockReturnValue({
161
+ namespace: 'default',
162
+ type: 'source',
163
+ name: 'default.repair_orders',
164
+ display_name: 'Default: Repair Orders',
165
+ });
166
+ const element = testElement(mockDjClient);
167
+ renderEditNode(element);
168
+
169
+ await waitFor(() => {
170
+ expect(mockDjClient.DataJunctionAPI.node).toBeCalledTimes(1);
171
+ expect(
172
+ screen.getByText(
173
+ 'Node default.num_repair_orders is of type source and cannot be edited',
174
+ ),
175
+ ).toBeInTheDocument();
176
+ });
177
+ }, 60000);
178
+ });
@@ -0,0 +1,357 @@
1
+ /**
2
+ * Node add + edit page for transforms, metrics, and dimensions. The creation and edit flow for these
3
+ * node types is largely the same, with minor differences handled server-side. For the `query`
4
+ * field, this page will render a CodeMirror SQL editor with autocompletion and syntax highlighting.
5
+ */
6
+ import { ErrorMessage, Field, Form, Formik } from 'formik';
7
+
8
+ import NamespaceHeader from '../../components/NamespaceHeader';
9
+ import { useContext, useEffect, useState } from 'react';
10
+ import DJClientContext from '../../providers/djclient';
11
+ import 'styles/node-creation.scss';
12
+ import AlertIcon from '../../icons/AlertIcon';
13
+ import ValidIcon from '../../icons/ValidIcon';
14
+ import { useParams } from 'react-router-dom';
15
+ import { FullNameField } from './FullNameField';
16
+ import { FormikSelect } from './FormikSelect';
17
+ import { NodeQueryField } from './NodeQueryField';
18
+
19
+ class Action {
20
+ static Add = new Action('add');
21
+ static Edit = new Action('edit');
22
+
23
+ constructor(name) {
24
+ this.name = name;
25
+ }
26
+ }
27
+
28
+ export function AddEditNodePage() {
29
+ const djClient = useContext(DJClientContext).DataJunctionAPI;
30
+
31
+ let { nodeType, initialNamespace, name } = useParams();
32
+ const action = name !== undefined ? Action.Edit : Action.Add;
33
+
34
+ const [namespaces, setNamespaces] = useState([]);
35
+
36
+ const initialValues = {
37
+ name: action === Action.Edit ? name : '',
38
+ namespace: action === Action.Add ? initialNamespace : '',
39
+ display_name: '',
40
+ query: '',
41
+ node_type: '',
42
+ description: '',
43
+ primary_key: '',
44
+ mode: 'draft',
45
+ };
46
+
47
+ const validator = values => {
48
+ const errors = {};
49
+ if (!values.name) {
50
+ errors.name = 'Required';
51
+ }
52
+ if (!values.query) {
53
+ errors.query = 'Required';
54
+ }
55
+ return errors;
56
+ };
57
+
58
+ const handleSubmit = (values, { setSubmitting, setStatus }) => {
59
+ if (action === Action.Add) {
60
+ setTimeout(() => {
61
+ createNode(values, setStatus);
62
+ setSubmitting(false);
63
+ }, 400);
64
+ } else {
65
+ setTimeout(() => {
66
+ patchNode(values, setStatus);
67
+ setSubmitting(false);
68
+ }, 400);
69
+ }
70
+ window.scrollTo({ top: 0, left: 0, behavior: 'smooth' });
71
+ };
72
+
73
+ const displayMessageAfterSubmit = status => {
74
+ return status?.success !== undefined ? (
75
+ <div className="message success">
76
+ <ValidIcon />
77
+ {status?.success}
78
+ </div>
79
+ ) : status?.failure !== undefined ? (
80
+ <div className="message alert">
81
+ <AlertIcon />
82
+ {status?.failure}
83
+ </div>
84
+ ) : (
85
+ ''
86
+ );
87
+ };
88
+
89
+ const pageTitle =
90
+ action === Action.Add ? (
91
+ <h2>
92
+ Create{' '}
93
+ <span className={`node_type__${nodeType} node_type_creation_heading`}>
94
+ {nodeType}
95
+ </span>
96
+ </h2>
97
+ ) : (
98
+ <h2>Edit</h2>
99
+ );
100
+
101
+ const staticFieldsInEdit = node => (
102
+ <>
103
+ <div className="NodeNameInput NodeCreationInput">
104
+ <label htmlFor="name">Name</label> {name}
105
+ </div>
106
+ <div className="NodeNameInput NodeCreationInput">
107
+ <label htmlFor="name">Type</label> {node.type}
108
+ </div>
109
+ </>
110
+ );
111
+
112
+ const primaryKeyToList = primaryKey => {
113
+ return primaryKey.split(',').map(columnName => columnName.trim());
114
+ };
115
+
116
+ const createNode = async (values, setStatus) => {
117
+ const { status, json } = await djClient.createNode(
118
+ nodeType,
119
+ values.name,
120
+ values.display_name,
121
+ values.description,
122
+ values.query,
123
+ values.mode,
124
+ values.namespace,
125
+ values.primary_key ? primaryKeyToList(values.primary_key) : null,
126
+ );
127
+ if (status === 200 || status === 201) {
128
+ setStatus({
129
+ success: (
130
+ <>
131
+ Successfully created {json.type} node{' '}
132
+ <a href={`/nodes/${json.name}`}>{json.name}</a>!
133
+ </>
134
+ ),
135
+ });
136
+ } else {
137
+ setStatus({
138
+ failure: `${json.message}`,
139
+ });
140
+ }
141
+ };
142
+
143
+ const patchNode = async (values, setStatus) => {
144
+ const { status, json } = await djClient.patchNode(
145
+ values.name,
146
+ values.display_name,
147
+ values.description,
148
+ values.query,
149
+ values.mode,
150
+ values.primary_key ? primaryKeyToList(values.primary_key) : null,
151
+ );
152
+ if (status === 200 || status === 201) {
153
+ setStatus({
154
+ success: (
155
+ <>
156
+ Successfully updated {json.type} node{' '}
157
+ <a href={`/nodes/${json.name}`}>{json.name}</a>!
158
+ </>
159
+ ),
160
+ });
161
+ } else {
162
+ setStatus({
163
+ failure: `${json.message}`,
164
+ });
165
+ }
166
+ };
167
+
168
+ const namespaceInput = (
169
+ <div className="NamespaceInput">
170
+ <ErrorMessage name="namespace" component="span" />
171
+ <label htmlFor="react-select-3-input">Namespace</label>
172
+ <FormikSelect
173
+ selectOptions={namespaces}
174
+ formikFieldName="namespace"
175
+ placeholder="Choose Namespace"
176
+ defaultValue={{
177
+ value: initialNamespace,
178
+ label: initialNamespace,
179
+ }}
180
+ />
181
+ </div>
182
+ );
183
+
184
+ const fullNameInput = (
185
+ <div className="FullNameInput NodeCreationInput">
186
+ <ErrorMessage name="name" component="span" />
187
+ <label htmlFor="FullName">Full Name</label>
188
+ <FullNameField type="text" name="name" />
189
+ </div>
190
+ );
191
+
192
+ const nodeCanBeEdited = nodeType => {
193
+ return new Set(['transform', 'metric', 'dimension']).has(nodeType);
194
+ };
195
+
196
+ const updateFieldsWithNodeData = (data, setFieldValue) => {
197
+ const fields = [
198
+ 'display_name',
199
+ 'query',
200
+ 'type',
201
+ 'description',
202
+ 'primary_key',
203
+ 'mode',
204
+ ];
205
+ fields.forEach(field => {
206
+ if (
207
+ field === 'primary_key' &&
208
+ data[field] !== undefined &&
209
+ Array.isArray(data[field])
210
+ ) {
211
+ data[field] = data[field].join(', ');
212
+ }
213
+ setFieldValue(field, data[field] || '', false);
214
+ });
215
+ };
216
+
217
+ const alertMessage = message => {
218
+ return (
219
+ <div className="message alert">
220
+ <AlertIcon />
221
+ {message}
222
+ </div>
223
+ );
224
+ };
225
+
226
+ // Get namespaces, only necessary when creating a node
227
+ useEffect(() => {
228
+ if (action === Action.Add) {
229
+ const fetchData = async () => {
230
+ const namespaces = await djClient.namespaces();
231
+ setNamespaces(
232
+ namespaces.map(m => ({
233
+ value: m['namespace'],
234
+ label: m['namespace'],
235
+ })),
236
+ );
237
+ };
238
+ fetchData().catch(console.error);
239
+ }
240
+ }, [action, djClient, djClient.metrics]);
241
+
242
+ return (
243
+ <div className="mid">
244
+ <NamespaceHeader namespace="" />
245
+ <div className="card">
246
+ <div className="card-header">
247
+ {pageTitle}
248
+ <center>
249
+ <Formik
250
+ initialValues={initialValues}
251
+ validate={validator}
252
+ onSubmit={handleSubmit}
253
+ >
254
+ {function Render({ isSubmitting, status, setFieldValue }) {
255
+ const [node, setNode] = useState([]);
256
+ const [message, setMessage] = useState('');
257
+ useEffect(() => {
258
+ const fetchData = async () => {
259
+ if (action === Action.Edit) {
260
+ const data = await djClient.node(name);
261
+
262
+ // Check if node exists
263
+ if (data.message !== undefined) {
264
+ setNode(null);
265
+ setMessage(`Node ${name} does not exist!`);
266
+ return;
267
+ }
268
+
269
+ // Check if node type can be edited
270
+ if (!nodeCanBeEdited(data.type)) {
271
+ setNode(null);
272
+ setMessage(
273
+ `Node ${name} is of type ${data.type} and cannot be edited`,
274
+ );
275
+ return;
276
+ }
277
+
278
+ // Update fields with existing data to prepare for edit
279
+ updateFieldsWithNodeData(data, setFieldValue);
280
+ setNode(data);
281
+ }
282
+ };
283
+ fetchData().catch(console.error);
284
+ }, [setFieldValue]);
285
+ return (
286
+ <Form>
287
+ {displayMessageAfterSubmit(status)}
288
+ {action === Action.Edit && message ? (
289
+ alertMessage(message)
290
+ ) : (
291
+ <>
292
+ {action === Action.Add
293
+ ? namespaceInput
294
+ : staticFieldsInEdit(node)}
295
+ <div className="DisplayNameInput NodeCreationInput">
296
+ <ErrorMessage name="display_name" component="span" />
297
+ <label htmlFor="displayName">Display Name</label>
298
+ <Field
299
+ type="text"
300
+ name="display_name"
301
+ id="displayName"
302
+ placeholder="Human readable display name"
303
+ />
304
+ </div>
305
+ {action === Action.Add ? fullNameInput : ''}
306
+ <div className="DescriptionInput NodeCreationInput">
307
+ <ErrorMessage name="description" component="span" />
308
+ <label htmlFor="Description">Description</label>
309
+ <Field
310
+ type="textarea"
311
+ as="textarea"
312
+ name="description"
313
+ id="Description"
314
+ placeholder="Describe your node"
315
+ />
316
+ </div>
317
+ <div className="QueryInput NodeCreationInput">
318
+ <ErrorMessage name="query" component="span" />
319
+ <label htmlFor="Query">Query</label>
320
+ <NodeQueryField
321
+ djClient={djClient}
322
+ value={node.query ? node.query : ''}
323
+ />
324
+ </div>
325
+ <div className="PrimaryKeyInput NodeCreationInput">
326
+ <ErrorMessage name="primary_key" component="span" />
327
+ <label htmlFor="primaryKey">Primary Key</label>
328
+ <Field
329
+ type="text"
330
+ name="primary_key"
331
+ id="primaryKey"
332
+ placeholder="Comma-separated list of PKs"
333
+ />
334
+ </div>
335
+ <div className="NodeModeInput NodeCreationInput">
336
+ <ErrorMessage name="mode" component="span" />
337
+ <label htmlFor="Mode">Mode</label>
338
+ <Field as="select" name="mode" id="Mode">
339
+ <option value="draft">Draft</option>
340
+ <option value="published">Published</option>
341
+ </Field>
342
+ </div>
343
+ <button type="submit" disabled={isSubmitting}>
344
+ {action === Action.Add ? 'Create' : 'Save'} {nodeType}
345
+ </button>
346
+ </>
347
+ )}
348
+ </Form>
349
+ );
350
+ }}
351
+ </Formik>
352
+ </center>
353
+ </div>
354
+ </div>
355
+ </div>
356
+ );
357
+ }
@@ -0,0 +1,70 @@
1
+ import React from 'react';
2
+ import { render, fireEvent, waitFor } from '@testing-library/react';
3
+ import { LoginPage } from '../index';
4
+
5
+ describe('LoginPage', () => {
6
+ const original = window.location;
7
+
8
+ const reloadFn = () => {
9
+ window.location.reload();
10
+ };
11
+
12
+ beforeAll(() => {
13
+ Object.defineProperty(window, 'location', {
14
+ configurable: true,
15
+ value: { reload: jest.fn() },
16
+ });
17
+ });
18
+
19
+ afterAll(() => {
20
+ Object.defineProperty(window, 'location', {
21
+ configurable: true,
22
+ value: original,
23
+ });
24
+ });
25
+
26
+ beforeEach(() => {
27
+ fetch.resetMocks();
28
+ });
29
+
30
+ afterEach(() => {
31
+ jest.clearAllMocks();
32
+ });
33
+
34
+ it('displays error messages when fields are empty and form is submitted', async () => {
35
+ const { getByText, queryAllByText } = render(<LoginPage />);
36
+ fireEvent.click(getByText('Login'));
37
+
38
+ await waitFor(() => {
39
+ expect(getByText('DataJunction')).toBeInTheDocument();
40
+ expect(queryAllByText('Required').length).toEqual(2);
41
+ });
42
+ });
43
+
44
+ it('calls fetch with correct data on submit', async () => {
45
+ const username = 'testUser';
46
+ const password = 'testPassword';
47
+ reloadFn();
48
+
49
+ const { getByText, getByPlaceholderText } = render(<LoginPage />);
50
+ fireEvent.change(getByPlaceholderText('Username'), {
51
+ target: { value: username },
52
+ });
53
+ fireEvent.change(getByPlaceholderText('Password'), {
54
+ target: { value: password },
55
+ });
56
+ fireEvent.click(getByText('Login'));
57
+
58
+ await waitFor(() => {
59
+ expect(fetch).toHaveBeenCalledWith(
60
+ `${process.env.REACT_APP_DJ_URL}/basic/login/`,
61
+ expect.objectContaining({
62
+ method: 'POST',
63
+ body: expect.any(FormData),
64
+ credentials: 'include',
65
+ }),
66
+ );
67
+ expect(window.location.reload).toHaveBeenCalled();
68
+ });
69
+ });
70
+ });