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.
Files changed (110) hide show
  1. package/Makefile +7 -1
  2. package/package.json +18 -7
  3. package/public/index.html +1 -1
  4. package/src/app/components/AddNodeDropdown.jsx +44 -0
  5. package/src/app/components/ListGroupItem.jsx +2 -1
  6. package/src/app/components/NodeListActions.jsx +69 -0
  7. package/src/app/components/NodeMaterializationDelete.jsx +80 -0
  8. package/src/app/components/QueryInfo.jsx +96 -1
  9. package/src/app/components/Search.jsx +94 -0
  10. package/src/app/components/__tests__/NodeListActions.test.jsx +94 -0
  11. package/src/app/components/__tests__/Search.test.jsx +63 -0
  12. package/src/app/components/__tests__/__snapshots__/ListGroupItem.test.tsx.snap +5 -3
  13. package/src/app/components/djgraph/Collapse.jsx +3 -2
  14. package/src/app/components/djgraph/DJNode.jsx +1 -1
  15. package/src/app/components/djgraph/DJNodeColumns.jsx +5 -1
  16. package/src/app/components/djgraph/LayoutFlow.jsx +5 -3
  17. package/src/app/components/forms/Action.jsx +8 -0
  18. package/src/app/components/forms/NodeNameField.jsx +64 -0
  19. package/src/app/components/search.css +17 -0
  20. package/src/app/icons/AddItemIcon.jsx +16 -0
  21. package/src/app/icons/CommitIcon.jsx +45 -0
  22. package/src/app/icons/DiffIcon.jsx +63 -0
  23. package/src/app/icons/EyeIcon.jsx +20 -0
  24. package/src/app/icons/FilterIcon.jsx +7 -0
  25. package/src/app/icons/JupyterExportIcon.jsx +25 -0
  26. package/src/app/icons/LoadingIcon.jsx +10 -10
  27. package/src/app/icons/PythonIcon.jsx +6 -44
  28. package/src/app/index.tsx +24 -0
  29. package/src/app/pages/AddEditNodePage/AlertMessage.jsx +10 -0
  30. package/src/app/pages/AddEditNodePage/ColumnsSelect.jsx +84 -0
  31. package/src/app/pages/AddEditNodePage/DescriptionField.jsx +17 -0
  32. package/src/app/pages/AddEditNodePage/DisplayNameField.jsx +16 -0
  33. package/src/app/pages/AddEditNodePage/FormikSelect.jsx +5 -0
  34. package/src/app/pages/AddEditNodePage/FullNameField.jsx +3 -2
  35. package/src/app/pages/AddEditNodePage/Loadable.jsx +6 -2
  36. package/src/app/pages/AddEditNodePage/MetricMetadataFields.jsx +75 -0
  37. package/src/app/pages/AddEditNodePage/MetricQueryField.jsx +71 -0
  38. package/src/app/pages/AddEditNodePage/NamespaceField.jsx +40 -0
  39. package/src/app/pages/AddEditNodePage/NodeModeField.jsx +14 -0
  40. package/src/app/pages/AddEditNodePage/NodeQueryField.jsx +8 -3
  41. package/src/app/pages/AddEditNodePage/RequiredDimensionsSelect.jsx +54 -0
  42. package/src/app/pages/AddEditNodePage/TagsField.jsx +47 -0
  43. package/src/app/pages/AddEditNodePage/UpstreamNodeField.jsx +49 -0
  44. package/src/app/pages/AddEditNodePage/__tests__/AddEditNodePageFormFailed.test.jsx +15 -9
  45. package/src/app/pages/AddEditNodePage/__tests__/AddEditNodePageFormSuccess.test.jsx +167 -24
  46. package/src/app/pages/AddEditNodePage/__tests__/index.test.jsx +55 -25
  47. package/src/app/pages/AddEditNodePage/index.jsx +275 -194
  48. package/src/app/pages/CubeBuilderPage/DimensionsSelect.jsx +154 -0
  49. package/src/app/pages/CubeBuilderPage/Loadable.jsx +16 -0
  50. package/src/app/pages/CubeBuilderPage/MetricsSelect.jsx +77 -0
  51. package/src/app/pages/CubeBuilderPage/__tests__/index.test.jsx +405 -0
  52. package/src/app/pages/CubeBuilderPage/index.jsx +267 -0
  53. package/src/app/pages/NamespacePage/AddNamespacePopover.jsx +5 -5
  54. package/src/app/pages/NamespacePage/Explorer.jsx +6 -2
  55. package/src/app/pages/NamespacePage/FieldControl.jsx +21 -0
  56. package/src/app/pages/NamespacePage/NodeTypeSelect.jsx +30 -0
  57. package/src/app/pages/NamespacePage/TagSelect.jsx +44 -0
  58. package/src/app/pages/NamespacePage/UserSelect.jsx +47 -0
  59. package/src/app/pages/NamespacePage/__tests__/index.test.jsx +98 -19
  60. package/src/app/pages/NamespacePage/index.jsx +272 -89
  61. package/src/app/pages/NodePage/AddBackfillPopover.jsx +61 -61
  62. package/src/app/pages/NodePage/AddMaterializationPopover.jsx +104 -51
  63. package/src/app/pages/NodePage/ClientCodePopover.jsx +73 -25
  64. package/src/app/pages/NodePage/DimensionFilter.jsx +86 -0
  65. package/src/app/pages/NodePage/EditColumnDescriptionPopover.jsx +116 -0
  66. package/src/app/pages/NodePage/LinkDimensionPopover.jsx +38 -23
  67. package/src/app/pages/NodePage/MaterializationConfigField.jsx +60 -0
  68. package/src/app/pages/NodePage/NodeColumnTab.jsx +183 -113
  69. package/src/app/pages/NodePage/NodeDependenciesTab.jsx +153 -0
  70. package/src/app/pages/NodePage/NodeGraphTab.jsx +56 -29
  71. package/src/app/pages/NodePage/NodeHistory.jsx +165 -161
  72. package/src/app/pages/NodePage/NodeInfoTab.jsx +148 -14
  73. package/src/app/pages/NodePage/NodeMaterializationTab.jsx +201 -104
  74. package/src/app/pages/NodePage/NodeStatus.jsx +96 -21
  75. package/src/app/pages/NodePage/NodeValidateTab.jsx +367 -0
  76. package/src/app/pages/NodePage/NotebookDownload.jsx +36 -0
  77. package/src/app/pages/NodePage/PartitionColumnPopover.jsx +3 -5
  78. package/src/app/pages/NodePage/PartitionValueForm.jsx +60 -0
  79. package/src/app/pages/NodePage/RevisionDiff.jsx +209 -0
  80. package/src/app/pages/NodePage/WatchNodeButton.jsx +226 -0
  81. package/src/app/pages/NodePage/__tests__/AddBackfillPopover.test.jsx +13 -4
  82. package/src/app/pages/NodePage/__tests__/AddMaterializationPopover.test.jsx +87 -0
  83. package/src/app/pages/NodePage/__tests__/DimensionFilter.test.jsx +74 -0
  84. package/src/app/pages/NodePage/__tests__/EditColumnDescriptionPopover.test.jsx +149 -0
  85. package/src/app/pages/NodePage/__tests__/LinkDimensionPopover.test.jsx +10 -14
  86. package/src/app/pages/NodePage/__tests__/NodeColumnTab.test.jsx +166 -0
  87. package/src/app/pages/NodePage/__tests__/NodeDependenciesTab.test.jsx +151 -0
  88. package/src/app/pages/NodePage/__tests__/NodeGraphTab.test.jsx +6 -2
  89. package/src/app/pages/NodePage/__tests__/NodeLineageTab.test.jsx +3 -2
  90. package/src/app/pages/NodePage/__tests__/NodeMaterializationTab.test.jsx +148 -0
  91. package/src/app/pages/NodePage/__tests__/NodePage.test.jsx +159 -57
  92. package/src/app/pages/NodePage/__tests__/RevisionDiff.test.jsx +164 -0
  93. package/src/app/pages/NodePage/__tests__/__snapshots__/NodePage.test.jsx.snap +2 -386
  94. package/src/app/pages/NodePage/index.jsx +94 -57
  95. package/src/app/pages/Root/__tests__/index.test.jsx +3 -1
  96. package/src/app/pages/Root/index.tsx +62 -12
  97. package/src/app/services/DJService.js +587 -55
  98. package/src/app/services/__tests__/DJService.test.jsx +382 -45
  99. package/src/index.tsx +1 -0
  100. package/src/mocks/mockNodes.jsx +265 -227
  101. package/src/styles/dag.css +4 -2
  102. package/src/styles/index.css +474 -10
  103. package/src/styles/loading.css +1 -1
  104. package/src/styles/node-creation.scss +84 -5
  105. package/src/styles/node-list.css +4 -0
  106. package/src/styles/sorted-table.css +15 -0
  107. package/src/app/components/DeleteNode.jsx +0 -55
  108. package/src/app/components/__tests__/DeleteNode.test.jsx +0 -53
  109. package/src/app/pages/NodePage/NodeSQLTab.jsx +0 -82
  110. package/src/app/pages/NodePage/__tests__/ClientCodePopover.test.jsx +0 -49
package/Makefile CHANGED
@@ -1,3 +1,9 @@
1
1
  dev-release:
2
2
  yarn version --prerelease --preid dev --no-git-tag-version
3
- npm publish
3
+ npm publish
4
+
5
+ test:
6
+ yarn test --coverage --watchAll --runInBand
7
+
8
+ lint:
9
+ npm run lint
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "datajunction-ui",
3
- "version": "0.0.1a1",
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": "^27.5.2",
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.3.0",
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": 90,
165
- "branches": 75,
166
- "lines": 90,
167
- "functions": 85
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="public/manifest.json" />
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
+ });