datajunction-ui 0.0.1-a70 → 0.0.1-a70.dev1

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "datajunction-ui",
3
- "version": "0.0.1a70",
3
+ "version": "0.0.1-a70.dev1",
4
4
  "description": "DataJunction Metrics Platform UI",
5
5
  "module": "src/index.tsx",
6
6
  "repository": {
@@ -1,6 +1,6 @@
1
1
  const AddItemIcon = props => (
2
2
  <svg
3
- enable-background="new 0 0 512 512"
3
+ enableBackground="new 0 0 512 512"
4
4
  height="20px"
5
5
  id="Layer_1"
6
6
  version="1.1"
@@ -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,9 @@ 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 className={`${expand ? '' : 'inactive'}`} key={`nested-${index}`}>
48
49
  <Explorer item={item} current={highlight} />
49
50
  </div>
50
51
  </div>
@@ -8,6 +8,7 @@ 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(),
12
13
  whoami: jest.fn(),
13
14
  users: jest.fn(),
@@ -85,6 +86,41 @@ describe('NamespacePage', () => {
85
86
  edited_by: ['dj'],
86
87
  },
87
88
  ]);
89
+ mockDjClient.listNodesForLanding.mockResolvedValue(
90
+ {
91
+ "data": {
92
+ "findNodesPaginated": {
93
+ "pageInfo": {
94
+ "hasNextPage": true,
95
+ "endCursor": "eyJjcmVhdGVkX2F0IjogIjIwMjQtMDQtMTZUMjM6MjI6MjIuNDQxNjg2KzAwOjAwIiwgImlkIjogNjE0fQ==",
96
+ "hasPrevPage": true,
97
+ "startCursor": "eyJjcmVhdGVkX2F0IjogIjIwMjQtMTAtMTZUMTY6MDM6MTcuMDgzMjY3KzAwOjAwIiwgImlkIjogMjQwOX0="
98
+ },
99
+ "edges": [
100
+ {
101
+ "node": {
102
+ "name": "default.test_node",
103
+ "type": "DIMENSION",
104
+ "currentVersion": "v4.0",
105
+ "tags": [],
106
+ "editedBy": [
107
+ "dj",
108
+ ],
109
+ "current": {
110
+ "displayName": "Test Node",
111
+ "status": "VALID",
112
+ "updatedAt": "2024-10-18T15:15:33.532949+00:00"
113
+ },
114
+ "createdBy": {
115
+ "username": "dj"
116
+ }
117
+ }
118
+ },
119
+ ]
120
+ }
121
+ }
122
+ }
123
+ );
88
124
  });
89
125
 
