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.
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 +60 -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
@@ -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 { FormikSelect } from '../AddEditNodePage/FormikSelect';
6
- import EditIcon from '../../icons/EditIcon';
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
- <EditIcon />
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 === undefined || current?.startsWith(item.path)) {
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 className={`${expand ? '' : 'inactive'}`}>
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
- expect(mockDjClient.namespaces).toHaveBeenCalledTimes(1);
103
- expect(screen.getByText('Namespaces')).toBeInTheDocument();
149
+ await waitFor(
150
+ () => {
151
+ expect(mockDjClient.listNodesForLanding).toHaveBeenCalled();
152
+ expect(screen.getByText('Namespaces')).toBeInTheDocument();
104
153
 
105
- // check that it displays namespaces
106
- expect(screen.getByText('common')).toBeInTheDocument();
107
- expect(screen.getByText('one')).toBeInTheDocument();
108
- expect(screen.getByText('fruits')).toBeInTheDocument();
109
- expect(screen.getByText('vegetables')).toBeInTheDocument();
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
- // check that it renders nodes
112
- expect(screen.getByText('Test Node')).toBeInTheDocument();
160
+ // check that it renders nodes
161
+ expect(screen.getByText('Test Node')).toBeInTheDocument();
113
162
 
114
- // click to open and close tab
115
- fireEvent.click(screen.getByText('common'));
116
- fireEvent.click(screen.getByText('common'));
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
- afterEach(() => {
121
- jest.clearAllMocks();
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();