datajunction-ui 0.0.1-a112 → 0.0.1-a113.dev0
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/package.json +1 -1
- package/runit.sh +30 -0
- package/runit2.sh +30 -0
- package/src/app/components/NodeMaterializationDelete.jsx +11 -1
- package/src/app/components/Search.jsx +1 -1
- package/src/app/icons/WrenchIcon.jsx +36 -0
- package/src/app/pages/AddEditNodePage/ColumnMetadata.jsx +61 -0
- package/src/app/pages/AddEditNodePage/ColumnsMetadataInput.jsx +72 -0
- package/src/app/pages/AddEditNodePage/ExperimentationExtension.jsx +338 -0
- package/src/app/pages/NamespacePage/__tests__/index.test.jsx +46 -45
- package/src/app/pages/NamespacePage/index.jsx +8 -26
- package/src/app/pages/NodePage/AddMaterializationPopover.jsx +17 -9
- package/src/app/pages/NodePage/AvailabilityStateBlock.jsx +67 -0
- package/src/app/pages/NodePage/LinkComplexDimensionPopover.jsx +139 -0
- package/src/app/pages/NodePage/NodeMaterializationTab.jsx +346 -91
- package/src/app/pages/NodePage/NodeRevisionMaterializationTab.jsx +58 -0
- package/src/app/pages/NodePage/__tests__/NodeMaterializationTab.test.jsx +43 -1
- package/src/app/pages/NodePage/__tests__/NodePage.test.jsx +23 -10
- package/src/app/services/DJService.js +59 -19
- package/src/app/services/__tests__/DJService.test.jsx +55 -1
- package/src/styles/index.css +9 -0
|
@@ -146,59 +146,60 @@ describe('NamespacePage', () => {
|
|
|
146
146
|
</MemoryRouter>,
|
|
147
147
|
);
|
|
148
148
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
149
|
+
// Wait for initial nodes to load
|
|
150
|
+
await waitFor(() => {
|
|
151
|
+
expect(mockDjClient.listNodesForLanding).toHaveBeenCalled();
|
|
152
|
+
expect(screen.getByText('Namespaces')).toBeInTheDocument();
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
// Check that it displays namespaces
|
|
156
|
+
expect(screen.getByText('common')).toBeInTheDocument();
|
|
157
|
+
expect(screen.getByText('one')).toBeInTheDocument();
|
|
158
|
+
expect(screen.getByText('fruits')).toBeInTheDocument();
|
|
159
|
+
expect(screen.getByText('vegetables')).toBeInTheDocument();
|
|
160
|
+
|
|
161
|
+
// Check that it renders nodes
|
|
162
|
+
expect(screen.getByText('Test Node')).toBeInTheDocument();
|
|
153
163
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
164
|
+
// --- Sorting ---
|
|
165
|
+
|
|
166
|
+
// sort by 'name'
|
|
167
|
+
fireEvent.click(screen.getByText('name'));
|
|
168
|
+
await waitFor(() => {
|
|
169
|
+
expect(mockDjClient.listNodesForLanding).toHaveBeenCalledTimes(2);
|
|
170
|
+
});
|
|
159
171
|
|
|
160
|
-
|
|
161
|
-
|
|
172
|
+
// flip direction
|
|
173
|
+
fireEvent.click(screen.getByText('name'));
|
|
174
|
+
await waitFor(() => {
|
|
175
|
+
expect(mockDjClient.listNodesForLanding).toHaveBeenCalledTimes(3);
|
|
176
|
+
});
|
|
162
177
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
178
|
+
// sort by 'displayName'
|
|
179
|
+
fireEvent.click(screen.getByText('display Name'));
|
|
180
|
+
await waitFor(() => {
|
|
181
|
+
expect(mockDjClient.listNodesForLanding).toHaveBeenCalledTimes(4);
|
|
182
|
+
});
|
|
167
183
|
|
|
168
|
-
|
|
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);
|
|
184
|
+
// --- Filters ---
|
|
175
185
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
fireEvent.keyDown(selectNodeType.firstChild, { key: 'ArrowDown' });
|
|
181
|
-
fireEvent.click(screen.getByText('Source'));
|
|
186
|
+
// Node type
|
|
187
|
+
const selectNodeType = screen.getAllByTestId('select-node-type')[0];
|
|
188
|
+
fireEvent.keyDown(selectNodeType.firstChild, { key: 'ArrowDown' });
|
|
189
|
+
fireEvent.click(screen.getByText('Source'));
|
|
182
190
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
expect(selectTag).not.toBeNull();
|
|
187
|
-
fireEvent.keyDown(selectTag.firstChild, { key: 'ArrowDown' });
|
|
191
|
+
// Tag filter
|
|
192
|
+
const selectTag = screen.getAllByTestId('select-tag')[0];
|
|
193
|
+
fireEvent.keyDown(selectTag.firstChild, { key: 'ArrowDown' });
|
|
188
194
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
expect(selectUser).not.toBeNull();
|
|
193
|
-
fireEvent.keyDown(selectUser.firstChild, { key: 'ArrowDown' });
|
|
195
|
+
// User filter
|
|
196
|
+
const selectUser = screen.getAllByTestId('select-user')[0];
|
|
197
|
+
fireEvent.keyDown(selectUser.firstChild, { key: 'ArrowDown' });
|
|
194
198
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
{ timeout: 3000 },
|
|
200
|
-
);
|
|
201
|
-
}, 60000);
|
|
199
|
+
// --- Expand/Collapse Namespace ---
|
|
200
|
+
fireEvent.click(screen.getByText('common'));
|
|
201
|
+
fireEvent.click(screen.getByText('common'));
|
|
202
|
+
});
|
|
202
203
|
|
|
203
204
|
it('can add new namespace via add namespace popover', async () => {
|
|
204
205
|
mockDjClient.addNamespace.mockReturnValue({
|
|
@@ -53,34 +53,14 @@ export function NamespacePage() {
|
|
|
53
53
|
const [hasNextPage, setHasNextPage] = useState(true);
|
|
54
54
|
const [hasPrevPage, setHasPrevPage] = useState(true);
|
|
55
55
|
|
|
56
|
-
const sortedNodes = React.useMemo(() => {
|
|
57
|
-
let sortableData = [...Object.values(state.nodes)];
|
|
58
|
-
if (sortConfig !== null) {
|
|
59
|
-
sortableData.sort((a, b) => {
|
|
60
|
-
if (
|
|
61
|
-
a[sortConfig.key] < b[sortConfig.key] ||
|
|
62
|
-
a.current[sortConfig.key] < b.current[sortConfig.key]
|
|
63
|
-
) {
|
|
64
|
-
return sortConfig.direction === ASC ? -1 : 1;
|
|
65
|
-
}
|
|
66
|
-
if (
|
|
67
|
-
a[sortConfig.key] > b[sortConfig.key] ||
|
|
68
|
-
a.current[sortConfig.key] > b.current[sortConfig.key]
|
|
69
|
-
) {
|
|
70
|
-
return sortConfig.direction === ASC ? 1 : -1;
|
|
71
|
-
}
|
|
72
|
-
return 0;
|
|
73
|
-
});
|
|
74
|
-
}
|
|
75
|
-
return sortableData;
|
|
76
|
-
}, [state.nodes, filters, sortConfig]);
|
|
77
|
-
|
|
78
56
|
const requestSort = key => {
|
|
79
57
|
let direction = ASC;
|
|
80
58
|
if (sortConfig.key === key && sortConfig.direction === ASC) {
|
|
81
59
|
direction = DESC;
|
|
82
60
|
}
|
|
83
|
-
|
|
61
|
+
if (sortConfig.key !== key || sortConfig.direction !== direction) {
|
|
62
|
+
setSortConfig({ key, direction });
|
|
63
|
+
}
|
|
84
64
|
};
|
|
85
65
|
|
|
86
66
|
const getClassNamesFor = name => {
|
|
@@ -141,6 +121,7 @@ export function NamespacePage() {
|
|
|
141
121
|
before,
|
|
142
122
|
after,
|
|
143
123
|
50,
|
|
124
|
+
sortConfig,
|
|
144
125
|
);
|
|
145
126
|
|
|
146
127
|
setState({
|
|
@@ -170,7 +151,8 @@ export function NamespacePage() {
|
|
|
170
151
|
setRetrieved(true);
|
|
171
152
|
};
|
|
172
153
|
fetchData().catch(console.error);
|
|
173
|
-
}, [djClient, filters, before, after]);
|
|
154
|
+
}, [djClient, filters, before, after, sortConfig.key, sortConfig.direction]);
|
|
155
|
+
|
|
174
156
|
const loadNext = () => {
|
|
175
157
|
if (nextCursor) {
|
|
176
158
|
setAfter(nextCursor);
|
|
@@ -185,8 +167,8 @@ export function NamespacePage() {
|
|
|
185
167
|
};
|
|
186
168
|
|
|
187
169
|
const nodesList = retrieved ? (
|
|
188
|
-
|
|
189
|
-
|
|
170
|
+
state.nodes.length > 0 ? (
|
|
171
|
+
state.nodes.map(node => (
|
|
190
172
|
<tr key={node.name}>
|
|
191
173
|
<td>
|
|
192
174
|
<a href={'/nodes/' + node.name} className="link-table">
|
|
@@ -124,7 +124,7 @@ export default function AddMaterializationPopover({ node, onSubmit }) {
|
|
|
124
124
|
{function Render({ isSubmitting, status, setFieldValue }) {
|
|
125
125
|
return (
|
|
126
126
|
<Form>
|
|
127
|
-
<h2>Configure Materialization</h2>
|
|
127
|
+
<h2>Configure Materialization for the Latest Node Version</h2>
|
|
128
128
|
{displayMessageAfterSubmit(status)}
|
|
129
129
|
{node.type === 'cube' ? (
|
|
130
130
|
<span data-testid="job-type">
|
|
@@ -195,15 +195,23 @@ export default function AddMaterializationPopover({ node, onSubmit }) {
|
|
|
195
195
|
'spark.memory.fraction': '0.3',
|
|
196
196
|
}}
|
|
197
197
|
/>
|
|
198
|
-
<
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
198
|
+
<div
|
|
199
|
+
style={{
|
|
200
|
+
display: 'flex',
|
|
201
|
+
justifyContent: 'flex-end',
|
|
202
|
+
marginTop: '20px',
|
|
203
|
+
}}
|
|
204
204
|
>
|
|
205
|
-
|
|
206
|
-
|
|
205
|
+
<button
|
|
206
|
+
className="add_node"
|
|
207
|
+
type="submit"
|
|
208
|
+
aria-label="SaveEditColumn"
|
|
209
|
+
aria-hidden="false"
|
|
210
|
+
disabled={isSubmitting}
|
|
211
|
+
>
|
|
212
|
+
{isSubmitting ? <LoadingIcon /> : 'Save'}
|
|
213
|
+
</button>
|
|
214
|
+
</div>
|
|
207
215
|
</Form>
|
|
208
216
|
);
|
|
209
217
|
}}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import TableIcon from '../../icons/TableIcon';
|
|
2
|
+
|
|
3
|
+
export default function AvailabilityStateBlock({ availability }) {
|
|
4
|
+
return (
|
|
5
|
+
<table
|
|
6
|
+
className="card-inner-table table"
|
|
7
|
+
aria-label="Availability"
|
|
8
|
+
aria-hidden="false"
|
|
9
|
+
style={{ marginBottom: '20px' }}
|
|
10
|
+
>
|
|
11
|
+
<thead className="fs-7 fw-bold text-gray-400 border-bottom-0">
|
|
12
|
+
<tr>
|
|
13
|
+
<th className="text-start">Output Dataset</th>
|
|
14
|
+
<th>Valid Through</th>
|
|
15
|
+
<th>Partitions</th>
|
|
16
|
+
<th>Links</th>
|
|
17
|
+
</tr>
|
|
18
|
+
</thead>
|
|
19
|
+
<tbody>
|
|
20
|
+
<tr>
|
|
21
|
+
<td>
|
|
22
|
+
<div className={`table__full`} key={availability.table}>
|
|
23
|
+
<div className="table__header">
|
|
24
|
+
<TableIcon />{' '}
|
|
25
|
+
<span className={`entity-info`}>
|
|
26
|
+
{availability.catalog + '.' + availability.schema_}
|
|
27
|
+
</span>
|
|
28
|
+
</div>
|
|
29
|
+
<div className={`table__body upstream_tables`}>
|
|
30
|
+
<a href={availability.url}>{availability.table}</a>
|
|
31
|
+
</div>
|
|
32
|
+
</div>
|
|
33
|
+
</td>
|
|
34
|
+
<td>{new Date(availability.valid_through_ts).toISOString()}</td>
|
|
35
|
+
<td>
|
|
36
|
+
<span
|
|
37
|
+
className={`badge partition_value`}
|
|
38
|
+
style={{ fontSize: '100%' }}
|
|
39
|
+
>
|
|
40
|
+
<span className={`badge partition_value_highlight`}>
|
|
41
|
+
{availability.min_temporal_partition?.join(', ') || 'N/A'}
|
|
42
|
+
</span>
|
|
43
|
+
to
|
|
44
|
+
<span className={`badge partition_value_highlight`}>
|
|
45
|
+
{availability.max_temporal_partition?.join(', ') || 'N/A'}
|
|
46
|
+
</span>
|
|
47
|
+
</span>
|
|
48
|
+
</td>
|
|
49
|
+
<td>
|
|
50
|
+
{availability.links &&
|
|
51
|
+
Object.keys(availability.links).length > 0 ? (
|
|
52
|
+
Object.entries(availability.links).map(([key, value]) => (
|
|
53
|
+
<div key={key}>
|
|
54
|
+
<a href={value} target="_blank" rel="noreferrer">
|
|
55
|
+
{key}
|
|
56
|
+
</a>
|
|
57
|
+
</div>
|
|
58
|
+
))
|
|
59
|
+
) : (
|
|
60
|
+
<></>
|
|
61
|
+
)}
|
|
62
|
+
</td>
|
|
63
|
+
</tr>
|
|
64
|
+
</tbody>
|
|
65
|
+
</table>
|
|
66
|
+
);
|
|
67
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { useContext, useEffect, useRef, useState } from 'react';
|
|
2
|
+
import * as React from 'react';
|
|
3
|
+
import DJClientContext from '../../providers/djclient';
|
|
4
|
+
import { Field, Form, Formik } from 'formik';
|
|
5
|
+
import { FormikSelect } from '../AddEditNodePage/FormikSelect';
|
|
6
|
+
import EditIcon from '../../icons/EditIcon';
|
|
7
|
+
import { displayMessageAfterSubmit } from '../../../utils/form';
|
|
8
|
+
import LoadingIcon from '../../icons/LoadingIcon';
|
|
9
|
+
import { MetricQueryField } from '../AddEditNodePage/MetricQueryField';
|
|
10
|
+
|
|
11
|
+
export default function LinkComplexDimensionPopover({ link, onSubmit }) {
|
|
12
|
+
const djClient = useContext(DJClientContext).DataJunctionAPI;
|
|
13
|
+
const [popoverAnchor, setPopoverAnchor] = useState(false);
|
|
14
|
+
const ref = useRef(null);
|
|
15
|
+
|
|
16
|
+
useEffect(() => {
|
|
17
|
+
const handleClickOutside = event => {
|
|
18
|
+
if (ref.current && !ref.current.contains(event.target)) {
|
|
19
|
+
setPopoverAnchor(false);
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
document.addEventListener('click', handleClickOutside, true);
|
|
23
|
+
return () => {
|
|
24
|
+
document.removeEventListener('click', handleClickOutside, true);
|
|
25
|
+
};
|
|
26
|
+
}, [setPopoverAnchor]);
|
|
27
|
+
|
|
28
|
+
const handleSubmit = async (
|
|
29
|
+
{ node, column, dimension },
|
|
30
|
+
{ setSubmitting, setStatus },
|
|
31
|
+
) => {
|
|
32
|
+
if (referencedDimensionNode && dimension === 'Remove') {
|
|
33
|
+
await unlinkDimension(
|
|
34
|
+
node,
|
|
35
|
+
column,
|
|
36
|
+
referencedDimensionNode,
|
|
37
|
+
setStatus,
|
|
38
|
+
).then(_ => setSubmitting(false));
|
|
39
|
+
} else {
|
|
40
|
+
await linkDimension(node, column, dimension, setStatus).then(_ =>
|
|
41
|
+
setSubmitting(false),
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
onSubmit();
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const linkDimension = async (node, column, dimension, setStatus) => {
|
|
48
|
+
const response = await djClient.linkDimension(node, column, dimension);
|
|
49
|
+
if (response.status === 200 || response.status === 201) {
|
|
50
|
+
setStatus({ success: 'Saved!' });
|
|
51
|
+
} else {
|
|
52
|
+
setStatus({
|
|
53
|
+
failure: `${response.json.message}`,
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const unlinkDimension = async (node, column, currentDimension, setStatus) => {
|
|
59
|
+
const response = await djClient.unlinkDimension(
|
|
60
|
+
node,
|
|
61
|
+
column,
|
|
62
|
+
currentDimension,
|
|
63
|
+
);
|
|
64
|
+
if (response.status === 200 || response.status === 201) {
|
|
65
|
+
setStatus({ success: 'Removed dimension link!' });
|
|
66
|
+
} else {
|
|
67
|
+
setStatus({
|
|
68
|
+
failure: `${response.json.message}`,
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
return (
|
|
74
|
+
<>
|
|
75
|
+
<button
|
|
76
|
+
className="edit_button"
|
|
77
|
+
aria-label="LinkDimension"
|
|
78
|
+
tabIndex="0"
|
|
79
|
+
onClick={() => {
|
|
80
|
+
setPopoverAnchor(!popoverAnchor);
|
|
81
|
+
}}
|
|
82
|
+
>
|
|
83
|
+
<EditIcon />
|
|
84
|
+
</button>
|
|
85
|
+
<div
|
|
86
|
+
className="popover"
|
|
87
|
+
role="dialog"
|
|
88
|
+
aria-label="client-code"
|
|
89
|
+
style={{ display: popoverAnchor === false ? 'none' : 'block' }}
|
|
90
|
+
ref={ref}
|
|
91
|
+
>
|
|
92
|
+
<Formik
|
|
93
|
+
initialValues={
|
|
94
|
+
{
|
|
95
|
+
// column: column.name,
|
|
96
|
+
// node: node.name,
|
|
97
|
+
// dimension: '',
|
|
98
|
+
// currentDimension: referencedDimensionNode,
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
onSubmit={handleSubmit}
|
|
102
|
+
>
|
|
103
|
+
{function Render({ isSubmitting, status, setFieldValue }) {
|
|
104
|
+
return (
|
|
105
|
+
<Form>
|
|
106
|
+
{displayMessageAfterSubmit(status)}
|
|
107
|
+
<span data-testid="link-dimension">
|
|
108
|
+
<label>Join Type</label>
|
|
109
|
+
<FormikSelect
|
|
110
|
+
selectOptions={[{ value: 'left', label: 'LEFT' }]}
|
|
111
|
+
formikFieldName="join_type"
|
|
112
|
+
placeholder="Select join type"
|
|
113
|
+
className="join_type"
|
|
114
|
+
defaultValue={link.join_type || ''}
|
|
115
|
+
isMulti={false}
|
|
116
|
+
/>
|
|
117
|
+
<label>Join On</label>
|
|
118
|
+
<MetricQueryField
|
|
119
|
+
djClient={djClient}
|
|
120
|
+
value={link.join_sql ? link.join_sql : ''}
|
|
121
|
+
/>
|
|
122
|
+
</span>
|
|
123
|
+
<button
|
|
124
|
+
className="add_node"
|
|
125
|
+
type="submit"
|
|
126
|
+
aria-label="SaveComplexDimension"
|
|
127
|
+
aria-hidden="false"
|
|
128
|
+
disabled={isSubmitting}
|
|
129
|
+
>
|
|
130
|
+
{isSubmitting ? <LoadingIcon /> : 'Save'}
|
|
131
|
+
</button>
|
|
132
|
+
</Form>
|
|
133
|
+
);
|
|
134
|
+
}}
|
|
135
|
+
</Formik>
|
|
136
|
+
</div>
|
|
137
|
+
</>
|
|
138
|
+
);
|
|
139
|
+
}
|