90
126
  afterEach(() => {
@@ -107,7 +143,7 @@ describe('NamespacePage', () => {
107
143
  );
108
144
 
109
145
  await waitFor(() => {
110
- expect(mockDjClient.namespaces).toHaveBeenCalledTimes(1);
146
+ expect(mockDjClient.listNodesForLanding).toHaveBeenCalled();
111
147
  expect(screen.getByText('Namespaces')).toBeInTheDocument();
112
148
 
113
149
  // check that it displays namespaces
@@ -121,7 +157,16 @@ describe('NamespacePage', () => {
121
157
 
122
158
  // check that it sorts nodes
123
159
  fireEvent.click(screen.getByText('name'));
124
- fireEvent.click(screen.getByText('display name'));
160
+ fireEvent.click(screen.getByText('name'));
161
+ fireEvent.click(screen.getByText('display Name'));
162
+
163
+ // paginate
164
+ const previousButton = screen.getByText('← Previous');
165
+ expect(previousButton).toBeDefined();
166
+ fireEvent.click(previousButton);
167
+ const nextButton = screen.getByText('Next →');
168
+ expect(nextButton).toBeDefined();
169
+ fireEvent.click(nextButton);
125
170
 
126
171
  // check that we can filter by node type
127
172
  const selectNodeType = screen.getAllByTestId('select-node-type')[0];
@@ -141,17 +186,13 @@ describe('NamespacePage', () => {
141
186
  expect(selectUser).toBeDefined();
142
187
  expect(selectUser).not.toBeNull();
143
188
  fireEvent.keyDown(selectUser.firstChild, { key: 'ArrowDown' });
144
- // fireEvent.click(screen.getByText('dj'));
145
189
 
146
190
  // click to open and close tab
147
191
  fireEvent.click(screen.getByText('common'));
148
192
  fireEvent.click(screen.getByText('common'));
149
- });
150
- });
151
-
152
- afterEach(() => {
153
- jest.clearAllMocks();
154
- });
193
+ },
194
+ { timeout: 3000 });
195
+ }, 60000);
155
196
 
156
197
  it('can add new namespace via add namespace popover', async () => {
157
198
  mockDjClient.addNamespace.mockReturnValue({
@@ -20,13 +20,13 @@ export function NamespacePage() {
20
20
  const ASC = 'ascending';
21
21
  const DESC = 'descending';
22
22
 
23
- const fields = ['name', 'display_name', 'type', 'status', 'updated_at'];
23
+ const fields = ['name', 'displayName', 'type', 'status', 'updatedAt'];
24
24
 
25
25
  const djClient = useContext(DJClientContext).DataJunctionAPI;
26
26
  var { namespace } = useParams();
27
27
 
28
28
  const [state, setState] = useState({
29
- namespace: namespace,
29
+ namespace: namespace ? namespace : '',
30
30
  nodes: [],
31
31
  });
32
32
  const [retrieved, setRetrieved] = useState(false);
@@ -35,39 +35,32 @@ export function NamespacePage() {
35
35
  const [filters, setFilters] = useState({
36
36
  tags: [],
37
37
  node_type: '',
38
- edited_by: currentUser?.username,
38
+ edited_by: '',
39
39
  });
40
40
 
41
41
  const [namespaceHierarchy, setNamespaceHierarchy] = useState([]);
42
42
 
43
43
  const [sortConfig, setSortConfig] = useState({
44
- key: 'updated_at',
44
+ key: 'updatedAt',
45
45
  direction: DESC,
46
46
  });
47
+
48
+ const [before, setBefore] = useState(null);
49
+ const [after, setAfter] = useState(null);
50
+ const [prevCursor, setPrevCursor] = useState(true);
51
+ const [nextCursor, setNextCursor] = useState(true);
52
+
53
+ const [hasNextPage, setHasNextPage] = useState(true);
54
+ const [hasPrevPage, setHasPrevPage] = useState(true);
55
+
47
56
  const sortedNodes = React.useMemo(() => {
48
57
  let sortableData = [...Object.values(state.nodes)];
49
- if (filters.node_type !== '' && filters.node_type !== null) {
50
- sortableData = sortableData.filter(
51
- node => node.type === filters.node_type,
52
- );
53
- }
54
- if (filters.tags) {
55
- sortableData = sortableData.filter(node => {
56
- const nodeTags = node.tags.map(tag => tag.name);
57
- return filters.tags.every(item => nodeTags.includes(item));
58
- });
59
- }
60
- if (filters.edited_by) {
61
- sortableData = sortableData.filter(node => {
62
- return node.edited_by.includes(filters.edited_by);
63
- });
64
- }
65
58
  if (sortConfig !== null) {
66
59
  sortableData.sort((a, b) => {
67
- if (a[sortConfig.key] < b[sortConfig.key]) {
60
+ if (a[sortConfig.key] < b[sortConfig.key] || a.current[sortConfig.key] < b.current[sortConfig.key]) {
68
61
  return sortConfig.direction === ASC ? -1 : 1;
69
62
  }
70
- if (a[sortConfig.key] > b[sortConfig.key]) {
63
+ if (a[sortConfig.key] > b[sortConfig.key] || a.current[sortConfig.key] > b.current[sortConfig.key]) {
71
64
  return sortConfig.direction === ASC ? 1 : -1;
72
65
  }
73
66
  return 0;
@@ -125,6 +118,7 @@ export function NamespacePage() {
125
118
  const hierarchy = createNamespaceHierarchy(namespaces);
126
119
  setNamespaceHierarchy(hierarchy);
127
120
  const currentUser = await djClient.whoami();
121
+ // setFilters({...filters, edited_by: currentUser?.username});
128
122
  setCurrentUser(currentUser);
129
123
  };
130
124
  fetchData().catch(console.error);
@@ -132,23 +126,46 @@ export function NamespacePage() {
132
126
 
133
127
  useEffect(() => {
134
128
  const fetchData = async () => {
135
- if (namespace === undefined && namespaceHierarchy !== undefined) {
136
- namespace = namespaceHierarchy[0].namespace;
137
- }
138
- const nodes = await djClient.namespace(namespace);
139
- const foundNodes = await Promise.all(nodes);
129
+ setRetrieved(false);
130
+ console.log('cursor', before, filters.edited_by);
131
+ const nodes = await djClient.listNodesForLanding(
132
+ namespace,
133
+ filters.node_type ? [filters.node_type.toUpperCase()] : [],
134
+ filters.tags, filters.edited_by, before, after, 50);
135
+ console.log('nodes', nodes);
136
+
140
137
  setState({
141
138
  namespace: namespace,
142
- nodes: foundNodes,
139
+ nodes: nodes.data ? nodes.data.findNodesPaginated.edges.map(n => n.node) : [],
143
140
  });
141
+ if (nodes.data) {
142
+ setPrevCursor(nodes.data ? nodes.data.findNodesPaginated.pageInfo.startCursor : '');
143
+ setNextCursor(nodes.data ? nodes.data.findNodesPaginated.pageInfo.endCursor : '');
144
+ console.log('setting hasPrevPage, ', nodes.data.findNodesPaginated.pageInfo.hasPrevPage);
145
+ setHasPrevPage(nodes.data ? nodes.data.findNodesPaginated.pageInfo.hasPrevPage : false);
146
+ setHasNextPage(nodes.data ? nodes.data.findNodesPaginated.pageInfo.hasNextPage : false);
147
+ }
144
148
  setRetrieved(true);
145
149
  };
146
150
  fetchData().catch(console.error);
147
- }, [djClient, namespace, namespaceHierarchy]);
151
+ }, [djClient, filters, before, after]);
152
+ const loadNext = () => {
153
+ if (nextCursor) {
154
+ setAfter(nextCursor);
155
+ setBefore(null);
156
+ }
157
+ };
158
+ const loadPrev = () => {
159
+ if (prevCursor) {
160
+ setAfter(null);
161
+ setBefore(prevCursor);
162
+ }
163
+ };
148
164
 
149
165
  const nodesList = retrieved ? (
166
+ sortedNodes.length > 0 ? (
150
167
  sortedNodes.map(node => (
151
- <tr>
168
+ <tr key={node.name}>
152
169
  <td>
153
170
  <a href={'/nodes/' + node.name} className="link-table">
154
171
  {node.name}
@@ -157,16 +174,16 @@ export function NamespacePage() {
157
174
  className="rounded-pill badge bg-secondary-soft"
158
175
  style={{ marginLeft: '0.5rem' }}
159
176
  >
160
- {node.version}
177
+ {node.currentVersion}
161
178
  </span>
162
179
  </td>
163
180
  <td>
164
181
  <a href={'/nodes/' + node.name} className="link-table">
165
- {node.type !== 'source' ? node.display_name : ''}
182
+ {node.type !== 'source' ? node.current.displayName : ''}
166
183
  </a>
167
184
  </td>
168
185
  <td>
169
- <span className={'node_type__' + node.type + ' badge node_type'}>
186
+ <span className={'node_type__' + node.type.toLowerCase() + ' badge node_type'}>
170
187
  {node.type}
171
188
  </span>
172
189
  </td>
@@ -175,7 +192,7 @@ export function NamespacePage() {
175
192
  </td>
176
193
  <td>
177
194
  <span className="status">
178
- {new Date(node.updated_at).toLocaleString('en-us')}
195
+ {new Date(node.current.updatedAt).toLocaleString('en-us')}
179
196
  </span>
180
197
  </td>
181
198
  <td>
@@ -184,9 +201,22 @@ export function NamespacePage() {
184
201
  </tr>
185
202
  ))
186
203
  ) : (
187
- <span style={{ display: 'block', marginTop: '2rem' }}>
188
- <LoadingIcon />
189
- </span>
204
+ <tr>
205
+ <td>
206
+ <span style={{ display: 'block', marginTop: '2rem', marginLeft: '2rem', fontSize: '16px' }}>
207
+ There are no nodes in <a href={`/namespaces/${namespace}`}>{namespace}</a> with the above filters!
208
+ </span>
209
+ </td>
210
+ </tr>
211
+ )
212
+ ) : (
213
+ <tr>
214
+ <td>
215
+ <span style={{ display: 'block', marginTop: '2rem' }}>
216
+ <LoadingIcon />
217
+ </span>
218
+ </td>
219
+ </tr>
190
220
  );
191
221
 
192
222
  return (
@@ -194,7 +224,7 @@ export function NamespacePage() {
194
224
  <div className="card">
195
225
  <div className="card-header">
196
226
  <h2>Explore</h2>
197
- <div class="menu" style={{ margin: '0 0 20px 0' }}>
227
+ <div className="menu" style={{ margin: '0 0 20px 0' }}>
198
228
  <div
199
229
  className="menu-link"
200
230
  style={{
@@ -260,6 +290,7 @@ export function NamespacePage() {
260
290
  item={child}
261
291
  current={state.namespace}
262
292
  defaultExpand={true}
293
+ key={child.namespace}
263
294
  />
264
295
  ))
265
296
  : null}
@@ -269,13 +300,13 @@ export function NamespacePage() {
269
300
  <tr>
270
301
  {fields.map(field => {
271
302
  return (
272
- <th>
303
+ <th key={field}>
273
304
  <button
274
305
  type="button"
275
306
  onClick={() => requestSort(field)}
276
307
  className={'sortable ' + getClassNamesFor(field)}
277
308
  >
278
- {field.replace('_', ' ')}
309
+ {field.replace(/([a-z](?=[A-Z]))/g, '$1 ')}
279
310
  </button>
280
311
  </th>
281
312
  );
@@ -284,6 +315,14 @@ export function NamespacePage() {
284
315
  </tr>
285
316
  </thead>
286
317
  <tbody>{nodesList}</tbody>
318
+ <tfoot>
319
+ <tr>
320
+ <td>
321
+ {retrieved && hasPrevPage ? <a onClick={loadPrev} className="previous round pagination">← Previous</a> : ''}
322
+ {retrieved && hasNextPage ? <a onClick={loadNext} className="next round pagination">Next →</a> : ''}
323
+ </td>
324
+ </tr>
325
+ </tfoot>
287
326
  </table>
288
327
  </div>
289
328
  </div>
@@ -81,7 +81,7 @@ export default function NodeStatus({ node, revalidate = true }) {
81
81
  <>
82
82
  {revalidate && validation?.errors?.length > 0 ? (
83
83
  displayValidation
84
- ) : validation?.status === 'valid' || node?.status === 'valid' ? (
84
+ ) : validation?.status === 'valid' || node?.status === 'valid' || node?.current?.status === 'VALID' ? (
85
85
  <span
86
86
  className="status__valid status"
87
87
  style={{ alignContent: 'center' }}
@@ -12,12 +12,12 @@ type DocsSites = {
12
12
 
13
13
  // Default docs sites if REACT_APP_DOCS_SITES is not defined
14
14
  const defaultDocsSites: DocsSites = {
15
- "Open-Source": "https://www.datajunction.io/"
15
+ 'Open-Source': 'https://www.datajunction.io/',
16
16
  };
17
17
 
18
18
  // Parse the JSON map from the environment variable or use the default
19
19
  const docsSites: DocsSites = process.env.REACT_APP_DOCS_SITES
20
- ? JSON.parse(process.env.REACT_APP_DOCS_SITES as string) as DocsSites
20
+ ? (JSON.parse(process.env.REACT_APP_DOCS_SITES as string) as DocsSites)
21
21
  : defaultDocsSites;
22
22
 
23
23
  export function Root() {
@@ -32,7 +32,10 @@ export function Root() {
32
32
  <>
33
33
  <Helmet>
34
34
  <title>DataJunction</title>
35
- <meta name="description" content="DataJunction Metrics Platform Webapp" />
35
+ <meta
36
+ name="description"
37
+ content="DataJunction Metrics Platform Webapp"
38
+ />
36
39
  </Helmet>
37
40
  <div className="container d-flex align-items-center justify-content-between">
38
41
  <div className="header">
@@ -76,10 +79,18 @@ export function Root() {
76
79
  >
77
80
  Docs
78
81
  </a>
79
- <ul className="dropdown-menu" aria-labelledby="docsDropdown">
82
+ <ul
83
+ className="dropdown-menu"
84
+ aria-labelledby="docsDropdown"
85
+ >
80
86
  {Object.entries(docsSites).map(([key, value]) => (
81
87
  <li key={key}>
82
- <a className="dropdown-item" href={value} target="_blank" rel="noreferrer">
88
+ <a
89
+ className="dropdown-item"
90
+ href={value}
91
+ target="_blank"
92
+ rel="noreferrer"
93
+ >
83
94
  {key}
84
95
  </a>
85
96
  </li>
@@ -106,4 +117,4 @@ export function Root() {
106
117
  <Outlet />
107
118
  </>
108
119
  );
109
- }
120
+ }
@@ -4,7 +4,84 @@ const DJ_URL = process.env.REACT_APP_DJ_URL
4
4
  ? process.env.REACT_APP_DJ_URL
5
5
  : 'http://localhost:8000';
6
6
 
7
+ const DJ_GQL = process.env.REACT_APP_DJ_GQL
8
+ ? process.env.REACT_APP_DJ_GQL
9
+ : 'http://localhost:8000/graphql';
10
+
7
11
  export const DataJunctionAPI = {
12
+ listNodesForLanding: async function (
13
+ namespace,
14
+ nodeTypes,
15
+ tags,
16
+ editedBy,
17
+ before,
18
+ after,
19
+ limit,
20
+ ) {
21
+ const query = `
22
+ query ListNodes($namespace: String, $nodeTypes: [NodeType!], $tags: [String!], $editedBy: String, $before: String, $after: String, $limit: Int) {
23
+ findNodesPaginated(
24
+ namespace: $namespace
25
+ nodeTypes: $nodeTypes
26
+ tags: $tags
27
+ editedBy: $editedBy
28
+ limit: $limit
29
+ before: $before
30
+ after: $after
31
+ ) {
32
+ pageInfo {
33
+ hasNextPage
34
+ endCursor
35
+ hasPrevPage
36
+ startCursor
37
+ }
38
+ edges {
39
+ node {
40
+ name
41
+ type
42
+ currentVersion
43
+ tags {
44
+ name
45
+ tagType
46
+ }
47
+ editedBy
48
+ current {
49
+ displayName
50
+ status
51
+ updatedAt
52
+ }
53
+ createdBy {
54
+ username
55
+ }
56
+ }
57
+ }
58
+ }
59
+ }
60
+ `;
61
+
62
+ return await (
63
+ await fetch(DJ_GQL, {
64
+ method: 'POST',
65
+ headers: {
66
+ 'Content-Type': 'application/json',
67
+ },
68
+ credentials: 'include',
69
+ body: JSON.stringify({
70
+ query,
71
+ variables: {
72
+ namespace: namespace,
73
+ nodeTypes: nodeTypes,
74
+ tags: tags,
75
+ editedBy: editedBy,
76
+ before: before,
77
+ after: after,
78
+ limit: limit,
79
+ },
80
+ }),
81
+ })
82
+ ).json();
83
+ },
84
+
8
85
  whoami: async function () {
9
86
  return await (
10
87
  await fetch(`${DJ_URL}/whoami/`, { credentials: 'include' })
@@ -346,11 +423,14 @@ export const DataJunctionAPI = {
346
423
  ).json();
347
424
  },
348
425
 
349
- namespace: async function (nmspce) {
426
+ namespace: async function (nmspce, editedBy) {
350
427
  return await (
351
- await fetch(`${DJ_URL}/namespaces/${nmspce}/?with_edited_by=true`, {
352
- credentials: 'include',
353
- })
428
+ await fetch(
429
+ `${DJ_URL}/namespaces/${nmspce}?edited_by=${editedBy}&with_edited_by=true`,
430
+ {
431
+ credentials: 'include',
432
+ },
433
+ )
354
434
  ).json();
355
435
  },
356
436
 
@@ -426,7 +426,7 @@ describe('DataJunctionAPI', () => {
426
426
  const nmspce = 'sampleNamespace';
427
427
  fetch.mockResponseOnce(JSON.stringify({}));
428
428
  await DataJunctionAPI.namespace(nmspce);
429
- expect(fetch).toHaveBeenCalledWith(`${DJ_URL}/namespaces/${nmspce}/?with_edited_by=true`, {
429
+ expect(fetch).toHaveBeenCalledWith(`${DJ_URL}/namespaces/${nmspce}?edited_by=undefined&with_edited_by=true`, {
430
430
  credentials: 'include',
431
431
  });
432
432
  });
@@ -1069,4 +1069,20 @@ describe('DataJunctionAPI', () => {
1069
1069
  },
1070
1070
  );
1071
1071
  });
1072
+
1073
+ it('calls listNodesForLanding correctly', () => {
1074
+ fetch.mockResponseOnce(JSON.stringify({}));
1075
+
1076
+ DataJunctionAPI.listNodesForLanding('', ['source'], [], '', null, null, 100);
1077
+ expect(fetch).toHaveBeenCalledWith(
1078
+ `${DJ_URL}/graphql`,
1079
+ expect.objectContaining({
1080
+ method: 'POST',
1081
+ credentials: 'include',
1082
+ headers: {
1083
+ 'Content-Type': 'application/json',
1084
+ },
1085
+ }),
1086
+ );
1087
+ });
1072
1088
  });
@@ -1050,6 +1050,32 @@ pre {
1050
1050
  white-space: break-spaces;
1051
1051
  }
1052
1052
 
1053
+ .pagination {
1054
+ background-color: #fff !important;
1055
+ color: #74b7c3;
1056
+ border: 1px solid #74b7c3;
1057
+ text-transform: none;
1058
+ vertical-align: middle;
1059
+ padding-right: 1rem;
1060
+ padding-left: 1rem;
1061
+ padding-top: 0.5rem;
1062
+ padding-bottom: 0.5rem;
1063
+ margin-left: 0.5rem !important;
1064
+ margin-bottom: 0.5rem !important;
1065
+ font-size: 1rem;
1066
+ border-radius: 0.2rem;
1067
+ word-wrap: break-word;
1068
+ white-space: break-spaces;
1069
+ text-decoration: none;
1070
+ }
1071
+ .pagination:hover {
1072
+ background-color: #b6dae0 !important;
1073
+ color: #436e76;
1074
+ text-transform: none;
1075
+ text-decoration: none;
1076
+ cursor: pointer;
1077
+ }
1078
+
1053
1079
  .edit_button {
1054
1080
  background: none;
1055
1081
  color: inherit;