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.
Files changed (38) hide show
  1. package/.env +1 -0
  2. package/package.json +3 -2
  3. package/src/app/components/Tab.jsx +0 -1
  4. package/src/app/constants.js +2 -2
  5. package/src/app/icons/LoadingIcon.jsx +14 -0
  6. package/src/app/index.tsx +11 -1
  7. package/src/app/pages/AddEditNodePage/__tests__/AddEditNodePageFormFailed.test.jsx +28 -2
  8. package/src/app/pages/AddEditNodePage/__tests__/AddEditNodePageFormSuccess.test.jsx +44 -9
  9. package/src/app/pages/AddEditNodePage/__tests__/__snapshots__/AddEditNodePageFormFailed.test.jsx.snap +1 -0
  10. package/src/app/pages/AddEditNodePage/__tests__/__snapshots__/AddEditNodePageFormSuccess.test.jsx.snap +0 -50
  11. package/src/app/pages/AddEditNodePage/__tests__/index.test.jsx +2 -0
  12. package/src/app/pages/AddEditNodePage/index.jsx +60 -6
  13. package/src/app/pages/AddEditTagPage/Loadable.jsx +16 -0
  14. package/src/app/pages/AddEditTagPage/__tests__/AddEditTagPage.test.jsx +107 -0
  15. package/src/app/pages/AddEditTagPage/index.jsx +132 -0
  16. package/src/app/pages/LoginPage/LoginForm.jsx +116 -0
  17. package/src/app/pages/LoginPage/SignupForm.jsx +144 -0
  18. package/src/app/pages/LoginPage/__tests__/index.test.jsx +34 -2
  19. package/src/app/pages/LoginPage/index.jsx +9 -82
  20. package/src/app/pages/NamespacePage/index.jsx +5 -0
  21. package/src/app/pages/NodePage/ClientCodePopover.jsx +15 -1
  22. package/src/app/pages/NodePage/EditColumnPopover.jsx +15 -1
  23. package/src/app/pages/NodePage/LinkDimensionPopover.jsx +16 -1
  24. package/src/app/pages/NodePage/NodeColumnTab.jsx +11 -0
  25. package/src/app/pages/NodePage/NodeInfoTab.jsx +5 -1
  26. package/src/app/pages/NodePage/__tests__/NodePage.test.jsx +6 -3
  27. package/src/app/pages/RegisterTablePage/__tests__/__snapshots__/RegisterTablePage.test.jsx.snap +1 -0
  28. package/src/app/pages/Root/index.tsx +1 -1
  29. package/src/app/pages/TagPage/Loadable.jsx +16 -0
  30. package/src/app/pages/TagPage/__tests__/TagPage.test.jsx +70 -0
  31. package/src/app/pages/TagPage/index.jsx +79 -0
  32. package/src/app/services/DJService.js +79 -1
  33. package/src/app/services/__tests__/DJService.test.jsx +84 -1
  34. package/src/mocks/mockNodes.jsx +88 -44
  35. package/src/styles/index.css +19 -0
  36. package/src/styles/loading.css +35 -0
  37. package/src/styles/login.css +17 -3
  38. 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 => <div>{tag}</div>);
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');
@@ -4,6 +4,7 @@ exports[`<RegisterTablePage /> registers a table correctly 1`] = `
4
4
  HTMLCollection [
5
5
  <div
6
6
  class="message success"
7
+ data-testid="success"
7
8
  >
8
9
  <svg
9
10
  class="bi bi-check-circle-fill"
@@ -59,7 +59,7 @@ export function Root() {
59
59
  ) : (
60
60
  <span className="menu-link">
61
61
  <span className="menu-title">
62
- <button onClick={handleLogout}>Logout</button>
62
+ <a onClick={handleLogout}>Logout</a>
63
63
  </span>
64
64
  </span>
65
65
  )}
@@ -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}/basic/logout/`, {
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}/basic/logout/`, {
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
  });