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,396 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Node add + edit page for transforms, metrics, and dimensions. The creation and edit flow for these
|
|
3
|
+
* node types is largely the same, with minor differences handled server-side. For the `query`
|
|
4
|
+
* field, this page will render a CodeMirror SQL editor with autocompletion and syntax highlighting.
|
|
5
|
+
*/
|
|
6
|
+
import { ErrorMessage, Field, Form, Formik } from 'formik';
|
|
7
|
+
|
|
8
|
+
import NamespaceHeader from '../../components/NamespaceHeader';
|
|
9
|
+
import { useContext, useEffect, useState } from 'react';
|
|
10
|
+
import DJClientContext from '../../providers/djclient';
|
|
11
|
+
import 'styles/node-creation.scss';
|
|
12
|
+
import AlertIcon from '../../icons/AlertIcon';
|
|
13
|
+
import { useParams } from 'react-router-dom';
|
|
14
|
+
import { FullNameField } from './FullNameField';
|
|
15
|
+
import { FormikSelect } from './FormikSelect';
|
|
16
|
+
import { NodeQueryField } from './NodeQueryField';
|
|
17
|
+
import { displayMessageAfterSubmit, labelize } from '../../../utils/form';
|
|
18
|
+
|
|
19
|
+
class Action {
|
|
20
|
+
static Add = new Action('add');
|
|
21
|
+
static Edit = new Action('edit');
|
|
22
|
+
|
|
23
|
+
constructor(name) {
|
|
24
|
+
this.name = name;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function AddEditNodePage() {
|
|
29
|
+
const djClient = useContext(DJClientContext).DataJunctionAPI;
|
|
30
|
+
|
|
31
|
+
let { nodeType, initialNamespace, name } = useParams();
|
|
32
|
+
const action = name !== undefined ? Action.Edit : Action.Add;
|
|
33
|
+
|
|
34
|
+
const [namespaces, setNamespaces] = useState([]);
|
|
35
|
+
const [tags, setTags] = useState([]);
|
|
36
|
+
|
|
37
|
+
const initialValues = {
|
|
38
|
+
name: action === Action.Edit ? name : '',
|
|
39
|
+
namespace: action === Action.Add ? initialNamespace : '',
|
|
40
|
+
display_name: '',
|
|
41
|
+
query: '',
|
|
42
|
+
node_type: '',
|
|
43
|
+
description: '',
|
|
44
|
+
primary_key: '',
|
|
45
|
+
mode: 'draft',
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const validator = values => {
|
|
49
|
+
const errors = {};
|
|
50
|
+
if (!values.name) {
|
|
51
|
+
errors.name = 'Required';
|
|
52
|
+
}
|
|
53
|
+
if (!values.query) {
|
|
54
|
+
errors.query = 'Required';
|
|
55
|
+
}
|
|
56
|
+
return errors;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const handleSubmit = (values, { setSubmitting, setStatus }) => {
|
|
60
|
+
if (action === Action.Add) {
|
|
61
|
+
setTimeout(() => {
|
|
62
|
+
createNode(values, setStatus);
|
|
63
|
+
setSubmitting(false);
|
|
64
|
+
}, 400);
|
|
65
|
+
} else {
|
|
66
|
+
setTimeout(() => {
|
|
67
|
+
patchNode(values, setStatus);
|
|
68
|
+
setSubmitting(false);
|
|
69
|
+
}, 400);
|
|
70
|
+
}
|
|
71
|
+
window.scrollTo({ top: 0, left: 0, behavior: 'smooth' });
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
const pageTitle =
|
|
75
|
+
action === Action.Add ? (
|
|
76
|
+
<h2>
|
|
77
|
+
Create{' '}
|
|
78
|
+
<span className={`node_type__${nodeType} node_type_creation_heading`}>
|
|
79
|
+
{nodeType}
|
|
80
|
+
</span>
|
|
81
|
+
</h2>
|
|
82
|
+
) : (
|
|
83
|
+
<h2>Edit</h2>
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
const staticFieldsInEdit = node => (
|
|
87
|
+
<>
|
|
88
|
+
<div className="NodeNameInput NodeCreationInput">
|
|
89
|
+
<label htmlFor="name">Name</label> {name}
|
|
90
|
+
</div>
|
|
91
|
+
<div className="NodeNameInput NodeCreationInput">
|
|
92
|
+
<label htmlFor="name">Type</label> {node.type}
|
|
93
|
+
</div>
|
|
94
|
+
</>
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
const primaryKeyToList = primaryKey => {
|
|
98
|
+
return primaryKey.split(',').map(columnName => columnName.trim());
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
const createNode = async (values, setStatus) => {
|
|
102
|
+
const { status, json } = await djClient.createNode(
|
|
103
|
+
nodeType,
|
|
104
|
+
values.name,
|
|
105
|
+
values.display_name,
|
|
106
|
+
values.description,
|
|
107
|
+
values.query,
|
|
108
|
+
values.mode,
|
|
109
|
+
values.namespace,
|
|
110
|
+
values.primary_key ? primaryKeyToList(values.primary_key) : null,
|
|
111
|
+
);
|
|
112
|
+
if (status === 200 || status === 201) {
|
|
113
|
+
await djClient.tagsNode(values.name, values.tags);
|
|
114
|
+
setStatus({
|
|
115
|
+
success: (
|
|
116
|
+
<>
|
|
117
|
+
Successfully created {json.type} node{' '}
|
|
118
|
+
<a href={`/nodes/${json.name}`}>{json.name}</a>!
|
|
119
|
+
</>
|
|
120
|
+
),
|
|
121
|
+
});
|
|
122
|
+
} else {
|
|
123
|
+
setStatus({
|
|
124
|
+
failure: `${json.message}`,
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
const patchNode = async (values, setStatus) => {
|
|
130
|
+
const { status, json } = await djClient.patchNode(
|
|
131
|
+
values.name,
|
|
132
|
+
values.display_name,
|
|
133
|
+
values.description,
|
|
134
|
+
values.query,
|
|
135
|
+
values.mode,
|
|
136
|
+
values.primary_key ? primaryKeyToList(values.primary_key) : null,
|
|
137
|
+
);
|
|
138
|
+
const tagsResponse = await djClient.tagsNode(values.name, values.tags);
|
|
139
|
+
if ((status === 200 || status === 201) && tagsResponse.status === 200) {
|
|
140
|
+
setStatus({
|
|
141
|
+
success: (
|
|
142
|
+
<>
|
|
143
|
+
Successfully updated {json.type} node{' '}
|
|
144
|
+
<a href={`/nodes/${json.name}`}>{json.name}</a>!
|
|
145
|
+
</>
|
|
146
|
+
),
|
|
147
|
+
});
|
|
148
|
+
} else {
|
|
149
|
+
setStatus({
|
|
150
|
+
failure: `${json.message}, ${tagsResponse.json.message}`,
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
const namespaceInput = (
|
|
156
|
+
<div className="NamespaceInput">
|
|
157
|
+
<ErrorMessage name="namespace" component="span" />
|
|
158
|
+
<label htmlFor="react-select-3-input">Namespace</label>
|
|
159
|
+
<FormikSelect
|
|
160
|
+
selectOptions={namespaces}
|
|
161
|
+
formikFieldName="namespace"
|
|
162
|
+
placeholder="Choose Namespace"
|
|
163
|
+
defaultValue={{
|
|
164
|
+
value: initialNamespace,
|
|
165
|
+
label: initialNamespace,
|
|
166
|
+
}}
|
|
167
|
+
/>
|
|
168
|
+
</div>
|
|
169
|
+
);
|
|
170
|
+
|
|
171
|
+
const fullNameInput = (
|
|
172
|
+
<div className="FullNameInput NodeCreationInput">
|
|
173
|
+
<ErrorMessage name="name" component="span" />
|
|
174
|
+
<label htmlFor="FullName">Full Name</label>
|
|
175
|
+
<FullNameField type="text" name="name" />
|
|
176
|
+
</div>
|
|
177
|
+
);
|
|
178
|
+
|
|
179
|
+
const nodeCanBeEdited = nodeType => {
|
|
180
|
+
return new Set(['transform', 'metric', 'dimension']).has(nodeType);
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
const updateFieldsWithNodeData = (data, setFieldValue) => {
|
|
184
|
+
const fields = [
|
|
185
|
+
'display_name',
|
|
186
|
+
'query',
|
|
187
|
+
'type',
|
|
188
|
+
'description',
|
|
189
|
+
'primary_key',
|
|
190
|
+
'mode',
|
|
191
|
+
'tags',
|
|
192
|
+
];
|
|
193
|
+
fields.forEach(field => {
|
|
194
|
+
if (
|
|
195
|
+
field === 'primary_key' &&
|
|
196
|
+
data[field] !== undefined &&
|
|
197
|
+
Array.isArray(data[field])
|
|
198
|
+
) {
|
|
199
|
+
data[field] = data[field].join(', ');
|
|
200
|
+
} else {
|
|
201
|
+
setFieldValue(field, data[field] || '', false);
|
|
202
|
+
}
|
|
203
|
+
});
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
const alertMessage = message => {
|
|
207
|
+
return (
|
|
208
|
+
<div className="message alert">
|
|
209
|
+
<AlertIcon />
|
|
210
|
+
{message}
|
|
211
|
+
</div>
|
|
212
|
+
);
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
// Get namespaces, only necessary when creating a node
|
|
216
|
+
useEffect(() => {
|
|
217
|
+
if (action === Action.Add) {
|
|
218
|
+
const fetchData = async () => {
|
|
219
|
+
const namespaces = await djClient.namespaces();
|
|
220
|
+
setNamespaces(
|
|
221
|
+
namespaces.map(m => ({
|
|
222
|
+
value: m['namespace'],
|
|
223
|
+
label: m['namespace'],
|
|
224
|
+
})),
|
|
225
|
+
);
|
|
226
|
+
};
|
|
227
|
+
fetchData().catch(console.error);
|
|
228
|
+
}
|
|
229
|
+
}, [action, djClient, djClient.metrics]);
|
|
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
|
+
|
|
245
|
+
return (
|
|
246
|
+
<div className="mid">
|
|
247
|
+
<NamespaceHeader namespace="" />
|
|
248
|
+
<div className="card">
|
|
249
|
+
<div className="card-header">
|
|
250
|
+
{pageTitle}
|
|
251
|
+
<center>
|
|
252
|
+
<Formik
|
|
253
|
+
initialValues={initialValues}
|
|
254
|
+
validate={validator}
|
|
255
|
+
onSubmit={handleSubmit}
|
|
256
|
+
>
|
|
257
|
+
{function Render({ isSubmitting, status, setFieldValue }) {
|
|
258
|
+
const [node, setNode] = useState([]);
|
|
259
|
+
const [selectTags, setSelectTags] = useState(null);
|
|
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
|
+
|
|
284
|
+
useEffect(() => {
|
|
285
|
+
const fetchData = async () => {
|
|
286
|
+
if (action === Action.Edit) {
|
|
287
|
+
const data = await djClient.node(name);
|
|
288
|
+
|
|
289
|
+
// Check if node exists
|
|
290
|
+
if (data.message !== undefined) {
|
|
291
|
+
setNode(null);
|
|
292
|
+
setMessage(`Node ${name} does not exist!`);
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// Check if node type can be edited
|
|
297
|
+
if (!nodeCanBeEdited(data.type)) {
|
|
298
|
+
setNode(null);
|
|
299
|
+
setMessage(
|
|
300
|
+
`Node ${name} is of type ${data.type} and cannot be edited`,
|
|
301
|
+
);
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// Update fields with existing data to prepare for edit
|
|
306
|
+
updateFieldsWithNodeData(data, setFieldValue);
|
|
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
|
+
);
|
|
319
|
+
}
|
|
320
|
+
};
|
|
321
|
+
fetchData().catch(console.error);
|
|
322
|
+
}, [setFieldValue, tags]);
|
|
323
|
+
return (
|
|
324
|
+
<Form>
|
|
325
|
+
{displayMessageAfterSubmit(status)}
|
|
326
|
+
{action === Action.Edit && message ? (
|
|
327
|
+
alertMessage(message)
|
|
328
|
+
) : (
|
|
329
|
+
<>
|
|
330
|
+
{action === Action.Add
|
|
331
|
+
? namespaceInput
|
|
332
|
+
: staticFieldsInEdit(node)}
|
|
333
|
+
<div className="DisplayNameInput NodeCreationInput">
|
|
334
|
+
<ErrorMessage name="display_name" component="span" />
|
|
335
|
+
<label htmlFor="displayName">Display Name</label>
|
|
336
|
+
<Field
|
|
337
|
+
type="text"
|
|
338
|
+
name="display_name"
|
|
339
|
+
id="displayName"
|
|
340
|
+
placeholder="Human readable display name"
|
|
341
|
+
/>
|
|
342
|
+
</div>
|
|
343
|
+
{action === Action.Add ? fullNameInput : ''}
|
|
344
|
+
<div className="DescriptionInput NodeCreationInput">
|
|
345
|
+
<ErrorMessage name="description" component="span" />
|
|
346
|
+
<label htmlFor="Description">Description</label>
|
|
347
|
+
<Field
|
|
348
|
+
type="textarea"
|
|
349
|
+
as="textarea"
|
|
350
|
+
name="description"
|
|
351
|
+
id="Description"
|
|
352
|
+
placeholder="Describe your node"
|
|
353
|
+
/>
|
|
354
|
+
</div>
|
|
355
|
+
<div className="QueryInput NodeCreationInput">
|
|
356
|
+
<ErrorMessage name="query" component="span" />
|
|
357
|
+
<label htmlFor="Query">Query</label>
|
|
358
|
+
<NodeQueryField
|
|
359
|
+
djClient={djClient}
|
|
360
|
+
value={node.query ? node.query : ''}
|
|
361
|
+
/>
|
|
362
|
+
</div>
|
|
363
|
+
<div className="PrimaryKeyInput NodeCreationInput">
|
|
364
|
+
<ErrorMessage name="primary_key" component="span" />
|
|
365
|
+
<label htmlFor="primaryKey">Primary Key</label>
|
|
366
|
+
<Field
|
|
367
|
+
type="text"
|
|
368
|
+
name="primary_key"
|
|
369
|
+
id="primaryKey"
|
|
370
|
+
placeholder="Comma-separated list of PKs"
|
|
371
|
+
/>
|
|
372
|
+
</div>
|
|
373
|
+
{tagsInput}
|
|
374
|
+
<div className="NodeModeInput NodeCreationInput">
|
|
375
|
+
<ErrorMessage name="mode" component="span" />
|
|
376
|
+
<label htmlFor="Mode">Mode</label>
|
|
377
|
+
<Field as="select" name="mode" id="Mode">
|
|
378
|
+
<option value="draft">Draft</option>
|
|
379
|
+
<option value="published">Published</option>
|
|
380
|
+
</Field>
|
|
381
|
+
</div>
|
|
382
|
+
<button type="submit" disabled={isSubmitting}>
|
|
383
|
+
{action === Action.Add ? 'Create' : 'Save'} {nodeType}
|
|
384
|
+
</button>
|
|
385
|
+
</>
|
|
386
|
+
)}
|
|
387
|
+
</Form>
|
|
388
|
+
);
|
|
389
|
+
}}
|
|
390
|
+
</Formik>
|
|
391
|
+
</center>
|
|
392
|
+
</div>
|
|
393
|
+
</div>
|
|
394
|
+
</div>
|
|
395
|
+
);
|
|
396
|
+
}
|
|
@@ -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
|
+
});
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Add or edit tags
|
|
3
|
+
*/
|
|
4
|
+
import { ErrorMessage, Field, Form, Formik } from 'formik';
|
|
5
|
+
|
|
6
|
+
import NamespaceHeader from '../../components/NamespaceHeader';
|
|
7
|
+
import React, { useContext } from 'react';
|
|
8
|
+
import DJClientContext from '../../providers/djclient';
|
|
9
|
+
import 'styles/node-creation.scss';
|
|
10
|
+
import { displayMessageAfterSubmit } from '../../../utils/form';
|
|
11
|
+
|
|
12
|
+
export function AddEditTagPage() {
|
|
13
|
+
const djClient = useContext(DJClientContext).DataJunctionAPI;
|
|
14
|
+
const initialValues = {
|
|
15
|
+
name: '',
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const validator = values => {
|
|
19
|
+
const errors = {};
|
|
20
|
+
if (!values.name) {
|
|
21
|
+
errors.name = 'Required';
|
|
22
|
+
}
|
|
23
|
+
return errors;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const handleSubmit = async (values, { setSubmitting, setStatus }) => {
|
|
27
|
+
const { status, json } = await djClient.addTag(
|
|
28
|
+
values.name,
|
|
29
|
+
values.display_name,
|
|
30
|
+
values.tag_type,
|
|
31
|
+
values.description,
|
|
32
|
+
);
|
|
33
|
+
if (status === 200 || status === 201) {
|
|
34
|
+
setStatus({
|
|
35
|
+
success: (
|
|
36
|
+
<>
|
|
37
|
+
Successfully added tag{' '}
|
|
38
|
+
<a href={`/tags/${json.name}`}>{json.display_name}</a>.
|
|
39
|
+
</>
|
|
40
|
+
),
|
|
41
|
+
});
|
|
42
|
+
} else {
|
|
43
|
+
setStatus({
|
|
44
|
+
failure: `${json.message}`,
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
setSubmitting(false);
|
|
48
|
+
window.scrollTo({ top: 0, left: 0, behavior: 'smooth' });
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
return (
|
|
52
|
+
<div className="mid">
|
|
53
|
+
<NamespaceHeader namespace="" />
|
|
54
|
+
<div className="card">
|
|
55
|
+
<div className="card-header">
|
|
56
|
+
<h2>
|
|
57
|
+
Add{' '}
|
|
58
|
+
<span className={`node_type__source node_type_creation_heading`}>
|
|
59
|
+
Tag
|
|
60
|
+
</span>
|
|
61
|
+
</h2>
|
|
62
|
+
<center>
|
|
63
|
+
<Formik
|
|
64
|
+
initialValues={initialValues}
|
|
65
|
+
validate={validator}
|
|
66
|
+
onSubmit={handleSubmit}
|
|
67
|
+
>
|
|
68
|
+
{function Render({ isSubmitting, status }) {
|
|
69
|
+
return (
|
|
70
|
+
<Form>
|
|
71
|
+
{displayMessageAfterSubmit(status)}
|
|
72
|
+
{
|
|
73
|
+
<>
|
|
74
|
+
<div className="NodeCreationInput">
|
|
75
|
+
<ErrorMessage name="name" component="span" />
|
|
76
|
+
<label htmlFor="name">Name</label>
|
|
77
|
+
<Field
|
|
78
|
+
type="text"
|
|
79
|
+
name="name"
|
|
80
|
+
id="name"
|
|
81
|
+
placeholder="Tag Name"
|
|
82
|
+
/>
|
|
83
|
+
</div>
|
|
84
|
+
<br />
|
|
85
|
+
<div className="FullNameInput NodeCreationInput">
|
|
86
|
+
<ErrorMessage name="display_name" component="span" />
|
|
87
|
+
<label htmlFor="display_name">Display Name</label>
|
|
88
|
+
<Field
|
|
89
|
+
type="text"
|
|
90
|
+
name="display_name"
|
|
91
|
+
id="display_name"
|
|
92
|
+
placeholder="Display Name"
|
|
93
|
+
class="FullNameField"
|
|
94
|
+
/>
|
|
95
|
+
</div>
|
|
96
|
+
<br />
|
|
97
|
+
<div className="NodeCreationInput">
|
|
98
|
+
<ErrorMessage name="tag_type" component="span" />
|
|
99
|
+
<label htmlFor="tag_type">Tag Type</label>
|
|
100
|
+
<Field
|
|
101
|
+
type="text"
|
|
102
|
+
name="tag_type"
|
|
103
|
+
id="tag_type"
|
|
104
|
+
placeholder="Tag Type"
|
|
105
|
+
/>
|
|
106
|
+
</div>
|
|
107
|
+
<div className="DescriptionInput NodeCreationInput">
|
|
108
|
+
<ErrorMessage name="description" component="span" />
|
|
109
|
+
<label htmlFor="description">Description</label>
|
|
110
|
+
<Field
|
|
111
|
+
type="textarea"
|
|
112
|
+
as="textarea"
|
|
113
|
+
name="description"
|
|
114
|
+
id="Description"
|
|
115
|
+
placeholder="Describe the tag"
|
|
116
|
+
/>
|
|
117
|
+
</div>
|
|
118
|
+
<button type="submit" disabled={isSubmitting}>
|
|
119
|
+
Add Tag
|
|
120
|
+
</button>
|
|
121
|
+
</>
|
|
122
|
+
}
|
|
123
|
+
</Form>
|
|
124
|
+
);
|
|
125
|
+
}}
|
|
126
|
+
</Formik>
|
|
127
|
+
</center>
|
|
128
|
+
</div>
|
|
129
|
+
</div>
|
|
130
|
+
</div>
|
|
131
|
+
);
|
|
132
|
+
}
|