datajunction-ui 0.0.1-a1 → 0.0.1-a101
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.
- package/Makefile +7 -1
- package/package.json +18 -7
- package/public/index.html +1 -1
- package/src/app/components/AddNodeDropdown.jsx +44 -0
- package/src/app/components/ListGroupItem.jsx +2 -1
- package/src/app/components/NodeListActions.jsx +69 -0
- package/src/app/components/NodeMaterializationDelete.jsx +80 -0
- package/src/app/components/QueryInfo.jsx +96 -1
- package/src/app/components/Search.jsx +94 -0
- package/src/app/components/__tests__/NodeListActions.test.jsx +94 -0
- package/src/app/components/__tests__/Search.test.jsx +63 -0
- package/src/app/components/__tests__/__snapshots__/ListGroupItem.test.tsx.snap +5 -3
- package/src/app/components/djgraph/Collapse.jsx +3 -2
- package/src/app/components/djgraph/DJNode.jsx +1 -1
- package/src/app/components/djgraph/DJNodeColumns.jsx +5 -1
- package/src/app/components/djgraph/LayoutFlow.jsx +5 -3
- package/src/app/components/forms/Action.jsx +8 -0
- package/src/app/components/forms/NodeNameField.jsx +64 -0
- package/src/app/components/search.css +17 -0
- package/src/app/icons/AddItemIcon.jsx +16 -0
- package/src/app/icons/CommitIcon.jsx +45 -0
- package/src/app/icons/DiffIcon.jsx +63 -0
- package/src/app/icons/EyeIcon.jsx +20 -0
- package/src/app/icons/FilterIcon.jsx +7 -0
- package/src/app/icons/JupyterExportIcon.jsx +25 -0
- package/src/app/icons/LoadingIcon.jsx +10 -10
- package/src/app/icons/PythonIcon.jsx +6 -44
- package/src/app/index.tsx +24 -0
- package/src/app/pages/AddEditNodePage/AlertMessage.jsx +10 -0
- package/src/app/pages/AddEditNodePage/ColumnsSelect.jsx +84 -0
- package/src/app/pages/AddEditNodePage/DescriptionField.jsx +17 -0
- package/src/app/pages/AddEditNodePage/DisplayNameField.jsx +16 -0
- package/src/app/pages/AddEditNodePage/FormikSelect.jsx +5 -0
- package/src/app/pages/AddEditNodePage/FullNameField.jsx +3 -2
- package/src/app/pages/AddEditNodePage/Loadable.jsx +6 -2
- package/src/app/pages/AddEditNodePage/MetricMetadataFields.jsx +75 -0
- package/src/app/pages/AddEditNodePage/MetricQueryField.jsx +71 -0
- package/src/app/pages/AddEditNodePage/NamespaceField.jsx +40 -0
- package/src/app/pages/AddEditNodePage/NodeModeField.jsx +14 -0
- package/src/app/pages/AddEditNodePage/NodeQueryField.jsx +8 -3
- package/src/app/pages/AddEditNodePage/RequiredDimensionsSelect.jsx +54 -0
- package/src/app/pages/AddEditNodePage/TagsField.jsx +47 -0
- package/src/app/pages/AddEditNodePage/UpstreamNodeField.jsx +49 -0
- package/src/app/pages/AddEditNodePage/__tests__/AddEditNodePageFormFailed.test.jsx +15 -9
- package/src/app/pages/AddEditNodePage/__tests__/AddEditNodePageFormSuccess.test.jsx +167 -24
- package/src/app/pages/AddEditNodePage/__tests__/index.test.jsx +55 -25
- package/src/app/pages/AddEditNodePage/index.jsx +275 -194
- package/src/app/pages/CubeBuilderPage/DimensionsSelect.jsx +154 -0
- package/src/app/pages/CubeBuilderPage/Loadable.jsx +16 -0
- package/src/app/pages/CubeBuilderPage/MetricsSelect.jsx +77 -0
- package/src/app/pages/CubeBuilderPage/__tests__/index.test.jsx +405 -0
- package/src/app/pages/CubeBuilderPage/index.jsx +267 -0
- package/src/app/pages/NamespacePage/AddNamespacePopover.jsx +5 -5
- package/src/app/pages/NamespacePage/Explorer.jsx +6 -2
- package/src/app/pages/NamespacePage/FieldControl.jsx +21 -0
- package/src/app/pages/NamespacePage/NodeTypeSelect.jsx +30 -0
- package/src/app/pages/NamespacePage/TagSelect.jsx +44 -0
- package/src/app/pages/NamespacePage/UserSelect.jsx +47 -0
- package/src/app/pages/NamespacePage/__tests__/index.test.jsx +98 -19
- package/src/app/pages/NamespacePage/index.jsx +272 -89
- package/src/app/pages/NodePage/AddBackfillPopover.jsx +60 -61
- package/src/app/pages/NodePage/AddMaterializationPopover.jsx +104 -51
- package/src/app/pages/NodePage/ClientCodePopover.jsx +73 -25
- package/src/app/pages/NodePage/DimensionFilter.jsx +86 -0
- package/src/app/pages/NodePage/EditColumnDescriptionPopover.jsx +116 -0
- package/src/app/pages/NodePage/LinkDimensionPopover.jsx +38 -23
- package/src/app/pages/NodePage/MaterializationConfigField.jsx +60 -0
- package/src/app/pages/NodePage/NodeColumnTab.jsx +183 -113
- package/src/app/pages/NodePage/NodeDependenciesTab.jsx +153 -0
- package/src/app/pages/NodePage/NodeGraphTab.jsx +56 -29
- package/src/app/pages/NodePage/NodeHistory.jsx +165 -161
- package/src/app/pages/NodePage/NodeInfoTab.jsx +148 -14
- package/src/app/pages/NodePage/NodeMaterializationTab.jsx +201 -104
- package/src/app/pages/NodePage/NodeStatus.jsx +96 -21
- package/src/app/pages/NodePage/NodeValidateTab.jsx +367 -0
- package/src/app/pages/NodePage/NotebookDownload.jsx +36 -0
- package/src/app/pages/NodePage/PartitionColumnPopover.jsx +3 -5
- package/src/app/pages/NodePage/PartitionValueForm.jsx +60 -0
- package/src/app/pages/NodePage/RevisionDiff.jsx +209 -0
- package/src/app/pages/NodePage/WatchNodeButton.jsx +226 -0
- package/src/app/pages/NodePage/__tests__/AddBackfillPopover.test.jsx +13 -4
- package/src/app/pages/NodePage/__tests__/AddMaterializationPopover.test.jsx +87 -0
- package/src/app/pages/NodePage/__tests__/DimensionFilter.test.jsx +74 -0
- package/src/app/pages/NodePage/__tests__/EditColumnDescriptionPopover.test.jsx +149 -0
- package/src/app/pages/NodePage/__tests__/LinkDimensionPopover.test.jsx +10 -14
- package/src/app/pages/NodePage/__tests__/NodeColumnTab.test.jsx +166 -0
- package/src/app/pages/NodePage/__tests__/NodeDependenciesTab.test.jsx +151 -0
- package/src/app/pages/NodePage/__tests__/NodeGraphTab.test.jsx +6 -2
- package/src/app/pages/NodePage/__tests__/NodeLineageTab.test.jsx +3 -2
- package/src/app/pages/NodePage/__tests__/NodeMaterializationTab.test.jsx +148 -0
- package/src/app/pages/NodePage/__tests__/NodePage.test.jsx +159 -57
- package/src/app/pages/NodePage/__tests__/RevisionDiff.test.jsx +164 -0
- package/src/app/pages/NodePage/__tests__/__snapshots__/NodePage.test.jsx.snap +2 -386
- package/src/app/pages/NodePage/index.jsx +94 -57
- package/src/app/pages/Root/__tests__/index.test.jsx +3 -1
- package/src/app/pages/Root/index.tsx +62 -12
- package/src/app/services/DJService.js +587 -55
- package/src/app/services/__tests__/DJService.test.jsx +382 -45
- package/src/index.tsx +1 -0
- package/src/mocks/mockNodes.jsx +265 -227
- package/src/styles/dag.css +4 -2
- package/src/styles/index.css +474 -10
- package/src/styles/loading.css +1 -1
- package/src/styles/node-creation.scss +84 -5
- package/src/styles/node-list.css +4 -0
- package/src/styles/sorted-table.css +15 -0
- package/src/app/components/DeleteNode.jsx +0 -55
- package/src/app/components/__tests__/DeleteNode.test.jsx +0 -53
- package/src/app/pages/NodePage/NodeSQLTab.jsx +0 -82
- package/src/app/pages/NodePage/__tests__/ClientCodePopover.test.jsx +0 -49
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
import React, { useEffect, useState } from 'react';
|
|
2
|
+
import Select from 'react-select';
|
|
3
|
+
import { Light as SyntaxHighlighter } from 'react-syntax-highlighter';
|
|
4
|
+
import { foundation } from 'react-syntax-highlighter/src/styles/hljs';
|
|
5
|
+
import { labelize } from '../../../utils/form';
|
|
6
|
+
import { Form, Formik } from 'formik';
|
|
7
|
+
import DimensionFilter from './DimensionFilter';
|
|
8
|
+
import QueryInfo from '../../components/QueryInfo';
|
|
9
|
+
import LoadingIcon from '../../icons/LoadingIcon';
|
|
10
|
+
|
|
11
|
+
export default function NodeValidateTab({ node, djClient }) {
|
|
12
|
+
const [query, setQuery] = useState('');
|
|
13
|
+
const [lookup, setLookup] = useState([]);
|
|
14
|
+
const [running, setRunning] = useState(false);
|
|
15
|
+
|
|
16
|
+
// These are a list of dimensions that are available for this node
|
|
17
|
+
const [dimensions, setDimensions] = useState([]);
|
|
18
|
+
|
|
19
|
+
// A separate structure used to store the selected dimensions to filter by and their values
|
|
20
|
+
const [selectedFilters, setSelectedFilters] = useState({});
|
|
21
|
+
|
|
22
|
+
// The set of dimensions and filters to pass to the /sql or /data endpoints for the node
|
|
23
|
+
const [selection, setSelection] = useState({
|
|
24
|
+
dimensions: [],
|
|
25
|
+
filters: [],
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
// Any query result info retrieved when a node query is run
|
|
29
|
+
const [queryInfo, setQueryInfo] = useState(null);
|
|
30
|
+
|
|
31
|
+
const initialValues = {};
|
|
32
|
+
|
|
33
|
+
const [state, setState] = useState({
|
|
34
|
+
selectedTab: 'results',
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
const switchTab = tabName => {
|
|
38
|
+
setState({ selectedTab: tabName });
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
useEffect(() => {
|
|
42
|
+
const fetchData = async () => {
|
|
43
|
+
if (node) {
|
|
44
|
+
// Find all the dimensions for this node
|
|
45
|
+
const dimensions = await djClient.nodeDimensions(node.name);
|
|
46
|
+
|
|
47
|
+
// Create a dimensions lookup object
|
|
48
|
+
const lookup = {};
|
|
49
|
+
dimensions.forEach(dimension => {
|
|
50
|
+
lookup[dimension.name] = dimension;
|
|
51
|
+
});
|
|
52
|
+
setLookup(lookup);
|
|
53
|
+
|
|
54
|
+
// Group the dimensions by dimension node
|
|
55
|
+
const grouped = Object.entries(
|
|
56
|
+
dimensions.reduce((group, dimension) => {
|
|
57
|
+
group[dimension.node_name + dimension.path] =
|
|
58
|
+
group[dimension.node_name + dimension.path] ?? [];
|
|
59
|
+
group[dimension.node_name + dimension.path].push(dimension);
|
|
60
|
+
return group;
|
|
61
|
+
}, {}),
|
|
62
|
+
);
|
|
63
|
+
setDimensions(grouped);
|
|
64
|
+
|
|
65
|
+
// Build the query for this node based on the user-provided dimensions and filters
|
|
66
|
+
const query = await djClient.sql(node.name, selection);
|
|
67
|
+
setQuery(query.sql);
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
fetchData().catch(console.error);
|
|
71
|
+
}, [djClient, node, selection]);
|
|
72
|
+
|
|
73
|
+
const dimensionsList = dimensions.flatMap(grouping => {
|
|
74
|
+
const dimensionsInGroup = grouping[1];
|
|
75
|
+
return dimensionsInGroup
|
|
76
|
+
.filter(dim => dim.properties.includes('primary_key') === true)
|
|
77
|
+
.map(dim => {
|
|
78
|
+
return {
|
|
79
|
+
value: dim.name,
|
|
80
|
+
label: (
|
|
81
|
+
<span>
|
|
82
|
+
{labelize(dim.name.split('.').slice(-1)[0])}{' '}
|
|
83
|
+
<small>{dim.name}</small>
|
|
84
|
+
</span>
|
|
85
|
+
),
|
|
86
|
+
};
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
// Run the query and use SSE to stream the status of the query execution results
|
|
91
|
+
const runQuery = async (values, setStatus, setSubmitting) => {
|
|
92
|
+
setRunning(true);
|
|
93
|
+
const sse = await djClient.streamNodeData(node?.name, selection);
|
|
94
|
+
sse.onmessage = e => {
|
|
95
|
+
const messageData = JSON.parse(JSON.parse(e.data));
|
|
96
|
+
if (
|
|
97
|
+
messageData !== null &&
|
|
98
|
+
messageData?.state !== 'FINISHED' &&
|
|
99
|
+
messageData?.state !== 'CANCELED' &&
|
|
100
|
+
messageData?.state !== 'FAILED'
|
|
101
|
+
) {
|
|
102
|
+
setRunning(false);
|
|
103
|
+
}
|
|
104
|
+
if (messageData.results && messageData.results?.length > 0) {
|
|
105
|
+
messageData.numRows = messageData.results?.length
|
|
106
|
+
? messageData.results[0].rows.length
|
|
107
|
+
: [];
|
|
108
|
+
switchTab('results');
|
|
109
|
+
setRunning(false);
|
|
110
|
+
} else {
|
|
111
|
+
switchTab('info');
|
|
112
|
+
}
|
|
113
|
+
setQueryInfo(messageData);
|
|
114
|
+
};
|
|
115
|
+
sse.onerror = () => sse.close();
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
// Handle form submission (runs the query)
|
|
119
|
+
const handleSubmit = async (values, { setSubmitting, setStatus }) => {
|
|
120
|
+
await runQuery(values, setStatus, setSubmitting);
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
// Handle when filter values are updated. This is available for all nodes.
|
|
124
|
+
const handleAddFilters = event => {
|
|
125
|
+
const updatedFilters = selectedFilters;
|
|
126
|
+
if (event.dimension in updatedFilters) {
|
|
127
|
+
updatedFilters[event.dimension].operator = event.operator;
|
|
128
|
+
updatedFilters[event.dimension].values = event.values;
|
|
129
|
+
} else {
|
|
130
|
+
updatedFilters[event.dimension] = {
|
|
131
|
+
operator: event.operator,
|
|
132
|
+
values: event.values,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
setSelectedFilters(updatedFilters);
|
|
136
|
+
const updatedDimensions = selection.dimensions.concat([event.dimension]);
|
|
137
|
+
setSelection({
|
|
138
|
+
filters: Object.entries(updatedFilters).map(obj =>
|
|
139
|
+
obj[1].values
|
|
140
|
+
? `${obj[0]} IN (${obj[1].values
|
|
141
|
+
.map(val =>
|
|
142
|
+
['int', 'bigint', 'float', 'double', 'long'].includes(
|
|
143
|
+
lookup[obj[0]].type,
|
|
144
|
+
)
|
|
145
|
+
? val.value
|
|
146
|
+
: "'" + val.value + "'",
|
|
147
|
+
)
|
|
148
|
+
.join(', ')})`
|
|
149
|
+
: '',
|
|
150
|
+
),
|
|
151
|
+
dimensions: updatedDimensions,
|
|
152
|
+
});
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
// Handle when one or more dimensions are selected from the dropdown
|
|
156
|
+
// Note that this is only available to metrics
|
|
157
|
+
const handleAddDimensions = event => {
|
|
158
|
+
const updatedDimensions = event.map(
|
|
159
|
+
selectedDimension => selectedDimension.value,
|
|
160
|
+
);
|
|
161
|
+
setSelection({
|
|
162
|
+
filters: selection.filters,
|
|
163
|
+
dimensions: updatedDimensions,
|
|
164
|
+
});
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
const filters = dimensions.map(grouping => {
|
|
168
|
+
const dimensionsInGroup = grouping[1];
|
|
169
|
+
const dimensionGroupOptions = dimensionsInGroup
|
|
170
|
+
.filter(dim => dim.properties.includes('primary_key') === true)
|
|
171
|
+
.map(dim => {
|
|
172
|
+
return {
|
|
173
|
+
value: dim.name,
|
|
174
|
+
label: labelize(dim.name.split('.').slice(-1)[0]),
|
|
175
|
+
metadata: dim,
|
|
176
|
+
};
|
|
177
|
+
});
|
|
178
|
+
return (
|
|
179
|
+
<>
|
|
180
|
+
<div className="dimensionsList">
|
|
181
|
+
{dimensionGroupOptions.map(dimension => {
|
|
182
|
+
return (
|
|
183
|
+
<DimensionFilter
|
|
184
|
+
dimension={dimension}
|
|
185
|
+
onChange={handleAddFilters}
|
|
186
|
+
/>
|
|
187
|
+
);
|
|
188
|
+
})}
|
|
189
|
+
</div>
|
|
190
|
+
</>
|
|
191
|
+
);
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
return (
|
|
195
|
+
<Formik initialValues={initialValues} onSubmit={handleSubmit}>
|
|
196
|
+
{function Render({ isSubmitting, status, setFieldValue }) {
|
|
197
|
+
return (
|
|
198
|
+
<Form>
|
|
199
|
+
<div className={'queryrunner'}>
|
|
200
|
+
<div className="queryrunner-filters left">
|
|
201
|
+
{node?.type === 'metric' ? (
|
|
202
|
+
<>
|
|
203
|
+
<span>
|
|
204
|
+
<label>Group By</label>
|
|
205
|
+
</span>
|
|
206
|
+
<Select
|
|
207
|
+
name="dimensions"
|
|
208
|
+
options={dimensionsList}
|
|
209
|
+
isMulti
|
|
210
|
+
isClearable
|
|
211
|
+
onChange={handleAddDimensions}
|
|
212
|
+
/>
|
|
213
|
+
<br />
|
|
214
|
+
</>
|
|
215
|
+
) : null}
|
|
216
|
+
<span>
|
|
217
|
+
<label>Add Filters</label>
|
|
218
|
+
</span>
|
|
219
|
+
{filters}
|
|
220
|
+
</div>
|
|
221
|
+
<div className={'righttop'}>
|
|
222
|
+
<label>Generated Query</label>
|
|
223
|
+
<div
|
|
224
|
+
style={{
|
|
225
|
+
height: '200px',
|
|
226
|
+
width: '80%',
|
|
227
|
+
overflow: 'scroll',
|
|
228
|
+
borderRadius: '0',
|
|
229
|
+
border: '1px solid #ccc',
|
|
230
|
+
}}
|
|
231
|
+
className="queryrunner-query"
|
|
232
|
+
>
|
|
233
|
+
<SyntaxHighlighter
|
|
234
|
+
language="sql"
|
|
235
|
+
style={foundation}
|
|
236
|
+
wrapLines={true}
|
|
237
|
+
>
|
|
238
|
+
{query}
|
|
239
|
+
</SyntaxHighlighter>
|
|
240
|
+
</div>
|
|
241
|
+
<button
|
|
242
|
+
type="submit"
|
|
243
|
+
disabled={
|
|
244
|
+
running ||
|
|
245
|
+
(queryInfo !== null &&
|
|
246
|
+
queryInfo?.state !== 'FINISHED' &&
|
|
247
|
+
queryInfo?.state !== 'CANCELED' &&
|
|
248
|
+
queryInfo?.state !== 'FAILED')
|
|
249
|
+
}
|
|
250
|
+
className="button-3 execute-button"
|
|
251
|
+
style={{ marginTop: '1rem' }}
|
|
252
|
+
>
|
|
253
|
+
{isSubmitting || running === true ? <LoadingIcon /> : '► Run'}
|
|
254
|
+
</button>
|
|
255
|
+
</div>
|
|
256
|
+
<div
|
|
257
|
+
style={{
|
|
258
|
+
width: window.innerWidth * 0.8,
|
|
259
|
+
marginTop: '1rem',
|
|
260
|
+
marginLeft: '0.5rem',
|
|
261
|
+
height: 'calc(70% - 5.5px)',
|
|
262
|
+
}}
|
|
263
|
+
className={'rightbottom'}
|
|
264
|
+
>
|
|
265
|
+
<div
|
|
266
|
+
className={'align-items-center row'}
|
|
267
|
+
style={{
|
|
268
|
+
borderBottom: '1px solid #dddddd',
|
|
269
|
+
width: '86%',
|
|
270
|
+
}}
|
|
271
|
+
>
|
|
272
|
+
<div
|
|
273
|
+
className={
|
|
274
|
+
'tab-item' +
|
|
275
|
+
(state.selectedTab === 'results' ? ' active' : '')
|
|
276
|
+
}
|
|
277
|
+
onClick={_ => switchTab('results')}
|
|
278
|
+
>
|
|
279
|
+
Results
|
|
280
|
+
</div>
|
|
281
|
+
<div
|
|
282
|
+
className={
|
|
283
|
+
'tab-item' +
|
|
284
|
+
(state.selectedTab === 'info' ? ' active' : '')
|
|
285
|
+
}
|
|
286
|
+
aria-label={'QueryInfo'}
|
|
287
|
+
role={'button'}
|
|
288
|
+
onClick={_ => switchTab('info')}
|
|
289
|
+
>
|
|
290
|
+
Info
|
|
291
|
+
</div>
|
|
292
|
+
</div>
|
|
293
|
+
{state.selectedTab === 'info' ? (
|
|
294
|
+
<div>
|
|
295
|
+
{queryInfo && queryInfo.id ? (
|
|
296
|
+
<QueryInfo {...queryInfo} isList={true} />
|
|
297
|
+
) : (
|
|
298
|
+
<></>
|
|
299
|
+
)}
|
|
300
|
+
</div>
|
|
301
|
+
) : null}
|
|
302
|
+
{state.selectedTab === 'results' ? (
|
|
303
|
+
<div>
|
|
304
|
+
{queryInfo !== null && queryInfo.state !== 'FINISHED' ? (
|
|
305
|
+
<div style={{ padding: '2rem' }}>
|
|
306
|
+
The query has status {queryInfo.state}! Check the INFO
|
|
307
|
+
tab for more details.
|
|
308
|
+
</div>
|
|
309
|
+
) : queryInfo !== null &&
|
|
310
|
+
queryInfo.results !== null &&
|
|
311
|
+
queryInfo.results.length === 0 ? (
|
|
312
|
+
<div style={{ padding: '2rem' }}>
|
|
313
|
+
The query finished but output no results.
|
|
314
|
+
</div>
|
|
315
|
+
) : queryInfo !== null &&
|
|
316
|
+
queryInfo.results !== null &&
|
|
317
|
+
queryInfo.results.length > 0 ? (
|
|
318
|
+
<div
|
|
319
|
+
className="table-responsive"
|
|
320
|
+
style={{
|
|
321
|
+
gridGap: '0',
|
|
322
|
+
width: '86%',
|
|
323
|
+
padding: '0',
|
|
324
|
+
maxHeight: '50%',
|
|
325
|
+
}}
|
|
326
|
+
>
|
|
327
|
+
<table
|
|
328
|
+
style={{ marginTop: '0 !important' }}
|
|
329
|
+
className="table"
|
|
330
|
+
>
|
|
331
|
+
<thead className="fs-7 fw-bold text-gray-400 border-bottom-0">
|
|
332
|
+
<tr>
|
|
333
|
+
{queryInfo.results[0]?.columns.map(columnName => (
|
|
334
|
+
<th key={columnName.name}>
|
|
335
|
+
{columnName.column}
|
|
336
|
+
</th>
|
|
337
|
+
))}
|
|
338
|
+
</tr>
|
|
339
|
+
</thead>
|
|
340
|
+
<tbody>
|
|
341
|
+
{queryInfo.results[0]?.rows
|
|
342
|
+
.slice(0, 100)
|
|
343
|
+
.map((rowData, index) => (
|
|
344
|
+
<tr key={`data-row:${index}`}>
|
|
345
|
+
{rowData.map(rowValue => (
|
|
346
|
+
<td key={rowValue}>{rowValue}</td>
|
|
347
|
+
))}
|
|
348
|
+
</tr>
|
|
349
|
+
))}
|
|
350
|
+
</tbody>
|
|
351
|
+
</table>
|
|
352
|
+
</div>
|
|
353
|
+
) : (
|
|
354
|
+
<div style={{ padding: '2rem' }}>
|
|
355
|
+
Click "Run" to execute the query.
|
|
356
|
+
</div>
|
|
357
|
+
)}
|
|
358
|
+
</div>
|
|
359
|
+
) : null}
|
|
360
|
+
</div>
|
|
361
|
+
</div>
|
|
362
|
+
</Form>
|
|
363
|
+
);
|
|
364
|
+
}}
|
|
365
|
+
</Formik>
|
|
366
|
+
);
|
|
367
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import DJClientContext from '../../providers/djclient';
|
|
2
|
+
import JupyterExportIcon from '../../icons/JupyterExportIcon';
|
|
3
|
+
import { useContext } from 'react';
|
|
4
|
+
|
|
5
|
+
export default function NotebookDownload({ node }) {
|
|
6
|
+
const djClient = useContext(DJClientContext).DataJunctionAPI;
|
|
7
|
+
|
|
8
|
+
const downloadFile = async () => {
|
|
9
|
+
try {
|
|
10
|
+
const response = await djClient.notebookExportCube(node.name);
|
|
11
|
+
const notebook = await response.blob();
|
|
12
|
+
const url = window.URL.createObjectURL(new Blob([notebook]));
|
|
13
|
+
|
|
14
|
+
const link = document.createElement('a');
|
|
15
|
+
link.href = url;
|
|
16
|
+
link.setAttribute('download', 'notebook.ipynb');
|
|
17
|
+
document.body.appendChild(link);
|
|
18
|
+
link.click();
|
|
19
|
+
link.parentNode.removeChild(link);
|
|
20
|
+
} catch (error) {
|
|
21
|
+
console.error('Error downloading file: ', error);
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
return (
|
|
26
|
+
<>
|
|
27
|
+
<button
|
|
28
|
+
className="button-3"
|
|
29
|
+
onClick={downloadFile}
|
|
30
|
+
style={{ height: '2.5rem' }}
|
|
31
|
+
>
|
|
32
|
+
<JupyterExportIcon /> Export as Notebook
|
|
33
|
+
</button>
|
|
34
|
+
</>
|
|
35
|
+
);
|
|
36
|
+
}
|
|
@@ -80,7 +80,7 @@ export default function PartitionColumnPopover({ column, node, onSubmit }) {
|
|
|
80
80
|
<Form>
|
|
81
81
|
{displayMessageAfterSubmit(status)}
|
|
82
82
|
<span data-testid="edit-partition">
|
|
83
|
-
<label htmlFor="
|
|
83
|
+
<label htmlFor="partitionType">Partition Type</label>
|
|
84
84
|
<Field
|
|
85
85
|
as="select"
|
|
86
86
|
name="partition_type"
|
|
@@ -108,9 +108,7 @@ export default function PartitionColumnPopover({ column, node, onSubmit }) {
|
|
|
108
108
|
<br />
|
|
109
109
|
{values.partition_type === 'temporal' ? (
|
|
110
110
|
<>
|
|
111
|
-
<label htmlFor="
|
|
112
|
-
Partition Format
|
|
113
|
-
</label>
|
|
111
|
+
<label htmlFor="partitionFormat">Partition Format</label>
|
|
114
112
|
<Field
|
|
115
113
|
type="text"
|
|
116
114
|
name="format"
|
|
@@ -119,7 +117,7 @@ export default function PartitionColumnPopover({ column, node, onSubmit }) {
|
|
|
119
117
|
/>
|
|
120
118
|
<br />
|
|
121
119
|
<br />
|
|
122
|
-
<label htmlFor="
|
|
120
|
+
<label htmlFor="partitionGranularity">
|
|
123
121
|
Partition Granularity
|
|
124
122
|
</label>
|
|
125
123
|
<Field
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { Field } from 'formik';
|
|
3
|
+
|
|
4
|
+
export default function PartitionValueForm({ col, materialization }) {
|
|
5
|
+
if (col.partition.type_ === 'temporal') {
|
|
6
|
+
return (
|
|
7
|
+
<>
|
|
8
|
+
<div
|
|
9
|
+
className="partition__full"
|
|
10
|
+
key={col.name}
|
|
11
|
+
style={{ width: '50%' }}
|
|
12
|
+
>
|
|
13
|
+
<div className="partition__header">{col.display_name}</div>
|
|
14
|
+
<div className="partition__body">
|
|
15
|
+
<span style={{ padding: '0.5rem' }}>From</span>{' '}
|
|
16
|
+
<Field
|
|
17
|
+
type="text"
|
|
18
|
+
name={`partitionValues.['${col.name}'].from`}
|
|
19
|
+
id={`${col.name}.from`}
|
|
20
|
+
placeholder="20230101"
|
|
21
|
+
default="20230101"
|
|
22
|
+
style={{ width: '7rem', paddingRight: '1rem' }}
|
|
23
|
+
/>{' '}
|
|
24
|
+
<span style={{ padding: '0.5rem' }}>To</span>
|
|
25
|
+
<Field
|
|
26
|
+
type="text"
|
|
27
|
+
name={`partitionValues.['${col.name}'].to`}
|
|
28
|
+
id={`${col.name}.to`}
|
|
29
|
+
placeholder="20230102"
|
|
30
|
+
default="20230102"
|
|
31
|
+
style={{ width: '7rem' }}
|
|
32
|
+
/>
|
|
33
|
+
</div>
|
|
34
|
+
</div>
|
|
35
|
+
</>
|
|
36
|
+
);
|
|
37
|
+
} else {
|
|
38
|
+
return (
|
|
39
|
+
<>
|
|
40
|
+
<div
|
|
41
|
+
className="partition__full"
|
|
42
|
+
key={col.name}
|
|
43
|
+
style={{ width: '50%' }}
|
|
44
|
+
>
|
|
45
|
+
<div className="partition__header">{col.display_name}</div>
|
|
46
|
+
<div className="partition__body">
|
|
47
|
+
<Field
|
|
48
|
+
type="text"
|
|
49
|
+
name={`partitionValues.['${col.name}']`}
|
|
50
|
+
id={col.name}
|
|
51
|
+
placeholder=""
|
|
52
|
+
default=""
|
|
53
|
+
style={{ width: '7rem', paddingRight: '1rem' }}
|
|
54
|
+
/>
|
|
55
|
+
</div>
|
|
56
|
+
</div>
|
|
57
|
+
</>
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
}
|