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.
@@ -146,59 +146,60 @@ describe('NamespacePage', () => {
146
146
  </MemoryRouter>,
147
147
  );
148
148
 
149
- await waitFor(
150
- () => {
151
- expect(mockDjClient.listNodesForLanding).toHaveBeenCalled();
152
- expect(screen.getByText('Namespaces')).toBeInTheDocument();
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
- // 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();
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
- // check that it renders nodes
161
- expect(screen.getByText('Test Node')).toBeInTheDocument();
172
+ // flip direction
173
+ fireEvent.click(screen.getByText('name'));
174
+ await waitFor(() => {
175
+ expect(mockDjClient.listNodesForLanding).toHaveBeenCalledTimes(3);
176
+ });
162
177
 
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'));
178
+ // sort by 'displayName'
179
+ fireEvent.click(screen.getByText('display Name'));
180
+ await waitFor(() => {
181
+ expect(mockDjClient.listNodesForLanding).toHaveBeenCalledTimes(4);
182
+ });
167
183
 
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);
184
+ // --- Filters ---
175
185
 
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'));
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
- // 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' });
191
+ // Tag filter
192
+ const selectTag = screen.getAllByTestId('select-tag')[0];
193
+ fireEvent.keyDown(selectTag.firstChild, { key: 'ArrowDown' });
188
194
 
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' });
195
+ // User filter
196
+ const selectUser = screen.getAllByTestId('select-user')[0];
197
+ fireEvent.keyDown(selectUser.firstChild, { key: 'ArrowDown' });
194
198
 
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);
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
- setSortConfig({ key, direction });
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
- sortedNodes.length > 0 ? (
189
- sortedNodes.map(node => (
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
- <button
199
- className="add_node"
200
- type="submit"
201
- aria-label="SaveEditColumn"
202
- aria-hidden="false"
203
- disabled={isSubmitting}
198
+ <div
199
+ style={{
200
+ display: 'flex',
201
+ justifyContent: 'flex-end',
202
+ marginTop: '20px',
203
+ }}
204
204
  >
205
- {isSubmitting ? <LoadingIcon /> : 'Save'}
206
- </button>
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
+ }