datajunction-ui 0.0.1-a70 → 0.0.1-a71
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/src/app/icons/AddItemIcon.jsx +1 -1
- package/src/app/pages/NamespacePage/Explorer.jsx +6 -2
- package/src/app/pages/NamespacePage/NodeTypeSelect.jsx +10 -7
- package/src/app/pages/NamespacePage/TagSelect.jsx +5 -1
- package/src/app/pages/NamespacePage/UserSelect.jsx +5 -1
- package/src/app/pages/NamespacePage/__tests__/index.test.jsx +96 -49
- package/src/app/pages/NamespacePage/index.jsx +159 -64
- package/src/app/pages/NodePage/NodeMaterializationTab.jsx +16 -0
- package/src/app/pages/NodePage/NodeStatus.jsx +3 -1
- package/src/app/pages/NodePage/NotebookDownload.jsx +1 -1
- package/src/app/pages/NodePage/__tests__/NodeMaterializationTab.test.jsx +148 -0
- package/src/app/pages/Root/index.tsx +18 -7
- package/src/app/services/DJService.js +84 -4
- package/src/app/services/__tests__/DJService.test.jsx +32 -5
- package/src/styles/index.css +88 -56
- package/src/styles/node-list.css +1 -1
package/package.json
CHANGED
|
@@ -10,7 +10,7 @@ const Explorer = ({ item = [], current }) => {
|
|
|
10
10
|
useEffect(() => {
|
|
11
11
|
setItems(item);
|
|
12
12
|
setHighlight(current);
|
|
13
|
-
if (current
|
|
13
|
+
if (current !== undefined && current?.startsWith(item.path)) {
|
|
14
14
|
setExpand(true);
|
|
15
15
|
} else setExpand(false);
|
|
16
16
|
}, [current, item]);
|
|
@@ -43,8 +43,12 @@ 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
|
|
48
|
+
<div
|
|
49
|
+
className={`${expand ? '' : 'inactive'}`}
|
|
50
|
+
key={`nested-${index}`}
|
|
51
|
+
>
|
|
48
52
|
<Explorer item={item} current={highlight} />
|
|
49
53
|
</div>
|
|
50
54
|
</div>
|
|
@@ -3,8 +3,11 @@ import Control from './FieldControl';
|
|
|
3
3
|
|
|
4
4
|
export default function NodeTypeSelect({ onChange }) {
|
|
5
5
|
return (
|
|
6
|
-
<span
|
|
7
|
-
|
|
6
|
+
<span
|
|
7
|
+
className="menu-link"
|
|
8
|
+
style={{ marginLeft: '30px', width: '300px' }}
|
|
9
|
+
data-testid="select-node-type"
|
|
10
|
+
>
|
|
8
11
|
<Select
|
|
9
12
|
name="node_type"
|
|
10
13
|
isClearable
|
|
@@ -15,11 +18,11 @@ export default function NodeTypeSelect({ onChange }) {
|
|
|
15
18
|
control: styles => ({ ...styles, backgroundColor: 'white' }),
|
|
16
19
|
}}
|
|
17
20
|
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'},
|
|
21
|
+
{ value: 'source', label: 'Source' },
|
|
22
|
+
{ value: 'transform', label: 'Transform' },
|
|
23
|
+
{ value: 'dimension', label: 'Dimension' },
|
|
24
|
+
{ value: 'metric', label: 'Metric' },
|
|
25
|
+
{ value: 'cube', label: 'Cube' },
|
|
23
26
|
]}
|
|
24
27
|
/>
|
|
25
28
|
</span>
|
|
@@ -20,7 +20,11 @@ export default function TagSelect({ onChange }) {
|
|
|
20
20
|
}, [djClient]);
|
|
21
21
|
|
|
22
22
|
return (
|
|
23
|
-
<span
|
|
23
|
+
<span
|
|
24
|
+
className="menu-link"
|
|
25
|
+
style={{ marginLeft: '30px', width: '350px' }}
|
|
26
|
+
data-testid="select-tag"
|
|
27
|
+
>
|
|
24
28
|
<Select
|
|
25
29
|
name="tags"
|
|
26
30
|
isClearable
|
|
@@ -19,7 +19,11 @@ export default function UserSelect({ onChange, currentUser }) {
|
|
|
19
19
|
}, [djClient]);
|
|
20
20
|
|
|
21
21
|
return (
|
|
22
|
-
<span
|
|
22
|
+
<span
|
|
23
|
+
className="menu-link"
|
|
24
|
+
style={{ marginLeft: '30px', width: '400px' }}
|
|
25
|
+
data-testid="select-user"
|
|
26
|
+
>
|
|
23
27
|
{retrieved ? (
|
|
24
28
|
<Select
|
|
25
29
|
name="edited_by"
|
|
@@ -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(),
|
|
@@ -37,9 +38,15 @@ describe('NamespacePage', () => {
|
|
|
37
38
|
|
|
38
39
|
beforeEach(() => {
|
|
39
40
|
fetch.resetMocks();
|
|
40
|
-
mockDjClient.whoami.mockResolvedValue({username: 'dj'});
|
|
41
|
-
mockDjClient.users.mockResolvedValue([
|
|
42
|
-
|
|
41
|
+
mockDjClient.whoami.mockResolvedValue({ username: 'dj' });
|
|
42
|
+
mockDjClient.users.mockResolvedValue([
|
|
43
|
+
{ username: 'dj' },
|
|
44
|
+
{ username: 'user1' },
|
|
45
|
+
]);
|
|
46
|
+
mockDjClient.listTags.mockResolvedValue([
|
|
47
|
+
{ name: 'tag1' },
|
|
48
|
+
{ name: 'tag2' },
|
|
49
|
+
]);
|
|
43
50
|
mockDjClient.namespaces.mockResolvedValue([
|
|
44
51
|
{
|
|
45
52
|
namespace: 'common.one',
|
|
@@ -81,10 +88,43 @@ describe('NamespacePage', () => {
|
|
|
81
88
|
type: 'transform',
|
|
82
89
|
mode: 'active',
|
|
83
90
|
updated_at: new Date(),
|
|
84
|
-
tags: [{name: 'tag1'}],
|
|
91
|
+
tags: [{ name: 'tag1' }],
|
|
85
92
|
edited_by: ['dj'],
|
|
86
93
|
},
|
|
87
94
|
]);
|
|
95
|
+
mockDjClient.listNodesForLanding.mockResolvedValue({
|
|
96
|
+
data: {
|
|
97
|
+
findNodesPaginated: {
|
|
98
|
+
pageInfo: {
|
|
99
|
+
hasNextPage: true,
|
|
100
|
+
endCursor:
|
|
101
|
+
'eyJjcmVhdGVkX2F0IjogIjIwMjQtMDQtMTZUMjM6MjI6MjIuNDQxNjg2KzAwOjAwIiwgImlkIjogNjE0fQ==',
|
|
102
|
+
hasPrevPage: true,
|
|
103
|
+
startCursor:
|
|
104
|
+
'eyJjcmVhdGVkX2F0IjogIjIwMjQtMTAtMTZUMTY6MDM6MTcuMDgzMjY3KzAwOjAwIiwgImlkIjogMjQwOX0=',
|
|
105
|
+
},
|
|
106
|
+
edges: [
|
|
107
|
+
{
|
|
108
|
+
node: {
|
|
109
|
+
name: 'default.test_node',
|
|
110
|
+
type: 'DIMENSION',
|
|
111
|
+
currentVersion: 'v4.0',
|
|
112
|
+
tags: [],
|
|
113
|
+
editedBy: ['dj'],
|
|
114
|
+
current: {
|
|
115
|
+
displayName: 'Test Node',
|
|
116
|
+
status: 'VALID',
|
|
117
|
+
updatedAt: '2024-10-18T15:15:33.532949+00:00',
|
|
118
|
+
},
|
|
119
|
+
createdBy: {
|
|
120
|
+
username: 'dj',
|
|
121
|
+
},
|
|
122
|
+
},
|
|
123
|
+
},
|
|
124
|
+
],
|
|
125
|
+
},
|
|
126
|
+
},
|
|
127
|
+
});
|
|
88
128
|
});
|
|
89
129
|
|
|
90
130
|
afterEach(() => {
|
|
@@ -106,52 +146,59 @@ describe('NamespacePage', () => {
|
|
|
106
146
|
</MemoryRouter>,
|
|
107
147
|
);
|
|
108
148
|
|
|
109
|
-
await waitFor(
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
// check that it displays namespaces
|
|
114
|
-
expect(screen.getByText('common')).toBeInTheDocument();
|
|
115
|
-
expect(screen.getByText('one')).toBeInTheDocument();
|
|
116
|
-
expect(screen.getByText('fruits')).toBeInTheDocument();
|
|
117
|
-
expect(screen.getByText('vegetables')).toBeInTheDocument();
|
|
118
|
-
|
|
119
|
-
// check that it renders nodes
|
|
120
|
-
expect(screen.getByText('Test Node')).toBeInTheDocument();
|
|
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
|
-
|
|
146
|
-
// click to open and close tab
|
|
147
|
-
fireEvent.click(screen.getByText('common'));
|
|
148
|
-
fireEvent.click(screen.getByText('common'));
|
|
149
|
-
});
|
|
150
|
-
});
|
|
149
|
+
await waitFor(
|
|
150
|
+
() => {
|
|
151
|
+
expect(mockDjClient.listNodesForLanding).toHaveBeenCalled();
|
|
152
|
+
expect(screen.getByText('Namespaces')).toBeInTheDocument();
|
|
151
153
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
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();
|
|
159
|
+
|
|
160
|
+
// check that it renders nodes
|
|
161
|
+
expect(screen.getByText('Test Node')).toBeInTheDocument();
|
|
162
|
+
|
|
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'));
|
|
167
|
+
|
|
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);
|
|
175
|
+
|
|
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'));
|
|
182
|
+
|
|
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' });
|
|
188
|
+
|
|
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' });
|
|
194
|
+
|
|
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);
|
|
155
202
|
|
|
156
203
|
it('can add new namespace via add namespace popover', async () => {
|
|
157
204
|
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', '
|
|
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,38 @@ export function NamespacePage() {
|
|
|
35
35
|
const [filters, setFilters] = useState({
|
|
36
36
|
tags: [],
|
|
37
37
|
node_type: '',
|
|
38
|
-
edited_by:
|
|
38
|
+
edited_by: '',
|
|
39
39
|
});
|
|
40
40
|
|
|
41
41
|
const [namespaceHierarchy, setNamespaceHierarchy] = useState([]);
|
|
42
42
|
|
|
43
43
|
const [sortConfig, setSortConfig] = useState({
|
|
44
|
-
key: '
|
|
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 (
|
|
60
|
+
if (
|
|
61
|
+
a[sortConfig.key] < b[sortConfig.key] ||
|
|
62
|
+
a.current[sortConfig.key] < b.current[sortConfig.key]
|
|
63
|
+
) {
|
|
68
64
|
return sortConfig.direction === ASC ? -1 : 1;
|
|
69
65
|
}
|
|
70
|
-
if (
|
|
66
|
+
if (
|
|
67
|
+
a[sortConfig.key] > b[sortConfig.key] ||
|
|
68
|
+
a.current[sortConfig.key] > b.current[sortConfig.key]
|
|
69
|
+
) {
|
|
71
70
|
return sortConfig.direction === ASC ? 1 : -1;
|
|
72
71
|
}
|
|
73
72
|
return 0;
|
|
@@ -125,6 +124,7 @@ export function NamespacePage() {
|
|
|
125
124
|
const hierarchy = createNamespaceHierarchy(namespaces);
|
|
126
125
|
setNamespaceHierarchy(hierarchy);
|
|
127
126
|
const currentUser = await djClient.whoami();
|
|
127
|
+
// setFilters({...filters, edited_by: currentUser?.username});
|
|
128
128
|
setCurrentUser(currentUser);
|
|
129
129
|
};
|
|
130
130
|
fetchData().catch(console.error);
|
|
@@ -132,61 +132,132 @@ export function NamespacePage() {
|
|
|
132
132
|
|
|
133
133
|
useEffect(() => {
|
|
134
134
|
const fetchData = async () => {
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
135
|
+
setRetrieved(false);
|
|
136
|
+
console.log('cursor', before, filters.edited_by);
|
|
137
|
+
const nodes = await djClient.listNodesForLanding(
|
|
138
|
+
namespace,
|
|
139
|
+
filters.node_type ? [filters.node_type.toUpperCase()] : [],
|
|
140
|
+
filters.tags,
|
|
141
|
+
filters.edited_by,
|
|
142
|
+
before,
|
|
143
|
+
after,
|
|
144
|
+
50,
|
|
145
|
+
);
|
|
146
|
+
console.log('nodes', nodes);
|
|
147
|
+
|
|
140
148
|
setState({
|
|
141
149
|
namespace: namespace,
|
|
142
|
-
nodes:
|
|
150
|
+
nodes: nodes.data
|
|
151
|
+
? nodes.data.findNodesPaginated.edges.map(n => n.node)
|
|
152
|
+
: [],
|
|
143
153
|
});
|
|
154
|
+
if (nodes.data) {
|
|
155
|
+
setPrevCursor(
|
|
156
|
+
nodes.data ? nodes.data.findNodesPaginated.pageInfo.startCursor : '',
|
|
157
|
+
);
|
|
158
|
+
setNextCursor(
|
|
159
|
+
nodes.data ? nodes.data.findNodesPaginated.pageInfo.endCursor : '',
|
|
160
|
+
);
|
|
161
|
+
console.log(
|
|
162
|
+
'setting hasPrevPage, ',
|
|
163
|
+
nodes.data.findNodesPaginated.pageInfo.hasPrevPage,
|
|
164
|
+
);
|
|
165
|
+
setHasPrevPage(
|
|
166
|
+
nodes.data
|
|
167
|
+
? nodes.data.findNodesPaginated.pageInfo.hasPrevPage
|
|
168
|
+
: false,
|
|
169
|
+
);
|
|
170
|
+
setHasNextPage(
|
|
171
|
+
nodes.data
|
|
172
|
+
? nodes.data.findNodesPaginated.pageInfo.hasNextPage
|
|
173
|
+
: false,
|
|
174
|
+
);
|
|
175
|
+
}
|
|
144
176
|
setRetrieved(true);
|
|
145
177
|
};
|
|
146
178
|
fetchData().catch(console.error);
|
|
147
|
-
}, [djClient,
|
|
179
|
+
}, [djClient, filters, before, after]);
|
|
180
|
+
const loadNext = () => {
|
|
181
|
+
if (nextCursor) {
|
|
182
|
+
setAfter(nextCursor);
|
|
183
|
+
setBefore(null);
|
|
184
|
+
}
|
|
185
|
+
};
|
|
186
|
+
const loadPrev = () => {
|
|
187
|
+
if (prevCursor) {
|
|
188
|
+
setAfter(null);
|
|
189
|
+
setBefore(prevCursor);
|
|
190
|
+
}
|
|
191
|
+
};
|
|
148
192
|
|
|
149
193
|
const nodesList = retrieved ? (
|
|
150
|
-
sortedNodes.
|
|
194
|
+
sortedNodes.length > 0 ? (
|
|
195
|
+
sortedNodes.map(node => (
|
|
196
|
+
<tr key={node.name}>
|
|
197
|
+
<td>
|
|
198
|
+
<a href={'/nodes/' + node.name} className="link-table">
|
|
199
|
+
{node.name}
|
|
200
|
+
</a>
|
|
201
|
+
<span
|
|
202
|
+
className="rounded-pill badge bg-secondary-soft"
|
|
203
|
+
style={{ marginLeft: '0.5rem' }}
|
|
204
|
+
>
|
|
205
|
+
{node.currentVersion}
|
|
206
|
+
</span>
|
|
207
|
+
</td>
|
|
208
|
+
<td>
|
|
209
|
+
<a href={'/nodes/' + node.name} className="link-table">
|
|
210
|
+
{node.type !== 'source' ? node.current.displayName : ''}
|
|
211
|
+
</a>
|
|
212
|
+
</td>
|
|
213
|
+
<td>
|
|
214
|
+
<span
|
|
215
|
+
className={
|
|
216
|
+
'node_type__' + node.type.toLowerCase() + ' badge node_type'
|
|
217
|
+
}
|
|
218
|
+
>
|
|
219
|
+
{node.type}
|
|
220
|
+
</span>
|
|
221
|
+
</td>
|
|
222
|
+
<td>
|
|
223
|
+
<NodeStatus node={node} revalidate={false} />
|
|
224
|
+
</td>
|
|
225
|
+
<td>
|
|
226
|
+
<span className="status">
|
|
227
|
+
{new Date(node.current.updatedAt).toLocaleString('en-us')}
|
|
228
|
+
</span>
|
|
229
|
+
</td>
|
|
230
|
+
<td>
|
|
231
|
+
<NodeListActions nodeName={node?.name} />
|
|
232
|
+
</td>
|
|
233
|
+
</tr>
|
|
234
|
+
))
|
|
235
|
+
) : (
|
|
151
236
|
<tr>
|
|
152
237
|
<td>
|
|
153
|
-
<a href={'/nodes/' + node.name} className="link-table">
|
|
154
|
-
{node.name}
|
|
155
|
-
</a>
|
|
156
238
|
<span
|
|
157
|
-
|
|
158
|
-
|
|
239
|
+
style={{
|
|
240
|
+
display: 'block',
|
|
241
|
+
marginTop: '2rem',
|
|
242
|
+
marginLeft: '2rem',
|
|
243
|
+
fontSize: '16px',
|
|
244
|
+
}}
|
|
159
245
|
>
|
|
160
|
-
{
|
|
246
|
+
There are no nodes in{' '}
|
|
247
|
+
<a href={`/namespaces/${namespace}`}>{namespace}</a> with the above
|
|
248
|
+
filters!
|
|
161
249
|
</span>
|
|
162
250
|
</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
251
|
</tr>
|
|
185
|
-
)
|
|
252
|
+
)
|
|
186
253
|
) : (
|
|
187
|
-
<
|
|
188
|
-
<
|
|
189
|
-
|
|
254
|
+
<tr>
|
|
255
|
+
<td>
|
|
256
|
+
<span style={{ display: 'block', marginTop: '2rem' }}>
|
|
257
|
+
<LoadingIcon />
|
|
258
|
+
</span>
|
|
259
|
+
</td>
|
|
260
|
+
</tr>
|
|
190
261
|
);
|
|
191
262
|
|
|
192
263
|
return (
|
|
@@ -194,7 +265,7 @@ export function NamespacePage() {
|
|
|
194
265
|
<div className="card">
|
|
195
266
|
<div className="card-header">
|
|
196
267
|
<h2>Explore</h2>
|
|
197
|
-
<div
|
|
268
|
+
<div className="menu" style={{ margin: '0 0 20px 0' }}>
|
|
198
269
|
<div
|
|
199
270
|
className="menu-link"
|
|
200
271
|
style={{
|
|
@@ -260,6 +331,7 @@ export function NamespacePage() {
|
|
|
260
331
|
item={child}
|
|
261
332
|
current={state.namespace}
|
|
262
333
|
defaultExpand={true}
|
|
334
|
+
key={child.namespace}
|
|
263
335
|
/>
|
|
264
336
|
))
|
|
265
337
|
: null}
|
|
@@ -269,13 +341,13 @@ export function NamespacePage() {
|
|
|
269
341
|
<tr>
|
|
270
342
|
{fields.map(field => {
|
|
271
343
|
return (
|
|
272
|
-
<th>
|
|
344
|
+
<th key={field}>
|
|
273
345
|
<button
|
|
274
346
|
type="button"
|
|
275
347
|
onClick={() => requestSort(field)}
|
|
276
348
|
className={'sortable ' + getClassNamesFor(field)}
|
|
277
349
|
>
|
|
278
|
-
{field.replace(
|
|
350
|
+
{field.replace(/([a-z](?=[A-Z]))/g, '$1 ')}
|
|
279
351
|
</button>
|
|
280
352
|
</th>
|
|
281
353
|
);
|
|
@@ -284,6 +356,29 @@ export function NamespacePage() {
|
|
|
284
356
|
</tr>
|
|
285
357
|
</thead>
|
|
286
358
|
<tbody>{nodesList}</tbody>
|
|
359
|
+
<tfoot>
|
|
360
|
+
<tr>
|
|
361
|
+
<td>
|
|
362
|
+
{retrieved && hasPrevPage ? (
|
|
363
|
+
<a
|
|
364
|
+
onClick={loadPrev}
|
|
365
|
+
className="previous round pagination"
|
|
366
|
+
>
|
|
367
|
+
← Previous
|
|
368
|
+
</a>
|
|
369
|
+
) : (
|
|
370
|
+
''
|
|
371
|
+
)}
|
|
372
|
+
{retrieved && hasNextPage ? (
|
|
373
|
+
<a onClick={loadNext} className="next round pagination">
|
|
374
|
+
Next →
|
|
375
|
+
</a>
|
|
376
|
+
) : (
|
|
377
|
+
''
|
|
378
|
+
)}
|
|
379
|
+
</td>
|
|
380
|
+
</tr>
|
|
381
|
+
</tfoot>
|
|
287
382
|
</table>
|
|
288
383
|
</div>
|
|
289
384
|
</div>
|
|
@@ -256,6 +256,7 @@ export default function NodeMaterializationTab({ node, djClient }) {
|
|
|
256
256
|
<th className="text-start">Output Dataset</th>
|
|
257
257
|
<th>Valid Through</th>
|
|
258
258
|
<th>Partitions</th>
|
|
259
|
+
<th>Links</th>
|
|
259
260
|
</tr>
|
|
260
261
|
</thead>
|
|
261
262
|
<tbody>
|
|
@@ -299,6 +300,21 @@ export default function NodeMaterializationTab({ node, djClient }) {
|
|
|
299
300
|
</span>
|
|
300
301
|
</span>
|
|
301
302
|
</td>
|
|
303
|
+
<td>
|
|
304
|
+
{node.availability.links !== null ? (
|
|
305
|
+
Object.entries(node.availability.links).map(
|
|
306
|
+
([key, value]) => (
|
|
307
|
+
<div key={key}>
|
|
308
|
+
<a href={value} target="_blank" rel="noreferrer">
|
|
309
|
+
{key}
|
|
310
|
+
</a>
|
|
311
|
+
</div>
|
|
312
|
+
),
|
|
313
|
+
)
|
|
314
|
+
) : (
|
|
315
|
+
<></>
|
|
316
|
+
)}
|
|
317
|
+
</td>
|
|
302
318
|
</tr>
|
|
303
319
|
</tbody>
|
|
304
320
|
</table>
|
|
@@ -81,7 +81,9 @@ export default function NodeStatus({ node, revalidate = true }) {
|
|
|
81
81
|
<>
|
|
82
82
|
{revalidate && validation?.errors?.length > 0 ? (
|
|
83
83
|
displayValidation
|
|
84
|
-
) : validation?.status === 'valid' ||
|
|
84
|
+
) : validation?.status === 'valid' ||
|
|
85
|
+
node?.status === 'valid' ||
|
|
86
|
+
node?.current?.status === 'VALID' ? (
|
|
85
87
|
<span
|
|
86
88
|
className="status__valid status"
|
|
87
89
|
style={{ alignContent: 'center' }}
|
|
@@ -25,7 +25,7 @@ export default function NotebookDownload({ node }) {
|
|
|
25
25
|
<>
|
|
26
26
|
<div
|
|
27
27
|
className="badge download_notebook"
|
|
28
|
-
style={{cursor: 'pointer', backgroundColor: '#ffefd0'}}
|
|
28
|
+
style={{ cursor: 'pointer', backgroundColor: '#ffefd0' }}
|
|
29
29
|
tabIndex="0"
|
|
30
30
|
height="45px"
|
|
31
31
|
onClick={downloadFile}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { render, waitFor, screen } from '@testing-library/react';
|
|
3
|
+
import NodeMaterializationTab from '../NodeMaterializationTab';
|
|
4
|
+
|
|
5
|
+
describe('<NodeMaterializationTab />', () => {
|
|
6
|
+
const mockDjClient = {
|
|
7
|
+
node: jest.fn(),
|
|
8
|
+
materializations: jest.fn(),
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
const mockMaterializations = [
|
|
12
|
+
{
|
|
13
|
+
name: 'mat_one',
|
|
14
|
+
config: {},
|
|
15
|
+
schedule: '@daily',
|
|
16
|
+
job: 'SparkSqlMaterializationJob',
|
|
17
|
+
backfills: [
|
|
18
|
+
{
|
|
19
|
+
spec: [
|
|
20
|
+
{
|
|
21
|
+
column_name: 'date',
|
|
22
|
+
values: ['20200101'],
|
|
23
|
+
range: ['20201010'],
|
|
24
|
+
},
|
|
25
|
+
],
|
|
26
|
+
urls: ['https://example.com/'],
|
|
27
|
+
},
|
|
28
|
+
],
|
|
29
|
+
strategy: 'full',
|
|
30
|
+
output_tables: ['table1'],
|
|
31
|
+
urls: ['https://example.com/'],
|
|
32
|
+
},
|
|
33
|
+
];
|
|
34
|
+
|
|
35
|
+
const mockNode = {
|
|
36
|
+
node_revision_id: 1,
|
|
37
|
+
node_id: 1,
|
|
38
|
+
type: 'source',
|
|
39
|
+
name: 'default.repair_orders',
|
|
40
|
+
display_name: 'Default: Repair Orders',
|
|
41
|
+
version: 'v1.0',
|
|
42
|
+
status: 'valid',
|
|
43
|
+
mode: 'published',
|
|
44
|
+
catalog: {
|
|
45
|
+
id: 1,
|
|
46
|
+
uuid: '0fc18295-e1a2-4c3c-b72a-894725c12488',
|
|
47
|
+
created_at: '2023-08-21T16:48:51.146121+00:00',
|
|
48
|
+
updated_at: '2023-08-21T16:48:51.146122+00:00',
|
|
49
|
+
extra_params: {},
|
|
50
|
+
name: 'warehouse',
|
|
51
|
+
},
|
|
52
|
+
schema_: 'roads',
|
|
53
|
+
table: 'repair_orders',
|
|
54
|
+
description: 'Repair orders',
|
|
55
|
+
query: null,
|
|
56
|
+
availability: {
|
|
57
|
+
catalog: 'default',
|
|
58
|
+
categorical_partitions: [],
|
|
59
|
+
max_temporal_partition: ['2023', '01', '25'],
|
|
60
|
+
min_temporal_partition: ['2022', '01', '01'],
|
|
61
|
+
partitions: [],
|
|
62
|
+
schema_: 'foo',
|
|
63
|
+
table: 'bar',
|
|
64
|
+
temporal_partitions: [],
|
|
65
|
+
valid_through_ts: 1729667463,
|
|
66
|
+
url: 'https://www.table.com',
|
|
67
|
+
links: { dashboard: 'https://www.foobar.com/dashboard' },
|
|
68
|
+
},
|
|
69
|
+
columns: [
|
|
70
|
+
{
|
|
71
|
+
name: 'repair_order_id',
|
|
72
|
+
type: 'int',
|
|
73
|
+
attributes: [],
|
|
74
|
+
dimension: null,
|
|
75
|
+
partition: {
|
|
76
|
+
type_: 'temporal',
|
|
77
|
+
format: 'YYYYMMDD',
|
|
78
|
+
granularity: 'day',
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
name: 'municipality_id',
|
|
83
|
+
type: 'string',
|
|
84
|
+
attributes: [],
|
|
85
|
+
dimension: null,
|
|
86
|
+
partition: null,
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
name: 'hard_hat_id',
|
|
90
|
+
type: 'int',
|
|
91
|
+
attributes: [],
|
|
92
|
+
dimension: null,
|
|
93
|
+
partition: null,
|
|
94
|
+
},
|
|
95
|
+
],
|
|
96
|
+
updated_at: '2023-08-21T16:48:52.880498+00:00',
|
|
97
|
+
materializations: [
|
|
98
|
+
{
|
|
99
|
+
name: 'mat1',
|
|
100
|
+
config: {},
|
|
101
|
+
schedule: 'string',
|
|
102
|
+
job: 'string',
|
|
103
|
+
backfills: [
|
|
104
|
+
{
|
|
105
|
+
spec: [
|
|
106
|
+
{
|
|
107
|
+
column_name: 'string',
|
|
108
|
+
values: ['string'],
|
|
109
|
+
range: ['string'],
|
|
110
|
+
},
|
|
111
|
+
],
|
|
112
|
+
urls: ['string'],
|
|
113
|
+
},
|
|
114
|
+
],
|
|
115
|
+
strategy: 'string',
|
|
116
|
+
output_tables: ['string'],
|
|
117
|
+
urls: ['https://example.com/'],
|
|
118
|
+
},
|
|
119
|
+
],
|
|
120
|
+
parents: [],
|
|
121
|
+
dimension_links: [
|
|
122
|
+
{
|
|
123
|
+
dimension: {
|
|
124
|
+
name: 'default.contractor',
|
|
125
|
+
},
|
|
126
|
+
join_type: 'left',
|
|
127
|
+
join_sql:
|
|
128
|
+
'default.contractor.contractor_id = default.repair_orders.contractor_id',
|
|
129
|
+
join_cardinality: 'one_to_one',
|
|
130
|
+
role: 'contractor',
|
|
131
|
+
},
|
|
132
|
+
],
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
beforeEach(() => {
|
|
136
|
+
mockDjClient.materializations.mockReset();
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it('renders NodeMaterializationTab tab correctly', async () => {
|
|
140
|
+
mockDjClient.materializations.mockReturnValue(mockMaterializations);
|
|
141
|
+
|
|
142
|
+
render(<NodeMaterializationTab node={mockNode} djClient={mockDjClient} />);
|
|
143
|
+
await waitFor(() => {
|
|
144
|
+
const link = screen.getByText('dashboard').closest('a');
|
|
145
|
+
expect(link).toHaveAttribute('href', `https://www.foobar.com/dashboard`);
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
});
|
|
@@ -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
|
-
|
|
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
|
|
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">
|
|
@@ -69,17 +72,25 @@ export function Root() {
|
|
|
69
72
|
<div className="dropdown">
|
|
70
73
|
<a
|
|
71
74
|
className="btn btn-link dropdown-toggle"
|
|
72
|
-
href="
|
|
75
|
+
href="/"
|
|
73
76
|
id="docsDropdown"
|
|
74
77
|
role="button"
|
|
75
78
|
aria-expanded="false"
|
|
76
79
|
>
|
|
77
80
|
Docs
|
|
78
81
|
</a>
|
|
79
|
-
<ul
|
|
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
|
|
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
|
+
: process.env.REACT_APP_DJ_URL + '/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(
|
|
352
|
-
|
|
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,9 +426,12 @@ describe('DataJunctionAPI', () => {
|
|
|
426
426
|
const nmspce = 'sampleNamespace';
|
|
427
427
|
fetch.mockResponseOnce(JSON.stringify({}));
|
|
428
428
|
await DataJunctionAPI.namespace(nmspce);
|
|
429
|
-
expect(fetch).toHaveBeenCalledWith(
|
|
430
|
-
|
|
431
|
-
|
|
429
|
+
expect(fetch).toHaveBeenCalledWith(
|
|
430
|
+
`${DJ_URL}/namespaces/${nmspce}?edited_by=undefined&with_edited_by=true`,
|
|
431
|
+
{
|
|
432
|
+
credentials: 'include',
|
|
433
|
+
},
|
|
434
|
+
);
|
|
432
435
|
});
|
|
433
436
|
|
|
434
437
|
it('calls sql correctly', async () => {
|
|
@@ -489,9 +492,9 @@ describe('DataJunctionAPI', () => {
|
|
|
489
492
|
const sampleNode = {
|
|
490
493
|
name: 'sampleNode',
|
|
491
494
|
columns: [
|
|
492
|
-
{ name: 'column1', dimension: { name: 'dimension1' }
|
|
495
|
+
{ name: 'column1', dimension: { name: 'dimension1' } },
|
|
493
496
|
{ name: 'column2', dimension: null },
|
|
494
|
-
{ name: 'column3', dimension: { name: 'dimension2' }
|
|
497
|
+
{ name: 'column3', dimension: { name: 'dimension2' } },
|
|
495
498
|
],
|
|
496
499
|
};
|
|
497
500
|
|
|
@@ -1069,4 +1072,28 @@ describe('DataJunctionAPI', () => {
|
|
|
1069
1072
|
},
|
|
1070
1073
|
);
|
|
1071
1074
|
});
|
|
1075
|
+
|
|
1076
|
+
it('calls listNodesForLanding correctly', () => {
|
|
1077
|
+
fetch.mockResponseOnce(JSON.stringify({}));
|
|
1078
|
+
|
|
1079
|
+
DataJunctionAPI.listNodesForLanding(
|
|
1080
|
+
'',
|
|
1081
|
+
['source'],
|
|
1082
|
+
[],
|
|
1083
|
+
'',
|
|
1084
|
+
null,
|
|
1085
|
+
null,
|
|
1086
|
+
100,
|
|
1087
|
+
);
|
|
1088
|
+
expect(fetch).toHaveBeenCalledWith(
|
|
1089
|
+
`${DJ_URL}/graphql`,
|
|
1090
|
+
expect.objectContaining({
|
|
1091
|
+
method: 'POST',
|
|
1092
|
+
credentials: 'include',
|
|
1093
|
+
headers: {
|
|
1094
|
+
'Content-Type': 'application/json',
|
|
1095
|
+
},
|
|
1096
|
+
}),
|
|
1097
|
+
);
|
|
1098
|
+
});
|
|
1072
1099
|
});
|
package/src/styles/index.css
CHANGED
|
@@ -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;
|
|
@@ -1258,67 +1284,67 @@ pre {
|
|
|
1258
1284
|
|
|
1259
1285
|
.backfills {
|
|
1260
1286
|
margin-left: -4rem;
|
|
1261
|
-
--spacing
|
|
1262
|
-
--radius
|
|
1287
|
+
--spacing: 1.5rem;
|
|
1288
|
+
--radius: 10px;
|
|
1263
1289
|
}
|
|
1264
1290
|
|
|
1265
|
-
.backfills li{
|
|
1266
|
-
display
|
|
1267
|
-
position
|
|
1268
|
-
padding-left
|
|
1291
|
+
.backfills li {
|
|
1292
|
+
display: block;
|
|
1293
|
+
position: relative;
|
|
1294
|
+
padding-left: calc(2 * var(--spacing) - var(--radius) - 2px);
|
|
1269
1295
|
}
|
|
1270
1296
|
|
|
1271
|
-
.backfills ul{
|
|
1272
|
-
margin-left
|
|
1273
|
-
padding-left
|
|
1297
|
+
.backfills ul {
|
|
1298
|
+
margin-left: calc(var(--radius) - var(--spacing));
|
|
1299
|
+
padding-left: 2rem;
|
|
1274
1300
|
}
|
|
1275
1301
|
|
|
1276
|
-
.backfills ul li{
|
|
1277
|
-
border-left
|
|
1302
|
+
.backfills ul li {
|
|
1303
|
+
border-left: 2px solid #ddd;
|
|
1278
1304
|
}
|
|
1279
1305
|
|
|
1280
|
-
.backfills ul li:last-child{
|
|
1281
|
-
border-color
|
|
1306
|
+
.backfills ul li:last-child {
|
|
1307
|
+
border-color: transparent;
|
|
1282
1308
|
}
|
|
1283
1309
|
|
|
1284
|
-
.backfills ul li::before{
|
|
1285
|
-
content
|
|
1286
|
-
display
|
|
1287
|
-
position
|
|
1288
|
-
top
|
|
1289
|
-
left
|
|
1290
|
-
width
|
|
1291
|
-
height
|
|
1292
|
-
border
|
|
1293
|
-
border-width
|
|
1310
|
+
.backfills ul li::before {
|
|
1311
|
+
content: '';
|
|
1312
|
+
display: block;
|
|
1313
|
+
position: absolute;
|
|
1314
|
+
top: calc(var(--spacing) / -2);
|
|
1315
|
+
left: -2px;
|
|
1316
|
+
width: calc(var(--spacing) + 2px);
|
|
1317
|
+
height: calc(var(--spacing) + 1px);
|
|
1318
|
+
border: solid #ddd;
|
|
1319
|
+
border-width: 0 0 2px 2px;
|
|
1294
1320
|
}
|
|
1295
1321
|
|
|
1296
|
-
.backfills summary{
|
|
1297
|
-
display
|
|
1298
|
-
cursor
|
|
1322
|
+
.backfills summary {
|
|
1323
|
+
display: block;
|
|
1324
|
+
cursor: pointer;
|
|
1299
1325
|
margin-bottom: 10px;
|
|
1300
1326
|
}
|
|
1301
1327
|
|
|
1302
1328
|
.backfills summary::marker,
|
|
1303
|
-
.backfills summary::-webkit-details-marker{
|
|
1304
|
-
display
|
|
1329
|
+
.backfills summary::-webkit-details-marker {
|
|
1330
|
+
display: none;
|
|
1305
1331
|
}
|
|
1306
1332
|
|
|
1307
|
-
.backfills summary:focus{
|
|
1308
|
-
outline
|
|
1333
|
+
.backfills summary:focus {
|
|
1334
|
+
outline: none;
|
|
1309
1335
|
}
|
|
1310
1336
|
|
|
1311
|
-
.backfills summary:focus-visible{
|
|
1312
|
-
outline
|
|
1337
|
+
.backfills summary:focus-visible {
|
|
1338
|
+
outline: 1px dotted #000;
|
|
1313
1339
|
}
|
|
1314
1340
|
|
|
1315
|
-
.backfills summary::before{
|
|
1316
|
-
z-index
|
|
1341
|
+
.backfills summary::before {
|
|
1342
|
+
z-index: 1;
|
|
1317
1343
|
/*background : #696 url('expand-collapse.svg') 0 0;*/
|
|
1318
1344
|
}
|
|
1319
1345
|
|
|
1320
|
-
.backfills details[open] > summary::before{
|
|
1321
|
-
background-position
|
|
1346
|
+
.backfills details[open] > summary::before {
|
|
1347
|
+
background-position: calc(-2 * var(--radius)) 0;
|
|
1322
1348
|
}
|
|
1323
1349
|
|
|
1324
1350
|
.backfills_header {
|
|
@@ -1345,32 +1371,32 @@ pre {
|
|
|
1345
1371
|
border-radius: 2px;
|
|
1346
1372
|
border-style: none;
|
|
1347
1373
|
padding: 0.4rem;
|
|
1348
|
-
font-family: Lato,
|
|
1374
|
+
font-family: Lato, 'sans-serif';
|
|
1349
1375
|
font-size: 110%;
|
|
1350
1376
|
border-right: 16px solid transparent;
|
|
1351
1377
|
margin-left: 0.4rem;
|
|
1352
1378
|
}
|
|
1353
1379
|
|
|
1354
1380
|
.backfills summary::marker,
|
|
1355
|
-
.backfills summary::-webkit-details-marker{
|
|
1356
|
-
display
|
|
1381
|
+
.backfills summary::-webkit-details-marker {
|
|
1382
|
+
display: none;
|
|
1357
1383
|
}
|
|
1358
1384
|
|
|
1359
|
-
.backfills summary:focus{
|
|
1360
|
-
outline
|
|
1385
|
+
.backfills summary:focus {
|
|
1386
|
+
outline: none;
|
|
1361
1387
|
}
|
|
1362
1388
|
|
|
1363
|
-
.backfills summary:focus-visible{
|
|
1364
|
-
outline
|
|
1389
|
+
.backfills summary:focus-visible {
|
|
1390
|
+
outline: 1px dotted #000;
|
|
1365
1391
|
}
|
|
1366
1392
|
|
|
1367
|
-
.backfills summary::before{
|
|
1368
|
-
z-index
|
|
1393
|
+
.backfills summary::before {
|
|
1394
|
+
z-index: 1;
|
|
1369
1395
|
/*background : #696 url('expand-collapse.svg') 0 0;*/
|
|
1370
1396
|
}
|
|
1371
1397
|
|
|
1372
|
-
.backfills details[open] > summary::before{
|
|
1373
|
-
background-position
|
|
1398
|
+
.backfills details[open] > summary::before {
|
|
1399
|
+
background-position: calc(-2 * var(--radius)) 0;
|
|
1374
1400
|
}
|
|
1375
1401
|
|
|
1376
1402
|
.backfills_header {
|
|
@@ -1419,8 +1445,8 @@ table {
|
|
|
1419
1445
|
position: absolute;
|
|
1420
1446
|
z-index: 1;
|
|
1421
1447
|
|
|
1422
|
-
|
|
1423
|
-
|
|
1448
|
+
top: -5px;
|
|
1449
|
+
left: 125%;
|
|
1424
1450
|
/*bottom: 125%;*/
|
|
1425
1451
|
/*left: 50%;*/
|
|
1426
1452
|
margin-left: -60px;
|
|
@@ -1429,7 +1455,7 @@ table {
|
|
|
1429
1455
|
}
|
|
1430
1456
|
|
|
1431
1457
|
.tooltip .tooltiptext::after {
|
|
1432
|
-
content:
|
|
1458
|
+
content: '';
|
|
1433
1459
|
position: absolute;
|
|
1434
1460
|
top: 100%;
|
|
1435
1461
|
left: 50%;
|
|
@@ -1450,13 +1476,19 @@ table {
|
|
|
1450
1476
|
grid-template-rows: 250px 1fr;
|
|
1451
1477
|
gap: 20px;
|
|
1452
1478
|
grid-template-areas:
|
|
1453
|
-
|
|
1454
|
-
|
|
1479
|
+
'left righttop'
|
|
1480
|
+
'left rightbottom';
|
|
1455
1481
|
margin-top: 1rem;
|
|
1456
1482
|
}
|
|
1457
|
-
.left {
|
|
1458
|
-
|
|
1459
|
-
|
|
1483
|
+
.left {
|
|
1484
|
+
grid-area: left;
|
|
1485
|
+
}
|
|
1486
|
+
.righttop {
|
|
1487
|
+
grid-area: righttop;
|
|
1488
|
+
}
|
|
1489
|
+
.rightbottom {
|
|
1490
|
+
grid-area: rightbottom;
|
|
1491
|
+
}
|
|
1460
1492
|
|
|
1461
1493
|
.queryrunner-query pre {
|
|
1462
1494
|
border-radius: 0;
|
|
@@ -1512,4 +1544,4 @@ table {
|
|
|
1512
1544
|
}
|
|
1513
1545
|
.dropdown:hover .dropdown-menu {
|
|
1514
1546
|
display: block;
|
|
1515
|
-
}
|
|
1547
|
+
}
|
package/src/styles/node-list.css
CHANGED