datajunction-ui 0.0.1-rc.2 → 0.0.1-rc.20

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 (135) hide show
  1. package/.env +1 -0
  2. package/.prettierignore +3 -1
  3. package/dj-logo.svg +10 -0
  4. package/package.json +43 -13
  5. package/public/favicon.ico +0 -0
  6. package/src/__tests__/reportWebVitals.test.jsx +44 -0
  7. package/src/app/__tests__/__snapshots__/index.test.tsx.snap +5 -39
  8. package/src/app/components/DeleteNode.jsx +79 -0
  9. package/src/app/components/ListGroupItem.jsx +8 -1
  10. package/src/app/components/NamespaceHeader.jsx +4 -13
  11. package/src/app/components/QueryInfo.jsx +77 -0
  12. package/src/app/components/Tab.jsx +3 -2
  13. package/src/app/components/ToggleSwitch.jsx +20 -0
  14. package/src/app/components/__tests__/QueryInfo.test.jsx +55 -0
  15. package/src/app/components/__tests__/Tab.test.jsx +27 -0
  16. package/src/app/components/__tests__/ToggleSwitch.test.jsx +43 -0
  17. package/src/app/components/__tests__/__snapshots__/ListGroupItem.test.tsx.snap +3 -0
  18. package/src/app/components/__tests__/__snapshots__/NamespaceHeader.test.jsx.snap +2 -18
  19. package/src/app/components/djgraph/Collapse.jsx +46 -0
  20. package/src/app/components/djgraph/DJNode.jsx +60 -82
  21. package/src/app/components/djgraph/DJNodeColumns.jsx +71 -0
  22. package/src/app/components/djgraph/DJNodeDimensions.jsx +75 -0
  23. package/src/app/components/djgraph/LayoutFlow.jsx +104 -0
  24. package/src/app/components/djgraph/__tests__/Collapse.test.jsx +51 -0
  25. package/src/app/components/djgraph/__tests__/DJNodeColumns.test.jsx +83 -0
  26. package/src/app/components/djgraph/__tests__/DJNodeDimensions.test.jsx +118 -0
  27. package/src/app/components/djgraph/__tests__/__snapshots__/DJNode.test.tsx.snap +84 -40
  28. package/src/app/constants.js +2 -0
  29. package/src/app/icons/AlertIcon.jsx +32 -0
  30. package/src/app/icons/CollapsedIcon.jsx +15 -0
  31. package/src/app/icons/DJLogo.jsx +36 -0
  32. package/src/app/icons/DeleteIcon.jsx +21 -0
  33. package/src/app/icons/EditIcon.jsx +18 -0
  34. package/src/app/icons/ExpandedIcon.jsx +15 -0
  35. package/src/app/icons/HorizontalHierarchyIcon.jsx +15 -0
  36. package/src/app/icons/InvalidIcon.jsx +14 -0
  37. package/src/app/icons/PythonIcon.jsx +52 -0
  38. package/src/app/icons/TableIcon.jsx +14 -0
  39. package/src/app/icons/ValidIcon.jsx +14 -0
  40. package/src/app/index.tsx +79 -26
  41. package/src/app/pages/AddEditNodePage/FormikSelect.jsx +46 -0
  42. package/src/app/pages/AddEditNodePage/FullNameField.jsx +37 -0
  43. package/src/app/pages/AddEditNodePage/Loadable.jsx +16 -0
  44. package/src/app/pages/AddEditNodePage/NodeQueryField.jsx +89 -0
  45. package/src/app/pages/AddEditNodePage/__tests__/AddEditNodePageFormFailed.test.jsx +77 -0
  46. package/src/app/pages/AddEditNodePage/__tests__/AddEditNodePageFormSuccess.test.jsx +93 -0
  47. package/src/app/pages/AddEditNodePage/__tests__/FormikSelect.test.jsx +75 -0
  48. package/src/app/pages/AddEditNodePage/__tests__/FullNameField.test.jsx +31 -0
  49. package/src/app/pages/AddEditNodePage/__tests__/NodeQueryField.test.jsx +30 -0
  50. package/src/app/pages/AddEditNodePage/__tests__/__snapshots__/AddEditNodePageFormFailed.test.jsx.snap +53 -0
  51. package/src/app/pages/AddEditNodePage/__tests__/__snapshots__/AddEditNodePageFormSuccess.test.jsx.snap +53 -0
  52. package/src/app/pages/AddEditNodePage/__tests__/__snapshots__/index.test.jsx.snap +3 -0
  53. package/src/app/pages/AddEditNodePage/__tests__/index.test.jsx +178 -0
  54. package/src/app/pages/AddEditNodePage/index.jsx +357 -0
  55. package/src/app/pages/LoginPage/__tests__/index.test.jsx +70 -0
  56. package/src/app/pages/LoginPage/assets/sign-in-with-github.png +0 -0
  57. package/src/app/pages/LoginPage/assets/sign-in-with-google.png +0 -0
  58. package/src/app/pages/LoginPage/index.jsx +90 -0
  59. package/src/app/pages/NamespacePage/Explorer.jsx +57 -0
  60. package/src/app/pages/NamespacePage/Loadable.jsx +9 -7
  61. package/src/app/pages/NamespacePage/__tests__/index.test.jsx +95 -0
  62. package/src/app/pages/NamespacePage/index.jsx +131 -31
  63. package/src/app/pages/NodePage/ClientCodePopover.jsx +32 -0
  64. package/src/app/pages/NodePage/EditColumnPopover.jsx +102 -0
  65. package/src/app/pages/NodePage/LinkDimensionPopover.jsx +135 -0
  66. package/src/app/pages/NodePage/Loadable.jsx +9 -7
  67. package/src/app/pages/NodePage/NodeColumnTab.jsx +106 -27
  68. package/src/app/pages/NodePage/NodeGraphTab.jsx +94 -148
  69. package/src/app/pages/NodePage/NodeHistory.jsx +212 -0
  70. package/src/app/pages/NodePage/NodeInfoTab.jsx +166 -51
  71. package/src/app/pages/NodePage/NodeLineageTab.jsx +84 -0
  72. package/src/app/pages/NodePage/NodeMaterializationTab.jsx +174 -0
  73. package/src/app/pages/NodePage/NodeSQLTab.jsx +82 -0
  74. package/src/app/pages/NodePage/NodeStatus.jsx +14 -20
  75. package/src/app/pages/NodePage/NodesWithDimension.jsx +42 -0
  76. package/src/app/pages/NodePage/__tests__/ClientCodePopover.test.jsx +49 -0
  77. package/src/app/pages/NodePage/__tests__/EditColumnPopover.test.jsx +148 -0
  78. package/src/app/pages/NodePage/__tests__/LinkDimensionPopover.test.jsx +165 -0
  79. package/src/app/pages/NodePage/__tests__/NodeGraphTab.test.jsx +591 -0
  80. package/src/app/pages/NodePage/__tests__/NodeLineageTab.test.jsx +57 -0
  81. package/src/app/pages/NodePage/__tests__/NodePage.test.jsx +725 -0
  82. package/src/app/pages/NodePage/__tests__/NodeWithDimension.test.jsx +175 -0
  83. package/src/app/pages/NodePage/__tests__/__snapshots__/NodePage.test.jsx.snap +402 -0
  84. package/src/app/pages/NodePage/index.jsx +151 -41
  85. package/src/app/pages/NotFoundPage/__tests__/index.test.jsx +16 -0
  86. package/src/app/pages/RegisterTablePage/Loadable.jsx +16 -0
  87. package/src/app/pages/RegisterTablePage/index.jsx +163 -0
  88. package/src/app/pages/Root/__tests__/index.test.jsx +77 -0
  89. package/src/app/pages/Root/index.tsx +32 -4
  90. package/src/app/pages/SQLBuilderPage/Loadable.jsx +16 -0
  91. package/src/app/pages/SQLBuilderPage/__tests__/index.test.jsx +173 -0
  92. package/src/app/pages/SQLBuilderPage/index.jsx +390 -0
  93. package/src/app/providers/djclient.jsx +5 -0
  94. package/src/app/services/DJService.js +388 -22
  95. package/src/app/services/__tests__/DJService.test.jsx +609 -0
  96. package/src/mocks/mockNodes.jsx +1397 -0
  97. package/src/setupTests.ts +31 -1
  98. package/src/styles/dag.css +111 -5
  99. package/src/styles/index.css +467 -31
  100. package/src/styles/login.css +67 -0
  101. package/src/styles/node-creation.scss +197 -0
  102. package/src/styles/styles.scss +44 -0
  103. package/src/styles/styles.scss.d.ts +9 -0
  104. package/src/utils/form.jsx +23 -0
  105. package/tsconfig.json +1 -5
  106. package/webpack.config.js +29 -6
  107. package/.babelrc +0 -4
  108. package/.env.local +0 -4
  109. package/.env.production +0 -1
  110. package/.github/pull_request_template.md +0 -11
  111. package/.github/workflows/ci.yml +0 -33
  112. package/.vscode/extensions.json +0 -7
  113. package/.vscode/launch.json +0 -15
  114. package/.vscode/settings.json +0 -25
  115. package/Dockerfile +0 -7
  116. package/dist/5fa71a03d45dc2e3d61447f3013a303d.png +0 -0
  117. package/dist/index.html +0 -1
  118. package/dist/main.js +0 -23303
  119. package/dist/static/main.05a86d446163fd5f17d3.js +0 -2
  120. package/dist/static/main.05a86d446163fd5f17d3.js.LICENSE.txt +0 -98
  121. package/dist/static/main.9e53bed734dae98e5b10.js +0 -2
  122. package/dist/static/main.9e53bed734dae98e5b10.js.LICENSE.txt +0 -98
  123. package/dist/static/main.js +0 -2
  124. package/dist/static/main.js.LICENSE.txt +0 -98
  125. package/dist/static/vendor.05a86d446163fd5f17d3.js +0 -2
  126. package/dist/static/vendor.05a86d446163fd5f17d3.js.LICENSE.txt +0 -29
  127. package/dist/static/vendor.9e53bed734dae98e5b10.js +0 -2
  128. package/dist/static/vendor.9e53bed734dae98e5b10.js.LICENSE.txt +0 -29
  129. package/dist/static/vendor.js +0 -2
  130. package/dist/static/vendor.js.LICENSE.txt +0 -29
  131. package/dist/vendor.js +0 -281
  132. package/src/app/pages/ListNamespacesPage/Loadable.jsx +0 -14
  133. package/src/app/pages/ListNamespacesPage/index.jsx +0 -52
  134. package/src/app/pages/NamespacePage/__tests__/__snapshots__/index.test.tsx.snap +0 -45
  135. package/src/app/pages/NamespacePage/__tests__/index.test.tsx +0 -14
