@bygd/nc-report-ui 0.1.41 → 0.1.43
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/app/esm/index.js +970 -529
- package/dist/default/cjs/index.cjs +963 -528
- package/dist/default/esm/index.js +962 -529
- package/package.json +1 -1
|
@@ -17,6 +17,7 @@ import { useInView } from 'react-intersection-observer';
|
|
|
17
17
|
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 as Tooltip$1, IconButton as IconButton$1, Paper as Paper$1, Tabs, Tab, Badge } from '@mui/material';
|
|
18
18
|
import CheckBoxOutlineBlankIcon from '@mui/icons-material/CheckBoxOutlineBlank';
|
|
19
19
|
import CheckBoxIcon from '@mui/icons-material/CheckBox';
|
|
20
|
+
import CheckCircleIcon from '@mui/icons-material/CheckCircle';
|
|
20
21
|
import EventEmitter from 'eventemitter3';
|
|
21
22
|
import Grid from '@mui/material/Grid';
|
|
22
23
|
import IconButton from '@mui/material/IconButton';
|
|
@@ -44,7 +45,7 @@ import ArrowBackIcon from '@mui/icons-material/ArrowBack';
|
|
|
44
45
|
import SaveIcon from '@mui/icons-material/Save';
|
|
45
46
|
import DownloadIcon from '@mui/icons-material/Download';
|
|
46
47
|
import { useSensors, useSensor, PointerSensor, KeyboardSensor, DndContext, closestCenter } from '@dnd-kit/core';
|
|
47
|
-
import { sortableKeyboardCoordinates, SortableContext, verticalListSortingStrategy,
|
|
48
|
+
import { sortableKeyboardCoordinates, SortableContext, verticalListSortingStrategy, useSortable, arrayMove } from '@dnd-kit/sortable';
|
|
48
49
|
import { CSS } from '@dnd-kit/utilities';
|
|
49
50
|
import DragIndicatorIcon from '@mui/icons-material/DragIndicator';
|
|
50
51
|
import ArrowUpwardIcon from '@mui/icons-material/ArrowUpward';
|
|
@@ -64,6 +65,7 @@ import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider';
|
|
|
64
65
|
import { DatePicker } from '@mui/x-date-pickers/DatePicker';
|
|
65
66
|
import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs';
|
|
66
67
|
import dayjs from 'dayjs';
|
|
68
|
+
import CalculateIcon from '@mui/icons-material/Calculate';
|
|
67
69
|
import { ThemeProvider, createTheme } from '@mui/material/styles';
|
|
68
70
|
|
|
69
71
|
function _extends() {
|
|
@@ -1054,7 +1056,8 @@ function SingleSelect({
|
|
|
1054
1056
|
key,
|
|
1055
1057
|
value,
|
|
1056
1058
|
disabled,
|
|
1057
|
-
icon
|
|
1059
|
+
icon,
|
|
1060
|
+
alreadySelected
|
|
1058
1061
|
} = itm;
|
|
1059
1062
|
return /*#__PURE__*/React__default.createElement(MenuItem, {
|
|
1060
1063
|
key: key,
|
|
@@ -1065,7 +1068,12 @@ function SingleSelect({
|
|
|
1065
1068
|
minHeight: "36px",
|
|
1066
1069
|
display: "flex",
|
|
1067
1070
|
alignItems: "center",
|
|
1068
|
-
gap: "6px"
|
|
1071
|
+
gap: "6px",
|
|
1072
|
+
...(alreadySelected && {
|
|
1073
|
+
color: "rgb(70, 134, 128)",
|
|
1074
|
+
backgroundColor: "rgba(70, 134, 128, 0.08)",
|
|
1075
|
+
fontWeight: 600
|
|
1076
|
+
})
|
|
1069
1077
|
}
|
|
1070
1078
|
}, icon && /*#__PURE__*/React__default.createElement(Box, {
|
|
1071
1079
|
component: "span",
|
|
@@ -1073,7 +1081,13 @@ function SingleSelect({
|
|
|
1073
1081
|
display: "inline-flex",
|
|
1074
1082
|
alignItems: "center"
|
|
1075
1083
|
}
|
|
1076
|
-
}, icon),
|
|
1084
|
+
}, icon), alreadySelected && /*#__PURE__*/React__default.createElement(CheckCircleIcon, {
|
|
1085
|
+
fontSize: "small",
|
|
1086
|
+
sx: {
|
|
1087
|
+
color: "rgb(70, 134, 128)",
|
|
1088
|
+
fontSize: "16px"
|
|
1089
|
+
}
|
|
1090
|
+
}), formatLabel(value));
|
|
1077
1091
|
}))));
|
|
1078
1092
|
}
|
|
1079
1093
|
|
|
@@ -2528,6 +2542,163 @@ const ProviderSelection = ({
|
|
|
2528
2542
|
}, renderSelectionChain()));
|
|
2529
2543
|
};
|
|
2530
2544
|
|
|
2545
|
+
// Shared drag-and-drop primitives for the index-identified, arrow-reorderable
|
|
2546
|
+
// lists used across the report builder (Dimensions, Metrics, Column Order).
|
|
2547
|
+
// Previously duplicated per-tab; consolidated here so all three behave and
|
|
2548
|
+
// look identical and only need to be fixed in one place.
|
|
2549
|
+
|
|
2550
|
+
const useSortableListSensors = () => useSensors(useSensor(PointerSensor), useSensor(KeyboardSensor, {
|
|
2551
|
+
coordinateGetter: sortableKeyboardCoordinates
|
|
2552
|
+
}));
|
|
2553
|
+
|
|
2554
|
+
// Builds drag/move handlers for a list where dnd-kit item ids are the array
|
|
2555
|
+
// index. `onReorder` receives the whole reordered list.
|
|
2556
|
+
const makeListReorderHandlers = (list, onReorder) => {
|
|
2557
|
+
const handleDragEnd = ({
|
|
2558
|
+
active,
|
|
2559
|
+
over
|
|
2560
|
+
}) => {
|
|
2561
|
+
if (over && active.id !== over.id) {
|
|
2562
|
+
const oldIndex = list.findIndex((_, i) => i === active.id);
|
|
2563
|
+
const newIndex = list.findIndex((_, i) => i === over.id);
|
|
2564
|
+
onReorder(arrayMove(list, oldIndex, newIndex));
|
|
2565
|
+
}
|
|
2566
|
+
};
|
|
2567
|
+
const handleMoveUp = index => {
|
|
2568
|
+
if (index > 0) {
|
|
2569
|
+
onReorder(arrayMove(list, index, index - 1));
|
|
2570
|
+
}
|
|
2571
|
+
};
|
|
2572
|
+
const handleMoveDown = index => {
|
|
2573
|
+
if (index < list.length - 1) {
|
|
2574
|
+
onReorder(arrayMove(list, index, index + 1));
|
|
2575
|
+
}
|
|
2576
|
+
};
|
|
2577
|
+
return {
|
|
2578
|
+
handleDragEnd,
|
|
2579
|
+
handleMoveUp,
|
|
2580
|
+
handleMoveDown
|
|
2581
|
+
};
|
|
2582
|
+
};
|
|
2583
|
+
|
|
2584
|
+
// Wraps DndContext + SortableContext + the vertical list container.
|
|
2585
|
+
// `itemIds` must line up with the ids passed to each item's useSortable().
|
|
2586
|
+
const SortableListContext = ({
|
|
2587
|
+
sensors,
|
|
2588
|
+
itemIds,
|
|
2589
|
+
onDragEnd,
|
|
2590
|
+
children
|
|
2591
|
+
}) => /*#__PURE__*/React__default.createElement(DndContext, {
|
|
2592
|
+
sensors: sensors,
|
|
2593
|
+
collisionDetection: closestCenter,
|
|
2594
|
+
onDragEnd: onDragEnd
|
|
2595
|
+
}, /*#__PURE__*/React__default.createElement(SortableContext, {
|
|
2596
|
+
items: itemIds,
|
|
2597
|
+
strategy: verticalListSortingStrategy
|
|
2598
|
+
}, /*#__PURE__*/React__default.createElement(Box$1, {
|
|
2599
|
+
sx: {
|
|
2600
|
+
display: "flex",
|
|
2601
|
+
flexDirection: "column",
|
|
2602
|
+
gap: 1
|
|
2603
|
+
}
|
|
2604
|
+
}, children)));
|
|
2605
|
+
|
|
2606
|
+
// Per-row useSortable() wiring shared by every sortable chip/row component.
|
|
2607
|
+
const useSortableRow = id => {
|
|
2608
|
+
const {
|
|
2609
|
+
attributes,
|
|
2610
|
+
listeners,
|
|
2611
|
+
setNodeRef,
|
|
2612
|
+
transform,
|
|
2613
|
+
transition,
|
|
2614
|
+
isDragging
|
|
2615
|
+
} = useSortable({
|
|
2616
|
+
id
|
|
2617
|
+
});
|
|
2618
|
+
const style = {
|
|
2619
|
+
transform: CSS.Transform.toString(transform),
|
|
2620
|
+
transition,
|
|
2621
|
+
opacity: isDragging ? 0.5 : 1,
|
|
2622
|
+
display: "flex",
|
|
2623
|
+
alignItems: "center",
|
|
2624
|
+
width: "100%"
|
|
2625
|
+
};
|
|
2626
|
+
return {
|
|
2627
|
+
attributes,
|
|
2628
|
+
listeners,
|
|
2629
|
+
setNodeRef,
|
|
2630
|
+
style
|
|
2631
|
+
};
|
|
2632
|
+
};
|
|
2633
|
+
const DragHandle = ({
|
|
2634
|
+
listeners
|
|
2635
|
+
}) => /*#__PURE__*/React__default.createElement(Box$1, _extends({}, listeners, {
|
|
2636
|
+
sx: {
|
|
2637
|
+
display: "flex",
|
|
2638
|
+
alignItems: "center",
|
|
2639
|
+
cursor: "grab",
|
|
2640
|
+
"&:active": {
|
|
2641
|
+
cursor: "grabbing"
|
|
2642
|
+
}
|
|
2643
|
+
}
|
|
2644
|
+
}), /*#__PURE__*/React__default.createElement(DragIndicatorIcon, {
|
|
2645
|
+
sx: {
|
|
2646
|
+
cursor: "grab",
|
|
2647
|
+
color: "rgba(110, 110, 110, 0.62)"
|
|
2648
|
+
}
|
|
2649
|
+
}));
|
|
2650
|
+
const MoveButtons = ({
|
|
2651
|
+
onMoveUp,
|
|
2652
|
+
onMoveDown,
|
|
2653
|
+
isFirst,
|
|
2654
|
+
isLast
|
|
2655
|
+
}) => /*#__PURE__*/React__default.createElement(Box$1, {
|
|
2656
|
+
className: "hover-icons",
|
|
2657
|
+
sx: {
|
|
2658
|
+
display: "flex",
|
|
2659
|
+
gap: 0.5,
|
|
2660
|
+
opacity: 0,
|
|
2661
|
+
transition: "opacity 0.2s"
|
|
2662
|
+
}
|
|
2663
|
+
}, /*#__PURE__*/React__default.createElement(IconButton$1, {
|
|
2664
|
+
size: "small",
|
|
2665
|
+
onClick: onMoveUp,
|
|
2666
|
+
disabled: isFirst,
|
|
2667
|
+
"aria-label": "move up"
|
|
2668
|
+
}, /*#__PURE__*/React__default.createElement(ArrowUpwardIcon, {
|
|
2669
|
+
fontSize: "small"
|
|
2670
|
+
})), /*#__PURE__*/React__default.createElement(IconButton$1, {
|
|
2671
|
+
size: "small",
|
|
2672
|
+
onClick: onMoveDown,
|
|
2673
|
+
disabled: isLast,
|
|
2674
|
+
"aria-label": "move down"
|
|
2675
|
+
}, /*#__PURE__*/React__default.createElement(ArrowDownwardIcon, {
|
|
2676
|
+
fontSize: "small"
|
|
2677
|
+
})));
|
|
2678
|
+
|
|
2679
|
+
// Builds an identity string for a saved dimension/metric instance. Deliberate
|
|
2680
|
+
// duplicates (the same field added more than once) are allowed - the first
|
|
2681
|
+
// occurrence keeps the plain fullPath (so existing saved reports and their
|
|
2682
|
+
// title-override keys stay backward compatible), later occurrences get a
|
|
2683
|
+
// `#n` suffix.
|
|
2684
|
+
const makeInstanceId = (fullPath, occurrenceIndex) => occurrenceIndex === 0 ? fullPath : `${fullPath}#${occurrenceIndex}`;
|
|
2685
|
+
|
|
2686
|
+
// Picks the next free instance id for a newly-added fullPath, given the
|
|
2687
|
+
// items already saved. Scans occurrence numbers from 0 rather than just
|
|
2688
|
+
// counting existing instances with this fullPath, because counting alone
|
|
2689
|
+
// can collide: e.g. add "ft.currency" twice (ids "ft.currency",
|
|
2690
|
+
// "ft.currency#1"), remove the first, then add "ft.currency" again - a
|
|
2691
|
+
// plain count of 1 would regenerate "ft.currency#1", clashing with the
|
|
2692
|
+
// surviving duplicate.
|
|
2693
|
+
const nextInstanceId = (fullPath, existingItems) => {
|
|
2694
|
+
const existingIds = new Set(existingItems.map(item => item.id));
|
|
2695
|
+
let occurrenceIndex = 0;
|
|
2696
|
+
while (existingIds.has(makeInstanceId(fullPath, occurrenceIndex))) {
|
|
2697
|
+
occurrenceIndex += 1;
|
|
2698
|
+
}
|
|
2699
|
+
return makeInstanceId(fullPath, occurrenceIndex);
|
|
2700
|
+
};
|
|
2701
|
+
|
|
2531
2702
|
// Replace {{key}} placeholders in a title with values from the report
|
|
2532
2703
|
// parameters (e.g. "Balance ({{base_currency}})" -> "Balance (EUR)").
|
|
2533
2704
|
// Unknown keys are left as-is so missing parameters remain visible.
|
|
@@ -2539,6 +2710,53 @@ const interpolateTitle = (template, params) => {
|
|
|
2539
2710
|
});
|
|
2540
2711
|
};
|
|
2541
2712
|
|
|
2713
|
+
// Resolves the final column display order for the results grid: dimensions,
|
|
2714
|
+
// computed dimensions, and metrics combined into a single list.
|
|
2715
|
+
//
|
|
2716
|
+
// `columnOrder` is the optional, explicitly-arranged order (raw entries
|
|
2717
|
+
// `{ type: 'dimension' | 'computed' | 'metric', key }`) set on the Column
|
|
2718
|
+
// Order tab. When present, its entries come first, in that order. Anything
|
|
2719
|
+
// not covered by it (new fields added since it was last touched, or when
|
|
2720
|
+
// it's empty/undefined because no custom order has ever been defined) is
|
|
2721
|
+
// appended in the pre-existing default order: dimensions/computed (in their
|
|
2722
|
+
// own `dimensionOrder`) first, then metrics (in their own array order) —
|
|
2723
|
+
// this keeps behavior unchanged for reports that never define a column order.
|
|
2724
|
+
const resolveColumnOrder = (columnOrder, orderedDimensionItems, metrics) => {
|
|
2725
|
+
// Normalise every candidate item to a uniform { kind, key, data } shape
|
|
2726
|
+
// up front, so callers (e.g. the Column Order tab) can always read
|
|
2727
|
+
// `.key` regardless of where the item came from.
|
|
2728
|
+
// Keyed by instance id (not fullPath) so that two instances of the same
|
|
2729
|
+
// dimension/metric - added deliberately as duplicates - are treated as
|
|
2730
|
+
// distinct columns rather than collapsing into one.
|
|
2731
|
+
const normalizedDimensionItems = orderedDimensionItems.map(item => ({
|
|
2732
|
+
kind: item.kind,
|
|
2733
|
+
key: item.kind === 'computed' ? item.data.name : item.data.id,
|
|
2734
|
+
data: item.data
|
|
2735
|
+
}));
|
|
2736
|
+
const normalizedMetricItems = metrics.map(m => ({
|
|
2737
|
+
kind: 'metric',
|
|
2738
|
+
key: m.id,
|
|
2739
|
+
data: m
|
|
2740
|
+
}));
|
|
2741
|
+
const byKey = Object.fromEntries([...normalizedDimensionItems, ...normalizedMetricItems].map(item => [`${item.kind}:${item.key}`, item]));
|
|
2742
|
+
const resolved = [];
|
|
2743
|
+
const seen = new Set();
|
|
2744
|
+
(columnOrder || []).forEach(entry => {
|
|
2745
|
+
const seenKey = `${entry.type}:${entry.key}`;
|
|
2746
|
+
if (seen.has(seenKey) || !byKey[seenKey]) return;
|
|
2747
|
+
resolved.push(byKey[seenKey]);
|
|
2748
|
+
seen.add(seenKey);
|
|
2749
|
+
});
|
|
2750
|
+
[...normalizedDimensionItems, ...normalizedMetricItems].forEach(item => {
|
|
2751
|
+
const seenKey = `${item.kind}:${item.key}`;
|
|
2752
|
+
if (!seen.has(seenKey)) {
|
|
2753
|
+
resolved.push(item);
|
|
2754
|
+
seen.add(seenKey);
|
|
2755
|
+
}
|
|
2756
|
+
});
|
|
2757
|
+
return resolved;
|
|
2758
|
+
};
|
|
2759
|
+
|
|
2542
2760
|
// Sortable Chip Component
|
|
2543
2761
|
const SortableChip$1 = ({
|
|
2544
2762
|
id,
|
|
@@ -2551,7 +2769,7 @@ const SortableChip$1 = ({
|
|
|
2551
2769
|
isLast,
|
|
2552
2770
|
sortOrder,
|
|
2553
2771
|
onSortOrderChange,
|
|
2554
|
-
|
|
2772
|
+
instanceId,
|
|
2555
2773
|
defaultTitle,
|
|
2556
2774
|
customTitle,
|
|
2557
2775
|
onUpdateTitle,
|
|
@@ -2567,20 +2785,8 @@ const SortableChip$1 = ({
|
|
|
2567
2785
|
attributes,
|
|
2568
2786
|
listeners,
|
|
2569
2787
|
setNodeRef,
|
|
2570
|
-
|
|
2571
|
-
|
|
2572
|
-
isDragging
|
|
2573
|
-
} = useSortable({
|
|
2574
|
-
id
|
|
2575
|
-
});
|
|
2576
|
-
const style = {
|
|
2577
|
-
transform: CSS.Transform.toString(transform),
|
|
2578
|
-
transition,
|
|
2579
|
-
opacity: isDragging ? 0.5 : 1,
|
|
2580
|
-
display: "flex",
|
|
2581
|
-
alignItems: "center",
|
|
2582
|
-
width: "100%"
|
|
2583
|
-
};
|
|
2788
|
+
style
|
|
2789
|
+
} = useSortableRow(id);
|
|
2584
2790
|
|
|
2585
2791
|
// Focus input when entering edit mode
|
|
2586
2792
|
useEffect(() => {
|
|
@@ -2646,7 +2852,7 @@ const SortableChip$1 = ({
|
|
|
2646
2852
|
handleCancel();
|
|
2647
2853
|
return;
|
|
2648
2854
|
}
|
|
2649
|
-
onUpdateTitle(
|
|
2855
|
+
onUpdateTitle(instanceId, trimmedValue);
|
|
2650
2856
|
setIsEditing(false);
|
|
2651
2857
|
};
|
|
2652
2858
|
const handleCancel = () => {
|
|
@@ -2654,7 +2860,7 @@ const SortableChip$1 = ({
|
|
|
2654
2860
|
setEditValue("");
|
|
2655
2861
|
};
|
|
2656
2862
|
const handleReset = () => {
|
|
2657
|
-
onResetTitle(
|
|
2863
|
+
onResetTitle(instanceId);
|
|
2658
2864
|
setIsEditing(false);
|
|
2659
2865
|
setEditValue("");
|
|
2660
2866
|
};
|
|
@@ -2689,21 +2895,9 @@ const SortableChip$1 = ({
|
|
|
2689
2895
|
opacity: 1 // show icons on hover
|
|
2690
2896
|
}
|
|
2691
2897
|
}
|
|
2692
|
-
}, /*#__PURE__*/React__default.createElement(
|
|
2693
|
-
|
|
2694
|
-
|
|
2695
|
-
alignItems: "center",
|
|
2696
|
-
cursor: "grab",
|
|
2697
|
-
"&:active": {
|
|
2698
|
-
cursor: "grabbing"
|
|
2699
|
-
}
|
|
2700
|
-
}
|
|
2701
|
-
}), /*#__PURE__*/React__default.createElement(DragIndicatorIcon, {
|
|
2702
|
-
sx: {
|
|
2703
|
-
cursor: "grab",
|
|
2704
|
-
color: "rgba(110, 110, 110, 0.62)"
|
|
2705
|
-
}
|
|
2706
|
-
})), !isEditing ? /*#__PURE__*/React__default.createElement(React__default.Fragment, null, /*#__PURE__*/React__default.createElement(Box$1, {
|
|
2898
|
+
}, /*#__PURE__*/React__default.createElement(DragHandle, {
|
|
2899
|
+
listeners: listeners
|
|
2900
|
+
}), !isEditing ? /*#__PURE__*/React__default.createElement(React__default.Fragment, null, /*#__PURE__*/React__default.createElement(Box$1, {
|
|
2707
2901
|
sx: {
|
|
2708
2902
|
minWidth: 0
|
|
2709
2903
|
}
|
|
@@ -2766,29 +2960,12 @@ const SortableChip$1 = ({
|
|
|
2766
2960
|
sx: {
|
|
2767
2961
|
flex: 1
|
|
2768
2962
|
}
|
|
2769
|
-
}), /*#__PURE__*/React__default.createElement(
|
|
2770
|
-
|
|
2771
|
-
|
|
2772
|
-
|
|
2773
|
-
|
|
2774
|
-
|
|
2775
|
-
transition: "opacity 0.2s"
|
|
2776
|
-
}
|
|
2777
|
-
}, /*#__PURE__*/React__default.createElement(IconButton$1, {
|
|
2778
|
-
size: "small",
|
|
2779
|
-
onClick: onMoveUp,
|
|
2780
|
-
disabled: isFirst,
|
|
2781
|
-
"aria-label": "move up"
|
|
2782
|
-
}, /*#__PURE__*/React__default.createElement(ArrowUpwardIcon, {
|
|
2783
|
-
fontSize: "small"
|
|
2784
|
-
})), /*#__PURE__*/React__default.createElement(IconButton$1, {
|
|
2785
|
-
size: "small",
|
|
2786
|
-
onClick: onMoveDown,
|
|
2787
|
-
disabled: isLast,
|
|
2788
|
-
"aria-label": "move down"
|
|
2789
|
-
}, /*#__PURE__*/React__default.createElement(ArrowDownwardIcon, {
|
|
2790
|
-
fontSize: "small"
|
|
2791
|
-
})))) : /*#__PURE__*/React__default.createElement(React__default.Fragment, null, /*#__PURE__*/React__default.createElement(TextField, {
|
|
2963
|
+
}), /*#__PURE__*/React__default.createElement(MoveButtons, {
|
|
2964
|
+
onMoveUp: onMoveUp,
|
|
2965
|
+
onMoveDown: onMoveDown,
|
|
2966
|
+
isFirst: isFirst,
|
|
2967
|
+
isLast: isLast
|
|
2968
|
+
})) : /*#__PURE__*/React__default.createElement(React__default.Fragment, null, /*#__PURE__*/React__default.createElement(TextField, {
|
|
2792
2969
|
inputRef: inputRef,
|
|
2793
2970
|
value: editValue,
|
|
2794
2971
|
onChange: e => setEditValue(e.target.value),
|
|
@@ -2864,24 +3041,12 @@ const SortableComputedChip = ({
|
|
|
2864
3041
|
attributes,
|
|
2865
3042
|
listeners,
|
|
2866
3043
|
setNodeRef,
|
|
2867
|
-
|
|
2868
|
-
|
|
2869
|
-
isDragging
|
|
2870
|
-
} = useSortable({
|
|
2871
|
-
id
|
|
2872
|
-
});
|
|
3044
|
+
style
|
|
3045
|
+
} = useSortableRow(id);
|
|
2873
3046
|
const [isEditing, setIsEditing] = useState(false);
|
|
2874
3047
|
const [editName, setEditName] = useState("");
|
|
2875
3048
|
const [editValue, setEditValue] = useState("");
|
|
2876
3049
|
const containerRef = useRef(null);
|
|
2877
|
-
const style = {
|
|
2878
|
-
transform: CSS.Transform.toString(transform),
|
|
2879
|
-
transition,
|
|
2880
|
-
opacity: isDragging ? 0.5 : 1,
|
|
2881
|
-
display: "flex",
|
|
2882
|
-
alignItems: "center",
|
|
2883
|
-
width: "100%"
|
|
2884
|
-
};
|
|
2885
3050
|
|
|
2886
3051
|
// Handle click outside to cancel
|
|
2887
3052
|
useEffect(() => {
|
|
@@ -2906,7 +3071,7 @@ const SortableComputedChip = ({
|
|
|
2906
3071
|
const handleSave = () => {
|
|
2907
3072
|
const trimmedName = editName.trim();
|
|
2908
3073
|
const trimmedValue = editValue.trim();
|
|
2909
|
-
if (!trimmedName
|
|
3074
|
+
if (!trimmedName) return;
|
|
2910
3075
|
if (isNameTaken(trimmedName, name)) return;
|
|
2911
3076
|
onUpdate(name, {
|
|
2912
3077
|
name: trimmedName,
|
|
@@ -2944,21 +3109,9 @@ const SortableComputedChip = ({
|
|
|
2944
3109
|
opacity: 1
|
|
2945
3110
|
}
|
|
2946
3111
|
}
|
|
2947
|
-
}, /*#__PURE__*/React__default.createElement(
|
|
2948
|
-
|
|
2949
|
-
|
|
2950
|
-
alignItems: "center",
|
|
2951
|
-
cursor: "grab",
|
|
2952
|
-
"&:active": {
|
|
2953
|
-
cursor: "grabbing"
|
|
2954
|
-
}
|
|
2955
|
-
}
|
|
2956
|
-
}), /*#__PURE__*/React__default.createElement(DragIndicatorIcon, {
|
|
2957
|
-
sx: {
|
|
2958
|
-
cursor: "grab",
|
|
2959
|
-
color: "rgba(110, 110, 110, 0.62)"
|
|
2960
|
-
}
|
|
2961
|
-
})), !isEditing ? /*#__PURE__*/React__default.createElement(React__default.Fragment, null, /*#__PURE__*/React__default.createElement(Chip, {
|
|
3112
|
+
}, /*#__PURE__*/React__default.createElement(DragHandle, {
|
|
3113
|
+
listeners: listeners
|
|
3114
|
+
}), !isEditing ? /*#__PURE__*/React__default.createElement(React__default.Fragment, null, /*#__PURE__*/React__default.createElement(Chip, {
|
|
2962
3115
|
icon: /*#__PURE__*/React__default.createElement(FunctionsIcon, {
|
|
2963
3116
|
sx: {
|
|
2964
3117
|
fontSize: "15px !important"
|
|
@@ -3022,29 +3175,12 @@ const SortableComputedChip = ({
|
|
|
3022
3175
|
sx: {
|
|
3023
3176
|
flex: 1
|
|
3024
3177
|
}
|
|
3025
|
-
}), /*#__PURE__*/React__default.createElement(
|
|
3026
|
-
|
|
3027
|
-
|
|
3028
|
-
|
|
3029
|
-
|
|
3030
|
-
|
|
3031
|
-
transition: "opacity 0.2s"
|
|
3032
|
-
}
|
|
3033
|
-
}, /*#__PURE__*/React__default.createElement(IconButton$1, {
|
|
3034
|
-
size: "small",
|
|
3035
|
-
onClick: onMoveUp,
|
|
3036
|
-
disabled: isFirst,
|
|
3037
|
-
"aria-label": "move up"
|
|
3038
|
-
}, /*#__PURE__*/React__default.createElement(ArrowUpwardIcon, {
|
|
3039
|
-
fontSize: "small"
|
|
3040
|
-
})), /*#__PURE__*/React__default.createElement(IconButton$1, {
|
|
3041
|
-
size: "small",
|
|
3042
|
-
onClick: onMoveDown,
|
|
3043
|
-
disabled: isLast,
|
|
3044
|
-
"aria-label": "move down"
|
|
3045
|
-
}, /*#__PURE__*/React__default.createElement(ArrowDownwardIcon, {
|
|
3046
|
-
fontSize: "small"
|
|
3047
|
-
})))) : /*#__PURE__*/React__default.createElement(React__default.Fragment, null, /*#__PURE__*/React__default.createElement(TextField, {
|
|
3178
|
+
}), /*#__PURE__*/React__default.createElement(MoveButtons, {
|
|
3179
|
+
onMoveUp: onMoveUp,
|
|
3180
|
+
onMoveDown: onMoveDown,
|
|
3181
|
+
isFirst: isFirst,
|
|
3182
|
+
isLast: isLast
|
|
3183
|
+
})) : /*#__PURE__*/React__default.createElement(React__default.Fragment, null, /*#__PURE__*/React__default.createElement(TextField, {
|
|
3048
3184
|
value: editName,
|
|
3049
3185
|
onChange: e => setEditName(e.target.value),
|
|
3050
3186
|
onKeyDown: handleKeyDown,
|
|
@@ -3085,7 +3221,7 @@ const SortableComputedChip = ({
|
|
|
3085
3221
|
onClick: handleSave,
|
|
3086
3222
|
color: "primary",
|
|
3087
3223
|
"aria-label": "save computed dimension",
|
|
3088
|
-
disabled: !editName.trim() ||
|
|
3224
|
+
disabled: !editName.trim() || nameError
|
|
3089
3225
|
}, /*#__PURE__*/React__default.createElement(CheckIcon, {
|
|
3090
3226
|
fontSize: "small"
|
|
3091
3227
|
})))), /*#__PURE__*/React__default.createElement(Tooltip$1, {
|
|
@@ -3129,55 +3265,39 @@ const Dimensions = ({
|
|
|
3129
3265
|
const [computedValue, setComputedValue] = useState("");
|
|
3130
3266
|
|
|
3131
3267
|
// Setup drag and drop sensors
|
|
3132
|
-
const sensors =
|
|
3133
|
-
coordinateGetter: sortableKeyboardCoordinates
|
|
3134
|
-
}));
|
|
3268
|
+
const sensors = useSortableListSensors();
|
|
3135
3269
|
|
|
3136
3270
|
// Resolve the unified dimensionOrder into the actual dimension / computed
|
|
3137
3271
|
// dimension objects, so dimensions and computed dimensions can be shown
|
|
3138
3272
|
// (and dragged) as a single list while staying stored in their own arrays.
|
|
3139
|
-
|
|
3273
|
+
// Keyed by instance id rather than fullPath, so two instances of the same
|
|
3274
|
+
// dimension (added deliberately as a duplicate) stay distinct entries.
|
|
3275
|
+
const dimensionById = Object.fromEntries(savedDimensions.map(d => [d.id, d]));
|
|
3140
3276
|
const computedDimensionByName = Object.fromEntries(savedComputedDimensions.map(cd => [cd.name, cd]));
|
|
3141
3277
|
const orderedItems = dimensionOrder.map(entry => entry.type === "dimension" ? {
|
|
3142
3278
|
kind: "dimension",
|
|
3143
3279
|
key: entry.key,
|
|
3144
|
-
data:
|
|
3280
|
+
data: dimensionById[entry.key]
|
|
3145
3281
|
} : {
|
|
3146
3282
|
kind: "computed",
|
|
3147
3283
|
key: entry.key,
|
|
3148
3284
|
data: computedDimensionByName[entry.key]
|
|
3149
3285
|
}).filter(item => item.data);
|
|
3150
3286
|
|
|
3151
|
-
// Handle drag
|
|
3152
|
-
const
|
|
3153
|
-
|
|
3154
|
-
|
|
3155
|
-
|
|
3156
|
-
|
|
3157
|
-
if (over && active.id !== over.id) {
|
|
3158
|
-
const oldIndex = dimensionOrder.findIndex((_, i) => i === active.id);
|
|
3159
|
-
const newIndex = dimensionOrder.findIndex((_, i) => i === over.id);
|
|
3160
|
-
onReorderDimensionItems(arrayMove(dimensionOrder, oldIndex, newIndex));
|
|
3161
|
-
}
|
|
3162
|
-
};
|
|
3163
|
-
|
|
3164
|
-
// Handle move up
|
|
3165
|
-
const handleMoveUp = index => {
|
|
3166
|
-
if (index > 0) {
|
|
3167
|
-
onReorderDimensionItems(arrayMove(dimensionOrder, index, index - 1));
|
|
3168
|
-
}
|
|
3169
|
-
};
|
|
3170
|
-
|
|
3171
|
-
// Handle move down
|
|
3172
|
-
const handleMoveDown = index => {
|
|
3173
|
-
if (index < dimensionOrder.length - 1) {
|
|
3174
|
-
onReorderDimensionItems(arrayMove(dimensionOrder, index, index + 1));
|
|
3175
|
-
}
|
|
3176
|
-
};
|
|
3287
|
+
// Handle drag/move for the unified list
|
|
3288
|
+
const {
|
|
3289
|
+
handleDragEnd,
|
|
3290
|
+
handleMoveUp,
|
|
3291
|
+
handleMoveDown
|
|
3292
|
+
} = makeListReorderHandlers(dimensionOrder, onReorderDimensionItems);
|
|
3177
3293
|
|
|
3178
|
-
// Handle sort order change (dimensions only - computed dimensions have no
|
|
3179
|
-
|
|
3180
|
-
|
|
3294
|
+
// Handle sort order change (dimensions only - computed dimensions have no
|
|
3295
|
+
// order_by). Keyed by instance id but applied query-wide: SQL can only
|
|
3296
|
+
// sort by a given column once, so toggling sort on any duplicate instance
|
|
3297
|
+
// of the same field is propagated to all instances sharing its fullPath
|
|
3298
|
+
// (handled in ReportBuilder's onUpdateDimensionSortOrder).
|
|
3299
|
+
const handleSortOrderChange = (id, sortOrder) => {
|
|
3300
|
+
onUpdateDimensionSortOrder(id, sortOrder);
|
|
3181
3301
|
};
|
|
3182
3302
|
|
|
3183
3303
|
// Computed Dimensions: { name, value } pairs independent of any provider.
|
|
@@ -3198,7 +3318,7 @@ const Dimensions = ({
|
|
|
3198
3318
|
const handleSaveComputed = () => {
|
|
3199
3319
|
const trimmedName = computedName.trim();
|
|
3200
3320
|
const trimmedValue = computedValue.trim();
|
|
3201
|
-
if (!trimmedName
|
|
3321
|
+
if (!trimmedName) return;
|
|
3202
3322
|
if (isComputedNameTaken(trimmedName)) return;
|
|
3203
3323
|
onSaveComputedDimension({
|
|
3204
3324
|
name: trimmedName,
|
|
@@ -3240,7 +3360,10 @@ const Dimensions = ({
|
|
|
3240
3360
|
currentProviderKey = selection.targetKey;
|
|
3241
3361
|
});
|
|
3242
3362
|
|
|
3243
|
-
// Create a Set of already selected dimension fullPaths for quick lookup
|
|
3363
|
+
// Create a Set of already selected dimension fullPaths for quick lookup.
|
|
3364
|
+
// Already-selected items stay selectable (so the same dimension can be
|
|
3365
|
+
// added again on purpose) - they're just flagged so the dropdown can
|
|
3366
|
+
// highlight them instead of blocking the click.
|
|
3244
3367
|
const selectedFullPaths = new Set(savedDimensions.map(dim => dim.fullPath));
|
|
3245
3368
|
const items = [];
|
|
3246
3369
|
// Iterate through aliases (e.g., 'ba', 'pc')
|
|
@@ -3252,9 +3375,6 @@ const Dimensions = ({
|
|
|
3252
3375
|
|
|
3253
3376
|
// Construct the fullPath for this dimension
|
|
3254
3377
|
const fullPath = `${aliasPath.join("_")}.${dimKey}`;
|
|
3255
|
-
|
|
3256
|
-
// Check if this dimension is already selected
|
|
3257
|
-
const isAlreadySelected = selectedFullPaths.has(fullPath);
|
|
3258
3378
|
items.push({
|
|
3259
3379
|
// Include provider name to ensure uniqueness across different providers
|
|
3260
3380
|
key: `${currentProvider}_${alias}.${dimKey}`,
|
|
@@ -3262,7 +3382,7 @@ const Dimensions = ({
|
|
|
3262
3382
|
dimensionKey: dimKey,
|
|
3263
3383
|
alias: alias,
|
|
3264
3384
|
dimension: dimension,
|
|
3265
|
-
|
|
3385
|
+
alreadySelected: selectedFullPaths.has(fullPath)
|
|
3266
3386
|
});
|
|
3267
3387
|
});
|
|
3268
3388
|
});
|
|
@@ -3324,7 +3444,10 @@ const Dimensions = ({
|
|
|
3324
3444
|
dimensionKey: selectedItem.dimensionKey,
|
|
3325
3445
|
dimensionTitle: selectedItem.value,
|
|
3326
3446
|
// The constructed path for server
|
|
3327
|
-
fullPath: fullPath
|
|
3447
|
+
fullPath: fullPath,
|
|
3448
|
+
// Unique per-instance identity, so this field can be added more than
|
|
3449
|
+
// once and each occurrence stays independently orderable/removable.
|
|
3450
|
+
id: nextInstanceId(fullPath, savedDimensions)
|
|
3328
3451
|
};
|
|
3329
3452
|
onSaveDimension(dimensionData);
|
|
3330
3453
|
handleCancel();
|
|
@@ -3335,8 +3458,9 @@ const Dimensions = ({
|
|
|
3335
3458
|
const handleAddAll = () => {
|
|
3336
3459
|
const dimensionItems = getDimensionItems();
|
|
3337
3460
|
|
|
3338
|
-
// Filter out already selected dimensions
|
|
3339
|
-
|
|
3461
|
+
// Filter out already selected dimensions - "Add all" only fills in
|
|
3462
|
+
// missing ones; intentional duplicates are added one at a time above.
|
|
3463
|
+
const availableItems = dimensionItems.filter(item => !item.alreadySelected);
|
|
3340
3464
|
if (availableItems.length === 0) return;
|
|
3341
3465
|
|
|
3342
3466
|
// Build the complete relation objects array (same for all dimensions in this provider)
|
|
@@ -3381,7 +3505,10 @@ const Dimensions = ({
|
|
|
3381
3505
|
dimensionKey: item.dimensionKey,
|
|
3382
3506
|
dimensionTitle: item.value,
|
|
3383
3507
|
// The constructed path for server
|
|
3384
|
-
fullPath: fullPath
|
|
3508
|
+
fullPath: fullPath,
|
|
3509
|
+
// "Add all" only ever adds fields that aren't already selected, so
|
|
3510
|
+
// this is always the first (and only) occurrence of this fullPath.
|
|
3511
|
+
id: fullPath
|
|
3385
3512
|
};
|
|
3386
3513
|
onSaveDimension(dimensionData);
|
|
3387
3514
|
});
|
|
@@ -3515,7 +3642,7 @@ const Dimensions = ({
|
|
|
3515
3642
|
}, "Cancel"), /*#__PURE__*/React__default.createElement(Button, {
|
|
3516
3643
|
variant: "contained",
|
|
3517
3644
|
onClick: handleSaveComputed,
|
|
3518
|
-
disabled: !computedName.trim() ||
|
|
3645
|
+
disabled: !computedName.trim() || computedNameError,
|
|
3519
3646
|
sx: {
|
|
3520
3647
|
height: "40px",
|
|
3521
3648
|
fontFamily: "system-ui",
|
|
@@ -3591,7 +3718,7 @@ const Dimensions = ({
|
|
|
3591
3718
|
placement: "top"
|
|
3592
3719
|
}, /*#__PURE__*/React__default.createElement("span", null, /*#__PURE__*/React__default.createElement(IconButton$1, {
|
|
3593
3720
|
onClick: handleAddAll,
|
|
3594
|
-
disabled: getDimensionItems().filter(item => !item.
|
|
3721
|
+
disabled: getDimensionItems().filter(item => !item.alreadySelected).length === 0,
|
|
3595
3722
|
"aria-label": "add all dimensions",
|
|
3596
3723
|
sx: {
|
|
3597
3724
|
color: "rgb(70, 134, 128)",
|
|
@@ -3666,19 +3793,10 @@ const Dimensions = ({
|
|
|
3666
3793
|
color: "#666",
|
|
3667
3794
|
marginBottom: 2
|
|
3668
3795
|
}
|
|
3669
|
-
}, "Drag to reorder or use arrows"), /*#__PURE__*/React__default.createElement(
|
|
3796
|
+
}, "Drag to reorder or use arrows"), /*#__PURE__*/React__default.createElement(SortableListContext, {
|
|
3670
3797
|
sensors: sensors,
|
|
3671
|
-
|
|
3798
|
+
itemIds: orderedItems.map((_, index) => index),
|
|
3672
3799
|
onDragEnd: handleDragEnd
|
|
3673
|
-
}, /*#__PURE__*/React__default.createElement(SortableContext, {
|
|
3674
|
-
items: orderedItems.map((_, index) => index),
|
|
3675
|
-
strategy: verticalListSortingStrategy
|
|
3676
|
-
}, /*#__PURE__*/React__default.createElement(Box$1, {
|
|
3677
|
-
sx: {
|
|
3678
|
-
display: "flex",
|
|
3679
|
-
flexDirection: "column",
|
|
3680
|
-
gap: 1
|
|
3681
|
-
}
|
|
3682
3800
|
}, orderedItems.map((item, index) => item.kind === "dimension" ? /*#__PURE__*/React__default.createElement(SortableChip$1, {
|
|
3683
3801
|
key: `dim-${item.key}`,
|
|
3684
3802
|
id: index,
|
|
@@ -3691,9 +3809,9 @@ const Dimensions = ({
|
|
|
3691
3809
|
isLast: index === orderedItems.length - 1,
|
|
3692
3810
|
sortOrder: item.data.sortOrder || null,
|
|
3693
3811
|
onSortOrderChange: sortOrder => handleSortOrderChange(item.key, sortOrder),
|
|
3694
|
-
|
|
3812
|
+
instanceId: item.data.id,
|
|
3695
3813
|
defaultTitle: item.data.dimensionTitle,
|
|
3696
|
-
customTitle: titleOverrides[item.data.
|
|
3814
|
+
customTitle: titleOverrides[item.data.id],
|
|
3697
3815
|
onUpdateTitle: onUpdateTitle,
|
|
3698
3816
|
onResetTitle: onResetTitle
|
|
3699
3817
|
}) : /*#__PURE__*/React__default.createElement(SortableComputedChip, {
|
|
@@ -3708,7 +3826,7 @@ const Dimensions = ({
|
|
|
3708
3826
|
onMoveDown: () => handleMoveDown(index),
|
|
3709
3827
|
isFirst: index === 0,
|
|
3710
3828
|
isLast: index === orderedItems.length - 1
|
|
3711
|
-
})))))
|
|
3829
|
+
})))));
|
|
3712
3830
|
};
|
|
3713
3831
|
|
|
3714
3832
|
const MetricSourceIcon = ({
|
|
@@ -3766,7 +3884,7 @@ const SortableChip = ({
|
|
|
3766
3884
|
onMoveDown,
|
|
3767
3885
|
isFirst,
|
|
3768
3886
|
isLast,
|
|
3769
|
-
|
|
3887
|
+
instanceId,
|
|
3770
3888
|
defaultTitle,
|
|
3771
3889
|
customTitle,
|
|
3772
3890
|
onUpdateTitle,
|
|
@@ -3783,20 +3901,8 @@ const SortableChip = ({
|
|
|
3783
3901
|
attributes,
|
|
3784
3902
|
listeners,
|
|
3785
3903
|
setNodeRef,
|
|
3786
|
-
|
|
3787
|
-
|
|
3788
|
-
isDragging
|
|
3789
|
-
} = useSortable({
|
|
3790
|
-
id
|
|
3791
|
-
});
|
|
3792
|
-
const style = {
|
|
3793
|
-
transform: CSS.Transform.toString(transform),
|
|
3794
|
-
transition,
|
|
3795
|
-
opacity: isDragging ? 0.5 : 1,
|
|
3796
|
-
display: 'flex',
|
|
3797
|
-
alignItems: 'center',
|
|
3798
|
-
width: '100%'
|
|
3799
|
-
};
|
|
3904
|
+
style
|
|
3905
|
+
} = useSortableRow(id);
|
|
3800
3906
|
|
|
3801
3907
|
// Focus input when entering edit mode
|
|
3802
3908
|
useEffect(() => {
|
|
@@ -3831,7 +3937,7 @@ const SortableChip = ({
|
|
|
3831
3937
|
handleCancel();
|
|
3832
3938
|
return;
|
|
3833
3939
|
}
|
|
3834
|
-
onUpdateTitle(
|
|
3940
|
+
onUpdateTitle(instanceId, trimmedValue);
|
|
3835
3941
|
setIsEditing(false);
|
|
3836
3942
|
};
|
|
3837
3943
|
const handleCancel = () => {
|
|
@@ -3839,7 +3945,7 @@ const SortableChip = ({
|
|
|
3839
3945
|
setEditValue('');
|
|
3840
3946
|
};
|
|
3841
3947
|
const handleReset = () => {
|
|
3842
|
-
onResetTitle(
|
|
3948
|
+
onResetTitle(instanceId);
|
|
3843
3949
|
setIsEditing(false);
|
|
3844
3950
|
setEditValue('');
|
|
3845
3951
|
};
|
|
@@ -3874,21 +3980,9 @@ const SortableChip = ({
|
|
|
3874
3980
|
opacity: 1 // show icons on hover
|
|
3875
3981
|
}
|
|
3876
3982
|
}
|
|
3877
|
-
}, /*#__PURE__*/React__default.createElement(
|
|
3878
|
-
|
|
3879
|
-
|
|
3880
|
-
alignItems: "center",
|
|
3881
|
-
cursor: "grab",
|
|
3882
|
-
"&:active": {
|
|
3883
|
-
cursor: "grabbing"
|
|
3884
|
-
}
|
|
3885
|
-
}
|
|
3886
|
-
}), /*#__PURE__*/React__default.createElement(DragIndicatorIcon, {
|
|
3887
|
-
sx: {
|
|
3888
|
-
cursor: "grab",
|
|
3889
|
-
color: "rgba(110, 110, 110, 0.62)"
|
|
3890
|
-
}
|
|
3891
|
-
})), !isEditing ? /*#__PURE__*/React__default.createElement(React__default.Fragment, null, source && /*#__PURE__*/React__default.createElement(Box$1, {
|
|
3983
|
+
}, /*#__PURE__*/React__default.createElement(DragHandle, {
|
|
3984
|
+
listeners: listeners
|
|
3985
|
+
}), !isEditing ? /*#__PURE__*/React__default.createElement(React__default.Fragment, null, source && /*#__PURE__*/React__default.createElement(Box$1, {
|
|
3892
3986
|
sx: {
|
|
3893
3987
|
display: 'flex',
|
|
3894
3988
|
alignItems: 'center',
|
|
@@ -3950,29 +4044,12 @@ const SortableChip = ({
|
|
|
3950
4044
|
sx: {
|
|
3951
4045
|
flex: 1
|
|
3952
4046
|
}
|
|
3953
|
-
}), /*#__PURE__*/React__default.createElement(
|
|
3954
|
-
|
|
3955
|
-
|
|
3956
|
-
|
|
3957
|
-
|
|
3958
|
-
|
|
3959
|
-
transition: "opacity 0.2s"
|
|
3960
|
-
}
|
|
3961
|
-
}, /*#__PURE__*/React__default.createElement(IconButton$1, {
|
|
3962
|
-
size: "small",
|
|
3963
|
-
onClick: onMoveUp,
|
|
3964
|
-
disabled: isFirst,
|
|
3965
|
-
"aria-label": "move up"
|
|
3966
|
-
}, /*#__PURE__*/React__default.createElement(ArrowUpwardIcon, {
|
|
3967
|
-
fontSize: "small"
|
|
3968
|
-
})), /*#__PURE__*/React__default.createElement(IconButton$1, {
|
|
3969
|
-
size: "small",
|
|
3970
|
-
onClick: onMoveDown,
|
|
3971
|
-
disabled: isLast,
|
|
3972
|
-
"aria-label": "move down"
|
|
3973
|
-
}, /*#__PURE__*/React__default.createElement(ArrowDownwardIcon, {
|
|
3974
|
-
fontSize: "small"
|
|
3975
|
-
})))) : /*#__PURE__*/React__default.createElement(React__default.Fragment, null, /*#__PURE__*/React__default.createElement(TextField, {
|
|
4047
|
+
}), /*#__PURE__*/React__default.createElement(MoveButtons, {
|
|
4048
|
+
onMoveUp: onMoveUp,
|
|
4049
|
+
onMoveDown: onMoveDown,
|
|
4050
|
+
isFirst: isFirst,
|
|
4051
|
+
isLast: isLast
|
|
4052
|
+
})) : /*#__PURE__*/React__default.createElement(React__default.Fragment, null, /*#__PURE__*/React__default.createElement(TextField, {
|
|
3976
4053
|
inputRef: inputRef,
|
|
3977
4054
|
value: editValue,
|
|
3978
4055
|
onChange: e => setEditValue(e.target.value),
|
|
@@ -4047,39 +4124,14 @@ const Metrics = ({
|
|
|
4047
4124
|
const [selectedMetric, setSelectedMetric] = useState('');
|
|
4048
4125
|
|
|
4049
4126
|
// Setup drag and drop sensors
|
|
4050
|
-
const sensors =
|
|
4051
|
-
coordinateGetter: sortableKeyboardCoordinates
|
|
4052
|
-
}));
|
|
4053
|
-
|
|
4054
|
-
// Handle drag end
|
|
4055
|
-
const handleDragEnd = event => {
|
|
4056
|
-
const {
|
|
4057
|
-
active,
|
|
4058
|
-
over
|
|
4059
|
-
} = event;
|
|
4060
|
-
if (over && active.id !== over.id) {
|
|
4061
|
-
const oldIndex = savedMetrics.findIndex((_, i) => i === active.id);
|
|
4062
|
-
const newIndex = savedMetrics.findIndex((_, i) => i === over.id);
|
|
4063
|
-
const newOrder = arrayMove(savedMetrics, oldIndex, newIndex);
|
|
4064
|
-
onReorderMetrics(newOrder);
|
|
4065
|
-
}
|
|
4066
|
-
};
|
|
4127
|
+
const sensors = useSortableListSensors();
|
|
4067
4128
|
|
|
4068
|
-
// Handle move
|
|
4069
|
-
const
|
|
4070
|
-
|
|
4071
|
-
|
|
4072
|
-
|
|
4073
|
-
|
|
4074
|
-
};
|
|
4075
|
-
|
|
4076
|
-
// Handle move down
|
|
4077
|
-
const handleMoveDown = index => {
|
|
4078
|
-
if (index < savedMetrics.length - 1) {
|
|
4079
|
-
const newOrder = arrayMove(savedMetrics, index, index + 1);
|
|
4080
|
-
onReorderMetrics(newOrder);
|
|
4081
|
-
}
|
|
4082
|
-
};
|
|
4129
|
+
// Handle drag/move
|
|
4130
|
+
const {
|
|
4131
|
+
handleDragEnd,
|
|
4132
|
+
handleMoveUp,
|
|
4133
|
+
handleMoveDown
|
|
4134
|
+
} = makeListReorderHandlers(savedMetrics, onReorderMetrics);
|
|
4083
4135
|
|
|
4084
4136
|
// Get the current provider based on selection chain
|
|
4085
4137
|
const getCurrentProvider = () => {
|
|
@@ -4114,7 +4166,10 @@ const Metrics = ({
|
|
|
4114
4166
|
currentProviderKey = selection.targetKey;
|
|
4115
4167
|
});
|
|
4116
4168
|
|
|
4117
|
-
// Create a Set of already selected metric fullPaths for quick lookup
|
|
4169
|
+
// Create a Set of already selected metric fullPaths for quick lookup.
|
|
4170
|
+
// Already-selected items stay selectable (so the same metric can be
|
|
4171
|
+
// added again on purpose) - they're just flagged so the dropdown can
|
|
4172
|
+
// highlight them instead of blocking the click.
|
|
4118
4173
|
const selectedFullPaths = new Set(savedMetrics.map(metric => metric.fullPath));
|
|
4119
4174
|
|
|
4120
4175
|
// Metrics are a direct array, not nested by alias
|
|
@@ -4122,15 +4177,12 @@ const Metrics = ({
|
|
|
4122
4177
|
const items = provider.metrics.map((metric, index) => {
|
|
4123
4178
|
// Construct the fullPath for this metric
|
|
4124
4179
|
const fullPath = `${aliasPath.join('_')}.${metric.name}`;
|
|
4125
|
-
|
|
4126
|
-
// Check if this metric is already selected
|
|
4127
|
-
const isAlreadySelected = selectedFullPaths.has(fullPath);
|
|
4128
4180
|
return {
|
|
4129
4181
|
key: `${currentProvider}_${metric.name}_${index}`,
|
|
4130
4182
|
value: metric.title || metric.name,
|
|
4131
4183
|
metricName: metric.name,
|
|
4132
4184
|
metric: metric,
|
|
4133
|
-
|
|
4185
|
+
alreadySelected: selectedFullPaths.has(fullPath),
|
|
4134
4186
|
icon: metric.source ? /*#__PURE__*/React__default.createElement(MetricSourceIcon, {
|
|
4135
4187
|
source: metric.source
|
|
4136
4188
|
}) : undefined
|
|
@@ -4193,7 +4245,11 @@ const Metrics = ({
|
|
|
4193
4245
|
metricName: selectedItem.metricName,
|
|
4194
4246
|
metricTitle: selectedItem.value,
|
|
4195
4247
|
// The constructed path for server
|
|
4196
|
-
fullPath: fullPath
|
|
4248
|
+
fullPath: fullPath,
|
|
4249
|
+
// Unique per-instance identity, so this field can be added more
|
|
4250
|
+
// than once and each occurrence stays independently
|
|
4251
|
+
// orderable/removable/titleable.
|
|
4252
|
+
id: nextInstanceId(fullPath, savedMetrics)
|
|
4197
4253
|
};
|
|
4198
4254
|
onSaveMetric(metricData);
|
|
4199
4255
|
handleCancel();
|
|
@@ -4204,8 +4260,9 @@ const Metrics = ({
|
|
|
4204
4260
|
const handleAddAll = () => {
|
|
4205
4261
|
const metricItems = getMetricItems();
|
|
4206
4262
|
|
|
4207
|
-
// Filter out already selected metrics
|
|
4208
|
-
|
|
4263
|
+
// Filter out already selected metrics - "Add all" only fills in
|
|
4264
|
+
// missing ones; intentional duplicates are added one at a time above.
|
|
4265
|
+
const availableItems = metricItems.filter(item => !item.alreadySelected);
|
|
4209
4266
|
if (availableItems.length === 0) return;
|
|
4210
4267
|
|
|
4211
4268
|
// Build the complete relation objects array (same for all metrics in this provider)
|
|
@@ -4250,7 +4307,10 @@ const Metrics = ({
|
|
|
4250
4307
|
metricName: item.metricName,
|
|
4251
4308
|
metricTitle: item.value,
|
|
4252
4309
|
// The constructed path for server
|
|
4253
|
-
fullPath: fullPath
|
|
4310
|
+
fullPath: fullPath,
|
|
4311
|
+
// "Add all" only ever adds fields that aren't already
|
|
4312
|
+
// selected, so this is always the first occurrence.
|
|
4313
|
+
id: fullPath
|
|
4254
4314
|
};
|
|
4255
4315
|
onSaveMetric(metricData);
|
|
4256
4316
|
});
|
|
@@ -4349,7 +4409,7 @@ const Metrics = ({
|
|
|
4349
4409
|
placement: "top"
|
|
4350
4410
|
}, /*#__PURE__*/React__default.createElement("span", null, /*#__PURE__*/React__default.createElement(IconButton$1, {
|
|
4351
4411
|
onClick: handleAddAll,
|
|
4352
|
-
disabled: getMetricItems().filter(item => !item.
|
|
4412
|
+
disabled: getMetricItems().filter(item => !item.alreadySelected).length === 0,
|
|
4353
4413
|
"aria-label": "add all metrics",
|
|
4354
4414
|
sx: {
|
|
4355
4415
|
color: "rgb(70, 134, 128)",
|
|
@@ -4418,36 +4478,27 @@ const Metrics = ({
|
|
|
4418
4478
|
marginBottom: 1,
|
|
4419
4479
|
color: "rgb(37, 37, 37)"
|
|
4420
4480
|
}
|
|
4421
|
-
}, "Saved Metrics"), /*#__PURE__*/React__default.createElement(
|
|
4481
|
+
}, "Saved Metrics"), /*#__PURE__*/React__default.createElement(SortableListContext, {
|
|
4422
4482
|
sensors: sensors,
|
|
4423
|
-
|
|
4483
|
+
itemIds: savedMetrics.map((_, index) => index),
|
|
4424
4484
|
onDragEnd: handleDragEnd
|
|
4425
|
-
}, /*#__PURE__*/React__default.createElement(SortableContext, {
|
|
4426
|
-
items: savedMetrics.map((_, index) => index),
|
|
4427
|
-
strategy: verticalListSortingStrategy
|
|
4428
|
-
}, /*#__PURE__*/React__default.createElement(Box$1, {
|
|
4429
|
-
sx: {
|
|
4430
|
-
display: 'flex',
|
|
4431
|
-
flexDirection: 'column',
|
|
4432
|
-
gap: 1
|
|
4433
|
-
}
|
|
4434
4485
|
}, savedMetrics.map((metric, index) => /*#__PURE__*/React__default.createElement(SortableChip, {
|
|
4435
4486
|
key: index,
|
|
4436
4487
|
id: index,
|
|
4437
4488
|
label: metric.metricTitle,
|
|
4438
4489
|
fullLabel: `${formatProviderPath(metric)} → ${metric.metricTitle} (${metric.fullPath})`,
|
|
4439
|
-
onDelete: () => onRemoveMetric(
|
|
4490
|
+
onDelete: () => onRemoveMetric(metric.id),
|
|
4440
4491
|
onMoveUp: () => handleMoveUp(index),
|
|
4441
4492
|
onMoveDown: () => handleMoveDown(index),
|
|
4442
4493
|
isFirst: index === 0,
|
|
4443
4494
|
isLast: index === savedMetrics.length - 1,
|
|
4444
|
-
|
|
4495
|
+
instanceId: metric.id,
|
|
4445
4496
|
defaultTitle: metric.metricTitle,
|
|
4446
|
-
customTitle: titleOverrides[metric.
|
|
4497
|
+
customTitle: titleOverrides[metric.id],
|
|
4447
4498
|
onUpdateTitle: onUpdateTitle,
|
|
4448
4499
|
onResetTitle: onResetTitle,
|
|
4449
4500
|
source: metric.metric?.source
|
|
4450
|
-
})))))
|
|
4501
|
+
})))));
|
|
4451
4502
|
};
|
|
4452
4503
|
|
|
4453
4504
|
const Filters = ({
|
|
@@ -5280,7 +5331,12 @@ const Filters = ({
|
|
|
5280
5331
|
mb: 3
|
|
5281
5332
|
}
|
|
5282
5333
|
}, /*#__PURE__*/React__default.createElement(SingleSelect, {
|
|
5283
|
-
items:
|
|
5334
|
+
items: Array.from(
|
|
5335
|
+
// Dimensions can now be added more than once (same fullPath,
|
|
5336
|
+
// different instance), but a filter targets the underlying
|
|
5337
|
+
// field regardless of how many columns display it - so this
|
|
5338
|
+
// list is deduped by fullPath to avoid showing it twice.
|
|
5339
|
+
new Map(existingDimensions.map(dim => [dim.fullPath, dim])).values()).map(dim => ({
|
|
5284
5340
|
key: dim.fullPath,
|
|
5285
5341
|
value: interpolateTitle(dimensionTitleOverrides[dim.fullPath] || dim.dimensionTitle, parameters)
|
|
5286
5342
|
})).sort((a, b) => a.value.localeCompare(b.value)),
|
|
@@ -5674,6 +5730,205 @@ const Filters = ({
|
|
|
5674
5730
|
}))));
|
|
5675
5731
|
};
|
|
5676
5732
|
|
|
5733
|
+
const KIND_LABEL = {
|
|
5734
|
+
dimension: "Dimension",
|
|
5735
|
+
computed: "Computed",
|
|
5736
|
+
metric: "Metric"
|
|
5737
|
+
};
|
|
5738
|
+
const KindChip = ({
|
|
5739
|
+
kind,
|
|
5740
|
+
source
|
|
5741
|
+
}) => {
|
|
5742
|
+
if (kind === "metric") {
|
|
5743
|
+
return /*#__PURE__*/React__default.createElement(Box$1, {
|
|
5744
|
+
sx: {
|
|
5745
|
+
display: "flex",
|
|
5746
|
+
alignItems: "center",
|
|
5747
|
+
flexShrink: 0
|
|
5748
|
+
}
|
|
5749
|
+
}, /*#__PURE__*/React__default.createElement(MetricSourceIcon, {
|
|
5750
|
+
source: source
|
|
5751
|
+
}));
|
|
5752
|
+
}
|
|
5753
|
+
return /*#__PURE__*/React__default.createElement(Chip, {
|
|
5754
|
+
icon: kind === "computed" ? /*#__PURE__*/React__default.createElement(CalculateIcon, {
|
|
5755
|
+
sx: {
|
|
5756
|
+
fontSize: "15px !important"
|
|
5757
|
+
}
|
|
5758
|
+
}) : undefined,
|
|
5759
|
+
label: KIND_LABEL[kind],
|
|
5760
|
+
size: "small",
|
|
5761
|
+
sx: {
|
|
5762
|
+
fontWeight: 500,
|
|
5763
|
+
backgroundColor: "rgba(70, 134, 128, 0.1)",
|
|
5764
|
+
color: "rgb(70, 134, 128)",
|
|
5765
|
+
flexShrink: 0
|
|
5766
|
+
}
|
|
5767
|
+
});
|
|
5768
|
+
};
|
|
5769
|
+
|
|
5770
|
+
// Read-only sortable row: this tab only reorders columns. Adding, removing,
|
|
5771
|
+
// and renaming stay on the Dimensions/Metrics tabs so there's one place each
|
|
5772
|
+
// field is managed.
|
|
5773
|
+
const SortableColumnRow = ({
|
|
5774
|
+
id,
|
|
5775
|
+
label,
|
|
5776
|
+
fullPath,
|
|
5777
|
+
kind,
|
|
5778
|
+
source,
|
|
5779
|
+
onMoveUp,
|
|
5780
|
+
onMoveDown,
|
|
5781
|
+
isFirst,
|
|
5782
|
+
isLast
|
|
5783
|
+
}) => {
|
|
5784
|
+
const reportingContext = useReportingContextOptional();
|
|
5785
|
+
const parameters = reportingContext?.parameters;
|
|
5786
|
+
const {
|
|
5787
|
+
attributes,
|
|
5788
|
+
listeners,
|
|
5789
|
+
setNodeRef,
|
|
5790
|
+
style
|
|
5791
|
+
} = useSortableRow(id);
|
|
5792
|
+
const displayLabel = interpolateTitle(label, parameters);
|
|
5793
|
+
return /*#__PURE__*/React__default.createElement("div", _extends({
|
|
5794
|
+
ref: setNodeRef,
|
|
5795
|
+
style: style
|
|
5796
|
+
}, attributes), /*#__PURE__*/React__default.createElement(Box$1, {
|
|
5797
|
+
sx: {
|
|
5798
|
+
display: "flex",
|
|
5799
|
+
alignItems: "center",
|
|
5800
|
+
width: "100%",
|
|
5801
|
+
gap: 1,
|
|
5802
|
+
backgroundColor: "white",
|
|
5803
|
+
border: "1px solid #e0e0e0",
|
|
5804
|
+
borderRadius: 2,
|
|
5805
|
+
padding: 1,
|
|
5806
|
+
transition: "transform 0.2s ease, box-shadow 0.2s ease",
|
|
5807
|
+
"&:hover .hover-icons": {
|
|
5808
|
+
opacity: 1
|
|
5809
|
+
}
|
|
5810
|
+
}
|
|
5811
|
+
}, /*#__PURE__*/React__default.createElement(DragHandle, {
|
|
5812
|
+
listeners: listeners
|
|
5813
|
+
}), /*#__PURE__*/React__default.createElement(KindChip, {
|
|
5814
|
+
kind: kind,
|
|
5815
|
+
source: source
|
|
5816
|
+
}), /*#__PURE__*/React__default.createElement(Tooltip$1, {
|
|
5817
|
+
title: fullPath,
|
|
5818
|
+
arrow: true,
|
|
5819
|
+
placement: "top"
|
|
5820
|
+
}, /*#__PURE__*/React__default.createElement(Typography$1, {
|
|
5821
|
+
variant: "h6",
|
|
5822
|
+
sx: {
|
|
5823
|
+
fontWeight: 500,
|
|
5824
|
+
color: "#1a1a1a",
|
|
5825
|
+
fontSize: "14px",
|
|
5826
|
+
whiteSpace: "nowrap",
|
|
5827
|
+
overflow: "hidden",
|
|
5828
|
+
textOverflow: "ellipsis"
|
|
5829
|
+
}
|
|
5830
|
+
}, displayLabel)), /*#__PURE__*/React__default.createElement(Box$1, {
|
|
5831
|
+
sx: {
|
|
5832
|
+
flex: 1
|
|
5833
|
+
}
|
|
5834
|
+
}), /*#__PURE__*/React__default.createElement(MoveButtons, {
|
|
5835
|
+
onMoveUp: onMoveUp,
|
|
5836
|
+
onMoveDown: onMoveDown,
|
|
5837
|
+
isFirst: isFirst,
|
|
5838
|
+
isLast: isLast
|
|
5839
|
+
})));
|
|
5840
|
+
};
|
|
5841
|
+
const ColumnOrder = ({
|
|
5842
|
+
orderedDimensionItems = [],
|
|
5843
|
+
metrics = [],
|
|
5844
|
+
columnOrder = [],
|
|
5845
|
+
onReorderColumnItems,
|
|
5846
|
+
titleOverrides = {
|
|
5847
|
+
dimensions: {},
|
|
5848
|
+
metrics: {}
|
|
5849
|
+
}
|
|
5850
|
+
}) => {
|
|
5851
|
+
const isCustomized = columnOrder.length > 0;
|
|
5852
|
+
const resolvedItems = resolveColumnOrder(columnOrder, orderedDimensionItems, metrics);
|
|
5853
|
+
const sensors = useSortableListSensors();
|
|
5854
|
+
const reorderResolved = newResolvedItems => {
|
|
5855
|
+
onReorderColumnItems(newResolvedItems.map(item => ({
|
|
5856
|
+
type: item.kind,
|
|
5857
|
+
key: item.key
|
|
5858
|
+
})));
|
|
5859
|
+
};
|
|
5860
|
+
const {
|
|
5861
|
+
handleDragEnd,
|
|
5862
|
+
handleMoveUp,
|
|
5863
|
+
handleMoveDown
|
|
5864
|
+
} = makeListReorderHandlers(resolvedItems, reorderResolved);
|
|
5865
|
+
const labelFor = item => {
|
|
5866
|
+
if (item.kind === "computed") return item.data.name;
|
|
5867
|
+
if (item.kind === "metric") {
|
|
5868
|
+
return titleOverrides.metrics[item.key] || item.data.metricTitle || item.key;
|
|
5869
|
+
}
|
|
5870
|
+
return titleOverrides.dimensions[item.key] || item.data.dimensionTitle || item.key;
|
|
5871
|
+
};
|
|
5872
|
+
return /*#__PURE__*/React__default.createElement("div", null, /*#__PURE__*/React__default.createElement(Box$1, {
|
|
5873
|
+
sx: {
|
|
5874
|
+
display: "flex",
|
|
5875
|
+
justifyContent: "space-between",
|
|
5876
|
+
alignItems: "flex-start",
|
|
5877
|
+
mb: 2,
|
|
5878
|
+
gap: 2
|
|
5879
|
+
}
|
|
5880
|
+
}, /*#__PURE__*/React__default.createElement(Typography$1, {
|
|
5881
|
+
sx: {
|
|
5882
|
+
fontSize: "13px",
|
|
5883
|
+
color: "#666"
|
|
5884
|
+
}
|
|
5885
|
+
}, "Optional: define a single order for all columns (dimensions, computed dimensions and metrics together) in the results grid. Drag to reorder or use the arrows. Newly added dimensions/metrics are appended to the end automatically. Without a custom order, columns follow the Dimensions tab order, then the Metrics tab order."), isCustomized && /*#__PURE__*/React__default.createElement(Tooltip$1, {
|
|
5886
|
+
title: "Discard the custom order and go back to the default (Dimensions, then Metrics)",
|
|
5887
|
+
arrow: true
|
|
5888
|
+
}, /*#__PURE__*/React__default.createElement(Button, {
|
|
5889
|
+
variant: "outlined",
|
|
5890
|
+
size: "small",
|
|
5891
|
+
startIcon: /*#__PURE__*/React__default.createElement(RestartAltIcon, null),
|
|
5892
|
+
onClick: () => onReorderColumnItems([]),
|
|
5893
|
+
sx: {
|
|
5894
|
+
height: "36px",
|
|
5895
|
+
flexShrink: 0,
|
|
5896
|
+
fontFamily: "system-ui",
|
|
5897
|
+
fontSize: "13px",
|
|
5898
|
+
fontWeight: 500,
|
|
5899
|
+
borderRadius: "8px",
|
|
5900
|
+
boxShadow: "none",
|
|
5901
|
+
textTransform: "none",
|
|
5902
|
+
borderColor: "#e0e0e0",
|
|
5903
|
+
color: "rgb(37, 37, 37)",
|
|
5904
|
+
"&:hover": {
|
|
5905
|
+
backgroundColor: "#f5f5f5",
|
|
5906
|
+
borderColor: "#d0d0d0"
|
|
5907
|
+
}
|
|
5908
|
+
}
|
|
5909
|
+
}, "Reset to Default"))), resolvedItems.length === 0 ? /*#__PURE__*/React__default.createElement(Typography$1, {
|
|
5910
|
+
sx: {
|
|
5911
|
+
fontSize: "13px",
|
|
5912
|
+
color: "#999"
|
|
5913
|
+
}
|
|
5914
|
+
}, "Add dimensions or metrics in the other tabs first.") : /*#__PURE__*/React__default.createElement(SortableListContext, {
|
|
5915
|
+
sensors: sensors,
|
|
5916
|
+
itemIds: resolvedItems.map((_, index) => index),
|
|
5917
|
+
onDragEnd: handleDragEnd
|
|
5918
|
+
}, resolvedItems.map((item, index) => /*#__PURE__*/React__default.createElement(SortableColumnRow, {
|
|
5919
|
+
key: `${item.kind}-${item.key}`,
|
|
5920
|
+
id: index,
|
|
5921
|
+
label: labelFor(item),
|
|
5922
|
+
fullPath: item.kind === "computed" ? item.data.value : item.data.fullPath,
|
|
5923
|
+
kind: item.kind,
|
|
5924
|
+
source: item.kind === "metric" ? item.data.metric?.source : undefined,
|
|
5925
|
+
onMoveUp: () => handleMoveUp(index),
|
|
5926
|
+
onMoveDown: () => handleMoveDown(index),
|
|
5927
|
+
isFirst: index === 0,
|
|
5928
|
+
isLast: index === resolvedItems.length - 1
|
|
5929
|
+
}))));
|
|
5930
|
+
};
|
|
5931
|
+
|
|
5677
5932
|
// Default numeral.js formats applied when a dimension/metric definition has a
|
|
5678
5933
|
// known type but no explicit `format` set on the provider.
|
|
5679
5934
|
const DEFAULT_FORMATS_BY_TYPE = {
|
|
@@ -5688,10 +5943,122 @@ const formatValue = (value, def) => {
|
|
|
5688
5943
|
return def?.prefix ? `${def.prefix} ${formatted}` : formatted;
|
|
5689
5944
|
};
|
|
5690
5945
|
const isNumericType = type => type === 'integer' || type === 'currency';
|
|
5946
|
+
|
|
5947
|
+
// Builds a single DataGrid column definition for one resolved column item
|
|
5948
|
+
// (a dimension, computed dimension, or metric). The DataGrid `field` is the
|
|
5949
|
+
// item's instance id (so two duplicate instances of the same underlying
|
|
5950
|
+
// dimension/metric still get distinct columns); the actual cell value is
|
|
5951
|
+
// read via `valueGetter` from `dataFieldName`, the key the API response
|
|
5952
|
+
// actually uses - shared by every duplicate instance, since the backend
|
|
5953
|
+
// only ever returns one value per fullPath. dataFieldName rules:
|
|
5954
|
+
// - Dimensions from base provider: baseAlias.field -> baseAlias_field
|
|
5955
|
+
// - Dimensions from nested provider: baseAlias_rel1_rel2.field -> rel1_rel2_field (drops base alias)
|
|
5956
|
+
// - Computed dimensions have no provider/relations chain, so the API returns
|
|
5957
|
+
// each row keyed by the computed dimension's own `name` (and can't be
|
|
5958
|
+
// duplicated - names must be unique - so field/dataFieldName coincide).
|
|
5959
|
+
// - Metrics: API returns metric keys in different forms depending on path depth
|
|
5960
|
+
// (see comment inline below), normalised to match the row-key normalisation
|
|
5961
|
+
// done when building `rows`.
|
|
5962
|
+
const buildColumn = (item, titleOverrides, parameters) => {
|
|
5963
|
+
if (item.kind === 'computed') {
|
|
5964
|
+
const cd = item.data;
|
|
5965
|
+
return {
|
|
5966
|
+
field: cd.name,
|
|
5967
|
+
headerName: cd.name,
|
|
5968
|
+
flex: 1,
|
|
5969
|
+
minWidth: 150,
|
|
5970
|
+
type: 'string'
|
|
5971
|
+
};
|
|
5972
|
+
}
|
|
5973
|
+
if (item.kind === 'metric') {
|
|
5974
|
+
const metric = item.data;
|
|
5975
|
+
const metricDef = metric.metric;
|
|
5976
|
+
let dataFieldName;
|
|
5977
|
+
const dotCount = (metric.fullPath.match(/\./g) || []).length;
|
|
5978
|
+
if (dotCount >= 2) {
|
|
5979
|
+
// 3+-part namespaced path: API returns the full dotted path as the key.
|
|
5980
|
+
// Normalise dots to underscores to match the row key normalisation below.
|
|
5981
|
+
// Example: "mc.dkpi.activeAgreementsCount" -> "mc_dkpi_activeAgreementsCount"
|
|
5982
|
+
dataFieldName = metric.fullPath.replace(/\./g, '_');
|
|
5983
|
+
} else if (metric.relations && metric.relations.length > 0) {
|
|
5984
|
+
// 2-part path from a nested provider: API drops the base provider alias.
|
|
5985
|
+
// Example: "mc_fa.total_amount" -> "fa_total_amount"
|
|
5986
|
+
const parts = metric.fullPath.split('.');
|
|
5987
|
+
const pathWithoutField = parts[0]; // e.g., "mc_fa"
|
|
5988
|
+
const field = parts[1]; // e.g., "total_amount"
|
|
5989
|
+
|
|
5990
|
+
const pathParts = pathWithoutField.split('_');
|
|
5991
|
+
pathParts.shift(); // remove base provider alias
|
|
5992
|
+
const pathWithoutBase = pathParts.join('_');
|
|
5993
|
+
dataFieldName = pathWithoutBase ? `${pathWithoutBase}_${field}` : field;
|
|
5994
|
+
} else {
|
|
5995
|
+
// 2-part path from the base provider: API returns just the metric name.
|
|
5996
|
+
// Example: "mc.record_count" -> "record_count"
|
|
5997
|
+
dataFieldName = metric.metricName;
|
|
5998
|
+
}
|
|
5999
|
+
const headerName = interpolateTitle(titleOverrides.metrics[metric.id] || metric.metricTitle || dataFieldName, parameters);
|
|
6000
|
+
return {
|
|
6001
|
+
// `field` is the instance id (unique even for duplicate columns
|
|
6002
|
+
// pointing at the same underlying metric); the actual value is
|
|
6003
|
+
// read from the row via valueGetter using dataFieldName, which is
|
|
6004
|
+
// the key the API response actually uses (shared across duplicates).
|
|
6005
|
+
field: metric.id.replace(/[.#]/g, '_'),
|
|
6006
|
+
headerName: headerName,
|
|
6007
|
+
flex: 1,
|
|
6008
|
+
minWidth: 150,
|
|
6009
|
+
type: isNumericType(metricDef?.type) ? 'number' : 'string',
|
|
6010
|
+
valueGetter: (_value, row) => row[dataFieldName],
|
|
6011
|
+
renderHeader: () => /*#__PURE__*/React__default.createElement(Box$1, {
|
|
6012
|
+
sx: {
|
|
6013
|
+
display: 'flex',
|
|
6014
|
+
alignItems: 'center',
|
|
6015
|
+
gap: 0.5
|
|
6016
|
+
}
|
|
6017
|
+
}, /*#__PURE__*/React__default.createElement(MetricSourceIcon, {
|
|
6018
|
+
source: metricDef?.source
|
|
6019
|
+
}), /*#__PURE__*/React__default.createElement("span", null, headerName)),
|
|
6020
|
+
valueFormatter: value => formatValue(value, metricDef)
|
|
6021
|
+
};
|
|
6022
|
+
}
|
|
6023
|
+
|
|
6024
|
+
// Dimension
|
|
6025
|
+
const dim = item.data;
|
|
6026
|
+
let dataFieldName;
|
|
6027
|
+
if (dim.relations && dim.relations.length > 0) {
|
|
6028
|
+
// From nested provider: drop the base provider alias
|
|
6029
|
+
// Example: ft_fa_db.currency -> fa_db_currency
|
|
6030
|
+
const parts = dim.fullPath.split('.');
|
|
6031
|
+
const pathWithoutField = parts[0]; // e.g., "ft_fa_db"
|
|
6032
|
+
const field = parts[1]; // e.g., "currency"
|
|
6033
|
+
|
|
6034
|
+
const pathParts = pathWithoutField.split('_');
|
|
6035
|
+
pathParts.shift(); // Remove base provider alias
|
|
6036
|
+
const pathWithoutBase = pathParts.join('_'); // e.g., "fa_db"
|
|
6037
|
+
|
|
6038
|
+
dataFieldName = pathWithoutBase ? `${pathWithoutBase}_${field}` : field;
|
|
6039
|
+
} else {
|
|
6040
|
+
// From base provider: keep the full path with underscore
|
|
6041
|
+
// Example: ba.created_at -> ba_created_at
|
|
6042
|
+
dataFieldName = dim.fullPath.replace('.', '_');
|
|
6043
|
+
}
|
|
6044
|
+
const headerName = interpolateTitle(titleOverrides.dimensions[dim.id] || dim.dimensionTitle || dataFieldName, parameters);
|
|
6045
|
+
const dimDef = dim.dimension;
|
|
6046
|
+
return {
|
|
6047
|
+
// Same rationale as the metric branch above: `field` must be unique
|
|
6048
|
+
// per instance, while the value always comes from the one real
|
|
6049
|
+
// underlying row key (dataFieldName), shared by every duplicate.
|
|
6050
|
+
field: dim.id.replace(/[.#]/g, '_'),
|
|
6051
|
+
headerName: headerName,
|
|
6052
|
+
flex: 1,
|
|
6053
|
+
minWidth: 150,
|
|
6054
|
+
type: isNumericType(dimDef?.type) ? 'number' : 'string',
|
|
6055
|
+
valueGetter: (_value, row) => row[dataFieldName],
|
|
6056
|
+
valueFormatter: value => formatValue(value, dimDef)
|
|
6057
|
+
};
|
|
6058
|
+
};
|
|
5691
6059
|
const ReportDataGrid = ({
|
|
5692
6060
|
reportData,
|
|
5693
|
-
|
|
5694
|
-
metrics,
|
|
6061
|
+
columnItems = [],
|
|
5695
6062
|
loading,
|
|
5696
6063
|
onPageChange,
|
|
5697
6064
|
totalRows = 0,
|
|
@@ -5707,127 +6074,10 @@ const ReportDataGrid = ({
|
|
|
5707
6074
|
pageSize: 50
|
|
5708
6075
|
});
|
|
5709
6076
|
|
|
5710
|
-
// Generate columns dynamically
|
|
5711
|
-
|
|
5712
|
-
|
|
5713
|
-
|
|
5714
|
-
// Add dimension + computed-dimension columns, in the single unified
|
|
5715
|
-
// order the user arranged them in on the Dimensions tab.
|
|
5716
|
-
// Field naming logic (dimensions):
|
|
5717
|
-
// - If from base provider: baseAlias.field -> baseAlias_field
|
|
5718
|
-
// - If from nested provider: baseAlias_rel1_rel2.field -> rel1_rel2_field (drops base alias)
|
|
5719
|
-
// Computed dimensions have no provider/relations chain, so the API
|
|
5720
|
-
// returns each row keyed by the computed dimension's own `name`.
|
|
5721
|
-
orderedDimensionItems.forEach(item => {
|
|
5722
|
-
if (item.kind === 'computed') {
|
|
5723
|
-
const cd = item.data;
|
|
5724
|
-
cols.push({
|
|
5725
|
-
field: cd.name,
|
|
5726
|
-
headerName: cd.name,
|
|
5727
|
-
flex: 1,
|
|
5728
|
-
minWidth: 150,
|
|
5729
|
-
type: 'string'
|
|
5730
|
-
});
|
|
5731
|
-
return;
|
|
5732
|
-
}
|
|
5733
|
-
const dim = item.data;
|
|
5734
|
-
let fieldName;
|
|
5735
|
-
let headerName;
|
|
5736
|
-
|
|
5737
|
-
// Check if there are relations (nested providers)
|
|
5738
|
-
if (dim.relations && dim.relations.length > 0) {
|
|
5739
|
-
// From nested provider: drop the base provider alias
|
|
5740
|
-
// Example: ft_fa_db.currency -> fa_db_currency
|
|
5741
|
-
const parts = dim.fullPath.split('.');
|
|
5742
|
-
const pathWithoutField = parts[0]; // e.g., "ft_fa_db"
|
|
5743
|
-
const field = parts[1]; // e.g., "currency"
|
|
5744
|
-
|
|
5745
|
-
// Remove the base provider alias (first part before first underscore)
|
|
5746
|
-
const pathParts = pathWithoutField.split('_');
|
|
5747
|
-
pathParts.shift(); // Remove base provider alias
|
|
5748
|
-
const pathWithoutBase = pathParts.join('_'); // e.g., "fa_db"
|
|
5749
|
-
|
|
5750
|
-
fieldName = pathWithoutBase ? `${pathWithoutBase}_${field}` : field;
|
|
5751
|
-
} else {
|
|
5752
|
-
// From base provider: keep the full path with underscore
|
|
5753
|
-
// Example: ba.created_at -> ba_created_at
|
|
5754
|
-
fieldName = dim.fullPath.replace('.', '_');
|
|
5755
|
-
}
|
|
5756
|
-
|
|
5757
|
-
// Check for title override, otherwise use the friendly dimension title from provider
|
|
5758
|
-
headerName = titleOverrides.dimensions[dim.fullPath] || dim.dimensionTitle || fieldName;
|
|
5759
|
-
headerName = interpolateTitle(headerName, parameters);
|
|
5760
|
-
const dimDef = dim.dimension;
|
|
5761
|
-
cols.push({
|
|
5762
|
-
field: fieldName,
|
|
5763
|
-
headerName: headerName,
|
|
5764
|
-
flex: 1,
|
|
5765
|
-
minWidth: 150,
|
|
5766
|
-
type: isNumericType(dimDef?.type) ? 'number' : 'string',
|
|
5767
|
-
valueFormatter: value => formatValue(value, dimDef)
|
|
5768
|
-
});
|
|
5769
|
-
});
|
|
5770
|
-
|
|
5771
|
-
// Add metric columns
|
|
5772
|
-
// The API returns metric keys in two different forms depending on the path depth:
|
|
5773
|
-
//
|
|
5774
|
-
// • 2-part path "mc.record_count" → API key: "record_count"
|
|
5775
|
-
// • 2-part path "mc_fa.total_amount" (nested) → API key: "fa_total_amount"
|
|
5776
|
-
// • 3+-part path "mc.dkpi.activeAgreementsCount"→ API key: "mc.dkpi.activeAgreementsCount" (dots)
|
|
5777
|
-
//
|
|
5778
|
-
// For 3+-part paths the API key contains dots. MUI DataGrid interprets dots
|
|
5779
|
-
// in field names as nested-object accessors, so the rows useMemo normalises
|
|
5780
|
-
// those keys (dots → underscores). The column field must match that normalised key.
|
|
5781
|
-
metrics.forEach(metric => {
|
|
5782
|
-
const metricDef = metric.metric;
|
|
5783
|
-
let fieldName;
|
|
5784
|
-
let headerName;
|
|
5785
|
-
const dotCount = (metric.fullPath.match(/\./g) || []).length;
|
|
5786
|
-
if (dotCount >= 2) {
|
|
5787
|
-
// 3+-part namespaced path: API returns the full dotted path as the key.
|
|
5788
|
-
// Normalise dots to underscores to match the row key normalisation below.
|
|
5789
|
-
// Example: "mc.dkpi.activeAgreementsCount" -> "mc_dkpi_activeAgreementsCount"
|
|
5790
|
-
fieldName = metric.fullPath.replace(/\./g, '_');
|
|
5791
|
-
} else if (metric.relations && metric.relations.length > 0) {
|
|
5792
|
-
// 2-part path from a nested provider: API drops the base provider alias.
|
|
5793
|
-
// Example: "mc_fa.total_amount" -> "fa_total_amount"
|
|
5794
|
-
const parts = metric.fullPath.split('.');
|
|
5795
|
-
const pathWithoutField = parts[0]; // e.g., "mc_fa"
|
|
5796
|
-
const field = parts[1]; // e.g., "total_amount"
|
|
5797
|
-
|
|
5798
|
-
const pathParts = pathWithoutField.split('_');
|
|
5799
|
-
pathParts.shift(); // remove base provider alias
|
|
5800
|
-
const pathWithoutBase = pathParts.join('_');
|
|
5801
|
-
fieldName = pathWithoutBase ? `${pathWithoutBase}_${field}` : field;
|
|
5802
|
-
} else {
|
|
5803
|
-
// 2-part path from the base provider: API returns just the metric name.
|
|
5804
|
-
// Example: "mc.record_count" -> "record_count"
|
|
5805
|
-
fieldName = metric.metricName;
|
|
5806
|
-
}
|
|
5807
|
-
|
|
5808
|
-
// Check for title override, otherwise use the friendly metric title from provider
|
|
5809
|
-
headerName = titleOverrides.metrics[metric.fullPath] || metric.metricTitle || fieldName;
|
|
5810
|
-
headerName = interpolateTitle(headerName, parameters);
|
|
5811
|
-
cols.push({
|
|
5812
|
-
field: fieldName,
|
|
5813
|
-
headerName: headerName,
|
|
5814
|
-
flex: 1,
|
|
5815
|
-
minWidth: 150,
|
|
5816
|
-
type: isNumericType(metricDef?.type) ? 'number' : 'string',
|
|
5817
|
-
renderHeader: () => /*#__PURE__*/React__default.createElement(Box$1, {
|
|
5818
|
-
sx: {
|
|
5819
|
-
display: 'flex',
|
|
5820
|
-
alignItems: 'center',
|
|
5821
|
-
gap: 0.5
|
|
5822
|
-
}
|
|
5823
|
-
}, /*#__PURE__*/React__default.createElement(MetricSourceIcon, {
|
|
5824
|
-
source: metricDef?.source
|
|
5825
|
-
}), /*#__PURE__*/React__default.createElement("span", null, headerName)),
|
|
5826
|
-
valueFormatter: value => formatValue(value, metricDef)
|
|
5827
|
-
});
|
|
5828
|
-
});
|
|
5829
|
-
return cols;
|
|
5830
|
-
}, [orderedDimensionItems, metrics, titleOverrides, parameters]);
|
|
6077
|
+
// Generate columns dynamically, in the final resolved column order
|
|
6078
|
+
// (custom Column Order tab arrangement when defined, otherwise
|
|
6079
|
+
// dimensions/computed then metrics).
|
|
6080
|
+
const columns = React__default.useMemo(() => columnItems.map(item => buildColumn(item, titleOverrides, parameters)), [columnItems, titleOverrides, parameters]);
|
|
5831
6081
|
|
|
5832
6082
|
// Transform report data to rows with unique IDs.
|
|
5833
6083
|
// Keys are normalised by replacing dots with underscores so that MUI DataGrid
|
|
@@ -5937,7 +6187,12 @@ const ReportBuilder = ({
|
|
|
5937
6187
|
computedDimensions: [],
|
|
5938
6188
|
// Unified display/execution order across dimensions + computedDimensions.
|
|
5939
6189
|
// Entries: { type: 'dimension', key: fullPath } | { type: 'computed', key: name }
|
|
5940
|
-
dimensionOrder: []
|
|
6190
|
+
dimensionOrder: [],
|
|
6191
|
+
// Optional unified display order across dimensions + computedDimensions +
|
|
6192
|
+
// metrics, set on the Column Order tab. Empty means "not defined" — the
|
|
6193
|
+
// results grid falls back to dimensionOrder followed by metrics order.
|
|
6194
|
+
// Entries: { type: 'dimension' | 'computed' | 'metric', key }
|
|
6195
|
+
columnOrder: []
|
|
5941
6196
|
});
|
|
5942
6197
|
const [titleOverrides, setTitleOverrides] = useState({
|
|
5943
6198
|
dimensions: {},
|
|
@@ -6123,6 +6378,72 @@ const ReportBuilder = ({
|
|
|
6123
6378
|
}
|
|
6124
6379
|
};
|
|
6125
6380
|
|
|
6381
|
+
// Reconstructs saved dimensions as independent, individually-identified
|
|
6382
|
+
// instances so that deliberate duplicates (the same field added more than
|
|
6383
|
+
// once) survive a save/reload round trip. Prefers `ordered_dimensions`
|
|
6384
|
+
// (one ref per instance - the nth duplicate of a path is suffixed
|
|
6385
|
+
// `#n`) when present; falls back to one instance per entry in the
|
|
6386
|
+
// (necessarily duplicate-free) `dimensions` path list for reports saved
|
|
6387
|
+
// before this field existed.
|
|
6388
|
+
const reconstructDimensions = (reportDef, providersData) => {
|
|
6389
|
+
const orderByMap = {};
|
|
6390
|
+
if (reportDef.definition?.doc?.query?.order_by) {
|
|
6391
|
+
for (const orderItem of reportDef.definition.doc.query.order_by) {
|
|
6392
|
+
orderByMap[orderItem.name] = orderItem.desc === true ? "desc" : "asc";
|
|
6393
|
+
}
|
|
6394
|
+
}
|
|
6395
|
+
const reconstructed = [];
|
|
6396
|
+
const orderedRefs = (reportDef.definition?.doc?.query?.ordered_dimensions || []).filter(entry => entry.type === "dimension").map(entry => entry.ref);
|
|
6397
|
+
if (orderedRefs.length > 0) {
|
|
6398
|
+
orderedRefs.forEach(ref => {
|
|
6399
|
+
const fullPath = ref.split("#")[0];
|
|
6400
|
+
const dim = reconstructDimensionFromPath(fullPath, providersData, reportDef.provider);
|
|
6401
|
+
if (dim) {
|
|
6402
|
+
dim.id = ref;
|
|
6403
|
+
dim.sortOrder = orderByMap[fullPath] || null;
|
|
6404
|
+
reconstructed.push(dim);
|
|
6405
|
+
}
|
|
6406
|
+
});
|
|
6407
|
+
} else if (reportDef.definition?.doc?.query?.dimensions) {
|
|
6408
|
+
for (const dimPath of reportDef.definition.doc.query.dimensions) {
|
|
6409
|
+
const dim = reconstructDimensionFromPath(dimPath, providersData, reportDef.provider);
|
|
6410
|
+
if (dim) {
|
|
6411
|
+
dim.id = dimPath;
|
|
6412
|
+
dim.sortOrder = orderByMap[dimPath] || null;
|
|
6413
|
+
reconstructed.push(dim);
|
|
6414
|
+
}
|
|
6415
|
+
}
|
|
6416
|
+
}
|
|
6417
|
+
return reconstructed;
|
|
6418
|
+
};
|
|
6419
|
+
|
|
6420
|
+
// Mirrors reconstructDimensions for metrics, using the analogous
|
|
6421
|
+
// `ordered_metrics` field (metrics have no other unified-order concept to
|
|
6422
|
+
// fall back on, so this is always written whenever there are metrics).
|
|
6423
|
+
const reconstructMetrics = (reportDef, providersData) => {
|
|
6424
|
+
const reconstructed = [];
|
|
6425
|
+
const orderedRefs = reportDef.definition?.doc?.query?.ordered_metrics || [];
|
|
6426
|
+
if (orderedRefs.length > 0) {
|
|
6427
|
+
orderedRefs.forEach(ref => {
|
|
6428
|
+
const fullPath = ref.split("#")[0];
|
|
6429
|
+
const metric = reconstructMetricFromPath(fullPath, providersData, reportDef.provider);
|
|
6430
|
+
if (metric) {
|
|
6431
|
+
metric.id = ref;
|
|
6432
|
+
reconstructed.push(metric);
|
|
6433
|
+
}
|
|
6434
|
+
});
|
|
6435
|
+
} else if (reportDef.definition?.doc?.query?.metrics) {
|
|
6436
|
+
for (const metricPath of reportDef.definition.doc.query.metrics) {
|
|
6437
|
+
const metric = reconstructMetricFromPath(metricPath, providersData, reportDef.provider);
|
|
6438
|
+
if (metric) {
|
|
6439
|
+
metric.id = metricPath;
|
|
6440
|
+
reconstructed.push(metric);
|
|
6441
|
+
}
|
|
6442
|
+
}
|
|
6443
|
+
}
|
|
6444
|
+
return reconstructed;
|
|
6445
|
+
};
|
|
6446
|
+
|
|
6126
6447
|
// Build the unified display/execution order for dimensions + computed
|
|
6127
6448
|
// dimensions. Prefers the persisted `ordered_dimensions` field; falls back
|
|
6128
6449
|
// to "dimensions first, then computed dimensions" for report definitions
|
|
@@ -6133,7 +6454,7 @@ const ReportBuilder = ({
|
|
|
6133
6454
|
const seenComputedKeys = new Set();
|
|
6134
6455
|
if (Array.isArray(rawOrderedDimensions)) {
|
|
6135
6456
|
rawOrderedDimensions.forEach(entry => {
|
|
6136
|
-
if (entry.type === "dimension" && dimensions.some(d => d.
|
|
6457
|
+
if (entry.type === "dimension" && dimensions.some(d => d.id === entry.ref)) {
|
|
6137
6458
|
dimensionOrder.push({
|
|
6138
6459
|
type: "dimension",
|
|
6139
6460
|
key: entry.ref
|
|
@@ -6151,10 +6472,10 @@ const ReportBuilder = ({
|
|
|
6151
6472
|
|
|
6152
6473
|
// Append anything not covered by ordered_dimensions (older reports, or drift)
|
|
6153
6474
|
dimensions.forEach(d => {
|
|
6154
|
-
if (!seenDimensionKeys.has(d.
|
|
6475
|
+
if (!seenDimensionKeys.has(d.id)) {
|
|
6155
6476
|
dimensionOrder.push({
|
|
6156
6477
|
type: "dimension",
|
|
6157
|
-
key: d.
|
|
6478
|
+
key: d.id
|
|
6158
6479
|
});
|
|
6159
6480
|
}
|
|
6160
6481
|
});
|
|
@@ -6168,6 +6489,46 @@ const ReportBuilder = ({
|
|
|
6168
6489
|
});
|
|
6169
6490
|
return dimensionOrder;
|
|
6170
6491
|
};
|
|
6492
|
+
|
|
6493
|
+
// Build the optional unified column order (dimensions + computed
|
|
6494
|
+
// dimensions + metrics) from the persisted `ordered_columns` field.
|
|
6495
|
+
// Unlike buildDimensionOrder, entries that no longer resolve are simply
|
|
6496
|
+
// dropped rather than backfilled — an empty/partial result just means "no
|
|
6497
|
+
// custom order defined (yet)", which resolveColumnOrder treats as
|
|
6498
|
+
// "fall back to the default order".
|
|
6499
|
+
const buildColumnOrder = (rawOrderedColumns, dimensions, computedDimensions, metrics) => {
|
|
6500
|
+
if (!Array.isArray(rawOrderedColumns)) return [];
|
|
6501
|
+
const dimensionIds = new Set(dimensions.map(d => d.id));
|
|
6502
|
+
const computedNames = new Set(computedDimensions.map(cd => cd.name));
|
|
6503
|
+
const metricIds = new Set(metrics.map(m => m.id));
|
|
6504
|
+
const columnOrder = [];
|
|
6505
|
+
rawOrderedColumns.forEach(entry => {
|
|
6506
|
+
if (entry.type === "dimension" && dimensionIds.has(entry.ref)) {
|
|
6507
|
+
columnOrder.push({
|
|
6508
|
+
type: "dimension",
|
|
6509
|
+
key: entry.ref
|
|
6510
|
+
});
|
|
6511
|
+
} else if (entry.type === "computed" && computedNames.has(entry.ref)) {
|
|
6512
|
+
columnOrder.push({
|
|
6513
|
+
type: "computed",
|
|
6514
|
+
key: entry.ref
|
|
6515
|
+
});
|
|
6516
|
+
} else if (entry.type === "metric" && metricIds.has(entry.ref)) {
|
|
6517
|
+
columnOrder.push({
|
|
6518
|
+
type: "metric",
|
|
6519
|
+
key: entry.ref
|
|
6520
|
+
});
|
|
6521
|
+
}
|
|
6522
|
+
});
|
|
6523
|
+
return columnOrder;
|
|
6524
|
+
};
|
|
6525
|
+
|
|
6526
|
+
// Auto-append a newly added dimension/computed dimension/metric to the end
|
|
6527
|
+
// of columnOrder — but only once a custom order actually exists. While
|
|
6528
|
+
// columnOrder is empty (no custom order defined) it's left untouched, so
|
|
6529
|
+
// the results grid keeps falling back to the default dimensions-then-
|
|
6530
|
+
// metrics order until the user visits the Column Order tab.
|
|
6531
|
+
const appendToColumnOrder = (columnOrder, entry) => columnOrder.length > 0 ? [...columnOrder, entry] : columnOrder;
|
|
6171
6532
|
const loadReportDefinition = async id => {
|
|
6172
6533
|
try {
|
|
6173
6534
|
console.log("Loading report definition:", id);
|
|
@@ -6182,43 +6543,10 @@ const ReportBuilder = ({
|
|
|
6182
6543
|
// Set the title
|
|
6183
6544
|
setReportTitle(reportDef.title || "");
|
|
6184
6545
|
|
|
6185
|
-
// Reconstruct dimensions
|
|
6186
|
-
|
|
6187
|
-
const
|
|
6188
|
-
|
|
6189
|
-
// First, build a map of fullPath -> sortOrder from order_by
|
|
6190
|
-
if (reportDef.definition?.doc?.query?.order_by) {
|
|
6191
|
-
for (const orderItem of reportDef.definition.doc.query.order_by) {
|
|
6192
|
-
if (orderItem.desc === true) {
|
|
6193
|
-
orderByMap[orderItem.name] = "desc";
|
|
6194
|
-
} else {
|
|
6195
|
-
orderByMap[orderItem.name] = "asc";
|
|
6196
|
-
}
|
|
6197
|
-
}
|
|
6198
|
-
}
|
|
6199
|
-
|
|
6200
|
-
// Now reconstruct dimensions and add sortOrder from the map
|
|
6201
|
-
if (reportDef.definition?.doc?.query?.dimensions) {
|
|
6202
|
-
for (const dimPath of reportDef.definition.doc.query.dimensions) {
|
|
6203
|
-
const dim = reconstructDimensionFromPath(dimPath, providersData, reportDef.provider);
|
|
6204
|
-
if (dim) {
|
|
6205
|
-
// Add sortOrder from order_by if it exists
|
|
6206
|
-
dim.sortOrder = orderByMap[dimPath] || null;
|
|
6207
|
-
reconstructedDimensions.push(dim);
|
|
6208
|
-
}
|
|
6209
|
-
}
|
|
6210
|
-
}
|
|
6211
|
-
|
|
6212
|
-
// Reconstruct metrics
|
|
6213
|
-
const reconstructedMetrics = [];
|
|
6214
|
-
if (reportDef.definition?.doc?.query?.metrics) {
|
|
6215
|
-
for (const metricPath of reportDef.definition.doc.query.metrics) {
|
|
6216
|
-
const metric = reconstructMetricFromPath(metricPath, providersData, reportDef.provider);
|
|
6217
|
-
if (metric) {
|
|
6218
|
-
reconstructedMetrics.push(metric);
|
|
6219
|
-
}
|
|
6220
|
-
}
|
|
6221
|
-
}
|
|
6546
|
+
// Reconstruct dimensions and metrics as independent instances
|
|
6547
|
+
// (preserving deliberate duplicates - see reconstructDimensions/Metrics)
|
|
6548
|
+
const reconstructedDimensions = reconstructDimensions(reportDef, providersData);
|
|
6549
|
+
const reconstructedMetrics = reconstructMetrics(reportDef, providersData);
|
|
6222
6550
|
|
|
6223
6551
|
// Load title overrides if they exist
|
|
6224
6552
|
const loadedTitleOverrides = {
|
|
@@ -6264,6 +6592,7 @@ const ReportBuilder = ({
|
|
|
6264
6592
|
// provider/relation chain, so they're loaded as-is (no reconstruction needed)
|
|
6265
6593
|
const computedDimensions = reportDef.definition?.doc?.query?.computed_dimensions || [];
|
|
6266
6594
|
const dimensionOrder = buildDimensionOrder(reportDef.definition?.doc?.query?.ordered_dimensions, reconstructedDimensions, computedDimensions);
|
|
6595
|
+
const columnOrder = buildColumnOrder(reportDef.definition?.doc?.query?.ordered_columns, reconstructedDimensions, computedDimensions, reconstructedMetrics);
|
|
6267
6596
|
|
|
6268
6597
|
// Set the report state
|
|
6269
6598
|
setReport({
|
|
@@ -6271,7 +6600,8 @@ const ReportBuilder = ({
|
|
|
6271
6600
|
metrics: reconstructedMetrics,
|
|
6272
6601
|
filters: loadedFilters,
|
|
6273
6602
|
computedDimensions,
|
|
6274
|
-
dimensionOrder
|
|
6603
|
+
dimensionOrder,
|
|
6604
|
+
columnOrder
|
|
6275
6605
|
});
|
|
6276
6606
|
|
|
6277
6607
|
// Set title overrides
|
|
@@ -6300,43 +6630,10 @@ const ReportBuilder = ({
|
|
|
6300
6630
|
// Set the title (already modified with " (Copy)" suffix)
|
|
6301
6631
|
setReportTitle(reportDef.title || "");
|
|
6302
6632
|
|
|
6303
|
-
// Reconstruct dimensions
|
|
6304
|
-
|
|
6305
|
-
const
|
|
6306
|
-
|
|
6307
|
-
// First, build a map of fullPath -> sortOrder from order_by
|
|
6308
|
-
if (reportDef.definition?.doc?.query?.order_by) {
|
|
6309
|
-
for (const orderItem of reportDef.definition.doc.query.order_by) {
|
|
6310
|
-
if (orderItem.desc === true) {
|
|
6311
|
-
orderByMap[orderItem.name] = "desc";
|
|
6312
|
-
} else {
|
|
6313
|
-
orderByMap[orderItem.name] = "asc";
|
|
6314
|
-
}
|
|
6315
|
-
}
|
|
6316
|
-
}
|
|
6317
|
-
|
|
6318
|
-
// Now reconstruct dimensions and add sortOrder from the map
|
|
6319
|
-
if (reportDef.definition?.doc?.query?.dimensions) {
|
|
6320
|
-
for (const dimPath of reportDef.definition.doc.query.dimensions) {
|
|
6321
|
-
const dim = reconstructDimensionFromPath(dimPath, providersData, reportDef.provider);
|
|
6322
|
-
if (dim) {
|
|
6323
|
-
// Add sortOrder from order_by if it exists
|
|
6324
|
-
dim.sortOrder = orderByMap[dimPath] || null;
|
|
6325
|
-
reconstructedDimensions.push(dim);
|
|
6326
|
-
}
|
|
6327
|
-
}
|
|
6328
|
-
}
|
|
6329
|
-
|
|
6330
|
-
// Reconstruct metrics
|
|
6331
|
-
const reconstructedMetrics = [];
|
|
6332
|
-
if (reportDef.definition?.doc?.query?.metrics) {
|
|
6333
|
-
for (const metricPath of reportDef.definition.doc.query.metrics) {
|
|
6334
|
-
const metric = reconstructMetricFromPath(metricPath, providersData, reportDef.provider);
|
|
6335
|
-
if (metric) {
|
|
6336
|
-
reconstructedMetrics.push(metric);
|
|
6337
|
-
}
|
|
6338
|
-
}
|
|
6339
|
-
}
|
|
6633
|
+
// Reconstruct dimensions and metrics as independent instances
|
|
6634
|
+
// (preserving deliberate duplicates - see reconstructDimensions/Metrics)
|
|
6635
|
+
const reconstructedDimensions = reconstructDimensions(reportDef, providersData);
|
|
6636
|
+
const reconstructedMetrics = reconstructMetrics(reportDef, providersData);
|
|
6340
6637
|
|
|
6341
6638
|
// Load title overrides if they exist
|
|
6342
6639
|
const loadedTitleOverrides = {
|
|
@@ -6381,6 +6678,7 @@ const ReportBuilder = ({
|
|
|
6381
6678
|
// provider/relation chain, so they're loaded as-is (no reconstruction needed)
|
|
6382
6679
|
const computedDimensions = reportDef.definition?.doc?.query?.computed_dimensions || [];
|
|
6383
6680
|
const dimensionOrder = buildDimensionOrder(reportDef.definition?.doc?.query?.ordered_dimensions, reconstructedDimensions, computedDimensions);
|
|
6681
|
+
const columnOrder = buildColumnOrder(reportDef.definition?.doc?.query?.ordered_columns, reconstructedDimensions, computedDimensions, reconstructedMetrics);
|
|
6384
6682
|
|
|
6385
6683
|
// Set the report state
|
|
6386
6684
|
setReport({
|
|
@@ -6388,7 +6686,8 @@ const ReportBuilder = ({
|
|
|
6388
6686
|
metrics: reconstructedMetrics,
|
|
6389
6687
|
filters: loadedFilters,
|
|
6390
6688
|
computedDimensions,
|
|
6391
|
-
dimensionOrder
|
|
6689
|
+
dimensionOrder,
|
|
6690
|
+
columnOrder
|
|
6392
6691
|
});
|
|
6393
6692
|
|
|
6394
6693
|
// Set title overrides
|
|
@@ -6422,7 +6721,8 @@ const ReportBuilder = ({
|
|
|
6422
6721
|
metrics: [],
|
|
6423
6722
|
filters: {},
|
|
6424
6723
|
computedDimensions: [],
|
|
6425
|
-
dimensionOrder: []
|
|
6724
|
+
dimensionOrder: [],
|
|
6725
|
+
columnOrder: []
|
|
6426
6726
|
});
|
|
6427
6727
|
// Reset title overrides
|
|
6428
6728
|
setTitleOverrides({
|
|
@@ -6432,42 +6732,45 @@ const ReportBuilder = ({
|
|
|
6432
6732
|
});
|
|
6433
6733
|
console.log("Selected root provider:", event.target.value);
|
|
6434
6734
|
};
|
|
6435
|
-
|
|
6735
|
+
|
|
6736
|
+
// Keyed by instance id, so two instances of the same dimension can carry
|
|
6737
|
+
// independent custom titles.
|
|
6738
|
+
const handleUpdateDimensionTitle = (id, customTitle) => {
|
|
6436
6739
|
setTitleOverrides(prev => ({
|
|
6437
6740
|
...prev,
|
|
6438
6741
|
dimensions: {
|
|
6439
6742
|
...prev.dimensions,
|
|
6440
|
-
[
|
|
6743
|
+
[id]: customTitle
|
|
6441
6744
|
}
|
|
6442
6745
|
}));
|
|
6443
6746
|
};
|
|
6444
|
-
const handleResetDimensionTitle =
|
|
6747
|
+
const handleResetDimensionTitle = id => {
|
|
6445
6748
|
setTitleOverrides(prev => {
|
|
6446
6749
|
const newDimensions = {
|
|
6447
6750
|
...prev.dimensions
|
|
6448
6751
|
};
|
|
6449
|
-
delete newDimensions[
|
|
6752
|
+
delete newDimensions[id];
|
|
6450
6753
|
return {
|
|
6451
6754
|
...prev,
|
|
6452
6755
|
dimensions: newDimensions
|
|
6453
6756
|
};
|
|
6454
6757
|
});
|
|
6455
6758
|
};
|
|
6456
|
-
const handleUpdateMetricTitle = (
|
|
6759
|
+
const handleUpdateMetricTitle = (id, customTitle) => {
|
|
6457
6760
|
setTitleOverrides(prev => ({
|
|
6458
6761
|
...prev,
|
|
6459
6762
|
metrics: {
|
|
6460
6763
|
...prev.metrics,
|
|
6461
|
-
[
|
|
6764
|
+
[id]: customTitle
|
|
6462
6765
|
}
|
|
6463
6766
|
}));
|
|
6464
6767
|
};
|
|
6465
|
-
const handleResetMetricTitle =
|
|
6768
|
+
const handleResetMetricTitle = id => {
|
|
6466
6769
|
setTitleOverrides(prev => {
|
|
6467
6770
|
const newMetrics = {
|
|
6468
6771
|
...prev.metrics
|
|
6469
6772
|
};
|
|
6470
|
-
delete newMetrics[
|
|
6773
|
+
delete newMetrics[id];
|
|
6471
6774
|
return {
|
|
6472
6775
|
...prev,
|
|
6473
6776
|
metrics: newMetrics
|
|
@@ -6502,29 +6805,42 @@ const ReportBuilder = ({
|
|
|
6502
6805
|
dimensions: [...prev.dimensions, dimensionData],
|
|
6503
6806
|
dimensionOrder: [...prev.dimensionOrder, {
|
|
6504
6807
|
type: "dimension",
|
|
6505
|
-
key: dimensionData.
|
|
6506
|
-
}]
|
|
6808
|
+
key: dimensionData.id
|
|
6809
|
+
}],
|
|
6810
|
+
columnOrder: appendToColumnOrder(prev.columnOrder, {
|
|
6811
|
+
type: "dimension",
|
|
6812
|
+
key: dimensionData.id
|
|
6813
|
+
})
|
|
6507
6814
|
};
|
|
6508
6815
|
console.log("Dimension saved:", dimensionData);
|
|
6509
6816
|
console.log("Complete report:", newReport);
|
|
6510
6817
|
return newReport;
|
|
6511
6818
|
});
|
|
6512
6819
|
};
|
|
6513
|
-
const handleRemoveDimension =
|
|
6820
|
+
const handleRemoveDimension = id => {
|
|
6514
6821
|
setReport(prev => ({
|
|
6515
6822
|
...prev,
|
|
6516
|
-
dimensions: prev.dimensions.filter(d => d.
|
|
6517
|
-
dimensionOrder: prev.dimensionOrder.filter(entry => !(entry.type === "dimension" && entry.key ===
|
|
6823
|
+
dimensions: prev.dimensions.filter(d => d.id !== id),
|
|
6824
|
+
dimensionOrder: prev.dimensionOrder.filter(entry => !(entry.type === "dimension" && entry.key === id)),
|
|
6825
|
+
columnOrder: prev.columnOrder.filter(entry => !(entry.type === "dimension" && entry.key === id))
|
|
6518
6826
|
}));
|
|
6519
6827
|
};
|
|
6520
|
-
|
|
6521
|
-
|
|
6522
|
-
|
|
6523
|
-
|
|
6524
|
-
|
|
6525
|
-
|
|
6526
|
-
|
|
6527
|
-
|
|
6828
|
+
|
|
6829
|
+
// sortOrder is fundamentally a per-field (query-level) setting - SQL can
|
|
6830
|
+
// only sort by a given column once - so toggling it on any instance of a
|
|
6831
|
+
// duplicated dimension applies to every instance sharing that fullPath.
|
|
6832
|
+
const handleUpdateDimensionSortOrder = (id, sortOrder) => {
|
|
6833
|
+
setReport(prev => {
|
|
6834
|
+
const target = prev.dimensions.find(d => d.id === id);
|
|
6835
|
+
if (!target) return prev;
|
|
6836
|
+
return {
|
|
6837
|
+
...prev,
|
|
6838
|
+
dimensions: prev.dimensions.map(d => d.fullPath === target.fullPath ? {
|
|
6839
|
+
...d,
|
|
6840
|
+
sortOrder
|
|
6841
|
+
} : d)
|
|
6842
|
+
};
|
|
6843
|
+
});
|
|
6528
6844
|
};
|
|
6529
6845
|
|
|
6530
6846
|
// Reorders the single unified dimensions + computed-dimensions list;
|
|
@@ -6543,7 +6859,11 @@ const ReportBuilder = ({
|
|
|
6543
6859
|
dimensionOrder: [...prev.dimensionOrder, {
|
|
6544
6860
|
type: "computed",
|
|
6545
6861
|
key: computedDimensionData.name
|
|
6546
|
-
}]
|
|
6862
|
+
}],
|
|
6863
|
+
columnOrder: appendToColumnOrder(prev.columnOrder, {
|
|
6864
|
+
type: "computed",
|
|
6865
|
+
key: computedDimensionData.name
|
|
6866
|
+
})
|
|
6547
6867
|
}));
|
|
6548
6868
|
};
|
|
6549
6869
|
const handleUpdateComputedDimension = (name, computedDimensionData) => {
|
|
@@ -6553,6 +6873,10 @@ const ReportBuilder = ({
|
|
|
6553
6873
|
dimensionOrder: prev.dimensionOrder.map(entry => entry.type === "computed" && entry.key === name ? {
|
|
6554
6874
|
...entry,
|
|
6555
6875
|
key: computedDimensionData.name
|
|
6876
|
+
} : entry),
|
|
6877
|
+
columnOrder: prev.columnOrder.map(entry => entry.type === "computed" && entry.key === name ? {
|
|
6878
|
+
...entry,
|
|
6879
|
+
key: computedDimensionData.name
|
|
6556
6880
|
} : entry)
|
|
6557
6881
|
}));
|
|
6558
6882
|
};
|
|
@@ -6560,24 +6884,30 @@ const ReportBuilder = ({
|
|
|
6560
6884
|
setReport(prev => ({
|
|
6561
6885
|
...prev,
|
|
6562
6886
|
computedDimensions: prev.computedDimensions.filter(cd => cd.name !== name),
|
|
6563
|
-
dimensionOrder: prev.dimensionOrder.filter(entry => !(entry.type === "computed" && entry.key === name))
|
|
6887
|
+
dimensionOrder: prev.dimensionOrder.filter(entry => !(entry.type === "computed" && entry.key === name)),
|
|
6888
|
+
columnOrder: prev.columnOrder.filter(entry => !(entry.type === "computed" && entry.key === name))
|
|
6564
6889
|
}));
|
|
6565
6890
|
};
|
|
6566
6891
|
const handleSaveMetric = metricData => {
|
|
6567
6892
|
setReport(prev => {
|
|
6568
6893
|
const newReport = {
|
|
6569
6894
|
...prev,
|
|
6570
|
-
metrics: [...prev.metrics, metricData]
|
|
6895
|
+
metrics: [...prev.metrics, metricData],
|
|
6896
|
+
columnOrder: appendToColumnOrder(prev.columnOrder, {
|
|
6897
|
+
type: "metric",
|
|
6898
|
+
key: metricData.id
|
|
6899
|
+
})
|
|
6571
6900
|
};
|
|
6572
6901
|
console.log("Metric saved:", metricData);
|
|
6573
6902
|
console.log("Complete report:", newReport);
|
|
6574
6903
|
return newReport;
|
|
6575
6904
|
});
|
|
6576
6905
|
};
|
|
6577
|
-
const handleRemoveMetric =
|
|
6906
|
+
const handleRemoveMetric = id => {
|
|
6578
6907
|
setReport(prev => ({
|
|
6579
6908
|
...prev,
|
|
6580
|
-
metrics: prev.metrics.filter(
|
|
6909
|
+
metrics: prev.metrics.filter(m => m.id !== id),
|
|
6910
|
+
columnOrder: prev.columnOrder.filter(entry => !(entry.type === "metric" && entry.key === id))
|
|
6581
6911
|
}));
|
|
6582
6912
|
};
|
|
6583
6913
|
const handleReorderMetrics = newOrder => {
|
|
@@ -6586,6 +6916,16 @@ const ReportBuilder = ({
|
|
|
6586
6916
|
metrics: newOrder
|
|
6587
6917
|
}));
|
|
6588
6918
|
};
|
|
6919
|
+
|
|
6920
|
+
// Sets the unified column order shown on the Column Order tab. An empty
|
|
6921
|
+
// array (e.g. via "Reset to Default") clears the customization, and the
|
|
6922
|
+
// results grid falls back to dimensionOrder followed by metrics order.
|
|
6923
|
+
const handleReorderColumnItems = newColumnOrder => {
|
|
6924
|
+
setReport(prev => ({
|
|
6925
|
+
...prev,
|
|
6926
|
+
columnOrder: newColumnOrder
|
|
6927
|
+
}));
|
|
6928
|
+
};
|
|
6589
6929
|
const handleSaveFilter = (fullPath, filterData) => {
|
|
6590
6930
|
setReport(prev => {
|
|
6591
6931
|
const newReport = {
|
|
@@ -6623,13 +6963,30 @@ const ReportBuilder = ({
|
|
|
6623
6963
|
// persisted `ordered_dimensions` field all stay consistent with whatever
|
|
6624
6964
|
// order the user arranged on screen.
|
|
6625
6965
|
const buildQueryObject = () => {
|
|
6626
|
-
const
|
|
6966
|
+
const dimensionById = Object.fromEntries(report.dimensions.map(d => [d.id, d]));
|
|
6627
6967
|
const computedByName = Object.fromEntries(report.computedDimensions.map(cd => [cd.name, cd]));
|
|
6628
|
-
|
|
6968
|
+
|
|
6969
|
+
// May contain more than one instance of the same fullPath (deliberate
|
|
6970
|
+
// duplicates), kept in dimensionOrder's display order.
|
|
6971
|
+
const orderedDimensionsOnly = report.dimensionOrder.filter(entry => entry.type === "dimension").map(entry => dimensionById[entry.key]).filter(Boolean);
|
|
6629
6972
|
const orderedComputedOnly = report.dimensionOrder.filter(entry => entry.type === "computed").map(entry => computedByName[entry.key]).filter(Boolean);
|
|
6630
6973
|
|
|
6974
|
+
// The API keys each result row by fullPath, so duplicate instances of
|
|
6975
|
+
// the same field can only ever carry one value/one sort direction at
|
|
6976
|
+
// the query level - dedupe by fullPath (keeping first-occurrence order)
|
|
6977
|
+
// before sending. The builder still shows/orders/titles each instance
|
|
6978
|
+
// independently; they just read from the same underlying query column.
|
|
6979
|
+
const uniqueDimensionsByPath = [];
|
|
6980
|
+
const seenDimensionPaths = new Set();
|
|
6981
|
+
orderedDimensionsOnly.forEach(dim => {
|
|
6982
|
+
if (!seenDimensionPaths.has(dim.fullPath)) {
|
|
6983
|
+
seenDimensionPaths.add(dim.fullPath);
|
|
6984
|
+
uniqueDimensionsByPath.push(dim);
|
|
6985
|
+
}
|
|
6986
|
+
});
|
|
6987
|
+
|
|
6631
6988
|
// Build order_by array - include all dimensions, add desc property only when needed
|
|
6632
|
-
const orderBy =
|
|
6989
|
+
const orderBy = uniqueDimensionsByPath.map(dim => {
|
|
6633
6990
|
const orderItem = {
|
|
6634
6991
|
name: dim.fullPath
|
|
6635
6992
|
};
|
|
@@ -6684,9 +7041,20 @@ const ReportBuilder = ({
|
|
|
6684
7041
|
if (Object.keys(titleOverrides.filters).length > 0) {
|
|
6685
7042
|
titles.filters = titleOverrides.filters;
|
|
6686
7043
|
}
|
|
7044
|
+
|
|
7045
|
+
// Same dedupe rationale as dimensions above: the query only needs each
|
|
7046
|
+
// metric fullPath once, however many display instances reference it.
|
|
7047
|
+
const uniqueMetricPaths = [];
|
|
7048
|
+
const seenMetricPaths = new Set();
|
|
7049
|
+
report.metrics.forEach(metric => {
|
|
7050
|
+
if (!seenMetricPaths.has(metric.fullPath)) {
|
|
7051
|
+
seenMetricPaths.add(metric.fullPath);
|
|
7052
|
+
uniqueMetricPaths.push(metric.fullPath);
|
|
7053
|
+
}
|
|
7054
|
+
});
|
|
6687
7055
|
const queryObj = {
|
|
6688
|
-
dimensions:
|
|
6689
|
-
metrics:
|
|
7056
|
+
dimensions: uniqueDimensionsByPath.map(dim => dim.fullPath),
|
|
7057
|
+
metrics: uniqueMetricPaths,
|
|
6690
7058
|
order_by: orderBy
|
|
6691
7059
|
};
|
|
6692
7060
|
|
|
@@ -6700,7 +7068,9 @@ const ReportBuilder = ({
|
|
|
6700
7068
|
|
|
6701
7069
|
// Persist the unified dimension + computed-dimension order so it can be
|
|
6702
7070
|
// reconstructed on reload and so executed reports render columns in the
|
|
6703
|
-
// same order they were arranged in the builder.
|
|
7071
|
+
// same order they were arranged in the builder. Entries use each
|
|
7072
|
+
// dimension's instance id as `ref`, so deliberate duplicates (the nth
|
|
7073
|
+
// occurrence of a fullPath is suffixed `#n`) round-trip correctly.
|
|
6704
7074
|
if (report.dimensionOrder.length > 0) {
|
|
6705
7075
|
queryObj.ordered_dimensions = report.dimensionOrder.map(entry => ({
|
|
6706
7076
|
type: entry.type,
|
|
@@ -6708,6 +7078,23 @@ const ReportBuilder = ({
|
|
|
6708
7078
|
}));
|
|
6709
7079
|
}
|
|
6710
7080
|
|
|
7081
|
+
// Persist metric instance order/identity the same way (metrics have no
|
|
7082
|
+
// other unified-order field to piggyback on).
|
|
7083
|
+
if (report.metrics.length > 0) {
|
|
7084
|
+
queryObj.ordered_metrics = report.metrics.map(metric => metric.id);
|
|
7085
|
+
}
|
|
7086
|
+
|
|
7087
|
+
// Persist the optional unified column order (Column Order tab) so the
|
|
7088
|
+
// results grid can reproduce it on reload. Absent when no custom order
|
|
7089
|
+
// has been defined, so old/untouched reports keep their current
|
|
7090
|
+
// dimensions-then-metrics column order unchanged.
|
|
7091
|
+
if (report.columnOrder.length > 0) {
|
|
7092
|
+
queryObj.ordered_columns = report.columnOrder.map(entry => ({
|
|
7093
|
+
type: entry.type,
|
|
7094
|
+
ref: entry.key
|
|
7095
|
+
}));
|
|
7096
|
+
}
|
|
7097
|
+
|
|
6711
7098
|
// Only add titles if there are any overrides
|
|
6712
7099
|
if (Object.keys(titles).length > 0) {
|
|
6713
7100
|
queryObj.titles = titles;
|
|
@@ -6767,8 +7154,8 @@ const ReportBuilder = ({
|
|
|
6767
7154
|
const handleRunReport = async () => {
|
|
6768
7155
|
setCurrentPage(0);
|
|
6769
7156
|
await runReportWithPagination(0, pageSize);
|
|
6770
|
-
// Switch to Results tab after running report (
|
|
6771
|
-
setActiveTab(
|
|
7157
|
+
// Switch to Results tab after running report (index 4: Dimensions, Metrics, Filters, Column Order, Results)
|
|
7158
|
+
setActiveTab(4);
|
|
6772
7159
|
};
|
|
6773
7160
|
const handleDownloadReport = async () => {
|
|
6774
7161
|
try {
|
|
@@ -6866,15 +7253,20 @@ const ReportBuilder = ({
|
|
|
6866
7253
|
|
|
6867
7254
|
// Resolve the unified dimensionOrder into actual dimension/computed-dimension
|
|
6868
7255
|
// objects so the results grid can render columns in that same order.
|
|
6869
|
-
const
|
|
7256
|
+
const dimensionById = Object.fromEntries(report.dimensions.map(d => [d.id, d]));
|
|
6870
7257
|
const computedDimensionByName = Object.fromEntries(report.computedDimensions.map(cd => [cd.name, cd]));
|
|
6871
7258
|
const orderedDimensionItems = report.dimensionOrder.map(entry => entry.type === "dimension" ? {
|
|
6872
7259
|
kind: "dimension",
|
|
6873
|
-
data:
|
|
7260
|
+
data: dimensionById[entry.key]
|
|
6874
7261
|
} : {
|
|
6875
7262
|
kind: "computed",
|
|
6876
7263
|
data: computedDimensionByName[entry.key]
|
|
6877
7264
|
}).filter(item => item.data);
|
|
7265
|
+
|
|
7266
|
+
// Final column list for the results grid: the explicit Column Order tab
|
|
7267
|
+
// arrangement when defined, otherwise dimensions/computed (in their own
|
|
7268
|
+
// order) followed by metrics (in their own order) — today's behavior.
|
|
7269
|
+
const columnItems = resolveColumnOrder(report.columnOrder, orderedDimensionItems, report.metrics);
|
|
6878
7270
|
return /*#__PURE__*/React__default.createElement(Box$1, {
|
|
6879
7271
|
sx: {
|
|
6880
7272
|
p: 3,
|
|
@@ -7129,9 +7521,36 @@ const ReportBuilder = ({
|
|
|
7129
7521
|
}
|
|
7130
7522
|
}
|
|
7131
7523
|
}), /*#__PURE__*/React__default.createElement(Tab, {
|
|
7132
|
-
label:
|
|
7524
|
+
label: /*#__PURE__*/React__default.createElement(Badge, {
|
|
7525
|
+
variant: "dot",
|
|
7526
|
+
invisible: report.columnOrder.length === 0,
|
|
7527
|
+
sx: {
|
|
7528
|
+
"& .MuiBadge-dot": {
|
|
7529
|
+
backgroundColor: "rgb(70, 134, 128)"
|
|
7530
|
+
}
|
|
7531
|
+
}
|
|
7532
|
+
}, /*#__PURE__*/React__default.createElement("span", {
|
|
7533
|
+
style: {
|
|
7534
|
+
marginRight: report.columnOrder.length > 0 ? "8px" : "0"
|
|
7535
|
+
}
|
|
7536
|
+
}, "Column Order")),
|
|
7133
7537
|
id: "report-tab-3",
|
|
7134
7538
|
"aria-controls": "report-tabpanel-3",
|
|
7539
|
+
sx: {
|
|
7540
|
+
height: "41px",
|
|
7541
|
+
fontFamily: "system-ui",
|
|
7542
|
+
borderRadius: "0.5rem",
|
|
7543
|
+
boxShadow: "none",
|
|
7544
|
+
textTransform: "none",
|
|
7545
|
+
color: "rgb(37, 37, 37)",
|
|
7546
|
+
"&.Mui-selected": {
|
|
7547
|
+
color: "rgb(70, 134, 128)"
|
|
7548
|
+
}
|
|
7549
|
+
}
|
|
7550
|
+
}), /*#__PURE__*/React__default.createElement(Tab, {
|
|
7551
|
+
label: reportData ? "Results" : "Results (Run report first)",
|
|
7552
|
+
id: "report-tab-4",
|
|
7553
|
+
"aria-controls": "report-tabpanel-4",
|
|
7135
7554
|
disabled: !reportData,
|
|
7136
7555
|
sx: {
|
|
7137
7556
|
height: "41px",
|
|
@@ -7198,10 +7617,18 @@ const ReportBuilder = ({
|
|
|
7198
7617
|
})), /*#__PURE__*/React__default.createElement(TabPanel, {
|
|
7199
7618
|
value: activeTab,
|
|
7200
7619
|
index: 3
|
|
7201
|
-
},
|
|
7202
|
-
reportData: reportData,
|
|
7620
|
+
}, /*#__PURE__*/React__default.createElement(ColumnOrder, {
|
|
7203
7621
|
orderedDimensionItems: orderedDimensionItems,
|
|
7204
7622
|
metrics: report.metrics,
|
|
7623
|
+
columnOrder: report.columnOrder,
|
|
7624
|
+
onReorderColumnItems: handleReorderColumnItems,
|
|
7625
|
+
titleOverrides: titleOverrides
|
|
7626
|
+
})), /*#__PURE__*/React__default.createElement(TabPanel, {
|
|
7627
|
+
value: activeTab,
|
|
7628
|
+
index: 4
|
|
7629
|
+
}, reportData && /*#__PURE__*/React__default.createElement(ReportDataGrid, {
|
|
7630
|
+
reportData: reportData,
|
|
7631
|
+
columnItems: columnItems,
|
|
7205
7632
|
loading: loading,
|
|
7206
7633
|
onPageChange: handlePageChange,
|
|
7207
7634
|
totalRows: totalRows,
|
|
@@ -7289,6 +7716,12 @@ const CurrencySelector = () => {
|
|
|
7289
7716
|
parameters,
|
|
7290
7717
|
setParameters
|
|
7291
7718
|
} = useReportingContext();
|
|
7719
|
+
|
|
7720
|
+
// Hidden by default; only shown when a `cur` query parameter is present in the top-level URL.
|
|
7721
|
+
const showCurrencySelector = new URLSearchParams(window.location.search).has("cur");
|
|
7722
|
+
if (!showCurrencySelector) {
|
|
7723
|
+
return null;
|
|
7724
|
+
}
|
|
7292
7725
|
return /*#__PURE__*/React__default.createElement(SingleSelect, {
|
|
7293
7726
|
items: CURRENCY_OPTIONS,
|
|
7294
7727
|
value: parameters.base_currency,
|