datajunction-ui 0.0.1-a1 → 0.0.1-a101
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/Makefile +7 -1
- package/package.json +18 -7
- package/public/index.html +1 -1
- package/src/app/components/AddNodeDropdown.jsx +44 -0
- package/src/app/components/ListGroupItem.jsx +2 -1
- package/src/app/components/NodeListActions.jsx +69 -0
- package/src/app/components/NodeMaterializationDelete.jsx +80 -0
- package/src/app/components/QueryInfo.jsx +96 -1
- package/src/app/components/Search.jsx +94 -0
- package/src/app/components/__tests__/NodeListActions.test.jsx +94 -0
- package/src/app/components/__tests__/Search.test.jsx +63 -0
- package/src/app/components/__tests__/__snapshots__/ListGroupItem.test.tsx.snap +5 -3
- package/src/app/components/djgraph/Collapse.jsx +3 -2
- package/src/app/components/djgraph/DJNode.jsx +1 -1
- package/src/app/components/djgraph/DJNodeColumns.jsx +5 -1
- package/src/app/components/djgraph/LayoutFlow.jsx +5 -3
- package/src/app/components/forms/Action.jsx +8 -0
- package/src/app/components/forms/NodeNameField.jsx +64 -0
- package/src/app/components/search.css +17 -0
- package/src/app/icons/AddItemIcon.jsx +16 -0
- package/src/app/icons/CommitIcon.jsx +45 -0
- package/src/app/icons/DiffIcon.jsx +63 -0
- package/src/app/icons/EyeIcon.jsx +20 -0
- package/src/app/icons/FilterIcon.jsx +7 -0
- package/src/app/icons/JupyterExportIcon.jsx +25 -0
- package/src/app/icons/LoadingIcon.jsx +10 -10
- package/src/app/icons/PythonIcon.jsx +6 -44
- package/src/app/index.tsx +24 -0
- package/src/app/pages/AddEditNodePage/AlertMessage.jsx +10 -0
- package/src/app/pages/AddEditNodePage/ColumnsSelect.jsx +84 -0
- package/src/app/pages/AddEditNodePage/DescriptionField.jsx +17 -0
- package/src/app/pages/AddEditNodePage/DisplayNameField.jsx +16 -0
- package/src/app/pages/AddEditNodePage/FormikSelect.jsx +5 -0
- package/src/app/pages/AddEditNodePage/FullNameField.jsx +3 -2
- package/src/app/pages/AddEditNodePage/Loadable.jsx +6 -2
- package/src/app/pages/AddEditNodePage/MetricMetadataFields.jsx +75 -0
- package/src/app/pages/AddEditNodePage/MetricQueryField.jsx +71 -0
- package/src/app/pages/AddEditNodePage/NamespaceField.jsx +40 -0
- package/src/app/pages/AddEditNodePage/NodeModeField.jsx +14 -0
- package/src/app/pages/AddEditNodePage/NodeQueryField.jsx +8 -3
- package/src/app/pages/AddEditNodePage/RequiredDimensionsSelect.jsx +54 -0
- package/src/app/pages/AddEditNodePage/TagsField.jsx +47 -0
- package/src/app/pages/AddEditNodePage/UpstreamNodeField.jsx +49 -0
- package/src/app/pages/AddEditNodePage/__tests__/AddEditNodePageFormFailed.test.jsx +15 -9
- package/src/app/pages/AddEditNodePage/__tests__/AddEditNodePageFormSuccess.test.jsx +167 -24
- package/src/app/pages/AddEditNodePage/__tests__/index.test.jsx +55 -25
- package/src/app/pages/AddEditNodePage/index.jsx +275 -194
- package/src/app/pages/CubeBuilderPage/DimensionsSelect.jsx +154 -0
- package/src/app/pages/CubeBuilderPage/Loadable.jsx +16 -0
- package/src/app/pages/CubeBuilderPage/MetricsSelect.jsx +77 -0
- package/src/app/pages/CubeBuilderPage/__tests__/index.test.jsx +405 -0
- package/src/app/pages/CubeBuilderPage/index.jsx +267 -0
- package/src/app/pages/NamespacePage/AddNamespacePopover.jsx +5 -5
- package/src/app/pages/NamespacePage/Explorer.jsx +6 -2
- package/src/app/pages/NamespacePage/FieldControl.jsx +21 -0
- package/src/app/pages/NamespacePage/NodeTypeSelect.jsx +30 -0
- package/src/app/pages/NamespacePage/TagSelect.jsx +44 -0
- package/src/app/pages/NamespacePage/UserSelect.jsx +47 -0
- package/src/app/pages/NamespacePage/__tests__/index.test.jsx +98 -19
- package/src/app/pages/NamespacePage/index.jsx +272 -89
- package/src/app/pages/NodePage/AddBackfillPopover.jsx +60 -61
- package/src/app/pages/NodePage/AddMaterializationPopover.jsx +104 -51
- package/src/app/pages/NodePage/ClientCodePopover.jsx +73 -25
- package/src/app/pages/NodePage/DimensionFilter.jsx +86 -0
- package/src/app/pages/NodePage/EditColumnDescriptionPopover.jsx +116 -0
- package/src/app/pages/NodePage/LinkDimensionPopover.jsx +38 -23
- package/src/app/pages/NodePage/MaterializationConfigField.jsx +60 -0
- package/src/app/pages/NodePage/NodeColumnTab.jsx +183 -113
- package/src/app/pages/NodePage/NodeDependenciesTab.jsx +153 -0
- package/src/app/pages/NodePage/NodeGraphTab.jsx +56 -29
- package/src/app/pages/NodePage/NodeHistory.jsx +165 -161
- package/src/app/pages/NodePage/NodeInfoTab.jsx +148 -14
- package/src/app/pages/NodePage/NodeMaterializationTab.jsx +201 -104
- package/src/app/pages/NodePage/NodeStatus.jsx +96 -21
- package/src/app/pages/NodePage/NodeValidateTab.jsx +367 -0
- package/src/app/pages/NodePage/NotebookDownload.jsx +36 -0
- package/src/app/pages/NodePage/PartitionColumnPopover.jsx +3 -5
- package/src/app/pages/NodePage/PartitionValueForm.jsx +60 -0
- package/src/app/pages/NodePage/RevisionDiff.jsx +209 -0
- package/src/app/pages/NodePage/WatchNodeButton.jsx +226 -0
- package/src/app/pages/NodePage/__tests__/AddBackfillPopover.test.jsx +13 -4
- package/src/app/pages/NodePage/__tests__/AddMaterializationPopover.test.jsx +87 -0
- package/src/app/pages/NodePage/__tests__/DimensionFilter.test.jsx +74 -0
- package/src/app/pages/NodePage/__tests__/EditColumnDescriptionPopover.test.jsx +149 -0
- package/src/app/pages/NodePage/__tests__/LinkDimensionPopover.test.jsx +10 -14
- package/src/app/pages/NodePage/__tests__/NodeColumnTab.test.jsx +166 -0
- package/src/app/pages/NodePage/__tests__/NodeDependenciesTab.test.jsx +151 -0
- package/src/app/pages/NodePage/__tests__/NodeGraphTab.test.jsx +6 -2
- package/src/app/pages/NodePage/__tests__/NodeLineageTab.test.jsx +3 -2
- package/src/app/pages/NodePage/__tests__/NodeMaterializationTab.test.jsx +148 -0
- package/src/app/pages/NodePage/__tests__/NodePage.test.jsx +159 -57
- package/src/app/pages/NodePage/__tests__/RevisionDiff.test.jsx +164 -0
- package/src/app/pages/NodePage/__tests__/__snapshots__/NodePage.test.jsx.snap +2 -386
- package/src/app/pages/NodePage/index.jsx +94 -57
- package/src/app/pages/Root/__tests__/index.test.jsx +3 -1
- package/src/app/pages/Root/index.tsx +62 -12
- package/src/app/services/DJService.js +587 -55
- package/src/app/services/__tests__/DJService.test.jsx +382 -45
- package/src/index.tsx +1 -0
- package/src/mocks/mockNodes.jsx +265 -227
- package/src/styles/dag.css +4 -2
- package/src/styles/index.css +474 -10
- package/src/styles/loading.css +1 -1
- package/src/styles/node-creation.scss +84 -5
- package/src/styles/node-list.css +4 -0
- package/src/styles/sorted-table.css +15 -0
- package/src/app/components/DeleteNode.jsx +0 -55
- package/src/app/components/__tests__/DeleteNode.test.jsx +0 -53
- package/src/app/pages/NodePage/NodeSQLTab.jsx +0 -82
- package/src/app/pages/NodePage/__tests__/ClientCodePopover.test.jsx +0 -49
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
import React, { useContext, useEffect, useState } from 'react';
|
|
2
|
+
import NamespaceHeader from '../../components/NamespaceHeader';
|
|
3
|
+
import { DataJunctionAPI } from '../../services/DJService';
|
|
4
|
+
import DJClientContext from '../../providers/djclient';
|
|
5
|
+
import 'react-querybuilder/dist/query-builder.scss';
|
|
6
|
+
import 'styles/styles.scss';
|
|
7
|
+
import { ErrorMessage, Field, Form, Formik } from 'formik';
|
|
8
|
+
import { displayMessageAfterSubmit } from '../../../utils/form';
|
|
9
|
+
import { useParams } from 'react-router-dom';
|
|
10
|
+
import { Action } from '../../components/forms/Action';
|
|
11
|
+
import NodeNameField from '../../components/forms/NodeNameField';
|
|
12
|
+
import { MetricsSelect } from './MetricsSelect';
|
|
13
|
+
import { DimensionsSelect } from './DimensionsSelect';
|
|
14
|
+
import { TagsField } from '../AddEditNodePage/TagsField';
|
|
15
|
+
|
|
16
|
+
export function CubeBuilderPage() {
|
|
17
|
+
const djClient = useContext(DJClientContext).DataJunctionAPI;
|
|
18
|
+
|
|
19
|
+
let { nodeType, initialNamespace, name } = useParams();
|
|
20
|
+
const action = name !== undefined ? Action.Edit : Action.Add;
|
|
21
|
+
const validator = ruleType => !!ruleType.value;
|
|
22
|
+
|
|
23
|
+
const initialValues = {
|
|
24
|
+
name: action === Action.Edit ? name : '',
|
|
25
|
+
namespace: action === Action.Add ? initialNamespace : '',
|
|
26
|
+
display_name: '',
|
|
27
|
+
description: '',
|
|
28
|
+
mode: 'published',
|
|
29
|
+
metrics: [],
|
|
30
|
+
dimensions: [],
|
|
31
|
+
filters: [],
|
|
32
|
+
tags: [],
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const handleSubmit = (values, { setSubmitting, setStatus }) => {
|
|
36
|
+
if (action === Action.Add) {
|
|
37
|
+
setTimeout(() => {
|
|
38
|
+
createNode(values, setStatus);
|
|
39
|
+
setSubmitting(false);
|
|
40
|
+
}, 400);
|
|
41
|
+
} else {
|
|
42
|
+
setTimeout(() => {
|
|
43
|
+
patchNode(values, setStatus);
|
|
44
|
+
setSubmitting(false);
|
|
45
|
+
}, 400);
|
|
46
|
+
}
|
|
47
|
+
window.scrollTo({ top: 0, left: 0, behavior: 'smooth' });
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const createNode = async (values, setStatus) => {
|
|
51
|
+
const { status, json } = await djClient.createCube(
|
|
52
|
+
values.name,
|
|
53
|
+
values.display_name,
|
|
54
|
+
values.description,
|
|
55
|
+
values.mode,
|
|
56
|
+
values.metrics,
|
|
57
|
+
values.dimensions,
|
|
58
|
+
values.filters || [],
|
|
59
|
+
);
|
|
60
|
+
if (status === 200 || status === 201) {
|
|
61
|
+
if (values.tags) {
|
|
62
|
+
await djClient.tagsNode(values.name, values.tags);
|
|
63
|
+
}
|
|
64
|
+
setStatus({
|
|
65
|
+
success: (
|
|
66
|
+
<>
|
|
67
|
+
Successfully created {json.type} node{' '}
|
|
68
|
+
<a href={`/nodes/${json.name}`}>{json.name}</a>!
|
|
69
|
+
</>
|
|
70
|
+
),
|
|
71
|
+
});
|
|
72
|
+
} else {
|
|
73
|
+
setStatus({
|
|
74
|
+
failure: `${json.message}`,
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
const patchNode = async (values, setStatus) => {
|
|
80
|
+
const { status, json } = await djClient.patchCube(
|
|
81
|
+
values.name,
|
|
82
|
+
values.display_name,
|
|
83
|
+
values.description,
|
|
84
|
+
values.mode,
|
|
85
|
+
values.metrics,
|
|
86
|
+
values.dimensions,
|
|
87
|
+
values.filters || [],
|
|
88
|
+
);
|
|
89
|
+
const tagsResponse = await djClient.tagsNode(
|
|
90
|
+
values.name,
|
|
91
|
+
(values.tags || []).map(tag => tag),
|
|
92
|
+
);
|
|
93
|
+
if ((status === 200 || status === 201) && tagsResponse.status === 200) {
|
|
94
|
+
setStatus({
|
|
95
|
+
success: (
|
|
96
|
+
<>
|
|
97
|
+
Successfully updated {json.type} node{' '}
|
|
98
|
+
<a href={`/nodes/${json.name}`}>{json.name}</a>!
|
|
99
|
+
</>
|
|
100
|
+
),
|
|
101
|
+
});
|
|
102
|
+
} else {
|
|
103
|
+
setStatus({
|
|
104
|
+
failure: `${json.message}`,
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
const updateFieldsWithNodeData = (data, setFieldValue, setSelectTags) => {
|
|
110
|
+
setFieldValue('display_name', data.display_name || '', false);
|
|
111
|
+
setFieldValue('description', data.description || '', false);
|
|
112
|
+
setFieldValue('mode', data.mode || 'draft', false);
|
|
113
|
+
setFieldValue(
|
|
114
|
+
'tags',
|
|
115
|
+
data.tags.map(tag => tag.name),
|
|
116
|
+
);
|
|
117
|
+
// For react-select fields, we have to explicitly set the entire
|
|
118
|
+
// field rather than just the values
|
|
119
|
+
setSelectTags(
|
|
120
|
+
<TagsField
|
|
121
|
+
defaultValue={data.tags.map(t => {
|
|
122
|
+
return { value: t.name, label: t.display_name };
|
|
123
|
+
})}
|
|
124
|
+
/>,
|
|
125
|
+
);
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
const staticFieldsInEdit = () => (
|
|
129
|
+
<>
|
|
130
|
+
<div className="NodeNameInput NodeCreationInput">
|
|
131
|
+
<label htmlFor="name">Name</label> {name}
|
|
132
|
+
</div>
|
|
133
|
+
<div className="NodeNameInput NodeCreationInput">
|
|
134
|
+
<label htmlFor="name">Type</label> cube
|
|
135
|
+
</div>
|
|
136
|
+
<div className="DisplayNameInput NodeCreationInput">
|
|
137
|
+
<ErrorMessage name="display_name" component="span" />
|
|
138
|
+
<label htmlFor="displayName">Display Name</label>
|
|
139
|
+
<Field
|
|
140
|
+
type="text"
|
|
141
|
+
name="display_name"
|
|
142
|
+
id="displayName"
|
|
143
|
+
placeholder="Human readable display name"
|
|
144
|
+
/>
|
|
145
|
+
</div>
|
|
146
|
+
</>
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
// @ts-ignore
|
|
150
|
+
return (
|
|
151
|
+
<>
|
|
152
|
+
<div className="mid">
|
|
153
|
+
<NamespaceHeader namespace="" />
|
|
154
|
+
<Formik
|
|
155
|
+
initialValues={initialValues}
|
|
156
|
+
validate={validator}
|
|
157
|
+
onSubmit={handleSubmit}
|
|
158
|
+
>
|
|
159
|
+
{function Render({ isSubmitting, status, setFieldValue, props }) {
|
|
160
|
+
const [node, setNode] = useState([]);
|
|
161
|
+
const [selectTags, setSelectTags] = useState(null);
|
|
162
|
+
|
|
163
|
+
// Get cube
|
|
164
|
+
useEffect(() => {
|
|
165
|
+
const fetchData = async () => {
|
|
166
|
+
if (name) {
|
|
167
|
+
const cube = await djClient.cube(name);
|
|
168
|
+
setNode(cube);
|
|
169
|
+
updateFieldsWithNodeData(cube, setFieldValue, setSelectTags);
|
|
170
|
+
}
|
|
171
|
+
};
|
|
172
|
+
fetchData().catch(console.error);
|
|
173
|
+
}, [setFieldValue]);
|
|
174
|
+
|
|
175
|
+
return (
|
|
176
|
+
<Form>
|
|
177
|
+
<div className="card">
|
|
178
|
+
<div className="card-header">
|
|
179
|
+
<h2>
|
|
180
|
+
{action === Action.Edit ? 'Edit' : 'Create'}{' '}
|
|
181
|
+
<span
|
|
182
|
+
className={`node_type__cube node_type_creation_heading`}
|
|
183
|
+
>
|
|
184
|
+
Cube
|
|
185
|
+
</span>
|
|
186
|
+
</h2>
|
|
187
|
+
{displayMessageAfterSubmit(status)}
|
|
188
|
+
{action === Action.Add ? (
|
|
189
|
+
<NodeNameField />
|
|
190
|
+
) : (
|
|
191
|
+
staticFieldsInEdit(node)
|
|
192
|
+
)}
|
|
193
|
+
<div className="DescriptionInput NodeCreationInput">
|
|
194
|
+
<ErrorMessage name="description" component="span" />
|
|
195
|
+
<label htmlFor="Description">Description</label>
|
|
196
|
+
<Field
|
|
197
|
+
type="textarea"
|
|
198
|
+
as="textarea"
|
|
199
|
+
name="description"
|
|
200
|
+
id="Description"
|
|
201
|
+
placeholder="Describe your node"
|
|
202
|
+
/>
|
|
203
|
+
</div>
|
|
204
|
+
<div className="CubeCreationInput">
|
|
205
|
+
<label>Metrics *</label>
|
|
206
|
+
<p>Select metrics to include in the cube.</p>
|
|
207
|
+
<span
|
|
208
|
+
data-testid="select-metrics"
|
|
209
|
+
style={{ marginTop: '15px' }}
|
|
210
|
+
>
|
|
211
|
+
{action === Action.Edit ? (
|
|
212
|
+
<MetricsSelect cube={node} />
|
|
213
|
+
) : (
|
|
214
|
+
<MetricsSelect />
|
|
215
|
+
)}
|
|
216
|
+
</span>
|
|
217
|
+
</div>
|
|
218
|
+
<br />
|
|
219
|
+
<br />
|
|
220
|
+
<div className="CubeCreationInput">
|
|
221
|
+
<label>Dimensions *</label>
|
|
222
|
+
<p>
|
|
223
|
+
Select dimensions to include in the cube. As metrics are
|
|
224
|
+
selected above, the list of available dimensions will be
|
|
225
|
+
filtered to those shared by the selected metrics. If the
|
|
226
|
+
dimensions list is empty, no shared dimensions were
|
|
227
|
+
discovered.
|
|
228
|
+
</p>
|
|
229
|
+
<span data-testid="select-dimensions">
|
|
230
|
+
{action === Action.Edit ? (
|
|
231
|
+
<DimensionsSelect cube={node} />
|
|
232
|
+
) : (
|
|
233
|
+
<DimensionsSelect />
|
|
234
|
+
)}
|
|
235
|
+
</span>
|
|
236
|
+
</div>
|
|
237
|
+
<div className="NodeModeInput NodeCreationInput">
|
|
238
|
+
<ErrorMessage name="mode" component="span" />
|
|
239
|
+
<label htmlFor="Mode">Mode</label>
|
|
240
|
+
<Field as="select" name="mode" id="Mode">
|
|
241
|
+
<option value="draft">Draft</option>
|
|
242
|
+
<option value="published">Published</option>
|
|
243
|
+
</Field>
|
|
244
|
+
</div>
|
|
245
|
+
{action === Action.Edit ? selectTags : <TagsField />}
|
|
246
|
+
<button
|
|
247
|
+
type="submit"
|
|
248
|
+
disabled={isSubmitting}
|
|
249
|
+
aria-label="CreateCube"
|
|
250
|
+
>
|
|
251
|
+
{action === Action.Add ? 'Create Cube' : 'Save'}{' '}
|
|
252
|
+
{nodeType}
|
|
253
|
+
</button>
|
|
254
|
+
</div>
|
|
255
|
+
</div>
|
|
256
|
+
</Form>
|
|
257
|
+
);
|
|
258
|
+
}}
|
|
259
|
+
</Formik>
|
|
260
|
+
</div>
|
|
261
|
+
</>
|
|
262
|
+
);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
CubeBuilderPage.defaultProps = {
|
|
266
|
+
djClient: DataJunctionAPI,
|
|
267
|
+
};
|
|
@@ -2,9 +2,8 @@ import { useContext, useState } from 'react';
|
|
|
2
2
|
import * as React from 'react';
|
|
3
3
|
import DJClientContext from '../../providers/djclient';
|
|
4
4
|
import { ErrorMessage, Field, Form, Formik } from 'formik';
|
|
5
|
-
import
|
|
6
|
-
import
|
|
7
|
-
import { displayMessageAfterSubmit, labelize } from '../../../utils/form';
|
|
5
|
+
import AddItemIcon from '../../icons/AddItemIcon';
|
|
6
|
+
import { displayMessageAfterSubmit } from '../../../utils/form';
|
|
8
7
|
|
|
9
8
|
export default function AddNamespacePopover({ namespace }) {
|
|
10
9
|
const djClient = useContext(DJClientContext).DataJunctionAPI;
|
|
@@ -33,7 +32,7 @@ export default function AddNamespacePopover({ namespace }) {
|
|
|
33
32
|
setPopoverAnchor(!popoverAnchor);
|
|
34
33
|
}}
|
|
35
34
|
>
|
|
36
|
-
<
|
|
35
|
+
<AddItemIcon />
|
|
37
36
|
</button>
|
|
38
37
|
<div
|
|
39
38
|
className="popover"
|
|
@@ -48,7 +47,7 @@ export default function AddNamespacePopover({ namespace }) {
|
|
|
48
47
|
>
|
|
49
48
|
<Formik
|
|
50
49
|
initialValues={{
|
|
51
|
-
namespace: '',
|
|
50
|
+
namespace: namespace + '.',
|
|
52
51
|
}}
|
|
53
52
|
onSubmit={addNamespace}
|
|
54
53
|
>
|
|
@@ -64,6 +63,7 @@ export default function AddNamespacePopover({ namespace }) {
|
|
|
64
63
|
name="namespace"
|
|
65
64
|
id="namespace"
|
|
66
65
|
placeholder="New namespace"
|
|
66
|
+
default={namespace}
|
|
67
67
|
/>
|
|
68
68
|
</span>
|
|
69
69
|
<button
|
|
@@ -10,7 +10,7 @@ const Explorer = ({ item = [], current }) => {
|
|
|
10
10
|
useEffect(() => {
|
|
11
11
|
setItems(item);
|
|
12
12
|
setHighlight(current);
|
|
13
|
-
if (current
|
|
13
|
+
if (current !== undefined && current?.startsWith(item.path)) {
|
|
14
14
|
setExpand(true);
|
|
15
15
|
} else setExpand(false);
|
|
16
16
|
}, [current, item]);
|
|
@@ -43,8 +43,12 @@ const Explorer = ({ item = [], current }) => {
|
|
|
43
43
|
marginLeft: '1rem',
|
|
44
44
|
borderLeft: '1px solid rgb(218 233 255)',
|
|
45
45
|
}}
|
|
46
|
+
key={index}
|
|
46
47
|
>
|
|
47
|
-
<div
|
|
48
|
+
<div
|
|
49
|
+
className={`${expand ? '' : 'inactive'}`}
|
|
50
|
+
key={`nested-${index}`}
|
|
51
|
+
>
|
|
48
52
|
<Explorer item={item} current={highlight} />
|
|
49
53
|
</div>
|
|
50
54
|
</div>
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { components } from 'react-select';
|
|
2
|
+
|
|
3
|
+
const Control = ({ children, ...props }) => {
|
|
4
|
+
const { label, onLabelClick } = props.selectProps;
|
|
5
|
+
const style = {
|
|
6
|
+
cursor: 'pointer',
|
|
7
|
+
padding: '10px 5px 10px 12px',
|
|
8
|
+
color: 'rgb(112, 110, 115)',
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
return (
|
|
12
|
+
<components.Control {...props}>
|
|
13
|
+
<span onMouseDown={onLabelClick} style={style}>
|
|
14
|
+
{label}
|
|
15
|
+
</span>
|
|
16
|
+
{children}
|
|
17
|
+
</components.Control>
|
|
18
|
+
);
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export default Control;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import Select from 'react-select';
|
|
2
|
+
import Control from './FieldControl';
|
|
3
|
+
|
|
4
|
+
export default function NodeTypeSelect({ onChange }) {
|
|
5
|
+
return (
|
|
6
|
+
<span
|
|
7
|
+
className="menu-link"
|
|
8
|
+
style={{ marginLeft: '30px', width: '300px' }}
|
|
9
|
+
data-testid="select-node-type"
|
|
10
|
+
>
|
|
11
|
+
<Select
|
|
12
|
+
name="node_type"
|
|
13
|
+
isClearable
|
|
14
|
+
label="Node Type"
|
|
15
|
+
components={{ Control }}
|
|
16
|
+
onChange={e => onChange(e)}
|
|
17
|
+
styles={{
|
|
18
|
+
control: styles => ({ ...styles, backgroundColor: 'white' }),
|
|
19
|
+
}}
|
|
20
|
+
options={[
|
|
21
|
+
{ value: 'source', label: 'Source' },
|
|
22
|
+
{ value: 'transform', label: 'Transform' },
|
|
23
|
+
{ value: 'dimension', label: 'Dimension' },
|
|
24
|
+
{ value: 'metric', label: 'Metric' },
|
|
25
|
+
{ value: 'cube', label: 'Cube' },
|
|
26
|
+
]}
|
|
27
|
+
/>
|
|
28
|
+
</span>
|
|
29
|
+
);
|
|
30
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { useContext, useEffect, useState } from 'react';
|
|
2
|
+
import DJClientContext from '../../providers/djclient';
|
|
3
|
+
import Control from './FieldControl';
|
|
4
|
+
|
|
5
|
+
import Select from 'react-select';
|
|
6
|
+
|
|
7
|
+
export default function TagSelect({ onChange }) {
|
|
8
|
+
const djClient = useContext(DJClientContext).DataJunctionAPI;
|
|
9
|
+
|
|
10
|
+
const [retrieved, setRetrieved] = useState(false);
|
|
11
|
+
const [tags, setTags] = useState([]);
|
|
12
|
+
|
|
13
|
+
useEffect(() => {
|
|
14
|
+
const fetchData = async () => {
|
|
15
|
+
const tags = await djClient.listTags();
|
|
16
|
+
setTags(tags);
|
|
17
|
+
setRetrieved(true);
|
|
18
|
+
};
|
|
19
|
+
fetchData().catch(console.error);
|
|
20
|
+
}, [djClient]);
|
|
21
|
+
|
|
22
|
+
return (
|
|
23
|
+
<span
|
|
24
|
+
className="menu-link"
|
|
25
|
+
style={{ marginLeft: '30px', width: '350px' }}
|
|
26
|
+
data-testid="select-tag"
|
|
27
|
+
>
|
|
28
|
+
<Select
|
|
29
|
+
name="tags"
|
|
30
|
+
isClearable
|
|
31
|
+
isMulti
|
|
32
|
+
label="Tags"
|
|
33
|
+
components={{ Control }}
|
|
34
|
+
onChange={e => onChange(e)}
|
|
35
|
+
options={tags?.map(tag => {
|
|
36
|
+
return {
|
|
37
|
+
value: tag.name,
|
|
38
|
+
label: tag.display_name,
|
|
39
|
+
};
|
|
40
|
+
})}
|
|
41
|
+
/>
|
|
42
|
+
</span>
|
|
43
|
+
);
|
|
44
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { useContext, useEffect, useState } from 'react';
|
|
2
|
+
import DJClientContext from '../../providers/djclient';
|
|
3
|
+
import Control from './FieldControl';
|
|
4
|
+
|
|
5
|
+
import Select from 'react-select';
|
|
6
|
+
|
|
7
|
+
export default function UserSelect({ onChange, currentUser }) {
|
|
8
|
+
const djClient = useContext(DJClientContext).DataJunctionAPI;
|
|
9
|
+
const [retrieved, setRetrieved] = useState(false);
|
|
10
|
+
const [users, setUsers] = useState([]);
|
|
11
|
+
|
|
12
|
+
useEffect(() => {
|
|
13
|
+
const fetchData = async () => {
|
|
14
|
+
const users = await djClient.users();
|
|
15
|
+
setUsers(users);
|
|
16
|
+
setRetrieved(true);
|
|
17
|
+
};
|
|
18
|
+
fetchData().catch(console.error);
|
|
19
|
+
}, [djClient]);
|
|
20
|
+
|
|
21
|
+
return (
|
|
22
|
+
<span
|
|
23
|
+
className="menu-link"
|
|
24
|
+
style={{ marginLeft: '30px', width: '400px' }}
|
|
25
|
+
data-testid="select-user"
|
|
26
|
+
>
|
|
27
|
+
{retrieved ? (
|
|
28
|
+
<Select
|
|
29
|
+
name="edited_by"
|
|
30
|
+
isClearable
|
|
31
|
+
label="Edited By"
|
|
32
|
+
components={{ Control }}
|
|
33
|
+
onChange={e => onChange(e)}
|
|
34
|
+
defaultValue={{
|
|
35
|
+
value: currentUser,
|
|
36
|
+
label: currentUser,
|
|
37
|
+
}}
|
|
38
|
+
options={users?.map(user => {
|
|
39
|
+
return { value: user.username, label: user.username };
|
|
40
|
+
})}
|
|
41
|
+
/>
|
|
42
|
+
) : (
|
|
43
|
+
''
|
|
44
|
+
)}
|
|
45
|
+
</span>
|
|
46
|
+
);
|
|
47
|
+
}
|
|
@@ -8,7 +8,11 @@ import userEvent from '@testing-library/user-event';
|
|
|
8
8
|
const mockDjClient = {
|
|
9
9
|
namespaces: jest.fn(),
|
|
10
10
|
namespace: jest.fn(),
|
|
11
|
+
listNodesForLanding: jest.fn(),
|
|
11
12
|
addNamespace: jest.fn(),
|
|
13
|
+
whoami: jest.fn(),
|
|
14
|
+
users: jest.fn(),
|
|
15
|
+
listTags: jest.fn(),
|
|
12
16
|
};
|
|
13
17
|
|
|
14
18
|
describe('NamespacePage', () => {
|
|
@@ -34,6 +38,15 @@ describe('NamespacePage', () => {
|
|
|
34
38
|
|
|
35
39
|
beforeEach(() => {
|
|
36
40
|
fetch.resetMocks();
|
|
41
|
+
mockDjClient.whoami.mockResolvedValue({ username: 'dj' });
|
|
42
|
+
mockDjClient.users.mockResolvedValue([
|
|
43
|
+
{ username: 'dj' },
|
|
44
|
+
{ username: 'user1' },
|
|
45
|
+
]);
|
|
46
|
+
mockDjClient.listTags.mockResolvedValue([
|
|
47
|
+
{ name: 'tag1' },
|
|
48
|
+
{ name: 'tag2' },
|
|
49
|
+
]);
|
|
37
50
|
mockDjClient.namespaces.mockResolvedValue([
|
|
38
51
|
{
|
|
39
52
|
namespace: 'common.one',
|
|
@@ -75,8 +88,43 @@ describe('NamespacePage', () => {
|
|
|
75
88
|
type: 'transform',
|
|
76
89
|
mode: 'active',
|
|
77
90
|
updated_at: new Date(),
|
|
91
|
+
tags: [{ name: 'tag1' }],
|
|
92
|
+
edited_by: ['dj'],
|
|
78
93
|
},
|
|
79
94
|
]);
|
|
95
|
+
mockDjClient.listNodesForLanding.mockResolvedValue({
|
|
96
|
+
data: {
|
|
97
|
+
findNodesPaginated: {
|
|
98
|
+
pageInfo: {
|
|
99
|
+
hasNextPage: true,
|
|
100
|
+
endCursor:
|
|
101
|
+
'eyJjcmVhdGVkX2F0IjogIjIwMjQtMDQtMTZUMjM6MjI6MjIuNDQxNjg2KzAwOjAwIiwgImlkIjogNjE0fQ==',
|
|
102
|
+
hasPrevPage: true,
|
|
103
|
+
startCursor:
|
|
104
|
+
'eyJjcmVhdGVkX2F0IjogIjIwMjQtMTAtMTZUMTY6MDM6MTcuMDgzMjY3KzAwOjAwIiwgImlkIjogMjQwOX0=',
|
|
105
|
+
},
|
|
106
|
+
edges: [
|
|
107
|
+
{
|
|
108
|
+
node: {
|
|
109
|
+
name: 'default.test_node',
|
|
110
|
+
type: 'DIMENSION',
|
|
111
|
+
currentVersion: 'v4.0',
|
|
112
|
+
tags: [],
|
|
113
|
+
editedBy: ['dj'],
|
|
114
|
+
current: {
|
|
115
|
+
displayName: 'Test Node',
|
|
116
|
+
status: 'VALID',
|
|
117
|
+
updatedAt: '2024-10-18T15:15:33.532949+00:00',
|
|
118
|
+
},
|
|
119
|
+
createdBy: {
|
|
120
|
+
username: 'dj',
|
|
121
|
+
},
|
|
122
|
+
},
|
|
123
|
+
},
|
|
124
|
+
],
|
|
125
|
+
},
|
|
126
|
+
},
|
|
127
|
+
});
|
|
80
128
|
});
|
|
81
129
|
|
|
82
130
|
afterEach(() => {
|
|
@@ -98,28 +146,59 @@ describe('NamespacePage', () => {
|
|
|
98
146
|
</MemoryRouter>,
|
|
99
147
|
);
|
|
100
148
|
|
|
101
|
-
await waitFor(
|
|
102
|
-
|
|
103
|
-
|
|
149
|
+
await waitFor(
|
|
150
|
+
() => {
|
|
151
|
+
expect(mockDjClient.listNodesForLanding).toHaveBeenCalled();
|
|
152
|
+
expect(screen.getByText('Namespaces')).toBeInTheDocument();
|
|
104
153
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
154
|
+
// check that it displays namespaces
|
|
155
|
+
expect(screen.getByText('common')).toBeInTheDocument();
|
|
156
|
+
expect(screen.getByText('one')).toBeInTheDocument();
|
|
157
|
+
expect(screen.getByText('fruits')).toBeInTheDocument();
|
|
158
|
+
expect(screen.getByText('vegetables')).toBeInTheDocument();
|
|
110
159
|
|
|
111
|
-
|
|
112
|
-
|
|
160
|
+
// check that it renders nodes
|
|
161
|
+
expect(screen.getByText('Test Node')).toBeInTheDocument();
|
|
113
162
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
});
|
|
163
|
+
// check that it sorts nodes
|
|
164
|
+
fireEvent.click(screen.getByText('name'));
|
|
165
|
+
fireEvent.click(screen.getByText('name'));
|
|
166
|
+
fireEvent.click(screen.getByText('display Name'));
|
|
119
167
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
168
|
+
// paginate
|
|
169
|
+
const previousButton = screen.getByText('← Previous');
|
|
170
|
+
expect(previousButton).toBeDefined();
|
|
171
|
+
fireEvent.click(previousButton);
|
|
172
|
+
const nextButton = screen.getByText('Next →');
|
|
173
|
+
expect(nextButton).toBeDefined();
|
|
174
|
+
fireEvent.click(nextButton);
|
|
175
|
+
|
|
176
|
+
// check that we can filter by node type
|
|
177
|
+
const selectNodeType = screen.getAllByTestId('select-node-type')[0];
|
|
178
|
+
expect(selectNodeType).toBeDefined();
|
|
179
|
+
expect(selectNodeType).not.toBeNull();
|
|
180
|
+
fireEvent.keyDown(selectNodeType.firstChild, { key: 'ArrowDown' });
|
|
181
|
+
fireEvent.click(screen.getByText('Source'));
|
|
182
|
+
|
|
183
|
+
// check that we can filter by tag
|
|
184
|
+
const selectTag = screen.getAllByTestId('select-tag')[0];
|
|
185
|
+
expect(selectTag).toBeDefined();
|
|
186
|
+
expect(selectTag).not.toBeNull();
|
|
187
|
+
fireEvent.keyDown(selectTag.firstChild, { key: 'ArrowDown' });
|
|
188
|
+
|
|
189
|
+
// check that we can filter by user
|
|
190
|
+
const selectUser = screen.getAllByTestId('select-user')[0];
|
|
191
|
+
expect(selectUser).toBeDefined();
|
|
192
|
+
expect(selectUser).not.toBeNull();
|
|
193
|
+
fireEvent.keyDown(selectUser.firstChild, { key: 'ArrowDown' });
|
|
194
|
+
|
|
195
|
+
// click to open and close tab
|
|
196
|
+
fireEvent.click(screen.getByText('common'));
|
|
197
|
+
fireEvent.click(screen.getByText('common'));
|
|
198
|
+
},
|
|
199
|
+
{ timeout: 3000 },
|
|
200
|
+
);
|
|
201
|
+
}, 60000);
|
|
123
202
|
|
|
124
203
|
it('can add new namespace via add namespace popover', async () => {
|
|
125
204
|
mockDjClient.addNamespace.mockReturnValue({
|
|
@@ -167,7 +246,7 @@ describe('NamespacePage', () => {
|
|
|
167
246
|
});
|
|
168
247
|
expect(mockDjClient.addNamespace).toHaveBeenCalled();
|
|
169
248
|
expect(mockDjClient.addNamespace).toHaveBeenCalledWith(
|
|
170
|
-
'some.random.namespace',
|
|
249
|
+
'test.namespace.some.random.namespace',
|
|
171
250
|
);
|
|
172
251
|
expect(screen.getByText('Saved')).toBeInTheDocument();
|
|
173
252
|
expect(window.location.reload).toHaveBeenCalled();
|