@bygd/nc-report-ui 0.1.19 → 0.1.21
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/dist/default/cjs/index.cjs +1 -1
- package/dist/default/esm/index.js +3917 -15
- package/dist/default/iife/index.js +149 -23
- package/package.json +7 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as React from 'react';
|
|
2
|
-
import React__default, { useEffect, useMemo, useState, useRef } from 'react';
|
|
2
|
+
import React__default, { useEffect, useMemo, useState, useRef, createContext, useContext } from 'react';
|
|
3
3
|
import Paper from '@material-ui/core/Paper';
|
|
4
4
|
import { makeStyles } from '@material-ui/core/styles';
|
|
5
5
|
import { CircularProgress } from '@material-ui/core';
|
|
@@ -12,7 +12,7 @@ import FormControl$2 from '@material-ui/core/FormControl';
|
|
|
12
12
|
import Select$1 from '@material-ui/core/Select';
|
|
13
13
|
import MenuItem$1 from '@material-ui/core/MenuItem';
|
|
14
14
|
import { useInView } from 'react-intersection-observer';
|
|
15
|
-
import { FormControl, Autocomplete, TextField, CircularProgress as CircularProgress$1, Chip, Checkbox, FormHelperText } from '@mui/material';
|
|
15
|
+
import { FormControl, Autocomplete, TextField, CircularProgress as CircularProgress$1, Chip, Checkbox, FormHelperText, Snackbar, Alert, Dialog, DialogTitle, DialogContent, DialogContentText, DialogActions, Button, Box as Box$1, Typography as Typography$1, Tooltip, IconButton, Paper as Paper$1, Badge, Tabs, Tab } from '@mui/material';
|
|
16
16
|
import CheckBoxOutlineBlankIcon from '@mui/icons-material/CheckBoxOutlineBlank';
|
|
17
17
|
import CheckBoxIcon from '@mui/icons-material/CheckBox';
|
|
18
18
|
import Box from '@mui/material/Box';
|
|
@@ -23,6 +23,31 @@ import Select from '@mui/material/Select';
|
|
|
23
23
|
import EventEmitter from 'eventemitter3';
|
|
24
24
|
import Grid from '@material-ui/core/Grid';
|
|
25
25
|
import Container from '@material-ui/core/Container';
|
|
26
|
+
import { DataGrid } from '@mui/x-data-grid';
|
|
27
|
+
import AddIcon from '@mui/icons-material/Add';
|
|
28
|
+
import EditIcon from '@mui/icons-material/Edit';
|
|
29
|
+
import ContentCopyIcon from '@mui/icons-material/ContentCopy';
|
|
30
|
+
import DeleteIcon from '@mui/icons-material/Delete';
|
|
31
|
+
import PlayArrowIcon from '@mui/icons-material/PlayArrow';
|
|
32
|
+
import ArrowBackIcon from '@mui/icons-material/ArrowBack';
|
|
33
|
+
import SaveIcon from '@mui/icons-material/Save';
|
|
34
|
+
import DownloadIcon from '@mui/icons-material/Download';
|
|
35
|
+
import { useSensors, useSensor, PointerSensor, KeyboardSensor, DndContext, closestCenter } from '@dnd-kit/core';
|
|
36
|
+
import { sortableKeyboardCoordinates, SortableContext, verticalListSortingStrategy, arrayMove, useSortable } from '@dnd-kit/sortable';
|
|
37
|
+
import { CSS } from '@dnd-kit/utilities';
|
|
38
|
+
import DragIndicatorIcon from '@mui/icons-material/DragIndicator';
|
|
39
|
+
import ArrowUpwardIcon from '@mui/icons-material/ArrowUpward';
|
|
40
|
+
import ArrowDownwardIcon from '@mui/icons-material/ArrowDownward';
|
|
41
|
+
import SortIcon from '@mui/icons-material/Sort';
|
|
42
|
+
import CheckIcon from '@mui/icons-material/Check';
|
|
43
|
+
import CloseIcon from '@mui/icons-material/Close';
|
|
44
|
+
import RestartAltIcon from '@mui/icons-material/RestartAlt';
|
|
45
|
+
import PlaylistAddIcon from '@mui/icons-material/PlaylistAdd';
|
|
46
|
+
import FilterAltIcon from '@mui/icons-material/FilterAlt';
|
|
47
|
+
import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider';
|
|
48
|
+
import { DatePicker } from '@mui/x-date-pickers/DatePicker';
|
|
49
|
+
import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs';
|
|
50
|
+
import dayjs from 'dayjs';
|
|
26
51
|
|
|
27
52
|
function _extends() {
|
|
28
53
|
return _extends = Object.assign ? Object.assign.bind() : function (n) {
|
|
@@ -280,6 +305,43 @@ const Api = {
|
|
|
280
305
|
} = await apiClient.post(`/report-build/run`, report);
|
|
281
306
|
return data;
|
|
282
307
|
},
|
|
308
|
+
/**
|
|
309
|
+
* Download ad-hoc report as CSV
|
|
310
|
+
* @param {Object} params - Parameters object
|
|
311
|
+
* @param {Object} params.report - Report definition object containing dimensions, metrics, etc.
|
|
312
|
+
* @param {string} [params.filename] - Optional filename for the downloaded file
|
|
313
|
+
* @returns {Promise} Downloads CSV file
|
|
314
|
+
*/
|
|
315
|
+
downloadAdHocReport: async ({
|
|
316
|
+
report,
|
|
317
|
+
filename
|
|
318
|
+
}) => {
|
|
319
|
+
const response = await apiClient.post(`/report-build/run?download=csv`, report, {
|
|
320
|
+
responseType: 'blob'
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
// Create a download link and trigger download
|
|
324
|
+
const url = window.URL.createObjectURL(new Blob([response.data]));
|
|
325
|
+
const link = document.createElement('a');
|
|
326
|
+
link.href = url;
|
|
327
|
+
|
|
328
|
+
// Use provided filename, or extract from Content-Disposition header, or use default
|
|
329
|
+
let finalFilename = filename || 'report.csv';
|
|
330
|
+
if (!filename) {
|
|
331
|
+
const contentDisposition = response.headers['content-disposition'];
|
|
332
|
+
if (contentDisposition) {
|
|
333
|
+
const filenameMatch = contentDisposition.match(/filename="?(.+)"?/i);
|
|
334
|
+
if (filenameMatch && filenameMatch[1]) {
|
|
335
|
+
finalFilename = filenameMatch[1];
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
link.setAttribute('download', finalFilename);
|
|
340
|
+
document.body.appendChild(link);
|
|
341
|
+
link.click();
|
|
342
|
+
link.remove();
|
|
343
|
+
window.URL.revokeObjectURL(url);
|
|
344
|
+
},
|
|
283
345
|
getDateRanges: async ({
|
|
284
346
|
dashboardId
|
|
285
347
|
} = {}) => {
|
|
@@ -747,7 +809,8 @@ function SingleSelect({
|
|
|
747
809
|
onChange,
|
|
748
810
|
sx = {
|
|
749
811
|
width: '100%'
|
|
750
|
-
}
|
|
812
|
+
},
|
|
813
|
+
disabled = false
|
|
751
814
|
}) {
|
|
752
815
|
// Check if the current value exists in items, otherwise use empty string
|
|
753
816
|
const validValue = items.some(itm => itm.key === value) ? value : '';
|
|
@@ -763,6 +826,7 @@ function SingleSelect({
|
|
|
763
826
|
value: validValue,
|
|
764
827
|
label: label,
|
|
765
828
|
onChange: onChange,
|
|
829
|
+
disabled: disabled,
|
|
766
830
|
MenuProps: {
|
|
767
831
|
style: {
|
|
768
832
|
maxHeight: '300px'
|
|
@@ -1129,11 +1193,67 @@ var Chart = ({
|
|
|
1129
1193
|
}), chartType)));
|
|
1130
1194
|
};
|
|
1131
1195
|
|
|
1196
|
+
// Create the context
|
|
1197
|
+
const ReportingContext = /*#__PURE__*/createContext(undefined);
|
|
1198
|
+
|
|
1199
|
+
/**
|
|
1200
|
+
* ReportingProvider - Provides reporting configuration context to child components
|
|
1201
|
+
*
|
|
1202
|
+
* @param {Object} props
|
|
1203
|
+
* @param {Object} [props.defaultParameters] - Default parameters for reports (base_currency, merchants, client, etc.)
|
|
1204
|
+
* @param {Object} [props.defaultApi] - Default API configuration (token, base_url)
|
|
1205
|
+
* @param {React.ReactNode} props.children - Child components
|
|
1206
|
+
*/
|
|
1207
|
+
const ReportingProvider = ({
|
|
1208
|
+
defaultParameters = {},
|
|
1209
|
+
defaultApi = {},
|
|
1210
|
+
children
|
|
1211
|
+
}) => {
|
|
1212
|
+
const [parameters, setParameters] = useState(defaultParameters);
|
|
1213
|
+
const [api, setApi] = useState(defaultApi);
|
|
1214
|
+
const value = {
|
|
1215
|
+
parameters,
|
|
1216
|
+
setParameters,
|
|
1217
|
+
api,
|
|
1218
|
+
setApi,
|
|
1219
|
+
// Convenience method to update both at once
|
|
1220
|
+
setReportingContext: ({
|
|
1221
|
+
parameters: newParams,
|
|
1222
|
+
api: newApi
|
|
1223
|
+
}) => {
|
|
1224
|
+
if (newParams !== undefined) {
|
|
1225
|
+
setParameters(newParams);
|
|
1226
|
+
}
|
|
1227
|
+
if (newApi !== undefined) {
|
|
1228
|
+
setApi(newApi);
|
|
1229
|
+
}
|
|
1230
|
+
}
|
|
1231
|
+
};
|
|
1232
|
+
return /*#__PURE__*/React__default.createElement(ReportingContext.Provider, {
|
|
1233
|
+
value: value
|
|
1234
|
+
}, children);
|
|
1235
|
+
};
|
|
1236
|
+
|
|
1237
|
+
/**
|
|
1238
|
+
* useReportingContextOptional - Hook to optionally access reporting context
|
|
1239
|
+
* Returns null if used outside of ReportingProvider (useful for optional fallback)
|
|
1240
|
+
*
|
|
1241
|
+
* @returns {Object|null} Context value or null if not within provider
|
|
1242
|
+
*/
|
|
1243
|
+
const useReportingContextOptional = () => {
|
|
1244
|
+
return useContext(ReportingContext);
|
|
1245
|
+
};
|
|
1246
|
+
|
|
1132
1247
|
function Dashboard({
|
|
1133
1248
|
id = "sample_dashboard",
|
|
1134
1249
|
api,
|
|
1135
1250
|
params
|
|
1136
1251
|
}) {
|
|
1252
|
+
const reportingContext = useReportingContextOptional();
|
|
1253
|
+
|
|
1254
|
+
// Use props if provided, otherwise fall back to context
|
|
1255
|
+
const finalApi = api || reportingContext?.api || {};
|
|
1256
|
+
const finalParams = params || reportingContext?.parameters || {};
|
|
1137
1257
|
const [dashboard, setDashboard] = React__default.useState();
|
|
1138
1258
|
//const [schema, setSchema] = React.useState();
|
|
1139
1259
|
const [schema] = React__default.useState();
|
|
@@ -1148,17 +1268,17 @@ function Dashboard({
|
|
|
1148
1268
|
}, {
|
|
1149
1269
|
schema
|
|
1150
1270
|
}, {
|
|
1151
|
-
params
|
|
1271
|
+
params: finalParams
|
|
1152
1272
|
});
|
|
1153
1273
|
useEffect(() => {
|
|
1154
|
-
// console.log('token changed',
|
|
1274
|
+
// console.log('token changed',finalApi);
|
|
1155
1275
|
|
|
1156
|
-
Api.setBaseUrl(
|
|
1157
|
-
Api.setToken(
|
|
1158
|
-
}, [
|
|
1276
|
+
Api.setBaseUrl(finalApi.base_url);
|
|
1277
|
+
Api.setToken(finalApi.token);
|
|
1278
|
+
}, [finalApi]);
|
|
1159
1279
|
const init = async () => {
|
|
1160
|
-
Api.setBaseUrl(
|
|
1161
|
-
Api.setToken(
|
|
1280
|
+
Api.setBaseUrl(finalApi.base_url);
|
|
1281
|
+
Api.setToken(finalApi.token);
|
|
1162
1282
|
await Api.loadDashboardMeta({
|
|
1163
1283
|
dashboardId: id
|
|
1164
1284
|
});
|
|
@@ -1258,14 +1378,14 @@ function Dashboard({
|
|
|
1258
1378
|
xs: 12,
|
|
1259
1379
|
md: md
|
|
1260
1380
|
}, /*#__PURE__*/React__default.createElement(Chart, {
|
|
1261
|
-
api:
|
|
1381
|
+
api: finalApi,
|
|
1262
1382
|
cache: cache.current,
|
|
1263
1383
|
id: column.id,
|
|
1264
1384
|
dashboard: dashboard,
|
|
1265
1385
|
schema: schema,
|
|
1266
1386
|
title: column.title,
|
|
1267
1387
|
filter: column.filter,
|
|
1268
|
-
params:
|
|
1388
|
+
params: finalParams
|
|
1269
1389
|
}))))) :
|
|
1270
1390
|
// All other rows behave normally
|
|
1271
1391
|
row.columns.map((column, i) => /*#__PURE__*/React__default.createElement(Grid, {
|
|
@@ -1274,22 +1394,3804 @@ function Dashboard({
|
|
|
1274
1394
|
xs: 12,
|
|
1275
1395
|
md: md
|
|
1276
1396
|
}, /*#__PURE__*/React__default.createElement(Chart, {
|
|
1277
|
-
api:
|
|
1397
|
+
api: finalApi,
|
|
1278
1398
|
cache: cache.current,
|
|
1279
1399
|
id: column.id,
|
|
1280
1400
|
dashboard: dashboard,
|
|
1281
1401
|
schema: schema,
|
|
1282
1402
|
title: column.title,
|
|
1283
1403
|
filter: column.filter,
|
|
1284
|
-
params:
|
|
1404
|
+
params: finalParams
|
|
1285
1405
|
}))));
|
|
1286
1406
|
})))
|
|
1287
1407
|
);
|
|
1288
1408
|
}
|
|
1289
1409
|
|
|
1410
|
+
// Create a context for the notification system
|
|
1411
|
+
const NotifyContext = /*#__PURE__*/createContext();
|
|
1412
|
+
|
|
1413
|
+
/**
|
|
1414
|
+
* NotifyProvider component that wraps your app to provide notification functionality
|
|
1415
|
+
*
|
|
1416
|
+
* Usage:
|
|
1417
|
+
* Wrap your app with <NotifyProvider>:
|
|
1418
|
+
*
|
|
1419
|
+
* <NotifyProvider>
|
|
1420
|
+
* <App />
|
|
1421
|
+
* </NotifyProvider>
|
|
1422
|
+
*/
|
|
1423
|
+
const NotifyProvider = ({
|
|
1424
|
+
children
|
|
1425
|
+
}) => {
|
|
1426
|
+
const [notification, setNotification] = useState({
|
|
1427
|
+
open: false,
|
|
1428
|
+
message: '',
|
|
1429
|
+
severity: 'info',
|
|
1430
|
+
// 'success' | 'error' | 'warning' | 'info'
|
|
1431
|
+
duration: 6000
|
|
1432
|
+
});
|
|
1433
|
+
const showNotification = (message, severity = 'info', duration = 6000) => {
|
|
1434
|
+
setNotification({
|
|
1435
|
+
open: true,
|
|
1436
|
+
message,
|
|
1437
|
+
severity,
|
|
1438
|
+
duration
|
|
1439
|
+
});
|
|
1440
|
+
};
|
|
1441
|
+
const handleClose = (_event, reason) => {
|
|
1442
|
+
// Don't close on clickaway to ensure user sees the message
|
|
1443
|
+
if (reason === 'clickaway') {
|
|
1444
|
+
return;
|
|
1445
|
+
}
|
|
1446
|
+
setNotification(prev => ({
|
|
1447
|
+
...prev,
|
|
1448
|
+
open: false
|
|
1449
|
+
}));
|
|
1450
|
+
};
|
|
1451
|
+
const notify = {
|
|
1452
|
+
success: (message, duration) => showNotification(message, 'success', duration),
|
|
1453
|
+
error: (message, duration) => showNotification(message, 'error', duration),
|
|
1454
|
+
warning: (message, duration) => showNotification(message, 'warning', duration),
|
|
1455
|
+
info: (message, duration) => showNotification(message, 'info', duration)
|
|
1456
|
+
};
|
|
1457
|
+
return /*#__PURE__*/React__default.createElement(NotifyContext.Provider, {
|
|
1458
|
+
value: notify
|
|
1459
|
+
}, children, /*#__PURE__*/React__default.createElement(Snackbar, {
|
|
1460
|
+
open: notification.open,
|
|
1461
|
+
autoHideDuration: notification.duration,
|
|
1462
|
+
onClose: handleClose,
|
|
1463
|
+
anchorOrigin: {
|
|
1464
|
+
vertical: 'top',
|
|
1465
|
+
horizontal: 'right'
|
|
1466
|
+
}
|
|
1467
|
+
}, /*#__PURE__*/React__default.createElement(Alert, {
|
|
1468
|
+
onClose: handleClose,
|
|
1469
|
+
severity: notification.severity,
|
|
1470
|
+
variant: "filled",
|
|
1471
|
+
sx: {
|
|
1472
|
+
width: '100%'
|
|
1473
|
+
}
|
|
1474
|
+
}, notification.message)));
|
|
1475
|
+
};
|
|
1476
|
+
|
|
1477
|
+
/**
|
|
1478
|
+
* Hook to use notifications in any component
|
|
1479
|
+
*
|
|
1480
|
+
* Usage in components:
|
|
1481
|
+
*
|
|
1482
|
+
* import { useNotify } from '../components/Notify';
|
|
1483
|
+
*
|
|
1484
|
+
* const MyComponent = () => {
|
|
1485
|
+
* const notify = useNotify();
|
|
1486
|
+
*
|
|
1487
|
+
* const handleSuccess = () => {
|
|
1488
|
+
* notify.success('Operation completed successfully!');
|
|
1489
|
+
* };
|
|
1490
|
+
*
|
|
1491
|
+
* const handleError = () => {
|
|
1492
|
+
* notify.error('Something went wrong!');
|
|
1493
|
+
* };
|
|
1494
|
+
*
|
|
1495
|
+
* const handleWarning = () => {
|
|
1496
|
+
* notify.warning('Please be careful!');
|
|
1497
|
+
* };
|
|
1498
|
+
*
|
|
1499
|
+
* const handleInfo = () => {
|
|
1500
|
+
* notify.info('Here is some information');
|
|
1501
|
+
* };
|
|
1502
|
+
*
|
|
1503
|
+
* return <button onClick={handleSuccess}>Click me</button>;
|
|
1504
|
+
* };
|
|
1505
|
+
*/
|
|
1506
|
+
const useNotify = () => {
|
|
1507
|
+
const context = useContext(NotifyContext);
|
|
1508
|
+
if (!context) {
|
|
1509
|
+
throw new Error('useNotify must be used within a NotifyProvider');
|
|
1510
|
+
}
|
|
1511
|
+
return context;
|
|
1512
|
+
};
|
|
1513
|
+
|
|
1514
|
+
/**
|
|
1515
|
+
* Reusable confirmation dialog component
|
|
1516
|
+
* @param {boolean} open - Whether the dialog is open
|
|
1517
|
+
* @param {string} title - Dialog title
|
|
1518
|
+
* @param {string} message - Dialog message/content
|
|
1519
|
+
* @param {function} onConfirm - Callback when user confirms
|
|
1520
|
+
* @param {function} onCancel - Callback when user cancels
|
|
1521
|
+
* @param {string} confirmText - Text for confirm button (default: "Confirm")
|
|
1522
|
+
* @param {string} cancelText - Text for cancel button (default: "Cancel")
|
|
1523
|
+
* @param {string} confirmColor - Color for confirm button (default: "error")
|
|
1524
|
+
*/
|
|
1525
|
+
const ConfirmDialog = ({
|
|
1526
|
+
open,
|
|
1527
|
+
title = 'Confirm Action',
|
|
1528
|
+
message,
|
|
1529
|
+
onConfirm,
|
|
1530
|
+
onCancel,
|
|
1531
|
+
confirmText = 'Confirm',
|
|
1532
|
+
cancelText = 'Cancel',
|
|
1533
|
+
confirmColor = 'error'
|
|
1534
|
+
}) => {
|
|
1535
|
+
return /*#__PURE__*/React__default.createElement(Dialog, {
|
|
1536
|
+
open: open,
|
|
1537
|
+
onClose: onCancel,
|
|
1538
|
+
"aria-labelledby": "confirm-dialog-title",
|
|
1539
|
+
"aria-describedby": "confirm-dialog-description"
|
|
1540
|
+
}, /*#__PURE__*/React__default.createElement(DialogTitle, {
|
|
1541
|
+
id: "confirm-dialog-title"
|
|
1542
|
+
}, title), /*#__PURE__*/React__default.createElement(DialogContent, null, /*#__PURE__*/React__default.createElement(DialogContentText, {
|
|
1543
|
+
id: "confirm-dialog-description"
|
|
1544
|
+
}, message)), /*#__PURE__*/React__default.createElement(DialogActions, null, /*#__PURE__*/React__default.createElement(Button, {
|
|
1545
|
+
onClick: onCancel,
|
|
1546
|
+
variant: "outlined"
|
|
1547
|
+
}, cancelText), /*#__PURE__*/React__default.createElement(Button, {
|
|
1548
|
+
onClick: onConfirm,
|
|
1549
|
+
variant: "contained",
|
|
1550
|
+
color: confirmColor,
|
|
1551
|
+
autoFocus: true
|
|
1552
|
+
}, confirmText)));
|
|
1553
|
+
};
|
|
1554
|
+
|
|
1555
|
+
const ReportDefinitionsList = ({
|
|
1556
|
+
onSelectReport,
|
|
1557
|
+
onAddNew,
|
|
1558
|
+
onCloneReport,
|
|
1559
|
+
onRunReport,
|
|
1560
|
+
refreshTrigger
|
|
1561
|
+
}) => {
|
|
1562
|
+
const notify = useNotify();
|
|
1563
|
+
const [reportDefinitions, setReportDefinitions] = useState([]);
|
|
1564
|
+
const [loading, setLoading] = useState(true);
|
|
1565
|
+
const [error, setError] = useState(null);
|
|
1566
|
+
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
|
1567
|
+
const [reportToDelete, setReportToDelete] = useState(null);
|
|
1568
|
+
useEffect(() => {
|
|
1569
|
+
loadReportDefinitions();
|
|
1570
|
+
}, [refreshTrigger]);
|
|
1571
|
+
const loadReportDefinitions = async () => {
|
|
1572
|
+
try {
|
|
1573
|
+
setLoading(true);
|
|
1574
|
+
setError(null);
|
|
1575
|
+
const data = await Api.getReportDefinitions();
|
|
1576
|
+
setReportDefinitions(data);
|
|
1577
|
+
} catch (err) {
|
|
1578
|
+
console.error('Error loading report definitions:', err);
|
|
1579
|
+
const errorMessage = 'Failed to load report definitions: ' + (err.message || 'Unknown error');
|
|
1580
|
+
setError(errorMessage);
|
|
1581
|
+
notify.error(errorMessage);
|
|
1582
|
+
} finally {
|
|
1583
|
+
setLoading(false);
|
|
1584
|
+
}
|
|
1585
|
+
};
|
|
1586
|
+
const handleEdit = (id, event) => {
|
|
1587
|
+
event.stopPropagation();
|
|
1588
|
+
onSelectReport(id);
|
|
1589
|
+
};
|
|
1590
|
+
const handleRun = (id, event) => {
|
|
1591
|
+
event.stopPropagation();
|
|
1592
|
+
onRunReport(id);
|
|
1593
|
+
};
|
|
1594
|
+
const handleClone = async (id, event) => {
|
|
1595
|
+
event.stopPropagation();
|
|
1596
|
+
try {
|
|
1597
|
+
// Load the full report definition
|
|
1598
|
+
const reportDef = await Api.getReportDefinition({
|
|
1599
|
+
id
|
|
1600
|
+
});
|
|
1601
|
+
|
|
1602
|
+
// Modify the title to indicate it's a copy
|
|
1603
|
+
const clonedReport = {
|
|
1604
|
+
...reportDef,
|
|
1605
|
+
title: `${reportDef.title} (Copy)`
|
|
1606
|
+
};
|
|
1607
|
+
|
|
1608
|
+
// Pass the cloned report data to the parent
|
|
1609
|
+
onCloneReport(clonedReport);
|
|
1610
|
+
} catch (error) {
|
|
1611
|
+
console.error('Error cloning report:', error);
|
|
1612
|
+
notify.error('Failed to clone report: ' + (error.message || 'Unknown error'));
|
|
1613
|
+
}
|
|
1614
|
+
};
|
|
1615
|
+
const handleDelete = (id, event) => {
|
|
1616
|
+
event.stopPropagation();
|
|
1617
|
+
// Find the report to get its title for the confirmation dialog
|
|
1618
|
+
const report = reportDefinitions.find(r => r.id === id);
|
|
1619
|
+
setReportToDelete(report);
|
|
1620
|
+
setDeleteDialogOpen(true);
|
|
1621
|
+
};
|
|
1622
|
+
const handleConfirmDelete = async () => {
|
|
1623
|
+
if (!reportToDelete) return;
|
|
1624
|
+
try {
|
|
1625
|
+
await Api.deleteReportDefinition({
|
|
1626
|
+
id: reportToDelete.id
|
|
1627
|
+
});
|
|
1628
|
+
notify.success(`Report "${reportToDelete.title}" deleted successfully!`);
|
|
1629
|
+
|
|
1630
|
+
// Close dialog and reset state
|
|
1631
|
+
setDeleteDialogOpen(false);
|
|
1632
|
+
setReportToDelete(null);
|
|
1633
|
+
|
|
1634
|
+
// Reload the report definitions list
|
|
1635
|
+
loadReportDefinitions();
|
|
1636
|
+
} catch (error) {
|
|
1637
|
+
console.error('Error deleting report:', error);
|
|
1638
|
+
notify.error('Failed to delete report: ' + (error.response?.data?.message || error.message || 'Unknown error'));
|
|
1639
|
+
setDeleteDialogOpen(false);
|
|
1640
|
+
setReportToDelete(null);
|
|
1641
|
+
}
|
|
1642
|
+
};
|
|
1643
|
+
const handleCancelDelete = () => {
|
|
1644
|
+
setDeleteDialogOpen(false);
|
|
1645
|
+
setReportToDelete(null);
|
|
1646
|
+
};
|
|
1647
|
+
const columns = [{
|
|
1648
|
+
field: 'title',
|
|
1649
|
+
headerName: 'Title',
|
|
1650
|
+
flex: 1,
|
|
1651
|
+
minWidth: 200
|
|
1652
|
+
}, {
|
|
1653
|
+
field: 'provider',
|
|
1654
|
+
headerName: 'Provider',
|
|
1655
|
+
flex: 1,
|
|
1656
|
+
minWidth: 200
|
|
1657
|
+
}, {
|
|
1658
|
+
field: 'actions',
|
|
1659
|
+
headerName: 'Actions',
|
|
1660
|
+
width: 200,
|
|
1661
|
+
sortable: false,
|
|
1662
|
+
filterable: false,
|
|
1663
|
+
disableColumnMenu: true,
|
|
1664
|
+
renderCell: params => /*#__PURE__*/React__default.createElement(Box$1, {
|
|
1665
|
+
sx: {
|
|
1666
|
+
display: 'flex',
|
|
1667
|
+
gap: 1
|
|
1668
|
+
}
|
|
1669
|
+
}, /*#__PURE__*/React__default.createElement(Tooltip, {
|
|
1670
|
+
title: "Run"
|
|
1671
|
+
}, /*#__PURE__*/React__default.createElement(IconButton, {
|
|
1672
|
+
size: "small",
|
|
1673
|
+
color: "success",
|
|
1674
|
+
onClick: e => handleRun(params.row.id, e)
|
|
1675
|
+
}, /*#__PURE__*/React__default.createElement(PlayArrowIcon, {
|
|
1676
|
+
fontSize: "small"
|
|
1677
|
+
}))), /*#__PURE__*/React__default.createElement(Tooltip, {
|
|
1678
|
+
title: "Edit"
|
|
1679
|
+
}, /*#__PURE__*/React__default.createElement(IconButton, {
|
|
1680
|
+
size: "small",
|
|
1681
|
+
color: "primary",
|
|
1682
|
+
onClick: e => handleEdit(params.row.id, e)
|
|
1683
|
+
}, /*#__PURE__*/React__default.createElement(EditIcon, {
|
|
1684
|
+
fontSize: "small"
|
|
1685
|
+
}))), /*#__PURE__*/React__default.createElement(Tooltip, {
|
|
1686
|
+
title: "Clone"
|
|
1687
|
+
}, /*#__PURE__*/React__default.createElement(IconButton, {
|
|
1688
|
+
size: "small",
|
|
1689
|
+
onClick: e => handleClone(params.row.id, e)
|
|
1690
|
+
}, /*#__PURE__*/React__default.createElement(ContentCopyIcon, {
|
|
1691
|
+
fontSize: "small"
|
|
1692
|
+
}))), /*#__PURE__*/React__default.createElement(Tooltip, {
|
|
1693
|
+
title: "Delete"
|
|
1694
|
+
}, /*#__PURE__*/React__default.createElement(IconButton, {
|
|
1695
|
+
size: "small",
|
|
1696
|
+
color: "error",
|
|
1697
|
+
onClick: e => handleDelete(params.row.id, e)
|
|
1698
|
+
}, /*#__PURE__*/React__default.createElement(DeleteIcon, {
|
|
1699
|
+
fontSize: "small"
|
|
1700
|
+
}))))
|
|
1701
|
+
}];
|
|
1702
|
+
const rows = reportDefinitions.map(def => ({
|
|
1703
|
+
id: def.id,
|
|
1704
|
+
title: def.title,
|
|
1705
|
+
provider: def.provider
|
|
1706
|
+
}));
|
|
1707
|
+
const handleRowClick = params => {
|
|
1708
|
+
onSelectReport(params.row.id);
|
|
1709
|
+
};
|
|
1710
|
+
if (loading) {
|
|
1711
|
+
return /*#__PURE__*/React__default.createElement(Box$1, {
|
|
1712
|
+
display: "flex",
|
|
1713
|
+
justifyContent: "center",
|
|
1714
|
+
alignItems: "center",
|
|
1715
|
+
minHeight: 400
|
|
1716
|
+
}, /*#__PURE__*/React__default.createElement(CircularProgress$1, null));
|
|
1717
|
+
}
|
|
1718
|
+
if (error) {
|
|
1719
|
+
return /*#__PURE__*/React__default.createElement(Box$1, {
|
|
1720
|
+
display: "flex",
|
|
1721
|
+
flexDirection: "column",
|
|
1722
|
+
justifyContent: "center",
|
|
1723
|
+
alignItems: "center",
|
|
1724
|
+
minHeight: 400,
|
|
1725
|
+
gap: 2
|
|
1726
|
+
}, /*#__PURE__*/React__default.createElement(Typography$1, {
|
|
1727
|
+
variant: "body1",
|
|
1728
|
+
color: "error"
|
|
1729
|
+
}, error), /*#__PURE__*/React__default.createElement(Button, {
|
|
1730
|
+
variant: "contained",
|
|
1731
|
+
onClick: loadReportDefinitions
|
|
1732
|
+
}, "Retry"));
|
|
1733
|
+
}
|
|
1734
|
+
return /*#__PURE__*/React__default.createElement(Box$1, {
|
|
1735
|
+
sx: {
|
|
1736
|
+
p: 3
|
|
1737
|
+
}
|
|
1738
|
+
}, /*#__PURE__*/React__default.createElement(Box$1, {
|
|
1739
|
+
sx: {
|
|
1740
|
+
display: 'flex',
|
|
1741
|
+
justifyContent: 'space-between',
|
|
1742
|
+
alignItems: 'center',
|
|
1743
|
+
mb: 3
|
|
1744
|
+
}
|
|
1745
|
+
}, /*#__PURE__*/React__default.createElement(Typography$1, {
|
|
1746
|
+
variant: "h4",
|
|
1747
|
+
component: "h1"
|
|
1748
|
+
}, "Report Definitions"), /*#__PURE__*/React__default.createElement(Button, {
|
|
1749
|
+
variant: "contained",
|
|
1750
|
+
color: "primary",
|
|
1751
|
+
startIcon: /*#__PURE__*/React__default.createElement(AddIcon, null),
|
|
1752
|
+
onClick: onAddNew
|
|
1753
|
+
}, "Add New Report")), /*#__PURE__*/React__default.createElement(Box$1, {
|
|
1754
|
+
sx: {
|
|
1755
|
+
height: 600,
|
|
1756
|
+
width: '100%'
|
|
1757
|
+
}
|
|
1758
|
+
}, /*#__PURE__*/React__default.createElement(DataGrid, {
|
|
1759
|
+
rows: rows,
|
|
1760
|
+
columns: columns,
|
|
1761
|
+
pageSize: 10,
|
|
1762
|
+
rowsPerPageOptions: [10, 25, 50],
|
|
1763
|
+
disableSelectionOnClick: true,
|
|
1764
|
+
onRowClick: handleRowClick,
|
|
1765
|
+
sx: {
|
|
1766
|
+
'& .MuiDataGrid-row': {
|
|
1767
|
+
cursor: 'pointer'
|
|
1768
|
+
},
|
|
1769
|
+
'& .MuiDataGrid-row:hover': {
|
|
1770
|
+
backgroundColor: '#f5f5f5'
|
|
1771
|
+
},
|
|
1772
|
+
'& .MuiDataGrid-columnHeader': {
|
|
1773
|
+
backgroundColor: '#f5f5f5',
|
|
1774
|
+
fontWeight: 'bold'
|
|
1775
|
+
}
|
|
1776
|
+
}
|
|
1777
|
+
})), /*#__PURE__*/React__default.createElement(ConfirmDialog, {
|
|
1778
|
+
open: deleteDialogOpen,
|
|
1779
|
+
title: "Delete Report Definition",
|
|
1780
|
+
message: reportToDelete ? `Are you sure you want to delete "${reportToDelete.title}"? This action cannot be undone.` : '',
|
|
1781
|
+
onConfirm: handleConfirmDelete,
|
|
1782
|
+
onCancel: handleCancelDelete,
|
|
1783
|
+
confirmText: "Delete",
|
|
1784
|
+
cancelText: "Cancel",
|
|
1785
|
+
confirmColor: "error"
|
|
1786
|
+
}));
|
|
1787
|
+
};
|
|
1788
|
+
|
|
1789
|
+
const ProviderSelection = ({
|
|
1790
|
+
providersData,
|
|
1791
|
+
rootProvider,
|
|
1792
|
+
onSelectionChange,
|
|
1793
|
+
existingDimensions = [],
|
|
1794
|
+
existingMetrics = [],
|
|
1795
|
+
existingFilters = {}
|
|
1796
|
+
}) => {
|
|
1797
|
+
const [selectionChain, setSelectionChain] = useState([]); // Array of {providerKey, relationName, targetKey}
|
|
1798
|
+
|
|
1799
|
+
// Reset selection chain when root provider changes
|
|
1800
|
+
useEffect(() => {
|
|
1801
|
+
setSelectionChain([]);
|
|
1802
|
+
if (onSelectionChange) {
|
|
1803
|
+
onSelectionChange([]);
|
|
1804
|
+
}
|
|
1805
|
+
}, [rootProvider]);
|
|
1806
|
+
|
|
1807
|
+
// Helper function to extract used typed relations from existing items
|
|
1808
|
+
// Returns a Map: providerKey -> Set of used typed relation names
|
|
1809
|
+
const getUsedTypedRelations = () => {
|
|
1810
|
+
const usedTypedRelationsMap = new Map();
|
|
1811
|
+
|
|
1812
|
+
// Helper to process a single item's relations
|
|
1813
|
+
const processItemRelations = item => {
|
|
1814
|
+
if (!item.relations || !item.providerPath) return;
|
|
1815
|
+
|
|
1816
|
+
// Walk through the provider path and relations together
|
|
1817
|
+
for (let i = 0; i < item.relations.length; i++) {
|
|
1818
|
+
const relation = item.relations[i];
|
|
1819
|
+
const currentProviderKey = item.providerPath[i]; // The provider this relation comes from
|
|
1820
|
+
|
|
1821
|
+
// Check if this relation has a 'type' property
|
|
1822
|
+
if (relation.type) {
|
|
1823
|
+
// Track this typed relation usage
|
|
1824
|
+
if (!usedTypedRelationsMap.has(currentProviderKey)) {
|
|
1825
|
+
usedTypedRelationsMap.set(currentProviderKey, new Set());
|
|
1826
|
+
}
|
|
1827
|
+
usedTypedRelationsMap.get(currentProviderKey).add(relation.name);
|
|
1828
|
+
}
|
|
1829
|
+
}
|
|
1830
|
+
};
|
|
1831
|
+
|
|
1832
|
+
// Process dimensions
|
|
1833
|
+
existingDimensions.forEach(processItemRelations);
|
|
1834
|
+
|
|
1835
|
+
// Process metrics
|
|
1836
|
+
existingMetrics.forEach(processItemRelations);
|
|
1837
|
+
|
|
1838
|
+
// Process filters (convert object to array of filter data)
|
|
1839
|
+
Object.values(existingFilters).forEach(processItemRelations);
|
|
1840
|
+
return usedTypedRelationsMap;
|
|
1841
|
+
};
|
|
1842
|
+
|
|
1843
|
+
// Get items for a dropdown based on the selected provider's relations
|
|
1844
|
+
const getRelationItems = providerKey => {
|
|
1845
|
+
if (!providersData || !providerKey) return [];
|
|
1846
|
+
const provider = providersData[providerKey];
|
|
1847
|
+
if (!provider || !provider.relations || provider.relations.length === 0) {
|
|
1848
|
+
return [];
|
|
1849
|
+
}
|
|
1850
|
+
|
|
1851
|
+
// Get the map of used typed relations
|
|
1852
|
+
const usedTypedRelationsMap = getUsedTypedRelations();
|
|
1853
|
+
|
|
1854
|
+
// Check if this provider has any typed relations already in use
|
|
1855
|
+
const usedTypedRelations = usedTypedRelationsMap.get(providerKey);
|
|
1856
|
+
|
|
1857
|
+
// Map relations to items, using relation name as key and target as metadata
|
|
1858
|
+
return provider.relations.filter(relation => providersData[relation.target]).map(relation => {
|
|
1859
|
+
const hasType = !!relation.type;
|
|
1860
|
+
let disabled = false;
|
|
1861
|
+
let disabledReason = null;
|
|
1862
|
+
|
|
1863
|
+
// If this relation has a type property
|
|
1864
|
+
if (hasType && usedTypedRelations && usedTypedRelations.size > 0) {
|
|
1865
|
+
// Check if a different typed relation from this provider is already in use
|
|
1866
|
+
const isThisRelationUsed = usedTypedRelations.has(relation.name);
|
|
1867
|
+
const otherTypedRelationUsed = !isThisRelationUsed && usedTypedRelations.size > 0;
|
|
1868
|
+
if (otherTypedRelationUsed) {
|
|
1869
|
+
disabled = true;
|
|
1870
|
+
// Find which relation is in use
|
|
1871
|
+
const usedRelationName = Array.from(usedTypedRelations)[0];
|
|
1872
|
+
disabledReason = `Cannot select: '${usedRelationName}' is already in use`;
|
|
1873
|
+
}
|
|
1874
|
+
}
|
|
1875
|
+
return {
|
|
1876
|
+
key: relation.name,
|
|
1877
|
+
// Use relation name as the unique key
|
|
1878
|
+
value: relation.name,
|
|
1879
|
+
targetKey: relation.target,
|
|
1880
|
+
// Store the target provider key separately
|
|
1881
|
+
disabled: disabled,
|
|
1882
|
+
disabledReason: disabledReason
|
|
1883
|
+
};
|
|
1884
|
+
});
|
|
1885
|
+
};
|
|
1886
|
+
|
|
1887
|
+
// Handle selection at a specific level
|
|
1888
|
+
const handleSelectionChange = (level, event) => {
|
|
1889
|
+
const selectedKey = event.target.value;
|
|
1890
|
+
|
|
1891
|
+
// Find the targetKey from the items
|
|
1892
|
+
const parentProvider = level === 0 ? rootProvider : selectionChain[level - 1].targetKey;
|
|
1893
|
+
const items = getRelationItems(parentProvider);
|
|
1894
|
+
const selectedItem = items.find(item => item.key === selectedKey);
|
|
1895
|
+
if (!selectedItem) return;
|
|
1896
|
+
|
|
1897
|
+
// Truncate the chain to this level and add the new selection
|
|
1898
|
+
const newChain = selectionChain.slice(0, level);
|
|
1899
|
+
newChain.push({
|
|
1900
|
+
providerKey: selectedKey,
|
|
1901
|
+
// The key used in the dropdown (relation name)
|
|
1902
|
+
relationName: selectedKey,
|
|
1903
|
+
// The relation name
|
|
1904
|
+
targetKey: selectedItem.targetKey // The actual provider this points to
|
|
1905
|
+
});
|
|
1906
|
+
setSelectionChain(newChain);
|
|
1907
|
+
console.log('Selection chain:', newChain);
|
|
1908
|
+
|
|
1909
|
+
// Notify parent of selection change
|
|
1910
|
+
if (onSelectionChange) {
|
|
1911
|
+
onSelectionChange(newChain);
|
|
1912
|
+
}
|
|
1913
|
+
};
|
|
1914
|
+
|
|
1915
|
+
// Handle removing a provider from the chain (and all subsequent providers)
|
|
1916
|
+
const handleRemoveFromChain = level => {
|
|
1917
|
+
// Truncate the chain to remove this level and everything after it
|
|
1918
|
+
const newChain = selectionChain.slice(0, level);
|
|
1919
|
+
setSelectionChain(newChain);
|
|
1920
|
+
|
|
1921
|
+
// Notify parent of selection change
|
|
1922
|
+
if (onSelectionChange) {
|
|
1923
|
+
onSelectionChange(newChain);
|
|
1924
|
+
}
|
|
1925
|
+
};
|
|
1926
|
+
|
|
1927
|
+
// Render all dropdowns in the chain
|
|
1928
|
+
const renderSelectionChain = () => {
|
|
1929
|
+
if (!providersData || !rootProvider) return null;
|
|
1930
|
+
const dropdowns = [];
|
|
1931
|
+
|
|
1932
|
+
// First dropdown - relations of root provider
|
|
1933
|
+
const rootRelationItems = getRelationItems(rootProvider);
|
|
1934
|
+
if (rootRelationItems.length > 0) {
|
|
1935
|
+
dropdowns.push(/*#__PURE__*/React__default.createElement("div", {
|
|
1936
|
+
key: "level-0",
|
|
1937
|
+
style: {
|
|
1938
|
+
marginRight: '16px'
|
|
1939
|
+
}
|
|
1940
|
+
}, /*#__PURE__*/React__default.createElement(SingleSelect, {
|
|
1941
|
+
items: rootRelationItems,
|
|
1942
|
+
value: selectionChain[0]?.providerKey || '',
|
|
1943
|
+
label: `Related to ${rootProvider}`,
|
|
1944
|
+
onChange: e => handleSelectionChange(0, e),
|
|
1945
|
+
sx: {
|
|
1946
|
+
width: '300px'
|
|
1947
|
+
}
|
|
1948
|
+
})));
|
|
1949
|
+
}
|
|
1950
|
+
|
|
1951
|
+
// Subsequent dropdowns based on relations
|
|
1952
|
+
for (let i = 0; i < selectionChain.length; i++) {
|
|
1953
|
+
const currentSelection = selectionChain[i];
|
|
1954
|
+
const relationItems = getRelationItems(currentSelection.targetKey);
|
|
1955
|
+
|
|
1956
|
+
// Only show next dropdown if current selection has relations
|
|
1957
|
+
if (relationItems.length > 0) {
|
|
1958
|
+
const nextSelection = selectionChain[i + 1];
|
|
1959
|
+
dropdowns.push(/*#__PURE__*/React__default.createElement("div", {
|
|
1960
|
+
key: `level-${i + 1}`,
|
|
1961
|
+
style: {
|
|
1962
|
+
marginRight: '16px'
|
|
1963
|
+
}
|
|
1964
|
+
}, /*#__PURE__*/React__default.createElement(SingleSelect, {
|
|
1965
|
+
items: relationItems,
|
|
1966
|
+
value: nextSelection?.providerKey || '',
|
|
1967
|
+
label: `Related to ${currentSelection.targetKey}`,
|
|
1968
|
+
onChange: e => handleSelectionChange(i + 1, e),
|
|
1969
|
+
sx: {
|
|
1970
|
+
width: '300px'
|
|
1971
|
+
}
|
|
1972
|
+
})));
|
|
1973
|
+
}
|
|
1974
|
+
}
|
|
1975
|
+
return dropdowns;
|
|
1976
|
+
};
|
|
1977
|
+
return /*#__PURE__*/React__default.createElement(Box$1, null, selectionChain.length > 0 && /*#__PURE__*/React__default.createElement(Box$1, {
|
|
1978
|
+
sx: {
|
|
1979
|
+
marginBottom: 2
|
|
1980
|
+
}
|
|
1981
|
+
}, /*#__PURE__*/React__default.createElement(Typography$1, {
|
|
1982
|
+
variant: "subtitle2",
|
|
1983
|
+
sx: {
|
|
1984
|
+
marginBottom: 1,
|
|
1985
|
+
color: 'text.secondary'
|
|
1986
|
+
}
|
|
1987
|
+
}, "Selected Path:"), /*#__PURE__*/React__default.createElement(Box$1, {
|
|
1988
|
+
sx: {
|
|
1989
|
+
display: 'flex',
|
|
1990
|
+
alignItems: 'center',
|
|
1991
|
+
gap: 1,
|
|
1992
|
+
flexWrap: 'wrap'
|
|
1993
|
+
}
|
|
1994
|
+
}, /*#__PURE__*/React__default.createElement(Chip, {
|
|
1995
|
+
label: rootProvider,
|
|
1996
|
+
size: "small",
|
|
1997
|
+
color: "primary",
|
|
1998
|
+
variant: "outlined"
|
|
1999
|
+
}), selectionChain.map((selection, index) => /*#__PURE__*/React__default.createElement(React__default.Fragment, {
|
|
2000
|
+
key: index
|
|
2001
|
+
}, /*#__PURE__*/React__default.createElement(Typography$1, {
|
|
2002
|
+
variant: "body2",
|
|
2003
|
+
sx: {
|
|
2004
|
+
color: 'text.secondary'
|
|
2005
|
+
}
|
|
2006
|
+
}, "\u2192"), /*#__PURE__*/React__default.createElement(Chip, {
|
|
2007
|
+
label: selection.targetKey,
|
|
2008
|
+
size: "small",
|
|
2009
|
+
color: "primary",
|
|
2010
|
+
onDelete: () => handleRemoveFromChain(index),
|
|
2011
|
+
sx: {
|
|
2012
|
+
fontWeight: 500
|
|
2013
|
+
}
|
|
2014
|
+
}))))), /*#__PURE__*/React__default.createElement("div", {
|
|
2015
|
+
style: {
|
|
2016
|
+
display: 'flex',
|
|
2017
|
+
flexWrap: 'wrap',
|
|
2018
|
+
alignItems: 'flex-start',
|
|
2019
|
+
gap: '8px'
|
|
2020
|
+
}
|
|
2021
|
+
}, renderSelectionChain()));
|
|
2022
|
+
};
|
|
2023
|
+
|
|
2024
|
+
// Sortable Chip Component
|
|
2025
|
+
const SortableChip$1 = ({
|
|
2026
|
+
id,
|
|
2027
|
+
label,
|
|
2028
|
+
fullLabel,
|
|
2029
|
+
onDelete,
|
|
2030
|
+
onMoveUp,
|
|
2031
|
+
onMoveDown,
|
|
2032
|
+
isFirst,
|
|
2033
|
+
isLast,
|
|
2034
|
+
sortOrder,
|
|
2035
|
+
onSortOrderChange,
|
|
2036
|
+
fullPath,
|
|
2037
|
+
defaultTitle,
|
|
2038
|
+
customTitle,
|
|
2039
|
+
onUpdateTitle,
|
|
2040
|
+
onResetTitle
|
|
2041
|
+
}) => {
|
|
2042
|
+
const [isEditing, setIsEditing] = useState(false);
|
|
2043
|
+
const [editValue, setEditValue] = useState('');
|
|
2044
|
+
const inputRef = useRef(null);
|
|
2045
|
+
const containerRef = useRef(null);
|
|
2046
|
+
const {
|
|
2047
|
+
attributes,
|
|
2048
|
+
listeners,
|
|
2049
|
+
setNodeRef,
|
|
2050
|
+
transform,
|
|
2051
|
+
transition,
|
|
2052
|
+
isDragging
|
|
2053
|
+
} = useSortable({
|
|
2054
|
+
id
|
|
2055
|
+
});
|
|
2056
|
+
const style = {
|
|
2057
|
+
transform: CSS.Transform.toString(transform),
|
|
2058
|
+
transition,
|
|
2059
|
+
opacity: isDragging ? 0.5 : 1,
|
|
2060
|
+
display: 'flex',
|
|
2061
|
+
alignItems: 'center',
|
|
2062
|
+
width: '100%'
|
|
2063
|
+
};
|
|
2064
|
+
|
|
2065
|
+
// Focus input when entering edit mode
|
|
2066
|
+
useEffect(() => {
|
|
2067
|
+
if (isEditing && inputRef.current) {
|
|
2068
|
+
inputRef.current.focus();
|
|
2069
|
+
inputRef.current.select();
|
|
2070
|
+
}
|
|
2071
|
+
}, [isEditing]);
|
|
2072
|
+
|
|
2073
|
+
// Handle click outside to cancel
|
|
2074
|
+
useEffect(() => {
|
|
2075
|
+
const handleClickOutside = event => {
|
|
2076
|
+
if (isEditing && containerRef.current && !containerRef.current.contains(event.target)) {
|
|
2077
|
+
handleCancel();
|
|
2078
|
+
}
|
|
2079
|
+
};
|
|
2080
|
+
if (isEditing) {
|
|
2081
|
+
document.addEventListener('mousedown', handleClickOutside);
|
|
2082
|
+
return () => {
|
|
2083
|
+
document.removeEventListener('mousedown', handleClickOutside);
|
|
2084
|
+
};
|
|
2085
|
+
}
|
|
2086
|
+
}, [isEditing]);
|
|
2087
|
+
|
|
2088
|
+
// Cycle through sort states: null -> 'asc' -> 'desc' -> null
|
|
2089
|
+
const handleSortToggle = () => {
|
|
2090
|
+
if (sortOrder === null) {
|
|
2091
|
+
onSortOrderChange('asc');
|
|
2092
|
+
} else if (sortOrder === 'asc') {
|
|
2093
|
+
onSortOrderChange('desc');
|
|
2094
|
+
} else {
|
|
2095
|
+
onSortOrderChange(null);
|
|
2096
|
+
}
|
|
2097
|
+
};
|
|
2098
|
+
|
|
2099
|
+
// Determine which icon to show
|
|
2100
|
+
const getSortIcon = () => {
|
|
2101
|
+
if (sortOrder === 'asc') {
|
|
2102
|
+
return /*#__PURE__*/React__default.createElement(ArrowUpwardIcon, {
|
|
2103
|
+
fontSize: "small"
|
|
2104
|
+
});
|
|
2105
|
+
} else if (sortOrder === 'desc') {
|
|
2106
|
+
return /*#__PURE__*/React__default.createElement(ArrowDownwardIcon, {
|
|
2107
|
+
fontSize: "small"
|
|
2108
|
+
});
|
|
2109
|
+
} else {
|
|
2110
|
+
return /*#__PURE__*/React__default.createElement(SortIcon, {
|
|
2111
|
+
fontSize: "small",
|
|
2112
|
+
sx: {
|
|
2113
|
+
opacity: 0.3
|
|
2114
|
+
}
|
|
2115
|
+
});
|
|
2116
|
+
}
|
|
2117
|
+
};
|
|
2118
|
+
const handleEditClick = () => {
|
|
2119
|
+
setEditValue(customTitle || defaultTitle);
|
|
2120
|
+
setIsEditing(true);
|
|
2121
|
+
};
|
|
2122
|
+
const handleSave = () => {
|
|
2123
|
+
const trimmedValue = editValue.trim();
|
|
2124
|
+
if (trimmedValue === '') {
|
|
2125
|
+
// Empty string validation - treat as cancel
|
|
2126
|
+
handleCancel();
|
|
2127
|
+
return;
|
|
2128
|
+
}
|
|
2129
|
+
onUpdateTitle(fullPath, trimmedValue);
|
|
2130
|
+
setIsEditing(false);
|
|
2131
|
+
};
|
|
2132
|
+
const handleCancel = () => {
|
|
2133
|
+
setIsEditing(false);
|
|
2134
|
+
setEditValue('');
|
|
2135
|
+
};
|
|
2136
|
+
const handleReset = () => {
|
|
2137
|
+
onResetTitle(fullPath);
|
|
2138
|
+
setIsEditing(false);
|
|
2139
|
+
setEditValue('');
|
|
2140
|
+
};
|
|
2141
|
+
const handleKeyDown = e => {
|
|
2142
|
+
if (e.key === 'Enter') {
|
|
2143
|
+
handleSave();
|
|
2144
|
+
} else if (e.key === 'Escape') {
|
|
2145
|
+
handleCancel();
|
|
2146
|
+
}
|
|
2147
|
+
};
|
|
2148
|
+
const displayLabel = customTitle || label;
|
|
2149
|
+
const hasCustomTitle = !!customTitle;
|
|
2150
|
+
return /*#__PURE__*/React__default.createElement("div", _extends({
|
|
2151
|
+
ref: setNodeRef,
|
|
2152
|
+
style: style
|
|
2153
|
+
}, attributes), /*#__PURE__*/React__default.createElement(Box$1, {
|
|
2154
|
+
ref: containerRef,
|
|
2155
|
+
sx: {
|
|
2156
|
+
display: 'flex',
|
|
2157
|
+
alignItems: 'center',
|
|
2158
|
+
width: '100%',
|
|
2159
|
+
gap: 1
|
|
2160
|
+
}
|
|
2161
|
+
}, /*#__PURE__*/React__default.createElement(Box$1, _extends({}, listeners, {
|
|
2162
|
+
sx: {
|
|
2163
|
+
display: 'flex',
|
|
2164
|
+
alignItems: 'center',
|
|
2165
|
+
cursor: 'grab',
|
|
2166
|
+
'&:active': {
|
|
2167
|
+
cursor: 'grabbing'
|
|
2168
|
+
}
|
|
2169
|
+
}
|
|
2170
|
+
}), /*#__PURE__*/React__default.createElement(DragIndicatorIcon, {
|
|
2171
|
+
sx: {
|
|
2172
|
+
cursor: 'grab'
|
|
2173
|
+
}
|
|
2174
|
+
})), !isEditing ? /*#__PURE__*/React__default.createElement(React__default.Fragment, null, /*#__PURE__*/React__default.createElement(Tooltip, {
|
|
2175
|
+
title: fullLabel,
|
|
2176
|
+
arrow: true,
|
|
2177
|
+
placement: "top"
|
|
2178
|
+
}, /*#__PURE__*/React__default.createElement(Chip, {
|
|
2179
|
+
label: displayLabel,
|
|
2180
|
+
onDelete: onDelete,
|
|
2181
|
+
color: "primary",
|
|
2182
|
+
variant: "outlined",
|
|
2183
|
+
sx: {
|
|
2184
|
+
fontWeight: hasCustomTitle ? 'bold' : 'normal',
|
|
2185
|
+
fontStyle: hasCustomTitle ? 'italic' : 'normal'
|
|
2186
|
+
}
|
|
2187
|
+
})), /*#__PURE__*/React__default.createElement(Tooltip, {
|
|
2188
|
+
title: "Edit title",
|
|
2189
|
+
arrow: true,
|
|
2190
|
+
placement: "top"
|
|
2191
|
+
}, /*#__PURE__*/React__default.createElement(IconButton, {
|
|
2192
|
+
size: "small",
|
|
2193
|
+
onClick: handleEditClick,
|
|
2194
|
+
"aria-label": "edit title"
|
|
2195
|
+
}, /*#__PURE__*/React__default.createElement(EditIcon, {
|
|
2196
|
+
fontSize: "small"
|
|
2197
|
+
})))) : /*#__PURE__*/React__default.createElement(React__default.Fragment, null, /*#__PURE__*/React__default.createElement(TextField, {
|
|
2198
|
+
inputRef: inputRef,
|
|
2199
|
+
value: editValue,
|
|
2200
|
+
onChange: e => setEditValue(e.target.value),
|
|
2201
|
+
onKeyDown: handleKeyDown,
|
|
2202
|
+
size: "small",
|
|
2203
|
+
sx: {
|
|
2204
|
+
width: '200px'
|
|
2205
|
+
}
|
|
2206
|
+
}), /*#__PURE__*/React__default.createElement(Tooltip, {
|
|
2207
|
+
title: "Save",
|
|
2208
|
+
arrow: true,
|
|
2209
|
+
placement: "top"
|
|
2210
|
+
}, /*#__PURE__*/React__default.createElement(IconButton, {
|
|
2211
|
+
size: "small",
|
|
2212
|
+
onClick: handleSave,
|
|
2213
|
+
color: "primary",
|
|
2214
|
+
"aria-label": "save title"
|
|
2215
|
+
}, /*#__PURE__*/React__default.createElement(CheckIcon, {
|
|
2216
|
+
fontSize: "small"
|
|
2217
|
+
}))), /*#__PURE__*/React__default.createElement(Tooltip, {
|
|
2218
|
+
title: "Cancel",
|
|
2219
|
+
arrow: true,
|
|
2220
|
+
placement: "top"
|
|
2221
|
+
}, /*#__PURE__*/React__default.createElement(IconButton, {
|
|
2222
|
+
size: "small",
|
|
2223
|
+
onClick: handleCancel,
|
|
2224
|
+
"aria-label": "cancel edit"
|
|
2225
|
+
}, /*#__PURE__*/React__default.createElement(CloseIcon, {
|
|
2226
|
+
fontSize: "small"
|
|
2227
|
+
}))), hasCustomTitle && /*#__PURE__*/React__default.createElement(Tooltip, {
|
|
2228
|
+
title: "Reset to default",
|
|
2229
|
+
arrow: true,
|
|
2230
|
+
placement: "top"
|
|
2231
|
+
}, /*#__PURE__*/React__default.createElement(IconButton, {
|
|
2232
|
+
size: "small",
|
|
2233
|
+
onClick: handleReset,
|
|
2234
|
+
color: "warning",
|
|
2235
|
+
"aria-label": "reset title"
|
|
2236
|
+
}, /*#__PURE__*/React__default.createElement(RestartAltIcon, {
|
|
2237
|
+
fontSize: "small"
|
|
2238
|
+
})))), !isEditing && /*#__PURE__*/React__default.createElement(React__default.Fragment, null, /*#__PURE__*/React__default.createElement(Tooltip, {
|
|
2239
|
+
title: sortOrder === null ? 'No sort' : sortOrder === 'asc' ? 'Ascending' : 'Descending',
|
|
2240
|
+
arrow: true,
|
|
2241
|
+
placement: "top"
|
|
2242
|
+
}, /*#__PURE__*/React__default.createElement(IconButton, {
|
|
2243
|
+
size: "small",
|
|
2244
|
+
onClick: handleSortToggle,
|
|
2245
|
+
"aria-label": "toggle sort order",
|
|
2246
|
+
color: sortOrder ? 'primary' : 'default'
|
|
2247
|
+
}, getSortIcon())), /*#__PURE__*/React__default.createElement(Box$1, {
|
|
2248
|
+
sx: {
|
|
2249
|
+
flex: 1
|
|
2250
|
+
}
|
|
2251
|
+
}), /*#__PURE__*/React__default.createElement(Box$1, {
|
|
2252
|
+
sx: {
|
|
2253
|
+
display: 'flex',
|
|
2254
|
+
gap: 0.5
|
|
2255
|
+
}
|
|
2256
|
+
}, /*#__PURE__*/React__default.createElement(IconButton, {
|
|
2257
|
+
size: "small",
|
|
2258
|
+
onClick: onMoveUp,
|
|
2259
|
+
disabled: isFirst,
|
|
2260
|
+
"aria-label": "move up"
|
|
2261
|
+
}, /*#__PURE__*/React__default.createElement(ArrowUpwardIcon, {
|
|
2262
|
+
fontSize: "small"
|
|
2263
|
+
})), /*#__PURE__*/React__default.createElement(IconButton, {
|
|
2264
|
+
size: "small",
|
|
2265
|
+
onClick: onMoveDown,
|
|
2266
|
+
disabled: isLast,
|
|
2267
|
+
"aria-label": "move down"
|
|
2268
|
+
}, /*#__PURE__*/React__default.createElement(ArrowDownwardIcon, {
|
|
2269
|
+
fontSize: "small"
|
|
2270
|
+
}))))));
|
|
2271
|
+
};
|
|
2272
|
+
const Dimensions = ({
|
|
2273
|
+
providersData,
|
|
2274
|
+
rootProvider,
|
|
2275
|
+
savedDimensions = [],
|
|
2276
|
+
onSaveDimension,
|
|
2277
|
+
onRemoveDimension,
|
|
2278
|
+
onReorderDimensions,
|
|
2279
|
+
titleOverrides = {},
|
|
2280
|
+
onUpdateTitle,
|
|
2281
|
+
onResetTitle,
|
|
2282
|
+
existingMetrics = [],
|
|
2283
|
+
existingFilters = {}
|
|
2284
|
+
}) => {
|
|
2285
|
+
const [isAdding, setIsAdding] = useState(false);
|
|
2286
|
+
const [dimensionSelectionChain, setDimensionSelectionChain] = useState([]);
|
|
2287
|
+
const [selectedDimension, setSelectedDimension] = useState('');
|
|
2288
|
+
|
|
2289
|
+
// Setup drag and drop sensors
|
|
2290
|
+
const sensors = useSensors(useSensor(PointerSensor), useSensor(KeyboardSensor, {
|
|
2291
|
+
coordinateGetter: sortableKeyboardCoordinates
|
|
2292
|
+
}));
|
|
2293
|
+
|
|
2294
|
+
// Handle drag end
|
|
2295
|
+
const handleDragEnd = event => {
|
|
2296
|
+
const {
|
|
2297
|
+
active,
|
|
2298
|
+
over
|
|
2299
|
+
} = event;
|
|
2300
|
+
if (over && active.id !== over.id) {
|
|
2301
|
+
const oldIndex = savedDimensions.findIndex((_, i) => i === active.id);
|
|
2302
|
+
const newIndex = savedDimensions.findIndex((_, i) => i === over.id);
|
|
2303
|
+
const newOrder = arrayMove(savedDimensions, oldIndex, newIndex);
|
|
2304
|
+
onReorderDimensions(newOrder);
|
|
2305
|
+
}
|
|
2306
|
+
};
|
|
2307
|
+
|
|
2308
|
+
// Handle move up
|
|
2309
|
+
const handleMoveUp = index => {
|
|
2310
|
+
if (index > 0) {
|
|
2311
|
+
const newOrder = arrayMove(savedDimensions, index, index - 1);
|
|
2312
|
+
onReorderDimensions(newOrder);
|
|
2313
|
+
}
|
|
2314
|
+
};
|
|
2315
|
+
|
|
2316
|
+
// Handle move down
|
|
2317
|
+
const handleMoveDown = index => {
|
|
2318
|
+
if (index < savedDimensions.length - 1) {
|
|
2319
|
+
const newOrder = arrayMove(savedDimensions, index, index + 1);
|
|
2320
|
+
onReorderDimensions(newOrder);
|
|
2321
|
+
}
|
|
2322
|
+
};
|
|
2323
|
+
|
|
2324
|
+
// Handle sort order change
|
|
2325
|
+
const handleSortOrderChange = (index, sortOrder) => {
|
|
2326
|
+
const newDimensions = [...savedDimensions];
|
|
2327
|
+
newDimensions[index] = {
|
|
2328
|
+
...newDimensions[index],
|
|
2329
|
+
sortOrder
|
|
2330
|
+
};
|
|
2331
|
+
onReorderDimensions(newDimensions);
|
|
2332
|
+
};
|
|
2333
|
+
|
|
2334
|
+
// Get the current provider based on selection chain
|
|
2335
|
+
const getCurrentProvider = () => {
|
|
2336
|
+
if (!rootProvider) return null;
|
|
2337
|
+
if (dimensionSelectionChain.length === 0) return rootProvider;
|
|
2338
|
+
return dimensionSelectionChain[dimensionSelectionChain.length - 1].targetKey;
|
|
2339
|
+
};
|
|
2340
|
+
|
|
2341
|
+
// Get dimensions for the current provider
|
|
2342
|
+
const getDimensionItems = () => {
|
|
2343
|
+
const currentProvider = getCurrentProvider();
|
|
2344
|
+
if (!currentProvider || !providersData || !providersData[currentProvider]) {
|
|
2345
|
+
return [];
|
|
2346
|
+
}
|
|
2347
|
+
const provider = providersData[currentProvider];
|
|
2348
|
+
if (!provider.dimensions) return [];
|
|
2349
|
+
|
|
2350
|
+
// Build the alias path for the current selection chain
|
|
2351
|
+
const rootProviderData = providersData[rootProvider];
|
|
2352
|
+
const aliasPath = [rootProviderData.default_alias];
|
|
2353
|
+
|
|
2354
|
+
// Add relation aliases from the selection chain
|
|
2355
|
+
let currentProviderKey = rootProvider;
|
|
2356
|
+
dimensionSelectionChain.forEach(selection => {
|
|
2357
|
+
const providerData = providersData[currentProviderKey];
|
|
2358
|
+
if (providerData && providerData.relations) {
|
|
2359
|
+
const relationObj = providerData.relations.find(rel => rel.name === selection.relationName);
|
|
2360
|
+
if (relationObj) {
|
|
2361
|
+
aliasPath.push(relationObj.alias);
|
|
2362
|
+
}
|
|
2363
|
+
}
|
|
2364
|
+
currentProviderKey = selection.targetKey;
|
|
2365
|
+
});
|
|
2366
|
+
|
|
2367
|
+
// Create a Set of already selected dimension fullPaths for quick lookup
|
|
2368
|
+
const selectedFullPaths = new Set(savedDimensions.map(dim => dim.fullPath));
|
|
2369
|
+
const items = [];
|
|
2370
|
+
// Iterate through aliases (e.g., 'ba', 'pc')
|
|
2371
|
+
Object.keys(provider.dimensions).forEach(alias => {
|
|
2372
|
+
const dimensionsForAlias = provider.dimensions[alias];
|
|
2373
|
+
// Iterate through dimension keys (e.g., 'created_at', 'status')
|
|
2374
|
+
Object.keys(dimensionsForAlias).forEach(dimKey => {
|
|
2375
|
+
const dimension = dimensionsForAlias[dimKey];
|
|
2376
|
+
|
|
2377
|
+
// Construct the fullPath for this dimension
|
|
2378
|
+
const fullPath = `${aliasPath.join('_')}.${dimKey}`;
|
|
2379
|
+
|
|
2380
|
+
// Check if this dimension is already selected
|
|
2381
|
+
const isAlreadySelected = selectedFullPaths.has(fullPath);
|
|
2382
|
+
items.push({
|
|
2383
|
+
// Include provider name to ensure uniqueness across different providers
|
|
2384
|
+
key: `${currentProvider}_${alias}.${dimKey}`,
|
|
2385
|
+
value: dimension.title || dimKey,
|
|
2386
|
+
dimensionKey: dimKey,
|
|
2387
|
+
alias: alias,
|
|
2388
|
+
dimension: dimension,
|
|
2389
|
+
disabled: isAlreadySelected
|
|
2390
|
+
});
|
|
2391
|
+
});
|
|
2392
|
+
});
|
|
2393
|
+
return items;
|
|
2394
|
+
};
|
|
2395
|
+
const handleAddClick = () => {
|
|
2396
|
+
setIsAdding(true);
|
|
2397
|
+
setDimensionSelectionChain([]);
|
|
2398
|
+
setSelectedDimension('');
|
|
2399
|
+
};
|
|
2400
|
+
const handleCancel = () => {
|
|
2401
|
+
setIsAdding(false);
|
|
2402
|
+
setDimensionSelectionChain([]);
|
|
2403
|
+
setSelectedDimension('');
|
|
2404
|
+
};
|
|
2405
|
+
const handleSave = () => {
|
|
2406
|
+
if (!selectedDimension) return;
|
|
2407
|
+
const dimensionItems = getDimensionItems();
|
|
2408
|
+
const selectedItem = dimensionItems.find(item => item.key === selectedDimension);
|
|
2409
|
+
if (!selectedItem) return;
|
|
2410
|
+
|
|
2411
|
+
// Build the complete relation objects array
|
|
2412
|
+
const relations = [];
|
|
2413
|
+
const providerPath = [rootProvider];
|
|
2414
|
+
const relationNames = [];
|
|
2415
|
+
|
|
2416
|
+
// Collect all relation objects from the selection chain
|
|
2417
|
+
let currentProviderKey = rootProvider;
|
|
2418
|
+
dimensionSelectionChain.forEach(selection => {
|
|
2419
|
+
const provider = providersData[currentProviderKey];
|
|
2420
|
+
if (provider && provider.relations) {
|
|
2421
|
+
// Find the complete relation object
|
|
2422
|
+
const relationObj = provider.relations.find(rel => rel.name === selection.relationName);
|
|
2423
|
+
if (relationObj) {
|
|
2424
|
+
relations.push(relationObj);
|
|
2425
|
+
relationNames.push(relationObj.name);
|
|
2426
|
+
}
|
|
2427
|
+
}
|
|
2428
|
+
providerPath.push(selection.targetKey);
|
|
2429
|
+
currentProviderKey = selection.targetKey;
|
|
2430
|
+
});
|
|
2431
|
+
|
|
2432
|
+
// Build the alias path: root_alias + relation_aliases + dimension_key
|
|
2433
|
+
const rootProviderData = providersData[rootProvider];
|
|
2434
|
+
const aliasPath = [rootProviderData.default_alias];
|
|
2435
|
+
relations.forEach(rel => {
|
|
2436
|
+
aliasPath.push(rel.alias);
|
|
2437
|
+
});
|
|
2438
|
+
const fullPath = `${aliasPath.join('_')}.${selectedItem.dimensionKey}`;
|
|
2439
|
+
const dimensionData = {
|
|
2440
|
+
// Complete dimension object from the final provider
|
|
2441
|
+
dimension: selectedItem.dimension,
|
|
2442
|
+
// Array of complete relation objects
|
|
2443
|
+
relations: relations,
|
|
2444
|
+
// Metadata
|
|
2445
|
+
providerPath: providerPath,
|
|
2446
|
+
relationNames: relationNames,
|
|
2447
|
+
dimensionKey: selectedItem.dimensionKey,
|
|
2448
|
+
dimensionTitle: selectedItem.value,
|
|
2449
|
+
// The constructed path for server
|
|
2450
|
+
fullPath: fullPath
|
|
2451
|
+
};
|
|
2452
|
+
onSaveDimension(dimensionData);
|
|
2453
|
+
handleCancel();
|
|
2454
|
+
};
|
|
2455
|
+
const handleDimensionChange = event => {
|
|
2456
|
+
setSelectedDimension(event.target.value);
|
|
2457
|
+
};
|
|
2458
|
+
const handleAddAll = () => {
|
|
2459
|
+
const dimensionItems = getDimensionItems();
|
|
2460
|
+
|
|
2461
|
+
// Filter out already selected dimensions (disabled items)
|
|
2462
|
+
const availableItems = dimensionItems.filter(item => !item.disabled);
|
|
2463
|
+
if (availableItems.length === 0) return;
|
|
2464
|
+
|
|
2465
|
+
// Build the complete relation objects array (same for all dimensions in this provider)
|
|
2466
|
+
const relations = [];
|
|
2467
|
+
const providerPath = [rootProvider];
|
|
2468
|
+
const relationNames = [];
|
|
2469
|
+
|
|
2470
|
+
// Collect all relation objects from the selection chain
|
|
2471
|
+
let currentProviderKey = rootProvider;
|
|
2472
|
+
dimensionSelectionChain.forEach(selection => {
|
|
2473
|
+
const provider = providersData[currentProviderKey];
|
|
2474
|
+
if (provider && provider.relations) {
|
|
2475
|
+
// Find the complete relation object
|
|
2476
|
+
const relationObj = provider.relations.find(rel => rel.name === selection.relationName);
|
|
2477
|
+
if (relationObj) {
|
|
2478
|
+
relations.push(relationObj);
|
|
2479
|
+
relationNames.push(relationObj.name);
|
|
2480
|
+
}
|
|
2481
|
+
}
|
|
2482
|
+
providerPath.push(selection.targetKey);
|
|
2483
|
+
currentProviderKey = selection.targetKey;
|
|
2484
|
+
});
|
|
2485
|
+
|
|
2486
|
+
// Build the alias path: root_alias + relation_aliases
|
|
2487
|
+
const rootProviderData = providersData[rootProvider];
|
|
2488
|
+
const aliasPath = [rootProviderData.default_alias];
|
|
2489
|
+
relations.forEach(rel => {
|
|
2490
|
+
aliasPath.push(rel.alias);
|
|
2491
|
+
});
|
|
2492
|
+
|
|
2493
|
+
// Create dimension data for each available dimension
|
|
2494
|
+
availableItems.forEach(item => {
|
|
2495
|
+
const fullPath = `${aliasPath.join('_')}.${item.dimensionKey}`;
|
|
2496
|
+
const dimensionData = {
|
|
2497
|
+
// Complete dimension object from the final provider
|
|
2498
|
+
dimension: item.dimension,
|
|
2499
|
+
// Array of complete relation objects
|
|
2500
|
+
relations: relations,
|
|
2501
|
+
// Metadata
|
|
2502
|
+
providerPath: providerPath,
|
|
2503
|
+
relationNames: relationNames,
|
|
2504
|
+
dimensionKey: item.dimensionKey,
|
|
2505
|
+
dimensionTitle: item.value,
|
|
2506
|
+
// The constructed path for server
|
|
2507
|
+
fullPath: fullPath
|
|
2508
|
+
};
|
|
2509
|
+
onSaveDimension(dimensionData);
|
|
2510
|
+
});
|
|
2511
|
+
handleCancel();
|
|
2512
|
+
};
|
|
2513
|
+
const formatProviderPath = dim => {
|
|
2514
|
+
// Build path using root provider + relation names
|
|
2515
|
+
const pathParts = [rootProvider];
|
|
2516
|
+
if (dim.relationNames && dim.relationNames.length > 0) {
|
|
2517
|
+
pathParts.push(...dim.relationNames);
|
|
2518
|
+
}
|
|
2519
|
+
return pathParts.join(' → ');
|
|
2520
|
+
};
|
|
2521
|
+
return /*#__PURE__*/React__default.createElement("div", null, /*#__PURE__*/React__default.createElement(Box$1, {
|
|
2522
|
+
sx: {
|
|
2523
|
+
display: 'flex',
|
|
2524
|
+
alignItems: 'center',
|
|
2525
|
+
gap: 2,
|
|
2526
|
+
marginBottom: 2
|
|
2527
|
+
}
|
|
2528
|
+
}, !isAdding ? /*#__PURE__*/React__default.createElement(Button, {
|
|
2529
|
+
variant: "contained",
|
|
2530
|
+
onClick: handleAddClick
|
|
2531
|
+
}, "Add Dimension") : /*#__PURE__*/React__default.createElement(React__default.Fragment, null, /*#__PURE__*/React__default.createElement(Button, {
|
|
2532
|
+
variant: "outlined",
|
|
2533
|
+
onClick: handleCancel
|
|
2534
|
+
}, "Cancel"), /*#__PURE__*/React__default.createElement(Button, {
|
|
2535
|
+
variant: "contained",
|
|
2536
|
+
onClick: handleSave,
|
|
2537
|
+
disabled: !selectedDimension
|
|
2538
|
+
}, "Save Dimension"))), isAdding && /*#__PURE__*/React__default.createElement(Paper$1, {
|
|
2539
|
+
sx: {
|
|
2540
|
+
padding: 3,
|
|
2541
|
+
marginBottom: 3
|
|
2542
|
+
}
|
|
2543
|
+
}, /*#__PURE__*/React__default.createElement(Typography$1, {
|
|
2544
|
+
variant: "h6",
|
|
2545
|
+
sx: {
|
|
2546
|
+
marginBottom: 2
|
|
2547
|
+
}
|
|
2548
|
+
}, "Select Provider Path"), /*#__PURE__*/React__default.createElement(ProviderSelection, {
|
|
2549
|
+
providersData: providersData,
|
|
2550
|
+
rootProvider: rootProvider,
|
|
2551
|
+
onSelectionChange: setDimensionSelectionChain,
|
|
2552
|
+
existingDimensions: savedDimensions,
|
|
2553
|
+
existingMetrics: existingMetrics,
|
|
2554
|
+
existingFilters: existingFilters
|
|
2555
|
+
}), /*#__PURE__*/React__default.createElement(Typography$1, {
|
|
2556
|
+
variant: "h6",
|
|
2557
|
+
sx: {
|
|
2558
|
+
marginTop: 3,
|
|
2559
|
+
marginBottom: 2
|
|
2560
|
+
}
|
|
2561
|
+
}, "Select Dimension"), /*#__PURE__*/React__default.createElement(Box$1, {
|
|
2562
|
+
sx: {
|
|
2563
|
+
display: 'flex',
|
|
2564
|
+
alignItems: 'center',
|
|
2565
|
+
gap: 1
|
|
2566
|
+
}
|
|
2567
|
+
}, /*#__PURE__*/React__default.createElement(SingleSelect, {
|
|
2568
|
+
items: getDimensionItems(),
|
|
2569
|
+
value: selectedDimension,
|
|
2570
|
+
label: "Choose Dimension",
|
|
2571
|
+
onChange: handleDimensionChange,
|
|
2572
|
+
sx: {
|
|
2573
|
+
width: '400px'
|
|
2574
|
+
}
|
|
2575
|
+
}), /*#__PURE__*/React__default.createElement(Tooltip, {
|
|
2576
|
+
title: "Add all available dimensions",
|
|
2577
|
+
arrow: true,
|
|
2578
|
+
placement: "top"
|
|
2579
|
+
}, /*#__PURE__*/React__default.createElement("span", null, /*#__PURE__*/React__default.createElement(IconButton, {
|
|
2580
|
+
color: "primary",
|
|
2581
|
+
onClick: handleAddAll,
|
|
2582
|
+
disabled: getDimensionItems().filter(item => !item.disabled).length === 0,
|
|
2583
|
+
"aria-label": "add all dimensions"
|
|
2584
|
+
}, /*#__PURE__*/React__default.createElement(PlaylistAddIcon, null)))))), savedDimensions.length > 0 && /*#__PURE__*/React__default.createElement(Box$1, {
|
|
2585
|
+
sx: {
|
|
2586
|
+
marginTop: 3
|
|
2587
|
+
}
|
|
2588
|
+
}, /*#__PURE__*/React__default.createElement(Typography$1, {
|
|
2589
|
+
variant: "h6",
|
|
2590
|
+
sx: {
|
|
2591
|
+
marginBottom: 2
|
|
2592
|
+
}
|
|
2593
|
+
}, "Saved Dimensions (Drag to reorder or use arrows)"), /*#__PURE__*/React__default.createElement(DndContext, {
|
|
2594
|
+
sensors: sensors,
|
|
2595
|
+
collisionDetection: closestCenter,
|
|
2596
|
+
onDragEnd: handleDragEnd
|
|
2597
|
+
}, /*#__PURE__*/React__default.createElement(SortableContext, {
|
|
2598
|
+
items: savedDimensions.map((_, index) => index),
|
|
2599
|
+
strategy: verticalListSortingStrategy
|
|
2600
|
+
}, /*#__PURE__*/React__default.createElement(Box$1, {
|
|
2601
|
+
sx: {
|
|
2602
|
+
display: 'flex',
|
|
2603
|
+
flexDirection: 'column',
|
|
2604
|
+
gap: 1
|
|
2605
|
+
}
|
|
2606
|
+
}, savedDimensions.map((dim, index) => /*#__PURE__*/React__default.createElement(SortableChip$1, {
|
|
2607
|
+
key: index,
|
|
2608
|
+
id: index,
|
|
2609
|
+
label: dim.dimensionTitle,
|
|
2610
|
+
fullLabel: `${formatProviderPath(dim)} → ${dim.dimensionTitle} (${dim.fullPath})`,
|
|
2611
|
+
onDelete: () => onRemoveDimension(index),
|
|
2612
|
+
onMoveUp: () => handleMoveUp(index),
|
|
2613
|
+
onMoveDown: () => handleMoveDown(index),
|
|
2614
|
+
isFirst: index === 0,
|
|
2615
|
+
isLast: index === savedDimensions.length - 1,
|
|
2616
|
+
sortOrder: dim.sortOrder || null,
|
|
2617
|
+
onSortOrderChange: sortOrder => handleSortOrderChange(index, sortOrder),
|
|
2618
|
+
fullPath: dim.fullPath,
|
|
2619
|
+
defaultTitle: dim.dimensionTitle,
|
|
2620
|
+
customTitle: titleOverrides[dim.fullPath],
|
|
2621
|
+
onUpdateTitle: onUpdateTitle,
|
|
2622
|
+
onResetTitle: onResetTitle
|
|
2623
|
+
})))))));
|
|
2624
|
+
};
|
|
2625
|
+
|
|
2626
|
+
// Sortable Chip Component
|
|
2627
|
+
const SortableChip = ({
|
|
2628
|
+
id,
|
|
2629
|
+
label,
|
|
2630
|
+
fullLabel,
|
|
2631
|
+
onDelete,
|
|
2632
|
+
onMoveUp,
|
|
2633
|
+
onMoveDown,
|
|
2634
|
+
isFirst,
|
|
2635
|
+
isLast,
|
|
2636
|
+
fullPath,
|
|
2637
|
+
defaultTitle,
|
|
2638
|
+
customTitle,
|
|
2639
|
+
onUpdateTitle,
|
|
2640
|
+
onResetTitle
|
|
2641
|
+
}) => {
|
|
2642
|
+
const [isEditing, setIsEditing] = useState(false);
|
|
2643
|
+
const [editValue, setEditValue] = useState('');
|
|
2644
|
+
const inputRef = useRef(null);
|
|
2645
|
+
const containerRef = useRef(null);
|
|
2646
|
+
const {
|
|
2647
|
+
attributes,
|
|
2648
|
+
listeners,
|
|
2649
|
+
setNodeRef,
|
|
2650
|
+
transform,
|
|
2651
|
+
transition,
|
|
2652
|
+
isDragging
|
|
2653
|
+
} = useSortable({
|
|
2654
|
+
id
|
|
2655
|
+
});
|
|
2656
|
+
const style = {
|
|
2657
|
+
transform: CSS.Transform.toString(transform),
|
|
2658
|
+
transition,
|
|
2659
|
+
opacity: isDragging ? 0.5 : 1,
|
|
2660
|
+
display: 'flex',
|
|
2661
|
+
alignItems: 'center',
|
|
2662
|
+
width: '100%'
|
|
2663
|
+
};
|
|
2664
|
+
|
|
2665
|
+
// Focus input when entering edit mode
|
|
2666
|
+
useEffect(() => {
|
|
2667
|
+
if (isEditing && inputRef.current) {
|
|
2668
|
+
inputRef.current.focus();
|
|
2669
|
+
inputRef.current.select();
|
|
2670
|
+
}
|
|
2671
|
+
}, [isEditing]);
|
|
2672
|
+
|
|
2673
|
+
// Handle click outside to cancel
|
|
2674
|
+
useEffect(() => {
|
|
2675
|
+
const handleClickOutside = event => {
|
|
2676
|
+
if (isEditing && containerRef.current && !containerRef.current.contains(event.target)) {
|
|
2677
|
+
handleCancel();
|
|
2678
|
+
}
|
|
2679
|
+
};
|
|
2680
|
+
if (isEditing) {
|
|
2681
|
+
document.addEventListener('mousedown', handleClickOutside);
|
|
2682
|
+
return () => {
|
|
2683
|
+
document.removeEventListener('mousedown', handleClickOutside);
|
|
2684
|
+
};
|
|
2685
|
+
}
|
|
2686
|
+
}, [isEditing]);
|
|
2687
|
+
const handleEditClick = () => {
|
|
2688
|
+
setEditValue(customTitle || defaultTitle);
|
|
2689
|
+
setIsEditing(true);
|
|
2690
|
+
};
|
|
2691
|
+
const handleSave = () => {
|
|
2692
|
+
const trimmedValue = editValue.trim();
|
|
2693
|
+
if (trimmedValue === '') {
|
|
2694
|
+
// Empty string validation - treat as cancel
|
|
2695
|
+
handleCancel();
|
|
2696
|
+
return;
|
|
2697
|
+
}
|
|
2698
|
+
onUpdateTitle(fullPath, trimmedValue);
|
|
2699
|
+
setIsEditing(false);
|
|
2700
|
+
};
|
|
2701
|
+
const handleCancel = () => {
|
|
2702
|
+
setIsEditing(false);
|
|
2703
|
+
setEditValue('');
|
|
2704
|
+
};
|
|
2705
|
+
const handleReset = () => {
|
|
2706
|
+
onResetTitle(fullPath);
|
|
2707
|
+
setIsEditing(false);
|
|
2708
|
+
setEditValue('');
|
|
2709
|
+
};
|
|
2710
|
+
const handleKeyDown = e => {
|
|
2711
|
+
if (e.key === 'Enter') {
|
|
2712
|
+
handleSave();
|
|
2713
|
+
} else if (e.key === 'Escape') {
|
|
2714
|
+
handleCancel();
|
|
2715
|
+
}
|
|
2716
|
+
};
|
|
2717
|
+
const displayLabel = customTitle || label;
|
|
2718
|
+
const hasCustomTitle = !!customTitle;
|
|
2719
|
+
return /*#__PURE__*/React__default.createElement("div", _extends({
|
|
2720
|
+
ref: setNodeRef,
|
|
2721
|
+
style: style
|
|
2722
|
+
}, attributes), /*#__PURE__*/React__default.createElement(Box$1, {
|
|
2723
|
+
ref: containerRef,
|
|
2724
|
+
sx: {
|
|
2725
|
+
display: 'flex',
|
|
2726
|
+
alignItems: 'center',
|
|
2727
|
+
width: '100%',
|
|
2728
|
+
gap: 1
|
|
2729
|
+
}
|
|
2730
|
+
}, /*#__PURE__*/React__default.createElement(Box$1, _extends({}, listeners, {
|
|
2731
|
+
sx: {
|
|
2732
|
+
display: 'flex',
|
|
2733
|
+
alignItems: 'center',
|
|
2734
|
+
cursor: 'grab',
|
|
2735
|
+
'&:active': {
|
|
2736
|
+
cursor: 'grabbing'
|
|
2737
|
+
}
|
|
2738
|
+
}
|
|
2739
|
+
}), /*#__PURE__*/React__default.createElement(DragIndicatorIcon, {
|
|
2740
|
+
sx: {
|
|
2741
|
+
cursor: 'grab'
|
|
2742
|
+
}
|
|
2743
|
+
})), !isEditing ? /*#__PURE__*/React__default.createElement(React__default.Fragment, null, /*#__PURE__*/React__default.createElement(Tooltip, {
|
|
2744
|
+
title: fullLabel,
|
|
2745
|
+
arrow: true,
|
|
2746
|
+
placement: "top"
|
|
2747
|
+
}, /*#__PURE__*/React__default.createElement(Chip, {
|
|
2748
|
+
label: displayLabel,
|
|
2749
|
+
onDelete: onDelete,
|
|
2750
|
+
color: "secondary",
|
|
2751
|
+
variant: "outlined",
|
|
2752
|
+
sx: {
|
|
2753
|
+
fontWeight: hasCustomTitle ? 'bold' : 'normal',
|
|
2754
|
+
fontStyle: hasCustomTitle ? 'italic' : 'normal'
|
|
2755
|
+
}
|
|
2756
|
+
})), /*#__PURE__*/React__default.createElement(Tooltip, {
|
|
2757
|
+
title: "Edit title",
|
|
2758
|
+
arrow: true,
|
|
2759
|
+
placement: "top"
|
|
2760
|
+
}, /*#__PURE__*/React__default.createElement(IconButton, {
|
|
2761
|
+
size: "small",
|
|
2762
|
+
onClick: handleEditClick,
|
|
2763
|
+
"aria-label": "edit title"
|
|
2764
|
+
}, /*#__PURE__*/React__default.createElement(EditIcon, {
|
|
2765
|
+
fontSize: "small"
|
|
2766
|
+
})))) : /*#__PURE__*/React__default.createElement(React__default.Fragment, null, /*#__PURE__*/React__default.createElement(TextField, {
|
|
2767
|
+
inputRef: inputRef,
|
|
2768
|
+
value: editValue,
|
|
2769
|
+
onChange: e => setEditValue(e.target.value),
|
|
2770
|
+
onKeyDown: handleKeyDown,
|
|
2771
|
+
size: "small",
|
|
2772
|
+
sx: {
|
|
2773
|
+
width: '200px'
|
|
2774
|
+
}
|
|
2775
|
+
}), /*#__PURE__*/React__default.createElement(Tooltip, {
|
|
2776
|
+
title: "Save",
|
|
2777
|
+
arrow: true,
|
|
2778
|
+
placement: "top"
|
|
2779
|
+
}, /*#__PURE__*/React__default.createElement(IconButton, {
|
|
2780
|
+
size: "small",
|
|
2781
|
+
onClick: handleSave,
|
|
2782
|
+
color: "primary",
|
|
2783
|
+
"aria-label": "save title"
|
|
2784
|
+
}, /*#__PURE__*/React__default.createElement(CheckIcon, {
|
|
2785
|
+
fontSize: "small"
|
|
2786
|
+
}))), /*#__PURE__*/React__default.createElement(Tooltip, {
|
|
2787
|
+
title: "Cancel",
|
|
2788
|
+
arrow: true,
|
|
2789
|
+
placement: "top"
|
|
2790
|
+
}, /*#__PURE__*/React__default.createElement(IconButton, {
|
|
2791
|
+
size: "small",
|
|
2792
|
+
onClick: handleCancel,
|
|
2793
|
+
"aria-label": "cancel edit"
|
|
2794
|
+
}, /*#__PURE__*/React__default.createElement(CloseIcon, {
|
|
2795
|
+
fontSize: "small"
|
|
2796
|
+
}))), hasCustomTitle && /*#__PURE__*/React__default.createElement(Tooltip, {
|
|
2797
|
+
title: "Reset to default",
|
|
2798
|
+
arrow: true,
|
|
2799
|
+
placement: "top"
|
|
2800
|
+
}, /*#__PURE__*/React__default.createElement(IconButton, {
|
|
2801
|
+
size: "small",
|
|
2802
|
+
onClick: handleReset,
|
|
2803
|
+
color: "warning",
|
|
2804
|
+
"aria-label": "reset title"
|
|
2805
|
+
}, /*#__PURE__*/React__default.createElement(RestartAltIcon, {
|
|
2806
|
+
fontSize: "small"
|
|
2807
|
+
})))), !isEditing && /*#__PURE__*/React__default.createElement(React__default.Fragment, null, /*#__PURE__*/React__default.createElement(Box$1, {
|
|
2808
|
+
sx: {
|
|
2809
|
+
flex: 1
|
|
2810
|
+
}
|
|
2811
|
+
}), /*#__PURE__*/React__default.createElement(Box$1, {
|
|
2812
|
+
sx: {
|
|
2813
|
+
display: 'flex',
|
|
2814
|
+
gap: 0.5
|
|
2815
|
+
}
|
|
2816
|
+
}, /*#__PURE__*/React__default.createElement(IconButton, {
|
|
2817
|
+
size: "small",
|
|
2818
|
+
onClick: onMoveUp,
|
|
2819
|
+
disabled: isFirst,
|
|
2820
|
+
"aria-label": "move up"
|
|
2821
|
+
}, /*#__PURE__*/React__default.createElement(ArrowUpwardIcon, {
|
|
2822
|
+
fontSize: "small"
|
|
2823
|
+
})), /*#__PURE__*/React__default.createElement(IconButton, {
|
|
2824
|
+
size: "small",
|
|
2825
|
+
onClick: onMoveDown,
|
|
2826
|
+
disabled: isLast,
|
|
2827
|
+
"aria-label": "move down"
|
|
2828
|
+
}, /*#__PURE__*/React__default.createElement(ArrowDownwardIcon, {
|
|
2829
|
+
fontSize: "small"
|
|
2830
|
+
}))))));
|
|
2831
|
+
};
|
|
2832
|
+
const Metrics = ({
|
|
2833
|
+
providersData,
|
|
2834
|
+
rootProvider,
|
|
2835
|
+
savedMetrics = [],
|
|
2836
|
+
onSaveMetric,
|
|
2837
|
+
onRemoveMetric,
|
|
2838
|
+
onReorderMetrics,
|
|
2839
|
+
titleOverrides = {},
|
|
2840
|
+
onUpdateTitle,
|
|
2841
|
+
onResetTitle,
|
|
2842
|
+
existingDimensions = [],
|
|
2843
|
+
existingFilters = {}
|
|
2844
|
+
}) => {
|
|
2845
|
+
const [isAdding, setIsAdding] = useState(false);
|
|
2846
|
+
const [metricSelectionChain, setMetricSelectionChain] = useState([]);
|
|
2847
|
+
const [selectedMetric, setSelectedMetric] = useState('');
|
|
2848
|
+
|
|
2849
|
+
// Setup drag and drop sensors
|
|
2850
|
+
const sensors = useSensors(useSensor(PointerSensor), useSensor(KeyboardSensor, {
|
|
2851
|
+
coordinateGetter: sortableKeyboardCoordinates
|
|
2852
|
+
}));
|
|
2853
|
+
|
|
2854
|
+
// Handle drag end
|
|
2855
|
+
const handleDragEnd = event => {
|
|
2856
|
+
const {
|
|
2857
|
+
active,
|
|
2858
|
+
over
|
|
2859
|
+
} = event;
|
|
2860
|
+
if (over && active.id !== over.id) {
|
|
2861
|
+
const oldIndex = savedMetrics.findIndex((_, i) => i === active.id);
|
|
2862
|
+
const newIndex = savedMetrics.findIndex((_, i) => i === over.id);
|
|
2863
|
+
const newOrder = arrayMove(savedMetrics, oldIndex, newIndex);
|
|
2864
|
+
onReorderMetrics(newOrder);
|
|
2865
|
+
}
|
|
2866
|
+
};
|
|
2867
|
+
|
|
2868
|
+
// Handle move up
|
|
2869
|
+
const handleMoveUp = index => {
|
|
2870
|
+
if (index > 0) {
|
|
2871
|
+
const newOrder = arrayMove(savedMetrics, index, index - 1);
|
|
2872
|
+
onReorderMetrics(newOrder);
|
|
2873
|
+
}
|
|
2874
|
+
};
|
|
2875
|
+
|
|
2876
|
+
// Handle move down
|
|
2877
|
+
const handleMoveDown = index => {
|
|
2878
|
+
if (index < savedMetrics.length - 1) {
|
|
2879
|
+
const newOrder = arrayMove(savedMetrics, index, index + 1);
|
|
2880
|
+
onReorderMetrics(newOrder);
|
|
2881
|
+
}
|
|
2882
|
+
};
|
|
2883
|
+
|
|
2884
|
+
// Get the current provider based on selection chain
|
|
2885
|
+
const getCurrentProvider = () => {
|
|
2886
|
+
if (!rootProvider) return null;
|
|
2887
|
+
if (metricSelectionChain.length === 0) return rootProvider;
|
|
2888
|
+
return metricSelectionChain[metricSelectionChain.length - 1].targetKey;
|
|
2889
|
+
};
|
|
2890
|
+
|
|
2891
|
+
// Get metrics for the current provider
|
|
2892
|
+
const getMetricItems = () => {
|
|
2893
|
+
const currentProvider = getCurrentProvider();
|
|
2894
|
+
if (!currentProvider || !providersData || !providersData[currentProvider]) {
|
|
2895
|
+
return [];
|
|
2896
|
+
}
|
|
2897
|
+
const provider = providersData[currentProvider];
|
|
2898
|
+
if (!provider.metrics) return [];
|
|
2899
|
+
|
|
2900
|
+
// Build the alias path for the current selection chain
|
|
2901
|
+
const rootProviderData = providersData[rootProvider];
|
|
2902
|
+
const aliasPath = [rootProviderData.default_alias];
|
|
2903
|
+
|
|
2904
|
+
// Add relation aliases from the selection chain
|
|
2905
|
+
let currentProviderKey = rootProvider;
|
|
2906
|
+
metricSelectionChain.forEach(selection => {
|
|
2907
|
+
const providerData = providersData[currentProviderKey];
|
|
2908
|
+
if (providerData && providerData.relations) {
|
|
2909
|
+
const relationObj = providerData.relations.find(rel => rel.name === selection.relationName);
|
|
2910
|
+
if (relationObj) {
|
|
2911
|
+
aliasPath.push(relationObj.alias);
|
|
2912
|
+
}
|
|
2913
|
+
}
|
|
2914
|
+
currentProviderKey = selection.targetKey;
|
|
2915
|
+
});
|
|
2916
|
+
|
|
2917
|
+
// Create a Set of already selected metric fullPaths for quick lookup
|
|
2918
|
+
const selectedFullPaths = new Set(savedMetrics.map(metric => metric.fullPath));
|
|
2919
|
+
|
|
2920
|
+
// Metrics are a direct array, not nested by alias
|
|
2921
|
+
// Use index to ensure unique keys even if metric names are duplicated
|
|
2922
|
+
const items = provider.metrics.map((metric, index) => {
|
|
2923
|
+
// Construct the fullPath for this metric
|
|
2924
|
+
const fullPath = `${aliasPath.join('_')}.${metric.name}`;
|
|
2925
|
+
|
|
2926
|
+
// Check if this metric is already selected
|
|
2927
|
+
const isAlreadySelected = selectedFullPaths.has(fullPath);
|
|
2928
|
+
return {
|
|
2929
|
+
key: `${currentProvider}_${metric.name}_${index}`,
|
|
2930
|
+
value: metric.title || metric.name,
|
|
2931
|
+
metricName: metric.name,
|
|
2932
|
+
metric: metric,
|
|
2933
|
+
disabled: isAlreadySelected
|
|
2934
|
+
};
|
|
2935
|
+
});
|
|
2936
|
+
return items;
|
|
2937
|
+
};
|
|
2938
|
+
const handleAddClick = () => {
|
|
2939
|
+
setIsAdding(true);
|
|
2940
|
+
setMetricSelectionChain([]);
|
|
2941
|
+
setSelectedMetric('');
|
|
2942
|
+
};
|
|
2943
|
+
const handleCancel = () => {
|
|
2944
|
+
setIsAdding(false);
|
|
2945
|
+
setMetricSelectionChain([]);
|
|
2946
|
+
setSelectedMetric('');
|
|
2947
|
+
};
|
|
2948
|
+
const handleSave = () => {
|
|
2949
|
+
if (!selectedMetric) return;
|
|
2950
|
+
const metricItems = getMetricItems();
|
|
2951
|
+
const selectedItem = metricItems.find(item => item.key === selectedMetric);
|
|
2952
|
+
if (!selectedItem) return;
|
|
2953
|
+
|
|
2954
|
+
// Build the complete relation objects array
|
|
2955
|
+
const relations = [];
|
|
2956
|
+
const providerPath = [rootProvider];
|
|
2957
|
+
const relationNames = [];
|
|
2958
|
+
|
|
2959
|
+
// Collect all relation objects from the selection chain
|
|
2960
|
+
let currentProviderKey = rootProvider;
|
|
2961
|
+
metricSelectionChain.forEach(selection => {
|
|
2962
|
+
const provider = providersData[currentProviderKey];
|
|
2963
|
+
if (provider && provider.relations) {
|
|
2964
|
+
// Find the complete relation object
|
|
2965
|
+
const relationObj = provider.relations.find(rel => rel.name === selection.relationName);
|
|
2966
|
+
if (relationObj) {
|
|
2967
|
+
relations.push(relationObj);
|
|
2968
|
+
relationNames.push(relationObj.name);
|
|
2969
|
+
}
|
|
2970
|
+
}
|
|
2971
|
+
providerPath.push(selection.targetKey);
|
|
2972
|
+
currentProviderKey = selection.targetKey;
|
|
2973
|
+
});
|
|
2974
|
+
|
|
2975
|
+
// Build the alias path: root_alias + relation_aliases + metric_name
|
|
2976
|
+
const rootProviderData = providersData[rootProvider];
|
|
2977
|
+
const aliasPath = [rootProviderData.default_alias];
|
|
2978
|
+
relations.forEach(rel => {
|
|
2979
|
+
aliasPath.push(rel.alias);
|
|
2980
|
+
});
|
|
2981
|
+
const fullPath = `${aliasPath.join('_')}.${selectedItem.metricName}`;
|
|
2982
|
+
const metricData = {
|
|
2983
|
+
// Complete metric object from the final provider
|
|
2984
|
+
metric: selectedItem.metric,
|
|
2985
|
+
// Array of complete relation objects
|
|
2986
|
+
relations: relations,
|
|
2987
|
+
// Metadata
|
|
2988
|
+
providerPath: providerPath,
|
|
2989
|
+
relationNames: relationNames,
|
|
2990
|
+
metricName: selectedItem.metricName,
|
|
2991
|
+
metricTitle: selectedItem.value,
|
|
2992
|
+
// The constructed path for server
|
|
2993
|
+
fullPath: fullPath
|
|
2994
|
+
};
|
|
2995
|
+
onSaveMetric(metricData);
|
|
2996
|
+
handleCancel();
|
|
2997
|
+
};
|
|
2998
|
+
const handleMetricChange = event => {
|
|
2999
|
+
setSelectedMetric(event.target.value);
|
|
3000
|
+
};
|
|
3001
|
+
const handleAddAll = () => {
|
|
3002
|
+
const metricItems = getMetricItems();
|
|
3003
|
+
|
|
3004
|
+
// Filter out already selected metrics (disabled items)
|
|
3005
|
+
const availableItems = metricItems.filter(item => !item.disabled);
|
|
3006
|
+
if (availableItems.length === 0) return;
|
|
3007
|
+
|
|
3008
|
+
// Build the complete relation objects array (same for all metrics in this provider)
|
|
3009
|
+
const relations = [];
|
|
3010
|
+
const providerPath = [rootProvider];
|
|
3011
|
+
const relationNames = [];
|
|
3012
|
+
|
|
3013
|
+
// Collect all relation objects from the selection chain
|
|
3014
|
+
let currentProviderKey = rootProvider;
|
|
3015
|
+
metricSelectionChain.forEach(selection => {
|
|
3016
|
+
const provider = providersData[currentProviderKey];
|
|
3017
|
+
if (provider && provider.relations) {
|
|
3018
|
+
// Find the complete relation object
|
|
3019
|
+
const relationObj = provider.relations.find(rel => rel.name === selection.relationName);
|
|
3020
|
+
if (relationObj) {
|
|
3021
|
+
relations.push(relationObj);
|
|
3022
|
+
relationNames.push(relationObj.name);
|
|
3023
|
+
}
|
|
3024
|
+
}
|
|
3025
|
+
providerPath.push(selection.targetKey);
|
|
3026
|
+
currentProviderKey = selection.targetKey;
|
|
3027
|
+
});
|
|
3028
|
+
|
|
3029
|
+
// Build the alias path: root_alias + relation_aliases
|
|
3030
|
+
const rootProviderData = providersData[rootProvider];
|
|
3031
|
+
const aliasPath = [rootProviderData.default_alias];
|
|
3032
|
+
relations.forEach(rel => {
|
|
3033
|
+
aliasPath.push(rel.alias);
|
|
3034
|
+
});
|
|
3035
|
+
|
|
3036
|
+
// Create metric data for each available metric
|
|
3037
|
+
availableItems.forEach(item => {
|
|
3038
|
+
const fullPath = `${aliasPath.join('_')}.${item.metricName}`;
|
|
3039
|
+
const metricData = {
|
|
3040
|
+
// Complete metric object from the final provider
|
|
3041
|
+
metric: item.metric,
|
|
3042
|
+
// Array of complete relation objects
|
|
3043
|
+
relations: relations,
|
|
3044
|
+
// Metadata
|
|
3045
|
+
providerPath: providerPath,
|
|
3046
|
+
relationNames: relationNames,
|
|
3047
|
+
metricName: item.metricName,
|
|
3048
|
+
metricTitle: item.value,
|
|
3049
|
+
// The constructed path for server
|
|
3050
|
+
fullPath: fullPath
|
|
3051
|
+
};
|
|
3052
|
+
onSaveMetric(metricData);
|
|
3053
|
+
});
|
|
3054
|
+
handleCancel();
|
|
3055
|
+
};
|
|
3056
|
+
const formatProviderPath = metric => {
|
|
3057
|
+
// Build path using root provider + relation names
|
|
3058
|
+
const pathParts = [rootProvider];
|
|
3059
|
+
if (metric.relationNames && metric.relationNames.length > 0) {
|
|
3060
|
+
pathParts.push(...metric.relationNames);
|
|
3061
|
+
}
|
|
3062
|
+
return pathParts.join(' → ');
|
|
3063
|
+
};
|
|
3064
|
+
return /*#__PURE__*/React__default.createElement("div", null, /*#__PURE__*/React__default.createElement(Box$1, {
|
|
3065
|
+
sx: {
|
|
3066
|
+
display: 'flex',
|
|
3067
|
+
alignItems: 'center',
|
|
3068
|
+
gap: 2,
|
|
3069
|
+
marginBottom: 2
|
|
3070
|
+
}
|
|
3071
|
+
}, !isAdding ? /*#__PURE__*/React__default.createElement(Button, {
|
|
3072
|
+
variant: "contained",
|
|
3073
|
+
onClick: handleAddClick
|
|
3074
|
+
}, "Add Metric") : /*#__PURE__*/React__default.createElement(React__default.Fragment, null, /*#__PURE__*/React__default.createElement(Button, {
|
|
3075
|
+
variant: "outlined",
|
|
3076
|
+
onClick: handleCancel
|
|
3077
|
+
}, "Cancel"), /*#__PURE__*/React__default.createElement(Button, {
|
|
3078
|
+
variant: "contained",
|
|
3079
|
+
onClick: handleSave,
|
|
3080
|
+
disabled: !selectedMetric
|
|
3081
|
+
}, "Save Metric"))), isAdding && /*#__PURE__*/React__default.createElement(Paper$1, {
|
|
3082
|
+
sx: {
|
|
3083
|
+
padding: 3,
|
|
3084
|
+
marginBottom: 3
|
|
3085
|
+
}
|
|
3086
|
+
}, /*#__PURE__*/React__default.createElement(Typography$1, {
|
|
3087
|
+
variant: "h6",
|
|
3088
|
+
sx: {
|
|
3089
|
+
marginBottom: 2
|
|
3090
|
+
}
|
|
3091
|
+
}, "Select Provider Path"), /*#__PURE__*/React__default.createElement(ProviderSelection, {
|
|
3092
|
+
providersData: providersData,
|
|
3093
|
+
rootProvider: rootProvider,
|
|
3094
|
+
onSelectionChange: setMetricSelectionChain,
|
|
3095
|
+
existingDimensions: existingDimensions,
|
|
3096
|
+
existingMetrics: savedMetrics,
|
|
3097
|
+
existingFilters: existingFilters
|
|
3098
|
+
}), /*#__PURE__*/React__default.createElement(Typography$1, {
|
|
3099
|
+
variant: "h6",
|
|
3100
|
+
sx: {
|
|
3101
|
+
marginTop: 3,
|
|
3102
|
+
marginBottom: 2
|
|
3103
|
+
}
|
|
3104
|
+
}, "Select Metric"), /*#__PURE__*/React__default.createElement(Box$1, {
|
|
3105
|
+
sx: {
|
|
3106
|
+
display: 'flex',
|
|
3107
|
+
alignItems: 'center',
|
|
3108
|
+
gap: 1
|
|
3109
|
+
}
|
|
3110
|
+
}, /*#__PURE__*/React__default.createElement(SingleSelect, {
|
|
3111
|
+
items: getMetricItems(),
|
|
3112
|
+
value: selectedMetric,
|
|
3113
|
+
label: "Choose Metric",
|
|
3114
|
+
onChange: handleMetricChange,
|
|
3115
|
+
sx: {
|
|
3116
|
+
width: '400px'
|
|
3117
|
+
}
|
|
3118
|
+
}), /*#__PURE__*/React__default.createElement(Tooltip, {
|
|
3119
|
+
title: "Add all available metrics",
|
|
3120
|
+
arrow: true,
|
|
3121
|
+
placement: "top"
|
|
3122
|
+
}, /*#__PURE__*/React__default.createElement("span", null, /*#__PURE__*/React__default.createElement(IconButton, {
|
|
3123
|
+
color: "primary",
|
|
3124
|
+
onClick: handleAddAll,
|
|
3125
|
+
disabled: getMetricItems().filter(item => !item.disabled).length === 0,
|
|
3126
|
+
"aria-label": "add all metrics"
|
|
3127
|
+
}, /*#__PURE__*/React__default.createElement(PlaylistAddIcon, null)))))), savedMetrics.length > 0 && /*#__PURE__*/React__default.createElement(Box$1, {
|
|
3128
|
+
sx: {
|
|
3129
|
+
marginTop: 3
|
|
3130
|
+
}
|
|
3131
|
+
}, /*#__PURE__*/React__default.createElement(Typography$1, {
|
|
3132
|
+
variant: "h6",
|
|
3133
|
+
sx: {
|
|
3134
|
+
marginBottom: 2
|
|
3135
|
+
}
|
|
3136
|
+
}, "Saved Metrics (Drag to reorder or use arrows)"), /*#__PURE__*/React__default.createElement(DndContext, {
|
|
3137
|
+
sensors: sensors,
|
|
3138
|
+
collisionDetection: closestCenter,
|
|
3139
|
+
onDragEnd: handleDragEnd
|
|
3140
|
+
}, /*#__PURE__*/React__default.createElement(SortableContext, {
|
|
3141
|
+
items: savedMetrics.map((_, index) => index),
|
|
3142
|
+
strategy: verticalListSortingStrategy
|
|
3143
|
+
}, /*#__PURE__*/React__default.createElement(Box$1, {
|
|
3144
|
+
sx: {
|
|
3145
|
+
display: 'flex',
|
|
3146
|
+
flexDirection: 'column',
|
|
3147
|
+
gap: 1
|
|
3148
|
+
}
|
|
3149
|
+
}, savedMetrics.map((metric, index) => /*#__PURE__*/React__default.createElement(SortableChip, {
|
|
3150
|
+
key: index,
|
|
3151
|
+
id: index,
|
|
3152
|
+
label: metric.metricTitle,
|
|
3153
|
+
fullLabel: `${formatProviderPath(metric)} → ${metric.metricTitle} (${metric.fullPath})`,
|
|
3154
|
+
onDelete: () => onRemoveMetric(index),
|
|
3155
|
+
onMoveUp: () => handleMoveUp(index),
|
|
3156
|
+
onMoveDown: () => handleMoveDown(index),
|
|
3157
|
+
isFirst: index === 0,
|
|
3158
|
+
isLast: index === savedMetrics.length - 1,
|
|
3159
|
+
fullPath: metric.fullPath,
|
|
3160
|
+
defaultTitle: metric.metricTitle,
|
|
3161
|
+
customTitle: titleOverrides[metric.fullPath],
|
|
3162
|
+
onUpdateTitle: onUpdateTitle,
|
|
3163
|
+
onResetTitle: onResetTitle
|
|
3164
|
+
})))))));
|
|
3165
|
+
};
|
|
3166
|
+
|
|
3167
|
+
const Filters = ({
|
|
3168
|
+
providersData,
|
|
3169
|
+
rootProvider,
|
|
3170
|
+
savedFilters = {},
|
|
3171
|
+
onSaveFilter,
|
|
3172
|
+
onRemoveFilter,
|
|
3173
|
+
existingDimensions = [],
|
|
3174
|
+
existingMetrics = [],
|
|
3175
|
+
titleOverrides = {},
|
|
3176
|
+
onUpdateTitle,
|
|
3177
|
+
onResetTitle
|
|
3178
|
+
}) => {
|
|
3179
|
+
const reportingContext = useReportingContextOptional();
|
|
3180
|
+
const [isAdding, setIsAdding] = useState(false);
|
|
3181
|
+
const [dimensionSelectionChain, setDimensionSelectionChain] = useState([]);
|
|
3182
|
+
const [selectedDimension, setSelectedDimension] = useState('');
|
|
3183
|
+
const [availableFilterValues, setAvailableFilterValues] = useState([]);
|
|
3184
|
+
const [selectedFilterValues, setSelectedFilterValues] = useState([]);
|
|
3185
|
+
const [loadingFilterValues, setLoadingFilterValues] = useState(false);
|
|
3186
|
+
const [editingFilterPath, setEditingFilterPath] = useState(null); // Track which filter is being edited
|
|
3187
|
+
|
|
3188
|
+
// Date range state for date/timestamp filters
|
|
3189
|
+
const [dateRangeFrom, setDateRangeFrom] = useState(null);
|
|
3190
|
+
const [dateRangeTo, setDateRangeTo] = useState(null);
|
|
3191
|
+
|
|
3192
|
+
// Title editing state
|
|
3193
|
+
const [editingTitlePath, setEditingTitlePath] = useState(null);
|
|
3194
|
+
const [editTitleValue, setEditTitleValue] = useState('');
|
|
3195
|
+
|
|
3196
|
+
// Reset filter-related states when provider chain changes
|
|
3197
|
+
useEffect(() => {
|
|
3198
|
+
// Reset dimension selection and filter values when the provider chain changes
|
|
3199
|
+
setSelectedDimension('');
|
|
3200
|
+
setAvailableFilterValues([]);
|
|
3201
|
+
setSelectedFilterValues([]);
|
|
3202
|
+
setDateRangeFrom(null);
|
|
3203
|
+
setDateRangeTo(null);
|
|
3204
|
+
}, [dimensionSelectionChain]);
|
|
3205
|
+
|
|
3206
|
+
// Get the current provider based on selection chain
|
|
3207
|
+
const getCurrentProvider = () => {
|
|
3208
|
+
if (!rootProvider) return null;
|
|
3209
|
+
if (dimensionSelectionChain.length === 0) return rootProvider;
|
|
3210
|
+
return dimensionSelectionChain[dimensionSelectionChain.length - 1].targetKey;
|
|
3211
|
+
};
|
|
3212
|
+
|
|
3213
|
+
// Check if a dimension is suitable for filtering (simple column, not expression-based)
|
|
3214
|
+
const isFilterableDimension = dimension => {
|
|
3215
|
+
if (!dimension.column) return false;
|
|
3216
|
+
|
|
3217
|
+
// Simple column names should only contain:
|
|
3218
|
+
// - letters (a-z, A-Z)
|
|
3219
|
+
// - underscores (_)
|
|
3220
|
+
// - dots (.) for table prefixes like "ft.currency"
|
|
3221
|
+
// - numbers (0-9)
|
|
3222
|
+
// If it contains anything else (spaces, parentheses, operators, etc.), it's an expression
|
|
3223
|
+
const simpleColumnPattern = /^[a-zA-Z0-9_.]+$/;
|
|
3224
|
+
return simpleColumnPattern.test(dimension.column);
|
|
3225
|
+
};
|
|
3226
|
+
|
|
3227
|
+
// Check if a dimension is a date or timestamp type
|
|
3228
|
+
const isDateDimension = dimension => {
|
|
3229
|
+
return dimension && (dimension.type === 'date' || dimension.type === 'timestamp');
|
|
3230
|
+
};
|
|
3231
|
+
|
|
3232
|
+
// Get dimensions for the current provider
|
|
3233
|
+
const getDimensionItems = () => {
|
|
3234
|
+
const currentProvider = getCurrentProvider();
|
|
3235
|
+
if (!currentProvider || !providersData || !providersData[currentProvider]) {
|
|
3236
|
+
return [];
|
|
3237
|
+
}
|
|
3238
|
+
const provider = providersData[currentProvider];
|
|
3239
|
+
if (!provider.dimensions) return [];
|
|
3240
|
+
const items = [];
|
|
3241
|
+
// Iterate through aliases (e.g., 'ba', 'pc')
|
|
3242
|
+
Object.keys(provider.dimensions).forEach(alias => {
|
|
3243
|
+
const dimensionsForAlias = provider.dimensions[alias];
|
|
3244
|
+
// Iterate through dimension keys (e.g., 'created_at', 'status')
|
|
3245
|
+
Object.keys(dimensionsForAlias).forEach(dimKey => {
|
|
3246
|
+
const dimension = dimensionsForAlias[dimKey];
|
|
3247
|
+
|
|
3248
|
+
// Only include dimensions that are suitable for filtering (simple columns, not expressions)
|
|
3249
|
+
if (isFilterableDimension(dimension)) {
|
|
3250
|
+
items.push({
|
|
3251
|
+
// Include provider name to ensure uniqueness across different providers
|
|
3252
|
+
key: `${currentProvider}_${alias}.${dimKey}`,
|
|
3253
|
+
value: dimension.title || dimKey,
|
|
3254
|
+
dimensionKey: dimKey,
|
|
3255
|
+
alias: alias,
|
|
3256
|
+
dimension: dimension
|
|
3257
|
+
});
|
|
3258
|
+
}
|
|
3259
|
+
});
|
|
3260
|
+
});
|
|
3261
|
+
return items;
|
|
3262
|
+
};
|
|
3263
|
+
const handleAddClick = () => {
|
|
3264
|
+
setIsAdding(true);
|
|
3265
|
+
setDimensionSelectionChain([]);
|
|
3266
|
+
setSelectedDimension('');
|
|
3267
|
+
setAvailableFilterValues([]);
|
|
3268
|
+
setSelectedFilterValues([]);
|
|
3269
|
+
setDateRangeFrom(null);
|
|
3270
|
+
setDateRangeTo(null);
|
|
3271
|
+
setEditingFilterPath(null); // Close any editing
|
|
3272
|
+
};
|
|
3273
|
+
const handleCancel = () => {
|
|
3274
|
+
setIsAdding(false);
|
|
3275
|
+
setDimensionSelectionChain([]);
|
|
3276
|
+
setSelectedDimension('');
|
|
3277
|
+
setAvailableFilterValues([]);
|
|
3278
|
+
setSelectedFilterValues([]);
|
|
3279
|
+
setDateRangeFrom(null);
|
|
3280
|
+
setDateRangeTo(null);
|
|
3281
|
+
setEditingFilterPath(null); // Close any editing
|
|
3282
|
+
};
|
|
3283
|
+
const handleEditFilter = async (fullPath, filter) => {
|
|
3284
|
+
// Close add mode if open
|
|
3285
|
+
setIsAdding(false);
|
|
3286
|
+
|
|
3287
|
+
// Set editing mode for this filter
|
|
3288
|
+
setEditingFilterPath(fullPath);
|
|
3289
|
+
|
|
3290
|
+
// Check if this is a date filter
|
|
3291
|
+
if (isDateDimension(filter.dimension)) {
|
|
3292
|
+
// For date filters, set date range values
|
|
3293
|
+
if (filter.values && typeof filter.values === 'object' && !Array.isArray(filter.values)) {
|
|
3294
|
+
setDateRangeFrom(filter.values.gte ? dayjs(filter.values.gte) : null);
|
|
3295
|
+
setDateRangeTo(filter.values.lte ? dayjs(filter.values.lte) : null);
|
|
3296
|
+
}
|
|
3297
|
+
} else {
|
|
3298
|
+
// For regular filters, set selected values and fetch available values
|
|
3299
|
+
setSelectedFilterValues(filter.values || []);
|
|
3300
|
+
await fetchFilterValues(fullPath);
|
|
3301
|
+
}
|
|
3302
|
+
};
|
|
3303
|
+
const handleSaveEditedFilter = fullPath => {
|
|
3304
|
+
// Get the existing filter data
|
|
3305
|
+
const existingFilter = savedFilters[fullPath];
|
|
3306
|
+
|
|
3307
|
+
// Check if this is a date filter
|
|
3308
|
+
if (isDateDimension(existingFilter.dimension)) {
|
|
3309
|
+
// For date filters, validate that at least one date is provided
|
|
3310
|
+
if (!dateRangeFrom && !dateRangeTo) {
|
|
3311
|
+
return; // Don't save if no dates provided
|
|
3312
|
+
}
|
|
3313
|
+
|
|
3314
|
+
// Update with new date range values (only include provided dates)
|
|
3315
|
+
const dateValues = {};
|
|
3316
|
+
if (dateRangeFrom) {
|
|
3317
|
+
dateValues.gte = dateRangeFrom.format('YYYY-MM-DD');
|
|
3318
|
+
}
|
|
3319
|
+
if (dateRangeTo) {
|
|
3320
|
+
dateValues.lte = dateRangeTo.format('YYYY-MM-DD');
|
|
3321
|
+
}
|
|
3322
|
+
const updatedFilter = {
|
|
3323
|
+
...existingFilter,
|
|
3324
|
+
values: dateValues
|
|
3325
|
+
};
|
|
3326
|
+
|
|
3327
|
+
// Save the updated filter
|
|
3328
|
+
onSaveFilter(fullPath, updatedFilter);
|
|
3329
|
+
} else {
|
|
3330
|
+
// For regular filters, validate selected values
|
|
3331
|
+
if (selectedFilterValues.length === 0) {
|
|
3332
|
+
return; // Don't save if no values selected
|
|
3333
|
+
}
|
|
3334
|
+
|
|
3335
|
+
// Update with new values
|
|
3336
|
+
const updatedFilter = {
|
|
3337
|
+
...existingFilter,
|
|
3338
|
+
values: selectedFilterValues
|
|
3339
|
+
};
|
|
3340
|
+
|
|
3341
|
+
// Save the updated filter
|
|
3342
|
+
onSaveFilter(fullPath, updatedFilter);
|
|
3343
|
+
}
|
|
3344
|
+
|
|
3345
|
+
// Close editing mode
|
|
3346
|
+
setEditingFilterPath(null);
|
|
3347
|
+
setAvailableFilterValues([]);
|
|
3348
|
+
setSelectedFilterValues([]);
|
|
3349
|
+
setDateRangeFrom(null);
|
|
3350
|
+
setDateRangeTo(null);
|
|
3351
|
+
};
|
|
3352
|
+
const handleCancelEdit = () => {
|
|
3353
|
+
setEditingFilterPath(null);
|
|
3354
|
+
setAvailableFilterValues([]);
|
|
3355
|
+
setSelectedFilterValues([]);
|
|
3356
|
+
setDateRangeFrom(null);
|
|
3357
|
+
setDateRangeTo(null);
|
|
3358
|
+
};
|
|
3359
|
+
|
|
3360
|
+
// Title editing handlers
|
|
3361
|
+
const handleStartEditTitle = (fullPath, currentTitle) => {
|
|
3362
|
+
setEditingTitlePath(fullPath);
|
|
3363
|
+
setEditTitleValue(titleOverrides[fullPath] || currentTitle || '');
|
|
3364
|
+
};
|
|
3365
|
+
const handleSaveTitle = fullPath => {
|
|
3366
|
+
const trimmedValue = editTitleValue.trim();
|
|
3367
|
+
if (trimmedValue === '') {
|
|
3368
|
+
// Empty string validation - treat as cancel
|
|
3369
|
+
handleCancelEditTitle();
|
|
3370
|
+
return;
|
|
3371
|
+
}
|
|
3372
|
+
onUpdateTitle(fullPath, trimmedValue);
|
|
3373
|
+
setEditingTitlePath(null);
|
|
3374
|
+
setEditTitleValue('');
|
|
3375
|
+
};
|
|
3376
|
+
const handleCancelEditTitle = () => {
|
|
3377
|
+
setEditingTitlePath(null);
|
|
3378
|
+
setEditTitleValue('');
|
|
3379
|
+
};
|
|
3380
|
+
const handleResetTitle = fullPath => {
|
|
3381
|
+
onResetTitle(fullPath);
|
|
3382
|
+
setEditingTitlePath(null);
|
|
3383
|
+
setEditTitleValue('');
|
|
3384
|
+
};
|
|
3385
|
+
const handleKeyDown = (e, fullPath) => {
|
|
3386
|
+
if (e.key === 'Enter') {
|
|
3387
|
+
handleSaveTitle(fullPath);
|
|
3388
|
+
} else if (e.key === 'Escape') {
|
|
3389
|
+
handleCancelEditTitle();
|
|
3390
|
+
}
|
|
3391
|
+
};
|
|
3392
|
+
|
|
3393
|
+
// Fetch distinct values for the selected dimension
|
|
3394
|
+
const fetchFilterValues = async fullPath => {
|
|
3395
|
+
setLoadingFilterValues(true);
|
|
3396
|
+
try {
|
|
3397
|
+
// Get parameters from context if available, otherwise use default
|
|
3398
|
+
const parameters = reportingContext?.parameters || {
|
|
3399
|
+
base_currency: "EUR"
|
|
3400
|
+
};
|
|
3401
|
+
|
|
3402
|
+
// Build a temporary report to fetch distinct values
|
|
3403
|
+
const reportPayload = {
|
|
3404
|
+
provider: rootProvider,
|
|
3405
|
+
doc: {
|
|
3406
|
+
query: {
|
|
3407
|
+
dimensions: [fullPath],
|
|
3408
|
+
metrics: [],
|
|
3409
|
+
order_by: [{
|
|
3410
|
+
"name": fullPath
|
|
3411
|
+
}]
|
|
3412
|
+
}
|
|
3413
|
+
},
|
|
3414
|
+
parameters
|
|
3415
|
+
};
|
|
3416
|
+
console.log('Fetching filter values with payload:', reportPayload);
|
|
3417
|
+
const results = await Api.runAdHocReport({
|
|
3418
|
+
report: reportPayload
|
|
3419
|
+
});
|
|
3420
|
+
console.log('Filter values results:', results);
|
|
3421
|
+
|
|
3422
|
+
// Extract distinct values from results
|
|
3423
|
+
// Results format: [{ "ft_currency": "USD" }, { "ft_currency": "EUR" }, ...]
|
|
3424
|
+
// Note: The API might return keys with underscores instead of dots
|
|
3425
|
+
// The results might be directly an array or nested under 'data' property
|
|
3426
|
+
const dataArray = Array.isArray(results) ? results : results.data || [];
|
|
3427
|
+
const distinctValues = dataArray.map(row => {
|
|
3428
|
+
// Try to find the value using the fullPath or any key in the row
|
|
3429
|
+
let value = row[fullPath];
|
|
3430
|
+
|
|
3431
|
+
// If not found, try with underscores instead of dots
|
|
3432
|
+
if (value === undefined) {
|
|
3433
|
+
const alternateKey = fullPath.replace(/\./g, '_');
|
|
3434
|
+
value = row[alternateKey];
|
|
3435
|
+
}
|
|
3436
|
+
|
|
3437
|
+
// If still not found, use the first value in the row
|
|
3438
|
+
if (value === undefined) {
|
|
3439
|
+
const keys = Object.keys(row);
|
|
3440
|
+
if (keys.length > 0) {
|
|
3441
|
+
value = row[keys[0]];
|
|
3442
|
+
}
|
|
3443
|
+
}
|
|
3444
|
+
return {
|
|
3445
|
+
key: String(value),
|
|
3446
|
+
value: String(value)
|
|
3447
|
+
};
|
|
3448
|
+
});
|
|
3449
|
+
console.log('Transformed distinct values:', distinctValues);
|
|
3450
|
+
setAvailableFilterValues(distinctValues);
|
|
3451
|
+
} catch (error) {
|
|
3452
|
+
console.error('Error fetching filter values:', error);
|
|
3453
|
+
setAvailableFilterValues([]);
|
|
3454
|
+
} finally {
|
|
3455
|
+
setLoadingFilterValues(false);
|
|
3456
|
+
}
|
|
3457
|
+
};
|
|
3458
|
+
const handleSave = () => {
|
|
3459
|
+
const dimensionItems = getDimensionItems();
|
|
3460
|
+
const selectedItem = dimensionItems.find(item => item.key === selectedDimension);
|
|
3461
|
+
if (!selectedItem) return;
|
|
3462
|
+
|
|
3463
|
+
// Check if this is a date filter
|
|
3464
|
+
const isDate = isDateDimension(selectedItem.dimension);
|
|
3465
|
+
|
|
3466
|
+
// Validate based on filter type
|
|
3467
|
+
if (isDate) {
|
|
3468
|
+
if (!dateRangeFrom && !dateRangeTo) return; // At least one date must be provided
|
|
3469
|
+
} else {
|
|
3470
|
+
if (selectedFilterValues.length === 0) return; // Regular filters must have values
|
|
3471
|
+
}
|
|
3472
|
+
if (!selectedDimension) return;
|
|
3473
|
+
|
|
3474
|
+
// Build the complete relation objects array
|
|
3475
|
+
const relations = [];
|
|
3476
|
+
const providerPath = [rootProvider];
|
|
3477
|
+
const relationNames = [];
|
|
3478
|
+
|
|
3479
|
+
// Collect all relation objects from the selection chain
|
|
3480
|
+
let currentProviderKey = rootProvider;
|
|
3481
|
+
dimensionSelectionChain.forEach(selection => {
|
|
3482
|
+
const provider = providersData[currentProviderKey];
|
|
3483
|
+
if (provider && provider.relations) {
|
|
3484
|
+
// Find the complete relation object
|
|
3485
|
+
const relationObj = provider.relations.find(rel => rel.name === selection.relationName);
|
|
3486
|
+
if (relationObj) {
|
|
3487
|
+
relations.push(relationObj);
|
|
3488
|
+
relationNames.push(relationObj.name);
|
|
3489
|
+
}
|
|
3490
|
+
}
|
|
3491
|
+
providerPath.push(selection.targetKey);
|
|
3492
|
+
currentProviderKey = selection.targetKey;
|
|
3493
|
+
});
|
|
3494
|
+
|
|
3495
|
+
// Build the alias path: root_alias + relation_aliases + dimension_key
|
|
3496
|
+
const rootProviderData = providersData[rootProvider];
|
|
3497
|
+
const aliasPath = [rootProviderData.default_alias];
|
|
3498
|
+
relations.forEach(rel => {
|
|
3499
|
+
aliasPath.push(rel.alias);
|
|
3500
|
+
});
|
|
3501
|
+
const fullPath = `${aliasPath.join('_')}.${selectedItem.dimensionKey}`;
|
|
3502
|
+
|
|
3503
|
+
// Prepare filter values based on type
|
|
3504
|
+
let filterValues;
|
|
3505
|
+
if (isDate) {
|
|
3506
|
+
// For date filters, store as {gte, lte} object (only include provided dates)
|
|
3507
|
+
filterValues = {};
|
|
3508
|
+
if (dateRangeFrom) {
|
|
3509
|
+
filterValues.gte = dateRangeFrom.format('YYYY-MM-DD');
|
|
3510
|
+
}
|
|
3511
|
+
if (dateRangeTo) {
|
|
3512
|
+
filterValues.lte = dateRangeTo.format('YYYY-MM-DD');
|
|
3513
|
+
}
|
|
3514
|
+
} else {
|
|
3515
|
+
// For regular filters, store as array
|
|
3516
|
+
filterValues = selectedFilterValues;
|
|
3517
|
+
}
|
|
3518
|
+
const filterData = {
|
|
3519
|
+
// Complete dimension object from the final provider
|
|
3520
|
+
dimension: selectedItem.dimension,
|
|
3521
|
+
// Array of complete relation objects
|
|
3522
|
+
relations: relations,
|
|
3523
|
+
// Metadata
|
|
3524
|
+
providerPath: providerPath,
|
|
3525
|
+
relationNames: relationNames,
|
|
3526
|
+
dimensionKey: selectedItem.dimensionKey,
|
|
3527
|
+
dimensionTitle: selectedItem.value,
|
|
3528
|
+
// The constructed path for server
|
|
3529
|
+
fullPath: fullPath,
|
|
3530
|
+
// Selected filter values (array for regular, object for date)
|
|
3531
|
+
values: filterValues
|
|
3532
|
+
};
|
|
3533
|
+
onSaveFilter(fullPath, filterData);
|
|
3534
|
+
handleCancel();
|
|
3535
|
+
};
|
|
3536
|
+
const handleDimensionChange = event => {
|
|
3537
|
+
const dimensionKey = event.target.value;
|
|
3538
|
+
setSelectedDimension(dimensionKey);
|
|
3539
|
+
setSelectedFilterValues([]);
|
|
3540
|
+
setDateRangeFrom(null);
|
|
3541
|
+
setDateRangeTo(null);
|
|
3542
|
+
if (dimensionKey) {
|
|
3543
|
+
// Build the fullPath for this dimension to fetch its values
|
|
3544
|
+
const dimensionItems = getDimensionItems();
|
|
3545
|
+
const selectedItem = dimensionItems.find(item => item.key === dimensionKey);
|
|
3546
|
+
if (selectedItem) {
|
|
3547
|
+
// Check if this is a date dimension
|
|
3548
|
+
const isDate = isDateDimension(selectedItem.dimension);
|
|
3549
|
+
|
|
3550
|
+
// Only fetch values for non-date dimensions
|
|
3551
|
+
if (!isDate) {
|
|
3552
|
+
// Build the complete relation objects array
|
|
3553
|
+
const relations = [];
|
|
3554
|
+
let currentProviderKey = rootProvider;
|
|
3555
|
+
dimensionSelectionChain.forEach(selection => {
|
|
3556
|
+
const provider = providersData[currentProviderKey];
|
|
3557
|
+
if (provider && provider.relations) {
|
|
3558
|
+
const relationObj = provider.relations.find(rel => rel.name === selection.relationName);
|
|
3559
|
+
if (relationObj) {
|
|
3560
|
+
relations.push(relationObj);
|
|
3561
|
+
}
|
|
3562
|
+
}
|
|
3563
|
+
currentProviderKey = selection.targetKey;
|
|
3564
|
+
});
|
|
3565
|
+
|
|
3566
|
+
// Build the alias path
|
|
3567
|
+
const rootProviderData = providersData[rootProvider];
|
|
3568
|
+
const aliasPath = [rootProviderData.default_alias];
|
|
3569
|
+
relations.forEach(rel => {
|
|
3570
|
+
aliasPath.push(rel.alias);
|
|
3571
|
+
});
|
|
3572
|
+
const fullPath = `${aliasPath.join('_')}.${selectedItem.dimensionKey}`;
|
|
3573
|
+
|
|
3574
|
+
// Fetch distinct values for this dimension
|
|
3575
|
+
fetchFilterValues(fullPath);
|
|
3576
|
+
}
|
|
3577
|
+
}
|
|
3578
|
+
} else {
|
|
3579
|
+
setAvailableFilterValues([]);
|
|
3580
|
+
}
|
|
3581
|
+
};
|
|
3582
|
+
const formatProviderPath = filter => {
|
|
3583
|
+
// Build path using root provider + relation names
|
|
3584
|
+
const pathParts = [rootProvider];
|
|
3585
|
+
if (filter.relationNames && filter.relationNames.length > 0) {
|
|
3586
|
+
pathParts.push(...filter.relationNames);
|
|
3587
|
+
}
|
|
3588
|
+
return pathParts.join(' → ');
|
|
3589
|
+
};
|
|
3590
|
+
|
|
3591
|
+
// Convert savedFilters object to array for display
|
|
3592
|
+
const filterEntries = Object.entries(savedFilters);
|
|
3593
|
+
return /*#__PURE__*/React__default.createElement("div", null, /*#__PURE__*/React__default.createElement(Box$1, {
|
|
3594
|
+
sx: {
|
|
3595
|
+
display: 'flex',
|
|
3596
|
+
alignItems: 'center',
|
|
3597
|
+
gap: 2,
|
|
3598
|
+
marginBottom: 2
|
|
3599
|
+
}
|
|
3600
|
+
}, !isAdding ? /*#__PURE__*/React__default.createElement(Button, {
|
|
3601
|
+
variant: "contained",
|
|
3602
|
+
onClick: handleAddClick
|
|
3603
|
+
}, "Add Filter") : /*#__PURE__*/React__default.createElement(React__default.Fragment, null, /*#__PURE__*/React__default.createElement(Button, {
|
|
3604
|
+
variant: "outlined",
|
|
3605
|
+
onClick: handleCancel
|
|
3606
|
+
}, "Cancel"), /*#__PURE__*/React__default.createElement(Button, {
|
|
3607
|
+
variant: "contained",
|
|
3608
|
+
onClick: handleSave,
|
|
3609
|
+
disabled: (() => {
|
|
3610
|
+
if (!selectedDimension) return true;
|
|
3611
|
+
const dimensionItems = getDimensionItems();
|
|
3612
|
+
const selectedItem = dimensionItems.find(item => item.key === selectedDimension);
|
|
3613
|
+
if (!selectedItem) return true;
|
|
3614
|
+
const isDate = isDateDimension(selectedItem.dimension);
|
|
3615
|
+
return isDate ? !dateRangeFrom && !dateRangeTo : selectedFilterValues.length === 0;
|
|
3616
|
+
})()
|
|
3617
|
+
}, "Save Filter"))), isAdding && /*#__PURE__*/React__default.createElement(Paper$1, {
|
|
3618
|
+
sx: {
|
|
3619
|
+
padding: 3,
|
|
3620
|
+
marginBottom: 3
|
|
3621
|
+
}
|
|
3622
|
+
}, /*#__PURE__*/React__default.createElement(Typography$1, {
|
|
3623
|
+
variant: "h6",
|
|
3624
|
+
sx: {
|
|
3625
|
+
marginBottom: 2
|
|
3626
|
+
}
|
|
3627
|
+
}, "Select Provider Path"), /*#__PURE__*/React__default.createElement(ProviderSelection, {
|
|
3628
|
+
providersData: providersData,
|
|
3629
|
+
rootProvider: rootProvider,
|
|
3630
|
+
onSelectionChange: setDimensionSelectionChain,
|
|
3631
|
+
existingDimensions: existingDimensions,
|
|
3632
|
+
existingMetrics: existingMetrics,
|
|
3633
|
+
existingFilters: savedFilters
|
|
3634
|
+
}), /*#__PURE__*/React__default.createElement(Typography$1, {
|
|
3635
|
+
variant: "h6",
|
|
3636
|
+
sx: {
|
|
3637
|
+
marginTop: 3,
|
|
3638
|
+
marginBottom: 2
|
|
3639
|
+
}
|
|
3640
|
+
}, "Select Dimension for Filter"), /*#__PURE__*/React__default.createElement(SingleSelect, {
|
|
3641
|
+
items: getDimensionItems(),
|
|
3642
|
+
value: selectedDimension,
|
|
3643
|
+
label: "Choose Dimension",
|
|
3644
|
+
onChange: handleDimensionChange,
|
|
3645
|
+
sx: {
|
|
3646
|
+
width: '400px'
|
|
3647
|
+
}
|
|
3648
|
+
}), selectedDimension && (() => {
|
|
3649
|
+
const dimensionItems = getDimensionItems();
|
|
3650
|
+
const selectedItem = dimensionItems.find(item => item.key === selectedDimension);
|
|
3651
|
+
const isDate = selectedItem && isDateDimension(selectedItem.dimension);
|
|
3652
|
+
return /*#__PURE__*/React__default.createElement(React__default.Fragment, null, /*#__PURE__*/React__default.createElement(Typography$1, {
|
|
3653
|
+
variant: "h6",
|
|
3654
|
+
sx: {
|
|
3655
|
+
marginTop: 3,
|
|
3656
|
+
marginBottom: 2
|
|
3657
|
+
}
|
|
3658
|
+
}, isDate ? 'Select Date Range' : 'Select Filter Values'), isDate ? /*#__PURE__*/React__default.createElement(LocalizationProvider, {
|
|
3659
|
+
dateAdapter: AdapterDayjs
|
|
3660
|
+
}, /*#__PURE__*/React__default.createElement(Box$1, {
|
|
3661
|
+
sx: {
|
|
3662
|
+
display: 'flex',
|
|
3663
|
+
gap: 2,
|
|
3664
|
+
width: '400px'
|
|
3665
|
+
}
|
|
3666
|
+
}, /*#__PURE__*/React__default.createElement(DatePicker, {
|
|
3667
|
+
label: "From Date",
|
|
3668
|
+
value: dateRangeFrom,
|
|
3669
|
+
onChange: newValue => setDateRangeFrom(newValue),
|
|
3670
|
+
slotProps: {
|
|
3671
|
+
textField: {
|
|
3672
|
+
size: 'small',
|
|
3673
|
+
fullWidth: true
|
|
3674
|
+
},
|
|
3675
|
+
field: {
|
|
3676
|
+
clearable: true
|
|
3677
|
+
}
|
|
3678
|
+
}
|
|
3679
|
+
}), /*#__PURE__*/React__default.createElement(DatePicker, {
|
|
3680
|
+
label: "To Date",
|
|
3681
|
+
value: dateRangeTo,
|
|
3682
|
+
onChange: newValue => setDateRangeTo(newValue),
|
|
3683
|
+
slotProps: {
|
|
3684
|
+
textField: {
|
|
3685
|
+
size: 'small',
|
|
3686
|
+
fullWidth: true
|
|
3687
|
+
},
|
|
3688
|
+
field: {
|
|
3689
|
+
clearable: true
|
|
3690
|
+
}
|
|
3691
|
+
}
|
|
3692
|
+
}))) : /*#__PURE__*/React__default.createElement(Box$1, {
|
|
3693
|
+
sx: {
|
|
3694
|
+
width: '400px'
|
|
3695
|
+
}
|
|
3696
|
+
}, /*#__PURE__*/React__default.createElement(CheckboxMultiAutocomplete, {
|
|
3697
|
+
items: availableFilterValues,
|
|
3698
|
+
selectedKeys: selectedFilterValues,
|
|
3699
|
+
onChange: keys => setSelectedFilterValues(keys),
|
|
3700
|
+
label: "Choose Values",
|
|
3701
|
+
placeholder: "Select one or more values...",
|
|
3702
|
+
loading: loadingFilterValues,
|
|
3703
|
+
helperText: selectedFilterValues.length > 0 ? `${selectedFilterValues.length} value(s) selected` : 'Select at least one value'
|
|
3704
|
+
})));
|
|
3705
|
+
})()), filterEntries.length > 0 && /*#__PURE__*/React__default.createElement(Box$1, {
|
|
3706
|
+
sx: {
|
|
3707
|
+
marginTop: 3
|
|
3708
|
+
}
|
|
3709
|
+
}, /*#__PURE__*/React__default.createElement(Typography$1, {
|
|
3710
|
+
variant: "h6",
|
|
3711
|
+
sx: {
|
|
3712
|
+
marginBottom: 2
|
|
3713
|
+
}
|
|
3714
|
+
}, "Saved Filters"), /*#__PURE__*/React__default.createElement(Box$1, {
|
|
3715
|
+
sx: {
|
|
3716
|
+
display: 'flex',
|
|
3717
|
+
flexDirection: 'column',
|
|
3718
|
+
gap: 2
|
|
3719
|
+
}
|
|
3720
|
+
}, filterEntries.map(([fullPath, filter]) => {
|
|
3721
|
+
const isDate = isDateDimension(filter.dimension);
|
|
3722
|
+
const isEditing = editingFilterPath === fullPath;
|
|
3723
|
+
const isEditingTitle = editingTitlePath === fullPath;
|
|
3724
|
+
|
|
3725
|
+
// Format display values based on filter type
|
|
3726
|
+
let valueCount, valuesList, badgeContent;
|
|
3727
|
+
if (isDate && filter.values && typeof filter.values === 'object' && !Array.isArray(filter.values)) {
|
|
3728
|
+
// Date range filter - handle cases where only one date is provided
|
|
3729
|
+
if (filter.values.gte && filter.values.lte) {
|
|
3730
|
+
valuesList = `${filter.values.gte} to ${filter.values.lte}`;
|
|
3731
|
+
} else if (filter.values.gte) {
|
|
3732
|
+
valuesList = `From ${filter.values.gte}`;
|
|
3733
|
+
} else if (filter.values.lte) {
|
|
3734
|
+
valuesList = `Until ${filter.values.lte}`;
|
|
3735
|
+
} else {
|
|
3736
|
+
valuesList = 'No dates';
|
|
3737
|
+
}
|
|
3738
|
+
badgeContent = '📅'; // Calendar emoji for date filters
|
|
3739
|
+
} else {
|
|
3740
|
+
// Regular filter
|
|
3741
|
+
valueCount = filter.values?.length || 0;
|
|
3742
|
+
valuesList = filter.values?.join(', ') || 'No values';
|
|
3743
|
+
badgeContent = valueCount;
|
|
3744
|
+
}
|
|
3745
|
+
const displayLabel = titleOverrides[fullPath] || filter.dimensionTitle;
|
|
3746
|
+
const hasCustomTitle = !!titleOverrides[fullPath];
|
|
3747
|
+
return /*#__PURE__*/React__default.createElement(Box$1, {
|
|
3748
|
+
key: fullPath
|
|
3749
|
+
}, /*#__PURE__*/React__default.createElement(Box$1, {
|
|
3750
|
+
sx: {
|
|
3751
|
+
display: 'flex',
|
|
3752
|
+
alignItems: 'center',
|
|
3753
|
+
gap: 1,
|
|
3754
|
+
marginBottom: isEditing ? 2 : 0
|
|
3755
|
+
}
|
|
3756
|
+
}, !isEditingTitle ? /*#__PURE__*/React__default.createElement(React__default.Fragment, null, /*#__PURE__*/React__default.createElement(Tooltip, {
|
|
3757
|
+
title: /*#__PURE__*/React__default.createElement("div", null, /*#__PURE__*/React__default.createElement("div", null, /*#__PURE__*/React__default.createElement("strong", null, "Path:"), " ", formatProviderPath(filter), " \u2192 ", filter.dimensionTitle), /*#__PURE__*/React__default.createElement("div", null, /*#__PURE__*/React__default.createElement("strong", null, "Full Path:"), " ", fullPath), /*#__PURE__*/React__default.createElement("div", null, /*#__PURE__*/React__default.createElement("strong", null, isDate ? 'Date Range' : `Values (${valueCount})`, ":"), " ", valuesList)),
|
|
3758
|
+
arrow: true,
|
|
3759
|
+
placement: "top"
|
|
3760
|
+
}, /*#__PURE__*/React__default.createElement(Badge, {
|
|
3761
|
+
badgeContent: badgeContent,
|
|
3762
|
+
color: "secondary"
|
|
3763
|
+
}, /*#__PURE__*/React__default.createElement(Chip, {
|
|
3764
|
+
label: displayLabel,
|
|
3765
|
+
onDelete: () => onRemoveFilter(fullPath),
|
|
3766
|
+
color: "secondary",
|
|
3767
|
+
variant: "outlined",
|
|
3768
|
+
sx: {
|
|
3769
|
+
fontWeight: hasCustomTitle ? 'bold' : 'normal',
|
|
3770
|
+
fontStyle: hasCustomTitle ? 'italic' : 'normal'
|
|
3771
|
+
}
|
|
3772
|
+
}))), /*#__PURE__*/React__default.createElement(Tooltip, {
|
|
3773
|
+
title: "Rename filter",
|
|
3774
|
+
arrow: true
|
|
3775
|
+
}, /*#__PURE__*/React__default.createElement(IconButton, {
|
|
3776
|
+
size: "small",
|
|
3777
|
+
onClick: () => handleStartEditTitle(fullPath, filter.dimensionTitle),
|
|
3778
|
+
color: "primary",
|
|
3779
|
+
disabled: isEditing
|
|
3780
|
+
}, /*#__PURE__*/React__default.createElement(EditIcon, {
|
|
3781
|
+
fontSize: "small"
|
|
3782
|
+
}))), /*#__PURE__*/React__default.createElement(Tooltip, {
|
|
3783
|
+
title: "Edit filter values",
|
|
3784
|
+
arrow: true
|
|
3785
|
+
}, /*#__PURE__*/React__default.createElement(IconButton, {
|
|
3786
|
+
size: "small",
|
|
3787
|
+
onClick: () => handleEditFilter(fullPath, filter),
|
|
3788
|
+
color: "primary",
|
|
3789
|
+
disabled: isEditing
|
|
3790
|
+
}, /*#__PURE__*/React__default.createElement(FilterAltIcon, {
|
|
3791
|
+
fontSize: "small"
|
|
3792
|
+
})))) : /*#__PURE__*/React__default.createElement(React__default.Fragment, null, /*#__PURE__*/React__default.createElement(TextField, {
|
|
3793
|
+
value: editTitleValue,
|
|
3794
|
+
onChange: e => setEditTitleValue(e.target.value),
|
|
3795
|
+
onKeyDown: e => handleKeyDown(e, fullPath),
|
|
3796
|
+
size: "small",
|
|
3797
|
+
autoFocus: true,
|
|
3798
|
+
placeholder: "Enter custom title",
|
|
3799
|
+
sx: {
|
|
3800
|
+
width: '300px'
|
|
3801
|
+
}
|
|
3802
|
+
}), /*#__PURE__*/React__default.createElement(Tooltip, {
|
|
3803
|
+
title: "Save",
|
|
3804
|
+
arrow: true
|
|
3805
|
+
}, /*#__PURE__*/React__default.createElement(IconButton, {
|
|
3806
|
+
size: "small",
|
|
3807
|
+
onClick: () => handleSaveTitle(fullPath),
|
|
3808
|
+
color: "primary"
|
|
3809
|
+
}, /*#__PURE__*/React__default.createElement(CheckIcon, {
|
|
3810
|
+
fontSize: "small"
|
|
3811
|
+
}))), /*#__PURE__*/React__default.createElement(Tooltip, {
|
|
3812
|
+
title: "Cancel",
|
|
3813
|
+
arrow: true
|
|
3814
|
+
}, /*#__PURE__*/React__default.createElement(IconButton, {
|
|
3815
|
+
size: "small",
|
|
3816
|
+
onClick: handleCancelEditTitle,
|
|
3817
|
+
color: "default"
|
|
3818
|
+
}, /*#__PURE__*/React__default.createElement(CloseIcon, {
|
|
3819
|
+
fontSize: "small"
|
|
3820
|
+
}))), hasCustomTitle && /*#__PURE__*/React__default.createElement(Tooltip, {
|
|
3821
|
+
title: "Reset to default",
|
|
3822
|
+
arrow: true
|
|
3823
|
+
}, /*#__PURE__*/React__default.createElement(IconButton, {
|
|
3824
|
+
size: "small",
|
|
3825
|
+
onClick: () => handleResetTitle(fullPath),
|
|
3826
|
+
color: "warning"
|
|
3827
|
+
}, /*#__PURE__*/React__default.createElement(RestartAltIcon, {
|
|
3828
|
+
fontSize: "small"
|
|
3829
|
+
}))))), isEditing && /*#__PURE__*/React__default.createElement(Paper$1, {
|
|
3830
|
+
sx: {
|
|
3831
|
+
padding: 2,
|
|
3832
|
+
marginLeft: 2
|
|
3833
|
+
}
|
|
3834
|
+
}, /*#__PURE__*/React__default.createElement(Typography$1, {
|
|
3835
|
+
variant: "subtitle2",
|
|
3836
|
+
sx: {
|
|
3837
|
+
marginBottom: 1
|
|
3838
|
+
}
|
|
3839
|
+
}, "Edit Filter ", isDate ? 'Date Range' : 'Values', " for: ", filter.dimensionTitle), isDate ? /*#__PURE__*/React__default.createElement(LocalizationProvider, {
|
|
3840
|
+
dateAdapter: AdapterDayjs
|
|
3841
|
+
}, /*#__PURE__*/React__default.createElement(Box$1, {
|
|
3842
|
+
sx: {
|
|
3843
|
+
display: 'flex',
|
|
3844
|
+
gap: 2,
|
|
3845
|
+
width: '400px',
|
|
3846
|
+
marginBottom: 2
|
|
3847
|
+
}
|
|
3848
|
+
}, /*#__PURE__*/React__default.createElement(DatePicker, {
|
|
3849
|
+
label: "From Date",
|
|
3850
|
+
value: dateRangeFrom,
|
|
3851
|
+
onChange: newValue => setDateRangeFrom(newValue),
|
|
3852
|
+
slotProps: {
|
|
3853
|
+
textField: {
|
|
3854
|
+
size: 'small',
|
|
3855
|
+
fullWidth: true
|
|
3856
|
+
},
|
|
3857
|
+
field: {
|
|
3858
|
+
clearable: true
|
|
3859
|
+
}
|
|
3860
|
+
}
|
|
3861
|
+
}), /*#__PURE__*/React__default.createElement(DatePicker, {
|
|
3862
|
+
label: "To Date",
|
|
3863
|
+
value: dateRangeTo,
|
|
3864
|
+
onChange: newValue => setDateRangeTo(newValue),
|
|
3865
|
+
slotProps: {
|
|
3866
|
+
textField: {
|
|
3867
|
+
size: 'small',
|
|
3868
|
+
fullWidth: true
|
|
3869
|
+
},
|
|
3870
|
+
field: {
|
|
3871
|
+
clearable: true
|
|
3872
|
+
}
|
|
3873
|
+
}
|
|
3874
|
+
}))) : /*#__PURE__*/React__default.createElement(Box$1, {
|
|
3875
|
+
sx: {
|
|
3876
|
+
width: '400px',
|
|
3877
|
+
marginBottom: 2
|
|
3878
|
+
}
|
|
3879
|
+
}, /*#__PURE__*/React__default.createElement(CheckboxMultiAutocomplete, {
|
|
3880
|
+
items: availableFilterValues,
|
|
3881
|
+
selectedKeys: selectedFilterValues,
|
|
3882
|
+
onChange: keys => setSelectedFilterValues(keys),
|
|
3883
|
+
label: "Choose Values",
|
|
3884
|
+
placeholder: "Select one or more values...",
|
|
3885
|
+
loading: loadingFilterValues,
|
|
3886
|
+
helperText: selectedFilterValues.length > 0 ? `${selectedFilterValues.length} value(s) selected` : 'Select at least one value'
|
|
3887
|
+
})), /*#__PURE__*/React__default.createElement(Box$1, {
|
|
3888
|
+
sx: {
|
|
3889
|
+
display: 'flex',
|
|
3890
|
+
gap: 1
|
|
3891
|
+
}
|
|
3892
|
+
}, /*#__PURE__*/React__default.createElement(Button, {
|
|
3893
|
+
variant: "outlined",
|
|
3894
|
+
size: "small",
|
|
3895
|
+
onClick: handleCancelEdit
|
|
3896
|
+
}, "Cancel"), /*#__PURE__*/React__default.createElement(Button, {
|
|
3897
|
+
variant: "contained",
|
|
3898
|
+
size: "small",
|
|
3899
|
+
onClick: () => handleSaveEditedFilter(fullPath),
|
|
3900
|
+
disabled: isDate ? !dateRangeFrom && !dateRangeTo : selectedFilterValues.length === 0
|
|
3901
|
+
}, "Save Changes"))));
|
|
3902
|
+
}))));
|
|
3903
|
+
};
|
|
3904
|
+
|
|
3905
|
+
const ReportDataGrid = ({
|
|
3906
|
+
reportData,
|
|
3907
|
+
dimensions,
|
|
3908
|
+
metrics,
|
|
3909
|
+
loading,
|
|
3910
|
+
onPageChange,
|
|
3911
|
+
totalRows = 0,
|
|
3912
|
+
titleOverrides = {
|
|
3913
|
+
dimensions: {},
|
|
3914
|
+
metrics: {}
|
|
3915
|
+
}
|
|
3916
|
+
}) => {
|
|
3917
|
+
const [paginationModel, setPaginationModel] = useState({
|
|
3918
|
+
page: 0,
|
|
3919
|
+
pageSize: 50
|
|
3920
|
+
});
|
|
3921
|
+
|
|
3922
|
+
// Generate columns dynamically from dimensions and metrics
|
|
3923
|
+
const columns = React__default.useMemo(() => {
|
|
3924
|
+
const cols = [];
|
|
3925
|
+
|
|
3926
|
+
// Add dimension columns
|
|
3927
|
+
// Field naming logic:
|
|
3928
|
+
// - If from base provider: baseAlias.field -> baseAlias_field
|
|
3929
|
+
// - If from nested provider: baseAlias_rel1_rel2.field -> rel1_rel2_field (drops base alias)
|
|
3930
|
+
dimensions.forEach(dim => {
|
|
3931
|
+
let fieldName;
|
|
3932
|
+
let headerName;
|
|
3933
|
+
|
|
3934
|
+
// Check if there are relations (nested providers)
|
|
3935
|
+
if (dim.relations && dim.relations.length > 0) {
|
|
3936
|
+
// From nested provider: drop the base provider alias
|
|
3937
|
+
// Example: ft_fa_db.currency -> fa_db_currency
|
|
3938
|
+
const parts = dim.fullPath.split('.');
|
|
3939
|
+
const pathWithoutField = parts[0]; // e.g., "ft_fa_db"
|
|
3940
|
+
const field = parts[1]; // e.g., "currency"
|
|
3941
|
+
|
|
3942
|
+
// Remove the base provider alias (first part before first underscore)
|
|
3943
|
+
const pathParts = pathWithoutField.split('_');
|
|
3944
|
+
pathParts.shift(); // Remove base provider alias
|
|
3945
|
+
const pathWithoutBase = pathParts.join('_'); // e.g., "fa_db"
|
|
3946
|
+
|
|
3947
|
+
fieldName = pathWithoutBase ? `${pathWithoutBase}_${field}` : field;
|
|
3948
|
+
} else {
|
|
3949
|
+
// From base provider: keep the full path with underscore
|
|
3950
|
+
// Example: ba.created_at -> ba_created_at
|
|
3951
|
+
fieldName = dim.fullPath.replace('.', '_');
|
|
3952
|
+
}
|
|
3953
|
+
|
|
3954
|
+
// Check for title override, otherwise use the friendly dimension title from provider
|
|
3955
|
+
headerName = titleOverrides.dimensions[dim.fullPath] || dim.dimensionTitle || fieldName;
|
|
3956
|
+
cols.push({
|
|
3957
|
+
field: fieldName,
|
|
3958
|
+
headerName: headerName,
|
|
3959
|
+
flex: 1,
|
|
3960
|
+
minWidth: 150
|
|
3961
|
+
});
|
|
3962
|
+
});
|
|
3963
|
+
|
|
3964
|
+
// Add metric columns
|
|
3965
|
+
// Metric field naming logic:
|
|
3966
|
+
// - If from base provider: baseAlias.metric -> metric (no prefix at all)
|
|
3967
|
+
// - If from nested provider: baseAlias_rel1_rel2.metric -> rel1_rel2_metric (drops base alias)
|
|
3968
|
+
metrics.forEach(metric => {
|
|
3969
|
+
const metricDef = metric.metric;
|
|
3970
|
+
let fieldName;
|
|
3971
|
+
let headerName;
|
|
3972
|
+
|
|
3973
|
+
// Check if there are relations (nested providers)
|
|
3974
|
+
if (metric.relations && metric.relations.length > 0) {
|
|
3975
|
+
// From nested provider: drop the base provider alias
|
|
3976
|
+
// Example: ft_fa.total_amount -> fa_total_amount
|
|
3977
|
+
const parts = metric.fullPath.split('.');
|
|
3978
|
+
const pathWithoutField = parts[0]; // e.g., "ft_fa"
|
|
3979
|
+
const field = parts[1]; // e.g., "total_amount"
|
|
3980
|
+
|
|
3981
|
+
// Remove the base provider alias (first part before first underscore)
|
|
3982
|
+
const pathParts = pathWithoutField.split('_');
|
|
3983
|
+
pathParts.shift(); // Remove base provider alias
|
|
3984
|
+
const pathWithoutBase = pathParts.join('_'); // e.g., "fa"
|
|
3985
|
+
|
|
3986
|
+
fieldName = pathWithoutBase ? `${pathWithoutBase}_${field}` : field;
|
|
3987
|
+
} else {
|
|
3988
|
+
// From base provider: no prefix at all, just the metric name
|
|
3989
|
+
// Example: fo.total_amount -> total_amount
|
|
3990
|
+
fieldName = metric.metricName;
|
|
3991
|
+
}
|
|
3992
|
+
|
|
3993
|
+
// Check for title override, otherwise use the friendly metric title from provider
|
|
3994
|
+
headerName = titleOverrides.metrics[metric.fullPath] || metric.metricTitle || fieldName;
|
|
3995
|
+
cols.push({
|
|
3996
|
+
field: fieldName,
|
|
3997
|
+
headerName: headerName,
|
|
3998
|
+
flex: 1,
|
|
3999
|
+
minWidth: 150,
|
|
4000
|
+
type: metricDef?.type === 'integer' || metricDef?.type === 'currency' ? 'number' : 'string',
|
|
4001
|
+
valueFormatter: value => {
|
|
4002
|
+
if (value == null) return '';
|
|
4003
|
+
|
|
4004
|
+
// Format based on metric type
|
|
4005
|
+
if (metricDef?.format) {
|
|
4006
|
+
const formatted = numeral(value).format(metricDef.format);
|
|
4007
|
+
if (metricDef?.prefix) {
|
|
4008
|
+
return `${metricDef.prefix} ${formatted}`;
|
|
4009
|
+
}
|
|
4010
|
+
return formatted;
|
|
4011
|
+
}
|
|
4012
|
+
return value;
|
|
4013
|
+
}
|
|
4014
|
+
});
|
|
4015
|
+
});
|
|
4016
|
+
return cols;
|
|
4017
|
+
}, [dimensions, metrics, titleOverrides]);
|
|
4018
|
+
|
|
4019
|
+
// Transform report data to rows with unique IDs
|
|
4020
|
+
const rows = React__default.useMemo(() => {
|
|
4021
|
+
if (!reportData || !Array.isArray(reportData)) return [];
|
|
4022
|
+
return reportData.map((row, index) => ({
|
|
4023
|
+
id: index,
|
|
4024
|
+
// DataGrid requires unique id for each row
|
|
4025
|
+
...row
|
|
4026
|
+
}));
|
|
4027
|
+
}, [reportData]);
|
|
4028
|
+
|
|
4029
|
+
// Handle pagination change
|
|
4030
|
+
const handlePaginationModelChange = newModel => {
|
|
4031
|
+
setPaginationModel(newModel);
|
|
4032
|
+
|
|
4033
|
+
// Notify parent component about page change
|
|
4034
|
+
if (onPageChange && newModel.page !== paginationModel.page) {
|
|
4035
|
+
onPageChange(newModel.page, newModel.pageSize);
|
|
4036
|
+
}
|
|
4037
|
+
};
|
|
4038
|
+
if (loading) {
|
|
4039
|
+
return /*#__PURE__*/React__default.createElement(Box$1, {
|
|
4040
|
+
display: "flex",
|
|
4041
|
+
justifyContent: "center",
|
|
4042
|
+
alignItems: "center",
|
|
4043
|
+
minHeight: 400
|
|
4044
|
+
}, /*#__PURE__*/React__default.createElement(CircularProgress$1, null));
|
|
4045
|
+
}
|
|
4046
|
+
if (!reportData || reportData.length === 0) {
|
|
4047
|
+
return /*#__PURE__*/React__default.createElement(Box$1, {
|
|
4048
|
+
display: "flex",
|
|
4049
|
+
justifyContent: "center",
|
|
4050
|
+
alignItems: "center",
|
|
4051
|
+
minHeight: 400
|
|
4052
|
+
}, /*#__PURE__*/React__default.createElement(Typography$1, {
|
|
4053
|
+
variant: "body1",
|
|
4054
|
+
color: "textSecondary"
|
|
4055
|
+
}, "No data available. Run a report to see results."));
|
|
4056
|
+
}
|
|
4057
|
+
return /*#__PURE__*/React__default.createElement(Box$1, {
|
|
4058
|
+
sx: {
|
|
4059
|
+
height: 600,
|
|
4060
|
+
width: '100%'
|
|
4061
|
+
}
|
|
4062
|
+
}, /*#__PURE__*/React__default.createElement(DataGrid, {
|
|
4063
|
+
rows: rows,
|
|
4064
|
+
columns: columns,
|
|
4065
|
+
paginationModel: paginationModel,
|
|
4066
|
+
onPaginationModelChange: handlePaginationModelChange,
|
|
4067
|
+
pageSizeOptions: [10, 25, 50, 100],
|
|
4068
|
+
paginationMode: "server",
|
|
4069
|
+
rowCount: totalRows || rows.length,
|
|
4070
|
+
loading: loading,
|
|
4071
|
+
disableRowSelectionOnClick: true,
|
|
4072
|
+
sx: {
|
|
4073
|
+
'& .MuiDataGrid-cell': {
|
|
4074
|
+
padding: '8px'
|
|
4075
|
+
},
|
|
4076
|
+
'& .MuiDataGrid-columnHeader': {
|
|
4077
|
+
backgroundColor: '#f5f5f5',
|
|
4078
|
+
fontWeight: 'bold'
|
|
4079
|
+
}
|
|
4080
|
+
}
|
|
4081
|
+
}));
|
|
4082
|
+
};
|
|
4083
|
+
|
|
4084
|
+
// TabPanel component to show/hide tab content
|
|
4085
|
+
function TabPanel({
|
|
4086
|
+
children,
|
|
4087
|
+
value,
|
|
4088
|
+
index
|
|
4089
|
+
}) {
|
|
4090
|
+
return /*#__PURE__*/React__default.createElement("div", {
|
|
4091
|
+
role: "tabpanel",
|
|
4092
|
+
hidden: value !== index,
|
|
4093
|
+
id: `report-tabpanel-${index}`,
|
|
4094
|
+
"aria-labelledby": `report-tab-${index}`
|
|
4095
|
+
}, value === index && /*#__PURE__*/React__default.createElement(Box$1, {
|
|
4096
|
+
sx: {
|
|
4097
|
+
py: 3
|
|
4098
|
+
}
|
|
4099
|
+
}, children));
|
|
4100
|
+
}
|
|
4101
|
+
const ReportBuilder = ({
|
|
4102
|
+
reportDefinitionId,
|
|
4103
|
+
cloneData,
|
|
4104
|
+
autoRun,
|
|
4105
|
+
onBackToList
|
|
4106
|
+
}) => {
|
|
4107
|
+
const notify = useNotify();
|
|
4108
|
+
const reportingContext = useReportingContextOptional();
|
|
4109
|
+
const [providersData, setProvidersData] = useState(null);
|
|
4110
|
+
const [selectedProvider, setSelectedProvider] = useState('');
|
|
4111
|
+
const [reportTitle, setReportTitle] = useState('');
|
|
4112
|
+
const [report, setReport] = useState({
|
|
4113
|
+
dimensions: [],
|
|
4114
|
+
metrics: [],
|
|
4115
|
+
filters: {}
|
|
4116
|
+
});
|
|
4117
|
+
const [titleOverrides, setTitleOverrides] = useState({
|
|
4118
|
+
dimensions: {},
|
|
4119
|
+
metrics: {},
|
|
4120
|
+
filters: {}
|
|
4121
|
+
});
|
|
4122
|
+
const [reportData, setReportData] = useState(null);
|
|
4123
|
+
const [loading, setLoading] = useState(false);
|
|
4124
|
+
const [totalRows, setTotalRows] = useState(0);
|
|
4125
|
+
const [currentPage, setCurrentPage] = useState(0);
|
|
4126
|
+
const [pageSize, setPageSize] = useState(50);
|
|
4127
|
+
const [activeTab, setActiveTab] = useState(0);
|
|
4128
|
+
const [saving, setSaving] = useState(false);
|
|
4129
|
+
const [reportLoaded, setReportLoaded] = useState(false);
|
|
4130
|
+
useEffect(() => {
|
|
4131
|
+
(async () => {
|
|
4132
|
+
const providers = await Api.getProviders();
|
|
4133
|
+
console.log({
|
|
4134
|
+
providers
|
|
4135
|
+
});
|
|
4136
|
+
setProvidersData(providers);
|
|
4137
|
+
})();
|
|
4138
|
+
}, []);
|
|
4139
|
+
|
|
4140
|
+
// Load existing report definition when ID is provided
|
|
4141
|
+
useEffect(() => {
|
|
4142
|
+
if (reportDefinitionId && providersData) {
|
|
4143
|
+
loadReportDefinition(reportDefinitionId);
|
|
4144
|
+
}
|
|
4145
|
+
}, [reportDefinitionId, providersData]);
|
|
4146
|
+
|
|
4147
|
+
// Load cloned report data when cloneData is provided
|
|
4148
|
+
useEffect(() => {
|
|
4149
|
+
if (cloneData && providersData) {
|
|
4150
|
+
loadClonedReport(cloneData);
|
|
4151
|
+
}
|
|
4152
|
+
}, [cloneData, providersData]);
|
|
4153
|
+
|
|
4154
|
+
// Auto-run report when autoRun is true and report is loaded
|
|
4155
|
+
useEffect(() => {
|
|
4156
|
+
if (autoRun && reportLoaded && selectedProvider && report.dimensions.length > 0) {
|
|
4157
|
+
handleRunReport();
|
|
4158
|
+
}
|
|
4159
|
+
}, [autoRun, reportLoaded]);
|
|
4160
|
+
|
|
4161
|
+
// Utility function to reconstruct dimension object from fullPath
|
|
4162
|
+
const reconstructDimensionFromPath = (fullPath, providersData, rootProvider) => {
|
|
4163
|
+
try {
|
|
4164
|
+
// Parse: "ft_sa_db_pc.legal_name" -> ["ft", "sa", "db", "pc"], "legal_name"
|
|
4165
|
+
const [aliasPath, fieldKey] = fullPath.split('.');
|
|
4166
|
+
const aliases = aliasPath.split('_');
|
|
4167
|
+
|
|
4168
|
+
// Walk the chain to build relations array
|
|
4169
|
+
let currentProvider = rootProvider;
|
|
4170
|
+
const relations = [];
|
|
4171
|
+
const providerPath = [rootProvider];
|
|
4172
|
+
const relationNames = [];
|
|
4173
|
+
|
|
4174
|
+
// Skip first alias (root), process the rest
|
|
4175
|
+
for (let i = 1; i < aliases.length; i++) {
|
|
4176
|
+
const alias = aliases[i];
|
|
4177
|
+
const provider = providersData[currentProvider];
|
|
4178
|
+
if (!provider || !provider.relations) {
|
|
4179
|
+
console.warn(`Provider ${currentProvider} not found or has no relations`);
|
|
4180
|
+
return null;
|
|
4181
|
+
}
|
|
4182
|
+
const relation = provider.relations.find(r => r.alias === alias);
|
|
4183
|
+
if (!relation) {
|
|
4184
|
+
console.warn(`Relation with alias ${alias} not found in provider ${currentProvider}`);
|
|
4185
|
+
return null;
|
|
4186
|
+
}
|
|
4187
|
+
relations.push(relation);
|
|
4188
|
+
relationNames.push(relation.name);
|
|
4189
|
+
currentProvider = relation.target;
|
|
4190
|
+
providerPath.push(currentProvider);
|
|
4191
|
+
}
|
|
4192
|
+
|
|
4193
|
+
// Get the dimension from final provider
|
|
4194
|
+
const finalProvider = providersData[currentProvider];
|
|
4195
|
+
if (!finalProvider) {
|
|
4196
|
+
console.warn(`Final provider ${currentProvider} not found`);
|
|
4197
|
+
return null;
|
|
4198
|
+
}
|
|
4199
|
+
|
|
4200
|
+
// Use the provider's default_alias, not the relation alias
|
|
4201
|
+
const finalAlias = finalProvider.default_alias;
|
|
4202
|
+
if (!finalProvider.dimensions || !finalProvider.dimensions[finalAlias]) {
|
|
4203
|
+
console.warn(`Dimensions not found for alias ${finalAlias} in provider ${currentProvider}`);
|
|
4204
|
+
return null;
|
|
4205
|
+
}
|
|
4206
|
+
const dimension = finalProvider.dimensions[finalAlias][fieldKey];
|
|
4207
|
+
if (!dimension) {
|
|
4208
|
+
console.warn(`Dimension ${fieldKey} not found in provider ${currentProvider}, alias ${finalAlias}`);
|
|
4209
|
+
return null;
|
|
4210
|
+
}
|
|
4211
|
+
return {
|
|
4212
|
+
dimension,
|
|
4213
|
+
relations,
|
|
4214
|
+
providerPath,
|
|
4215
|
+
relationNames,
|
|
4216
|
+
dimensionKey: fieldKey,
|
|
4217
|
+
dimensionTitle: dimension.title || fieldKey,
|
|
4218
|
+
fullPath
|
|
4219
|
+
};
|
|
4220
|
+
} catch (error) {
|
|
4221
|
+
console.warn(`Error reconstructing dimension from path ${fullPath}:`, error);
|
|
4222
|
+
return null;
|
|
4223
|
+
}
|
|
4224
|
+
};
|
|
4225
|
+
|
|
4226
|
+
// Utility function to reconstruct metric object from fullPath
|
|
4227
|
+
const reconstructMetricFromPath = (fullPath, providersData, rootProvider) => {
|
|
4228
|
+
try {
|
|
4229
|
+
// Parse: "ft_sa.financing_internal" -> ["ft", "sa"], "financing_internal"
|
|
4230
|
+
const [aliasPath, metricName] = fullPath.split('.');
|
|
4231
|
+
const aliases = aliasPath.split('_');
|
|
4232
|
+
|
|
4233
|
+
// Walk the chain to build relations array
|
|
4234
|
+
let currentProvider = rootProvider;
|
|
4235
|
+
const relations = [];
|
|
4236
|
+
const providerPath = [rootProvider];
|
|
4237
|
+
const relationNames = [];
|
|
4238
|
+
|
|
4239
|
+
// Skip first alias (root), process the rest
|
|
4240
|
+
for (let i = 1; i < aliases.length; i++) {
|
|
4241
|
+
const alias = aliases[i];
|
|
4242
|
+
const provider = providersData[currentProvider];
|
|
4243
|
+
if (!provider || !provider.relations) {
|
|
4244
|
+
console.warn(`Provider ${currentProvider} not found or has no relations`);
|
|
4245
|
+
return null;
|
|
4246
|
+
}
|
|
4247
|
+
const relation = provider.relations.find(r => r.alias === alias);
|
|
4248
|
+
if (!relation) {
|
|
4249
|
+
console.warn(`Relation with alias ${alias} not found in provider ${currentProvider}`);
|
|
4250
|
+
return null;
|
|
4251
|
+
}
|
|
4252
|
+
relations.push(relation);
|
|
4253
|
+
relationNames.push(relation.name);
|
|
4254
|
+
currentProvider = relation.target;
|
|
4255
|
+
providerPath.push(currentProvider);
|
|
4256
|
+
}
|
|
4257
|
+
|
|
4258
|
+
// Get the metric from final provider
|
|
4259
|
+
const finalProvider = providersData[currentProvider];
|
|
4260
|
+
if (!finalProvider) {
|
|
4261
|
+
console.warn(`Final provider ${currentProvider} not found`);
|
|
4262
|
+
return null;
|
|
4263
|
+
}
|
|
4264
|
+
if (!finalProvider.metrics) {
|
|
4265
|
+
console.warn(`Metrics not found in provider ${currentProvider}`);
|
|
4266
|
+
return null;
|
|
4267
|
+
}
|
|
4268
|
+
const metric = finalProvider.metrics.find(m => m.name === metricName);
|
|
4269
|
+
if (!metric) {
|
|
4270
|
+
console.warn(`Metric ${metricName} not found in provider ${currentProvider}`);
|
|
4271
|
+
return null;
|
|
4272
|
+
}
|
|
4273
|
+
return {
|
|
4274
|
+
metric,
|
|
4275
|
+
relations,
|
|
4276
|
+
providerPath,
|
|
4277
|
+
relationNames,
|
|
4278
|
+
metricName: metricName,
|
|
4279
|
+
metricTitle: metric.title || metricName,
|
|
4280
|
+
fullPath
|
|
4281
|
+
};
|
|
4282
|
+
} catch (error) {
|
|
4283
|
+
console.warn(`Error reconstructing metric from path ${fullPath}:`, error);
|
|
4284
|
+
return null;
|
|
4285
|
+
}
|
|
4286
|
+
};
|
|
4287
|
+
const loadReportDefinition = async id => {
|
|
4288
|
+
try {
|
|
4289
|
+
console.log('Loading report definition:', id);
|
|
4290
|
+
const reportDef = await Api.getReportDefinition({
|
|
4291
|
+
id
|
|
4292
|
+
});
|
|
4293
|
+
console.log('Loaded report definition:', reportDef);
|
|
4294
|
+
|
|
4295
|
+
// Set the provider
|
|
4296
|
+
setSelectedProvider(reportDef.provider);
|
|
4297
|
+
|
|
4298
|
+
// Set the title
|
|
4299
|
+
setReportTitle(reportDef.title || '');
|
|
4300
|
+
|
|
4301
|
+
// Reconstruct dimensions
|
|
4302
|
+
const reconstructedDimensions = [];
|
|
4303
|
+
const orderByMap = {}; // Map to store sort order from order_by array
|
|
4304
|
+
|
|
4305
|
+
// First, build a map of fullPath -> sortOrder from order_by
|
|
4306
|
+
if (reportDef.definition?.doc?.query?.order_by) {
|
|
4307
|
+
for (const orderItem of reportDef.definition.doc.query.order_by) {
|
|
4308
|
+
if (orderItem.desc === true) {
|
|
4309
|
+
orderByMap[orderItem.name] = 'desc';
|
|
4310
|
+
} else {
|
|
4311
|
+
orderByMap[orderItem.name] = 'asc';
|
|
4312
|
+
}
|
|
4313
|
+
}
|
|
4314
|
+
}
|
|
4315
|
+
|
|
4316
|
+
// Now reconstruct dimensions and add sortOrder from the map
|
|
4317
|
+
if (reportDef.definition?.doc?.query?.dimensions) {
|
|
4318
|
+
for (const dimPath of reportDef.definition.doc.query.dimensions) {
|
|
4319
|
+
const dim = reconstructDimensionFromPath(dimPath, providersData, reportDef.provider);
|
|
4320
|
+
if (dim) {
|
|
4321
|
+
// Add sortOrder from order_by if it exists
|
|
4322
|
+
dim.sortOrder = orderByMap[dimPath] || null;
|
|
4323
|
+
reconstructedDimensions.push(dim);
|
|
4324
|
+
}
|
|
4325
|
+
}
|
|
4326
|
+
}
|
|
4327
|
+
|
|
4328
|
+
// Reconstruct metrics
|
|
4329
|
+
const reconstructedMetrics = [];
|
|
4330
|
+
if (reportDef.definition?.doc?.query?.metrics) {
|
|
4331
|
+
for (const metricPath of reportDef.definition.doc.query.metrics) {
|
|
4332
|
+
const metric = reconstructMetricFromPath(metricPath, providersData, reportDef.provider);
|
|
4333
|
+
if (metric) {
|
|
4334
|
+
reconstructedMetrics.push(metric);
|
|
4335
|
+
}
|
|
4336
|
+
}
|
|
4337
|
+
}
|
|
4338
|
+
|
|
4339
|
+
// Load title overrides if they exist
|
|
4340
|
+
const loadedTitleOverrides = {
|
|
4341
|
+
dimensions: {},
|
|
4342
|
+
metrics: {},
|
|
4343
|
+
filters: {}
|
|
4344
|
+
};
|
|
4345
|
+
if (reportDef.definition?.doc?.query?.titles) {
|
|
4346
|
+
if (reportDef.definition.doc.query.titles.dimensions) {
|
|
4347
|
+
loadedTitleOverrides.dimensions = reportDef.definition.doc.query.titles.dimensions;
|
|
4348
|
+
}
|
|
4349
|
+
if (reportDef.definition.doc.query.titles.metrics) {
|
|
4350
|
+
loadedTitleOverrides.metrics = reportDef.definition.doc.query.titles.metrics;
|
|
4351
|
+
}
|
|
4352
|
+
if (reportDef.definition.doc.query.titles.filters) {
|
|
4353
|
+
loadedTitleOverrides.filters = reportDef.definition.doc.query.titles.filters;
|
|
4354
|
+
}
|
|
4355
|
+
}
|
|
4356
|
+
|
|
4357
|
+
// Load filters if they exist
|
|
4358
|
+
// Format: { and: [ { "ft.currency": ["USD", "EUR"] }, { "ft_ba.created_at": {gte: "...", lte: "..."} } ] }
|
|
4359
|
+
// Need to reconstruct to full internal format: { "ft.currency": { dimension: {...}, values: [...], ... } }
|
|
4360
|
+
const loadedFilters = {};
|
|
4361
|
+
if (reportDef.definition?.doc?.query?.filter) {
|
|
4362
|
+
const filterObj = reportDef.definition.doc.query.filter;
|
|
4363
|
+
|
|
4364
|
+
// Extract conditions from the 'and' or 'or' array
|
|
4365
|
+
const conditions = filterObj.and || filterObj.or || [];
|
|
4366
|
+
conditions.forEach(condition => {
|
|
4367
|
+
// Each condition is an object with one key (the fullPath)
|
|
4368
|
+
const [fullPath, values] = Object.entries(condition)[0];
|
|
4369
|
+
const filterDim = reconstructDimensionFromPath(fullPath, providersData, reportDef.provider);
|
|
4370
|
+
if (filterDim) {
|
|
4371
|
+
loadedFilters[fullPath] = {
|
|
4372
|
+
...filterDim,
|
|
4373
|
+
values: values // values can be array (regular) or object (date range)
|
|
4374
|
+
};
|
|
4375
|
+
}
|
|
4376
|
+
});
|
|
4377
|
+
}
|
|
4378
|
+
|
|
4379
|
+
// Set the report state
|
|
4380
|
+
setReport({
|
|
4381
|
+
dimensions: reconstructedDimensions,
|
|
4382
|
+
metrics: reconstructedMetrics,
|
|
4383
|
+
filters: loadedFilters
|
|
4384
|
+
});
|
|
4385
|
+
|
|
4386
|
+
// Set title overrides
|
|
4387
|
+
setTitleOverrides(loadedTitleOverrides);
|
|
4388
|
+
|
|
4389
|
+
// Mark report as loaded
|
|
4390
|
+
setReportLoaded(true);
|
|
4391
|
+
console.log('Reconstructed report:', {
|
|
4392
|
+
dimensions: reconstructedDimensions,
|
|
4393
|
+
metrics: reconstructedMetrics,
|
|
4394
|
+
filters: loadedFilters,
|
|
4395
|
+
titleOverrides: loadedTitleOverrides
|
|
4396
|
+
});
|
|
4397
|
+
} catch (error) {
|
|
4398
|
+
console.error('Error loading report definition:', error);
|
|
4399
|
+
notify.error('Error loading report definition: ' + (error.message || 'Unknown error'));
|
|
4400
|
+
}
|
|
4401
|
+
};
|
|
4402
|
+
const loadClonedReport = reportDef => {
|
|
4403
|
+
try {
|
|
4404
|
+
console.log('Loading cloned report:', reportDef);
|
|
4405
|
+
|
|
4406
|
+
// Set the provider
|
|
4407
|
+
setSelectedProvider(reportDef.provider);
|
|
4408
|
+
|
|
4409
|
+
// Set the title (already modified with " (Copy)" suffix)
|
|
4410
|
+
setReportTitle(reportDef.title || '');
|
|
4411
|
+
|
|
4412
|
+
// Reconstruct dimensions
|
|
4413
|
+
const reconstructedDimensions = [];
|
|
4414
|
+
const orderByMap = {}; // Map to store sort order from order_by array
|
|
4415
|
+
|
|
4416
|
+
// First, build a map of fullPath -> sortOrder from order_by
|
|
4417
|
+
if (reportDef.definition?.doc?.query?.order_by) {
|
|
4418
|
+
for (const orderItem of reportDef.definition.doc.query.order_by) {
|
|
4419
|
+
if (orderItem.desc === true) {
|
|
4420
|
+
orderByMap[orderItem.name] = 'desc';
|
|
4421
|
+
} else {
|
|
4422
|
+
orderByMap[orderItem.name] = 'asc';
|
|
4423
|
+
}
|
|
4424
|
+
}
|
|
4425
|
+
}
|
|
4426
|
+
|
|
4427
|
+
// Now reconstruct dimensions and add sortOrder from the map
|
|
4428
|
+
if (reportDef.definition?.doc?.query?.dimensions) {
|
|
4429
|
+
for (const dimPath of reportDef.definition.doc.query.dimensions) {
|
|
4430
|
+
const dim = reconstructDimensionFromPath(dimPath, providersData, reportDef.provider);
|
|
4431
|
+
if (dim) {
|
|
4432
|
+
// Add sortOrder from order_by if it exists
|
|
4433
|
+
dim.sortOrder = orderByMap[dimPath] || null;
|
|
4434
|
+
reconstructedDimensions.push(dim);
|
|
4435
|
+
}
|
|
4436
|
+
}
|
|
4437
|
+
}
|
|
4438
|
+
|
|
4439
|
+
// Reconstruct metrics
|
|
4440
|
+
const reconstructedMetrics = [];
|
|
4441
|
+
if (reportDef.definition?.doc?.query?.metrics) {
|
|
4442
|
+
for (const metricPath of reportDef.definition.doc.query.metrics) {
|
|
4443
|
+
const metric = reconstructMetricFromPath(metricPath, providersData, reportDef.provider);
|
|
4444
|
+
if (metric) {
|
|
4445
|
+
reconstructedMetrics.push(metric);
|
|
4446
|
+
}
|
|
4447
|
+
}
|
|
4448
|
+
}
|
|
4449
|
+
|
|
4450
|
+
// Load title overrides if they exist
|
|
4451
|
+
const loadedTitleOverrides = {
|
|
4452
|
+
dimensions: {},
|
|
4453
|
+
metrics: {},
|
|
4454
|
+
filters: {}
|
|
4455
|
+
};
|
|
4456
|
+
if (reportDef.definition?.doc?.query?.titles) {
|
|
4457
|
+
if (reportDef.definition.doc.query.titles.dimensions) {
|
|
4458
|
+
loadedTitleOverrides.dimensions = reportDef.definition.doc.query.titles.dimensions;
|
|
4459
|
+
}
|
|
4460
|
+
if (reportDef.definition.doc.query.titles.metrics) {
|
|
4461
|
+
loadedTitleOverrides.metrics = reportDef.definition.doc.query.titles.metrics;
|
|
4462
|
+
}
|
|
4463
|
+
if (reportDef.definition.doc.query.titles.filters) {
|
|
4464
|
+
loadedTitleOverrides.filters = reportDef.definition.doc.query.titles.filters;
|
|
4465
|
+
}
|
|
4466
|
+
}
|
|
4467
|
+
|
|
4468
|
+
// Load filters if they exist
|
|
4469
|
+
// Format: { and: [ { "ft.currency": ["USD", "EUR"] }, { "ft_ba.created_at": {gte: "...", lte: "..."} } ] }
|
|
4470
|
+
const loadedFilters = {};
|
|
4471
|
+
if (reportDef.definition?.doc?.query?.filter) {
|
|
4472
|
+
const filterObj = reportDef.definition.doc.query.filter;
|
|
4473
|
+
|
|
4474
|
+
// Extract conditions from the 'and' or 'or' array
|
|
4475
|
+
const conditions = filterObj.and || filterObj.or || [];
|
|
4476
|
+
conditions.forEach(condition => {
|
|
4477
|
+
// Each condition is an object with one key (the fullPath)
|
|
4478
|
+
const [fullPath, values] = Object.entries(condition)[0];
|
|
4479
|
+
const filterDim = reconstructDimensionFromPath(fullPath, providersData, reportDef.provider);
|
|
4480
|
+
if (filterDim) {
|
|
4481
|
+
loadedFilters[fullPath] = {
|
|
4482
|
+
...filterDim,
|
|
4483
|
+
values: values
|
|
4484
|
+
};
|
|
4485
|
+
}
|
|
4486
|
+
});
|
|
4487
|
+
}
|
|
4488
|
+
|
|
4489
|
+
// Set the report state
|
|
4490
|
+
setReport({
|
|
4491
|
+
dimensions: reconstructedDimensions,
|
|
4492
|
+
metrics: reconstructedMetrics,
|
|
4493
|
+
filters: loadedFilters
|
|
4494
|
+
});
|
|
4495
|
+
|
|
4496
|
+
// Set title overrides
|
|
4497
|
+
setTitleOverrides(loadedTitleOverrides);
|
|
4498
|
+
console.log('Loaded cloned report:', {
|
|
4499
|
+
dimensions: reconstructedDimensions,
|
|
4500
|
+
metrics: reconstructedMetrics,
|
|
4501
|
+
filters: loadedFilters,
|
|
4502
|
+
titleOverrides: loadedTitleOverrides
|
|
4503
|
+
});
|
|
4504
|
+
} catch (error) {
|
|
4505
|
+
console.error('Error loading cloned report:', error);
|
|
4506
|
+
notify.error('Error loading cloned report: ' + (error.message || 'Unknown error'));
|
|
4507
|
+
}
|
|
4508
|
+
};
|
|
4509
|
+
|
|
4510
|
+
// Get items for the initial provider dropdown
|
|
4511
|
+
const getProviderItems = () => {
|
|
4512
|
+
if (!providersData) return [];
|
|
4513
|
+
return Object.keys(providersData).map(key => ({
|
|
4514
|
+
key: key,
|
|
4515
|
+
value: providersData[key].name || key
|
|
4516
|
+
}));
|
|
4517
|
+
};
|
|
4518
|
+
const handleProviderChange = event => {
|
|
4519
|
+
setSelectedProvider(event.target.value);
|
|
4520
|
+
// Reset dimensions, metrics, and filters when root provider changes
|
|
4521
|
+
setReport({
|
|
4522
|
+
dimensions: [],
|
|
4523
|
+
metrics: [],
|
|
4524
|
+
filters: {}
|
|
4525
|
+
});
|
|
4526
|
+
// Reset title overrides
|
|
4527
|
+
setTitleOverrides({
|
|
4528
|
+
dimensions: {},
|
|
4529
|
+
metrics: {},
|
|
4530
|
+
filters: {}
|
|
4531
|
+
});
|
|
4532
|
+
console.log('Selected root provider:', event.target.value);
|
|
4533
|
+
};
|
|
4534
|
+
const handleUpdateDimensionTitle = (fullPath, customTitle) => {
|
|
4535
|
+
setTitleOverrides(prev => ({
|
|
4536
|
+
...prev,
|
|
4537
|
+
dimensions: {
|
|
4538
|
+
...prev.dimensions,
|
|
4539
|
+
[fullPath]: customTitle
|
|
4540
|
+
}
|
|
4541
|
+
}));
|
|
4542
|
+
};
|
|
4543
|
+
const handleResetDimensionTitle = fullPath => {
|
|
4544
|
+
setTitleOverrides(prev => {
|
|
4545
|
+
const newDimensions = {
|
|
4546
|
+
...prev.dimensions
|
|
4547
|
+
};
|
|
4548
|
+
delete newDimensions[fullPath];
|
|
4549
|
+
return {
|
|
4550
|
+
...prev,
|
|
4551
|
+
dimensions: newDimensions
|
|
4552
|
+
};
|
|
4553
|
+
});
|
|
4554
|
+
};
|
|
4555
|
+
const handleUpdateMetricTitle = (fullPath, customTitle) => {
|
|
4556
|
+
setTitleOverrides(prev => ({
|
|
4557
|
+
...prev,
|
|
4558
|
+
metrics: {
|
|
4559
|
+
...prev.metrics,
|
|
4560
|
+
[fullPath]: customTitle
|
|
4561
|
+
}
|
|
4562
|
+
}));
|
|
4563
|
+
};
|
|
4564
|
+
const handleResetMetricTitle = fullPath => {
|
|
4565
|
+
setTitleOverrides(prev => {
|
|
4566
|
+
const newMetrics = {
|
|
4567
|
+
...prev.metrics
|
|
4568
|
+
};
|
|
4569
|
+
delete newMetrics[fullPath];
|
|
4570
|
+
return {
|
|
4571
|
+
...prev,
|
|
4572
|
+
metrics: newMetrics
|
|
4573
|
+
};
|
|
4574
|
+
});
|
|
4575
|
+
};
|
|
4576
|
+
const handleUpdateFilterTitle = (fullPath, customTitle) => {
|
|
4577
|
+
setTitleOverrides(prev => ({
|
|
4578
|
+
...prev,
|
|
4579
|
+
filters: {
|
|
4580
|
+
...prev.filters,
|
|
4581
|
+
[fullPath]: customTitle
|
|
4582
|
+
}
|
|
4583
|
+
}));
|
|
4584
|
+
};
|
|
4585
|
+
const handleResetFilterTitle = fullPath => {
|
|
4586
|
+
setTitleOverrides(prev => {
|
|
4587
|
+
const newFilters = {
|
|
4588
|
+
...prev.filters
|
|
4589
|
+
};
|
|
4590
|
+
delete newFilters[fullPath];
|
|
4591
|
+
return {
|
|
4592
|
+
...prev,
|
|
4593
|
+
filters: newFilters
|
|
4594
|
+
};
|
|
4595
|
+
});
|
|
4596
|
+
};
|
|
4597
|
+
const handleSaveDimension = dimensionData => {
|
|
4598
|
+
setReport(prev => {
|
|
4599
|
+
const newReport = {
|
|
4600
|
+
...prev,
|
|
4601
|
+
dimensions: [...prev.dimensions, dimensionData]
|
|
4602
|
+
};
|
|
4603
|
+
console.log('Dimension saved:', dimensionData);
|
|
4604
|
+
console.log('Complete report:', newReport);
|
|
4605
|
+
return newReport;
|
|
4606
|
+
});
|
|
4607
|
+
};
|
|
4608
|
+
const handleRemoveDimension = index => {
|
|
4609
|
+
setReport(prev => ({
|
|
4610
|
+
...prev,
|
|
4611
|
+
dimensions: prev.dimensions.filter((_, i) => i !== index)
|
|
4612
|
+
}));
|
|
4613
|
+
};
|
|
4614
|
+
const handleReorderDimensions = newOrder => {
|
|
4615
|
+
setReport(prev => ({
|
|
4616
|
+
...prev,
|
|
4617
|
+
dimensions: newOrder
|
|
4618
|
+
}));
|
|
4619
|
+
};
|
|
4620
|
+
const handleSaveMetric = metricData => {
|
|
4621
|
+
setReport(prev => {
|
|
4622
|
+
const newReport = {
|
|
4623
|
+
...prev,
|
|
4624
|
+
metrics: [...prev.metrics, metricData]
|
|
4625
|
+
};
|
|
4626
|
+
console.log('Metric saved:', metricData);
|
|
4627
|
+
console.log('Complete report:', newReport);
|
|
4628
|
+
return newReport;
|
|
4629
|
+
});
|
|
4630
|
+
};
|
|
4631
|
+
const handleRemoveMetric = index => {
|
|
4632
|
+
setReport(prev => ({
|
|
4633
|
+
...prev,
|
|
4634
|
+
metrics: prev.metrics.filter((_, i) => i !== index)
|
|
4635
|
+
}));
|
|
4636
|
+
};
|
|
4637
|
+
const handleReorderMetrics = newOrder => {
|
|
4638
|
+
setReport(prev => ({
|
|
4639
|
+
...prev,
|
|
4640
|
+
metrics: newOrder
|
|
4641
|
+
}));
|
|
4642
|
+
};
|
|
4643
|
+
const handleSaveFilter = (fullPath, filterData) => {
|
|
4644
|
+
setReport(prev => {
|
|
4645
|
+
const newReport = {
|
|
4646
|
+
...prev,
|
|
4647
|
+
filters: {
|
|
4648
|
+
...prev.filters,
|
|
4649
|
+
[fullPath]: filterData
|
|
4650
|
+
}
|
|
4651
|
+
};
|
|
4652
|
+
console.log('Filter saved:', {
|
|
4653
|
+
fullPath,
|
|
4654
|
+
filterData
|
|
4655
|
+
});
|
|
4656
|
+
console.log('Complete report:', newReport);
|
|
4657
|
+
return newReport;
|
|
4658
|
+
});
|
|
4659
|
+
};
|
|
4660
|
+
const handleRemoveFilter = fullPath => {
|
|
4661
|
+
setReport(prev => {
|
|
4662
|
+
const newFilters = {
|
|
4663
|
+
...prev.filters
|
|
4664
|
+
};
|
|
4665
|
+
delete newFilters[fullPath];
|
|
4666
|
+
return {
|
|
4667
|
+
...prev,
|
|
4668
|
+
filters: newFilters
|
|
4669
|
+
};
|
|
4670
|
+
});
|
|
4671
|
+
};
|
|
4672
|
+
|
|
4673
|
+
// Convert internal report structure to API format
|
|
4674
|
+
const convertReportToApiFormat = (page = 0, size = 50) => {
|
|
4675
|
+
// Build order_by array - include all dimensions, add desc property only when needed
|
|
4676
|
+
const orderBy = report.dimensions.map(dim => {
|
|
4677
|
+
const orderItem = {
|
|
4678
|
+
name: dim.fullPath
|
|
4679
|
+
};
|
|
4680
|
+
if (dim.sortOrder === 'desc') {
|
|
4681
|
+
orderItem.desc = true;
|
|
4682
|
+
}
|
|
4683
|
+
// For 'asc' or null, just include { name: fullPath } without desc property
|
|
4684
|
+
return orderItem;
|
|
4685
|
+
});
|
|
4686
|
+
|
|
4687
|
+
// Build filter object - convert from our internal format to NEW API format
|
|
4688
|
+
// Internal format (saved):
|
|
4689
|
+
// Regular: { "ft.currency": { dimension: {...}, values: ["USD", "EUR"], ... } }
|
|
4690
|
+
// Date: { "ft_ba.created_at": { dimension: {...}, values: {gte: "2025-05-01", lte: "2025-12-31"}, ... } }
|
|
4691
|
+
// NEW API format (for running):
|
|
4692
|
+
// { and: [ { "ft.currency": ["USD", "EUR"] }, { "ft_ba.created_at": {gte: "2025-05-01", lte: "2025-12-31"} } ] }
|
|
4693
|
+
const conditions = [];
|
|
4694
|
+
if (report.filters && Object.keys(report.filters).length > 0) {
|
|
4695
|
+
Object.entries(report.filters).forEach(([fullPath, filterData]) => {
|
|
4696
|
+
if (filterData.values) {
|
|
4697
|
+
// Check if values is an object (date range) or array (regular filter)
|
|
4698
|
+
if (Array.isArray(filterData.values)) {
|
|
4699
|
+
// Regular filter - only add if array has values
|
|
4700
|
+
if (filterData.values.length > 0) {
|
|
4701
|
+
conditions.push({
|
|
4702
|
+
[fullPath]: filterData.values
|
|
4703
|
+
});
|
|
4704
|
+
}
|
|
4705
|
+
} else if (typeof filterData.values === 'object') {
|
|
4706
|
+
// Date range filter - add the object as-is
|
|
4707
|
+
conditions.push({
|
|
4708
|
+
[fullPath]: filterData.values
|
|
4709
|
+
});
|
|
4710
|
+
}
|
|
4711
|
+
}
|
|
4712
|
+
});
|
|
4713
|
+
}
|
|
4714
|
+
|
|
4715
|
+
// Build the new filter structure with top-level 'and' operator
|
|
4716
|
+
const filter = conditions.length > 0 ? {
|
|
4717
|
+
and: conditions
|
|
4718
|
+
} : null;
|
|
4719
|
+
|
|
4720
|
+
// Build titles object - only include overridden titles
|
|
4721
|
+
const titles = {};
|
|
4722
|
+
if (Object.keys(titleOverrides.dimensions).length > 0) {
|
|
4723
|
+
titles.dimensions = titleOverrides.dimensions;
|
|
4724
|
+
}
|
|
4725
|
+
if (Object.keys(titleOverrides.metrics).length > 0) {
|
|
4726
|
+
titles.metrics = titleOverrides.metrics;
|
|
4727
|
+
}
|
|
4728
|
+
if (Object.keys(titleOverrides.filters).length > 0) {
|
|
4729
|
+
titles.filters = titleOverrides.filters;
|
|
4730
|
+
}
|
|
4731
|
+
const queryObj = {
|
|
4732
|
+
dimensions: report.dimensions.map(dim => dim.fullPath),
|
|
4733
|
+
metrics: report.metrics.map(metric => metric.fullPath),
|
|
4734
|
+
order_by: orderBy,
|
|
4735
|
+
limit: size,
|
|
4736
|
+
offset: page * size
|
|
4737
|
+
};
|
|
4738
|
+
|
|
4739
|
+
// Only add titles if there are any overrides
|
|
4740
|
+
if (Object.keys(titles).length > 0) {
|
|
4741
|
+
queryObj.titles = titles;
|
|
4742
|
+
}
|
|
4743
|
+
|
|
4744
|
+
// Only add filter to query if there are any conditions
|
|
4745
|
+
if (filter) {
|
|
4746
|
+
queryObj.filter = filter;
|
|
4747
|
+
}
|
|
4748
|
+
|
|
4749
|
+
// Get parameters from context if available, otherwise use default
|
|
4750
|
+
const parameters = reportingContext?.parameters || {
|
|
4751
|
+
base_currency: "EUR"
|
|
4752
|
+
};
|
|
4753
|
+
return {
|
|
4754
|
+
provider: selectedProvider,
|
|
4755
|
+
doc: {
|
|
4756
|
+
query: queryObj
|
|
4757
|
+
},
|
|
4758
|
+
parameters
|
|
4759
|
+
};
|
|
4760
|
+
};
|
|
4761
|
+
const runReportWithPagination = async (page, size) => {
|
|
4762
|
+
try {
|
|
4763
|
+
setLoading(true);
|
|
4764
|
+
const apiReport = convertReportToApiFormat(page, size);
|
|
4765
|
+
console.log('Running report with:', apiReport);
|
|
4766
|
+
const result = await Api.runAdHocReport({
|
|
4767
|
+
report: apiReport
|
|
4768
|
+
});
|
|
4769
|
+
console.log('Report result:', result);
|
|
4770
|
+
setReportData(result);
|
|
4771
|
+
// Note: The API should ideally return total count, but for now we'll use the result length
|
|
4772
|
+
// If the API returns fewer rows than the limit, we know we're at the end
|
|
4773
|
+
if (result && result.length < size) {
|
|
4774
|
+
setTotalRows(page * size + result.length);
|
|
4775
|
+
} else {
|
|
4776
|
+
// Estimate total rows (this is a limitation without a count endpoint)
|
|
4777
|
+
setTotalRows((page + 2) * size);
|
|
4778
|
+
}
|
|
4779
|
+
} catch (error) {
|
|
4780
|
+
console.error('Error running report:', error);
|
|
4781
|
+
notify.error('Error running report: ' + (error.message || 'Unknown error'));
|
|
4782
|
+
setReportData(null);
|
|
4783
|
+
} finally {
|
|
4784
|
+
setLoading(false);
|
|
4785
|
+
}
|
|
4786
|
+
};
|
|
4787
|
+
const handleRunReport = async () => {
|
|
4788
|
+
setCurrentPage(0);
|
|
4789
|
+
await runReportWithPagination(0, pageSize);
|
|
4790
|
+
// Switch to Results tab after running report (now index 3 after adding Filters tab)
|
|
4791
|
+
setActiveTab(3);
|
|
4792
|
+
};
|
|
4793
|
+
const handleDownloadReport = async () => {
|
|
4794
|
+
try {
|
|
4795
|
+
setLoading(true);
|
|
4796
|
+
const apiReport = convertReportToApiFormat();
|
|
4797
|
+
console.log('Downloading report with:', apiReport);
|
|
4798
|
+
|
|
4799
|
+
// Generate filename from report title and current date
|
|
4800
|
+
const date = new Date().toISOString().split('T')[0]; // Format: YYYY-MM-DD
|
|
4801
|
+
const sanitizedTitle = (reportTitle || 'report').replace(/[^a-z0-9]/gi, '_').toLowerCase();
|
|
4802
|
+
const filename = `${sanitizedTitle}_${date}.csv`;
|
|
4803
|
+
await Api.downloadAdHocReport({
|
|
4804
|
+
report: apiReport,
|
|
4805
|
+
filename
|
|
4806
|
+
});
|
|
4807
|
+
notify.success('Report downloaded successfully!');
|
|
4808
|
+
} catch (error) {
|
|
4809
|
+
console.error('Error downloading report:', error);
|
|
4810
|
+
notify.error('Error downloading report: ' + (error.message || 'Unknown error'));
|
|
4811
|
+
} finally {
|
|
4812
|
+
setLoading(false);
|
|
4813
|
+
}
|
|
4814
|
+
};
|
|
4815
|
+
const handlePageChange = async (newPage, newPageSize) => {
|
|
4816
|
+
setCurrentPage(newPage);
|
|
4817
|
+
if (newPageSize !== pageSize) {
|
|
4818
|
+
setPageSize(newPageSize);
|
|
4819
|
+
}
|
|
4820
|
+
await runReportWithPagination(newPage, newPageSize);
|
|
4821
|
+
};
|
|
4822
|
+
const canRunReport = selectedProvider;
|
|
4823
|
+
const handleTabChange = (_event, newValue) => {
|
|
4824
|
+
setActiveTab(newValue);
|
|
4825
|
+
};
|
|
4826
|
+
const handleSaveReport = async () => {
|
|
4827
|
+
if (!selectedProvider) {
|
|
4828
|
+
notify.warning('Please select a provider first');
|
|
4829
|
+
return;
|
|
4830
|
+
}
|
|
4831
|
+
if (!reportTitle.trim()) {
|
|
4832
|
+
notify.warning('Please enter a report title');
|
|
4833
|
+
return;
|
|
4834
|
+
}
|
|
4835
|
+
if (report.dimensions.length === 0 && report.metrics.length === 0) {
|
|
4836
|
+
notify.warning('Please add at least one dimension or metric');
|
|
4837
|
+
return;
|
|
4838
|
+
}
|
|
4839
|
+
try {
|
|
4840
|
+
setSaving(true);
|
|
4841
|
+
|
|
4842
|
+
// Build order_by array - include all dimensions, add desc property only when needed
|
|
4843
|
+
const orderBy = report.dimensions.map(dim => {
|
|
4844
|
+
const orderItem = {
|
|
4845
|
+
name: dim.fullPath
|
|
4846
|
+
};
|
|
4847
|
+
if (dim.sortOrder === 'desc') {
|
|
4848
|
+
orderItem.desc = true;
|
|
4849
|
+
}
|
|
4850
|
+
// For 'asc' or null, just include { name: fullPath } without desc property
|
|
4851
|
+
return orderItem;
|
|
4852
|
+
});
|
|
4853
|
+
|
|
4854
|
+
// Build titles object - only include overridden titles
|
|
4855
|
+
const titles = {};
|
|
4856
|
+
if (Object.keys(titleOverrides.dimensions).length > 0) {
|
|
4857
|
+
titles.dimensions = titleOverrides.dimensions;
|
|
4858
|
+
}
|
|
4859
|
+
if (Object.keys(titleOverrides.metrics).length > 0) {
|
|
4860
|
+
titles.metrics = titleOverrides.metrics;
|
|
4861
|
+
}
|
|
4862
|
+
if (Object.keys(titleOverrides.filters).length > 0) {
|
|
4863
|
+
titles.filters = titleOverrides.filters;
|
|
4864
|
+
}
|
|
4865
|
+
const queryObj = {
|
|
4866
|
+
dimensions: report.dimensions.map(dim => dim.fullPath),
|
|
4867
|
+
metrics: report.metrics.map(metric => metric.fullPath),
|
|
4868
|
+
order_by: orderBy
|
|
4869
|
+
};
|
|
4870
|
+
|
|
4871
|
+
// Only add titles if there are any overrides
|
|
4872
|
+
if (Object.keys(titles).length > 0) {
|
|
4873
|
+
queryObj.titles = titles;
|
|
4874
|
+
}
|
|
4875
|
+
|
|
4876
|
+
// Add filter if there are any - NEW API format with top-level 'and' operator
|
|
4877
|
+
// NEW format: { and: [ { "ft.currency": ["USD", "EUR"] }, { "ft_ba.created_at": {gte: "2025-05-01", lte: "2025-12-31"} } ] }
|
|
4878
|
+
if (Object.keys(report.filters).length > 0) {
|
|
4879
|
+
const conditions = [];
|
|
4880
|
+
Object.entries(report.filters).forEach(([fullPath, filterData]) => {
|
|
4881
|
+
if (filterData.values) {
|
|
4882
|
+
// Check if values is an object (date range) or array (regular filter)
|
|
4883
|
+
if (Array.isArray(filterData.values)) {
|
|
4884
|
+
// Regular filter - only add if array has values
|
|
4885
|
+
if (filterData.values.length > 0) {
|
|
4886
|
+
conditions.push({
|
|
4887
|
+
[fullPath]: filterData.values
|
|
4888
|
+
});
|
|
4889
|
+
}
|
|
4890
|
+
} else if (typeof filterData.values === 'object') {
|
|
4891
|
+
// Date range filter - add the object as-is
|
|
4892
|
+
conditions.push({
|
|
4893
|
+
[fullPath]: filterData.values
|
|
4894
|
+
});
|
|
4895
|
+
}
|
|
4896
|
+
}
|
|
4897
|
+
});
|
|
4898
|
+
if (conditions.length > 0) {
|
|
4899
|
+
queryObj.filter = {
|
|
4900
|
+
and: conditions
|
|
4901
|
+
};
|
|
4902
|
+
}
|
|
4903
|
+
}
|
|
4904
|
+
|
|
4905
|
+
// Build the doc object
|
|
4906
|
+
const docObj = {
|
|
4907
|
+
query: queryObj
|
|
4908
|
+
};
|
|
4909
|
+
const payload = {
|
|
4910
|
+
provider: selectedProvider,
|
|
4911
|
+
title: reportTitle.trim(),
|
|
4912
|
+
definition: {
|
|
4913
|
+
provider: selectedProvider,
|
|
4914
|
+
doc: docObj,
|
|
4915
|
+
parameters: {
|
|
4916
|
+
base_currency: "EUR"
|
|
4917
|
+
}
|
|
4918
|
+
}
|
|
4919
|
+
};
|
|
4920
|
+
console.log('Saving report definition:', payload);
|
|
4921
|
+
if (reportDefinitionId) {
|
|
4922
|
+
// Update existing report definition
|
|
4923
|
+
await Api.updateReportDefinition({
|
|
4924
|
+
id: reportDefinitionId,
|
|
4925
|
+
reportDefinition: payload
|
|
4926
|
+
});
|
|
4927
|
+
notify.success('Report definition updated successfully!');
|
|
4928
|
+
} else {
|
|
4929
|
+
// Create new report definition
|
|
4930
|
+
await Api.createReportDefinition({
|
|
4931
|
+
reportDefinition: payload
|
|
4932
|
+
});
|
|
4933
|
+
notify.success('Report definition created successfully!');
|
|
4934
|
+
}
|
|
4935
|
+
|
|
4936
|
+
// Navigate back to list
|
|
4937
|
+
if (onBackToList) {
|
|
4938
|
+
onBackToList();
|
|
4939
|
+
}
|
|
4940
|
+
} catch (error) {
|
|
4941
|
+
console.error('Error saving report definition:', error);
|
|
4942
|
+
notify.error('Error saving report definition: ' + (error.message || 'Unknown error'));
|
|
4943
|
+
} finally {
|
|
4944
|
+
setSaving(false);
|
|
4945
|
+
}
|
|
4946
|
+
};
|
|
4947
|
+
const canSaveReport = selectedProvider && reportTitle.trim() && (report.dimensions.length > 0 || report.metrics.length > 0);
|
|
4948
|
+
return /*#__PURE__*/React__default.createElement(Box$1, {
|
|
4949
|
+
sx: {
|
|
4950
|
+
p: 3
|
|
4951
|
+
}
|
|
4952
|
+
}, /*#__PURE__*/React__default.createElement(Box$1, {
|
|
4953
|
+
sx: {
|
|
4954
|
+
display: 'flex',
|
|
4955
|
+
justifyContent: 'space-between',
|
|
4956
|
+
alignItems: 'center',
|
|
4957
|
+
mb: 2
|
|
4958
|
+
}
|
|
4959
|
+
}, /*#__PURE__*/React__default.createElement("h1", null, reportDefinitionId ? 'Edit Report' : 'Create New Report'), onBackToList && /*#__PURE__*/React__default.createElement(Button, {
|
|
4960
|
+
variant: "outlined",
|
|
4961
|
+
startIcon: /*#__PURE__*/React__default.createElement(ArrowBackIcon, null),
|
|
4962
|
+
onClick: onBackToList
|
|
4963
|
+
}, "Back to List")), /*#__PURE__*/React__default.createElement(Box$1, {
|
|
4964
|
+
sx: {
|
|
4965
|
+
mt: 2,
|
|
4966
|
+
display: 'flex',
|
|
4967
|
+
gap: 2,
|
|
4968
|
+
alignItems: 'flex-start',
|
|
4969
|
+
flexWrap: 'wrap'
|
|
4970
|
+
}
|
|
4971
|
+
}, /*#__PURE__*/React__default.createElement(TextField, {
|
|
4972
|
+
label: "Report Title",
|
|
4973
|
+
value: reportTitle,
|
|
4974
|
+
onChange: e => setReportTitle(e.target.value),
|
|
4975
|
+
placeholder: "Enter report title",
|
|
4976
|
+
sx: {
|
|
4977
|
+
width: '300px'
|
|
4978
|
+
},
|
|
4979
|
+
size: "small"
|
|
4980
|
+
}), /*#__PURE__*/React__default.createElement(SingleSelect, {
|
|
4981
|
+
items: getProviderItems(),
|
|
4982
|
+
value: selectedProvider,
|
|
4983
|
+
label: "Select Root Provider",
|
|
4984
|
+
onChange: handleProviderChange,
|
|
4985
|
+
sx: {
|
|
4986
|
+
width: '300px'
|
|
4987
|
+
},
|
|
4988
|
+
disabled: !!reportDefinitionId
|
|
4989
|
+
}), /*#__PURE__*/React__default.createElement(Button, {
|
|
4990
|
+
variant: "contained",
|
|
4991
|
+
onClick: handleRunReport,
|
|
4992
|
+
disabled: !canRunReport,
|
|
4993
|
+
sx: {
|
|
4994
|
+
height: '40px'
|
|
4995
|
+
}
|
|
4996
|
+
}, "Run Report"), /*#__PURE__*/React__default.createElement(Button, {
|
|
4997
|
+
variant: "contained",
|
|
4998
|
+
color: "secondary",
|
|
4999
|
+
startIcon: /*#__PURE__*/React__default.createElement(DownloadIcon, null),
|
|
5000
|
+
onClick: handleDownloadReport,
|
|
5001
|
+
disabled: !canRunReport || loading,
|
|
5002
|
+
sx: {
|
|
5003
|
+
height: '40px'
|
|
5004
|
+
}
|
|
5005
|
+
}, "Download CSV"), /*#__PURE__*/React__default.createElement(Button, {
|
|
5006
|
+
variant: "contained",
|
|
5007
|
+
color: "success",
|
|
5008
|
+
startIcon: /*#__PURE__*/React__default.createElement(SaveIcon, null),
|
|
5009
|
+
onClick: handleSaveReport,
|
|
5010
|
+
disabled: !canSaveReport || saving,
|
|
5011
|
+
sx: {
|
|
5012
|
+
height: '40px'
|
|
5013
|
+
}
|
|
5014
|
+
}, saving ? 'Saving...' : reportDefinitionId ? 'Update Report' : 'Save Report')), selectedProvider && /*#__PURE__*/React__default.createElement(Box$1, {
|
|
5015
|
+
sx: {
|
|
5016
|
+
mt: 4
|
|
5017
|
+
}
|
|
5018
|
+
}, /*#__PURE__*/React__default.createElement(Box$1, {
|
|
5019
|
+
sx: {
|
|
5020
|
+
borderBottom: 1,
|
|
5021
|
+
borderColor: 'divider'
|
|
5022
|
+
}
|
|
5023
|
+
}, /*#__PURE__*/React__default.createElement(Tabs, {
|
|
5024
|
+
value: activeTab,
|
|
5025
|
+
onChange: handleTabChange,
|
|
5026
|
+
"aria-label": "report builder tabs"
|
|
5027
|
+
}, /*#__PURE__*/React__default.createElement(Tab, {
|
|
5028
|
+
label: /*#__PURE__*/React__default.createElement(Badge, {
|
|
5029
|
+
badgeContent: report.dimensions.length,
|
|
5030
|
+
color: "primary"
|
|
5031
|
+
}, /*#__PURE__*/React__default.createElement("span", {
|
|
5032
|
+
style: {
|
|
5033
|
+
marginRight: report.dimensions.length > 0 ? '12px' : '0'
|
|
5034
|
+
}
|
|
5035
|
+
}, "Dimensions")),
|
|
5036
|
+
id: "report-tab-0",
|
|
5037
|
+
"aria-controls": "report-tabpanel-0"
|
|
5038
|
+
}), /*#__PURE__*/React__default.createElement(Tab, {
|
|
5039
|
+
label: /*#__PURE__*/React__default.createElement(Badge, {
|
|
5040
|
+
badgeContent: report.metrics.length,
|
|
5041
|
+
color: "primary"
|
|
5042
|
+
}, /*#__PURE__*/React__default.createElement("span", {
|
|
5043
|
+
style: {
|
|
5044
|
+
marginRight: report.metrics.length > 0 ? '12px' : '0'
|
|
5045
|
+
}
|
|
5046
|
+
}, "Metrics")),
|
|
5047
|
+
id: "report-tab-1",
|
|
5048
|
+
"aria-controls": "report-tabpanel-1"
|
|
5049
|
+
}), /*#__PURE__*/React__default.createElement(Tab, {
|
|
5050
|
+
label: /*#__PURE__*/React__default.createElement(Badge, {
|
|
5051
|
+
badgeContent: Object.keys(report.filters).length,
|
|
5052
|
+
color: "secondary"
|
|
5053
|
+
}, /*#__PURE__*/React__default.createElement("span", {
|
|
5054
|
+
style: {
|
|
5055
|
+
marginRight: Object.keys(report.filters).length > 0 ? '12px' : '0'
|
|
5056
|
+
}
|
|
5057
|
+
}, "Filters")),
|
|
5058
|
+
id: "report-tab-2",
|
|
5059
|
+
"aria-controls": "report-tabpanel-2"
|
|
5060
|
+
}), /*#__PURE__*/React__default.createElement(Tab, {
|
|
5061
|
+
label: reportData ? "Results" : "Results (Run report first)",
|
|
5062
|
+
id: "report-tab-3",
|
|
5063
|
+
"aria-controls": "report-tabpanel-3",
|
|
5064
|
+
disabled: !reportData
|
|
5065
|
+
}))), /*#__PURE__*/React__default.createElement(TabPanel, {
|
|
5066
|
+
value: activeTab,
|
|
5067
|
+
index: 0
|
|
5068
|
+
}, /*#__PURE__*/React__default.createElement(Dimensions, {
|
|
5069
|
+
providersData: providersData,
|
|
5070
|
+
rootProvider: selectedProvider,
|
|
5071
|
+
savedDimensions: report.dimensions,
|
|
5072
|
+
onSaveDimension: handleSaveDimension,
|
|
5073
|
+
onRemoveDimension: handleRemoveDimension,
|
|
5074
|
+
onReorderDimensions: handleReorderDimensions,
|
|
5075
|
+
titleOverrides: titleOverrides.dimensions,
|
|
5076
|
+
onUpdateTitle: handleUpdateDimensionTitle,
|
|
5077
|
+
onResetTitle: handleResetDimensionTitle,
|
|
5078
|
+
existingMetrics: report.metrics,
|
|
5079
|
+
existingFilters: report.filters
|
|
5080
|
+
})), /*#__PURE__*/React__default.createElement(TabPanel, {
|
|
5081
|
+
value: activeTab,
|
|
5082
|
+
index: 1
|
|
5083
|
+
}, /*#__PURE__*/React__default.createElement(Metrics, {
|
|
5084
|
+
providersData: providersData,
|
|
5085
|
+
rootProvider: selectedProvider,
|
|
5086
|
+
savedMetrics: report.metrics,
|
|
5087
|
+
onSaveMetric: handleSaveMetric,
|
|
5088
|
+
onRemoveMetric: handleRemoveMetric,
|
|
5089
|
+
onReorderMetrics: handleReorderMetrics,
|
|
5090
|
+
titleOverrides: titleOverrides.metrics,
|
|
5091
|
+
onUpdateTitle: handleUpdateMetricTitle,
|
|
5092
|
+
onResetTitle: handleResetMetricTitle,
|
|
5093
|
+
existingDimensions: report.dimensions,
|
|
5094
|
+
existingFilters: report.filters
|
|
5095
|
+
})), /*#__PURE__*/React__default.createElement(TabPanel, {
|
|
5096
|
+
value: activeTab,
|
|
5097
|
+
index: 2
|
|
5098
|
+
}, /*#__PURE__*/React__default.createElement(Filters, {
|
|
5099
|
+
providersData: providersData,
|
|
5100
|
+
rootProvider: selectedProvider,
|
|
5101
|
+
savedFilters: report.filters,
|
|
5102
|
+
existingDimensions: report.dimensions,
|
|
5103
|
+
existingMetrics: report.metrics,
|
|
5104
|
+
onSaveFilter: handleSaveFilter,
|
|
5105
|
+
onRemoveFilter: handleRemoveFilter,
|
|
5106
|
+
titleOverrides: titleOverrides.filters,
|
|
5107
|
+
onUpdateTitle: handleUpdateFilterTitle,
|
|
5108
|
+
onResetTitle: handleResetFilterTitle
|
|
5109
|
+
})), /*#__PURE__*/React__default.createElement(TabPanel, {
|
|
5110
|
+
value: activeTab,
|
|
5111
|
+
index: 3
|
|
5112
|
+
}, reportData && /*#__PURE__*/React__default.createElement(ReportDataGrid, {
|
|
5113
|
+
reportData: reportData,
|
|
5114
|
+
dimensions: report.dimensions,
|
|
5115
|
+
metrics: report.metrics,
|
|
5116
|
+
loading: loading,
|
|
5117
|
+
onPageChange: handlePageChange,
|
|
5118
|
+
totalRows: totalRows,
|
|
5119
|
+
titleOverrides: titleOverrides
|
|
5120
|
+
}))));
|
|
5121
|
+
};
|
|
5122
|
+
|
|
5123
|
+
const ReportDefinitionsManager = () => {
|
|
5124
|
+
const [view, setView] = useState('list'); // 'list' or 'builder'
|
|
5125
|
+
const [selectedReportId, setSelectedReportId] = useState(null);
|
|
5126
|
+
const [cloneData, setCloneData] = useState(null);
|
|
5127
|
+
const [autoRun, setAutoRun] = useState(false);
|
|
5128
|
+
const [refreshTrigger, setRefreshTrigger] = useState(0);
|
|
5129
|
+
const handleSelectReport = id => {
|
|
5130
|
+
setSelectedReportId(id);
|
|
5131
|
+
setCloneData(null);
|
|
5132
|
+
setAutoRun(false);
|
|
5133
|
+
setView('builder');
|
|
5134
|
+
};
|
|
5135
|
+
const handleAddNew = () => {
|
|
5136
|
+
setSelectedReportId(null);
|
|
5137
|
+
setCloneData(null);
|
|
5138
|
+
setAutoRun(false);
|
|
5139
|
+
setView('builder');
|
|
5140
|
+
};
|
|
5141
|
+
const handleRunReport = id => {
|
|
5142
|
+
setSelectedReportId(id);
|
|
5143
|
+
setCloneData(null);
|
|
5144
|
+
setAutoRun(true);
|
|
5145
|
+
setView('builder');
|
|
5146
|
+
};
|
|
5147
|
+
const handleCloneReport = reportDefinition => {
|
|
5148
|
+
setSelectedReportId(null);
|
|
5149
|
+
setCloneData(reportDefinition);
|
|
5150
|
+
setAutoRun(false);
|
|
5151
|
+
setView('builder');
|
|
5152
|
+
};
|
|
5153
|
+
const handleBackToList = () => {
|
|
5154
|
+
setSelectedReportId(null);
|
|
5155
|
+
setCloneData(null);
|
|
5156
|
+
setAutoRun(false);
|
|
5157
|
+
setView('list');
|
|
5158
|
+
// Trigger refresh of the list
|
|
5159
|
+
setRefreshTrigger(prev => prev + 1);
|
|
5160
|
+
};
|
|
5161
|
+
if (view === 'list') {
|
|
5162
|
+
return /*#__PURE__*/React__default.createElement(ReportDefinitionsList, {
|
|
5163
|
+
onSelectReport: handleSelectReport,
|
|
5164
|
+
onAddNew: handleAddNew,
|
|
5165
|
+
onCloneReport: handleCloneReport,
|
|
5166
|
+
onRunReport: handleRunReport,
|
|
5167
|
+
refreshTrigger: refreshTrigger
|
|
5168
|
+
});
|
|
5169
|
+
}
|
|
5170
|
+
return /*#__PURE__*/React__default.createElement(ReportBuilder, {
|
|
5171
|
+
reportDefinitionId: selectedReportId,
|
|
5172
|
+
cloneData: cloneData,
|
|
5173
|
+
autoRun: autoRun,
|
|
5174
|
+
onBackToList: handleBackToList
|
|
5175
|
+
});
|
|
5176
|
+
};
|
|
5177
|
+
|
|
5178
|
+
const ReportApp = ({
|
|
5179
|
+
paramsDefault,
|
|
5180
|
+
apiDefaults
|
|
5181
|
+
}) => {
|
|
5182
|
+
return /*#__PURE__*/React__default.createElement(NotifyProvider, null, /*#__PURE__*/React__default.createElement(ReportingProvider, {
|
|
5183
|
+
defaultParameters: paramsDefault,
|
|
5184
|
+
defaultApi: apiDefaults
|
|
5185
|
+
}, /*#__PURE__*/React__default.createElement(ReportDefinitionsManager, null)));
|
|
5186
|
+
};
|
|
5187
|
+
|
|
1290
5188
|
var index = {
|
|
1291
5189
|
Chart,
|
|
1292
|
-
Dashboard
|
|
5190
|
+
Dashboard,
|
|
5191
|
+
ReportApp
|
|
5192
|
+
// ReportingProvider,
|
|
5193
|
+
// useReportingContext,
|
|
5194
|
+
// useReportingContextOptional
|
|
1293
5195
|
};
|
|
1294
5196
|
|
|
1295
5197
|
export { index as default };
|