datajunction-ui 0.0.94 → 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.
@@ -714,8 +714,10 @@ describe('SelectionPanel', () => {
714
714
  />,
715
715
  );
716
716
 
717
- // Should show the path
718
- expect(screen.getByText('default.date_dim.dateint')).toBeInTheDocument();
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(selectedMetrics, selectedDimensions),
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>Query Planner</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
  <>