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.
@@ -15,22 +15,22 @@ const mockDimensions = [
15
15
  {
16
16
  name: 'default.date_dim.dateint',
17
17
  type: 'timestamp',
18
- path: ['default.orders', 'default.date_dim.dateint'],
18
+ path: ['default.date_dim'],
19
19
  },
20
20
  {
21
21
  name: 'default.date_dim.month',
22
22
  type: 'int',
23
- path: ['default.orders', 'default.date_dim.month'],
23
+ path: ['default.date_dim'],
24
24
  },
25
25
  {
26
26
  name: 'default.date_dim.year',
27
27
  type: 'int',
28
- path: ['default.orders', 'default.date_dim.year'],
28
+ path: ['default.date_dim'],
29
29
  },
30
30
  {
31
31
  name: 'default.customer.country',
32
32
  type: 'string',
33
- path: ['default.orders', 'default.customer.country'],
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', 'path2', 'path3'] },
317
- { name: 'default.date_dim.month', path: ['short', 'path'] },
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.orders', 'default.date_dim.dateint'],
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
- // Full name appears in both the dimension-full-name span and the path span
717
+ // Display name (last 2 segments) appears in the dimension item
718
718
  expect(
719
- screen.getAllByText('default.date_dim.dateint').length,
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.repair_orders', 'default.date_dim.dateint'],
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.repair_orders', 'default.date_dim.month'],
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.repair_orders', 'default.hard_hat.country'],
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
- {/* Header */}
1198
- <header className="planner-header">
1199
- <div className="planner-header-content">
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: visible; /* Allow cube dropdown to 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
- height: 100%;
484
- overflow: visible; /* Allow cube dropdown to overflow */
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
- flex: 0 0 auto !important;
3473
- min-height: auto !important;
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('Explore')).toHaveLength(1);
46
+ expect(screen.getAllByText('Explorer')).toHaveLength(1);
47
47
  });
48
48
 
49
49
  it('renders Docs dropdown', async () => {
@@ -61,7 +61,7 @@ export function Root() {
61
61
  </span>
62
62
  <span className="menu-link">
63
63
  <span className="menu-title">
64
- <a href="/planner">Explore</a>
64
+ <a href="/planner">Explorer</a>
65
65
  </span>
66
66
  </span>
67
67
  <span className="menu-link">
@@ -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(