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,233 @@
1
+ import { useEffect, useState } from 'react';
2
+ import ClientCodePopover from './ClientCodePopover';
3
+ import TableIcon from '../../icons/TableIcon';
4
+ import AddMaterializationPopover from './AddMaterializationPopover';
5
+ import * as React from 'react';
6
+ import AddBackfillPopover from './AddBackfillPopover';
7
+
8
+ const cronstrue = require('cronstrue');
9
+
10
+ export default function NodeMaterializationTab({ node, djClient }) {
11
+ const [materializations, setMaterializations] = useState([]);
12
+ useEffect(() => {
13
+ const fetchData = async () => {
14
+ if (node) {
15
+ const data = await djClient.materializations(node.name);
16
+ setMaterializations(data);
17
+ }
18
+ };
19
+ fetchData().catch(console.error);
20
+ }, [djClient, node]);
21
+ //
22
+ // const rangePartition = partition => {
23
+ // return (
24
+ // <div>
25
+ // <span className="badge partition_value">
26
+ // <span className="badge partition_value">{partition.range[0]}</span>to
27
+ // <span className="badge partition_value">{partition.range[1]}</span>
28
+ // </span>
29
+ // </div>
30
+ // );
31
+ // };
32
+
33
+ const partitionColumnsMap = node
34
+ ? Object.fromEntries(
35
+ node?.columns
36
+ .filter(col => col.partition !== null)
37
+ .map(col => [col.name, col.display_name]),
38
+ )
39
+ : {};
40
+ const cron = materialization => {
41
+ var parsedCron = '';
42
+ try {
43
+ parsedCron = cronstrue.toString(materialization.schedule);
44
+ } catch (e) {}
45
+ return parsedCron;
46
+ };
47
+
48
+ const materializationRows = materializations => {
49
+ return materializations.map(materialization => (
50
+ <tr key={materialization.name}>
51
+ <td className="text-start node_name">
52
+ <span className={`badge cron`}>{materialization.schedule}</span>
53
+ <div className={`cron-description`}>{cron(materialization)} </div>
54
+ </td>
55
+ <td>
56
+ {materialization.engine.name}
57
+ <br />
58
+ {materialization.engine.version}
59
+ <ClientCodePopover code={materialization.clientCode} />
60
+ </td>
61
+ <td>
62
+ {node.columns
63
+ .filter(col => col.partition !== null)
64
+ .map(column => {
65
+ return (
66
+ <div className="partition__full" key={column.name}>
67
+ <div className="partition__header">{column.display_name}</div>
68
+ <div className="partition__body">
69
+ <code>{column.name}</code>
70
+ <span className="badge partition_value">
71
+ {column.partition.type_}
72
+ </span>
73
+ </div>
74
+ </div>
75
+ );
76
+ })}
77
+ </td>
78
+ <td>
79
+ {materialization.output_tables.map(table => (
80
+ <div className={`table__full`} key={table}>
81
+ <div className="table__header">
82
+ <TableIcon />{' '}
83
+ <span className={`entity-info`}>
84
+ {table.split('.')[0] + '.' + table.split('.')[1]}
85
+ </span>
86
+ </div>
87
+ <div className={`table__body upstream_tables`}>
88
+ {table.split('.')[2]}
89
+ </div>
90
+ </div>
91
+ ))}
92
+ </td>
93
+ <td>
94
+ {materialization.backfills.map(backfill => (
95
+ <a href={backfill.urls[0]} className="partitionLink">
96
+ <div className="partition__full" key={backfill.spec.column_name}>
97
+ <div className="partition__header">
98
+ {partitionColumnsMap[backfill.spec.column_name]}
99
+ </div>
100
+ <div className="partition__body">
101
+ <span className="badge partition_value">
102
+ {backfill.spec.range[0]}
103
+ </span>
104
+ to
105
+ <span className="badge partition_value">
106
+ {backfill.spec.range[1]}
107
+ </span>
108
+ </div>
109
+ </div>
110
+ </a>
111
+ ))}
112
+ <AddBackfillPopover node={node} materialization={materialization} />
113
+ </td>
114
+ <td>
115
+ {materialization.urls.map((url, idx) => (
116
+ <a href={url} key={`url-${idx}`}>
117
+ [{idx + 1}]
118
+ </a>
119
+ ))}
120
+ </td>
121
+ </tr>
122
+ ));
123
+ };
124
+ return (
125
+ <>
126
+ <div className="table-vertical">
127
+ <div>
128
+ <h2>Materializations</h2>
129
+ <AddMaterializationPopover node={node} />
130
+ {materializations.length > 0 ? (
131
+ <table
132
+ className="card-inner-table table"
133
+ aria-label="Materializations"
134
+ aria-hidden="false"
135
+ >
136
+ <thead className="fs-7 fw-bold text-gray-400 border-bottom-0">
137
+ <tr>
138
+ <th className="text-start">Schedule</th>
139
+ <th>Engine</th>
140
+ <th>Partitions</th>
141
+ <th>Output Tables</th>
142
+ <th>Backfills</th>
143
+ <th>URLs</th>
144
+ </tr>
145
+ </thead>
146
+ <tbody>
147
+ {materializationRows(
148
+ materializations.filter(
149
+ materialization =>
150
+ !(
151
+ materialization.name === 'default' &&
152
+ node.type === 'cube'
153
+ ),
154
+ ),
155
+ )}
156
+ </tbody>
157
+ </table>
158
+ ) : (
159
+ <div className="message alert" style={{ marginTop: '10px' }}>
160
+ No materialization workflows configured for this node.
161
+ </div>
162
+ )}
163
+ </div>
164
+ <div>
165
+ <h2>Materialized Datasets</h2>
166
+ {node && node.availability !== null ? (
167
+ <table
168
+ className="card-inner-table table"
169
+ aria-label="Availability"
170
+ aria-hidden="false"
171
+ >
172
+ <thead className="fs-7 fw-bold text-gray-400 border-bottom-0">
173
+ <tr>
174
+ <th className="text-start">Catalog</th>
175
+ <th>Schema</th>
176
+ <th>Table</th>
177
+ <th>Valid Through</th>
178
+ <th>Partitions</th>
179
+ </tr>
180
+ </thead>
181
+ <tbody>
182
+ <tr>
183
+ <td>{node.availability.schema_}</td>
184
+ <td>
185
+ {
186
+ <div
187
+ className={`table__full`}
188
+ key={node.availability.table}
189
+ >
190
+ <div className="table__header">
191
+ <TableIcon />{' '}
192
+ <span className={`entity-info`}>
193
+ {node.availability.catalog +
194
+ '.' +
195
+ node.availability.schema_}
196
+ </span>
197
+ </div>
198
+ <div className={`table__body upstream_tables`}>
199
+ <a href={node.availability.url}>
200
+ {node.availability.table}
201
+ </a>
202
+ </div>
203
+ </div>
204
+ }
205
+ </td>
206
+ <td>{node.availability.valid_through_ts}</td>
207
+ <td>
208
+ <span
209
+ className={`badge partition_value`}
210
+ style={{ fontSize: '100%' }}
211
+ >
212
+ <span className={`badge partition_value_highlight`}>
213
+ {node.availability.min_temporal_partition}
214
+ </span>
215
+ to
216
+ <span className={`badge partition_value_highlight`}>
217
+ {node.availability.max_temporal_partition}
218
+ </span>
219
+ </span>
220
+ </td>
221
+ </tr>
222
+ </tbody>
223
+ </table>
224
+ ) : (
225
+ <div className="message alert" style={{ marginTop: '10px' }}>
226
+ No materialized datasets available for this node.
227
+ </div>
228
+ )}
229
+ </div>
230
+ </div>
231
+ </>
232
+ );
233
+ }
@@ -0,0 +1,82 @@
1
+ import { useContext, useEffect, useState } from 'react';
2
+ import Select from 'react-select';
3
+ import DJClientContext from '../../providers/djclient';
4
+ import { Light as SyntaxHighlighter } from 'react-syntax-highlighter';
5
+ import { foundation } from 'react-syntax-highlighter/src/styles/hljs';
6
+
7
+ const NodeSQLTab = djNode => {
8
+ const djClient = useContext(DJClientContext).DataJunctionAPI;
9
+ const [query, setQuery] = useState('');
10
+
11
+ const [selection, setSelection] = useState({
12
+ dimensions: [],
13
+ filters: [],
14
+ });
15
+
16
+ useEffect(() => {
17
+ const fetchData = async () => {
18
+ const query = await djClient.sql(djNode.djNode.name, selection);
19
+ setQuery(query.sql);
20
+ };
21
+ fetchData().catch(console.error);
22
+ }, [djClient, djNode.djNode.name, selection]);
23
+ const dimensionsList = djNode.djNode.dimensions
24
+ ? djNode.djNode.dimensions.map(dim => ({
25
+ value: dim.name,
26
+ label: dim.name + ` (${dim.type})`,
27
+ }))
28
+ : [''];
29
+
30
+ const handleSubmit = event => {
31
+ event.preventDefault();
32
+ };
33
+
34
+ const handleChange = event => {
35
+ setSelection({ filters: [], dimensions: event.map(dim => dim.value) });
36
+ };
37
+
38
+ return (
39
+ <form
40
+ id="retrieve-sql"
41
+ name="retrieve-sql"
42
+ onSubmit={handleSubmit.bind(this)}
43
+ >
44
+ <div>
45
+ <h4>Group By</h4>
46
+ <Select
47
+ name="dimensions"
48
+ options={dimensionsList}
49
+ isMulti
50
+ isClearable
51
+ onChange={handleChange}
52
+ />
53
+ {/*<h4>Filters</h4>*/}
54
+ {/*<Select*/}
55
+ {/* name="filter_name"*/}
56
+ {/* options={dimensionsList}*/}
57
+ {/* className="filters_attribute"*/}
58
+ {/*/>*/}
59
+ {/*<Select*/}
60
+ {/* name="filter_operator"*/}
61
+ {/* options={options}*/}
62
+ {/* className="filters_attribute"*/}
63
+ {/*/>*/}
64
+ {/*<textarea name="filter_value" className="filters_attribute" />*/}
65
+
66
+ <div
67
+ style={{
68
+ width: window.innerWidth * 0.8,
69
+ marginTop: '2rem',
70
+ }}
71
+ >
72
+ <h6 className="mb-0 w-100">Query</h6>
73
+ <SyntaxHighlighter language="sql" style={foundation}>
74
+ {query}
75
+ </SyntaxHighlighter>
76
+ </div>
77
+ </div>
78
+ </form>
79
+ );
80
+ };
81
+
82
+ export default NodeSQLTab;
@@ -0,0 +1,28 @@
1
+ import { Component } from 'react';
2
+ import ValidIcon from '../../icons/ValidIcon';
3
+ import InvalidIcon from '../../icons/InvalidIcon';
4
+
5
+ export default class NodeStatus extends Component {
6
+ render() {
7
+ const { node } = this.props;
8
+ return (
9
+ <>
10
+ {node?.status === 'valid' ? (
11
+ <span
12
+ className="status__valid status"
13
+ style={{ alignContent: 'center' }}
14
+ >
15
+ <ValidIcon />
16
+ </span>
17
+ ) : (
18
+ <span
19
+ className="status__invalid status"
20
+ style={{ alignContent: 'center' }}
21
+ >
22
+ <InvalidIcon />
23
+ </span>
24
+ )}
25
+ </>
26
+ );
27
+ }
28
+ }
@@ -0,0 +1,42 @@
1
+ import { useEffect, useState } from 'react';
2
+ import * as React from 'react';
3
+
4
+ export default function NodesWithDimension({ node, djClient }) {
5
+ const [availableNodes, setAvailableNodes] = useState([]);
6
+
7
+ useEffect(() => {
8
+ const fetchData = async () => {
9
+ const data = await djClient.nodesWithDimension(node.name);
10
+ setAvailableNodes(data);
11
+ };
12
+ fetchData().catch(console.error);
13
+ }, [djClient, node]);
14
+ return (
15
+ <div className="table-responsive">
16
+ <table className="card-inner-table table">
17
+ <thead className="fs-7 fw-bold text-gray-400 border-bottom-0">
18
+ <tr>
19
+ <th className="text-start">Name</th>
20
+ <th>Type</th>
21
+ </tr>
22
+ </thead>
23
+ <tbody>
24
+ {availableNodes.map(node => (
25
+ <tr>
26
+ <td>
27
+ <a href={`/nodes/${node.name}`}>{node.display_name}</a>
28
+ </td>
29
+ <td>
30
+ <span
31
+ className={'node_type__' + node.type + ' badge node_type'}
32
+ >
33
+ {node.type}
34
+ </span>
35
+ </td>
36
+ </tr>
37
+ ))}
38
+ </tbody>
39
+ </table>
40
+ </div>
41
+ );
42
+ }
@@ -0,0 +1,153 @@
1
+ import { useContext, useEffect, useRef, useState } from 'react';
2
+ import * as React from 'react';
3
+ import DJClientContext from '../../providers/djclient';
4
+ import { 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 PartitionColumnPopover({ column, node, onSubmit }) {
10
+ const djClient = useContext(DJClientContext).DataJunctionAPI;
11
+ const [popoverAnchor, setPopoverAnchor] = useState(false);
12
+ const ref = useRef(null);
13
+
14
+ useEffect(() => {
15
+ const handleClickOutside = event => {
16
+ if (ref.current && !ref.current.contains(event.target)) {
17
+ setPopoverAnchor(false);
18
+ }
19
+ };
20
+ document.addEventListener('click', handleClickOutside, true);
21
+ return () => {
22
+ document.removeEventListener('click', handleClickOutside, true);
23
+ };
24
+ }, [setPopoverAnchor]);
25
+
26
+ const savePartition = async (
27
+ { node, column, partition_type, format, granularity },
28
+ { setSubmitting, setStatus },
29
+ ) => {
30
+ setSubmitting(false);
31
+ const response = await djClient.setPartition(
32
+ node,
33
+ column,
34
+ partition_type,
35
+ format,
36
+ granularity,
37
+ );
38
+ if (response.status === 200 || response.status === 201) {
39
+ setStatus({ success: 'Saved!' });
40
+ } else {
41
+ setStatus({
42
+ failure: `${response.json.message}`,
43
+ });
44
+ }
45
+ onSubmit();
46
+ // window.location.reload();
47
+ };
48
+
49
+ return (
50
+ <>
51
+ <button
52
+ className="edit_button"
53
+ aria-label="PartitionColumn"
54
+ tabIndex="0"
55
+ onClick={() => {
56
+ setPopoverAnchor(!popoverAnchor);
57
+ }}
58
+ >
59
+ <EditIcon />
60
+ </button>
61
+ <div
62
+ className="popover"
63
+ role="dialog"
64
+ aria-label="client-code"
65
+ style={{ display: popoverAnchor === false ? 'none' : 'block' }}
66
+ ref={ref}
67
+ >
68
+ <Formik
69
+ initialValues={{
70
+ column: column.name,
71
+ node: node.name,
72
+ partition_type: '',
73
+ format: 'yyyyMMdd',
74
+ granularity: 'day',
75
+ }}
76
+ onSubmit={savePartition}
77
+ >
78
+ {function Render({ values, isSubmitting, status, setFieldValue }) {
79
+ return (
80
+ <Form>
81
+ {displayMessageAfterSubmit(status)}
82
+ <span data-testid="edit-partition">
83
+ <label htmlFor="react-select-3-input">Partition Type</label>
84
+ <Field
85
+ as="select"
86
+ name="partition_type"
87
+ id="partitionType"
88
+ placeholder="Partition Type"
89
+ >
90
+ <option value=""></option>
91
+ <option value="temporal">Temporal</option>
92
+ <option value="categorical">Categorical</option>
93
+ </Field>
94
+ </span>
95
+ <input
96
+ hidden={true}
97
+ name="column"
98
+ value={column.name}
99
+ readOnly={true}
100
+ />
101
+ <input
102
+ hidden={true}
103
+ name="node"
104
+ value={node.name}
105
+ readOnly={true}
106
+ />
107
+ <br />
108
+ <br />
109
+ {values.partition_type === 'temporal' ? (
110
+ <>
111
+ <label htmlFor="react-select-3-input">
112
+ Partition Format
113
+ </label>
114
+ <Field
115
+ type="text"
116
+ name="format"
117
+ id="partitionFormat"
118
+ placeholder="Optional temporal partition format (ex: yyyyMMdd)"
119
+ />
120
+ <br />
121
+ <br />
122
+ <label htmlFor="react-select-3-input">
123
+ Partition Granularity
124
+ </label>
125
+ <Field
126
+ as="select"
127
+ name="granularity"
128
+ id="partitionGranularity"
129
+ placeholder="Granularity"
130
+ >
131
+ <option value="day">Day</option>
132
+ <option value="hour">Hour</option>
133
+ </Field>
134
+ </>
135
+ ) : (
136
+ ''
137
+ )}
138
+ <button
139
+ className="add_node"
140
+ type="submit"
141
+ aria-label="SaveEditColumn"
142
+ aria-hidden="false"
143
+ >
144
+ Save
145
+ </button>
146
+ </Form>
147
+ );
148
+ }}
149
+ </Formik>
150
+ </div>
151
+ </>
152
+ );
153
+ }
@@ -0,0 +1,47 @@
1
+ import React from 'react';
2
+ import { render, fireEvent, waitFor, screen } from '@testing-library/react';
3
+ import EditColumnPopover from '../EditColumnPopover';
4
+ import DJClientContext from '../../../providers/djclient';
5
+ import AddBackfillPopover from '../AddBackfillPopover';
6
+ import { mocks } from '../../../../mocks/mockNodes';
7
+
8
+ const mockDjClient = {
9
+ DataJunctionAPI: {
10
+ runBackfill: jest.fn(),
11
+ },
12
+ };
13
+
14
+ describe('<AddBackfillPopover />', () => {
15
+ it('renders correctly and handles form submission', async () => {
16
+ // Mock onSubmit function
17
+ const onSubmitMock = jest.fn();
18
+
19
+ mockDjClient.DataJunctionAPI.runBackfill.mockReturnValue({
20
+ status: 201,
21
+ json: { message: '' },
22
+ });
23
+
24
+ // Render the component
25
+ const { getByLabelText, getByText } = render(
26
+ <DJClientContext.Provider value={mockDjClient}>
27
+ <AddBackfillPopover
28
+ node={mocks.mockMetricNode}
29
+ materialization={mocks.nodeMaterializations}
30
+ onSubmit={onSubmitMock}
31
+ />
32
+ </DJClientContext.Provider>,
33
+ );
34
+
35
+ // Open the popover
36
+ fireEvent.click(getByLabelText('AddBackfill'));
37
+
38
+ fireEvent.click(getByText('Save'));
39
+ getByText('Save').click();
40
+
41
+ // Expect setAttributes to be called
42
+ await waitFor(() => {
43
+ expect(mockDjClient.DataJunctionAPI.runBackfill).toHaveBeenCalled();
44
+ expect(getByText('Saved!')).toBeInTheDocument();
45
+ });
46
+ });
47
+ });
@@ -0,0 +1,49 @@
1
+ import React from 'react';
2
+ import { render, fireEvent, screen, waitFor } from '@testing-library/react';
3
+ import ClientCodePopover from '../ClientCodePopover';
4
+ import userEvent from '@testing-library/user-event';
5
+
6
+ describe('<ClientCodePopover />', () => {
7
+ const defaultProps = {
8
+ code: "print('Hello, World!')",
9
+ };
10
+
11
+ it('toggles the code popover visibility when the button is clicked', async () => {
12
+ render(<ClientCodePopover {...defaultProps} />);
13
+
14
+ const button = screen.getByRole('button', 'code-button');
15
+
16
+ // Initially, the popover should be hidden
17
+ expect(screen.getByRole('dialog', { hidden: true })).toHaveStyle(
18
+ 'display: none',
19
+ );
20
+
21
+ // Clicking the button should display the popover
22
+ fireEvent.click(button);
23
+ expect(screen.getByRole('dialog', { hidden: true })).not.toHaveStyle(
24
+ 'display: none',
25
+ );
26
+
27
+ // Clicking the button again should hide the popover
28
+ fireEvent.click(button);
29
+ expect(screen.getByRole('dialog', { hidden: true })).toHaveStyle(
30
+ 'display: none',
31
+ );
32
+
33
+ // Trigger onClose by pressing <escape>
34
+ userEvent.keyboard('{Escape}');
35
+ // fireEvent.click(screen.getByTestId('body').firstChild());
36
+ await waitFor(() => {
37
+ expect(screen.getByRole('dialog', { hidden: true })).toHaveStyle(
38
+ 'display: none',
39
+ );
40
+ });
41
+ });
42
+
43
+ it('renders the provided code within the SyntaxHighlighter', () => {
44
+ render(<ClientCodePopover {...defaultProps} />);
45
+ expect(screen.getByRole('dialog', { hidden: true })).toHaveTextContent(
46
+ defaultProps.code,
47
+ );
48
+ });
49
+ });