datajunction-ui 0.0.26 → 0.0.27
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/package.json +2 -2
- package/src/app/components/Search.jsx +41 -33
- package/src/app/components/__tests__/Search.test.jsx +46 -11
- package/src/app/index.tsx +3 -3
- package/src/app/pages/AddEditNodePage/MetricQueryField.jsx +57 -8
- package/src/app/pages/AddEditNodePage/UpstreamNodeField.jsx +17 -5
- package/src/app/pages/AddEditNodePage/__tests__/index.test.jsx +97 -1
- package/src/app/pages/AddEditNodePage/index.jsx +61 -17
- package/src/app/pages/NodePage/WatchNodeButton.jsx +12 -5
- package/src/app/pages/QueryPlannerPage/MetricFlowGraph.jsx +93 -15
- package/src/app/pages/QueryPlannerPage/PreAggDetailsPanel.jsx +2320 -65
- package/src/app/pages/QueryPlannerPage/SelectionPanel.jsx +234 -25
- package/src/app/pages/QueryPlannerPage/__tests__/MetricFlowGraph.test.jsx +315 -122
- package/src/app/pages/QueryPlannerPage/__tests__/PreAggDetailsPanel.test.jsx +2672 -314
- package/src/app/pages/QueryPlannerPage/__tests__/SelectionPanel.test.jsx +567 -0
- package/src/app/pages/QueryPlannerPage/__tests__/index.test.jsx +480 -55
- package/src/app/pages/QueryPlannerPage/index.jsx +1021 -14
- package/src/app/pages/QueryPlannerPage/styles.css +1990 -62
- package/src/app/pages/Root/__tests__/index.test.jsx +79 -8
- package/src/app/pages/Root/index.tsx +1 -6
- package/src/app/pages/SQLBuilderPage/__tests__/index.test.jsx +82 -0
- package/src/app/pages/SettingsPage/__tests__/CreateServiceAccountModal.test.jsx +37 -0
- package/src/app/pages/SettingsPage/__tests__/ServiceAccountsSection.test.jsx +48 -0
- package/src/app/pages/SettingsPage/__tests__/index.test.jsx +169 -1
- package/src/app/services/DJService.js +492 -3
- package/src/app/services/__tests__/DJService.test.jsx +582 -0
- package/src/mocks/mockNodes.jsx +36 -0
- package/webpack.config.js +27 -0
|
@@ -120,6 +120,19 @@ export function AddEditNodePage({ extensions = {} }) {
|
|
|
120
120
|
}
|
|
121
121
|
};
|
|
122
122
|
|
|
123
|
+
/**
|
|
124
|
+
* Build the metric query based on whether an upstream node is provided.
|
|
125
|
+
* - With upstream node: `SELECT <expression> FROM <upstream_node>` (regular metric)
|
|
126
|
+
* - Without upstream node: `SELECT <expression>` (derived metric referencing other metrics)
|
|
127
|
+
*/
|
|
128
|
+
const buildMetricQuery = (aggregateExpression, upstreamNode) => {
|
|
129
|
+
if (upstreamNode && upstreamNode.trim() !== '') {
|
|
130
|
+
return `SELECT ${aggregateExpression} \n FROM ${upstreamNode}`;
|
|
131
|
+
}
|
|
132
|
+
// Derived metric - no FROM clause needed, expression references other metrics directly
|
|
133
|
+
return `SELECT ${aggregateExpression}`;
|
|
134
|
+
};
|
|
135
|
+
|
|
123
136
|
const createNode = async (values, setStatus) => {
|
|
124
137
|
const { status, json } = await djClient.createNode(
|
|
125
138
|
nodeType,
|
|
@@ -127,7 +140,7 @@ export function AddEditNodePage({ extensions = {} }) {
|
|
|
127
140
|
values.display_name,
|
|
128
141
|
values.description,
|
|
129
142
|
values.type === 'metric'
|
|
130
|
-
?
|
|
143
|
+
? buildMetricQuery(values.aggregate_expression, values.upstream_node)
|
|
131
144
|
: values.query,
|
|
132
145
|
values.mode,
|
|
133
146
|
values.namespace,
|
|
@@ -162,7 +175,7 @@ export function AddEditNodePage({ extensions = {} }) {
|
|
|
162
175
|
values.display_name,
|
|
163
176
|
values.description,
|
|
164
177
|
values.type === 'metric'
|
|
165
|
-
?
|
|
178
|
+
? buildMetricQuery(values.aggregate_expression, values.upstream_node)
|
|
166
179
|
: values.query,
|
|
167
180
|
values.mode,
|
|
168
181
|
values.primary_key ? primaryKeyToList(values.primary_key) : null,
|
|
@@ -224,17 +237,46 @@ export function AddEditNodePage({ extensions = {} }) {
|
|
|
224
237
|
};
|
|
225
238
|
|
|
226
239
|
if (node.type === 'METRIC') {
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
240
|
+
// Check if this is a derived metric (parent is another metric)
|
|
241
|
+
const firstParent = node.current.parents[0];
|
|
242
|
+
const isDerivedMetric = firstParent?.type === 'METRIC';
|
|
243
|
+
|
|
244
|
+
if (isDerivedMetric) {
|
|
245
|
+
// Derived metric: no upstream node, expression is the full query projection
|
|
246
|
+
// Parse the expression from the query (format: "SELECT <expression>")
|
|
247
|
+
const query = node.current.query || '';
|
|
248
|
+
const selectMatch = query.match(/SELECT\s+(.+)/is);
|
|
249
|
+
const derivedExpression = selectMatch
|
|
250
|
+
? selectMatch[1].trim()
|
|
251
|
+
: node.current.metricMetadata?.expression || '';
|
|
252
|
+
|
|
253
|
+
return {
|
|
254
|
+
...baseData,
|
|
255
|
+
metric_direction:
|
|
256
|
+
node.current.metricMetadata?.direction?.toLowerCase(),
|
|
257
|
+
metric_unit: node.current.metricMetadata?.unit?.name?.toLowerCase(),
|
|
258
|
+
significant_digits: node.current.metricMetadata?.significantDigits,
|
|
259
|
+
required_dimensions: node.current.requiredDimensions.map(
|
|
260
|
+
dim => dim.name,
|
|
261
|
+
),
|
|
262
|
+
upstream_node: '', // Derived metrics have no upstream node
|
|
263
|
+
aggregate_expression: derivedExpression,
|
|
264
|
+
};
|
|
265
|
+
} else {
|
|
266
|
+
// Regular metric: has upstream node
|
|
267
|
+
return {
|
|
268
|
+
...baseData,
|
|
269
|
+
metric_direction:
|
|
270
|
+
node.current.metricMetadata?.direction?.toLowerCase(),
|
|
271
|
+
metric_unit: node.current.metricMetadata?.unit?.name?.toLowerCase(),
|
|
272
|
+
significant_digits: node.current.metricMetadata?.significantDigits,
|
|
273
|
+
required_dimensions: node.current.requiredDimensions.map(
|
|
274
|
+
dim => dim.name,
|
|
275
|
+
),
|
|
276
|
+
upstream_node: firstParent?.name || '',
|
|
277
|
+
aggregate_expression: node.current.metricMetadata?.expression,
|
|
278
|
+
};
|
|
279
|
+
}
|
|
238
280
|
}
|
|
239
281
|
return baseData;
|
|
240
282
|
};
|
|
@@ -329,12 +371,14 @@ export function AddEditNodePage({ extensions = {} }) {
|
|
|
329
371
|
/>,
|
|
330
372
|
);
|
|
331
373
|
}
|
|
374
|
+
// For derived metrics, upstream_node is empty - pass null to clear the select
|
|
332
375
|
setSelectUpstreamNode(
|
|
333
376
|
<UpstreamNodeField
|
|
334
|
-
defaultValue={
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
377
|
+
defaultValue={
|
|
378
|
+
data.upstream_node
|
|
379
|
+
? { value: data.upstream_node, label: data.upstream_node }
|
|
380
|
+
: null
|
|
381
|
+
}
|
|
338
382
|
/>,
|
|
339
383
|
);
|
|
340
384
|
if (data.owners) {
|
|
@@ -7,10 +7,7 @@ import CollapsedIcon from '../../icons/CollapsedIcon';
|
|
|
7
7
|
const EVENT_TYPES = ['delete', 'update'];
|
|
8
8
|
|
|
9
9
|
export default function WatchButton({ node }) {
|
|
10
|
-
|
|
11
|
-
return null;
|
|
12
|
-
}
|
|
13
|
-
|
|
10
|
+
// All hooks must be called before any early returns
|
|
14
11
|
const djClient = useContext(DJClientContext).DataJunctionAPI;
|
|
15
12
|
const [selectedEvents, setSelectedEvents] = useState([]);
|
|
16
13
|
const [loading, setLoading] = useState(false);
|
|
@@ -19,6 +16,10 @@ export default function WatchButton({ node }) {
|
|
|
19
16
|
|
|
20
17
|
// Load existing preferences
|
|
21
18
|
useEffect(() => {
|
|
19
|
+
if (!node || !node.name || !node.type) {
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
|
|
22
23
|
const loadPreferences = async () => {
|
|
23
24
|
try {
|
|
24
25
|
const preferences = await djClient.getNotificationPreferences({
|
|
@@ -38,7 +39,7 @@ export default function WatchButton({ node }) {
|
|
|
38
39
|
};
|
|
39
40
|
|
|
40
41
|
loadPreferences();
|
|
41
|
-
}, [djClient, node
|
|
42
|
+
}, [djClient, node?.name, node?.type]);
|
|
42
43
|
|
|
43
44
|
// Close dropdown on outside click
|
|
44
45
|
useEffect(() => {
|
|
@@ -51,6 +52,12 @@ export default function WatchButton({ node }) {
|
|
|
51
52
|
document.addEventListener('mousedown', handleClickOutside);
|
|
52
53
|
return () => document.removeEventListener('mousedown', handleClickOutside);
|
|
53
54
|
}, []);
|
|
55
|
+
|
|
56
|
+
// Early return after all hooks are called
|
|
57
|
+
if (!node || !node.name || !node.type) {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
|
|
54
61
|
const toggleEvent = async event => {
|
|
55
62
|
const isSelected = selectedEvents.includes(event);
|
|
56
63
|
|
|
@@ -60,8 +60,32 @@ function MetricNode({ data, selected }) {
|
|
|
60
60
|
);
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
+
/**
|
|
64
|
+
* Component node - shows metric building blocks (e.g., SUM, COUNT)
|
|
65
|
+
*/
|
|
66
|
+
function ComponentNode({ data, selected }) {
|
|
67
|
+
return (
|
|
68
|
+
<div
|
|
69
|
+
className={`compact-node compact-node-component ${
|
|
70
|
+
selected ? 'selected' : ''
|
|
71
|
+
}`}
|
|
72
|
+
>
|
|
73
|
+
<Handle type="target" position={Position.Left} />
|
|
74
|
+
<div className="compact-node-icon">●</div>
|
|
75
|
+
<div className="compact-node-content">
|
|
76
|
+
<div className="compact-node-name">{data.shortName}</div>
|
|
77
|
+
<div className="compact-node-meta">
|
|
78
|
+
<span className="meta-item">{data.aggregation || 'RAW'}</span>
|
|
79
|
+
</div>
|
|
80
|
+
</div>
|
|
81
|
+
<Handle type="source" position={Position.Right} />
|
|
82
|
+
</div>
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
63
86
|
const nodeTypes = {
|
|
64
87
|
preagg: PreAggNode,
|
|
88
|
+
component: ComponentNode,
|
|
65
89
|
metric: MetricNode,
|
|
66
90
|
};
|
|
67
91
|
|
|
@@ -132,6 +156,7 @@ export function MetricFlowGraph({
|
|
|
132
156
|
|
|
133
157
|
// Track mappings
|
|
134
158
|
const preAggNodesMap = new Map();
|
|
159
|
+
const componentNodeIds = new Map();
|
|
135
160
|
const componentToPreAgg = new Map();
|
|
136
161
|
|
|
137
162
|
let nodeId = 0;
|
|
@@ -167,6 +192,37 @@ export function MetricFlowGraph({
|
|
|
167
192
|
});
|
|
168
193
|
});
|
|
169
194
|
|
|
195
|
+
// Create component nodes (building blocks for metrics)
|
|
196
|
+
grainGroups.forEach((gg, ggIdx) => {
|
|
197
|
+
gg.components?.forEach(comp => {
|
|
198
|
+
if (!componentNodeIds.has(comp.name)) {
|
|
199
|
+
const id = getNextId();
|
|
200
|
+
componentNodeIds.set(comp.name, id);
|
|
201
|
+
// Shorten name for display (e.g., "unit_price_sum" -> "price_sum")
|
|
202
|
+
const shortName =
|
|
203
|
+
comp.name.length > 40
|
|
204
|
+
? '...' + comp.name.split('_').slice(-2).join('_')
|
|
205
|
+
: comp.name;
|
|
206
|
+
|
|
207
|
+
rawNodes.push({
|
|
208
|
+
id,
|
|
209
|
+
type: 'component',
|
|
210
|
+
position: { x: 0, y: 0 },
|
|
211
|
+
data: {
|
|
212
|
+
name: comp.name,
|
|
213
|
+
shortName,
|
|
214
|
+
aggregation: comp.aggregation,
|
|
215
|
+
merge: comp.merge,
|
|
216
|
+
grainGroupIndex: ggIdx,
|
|
217
|
+
},
|
|
218
|
+
selected:
|
|
219
|
+
selectedNode?.type === 'component' &&
|
|
220
|
+
selectedNode?.name === comp.name,
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
});
|
|
224
|
+
});
|
|
225
|
+
|
|
170
226
|
// Create metric nodes
|
|
171
227
|
const metricNodeIds = new Map();
|
|
172
228
|
|
|
@@ -191,26 +247,38 @@ export function MetricFlowGraph({
|
|
|
191
247
|
});
|
|
192
248
|
});
|
|
193
249
|
|
|
194
|
-
// Create edges
|
|
195
|
-
|
|
196
|
-
const
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
250
|
+
// Create edges: PreAgg -> Component
|
|
251
|
+
grainGroups.forEach((gg, ggIdx) => {
|
|
252
|
+
const preAggId = preAggNodesMap.get(ggIdx);
|
|
253
|
+
gg.components?.forEach(comp => {
|
|
254
|
+
const compId = componentNodeIds.get(comp.name);
|
|
255
|
+
if (preAggId && compId) {
|
|
256
|
+
rawEdges.push({
|
|
257
|
+
id: `edge-preagg-${preAggId}-${compId}`,
|
|
258
|
+
source: preAggId,
|
|
259
|
+
target: compId,
|
|
260
|
+
style: { stroke: '#64748b', strokeWidth: 2 },
|
|
261
|
+
markerEnd: {
|
|
262
|
+
type: MarkerType.ArrowClosed,
|
|
263
|
+
color: '#64748b',
|
|
264
|
+
width: 16,
|
|
265
|
+
height: 16,
|
|
266
|
+
},
|
|
267
|
+
});
|
|
203
268
|
}
|
|
204
269
|
});
|
|
270
|
+
});
|
|
205
271
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
272
|
+
// Create edges: Component -> Metric
|
|
273
|
+
metricFormulas.forEach(metric => {
|
|
274
|
+
const metricId = metricNodeIds.get(metric.name);
|
|
275
|
+
metric.components?.forEach(compName => {
|
|
276
|
+
const compId = componentNodeIds.get(compName);
|
|
277
|
+
if (compId && metricId) {
|
|
209
278
|
rawEdges.push({
|
|
210
|
-
id: `edge-${
|
|
211
|
-
source:
|
|
279
|
+
id: `edge-comp-${compId}-${metricId}`,
|
|
280
|
+
source: compId,
|
|
212
281
|
target: metricId,
|
|
213
|
-
type: 'default', // Straight/bezier edges
|
|
214
282
|
style: { stroke: '#64748b', strokeWidth: 2 },
|
|
215
283
|
markerEnd: {
|
|
216
284
|
type: MarkerType.ArrowClosed,
|
|
@@ -244,6 +312,12 @@ export function MetricFlowGraph({
|
|
|
244
312
|
index: node.data.grainGroupIndex,
|
|
245
313
|
data: grainGroups[node.data.grainGroupIndex],
|
|
246
314
|
});
|
|
315
|
+
} else if (node.type === 'component') {
|
|
316
|
+
onNodeSelect?.({
|
|
317
|
+
type: 'component',
|
|
318
|
+
name: node.data.name,
|
|
319
|
+
data: node.data,
|
|
320
|
+
});
|
|
247
321
|
} else if (node.type === 'metric') {
|
|
248
322
|
onNodeSelect?.({
|
|
249
323
|
type: 'metric',
|
|
@@ -295,6 +369,10 @@ export function MetricFlowGraph({
|
|
|
295
369
|
<span className="legend-dot preagg"></span>
|
|
296
370
|
<span>Pre-agg</span>
|
|
297
371
|
</div>
|
|
372
|
+
<div className="legend-item">
|
|
373
|
+
<span className="legend-dot component"></span>
|
|
374
|
+
<span>Component</span>
|
|
375
|
+
</div>
|
|
298
376
|
<div className="legend-item">
|
|
299
377
|
<span className="legend-dot metric"></span>
|
|
300
378
|
<span>Metric</span>
|