datajunction-ui 0.0.1-rc.23 → 0.0.1-rc.25
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/.env +1 -0
- package/package.json +3 -2
- package/src/app/components/Tab.jsx +0 -1
- package/src/app/constants.js +2 -2
- package/src/app/icons/LoadingIcon.jsx +14 -0
- package/src/app/index.tsx +11 -1
- package/src/app/pages/AddEditNodePage/__tests__/AddEditNodePageFormFailed.test.jsx +28 -2
- package/src/app/pages/AddEditNodePage/__tests__/AddEditNodePageFormSuccess.test.jsx +44 -9
- package/src/app/pages/AddEditNodePage/__tests__/__snapshots__/AddEditNodePageFormFailed.test.jsx.snap +1 -0
- package/src/app/pages/AddEditNodePage/__tests__/__snapshots__/AddEditNodePageFormSuccess.test.jsx.snap +0 -50
- package/src/app/pages/AddEditNodePage/__tests__/index.test.jsx +2 -0
- package/src/app/pages/AddEditNodePage/index.jsx +60 -6
- package/src/app/pages/AddEditTagPage/Loadable.jsx +16 -0
- package/src/app/pages/AddEditTagPage/__tests__/AddEditTagPage.test.jsx +107 -0
- package/src/app/pages/AddEditTagPage/index.jsx +132 -0
- package/src/app/pages/LoginPage/LoginForm.jsx +116 -0
- package/src/app/pages/LoginPage/SignupForm.jsx +144 -0
- package/src/app/pages/LoginPage/__tests__/index.test.jsx +34 -2
- package/src/app/pages/LoginPage/index.jsx +9 -82
- package/src/app/pages/NamespacePage/index.jsx +5 -0
- package/src/app/pages/NodePage/ClientCodePopover.jsx +15 -1
- package/src/app/pages/NodePage/EditColumnPopover.jsx +15 -1
- package/src/app/pages/NodePage/LinkDimensionPopover.jsx +16 -1
- package/src/app/pages/NodePage/NodeColumnTab.jsx +11 -0
- package/src/app/pages/NodePage/NodeInfoTab.jsx +5 -1
- package/src/app/pages/NodePage/__tests__/NodePage.test.jsx +6 -3
- package/src/app/pages/RegisterTablePage/__tests__/__snapshots__/RegisterTablePage.test.jsx.snap +1 -0
- package/src/app/pages/Root/index.tsx +1 -1
- package/src/app/pages/TagPage/Loadable.jsx +16 -0
- package/src/app/pages/TagPage/__tests__/TagPage.test.jsx +70 -0
- package/src/app/pages/TagPage/index.jsx +79 -0
- package/src/app/services/DJService.js +79 -1
- package/src/app/services/__tests__/DJService.test.jsx +84 -1
- package/src/mocks/mockNodes.jsx +88 -44
- package/src/styles/index.css +19 -0
- package/src/styles/loading.css +35 -0
- package/src/styles/login.css +17 -3
- package/src/utils/form.jsx +2 -2
|
@@ -13,7 +13,11 @@ foundation.hljs['padding'] = '2rem';
|
|
|
13
13
|
export default function NodeInfoTab({ node }) {
|
|
14
14
|
const [compiledSQL, setCompiledSQL] = useState('');
|
|
15
15
|
const [checked, setChecked] = useState(false);
|
|
16
|
-
const nodeTags = node?.tags.map(tag =>
|
|
16
|
+
const nodeTags = node?.tags.map(tag => (
|
|
17
|
+
<div className={'badge tag_value'}>
|
|
18
|
+
<a href={`/tags/${tag.name}`}>{tag.display_name}</a>
|
|
19
|
+
</div>
|
|
20
|
+
));
|
|
17
21
|
const djClient = useContext(DJClientContext).DataJunctionAPI;
|
|
18
22
|
useEffect(() => {
|
|
19
23
|
const fetchData = async () => {
|
|
@@ -70,6 +70,7 @@ describe('<NodePage />', () => {
|
|
|
70
70
|
{
|
|
71
71
|
name: 'default_DOT_avg_repair_price',
|
|
72
72
|
type: 'double',
|
|
73
|
+
display_name: 'Default DOT avg repair price',
|
|
73
74
|
attributes: [],
|
|
74
75
|
dimension: null,
|
|
75
76
|
},
|
|
@@ -82,7 +83,7 @@ describe('<NodePage />', () => {
|
|
|
82
83
|
},
|
|
83
84
|
],
|
|
84
85
|
created_at: '2023-08-21T16:48:56.932162+00:00',
|
|
85
|
-
tags: [],
|
|
86
|
+
tags: [{ name: 'purpose', display_name: 'Purpose' }],
|
|
86
87
|
primary_key: [],
|
|
87
88
|
createNodeClientCode:
|
|
88
89
|
'dj = DJBuilder(DJ_URL)\n\navg_repair_price = dj.create_metric(\n description="Average repair price",\n display_name="Default: Avg Repair Price",\n name="default.avg_repair_price",\n primary_key=[],\n query="""SELECT avg(price) default_DOT_avg_repair_price \n FROM default.repair_order_details\n\n"""\n)',
|
|
@@ -303,13 +304,12 @@ describe('<NodePage />', () => {
|
|
|
303
304
|
'v1.0',
|
|
304
305
|
);
|
|
305
306
|
|
|
306
|
-
// expect(screen.getByRole('dialog', { name: 'Table' })).not.toBeInTheDocument();
|
|
307
307
|
expect(
|
|
308
308
|
screen.getByRole('dialog', { name: 'NodeStatus' }),
|
|
309
309
|
).toBeInTheDocument();
|
|
310
310
|
|
|
311
311
|
expect(screen.getByRole('dialog', { name: 'Tags' })).toHaveTextContent(
|
|
312
|
-
'',
|
|
312
|
+
'Purpose',
|
|
313
313
|
);
|
|
314
314
|
|
|
315
315
|
expect(
|
|
@@ -416,6 +416,9 @@ describe('<NodePage />', () => {
|
|
|
416
416
|
expect(
|
|
417
417
|
screen.getByRole('columnheader', { name: 'ColumnName' }),
|
|
418
418
|
).toHaveTextContent('default_DOT_avg_repair_price');
|
|
419
|
+
expect(
|
|
420
|
+
screen.getByRole('columnheader', { name: 'ColumnDisplayName' }),
|
|
421
|
+
).toHaveTextContent('Default DOT avg repair price');
|
|
419
422
|
expect(
|
|
420
423
|
screen.getByRole('columnheader', { name: 'ColumnType' }),
|
|
421
424
|
).toHaveTextContent('double');
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Asynchronously loads the component for the Node page
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import * as React from 'react';
|
|
6
|
+
import { lazyLoad } from '../../../utils/loadable';
|
|
7
|
+
|
|
8
|
+
export const TagPage = () => {
|
|
9
|
+
return lazyLoad(
|
|
10
|
+
() => import('./index'),
|
|
11
|
+
module => module.TagPage,
|
|
12
|
+
{
|
|
13
|
+
fallback: <div></div>,
|
|
14
|
+
},
|
|
15
|
+
)();
|
|
16
|
+
};
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { screen, waitFor } from '@testing-library/react';
|
|
3
|
+
import fetchMock from 'jest-fetch-mock';
|
|
4
|
+
import { render } from '../../../../setupTests';
|
|
5
|
+
import { MemoryRouter, Route, Routes } from 'react-router-dom';
|
|
6
|
+
import DJClientContext from '../../../providers/djclient';
|
|
7
|
+
import { TagPage } from '../index';
|
|
8
|
+
|
|
9
|
+
describe('<TagPage />', () => {
|
|
10
|
+
const initializeMockDJClient = () => {
|
|
11
|
+
return {
|
|
12
|
+
DataJunctionAPI: {
|
|
13
|
+
getTag: jest.fn(),
|
|
14
|
+
listNodesForTag: jest.fn(),
|
|
15
|
+
},
|
|
16
|
+
};
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const mockDjClient = initializeMockDJClient();
|
|
20
|
+
|
|
21
|
+
beforeEach(() => {
|
|
22
|
+
fetchMock.resetMocks();
|
|
23
|
+
jest.clearAllMocks();
|
|
24
|
+
window.scrollTo = jest.fn();
|
|
25
|
+
|
|
26
|
+
mockDjClient.DataJunctionAPI.getTag.mockReturnValue({
|
|
27
|
+
name: 'domains.com',
|
|
28
|
+
tag_type: 'domains',
|
|
29
|
+
description: 'Top-level domain .com',
|
|
30
|
+
});
|
|
31
|
+
mockDjClient.DataJunctionAPI.listNodesForTag.mockReturnValue([
|
|
32
|
+
{
|
|
33
|
+
name: 'random.node_a',
|
|
34
|
+
type: 'metric',
|
|
35
|
+
display_name: 'Node A',
|
|
36
|
+
},
|
|
37
|
+
]);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
const renderTagsPage = element => {
|
|
41
|
+
return render(
|
|
42
|
+
<MemoryRouter initialEntries={['/tags/:name']}>
|
|
43
|
+
<Routes>
|
|
44
|
+
<Route path="tags/:name" element={element} />
|
|
45
|
+
</Routes>
|
|
46
|
+
</MemoryRouter>,
|
|
47
|
+
);
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const testElement = djClient => {
|
|
51
|
+
return (
|
|
52
|
+
<DJClientContext.Provider value={djClient}>
|
|
53
|
+
<TagPage />
|
|
54
|
+
</DJClientContext.Provider>
|
|
55
|
+
);
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
it('renders the tag page correctly', async () => {
|
|
59
|
+
const element = testElement(mockDjClient);
|
|
60
|
+
renderTagsPage(element);
|
|
61
|
+
await waitFor(() => {
|
|
62
|
+
expect(mockDjClient.DataJunctionAPI.getTag).toHaveBeenCalledTimes(1);
|
|
63
|
+
expect(
|
|
64
|
+
mockDjClient.DataJunctionAPI.listNodesForTag,
|
|
65
|
+
).toHaveBeenCalledTimes(1);
|
|
66
|
+
});
|
|
67
|
+
expect(screen.getByText('Nodes')).toBeInTheDocument();
|
|
68
|
+
expect(screen.getByText('Node A')).toBeInTheDocument();
|
|
69
|
+
}, 60000);
|
|
70
|
+
});
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* For a given tag, displays nodes tagged with it
|
|
3
|
+
*/
|
|
4
|
+
import NamespaceHeader from '../../components/NamespaceHeader';
|
|
5
|
+
import React, { useContext, useEffect, useState } from 'react';
|
|
6
|
+
import 'styles/node-creation.scss';
|
|
7
|
+
import DJClientContext from '../../providers/djclient';
|
|
8
|
+
import { useParams } from 'react-router-dom';
|
|
9
|
+
|
|
10
|
+
export function TagPage() {
|
|
11
|
+
const [nodes, setNodes] = useState([]);
|
|
12
|
+
const [tag, setTag] = useState([]);
|
|
13
|
+
|
|
14
|
+
const { name } = useParams();
|
|
15
|
+
|
|
16
|
+
const djClient = useContext(DJClientContext).DataJunctionAPI;
|
|
17
|
+
useEffect(() => {
|
|
18
|
+
const fetchData = async () => {
|
|
19
|
+
const data = await djClient.listNodesForTag(name);
|
|
20
|
+
const tagData = await djClient.getTag(name);
|
|
21
|
+
setNodes(data);
|
|
22
|
+
setTag(tagData);
|
|
23
|
+
};
|
|
24
|
+
fetchData().catch(console.error);
|
|
25
|
+
}, [djClient, name]);
|
|
26
|
+
|
|
27
|
+
return (
|
|
28
|
+
<div className="mid">
|
|
29
|
+
<NamespaceHeader namespace="" />
|
|
30
|
+
<div className="card">
|
|
31
|
+
<div className="card-header">
|
|
32
|
+
<h3
|
|
33
|
+
className="card-title align-items-start flex-column"
|
|
34
|
+
style={{ display: 'inline-block' }}
|
|
35
|
+
>
|
|
36
|
+
Tag
|
|
37
|
+
</h3>
|
|
38
|
+
<div>
|
|
39
|
+
<div style={{ marginBottom: '1.5rem' }}>
|
|
40
|
+
<h6 className="mb-0 w-100">Display Name</h6>
|
|
41
|
+
<p className="mb-0 opacity-75">{tag.display_name}</p>
|
|
42
|
+
</div>
|
|
43
|
+
<div style={{ marginBottom: '1.5rem' }}>
|
|
44
|
+
<h6 className="mb-0 w-100">Name</h6>
|
|
45
|
+
<p className="mb-0 opacity-75">{name}</p>
|
|
46
|
+
</div>
|
|
47
|
+
<div style={{ marginBottom: '1.5rem' }}>
|
|
48
|
+
<h6 className="mb-0 w-100">Tag Type</h6>
|
|
49
|
+
<p className="mb-0 opacity-75">{tag.tag_type}</p>
|
|
50
|
+
</div>
|
|
51
|
+
<div style={{ marginBottom: '1.5rem' }}>
|
|
52
|
+
<h6 className="mb-0 w-100">Description</h6>
|
|
53
|
+
<p className="mb-0 opacity-75">{tag.description}</p>
|
|
54
|
+
</div>
|
|
55
|
+
<div style={{ marginBottom: '1.5rem' }}>
|
|
56
|
+
<h6 className="mb-0 w-100">Nodes</h6>
|
|
57
|
+
<div className={`list-group-item`}>
|
|
58
|
+
{nodes?.map(node => (
|
|
59
|
+
<div
|
|
60
|
+
className="button-3 cube-element"
|
|
61
|
+
key={node.name}
|
|
62
|
+
role="cell"
|
|
63
|
+
aria-label="CubeElement"
|
|
64
|
+
aria-hidden="false"
|
|
65
|
+
>
|
|
66
|
+
<a href={`/nodes/${node.name}`}>{node.display_name}</a>
|
|
67
|
+
<span className={`badge node_type__${node.type}`}>
|
|
68
|
+
{node.type}
|
|
69
|
+
</span>
|
|
70
|
+
</div>
|
|
71
|
+
))}
|
|
72
|
+
</div>
|
|
73
|
+
</div>
|
|
74
|
+
</div>
|
|
75
|
+
</div>
|
|
76
|
+
</div>
|
|
77
|
+
</div>
|
|
78
|
+
);
|
|
79
|
+
}
|
|
@@ -12,7 +12,7 @@ export const DataJunctionAPI = {
|
|
|
12
12
|
},
|
|
13
13
|
|
|
14
14
|
logout: async function () {
|
|
15
|
-
await await fetch(`${DJ_URL}/
|
|
15
|
+
await await fetch(`${DJ_URL}/logout/`, {
|
|
16
16
|
credentials: 'include',
|
|
17
17
|
method: 'POST',
|
|
18
18
|
});
|
|
@@ -497,4 +497,82 @@ export const DataJunctionAPI = {
|
|
|
497
497
|
});
|
|
498
498
|
return { status: response.status, json: await response.json() };
|
|
499
499
|
},
|
|
500
|
+
listTags: async function () {
|
|
501
|
+
const response = await fetch(`${DJ_URL}/tags`, {
|
|
502
|
+
method: 'GET',
|
|
503
|
+
headers: {
|
|
504
|
+
'Content-Type': 'application/json',
|
|
505
|
+
},
|
|
506
|
+
credentials: 'include',
|
|
507
|
+
});
|
|
508
|
+
return await response.json();
|
|
509
|
+
},
|
|
510
|
+
getTag: async function (tagName) {
|
|
511
|
+
const response = await fetch(`${DJ_URL}/tags/${tagName}`, {
|
|
512
|
+
method: 'GET',
|
|
513
|
+
headers: {
|
|
514
|
+
'Content-Type': 'application/json',
|
|
515
|
+
},
|
|
516
|
+
credentials: 'include',
|
|
517
|
+
});
|
|
518
|
+
return await response.json();
|
|
519
|
+
},
|
|
520
|
+
listNodesForTag: async function (tagName) {
|
|
521
|
+
const response = await fetch(`${DJ_URL}/tags/${tagName}/nodes`, {
|
|
522
|
+
method: 'GET',
|
|
523
|
+
headers: {
|
|
524
|
+
'Content-Type': 'application/json',
|
|
525
|
+
},
|
|
526
|
+
credentials: 'include',
|
|
527
|
+
});
|
|
528
|
+
return await response.json();
|
|
529
|
+
},
|
|
530
|
+
tagsNode: async function (nodeName, tagNames) {
|
|
531
|
+
const url = tagNames
|
|
532
|
+
.map(value => `tag_names=${encodeURIComponent(value)}`)
|
|
533
|
+
.join('&');
|
|
534
|
+
const response = await fetch(`${DJ_URL}/nodes/${nodeName}/tags?${url}`, {
|
|
535
|
+
method: 'POST',
|
|
536
|
+
headers: {
|
|
537
|
+
'Content-Type': 'application/json',
|
|
538
|
+
},
|
|
539
|
+
credentials: 'include',
|
|
540
|
+
});
|
|
541
|
+
return { status: response.status, json: await response.json() };
|
|
542
|
+
},
|
|
543
|
+
addTag: async function (name, displayName, tagType, description) {
|
|
544
|
+
const response = await fetch(`${DJ_URL}/tags`, {
|
|
545
|
+
method: 'POST',
|
|
546
|
+
headers: {
|
|
547
|
+
'Content-Type': 'application/json',
|
|
548
|
+
},
|
|
549
|
+
body: JSON.stringify({
|
|
550
|
+
name: name,
|
|
551
|
+
display_name: displayName,
|
|
552
|
+
tag_type: tagType,
|
|
553
|
+
description: description,
|
|
554
|
+
}),
|
|
555
|
+
credentials: 'include',
|
|
556
|
+
});
|
|
557
|
+
return { status: response.status, json: await response.json() };
|
|
558
|
+
},
|
|
559
|
+
editTag: async function (name, description, displayName) {
|
|
560
|
+
const updates = {};
|
|
561
|
+
if (description) {
|
|
562
|
+
updates.description = description;
|
|
563
|
+
}
|
|
564
|
+
if (displayName) {
|
|
565
|
+
updates.display_name = displayName;
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
const response = await fetch(`${DJ_URL}/tags/${name}`, {
|
|
569
|
+
method: 'PATCH',
|
|
570
|
+
headers: {
|
|
571
|
+
'Content-Type': 'application/json',
|
|
572
|
+
},
|
|
573
|
+
body: JSON.stringify(updates),
|
|
574
|
+
credentials: 'include',
|
|
575
|
+
});
|
|
576
|
+
return { status: response.status, json: await response.json() };
|
|
577
|
+
},
|
|
500
578
|
};
|
|
@@ -28,7 +28,7 @@ describe('DataJunctionAPI', () => {
|
|
|
28
28
|
it('calls logout correctly', async () => {
|
|
29
29
|
fetch.mockResponseOnce(JSON.stringify({}));
|
|
30
30
|
await DataJunctionAPI.logout();
|
|
31
|
-
expect(fetch).toHaveBeenCalledWith(`${DJ_URL}/
|
|
31
|
+
expect(fetch).toHaveBeenCalledWith(`${DJ_URL}/logout/`, {
|
|
32
32
|
credentials: 'include',
|
|
33
33
|
method: 'POST',
|
|
34
34
|
});
|
|
@@ -606,4 +606,87 @@ describe('DataJunctionAPI', () => {
|
|
|
606
606
|
},
|
|
607
607
|
);
|
|
608
608
|
});
|
|
609
|
+
|
|
610
|
+
it('calls listTags correctly', async () => {
|
|
611
|
+
fetch.mockResponseOnce(JSON.stringify(mocks.tags));
|
|
612
|
+
const res = await DataJunctionAPI.listTags();
|
|
613
|
+
expect(fetch).toHaveBeenCalledWith(`${DJ_URL}/tags`, {
|
|
614
|
+
credentials: 'include',
|
|
615
|
+
headers: {
|
|
616
|
+
'Content-Type': 'application/json',
|
|
617
|
+
},
|
|
618
|
+
method: 'GET',
|
|
619
|
+
});
|
|
620
|
+
expect(res).toEqual(mocks.tags);
|
|
621
|
+
});
|
|
622
|
+
|
|
623
|
+
it('calls getTag correctly', async () => {
|
|
624
|
+
fetch.mockResponseOnce(JSON.stringify(mocks.tags[0]));
|
|
625
|
+
const res = await DataJunctionAPI.getTag('report.financials');
|
|
626
|
+
expect(fetch).toHaveBeenCalledWith(`${DJ_URL}/tags/report.financials`, {
|
|
627
|
+
credentials: 'include',
|
|
628
|
+
headers: {
|
|
629
|
+
'Content-Type': 'application/json',
|
|
630
|
+
},
|
|
631
|
+
method: 'GET',
|
|
632
|
+
});
|
|
633
|
+
expect(res).toEqual(mocks.tags[0]);
|
|
634
|
+
});
|
|
635
|
+
|
|
636
|
+
it('calls listNodesForTag correctly', async () => {
|
|
637
|
+
fetch.mockResponseOnce(JSON.stringify(mocks.mockLinkedNodes));
|
|
638
|
+
const res = await DataJunctionAPI.listNodesForTag('report.financials');
|
|
639
|
+
expect(fetch).toHaveBeenCalledWith(
|
|
640
|
+
`${DJ_URL}/tags/report.financials/nodes`,
|
|
641
|
+
{
|
|
642
|
+
credentials: 'include',
|
|
643
|
+
headers: {
|
|
644
|
+
'Content-Type': 'application/json',
|
|
645
|
+
},
|
|
646
|
+
method: 'GET',
|
|
647
|
+
},
|
|
648
|
+
);
|
|
649
|
+
expect(res).toEqual(mocks.mockLinkedNodes);
|
|
650
|
+
});
|
|
651
|
+
|
|
652
|
+
it('calls tagsNode correctly', async () => {
|
|
653
|
+
fetch.mockResponseOnce(JSON.stringify(mocks.mockLinkedNodes));
|
|
654
|
+
await DataJunctionAPI.tagsNode('default.num_repair_orders', [
|
|
655
|
+
'report.financials',
|
|
656
|
+
'report.forecasts',
|
|
657
|
+
]);
|
|
658
|
+
expect(fetch).toHaveBeenCalledWith(
|
|
659
|
+
`${DJ_URL}/nodes/default.num_repair_orders/tags?tag_names=report.financials&tag_names=report.forecasts`,
|
|
660
|
+
{
|
|
661
|
+
credentials: 'include',
|
|
662
|
+
headers: {
|
|
663
|
+
'Content-Type': 'application/json',
|
|
664
|
+
},
|
|
665
|
+
method: 'POST',
|
|
666
|
+
},
|
|
667
|
+
);
|
|
668
|
+
});
|
|
669
|
+
|
|
670
|
+
it('calls addTag correctly', async () => {
|
|
671
|
+
fetch.mockResponseOnce(JSON.stringify(mocks.mockLinkedNodes));
|
|
672
|
+
await DataJunctionAPI.addTag(
|
|
673
|
+
'report.financials',
|
|
674
|
+
'Financial Reports',
|
|
675
|
+
'report',
|
|
676
|
+
'financial reports',
|
|
677
|
+
);
|
|
678
|
+
expect(fetch).toHaveBeenCalledWith(`${DJ_URL}/tags`, {
|
|
679
|
+
credentials: 'include',
|
|
680
|
+
headers: {
|
|
681
|
+
'Content-Type': 'application/json',
|
|
682
|
+
},
|
|
683
|
+
body: JSON.stringify({
|
|
684
|
+
name: 'report.financials',
|
|
685
|
+
display_name: 'Financial Reports',
|
|
686
|
+
tag_type: 'report',
|
|
687
|
+
description: 'financial reports',
|
|
688
|
+
}),
|
|
689
|
+
method: 'POST',
|
|
690
|
+
});
|
|
691
|
+
});
|
|
609
692
|
});
|