datajunction-ui 0.0.1-rc.23 → 0.0.1-rc.25

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 (38) hide show
  1. package/.env +1 -0
  2. package/package.json +3 -2
  3. package/src/app/components/Tab.jsx +0 -1
  4. package/src/app/constants.js +2 -2
  5. package/src/app/icons/LoadingIcon.jsx +14 -0
  6. package/src/app/index.tsx +11 -1
  7. package/src/app/pages/AddEditNodePage/__tests__/AddEditNodePageFormFailed.test.jsx +28 -2
  8. package/src/app/pages/AddEditNodePage/__tests__/AddEditNodePageFormSuccess.test.jsx +44 -9
  9. package/src/app/pages/AddEditNodePage/__tests__/__snapshots__/AddEditNodePageFormFailed.test.jsx.snap +1 -0
  10. package/src/app/pages/AddEditNodePage/__tests__/__snapshots__/AddEditNodePageFormSuccess.test.jsx.snap +0 -50
  11. package/src/app/pages/AddEditNodePage/__tests__/index.test.jsx +2 -0
  12. package/src/app/pages/AddEditNodePage/index.jsx +60 -6
  13. package/src/app/pages/AddEditTagPage/Loadable.jsx +16 -0
  14. package/src/app/pages/AddEditTagPage/__tests__/AddEditTagPage.test.jsx +107 -0
  15. package/src/app/pages/AddEditTagPage/index.jsx +132 -0
  16. package/src/app/pages/LoginPage/LoginForm.jsx +116 -0
  17. package/src/app/pages/LoginPage/SignupForm.jsx +144 -0
  18. package/src/app/pages/LoginPage/__tests__/index.test.jsx +34 -2
  19. package/src/app/pages/LoginPage/index.jsx +9 -82
  20. package/src/app/pages/NamespacePage/index.jsx +5 -0
  21. package/src/app/pages/NodePage/ClientCodePopover.jsx +15 -1
  22. package/src/app/pages/NodePage/EditColumnPopover.jsx +15 -1
  23. package/src/app/pages/NodePage/LinkDimensionPopover.jsx +16 -1
  24. package/src/app/pages/NodePage/NodeColumnTab.jsx +11 -0
  25. package/src/app/pages/NodePage/NodeInfoTab.jsx +5 -1
  26. package/src/app/pages/NodePage/__tests__/NodePage.test.jsx +6 -3
  27. package/src/app/pages/RegisterTablePage/__tests__/__snapshots__/RegisterTablePage.test.jsx.snap +1 -0
  28. package/src/app/pages/Root/index.tsx +1 -1
  29. package/src/app/pages/TagPage/Loadable.jsx +16 -0
  30. package/src/app/pages/TagPage/__tests__/TagPage.test.jsx +70 -0
  31. package/src/app/pages/TagPage/index.jsx +79 -0
  32. package/src/app/services/DJService.js +79 -1
  33. package/src/app/services/__tests__/DJService.test.jsx +84 -1
  34. package/src/mocks/mockNodes.jsx +88 -44
  35. package/src/styles/index.css +19 -0
  36. package/src/styles/loading.css +35 -0
  37. package/src/styles/login.css +17 -3
  38. package/src/utils/form.jsx +2 -2
package/.env CHANGED
@@ -1,2 +1,3 @@
1
1
  REACT_APP_DJ_URL=http://localhost:8000
2
2
  REACT_USE_SSE=true
3
+ REACT_ENABLE_GOOGLE_OAUTH=true
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "datajunction-ui",
3
- "version": "0.0.1-rc.23",
3
+ "version": "0.0.1-rc.25",
4
4
  "description": "DataJunction Metrics Platform UI",
5
5
  "module": "src/index.tsx",
6
6
  "repository": {
@@ -98,7 +98,8 @@
98
98
  "web-vitals": "2.1.4",
99
99
  "webpack": "5.81.0",
100
100
  "webpack-cli": "5.0.2",
101
- "webpack-dev-server": "4.13.3"
101
+ "webpack-dev-server": "4.13.3",
102
+ "yup": "1.3.2"
102
103
  },
