datajunction-ui 0.0.1-a1

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 (154) hide show
  1. package/.babel-plugin-macrosrc.js +5 -0
  2. package/.env +3 -0
  3. package/.eslintrc.js +20 -0
  4. package/.gitattributes +201 -0
  5. package/.husky/pre-commit +6 -0
  6. package/.nvmrc +1 -0
  7. package/.prettierignore +6 -0
  8. package/.prettierrc +9 -0
  9. package/.stylelintrc +7 -0
  10. package/LICENSE +22 -0
  11. package/Makefile +3 -0
  12. package/README.md +10 -0
  13. package/dj-logo.svg +10 -0
  14. package/internals/testing/loadable.mock.tsx +6 -0
  15. package/package.json +189 -0
  16. package/public/favicon.ico +0 -0
  17. package/public/index.html +26 -0
  18. package/public/manifest.json +15 -0
  19. package/public/robots.txt +3 -0
  20. package/src/__tests__/reportWebVitals.test.jsx +44 -0
  21. package/src/app/__tests__/__snapshots__/index.test.tsx.snap +9 -0
  22. package/src/app/__tests__/index.test.tsx +14 -0
  23. package/src/app/components/DeleteNode.jsx +55 -0
  24. package/src/app/components/ListGroupItem.jsx +24 -0
  25. package/src/app/components/NamespaceHeader.jsx +31 -0
  26. package/src/app/components/QueryInfo.jsx +77 -0
  27. package/src/app/components/Tab.jsx +25 -0
  28. package/src/app/components/ToggleSwitch.jsx +20 -0
  29. package/src/app/components/__tests__/DeleteNode.test.jsx +53 -0
  30. package/src/app/components/__tests__/ListGroupItem.test.tsx +16 -0
  31. package/src/app/components/__tests__/NamespaceHeader.test.jsx +14 -0
  32. package/src/app/components/__tests__/QueryInfo.test.jsx +55 -0
  33. package/src/app/components/__tests__/Tab.test.jsx +27 -0
  34. package/src/app/components/__tests__/ToggleSwitch.test.jsx +43 -0
  35. package/src/app/components/__tests__/__snapshots__/ListGroupItem.test.tsx.snap +29 -0
  36. package/src/app/components/__tests__/__snapshots__/NamespaceHeader.test.jsx.snap +47 -0
  37. package/src/app/components/djgraph/Collapse.jsx +46 -0
  38. package/src/app/components/djgraph/DJNode.jsx +89 -0
  39. package/src/app/components/djgraph/DJNodeColumns.jsx +71 -0
  40. package/src/app/components/djgraph/DJNodeDimensions.jsx +75 -0
  41. package/src/app/components/djgraph/LayoutFlow.jsx +104 -0
  42. package/src/app/components/djgraph/__tests__/Collapse.test.jsx +51 -0
  43. package/src/app/components/djgraph/__tests__/DJNode.test.tsx +24 -0
  44. package/src/app/components/djgraph/__tests__/DJNodeColumns.test.jsx +83 -0
  45. package/src/app/components/djgraph/__tests__/DJNodeDimensions.test.jsx +118 -0
  46. package/src/app/components/djgraph/__tests__/__snapshots__/DJNode.test.tsx.snap +117 -0
  47. package/src/app/constants.js +2 -0
  48. package/src/app/icons/AlertIcon.jsx +32 -0
  49. package/src/app/icons/CollapsedIcon.jsx +15 -0
  50. package/src/app/icons/DJLogo.jsx +36 -0
  51. package/src/app/icons/DeleteIcon.jsx +21 -0
  52. package/src/app/icons/EditIcon.jsx +18 -0
  53. package/src/app/icons/ExpandedIcon.jsx +15 -0
  54. package/src/app/icons/HorizontalHierarchyIcon.jsx +15 -0
  55. package/src/app/icons/InvalidIcon.jsx +14 -0
  56. package/src/app/icons/LoadingIcon.jsx +14 -0
  57. package/src/app/icons/PythonIcon.jsx +52 -0
  58. package/src/app/icons/TableIcon.jsx +14 -0
  59. package/src/app/icons/ValidIcon.jsx +14 -0
  60. package/src/app/index.tsx +108 -0
  61. package/src/app/pages/AddEditNodePage/FormikSelect.jsx +46 -0
  62. package/src/app/pages/AddEditNodePage/FullNameField.jsx +37 -0
  63. package/src/app/pages/AddEditNodePage/Loadable.jsx +16 -0
  64. package/src/app/pages/AddEditNodePage/NodeQueryField.jsx +89 -0
  65. package/src/app/pages/AddEditNodePage/__tests__/AddEditNodePageFormFailed.test.jsx +103 -0
  66. package/src/app/pages/AddEditNodePage/__tests__/AddEditNodePageFormSuccess.test.jsx +132 -0
  67. package/src/app/pages/AddEditNodePage/__tests__/FormikSelect.test.jsx +75 -0
  68. package/src/app/pages/AddEditNodePage/__tests__/FullNameField.test.jsx +31 -0
  69. package/src/app/pages/AddEditNodePage/__tests__/NodeQueryField.test.jsx +30 -0
  70. package/src/app/pages/AddEditNodePage/__tests__/__snapshots__/AddEditNodePageFormFailed.test.jsx.snap +54 -0
  71. package/src/app/pages/AddEditNodePage/__tests__/__snapshots__/AddEditNodePageFormSuccess.test.jsx.snap +3 -0
  72. package/src/app/pages/AddEditNodePage/__tests__/__snapshots__/index.test.jsx.snap +3 -0
  73. package/src/app/pages/AddEditNodePage/__tests__/index.test.jsx +180 -0
  74. package/src/app/pages/AddEditNodePage/index.jsx +396 -0
  75. package/src/app/pages/AddEditTagPage/Loadable.jsx +16 -0
  76. package/src/app/pages/AddEditTagPage/__tests__/AddEditTagPage.test.jsx +107 -0
  77. package/src/app/pages/AddEditTagPage/index.jsx +132 -0
  78. package/src/app/pages/LoginPage/LoginForm.jsx +124 -0
  79. package/src/app/pages/LoginPage/SignupForm.jsx +156 -0
  80. package/src/app/pages/LoginPage/__tests__/index.test.jsx +97 -0
  81. package/src/app/pages/LoginPage/assets/sign-in-with-github.png +0 -0
  82. package/src/app/pages/LoginPage/assets/sign-in-with-google.png +0 -0
  83. package/src/app/pages/LoginPage/index.jsx +17 -0
  84. package/src/app/pages/NamespacePage/AddNamespacePopover.jsx +85 -0
  85. package/src/app/pages/NamespacePage/Explorer.jsx +57 -0
  86. package/src/app/pages/NamespacePage/Loadable.jsx +16 -0
  87. package/src/app/pages/NamespacePage/__tests__/index.test.jsx +217 -0
  88. package/src/app/pages/NamespacePage/index.jsx +199 -0
  89. package/src/app/pages/NodePage/AddBackfillPopover.jsx +166 -0
  90. package/src/app/pages/NodePage/AddMaterializationPopover.jsx +161 -0
  91. package/src/app/pages/NodePage/ClientCodePopover.jsx +46 -0
  92. package/src/app/pages/NodePage/EditColumnPopover.jsx +116 -0
  93. package/src/app/pages/NodePage/LinkDimensionPopover.jsx +149 -0
  94. package/src/app/pages/NodePage/Loadable.jsx +16 -0
  95. package/src/app/pages/NodePage/NodeColumnTab.jsx +200 -0
  96. package/src/app/pages/NodePage/NodeGraphTab.jsx +112 -0
  97. package/src/app/pages/NodePage/NodeHistory.jsx +212 -0
  98. package/src/app/pages/NodePage/NodeInfoTab.jsx +212 -0
  99. package/src/app/pages/NodePage/NodeLineageTab.jsx +84 -0
  100. package/src/app/pages/NodePage/NodeMaterializationTab.jsx +233 -0
  101. package/src/app/pages/NodePage/NodeSQLTab.jsx +82 -0
  102. package/src/app/pages/NodePage/NodeStatus.jsx +28 -0
  103. package/src/app/pages/NodePage/NodesWithDimension.jsx +42 -0
  104. package/src/app/pages/NodePage/PartitionColumnPopover.jsx +153 -0
  105. package/src/app/pages/NodePage/__tests__/AddBackfillPopover.test.jsx +47 -0
  106. package/src/app/pages/NodePage/__tests__/ClientCodePopover.test.jsx +49 -0
  107. package/src/app/pages/NodePage/__tests__/EditColumnPopover.test.jsx +148 -0
  108. package/src/app/pages/NodePage/__tests__/LinkDimensionPopover.test.jsx +165 -0
  109. package/src/app/pages/NodePage/__tests__/NodeGraphTab.test.jsx +591 -0
  110. package/src/app/pages/NodePage/__tests__/NodeLineageTab.test.jsx +57 -0
  111. package/src/app/pages/NodePage/__tests__/NodePage.test.jsx +757 -0
  112. package/src/app/pages/NodePage/__tests__/NodeWithDimension.test.jsx +175 -0
  113. package/src/app/pages/NodePage/__tests__/__snapshots__/NodePage.test.jsx.snap +403 -0
  114. package/src/app/pages/NodePage/index.jsx +210 -0
  115. package/src/app/pages/NotFoundPage/Loadable.tsx +14 -0
  116. package/src/app/pages/NotFoundPage/__tests__/index.test.jsx +16 -0
  117. package/src/app/pages/NotFoundPage/index.tsx +23 -0
  118. package/src/app/pages/RegisterTablePage/Loadable.jsx +16 -0
  119. package/src/app/pages/RegisterTablePage/__tests__/RegisterTablePage.test.jsx +110 -0
  120. package/src/app/pages/RegisterTablePage/__tests__/__snapshots__/RegisterTablePage.test.jsx.snap +37 -0
  121. package/src/app/pages/RegisterTablePage/index.jsx +142 -0
  122. package/src/app/pages/Root/Loadable.tsx +14 -0
  123. package/src/app/pages/Root/__tests__/index.test.jsx +77 -0
  124. package/src/app/pages/Root/assets/dj-logo.png +0 -0
  125. package/src/app/pages/Root/index.tsx +70 -0
  126. package/src/app/pages/SQLBuilderPage/Loadable.jsx +16 -0
  127. package/src/app/pages/SQLBuilderPage/__tests__/index.test.jsx +173 -0
  128. package/src/app/pages/SQLBuilderPage/index.jsx +390 -0
  129. package/src/app/pages/TagPage/Loadable.jsx +16 -0
  130. package/src/app/pages/TagPage/__tests__/TagPage.test.jsx +70 -0
  131. package/src/app/pages/TagPage/index.jsx +79 -0
  132. package/src/app/providers/djclient.jsx +5 -0
  133. package/src/app/services/DJService.js +665 -0
  134. package/src/app/services/__tests__/DJService.test.jsx +804 -0
  135. package/src/index.tsx +48 -0
  136. package/src/mocks/mockNodes.jsx +1430 -0
  137. package/src/react-app-env.d.ts +4 -0
  138. package/src/reportWebVitals.ts +15 -0
  139. package/src/setupTests.ts +36 -0
  140. package/src/styles/dag.css +228 -0
  141. package/src/styles/index.css +1083 -0
  142. package/src/styles/loading.css +34 -0
  143. package/src/styles/login.css +81 -0
  144. package/src/styles/node-creation.scss +197 -0
  145. package/src/styles/styles.scss +44 -0
  146. package/src/styles/styles.scss.d.ts +9 -0
  147. package/src/utils/__tests__/__snapshots__/loadable.test.tsx.snap +17 -0
  148. package/src/utils/__tests__/loadable.test.tsx +53 -0
  149. package/src/utils/__tests__/request.test.ts +82 -0
  150. package/src/utils/form.jsx +23 -0
  151. package/src/utils/loadable.tsx +30 -0
  152. package/src/utils/request.ts +54 -0
  153. package/tsconfig.json +34 -0
  154. package/webpack.config.js +118 -0
