datajunction-ui 0.0.93 → 0.0.95
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/components/NodeComponents.jsx +4 -0
- package/src/app/components/Tab.jsx +11 -16
- package/src/app/components/__tests__/Tab.test.jsx +4 -2
- package/src/app/hooks/useWorkspaceData.js +226 -0
- package/src/app/index.tsx +17 -1
- package/src/app/pages/MyWorkspacePage/ActiveBranchesSection.jsx +38 -107
- package/src/app/pages/MyWorkspacePage/MyNodesSection.jsx +31 -6
- package/src/app/pages/MyWorkspacePage/MyWorkspacePage.css +5 -0
- package/src/app/pages/MyWorkspacePage/NeedsAttentionSection.jsx +86 -100
- package/src/app/pages/MyWorkspacePage/TypeGroupGrid.jsx +7 -11
- package/src/app/pages/MyWorkspacePage/__tests__/ActiveBranchesSection.test.jsx +79 -11
- package/src/app/pages/MyWorkspacePage/__tests__/CollectionsSection.test.jsx +22 -0
- package/src/app/pages/MyWorkspacePage/__tests__/MaterializationsSection.test.jsx +57 -0
- package/src/app/pages/MyWorkspacePage/__tests__/MyNodesSection.test.jsx +60 -18
- package/src/app/pages/MyWorkspacePage/__tests__/MyWorkspacePage.test.jsx +156 -162
- package/src/app/pages/MyWorkspacePage/__tests__/NeedsAttentionSection.test.jsx +17 -18
- package/src/app/pages/MyWorkspacePage/__tests__/NotificationsSection.test.jsx +179 -0
- package/src/app/pages/MyWorkspacePage/__tests__/TypeGroupGrid.test.jsx +169 -49
- package/src/app/pages/MyWorkspacePage/index.jsx +41 -73
- package/src/app/pages/NodePage/NodeDataFlowTab.jsx +464 -0
- package/src/app/pages/NodePage/NodeDependenciesTab.jsx +1 -1
- package/src/app/pages/NodePage/NodeDimensionsTab.jsx +362 -0
- package/src/app/pages/NodePage/NodeLineageTab.jsx +1 -0
- package/src/app/pages/NodePage/NodesWithDimension.jsx +3 -3
- package/src/app/pages/NodePage/__tests__/NodeDataFlowTab.test.jsx +428 -0
- package/src/app/pages/NodePage/__tests__/NodeDependenciesTab.test.jsx +18 -1
- package/src/app/pages/NodePage/__tests__/NodeDimensionsTab.test.jsx +412 -0
- package/src/app/pages/NodePage/__tests__/NodePage.test.jsx +28 -3
- package/src/app/pages/NodePage/__tests__/NodeWithDimension.test.jsx +2 -2
- package/src/app/pages/NodePage/index.jsx +15 -8
- package/src/app/pages/QueryPlannerPage/ResultsView.jsx +420 -86
- package/src/app/pages/QueryPlannerPage/SelectionPanel.jsx +32 -1
- package/src/app/pages/QueryPlannerPage/__tests__/ResultsView.test.jsx +322 -0
- package/src/app/pages/QueryPlannerPage/__tests__/SelectionPanel.test.jsx +431 -2
- package/src/app/pages/QueryPlannerPage/index.jsx +31 -5
- package/src/app/pages/QueryPlannerPage/styles.css +211 -2
- package/src/app/pages/Root/__tests__/index.test.jsx +2 -3
- package/src/app/pages/Root/index.tsx +1 -1
- package/src/app/services/DJService.js +133 -23
- package/src/app/services/__tests__/DJService.test.jsx +600 -11
- package/src/styles/index.css +32 -0
|
@@ -714,8 +714,10 @@ describe('SelectionPanel', () => {
|
|
|
714
714
|
/>,
|
|
715
715
|
);
|
|
716
716
|
|
|
717
|
-
//
|
|
718
|
-
expect(
|
|
717
|
+
// Full name appears in both the dimension-full-name span and the path span
|
|
718
|
+
expect(
|
|
719
|
+
screen.getAllByText('default.date_dim.dateint').length,
|
|
720
|
+
).toBeGreaterThanOrEqual(1);
|
|
719
721
|
});
|
|
720
722
|
});
|
|
721
723
|
|
|
@@ -973,4 +975,431 @@ describe('SelectionPanel', () => {
|
|
|
973
975
|
expect(onDimensionsChange).toHaveBeenCalledWith([]);
|
|
974
976
|
});
|
|
975
977
|
});
|
|
978
|
+
|
|
979
|
+
describe('Filters Section', () => {
|
|
980
|
+
it('renders filter input with placeholder', () => {
|
|
981
|
+
render(<SelectionPanel {...defaultProps} onFiltersChange={jest.fn()} />);
|
|
982
|
+
expect(
|
|
983
|
+
screen.getByPlaceholderText("e.g. v3.date.date_id >= '2024-01-01'"),
|
|
984
|
+
).toBeInTheDocument();
|
|
985
|
+
});
|
|
986
|
+
|
|
987
|
+
it('renders "Add" button for filters', () => {
|
|
988
|
+
render(<SelectionPanel {...defaultProps} onFiltersChange={jest.fn()} />);
|
|
989
|
+
expect(screen.getByText('Add')).toBeInTheDocument();
|
|
990
|
+
});
|
|
991
|
+
|
|
992
|
+
it('Add button is disabled when filter input is empty', () => {
|
|
993
|
+
render(<SelectionPanel {...defaultProps} onFiltersChange={jest.fn()} />);
|
|
994
|
+
expect(screen.getByText('Add')).toBeDisabled();
|
|
995
|
+
});
|
|
996
|
+
|
|
997
|
+
it('Add button is enabled when filter input has text', () => {
|
|
998
|
+
render(<SelectionPanel {...defaultProps} onFiltersChange={jest.fn()} />);
|
|
999
|
+
|
|
1000
|
+
const filterInput = screen.getByPlaceholderText(
|
|
1001
|
+
"e.g. v3.date.date_id >= '2024-01-01'",
|
|
1002
|
+
);
|
|
1003
|
+
fireEvent.change(filterInput, {
|
|
1004
|
+
target: { value: "date >= '2024-01-01'" },
|
|
1005
|
+
});
|
|
1006
|
+
|
|
1007
|
+
expect(screen.getByText('Add')).not.toBeDisabled();
|
|
1008
|
+
});
|
|
1009
|
+
|
|
1010
|
+
it('calls onFiltersChange when Add button is clicked with non-empty input (lines 281-284)', () => {
|
|
1011
|
+
const onFiltersChange = jest.fn();
|
|
1012
|
+
render(
|
|
1013
|
+
<SelectionPanel
|
|
1014
|
+
{...defaultProps}
|
|
1015
|
+
filters={[]}
|
|
1016
|
+
onFiltersChange={onFiltersChange}
|
|
1017
|
+
/>,
|
|
1018
|
+
);
|
|
1019
|
+
|
|
1020
|
+
const filterInput = screen.getByPlaceholderText(
|
|
1021
|
+
"e.g. v3.date.date_id >= '2024-01-01'",
|
|
1022
|
+
);
|
|
1023
|
+
fireEvent.change(filterInput, {
|
|
1024
|
+
target: { value: "date >= '2024-01-01'" },
|
|
1025
|
+
});
|
|
1026
|
+
fireEvent.click(screen.getByText('Add'));
|
|
1027
|
+
|
|
1028
|
+
expect(onFiltersChange).toHaveBeenCalledWith(["date >= '2024-01-01'"]);
|
|
1029
|
+
});
|
|
1030
|
+
|
|
1031
|
+
it('clears filter input after adding a filter', () => {
|
|
1032
|
+
const onFiltersChange = jest.fn();
|
|
1033
|
+
render(
|
|
1034
|
+
<SelectionPanel
|
|
1035
|
+
{...defaultProps}
|
|
1036
|
+
filters={[]}
|
|
1037
|
+
onFiltersChange={onFiltersChange}
|
|
1038
|
+
/>,
|
|
1039
|
+
);
|
|
1040
|
+
|
|
1041
|
+
const filterInput = screen.getByPlaceholderText(
|
|
1042
|
+
"e.g. v3.date.date_id >= '2024-01-01'",
|
|
1043
|
+
);
|
|
1044
|
+
fireEvent.change(filterInput, { target: { value: 'status = active' } });
|
|
1045
|
+
fireEvent.click(screen.getByText('Add'));
|
|
1046
|
+
|
|
1047
|
+
expect(filterInput.value).toBe('');
|
|
1048
|
+
});
|
|
1049
|
+
|
|
1050
|
+
it('does not add duplicate filters', () => {
|
|
1051
|
+
const onFiltersChange = jest.fn();
|
|
1052
|
+
render(
|
|
1053
|
+
<SelectionPanel
|
|
1054
|
+
{...defaultProps}
|
|
1055
|
+
filters={["date >= '2024-01-01'"]}
|
|
1056
|
+
onFiltersChange={onFiltersChange}
|
|
1057
|
+
/>,
|
|
1058
|
+
);
|
|
1059
|
+
|
|
1060
|
+
const filterInput = screen.getByPlaceholderText(
|
|
1061
|
+
"e.g. v3.date.date_id >= '2024-01-01'",
|
|
1062
|
+
);
|
|
1063
|
+
fireEvent.change(filterInput, {
|
|
1064
|
+
target: { value: "date >= '2024-01-01'" },
|
|
1065
|
+
});
|
|
1066
|
+
fireEvent.click(screen.getByText('Add'));
|
|
1067
|
+
|
|
1068
|
+
expect(onFiltersChange).not.toHaveBeenCalled();
|
|
1069
|
+
});
|
|
1070
|
+
|
|
1071
|
+
it('adds filter on Enter key press (lines 289-291)', () => {
|
|
1072
|
+
const onFiltersChange = jest.fn();
|
|
1073
|
+
render(
|
|
1074
|
+
<SelectionPanel
|
|
1075
|
+
{...defaultProps}
|
|
1076
|
+
filters={[]}
|
|
1077
|
+
onFiltersChange={onFiltersChange}
|
|
1078
|
+
/>,
|
|
1079
|
+
);
|
|
1080
|
+
|
|
1081
|
+
const filterInput = screen.getByPlaceholderText(
|
|
1082
|
+
"e.g. v3.date.date_id >= '2024-01-01'",
|
|
1083
|
+
);
|
|
1084
|
+
fireEvent.change(filterInput, { target: { value: 'status = active' } });
|
|
1085
|
+
fireEvent.keyDown(filterInput, { key: 'Enter' });
|
|
1086
|
+
|
|
1087
|
+
expect(onFiltersChange).toHaveBeenCalledWith(['status = active']);
|
|
1088
|
+
});
|
|
1089
|
+
|
|
1090
|
+
it('does not add filter on non-Enter key press', () => {
|
|
1091
|
+
const onFiltersChange = jest.fn();
|
|
1092
|
+
render(
|
|
1093
|
+
<SelectionPanel
|
|
1094
|
+
{...defaultProps}
|
|
1095
|
+
filters={[]}
|
|
1096
|
+
onFiltersChange={onFiltersChange}
|
|
1097
|
+
/>,
|
|
1098
|
+
);
|
|
1099
|
+
|
|
1100
|
+
const filterInput = screen.getByPlaceholderText(
|
|
1101
|
+
"e.g. v3.date.date_id >= '2024-01-01'",
|
|
1102
|
+
);
|
|
1103
|
+
fireEvent.change(filterInput, { target: { value: 'status = active' } });
|
|
1104
|
+
fireEvent.keyDown(filterInput, { key: 'Tab' });
|
|
1105
|
+
|
|
1106
|
+
expect(onFiltersChange).not.toHaveBeenCalled();
|
|
1107
|
+
});
|
|
1108
|
+
|
|
1109
|
+
it('renders existing filter chips (lines 665-686)', () => {
|
|
1110
|
+
render(
|
|
1111
|
+
<SelectionPanel
|
|
1112
|
+
{...defaultProps}
|
|
1113
|
+
filters={["date >= '2024-01-01'", 'status = active']}
|
|
1114
|
+
onFiltersChange={jest.fn()}
|
|
1115
|
+
/>,
|
|
1116
|
+
);
|
|
1117
|
+
|
|
1118
|
+
expect(screen.getByText("date >= '2024-01-01'")).toBeInTheDocument();
|
|
1119
|
+
expect(screen.getByText('status = active')).toBeInTheDocument();
|
|
1120
|
+
});
|
|
1121
|
+
|
|
1122
|
+
it('calls onFiltersChange when filter chip remove button is clicked (lines 296-297)', () => {
|
|
1123
|
+
const onFiltersChange = jest.fn();
|
|
1124
|
+
render(
|
|
1125
|
+
<SelectionPanel
|
|
1126
|
+
{...defaultProps}
|
|
1127
|
+
filters={["date >= '2024-01-01'", 'status = active']}
|
|
1128
|
+
onFiltersChange={onFiltersChange}
|
|
1129
|
+
/>,
|
|
1130
|
+
);
|
|
1131
|
+
|
|
1132
|
+
// Get all "Remove filter" buttons and click the first one
|
|
1133
|
+
const removeBtns = screen.getAllByTitle('Remove filter');
|
|
1134
|
+
fireEvent.click(removeBtns[0]);
|
|
1135
|
+
|
|
1136
|
+
expect(onFiltersChange).toHaveBeenCalledWith(['status = active']);
|
|
1137
|
+
});
|
|
1138
|
+
|
|
1139
|
+
it('shows 0 applied count when no filters', () => {
|
|
1140
|
+
render(<SelectionPanel {...defaultProps} />);
|
|
1141
|
+
expect(screen.getByText('0 applied')).toBeInTheDocument();
|
|
1142
|
+
});
|
|
1143
|
+
|
|
1144
|
+
it('shows correct applied count when filters present', () => {
|
|
1145
|
+
render(
|
|
1146
|
+
<SelectionPanel
|
|
1147
|
+
{...defaultProps}
|
|
1148
|
+
filters={['filter1', 'filter2']}
|
|
1149
|
+
onFiltersChange={jest.fn()}
|
|
1150
|
+
/>,
|
|
1151
|
+
);
|
|
1152
|
+
expect(screen.getByText('2 applied')).toBeInTheDocument();
|
|
1153
|
+
});
|
|
1154
|
+
});
|
|
1155
|
+
|
|
1156
|
+
describe('Engine Selection (line 709)', () => {
|
|
1157
|
+
it('renders engine pills', () => {
|
|
1158
|
+
render(<SelectionPanel {...defaultProps} />);
|
|
1159
|
+
expect(screen.getByText('Auto')).toBeInTheDocument();
|
|
1160
|
+
expect(screen.getByText('Druid')).toBeInTheDocument();
|
|
1161
|
+
expect(screen.getByText('Trino')).toBeInTheDocument();
|
|
1162
|
+
});
|
|
1163
|
+
|
|
1164
|
+
it('highlights the active engine pill', () => {
|
|
1165
|
+
render(<SelectionPanel {...defaultProps} selectedEngine="druid" />);
|
|
1166
|
+
const druidPill = screen.getByText('Druid').closest('button');
|
|
1167
|
+
expect(druidPill).toHaveClass('active');
|
|
1168
|
+
});
|
|
1169
|
+
|
|
1170
|
+
it('auto engine pill is active when selectedEngine is null', () => {
|
|
1171
|
+
render(<SelectionPanel {...defaultProps} selectedEngine={null} />);
|
|
1172
|
+
const autoPill = screen.getByText('Auto').closest('button');
|
|
1173
|
+
expect(autoPill).toHaveClass('active');
|
|
1174
|
+
});
|
|
1175
|
+
|
|
1176
|
+
it('calls onEngineChange when engine pill is clicked', () => {
|
|
1177
|
+
const onEngineChange = jest.fn();
|
|
1178
|
+
render(
|
|
1179
|
+
<SelectionPanel
|
|
1180
|
+
{...defaultProps}
|
|
1181
|
+
selectedEngine={null}
|
|
1182
|
+
onEngineChange={onEngineChange}
|
|
1183
|
+
/>,
|
|
1184
|
+
);
|
|
1185
|
+
|
|
1186
|
+
fireEvent.click(screen.getByText('Trino'));
|
|
1187
|
+
expect(onEngineChange).toHaveBeenCalledWith('trino');
|
|
1188
|
+
});
|
|
1189
|
+
|
|
1190
|
+
it('does not throw when onEngineChange is not provided', () => {
|
|
1191
|
+
render(
|
|
1192
|
+
<SelectionPanel
|
|
1193
|
+
{...defaultProps}
|
|
1194
|
+
selectedEngine={null}
|
|
1195
|
+
onEngineChange={undefined}
|
|
1196
|
+
/>,
|
|
1197
|
+
);
|
|
1198
|
+
|
|
1199
|
+
expect(() => {
|
|
1200
|
+
fireEvent.click(screen.getByText('Druid'));
|
|
1201
|
+
}).not.toThrow();
|
|
1202
|
+
});
|
|
1203
|
+
});
|
|
1204
|
+
|
|
1205
|
+
describe('Metrics combobox-input click handler (line 386)', () => {
|
|
1206
|
+
it('clicking combobox-input container focuses the metrics search input', () => {
|
|
1207
|
+
render(<SelectionPanel {...defaultProps} />);
|
|
1208
|
+
const comboboxInputs = document.querySelectorAll('.combobox-input');
|
|
1209
|
+
// The first combobox-input is for metrics
|
|
1210
|
+
expect(comboboxInputs[0]).toBeInTheDocument();
|
|
1211
|
+
// Click the container — should not throw
|
|
1212
|
+
expect(() => fireEvent.click(comboboxInputs[0])).not.toThrow();
|
|
1213
|
+
});
|
|
1214
|
+
});
|
|
1215
|
+
|
|
1216
|
+
describe('Metrics search input stopPropagation (line 423)', () => {
|
|
1217
|
+
it('clicking metrics search input does not bubble to combobox container', () => {
|
|
1218
|
+
render(<SelectionPanel {...defaultProps} />);
|
|
1219
|
+
const searchInput = screen.getByPlaceholderText('Search metrics...');
|
|
1220
|
+
// Clicking the input itself should not throw
|
|
1221
|
+
expect(() => fireEvent.click(searchInput)).not.toThrow();
|
|
1222
|
+
});
|
|
1223
|
+
});
|
|
1224
|
+
|
|
1225
|
+
describe('Clear metrics button (lines 440-441)', () => {
|
|
1226
|
+
it('shows inline Clear button when metrics are selected', () => {
|
|
1227
|
+
render(
|
|
1228
|
+
<SelectionPanel
|
|
1229
|
+
{...defaultProps}
|
|
1230
|
+
selectedMetrics={['default.num_repair_orders']}
|
|
1231
|
+
/>,
|
|
1232
|
+
);
|
|
1233
|
+
// The combobox-action Clear button inside metrics combobox
|
|
1234
|
+
const clearBtns = document.querySelectorAll('.combobox-action');
|
|
1235
|
+
const clearMetricsBtns = Array.from(clearBtns).filter(
|
|
1236
|
+
btn => btn.textContent === 'Clear',
|
|
1237
|
+
);
|
|
1238
|
+
expect(clearMetricsBtns.length).toBeGreaterThanOrEqual(1);
|
|
1239
|
+
});
|
|
1240
|
+
|
|
1241
|
+
it('calls onMetricsChange([]) when inline Clear is clicked', () => {
|
|
1242
|
+
const onMetricsChange = jest.fn();
|
|
1243
|
+
render(
|
|
1244
|
+
<SelectionPanel
|
|
1245
|
+
{...defaultProps}
|
|
1246
|
+
selectedMetrics={[
|
|
1247
|
+
'default.num_repair_orders',
|
|
1248
|
+
'default.avg_repair_price',
|
|
1249
|
+
]}
|
|
1250
|
+
onMetricsChange={onMetricsChange}
|
|
1251
|
+
/>,
|
|
1252
|
+
);
|
|
1253
|
+
|
|
1254
|
+
// Find the Clear button inside metrics combobox (not the namespace-level Clear)
|
|
1255
|
+
const metricsSection = document.querySelector('.combobox-input');
|
|
1256
|
+
const clearBtn = metricsSection.querySelector('.combobox-action');
|
|
1257
|
+
fireEvent.click(clearBtn);
|
|
1258
|
+
|
|
1259
|
+
expect(onMetricsChange).toHaveBeenCalledWith([]);
|
|
1260
|
+
});
|
|
1261
|
+
});
|
|
1262
|
+
|
|
1263
|
+
describe('Dimensions combobox-input click handler (line 546)', () => {
|
|
1264
|
+
it('clicking dimensions combobox-input container does not throw', () => {
|
|
1265
|
+
render(
|
|
1266
|
+
<SelectionPanel {...defaultProps} selectedMetrics={['default.test']} />,
|
|
1267
|
+
);
|
|
1268
|
+
const comboboxInputs = document.querySelectorAll('.combobox-input');
|
|
1269
|
+
// The second combobox-input is for dimensions
|
|
1270
|
+
if (comboboxInputs.length >= 2) {
|
|
1271
|
+
expect(() => fireEvent.click(comboboxInputs[1])).not.toThrow();
|
|
1272
|
+
}
|
|
1273
|
+
});
|
|
1274
|
+
});
|
|
1275
|
+
|
|
1276
|
+
describe('Dimensions search input stopPropagation (line 586)', () => {
|
|
1277
|
+
it('clicking dimensions search input does not throw', () => {
|
|
1278
|
+
render(
|
|
1279
|
+
<SelectionPanel {...defaultProps} selectedMetrics={['default.test']} />,
|
|
1280
|
+
);
|
|
1281
|
+
const searchInput = screen.getByPlaceholderText('Search dimensions...');
|
|
1282
|
+
expect(() => fireEvent.click(searchInput)).not.toThrow();
|
|
1283
|
+
});
|
|
1284
|
+
});
|
|
1285
|
+
|
|
1286
|
+
describe('Clear dimensions button (lines 603-604)', () => {
|
|
1287
|
+
it('shows inline Clear button for dimensions when dimensions are selected', () => {
|
|
1288
|
+
render(
|
|
1289
|
+
<SelectionPanel
|
|
1290
|
+
{...defaultProps}
|
|
1291
|
+
selectedMetrics={['default.test']}
|
|
1292
|
+
selectedDimensions={['default.date_dim.dateint']}
|
|
1293
|
+
/>,
|
|
1294
|
+
);
|
|
1295
|
+
const clearBtns = document.querySelectorAll('.combobox-action');
|
|
1296
|
+
const clearDimBtns = Array.from(clearBtns).filter(
|
|
1297
|
+
btn => btn.textContent === 'Clear',
|
|
1298
|
+
);
|
|
1299
|
+
expect(clearDimBtns.length).toBeGreaterThanOrEqual(1);
|
|
1300
|
+
});
|
|
1301
|
+
|
|
1302
|
+
it('calls onDimensionsChange([]) when dimensions inline Clear is clicked', () => {
|
|
1303
|
+
const onDimensionsChange = jest.fn();
|
|
1304
|
+
render(
|
|
1305
|
+
<SelectionPanel
|
|
1306
|
+
{...defaultProps}
|
|
1307
|
+
selectedMetrics={['default.test']}
|
|
1308
|
+
selectedDimensions={[
|
|
1309
|
+
'default.date_dim.dateint',
|
|
1310
|
+
'default.date_dim.month',
|
|
1311
|
+
]}
|
|
1312
|
+
onDimensionsChange={onDimensionsChange}
|
|
1313
|
+
/>,
|
|
1314
|
+
);
|
|
1315
|
+
|
|
1316
|
+
// The combobox-action buttons — the last set belongs to dimensions
|
|
1317
|
+
const comboboxActions = document.querySelectorAll('.combobox-action');
|
|
1318
|
+
// Last Clear button is for dimensions (metrics Clear is first if only one metric selected
|
|
1319
|
+
// but here no many-metrics Show all button, so first Clear = metrics, second = dims)
|
|
1320
|
+
const clearBtns = Array.from(comboboxActions).filter(
|
|
1321
|
+
btn => btn.textContent === 'Clear',
|
|
1322
|
+
);
|
|
1323
|
+
// Click the last Clear button (dimensions)
|
|
1324
|
+
fireEvent.click(clearBtns[clearBtns.length - 1]);
|
|
1325
|
+
|
|
1326
|
+
expect(onDimensionsChange).toHaveBeenCalledWith([]);
|
|
1327
|
+
});
|
|
1328
|
+
});
|
|
1329
|
+
|
|
1330
|
+
describe('Run Query Section', () => {
|
|
1331
|
+
it('renders Run Query button', () => {
|
|
1332
|
+
render(<SelectionPanel {...defaultProps} />);
|
|
1333
|
+
expect(screen.getByText('Run Query')).toBeInTheDocument();
|
|
1334
|
+
});
|
|
1335
|
+
|
|
1336
|
+
it('Run Query button is disabled when canRunQuery is false', () => {
|
|
1337
|
+
render(<SelectionPanel {...defaultProps} canRunQuery={false} />);
|
|
1338
|
+
expect(screen.getByText('Run Query').closest('button')).toBeDisabled();
|
|
1339
|
+
});
|
|
1340
|
+
|
|
1341
|
+
it('Run Query button is enabled when canRunQuery is true', () => {
|
|
1342
|
+
render(
|
|
1343
|
+
<SelectionPanel
|
|
1344
|
+
{...defaultProps}
|
|
1345
|
+
canRunQuery={true}
|
|
1346
|
+
onRunQuery={jest.fn()}
|
|
1347
|
+
/>,
|
|
1348
|
+
);
|
|
1349
|
+
expect(
|
|
1350
|
+
screen.getByText('Run Query').closest('button'),
|
|
1351
|
+
).not.toBeDisabled();
|
|
1352
|
+
});
|
|
1353
|
+
|
|
1354
|
+
it('shows running state when queryLoading is true', () => {
|
|
1355
|
+
render(
|
|
1356
|
+
<SelectionPanel
|
|
1357
|
+
{...defaultProps}
|
|
1358
|
+
canRunQuery={true}
|
|
1359
|
+
queryLoading={true}
|
|
1360
|
+
onRunQuery={jest.fn()}
|
|
1361
|
+
/>,
|
|
1362
|
+
);
|
|
1363
|
+
expect(screen.getByText('Running...')).toBeInTheDocument();
|
|
1364
|
+
});
|
|
1365
|
+
|
|
1366
|
+
it('calls onRunQuery when Run Query is clicked', () => {
|
|
1367
|
+
const onRunQuery = jest.fn();
|
|
1368
|
+
render(
|
|
1369
|
+
<SelectionPanel
|
|
1370
|
+
{...defaultProps}
|
|
1371
|
+
canRunQuery={true}
|
|
1372
|
+
onRunQuery={onRunQuery}
|
|
1373
|
+
/>,
|
|
1374
|
+
);
|
|
1375
|
+
fireEvent.click(screen.getByText('Run Query').closest('button'));
|
|
1376
|
+
expect(onRunQuery).toHaveBeenCalled();
|
|
1377
|
+
});
|
|
1378
|
+
|
|
1379
|
+
it('shows hint when metrics selected but no dimensions', () => {
|
|
1380
|
+
render(
|
|
1381
|
+
<SelectionPanel
|
|
1382
|
+
{...defaultProps}
|
|
1383
|
+
selectedMetrics={['default.num_repair_orders']}
|
|
1384
|
+
canRunQuery={false}
|
|
1385
|
+
/>,
|
|
1386
|
+
);
|
|
1387
|
+
expect(
|
|
1388
|
+
screen.getByText('Select at least one dimension'),
|
|
1389
|
+
).toBeInTheDocument();
|
|
1390
|
+
});
|
|
1391
|
+
|
|
1392
|
+
it('shows hint when no metrics and no dimensions selected', () => {
|
|
1393
|
+
render(
|
|
1394
|
+
<SelectionPanel
|
|
1395
|
+
{...defaultProps}
|
|
1396
|
+
selectedMetrics={[]}
|
|
1397
|
+
canRunQuery={false}
|
|
1398
|
+
/>,
|
|
1399
|
+
);
|
|
1400
|
+
expect(
|
|
1401
|
+
screen.getByText('Select metrics and dimensions to run a query'),
|
|
1402
|
+
).toBeInTheDocument();
|
|
1403
|
+
});
|
|
1404
|
+
});
|
|
976
1405
|
});
|
|
@@ -67,10 +67,14 @@ export function QueryPlannerPage() {
|
|
|
67
67
|
const [queryError, setQueryError] = useState(null);
|
|
68
68
|
const [queryStartTime, setQueryStartTime] = useState(null);
|
|
69
69
|
const [queryElapsedTime, setQueryElapsedTime] = useState(null);
|
|
70
|
+
const [queryLinks, setQueryLinks] = useState([]);
|
|
70
71
|
|
|
71
72
|
// Filters state
|
|
72
73
|
const [filters, setFilters] = useState([]);
|
|
73
74
|
|
|
75
|
+
// Engine selection: null = auto, 'druid', 'trino'
|
|
76
|
+
const [selectedEngine, setSelectedEngine] = useState(null);
|
|
77
|
+
|
|
74
78
|
// Cube availability state (for displaying freshness info)
|
|
75
79
|
const [cubeAvailability, setCubeAvailability] = useState(null);
|
|
76
80
|
|
|
@@ -95,8 +99,13 @@ export function QueryPlannerPage() {
|
|
|
95
99
|
const urlMetrics = params.get('metrics')?.split(',').filter(Boolean) || [];
|
|
96
100
|
const urlDimensions =
|
|
97
101
|
params.get('dimensions')?.split(',').filter(Boolean) || [];
|
|
102
|
+
const urlFilters = params.getAll('filters');
|
|
98
103
|
const urlCube = params.get('cube');
|
|
99
104
|
|
|
105
|
+
if (urlFilters.length > 0) {
|
|
106
|
+
setFilters(urlFilters);
|
|
107
|
+
}
|
|
108
|
+
|
|
100
109
|
if (urlMetrics.length > 0) {
|
|
101
110
|
setSelectedMetrics(urlMetrics);
|
|
102
111
|
// Store dimensions to apply after commonDimensions are loaded
|
|
@@ -139,6 +148,7 @@ export function QueryPlannerPage() {
|
|
|
139
148
|
if (selectedDimensions.length > 0) {
|
|
140
149
|
params.set('dimensions', selectedDimensions.join(','));
|
|
141
150
|
}
|
|
151
|
+
filters.forEach(f => params.append('filters', f));
|
|
142
152
|
}
|
|
143
153
|
|
|
144
154
|
const newSearch = params.toString();
|
|
@@ -157,6 +167,7 @@ export function QueryPlannerPage() {
|
|
|
157
167
|
}, [
|
|
158
168
|
selectedMetrics,
|
|
159
169
|
selectedDimensions,
|
|
170
|
+
filters,
|
|
160
171
|
loadedCubeName,
|
|
161
172
|
location.pathname,
|
|
162
173
|
navigate,
|
|
@@ -248,7 +259,7 @@ export function QueryPlannerPage() {
|
|
|
248
259
|
}
|
|
249
260
|
}, [commonDimensions, selectedDimensions]);
|
|
250
261
|
|
|
251
|
-
// Fetch V3 measures and metrics SQL when selection changes
|
|
262
|
+
// Fetch V3 measures and metrics SQL when selection, filters, or engine changes
|
|
252
263
|
useEffect(() => {
|
|
253
264
|
const fetchData = async () => {
|
|
254
265
|
if (selectedMetrics.length > 0 && selectedDimensions.length > 0) {
|
|
@@ -256,10 +267,19 @@ export function QueryPlannerPage() {
|
|
|
256
267
|
setError(null);
|
|
257
268
|
setSelectedNode(null);
|
|
258
269
|
try {
|
|
270
|
+
// Derive useMaterialized and dialect from selectedEngine
|
|
271
|
+
const useMaterialized = selectedEngine !== 'trino';
|
|
272
|
+
const dialect = selectedEngine || null;
|
|
259
273
|
// Fetch both measures and metrics SQL in parallel
|
|
260
274
|
const [measures, metrics] = await Promise.all([
|
|
261
275
|
djClient.measuresV3(selectedMetrics, selectedDimensions),
|
|
262
|
-
djClient.metricsV3(
|
|
276
|
+
djClient.metricsV3(
|
|
277
|
+
selectedMetrics,
|
|
278
|
+
selectedDimensions,
|
|
279
|
+
filters,
|
|
280
|
+
useMaterialized,
|
|
281
|
+
dialect,
|
|
282
|
+
),
|
|
263
283
|
]);
|
|
264
284
|
setMeasuresResult(measures);
|
|
265
285
|
setMetricsResult(metrics);
|
|
@@ -275,7 +295,7 @@ export function QueryPlannerPage() {
|
|
|
275
295
|
}
|
|
276
296
|
};
|
|
277
297
|
fetchData().catch(console.error);
|
|
278
|
-
}, [djClient, selectedMetrics, selectedDimensions]);
|
|
298
|
+
}, [djClient, selectedMetrics, selectedDimensions, filters, selectedEngine]);
|
|
279
299
|
|
|
280
300
|
// Fetch existing pre-aggregations for the grain groups
|
|
281
301
|
useEffect(() => {
|
|
@@ -1134,6 +1154,7 @@ export function QueryPlannerPage() {
|
|
|
1134
1154
|
setQueryLoading(true);
|
|
1135
1155
|
setQueryError(null);
|
|
1136
1156
|
setQueryResults(null);
|
|
1157
|
+
setQueryLinks([]);
|
|
1137
1158
|
setShowResults(true);
|
|
1138
1159
|
const startTime = Date.now();
|
|
1139
1160
|
setQueryStartTime(startTime);
|
|
@@ -1145,6 +1166,8 @@ export function QueryPlannerPage() {
|
|
|
1145
1166
|
selectedMetrics,
|
|
1146
1167
|
selectedDimensions,
|
|
1147
1168
|
filters,
|
|
1169
|
+
selectedEngine,
|
|
1170
|
+
progress => setQueryLinks(progress.links || []),
|
|
1148
1171
|
);
|
|
1149
1172
|
const elapsed = (Date.now() - startTime) / 1000;
|
|
1150
1173
|
setQueryElapsedTime(elapsed);
|
|
@@ -1155,7 +1178,7 @@ export function QueryPlannerPage() {
|
|
|
1155
1178
|
} finally {
|
|
1156
1179
|
setQueryLoading(false);
|
|
1157
1180
|
}
|
|
1158
|
-
}, [djClient, selectedMetrics, selectedDimensions, filters]);
|
|
1181
|
+
}, [djClient, selectedMetrics, selectedDimensions, filters, selectedEngine]);
|
|
1159
1182
|
|
|
1160
1183
|
// Handle back to plan view
|
|
1161
1184
|
const handleBackToPlan = useCallback(() => {
|
|
@@ -1174,7 +1197,7 @@ export function QueryPlannerPage() {
|
|
|
1174
1197
|
{/* Header */}
|
|
1175
1198
|
<header className="planner-header">
|
|
1176
1199
|
<div className="planner-header-content">
|
|
1177
|
-
<h1>
|
|
1200
|
+
<h1>Explore</h1>
|
|
1178
1201
|
{/* <p>Explore metrics and dimensions and plan materializations</p> */}
|
|
1179
1202
|
</div>
|
|
1180
1203
|
{error && <div className="header-error">{error}</div>}
|
|
@@ -1198,6 +1221,8 @@ export function QueryPlannerPage() {
|
|
|
1198
1221
|
onClearSelection={handleClearSelection}
|
|
1199
1222
|
filters={filters}
|
|
1200
1223
|
onFiltersChange={handleFiltersChange}
|
|
1224
|
+
selectedEngine={selectedEngine}
|
|
1225
|
+
onEngineChange={setSelectedEngine}
|
|
1201
1226
|
onRunQuery={handleRunQuery}
|
|
1202
1227
|
canRunQuery={
|
|
1203
1228
|
selectedMetrics.length > 0 && selectedDimensions.length > 0
|
|
@@ -1221,6 +1246,7 @@ export function QueryPlannerPage() {
|
|
|
1221
1246
|
dialect={metricsResult?.dialect}
|
|
1222
1247
|
cubeName={metricsResult?.cube_name}
|
|
1223
1248
|
availability={cubeAvailability}
|
|
1249
|
+
links={queryLinks}
|
|
1224
1250
|
/>
|
|
1225
1251
|
) : (
|
|
1226
1252
|
<>
|