datajunction-ui 0.0.1-a60 → 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 +2 -2
- package/src/app/components/AddNodeDropdown.jsx +41 -0
- package/src/app/icons/FilterIcon.jsx +7 -0
- package/src/app/pages/NamespacePage/FieldControl.jsx +21 -0
- package/src/app/pages/NamespacePage/NodeTypeSelect.jsx +27 -0
- package/src/app/pages/NamespacePage/TagSelect.jsx +40 -0
- package/src/app/pages/NamespacePage/UserSelect.jsx +43 -0
- package/src/app/pages/NamespacePage/__tests__/index.test.jsx +32 -0
- package/src/app/pages/NamespacePage/index.jsx +178 -86
- package/src/app/services/DJService.js +20 -7
- package/src/app/services/__tests__/DJService.test.jsx +1 -1
- package/src/styles/index.css +10 -0
- package/src/styles/sorted-table.css +15 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "datajunction-ui",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.1a62",
|
|
4
4
|
"description": "DataJunction Metrics Platform UI",
|
|
5
5
|
"module": "src/index.tsx",
|
|
6
6
|
"repository": {
|
|
@@ -165,7 +165,7 @@
|
|
|
165
165
|
"coverageThreshold": {
|
|
166
166
|
"global": {
|
|
167
167
|
"statements": 87,
|
|
168
|
-
"branches":
|
|
168
|
+
"branches": 74,
|
|
169
169
|
"lines": 80,
|
|
170
170
|
"functions": 85
|
|
171
171
|
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
export default function AddNodeDropdown({ namespace }) {
|
|
2
|
+
return (
|
|
3
|
+
<span className="menu-link" style={{ margin: '0.5em 0 0 1em' }}>
|
|
4
|
+
<span className="menu-title">
|
|
5
|
+
<div className="dropdown">
|
|
6
|
+
<span className="add_node">+ Add Node</span>
|
|
7
|
+
<div className="dropdown-content">
|
|
8
|
+
<a href={`/create/source`}>
|
|
9
|
+
<div className="node_type__source node_type_creation_heading">
|
|
10
|
+
Register Table
|
|
11
|
+
</div>
|
|
12
|
+
</a>
|
|
13
|
+
<a href={`/create/transform/${namespace}`}>
|
|
14
|
+
<div className="node_type__transform node_type_creation_heading">
|
|
15
|
+
Transform
|
|
16
|
+
</div>
|
|
17
|
+
</a>
|
|
18
|
+
<a href={`/create/metric/${namespace}`}>
|
|
19
|
+
<div className="node_type__metric node_type_creation_heading">
|
|
20
|
+
Metric
|
|
21
|
+
</div>
|
|
22
|
+
</a>
|
|
23
|
+
<a href={`/create/dimension/${namespace}`}>
|
|
24
|
+
<div className="node_type__dimension node_type_creation_heading">
|
|
25
|
+
Dimension
|
|
26
|
+
</div>
|
|
27
|
+
</a>
|
|
28
|
+
<a href={`/create/tag`}>
|
|
29
|
+
<div className="entity__tag node_type_creation_heading">Tag</div>
|
|
30
|
+
</a>
|
|
31
|
+
<a href={`/create/cube/${namespace}`}>
|
|
32
|
+
<div className="node_type__cube node_type_creation_heading">
|
|
33
|
+
Cube
|
|
34
|
+
</div>
|
|
35
|
+
</a>
|
|
36
|
+
</div>
|
|
37
|
+
</div>
|
|
38
|
+
</span>
|
|
39
|
+
</span>
|
|
40
|
+
);
|
|
41
|
+
}
|
|
@@ -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'));
|
|
@@ -4,11 +4,24 @@ import { useContext, useEffect, useState } from 'react';
|
|
|
4
4
|
import NodeStatus from '../NodePage/NodeStatus';
|
|
5
5
|
import DJClientContext from '../../providers/djclient';
|
|
6
6
|
import Explorer from '../NamespacePage/Explorer';
|
|
7
|
+
import AddNodeDropdown from '../../components/AddNodeDropdown';
|
|
7
8
|
import NodeListActions from '../../components/NodeListActions';
|
|
8
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
|
+
|
|
9
16
|
import 'styles/node-list.css';
|
|
17
|
+
import 'styles/sorted-table.css';
|
|
10
18
|
|
|
11
19
|
export function NamespacePage() {
|
|
20
|
+
const ASC = 'ascending';
|
|
21
|
+
const DESC = 'descending';
|
|
22
|
+
|
|
23
|
+
const fields = ['name', 'display_name', 'type', 'status', 'updated_at'];
|
|
24
|
+
|
|
12
25
|
const djClient = useContext(DJClientContext).DataJunctionAPI;
|
|
13
26
|
var { namespace } = useParams();
|
|
14
27
|
|
|
@@ -16,9 +29,68 @@ export function NamespacePage() {
|
|
|
16
29
|
namespace: namespace,
|
|
17
30
|
nodes: [],
|
|
18
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
|
+
});
|
|
19
40
|
|
|
20
41
|
const [namespaceHierarchy, setNamespaceHierarchy] = useState([]);
|
|
21
42
|
|
|
43
|
+
const [sortConfig, setSortConfig] = useState({
|
|
44
|
+
key: 'updated_at',
|
|
45
|
+
direction: DESC,
|
|
46
|
+
});
|
|
47
|
+
const sortedNodes = React.useMemo(() => {
|
|
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
|
+
}
|
|
65
|
+
if (sortConfig !== null) {
|
|
66
|
+
sortableData.sort((a, b) => {
|
|
67
|
+
if (a[sortConfig.key] < b[sortConfig.key]) {
|
|
68
|
+
return sortConfig.direction === ASC ? -1 : 1;
|
|
69
|
+
}
|
|
70
|
+
if (a[sortConfig.key] > b[sortConfig.key]) {
|
|
71
|
+
return sortConfig.direction === ASC ? 1 : -1;
|
|
72
|
+
}
|
|
73
|
+
return 0;
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
return sortableData;
|
|
77
|
+
}, [state.nodes, filters, sortConfig]);
|
|
78
|
+
|
|
79
|
+
const requestSort = key => {
|
|
80
|
+
let direction = ASC;
|
|
81
|
+
if (sortConfig.key === key && sortConfig.direction === ASC) {
|
|
82
|
+
direction = DESC;
|
|
83
|
+
}
|
|
84
|
+
setSortConfig({ key, direction });
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const getClassNamesFor = name => {
|
|
88
|
+
if (sortConfig.key === name) {
|
|
89
|
+
return sortConfig.direction;
|
|
90
|
+
}
|
|
91
|
+
return undefined;
|
|
92
|
+
};
|
|
93
|
+
|
|
22
94
|
const createNamespaceHierarchy = namespaceList => {
|
|
23
95
|
const hierarchy = [];
|
|
24
96
|
|
|
@@ -52,6 +124,8 @@ export function NamespacePage() {
|
|
|
52
124
|
const namespaces = await djClient.namespaces();
|
|
53
125
|
const hierarchy = createNamespaceHierarchy(namespaces);
|
|
54
126
|
setNamespaceHierarchy(hierarchy);
|
|
127
|
+
const currentUser = await djClient.whoami();
|
|
128
|
+
setCurrentUser(currentUser);
|
|
55
129
|
};
|
|
56
130
|
fetchData().catch(console.error);
|
|
57
131
|
}, [djClient, djClient.namespaces]);
|
|
@@ -67,95 +141,106 @@ export function NamespacePage() {
|
|
|
67
141
|
namespace: namespace,
|
|
68
142
|
nodes: foundNodes,
|
|
69
143
|
});
|
|
144
|
+
setRetrieved(true);
|
|
70
145
|
};
|
|
71
146
|
fetchData().catch(console.error);
|
|
72
147
|
}, [djClient, namespace, namespaceHierarchy]);
|
|
73
148
|
|
|
74
|
-
const nodesList =
|
|
75
|
-
|
|
76
|
-
<
|
|
77
|
-
<
|
|
78
|
-
{node.name}
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
<
|
|
89
|
-
{
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
<
|
|
94
|
-
{node.type}
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
<
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
<
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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
|
+
);
|
|
113
191
|
|
|
114
192
|
return (
|
|
115
193
|
<div className="mid">
|
|
116
194
|
<div className="card">
|
|
117
195
|
<div className="card-header">
|
|
118
196
|
<h2>Explore</h2>
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
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>
|
|
159
244
|
<div className="table-responsive">
|
|
160
245
|
<div className={`sidebar`}>
|
|
161
246
|
<span
|
|
@@ -167,7 +252,7 @@ export function NamespacePage() {
|
|
|
167
252
|
padding: '1rem 1rem 1rem 0',
|
|
168
253
|
}}
|
|
169
254
|
>
|
|
170
|
-
Namespaces <AddNamespacePopover namespace={namespace}/>
|
|
255
|
+
Namespaces <AddNamespacePopover namespace={namespace} />
|
|
171
256
|
</span>
|
|
172
257
|
{namespaceHierarchy
|
|
173
258
|
? namespaceHierarchy.map(child => (
|
|
@@ -182,12 +267,19 @@ export function NamespacePage() {
|
|
|
182
267
|
<table className="card-table table">
|
|
183
268
|
<thead>
|
|
184
269
|
<tr>
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
270
|
+
{fields.map(field => {
|
|
271
|
+
return (
|
|
272
|
+
<th>
|
|
273
|
+
<button
|
|
274
|
+
type="button"
|
|
275
|
+
onClick={() => requestSort(field)}
|
|
276
|
+
className={'sortable ' + getClassNamesFor(field)}
|
|
277
|
+
>
|
|
278
|
+
{field.replace('_', ' ')}
|
|
279
|
+
</button>
|
|
280
|
+
</th>
|
|
281
|
+
);
|
|
282
|
+
})}
|
|
191
283
|
<th>Actions</th>
|
|
192
284
|
</tr>
|
|
193
285
|
</thead>
|
|
@@ -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(
|
|
457
|
-
|
|
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(
|
|
464
|
-
|
|
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
|
});
|
package/src/styles/index.css
CHANGED
|
@@ -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
|
}
|
|
@@ -730,6 +734,11 @@ pre {
|
|
|
730
734
|
margin: 0;
|
|
731
735
|
list-style: none;
|
|
732
736
|
}
|
|
737
|
+
.menu-link {
|
|
738
|
+
display: inline-grid;
|
|
739
|
+
margin: 0;
|
|
740
|
+
float: right;
|
|
741
|
+
}
|
|
733
742
|
.menu-item .menu-link {
|
|
734
743
|
cursor: pointer;
|
|
735
744
|
align-items: center;
|
|
@@ -773,6 +782,7 @@ pre {
|
|
|
773
782
|
|
|
774
783
|
.card-header h2 {
|
|
775
784
|
font-family: 'Jost';
|
|
785
|
+
display: inline-block;
|
|
776
786
|
}
|
|
777
787
|
|
|
778
788
|
.text-gray-400 {
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
.sortable {
|
|
2
|
+
background: none;
|
|
3
|
+
border: none;
|
|
4
|
+
cursor: pointer;
|
|
5
|
+
font-weight: bold;
|
|
6
|
+
font-family: inherit;
|
|
7
|
+
color: inherit;
|
|
8
|
+
text-transform: uppercase;
|
|
9
|
+
}
|
|
10
|
+
.sortable.ascending::after {
|
|
11
|
+
content: ' ▲';
|
|
12
|
+
}
|
|
13
|
+
.sortable.descending::after {
|
|
14
|
+
content: ' ▼';
|
|
15
|
+
}
|