@@ -0,0 +1,210 @@
1
+ import * as React from 'react';
2
+ import { useParams } from 'react-router-dom';
3
+ import { useContext, useEffect, useState } from 'react';
4
+ import Tab from '../../components/Tab';
5
+ import NamespaceHeader from '../../components/NamespaceHeader';
6
+ import NodeInfoTab from './NodeInfoTab';
7
+ import NodeColumnTab from './NodeColumnTab';
8
+ import NodeLineage from './NodeGraphTab';
9
+ import NodeHistory from './NodeHistory';
10
+ import DJClientContext from '../../providers/djclient';
11
+ import NodeSQLTab from './NodeSQLTab';
12
+ import NodeMaterializationTab from './NodeMaterializationTab';
13
+ import ClientCodePopover from './ClientCodePopover';
14
+ import NodesWithDimension from './NodesWithDimension';
15
+ import NodeColumnLineage from './NodeLineageTab';
16
+ import EditIcon from '../../icons/EditIcon';
17
+ import AlertIcon from '../../icons/AlertIcon';
18
+
19
+ export function NodePage() {
20
+ const djClient = useContext(DJClientContext).DataJunctionAPI;
21
+ const [state, setState] = useState({
22
+ selectedTab: 0,
23
+ });
24
+
25
+ const [node, setNode] = useState();
26
+
27
+ const onClickTab = id => () => {
28
+ setState({ selectedTab: id });
29
+ };
30
+
31
+ const buildTabs = tab => {
32
+ return tab.display ? (
33
+ <Tab
34
+ key={tab.id}
35
+ id={tab.id}
36
+ name={tab.name}
37
+ onClick={onClickTab(tab.id)}
38
+ selectedTab={state.selectedTab}
39
+ />
40
+ ) : null;
41
+ };
42
+
43
+ const { name } = useParams();
44
+
45
+ useEffect(() => {
46
+ const fetchData = async () => {
47
+ const data = await djClient.node(name);
48
+ data.createNodeClientCode = await djClient.clientCode(name);
49
+ setNode(data);
50
+ if (data.type === 'metric') {
51
+ const metric = await djClient.metric(name);
52
+ data.dimensions = metric.dimensions;
53
+ setNode(data);
54
+ }
55
+ if (data.type === 'cube') {
56
+ const cube = await djClient.cube(name);
57
+ data.cube_elements = cube.cube_elements;
58
+ setNode(data);
59
+ }
60
+ };
61
+ fetchData().catch(console.error);
62
+ }, [djClient, name]);
63
+
64
+ const tabsList = node => {
65
+ return [
66
+ {
67
+ id: 0,
68
+ name: 'Info',
69
+ display: true,
70
+ },
71
+ {
72
+ id: 1,
73
+ name: 'Columns',
74
+ display: true,
75
+ },
76
+ {
77
+ id: 2,
78
+ name: 'Graph',
79
+ display: true,
80
+ },
81
+ {
82
+ id: 3,
83
+ name: 'History',
84
+ display: true,
85
+ },
86
+ {
87
+ id: 4,
88
+ name: 'SQL',
89
+ display: node?.type !== 'dimension' && node?.type !== 'source',
90
+ },
91
+ {
92
+ id: 5,
93
+ name: 'Materializations',
94
+ display: node?.type !== 'source',
95
+ },
96
+ {
97
+ id: 6,
98
+ name: 'Linked Nodes',
99
+ display: node?.type === 'dimension',
100
+ },
101
+ {
102
+ id: 7,
103
+ name: 'Lineage',
104
+ display: node?.type === 'metric',
105
+ },
106
+ ];
107
+ };
108
+
109
+ //
110
+ //
111
+ let tabToDisplay = null;
112
+ switch (state.selectedTab) {
113
+ case 0:
114
+ tabToDisplay =
115
+ node && node.message === undefined ? <NodeInfoTab node={node} /> : '';
116
+ break;
117
+ case 1:
118
+ tabToDisplay = <NodeColumnTab node={node} djClient={djClient} />;
119
+ break;
120
+ case 2:
121
+ tabToDisplay = <NodeLineage djNode={node} djClient={djClient} />;
122
+ break;
123
+ case 3:
124
+ tabToDisplay = <NodeHistory node={node} djClient={djClient} />;
125
+ break;
126
+ case 4:
127
+ tabToDisplay =
128
+ node?.type === 'metric' ? <NodeSQLTab djNode={node} /> : <br />;
129
+ break;
130
+ case 5:
131
+ tabToDisplay = <NodeMaterializationTab node={node} djClient={djClient} />;
132
+ break;
133
+ case 6:
134
+ tabToDisplay = <NodesWithDimension node={node} djClient={djClient} />;
135
+ break;
136
+ case 7:
137
+ tabToDisplay = <NodeColumnLineage djNode={node} djClient={djClient} />;
138
+ break;
139
+ default: /* istanbul ignore next */
140
+ tabToDisplay = <NodeInfoTab node={node} />;
141
+ }
142
+ // @ts-ignore
143
+ return (
144
+ <div className="node__header">
145
+ <NamespaceHeader namespace={name.split('.').slice(0, -1).join('.')} />
146
+ <div className="card">
147
+ {node?.message === undefined ? (
148
+ <div className="card-header">
149
+ <h3
150
+ className="card-title align-items-start flex-column"
151
+ style={{ display: 'inline-block' }}
152
+ >
153
+ <span
154
+ className="card-label fw-bold text-gray-800"
155
+ role="dialog"
156
+ aria-hidden="false"
157
+ aria-label="DisplayName"
158
+ >
159
+ {node?.display_name}{' '}
160
+ <span
161
+ className={'node_type__' + node?.type + ' badge node_type'}
162
+ role="dialog"
163
+ aria-hidden="false"
164
+ aria-label="NodeType"
165
+ >
166
+ {node?.type}
167
+ </span>
168
+ </span>
169
+ </h3>
170
+ <a
171
+ href={`/nodes/${node?.name}/edit`}
172
+ style={{ marginLeft: '0.5rem' }}
173
+ >
174
+ <EditIcon />
175
+ </a>
176
+ <ClientCodePopover code={node?.createNodeClientCode} />
177
+ <div>
178
+ <a
179
+ href={'/nodes/' + node?.name}
180
+ className="link-table"
181
+ role="dialog"
182
+ aria-hidden="false"
183
+ aria-label="NodeName"
184
+ >
185
+ {node?.name}
186
+ </a>
187
+ <span
188
+ className="rounded-pill badge bg-secondary-soft"
189
+ style={{ marginLeft: '0.5rem' }}
190
+ >
191
+ {node?.version}
192
+ </span>
193
+ </div>
194
+ <div className="align-items-center row">
195
+ {tabsList(node).map(buildTabs)}
196
+ </div>
197
+ {tabToDisplay}
198
+ </div>
199
+ ) : node?.message !== undefined ? (
200
+ <div className="message alert" style={{ margin: '20px' }}>
201
+ <AlertIcon />
202
+ Node `{name}` does not exist!
203
+ </div>
204
+ ) : (
205
+ ''
206
+ )}
207
+ </div>
208
+ </div>
209
+ );
210
+ }
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Asynchronously loads the component for NotFoundPage
3
+ */
4
+
5
+ import * as React from 'react';
6
+ import { lazyLoad } from '../../../utils/loadable';
7
+
8
+ export const NotFoundPage = lazyLoad(
9
+ () => import('./index'),
10
+ module => module.NotFoundPage,
11
+ {
12
+ fallback: <></>,
13
+ },
14
+ );
@@ -0,0 +1,16 @@
1
+ import React from 'react';
2
+ import { render } from '@testing-library/react';
3
+ import { NotFoundPage } from '../index';
4
+ import { HelmetProvider } from 'react-helmet-async';
5
+
6
+ describe('<NotFoundPage />', () => {
7
+ it('displays the correct 404 message ', () => {
8
+ const { getByText } = render(
9
+ <HelmetProvider>
10
+ <NotFoundPage />
11
+ </HelmetProvider>,
12
+ );
13
+
14
+ expect(getByText('Page not found.')).toBeInTheDocument();
15
+ });
16
+ });
@@ -0,0 +1,23 @@
1
+ import * as React from 'react';
2
+ import { Helmet } from 'react-helmet-async';
3
+
4
+ export function NotFoundPage() {
5
+ return (
6
+ <>
7
+ <Helmet>
8
+ <title>404 Page Not Found</title>
9
+ <meta name="description" content="Page not found" />
10
+ </Helmet>
11
+ <div>
12
+ <label>
13
+ 4
14
+ <span role="img" aria-label="Crying Face">
15
+ 😢
16
+ </span>
17
+ 4
18
+ </label>
19
+ <p>Page not found.</p>
20
+ </div>
21
+ </>
22
+ );
23
+ }
@@ -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 RegisterTablePage = () => {
9
+ return lazyLoad(
10
+ () => import('./index'),
11
+ module => module.RegisterTablePage,
12
+ {
13
+ fallback: <div></div>,
14
+ },
15
+ )();
16
+ };
@@ -0,0 +1,110 @@
1
+ import React from 'react';
2
+ import { fireEvent, screen, waitFor } from '@testing-library/react';
3
+ import fetchMock from 'jest-fetch-mock';
4
+ import userEvent from '@testing-library/user-event';
5
+ import { render } from '../../../../setupTests';
6
+ import { MemoryRouter, Route, Routes } from 'react-router-dom';
7
+ import DJClientContext from '../../../providers/djclient';
8
+ import { RegisterTablePage } from '../index';
9
+
10
+ describe('<RegisterTablePage />', () => {
11
+ const initializeMockDJClient = () => {
12
+ return {
13
+ DataJunctionAPI: {
14
+ catalogs: jest.fn(),
15
+ registerTable: jest.fn(),
16
+ },
17
+ };
18
+ };
19
+
20
+ const mockDjClient = initializeMockDJClient();
21
+
22
+ beforeEach(() => {
23
+ fetchMock.resetMocks();
24
+ jest.clearAllMocks();
25
+ window.scrollTo = jest.fn();
26
+
27
+ mockDjClient.DataJunctionAPI.catalogs.mockReturnValue([
28
+ {
29
+ name: 'warehouse',
30
+ engines: [
31
+ {
32
+ name: 'duckdb',
33
+ version: '0.7.1',
34
+ uri: null,
35
+ dialect: null,
36
+ },
37
+ ],
38
+ },
39
+ ]);
40
+ });
41
+
42
+ const renderRegisterTable = element => {
43
+ return render(
44
+ <MemoryRouter initialEntries={['/create/source']}>
45
+ <Routes>
46
+ <Route path="create/source" element={element} />
47
+ </Routes>
48
+ </MemoryRouter>,
49
+ );
50
+ };
51
+
52
+ const testElement = djClient => {
53
+ return (
54
+ <DJClientContext.Provider value={djClient}>
55
+ <RegisterTablePage />
56
+ </DJClientContext.Provider>
57
+ );
58
+ };
59
+
60
+ it('registers a table correctly', async () => {
61
+ mockDjClient.DataJunctionAPI.registerTable.mockReturnValue({
62
+ status: 201,
63
+ json: { name: 'source.warehouse.schema.some_table' },
64
+ });
65
+
66
+ const element = testElement(mockDjClient);
67
+ const { container, getByTestId } = renderRegisterTable(element);
68
+
69
+ const catalog = getByTestId('choose-catalog');
70
+ await waitFor(async () => {
71
+ fireEvent.keyDown(catalog.firstChild, { key: 'ArrowDown' });
72
+ fireEvent.click(screen.getByText('warehouse'));
73
+ });
74
+
75
+ await userEvent.type(screen.getByLabelText('Schema'), 'schema');
76
+ await userEvent.type(screen.getByLabelText('Table'), 'some_table');
77
+ await userEvent.click(screen.getByRole('button'));
78
+
79
+ await waitFor(() => {
80
+ expect(mockDjClient.DataJunctionAPI.registerTable).toBeCalled();
81
+ expect(mockDjClient.DataJunctionAPI.registerTable).toBeCalledWith(
82
+ 'warehouse',
83
+ 'schema',
84
+ 'some_table',
85
+ );
86
+ });
87
+ expect(container.getElementsByClassName('message')).toMatchSnapshot();
88
+ }, 60000);
89
+
90
+ it('fails to register a table', async () => {
91
+ mockDjClient.DataJunctionAPI.registerTable.mockReturnValue({
92
+ status: 500,
93
+ json: { message: 'table not found' },
94
+ });
95
+
96
+ const element = testElement(mockDjClient);
97
+ const { getByTestId } = renderRegisterTable(element);
98
+
99
+ const catalog = getByTestId('choose-catalog');
100
+ await waitFor(async () => {
101
+ fireEvent.keyDown(catalog.firstChild, { key: 'ArrowDown' });
102
+ fireEvent.click(screen.getByText('warehouse'));
103
+ });
104
+
105
+ await userEvent.type(screen.getByLabelText('Schema'), 'schema');
106
+ await userEvent.type(screen.getByLabelText('Table'), 'some_table');
107
+ await userEvent.click(screen.getByRole('button'));
108
+ expect(screen.getByText('table not found')).toBeInTheDocument();
109
+ }, 60000);
110
+ });
@@ -0,0 +1,37 @@
1
+ // Jest Snapshot v1, https://goo.gl/fbAQLP
2
+
3
+ exports[`<RegisterTablePage /> registers a table correctly 1`] = `
4
+ HTMLCollection [
5
+ <div
6
+ class="message success"
7
+ data-testid="success"
8
+ >
9
+ <svg
10
+ class="bi bi-check-circle-fill"
11
+ fill="currentColor"
12
+ height="25"
13
+ viewBox="0 0 16 16"
14
+ width="25"
15
+ xmlns="http://www.w3.org/2000/svg"
16
+ >
17
+ <path
18
+ d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0zm-3.97-3.03a.75.75 0 0 0-1.08.022L7.477 9.417 5.384 7.323a.75.75 0 0 0-1.06 1.06L6.97 11.03a.75.75 0 0 0 1.079-.02l3.992-4.99a.75.75 0 0 0-.01-1.05z"
19
+ />
20
+ </svg>
21
+ Successfully registered source node
22
+
23
+ <a
24
+ href="/nodes/source.warehouse.schema.some_table"
25
+ >
26
+ source.warehouse.schema.some_table
27
+ </a>
28
+ , which references table
29
+ warehouse
30
+ .
31
+ schema
32
+ .
33
+ some_table
34
+ .
35
+ </div>,
36
+ ]
37
+ `;
@@ -0,0 +1,142 @@
1
+ /**
2
+ * Node add + edit page for transforms, metrics, and dimensions. The creation and edit flow for these
3
+ * node types is largely the same, with minor differences handled server-side. For the `query`
4
+ * field, this page will render a CodeMirror SQL editor with autocompletion and syntax highlighting.
5
+ */
6
+ import { ErrorMessage, Field, Form, Formik } from 'formik';
7
+
8
+ import NamespaceHeader from '../../components/NamespaceHeader';
9
+ import React, { useContext, useEffect, useState } from 'react';
10
+ import DJClientContext from '../../providers/djclient';
11
+ import 'styles/node-creation.scss';
12
+ import { FormikSelect } from '../AddEditNodePage/FormikSelect';
13
+ import { displayMessageAfterSubmit } from '../../../utils/form';
14
+
15
+ export function RegisterTablePage() {
16
+ const djClient = useContext(DJClientContext).DataJunctionAPI;
17
+ const [catalogs, setCatalogs] = useState([]);
18
+
19
+ useEffect(() => {
20
+ const fetchData = async () => {
21
+ const catalogs = await djClient.catalogs();
22
+ setCatalogs(
23
+ catalogs.map(catalog => {
24
+ return { value: catalog.name, label: catalog.name };
25
+ }),
26
+ );
27
+ };
28
+ fetchData().catch(console.error);
29
+ }, [djClient, djClient.namespaces]);
30
+
31
+ const initialValues = {
32
+ catalog: '',
33
+ schema: '',
34
+ table: '',
35
+ };
36
+
37
+ const validator = values => {
38
+ const errors = {};
39
+ if (!values.table) {
40
+ errors.table = 'Required';
41
+ }
42
+ if (!values.schema) {
43
+ errors.schema = 'Required';
44
+ }
45
+ return errors;
46
+ };
47
+
48
+ const handleSubmit = async (values, { setSubmitting, setStatus }) => {
49
+ const { status, json } = await djClient.registerTable(
50
+ values.catalog,
51
+ values.schema,
52
+ values.table,
53
+ );
54
+ if (status === 200 || status === 201) {
55
+ setStatus({
56
+ success: (
57
+ <>
58
+ Successfully registered source node{' '}
59
+ <a href={`/nodes/${json.name}`}>{json.name}</a>, which references
60
+ table {values.catalog}.{values.schema}.{values.table}.
61
+ </>
62
+ ),
63
+ });
64
+ } else {
65
+ setStatus({
66
+ failure: `${json.message}`,
67
+ });
68
+ }
69
+ setSubmitting(false);
70
+ window.scrollTo({ top: 0, left: 0, behavior: 'smooth' });
71
+ };
72
+
73
+ return (
74
+ <div className="mid">
75
+ <NamespaceHeader namespace="" />
76
+ <div className="card">
77
+ <div className="card-header">
78
+ <h2>
79
+ Register{' '}
80
+ <span className={`node_type__source node_type_creation_heading`}>
81
+ Source
82
+ </span>
83
+ </h2>
84
+ <center>
85
+ <Formik
86
+ initialValues={initialValues}
87
+ validate={validator}
88
+ onSubmit={handleSubmit}
89
+ >
90
+ {function Render({ isSubmitting, status }) {
91
+ return (
92
+ <Form>
93
+ {displayMessageAfterSubmit(status)}
94
+ {
95
+ <>
96
+ <div className="SourceCreationInput">
97
+ <ErrorMessage name="catalog" component="span" />
98
+ <label htmlFor="catalog">Catalog</label>
99
+ <span data-testid="choose-catalog">
100
+ <FormikSelect
101
+ selectOptions={catalogs}
102
+ formikFieldName="catalog"
103
+ placeholder="Choose Catalog"
104
+ defaultValue={catalogs[0]}
105
+ />
106
+ </span>
107
+ </div>
108
+ <div className="SourceCreationInput">
109
+ <ErrorMessage name="schema" component="span" />
110
+ <label htmlFor="schema">Schema</label>
111
+ <Field
112
+ type="text"
113
+ name="schema"
114
+ id="schema"
115
+ placeholder="Schema"
116
+ />
117
+ </div>
118
+ <div className="SourceCreationInput NodeCreationInput">
119
+ <ErrorMessage name="table" component="span" />
120
+ <label htmlFor="table">Table</label>
121
+ <Field
122
+ type="text"
123
+ name="table"
124
+ id="table"
125
+ placeholder="Table"
126
+ />
127
+ </div>
128
+ <button type="submit" disabled={isSubmitting}>
129
+ Register
130
+ </button>
131
+ </>
132
+ }
133
+ </Form>
134
+ );
135
+ }}
136
+ </Formik>
137
+ </center>
138
+ </div>
139
+ </div>
140
+ </div>
141
+ );
142
+ }
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Asynchronously loads the component for the root page
3
+ */
4
+
5
+ import * as React from 'react';
6
+ import { lazyLoad } from '../../../utils/loadable';
7
+
8
+ export const Root = lazyLoad(
9
+ () => import('./index'),
10
+ module => module.Root,
11
+ {
12
+ fallback: <></>,
13
+ },
14
+ );
@@ -0,0 +1,77 @@
1
+ import React from 'react';
2
+ import { render, screen, waitFor } from '@testing-library/react';
3
+ import { Root } from '../index';
4
+ import DJClientContext from '../../../providers/djclient';
5
+ import { HelmetProvider } from 'react-helmet-async';
6
+
7
+ describe('<Root />', () => {
8
+ const mockDjClient = {
9
+ logout: jest.fn(),
10
+ };
11
+
12
+ it('renders with the correct title and navigation', async () => {
13
+ render(
14
+ <HelmetProvider>
15
+ <DJClientContext.Provider value={{ DataJunctionAPI: mockDjClient }}>
16
+ <Root />
17
+ </DJClientContext.Provider>
18
+ </HelmetProvider>,
19
+ );
20
+
21
+ waitFor(() => {
22
+ expect(document.title).toEqual('DataJunction');
23
+ const metaDescription = document.querySelector(
24
+ "meta[name='description']",
25
+ );
26
+ expect(metaDescription).toBeInTheDocument();
27
+ expect(metaDescription.content).toBe(
28
+ 'DataJunction Metrics Platform Webapp',
29
+ );
30
+
31
+ expect(screen.getByText(/^DataJunction$/)).toBeInTheDocument();
32
+ expect(screen.getByText('Explore').closest('a')).toHaveAttribute(
33
+ 'href',
34
+ '/',
35
+ );
36
+ expect(screen.getByText('SQL').closest('a')).toHaveAttribute(
37
+ 'href',
38
+ '/sql',
39
+ );
40
+ expect(screen.getByText('Docs').closest('a')).toHaveAttribute(
41
+ 'href',
42
+ 'https://www.datajunction.io',
43
+ );
44
+ });
45
+ });
46
+
47
+ it('renders Logout button unless REACT_DISABLE_AUTH is true', () => {
48
+ process.env.REACT_DISABLE_AUTH = 'false';
49
+ render(
50
+ <HelmetProvider>
51
+ <DJClientContext.Provider value={{ DataJunctionAPI: mockDjClient }}>
52
+ <Root />
53
+ </DJClientContext.Provider>
54
+ </HelmetProvider>,
55
+ );
56
+ expect(screen.getByText('Logout')).toBeInTheDocument();
57
+ });
58
+
59
+ it('calls logout and reloads window on logout button click', () => {
60
+ process.env.REACT_DISABLE_AUTH = 'false';
61
+ const originalLocation = window.location;
62
+ delete window.location;
63
+ window.location = { ...originalLocation, reload: jest.fn() };
64
+
65
+ render(
66
+ <HelmetProvider>
67
+ <DJClientContext.Provider value={{ DataJunctionAPI: mockDjClient }}>
68
+ <Root />
69
+ </DJClientContext.Provider>
70
+ </HelmetProvider>,
71
+ );
72
+
73
+ screen.getByText('Logout').click();
74
+ expect(mockDjClient.logout).toHaveBeenCalled();
75
+ window.location = originalLocation;
76
+ });
77
+ });