@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.
@@ -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',api);
1274
+ // console.log('token changed',finalApi);
1155
1275
 
1156
- Api.setBaseUrl(api.base_url);
1157
- Api.setToken(api.token);
1158
- }, [api]);
1276
+ Api.setBaseUrl(finalApi.base_url);
1277
+ Api.setToken(finalApi.token);
1278
+ }, [finalApi]);
1159
1279
  const init = async () => {
1160
- Api.setBaseUrl(api.base_url);
1161
- Api.setToken(api.token);
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: 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: 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: 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: 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 };