datajunction-ui 0.0.1-a1 → 0.0.1-a100
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 +61 -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
package/Makefile
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "datajunction-ui",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.1a100",
|
|
4
4
|
"description": "DataJunction Metrics Platform UI",
|
|
5
5
|
"module": "src/index.tsx",
|
|
6
6
|
"repository": {
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
"@testing-library/jest-dom": "6.1.2",
|
|
29
29
|
"@testing-library/react": "14.0.0",
|
|
30
30
|
"@types/fontfaceobserver": "^2.1.0",
|
|
31
|
-
"@types/jest": "
|
|
31
|
+
"@types/jest": "29.5.14",
|
|
32
32
|
"@types/node": "^14.18.27",
|
|
33
33
|
"@types/react": "^18.0.20",
|
|
34
34
|
"@types/react-dom": "^18.0.6",
|
|
@@ -54,6 +54,7 @@
|
|
|
54
54
|
"file-loader": "6.2.0",
|
|
55
55
|
"fontfaceobserver": "2.3.0",
|
|
56
56
|
"formik": "2.4.3",
|
|
57
|
+
"fuse.js": "6.6.2",
|
|
57
58
|
"husky": "8.0.1",
|
|
58
59
|
"i18next": "21.9.2",
|
|
59
60
|
"i18next-browser-languagedetector": "6.1.5",
|
|
@@ -68,10 +69,12 @@
|
|
|
68
69
|
"react": "18.2.0",
|
|
69
70
|
"react-app-polyfill": "3.0.0",
|
|
70
71
|
"react-cookie": "4.1.1",
|
|
72
|
+
"react-diff-view": "3.2.1",
|
|
71
73
|
"react-dom": "18.2.0",
|
|
72
74
|
"react-helmet-async": "1.3.0",
|
|
73
75
|
"react-i18next": "11.18.6",
|
|
74
76
|
"react-is": "18.2.0",
|
|
77
|
+
"react-markdown": "9.0.1",
|
|
75
78
|
"react-querybuilder": "6.5.1",
|
|
76
79
|
"react-redux": "7.2.8",
|
|
77
80
|
"react-router-dom": "6.3.0",
|
|
@@ -80,7 +83,7 @@
|
|
|
80
83
|
"react-syntax-highlighter": "^15.5.0",
|
|
81
84
|
"react-test-renderer": "18.2.0",
|
|
82
85
|
"reactflow": "^11.7.0",
|
|
83
|
-
"redux-injectors": "1.
|
|
86
|
+
"redux-injectors": "2.1.0",
|
|
84
87
|
"redux-saga": "1.2.1",
|
|
85
88
|
"rimraf": "3.0.2",
|
|
86
89
|
"sanitize.css": "13.0.0",
|
|
@@ -95,6 +98,7 @@
|
|
|
95
98
|
"ts-loader": "9.4.2",
|
|
96
99
|
"ts-node": "10.9.1",
|
|
97
100
|
"typescript": "4.6.4",
|
|
101
|
+
"unidiff": "1.0.4",
|
|
98
102
|
"web-vitals": "2.1.4",
|
|
99
103
|
"webpack": "5.81.0",
|
|
100
104
|
"webpack-cli": "5.0.2",
|
|
@@ -161,11 +165,17 @@
|
|
|
161
165
|
],
|
|
162
166
|
"coverageThreshold": {
|
|
163
167
|
"global": {
|
|
164
|
-
"statements":
|
|
165
|
-
"branches":
|
|
166
|
-
"lines":
|
|
167
|
-
"functions":
|
|
168
|
+
"statements": 80,
|
|
169
|
+
"branches": 70,
|
|
170
|
+
"lines": 80,
|
|
171
|
+
"functions": 80
|
|
168
172
|
}
|
|
173
|
+
},
|
|
174
|
+
"moduleNameMapper": {
|
|
175
|
+
"unist-util-visit-parents/do-not-use-color": "<rootDir>/node_modules/unist-util-visit-parents/lib/color.js",
|
|
176
|
+
"^#minpath$": "<rootDir>/node_modules/vfile/lib/minpath.browser.js",
|
|
177
|
+
"^#minproc$": "<rootDir>/node_modules/vfile/lib/minproc.browser.js",
|
|
178
|
+
"^#minurl$": "<rootDir>/node_modules/vfile/lib/minurl.browser.js"
|
|
169
179
|
}
|
|
170
180
|
},
|
|
171
181
|
"resolutions": {
|
|
@@ -183,6 +193,7 @@
|
|
|
183
193
|
"html-webpack-plugin": "5.5.1",
|
|
184
194
|
"jest": "^29.5.0",
|
|
185
195
|
"jest-fetch-mock": "3.0.3",
|
|
196
|
+
"jest-watch-typeahead": "2.2.2",
|
|
186
197
|
"mini-css-extract-plugin": "2.7.6",
|
|
187
198
|
"resize-observer-polyfill": "1.5.1"
|
|
188
199
|
}
|
package/public/index.html
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
<link rel="icon" href="public/favicon.ico" />
|
|
7
7
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
8
8
|
<meta name="theme-color" content="#000000" />
|
|
9
|
-
<link rel="manifest" href="
|
|
9
|
+
<link rel="manifest" href="manifest.json" />
|
|
10
10
|
<link rel="manifest" href="src/styles/index.css" />
|
|
11
11
|
<meta property="og:title" content="DataJunction UI" />
|
|
12
12
|
<meta property="og:description" content="" />
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
export default function AddNodeDropdown({ namespace }) {
|
|
2
|
+
return (
|
|
3
|
+
<span
|
|
4
|
+
className="menu-link"
|
|
5
|
+
style={{ margin: '0.5em 0 0 1em', width: '130px' }}
|
|
6
|
+
>
|
|
7
|
+
<span className="menu-title">
|
|
8
|
+
<div className="dropdown">
|
|
9
|
+
<span className="add_node">+ Add Node</span>
|
|
10
|
+
<div className="dropdown-content">
|
|
11
|
+
<a href={`/create/source`}>
|
|
12
|
+
<div className="node_type__source node_type_creation_heading">
|
|
13
|
+
Register Table
|
|
14
|
+
</div>
|
|
15
|
+
</a>
|
|
16
|
+
<a href={`/create/transform/${namespace}`}>
|
|
17
|
+
<div className="node_type__transform node_type_creation_heading">
|
|
18
|
+
Transform
|
|
19
|
+
</div>
|
|
20
|
+
</a>
|
|
21
|
+
<a href={`/create/metric/${namespace}`}>
|
|
22
|
+
<div className="node_type__metric node_type_creation_heading">
|
|
23
|
+
Metric
|
|
24
|
+
</div>
|
|
25
|
+
</a>
|
|
26
|
+
<a href={`/create/dimension/${namespace}`}>
|
|
27
|
+
<div className="node_type__dimension node_type_creation_heading">
|
|
28
|
+
Dimension
|
|
29
|
+
</div>
|
|
30
|
+
</a>
|
|
31
|
+
<a href={`/create/tag`}>
|
|
32
|
+
<div className="entity__tag node_type_creation_heading">Tag</div>
|
|
33
|
+
</a>
|
|
34
|
+
<a href={`/create/cube/${namespace}`}>
|
|
35
|
+
<div className="node_type__cube node_type_creation_heading">
|
|
36
|
+
Cube
|
|
37
|
+
</div>
|
|
38
|
+
</a>
|
|
39
|
+
</div>
|
|
40
|
+
</div>
|
|
41
|
+
</span>
|
|
42
|
+
</span>
|
|
43
|
+
);
|
|
44
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Component } from 'react';
|
|
2
|
+
import Markdown from 'react-markdown';
|
|
2
3
|
|
|
3
4
|
export default class ListGroupItem extends Component {
|
|
4
5
|
render() {
|
|
@@ -14,7 +15,7 @@ export default class ListGroupItem extends Component {
|
|
|
14
15
|
aria-hidden="false"
|
|
15
16
|
aria-label={label}
|
|
16
17
|
>
|
|
17
|
-
{value}
|
|
18
|
+
<Markdown>{value}</Markdown>
|
|
18
19
|
</p>
|
|
19
20
|
</div>
|
|
20
21
|
</div>
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import DJClientContext from '../providers/djclient';
|
|
2
|
+
import * as React from 'react';
|
|
3
|
+
import DeleteIcon from '../icons/DeleteIcon';
|
|
4
|
+
import EditIcon from '../icons/EditIcon';
|
|
5
|
+
import { Form, Formik } from 'formik';
|
|
6
|
+
import { useContext } from 'react';
|
|
7
|
+
import { displayMessageAfterSubmit } from '../../utils/form';
|
|
8
|
+
|
|
9
|
+
export default function NodeListActions({ nodeName }) {
|
|
10
|
+
const [editButton, setEditButton] = React.useState(<EditIcon />);
|
|
11
|
+
const [deleteButton, setDeleteButton] = React.useState(<DeleteIcon />);
|
|
12
|
+
|
|
13
|
+
const djClient = useContext(DJClientContext).DataJunctionAPI;
|
|
14
|
+
const deleteNode = async (values, { setStatus }) => {
|
|
15
|
+
if (
|
|
16
|
+
!window.confirm('Deleting node ' + values.nodeName + '. Are you sure?')
|
|
17
|
+
) {
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
const { status, json } = await djClient.deactivate(values.nodeName);
|
|
21
|
+
if (status === 200 || status === 201 || status === 204) {
|
|
22
|
+
setStatus({
|
|
23
|
+
success: <>Successfully deleted node {values.nodeName}</>,
|
|
24
|
+
});
|
|
25
|
+
setEditButton(''); // hide the Edit button
|
|
26
|
+
setDeleteButton(''); // hide the Delete button
|
|
27
|
+
} else {
|
|
28
|
+
setStatus({
|
|
29
|
+
failure: `${json.message}`,
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const initialValues = {
|
|
35
|
+
nodeName: nodeName,
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
return (
|
|
39
|
+
<div>
|
|
40
|
+
<a href={`/nodes/${nodeName}/edit`} style={{ marginLeft: '0.5rem' }}>
|
|
41
|
+
{editButton}
|
|
42
|
+
</a>
|
|
43
|
+
<Formik initialValues={initialValues} onSubmit={deleteNode}>
|
|
44
|
+
{function Render({ status, setFieldValue }) {
|
|
45
|
+
return (
|
|
46
|
+
<Form className="deleteNode">
|
|
47
|
+
{displayMessageAfterSubmit(status)}
|
|
48
|
+
{
|
|
49
|
+
<>
|
|
50
|
+
<button
|
|
51
|
+
type="submit"
|
|
52
|
+
style={{
|
|
53
|
+
marginLeft: 0,
|
|
54
|
+
all: 'unset',
|
|
55
|
+
color: '#005c72',
|
|
56
|
+
cursor: 'pointer',
|
|
57
|
+
}}
|
|
58
|
+
>
|
|
59
|
+
{deleteButton}
|
|
60
|
+
</button>
|
|
61
|
+
</>
|
|
62
|
+
}
|
|
63
|
+
</Form>
|
|
64
|
+
);
|
|
65
|
+
}}
|
|
66
|
+
</Formik>
|
|
67
|
+
</div>
|
|
68
|
+
);
|
|
69
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import DJClientContext from '../providers/djclient';
|
|
2
|
+
import * as React from 'react';
|
|
3
|
+
import DeleteIcon from '../icons/DeleteIcon';
|
|
4
|
+
import { Form, Formik } from 'formik';
|
|
5
|
+
import { useContext } from 'react';
|
|
6
|
+
import { displayMessageAfterSubmit } from '../../utils/form';
|
|
7
|
+
|
|
8
|
+
export default function NodeMaterializationDelete({
|
|
9
|
+
nodeName,
|
|
10
|
+
materializationName,
|
|
11
|
+
}) {
|
|
12
|
+
const [deleteButton, setDeleteButton] = React.useState(<DeleteIcon />);
|
|
13
|
+
|
|
14
|
+
const djClient = useContext(DJClientContext).DataJunctionAPI;
|
|
15
|
+
const deleteNode = async (values, { setStatus }) => {
|
|
16
|
+
if (
|
|
17
|
+
!window.confirm(
|
|
18
|
+
'Deleting materialization job ' +
|
|
19
|
+
values.materializationName +
|
|
20
|
+
'. Are you sure?',
|
|
21
|
+
)
|
|
22
|
+
) {
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
const { status, json } = await djClient.deleteMaterialization(
|
|
26
|
+
values.nodeName,
|
|
27
|
+
values.materializationName,
|
|
28
|
+
);
|
|
29
|
+
if (status === 200 || status === 201 || status === 204) {
|
|
30
|
+
window.location.reload();
|
|
31
|
+
setStatus({
|
|
32
|
+
success: (
|
|
33
|
+
<>
|
|
34
|
+
Successfully deleted materialization job:{' '}
|
|
35
|
+
{values.materializationName}
|
|
36
|
+
</>
|
|
37
|
+
),
|
|
38
|
+
});
|
|
39
|
+
setDeleteButton(''); // hide the Delete button
|
|
40
|
+
} else {
|
|
41
|
+
setStatus({
|
|
42
|
+
failure: `${json.message}`,
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const initialValues = {
|
|
48
|
+
nodeName: nodeName,
|
|
49
|
+
materializationName: materializationName,
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
return (
|
|
53
|
+
<div>
|
|
54
|
+
<Formik initialValues={initialValues} onSubmit={deleteNode}>
|
|
55
|
+
{function Render({ status, setFieldValue }) {
|
|
56
|
+
return (
|
|
57
|
+
<Form className="deleteNode">
|
|
58
|
+
{displayMessageAfterSubmit(status)}
|
|
59
|
+
{
|
|
60
|
+
<>
|
|
61
|
+
<button
|
|
62
|
+
type="submit"
|
|
63
|
+
style={{
|
|
64
|
+
marginLeft: 0,
|
|
65
|
+
all: 'unset',
|
|
66
|
+
color: '#005c72',
|
|
67
|
+
cursor: 'pointer',
|
|
68
|
+
}}
|
|
69
|
+
>
|
|
70
|
+
{deleteButton}
|
|
71
|
+
</button>
|
|
72
|
+
</>
|
|
73
|
+
}
|
|
74
|
+
</Form>
|
|
75
|
+
);
|
|
76
|
+
}}
|
|
77
|
+
</Formik>
|
|
78
|
+
</div>
|
|
79
|
+
);
|
|
80
|
+
}
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import { Light as SyntaxHighlighter } from 'react-syntax-highlighter';
|
|
2
|
+
import { solarizedDark } from 'react-syntax-highlighter/src/styles/hljs';
|
|
3
|
+
import React from 'react';
|
|
4
|
+
|
|
1
5
|
export default function QueryInfo({
|
|
2
6
|
id,
|
|
3
7
|
state,
|
|
@@ -8,9 +12,11 @@ export default function QueryInfo({
|
|
|
8
12
|
output_table,
|
|
9
13
|
scheduled,
|
|
10
14
|
started,
|
|
15
|
+
finished,
|
|
11
16
|
numRows,
|
|
17
|
+
isList = false,
|
|
12
18
|
}) {
|
|
13
|
-
return (
|
|
19
|
+
return isList === false ? (
|
|
14
20
|
<div className="table-responsive">
|
|
15
21
|
<table className="card-inner-table table">
|
|
16
22
|
<thead className="fs-7 fw-bold text-gray-400 border-bottom-0">
|
|
@@ -73,5 +79,94 @@ export default function QueryInfo({
|
|
|
73
79
|
</tbody>
|
|
74
80
|
</table>
|
|
75
81
|
</div>
|
|
82
|
+
) : (
|
|
83
|
+
<div className="rightbottom">
|
|
84
|
+
<ul style={{ padding: '20px' }}>
|
|
85
|
+
<li className={'query-info'}>
|
|
86
|
+
<label>Query ID</label>{' '}
|
|
87
|
+
<span className="tag_value rounded-pill badge">
|
|
88
|
+
{links?.length ? (
|
|
89
|
+
<a
|
|
90
|
+
href={links[links.length - 1]}
|
|
91
|
+
target={'_blank'}
|
|
92
|
+
rel="noreferrer"
|
|
93
|
+
>
|
|
94
|
+
{id}
|
|
95
|
+
</a>
|
|
96
|
+
) : (
|
|
97
|
+
id
|
|
98
|
+
)}
|
|
99
|
+
</span>
|
|
100
|
+
</li>
|
|
101
|
+
<li className={'query-info'}>
|
|
102
|
+
<label>State</label>
|
|
103
|
+
<span className="tag_value rounded-pill badge">{state}</span>
|
|
104
|
+
</li>
|
|
105
|
+
<li className={'query-info'}>
|
|
106
|
+
<label>Engine</label>{' '}
|
|
107
|
+
<span className="tag_value rounded-pill badge">
|
|
108
|
+
{engine_name}
|
|
109
|
+
{' - '}
|
|
110
|
+
{engine_version}
|
|
111
|
+
</span>
|
|
112
|
+
</li>
|
|
113
|
+
<li className={'query-info'}>
|
|
114
|
+
<label>Scheduled</label> {scheduled}
|
|
115
|
+
</li>
|
|
116
|
+
<li className={'query-info'}>
|
|
117
|
+
<label>Started</label> {started}
|
|
118
|
+
</li>
|
|
119
|
+
<li className={'query-info'}>
|
|
120
|
+
<label>Finished</label> {finished}
|
|
121
|
+
</li>
|
|
122
|
+
<li className={'query-info'}>
|
|
123
|
+
<label>Logs</label>{' '}
|
|
124
|
+
{errors?.length ? (
|
|
125
|
+
errors.map(error => (
|
|
126
|
+
<div
|
|
127
|
+
style={{
|
|
128
|
+
height: '800px',
|
|
129
|
+
width: '80%',
|
|
130
|
+
overflow: 'scroll',
|
|
131
|
+
borderRadius: '0',
|
|
132
|
+
border: '1px solid #ccc',
|
|
133
|
+
}}
|
|
134
|
+
className="queryrunner-query"
|
|
135
|
+
>
|
|
136
|
+
<SyntaxHighlighter
|
|
137
|
+
language="javascript"
|
|
138
|
+
style={solarizedDark}
|
|
139
|
+
wrapLines={true}
|
|
140
|
+
>
|
|
141
|
+
{error}
|
|
142
|
+
</SyntaxHighlighter>
|
|
143
|
+
</div>
|
|
144
|
+
))
|
|
145
|
+
) : (
|
|
146
|
+
<></>
|
|
147
|
+
)}
|
|
148
|
+
</li>
|
|
149
|
+
<li className={'query-info'}>
|
|
150
|
+
<label>Links:</label>{' '}
|
|
151
|
+
{links?.length ? (
|
|
152
|
+
links.map((link, idx) => (
|
|
153
|
+
<p key={idx}>
|
|
154
|
+
<a href={link} target="_blank" rel="noreferrer">
|
|
155
|
+
{link}
|
|
156
|
+
</a>
|
|
157
|
+
</p>
|
|
158
|
+
))
|
|
159
|
+
) : (
|
|
160
|
+
<></>
|
|
161
|
+
)}
|
|
162
|
+
</li>
|
|
163
|
+
<li className={'query-info'}>
|
|
164
|
+
<label>Output Table:</label> {output_table}
|
|
165
|
+
</li>
|
|
166
|
+
<li className={'query-info'}>
|
|
167
|
+
<label>Rows:</label> {numRows}
|
|
168
|
+
</li>
|
|
169
|
+
</ul>
|
|
170
|
+
</div>
|
|
76
171
|
);
|
|
77
172
|
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { useState, useEffect, useContext } from 'react';
|
|
2
|
+
import DJClientContext from '../providers/djclient';
|
|
3
|
+
import Fuse from 'fuse.js';
|
|
4
|
+
|
|
5
|
+
import './search.css';
|
|
6
|
+
|
|
7
|
+
export default function Search() {
|
|
8
|
+
const [fuse, setFuse] = useState();
|
|
9
|
+
const [searchValue, setSearchValue] = useState('');
|
|
10
|
+
const [searchResults, setSearchResults] = useState([]);
|
|
11
|
+
|
|
12
|
+
const djClient = useContext(DJClientContext).DataJunctionAPI;
|
|
13
|
+
|
|
14
|
+
const truncate = str => {
|
|
15
|
+
if (str === null) {
|
|
16
|
+
return '';
|
|
17
|
+
}
|
|
18
|
+
return str.length > 100 ? str.substring(0, 90) + '...' : str;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
useEffect(() => {
|
|
22
|
+
const fetchNodes = async () => {
|
|
23
|
+
try {
|
|
24
|
+
const [data, tags] = await Promise.all([
|
|
25
|
+
djClient.nodeDetails(),
|
|
26
|
+
djClient.listTags(),
|
|
27
|
+
]);
|
|
28
|
+
const allEntities = data.concat(
|
|
29
|
+
(tags || []).map(tag => {
|
|
30
|
+
tag.type = 'tag';
|
|
31
|
+
return tag;
|
|
32
|
+
}),
|
|
33
|
+
);
|
|
34
|
+
const fuse = new Fuse(allEntities || [], {
|
|
35
|
+
keys: [
|
|
36
|
+
'name', // will be assigned a `weight` of 1
|
|
37
|
+
{ name: 'description', weight: 2 },
|
|
38
|
+
{ name: 'display_name', weight: 3 },
|
|
39
|
+
{ name: 'type', weight: 4 },
|
|
40
|
+
{ name: 'tag_type', weight: 5 },
|
|
41
|
+
],
|
|
42
|
+
});
|
|
43
|
+
setFuse(fuse);
|
|
44
|
+
} catch (error) {
|
|
45
|
+
console.error('Error fetching nodes or tags:', error);
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
fetchNodes();
|
|
49
|
+
}, []);
|
|
50
|
+
|
|
51
|
+
const handleChange = e => {
|
|
52
|
+
setSearchValue(e.target.value);
|
|
53
|
+
if (fuse) {
|
|
54
|
+
setSearchResults(fuse.search(e.target.value).map(result => result.item));
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
return (
|
|
59
|
+
<div>
|
|
60
|
+
<form
|
|
61
|
+
className="search-box"
|
|
62
|
+
onSubmit={e => {
|
|
63
|
+
e.preventDefault();
|
|
64
|
+
}}
|
|
65
|
+
>
|
|
66
|
+
<input
|
|
67
|
+
type="text"
|
|
68
|
+
placeholder="Search"
|
|
69
|
+
name="search"
|
|
70
|
+
value={searchValue}
|
|
71
|
+
onChange={handleChange}
|
|
72
|
+
/>
|
|
73
|
+
</form>
|
|
74
|
+
<div className="search-results">
|
|
75
|
+
{searchResults.map(item => {
|
|
76
|
+
const itemUrl =
|
|
77
|
+
item.type !== 'tag' ? `/nodes/${item.name}` : `/tags/${item.name}`;
|
|
78
|
+
return (
|
|
79
|
+
<a href={itemUrl}>
|
|
80
|
+
<div key={item.name} className="search-result-item">
|
|
81
|
+
<span className={`node_type__${item.type} badge node_type`}>
|
|
82
|
+
{item.type}
|
|
83
|
+
</span>
|
|
84
|
+
{item.display_name} (<b>{item.name}</b>){' '}
|
|
85
|
+
{item.description ? '- ' : ' '}
|
|
86
|
+
{truncate(item.description || '')}
|
|
87
|
+
</div>
|
|
88
|
+
</a>
|
|
89
|
+
);
|
|
90
|
+
})}
|
|
91
|
+
</div>
|
|
92
|
+
</div>
|
|
93
|
+
);
|
|
94
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
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 NodeListActions from '../NodeListActions';
|
|
8
|
+
|
|
9
|
+
describe('<NodeListActions />', () => {
|
|
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
|
+
<NodeListActions 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
|
+
global.confirm = () => true;
|
|
34
|
+
const mockDjClient = initializeMockDJClient();
|
|
35
|
+
mockDjClient.DataJunctionAPI.deactivate.mockReturnValue({
|
|
36
|
+
status: 204,
|
|
37
|
+
json: { name: 'source.warehouse.schema.some_table' },
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
renderElement(mockDjClient);
|
|
41
|
+
|
|
42
|
+
await userEvent.click(screen.getByRole('button'));
|
|
43
|
+
|
|
44
|
+
await waitFor(() => {
|
|
45
|
+
expect(mockDjClient.DataJunctionAPI.deactivate).toBeCalled();
|
|
46
|
+
expect(mockDjClient.DataJunctionAPI.deactivate).toBeCalledWith(
|
|
47
|
+
'default.hard_hat',
|
|
48
|
+
);
|
|
49
|
+
});
|
|
50
|
+
expect(
|
|
51
|
+
screen.getByText('Successfully deleted node default.hard_hat'),
|
|
52
|
+
).toBeInTheDocument();
|
|
53
|
+
}, 60000);
|
|
54
|
+
|
|
55
|
+
it('skips a node deletion during confirm', async () => {
|
|
56
|
+
global.confirm = () => false;
|
|
57
|
+
const mockDjClient = initializeMockDJClient();
|
|
58
|
+
mockDjClient.DataJunctionAPI.deactivate.mockReturnValue({
|
|
59
|
+
status: 204,
|
|
60
|
+
json: { name: 'source.warehouse.schema.some_table' },
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
renderElement(mockDjClient);
|
|
64
|
+
|
|
65
|
+
await userEvent.click(screen.getByRole('button'));
|
|
66
|
+
|
|
67
|
+
await waitFor(() => {
|
|
68
|
+
expect(mockDjClient.DataJunctionAPI.deactivate).not.toBeCalled();
|
|
69
|
+
});
|
|
70
|
+
}, 60000);
|
|
71
|
+
|
|
72
|
+
it('fail deleting a node when clicked', async () => {
|
|
73
|
+
global.confirm = () => true;
|
|
74
|
+
const mockDjClient = initializeMockDJClient();
|
|
75
|
+
mockDjClient.DataJunctionAPI.deactivate.mockReturnValue({
|
|
76
|
+
status: 777,
|
|
77
|
+
json: { message: 'source.warehouse.schema.some_table' },
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
renderElement(mockDjClient);
|
|
81
|
+
|
|
82
|
+
await userEvent.click(screen.getByRole('button'));
|
|
83
|
+
|
|
84
|
+
await waitFor(() => {
|
|
85
|
+
expect(mockDjClient.DataJunctionAPI.deactivate).toBeCalled();
|
|
86
|
+
expect(mockDjClient.DataJunctionAPI.deactivate).toBeCalledWith(
|
|
87
|
+
'default.hard_hat',
|
|
88
|
+
);
|
|
89
|
+
});
|
|
90
|
+
expect(
|
|
91
|
+
screen.getByText('source.warehouse.schema.some_table'),
|
|
92
|
+
).toBeInTheDocument();
|
|
93
|
+
}, 60000);
|
|
94
|
+
});
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { render, screen, fireEvent } from '@testing-library/react';
|
|
3
|
+
import Search from '../Search';
|
|
4
|
+
import DJClientContext from '../../providers/djclient';
|
|
5
|
+
import { Root } from '../../pages/Root';
|
|
6
|
+
import { HelmetProvider } from 'react-helmet-async';
|
|
7
|
+
|
|
8
|
+
describe('<Search />', () => {
|
|
9
|
+
const mockDjClient = {
|
|
10
|
+
logout: jest.fn(),
|
|
11
|
+
nodeDetails: async () => [
|
|
12
|
+
{
|
|
13
|
+
name: 'default.repair_orders',
|
|
14
|
+
display_name: 'Default: Repair Orders',
|
|
15
|
+
description: 'Repair orders',
|
|
16
|
+
version: 'v1.0',
|
|
17
|
+
type: 'source',
|
|
18
|
+
status: 'valid',
|
|
19
|
+
mode: 'published',
|
|
20
|
+
updated_at: '2023-08-21T16:48:52.880498+00:00',
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
name: 'default.repair_order_details',
|
|
24
|
+
display_name: 'Default: Repair Order Details',
|
|
25
|
+
description: 'Details on repair orders',
|
|
26
|
+
version: 'v1.0',
|
|
27
|
+
type: 'source',
|
|
28
|
+
status: 'valid',
|
|
29
|
+
mode: 'published',
|
|
30
|
+
updated_at: '2023-08-21T16:48:52.981201+00:00',
|
|
31
|
+
},
|
|
32
|
+
],
|
|
33
|
+
listTags: async () => [
|
|
34
|
+
{
|
|
35
|
+
description: 'something',
|
|
36
|
+
display_name: 'Report A',
|
|
37
|
+
tag_metadata: {},
|
|
38
|
+
name: 'report.a',
|
|
39
|
+
tag_type: 'report',
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
description: 'report B',
|
|
43
|
+
display_name: 'Report B',
|
|
44
|
+
tag_metadata: {},
|
|
45
|
+
name: 'report.b',
|
|
46
|
+
tag_type: 'report',
|
|
47
|
+
},
|
|
48
|
+
],
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
it('displays search results correctly', () => {
|
|
52
|
+
render(
|
|
53
|
+
<HelmetProvider>
|
|
54
|
+
<DJClientContext.Provider value={{ DataJunctionAPI: mockDjClient }}>
|
|
55
|
+
<Root />
|
|
56
|
+
</DJClientContext.Provider>
|
|
57
|
+
</HelmetProvider>,
|
|
58
|
+
);
|
|
59
|
+
const searchInput = screen.queryByPlaceholderText('Search');
|
|
60
|
+
fireEvent.change(searchInput, { target: { value: 'Repair' } });
|
|
61
|
+
expect(searchInput.value).toBe('Repair');
|
|
62
|
+
});
|
|
63
|
+
});
|