datajunction-ui 0.0.1-a61 → 0.0.1-a62

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.1a61",
3
+ "version": "0.0.1a62",
4
4
  "description": "DataJunction Metrics Platform UI",
5
5
  "module": "src/index.tsx",
6
6
  "repository": {
@@ -1,6 +1,6 @@
1
- export default function AddNodeDropDown({ namespace }) {
1
+ export default function AddNodeDropdown({ namespace }) {
2
2
  return (
3
- <span className="menu-link">
3
+ <span className="menu-link" style={{ margin: '0.5em 0 0 1em' }}>
4
4
  <span className="menu-title">
5
5
  <div className="dropdown">
6
6
  <span className="add_node">+ Add Node</span>
@@ -26,9 +26,7 @@ export default function AddNodeDropDown({ namespace }) {
26
26
  </div>
27
27
  </a>
28
28
  <a href={`/create/tag`}>
29
- <div className="entity__tag node_type_creation_heading">
30
- Tag
31
- </div>
29
+ <div className="entity__tag node_type_creation_heading">Tag</div>
32
30
  </a>
33
31
  <a href={`/create/cube/${namespace}`}>
34
32
  <div className="node_type__cube node_type_creation_heading">
@@ -0,0 +1,7 @@
1
+ const FilterIcon = props => (
2
+ <svg height="23px" viewBox="0 0 48 48" xmlns="http://www.w3.org/2000/svg">
3
+ <path d="M20 36h8v-4h-8v4zm-14-24v4h36v-4h-36zm6 14h24v-4h-24v4z" />
4
+ <path d="M0 0h48v48h-48z" fill="none" />
5
+ </svg>
6
+ );
7
+ export default FilterIcon;
@@ -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,27 @@
1
+ import Select from 'react-select';
2
+ import Control from './FieldControl';
3
+
4
+ export default function NodeTypeSelect({ onChange }) {
5
+ return (
6
+ <span className="menu-link" style={{ marginLeft: '30px', width: '300px' }}
7
+ data-testid="select-node-type">
8
+ <Select
9
+ name="node_type"
10
+ isClearable
11
+ label="Node Type"
12
+ components={{ Control }}
13
+ onChange={e => onChange(e)}
14
+ styles={{
15
+ control: styles => ({ ...styles, backgroundColor: 'white' }),
16
+ }}
17
+ options={[
18
+ {value: 'source', label: 'Source'},
19
+ {value: 'transform', label: 'Transform'},
20
+ {value: 'dimension', label: 'Dimension'},
21
+ {value: 'metric', label: 'Metric'},
22
+ {value: 'cube', label: 'Cube'},
23
+ ]}
24
+ />
25
+ </span>
26
+ );
27
+ }
@@ -0,0 +1,40 @@
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 className="menu-link" style={{ marginLeft: '30px', width: '350px' }} data-testid="select-tag">
24
+ <Select
25
+ name="tags"
26
+ isClearable
27
+ isMulti
28
+ label="Tags"
29
+ components={{ Control }}
30
+ onChange={e => onChange(e)}
31
+ options={tags?.map(tag => {
32
+ return {
33
+ value: tag.name,
34
+ label: tag.display_name,
35
+ };
36
+ })}
37
+ />
38
+ </span>
39
+ );
40
+ }
@@ -0,0 +1,43 @@
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 className="menu-link" style={{ marginLeft: '30px', width: '400px' }} data-testid="select-user">
23
+ {retrieved ? (
24
+ <Select
25
+ name="edited_by"
26
+ isClearable
27
+ label="Edited By"
28
+ components={{ Control }}
29
+ onChange={e => onChange(e)}
30
+ defaultValue={{
31
+ value: currentUser,
32
+ label: currentUser,
33
+ }}
34
+ options={users?.map(user => {
35
+ return { value: user.username, label: user.username };
36
+ })}
37
+ />
38
+ ) : (
39
+ ''
40
+ )}
41
+ </span>
42
+ );
43
+ }
@@ -9,6 +9,9 @@ const mockDjClient = {
9
9
  namespaces: jest.fn(),
10
10
  namespace: jest.fn(),
11
11
  addNamespace: jest.fn(),
12
+ whoami: jest.fn(),
13
+ users: jest.fn(),
14
+ listTags: jest.fn(),
12
15
  };
13
16
 
14
17
  describe('NamespacePage', () => {
@@ -34,6 +37,9 @@ describe('NamespacePage', () => {
34
37
 
35
38
  beforeEach(() => {
36
39
  fetch.resetMocks();
40
+ mockDjClient.whoami.mockResolvedValue({username: 'dj'});
41
+ mockDjClient.users.mockResolvedValue([{username: 'dj'}, {username: 'user1'}]);
42
+ mockDjClient.listTags.mockResolvedValue([{name: 'tag1'}, {name: 'tag2'}]);
37
43
  mockDjClient.namespaces.mockResolvedValue([
38
44
  {
39
45
  namespace: 'common.one',
@@ -75,6 +81,8 @@ describe('NamespacePage', () => {
75
81
  type: 'transform',
76
82
  mode: 'active',
77
83
  updated_at: new Date(),
84
+ tags: [{name: 'tag1'}],
85
+ edited_by: ['dj'],
78
86
  },
79
87
  ]);
80
88
  });
@@ -111,6 +119,30 @@ describe('NamespacePage', () => {
111
119
  // check that it renders nodes
112
120
  expect(screen.getByText('Test Node')).toBeInTheDocument();
113
121
 
122
+ // check that it sorts nodes
123
+ fireEvent.click(screen.getByText('name'));
124
+ fireEvent.click(screen.getByText('display name'));
125
+
126
+ // check that we can filter by node type
127
+ const selectNodeType = screen.getAllByTestId('select-node-type')[0];
128
+ expect(selectNodeType).toBeDefined();
129
+ expect(selectNodeType).not.toBeNull();
130
+ fireEvent.keyDown(selectNodeType.firstChild, { key: 'ArrowDown' });
131
+ fireEvent.click(screen.getByText('Source'));
132
+
133
+ // check that we can filter by tag
134
+ const selectTag = screen.getAllByTestId('select-tag')[0];
135
+ expect(selectTag).toBeDefined();
136
+ expect(selectTag).not.toBeNull();
137
+ fireEvent.keyDown(selectTag.firstChild, { key: 'ArrowDown' });
138
+
139
+ // check that we can filter by user
140
+ const selectUser = screen.getAllByTestId('select-user')[0];
141
+ expect(selectUser).toBeDefined();
142
+ expect(selectUser).not.toBeNull();
143
+ fireEvent.keyDown(selectUser.firstChild, { key: 'ArrowDown' });
144
+ // fireEvent.click(screen.getByText('dj'));
145
+
114
146
  // click to open and close tab
115
147
  fireEvent.click(screen.getByText('common'));
116
148
  fireEvent.click(screen.getByText('common'));
@@ -7,6 +7,12 @@ import Explorer from '../NamespacePage/Explorer';
7
7
  import AddNodeDropdown from '../../components/AddNodeDropdown';
8
8
  import NodeListActions from '../../components/NodeListActions';
9
9
  import AddNamespacePopover from './AddNamespacePopover';
10
+ import FilterIcon from '../../icons/FilterIcon';
11
+ import LoadingIcon from '../../icons/LoadingIcon';
12
+ import UserSelect from './UserSelect';
13
+ import NodeTypeSelect from './NodeTypeSelect';
14
+ import TagSelect from './TagSelect';
15
+
10
16
  import 'styles/node-list.css';
11
17
  import 'styles/sorted-table.css';
12
18
 
@@ -23,12 +29,39 @@ export function NamespacePage() {
23
29
  namespace: namespace,
24
30
  nodes: [],
25
31
  });
32
+ const [retrieved, setRetrieved] = useState(false);
33
+ const [currentUser, setCurrentUser] = useState(null);
34
+
35
+ const [filters, setFilters] = useState({
36
+ tags: [],
37
+ node_type: '',
38
+ edited_by: currentUser?.username,
39
+ });
26
40
 
27
41
  const [namespaceHierarchy, setNamespaceHierarchy] = useState([]);
28
42
 
29
- const [sortConfig, setSortConfig] = useState({ key: 'updated_at', direction: DESC });
43
+ const [sortConfig, setSortConfig] = useState({
44
+ key: 'updated_at',
45
+ direction: DESC,
46
+ });
30
47
  const sortedNodes = React.useMemo(() => {
31
48
  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
+ }
32
65
  if (sortConfig !== null) {
33
66
  sortableData.sort((a, b) => {
34
67
  if (a[sortConfig.key] < b[sortConfig.key]) {
@@ -41,9 +74,9 @@ export function NamespacePage() {
41
74
  });
42
75
  }
43
76
  return sortableData;
44
- }, [state.nodes, sortConfig]);
77
+ }, [state.nodes, filters, sortConfig]);
45
78
 
46
- const requestSort = (key) => {
79
+ const requestSort = key => {
47
80
  let direction = ASC;
48
81
  if (sortConfig.key === key && sortConfig.direction === ASC) {
49
82
  direction = DESC;
@@ -51,7 +84,7 @@ export function NamespacePage() {
51
84
  setSortConfig({ key, direction });
52
85
  };
53
86
 
54
- const getClassNamesFor = (name) => {
87
+ const getClassNamesFor = name => {
55
88
  if (sortConfig.key === name) {
56
89
  return sortConfig.direction;
57
90
  }
@@ -91,6 +124,8 @@ export function NamespacePage() {
91
124
  const namespaces = await djClient.namespaces();
92
125
  const hierarchy = createNamespaceHierarchy(namespaces);
93
126
  setNamespaceHierarchy(hierarchy);
127
+ const currentUser = await djClient.whoami();
128
+ setCurrentUser(currentUser);
94
129
  };
95
130
  fetchData().catch(console.error);
96
131
  }, [djClient, djClient.namespaces]);
@@ -106,53 +141,106 @@ export function NamespacePage() {
106
141
  namespace: namespace,
107
142
  nodes: foundNodes,
108
143
  });
144
+ setRetrieved(true);
109
145
  };
110
146
  fetchData().catch(console.error);
111
147
  }, [djClient, namespace, namespaceHierarchy]);
112
148
 
113
- const nodesList = sortedNodes.map(node => (
114
- <tr>
115
- <td>
116
- <a href={'/nodes/' + node.name} className="link-table">
117
- {node.name}
118
- </a>
119
- <span
120
- className="rounded-pill badge bg-secondary-soft"
121
- style={{ marginLeft: '0.5rem' }}
122
- >
123
- {node.version}
124
- </span>
125
- </td>
126
- <td>
127
- <a href={'/nodes/' + node.name} className="link-table">
128
- {node.type !== 'source' ? node.display_name : ''}
129
- </a>
130
- </td>
131
- <td>
132
- <span className={'node_type__' + node.type + ' badge node_type'}>
133
- {node.type}
134
- </span>
135
- </td>
136
- <td>
137
- <NodeStatus node={node} revalidate={false} />
138
- </td>
139
- <td>
140
- <span className="status">
141
- {new Date(node.updated_at).toLocaleString('en-us')}
142
- </span>
143
- </td>
144
- <td>
145
- <NodeListActions nodeName={node?.name} />
146
- </td>
147
- </tr>
148
- ));
149
+ const nodesList = retrieved ? (
150
+ sortedNodes.map(node => (
151
+ <tr>
152
+ <td>
153
+ <a href={'/nodes/' + node.name} className="link-table">
154
+ {node.name}
155
+ </a>
156
+ <span
157
+ className="rounded-pill badge bg-secondary-soft"
158
+ style={{ marginLeft: '0.5rem' }}
159
+ >
160
+ {node.version}
161
+ </span>
162
+ </td>
163
+ <td>
164
+ <a href={'/nodes/' + node.name} className="link-table">
165
+ {node.type !== 'source' ? node.display_name : ''}
166
+ </a>
167
+ </td>
168
+ <td>
169
+ <span className={'node_type__' + node.type + ' badge node_type'}>
170
+ {node.type}
171
+ </span>
172
+ </td>
173
+ <td>
174
+ <NodeStatus node={node} revalidate={false} />
175
+ </td>
176
+ <td>
177
+ <span className="status">
178
+ {new Date(node.updated_at).toLocaleString('en-us')}
179
+ </span>
180
+ </td>
181
+ <td>
182
+ <NodeListActions nodeName={node?.name} />
183
+ </td>
184
+ </tr>
185
+ ))
186
+ ) : (
187
+ <span style={{ display: 'block', marginTop: '2rem' }}>
188
+ <LoadingIcon />
189
+ </span>
190
+ );
149
191
 
150
192
  return (
151
193
  <div className="mid">
152
194
  <div className="card">
153
195
  <div className="card-header">
154
196
  <h2>Explore</h2>
155
- <AddNodeDropdown namespace={namespace} />
197
+ <div class="menu" style={{ margin: '0 0 20px 0' }}>
198
+ <div
199
+ className="menu-link"
200
+ style={{
201
+ marginTop: '0.7em',
202
+ color: '#777',
203
+ fontFamily: "'Jost'",
204
+ fontSize: '18px',
205
+ marginRight: '10px',
206
+ marginLeft: '15px',
207
+ }}
208
+ >
209
+ <FilterIcon />
210
+ </div>
211
+ <div
212
+ className="menu-link"
213
+ style={{
214
+ marginTop: '0.6em',
215
+ color: '#777',
216
+ fontFamily: "'Jost'",
217
+ fontSize: '18px',
218
+ marginRight: '10px',
219
+ }}
220
+ >
221
+ Filter By
222
+ </div>
223
+ <NodeTypeSelect
224
+ onChange={entry =>
225
+ setFilters({ ...filters, node_type: entry ? entry.value : '' })
226
+ }
227
+ />
228
+ <TagSelect
229
+ onChange={entry =>
230
+ setFilters({
231
+ ...filters,
232
+ tags: entry ? entry.map(tag => tag.value) : [],
233
+ })
234
+ }
235
+ />
236
+ <UserSelect
237
+ onChange={entry =>
238
+ setFilters({ ...filters, edited_by: entry ? entry.value : '' })
239
+ }
240
+ currentUser={currentUser?.username}
241
+ />
242
+ <AddNodeDropdown namespace={namespace} />
243
+ </div>
156
244
  <div className="table-responsive">
157
245
  <div className={`sidebar`}>
158
246
  <span
@@ -164,7 +252,7 @@ export function NamespacePage() {
164
252
  padding: '1rem 1rem 1rem 0',
165
253
  }}
166
254
  >
167
- Namespaces <AddNamespacePopover namespace={namespace}/>
255
+ Namespaces <AddNamespacePopover namespace={namespace} />
168
256
  </span>
169
257
  {namespaceHierarchy
170
258
  ? namespaceHierarchy.map(child => (
@@ -182,7 +270,11 @@ export function NamespacePage() {
182
270
  {fields.map(field => {
183
271
  return (
184
272
  <th>
185
- <button type="button" onClick={() => requestSort(field)} className={'sortable ' + getClassNamesFor(field)}>
273
+ <button
274
+ type="button"
275
+ onClick={() => requestSort(field)}
276
+ className={'sortable ' + getClassNamesFor(field)}
277
+ >
186
278
  {field.replace('_', ' ')}
187
279
  </button>
188
280
  </th>
@@ -348,7 +348,7 @@ export const DataJunctionAPI = {
348
348
 
349
349
  namespace: async function (nmspce) {
350
350
  return await (
351
- await fetch(`${DJ_URL}/namespaces/${nmspce}/`, {
351
+ await fetch(`${DJ_URL}/namespaces/${nmspce}/?with_edited_by=true`, {
352
352
  credentials: 'include',
353
353
  })
354
354
  ).json();
@@ -453,16 +453,22 @@ export const DataJunctionAPI = {
453
453
  },
454
454
 
455
455
  notebookExportCube: async function (cube) {
456
- return await fetch(`${DJ_URL}/datajunction-clients/python/notebook/?cube=${cube}`, {
457
- credentials: 'include',
458
- });
456
+ return await fetch(
457
+ `${DJ_URL}/datajunction-clients/python/notebook/?cube=${cube}`,
458
+ {
459
+ credentials: 'include',
460
+ },
461
+ );
459
462
  },
460
463
 
461
464
  notebookExportNamespace: async function (namespace) {
462
465
  return await (
463
- await fetch(`${DJ_URL}/datajunction-clients/python/notebook/?namespace=${namespace}`, {
464
- credentials: 'include',
465
- })
466
+ await fetch(
467
+ `${DJ_URL}/datajunction-clients/python/notebook/?namespace=${namespace}`,
468
+ {
469
+ credentials: 'include',
470
+ },
471
+ )
466
472
  ).json();
467
473
  },
468
474
 
@@ -732,6 +738,13 @@ export const DataJunctionAPI = {
732
738
  });
733
739
  return await response.json();
734
740
  },
741
+ users: async function () {
742
+ return await (
743
+ await fetch(`${DJ_URL}/users?with_activity=true`, {
744
+ credentials: 'include',
745
+ })
746
+ ).json();
747
+ },
735
748
  getTag: async function (tagName) {
736
749
  const response = await fetch(`${DJ_URL}/tags/${tagName}`, {
737
750
  method: 'GET',
@@ -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}/`, {
429
+ expect(fetch).toHaveBeenCalledWith(`${DJ_URL}/namespaces/${nmspce}/?with_edited_by=true`, {
430
430
  credentials: 'include',
431
431
  });
432
432
  });
@@ -289,6 +289,10 @@ tr {
289
289
  grid-gap: 8px;
290
290
  padding: 8px;
291
291
  }
292
+ .table-responsive {
293
+ width: auto;
294
+ grid-template-columns: 200px auto;
295
+ }
292
296
  .table-vertical {
293
297
  display: contents;
294
298
  }
@@ -732,7 +736,7 @@ pre {
732
736
  }
733
737
  .menu-link {
734
738
  display: inline-grid;
735
- margin: 2em 1em 0 0;
739
+ margin: 0;
736
740
  float: right;
737
741
  }
738
742
  .menu-item .menu-link {