@@ -0,0 +1,90 @@
1
+ import { useState } from 'react';
2
+ import { Formik, Form, Field, ErrorMessage } from 'formik';
3
+ import '../../../styles/login.css';
4
+ import logo from '../Root/assets/dj-logo.png';
5
+ import GitHubLoginButton from './assets/sign-in-with-github.png';
6
+
7
+ export function LoginPage() {
8
+ const [, setError] = useState('');
9
+ const githubLoginURL = new URL('/github/login/', process.env.REACT_APP_DJ_URL)
10
+ .href;
11
+
12
+ const handleBasicLogin = async ({ username, password }) => {
13
+ const data = new FormData();
14
+ data.append('username', username);
15
+ data.append('password', password);
16
+ await fetch(`${process.env.REACT_APP_DJ_URL}/basic/login/`, {
17
+ method: 'POST',
18
+ body: data,
19
+ credentials: 'include',
20
+ }).catch(error => {
21
+ setError(error ? JSON.stringify(error) : '');
22
+ });
23
+ window.location.reload();
24
+ };
25
+
26
+ return (
27
+ <div className="container">
28
+ <div className="login">
29
+ <center>
30
+ <Formik
31
+ initialValues={{ username: '', password: '' }}
32
+ validate={values => {
33
+ const errors = {};
34
+ if (!values.username) {
35
+ errors.username = 'Required';
36
+ }
37
+ if (!values.password) {
38
+ errors.password = 'Required';
39
+ }
40
+ return errors;
41
+ }}
42
+ onSubmit={(values, { setSubmitting }) => {
43
+ setTimeout(() => {
44
+ handleBasicLogin(values);
45
+ setSubmitting(false);
46
+ }, 400);
47
+ }}
48
+ >
49
+ {({ isSubmitting }) => (
50
+ <Form>
51
+ <div className="logo-title">
52
+ <img src={logo} alt="DJ Logo" width="75px" height="75px" />
53
+ <h2>DataJunction</h2>
54
+ </div>
55
+ <div className="inputContainer">
56
+ <ErrorMessage name="username" component="span" />
57
+ <Field type="text" name="username" placeholder="Username" />
58
+ </div>
59
+ <div>
60
+ <ErrorMessage name="password" component="span" />
61
+ <Field
62
+ type="password"
63
+ name="password"
64
+ placeholder="Password"
65
+ />
66
+ </div>
67
+ <button type="submit" disabled={isSubmitting}>
68
+ Login
69
+ </button>
70
+ {process.env.REACT_ENABLE_GITHUB_OAUTH === 'true' ? (
71
+ <div>
72
+ <a href={githubLoginURL}>
73
+ <img
74
+ src={GitHubLoginButton}
75
+ alt="Sign in with GitHub"
76
+ width="200px"
77
+ />
78
+ </a>
79
+ </div>
80
+ ) : (
81
+ ''
82
+ )}
83
+ </Form>
84
+ )}
85
+ </Formik>
86
+ </center>
87
+ </div>
88
+ </div>
89
+ );
90
+ }
@@ -0,0 +1,57 @@
1
+ import React, { useEffect, useState } from 'react';
2
+ import CollapsedIcon from '../../icons/CollapsedIcon';
3
+ import ExpandedIcon from '../../icons/ExpandedIcon';
4
+
5
+ const Explorer = ({ item = [], current }) => {
6
+ const [items, setItems] = useState([]);
7
+ const [expand, setExpand] = useState(false);
8
+ const [highlight, setHighlight] = useState(false);
9
+
10
+ useEffect(() => {
11
+ setItems(item);
12
+ setHighlight(current);
13
+ if (current === undefined || current?.startsWith(item.path)) {
14
+ setExpand(true);
15
+ } else setExpand(false);
16
+ }, [current, item]);
17
+
18
+ const handleClickOnParent = e => {
19
+ e.stopPropagation();
20
+ setExpand(prev => {
21
+ return !prev;
22
+ });
23
+ };
24
+
25
+ return (
26
+ <>
27
+ <div
28
+ className={`select-name ${
29
+ highlight === items.path ? 'select-name-highlight' : ''
30
+ }`}
31
+ onClick={handleClickOnParent}
32
+ >
33
+ {items.children && items.children.length > 0 ? (
34
+ <span>{!expand ? <CollapsedIcon /> : <ExpandedIcon />} </span>
35
+ ) : null}
36
+ <a href={`/namespaces/${items.path}`}>{items.namespace}</a>{' '}
37
+ </div>
38
+ {items.children
39
+ ? items.children.map((item, index) => (
40
+ <div
41
+ style={{
42
+ paddingLeft: '1.4rem',
43
+ marginLeft: '1rem',
44
+ borderLeft: '1px solid rgb(218 233 255)',
45
+ }}
46
+ >
47
+ <div className={`${expand ? '' : 'inactive'}`}>
48
+ <Explorer item={item} current={highlight} />
49
+ </div>
50
+ </div>
51
+ ))
52
+ : null}
53
+ </>
54
+ );
55
+ };
56
+
57
+ export default Explorer;
@@ -5,10 +5,12 @@
5
5
  import * as React from 'react';
