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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "datajunction-ui",
3
- "version": "0.0.1-rc.21",
3
+ "version": "0.0.1-rc.23",
4
4
  "description": "DataJunction Metrics Platform UI",
5
5
  "module": "src/index.tsx",
6
6
  "repository": {
@@ -1,14 +1,11 @@
1
1
  import DJClientContext from '../providers/djclient';
2
- import ValidIcon from '../icons/ValidIcon';
3
- import AlertIcon from '../icons/AlertIcon';
4
2
  import * as React from 'react';
5
3
  import DeleteIcon from '../icons/DeleteIcon';
6
4
  import { Form, Formik } from 'formik';
7
5
  import { useContext } from 'react';
6
+ import { displayMessageAfterSubmit } from '../../utils/form';
8
7
 
9
8
  export default function DeleteNode({ nodeName }) {
10
- console.log('nodeName', nodeName);
11
-
12
9
  const djClient = useContext(DJClientContext).DataJunctionAPI;
13
10
  const deleteNode = async (values, { setSubmitting, setStatus }) => {
14
11
  const { status, json } = await djClient.deactivate(values.nodeName);
@@ -24,27 +21,6 @@ export default function DeleteNode({ nodeName }) {
24
21
  setSubmitting(false);
25
22
  };
26
23
 
27
- const displayMessageAfterSubmit = status => {
28
- return status?.success !== undefined ? (
29
- <div className="message success">
30
- <ValidIcon />
31
- {status?.success}
32
- </div>
33
- ) : status?.failure !== undefined ? (
34
- alertMessage(status?.failure)
35
- ) : (
36
- ''
37
- );
38
- };
39
-
40
- const alertMessage = message => {
41
- return (
42
- <div className="message alert">
43
- <AlertIcon />
44
- {message}
45
- </div>
46
- );
47
- };
48
24
  const initialValues = {
49
25
  nodeName: nodeName,
50
26
  };
@@ -0,0 +1,53 @@
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 DJClientContext from '../../providers/djclient';
7
+ import DeleteNode from '../DeleteNode';
8
+
9
+ describe('<DeleteNode />', () => {
10
+ beforeEach(() => {
11
+ fetchMock.resetMocks();
12
+ jest.clearAllMocks();
13
+ window.scrollTo = jest.fn();
14
+ });
15
+
16
+ const renderElement = djClient => {
17
+ return render(
18
+ <DJClientContext.Provider value={djClient}>
19
+ <DeleteNode nodeName="default.hard_hat" />
20
+ </DJClientContext.Provider>,
21
+ );
22
+ };
23
+
24
+ const initializeMockDJClient = () => {
25
+ return {
26
+ DataJunctionAPI: {
27
+ deactivate: jest.fn(),
28
+ },
29
+ };
30
+ };
31
+
32
+ it('deletes a node when clicked', async () => {
33
+ const mockDjClient = initializeMockDJClient();
34
+ mockDjClient.DataJunctionAPI.deactivate.mockReturnValue({
35
+ status: 204,
36
+ json: { name: 'source.warehouse.schema.some_table' },
37
+ });
38
+
39
+ renderElement(mockDjClient);
40
+
41
+ await userEvent.click(screen.getByRole('button'));
42
+
43
+ await waitFor(() => {
44
+ expect(mockDjClient.DataJunctionAPI.deactivate).toBeCalled();
45
+ expect(mockDjClient.DataJunctionAPI.deactivate).toBeCalledWith(
46
+ 'default.hard_hat',
47
+ );
48
+ });
49
+ expect(
50
+ screen.getByText('Successfully deleted node default.hard_hat'),
51
+ ).toBeInTheDocument();
52
+ }, 60000);
53
+ });
@@ -9,6 +9,10 @@ import {
9
9
  testElement,
10
10
  } from './index.test';
11
11
  import { mocks } from '../../../../mocks/mockNodes';
12
+ import { render } from '../../../../setupTests';
13
+ import { MemoryRouter, Route, Routes } from 'react-router-dom';
14
+ import DJClientContext from '../../../providers/djclient';
15
+ import { AddEditNodePage } from '../index';
12
16
 
13
17
  describe('AddEditNodePage submission succeeded', () => {
14
18
  beforeEach(() => {
@@ -15,6 +15,7 @@ import { useParams } from 'react-router-dom';
15
15
  import { FullNameField } from './FullNameField';
16
16
  import { FormikSelect } from './FormikSelect';
17
17
  import { NodeQueryField } from './NodeQueryField';
18
+ import { displayMessageAfterSubmit } from '../../../utils/form';
18
19
 
19
20
  class Action {
20
21
  static Add = new Action('add');
@@ -70,22 +71,6 @@ export function AddEditNodePage() {
70
71
  window.scrollTo({ top: 0, left: 0, behavior: 'smooth' });
71
72
  };
72
73
 
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
74
  const pageTitle =
90
75
  action === Action.Add ? (
91
76
  <h2>
@@ -5,10 +5,6 @@ import { LoginPage } from '../index';
5
5
  describe('LoginPage', () => {
6
6
  const original = window.location;
7
7
 
8
- const reloadFn = () => {
9
- window.location.reload();
10
- };
11
-
12
8
  beforeAll(() => {
13
9
  Object.defineProperty(window, 'location', {
14
10
  configurable: true,
@@ -44,7 +40,6 @@ describe('LoginPage', () => {
44
40
  it('calls fetch with correct data on submit', async () => {
45
41
  const username = 'testUser';
46
42
  const password = 'testPassword';
47
- reloadFn();
48
43
 
49
44
  const { getByText, getByPlaceholderText } = render(<LoginPage />);
50
45
  fireEvent.change(getByPlaceholderText('Username'), {
@@ -6,7 +6,7 @@ import { FormikSelect } from '../AddEditNodePage/FormikSelect';
6
6
  import EditIcon from '../../icons/EditIcon';
7
7
  import { displayMessageAfterSubmit, labelize } from '../../../utils/form';
8
8
 
9
- export default function AddNamespacePopover({ namespace, onSubmit }) {
9
+ export default function AddNamespacePopover({ namespace }) {
10
10
  const djClient = useContext(DJClientContext).DataJunctionAPI;
11
11
  const [popoverAnchor, setPopoverAnchor] = useState(false);
12
12
 
@@ -20,7 +20,6 @@ export default function AddNamespacePopover({ namespace, onSubmit }) {
20
20
  failure: `${response.json.message}`,
21
21
  });
22
22
  }
23
- onSubmit();
24
23
  window.location.reload();
25
24
  };
26
25
 
@@ -3,14 +3,37 @@ import { MemoryRouter, Route, Routes } from 'react-router-dom';
3
3
  import DJClientContext from '../../../providers/djclient';
4
4
  import { NamespacePage } from '../index';
5
5
  import React from 'react';
6
+ import userEvent from '@testing-library/user-event';
6
7
 
7
8
  const mockDjClient = {
8
9
  namespaces: jest.fn(),
9
10
  namespace: jest.fn(),
11
+ addNamespace: jest.fn(),
10
12
  };
11
13
 
12
14
  describe('NamespacePage', () => {
15
+ const original = window.location;
16
+
17
+ const reloadFn = () => {
18
+ window.location.reload();
19
+ };
20
+
21
+ beforeAll(() => {
22
+ Object.defineProperty(window, 'location', {
23
+ configurable: true,
24
+ value: { reload: jest.fn() },
25
+ });
26
+ });
27
+
28
+ afterAll(() => {
29
+ Object.defineProperty(window, 'location', {
30
+ configurable: true,
31
+ value: original,
32
+ });
33
+ });
34
+
13
35
  beforeEach(() => {
36
+ fetch.resetMocks();
14
37
  mockDjClient.namespaces.mockResolvedValue([
15
38
  {
16
39
  namespace: 'common.one',
@@ -56,7 +79,12 @@ describe('NamespacePage', () => {
56
79
  ]);
57
80
  });
58
81
 
82
+ afterEach(() => {
83
+ jest.clearAllMocks();
84
+ });
85
+
59
86
  it('displays namespaces and renders nodes', async () => {
87
+ reloadFn();
60
88
  const element = (
61
89
  <DJClientContext.Provider value={{ DataJunctionAPI: mockDjClient }}>
62
90
  <NamespacePage />
@@ -92,4 +120,98 @@ describe('NamespacePage', () => {
92
120
  afterEach(() => {
93
121
  jest.clearAllMocks();
94
122
  });
123
+
124
+ it('can add new namespace via add namespace popover', async () => {
125
+ mockDjClient.addNamespace.mockReturnValue({
126
+ status: 201,
127
+ json: {},
128
+ });
129
+ const element = (
130
+ <DJClientContext.Provider value={{ DataJunctionAPI: mockDjClient }}>
131
+ <NamespacePage />
132
+ </DJClientContext.Provider>
133
+ );
134
+ render(
135
+ <MemoryRouter initialEntries={['/namespaces/test.namespace']}>
136
+ <Routes>
137
+ <Route path="namespaces/:namespace" element={element} />
138
+ </Routes>
139
+ </MemoryRouter>,
140
+ );
141
+
142
+ // Find the button to toggle the add namespace popover
143
+ const addNamespaceToggle = screen.getByRole('button', {
144
+ name: 'AddNamespaceTogglePopover',
145
+ });
146
+ expect(addNamespaceToggle).toBeInTheDocument();
147
+
148
+ // Click the toggle and verify that the popover displays
149
+ fireEvent.click(addNamespaceToggle);
150
+ const addNamespacePopover = screen.getByRole('dialog', {
151
+ name: 'AddNamespacePopover',
152
+ });
153
+ expect(addNamespacePopover).toBeInTheDocument();
154
+
155
+ // Type in the new namespace
156
+ await userEvent.type(
157
+ screen.getByLabelText('Namespace'),
158
+ 'some.random.namespace',
159
+ );
160
+
161
+ // Save
162
+ const saveNamespace = screen.getByRole('button', {
163
+ name: 'SaveNamespace',
164
+ });
165
+ await waitFor(() => {
166
+ fireEvent.click(saveNamespace);
167
+ });
168
+ expect(mockDjClient.addNamespace).toHaveBeenCalled();
169
+ expect(mockDjClient.addNamespace).toHaveBeenCalledWith(
170
+ 'some.random.namespace',
171
+ );
172
+ expect(screen.getByText('Saved')).toBeInTheDocument();
173
+ expect(window.location.reload).toHaveBeenCalled();
174
+ });
175
+
176
+ it('can fail to add namespace', async () => {
177
+ mockDjClient.addNamespace.mockReturnValue({
178
+ status: 500,
179
+ json: { message: 'you failed' },
180
+ });
181
+ const element = (
182
+ <DJClientContext.Provider value={{ DataJunctionAPI: mockDjClient }}>
183
+ <NamespacePage />
184
+ </DJClientContext.Provider>
185
+ );
186
+ render(
187
+ <MemoryRouter initialEntries={['/namespaces/test.namespace']}>
188
+ <Routes>
189
+ <Route path="namespaces/:namespace" element={element} />
190
+ </Routes>
191
+ </MemoryRouter>,
192
+ );
193
+
194
+ // Open the add namespace popover
195
+ const addNamespaceToggle = screen.getByRole('button', {
196
+ name: 'AddNamespaceTogglePopover',
197
+ });
198
+ fireEvent.click(addNamespaceToggle);
199
+
200
+ // Type in the new namespace
201
+ await userEvent.type(
202
+ screen.getByLabelText('Namespace'),
203
+ 'some.random.namespace',
204
+ );
205
+
206
+ // Save
207
+ const saveNamespace = screen.getByRole('button', {
208
+ name: 'SaveNamespace',
209
+ });
210
+ await waitFor(() => {
211
+ fireEvent.click(saveNamespace);
212
+ });
213
+
214
+ // Should display failure alert
215
+ expect(screen.getByText('you failed')).toBeInTheDocument();
216
+ });
95
217
  });
@@ -0,0 +1,110 @@
1
+ import React from 'react';
2
+ import { fireEvent, 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 { RegisterTablePage } from '../index';
9
+
10
+ describe('<RegisterTablePage />', () => {
11
+ const initializeMockDJClient = () => {
12
+ return {
13
+ DataJunctionAPI: {
14
+ catalogs: jest.fn(),
15
+ registerTable: jest.fn(),
16
+ },
17
+ };
18
+ };
19
+
20
+ const mockDjClient = initializeMockDJClient();
21
+
22
+ beforeEach(() => {
23
+ fetchMock.resetMocks();
24
+ jest.clearAllMocks();
25
+ window.scrollTo = jest.fn();
26
+
27
+ mockDjClient.DataJunctionAPI.catalogs.mockReturnValue([
28
+ {
29
+ name: 'warehouse',
30
+ engines: [
31
+ {
32
+ name: 'duckdb',
33
+ version: '0.7.1',
34
+ uri: null,
35
+ dialect: null,
36
+ },
37
+ ],
38
+ },
39
+ ]);
40
+ });
41
+
42
+ const renderRegisterTable = element => {
43
+ return render(
44
+ <MemoryRouter initialEntries={['/create/source']}>
45
+ <Routes>
46
+ <Route path="create/source" element={element} />
47
+ </Routes>
48
+ </MemoryRouter>,
49
+ );
50
+ };
51
+
52
+ const testElement = djClient => {
53
+ return (
54
+ <DJClientContext.Provider value={djClient}>
55
+ <RegisterTablePage />
56
+ </DJClientContext.Provider>
57
+ );
58
+ };
59
+
60
+ it('registers a table correctly', async () => {
61
+ mockDjClient.DataJunctionAPI.registerTable.mockReturnValue({
62
+ status: 201,
63
+ json: { name: 'source.warehouse.schema.some_table' },
64
+ });
65
+
66
+ const element = testElement(mockDjClient);
67
+ const { container, getByTestId } = renderRegisterTable(element);
68
+
69
+ const catalog = getByTestId('choose-catalog');
70
+ await waitFor(async () => {
71
+ fireEvent.keyDown(catalog.firstChild, { key: 'ArrowDown' });
72
+ fireEvent.click(screen.getByText('warehouse'));
73
+ });
74
+
75
+ await userEvent.type(screen.getByLabelText('Schema'), 'schema');
76
+ await userEvent.type(screen.getByLabelText('Table'), 'some_table');
77
+ await userEvent.click(screen.getByRole('button'));
78
+
79
+ await waitFor(() => {
80
+ expect(mockDjClient.DataJunctionAPI.registerTable).toBeCalled();
81
+ expect(mockDjClient.DataJunctionAPI.registerTable).toBeCalledWith(
82
+ 'warehouse',
83
+ 'schema',
84
+ 'some_table',
85
+ );
86
+ });
87
+ expect(container.getElementsByClassName('message')).toMatchSnapshot();
88
+ }, 60000);
89
+
90
+ it('fails to register a table', async () => {
91
+ mockDjClient.DataJunctionAPI.registerTable.mockReturnValue({
92
+ status: 500,
93
+ json: { message: 'table not found' },
94
+ });
95
+
96
+ const element = testElement(mockDjClient);
97
+ const { getByTestId } = renderRegisterTable(element);
98
+
99
+ const catalog = getByTestId('choose-catalog');
100
+ await waitFor(async () => {
101
+ fireEvent.keyDown(catalog.firstChild, { key: 'ArrowDown' });
102
+ fireEvent.click(screen.getByText('warehouse'));
103
+ });
104
+
105
+ await userEvent.type(screen.getByLabelText('Schema'), 'schema');
106
+ await userEvent.type(screen.getByLabelText('Table'), 'some_table');
107
+ await userEvent.click(screen.getByRole('button'));
108
+ expect(screen.getByText('table not found')).toBeInTheDocument();
109
+ }, 60000);
110
+ });
@@ -0,0 +1,36 @@
1
+ // Jest Snapshot v1, https://goo.gl/fbAQLP
2
+
3
+ exports[`<RegisterTablePage /> registers a table correctly 1`] = `
4
+ HTMLCollection [
5
+ <div
6
+ class="message success"
7
+ >
8
+ <svg
9
+ class="bi bi-check-circle-fill"
10
+ fill="currentColor"
11
+ height="25"
12
+ viewBox="0 0 16 16"
13
+ width="25"
14
+ xmlns="http://www.w3.org/2000/svg"
15
+ >
16
+ <path
17
+ 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"
18
+ />
19
+ </svg>
20
+ Successfully registered source node
21
+
22
+ <a
23
+ href="/nodes/source.warehouse.schema.some_table"
24
+ >
25
+ source.warehouse.schema.some_table
26
+ </a>
27
+ , which references table
28
+ warehouse
29
+ .
30
+ schema
31
+ .
32
+ some_table
33
+ .
34
+ </div>,
35
+ ]
36
+ `;
@@ -9,9 +9,8 @@ import NamespaceHeader from '../../components/NamespaceHeader';
9
9
  import React, { useContext, useEffect, useState } from 'react';
10
10
  import DJClientContext from '../../providers/djclient';
11
11
  import 'styles/node-creation.scss';
12
- import AlertIcon from '../../icons/AlertIcon';
13
- import ValidIcon from '../../icons/ValidIcon';
14
12
  import { FormikSelect } from '../AddEditNodePage/FormikSelect';
13
+ import { displayMessageAfterSubmit } from '../../../utils/form';
15
14
 
16
15
  export function RegisterTablePage() {
17
16
  const djClient = useContext(DJClientContext).DataJunctionAPI;
@@ -71,28 +70,6 @@ export function RegisterTablePage() {
71
70
  window.scrollTo({ top: 0, left: 0, behavior: 'smooth' });
72
71
  };
73
72
 
74
- const displayMessageAfterSubmit = status => {
75
- return status?.success !== undefined ? (
76
- <div className="message success">
77
- <ValidIcon />
78
- {status?.success}
79
- </div>
80
- ) : status?.failure !== undefined ? (
81
- alertMessage(status?.failure)
82
- ) : (
83
- ''
84
- );
85
- };
86
-
87
- const alertMessage = message => {
88
- return (
89
- <div className="message alert">
90
- <AlertIcon />
91
- {message}
92
- </div>
93
- );
94
- };
95
-
96
73
  return (
97
74
  <div className="mid">
98
75
  <NamespaceHeader namespace="" />
@@ -110,7 +87,7 @@ export function RegisterTablePage() {
110
87
  validate={validator}
111
88
  onSubmit={handleSubmit}
112
89
  >
113
- {function Render({ isSubmitting, status, setFieldValue }) {
90
+ {function Render({ isSubmitting, status }) {
114
91
  return (
115
92
  <Form>
116
93
  {displayMessageAfterSubmit(status)}
@@ -119,12 +96,14 @@ export function RegisterTablePage() {
119
96
  <div className="SourceCreationInput">
120
97
  <ErrorMessage name="catalog" component="span" />
121
98
  <label htmlFor="catalog">Catalog</label>
122
- <FormikSelect
123
- selectOptions={catalogs}
124
- formikFieldName="catalog"
125
- placeholder="Choose Catalog"
126
- defaultValue={catalogs[0]}
127
- />
99
+ <span data-testid="choose-catalog">
100
+ <FormikSelect
101
+ selectOptions={catalogs}
102
+ formikFieldName="catalog"
103
+ placeholder="Choose Catalog"
104
+ defaultValue={catalogs[0]}
105
+ />
106
+ </span>
128
107
  </div>
129
108
  <div className="SourceCreationInput">
130
109
  <ErrorMessage name="schema" component="span" />