datajunction-ui 0.0.98 → 0.0.99
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 +1 -1
- package/src/app/pages/QueryPlannerPage/ResultsView.jsx +6 -2
- package/src/app/pages/QueryPlannerPage/SelectionPanel.jsx +676 -356
- package/src/app/pages/QueryPlannerPage/__tests__/SelectionPanel.test.jsx +9 -9
- package/src/app/pages/QueryPlannerPage/__tests__/index.test.jsx +5 -3
- package/src/app/pages/QueryPlannerPage/index.jsx +34 -8
- package/src/app/pages/QueryPlannerPage/styles.css +177 -8
- package/src/app/pages/Root/__tests__/index.test.jsx +1 -1
- package/src/app/pages/Root/index.tsx +1 -1
- package/src/app/services/DJService.js +27 -0
|
@@ -15,22 +15,22 @@ const mockDimensions = [
|
|
|
15
15
|
{
|
|
16
16
|
name: 'default.date_dim.dateint',
|
|
17
17
|
type: 'timestamp',
|
|
18
|
-
path: ['default.
|
|
18
|
+
path: ['default.date_dim'],
|
|
19
19
|
},
|
|
20
20
|
{
|
|
21
21
|
name: 'default.date_dim.month',
|
|
22
22
|
type: 'int',
|
|
23
|
-
path: ['default.
|
|
23
|
+
path: ['default.date_dim'],
|
|
24
24
|
},
|
|
25
25
|
{
|
|
26
26
|
name: 'default.date_dim.year',
|
|
27
27
|
type: 'int',
|
|
28
|
-
path: ['default.
|
|
28
|
+
path: ['default.date_dim'],
|
|
29
29
|
},
|
|
30
30
|
{
|
|
31
31
|
name: 'default.customer.country',
|
|
32
32
|
type: 'string',
|
|
33
|
-
path: ['default.
|
|
33
|
+
path: ['default.customer'],
|
|
34
34
|
},
|
|
35
35
|
];
|
|
36
36
|
|
|
@@ -313,8 +313,8 @@ describe('SelectionPanel', () => {
|
|
|
313
313
|
|
|
314
314
|
it('deduplicates dimensions with same name', () => {
|
|
315
315
|
const duplicateDimensions = [
|
|
316
|
-
{ name: 'default.date_dim.month', path: ['path1'
|
|
317
|
-
{ name: 'default.date_dim.month', path: ['short'
|
|
316
|
+
{ name: 'default.date_dim.month', path: ['path1'] },
|
|
317
|
+
{ name: 'default.date_dim.month', path: ['short'] },
|
|
318
318
|
];
|
|
319
319
|
render(
|
|
320
320
|
<SelectionPanel
|
|
@@ -702,7 +702,7 @@ describe('SelectionPanel', () => {
|
|
|
702
702
|
{
|
|
703
703
|
name: 'default.date_dim.dateint',
|
|
704
704
|
type: 'timestamp',
|
|
705
|
-
path: ['default.
|
|
705
|
+
path: ['default.date_dim.dateint'],
|
|
706
706
|
},
|
|
707
707
|
];
|
|
708
708
|
|
|
@@ -714,9 +714,9 @@ describe('SelectionPanel', () => {
|
|
|
714
714
|
/>,
|
|
715
715
|
);
|
|
716
716
|
|
|
717
|
-
//
|
|
717
|
+
// Display name (last 2 segments) appears in the dimension item
|
|
718
718
|
expect(
|
|
719
|
-
screen.getAllByText('
|
|
719
|
+
screen.getAllByText('date_dim.dateint').length,
|
|
720
720
|
).toBeGreaterThanOrEqual(1);
|
|
721
721
|
});
|
|
722
722
|
});
|
|
@@ -55,6 +55,7 @@ jest.mock('../MetricFlowGraph', () => ({
|
|
|
55
55
|
const mockDjClient = {
|
|
56
56
|
metrics: jest.fn(),
|
|
57
57
|
commonDimensions: jest.fn(),
|
|
58
|
+
commonMetrics: jest.fn(),
|
|
58
59
|
measuresV3: jest.fn(),
|
|
59
60
|
metricsV3: jest.fn(),
|
|
60
61
|
listCubesForPreset: jest.fn(),
|
|
@@ -89,7 +90,7 @@ const mockCommonDimensions = [
|
|
|
89
90
|
node_name: 'default.date_dim',
|
|
90
91
|
node_display_name: 'Date',
|
|
91
92
|
properties: [],
|
|
92
|
-
path: ['default.
|
|
93
|
+
path: ['default.date_dim'],
|
|
93
94
|
},
|
|
94
95
|
{
|
|
95
96
|
name: 'default.date_dim.month',
|
|
@@ -97,7 +98,7 @@ const mockCommonDimensions = [
|
|
|
97
98
|
node_name: 'default.date_dim',
|
|
98
99
|
node_display_name: 'Date',
|
|
99
100
|
properties: [],
|
|
100
|
-
path: ['default.
|
|
101
|
+
path: ['default.date_dim'],
|
|
101
102
|
},
|
|
102
103
|
{
|
|
103
104
|
name: 'default.hard_hat.country',
|
|
@@ -105,7 +106,7 @@ const mockCommonDimensions = [
|
|
|
105
106
|
node_name: 'default.hard_hat',
|
|
106
107
|
node_display_name: 'Hard Hat',
|
|
107
108
|
properties: [],
|
|
108
|
-
path: ['default.
|
|
109
|
+
path: ['default.hard_hat'],
|
|
109
110
|
},
|
|
110
111
|
];
|
|
111
112
|
|
|
@@ -192,6 +193,7 @@ describe('QueryPlannerPage', () => {
|
|
|
192
193
|
beforeEach(() => {
|
|
193
194
|
mockDjClient.metrics.mockResolvedValue(mockMetrics);
|
|
194
195
|
mockDjClient.commonDimensions.mockResolvedValue(mockCommonDimensions);
|
|
196
|
+
mockDjClient.commonMetrics.mockResolvedValue([]);
|
|
195
197
|
mockDjClient.measuresV3.mockResolvedValue(mockMeasuresResult);
|
|
196
198
|
mockDjClient.metricsV3.mockResolvedValue(mockMetricsResult);
|
|
197
199
|
mockDjClient.listCubesForPreset.mockResolvedValue(mockCubes);
|
|
@@ -53,6 +53,9 @@ export function QueryPlannerPage() {
|
|
|
53
53
|
const pendingDimensionsFromUrl = useRef([]);
|
|
54
54
|
const pendingCubeFromUrl = useRef(null);
|
|
55
55
|
|
|
56
|
+
// Compatible metrics when dimensions are selected (phase 1: dimension-first flow)
|
|
57
|
+
const [compatibleMetrics, setCompatibleMetrics] = useState(null); // null = no filter active
|
|
58
|
+
|
|
56
59
|
// Results state
|
|
57
60
|
const [measuresResult, setMeasuresResult] = useState(null);
|
|
58
61
|
const [metricsResult, setMetricsResult] = useState(null);
|
|
@@ -259,6 +262,33 @@ export function QueryPlannerPage() {
|
|
|
259
262
|
}
|
|
260
263
|
}, [commonDimensions, selectedDimensions]);
|
|
261
264
|
|
|
265
|
+
// Phase 1: When dimensions are selected, reactively find compatible metrics
|
|
266
|
+
useEffect(() => {
|
|
267
|
+
if (selectedDimensions.length === 0) {
|
|
268
|
+
setCompatibleMetrics(null);
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
let cancelled = false;
|
|
272
|
+
djClient
|
|
273
|
+
.commonMetrics(selectedDimensions)
|
|
274
|
+
.then(result => {
|
|
275
|
+
if (cancelled) return;
|
|
276
|
+
// API returns array of node names (strings) or objects with a name field
|
|
277
|
+
if (Array.isArray(result)) {
|
|
278
|
+
const names = result.map(r => (typeof r === 'string' ? r : r.name));
|
|
279
|
+
setCompatibleMetrics(new Set(names));
|
|
280
|
+
} else {
|
|
281
|
+
setCompatibleMetrics(null);
|
|
282
|
+
}
|
|
283
|
+
})
|
|
284
|
+
.catch(() => {
|
|
285
|
+
if (!cancelled) setCompatibleMetrics(null);
|
|
286
|
+
});
|
|
287
|
+
return () => {
|
|
288
|
+
cancelled = true;
|
|
289
|
+
};
|
|
290
|
+
}, [selectedDimensions, djClient]);
|
|
291
|
+
|
|
262
292
|
// Fetch V3 measures and metrics SQL when selection, filters, or engine changes
|
|
263
293
|
useEffect(() => {
|
|
264
294
|
const fetchData = async () => {
|
|
@@ -1194,14 +1224,9 @@ export function QueryPlannerPage() {
|
|
|
1194
1224
|
|
|
1195
1225
|
return (
|
|
1196
1226
|
<div className="planner-page">
|
|
1197
|
-
{
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
<h1>Explore</h1>
|
|
1201
|
-
{/* <p>Explore metrics and dimensions and plan materializations</p> */}
|
|
1202
|
-
</div>
|
|
1203
|
-
{error && <div className="header-error">{error}</div>}
|
|
1204
|
-
</header>
|
|
1227
|
+
{error && (
|
|
1228
|
+
<div className="header-error planner-error-banner">{error}</div>
|
|
1229
|
+
)}
|
|
1205
1230
|
|
|
1206
1231
|
{/* Three-column layout */}
|
|
1207
1232
|
<div className="planner-layout">
|
|
@@ -1228,6 +1253,7 @@ export function QueryPlannerPage() {
|
|
|
1228
1253
|
selectedMetrics.length > 0 && selectedDimensions.length > 0
|
|
1229
1254
|
}
|
|
1230
1255
|
queryLoading={queryLoading}
|
|
1256
|
+
compatibleMetrics={compatibleMetrics}
|
|
1231
1257
|
/>
|
|
1232
1258
|
</aside>
|
|
1233
1259
|
|
|
@@ -45,13 +45,14 @@
|
|
|
45
45
|
|
|
46
46
|
/* Full page layout */
|
|
47
47
|
.planner-page {
|
|
48
|
-
height: 100vh;
|
|
48
|
+
height: calc(100vh - 90px);
|
|
49
49
|
display: flex;
|
|
50
50
|
flex-direction: column;
|
|
51
51
|
background: var(--planner-bg);
|
|
52
52
|
color: var(--planner-text);
|
|
53
53
|
font-family: var(--font-body);
|
|
54
54
|
overflow: hidden;
|
|
55
|
+
border-top: 1px solid #eee;
|
|
55
56
|
}
|
|
56
57
|
|
|
57
58
|
/* Header */
|
|
@@ -89,6 +90,14 @@
|
|
|
89
90
|
font-size: 13px;
|
|
90
91
|
}
|
|
91
92
|
|
|
93
|
+
.planner-error-banner {
|
|
94
|
+
flex-shrink: 0;
|
|
95
|
+
border-radius: 0;
|
|
96
|
+
border-left: none;
|
|
97
|
+
border-right: none;
|
|
98
|
+
border-top: none;
|
|
99
|
+
}
|
|
100
|
+
|
|
92
101
|
/* Materialization Error Banner */
|
|
93
102
|
.materialization-error {
|
|
94
103
|
display: flex;
|
|
@@ -374,6 +383,7 @@
|
|
|
374
383
|
.planner-layout {
|
|
375
384
|
display: flex;
|
|
376
385
|
flex: 1;
|
|
386
|
+
min-height: 0;
|
|
377
387
|
overflow: hidden;
|
|
378
388
|
}
|
|
379
389
|
|
|
@@ -381,9 +391,10 @@
|
|
|
381
391
|
.planner-selection {
|
|
382
392
|
width: 20%;
|
|
383
393
|
/* min-width: 280px; */
|
|
394
|
+
min-height: 0;
|
|
384
395
|
background: var(--planner-surface);
|
|
385
396
|
border-right: 1px solid var(--planner-border);
|
|
386
|
-
overflow:
|
|
397
|
+
overflow: hidden;
|
|
387
398
|
display: flex;
|
|
388
399
|
flex-direction: column;
|
|
389
400
|
}
|
|
@@ -480,8 +491,17 @@
|
|
|
480
491
|
.selection-panel {
|
|
481
492
|
display: flex;
|
|
482
493
|
flex-direction: column;
|
|
483
|
-
|
|
484
|
-
|
|
494
|
+
flex: 1;
|
|
495
|
+
min-height: 0;
|
|
496
|
+
overflow: hidden;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
.resizable-sections {
|
|
500
|
+
display: flex;
|
|
501
|
+
flex-direction: column;
|
|
502
|
+
flex: 1;
|
|
503
|
+
min-height: 0;
|
|
504
|
+
overflow: hidden;
|
|
485
505
|
}
|
|
486
506
|
|
|
487
507
|
/* Cube Preset Section */
|
|
@@ -850,6 +870,20 @@
|
|
|
850
870
|
accent-color: var(--accent-primary);
|
|
851
871
|
}
|
|
852
872
|
|
|
873
|
+
/* Incompatible metric (dims selected, this metric doesn't support them) */
|
|
874
|
+
.selection-item.metric-incompatible {
|
|
875
|
+
opacity: 0.35;
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
/* Compatible badge shown next to metrics that match selected dims */
|
|
879
|
+
.metric-compatible-badge {
|
|
880
|
+
margin-left: auto;
|
|
881
|
+
font-size: 11px;
|
|
882
|
+
color: var(--accent-success);
|
|
883
|
+
font-weight: 600;
|
|
884
|
+
flex-shrink: 0;
|
|
885
|
+
}
|
|
886
|
+
|
|
853
887
|
.item-name {
|
|
854
888
|
font-size: 13px;
|
|
855
889
|
color: var(--planner-text);
|
|
@@ -879,6 +913,115 @@
|
|
|
879
913
|
font-family: var(--font-display);
|
|
880
914
|
}
|
|
881
915
|
|
|
916
|
+
/* Dimension group (node-level header + items) */
|
|
917
|
+
.dim-group {
|
|
918
|
+
margin-bottom: 4px;
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
.dim-group-header {
|
|
922
|
+
display: flex;
|
|
923
|
+
align-items: center;
|
|
924
|
+
gap: 8px;
|
|
925
|
+
padding: 8px 12px;
|
|
926
|
+
cursor: pointer;
|
|
927
|
+
user-select: none;
|
|
928
|
+
transition: background 0.15s;
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
.dim-group-header:hover {
|
|
932
|
+
background: var(--planner-surface-hover);
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
.dim-group-name {
|
|
936
|
+
flex: 1;
|
|
937
|
+
font-size: 12px;
|
|
938
|
+
font-weight: 600;
|
|
939
|
+
color: var(--planner-text);
|
|
940
|
+
font-family: var(--font-display);
|
|
941
|
+
overflow: hidden;
|
|
942
|
+
text-overflow: ellipsis;
|
|
943
|
+
white-space: nowrap;
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
.dim-group-meta {
|
|
947
|
+
font-size: 10px;
|
|
948
|
+
font-weight: 400;
|
|
949
|
+
color: var(--planner-text-muted);
|
|
950
|
+
background: var(--planner-bg-subtle, rgba(0, 0, 0, 0.06));
|
|
951
|
+
padding: 1px 5px;
|
|
952
|
+
border-radius: 8px;
|
|
953
|
+
white-space: nowrap;
|
|
954
|
+
flex-shrink: 0;
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
.dim-group-count {
|
|
958
|
+
font-size: 11px;
|
|
959
|
+
color: var(--planner-text-dim);
|
|
960
|
+
flex-shrink: 0;
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
.dim-group-item {
|
|
964
|
+
padding-left: 28px;
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
/* Role path sub-group (second level) */
|
|
968
|
+
.dim-role-group {
|
|
969
|
+
padding-left: 20px;
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
.dim-role-header {
|
|
973
|
+
display: flex;
|
|
974
|
+
align-items: center;
|
|
975
|
+
gap: 8px;
|
|
976
|
+
padding: 5px 12px;
|
|
977
|
+
cursor: pointer;
|
|
978
|
+
user-select: none;
|
|
979
|
+
transition: background 0.15s;
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
.dim-role-header:hover {
|
|
983
|
+
background: var(--planner-surface-hover);
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
.dim-role-label {
|
|
987
|
+
flex: 1;
|
|
988
|
+
font-size: 11px;
|
|
989
|
+
color: var(--planner-text-muted);
|
|
990
|
+
font-family: var(--font-display);
|
|
991
|
+
white-space: nowrap;
|
|
992
|
+
overflow: hidden;
|
|
993
|
+
text-overflow: ellipsis;
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
.dim-role-item {
|
|
997
|
+
padding-left: 22px;
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
.dim-filter-btn {
|
|
1001
|
+
opacity: 0;
|
|
1002
|
+
pointer-events: none;
|
|
1003
|
+
margin-left: auto;
|
|
1004
|
+
align-self: center;
|
|
1005
|
+
flex-shrink: 0;
|
|
1006
|
+
background: none;
|
|
1007
|
+
border: none;
|
|
1008
|
+
color: var(--planner-text-dim);
|
|
1009
|
+
font-size: 10px;
|
|
1010
|
+
font-family: var(--font-display);
|
|
1011
|
+
padding: 0 2px;
|
|
1012
|
+
cursor: pointer;
|
|
1013
|
+
transition: opacity 0.1s, color 0.1s;
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
.dim-role-item:hover .dim-filter-btn {
|
|
1017
|
+
opacity: 1;
|
|
1018
|
+
pointer-events: auto;
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
.dim-filter-btn:hover {
|
|
1022
|
+
color: var(--accent-dimension);
|
|
1023
|
+
}
|
|
1024
|
+
|
|
882
1025
|
/* Search result items (flat list) */
|
|
883
1026
|
.search-result-item {
|
|
884
1027
|
padding: 8px 12px;
|
|
@@ -903,6 +1046,19 @@
|
|
|
903
1046
|
flex-shrink: 0;
|
|
904
1047
|
}
|
|
905
1048
|
|
|
1049
|
+
.section-divider.draggable-divider {
|
|
1050
|
+
height: 5px;
|
|
1051
|
+
cursor: row-resize;
|
|
1052
|
+
background: var(--planner-border);
|
|
1053
|
+
transition: background 0.15s;
|
|
1054
|
+
position: relative;
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
.section-divider.draggable-divider:hover,
|
|
1058
|
+
.section-divider.draggable-divider:active {
|
|
1059
|
+
background: var(--accent-primary);
|
|
1060
|
+
}
|
|
1061
|
+
|
|
906
1062
|
.empty-list {
|
|
907
1063
|
padding: 24px 16px;
|
|
908
1064
|
text-align: center;
|
|
@@ -3406,6 +3562,13 @@ a.action-btn {
|
|
|
3406
3562
|
font-family: var(--font-display);
|
|
3407
3563
|
transition: all 0.12s ease;
|
|
3408
3564
|
white-space: nowrap;
|
|
3565
|
+
max-width: 260px;
|
|
3566
|
+
}
|
|
3567
|
+
|
|
3568
|
+
.selected-chip .chip-label {
|
|
3569
|
+
overflow: hidden;
|
|
3570
|
+
text-overflow: ellipsis;
|
|
3571
|
+
white-space: nowrap;
|
|
3409
3572
|
}
|
|
3410
3573
|
|
|
3411
3574
|
/* Metrics: Pink (matches node_type__metric background #fad7dd) */
|
|
@@ -3469,9 +3632,8 @@ a.action-btn {
|
|
|
3469
3632
|
================================= */
|
|
3470
3633
|
|
|
3471
3634
|
.filters-section {
|
|
3472
|
-
|
|
3473
|
-
|
|
3474
|
-
overflow: visible !important;
|
|
3635
|
+
min-height: 0;
|
|
3636
|
+
overflow-y: auto;
|
|
3475
3637
|
}
|
|
3476
3638
|
|
|
3477
3639
|
.filter-chips-container {
|
|
@@ -3577,6 +3739,14 @@ a.action-btn {
|
|
|
3577
3739
|
Engine Selection
|
|
3578
3740
|
================================= */
|
|
3579
3741
|
|
|
3742
|
+
.engine-run-section {
|
|
3743
|
+
display: flex;
|
|
3744
|
+
flex-direction: column;
|
|
3745
|
+
overflow: hidden;
|
|
3746
|
+
min-height: 0;
|
|
3747
|
+
flex-shrink: 0;
|
|
3748
|
+
}
|
|
3749
|
+
|
|
3580
3750
|
.engine-section {
|
|
3581
3751
|
display: flex;
|
|
3582
3752
|
align-items: center;
|
|
@@ -3628,7 +3798,6 @@ a.action-btn {
|
|
|
3628
3798
|
|
|
3629
3799
|
.run-query-section {
|
|
3630
3800
|
padding: 16px 12px;
|
|
3631
|
-
border-top: 1px solid var(--planner-border);
|
|
3632
3801
|
background: var(--planner-bg);
|
|
3633
3802
|
flex-shrink: 0;
|
|
3634
3803
|
}
|
|
@@ -43,7 +43,7 @@ describe('<Root />', () => {
|
|
|
43
43
|
|
|
44
44
|
// Check navigation links exist
|
|
45
45
|
expect(screen.getAllByText('Catalog')).toHaveLength(1);
|
|
46
|
-
expect(screen.getAllByText('
|
|
46
|
+
expect(screen.getAllByText('Explorer')).toHaveLength(1);
|
|
47
47
|
});
|
|
48
48
|
|
|
49
49
|
it('renders Docs dropdown', async () => {
|
|
@@ -1053,6 +1053,33 @@ export const DataJunctionAPI = {
|
|
|
1053
1053
|
).json();
|
|
1054
1054
|
},
|
|
1055
1055
|
|
|
1056
|
+
commonMetrics: async function (dimensions) {
|
|
1057
|
+
// Dimension names are "node.attribute[role]" — strip role path and attribute to get node name
|
|
1058
|
+
const stripped = [
|
|
1059
|
+
...new Set(
|
|
1060
|
+
dimensions
|
|
1061
|
+
.map(d =>
|
|
1062
|
+
d
|
|
1063
|
+
.replace(/\[[^\]]*\]$/, '')
|
|
1064
|
+
.split('.')
|
|
1065
|
+
.slice(0, -1)
|
|
1066
|
+
.join('.'),
|
|
1067
|
+
)
|
|
1068
|
+
.filter(Boolean),
|
|
1069
|
+
),
|
|
1070
|
+
];
|
|
1071
|
+
if (stripped.length === 0) return [];
|
|
1072
|
+
const dimsQuery =
|
|
1073
|
+
'?' +
|
|
1074
|
+
stripped.map(d => `dimension=${encodeURIComponent(d)}`).join('&') +
|
|
1075
|
+
'&node_type=metric';
|
|
1076
|
+
return await (
|
|
1077
|
+
await fetch(`${DJ_URL}/dimensions/common/${dimsQuery}`, {
|
|
1078
|
+
credentials: 'include',
|
|
1079
|
+
})
|
|
1080
|
+
).json();
|
|
1081
|
+
},
|
|
1082
|
+
|
|
1056
1083
|
history: async function (type, name, offset, limit) {
|
|
1057
1084
|
return await (
|
|
1058
1085
|
await fetch(
|