datajunction-ui 0.0.1-a1
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/.babel-plugin-macrosrc.js +5 -0
- package/.env +3 -0
- package/.eslintrc.js +20 -0
- package/.gitattributes +201 -0
- package/.husky/pre-commit +6 -0
- package/.nvmrc +1 -0
- package/.prettierignore +6 -0
- package/.prettierrc +9 -0
- package/.stylelintrc +7 -0
- package/LICENSE +22 -0
- package/Makefile +3 -0
- package/README.md +10 -0
- package/dj-logo.svg +10 -0
- package/internals/testing/loadable.mock.tsx +6 -0
- package/package.json +189 -0
- package/public/favicon.ico +0 -0
- package/public/index.html +26 -0
- package/public/manifest.json +15 -0
- package/public/robots.txt +3 -0
- package/src/__tests__/reportWebVitals.test.jsx +44 -0
- package/src/app/__tests__/__snapshots__/index.test.tsx.snap +9 -0
- package/src/app/__tests__/index.test.tsx +14 -0
- package/src/app/components/DeleteNode.jsx +55 -0
- package/src/app/components/ListGroupItem.jsx +24 -0
- package/src/app/components/NamespaceHeader.jsx +31 -0
- package/src/app/components/QueryInfo.jsx +77 -0
- package/src/app/components/Tab.jsx +25 -0
- package/src/app/components/ToggleSwitch.jsx +20 -0
- package/src/app/components/__tests__/DeleteNode.test.jsx +53 -0
- package/src/app/components/__tests__/ListGroupItem.test.tsx +16 -0
- package/src/app/components/__tests__/NamespaceHeader.test.jsx +14 -0
- package/src/app/components/__tests__/QueryInfo.test.jsx +55 -0
- package/src/app/components/__tests__/Tab.test.jsx +27 -0
- package/src/app/components/__tests__/ToggleSwitch.test.jsx +43 -0
- package/src/app/components/__tests__/__snapshots__/ListGroupItem.test.tsx.snap +29 -0
- package/src/app/components/__tests__/__snapshots__/NamespaceHeader.test.jsx.snap +47 -0
- package/src/app/components/djgraph/Collapse.jsx +46 -0
- package/src/app/components/djgraph/DJNode.jsx +89 -0
- package/src/app/components/djgraph/DJNodeColumns.jsx +71 -0
- package/src/app/components/djgraph/DJNodeDimensions.jsx +75 -0
- package/src/app/components/djgraph/LayoutFlow.jsx +104 -0
- package/src/app/components/djgraph/__tests__/Collapse.test.jsx +51 -0
- package/src/app/components/djgraph/__tests__/DJNode.test.tsx +24 -0
- package/src/app/components/djgraph/__tests__/DJNodeColumns.test.jsx +83 -0
- package/src/app/components/djgraph/__tests__/DJNodeDimensions.test.jsx +118 -0
- package/src/app/components/djgraph/__tests__/__snapshots__/DJNode.test.tsx.snap +117 -0
- package/src/app/constants.js +2 -0
- package/src/app/icons/AlertIcon.jsx +32 -0
- package/src/app/icons/CollapsedIcon.jsx +15 -0
- package/src/app/icons/DJLogo.jsx +36 -0
- package/src/app/icons/DeleteIcon.jsx +21 -0
- package/src/app/icons/EditIcon.jsx +18 -0
- package/src/app/icons/ExpandedIcon.jsx +15 -0
- package/src/app/icons/HorizontalHierarchyIcon.jsx +15 -0
- package/src/app/icons/InvalidIcon.jsx +14 -0
- package/src/app/icons/LoadingIcon.jsx +14 -0
- package/src/app/icons/PythonIcon.jsx +52 -0
- package/src/app/icons/TableIcon.jsx +14 -0
- package/src/app/icons/ValidIcon.jsx +14 -0
- package/src/app/index.tsx +108 -0
- package/src/app/pages/AddEditNodePage/FormikSelect.jsx +46 -0
- package/src/app/pages/AddEditNodePage/FullNameField.jsx +37 -0
- package/src/app/pages/AddEditNodePage/Loadable.jsx +16 -0
- package/src/app/pages/AddEditNodePage/NodeQueryField.jsx +89 -0
- package/src/app/pages/AddEditNodePage/__tests__/AddEditNodePageFormFailed.test.jsx +103 -0
- package/src/app/pages/AddEditNodePage/__tests__/AddEditNodePageFormSuccess.test.jsx +132 -0
- package/src/app/pages/AddEditNodePage/__tests__/FormikSelect.test.jsx +75 -0
- package/src/app/pages/AddEditNodePage/__tests__/FullNameField.test.jsx +31 -0
- package/src/app/pages/AddEditNodePage/__tests__/NodeQueryField.test.jsx +30 -0
- package/src/app/pages/AddEditNodePage/__tests__/__snapshots__/AddEditNodePageFormFailed.test.jsx.snap +54 -0
- package/src/app/pages/AddEditNodePage/__tests__/__snapshots__/AddEditNodePageFormSuccess.test.jsx.snap +3 -0
- package/src/app/pages/AddEditNodePage/__tests__/__snapshots__/index.test.jsx.snap +3 -0
- package/src/app/pages/AddEditNodePage/__tests__/index.test.jsx +180 -0
- package/src/app/pages/AddEditNodePage/index.jsx +396 -0
- package/src/app/pages/AddEditTagPage/Loadable.jsx +16 -0
- package/src/app/pages/AddEditTagPage/__tests__/AddEditTagPage.test.jsx +107 -0
- package/src/app/pages/AddEditTagPage/index.jsx +132 -0
- package/src/app/pages/LoginPage/LoginForm.jsx +124 -0
- package/src/app/pages/LoginPage/SignupForm.jsx +156 -0
- package/src/app/pages/LoginPage/__tests__/index.test.jsx +97 -0
- package/src/app/pages/LoginPage/assets/sign-in-with-github.png +0 -0
- package/src/app/pages/LoginPage/assets/sign-in-with-google.png +0 -0
- package/src/app/pages/LoginPage/index.jsx +17 -0
- package/src/app/pages/NamespacePage/AddNamespacePopover.jsx +85 -0
- package/src/app/pages/NamespacePage/Explorer.jsx +57 -0
- package/src/app/pages/NamespacePage/Loadable.jsx +16 -0
- package/src/app/pages/NamespacePage/__tests__/index.test.jsx +217 -0
- package/src/app/pages/NamespacePage/index.jsx +199 -0
- package/src/app/pages/NodePage/AddBackfillPopover.jsx +166 -0
- package/src/app/pages/NodePage/AddMaterializationPopover.jsx +161 -0
- package/src/app/pages/NodePage/ClientCodePopover.jsx +46 -0
- package/src/app/pages/NodePage/EditColumnPopover.jsx +116 -0
- package/src/app/pages/NodePage/LinkDimensionPopover.jsx +149 -0
- package/src/app/pages/NodePage/Loadable.jsx +16 -0
- package/src/app/pages/NodePage/NodeColumnTab.jsx +200 -0
- package/src/app/pages/NodePage/NodeGraphTab.jsx +112 -0
- package/src/app/pages/NodePage/NodeHistory.jsx +212 -0
- package/src/app/pages/NodePage/NodeInfoTab.jsx +212 -0
- package/src/app/pages/NodePage/NodeLineageTab.jsx +84 -0
- package/src/app/pages/NodePage/NodeMaterializationTab.jsx +233 -0
- package/src/app/pages/NodePage/NodeSQLTab.jsx +82 -0
- package/src/app/pages/NodePage/NodeStatus.jsx +28 -0
- package/src/app/pages/NodePage/NodesWithDimension.jsx +42 -0
- package/src/app/pages/NodePage/PartitionColumnPopover.jsx +153 -0
- package/src/app/pages/NodePage/__tests__/AddBackfillPopover.test.jsx +47 -0
- package/src/app/pages/NodePage/__tests__/ClientCodePopover.test.jsx +49 -0
- package/src/app/pages/NodePage/__tests__/EditColumnPopover.test.jsx +148 -0
- package/src/app/pages/NodePage/__tests__/LinkDimensionPopover.test.jsx +165 -0
- package/src/app/pages/NodePage/__tests__/NodeGraphTab.test.jsx +591 -0
- package/src/app/pages/NodePage/__tests__/NodeLineageTab.test.jsx +57 -0
- package/src/app/pages/NodePage/__tests__/NodePage.test.jsx +757 -0
- package/src/app/pages/NodePage/__tests__/NodeWithDimension.test.jsx +175 -0
- package/src/app/pages/NodePage/__tests__/__snapshots__/NodePage.test.jsx.snap +403 -0
- package/src/app/pages/NodePage/index.jsx +210 -0
- package/src/app/pages/NotFoundPage/Loadable.tsx +14 -0
- package/src/app/pages/NotFoundPage/__tests__/index.test.jsx +16 -0
- package/src/app/pages/NotFoundPage/index.tsx +23 -0
- package/src/app/pages/RegisterTablePage/Loadable.jsx +16 -0
- package/src/app/pages/RegisterTablePage/__tests__/RegisterTablePage.test.jsx +110 -0
- package/src/app/pages/RegisterTablePage/__tests__/__snapshots__/RegisterTablePage.test.jsx.snap +37 -0
- package/src/app/pages/RegisterTablePage/index.jsx +142 -0
- package/src/app/pages/Root/Loadable.tsx +14 -0
- package/src/app/pages/Root/__tests__/index.test.jsx +77 -0
- package/src/app/pages/Root/assets/dj-logo.png +0 -0
- package/src/app/pages/Root/index.tsx +70 -0
- package/src/app/pages/SQLBuilderPage/Loadable.jsx +16 -0
- package/src/app/pages/SQLBuilderPage/__tests__/index.test.jsx +173 -0
- package/src/app/pages/SQLBuilderPage/index.jsx +390 -0
- package/src/app/pages/TagPage/Loadable.jsx +16 -0
- package/src/app/pages/TagPage/__tests__/TagPage.test.jsx +70 -0
- package/src/app/pages/TagPage/index.jsx +79 -0
- package/src/app/providers/djclient.jsx +5 -0
- package/src/app/services/DJService.js +665 -0
- package/src/app/services/__tests__/DJService.test.jsx +804 -0
- package/src/index.tsx +48 -0
- package/src/mocks/mockNodes.jsx +1430 -0
- package/src/react-app-env.d.ts +4 -0
- package/src/reportWebVitals.ts +15 -0
- package/src/setupTests.ts +36 -0
- package/src/styles/dag.css +228 -0
- package/src/styles/index.css +1083 -0
- package/src/styles/loading.css +34 -0
- package/src/styles/login.css +81 -0
- package/src/styles/node-creation.scss +197 -0
- package/src/styles/styles.scss +44 -0
- package/src/styles/styles.scss.d.ts +9 -0
- package/src/utils/__tests__/__snapshots__/loadable.test.tsx.snap +17 -0
- package/src/utils/__tests__/loadable.test.tsx +53 -0
- package/src/utils/__tests__/request.test.ts +82 -0
- package/src/utils/form.jsx +23 -0
- package/src/utils/loadable.tsx +30 -0
- package/src/utils/request.ts +54 -0
- package/tsconfig.json +34 -0
- package/webpack.config.js +118 -0
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { useParams } from 'react-router-dom';
|
|
3
|
+
import { useContext, useEffect, useState } from 'react';
|
|
4
|
+
import Tab from '../../components/Tab';
|
|
5
|
+
import NamespaceHeader from '../../components/NamespaceHeader';
|
|
6
|
+
import NodeInfoTab from './NodeInfoTab';
|
|
7
|
+
import NodeColumnTab from './NodeColumnTab';
|
|
8
|
+
import NodeLineage from './NodeGraphTab';
|
|
9
|
+
import NodeHistory from './NodeHistory';
|
|
10
|
+
import DJClientContext from '../../providers/djclient';
|
|
11
|
+
import NodeSQLTab from './NodeSQLTab';
|
|
12
|
+
import NodeMaterializationTab from './NodeMaterializationTab';
|
|
13
|
+
import ClientCodePopover from './ClientCodePopover';
|
|
14
|
+
import NodesWithDimension from './NodesWithDimension';
|
|
15
|
+
import NodeColumnLineage from './NodeLineageTab';
|
|
16
|
+
import EditIcon from '../../icons/EditIcon';
|
|
17
|
+
import AlertIcon from '../../icons/AlertIcon';
|
|
18
|
+
|
|
19
|
+
export function NodePage() {
|
|
20
|
+
const djClient = useContext(DJClientContext).DataJunctionAPI;
|
|
21
|
+
const [state, setState] = useState({
|
|
22
|
+
selectedTab: 0,
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
const [node, setNode] = useState();
|
|
26
|
+
|
|
27
|
+
const onClickTab = id => () => {
|
|
28
|
+
setState({ selectedTab: id });
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const buildTabs = tab => {
|
|
32
|
+
return tab.display ? (
|
|
33
|
+
<Tab
|
|
34
|
+
key={tab.id}
|
|
35
|
+
id={tab.id}
|
|
36
|
+
name={tab.name}
|
|
37
|
+
onClick={onClickTab(tab.id)}
|
|
38
|
+
selectedTab={state.selectedTab}
|
|
39
|
+
/>
|
|
40
|
+
) : null;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const { name } = useParams();
|
|
44
|
+
|
|
45
|
+
useEffect(() => {
|
|
46
|
+
const fetchData = async () => {
|
|
47
|
+
const data = await djClient.node(name);
|
|
48
|
+
data.createNodeClientCode = await djClient.clientCode(name);
|
|
49
|
+
setNode(data);
|
|
50
|
+
if (data.type === 'metric') {
|
|
51
|
+
const metric = await djClient.metric(name);
|
|
52
|
+
data.dimensions = metric.dimensions;
|
|
53
|
+
setNode(data);
|
|
54
|
+
}
|
|
55
|
+
if (data.type === 'cube') {
|
|
56
|
+
const cube = await djClient.cube(name);
|
|
57
|
+
data.cube_elements = cube.cube_elements;
|
|
58
|
+
setNode(data);
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
fetchData().catch(console.error);
|
|
62
|
+
}, [djClient, name]);
|
|
63
|
+
|
|
64
|
+
const tabsList = node => {
|
|
65
|
+
return [
|
|
66
|
+
{
|
|
67
|
+
id: 0,
|
|
68
|
+
name: 'Info',
|
|
69
|
+
display: true,
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
id: 1,
|
|
73
|
+
name: 'Columns',
|
|
74
|
+
display: true,
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
id: 2,
|
|
78
|
+
name: 'Graph',
|
|
79
|
+
display: true,
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
id: 3,
|
|
83
|
+
name: 'History',
|
|
84
|
+
display: true,
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
id: 4,
|
|
88
|
+
name: 'SQL',
|
|
89
|
+
display: node?.type !== 'dimension' && node?.type !== 'source',
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
id: 5,
|
|
93
|
+
name: 'Materializations',
|
|
94
|
+
display: node?.type !== 'source',
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
id: 6,
|
|
98
|
+
name: 'Linked Nodes',
|
|
99
|
+
display: node?.type === 'dimension',
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
id: 7,
|
|
103
|
+
name: 'Lineage',
|
|
104
|
+
display: node?.type === 'metric',
|
|
105
|
+
},
|
|
106
|
+
];
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
//
|
|
110
|
+
//
|
|
111
|
+
let tabToDisplay = null;
|
|
112
|
+
switch (state.selectedTab) {
|
|
113
|
+
case 0:
|
|
114
|
+
tabToDisplay =
|
|
115
|
+
node && node.message === undefined ? <NodeInfoTab node={node} /> : '';
|
|
116
|
+
break;
|
|
117
|
+
case 1:
|
|
118
|
+
tabToDisplay = <NodeColumnTab node={node} djClient={djClient} />;
|
|
119
|
+
break;
|
|
120
|
+
case 2:
|
|
121
|
+
tabToDisplay = <NodeLineage djNode={node} djClient={djClient} />;
|
|
122
|
+
break;
|
|
123
|
+
case 3:
|
|
124
|
+
tabToDisplay = <NodeHistory node={node} djClient={djClient} />;
|
|
125
|
+
break;
|
|
126
|
+
case 4:
|
|
127
|
+
tabToDisplay =
|
|
128
|
+
node?.type === 'metric' ? <NodeSQLTab djNode={node} /> : <br />;
|
|
129
|
+
break;
|
|
130
|
+
case 5:
|
|
131
|
+
tabToDisplay = <NodeMaterializationTab node={node} djClient={djClient} />;
|
|
132
|
+
break;
|
|
133
|
+
case 6:
|
|
134
|
+
tabToDisplay = <NodesWithDimension node={node} djClient={djClient} />;
|
|
135
|
+
break;
|
|
136
|
+
case 7:
|
|
137
|
+
tabToDisplay = <NodeColumnLineage djNode={node} djClient={djClient} />;
|
|
138
|
+
break;
|
|
139
|
+
default: /* istanbul ignore next */
|
|
140
|
+
tabToDisplay = <NodeInfoTab node={node} />;
|
|
141
|
+
}
|
|
142
|
+
// @ts-ignore
|
|
143
|
+
return (
|
|
144
|
+
<div className="node__header">
|
|
145
|
+
<NamespaceHeader namespace={name.split('.').slice(0, -1).join('.')} />
|
|
146
|
+
<div className="card">
|
|
147
|
+
{node?.message === undefined ? (
|
|
148
|
+
<div className="card-header">
|
|
149
|
+
<h3
|
|
150
|
+
className="card-title align-items-start flex-column"
|
|
151
|
+
style={{ display: 'inline-block' }}
|
|
152
|
+
>
|
|
153
|
+
<span
|
|
154
|
+
className="card-label fw-bold text-gray-800"
|
|
155
|
+
role="dialog"
|
|
156
|
+
aria-hidden="false"
|
|
157
|
+
aria-label="DisplayName"
|
|
158
|
+
>
|
|
159
|
+
{node?.display_name}{' '}
|
|
160
|
+
<span
|
|
161
|
+
className={'node_type__' + node?.type + ' badge node_type'}
|
|
162
|
+
role="dialog"
|
|
163
|
+
aria-hidden="false"
|
|
164
|
+
aria-label="NodeType"
|
|
165
|
+
>
|
|
166
|
+
{node?.type}
|
|
167
|
+
</span>
|
|
168
|
+
</span>
|
|
169
|
+
</h3>
|
|
170
|
+
<a
|
|
171
|
+
href={`/nodes/${node?.name}/edit`}
|
|
172
|
+
style={{ marginLeft: '0.5rem' }}
|
|
173
|
+
>
|
|
174
|
+
<EditIcon />
|
|
175
|
+
</a>
|
|
176
|
+
<ClientCodePopover code={node?.createNodeClientCode} />
|
|
177
|
+
<div>
|
|
178
|
+
<a
|
|
179
|
+
href={'/nodes/' + node?.name}
|
|
180
|
+
className="link-table"
|
|
181
|
+
role="dialog"
|
|
182
|
+
aria-hidden="false"
|
|
183
|
+
aria-label="NodeName"
|
|
184
|
+
>
|
|
185
|
+
{node?.name}
|
|
186
|
+
</a>
|
|
187
|
+
<span
|
|
188
|
+
className="rounded-pill badge bg-secondary-soft"
|
|
189
|
+
style={{ marginLeft: '0.5rem' }}
|
|
190
|
+
>
|
|
191
|
+
{node?.version}
|
|
192
|
+
</span>
|
|
193
|
+
</div>
|
|
194
|
+
<div className="align-items-center row">
|
|
195
|
+
{tabsList(node).map(buildTabs)}
|
|
196
|
+
</div>
|
|
197
|
+
{tabToDisplay}
|
|
198
|
+
</div>
|
|
199
|
+
) : node?.message !== undefined ? (
|
|
200
|
+
<div className="message alert" style={{ margin: '20px' }}>
|
|
201
|
+
<AlertIcon />
|
|
202
|
+
Node `{name}` does not exist!
|
|
203
|
+
</div>
|
|
204
|
+
) : (
|
|
205
|
+
''
|
|
206
|
+
)}
|
|
207
|
+
</div>
|
|
208
|
+
</div>
|
|
209
|
+
);
|
|
210
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Asynchronously loads the component for NotFoundPage
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import * as React from 'react';
|
|
6
|
+
import { lazyLoad } from '../../../utils/loadable';
|
|
7
|
+
|
|
8
|
+
export const NotFoundPage = lazyLoad(
|
|
9
|
+
() => import('./index'),
|
|
10
|
+
module => module.NotFoundPage,
|
|
11
|
+
{
|
|
12
|
+
fallback: <></>,
|
|
13
|
+
},
|
|
14
|
+
);
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { render } from '@testing-library/react';
|
|
3
|
+
import { NotFoundPage } from '../index';
|
|
4
|
+
import { HelmetProvider } from 'react-helmet-async';
|
|
5
|
+
|
|
6
|
+
describe('<NotFoundPage />', () => {
|
|
7
|
+
it('displays the correct 404 message ', () => {
|
|
8
|
+
const { getByText } = render(
|
|
9
|
+
<HelmetProvider>
|
|
10
|
+
<NotFoundPage />
|
|
11
|
+
</HelmetProvider>,
|
|
12
|
+
);
|
|
13
|
+
|
|
14
|
+
expect(getByText('Page not found.')).toBeInTheDocument();
|
|
15
|
+
});
|
|
16
|
+
});
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { Helmet } from 'react-helmet-async';
|
|
3
|
+
|
|
4
|
+
export function NotFoundPage() {
|
|
5
|
+
return (
|
|
6
|
+
<>
|
|
7
|
+
<Helmet>
|
|
8
|
+
<title>404 Page Not Found</title>
|
|
9
|
+
<meta name="description" content="Page not found" />
|
|
10
|
+
</Helmet>
|
|
11
|
+
<div>
|
|
12
|
+
<label>
|
|
13
|
+
4
|
|
14
|
+
<span role="img" aria-label="Crying Face">
|
|
15
|
+
😢
|
|
16
|
+
</span>
|
|
17
|
+
4
|
|
18
|
+
</label>
|
|
19
|
+
<p>Page not found.</p>
|
|
20
|
+
</div>
|
|
21
|
+
</>
|
|
22
|
+
);
|
|
23
|
+
}
|
|
@@ -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 RegisterTablePage = () => {
|
|
9
|
+
return lazyLoad(
|
|
10
|
+
() => import('./index'),
|
|
11
|
+
module => module.RegisterTablePage,
|
|
12
|
+
{
|
|
13
|
+
fallback: <div></div>,
|
|
14
|
+
},
|
|
15
|
+
)();
|
|
16
|
+
};
|
|
@@ -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,37 @@
|
|
|
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
|
+
data-testid="success"
|
|
8
|
+
>
|
|
9
|
+
<svg
|
|
10
|
+
class="bi bi-check-circle-fill"
|
|
11
|
+
fill="currentColor"
|
|
12
|
+
height="25"
|
|
13
|
+
viewBox="0 0 16 16"
|
|
14
|
+
width="25"
|
|
15
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
16
|
+
>
|
|
17
|
+
<path
|
|
18
|
+
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"
|
|
19
|
+
/>
|
|
20
|
+
</svg>
|
|
21
|
+
Successfully registered source node
|
|
22
|
+
|
|
23
|
+
<a
|
|
24
|
+
href="/nodes/source.warehouse.schema.some_table"
|
|
25
|
+
>
|
|
26
|
+
source.warehouse.schema.some_table
|
|
27
|
+
</a>
|
|
28
|
+
, which references table
|
|
29
|
+
warehouse
|
|
30
|
+
.
|
|
31
|
+
schema
|
|
32
|
+
.
|
|
33
|
+
some_table
|
|
34
|
+
.
|
|
35
|
+
</div>,
|
|
36
|
+
]
|
|
37
|
+
`;
|
|
@@ -0,0 +1,142 @@
|
|
|
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 React, { useContext, useEffect, useState } from 'react';
|
|
10
|
+
import DJClientContext from '../../providers/djclient';
|
|
11
|
+
import 'styles/node-creation.scss';
|
|
12
|
+
import { FormikSelect } from '../AddEditNodePage/FormikSelect';
|
|
13
|
+
import { displayMessageAfterSubmit } from '../../../utils/form';
|
|
14
|
+
|
|
15
|
+
export function RegisterTablePage() {
|
|
16
|
+
const djClient = useContext(DJClientContext).DataJunctionAPI;
|
|
17
|
+
const [catalogs, setCatalogs] = useState([]);
|
|
18
|
+
|
|
19
|
+
useEffect(() => {
|
|
20
|
+
const fetchData = async () => {
|
|
21
|
+
const catalogs = await djClient.catalogs();
|
|
22
|
+
setCatalogs(
|
|
23
|
+
catalogs.map(catalog => {
|
|
24
|
+
return { value: catalog.name, label: catalog.name };
|
|
25
|
+
}),
|
|
26
|
+
);
|
|
27
|
+
};
|
|
28
|
+
fetchData().catch(console.error);
|
|
29
|
+
}, [djClient, djClient.namespaces]);
|
|
30
|
+
|
|
31
|
+
const initialValues = {
|
|
32
|
+
catalog: '',
|
|
33
|
+
schema: '',
|
|
34
|
+
table: '',
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const validator = values => {
|
|
38
|
+
const errors = {};
|
|
39
|
+
if (!values.table) {
|
|
40
|
+
errors.table = 'Required';
|
|
41
|
+
}
|
|
42
|
+
if (!values.schema) {
|
|
43
|
+
errors.schema = 'Required';
|
|
44
|
+
}
|
|
45
|
+
return errors;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const handleSubmit = async (values, { setSubmitting, setStatus }) => {
|
|
49
|
+
const { status, json } = await djClient.registerTable(
|
|
50
|
+
values.catalog,
|
|
51
|
+
values.schema,
|
|
52
|
+
values.table,
|
|
53
|
+
);
|
|
54
|
+
if (status === 200 || status === 201) {
|
|
55
|
+
setStatus({
|
|
56
|
+
success: (
|
|
57
|
+
<>
|
|
58
|
+
Successfully registered source node{' '}
|
|
59
|
+
<a href={`/nodes/${json.name}`}>{json.name}</a>, which references
|
|
60
|
+
table {values.catalog}.{values.schema}.{values.table}.
|
|
61
|
+
</>
|
|
62
|
+
),
|
|
63
|
+
});
|
|
64
|
+
} else {
|
|
65
|
+
setStatus({
|
|
66
|
+
failure: `${json.message}`,
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
setSubmitting(false);
|
|
70
|
+
window.scrollTo({ top: 0, left: 0, behavior: 'smooth' });
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
return (
|
|
74
|
+
<div className="mid">
|
|
75
|
+
<NamespaceHeader namespace="" />
|
|
76
|
+
<div className="card">
|
|
77
|
+
<div className="card-header">
|
|
78
|
+
<h2>
|
|
79
|
+
Register{' '}
|
|
80
|
+
<span className={`node_type__source node_type_creation_heading`}>
|
|
81
|
+
Source
|
|
82
|
+
</span>
|
|
83
|
+
</h2>
|
|
84
|
+
<center>
|
|
85
|
+
<Formik
|
|
86
|
+
initialValues={initialValues}
|
|
87
|
+
validate={validator}
|
|
88
|
+
onSubmit={handleSubmit}
|
|
89
|
+
>
|
|
90
|
+
{function Render({ isSubmitting, status }) {
|
|
91
|
+
return (
|
|
92
|
+
<Form>
|
|
93
|
+
{displayMessageAfterSubmit(status)}
|
|
94
|
+
{
|
|
95
|
+
<>
|
|
96
|
+
<div className="SourceCreationInput">
|
|
97
|
+
<ErrorMessage name="catalog" component="span" />
|
|
98
|
+
<label htmlFor="catalog">Catalog</label>
|
|
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>
|
|
107
|
+
</div>
|
|
108
|
+
<div className="SourceCreationInput">
|
|
109
|
+
<ErrorMessage name="schema" component="span" />
|
|
110
|
+
<label htmlFor="schema">Schema</label>
|
|
111
|
+
<Field
|
|
112
|
+
type="text"
|
|
113
|
+
name="schema"
|
|
114
|
+
id="schema"
|
|
115
|
+
placeholder="Schema"
|
|
116
|
+
/>
|
|
117
|
+
</div>
|
|
118
|
+
<div className="SourceCreationInput NodeCreationInput">
|
|
119
|
+
<ErrorMessage name="table" component="span" />
|
|
120
|
+
<label htmlFor="table">Table</label>
|
|
121
|
+
<Field
|
|
122
|
+
type="text"
|
|
123
|
+
name="table"
|
|
124
|
+
id="table"
|
|
125
|
+
placeholder="Table"
|
|
126
|
+
/>
|
|
127
|
+
</div>
|
|
128
|
+
<button type="submit" disabled={isSubmitting}>
|
|
129
|
+
Register
|
|
130
|
+
</button>
|
|
131
|
+
</>
|
|
132
|
+
}
|
|
133
|
+
</Form>
|
|
134
|
+
);
|
|
135
|
+
}}
|
|
136
|
+
</Formik>
|
|
137
|
+
</center>
|
|
138
|
+
</div>
|
|
139
|
+
</div>
|
|
140
|
+
</div>
|
|
141
|
+
);
|
|
142
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Asynchronously loads the component for the root page
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import * as React from 'react';
|
|
6
|
+
import { lazyLoad } from '../../../utils/loadable';
|
|
7
|
+
|
|
8
|
+
export const Root = lazyLoad(
|
|
9
|
+
() => import('./index'),
|
|
10
|
+
module => module.Root,
|
|
11
|
+
{
|
|
12
|
+
fallback: <></>,
|
|
13
|
+
},
|
|
14
|
+
);
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { render, screen, waitFor } from '@testing-library/react';
|
|
3
|
+
import { Root } from '../index';
|
|
4
|
+
import DJClientContext from '../../../providers/djclient';
|
|
5
|
+
import { HelmetProvider } from 'react-helmet-async';
|
|
6
|
+
|
|
7
|
+
describe('<Root />', () => {
|
|
8
|
+
const mockDjClient = {
|
|
9
|
+
logout: jest.fn(),
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
it('renders with the correct title and navigation', async () => {
|
|
13
|
+
render(
|
|
14
|
+
<HelmetProvider>
|
|
15
|
+
<DJClientContext.Provider value={{ DataJunctionAPI: mockDjClient }}>
|
|
16
|
+
<Root />
|
|
17
|
+
</DJClientContext.Provider>
|
|
18
|
+
</HelmetProvider>,
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
waitFor(() => {
|
|
22
|
+
expect(document.title).toEqual('DataJunction');
|
|
23
|
+
const metaDescription = document.querySelector(
|
|
24
|
+
"meta[name='description']",
|
|
25
|
+
);
|
|
26
|
+
expect(metaDescription).toBeInTheDocument();
|
|
27
|
+
expect(metaDescription.content).toBe(
|
|
28
|
+
'DataJunction Metrics Platform Webapp',
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
expect(screen.getByText(/^DataJunction$/)).toBeInTheDocument();
|
|
32
|
+
expect(screen.getByText('Explore').closest('a')).toHaveAttribute(
|
|
33
|
+
'href',
|
|
34
|
+
'/',
|
|
35
|
+
);
|
|
36
|
+
expect(screen.getByText('SQL').closest('a')).toHaveAttribute(
|
|
37
|
+
'href',
|
|
38
|
+
'/sql',
|
|
39
|
+
);
|
|
40
|
+
expect(screen.getByText('Docs').closest('a')).toHaveAttribute(
|
|
41
|
+
'href',
|
|
42
|
+
'https://www.datajunction.io',
|
|
43
|
+
);
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('renders Logout button unless REACT_DISABLE_AUTH is true', () => {
|
|
48
|
+
process.env.REACT_DISABLE_AUTH = 'false';
|
|
49
|
+
render(
|
|
50
|
+
<HelmetProvider>
|
|
51
|
+
<DJClientContext.Provider value={{ DataJunctionAPI: mockDjClient }}>
|
|
52
|
+
<Root />
|
|
53
|
+
</DJClientContext.Provider>
|
|
54
|
+
</HelmetProvider>,
|
|
55
|
+
);
|
|
56
|
+
expect(screen.getByText('Logout')).toBeInTheDocument();
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('calls logout and reloads window on logout button click', () => {
|
|
60
|
+
process.env.REACT_DISABLE_AUTH = 'false';
|
|
61
|
+
const originalLocation = window.location;
|
|
62
|
+
delete window.location;
|
|
63
|
+
window.location = { ...originalLocation, reload: jest.fn() };
|
|
64
|
+
|
|
65
|
+
render(
|
|
66
|
+
<HelmetProvider>
|
|
67
|
+
<DJClientContext.Provider value={{ DataJunctionAPI: mockDjClient }}>
|
|
68
|
+
<Root />
|
|
69
|
+
</DJClientContext.Provider>
|
|
70
|
+
</HelmetProvider>,
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
screen.getByText('Logout').click();
|
|
74
|
+
expect(mockDjClient.logout).toHaveBeenCalled();
|
|
75
|
+
window.location = originalLocation;
|
|
76
|
+
});
|
|
77
|
+
});
|
|
Binary file
|