6
6
  import { lazyLoad } from '../../../utils/loadable';
7
7
 
8
- export const NamespacePage = lazyLoad(
9
- () => import('./index'),
10
- module => module.NamespacePage,
11
- {
12
- fallback: <div></div>,
13
- },
14
- );
8
+ export const NamespacePage = props => {
9
+ return lazyLoad(
10
+ () => import('./index'),
11
+ module => module.NamespacePage,
12
+ {
13
+ fallback: <div></div>,
14
+ },
15
+ )(props);
16
+ };
@@ -0,0 +1,95 @@
1
+ import { fireEvent, render, screen, waitFor } from '@testing-library/react';
2
+ import { MemoryRouter, Route, Routes } from 'react-router-dom';
3
+ import DJClientContext from '../../../providers/djclient';
4
+ import { NamespacePage } from '../index';
5
+ import React from 'react';
6
+
7
+ const mockDjClient = {
8
+ namespaces: jest.fn(),
9
+ namespace: jest.fn(),
10
+ };
11
+
12
+ describe('NamespacePage', () => {
13
+ beforeEach(() => {
14
+ mockDjClient.namespaces.mockResolvedValue([
15
+ {
16
+ namespace: 'common.one',
17
+ num_nodes: 3,
18
+ },
19
+ {
20
+ namespace: 'common.one.a',
21
+ num_nodes: 6,
22
+ },
23
+ {
24
+ namespace: 'common.one.b',
25
+ num_nodes: 17,
26
+ },
27
+ {
28
+ namespace: 'common.one.c',
29
+ num_nodes: 64,
30
+ },
31
+ {
32
+ namespace: 'default',
33
+ num_nodes: 41,
34
+ },
35
+ {
36
+ namespace: 'default.fruits',
37
+ num_nodes: 1,
38
+ },
39
+ {
40
+ namespace: 'default.fruits.citrus.lemons',
41
+ num_nodes: 1,
42
+ },
43
+ {
44
+ namespace: 'default.vegetables',
45
+ num_nodes: 2,
46
+ },
47
+ ]);
48
+ mockDjClient.namespace.mockResolvedValue([
49
+ {
50
+ name: 'testNode',
51
+ display_name: 'Test Node',
52
+ type: 'transform',
53
+ mode: 'active',
54
+ updated_at: new Date(),
55
+ },
56
+ ]);
57
+ });
58
+
59
+ it('displays namespaces and renders nodes', async () => {
60
+ const element = (
61
+ <DJClientContext.Provider value={{ DataJunctionAPI: mockDjClient }}>
62
+ <NamespacePage />
63
+ </DJClientContext.Provider>
64
+ );
65
+ render(
66
+ <MemoryRouter initialEntries={['/namespaces/test.namespace']}>
67
+ <Routes>
68
+ <Route path="namespaces/:namespace" element={element} />
69
+ </Routes>
70
+ </MemoryRouter>,
71
+ );
72
+
73
+ await waitFor(() => {
74
+ expect(mockDjClient.namespaces).toHaveBeenCalledTimes(1);
75
+ expect(screen.getByText('Namespaces')).toBeInTheDocument();
76
+
77
+ // check that it displays namespaces
78
+ expect(screen.getByText('common')).toBeInTheDocument();
79
+ expect(screen.getByText('one')).toBeInTheDocument();
80
+ expect(screen.getByText('fruits')).toBeInTheDocument();
81
+ expect(screen.getByText('vegetables')).toBeInTheDocument();
82
+
83
+ // check that it renders nodes
84
+ expect(screen.getByText('Test Node')).toBeInTheDocument();
85
+
86
+ // click to open and close tab
87
+ fireEvent.click(screen.getByText('common'));
88
+ fireEvent.click(screen.getByText('common'));
89
+ });
90
+ });
91
+
92
+ afterEach(() => {
93
+ jest.clearAllMocks();
94
+ });
95
+ });
@@ -1,33 +1,66 @@
1
1
  import * as React from 'react';
