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,217 @@
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
+ import userEvent from '@testing-library/user-event';
7
+
8
+ const mockDjClient = {
9
+ namespaces: jest.fn(),
10
+ namespace: jest.fn(),
11
+ addNamespace: jest.fn(),
12
+ };
13
+
14
+ describe('NamespacePage', () => {
15
+ const original = window.location;
16
+
17
+ const reloadFn = () => {
18
+ window.location.reload();
19
+ };
20
+
21
+ beforeAll(() => {
22
+ Object.defineProperty(window, 'location', {
23
+ configurable: true,
24
+ value: { reload: jest.fn() },
25
+ });
26
+ });
27
+
28
+ afterAll(() => {
29
+ Object.defineProperty(window, 'location', {
30
+ configurable: true,
31
+ value: original,
32
+ });
33
+ });
34
+
35
+ beforeEach(() => {
36
+ fetch.resetMocks();
37
+ mockDjClient.namespaces.mockResolvedValue([
38
+ {
39
+ namespace: 'common.one',
40
+ num_nodes: 3,
41
+ },
42
+ {
43
+ namespace: 'common.one.a',
44
+ num_nodes: 6,
45
+ },
46
+ {
47
+ namespace: 'common.one.b',
48
+ num_nodes: 17,
49
+ },
50
+ {
51
+ namespace: 'common.one.c',
52
+ num_nodes: 64,
53
+ },
54
+ {
55
+ namespace: 'default',
56
+ num_nodes: 41,
57
+ },
58
+ {
59
+ namespace: 'default.fruits',
60
+ num_nodes: 1,
61
+ },
62
+ {
63
+ namespace: 'default.fruits.citrus.lemons',
64
+ num_nodes: 1,
65
+ },
66
+ {
67
+ namespace: 'default.vegetables',
68
+ num_nodes: 2,
69
+ },
70
+ ]);
71
+ mockDjClient.namespace.mockResolvedValue([
72
+ {
73
+ name: 'testNode',
74
+ display_name: 'Test Node',
75
+ type: 'transform',
76
+ mode: 'active',
77
+ updated_at: new Date(),
78
+ },
79
+ ]);
80
+ });
81
+
82
+ afterEach(() => {
83
+ jest.clearAllMocks();
84
+ });
85
+
86
+ it('displays namespaces and renders nodes', async () => {
87
+ reloadFn();
88
+ const element = (
89
+ <DJClientContext.Provider value={{ DataJunctionAPI: mockDjClient }}>
90
+ <NamespacePage />
91
+ </DJClientContext.Provider>
92
+ );
93
+ render(
94
+ <MemoryRouter initialEntries={['/namespaces/test.namespace']}>
95
+ <Routes>
96
+ <Route path="namespaces/:namespace" element={element} />
97
+ </Routes>
98
+ </MemoryRouter>,
99
+ );
100
+
101
+ await waitFor(() => {
102
+ expect(mockDjClient.namespaces).toHaveBeenCalledTimes(1);
103
+ expect(screen.getByText('Namespaces')).toBeInTheDocument();
104
+
105
+ // check that it displays namespaces
106
+ expect(screen.getByText('common')).toBeInTheDocument();
107
+ expect(screen.getByText('one')).toBeInTheDocument();
108
+ expect(screen.getByText('fruits')).toBeInTheDocument();
109
+ expect(screen.getByText('vegetables')).toBeInTheDocument();
110
+
111
+ // check that it renders nodes
112
+ expect(screen.getByText('Test Node')).toBeInTheDocument();
113
+
114
+ // click to open and close tab
115
+ fireEvent.click(screen.getByText('common'));
116
+ fireEvent.click(screen.getByText('common'));
117
+ });
118
+ });
119
+
120
+ afterEach(() => {
121
+ jest.clearAllMocks();
122
+ });
123
+
124
+ it('can add new namespace via add namespace popover', async () => {
125
+ mockDjClient.addNamespace.mockReturnValue({
126
+ status: 201,
127
+ json: {},
128
+ });
129
+ const element = (
130
+ <DJClientContext.Provider value={{ DataJunctionAPI: mockDjClient }}>
131
+ <NamespacePage />
132
+ </DJClientContext.Provider>
133
+ );
134
+ render(
135
+ <MemoryRouter initialEntries={['/namespaces/test.namespace']}>
136
+ <Routes>
137
+ <Route path="namespaces/:namespace" element={element} />
138
+ </Routes>
139
+ </MemoryRouter>,
140
+ );
141
+
142
+ // Find the button to toggle the add namespace popover
143
+ const addNamespaceToggle = screen.getByRole('button', {
144
+ name: 'AddNamespaceTogglePopover',
145
+ });
146
+ expect(addNamespaceToggle).toBeInTheDocument();
147
+
148
+ // Click the toggle and verify that the popover displays
149
+ fireEvent.click(addNamespaceToggle);
150
+ const addNamespacePopover = screen.getByRole('dialog', {
151
+ name: 'AddNamespacePopover',
152
+ });
153
+ expect(addNamespacePopover).toBeInTheDocument();
154
+
155
+ // Type in the new namespace
156
+ await userEvent.type(
157
+ screen.getByLabelText('Namespace'),
158
+ 'some.random.namespace',
159
+ );
160
+
161
+ // Save
162
+ const saveNamespace = screen.getByRole('button', {
163
+ name: 'SaveNamespace',
164
+ });
165
+ await waitFor(() => {
166
+ fireEvent.click(saveNamespace);
167
+ });
168
+ expect(mockDjClient.addNamespace).toHaveBeenCalled();
169
+ expect(mockDjClient.addNamespace).toHaveBeenCalledWith(
170
+ 'some.random.namespace',
171
+ );
172
+ expect(screen.getByText('Saved')).toBeInTheDocument();
173
+ expect(window.location.reload).toHaveBeenCalled();
174
+ });
175
+
176
+ it('can fail to add namespace', async () => {
177
+ mockDjClient.addNamespace.mockReturnValue({
178
+ status: 500,
179
+ json: { message: 'you failed' },
180
+ });
181
+ const element = (
182
+ <DJClientContext.Provider value={{ DataJunctionAPI: mockDjClient }}>
183
+ <NamespacePage />
184
+ </DJClientContext.Provider>
185
+ );
186
+ render(
187
+ <MemoryRouter initialEntries={['/namespaces/test.namespace']}>
188
+ <Routes>
189
+ <Route path="namespaces/:namespace" element={element} />
190
+ </Routes>
191
+ </MemoryRouter>,
192
+ );
193
+
194
+ // Open the add namespace popover
195
+ const addNamespaceToggle = screen.getByRole('button', {
196
+ name: 'AddNamespaceTogglePopover',
197
+ });
198
+ fireEvent.click(addNamespaceToggle);
199
+
200
+ // Type in the new namespace
201
+ await userEvent.type(
202
+ screen.getByLabelText('Namespace'),
203
+ 'some.random.namespace',
204
+ );
205
+
206
+ // Save
207
+ const saveNamespace = screen.getByRole('button', {
208
+ name: 'SaveNamespace',
209
+ });
210
+ await waitFor(() => {
211
+ fireEvent.click(saveNamespace);
212
+ });
213
+
214
+ // Should display failure alert
215
+ expect(screen.getByText('you failed')).toBeInTheDocument();
216
+ });
217
+ });
@@ -0,0 +1,199 @@
1
+ import * as React from 'react';
2
+ import { useParams } from 'react-router-dom';
3
+ import { useContext, useEffect, useState } from 'react';
4
+ import NodeStatus from '../NodePage/NodeStatus';
5
+ import DJClientContext from '../../providers/djclient';
6
+ import Explorer from '../NamespacePage/Explorer';
7
+ import EditIcon from '../../icons/EditIcon';
8
+ import DeleteNode from '../../components/DeleteNode';
9
+ import AddNamespacePopover from './AddNamespacePopover';
10
+
11
+ export function NamespacePage() {
12
+ const djClient = useContext(DJClientContext).DataJunctionAPI;
13
+ var { namespace } = useParams();
14
+
15
+ const [state, setState] = useState({
16
+ namespace: namespace,
17
+ nodes: [],
18
+ });
19
+
20
+ const [namespaceHierarchy, setNamespaceHierarchy] = useState([]);
21
+
22
+ const createNamespaceHierarchy = namespaceList => {
23
+ const hierarchy = [];
24
+
25
+ for (const item of namespaceList) {
26
+ const namespaces = item.namespace.split('.');
27
+ let currentLevel = hierarchy;
28
+
29
+ let path = '';
30
+ for (const ns of namespaces) {
31
+ path += ns;
32
+
33
+ let existingNamespace = currentLevel.find(el => el.namespace === ns);
34
+ if (!existingNamespace) {
35
+ existingNamespace = {
36
+ namespace: ns,
37
+ children: [],
38
+ path: path,
39
+ };
40
+ currentLevel.push(existingNamespace);
41
+ }
42
+
43
+ currentLevel = existingNamespace.children;
44
+ path += '.';
45
+ }
46
+ }
47
+ return hierarchy;
48
+ };
49
+
50
+ useEffect(() => {
51
+ const fetchData = async () => {
52
+ const namespaces = await djClient.namespaces();
53
+ const hierarchy = createNamespaceHierarchy(namespaces);
54
+ setNamespaceHierarchy(hierarchy);
55
+ };
56
+ fetchData().catch(console.error);
57
+ }, [djClient, djClient.namespaces]);
58
+
59
+ useEffect(() => {
60
+ const fetchData = async () => {
61
+ if (namespace === undefined && namespaceHierarchy !== undefined) {
62
+ namespace = namespaceHierarchy[0].namespace;
63
+ }
64
+ const nodes = await djClient.namespace(namespace);
65
+ const foundNodes = await Promise.all(nodes);
66
+ setState({
67
+ namespace: namespace,
68
+ nodes: foundNodes,
69
+ });
70
+ };
71
+ fetchData().catch(console.error);
72
+ }, [djClient, namespace, namespaceHierarchy]);
73
+
74
+ const nodesList = state.nodes.map(node => (
75
+ <tr>
76
+ <td>
77
+ <a href={'/nodes/' + node.name} className="link-table">
78
+ {node.name}
79
+ </a>
80
+ <span
81
+ className="rounded-pill badge bg-secondary-soft"
82
+ style={{ marginLeft: '0.5rem' }}
83
+ >
84
+ {node.version}
85
+ </span>
86
+ </td>
87
+ <td>
88
+ <a href={'/nodes/' + node.name} className="link-table">
89
+ {node.type !== 'source' ? node.display_name : ''}
90
+ </a>
91
+ </td>
92
+ <td>
93
+ <span className={'node_type__' + node.type + ' badge node_type'}>
94
+ {node.type}
95
+ </span>
96
+ </td>
97
+ <td>
98
+ <NodeStatus node={node} />
99
+ </td>
100
+ <td>
101
+ <span className="status">{node.mode}</span>
102
+ </td>
103
+ <td>
104
+ <span className="status">
105
+ {new Date(node.updated_at).toLocaleString('en-us')}
106
+ </span>
107
+ </td>
108
+ <td>
109
+ <a href={`/nodes/${node?.name}/edit`} style={{ marginLeft: '0.5rem' }}>
110
+ <EditIcon />
111
+ </a>
112
+ <DeleteNode nodeName={node?.name} />
113
+ </td>
114
+ </tr>
115
+ ));
116
+
117
+ return (
118
+ <div className="mid">
119
+ <div className="card">
120
+ <div className="card-header">
121
+ <h2>Explore</h2>
122
+
123
+ <span className="menu-link">
124
+ <span className="menu-title">
125
+ <div className="dropdown">
126
+ <span className="add_node">+ Add Node</span>
127
+ <div className="dropdown-content">
128
+ <a href={`/create/source`}>
129
+ <div className="node_type__source node_type_creation_heading">
130
+ Register Table
131
+ </div>
132
+ </a>
133
+ <a href={`/create/transform/${namespace}`}>
134
+ <div className="node_type__transform node_type_creation_heading">
135
+ Transform
136
+ </div>
137
+ </a>
138
+ <a href={`/create/metric/${namespace}`}>
139
+ <div className="node_type__metric node_type_creation_heading">
140
+ Metric
141
+ </div>
142
+ </a>
143
+ <a href={`/create/dimension/${namespace}`}>
144
+ <div className="node_type__dimension node_type_creation_heading">
145
+ Dimension
146
+ </div>
147
+ </a>
148
+ <a href={`/create/tag`}>
149
+ <div className="entity__tag node_type_creation_heading">
150
+ Tag
151
+ </div>
152
+ </a>
153
+ </div>
154
+ </div>
155
+ </span>
156
+ </span>
157
+ <div className="table-responsive">
158
+ <div className={`sidebar`}>
159
+ <span
160
+ style={{
161
+ textTransform: 'uppercase',
162
+ fontSize: '0.8125rem',
163
+ fontWeight: '600',
164
+ color: '#95aac9',
165
+ padding: '1rem 1rem 1rem 0',
166
+ }}
167
+ >
168
+ Namespaces <AddNamespacePopover />
169
+ </span>
170
+ {namespaceHierarchy
171
+ ? namespaceHierarchy.map(child => (
172
+ <Explorer
173
+ item={child}
174
+ current={state.namespace}
175
+ defaultExpand={true}
176
+ />
177
+ ))
178
+ : null}
179
+ </div>
180
+ <table className="card-table table">
181
+ <thead>
182
+ <tr>
183
+ <th>Name</th>
184
+ <th>Display Name</th>
185
+ <th>Type</th>
186
+ <th>Status</th>
187
+ <th>Mode</th>
188
+ <th>Last Updated</th>
189
+ <th>Actions</th>
190
+ </tr>
191
+ </thead>
192
+ <tbody>{nodesList}</tbody>
193
+ </table>
194
+ </div>
195
+ </div>
196
+ </div>
197
+ </div>
198
+ );
199
+ }
@@ -0,0 +1,166 @@
1
+ import { useContext, useEffect, useRef, useState } from 'react';
2
+ import * as React from 'react';
3
+ import DJClientContext from '../../providers/djclient';
4
+ import { ErrorMessage, Field, 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 AddBackfillPopover({
10
+ node,
11
+ materialization,
12
+ onSubmit,
13
+ }) {
14
+ const djClient = useContext(DJClientContext).DataJunctionAPI;
15
+ const [popoverAnchor, setPopoverAnchor] = useState(false);
16
+ const ref = useRef(null);
17
+
18
+ useEffect(() => {
19
+ const handleClickOutside = event => {
20
+ if (ref.current && !ref.current.contains(event.target)) {
21
+ setPopoverAnchor(false);
22
+ }
23
+ };
24
+ document.addEventListener('click', handleClickOutside, true);
25
+ return () => {
26
+ document.removeEventListener('click', handleClickOutside, true);
27
+ };
28
+ }, [setPopoverAnchor]);
29
+
30
+ const partitionColumns = node.columns.filter(col => col.partition !== null);
31
+
32
+ const temporalPartitionColumns = partitionColumns.filter(
33
+ col => col.partition.type_ === 'temporal',
34
+ );
35
+
36
+ const initialValues = {
37
+ node: node.name,
38
+ materializationName: materialization.name,
39
+ partitionColumn:
40
+ temporalPartitionColumns.length > 0
41
+ ? temporalPartitionColumns[0].name
42
+ : '',
43
+ from: '',
44
+ to: '',
45
+ };
46
+
47
+ const savePartition = async (values, { setSubmitting, setStatus }) => {
48
+ setSubmitting(false);
49
+ const response = await djClient.runBackfill(
50
+ values.node,
51
+ values.materializationName,
52
+ values.partitionColumn,
53
+ values.from,
54
+ values.to,
55
+ );
56
+ if (response.status === 200 || response.status === 201) {
57
+ setStatus({ success: 'Saved!' });
58
+ } else {
59
+ setStatus({
60
+ failure: `${response.json.message}`,
61
+ });
62
+ }
63
+ onSubmit();
64
+ window.location.reload();
65
+ };
66
+
67
+ return (
68
+ <>
69
+ <button
70
+ className="edit_button"
71
+ aria-label="AddBackfill"
72
+ tabIndex="0"
73
+ onClick={() => {
74
+ setPopoverAnchor(!popoverAnchor);
75
+ }}
76
+ >
77
+ <span className="add_node">+ Add Backfill</span>
78
+ </button>
79
+ <div
80
+ className="fade modal-backdrop in"
81
+ style={{ display: popoverAnchor === false ? 'none' : 'block' }}
82
+ ></div>
83
+ <div
84
+ className="centerPopover"
85
+ role="dialog"
86
+ aria-label="client-code"
87
+ style={{
88
+ display: popoverAnchor === false ? 'none' : 'block',
89
+ width: '50%',
90
+ }}
91
+ ref={ref}
92
+ >
93
+ <Formik initialValues={initialValues} onSubmit={savePartition}>
94
+ {function Render({ isSubmitting, status, setFieldValue }) {
95
+ return (
96
+ <Form>
97
+ {displayMessageAfterSubmit(status)}
98
+ <h2>Run Backfill</h2>
99
+ <span data-testid="edit-partition">
100
+ <label htmlFor="engine" style={{ paddingBottom: '1rem' }}>
101
+ Engine
102
+ </label>
103
+ <Field as="select" name="engine" id="engine" disabled={true}>
104
+ <option value={materialization?.engine?.name}>
105
+ {materialization?.engine?.name}{' '}
106
+ {materialization?.engine?.version}
107
+ </option>
108
+ </Field>
109
+ </span>
110
+ <br />
111
+ <br />
112
+ <label htmlFor="partition" style={{ paddingBottom: '1rem' }}>
113
+ Partition Range
114
+ </label>
115
+ {node.columns
116
+ .filter(col => col.partition !== null)
117
+ .map(col => {
118
+ return (
119
+ <div
120
+ className="partition__full"
121
+ key={col.name}
122
+ style={{ width: '50%' }}
123
+ >
124
+ <div className="partition__header">
125
+ {col.display_name}
126
+ </div>
127
+ <div className="partition__body">
128
+ <span style={{ padding: '0.5rem' }}>From</span>{' '}
129
+ <Field
130
+ type="text"
131
+ name="from"
132
+ id={`${col.name}__from`}
133
+ placeholder="20230101"
134
+ default="20230101"
135
+ style={{ width: '7rem', paddingRight: '1rem' }}
136
+ />{' '}
137
+ <span style={{ padding: '0.5rem' }}>To</span>
138
+ <Field
139
+ type="text"
140
+ name="to"
141
+ id={`${col.name}__to`}
142
+ placeholder="20230102"
143
+ default="20230102"
144
+ style={{ width: '7rem' }}
145
+ />
146
+ </div>
147
+ </div>
148
+ );
149
+ })}
150
+ <br />
151
+ <button
152
+ className="add_node"
153
+ type="submit"
154
+ aria-label="SaveEditColumn"
155
+ aria-hidden="false"
156
+ >
157
+ Save
158
+ </button>
159
+ </Form>
160
+ );
161
+ }}
162
+ </Formik>
163
+ </div>
164
+ </>
165
+ );
166
+ }