datajunction-ui 0.0.1-rc.22 → 0.0.1-rc.24
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 +1 -1
- package/src/app/components/DeleteNode.jsx +1 -25
- package/src/app/components/__tests__/DeleteNode.test.jsx +53 -0
- package/src/app/index.tsx +2 -2
- package/src/app/pages/AddEditNodePage/__tests__/AddEditNodePageFormSuccess.test.jsx +4 -0
- package/src/app/pages/AddEditNodePage/index.jsx +1 -16
- package/src/app/pages/LoginPage/__tests__/index.test.jsx +0 -5
- package/src/app/pages/NamespacePage/AddNamespacePopover.jsx +85 -0
- package/src/app/pages/NamespacePage/__tests__/index.test.jsx +122 -0
- package/src/app/pages/NamespacePage/index.jsx +3 -2
- package/src/app/pages/NodePage/ClientCodePopover.jsx +15 -1
- package/src/app/pages/NodePage/EditColumnPopover.jsx +15 -1
- package/src/app/pages/NodePage/LinkDimensionPopover.jsx +16 -1
- package/src/app/pages/RegisterTablePage/__tests__/RegisterTablePage.test.jsx +110 -0
- package/src/app/pages/RegisterTablePage/__tests__/__snapshots__/RegisterTablePage.test.jsx.snap +36 -0
- package/src/app/pages/RegisterTablePage/index.jsx +10 -31
- package/src/app/providers/djclient.jsx +2 -2
- package/src/app/services/DJService.js +11 -1
- package/src/styles/index.css +1 -1
package/package.json
CHANGED
|
@@ -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
|
+
});
|
package/src/app/index.tsx
CHANGED
|
@@ -16,7 +16,7 @@ import { LoginPage } from './pages/LoginPage';
|
|
|
16
16
|
import { RegisterTablePage } from './pages/RegisterTablePage';
|
|
17
17
|
import { Root } from './pages/Root/Loadable';
|
|
18
18
|
import DJClientContext from './providers/djclient';
|
|
19
|
-
import { DataJunctionAPI
|
|
19
|
+
import { DataJunctionAPI } from './services/DJService';
|
|
20
20
|
import { CookiesProvider, useCookies } from 'react-cookie';
|
|
21
21
|
import * as Constants from './constants';
|
|
22
22
|
|
|
@@ -36,7 +36,7 @@ export function App() {
|
|
|
36
36
|
content="DataJunction serves as a semantic layer to help manage metrics"
|
|
37
37
|
/>
|
|
38
38
|
</Helmet>
|
|
39
|
-
<DJClientContext.Provider value={{ DataJunctionAPI
|
|
39
|
+
<DJClientContext.Provider value={{ DataJunctionAPI }}>
|
|
40
40
|
<Routes>
|
|
41
41
|
<Route
|
|
42
42
|
path="/"
|
|
@@ -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'), {
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { useContext, useState } from 'react';
|
|
2
|
+
import * as React from 'react';
|
|
3
|
+
import DJClientContext from '../../providers/djclient';
|
|
4
|
+
import { ErrorMessage, Field, Form, Formik } from 'formik';
|
|
5
|
+
import { FormikSelect } from '../AddEditNodePage/FormikSelect';
|
|
6
|
+
import EditIcon from '../../icons/EditIcon';
|
|
7
|
+
import { displayMessageAfterSubmit, labelize } from '../../../utils/form';
|
|
8
|
+
|
|
9
|
+
export default function AddNamespacePopover({ namespace }) {
|
|
10
|
+
const djClient = useContext(DJClientContext).DataJunctionAPI;
|
|
11
|
+
const [popoverAnchor, setPopoverAnchor] = useState(false);
|
|
12
|
+
|
|
13
|
+
const addNamespace = async ({ namespace }, { setSubmitting, setStatus }) => {
|
|
14
|
+
setSubmitting(false);
|
|
15
|
+
const response = await djClient.addNamespace(namespace);
|
|
16
|
+
if (response.status === 200 || response.status === 201) {
|
|
17
|
+
setStatus({ success: 'Saved' });
|
|
18
|
+
} else {
|
|
19
|
+
setStatus({
|
|
20
|
+
failure: `${response.json.message}`,
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
window.location.reload();
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
return (
|
|
27
|
+
<>
|
|
28
|
+
<button
|
|
29
|
+
className="edit_button"
|
|
30
|
+
aria-label="AddNamespaceTogglePopover"
|
|
31
|
+
tabIndex="0"
|
|
32
|
+
onClick={() => {
|
|
33
|
+
setPopoverAnchor(!popoverAnchor);
|
|
34
|
+
}}
|
|
35
|
+
>
|
|
36
|
+
<EditIcon />
|
|
37
|
+
</button>
|
|
38
|
+
<div
|
|
39
|
+
className="popover"
|
|
40
|
+
role="dialog"
|
|
41
|
+
aria-label="AddNamespacePopover"
|
|
42
|
+
style={{
|
|
43
|
+
display: popoverAnchor === false ? 'none' : 'block',
|
|
44
|
+
width: '200px !important',
|
|
45
|
+
textTransform: 'none',
|
|
46
|
+
fontWeight: 'normal',
|
|
47
|
+
}}
|
|
48
|
+
>
|
|
49
|
+
<Formik
|
|
50
|
+
initialValues={{
|
|
51
|
+
namespace: '',
|
|
52
|
+
}}
|
|
53
|
+
onSubmit={addNamespace}
|
|
54
|
+
>
|
|
55
|
+
{function Render({ isSubmitting, status, setFieldValue }) {
|
|
56
|
+
return (
|
|
57
|
+
<Form>
|
|
58
|
+
{displayMessageAfterSubmit(status)}
|
|
59
|
+
<span data-testid="add-namespace">
|
|
60
|
+
<ErrorMessage name="namespace" component="span" />
|
|
61
|
+
<label htmlFor="namespace">Namespace</label>
|
|
62
|
+
<Field
|
|
63
|
+
type="text"
|
|
64
|
+
name="namespace"
|
|
65
|
+
id="namespace"
|
|
66
|
+
placeholder="New namespace"
|
|
67
|
+
/>
|
|
68
|
+
</span>
|
|
69
|
+
<button
|
|
70
|
+
className="add_node"
|
|
71
|
+
type="submit"
|
|
72
|
+
aria-label="SaveNamespace"
|
|
73
|
+
aria-hidden="false"
|
|
74
|
+
style={{ marginTop: '1rem' }}
|
|
75
|
+
>
|
|
76
|
+
Save
|
|
77
|
+
</button>
|
|
78
|
+
</Form>
|
|
79
|
+
);
|
|
80
|
+
}}
|
|
81
|
+
</Formik>
|
|
82
|
+
</div>
|
|
83
|
+
</>
|
|
84
|
+
);
|
|
85
|
+
}
|
|
@@ -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
|
});
|
|
@@ -6,6 +6,7 @@ import DJClientContext from '../../providers/djclient';
|
|
|
6
6
|
import Explorer from '../NamespacePage/Explorer';
|
|
7
7
|
import EditIcon from '../../icons/EditIcon';
|
|
8
8
|
import DeleteNode from '../../components/DeleteNode';
|
|
9
|
+
import AddNamespacePopover from './AddNamespacePopover';
|
|
9
10
|
|
|
10
11
|
export function NamespacePage() {
|
|
11
12
|
const djClient = useContext(DJClientContext).DataJunctionAPI;
|
|
@@ -58,7 +59,7 @@ export function NamespacePage() {
|
|
|
58
59
|
useEffect(() => {
|
|
59
60
|
const fetchData = async () => {
|
|
60
61
|
if (namespace === undefined && namespaceHierarchy !== undefined) {
|
|
61
|
-
namespace = namespaceHierarchy
|
|
62
|
+
namespace = namespaceHierarchy[0].namespace;
|
|
62
63
|
}
|
|
63
64
|
const nodes = await djClient.namespace(namespace);
|
|
64
65
|
const foundNodes = await Promise.all(nodes);
|
|
@@ -159,7 +160,7 @@ export function NamespacePage() {
|
|
|
159
160
|
padding: '1rem 1rem 1rem 0',
|
|
160
161
|
}}
|
|
161
162
|
>
|
|
162
|
-
Namespaces
|
|
163
|
+
Namespaces <AddNamespacePopover />
|
|
163
164
|
</span>
|
|
164
165
|
{namespaceHierarchy
|
|
165
166
|
? namespaceHierarchy.map(child => (
|
|
@@ -1,10 +1,23 @@
|
|
|
1
1
|
import { Light as SyntaxHighlighter } from 'react-syntax-highlighter';
|
|
2
|
-
import { useState } from 'react';
|
|
2
|
+
import { useEffect, useRef, useState } from 'react';
|
|
3
3
|
import { nightOwl } from 'react-syntax-highlighter/src/styles/hljs';
|
|
4
4
|
import PythonIcon from '../../icons/PythonIcon';
|
|
5
5
|
|
|
6
6
|
export default function ClientCodePopover({ code }) {
|
|
7
7
|
const [codeAnchor, setCodeAnchor] = useState(false);
|
|
8
|
+
const ref = useRef(null);
|
|
9
|
+
|
|
10
|
+
useEffect(() => {
|
|
11
|
+
const handleClickOutside = event => {
|
|
12
|
+
if (ref.current && !ref.current.contains(event.target)) {
|
|
13
|
+
setCodeAnchor(false);
|
|
14
|
+
}
|
|
15
|
+
};
|
|
16
|
+
document.addEventListener('click', handleClickOutside, true);
|
|
17
|
+
return () => {
|
|
18
|
+
document.removeEventListener('click', handleClickOutside, true);
|
|
19
|
+
};
|
|
20
|
+
}, [setCodeAnchor]);
|
|
8
21
|
|
|
9
22
|
return (
|
|
10
23
|
<>
|
|
@@ -22,6 +35,7 @@ export default function ClientCodePopover({ code }) {
|
|
|
22
35
|
role="dialog"
|
|
23
36
|
aria-label="client-code"
|
|
24
37
|
style={{ display: codeAnchor === false ? 'none' : 'block' }}
|
|
38
|
+
ref={ref}
|
|
25
39
|
>
|
|
26
40
|
<SyntaxHighlighter language="python" style={nightOwl}>
|
|
27
41
|
{code}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {useContext, useEffect, useRef, useState} from 'react';
|
|
2
2
|
import * as React from 'react';
|
|
3
3
|
import DJClientContext from '../../providers/djclient';
|
|
4
4
|
import { Form, Formik } from 'formik';
|
|
@@ -9,6 +9,19 @@ import { displayMessageAfterSubmit, labelize } from '../../../utils/form';
|
|
|
9
9
|
export default function EditColumnPopover({ column, node, options, onSubmit }) {
|
|
10
10
|
const djClient = useContext(DJClientContext).DataJunctionAPI;
|
|
11
11
|
const [popoverAnchor, setPopoverAnchor] = useState(false);
|
|
12
|
+
const ref = useRef(null);
|
|
13
|
+
|
|
14
|
+
useEffect(() => {
|
|
15
|
+
const handleClickOutside = event => {
|
|
16
|
+
if (ref.current && !ref.current.contains(event.target)) {
|
|
17
|
+
setPopoverAnchor(false);
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
document.addEventListener('click', handleClickOutside, true);
|
|
21
|
+
return () => {
|
|
22
|
+
document.removeEventListener('click', handleClickOutside, true);
|
|
23
|
+
};
|
|
24
|
+
}, [setPopoverAnchor]);
|
|
12
25
|
|
|
13
26
|
const saveAttributes = async (
|
|
14
27
|
{ node, column, attributes },
|
|
@@ -44,6 +57,7 @@ export default function EditColumnPopover({ column, node, options, onSubmit }) {
|
|
|
44
57
|
role="dialog"
|
|
45
58
|
aria-label="client-code"
|
|
46
59
|
style={{ display: popoverAnchor === false ? 'none' : 'block' }}
|
|
60
|
+
ref={ref}
|
|
47
61
|
>
|
|
48
62
|
<Formik
|
|
49
63
|
initialValues={{
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useContext, useState } from 'react';
|
|
1
|
+
import { useContext, useEffect, useRef, useState } from 'react';
|
|
2
2
|
import * as React from 'react';
|
|
3
3
|
import DJClientContext from '../../providers/djclient';
|
|
4
4
|
import { Form, Formik } from 'formik';
|
|
@@ -14,6 +14,20 @@ export default function LinkDimensionPopover({
|
|
|
14
14
|
}) {
|
|
15
15
|
const djClient = useContext(DJClientContext).DataJunctionAPI;
|
|
16
16
|
const [popoverAnchor, setPopoverAnchor] = useState(false);
|
|
17
|
+
const ref = useRef(null);
|
|
18
|
+
|
|
19
|
+
useEffect(() => {
|
|
20
|
+
const handleClickOutside = event => {
|
|
21
|
+
if (ref.current && !ref.current.contains(event.target)) {
|
|
22
|
+
setPopoverAnchor(false);
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
document.addEventListener('click', handleClickOutside, true);
|
|
26
|
+
return () => {
|
|
27
|
+
document.removeEventListener('click', handleClickOutside, true);
|
|
28
|
+
};
|
|
29
|
+
}, [setPopoverAnchor]);
|
|
30
|
+
|
|
17
31
|
const columnDimension = column.dimension;
|
|
18
32
|
|
|
19
33
|
const handleSubmit = async (
|
|
@@ -73,6 +87,7 @@ export default function LinkDimensionPopover({
|
|
|
73
87
|
role="dialog"
|
|
74
88
|
aria-label="client-code"
|
|
75
89
|
style={{ display: popoverAnchor === false ? 'none' : 'block' }}
|
|
90
|
+
ref={ref}
|
|
76
91
|
>
|
|
77
92
|
<Formik
|
|
78
93
|
initialValues={{
|
|
@@ -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
|
+
});
|
package/src/app/pages/RegisterTablePage/__tests__/__snapshots__/RegisterTablePage.test.jsx.snap
ADDED
|
@@ -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
|
|
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
|
-
<
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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" />
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { DataJunctionAPI
|
|
1
|
+
import { DataJunctionAPI } from '../services/DJService';
|
|
2
2
|
import * as React from 'react';
|
|
3
3
|
|
|
4
|
-
const DJClientContext = React.createContext({ DataJunctionAPI
|
|
4
|
+
const DJClientContext = React.createContext({ DataJunctionAPI });
|
|
5
5
|
export default DJClientContext;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { MarkerType } from 'reactflow';
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
const DJ_URL = process.env.REACT_APP_DJ_URL
|
|
4
4
|
? process.env.REACT_APP_DJ_URL
|
|
5
5
|
: 'http://localhost:8000';
|
|
6
6
|
|
|
@@ -487,4 +487,14 @@ export const DataJunctionAPI = {
|
|
|
487
487
|
});
|
|
488
488
|
return { status: response.status, json: await response.json() };
|
|
489
489
|
},
|
|
490
|
+
addNamespace: async function (namespace) {
|
|
491
|
+
const response = await fetch(`${DJ_URL}/namespaces/${namespace}`, {
|
|
492
|
+
method: 'POST',
|
|
493
|
+
headers: {
|
|
494
|
+
'Content-Type': 'application/json',
|
|
495
|
+
},
|
|
496
|
+
credentials: 'include',
|
|
497
|
+
});
|
|
498
|
+
return { status: response.status, json: await response.json() };
|
|
499
|
+
},
|
|
490
500
|
};
|