2
2
  import { useParams } from 'react-router-dom';
3
- import { useEffect, useState } from 'react';
4
- import { DataJunctionAPI } from '../../services/DJService';
5
- import NamespaceHeader from '../../components/NamespaceHeader';
3
+ import { useContext, useEffect, useState } from 'react';
6
4
  import NodeStatus from '../NodePage/NodeStatus';
7
-
8
- export async function loader({ params }) {
9
- const djNode = await DataJunctionAPI.node(params.name);
10
- if (djNode.type === 'metric') {
11
- const metricNode = await DataJunctionAPI.metric(params.name);
12
- djNode.dimensions = metricNode.dimensions;
13
- }
14
- return djNode;
15
- }
5
+ import DJClientContext from '../../providers/djclient';
6
+ import Explorer from '../NamespacePage/Explorer';
7
+ import EditIcon from '../../icons/EditIcon';
8
+ import DeleteNode from '../../components/DeleteNode';
16
9
 
17
10
  export function NamespacePage() {
18
- const { namespace } = useParams();
11
+ const djClient = useContext(DJClientContext).DataJunctionAPI;
12
+ var { namespace } = useParams();
19
13
 
20
14
  const [state, setState] = useState({
21
15
  namespace: namespace,
22
16
  nodes: [],
23
17
  });
24
18
 
19
+ const [namespaceHierarchy, setNamespaceHierarchy] = useState([]);
20
+
21
+ const createNamespaceHierarchy = namespaceList => {
22
+ const hierarchy = [];
23
+
24
+ for (const item of namespaceList) {
25
+ const namespaces = item.namespace.split('.');
26
+ let currentLevel = hierarchy;
27
+
28
+ let path = '';
29
+ for (const ns of namespaces) {
30
+ path += ns;
31
+
32
+ let existingNamespace = currentLevel.find(el => el.namespace === ns);
33
+ if (!existingNamespace) {
34
+ existingNamespace = {
35
+ namespace: ns,
36
+ children: [],
37
+ path: path,
38
+ };
39
+ currentLevel.push(existingNamespace);
40
+ }
41
+
42
+ currentLevel = existingNamespace.children;
43
+ path += '.';
44
+ }
45
+ }
46
+ return hierarchy;
47
+ };
48
+
25
49
  useEffect(() => {
26
50
  const fetchData = async () => {
27
- const djNodes = await DataJunctionAPI.namespace(namespace);
28
- const nodes = djNodes.map(node => {
29
- return DataJunctionAPI.node(node);
30
- });
51
+ const namespaces = await djClient.namespaces();
52
+ const hierarchy = createNamespaceHierarchy(namespaces);
53
+ setNamespaceHierarchy(hierarchy);
54
+ };
55
+ fetchData().catch(console.error);
56
+ }, [djClient, djClient.namespaces]);
57
+
58
+ useEffect(() => {
59
+ const fetchData = async () => {
60
+ if (namespace === undefined && namespaceHierarchy !== undefined) {
61
+ namespace = namespaceHierarchy.children[0].path;
62
+ }
63
+ const nodes = await djClient.namespace(namespace);
31
64
  const foundNodes = await Promise.all(nodes);
32
65
  setState({
33
66
  namespace: namespace,
@@ -35,16 +68,13 @@ export function NamespacePage() {
35
68
  });
36
69
  };
37
70
  fetchData().catch(console.error);
38
- }, [namespace]);
71
+ }, [djClient, namespace, namespaceHierarchy]);
39
72
 