103
104
  "scripts": {
104
105
  "webpack-start": "webpack-dev-server --open",
@@ -12,7 +12,6 @@ export default class Tab extends Component {
12
12
  className="nav-link"
13
13
  tabIndex="0"
14
14
  onClick={onClick}
15
- role="button"
16
15
  aria-label={this.props.name}
17
16
  aria-hidden="false"
18
17
  >
@@ -1,2 +1,2 @@
1
- export const DJ_AUTH_COOKIE = '__dj';
2
- export const DJ_LOGGED_IN_FLAG_COOKIE = '__djlif';
1
+ export const AUTH_COOKIE = '__dj';
2
+ export const LOGGED_IN_FLAG_COOKIE = '__djlif';
@@ -0,0 +1,14 @@
1
+ import '../../styles/loading.css';
2
+
3
+ export default function LoadingIcon() {
4
+ return (
5
+ <center>
6
+ <div class="lds-ring">
7
+ <div></div>
8
+ <div></div>
9
+ <div></div>
10
+ <div></div>
11
+ </div>
12
+ </center>
13
+ );
14
+ }
package/src/app/index.tsx CHANGED
@@ -10,7 +10,9 @@ import { BrowserRouter, Routes, Route } from 'react-router-dom';
10
10
  import { NamespacePage } from './pages/NamespacePage/Loadable';
11
11
  import { NodePage } from './pages/NodePage/Loadable';
12
12
  import { SQLBuilderPage } from './pages/SQLBuilderPage/Loadable';
13
+ import { TagPage } from './pages/TagPage/Loadable';
13
14
  import { AddEditNodePage } from './pages/AddEditNodePage/Loadable';
15
+ import { AddEditTagPage } from './pages/AddEditTagPage/Loadable';
14
16
  import { NotFoundPage } from './pages/NotFoundPage/Loadable';
15
17
  import { LoginPage } from './pages/LoginPage';
16
18
  import { RegisterTablePage } from './pages/RegisterTablePage';
@@ -21,7 +23,7 @@ import { CookiesProvider, useCookies } from 'react-cookie';
21
23
  import * as Constants from './constants';
22
24
 
23
25
  export function App() {
24
- const [cookies] = useCookies([Constants.DJ_LOGGED_IN_FLAG_COOKIE]);
26
+ const [cookies] = useCookies([Constants.LOGGED_IN_FLAG_COOKIE]);
25
27
  return (
26
28
  <CookiesProvider>
27
29
  <BrowserRouter>
@@ -60,6 +62,11 @@ export function App() {
60
62
  key="namespaces"
61
63
  />
62
64
  </Route>
65
+ <Route
66
+ path="create/tag"
67
+ key="createtag"
68
+ element={<AddEditTagPage />}
69
+ ></Route>
63
70
  <Route
64
71
  path="create/source"
65
72
  key="register"
@@ -82,6 +89,9 @@ export function App() {
82
89
  key="sql"
83
90
  element={<SQLBuilderPage />}
84
91
  />
92
+ <Route path="tags" key="tags">
93
+ <Route path=":name" element={<TagPage />} />
94
+ </Route>
85
95
  </>
86
96
  }
87
97
  />
@@ -1,5 +1,5 @@
1
1
  import React from 'react';
2
- import { screen, waitFor } from '@testing-library/react';
2
+ import { fireEvent, screen, waitFor } from '@testing-library/react';
3
3
  import fetchMock from 'jest-fetch-mock';
4
4
  import userEvent from '@testing-library/user-event';
5
5
  import {
@@ -23,6 +23,10 @@ describe('AddEditNodePage submission failed', () => {
23
23
  status: 500,
24
24
  json: { message: 'Some columns in the primary key [] were not found' },
25
25
  });
26
+ mockDjClient.DataJunctionAPI.listTags.mockReturnValue([
27
+ { name: 'purpose', display_name: 'Purpose' },
28
+ { name: 'intent', display_name: 'Intent' },
29
+ ]);
26
30
 
27
31
  const element = testElement(mockDjClient);
28
32
  const { container } = renderCreateNode(element);
@@ -63,6 +67,16 @@ describe('AddEditNodePage submission failed', () => {
63
67
  json: { message: 'Update failed' },
64
68
  });
65
69
 
70
+ mockDjClient.DataJunctionAPI.tagsNode.mockReturnValue({
71
+ status: 404,
72
+ json: { message: 'Some tags were not found' },
73
+ });
74
+
75
+ mockDjClient.DataJunctionAPI.listTags.mockReturnValue([
76
+ { name: 'purpose', display_name: 'Purpose' },
77
+ { name: 'intent', display_name: 'Intent' },
78
+ ]);
79
+
66
80
  const element = testElement(mockDjClient);
67
81
  renderEditNode(element);
68
82
 
@@ -71,7 +85,19 @@ describe('AddEditNodePage submission failed', () => {
71
85
  await userEvent.click(screen.getByText('Save'));
72
86
  await waitFor(async () => {
73
87
  expect(mockDjClient.DataJunctionAPI.patchNode).toBeCalledTimes(1);
74
- expect(await screen.getByText('Update failed')).toBeInTheDocument();
88
+ expect(mockDjClient.DataJunctionAPI.tagsNode).toBeCalled();
89
+ expect(mockDjClient.DataJunctionAPI.tagsNode).toBeCalledWith(
90
+ 'default.num_repair_orders',
91
+ [{ display_name: 'Purpose', name: 'purpose' }],
92
+ );
93
+ expect(mockDjClient.DataJunctionAPI.tagsNode).toReturnWith({
94
+ json: { message: 'Some tags were not found' },
95
+ status: 404,
96
+ });
97
+
98
+ expect(
99
+ await screen.getByText('Update failed, Some tags were not found'),
100
+ ).toBeInTheDocument();
75
101
  });
76
102
  }, 60000);
77
103
  });
@@ -1,5 +1,5 @@
1
1
  import React from 'react';
2
- import { screen, waitFor } from '@testing-library/react';
2
+ import { fireEvent, screen, waitFor } from '@testing-library/react';
3
3
  import fetchMock from 'jest-fetch-mock';
4
4
  import userEvent from '@testing-library/user-event';
5
5
  import {
@@ -24,10 +24,20 @@ describe('AddEditNodePage submission succeeded', () => {
24
24
  it('for creating a node', async () => {
25
25
  const mockDjClient = initializeMockDJClient();
26
26
  mockDjClient.DataJunctionAPI.createNode.mockReturnValue({
27
- status: 500,
28
- json: { message: 'Some columns in the primary key [] were not found' },
27
+ status: 200,
28
+ json: { name: 'default.some_test_metric' },
29
29
  });
30
30
 
31
+ mockDjClient.DataJunctionAPI.tagsNode.mockReturnValue({
32
+ status: 200,
33
+ json: { message: 'Success' },
34
+ });
35
+
36
+ mockDjClient.DataJunctionAPI.listTags.mockReturnValue([
37
+ { name: 'purpose', display_name: 'Purpose' },
38
+ { name: 'intent', display_name: 'Intent' },
39
+ ]);
40
+
31
41
  const element = testElement(mockDjClient);
32
42
  const { container } = renderCreateNode(element);
33
43
 
@@ -50,13 +60,18 @@ describe('AddEditNodePage submission succeeded', () => {
50
60
  'default',
51
61
  null,
52
62
  );
53
- expect(
54
- screen.getByText(/Some columns in the primary key \[] were not found/),
55
- ).toBeInTheDocument();
63
+ expect(mockDjClient.DataJunctionAPI.tagsNode).toBeCalled();
64
+ expect(mockDjClient.DataJunctionAPI.tagsNode).toBeCalledWith(
65
+ 'default.some_test_metric',
66
+ undefined,
67
+ );
68
+ expect(screen.getByText(/default.some_test_metric/)).toBeInTheDocument();
56
69
  });
57
70
 
58
- // After failed creation, it should return a failure message
59
- expect(container.getElementsByClassName('alert')).toMatchSnapshot();
71
+ // After successful creation, it should return a success message
72
+ expect(screen.getByTestId('success')).toHaveTextContent(
73
+ 'Successfully created node default.some_test_metric',
74
+ );
60
75
  }, 60000);
61
76
 
62
77
  it('for editing a node', async () => {
@@ -69,13 +84,27 @@ describe('AddEditNodePage submission succeeded', () => {
69
84
  json: { name: 'default.num_repair_orders', type: 'metric' },
70
85
  });
71
86
 
87
+ mockDjClient.DataJunctionAPI.tagsNode.mockReturnValue({
88
+ status: 200,
89
+ json: { message: 'Success' },
90
+ });
91
+
92
+ mockDjClient.DataJunctionAPI.listTags.mockReturnValue([
93
+ { name: 'purpose', display_name: 'Purpose' },
94
+ { name: 'intent', display_name: 'Intent' },
95
+ ]);
96
+
72
97
  const element = testElement(mockDjClient);
73
- renderEditNode(element);
98
+ const { getByTestId } = renderEditNode(element);
74
99
 
75
100
  await userEvent.type(screen.getByLabelText('Display Name'), '!!!');
76
101
  await userEvent.type(screen.getByLabelText('Description'), '!!!');
77
102
  await userEvent.click(screen.getByText('Save'));
78
103
 
104
+ const selectTags = getByTestId('select-tags');
105
+ await fireEvent.keyDown(selectTags.firstChild, { key: 'ArrowDown' });
106
+ await fireEvent.click(screen.getByText('Purpose'));
107
+
79
108
  await waitFor(async () => {
80
109
  expect(mockDjClient.DataJunctionAPI.patchNode).toBeCalledTimes(1);
81
110
  expect(mockDjClient.DataJunctionAPI.patchNode).toBeCalledWith(
@@ -86,6 +115,12 @@ describe('AddEditNodePage submission succeeded', () => {
86
115
  'published',
87
116
  ['repair_order_id', 'country'],
88
117
  );
118
+ expect(mockDjClient.DataJunctionAPI.tagsNode).toBeCalledTimes(1);
119
+ expect(mockDjClient.DataJunctionAPI.tagsNode).toBeCalledWith(
120
+ 'default.num_repair_orders',
121
+ [{ display_name: 'Purpose', name: 'purpose' }],
122
+ );
123
+
89
124
  expect(
90
125
  await screen.getByDisplayValue('repair_order_id, country'),
91
126
  ).toBeInTheDocument();
@@ -6,6 +6,7 @@ exports[`AddEditNodePage submission failed for creating a node 1`] = `
6
6
  HTMLCollection [
7
7
  <div
8
8
  class="message alert"
9
+ data-testid="failure"
9
10
  >
10
11
  <svg
11
12
  height="2em"
@@ -1,53 +1,3 @@
1
1
  // Jest Snapshot v1, https://goo.gl/fbAQLP
2
2
 
3
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
- `;
@@ -51,6 +51,8 @@ export const initializeMockDJClient = () => {
51
51
  createNode: jest.fn(),
52
52
  patchNode: jest.fn(),
53
53
  node: jest.fn(),
54
+ tagsNode: jest.fn(),
55
+ listTags: jest.fn(),
54
56
  },
55
57
  };
56
58
  };
@@ -10,12 +10,11 @@ import { useContext, useEffect, useState } from 'react';
10
10
  import DJClientContext from '../../providers/djclient';
11
11
  import 'styles/node-creation.scss';
12
12
  import AlertIcon from '../../icons/AlertIcon';
13
- import ValidIcon from '../../icons/ValidIcon';
14
13
  import { useParams } from 'react-router-dom';
15
14
  import { FullNameField } from './FullNameField';
16
15
  import { FormikSelect } from './FormikSelect';
17
16
  import { NodeQueryField } from './NodeQueryField';
18
- import { displayMessageAfterSubmit } from '../../../utils/form';
17
+ import { displayMessageAfterSubmit, labelize } from '../../../utils/form';
19
18
 
20
19
  class Action {
21
20
  static Add = new Action('add');
@@ -33,6 +32,7 @@ export function AddEditNodePage() {
33
32
  const action = name !== undefined ? Action.Edit : Action.Add;
34
33
 
35
34
  const [namespaces, setNamespaces] = useState([]);
35
+ const [tags, setTags] = useState([]);
36
36
 
37
37
  const initialValues = {
38
38
  name: action === Action.Edit ? name : '',
@@ -110,6 +110,7 @@ export function AddEditNodePage() {
110
110
  values.primary_key ? primaryKeyToList(values.primary_key) : null,
111
111
  );
112
112
  if (status === 200 || status === 201) {
113
+ await djClient.tagsNode(values.name, values.tags);
113
114
  setStatus({
114
115
  success: (
115
116
  <>
@@ -134,7 +135,8 @@ export function AddEditNodePage() {
134
135
  values.mode,
135
136
  values.primary_key ? primaryKeyToList(values.primary_key) : null,
136
137
  );
137
- if (status === 200 || status === 201) {
138
+ const tagsResponse = await djClient.tagsNode(values.name, values.tags);
139
+ if ((status === 200 || status === 201) && tagsResponse.status === 200) {
138
140
  setStatus({
139
141
  success: (
140
142
  <>
@@ -145,7 +147,7 @@ export function AddEditNodePage() {
145
147
  });
146
148
  } else {
147
149
  setStatus({
148
- failure: `${json.message}`,
150
+ failure: `${json.message}, ${tagsResponse.json.message}`,
149
151
  });
150
152
  }
151
153
  };
@@ -186,6 +188,7 @@ export function AddEditNodePage() {
186
188
  'description',
187
189
  'primary_key',
188
190
  'mode',
191
+ 'tags',
189
192
  ];
190
193
  fields.forEach(field => {
191
194
  if (
@@ -194,8 +197,9 @@ export function AddEditNodePage() {
194
197
  Array.isArray(data[field])
195
198
  ) {
196
199
  data[field] = data[field].join(', ');
200
+ } else {
201
+ setFieldValue(field, data[field] || '', false);
197
202
  }
198
- setFieldValue(field, data[field] || '', false);
199
203
  });
200
204
  };
201
205
 
@@ -224,6 +228,20 @@ export function AddEditNodePage() {
224
228
  }
225
229
  }, [action, djClient, djClient.metrics]);
226
230
 
231
+ // Get list of tags
232
+ useEffect(() => {
233
+ const fetchData = async () => {
234
+ const tags = await djClient.listTags();
235
+ setTags(
236
+ tags.map(tag => ({
237
+ value: tag.name,
238
+ label: tag.display_name,
239
+ })),
240
+ );
241
+ };
242
+ fetchData().catch(console.error);
243
+ }, [djClient, djClient.listTags]);
244
+
227
245
  return (
228
246
  <div className="mid">
229
247
  <NamespaceHeader namespace="" />
@@ -238,7 +256,31 @@ export function AddEditNodePage() {
238
256
  >
239
257
  {function Render({ isSubmitting, status, setFieldValue }) {
240
258
  const [node, setNode] = useState([]);
259
+ const [selectTags, setSelectTags] = useState(null);
241
260
  const [message, setMessage] = useState('');
261
+
262
+ const tagsInput = (
263
+ <div
264
+ className="TagsInput"
265
+ style={{ width: '25%', margin: '1rem 0 1rem 1.2rem' }}
266
+ >
267
+ <ErrorMessage name="tags" component="span" />
268
+ <label htmlFor="react-select-3-input">Tags</label>
269
+ <span data-testid="select-tags">
270
+ {action === Action.Edit ? (
271
+ selectTags
272
+ ) : (
273
+ <FormikSelect
274
+ isMulti={true}
275
+ selectOptions={tags}
276
+ formikFieldName="tags"
277
+ placeholder="Choose Tags"
278
+ />
279
+ )}
280
+ </span>
281
+ </div>
282
+ );
283
+
242
284
  useEffect(() => {
243
285
  const fetchData = async () => {
244
286
  if (action === Action.Edit) {
@@ -263,10 +305,21 @@ export function AddEditNodePage() {
263
305
  // Update fields with existing data to prepare for edit
264
306
  updateFieldsWithNodeData(data, setFieldValue);
265
307
  setNode(data);
308
+ setSelectTags(
309
+ <FormikSelect
310
+ isMulti={true}
311
+ selectOptions={tags}
312
+ formikFieldName="tags"
313
+ placeholder="Choose Tags"
314
+ defaultValue={data.tags.map(t => {
315
+ return { value: t.name, label: t.display_name };
316
+ })}
317
+ />,
318
+ );
266
319
  }
267
320
  };
268
321
  fetchData().catch(console.error);
269
- }, [setFieldValue]);
322
+ }, [setFieldValue, tags]);
270
323
  return (
271
324
  <Form>
272
325
  {displayMessageAfterSubmit(status)}
@@ -317,6 +370,7 @@ export function AddEditNodePage() {
317
370
  placeholder="Comma-separated list of PKs"
318
371
  />
319
372
  </div>
373
+ {tagsInput}
320
374
  <div className="NodeModeInput NodeCreationInput">
321
375
  <ErrorMessage name="mode" component="span" />
322
376
  <label htmlFor="Mode">Mode</label>
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Asynchronously loads the component for the Node page
3
+ */
4
+
5
+ import * as React from 'react';
6
+ import { lazyLoad } from '../../../utils/loadable';
7
+
8
+ export const AddEditTagPage = () => {
9
+ return lazyLoad(
10
+ () => import('./index'),
11
+ module => module.AddEditTagPage,
12
+ {
13
+ fallback: <div></div>,
14
+ },
15
+ )();
16
+ };
@@ -0,0 +1,107 @@
1
+ import React from 'react';
2
+ import { screen, waitFor } from '@testing-library/react';
3
+ import fetchMock from 'jest-fetch-mock';
4
+ import userEvent from '@testing-library/user-event';
5
+ import { render } from '../../../../setupTests';
6
+ import { MemoryRouter, Route, Routes } from 'react-router-dom';
7
+ import DJClientContext from '../../../providers/djclient';
8
+ import { AddEditTagPage } from '../index';
9
+
10
+ describe('<AddEditTagPage />', () => {
11
+ const initializeMockDJClient = () => {
12
+ return {
13
+ DataJunctionAPI: {
14
+ addTag: jest.fn(),
15
+ },
16
+ };
17
+ };
18
+
19
+ const mockDjClient = initializeMockDJClient();
20
+
21
+ beforeEach(() => {
22
+ fetchMock.resetMocks();
23
+ jest.clearAllMocks();
24
+ window.scrollTo = jest.fn();
25
+ });
26
+
27
+ const renderAddEditTagPage = element => {
28
+ return render(
29
+ <MemoryRouter initialEntries={['/create/tag']}>
30
+ <Routes>
31
+ <Route path="create/tag" element={element} />
32
+ </Routes>
33
+ </MemoryRouter>,
34
+ );
35
+ };
36
+
37
+ const testElement = djClient => {
38
+ return (
39
+ <DJClientContext.Provider value={djClient}>
40
+ <AddEditTagPage />
41
+ </DJClientContext.Provider>
42
+ );
43
+ };
44
+
45
+ it('adds a tag correctly', async () => {
46
+ mockDjClient.DataJunctionAPI.addTag.mockReturnValue({
47
+ status: 200,
48
+ json: {
49
+ name: 'amanita_muscaria',
50
+ display_name: 'Amanita Muscaria',
51
+ tag_type: 'fungi',
52
+ description: 'Fly agaric, is poisonous',
53
+ },
54
+ });
55
+
56
+ const element = testElement(mockDjClient);
57
+ renderAddEditTagPage(element);
58
+
59
+ await userEvent.type(screen.getByLabelText('Name'), 'amanita_muscaria');
60
+ await userEvent.type(
61
+ screen.getByLabelText('Display Name'),
62
+ 'Amanita Muscaria',
63
+ );
64
+ await userEvent.type(screen.getByLabelText('Tag Type'), 'fungi');
65
+ await userEvent.click(screen.getByRole('button'));
66
+
67
+ await waitFor(() => {
68
+ expect(mockDjClient.DataJunctionAPI.addTag).toBeCalled();
69
+ expect(mockDjClient.DataJunctionAPI.addTag).toBeCalledWith(
70
+ 'amanita_muscaria',
71
+ 'Amanita Muscaria',
72
+ 'fungi',
73
+ undefined,
74
+ );
75
+ expect(screen.getByTestId('success')).toHaveTextContent(
76
+ 'Successfully added tag Amanita Muscaria',
77
+ );
78
+ });
79
+ }, 60000);
80
+
81
+ it('fails to add a tag', async () => {
82
+ mockDjClient.DataJunctionAPI.addTag.mockReturnValue({
83
+ status: 500,
84
+ json: { message: 'Tag exists' },
85
+ });
86
+
87
+ const element = testElement(mockDjClient);
88
+ renderAddEditTagPage(element);
89
+
90
+ await userEvent.click(screen.getByRole('button'));
91
+
92
+ await userEvent.type(screen.getByLabelText('Name'), 'amanita_muscaria');
93
+ await userEvent.type(
94
+ screen.getByLabelText('Display Name'),
95
+ 'Amanita Muscaria',
96
+ );
97
+ await userEvent.type(screen.getByLabelText('Tag Type'), 'fungi');
98
+ await userEvent.click(screen.getByRole('button'));
99
+
100
+ await waitFor(() => {
101
+ expect(mockDjClient.DataJunctionAPI.addTag).toBeCalled();
102
+ expect(screen.getByTestId('failure')).toHaveTextContent(
103
+ 'alert_fillTag exists',
104
+ );
105
+ });
106
+ }, 60000);
107
+ });