40
73
  const nodesList = state.nodes.map(node => (
41
74
  <tr>
42
- <td>
43
- <a href={'/namespaces/' + node.namespace}>{node.namespace}</a>
44
- </td>
45
75
  <td>
46
76
  <a href={'/nodes/' + node.name} className="link-table">
47
- {node.display_name}
77
+ {node.name}
48
78
  </a>
49
79
  <span
50
80
  className="rounded-pill badge bg-secondary-soft"
@@ -53,6 +83,11 @@ export function NamespacePage() {
53
83
  {node.version}
54
84
  </span>
55
85
  </td>
86
+ <td>
87
+ <a href={'/nodes/' + node.name} className="link-table">
88
+ {node.type !== 'source' ? node.display_name : ''}
89
+ </a>
90
+ </td>
56
91
  <td>
57
92
  <span className={'node_type__' + node.type + ' badge node_type'}>
58
93
  {node.type}
@@ -64,26 +99,91 @@ export function NamespacePage() {
64
99
  <td>
65
100
  <span className="status">{node.mode}</span>
66
101
  </td>
102
+ <td>
103
+ <span className="status">
104
+ {new Date(node.updated_at).toLocaleString('en-us')}
105
+ </span>
106
+ </td>
107
+ <td>
108
+ <a href={`/nodes/${node?.name}/edit`} style={{ marginLeft: '0.5rem' }}>
109
+ <EditIcon />
110
+ </a>
111
+ <DeleteNode nodeName={node?.name} />
112
+ </td>
67
113
  </tr>
68
114
  ));
69
115
 
70
- // @ts-ignore
71
116
  return (
72
117
  <div className="mid">
73
- <NamespaceHeader namespace={namespace} />
74
118
  <div className="card">
75
119
  <div className="card-header">
76
- <h2>Nodes</h2>
120
+ <h2>Explore</h2>
121
+
122
+ <span className="menu-link">
123
+ <span className="menu-title">
124
+ <div className="dropdown">
125
+ <span className="add_node">+ Add Node</span>
126
+ <div className="dropdown-content">
127
+ <a href={`/create/source`}>
128
+ <div className="node_type__source node_type_creation_heading">
129
+ Register Table
130
+ </div>
131
+ </a>
132
+ <a href={`/create/transform/${namespace}`}>
133
+ <div className="node_type__transform node_type_creation_heading">
134
+ Transform
135
+ </div>
136
+ </a>
137
+ <a href={`/create/metric/${namespace}`}>
138
+ <div className="node_type__metric node_type_creation_heading">
139
+ Metric
140
+ </div>
141
+ </a>
142
+ <a href={`/create/dimension/${namespace}`}>
143
+ <div className="node_type__dimension node_type_creation_heading">
144
+ Dimension
145
+ </div>
146
+ </a>
147
+ </div>
148
+ </div>
149
+ </span>
150
+ </span>
77
151
  <div className="table-responsive">
152
+ <div className={`sidebar`}>
153
+ <span
154
+ style={{
155
+ textTransform: 'uppercase',
156
+ fontSize: '0.8125rem',
157
+ fontWeight: '600',
158
+ color: '#95aac9',
159
+ padding: '1rem 1rem 1rem 0',
160
+ }}
161
+ >
162
+ Namespaces
163
+ </span>
164
+ {namespaceHierarchy
165
+ ? namespaceHierarchy.map(child => (
166
+ <Explorer
167
+ item={child}
168
+ current={state.namespace}
169
+ defaultExpand={true}
170
+ />
171
+ ))
172
+ : null}
173
+ </div>
78
174
  <table className="card-table table">
79
175
  <thead>
80
- <th>Namespace</th>
81
- <th>Name</th>
82
- <th>Type</th>
83
- <th>Status</th>
84
- <th>Mode</th>
176
+ <tr>
177
+ <th>Name</th>
178
+ <th>Display Name</th>
179
+ <th>Type</th>
180
+ <th>Status</th>
181
+ <th>Mode</th>
182
+ <th>Last Updated</th>
183
+ <th>Actions</th>
184
+ </tr>
85
185
  </thead>
86
- {nodesList}
186
+ <tbody>{nodesList}</tbody>
87
187
  </table>
88
188
  </div>
89
189
  </div>
@@ -0,0 +1,32 @@
1
+ import { Light as SyntaxHighlighter } from 'react-syntax-highlighter';
2
+ import { useState } from 'react';
3
+ import { nightOwl } from 'react-syntax-highlighter/src/styles/hljs';
4
+ import PythonIcon from '../../icons/PythonIcon';
5
+
6
+ export default function ClientCodePopover({ code }) {
7
+ const [codeAnchor, setCodeAnchor] = useState(false);
8
+
9
+ return (
10
+ <>
11
+ <button
12
+ className="code-button"
13
+ aria-label="code-button"
14
+ tabIndex="0"
15
+ height="45px"
16
+ onClick={() => setCodeAnchor(!codeAnchor)}
17
+ >
18
+ <PythonIcon />
19
+ </button>
20
+ <div
21
+ id={`node-create-code`}
22
+ role="dialog"
23
+ aria-label="client-code"
24
+ style={{ display: codeAnchor === false ? 'none' : 'block' }}
25
+ >
26
+ <SyntaxHighlighter language="python" style={nightOwl}>
27
+ {code}
28
+ </SyntaxHighlighter>
29
+ </div>
30
+ </>
31
+ );
32
+ }
@@ -0,0 +1,102 @@
1
+ import { useContext, useState } from 'react';
2
+ import * as React from 'react';
3
+ import DJClientContext from '../../providers/djclient';
4
+ import { Form, Formik } from 'formik';
5
+ import { FormikSelect } from '../AddEditNodePage/FormikSelect';
6
+ import EditIcon from '../../icons/EditIcon';
7
+ import { displayMessageAfterSubmit, labelize } from '../../../utils/form';
8
+
9
+ export default function EditColumnPopover({ column, node, options, onSubmit }) {
10
+ const djClient = useContext(DJClientContext).DataJunctionAPI;
11
+ const [popoverAnchor, setPopoverAnchor] = useState(false);
12
+
13
+ const saveAttributes = async (
14
+ { node, column, attributes },
15
+ { setSubmitting, setStatus },
16
+ ) => {
17
+ setSubmitting(false);
18
+ const response = await djClient.setAttributes(node, column, attributes);
19
+ if (response.status === 200 || response.status === 201) {
20
+ setStatus({ success: 'Saved!' });
21
+ } else {
22
+ setStatus({
23
+ failure: `${response.json.message}`,
24
+ });
25
+ }
26
+ onSubmit();
27
+ // window.location.reload();
28
+ };
29
+
30
+ return (
31
+ <>
32
+ <button
33
+ className="edit_button"
34
+ aria-label="EditColumn"
35
+ tabIndex="0"
36
+ onClick={() => {
37
+ setPopoverAnchor(!popoverAnchor);
38
+ }}
39
+ >
40
+ <EditIcon />
41
+ </button>
42
+ <div
43
+ className="popover"
44
+ role="dialog"
45
+ aria-label="client-code"
46
+ style={{ display: popoverAnchor === false ? 'none' : 'block' }}
47
+ >
48
+ <Formik
49
+ initialValues={{
50
+ column: column.name,
51
+ node: node.name,
52
+ attributes: [],
53
+ }}
54
+ onSubmit={saveAttributes}
55
+ >
56
+ {function Render({ isSubmitting, status, setFieldValue }) {
57
+ return (
58
+ <Form>
59
+ {displayMessageAfterSubmit(status)}
60
+ <span data-testid="edit-attributes">
61
+ <FormikSelect
62
+ selectOptions={options}
63
+ formikFieldName="attributes"
64
+ placeholder="Select column attributes"
65
+ className=""
66
+ defaultValue={column.attributes.map(attr => {
67
+ return {
68
+ value: attr.attribute_type.name,
69
+ label: labelize(attr.attribute_type.name),
70
+ };
71
+ })}
72
+ isMulti={true}
73
+ />
74
+ </span>
75
+ <input
76
+ hidden={true}
77
+ name="column"
78
+ value={column.name}
79
+ readOnly={true}
80
+ />
81
+ <input
82
+ hidden={true}
83
+ name="node"
84
+ value={node.name}
85
+ readOnly={true}
86
+ />
87
+ <button
88
+ className="add_node"
89
+ type="submit"
90
+ aria-label="SaveEditColumn"
91
+ aria-hidden="false"
92
+ >
93
+ Save
94
+ </button>
95
+ </Form>
96
+ );
97
+ }}
98
+ </Formik>
99
+ </div>
100
+ </>
101
+ );
102
+ }