@bygd/nc-report-ui 0.1.40 → 0.1.42
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 +965 -571
- package/dist/default/cjs/index.cjs +966 -574
- package/dist/default/esm/index.js +966 -575
- package/package.json +1 -1
|
@@ -44,7 +44,7 @@ import ArrowBackIcon from '@mui/icons-material/ArrowBack';
|
|
|
44
44
|
import SaveIcon from '@mui/icons-material/Save';
|
|
45
45
|
import DownloadIcon from '@mui/icons-material/Download';
|
|
46
46
|
import { useSensors, useSensor, PointerSensor, KeyboardSensor, DndContext, closestCenter } from '@dnd-kit/core';
|
|
47
|
-
import { sortableKeyboardCoordinates, SortableContext, verticalListSortingStrategy,
|
|
47
|
+
import { sortableKeyboardCoordinates, SortableContext, verticalListSortingStrategy, useSortable, arrayMove } from '@dnd-kit/sortable';
|
|
48
48
|
import { CSS } from '@dnd-kit/utilities';
|
|
49
49
|
import DragIndicatorIcon from '@mui/icons-material/DragIndicator';
|
|
50
50
|
import ArrowUpwardIcon from '@mui/icons-material/ArrowUpward';
|
|
@@ -64,6 +64,7 @@ import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider';
|
|
|
64
64
|
import { DatePicker } from '@mui/x-date-pickers/DatePicker';
|
|
65
65
|
import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs';
|
|
66
66
|
import dayjs from 'dayjs';
|
|
67
|
+
import CalculateIcon from '@mui/icons-material/Calculate';
|
|
67
68
|
import { ThemeProvider, createTheme } from '@mui/material/styles';
|
|
68
69
|
|
|
69
70
|
function _extends() {
|
|
@@ -1574,6 +1575,39 @@ const ReportingProvider = ({
|
|
|
1574
1575
|
}, children);
|
|
1575
1576
|
};
|
|
1576
1577
|
|
|
1578
|
+
/**
|
|
1579
|
+
* useReportingContext - Hook to access reporting context
|
|
1580
|
+
*
|
|
1581
|
+
* @returns {Object} Context value with parameters, api, and setter functions
|
|
1582
|
+
* @returns {Object} return.parameters - Current parameters object
|
|
1583
|
+
* @returns {Function} return.setParameters - Function to update parameters
|
|
1584
|
+
* @returns {Object} return.api - Current API configuration
|
|
1585
|
+
* @returns {Function} return.setApi - Function to update API configuration
|
|
1586
|
+
* @returns {Function} return.setReportingContext - Function to update both parameters and api
|
|
1587
|
+
*
|
|
1588
|
+
* @example
|
|
1589
|
+
* const { parameters, setParameters, api, setApi, setReportingContext } = useReportingContext();
|
|
1590
|
+
*
|
|
1591
|
+
* // Update parameters only
|
|
1592
|
+
* setParameters({ base_currency: 'USD', merchants: [...] });
|
|
1593
|
+
*
|
|
1594
|
+
* // Update API only
|
|
1595
|
+
* setApi({ token: 'new-token', base_url: 'https://api.example.com' });
|
|
1596
|
+
*
|
|
1597
|
+
* // Update both at once
|
|
1598
|
+
* setReportingContext({
|
|
1599
|
+
* parameters: { base_currency: 'EUR' },
|
|
1600
|
+
* api: { token: 'token', base_url: 'https://api.example.com' }
|
|
1601
|
+
* });
|
|
1602
|
+
*/
|
|
1603
|
+
const useReportingContext = () => {
|
|
1604
|
+
const context = useContext(ReportingContext);
|
|
1605
|
+
if (context === undefined) {
|
|
1606
|
+
throw new Error('useReportingContext must be used within a ReportingProvider');
|
|
1607
|
+
}
|
|
1608
|
+
return context;
|
|
1609
|
+
};
|
|
1610
|
+
|
|
1577
1611
|
/**
|
|
1578
1612
|
* useReportingContextOptional - Hook to optionally access reporting context
|
|
1579
1613
|
* Returns null if used outside of ReportingProvider (useful for optional fallback)
|
|
@@ -2495,6 +2529,140 @@ const ProviderSelection = ({
|
|
|
2495
2529
|
}, renderSelectionChain()));
|
|
2496
2530
|
};
|
|
2497
2531
|
|
|
2532
|
+
// Shared drag-and-drop primitives for the index-identified, arrow-reorderable
|
|
2533
|
+
// lists used across the report builder (Dimensions, Metrics, Column Order).
|
|
2534
|
+
// Previously duplicated per-tab; consolidated here so all three behave and
|
|
2535
|
+
// look identical and only need to be fixed in one place.
|
|
2536
|
+
|
|
2537
|
+
const useSortableListSensors = () => useSensors(useSensor(PointerSensor), useSensor(KeyboardSensor, {
|
|
2538
|
+
coordinateGetter: sortableKeyboardCoordinates
|
|
2539
|
+
}));
|
|
2540
|
+
|
|
2541
|
+
// Builds drag/move handlers for a list where dnd-kit item ids are the array
|
|
2542
|
+
// index. `onReorder` receives the whole reordered list.
|
|
2543
|
+
const makeListReorderHandlers = (list, onReorder) => {
|
|
2544
|
+
const handleDragEnd = ({
|
|
2545
|
+
active,
|
|
2546
|
+
over
|
|
2547
|
+
}) => {
|
|
2548
|
+
if (over && active.id !== over.id) {
|
|
2549
|
+
const oldIndex = list.findIndex((_, i) => i === active.id);
|
|
2550
|
+
const newIndex = list.findIndex((_, i) => i === over.id);
|
|
2551
|
+
onReorder(arrayMove(list, oldIndex, newIndex));
|
|
2552
|
+
}
|
|
2553
|
+
};
|
|
2554
|
+
const handleMoveUp = index => {
|
|
2555
|
+
if (index > 0) {
|
|
2556
|
+
onReorder(arrayMove(list, index, index - 1));
|
|
2557
|
+
}
|
|
2558
|
+
};
|
|
2559
|
+
const handleMoveDown = index => {
|
|
2560
|
+
if (index < list.length - 1) {
|
|
2561
|
+
onReorder(arrayMove(list, index, index + 1));
|
|
2562
|
+
}
|
|
2563
|
+
};
|
|
2564
|
+
return {
|
|
2565
|
+
handleDragEnd,
|
|
2566
|
+
handleMoveUp,
|
|
2567
|
+
handleMoveDown
|
|
2568
|
+
};
|
|
2569
|
+
};
|
|
2570
|
+
|
|
2571
|
+
// Wraps DndContext + SortableContext + the vertical list container.
|
|
2572
|
+
// `itemIds` must line up with the ids passed to each item's useSortable().
|
|
2573
|
+
const SortableListContext = ({
|
|
2574
|
+
sensors,
|
|
2575
|
+
itemIds,
|
|
2576
|
+
onDragEnd,
|
|
2577
|
+
children
|
|
2578
|
+
}) => /*#__PURE__*/React__default.createElement(DndContext, {
|
|
2579
|
+
sensors: sensors,
|
|
2580
|
+
collisionDetection: closestCenter,
|
|
2581
|
+
onDragEnd: onDragEnd
|
|
2582
|
+
}, /*#__PURE__*/React__default.createElement(SortableContext, {
|
|
2583
|
+
items: itemIds,
|
|
2584
|
+
strategy: verticalListSortingStrategy
|
|
2585
|
+
}, /*#__PURE__*/React__default.createElement(Box$1, {
|
|
2586
|
+
sx: {
|
|
2587
|
+
display: "flex",
|
|
2588
|
+
flexDirection: "column",
|
|
2589
|
+
gap: 1
|
|
2590
|
+
}
|
|
2591
|
+
}, children)));
|
|
2592
|
+
|
|
2593
|
+
// Per-row useSortable() wiring shared by every sortable chip/row component.
|
|
2594
|
+
const useSortableRow = id => {
|
|
2595
|
+
const {
|
|
2596
|
+
attributes,
|
|
2597
|
+
listeners,
|
|
2598
|
+
setNodeRef,
|
|
2599
|
+
transform,
|
|
2600
|
+
transition,
|
|
2601
|
+
isDragging
|
|
2602
|
+
} = useSortable({
|
|
2603
|
+
id
|
|
2604
|
+
});
|
|
2605
|
+
const style = {
|
|
2606
|
+
transform: CSS.Transform.toString(transform),
|
|
2607
|
+
transition,
|
|
2608
|
+
opacity: isDragging ? 0.5 : 1,
|
|
2609
|
+
display: "flex",
|
|
2610
|
+
alignItems: "center",
|
|
2611
|
+
width: "100%"
|
|
2612
|
+
};
|
|
2613
|
+
return {
|
|
2614
|
+
attributes,
|
|
2615
|
+
listeners,
|
|
2616
|
+
setNodeRef,
|
|
2617
|
+
style
|
|
2618
|
+
};
|
|
2619
|
+
};
|
|
2620
|
+
const DragHandle = ({
|
|
2621
|
+
listeners
|
|
2622
|
+
}) => /*#__PURE__*/React__default.createElement(Box$1, _extends({}, listeners, {
|
|
2623
|
+
sx: {
|
|
2624
|
+
display: "flex",
|
|
2625
|
+
alignItems: "center",
|
|
2626
|
+
cursor: "grab",
|
|
2627
|
+
"&:active": {
|
|
2628
|
+
cursor: "grabbing"
|
|
2629
|
+
}
|
|
2630
|
+
}
|
|
2631
|
+
}), /*#__PURE__*/React__default.createElement(DragIndicatorIcon, {
|
|
2632
|
+
sx: {
|
|
2633
|
+
cursor: "grab",
|
|
2634
|
+
color: "rgba(110, 110, 110, 0.62)"
|
|
2635
|
+
}
|
|
2636
|
+
}));
|
|
2637
|
+
const MoveButtons = ({
|
|
2638
|
+
onMoveUp,
|
|
2639
|
+
onMoveDown,
|
|
2640
|
+
isFirst,
|
|
2641
|
+
isLast
|
|
2642
|
+
}) => /*#__PURE__*/React__default.createElement(Box$1, {
|
|
2643
|
+
className: "hover-icons",
|
|
2644
|
+
sx: {
|
|
2645
|
+
display: "flex",
|
|
2646
|
+
gap: 0.5,
|
|
2647
|
+
opacity: 0,
|
|
2648
|
+
transition: "opacity 0.2s"
|
|
2649
|
+
}
|
|
2650
|
+
}, /*#__PURE__*/React__default.createElement(IconButton$1, {
|
|
2651
|
+
size: "small",
|
|
2652
|
+
onClick: onMoveUp,
|
|
2653
|
+
disabled: isFirst,
|
|
2654
|
+
"aria-label": "move up"
|
|
2655
|
+
}, /*#__PURE__*/React__default.createElement(ArrowUpwardIcon, {
|
|
2656
|
+
fontSize: "small"
|
|
2657
|
+
})), /*#__PURE__*/React__default.createElement(IconButton$1, {
|
|
2658
|
+
size: "small",
|
|
2659
|
+
onClick: onMoveDown,
|
|
2660
|
+
disabled: isLast,
|
|
2661
|
+
"aria-label": "move down"
|
|
2662
|
+
}, /*#__PURE__*/React__default.createElement(ArrowDownwardIcon, {
|
|
2663
|
+
fontSize: "small"
|
|
2664
|
+
})));
|
|
2665
|
+
|
|
2498
2666
|
// Replace {{key}} placeholders in a title with values from the report
|
|
2499
2667
|
// parameters (e.g. "Balance ({{base_currency}})" -> "Balance (EUR)").
|
|
2500
2668
|
// Unknown keys are left as-is so missing parameters remain visible.
|
|
@@ -2506,6 +2674,50 @@ const interpolateTitle = (template, params) => {
|
|
|
2506
2674
|
});
|
|
2507
2675
|
};
|
|
2508
2676
|
|
|
2677
|
+
// Resolves the final column display order for the results grid: dimensions,
|
|
2678
|
+
// computed dimensions, and metrics combined into a single list.
|
|
2679
|
+
//
|
|
2680
|
+
// `columnOrder` is the optional, explicitly-arranged order (raw entries
|
|
2681
|
+
// `{ type: 'dimension' | 'computed' | 'metric', key }`) set on the Column
|
|
2682
|
+
// Order tab. When present, its entries come first, in that order. Anything
|
|
2683
|
+
// not covered by it (new fields added since it was last touched, or when
|
|
2684
|
+
// it's empty/undefined because no custom order has ever been defined) is
|
|
2685
|
+
// appended in the pre-existing default order: dimensions/computed (in their
|
|
2686
|
+
// own `dimensionOrder`) first, then metrics (in their own array order) —
|
|
2687
|
+
// this keeps behavior unchanged for reports that never define a column order.
|
|
2688
|
+
const resolveColumnOrder = (columnOrder, orderedDimensionItems, metrics) => {
|
|
2689
|
+
// Normalise every candidate item to a uniform { kind, key, data } shape
|
|
2690
|
+
// up front, so callers (e.g. the Column Order tab) can always read
|
|
2691
|
+
// `.key` regardless of where the item came from.
|
|
2692
|
+
const normalizedDimensionItems = orderedDimensionItems.map(item => ({
|
|
2693
|
+
kind: item.kind,
|
|
2694
|
+
key: item.kind === 'computed' ? item.data.name : item.data.fullPath,
|
|
2695
|
+
data: item.data
|
|
2696
|
+
}));
|
|
2697
|
+
const normalizedMetricItems = metrics.map(m => ({
|
|
2698
|
+
kind: 'metric',
|
|
2699
|
+
key: m.fullPath,
|
|
2700
|
+
data: m
|
|
2701
|
+
}));
|
|
2702
|
+
const byKey = Object.fromEntries([...normalizedDimensionItems, ...normalizedMetricItems].map(item => [`${item.kind}:${item.key}`, item]));
|
|
2703
|
+
const resolved = [];
|
|
2704
|
+
const seen = new Set();
|
|
2705
|
+
(columnOrder || []).forEach(entry => {
|
|
2706
|
+
const seenKey = `${entry.type}:${entry.key}`;
|
|
2707
|
+
if (seen.has(seenKey) || !byKey[seenKey]) return;
|
|
2708
|
+
resolved.push(byKey[seenKey]);
|
|
2709
|
+
seen.add(seenKey);
|
|
2710
|
+
});
|
|
2711
|
+
[...normalizedDimensionItems, ...normalizedMetricItems].forEach(item => {
|
|
2712
|
+
const seenKey = `${item.kind}:${item.key}`;
|
|
2713
|
+
if (!seen.has(seenKey)) {
|
|
2714
|
+
resolved.push(item);
|
|
2715
|
+
seen.add(seenKey);
|
|
2716
|
+
}
|
|
2717
|
+
});
|
|
2718
|
+
return resolved;
|
|
2719
|
+
};
|
|
2720
|
+
|
|
2509
2721
|
// Sortable Chip Component
|
|
2510
2722
|
const SortableChip$1 = ({
|
|
2511
2723
|
id,
|
|
@@ -2534,20 +2746,8 @@ const SortableChip$1 = ({
|
|
|
2534
2746
|
attributes,
|
|
2535
2747
|
listeners,
|
|
2536
2748
|
setNodeRef,
|
|
2537
|
-
|
|
2538
|
-
|
|
2539
|
-
isDragging
|
|
2540
|
-
} = useSortable({
|
|
2541
|
-
id
|
|
2542
|
-
});
|
|
2543
|
-
const style = {
|
|
2544
|
-
transform: CSS.Transform.toString(transform),
|
|
2545
|
-
transition,
|
|
2546
|
-
opacity: isDragging ? 0.5 : 1,
|
|
2547
|
-
display: "flex",
|
|
2548
|
-
alignItems: "center",
|
|
2549
|
-
width: "100%"
|
|
2550
|
-
};
|
|
2749
|
+
style
|
|
2750
|
+
} = useSortableRow(id);
|
|
2551
2751
|
|
|
2552
2752
|
// Focus input when entering edit mode
|
|
2553
2753
|
useEffect(() => {
|
|
@@ -2656,21 +2856,9 @@ const SortableChip$1 = ({
|
|
|
2656
2856
|
opacity: 1 // show icons on hover
|
|
2657
2857
|
}
|
|
2658
2858
|
}
|
|
2659
|
-
}, /*#__PURE__*/React__default.createElement(
|
|
2660
|
-
|
|
2661
|
-
|
|
2662
|
-
alignItems: "center",
|
|
2663
|
-
cursor: "grab",
|
|
2664
|
-
"&:active": {
|
|
2665
|
-
cursor: "grabbing"
|
|
2666
|
-
}
|
|
2667
|
-
}
|
|
2668
|
-
}), /*#__PURE__*/React__default.createElement(DragIndicatorIcon, {
|
|
2669
|
-
sx: {
|
|
2670
|
-
cursor: "grab",
|
|
2671
|
-
color: "rgba(110, 110, 110, 0.62)"
|
|
2672
|
-
}
|
|
2673
|
-
})), !isEditing ? /*#__PURE__*/React__default.createElement(React__default.Fragment, null, /*#__PURE__*/React__default.createElement(Box$1, {
|
|
2859
|
+
}, /*#__PURE__*/React__default.createElement(DragHandle, {
|
|
2860
|
+
listeners: listeners
|
|
2861
|
+
}), !isEditing ? /*#__PURE__*/React__default.createElement(React__default.Fragment, null, /*#__PURE__*/React__default.createElement(Box$1, {
|
|
2674
2862
|
sx: {
|
|
2675
2863
|
minWidth: 0
|
|
2676
2864
|
}
|
|
@@ -2733,29 +2921,12 @@ const SortableChip$1 = ({
|
|
|
2733
2921
|
sx: {
|
|
2734
2922
|
flex: 1
|
|
2735
2923
|
}
|
|
2736
|
-
}), /*#__PURE__*/React__default.createElement(
|
|
2737
|
-
|
|
2738
|
-
|
|
2739
|
-
|
|
2740
|
-
|
|
2741
|
-
|
|
2742
|
-
transition: "opacity 0.2s"
|
|
2743
|
-
}
|
|
2744
|
-
}, /*#__PURE__*/React__default.createElement(IconButton$1, {
|
|
2745
|
-
size: "small",
|
|
2746
|
-
onClick: onMoveUp,
|
|
2747
|
-
disabled: isFirst,
|
|
2748
|
-
"aria-label": "move up"
|
|
2749
|
-
}, /*#__PURE__*/React__default.createElement(ArrowUpwardIcon, {
|
|
2750
|
-
fontSize: "small"
|
|
2751
|
-
})), /*#__PURE__*/React__default.createElement(IconButton$1, {
|
|
2752
|
-
size: "small",
|
|
2753
|
-
onClick: onMoveDown,
|
|
2754
|
-
disabled: isLast,
|
|
2755
|
-
"aria-label": "move down"
|
|
2756
|
-
}, /*#__PURE__*/React__default.createElement(ArrowDownwardIcon, {
|
|
2757
|
-
fontSize: "small"
|
|
2758
|
-
})))) : /*#__PURE__*/React__default.createElement(React__default.Fragment, null, /*#__PURE__*/React__default.createElement(TextField, {
|
|
2924
|
+
}), /*#__PURE__*/React__default.createElement(MoveButtons, {
|
|
2925
|
+
onMoveUp: onMoveUp,
|
|
2926
|
+
onMoveDown: onMoveDown,
|
|
2927
|
+
isFirst: isFirst,
|
|
2928
|
+
isLast: isLast
|
|
2929
|
+
})) : /*#__PURE__*/React__default.createElement(React__default.Fragment, null, /*#__PURE__*/React__default.createElement(TextField, {
|
|
2759
2930
|
inputRef: inputRef,
|
|
2760
2931
|
value: editValue,
|
|
2761
2932
|
onChange: e => setEditValue(e.target.value),
|
|
@@ -2817,7 +2988,6 @@ const SortableChip$1 = ({
|
|
|
2817
2988
|
// the top-of-page Add form, so it stays visible regardless of scroll position.
|
|
2818
2989
|
const SortableComputedChip = ({
|
|
2819
2990
|
id,
|
|
2820
|
-
index,
|
|
2821
2991
|
name,
|
|
2822
2992
|
value,
|
|
2823
2993
|
onDelete,
|
|
@@ -2832,24 +3002,12 @@ const SortableComputedChip = ({
|
|
|
2832
3002
|
attributes,
|
|
2833
3003
|
listeners,
|
|
2834
3004
|
setNodeRef,
|
|
2835
|
-
|
|
2836
|
-
|
|
2837
|
-
isDragging
|
|
2838
|
-
} = useSortable({
|
|
2839
|
-
id
|
|
2840
|
-
});
|
|
3005
|
+
style
|
|
3006
|
+
} = useSortableRow(id);
|
|
2841
3007
|
const [isEditing, setIsEditing] = useState(false);
|
|
2842
3008
|
const [editName, setEditName] = useState("");
|
|
2843
3009
|
const [editValue, setEditValue] = useState("");
|
|
2844
3010
|
const containerRef = useRef(null);
|
|
2845
|
-
const style = {
|
|
2846
|
-
transform: CSS.Transform.toString(transform),
|
|
2847
|
-
transition,
|
|
2848
|
-
opacity: isDragging ? 0.5 : 1,
|
|
2849
|
-
display: "flex",
|
|
2850
|
-
alignItems: "center",
|
|
2851
|
-
width: "100%"
|
|
2852
|
-
};
|
|
2853
3011
|
|
|
2854
3012
|
// Handle click outside to cancel
|
|
2855
3013
|
useEffect(() => {
|
|
@@ -2870,13 +3028,13 @@ const SortableComputedChip = ({
|
|
|
2870
3028
|
setEditValue(value);
|
|
2871
3029
|
setIsEditing(true);
|
|
2872
3030
|
};
|
|
2873
|
-
const nameError = editName.trim() !== "" && isNameTaken(editName.trim(),
|
|
3031
|
+
const nameError = editName.trim() !== "" && isNameTaken(editName.trim(), name);
|
|
2874
3032
|
const handleSave = () => {
|
|
2875
3033
|
const trimmedName = editName.trim();
|
|
2876
3034
|
const trimmedValue = editValue.trim();
|
|
2877
3035
|
if (!trimmedName || !trimmedValue) return;
|
|
2878
|
-
if (isNameTaken(trimmedName,
|
|
2879
|
-
onUpdate(
|
|
3036
|
+
if (isNameTaken(trimmedName, name)) return;
|
|
3037
|
+
onUpdate(name, {
|
|
2880
3038
|
name: trimmedName,
|
|
2881
3039
|
value: trimmedValue
|
|
2882
3040
|
});
|
|
@@ -2912,21 +3070,9 @@ const SortableComputedChip = ({
|
|
|
2912
3070
|
opacity: 1
|
|
2913
3071
|
}
|
|
2914
3072
|
}
|
|
2915
|
-
}, /*#__PURE__*/React__default.createElement(
|
|
2916
|
-
|
|
2917
|
-
|
|
2918
|
-
alignItems: "center",
|
|
2919
|
-
cursor: "grab",
|
|
2920
|
-
"&:active": {
|
|
2921
|
-
cursor: "grabbing"
|
|
2922
|
-
}
|
|
2923
|
-
}
|
|
2924
|
-
}), /*#__PURE__*/React__default.createElement(DragIndicatorIcon, {
|
|
2925
|
-
sx: {
|
|
2926
|
-
cursor: "grab",
|
|
2927
|
-
color: "rgba(110, 110, 110, 0.62)"
|
|
2928
|
-
}
|
|
2929
|
-
})), !isEditing ? /*#__PURE__*/React__default.createElement(React__default.Fragment, null, /*#__PURE__*/React__default.createElement(Chip, {
|
|
3073
|
+
}, /*#__PURE__*/React__default.createElement(DragHandle, {
|
|
3074
|
+
listeners: listeners
|
|
3075
|
+
}), !isEditing ? /*#__PURE__*/React__default.createElement(React__default.Fragment, null, /*#__PURE__*/React__default.createElement(Chip, {
|
|
2930
3076
|
icon: /*#__PURE__*/React__default.createElement(FunctionsIcon, {
|
|
2931
3077
|
sx: {
|
|
2932
3078
|
fontSize: "15px !important"
|
|
@@ -2990,29 +3136,12 @@ const SortableComputedChip = ({
|
|
|
2990
3136
|
sx: {
|
|
2991
3137
|
flex: 1
|
|
2992
3138
|
}
|
|
2993
|
-
}), /*#__PURE__*/React__default.createElement(
|
|
2994
|
-
|
|
2995
|
-
|
|
2996
|
-
|
|
2997
|
-
|
|
2998
|
-
|
|
2999
|
-
transition: "opacity 0.2s"
|
|
3000
|
-
}
|
|
3001
|
-
}, /*#__PURE__*/React__default.createElement(IconButton$1, {
|
|
3002
|
-
size: "small",
|
|
3003
|
-
onClick: onMoveUp,
|
|
3004
|
-
disabled: isFirst,
|
|
3005
|
-
"aria-label": "move up"
|
|
3006
|
-
}, /*#__PURE__*/React__default.createElement(ArrowUpwardIcon, {
|
|
3007
|
-
fontSize: "small"
|
|
3008
|
-
})), /*#__PURE__*/React__default.createElement(IconButton$1, {
|
|
3009
|
-
size: "small",
|
|
3010
|
-
onClick: onMoveDown,
|
|
3011
|
-
disabled: isLast,
|
|
3012
|
-
"aria-label": "move down"
|
|
3013
|
-
}, /*#__PURE__*/React__default.createElement(ArrowDownwardIcon, {
|
|
3014
|
-
fontSize: "small"
|
|
3015
|
-
})))) : /*#__PURE__*/React__default.createElement(React__default.Fragment, null, /*#__PURE__*/React__default.createElement(TextField, {
|
|
3139
|
+
}), /*#__PURE__*/React__default.createElement(MoveButtons, {
|
|
3140
|
+
onMoveUp: onMoveUp,
|
|
3141
|
+
onMoveDown: onMoveDown,
|
|
3142
|
+
isFirst: isFirst,
|
|
3143
|
+
isLast: isLast
|
|
3144
|
+
})) : /*#__PURE__*/React__default.createElement(React__default.Fragment, null, /*#__PURE__*/React__default.createElement(TextField, {
|
|
3016
3145
|
value: editName,
|
|
3017
3146
|
onChange: e => setEditName(e.target.value),
|
|
3018
3147
|
onKeyDown: handleKeyDown,
|
|
@@ -3074,7 +3203,7 @@ const Dimensions = ({
|
|
|
3074
3203
|
savedDimensions = [],
|
|
3075
3204
|
onSaveDimension,
|
|
3076
3205
|
onRemoveDimension,
|
|
3077
|
-
|
|
3206
|
+
onUpdateDimensionSortOrder,
|
|
3078
3207
|
titleOverrides = {},
|
|
3079
3208
|
onUpdateTitle,
|
|
3080
3209
|
onResetTitle,
|
|
@@ -3084,7 +3213,8 @@ const Dimensions = ({
|
|
|
3084
3213
|
onSaveComputedDimension,
|
|
3085
3214
|
onUpdateComputedDimension,
|
|
3086
3215
|
onRemoveComputedDimension,
|
|
3087
|
-
|
|
3216
|
+
dimensionOrder = [],
|
|
3217
|
+
onReorderDimensionItems
|
|
3088
3218
|
}) => {
|
|
3089
3219
|
const reportingContext = useReportingContextOptional();
|
|
3090
3220
|
const parameters = reportingContext?.parameters;
|
|
@@ -3096,53 +3226,38 @@ const Dimensions = ({
|
|
|
3096
3226
|
const [computedValue, setComputedValue] = useState("");
|
|
3097
3227
|
|
|
3098
3228
|
// Setup drag and drop sensors
|
|
3099
|
-
const sensors =
|
|
3100
|
-
|
|
3101
|
-
|
|
3102
|
-
|
|
3103
|
-
//
|
|
3104
|
-
const
|
|
3105
|
-
|
|
3106
|
-
|
|
3107
|
-
|
|
3108
|
-
|
|
3109
|
-
|
|
3110
|
-
|
|
3111
|
-
|
|
3112
|
-
|
|
3113
|
-
|
|
3114
|
-
|
|
3115
|
-
|
|
3116
|
-
|
|
3117
|
-
|
|
3118
|
-
|
|
3119
|
-
|
|
3120
|
-
|
|
3121
|
-
|
|
3122
|
-
}
|
|
3123
|
-
};
|
|
3124
|
-
|
|
3125
|
-
// Handle move down
|
|
3126
|
-
const handleMoveDown = index => {
|
|
3127
|
-
if (index < savedDimensions.length - 1) {
|
|
3128
|
-
const newOrder = arrayMove(savedDimensions, index, index + 1);
|
|
3129
|
-
onReorderDimensions(newOrder);
|
|
3130
|
-
}
|
|
3131
|
-
};
|
|
3229
|
+
const sensors = useSortableListSensors();
|
|
3230
|
+
|
|
3231
|
+
// Resolve the unified dimensionOrder into the actual dimension / computed
|
|
3232
|
+
// dimension objects, so dimensions and computed dimensions can be shown
|
|
3233
|
+
// (and dragged) as a single list while staying stored in their own arrays.
|
|
3234
|
+
const dimensionByFullPath = Object.fromEntries(savedDimensions.map(d => [d.fullPath, d]));
|
|
3235
|
+
const computedDimensionByName = Object.fromEntries(savedComputedDimensions.map(cd => [cd.name, cd]));
|
|
3236
|
+
const orderedItems = dimensionOrder.map(entry => entry.type === "dimension" ? {
|
|
3237
|
+
kind: "dimension",
|
|
3238
|
+
key: entry.key,
|
|
3239
|
+
data: dimensionByFullPath[entry.key]
|
|
3240
|
+
} : {
|
|
3241
|
+
kind: "computed",
|
|
3242
|
+
key: entry.key,
|
|
3243
|
+
data: computedDimensionByName[entry.key]
|
|
3244
|
+
}).filter(item => item.data);
|
|
3245
|
+
|
|
3246
|
+
// Handle drag/move for the unified list
|
|
3247
|
+
const {
|
|
3248
|
+
handleDragEnd,
|
|
3249
|
+
handleMoveUp,
|
|
3250
|
+
handleMoveDown
|
|
3251
|
+
} = makeListReorderHandlers(dimensionOrder, onReorderDimensionItems);
|
|
3132
3252
|
|
|
3133
|
-
// Handle sort order change
|
|
3134
|
-
const handleSortOrderChange = (
|
|
3135
|
-
|
|
3136
|
-
newDimensions[index] = {
|
|
3137
|
-
...newDimensions[index],
|
|
3138
|
-
sortOrder
|
|
3139
|
-
};
|
|
3140
|
-
onReorderDimensions(newDimensions);
|
|
3253
|
+
// Handle sort order change (dimensions only - computed dimensions have no order_by)
|
|
3254
|
+
const handleSortOrderChange = (fullPath, sortOrder) => {
|
|
3255
|
+
onUpdateDimensionSortOrder(fullPath, sortOrder);
|
|
3141
3256
|
};
|
|
3142
3257
|
|
|
3143
3258
|
// Computed Dimensions: { name, value } pairs independent of any provider.
|
|
3144
3259
|
// Their `value` is a raw SQL literal/expression sent to the API as-is.
|
|
3145
|
-
const isComputedNameTaken = (name,
|
|
3260
|
+
const isComputedNameTaken = (name, excludeName = null) => savedComputedDimensions.some(cd => cd.name === name) && name !== excludeName;
|
|
3146
3261
|
const computedNameError = computedName.trim() !== "" && isComputedNameTaken(computedName.trim());
|
|
3147
3262
|
const handleAddComputedClick = () => {
|
|
3148
3263
|
setIsAddingComputed(true);
|
|
@@ -3166,27 +3281,6 @@ const Dimensions = ({
|
|
|
3166
3281
|
});
|
|
3167
3282
|
handleCancelComputed();
|
|
3168
3283
|
};
|
|
3169
|
-
const handleComputedDragEnd = event => {
|
|
3170
|
-
const {
|
|
3171
|
-
active,
|
|
3172
|
-
over
|
|
3173
|
-
} = event;
|
|
3174
|
-
if (over && active.id !== over.id) {
|
|
3175
|
-
const oldIndex = savedComputedDimensions.findIndex((_, i) => i === active.id);
|
|
3176
|
-
const newIndex = savedComputedDimensions.findIndex((_, i) => i === over.id);
|
|
3177
|
-
onReorderComputedDimensions(arrayMove(savedComputedDimensions, oldIndex, newIndex));
|
|
3178
|
-
}
|
|
3179
|
-
};
|
|
3180
|
-
const handleMoveComputedUp = index => {
|
|
3181
|
-
if (index > 0) {
|
|
3182
|
-
onReorderComputedDimensions(arrayMove(savedComputedDimensions, index, index - 1));
|
|
3183
|
-
}
|
|
3184
|
-
};
|
|
3185
|
-
const handleMoveComputedDown = index => {
|
|
3186
|
-
if (index < savedComputedDimensions.length - 1) {
|
|
3187
|
-
onReorderComputedDimensions(arrayMove(savedComputedDimensions, index, index + 1));
|
|
3188
|
-
}
|
|
3189
|
-
};
|
|
3190
3284
|
|
|
3191
3285
|
// Get the current provider based on selection chain
|
|
3192
3286
|
const getCurrentProvider = () => {
|
|
@@ -3629,7 +3723,7 @@ const Dimensions = ({
|
|
|
3629
3723
|
boxShadow: "none"
|
|
3630
3724
|
}
|
|
3631
3725
|
}
|
|
3632
|
-
}, "Save Dimension"))),
|
|
3726
|
+
}, "Save Dimension"))), orderedItems.length > 0 && /*#__PURE__*/React__default.createElement(Box$1, {
|
|
3633
3727
|
sx: {
|
|
3634
3728
|
marginTop: 0
|
|
3635
3729
|
}
|
|
@@ -3647,81 +3741,40 @@ const Dimensions = ({
|
|
|
3647
3741
|
color: "#666",
|
|
3648
3742
|
marginBottom: 2
|
|
3649
3743
|
}
|
|
3650
|
-
}, "Drag to reorder or use arrows"), /*#__PURE__*/React__default.createElement(
|
|
3744
|
+
}, "Drag to reorder or use arrows"), /*#__PURE__*/React__default.createElement(SortableListContext, {
|
|
3651
3745
|
sensors: sensors,
|
|
3652
|
-
|
|
3746
|
+
itemIds: orderedItems.map((_, index) => index),
|
|
3653
3747
|
onDragEnd: handleDragEnd
|
|
3654
|
-
}, /*#__PURE__*/React__default.createElement(
|
|
3655
|
-
|
|
3656
|
-
strategy: verticalListSortingStrategy
|
|
3657
|
-
}, /*#__PURE__*/React__default.createElement(Box$1, {
|
|
3658
|
-
sx: {
|
|
3659
|
-
display: "flex",
|
|
3660
|
-
flexDirection: "column",
|
|
3661
|
-
gap: 1
|
|
3662
|
-
}
|
|
3663
|
-
}, savedDimensions.map((dim, index) => /*#__PURE__*/React__default.createElement(SortableChip$1, {
|
|
3664
|
-
key: index,
|
|
3748
|
+
}, orderedItems.map((item, index) => item.kind === "dimension" ? /*#__PURE__*/React__default.createElement(SortableChip$1, {
|
|
3749
|
+
key: `dim-${item.key}`,
|
|
3665
3750
|
id: index,
|
|
3666
|
-
label:
|
|
3667
|
-
fullLabel: `${formatProviderPath(
|
|
3668
|
-
onDelete: () => onRemoveDimension(
|
|
3751
|
+
label: item.data.dimensionTitle,
|
|
3752
|
+
fullLabel: `${formatProviderPath(item.data)} → ${item.data.dimensionTitle} (${item.data.fullPath})`,
|
|
3753
|
+
onDelete: () => onRemoveDimension(item.key),
|
|
3669
3754
|
onMoveUp: () => handleMoveUp(index),
|
|
3670
3755
|
onMoveDown: () => handleMoveDown(index),
|
|
3671
3756
|
isFirst: index === 0,
|
|
3672
|
-
isLast: index ===
|
|
3673
|
-
sortOrder:
|
|
3674
|
-
onSortOrderChange: sortOrder => handleSortOrderChange(
|
|
3675
|
-
fullPath:
|
|
3676
|
-
defaultTitle:
|
|
3677
|
-
customTitle: titleOverrides[
|
|
3757
|
+
isLast: index === orderedItems.length - 1,
|
|
3758
|
+
sortOrder: item.data.sortOrder || null,
|
|
3759
|
+
onSortOrderChange: sortOrder => handleSortOrderChange(item.key, sortOrder),
|
|
3760
|
+
fullPath: item.data.fullPath,
|
|
3761
|
+
defaultTitle: item.data.dimensionTitle,
|
|
3762
|
+
customTitle: titleOverrides[item.data.fullPath],
|
|
3678
3763
|
onUpdateTitle: onUpdateTitle,
|
|
3679
3764
|
onResetTitle: onResetTitle
|
|
3680
|
-
})
|
|
3681
|
-
|
|
3682
|
-
marginTop: 2
|
|
3683
|
-
}
|
|
3684
|
-
}, /*#__PURE__*/React__default.createElement(Typography$1, {
|
|
3685
|
-
variant: "h6",
|
|
3686
|
-
sx: {
|
|
3687
|
-
fontSize: "16px",
|
|
3688
|
-
fontWeight: 600,
|
|
3689
|
-
marginTop: 2,
|
|
3690
|
-
color: "rgb(37, 37, 37)"
|
|
3691
|
-
}
|
|
3692
|
-
}, "Saved Computed Dimensions"), /*#__PURE__*/React__default.createElement(Typography$1, {
|
|
3693
|
-
sx: {
|
|
3694
|
-
fontSize: "13px",
|
|
3695
|
-
color: "#666",
|
|
3696
|
-
marginBottom: 2
|
|
3697
|
-
}
|
|
3698
|
-
}, "Drag to reorder or use arrows"), /*#__PURE__*/React__default.createElement(DndContext, {
|
|
3699
|
-
sensors: sensors,
|
|
3700
|
-
collisionDetection: closestCenter,
|
|
3701
|
-
onDragEnd: handleComputedDragEnd
|
|
3702
|
-
}, /*#__PURE__*/React__default.createElement(SortableContext, {
|
|
3703
|
-
items: savedComputedDimensions.map((_, index) => index),
|
|
3704
|
-
strategy: verticalListSortingStrategy
|
|
3705
|
-
}, /*#__PURE__*/React__default.createElement(Box$1, {
|
|
3706
|
-
sx: {
|
|
3707
|
-
display: "flex",
|
|
3708
|
-
flexDirection: "column",
|
|
3709
|
-
gap: 1
|
|
3710
|
-
}
|
|
3711
|
-
}, savedComputedDimensions.map((cd, index) => /*#__PURE__*/React__default.createElement(SortableComputedChip, {
|
|
3712
|
-
key: index,
|
|
3765
|
+
}) : /*#__PURE__*/React__default.createElement(SortableComputedChip, {
|
|
3766
|
+
key: `comp-${item.key}`,
|
|
3713
3767
|
id: index,
|
|
3714
|
-
|
|
3715
|
-
|
|
3716
|
-
value: cd.value,
|
|
3768
|
+
name: item.data.name,
|
|
3769
|
+
value: item.data.value,
|
|
3717
3770
|
onUpdate: onUpdateComputedDimension,
|
|
3718
3771
|
isNameTaken: isComputedNameTaken,
|
|
3719
|
-
onDelete: () => onRemoveComputedDimension(
|
|
3720
|
-
onMoveUp: () =>
|
|
3721
|
-
onMoveDown: () =>
|
|
3772
|
+
onDelete: () => onRemoveComputedDimension(item.key),
|
|
3773
|
+
onMoveUp: () => handleMoveUp(index),
|
|
3774
|
+
onMoveDown: () => handleMoveDown(index),
|
|
3722
3775
|
isFirst: index === 0,
|
|
3723
|
-
isLast: index ===
|
|
3724
|
-
})))))
|
|
3776
|
+
isLast: index === orderedItems.length - 1
|
|
3777
|
+
})))));
|
|
3725
3778
|
};
|
|
3726
3779
|
|
|
3727
3780
|
const MetricSourceIcon = ({
|
|
@@ -3796,20 +3849,8 @@ const SortableChip = ({
|
|
|
3796
3849
|
attributes,
|
|
3797
3850
|
listeners,
|
|
3798
3851
|
setNodeRef,
|
|
3799
|
-
|
|
3800
|
-
|
|
3801
|
-
isDragging
|
|
3802
|
-
} = useSortable({
|
|
3803
|
-
id
|
|
3804
|
-
});
|
|
3805
|
-
const style = {
|
|
3806
|
-
transform: CSS.Transform.toString(transform),
|
|
3807
|
-
transition,
|
|
3808
|
-
opacity: isDragging ? 0.5 : 1,
|
|
3809
|
-
display: 'flex',
|
|
3810
|
-
alignItems: 'center',
|
|
3811
|
-
width: '100%'
|
|
3812
|
-
};
|
|
3852
|
+
style
|
|
3853
|
+
} = useSortableRow(id);
|
|
3813
3854
|
|
|
3814
3855
|
// Focus input when entering edit mode
|
|
3815
3856
|
useEffect(() => {
|
|
@@ -3887,21 +3928,9 @@ const SortableChip = ({
|
|
|
3887
3928
|
opacity: 1 // show icons on hover
|
|
3888
3929
|
}
|
|
3889
3930
|
}
|
|
3890
|
-
}, /*#__PURE__*/React__default.createElement(
|
|
3891
|
-
|
|
3892
|
-
|
|
3893
|
-
alignItems: "center",
|
|
3894
|
-
cursor: "grab",
|
|
3895
|
-
"&:active": {
|
|
3896
|
-
cursor: "grabbing"
|
|
3897
|
-
}
|
|
3898
|
-
}
|
|
3899
|
-
}), /*#__PURE__*/React__default.createElement(DragIndicatorIcon, {
|
|
3900
|
-
sx: {
|
|
3901
|
-
cursor: "grab",
|
|
3902
|
-
color: "rgba(110, 110, 110, 0.62)"
|
|
3903
|
-
}
|
|
3904
|
-
})), !isEditing ? /*#__PURE__*/React__default.createElement(React__default.Fragment, null, source && /*#__PURE__*/React__default.createElement(Box$1, {
|
|
3931
|
+
}, /*#__PURE__*/React__default.createElement(DragHandle, {
|
|
3932
|
+
listeners: listeners
|
|
3933
|
+
}), !isEditing ? /*#__PURE__*/React__default.createElement(React__default.Fragment, null, source && /*#__PURE__*/React__default.createElement(Box$1, {
|
|
3905
3934
|
sx: {
|
|
3906
3935
|
display: 'flex',
|
|
3907
3936
|
alignItems: 'center',
|
|
@@ -3963,29 +3992,12 @@ const SortableChip = ({
|
|
|
3963
3992
|
sx: {
|
|
3964
3993
|
flex: 1
|
|
3965
3994
|
}
|
|
3966
|
-
}), /*#__PURE__*/React__default.createElement(
|
|
3967
|
-
|
|
3968
|
-
|
|
3969
|
-
|
|
3970
|
-
|
|
3971
|
-
|
|
3972
|
-
transition: "opacity 0.2s"
|
|
3973
|
-
}
|
|
3974
|
-
}, /*#__PURE__*/React__default.createElement(IconButton$1, {
|
|
3975
|
-
size: "small",
|
|
3976
|
-
onClick: onMoveUp,
|
|
3977
|
-
disabled: isFirst,
|
|
3978
|
-
"aria-label": "move up"
|
|
3979
|
-
}, /*#__PURE__*/React__default.createElement(ArrowUpwardIcon, {
|
|
3980
|
-
fontSize: "small"
|
|
3981
|
-
})), /*#__PURE__*/React__default.createElement(IconButton$1, {
|
|
3982
|
-
size: "small",
|
|
3983
|
-
onClick: onMoveDown,
|
|
3984
|
-
disabled: isLast,
|
|
3985
|
-
"aria-label": "move down"
|
|
3986
|
-
}, /*#__PURE__*/React__default.createElement(ArrowDownwardIcon, {
|
|
3987
|
-
fontSize: "small"
|
|
3988
|
-
})))) : /*#__PURE__*/React__default.createElement(React__default.Fragment, null, /*#__PURE__*/React__default.createElement(TextField, {
|
|
3995
|
+
}), /*#__PURE__*/React__default.createElement(MoveButtons, {
|
|
3996
|
+
onMoveUp: onMoveUp,
|
|
3997
|
+
onMoveDown: onMoveDown,
|
|
3998
|
+
isFirst: isFirst,
|
|
3999
|
+
isLast: isLast
|
|
4000
|
+
})) : /*#__PURE__*/React__default.createElement(React__default.Fragment, null, /*#__PURE__*/React__default.createElement(TextField, {
|
|
3989
4001
|
inputRef: inputRef,
|
|
3990
4002
|
value: editValue,
|
|
3991
4003
|
onChange: e => setEditValue(e.target.value),
|
|
@@ -4060,39 +4072,14 @@ const Metrics = ({
|
|
|
4060
4072
|
const [selectedMetric, setSelectedMetric] = useState('');
|
|
4061
4073
|
|
|
4062
4074
|
// Setup drag and drop sensors
|
|
4063
|
-
const sensors =
|
|
4064
|
-
coordinateGetter: sortableKeyboardCoordinates
|
|
4065
|
-
}));
|
|
4066
|
-
|
|
4067
|
-
// Handle drag end
|
|
4068
|
-
const handleDragEnd = event => {
|
|
4069
|
-
const {
|
|
4070
|
-
active,
|
|
4071
|
-
over
|
|
4072
|
-
} = event;
|
|
4073
|
-
if (over && active.id !== over.id) {
|
|
4074
|
-
const oldIndex = savedMetrics.findIndex((_, i) => i === active.id);
|
|
4075
|
-
const newIndex = savedMetrics.findIndex((_, i) => i === over.id);
|
|
4076
|
-
const newOrder = arrayMove(savedMetrics, oldIndex, newIndex);
|
|
4077
|
-
onReorderMetrics(newOrder);
|
|
4078
|
-
}
|
|
4079
|
-
};
|
|
4080
|
-
|
|
4081
|
-
// Handle move up
|
|
4082
|
-
const handleMoveUp = index => {
|
|
4083
|
-
if (index > 0) {
|
|
4084
|
-
const newOrder = arrayMove(savedMetrics, index, index - 1);
|
|
4085
|
-
onReorderMetrics(newOrder);
|
|
4086
|
-
}
|
|
4087
|
-
};
|
|
4075
|
+
const sensors = useSortableListSensors();
|
|
4088
4076
|
|
|
4089
|
-
// Handle move
|
|
4090
|
-
const
|
|
4091
|
-
|
|
4092
|
-
|
|
4093
|
-
|
|
4094
|
-
|
|
4095
|
-
};
|
|
4077
|
+
// Handle drag/move
|
|
4078
|
+
const {
|
|
4079
|
+
handleDragEnd,
|
|
4080
|
+
handleMoveUp,
|
|
4081
|
+
handleMoveDown
|
|
4082
|
+
} = makeListReorderHandlers(savedMetrics, onReorderMetrics);
|
|
4096
4083
|
|
|
4097
4084
|
// Get the current provider based on selection chain
|
|
4098
4085
|
const getCurrentProvider = () => {
|
|
@@ -4431,19 +4418,10 @@ const Metrics = ({
|
|
|
4431
4418
|
marginBottom: 1,
|
|
4432
4419
|
color: "rgb(37, 37, 37)"
|
|
4433
4420
|
}
|
|
4434
|
-
}, "Saved Metrics"), /*#__PURE__*/React__default.createElement(
|
|
4421
|
+
}, "Saved Metrics"), /*#__PURE__*/React__default.createElement(SortableListContext, {
|
|
4435
4422
|
sensors: sensors,
|
|
4436
|
-
|
|
4423
|
+
itemIds: savedMetrics.map((_, index) => index),
|
|
4437
4424
|
onDragEnd: handleDragEnd
|
|
4438
|
-
}, /*#__PURE__*/React__default.createElement(SortableContext, {
|
|
4439
|
-
items: savedMetrics.map((_, index) => index),
|
|
4440
|
-
strategy: verticalListSortingStrategy
|
|
4441
|
-
}, /*#__PURE__*/React__default.createElement(Box$1, {
|
|
4442
|
-
sx: {
|
|
4443
|
-
display: 'flex',
|
|
4444
|
-
flexDirection: 'column',
|
|
4445
|
-
gap: 1
|
|
4446
|
-
}
|
|
4447
4425
|
}, savedMetrics.map((metric, index) => /*#__PURE__*/React__default.createElement(SortableChip, {
|
|
4448
4426
|
key: index,
|
|
4449
4427
|
id: index,
|
|
@@ -4460,7 +4438,7 @@ const Metrics = ({
|
|
|
4460
4438
|
onUpdateTitle: onUpdateTitle,
|
|
4461
4439
|
onResetTitle: onResetTitle,
|
|
4462
4440
|
source: metric.metric?.source
|
|
4463
|
-
})))))
|
|
4441
|
+
})))));
|
|
4464
4442
|
};
|
|
4465
4443
|
|
|
4466
4444
|
const Filters = ({
|
|
@@ -5687,9 +5665,208 @@ const Filters = ({
|
|
|
5687
5665
|
}))));
|
|
5688
5666
|
};
|
|
5689
5667
|
|
|
5690
|
-
|
|
5691
|
-
|
|
5692
|
-
|
|
5668
|
+
const KIND_LABEL = {
|
|
5669
|
+
dimension: "Dimension",
|
|
5670
|
+
computed: "Computed",
|
|
5671
|
+
metric: "Metric"
|
|
5672
|
+
};
|
|
5673
|
+
const KindChip = ({
|
|
5674
|
+
kind,
|
|
5675
|
+
source
|
|
5676
|
+
}) => {
|
|
5677
|
+
if (kind === "metric") {
|
|
5678
|
+
return /*#__PURE__*/React__default.createElement(Box$1, {
|
|
5679
|
+
sx: {
|
|
5680
|
+
display: "flex",
|
|
5681
|
+
alignItems: "center",
|
|
5682
|
+
flexShrink: 0
|
|
5683
|
+
}
|
|
5684
|
+
}, /*#__PURE__*/React__default.createElement(MetricSourceIcon, {
|
|
5685
|
+
source: source
|
|
5686
|
+
}));
|
|
5687
|
+
}
|
|
5688
|
+
return /*#__PURE__*/React__default.createElement(Chip, {
|
|
5689
|
+
icon: kind === "computed" ? /*#__PURE__*/React__default.createElement(CalculateIcon, {
|
|
5690
|
+
sx: {
|
|
5691
|
+
fontSize: "15px !important"
|
|
5692
|
+
}
|
|
5693
|
+
}) : undefined,
|
|
5694
|
+
label: KIND_LABEL[kind],
|
|
5695
|
+
size: "small",
|
|
5696
|
+
sx: {
|
|
5697
|
+
fontWeight: 500,
|
|
5698
|
+
backgroundColor: "rgba(70, 134, 128, 0.1)",
|
|
5699
|
+
color: "rgb(70, 134, 128)",
|
|
5700
|
+
flexShrink: 0
|
|
5701
|
+
}
|
|
5702
|
+
});
|
|
5703
|
+
};
|
|
5704
|
+
|
|
5705
|
+
// Read-only sortable row: this tab only reorders columns. Adding, removing,
|
|
5706
|
+
// and renaming stay on the Dimensions/Metrics tabs so there's one place each
|
|
5707
|
+
// field is managed.
|
|
5708
|
+
const SortableColumnRow = ({
|
|
5709
|
+
id,
|
|
5710
|
+
label,
|
|
5711
|
+
fullPath,
|
|
5712
|
+
kind,
|
|
5713
|
+
source,
|
|
5714
|
+
onMoveUp,
|
|
5715
|
+
onMoveDown,
|
|
5716
|
+
isFirst,
|
|
5717
|
+
isLast
|
|
5718
|
+
}) => {
|
|
5719
|
+
const reportingContext = useReportingContextOptional();
|
|
5720
|
+
const parameters = reportingContext?.parameters;
|
|
5721
|
+
const {
|
|
5722
|
+
attributes,
|
|
5723
|
+
listeners,
|
|
5724
|
+
setNodeRef,
|
|
5725
|
+
style
|
|
5726
|
+
} = useSortableRow(id);
|
|
5727
|
+
const displayLabel = interpolateTitle(label, parameters);
|
|
5728
|
+
return /*#__PURE__*/React__default.createElement("div", _extends({
|
|
5729
|
+
ref: setNodeRef,
|
|
5730
|
+
style: style
|
|
5731
|
+
}, attributes), /*#__PURE__*/React__default.createElement(Box$1, {
|
|
5732
|
+
sx: {
|
|
5733
|
+
display: "flex",
|
|
5734
|
+
alignItems: "center",
|
|
5735
|
+
width: "100%",
|
|
5736
|
+
gap: 1,
|
|
5737
|
+
backgroundColor: "white",
|
|
5738
|
+
border: "1px solid #e0e0e0",
|
|
5739
|
+
borderRadius: 2,
|
|
5740
|
+
padding: 1,
|
|
5741
|
+
transition: "transform 0.2s ease, box-shadow 0.2s ease",
|
|
5742
|
+
"&:hover .hover-icons": {
|
|
5743
|
+
opacity: 1
|
|
5744
|
+
}
|
|
5745
|
+
}
|
|
5746
|
+
}, /*#__PURE__*/React__default.createElement(DragHandle, {
|
|
5747
|
+
listeners: listeners
|
|
5748
|
+
}), /*#__PURE__*/React__default.createElement(KindChip, {
|
|
5749
|
+
kind: kind,
|
|
5750
|
+
source: source
|
|
5751
|
+
}), /*#__PURE__*/React__default.createElement(Tooltip$1, {
|
|
5752
|
+
title: fullPath,
|
|
5753
|
+
arrow: true,
|
|
5754
|
+
placement: "top"
|
|
5755
|
+
}, /*#__PURE__*/React__default.createElement(Typography$1, {
|
|
5756
|
+
variant: "h6",
|
|
5757
|
+
sx: {
|
|
5758
|
+
fontWeight: 500,
|
|
5759
|
+
color: "#1a1a1a",
|
|
5760
|
+
fontSize: "14px",
|
|
5761
|
+
whiteSpace: "nowrap",
|
|
5762
|
+
overflow: "hidden",
|
|
5763
|
+
textOverflow: "ellipsis"
|
|
5764
|
+
}
|
|
5765
|
+
}, displayLabel)), /*#__PURE__*/React__default.createElement(Box$1, {
|
|
5766
|
+
sx: {
|
|
5767
|
+
flex: 1
|
|
5768
|
+
}
|
|
5769
|
+
}), /*#__PURE__*/React__default.createElement(MoveButtons, {
|
|
5770
|
+
onMoveUp: onMoveUp,
|
|
5771
|
+
onMoveDown: onMoveDown,
|
|
5772
|
+
isFirst: isFirst,
|
|
5773
|
+
isLast: isLast
|
|
5774
|
+
})));
|
|
5775
|
+
};
|
|
5776
|
+
const ColumnOrder = ({
|
|
5777
|
+
orderedDimensionItems = [],
|
|
5778
|
+
metrics = [],
|
|
5779
|
+
columnOrder = [],
|
|
5780
|
+
onReorderColumnItems,
|
|
5781
|
+
titleOverrides = {
|
|
5782
|
+
dimensions: {},
|
|
5783
|
+
metrics: {}
|
|
5784
|
+
}
|
|
5785
|
+
}) => {
|
|
5786
|
+
const isCustomized = columnOrder.length > 0;
|
|
5787
|
+
const resolvedItems = resolveColumnOrder(columnOrder, orderedDimensionItems, metrics);
|
|
5788
|
+
const sensors = useSortableListSensors();
|
|
5789
|
+
const reorderResolved = newResolvedItems => {
|
|
5790
|
+
onReorderColumnItems(newResolvedItems.map(item => ({
|
|
5791
|
+
type: item.kind,
|
|
5792
|
+
key: item.key
|
|
5793
|
+
})));
|
|
5794
|
+
};
|
|
5795
|
+
const {
|
|
5796
|
+
handleDragEnd,
|
|
5797
|
+
handleMoveUp,
|
|
5798
|
+
handleMoveDown
|
|
5799
|
+
} = makeListReorderHandlers(resolvedItems, reorderResolved);
|
|
5800
|
+
const labelFor = item => {
|
|
5801
|
+
if (item.kind === "computed") return item.data.name;
|
|
5802
|
+
if (item.kind === "metric") {
|
|
5803
|
+
return titleOverrides.metrics[item.key] || item.data.metricTitle || item.key;
|
|
5804
|
+
}
|
|
5805
|
+
return titleOverrides.dimensions[item.key] || item.data.dimensionTitle || item.key;
|
|
5806
|
+
};
|
|
5807
|
+
return /*#__PURE__*/React__default.createElement("div", null, /*#__PURE__*/React__default.createElement(Box$1, {
|
|
5808
|
+
sx: {
|
|
5809
|
+
display: "flex",
|
|
5810
|
+
justifyContent: "space-between",
|
|
5811
|
+
alignItems: "flex-start",
|
|
5812
|
+
mb: 2,
|
|
5813
|
+
gap: 2
|
|
5814
|
+
}
|
|
5815
|
+
}, /*#__PURE__*/React__default.createElement(Typography$1, {
|
|
5816
|
+
sx: {
|
|
5817
|
+
fontSize: "13px",
|
|
5818
|
+
color: "#666"
|
|
5819
|
+
}
|
|
5820
|
+
}, "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, {
|
|
5821
|
+
title: "Discard the custom order and go back to the default (Dimensions, then Metrics)",
|
|
5822
|
+
arrow: true
|
|
5823
|
+
}, /*#__PURE__*/React__default.createElement(Button, {
|
|
5824
|
+
variant: "outlined",
|
|
5825
|
+
size: "small",
|
|
5826
|
+
startIcon: /*#__PURE__*/React__default.createElement(RestartAltIcon, null),
|
|
5827
|
+
onClick: () => onReorderColumnItems([]),
|
|
5828
|
+
sx: {
|
|
5829
|
+
height: "36px",
|
|
5830
|
+
flexShrink: 0,
|
|
5831
|
+
fontFamily: "system-ui",
|
|
5832
|
+
fontSize: "13px",
|
|
5833
|
+
fontWeight: 500,
|
|
5834
|
+
borderRadius: "8px",
|
|
5835
|
+
boxShadow: "none",
|
|
5836
|
+
textTransform: "none",
|
|
5837
|
+
borderColor: "#e0e0e0",
|
|
5838
|
+
color: "rgb(37, 37, 37)",
|
|
5839
|
+
"&:hover": {
|
|
5840
|
+
backgroundColor: "#f5f5f5",
|
|
5841
|
+
borderColor: "#d0d0d0"
|
|
5842
|
+
}
|
|
5843
|
+
}
|
|
5844
|
+
}, "Reset to Default"))), resolvedItems.length === 0 ? /*#__PURE__*/React__default.createElement(Typography$1, {
|
|
5845
|
+
sx: {
|
|
5846
|
+
fontSize: "13px",
|
|
5847
|
+
color: "#999"
|
|
5848
|
+
}
|
|
5849
|
+
}, "Add dimensions or metrics in the other tabs first.") : /*#__PURE__*/React__default.createElement(SortableListContext, {
|
|
5850
|
+
sensors: sensors,
|
|
5851
|
+
itemIds: resolvedItems.map((_, index) => index),
|
|
5852
|
+
onDragEnd: handleDragEnd
|
|
5853
|
+
}, resolvedItems.map((item, index) => /*#__PURE__*/React__default.createElement(SortableColumnRow, {
|
|
5854
|
+
key: `${item.kind}-${item.key}`,
|
|
5855
|
+
id: index,
|
|
5856
|
+
label: labelFor(item),
|
|
5857
|
+
fullPath: item.kind === "computed" ? item.data.value : item.key,
|
|
5858
|
+
kind: item.kind,
|
|
5859
|
+
source: item.kind === "metric" ? item.data.metric?.source : undefined,
|
|
5860
|
+
onMoveUp: () => handleMoveUp(index),
|
|
5861
|
+
onMoveDown: () => handleMoveDown(index),
|
|
5862
|
+
isFirst: index === 0,
|
|
5863
|
+
isLast: index === resolvedItems.length - 1
|
|
5864
|
+
}))));
|
|
5865
|
+
};
|
|
5866
|
+
|
|
5867
|
+
// Default numeral.js formats applied when a dimension/metric definition has a
|
|
5868
|
+
// known type but no explicit `format` set on the provider.
|
|
5869
|
+
const DEFAULT_FORMATS_BY_TYPE = {
|
|
5693
5870
|
currency: '0,0.00',
|
|
5694
5871
|
integer: '0,0'
|
|
5695
5872
|
};
|
|
@@ -5701,11 +5878,107 @@ const formatValue = (value, def) => {
|
|
|
5701
5878
|
return def?.prefix ? `${def.prefix} ${formatted}` : formatted;
|
|
5702
5879
|
};
|
|
5703
5880
|
const isNumericType = type => type === 'integer' || type === 'currency';
|
|
5881
|
+
|
|
5882
|
+
// Builds a single DataGrid column definition for one resolved column item
|
|
5883
|
+
// (a dimension, computed dimension, or metric). Field naming logic:
|
|
5884
|
+
// - Dimensions from base provider: baseAlias.field -> baseAlias_field
|
|
5885
|
+
// - Dimensions from nested provider: baseAlias_rel1_rel2.field -> rel1_rel2_field (drops base alias)
|
|
5886
|
+
// - Computed dimensions have no provider/relations chain, so the API returns
|
|
5887
|
+
// each row keyed by the computed dimension's own `name`.
|
|
5888
|
+
// - Metrics: API returns metric keys in different forms depending on path depth
|
|
5889
|
+
// (see comment inline below), normalised to match the row-key normalisation
|
|
5890
|
+
// done when building `rows`.
|
|
5891
|
+
const buildColumn = (item, titleOverrides, parameters) => {
|
|
5892
|
+
if (item.kind === 'computed') {
|
|
5893
|
+
const cd = item.data;
|
|
5894
|
+
return {
|
|
5895
|
+
field: cd.name,
|
|
5896
|
+
headerName: cd.name,
|
|
5897
|
+
flex: 1,
|
|
5898
|
+
minWidth: 150,
|
|
5899
|
+
type: 'string'
|
|
5900
|
+
};
|
|
5901
|
+
}
|
|
5902
|
+
if (item.kind === 'metric') {
|
|
5903
|
+
const metric = item.data;
|
|
5904
|
+
const metricDef = metric.metric;
|
|
5905
|
+
let fieldName;
|
|
5906
|
+
const dotCount = (metric.fullPath.match(/\./g) || []).length;
|
|
5907
|
+
if (dotCount >= 2) {
|
|
5908
|
+
// 3+-part namespaced path: API returns the full dotted path as the key.
|
|
5909
|
+
// Normalise dots to underscores to match the row key normalisation below.
|
|
5910
|
+
// Example: "mc.dkpi.activeAgreementsCount" -> "mc_dkpi_activeAgreementsCount"
|
|
5911
|
+
fieldName = metric.fullPath.replace(/\./g, '_');
|
|
5912
|
+
} else if (metric.relations && metric.relations.length > 0) {
|
|
5913
|
+
// 2-part path from a nested provider: API drops the base provider alias.
|
|
5914
|
+
// Example: "mc_fa.total_amount" -> "fa_total_amount"
|
|
5915
|
+
const parts = metric.fullPath.split('.');
|
|
5916
|
+
const pathWithoutField = parts[0]; // e.g., "mc_fa"
|
|
5917
|
+
const field = parts[1]; // e.g., "total_amount"
|
|
5918
|
+
|
|
5919
|
+
const pathParts = pathWithoutField.split('_');
|
|
5920
|
+
pathParts.shift(); // remove base provider alias
|
|
5921
|
+
const pathWithoutBase = pathParts.join('_');
|
|
5922
|
+
fieldName = pathWithoutBase ? `${pathWithoutBase}_${field}` : field;
|
|
5923
|
+
} else {
|
|
5924
|
+
// 2-part path from the base provider: API returns just the metric name.
|
|
5925
|
+
// Example: "mc.record_count" -> "record_count"
|
|
5926
|
+
fieldName = metric.metricName;
|
|
5927
|
+
}
|
|
5928
|
+
const headerName = interpolateTitle(titleOverrides.metrics[metric.fullPath] || metric.metricTitle || fieldName, parameters);
|
|
5929
|
+
return {
|
|
5930
|
+
field: fieldName,
|
|
5931
|
+
headerName: headerName,
|
|
5932
|
+
flex: 1,
|
|
5933
|
+
minWidth: 150,
|
|
5934
|
+
type: isNumericType(metricDef?.type) ? 'number' : 'string',
|
|
5935
|
+
renderHeader: () => /*#__PURE__*/React__default.createElement(Box$1, {
|
|
5936
|
+
sx: {
|
|
5937
|
+
display: 'flex',
|
|
5938
|
+
alignItems: 'center',
|
|
5939
|
+
gap: 0.5
|
|
5940
|
+
}
|
|
5941
|
+
}, /*#__PURE__*/React__default.createElement(MetricSourceIcon, {
|
|
5942
|
+
source: metricDef?.source
|
|
5943
|
+
}), /*#__PURE__*/React__default.createElement("span", null, headerName)),
|
|
5944
|
+
valueFormatter: value => formatValue(value, metricDef)
|
|
5945
|
+
};
|
|
5946
|
+
}
|
|
5947
|
+
|
|
5948
|
+
// Dimension
|
|
5949
|
+
const dim = item.data;
|
|
5950
|
+
let fieldName;
|
|
5951
|
+
if (dim.relations && dim.relations.length > 0) {
|
|
5952
|
+
// From nested provider: drop the base provider alias
|
|
5953
|
+
// Example: ft_fa_db.currency -> fa_db_currency
|
|
5954
|
+
const parts = dim.fullPath.split('.');
|
|
5955
|
+
const pathWithoutField = parts[0]; // e.g., "ft_fa_db"
|
|
5956
|
+
const field = parts[1]; // e.g., "currency"
|
|
5957
|
+
|
|
5958
|
+
const pathParts = pathWithoutField.split('_');
|
|
5959
|
+
pathParts.shift(); // Remove base provider alias
|
|
5960
|
+
const pathWithoutBase = pathParts.join('_'); // e.g., "fa_db"
|
|
5961
|
+
|
|
5962
|
+
fieldName = pathWithoutBase ? `${pathWithoutBase}_${field}` : field;
|
|
5963
|
+
} else {
|
|
5964
|
+
// From base provider: keep the full path with underscore
|
|
5965
|
+
// Example: ba.created_at -> ba_created_at
|
|
5966
|
+
fieldName = dim.fullPath.replace('.', '_');
|
|
5967
|
+
}
|
|
5968
|
+
const headerName = interpolateTitle(titleOverrides.dimensions[dim.fullPath] || dim.dimensionTitle || fieldName, parameters);
|
|
5969
|
+
const dimDef = dim.dimension;
|
|
5970
|
+
return {
|
|
5971
|
+
field: fieldName,
|
|
5972
|
+
headerName: headerName,
|
|
5973
|
+
flex: 1,
|
|
5974
|
+
minWidth: 150,
|
|
5975
|
+
type: isNumericType(dimDef?.type) ? 'number' : 'string',
|
|
5976
|
+
valueFormatter: value => formatValue(value, dimDef)
|
|
5977
|
+
};
|
|
5978
|
+
};
|
|
5704
5979
|
const ReportDataGrid = ({
|
|
5705
5980
|
reportData,
|
|
5706
|
-
|
|
5707
|
-
metrics,
|
|
5708
|
-
computedDimensions = [],
|
|
5981
|
+
columnItems = [],
|
|
5709
5982
|
loading,
|
|
5710
5983
|
onPageChange,
|
|
5711
5984
|
totalRows = 0,
|
|
@@ -5721,124 +5994,10 @@ const ReportDataGrid = ({
|
|
|
5721
5994
|
pageSize: 50
|
|
5722
5995
|
});
|
|
5723
5996
|
|
|
5724
|
-
// Generate columns dynamically
|
|
5725
|
-
|
|
5726
|
-
|
|
5727
|
-
|
|
5728
|
-
// Add dimension columns
|
|
5729
|
-
// Field naming logic:
|
|
5730
|
-
// - If from base provider: baseAlias.field -> baseAlias_field
|
|
5731
|
-
// - If from nested provider: baseAlias_rel1_rel2.field -> rel1_rel2_field (drops base alias)
|
|
5732
|
-
dimensions.forEach(dim => {
|
|
5733
|
-
let fieldName;
|
|
5734
|
-
let headerName;
|
|
5735
|
-
|
|
5736
|
-
// Check if there are relations (nested providers)
|
|
5737
|
-
if (dim.relations && dim.relations.length > 0) {
|
|
5738
|
-
// From nested provider: drop the base provider alias
|
|
5739
|
-
// Example: ft_fa_db.currency -> fa_db_currency
|
|
5740
|
-
const parts = dim.fullPath.split('.');
|
|
5741
|
-
const pathWithoutField = parts[0]; // e.g., "ft_fa_db"
|
|
5742
|
-
const field = parts[1]; // e.g., "currency"
|
|
5743
|
-
|
|
5744
|
-
// Remove the base provider alias (first part before first underscore)
|
|
5745
|
-
const pathParts = pathWithoutField.split('_');
|
|
5746
|
-
pathParts.shift(); // Remove base provider alias
|
|
5747
|
-
const pathWithoutBase = pathParts.join('_'); // e.g., "fa_db"
|
|
5748
|
-
|
|
5749
|
-
fieldName = pathWithoutBase ? `${pathWithoutBase}_${field}` : field;
|
|
5750
|
-
} else {
|
|
5751
|
-
// From base provider: keep the full path with underscore
|
|
5752
|
-
// Example: ba.created_at -> ba_created_at
|
|
5753
|
-
fieldName = dim.fullPath.replace('.', '_');
|
|
5754
|
-
}
|
|
5755
|
-
|
|
5756
|
-
// Check for title override, otherwise use the friendly dimension title from provider
|
|
5757
|
-
headerName = titleOverrides.dimensions[dim.fullPath] || dim.dimensionTitle || fieldName;
|
|
5758
|
-
headerName = interpolateTitle(headerName, parameters);
|
|
5759
|
-
const dimDef = dim.dimension;
|
|
5760
|
-
cols.push({
|
|
5761
|
-
field: fieldName,
|
|
5762
|
-
headerName: headerName,
|
|
5763
|
-
flex: 1,
|
|
5764
|
-
minWidth: 150,
|
|
5765
|
-
type: isNumericType(dimDef?.type) ? 'number' : 'string',
|
|
5766
|
-
valueFormatter: value => formatValue(value, dimDef)
|
|
5767
|
-
});
|
|
5768
|
-
});
|
|
5769
|
-
|
|
5770
|
-
// Add computed dimension columns. These have no provider/relations chain,
|
|
5771
|
-
// so the API returns each row keyed by the computed dimension's own `name`.
|
|
5772
|
-
computedDimensions.forEach(cd => {
|
|
5773
|
-
cols.push({
|
|
5774
|
-
field: cd.name,
|
|
5775
|
-
headerName: cd.name,
|
|
5776
|
-
flex: 1,
|
|
5777
|
-
minWidth: 150,
|
|
5778
|
-
type: 'string'
|
|
5779
|
-
});
|
|
5780
|
-
});
|
|
5781
|
-
|
|
5782
|
-
// Add metric columns
|
|
5783
|
-
// The API returns metric keys in two different forms depending on the path depth:
|
|
5784
|
-
//
|
|
5785
|
-
// • 2-part path "mc.record_count" → API key: "record_count"
|
|
5786
|
-
// • 2-part path "mc_fa.total_amount" (nested) → API key: "fa_total_amount"
|
|
5787
|
-
// • 3+-part path "mc.dkpi.activeAgreementsCount"→ API key: "mc.dkpi.activeAgreementsCount" (dots)
|
|
5788
|
-
//
|
|
5789
|
-
// For 3+-part paths the API key contains dots. MUI DataGrid interprets dots
|
|
5790
|
-
// in field names as nested-object accessors, so the rows useMemo normalises
|
|
5791
|
-
// those keys (dots → underscores). The column field must match that normalised key.
|
|
5792
|
-
metrics.forEach(metric => {
|
|
5793
|
-
const metricDef = metric.metric;
|
|
5794
|
-
let fieldName;
|
|
5795
|
-
let headerName;
|
|
5796
|
-
const dotCount = (metric.fullPath.match(/\./g) || []).length;
|
|
5797
|
-
if (dotCount >= 2) {
|
|
5798
|
-
// 3+-part namespaced path: API returns the full dotted path as the key.
|
|
5799
|
-
// Normalise dots to underscores to match the row key normalisation below.
|
|
5800
|
-
// Example: "mc.dkpi.activeAgreementsCount" -> "mc_dkpi_activeAgreementsCount"
|
|
5801
|
-
fieldName = metric.fullPath.replace(/\./g, '_');
|
|
5802
|
-
} else if (metric.relations && metric.relations.length > 0) {
|
|
5803
|
-
// 2-part path from a nested provider: API drops the base provider alias.
|
|
5804
|
-
// Example: "mc_fa.total_amount" -> "fa_total_amount"
|
|
5805
|
-
const parts = metric.fullPath.split('.');
|
|
5806
|
-
const pathWithoutField = parts[0]; // e.g., "mc_fa"
|
|
5807
|
-
const field = parts[1]; // e.g., "total_amount"
|
|
5808
|
-
|
|
5809
|
-
const pathParts = pathWithoutField.split('_');
|
|
5810
|
-
pathParts.shift(); // remove base provider alias
|
|
5811
|
-
const pathWithoutBase = pathParts.join('_');
|
|
5812
|
-
fieldName = pathWithoutBase ? `${pathWithoutBase}_${field}` : field;
|
|
5813
|
-
} else {
|
|
5814
|
-
// 2-part path from the base provider: API returns just the metric name.
|
|
5815
|
-
// Example: "mc.record_count" -> "record_count"
|
|
5816
|
-
fieldName = metric.metricName;
|
|
5817
|
-
}
|
|
5818
|
-
|
|
5819
|
-
// Check for title override, otherwise use the friendly metric title from provider
|
|
5820
|
-
headerName = titleOverrides.metrics[metric.fullPath] || metric.metricTitle || fieldName;
|
|
5821
|
-
headerName = interpolateTitle(headerName, parameters);
|
|
5822
|
-
cols.push({
|
|
5823
|
-
field: fieldName,
|
|
5824
|
-
headerName: headerName,
|
|
5825
|
-
flex: 1,
|
|
5826
|
-
minWidth: 150,
|
|
5827
|
-
type: isNumericType(metricDef?.type) ? 'number' : 'string',
|
|
5828
|
-
renderHeader: () => /*#__PURE__*/React__default.createElement(Box$1, {
|
|
5829
|
-
sx: {
|
|
5830
|
-
display: 'flex',
|
|
5831
|
-
alignItems: 'center',
|
|
5832
|
-
gap: 0.5
|
|
5833
|
-
}
|
|
5834
|
-
}, /*#__PURE__*/React__default.createElement(MetricSourceIcon, {
|
|
5835
|
-
source: metricDef?.source
|
|
5836
|
-
}), /*#__PURE__*/React__default.createElement("span", null, headerName)),
|
|
5837
|
-
valueFormatter: value => formatValue(value, metricDef)
|
|
5838
|
-
});
|
|
5839
|
-
});
|
|
5840
|
-
return cols;
|
|
5841
|
-
}, [dimensions, metrics, computedDimensions, titleOverrides, parameters]);
|
|
5997
|
+
// Generate columns dynamically, in the final resolved column order
|
|
5998
|
+
// (custom Column Order tab arrangement when defined, otherwise
|
|
5999
|
+
// dimensions/computed then metrics).
|
|
6000
|
+
const columns = React__default.useMemo(() => columnItems.map(item => buildColumn(item, titleOverrides, parameters)), [columnItems, titleOverrides, parameters]);
|
|
5842
6001
|
|
|
5843
6002
|
// Transform report data to rows with unique IDs.
|
|
5844
6003
|
// Keys are normalised by replacing dots with underscores so that MUI DataGrid
|
|
@@ -5945,7 +6104,15 @@ const ReportBuilder = ({
|
|
|
5945
6104
|
dimensions: [],
|
|
5946
6105
|
metrics: [],
|
|
5947
6106
|
filters: {},
|
|
5948
|
-
computedDimensions: []
|
|
6107
|
+
computedDimensions: [],
|
|
6108
|
+
// Unified display/execution order across dimensions + computedDimensions.
|
|
6109
|
+
// Entries: { type: 'dimension', key: fullPath } | { type: 'computed', key: name }
|
|
6110
|
+
dimensionOrder: [],
|
|
6111
|
+
// Optional unified display order across dimensions + computedDimensions +
|
|
6112
|
+
// metrics, set on the Column Order tab. Empty means "not defined" — the
|
|
6113
|
+
// results grid falls back to dimensionOrder followed by metrics order.
|
|
6114
|
+
// Entries: { type: 'dimension' | 'computed' | 'metric', key }
|
|
6115
|
+
columnOrder: []
|
|
5949
6116
|
});
|
|
5950
6117
|
const [titleOverrides, setTitleOverrides] = useState({
|
|
5951
6118
|
dimensions: {},
|
|
@@ -5991,6 +6158,16 @@ const ReportBuilder = ({
|
|
|
5991
6158
|
}
|
|
5992
6159
|
}, [autoRun, reportLoaded]);
|
|
5993
6160
|
|
|
6161
|
+
// Re-run the currently displayed report when base_currency changes in context
|
|
6162
|
+
const currentBaseCurrency = reportingContext?.parameters?.base_currency;
|
|
6163
|
+
const prevBaseCurrencyRef = useRef(currentBaseCurrency);
|
|
6164
|
+
useEffect(() => {
|
|
6165
|
+
if (reportData && prevBaseCurrencyRef.current !== undefined && prevBaseCurrencyRef.current !== currentBaseCurrency) {
|
|
6166
|
+
runReportWithPagination(currentPage, pageSize);
|
|
6167
|
+
}
|
|
6168
|
+
prevBaseCurrencyRef.current = currentBaseCurrency;
|
|
6169
|
+
}, [currentBaseCurrency]);
|
|
6170
|
+
|
|
5994
6171
|
// Utility function to reconstruct dimension object from fullPath
|
|
5995
6172
|
const reconstructDimensionFromPath = (fullPath, providersData, rootProvider) => {
|
|
5996
6173
|
try {
|
|
@@ -6120,6 +6297,92 @@ const ReportBuilder = ({
|
|
|
6120
6297
|
return null;
|
|
6121
6298
|
}
|
|
6122
6299
|
};
|
|
6300
|
+
|
|
6301
|
+
// Build the unified display/execution order for dimensions + computed
|
|
6302
|
+
// dimensions. Prefers the persisted `ordered_dimensions` field; falls back
|
|
6303
|
+
// to "dimensions first, then computed dimensions" for report definitions
|
|
6304
|
+
// saved before this field existed.
|
|
6305
|
+
const buildDimensionOrder = (rawOrderedDimensions, dimensions, computedDimensions) => {
|
|
6306
|
+
const dimensionOrder = [];
|
|
6307
|
+
const seenDimensionKeys = new Set();
|
|
6308
|
+
const seenComputedKeys = new Set();
|
|
6309
|
+
if (Array.isArray(rawOrderedDimensions)) {
|
|
6310
|
+
rawOrderedDimensions.forEach(entry => {
|
|
6311
|
+
if (entry.type === "dimension" && dimensions.some(d => d.fullPath === entry.ref)) {
|
|
6312
|
+
dimensionOrder.push({
|
|
6313
|
+
type: "dimension",
|
|
6314
|
+
key: entry.ref
|
|
6315
|
+
});
|
|
6316
|
+
seenDimensionKeys.add(entry.ref);
|
|
6317
|
+
} else if (entry.type === "computed" && computedDimensions.some(cd => cd.name === entry.ref)) {
|
|
6318
|
+
dimensionOrder.push({
|
|
6319
|
+
type: "computed",
|
|
6320
|
+
key: entry.ref
|
|
6321
|
+
});
|
|
6322
|
+
seenComputedKeys.add(entry.ref);
|
|
6323
|
+
}
|
|
6324
|
+
});
|
|
6325
|
+
}
|
|
6326
|
+
|
|
6327
|
+
// Append anything not covered by ordered_dimensions (older reports, or drift)
|
|
6328
|
+
dimensions.forEach(d => {
|
|
6329
|
+
if (!seenDimensionKeys.has(d.fullPath)) {
|
|
6330
|
+
dimensionOrder.push({
|
|
6331
|
+
type: "dimension",
|
|
6332
|
+
key: d.fullPath
|
|
6333
|
+
});
|
|
6334
|
+
}
|
|
6335
|
+
});
|
|
6336
|
+
computedDimensions.forEach(cd => {
|
|
6337
|
+
if (!seenComputedKeys.has(cd.name)) {
|
|
6338
|
+
dimensionOrder.push({
|
|
6339
|
+
type: "computed",
|
|
6340
|
+
key: cd.name
|
|
6341
|
+
});
|
|
6342
|
+
}
|
|
6343
|
+
});
|
|
6344
|
+
return dimensionOrder;
|
|
6345
|
+
};
|
|
6346
|
+
|
|
6347
|
+
// Build the optional unified column order (dimensions + computed
|
|
6348
|
+
// dimensions + metrics) from the persisted `ordered_columns` field.
|
|
6349
|
+
// Unlike buildDimensionOrder, entries that no longer resolve are simply
|
|
6350
|
+
// dropped rather than backfilled — an empty/partial result just means "no
|
|
6351
|
+
// custom order defined (yet)", which resolveColumnOrder treats as
|
|
6352
|
+
// "fall back to the default order".
|
|
6353
|
+
const buildColumnOrder = (rawOrderedColumns, dimensions, computedDimensions, metrics) => {
|
|
6354
|
+
if (!Array.isArray(rawOrderedColumns)) return [];
|
|
6355
|
+
const dimensionPaths = new Set(dimensions.map(d => d.fullPath));
|
|
6356
|
+
const computedNames = new Set(computedDimensions.map(cd => cd.name));
|
|
6357
|
+
const metricPaths = new Set(metrics.map(m => m.fullPath));
|
|
6358
|
+
const columnOrder = [];
|
|
6359
|
+
rawOrderedColumns.forEach(entry => {
|
|
6360
|
+
if (entry.type === "dimension" && dimensionPaths.has(entry.ref)) {
|
|
6361
|
+
columnOrder.push({
|
|
6362
|
+
type: "dimension",
|
|
6363
|
+
key: entry.ref
|
|
6364
|
+
});
|
|
6365
|
+
} else if (entry.type === "computed" && computedNames.has(entry.ref)) {
|
|
6366
|
+
columnOrder.push({
|
|
6367
|
+
type: "computed",
|
|
6368
|
+
key: entry.ref
|
|
6369
|
+
});
|
|
6370
|
+
} else if (entry.type === "metric" && metricPaths.has(entry.ref)) {
|
|
6371
|
+
columnOrder.push({
|
|
6372
|
+
type: "metric",
|
|
6373
|
+
key: entry.ref
|
|
6374
|
+
});
|
|
6375
|
+
}
|
|
6376
|
+
});
|
|
6377
|
+
return columnOrder;
|
|
6378
|
+
};
|
|
6379
|
+
|
|
6380
|
+
// Auto-append a newly added dimension/computed dimension/metric to the end
|
|
6381
|
+
// of columnOrder — but only once a custom order actually exists. While
|
|
6382
|
+
// columnOrder is empty (no custom order defined) it's left untouched, so
|
|
6383
|
+
// the results grid keeps falling back to the default dimensions-then-
|
|
6384
|
+
// metrics order until the user visits the Column Order tab.
|
|
6385
|
+
const appendToColumnOrder = (columnOrder, entry) => columnOrder.length > 0 ? [...columnOrder, entry] : columnOrder;
|
|
6123
6386
|
const loadReportDefinition = async id => {
|
|
6124
6387
|
try {
|
|
6125
6388
|
console.log("Loading report definition:", id);
|
|
@@ -6215,13 +6478,17 @@ const ReportBuilder = ({
|
|
|
6215
6478
|
// Computed dimensions are self-contained { name, value } pairs with no
|
|
6216
6479
|
// provider/relation chain, so they're loaded as-is (no reconstruction needed)
|
|
6217
6480
|
const computedDimensions = reportDef.definition?.doc?.query?.computed_dimensions || [];
|
|
6481
|
+
const dimensionOrder = buildDimensionOrder(reportDef.definition?.doc?.query?.ordered_dimensions, reconstructedDimensions, computedDimensions);
|
|
6482
|
+
const columnOrder = buildColumnOrder(reportDef.definition?.doc?.query?.ordered_columns, reconstructedDimensions, computedDimensions, reconstructedMetrics);
|
|
6218
6483
|
|
|
6219
6484
|
// Set the report state
|
|
6220
6485
|
setReport({
|
|
6221
6486
|
dimensions: reconstructedDimensions,
|
|
6222
6487
|
metrics: reconstructedMetrics,
|
|
6223
6488
|
filters: loadedFilters,
|
|
6224
|
-
computedDimensions
|
|
6489
|
+
computedDimensions,
|
|
6490
|
+
dimensionOrder,
|
|
6491
|
+
columnOrder
|
|
6225
6492
|
});
|
|
6226
6493
|
|
|
6227
6494
|
// Set title overrides
|
|
@@ -6330,13 +6597,17 @@ const ReportBuilder = ({
|
|
|
6330
6597
|
// Computed dimensions are self-contained { name, value } pairs with no
|
|
6331
6598
|
// provider/relation chain, so they're loaded as-is (no reconstruction needed)
|
|
6332
6599
|
const computedDimensions = reportDef.definition?.doc?.query?.computed_dimensions || [];
|
|
6600
|
+
const dimensionOrder = buildDimensionOrder(reportDef.definition?.doc?.query?.ordered_dimensions, reconstructedDimensions, computedDimensions);
|
|
6601
|
+
const columnOrder = buildColumnOrder(reportDef.definition?.doc?.query?.ordered_columns, reconstructedDimensions, computedDimensions, reconstructedMetrics);
|
|
6333
6602
|
|
|
6334
6603
|
// Set the report state
|
|
6335
6604
|
setReport({
|
|
6336
6605
|
dimensions: reconstructedDimensions,
|
|
6337
6606
|
metrics: reconstructedMetrics,
|
|
6338
6607
|
filters: loadedFilters,
|
|
6339
|
-
computedDimensions
|
|
6608
|
+
computedDimensions,
|
|
6609
|
+
dimensionOrder,
|
|
6610
|
+
columnOrder
|
|
6340
6611
|
});
|
|
6341
6612
|
|
|
6342
6613
|
// Set title overrides
|
|
@@ -6369,7 +6640,9 @@ const ReportBuilder = ({
|
|
|
6369
6640
|
dimensions: [],
|
|
6370
6641
|
metrics: [],
|
|
6371
6642
|
filters: {},
|
|
6372
|
-
computedDimensions: []
|
|
6643
|
+
computedDimensions: [],
|
|
6644
|
+
dimensionOrder: [],
|
|
6645
|
+
columnOrder: []
|
|
6373
6646
|
});
|
|
6374
6647
|
// Reset title overrides
|
|
6375
6648
|
setTitleOverrides({
|
|
@@ -6446,58 +6719,93 @@ const ReportBuilder = ({
|
|
|
6446
6719
|
setReport(prev => {
|
|
6447
6720
|
const newReport = {
|
|
6448
6721
|
...prev,
|
|
6449
|
-
dimensions: [...prev.dimensions, dimensionData]
|
|
6722
|
+
dimensions: [...prev.dimensions, dimensionData],
|
|
6723
|
+
dimensionOrder: [...prev.dimensionOrder, {
|
|
6724
|
+
type: "dimension",
|
|
6725
|
+
key: dimensionData.fullPath
|
|
6726
|
+
}],
|
|
6727
|
+
columnOrder: appendToColumnOrder(prev.columnOrder, {
|
|
6728
|
+
type: "dimension",
|
|
6729
|
+
key: dimensionData.fullPath
|
|
6730
|
+
})
|
|
6450
6731
|
};
|
|
6451
6732
|
console.log("Dimension saved:", dimensionData);
|
|
6452
6733
|
console.log("Complete report:", newReport);
|
|
6453
6734
|
return newReport;
|
|
6454
6735
|
});
|
|
6455
6736
|
};
|
|
6456
|
-
const handleRemoveDimension =
|
|
6737
|
+
const handleRemoveDimension = fullPath => {
|
|
6457
6738
|
setReport(prev => ({
|
|
6458
6739
|
...prev,
|
|
6459
|
-
dimensions: prev.dimensions.filter(
|
|
6740
|
+
dimensions: prev.dimensions.filter(d => d.fullPath !== fullPath),
|
|
6741
|
+
dimensionOrder: prev.dimensionOrder.filter(entry => !(entry.type === "dimension" && entry.key === fullPath)),
|
|
6742
|
+
columnOrder: prev.columnOrder.filter(entry => !(entry.type === "dimension" && entry.key === fullPath))
|
|
6460
6743
|
}));
|
|
6461
6744
|
};
|
|
6462
|
-
const
|
|
6745
|
+
const handleUpdateDimensionSortOrder = (fullPath, sortOrder) => {
|
|
6463
6746
|
setReport(prev => ({
|
|
6464
6747
|
...prev,
|
|
6465
|
-
dimensions:
|
|
6748
|
+
dimensions: prev.dimensions.map(d => d.fullPath === fullPath ? {
|
|
6749
|
+
...d,
|
|
6750
|
+
sortOrder
|
|
6751
|
+
} : d)
|
|
6466
6752
|
}));
|
|
6467
6753
|
};
|
|
6468
|
-
|
|
6754
|
+
|
|
6755
|
+
// Reorders the single unified dimensions + computed-dimensions list;
|
|
6756
|
+
// `dimensions`/`computedDimensions` arrays themselves are unordered from
|
|
6757
|
+
// this perspective, so only `dimensionOrder` needs to change.
|
|
6758
|
+
const handleReorderDimensionItems = newDimensionOrder => {
|
|
6469
6759
|
setReport(prev => ({
|
|
6470
6760
|
...prev,
|
|
6471
|
-
|
|
6761
|
+
dimensionOrder: newDimensionOrder
|
|
6472
6762
|
}));
|
|
6473
6763
|
};
|
|
6474
|
-
const
|
|
6475
|
-
setReport(prev => {
|
|
6476
|
-
|
|
6477
|
-
|
|
6478
|
-
|
|
6479
|
-
|
|
6480
|
-
|
|
6481
|
-
}
|
|
6482
|
-
|
|
6764
|
+
const handleSaveComputedDimension = computedDimensionData => {
|
|
6765
|
+
setReport(prev => ({
|
|
6766
|
+
...prev,
|
|
6767
|
+
computedDimensions: [...prev.computedDimensions, computedDimensionData],
|
|
6768
|
+
dimensionOrder: [...prev.dimensionOrder, {
|
|
6769
|
+
type: "computed",
|
|
6770
|
+
key: computedDimensionData.name
|
|
6771
|
+
}],
|
|
6772
|
+
columnOrder: appendToColumnOrder(prev.columnOrder, {
|
|
6773
|
+
type: "computed",
|
|
6774
|
+
key: computedDimensionData.name
|
|
6775
|
+
})
|
|
6776
|
+
}));
|
|
6483
6777
|
};
|
|
6484
|
-
const
|
|
6778
|
+
const handleUpdateComputedDimension = (name, computedDimensionData) => {
|
|
6485
6779
|
setReport(prev => ({
|
|
6486
6780
|
...prev,
|
|
6487
|
-
computedDimensions: prev.computedDimensions.
|
|
6781
|
+
computedDimensions: prev.computedDimensions.map(cd => cd.name === name ? computedDimensionData : cd),
|
|
6782
|
+
dimensionOrder: prev.dimensionOrder.map(entry => entry.type === "computed" && entry.key === name ? {
|
|
6783
|
+
...entry,
|
|
6784
|
+
key: computedDimensionData.name
|
|
6785
|
+
} : entry),
|
|
6786
|
+
columnOrder: prev.columnOrder.map(entry => entry.type === "computed" && entry.key === name ? {
|
|
6787
|
+
...entry,
|
|
6788
|
+
key: computedDimensionData.name
|
|
6789
|
+
} : entry)
|
|
6488
6790
|
}));
|
|
6489
6791
|
};
|
|
6490
|
-
const
|
|
6792
|
+
const handleRemoveComputedDimension = name => {
|
|
6491
6793
|
setReport(prev => ({
|
|
6492
6794
|
...prev,
|
|
6493
|
-
computedDimensions:
|
|
6795
|
+
computedDimensions: prev.computedDimensions.filter(cd => cd.name !== name),
|
|
6796
|
+
dimensionOrder: prev.dimensionOrder.filter(entry => !(entry.type === "computed" && entry.key === name)),
|
|
6797
|
+
columnOrder: prev.columnOrder.filter(entry => !(entry.type === "computed" && entry.key === name))
|
|
6494
6798
|
}));
|
|
6495
6799
|
};
|
|
6496
6800
|
const handleSaveMetric = metricData => {
|
|
6497
6801
|
setReport(prev => {
|
|
6498
6802
|
const newReport = {
|
|
6499
6803
|
...prev,
|
|
6500
|
-
metrics: [...prev.metrics, metricData]
|
|
6804
|
+
metrics: [...prev.metrics, metricData],
|
|
6805
|
+
columnOrder: appendToColumnOrder(prev.columnOrder, {
|
|
6806
|
+
type: "metric",
|
|
6807
|
+
key: metricData.fullPath
|
|
6808
|
+
})
|
|
6501
6809
|
};
|
|
6502
6810
|
console.log("Metric saved:", metricData);
|
|
6503
6811
|
console.log("Complete report:", newReport);
|
|
@@ -6505,15 +6813,29 @@ const ReportBuilder = ({
|
|
|
6505
6813
|
});
|
|
6506
6814
|
};
|
|
6507
6815
|
const handleRemoveMetric = index => {
|
|
6816
|
+
setReport(prev => {
|
|
6817
|
+
const fullPath = prev.metrics[index]?.fullPath;
|
|
6818
|
+
return {
|
|
6819
|
+
...prev,
|
|
6820
|
+
metrics: prev.metrics.filter((_, i) => i !== index),
|
|
6821
|
+
columnOrder: prev.columnOrder.filter(entry => !(entry.type === "metric" && entry.key === fullPath))
|
|
6822
|
+
};
|
|
6823
|
+
});
|
|
6824
|
+
};
|
|
6825
|
+
const handleReorderMetrics = newOrder => {
|
|
6508
6826
|
setReport(prev => ({
|
|
6509
6827
|
...prev,
|
|
6510
|
-
metrics:
|
|
6828
|
+
metrics: newOrder
|
|
6511
6829
|
}));
|
|
6512
6830
|
};
|
|
6513
|
-
|
|
6831
|
+
|
|
6832
|
+
// Sets the unified column order shown on the Column Order tab. An empty
|
|
6833
|
+
// array (e.g. via "Reset to Default") clears the customization, and the
|
|
6834
|
+
// results grid falls back to dimensionOrder followed by metrics order.
|
|
6835
|
+
const handleReorderColumnItems = newColumnOrder => {
|
|
6514
6836
|
setReport(prev => ({
|
|
6515
6837
|
...prev,
|
|
6516
|
-
|
|
6838
|
+
columnOrder: newColumnOrder
|
|
6517
6839
|
}));
|
|
6518
6840
|
};
|
|
6519
6841
|
const handleSaveFilter = (fullPath, filterData) => {
|
|
@@ -6546,10 +6868,20 @@ const ReportBuilder = ({
|
|
|
6546
6868
|
});
|
|
6547
6869
|
};
|
|
6548
6870
|
|
|
6549
|
-
//
|
|
6550
|
-
|
|
6871
|
+
// Builds the query object shared between running (paginated) and saving
|
|
6872
|
+
// (unpaginated) a report. Dimension and computed-dimension order both
|
|
6873
|
+
// derive from the single unified `report.dimensionOrder` list, so the
|
|
6874
|
+
// saved `dimensions`/`computed_dimensions` arrays, `order_by`, and the
|
|
6875
|
+
// persisted `ordered_dimensions` field all stay consistent with whatever
|
|
6876
|
+
// order the user arranged on screen.
|
|
6877
|
+
const buildQueryObject = () => {
|
|
6878
|
+
const dimensionByPath = Object.fromEntries(report.dimensions.map(d => [d.fullPath, d]));
|
|
6879
|
+
const computedByName = Object.fromEntries(report.computedDimensions.map(cd => [cd.name, cd]));
|
|
6880
|
+
const orderedDimensionsOnly = report.dimensionOrder.filter(entry => entry.type === "dimension").map(entry => dimensionByPath[entry.key]).filter(Boolean);
|
|
6881
|
+
const orderedComputedOnly = report.dimensionOrder.filter(entry => entry.type === "computed").map(entry => computedByName[entry.key]).filter(Boolean);
|
|
6882
|
+
|
|
6551
6883
|
// Build order_by array - include all dimensions, add desc property only when needed
|
|
6552
|
-
const orderBy =
|
|
6884
|
+
const orderBy = orderedDimensionsOnly.map(dim => {
|
|
6553
6885
|
const orderItem = {
|
|
6554
6886
|
name: dim.fullPath
|
|
6555
6887
|
};
|
|
@@ -6605,21 +6937,40 @@ const ReportBuilder = ({
|
|
|
6605
6937
|
titles.filters = titleOverrides.filters;
|
|
6606
6938
|
}
|
|
6607
6939
|
const queryObj = {
|
|
6608
|
-
dimensions:
|
|
6940
|
+
dimensions: orderedDimensionsOnly.map(dim => dim.fullPath),
|
|
6609
6941
|
metrics: report.metrics.map(metric => metric.fullPath),
|
|
6610
|
-
order_by: orderBy
|
|
6611
|
-
limit: size,
|
|
6612
|
-
offset: page * size
|
|
6942
|
+
order_by: orderBy
|
|
6613
6943
|
};
|
|
6614
6944
|
|
|
6615
6945
|
// Only add computed_dimensions if there are any
|
|
6616
|
-
if (
|
|
6617
|
-
queryObj.computed_dimensions =
|
|
6946
|
+
if (orderedComputedOnly.length > 0) {
|
|
6947
|
+
queryObj.computed_dimensions = orderedComputedOnly.map(cd => ({
|
|
6618
6948
|
name: cd.name,
|
|
6619
6949
|
value: cd.value
|
|
6620
6950
|
}));
|
|
6621
6951
|
}
|
|
6622
6952
|
|
|
6953
|
+
// Persist the unified dimension + computed-dimension order so it can be
|
|
6954
|
+
// reconstructed on reload and so executed reports render columns in the
|
|
6955
|
+
// same order they were arranged in the builder.
|
|
6956
|
+
if (report.dimensionOrder.length > 0) {
|
|
6957
|
+
queryObj.ordered_dimensions = report.dimensionOrder.map(entry => ({
|
|
6958
|
+
type: entry.type,
|
|
6959
|
+
ref: entry.key
|
|
6960
|
+
}));
|
|
6961
|
+
}
|
|
6962
|
+
|
|
6963
|
+
// Persist the optional unified column order (Column Order tab) so the
|
|
6964
|
+
// results grid can reproduce it on reload. Absent when no custom order
|
|
6965
|
+
// has been defined, so old/untouched reports keep their current
|
|
6966
|
+
// dimensions-then-metrics column order unchanged.
|
|
6967
|
+
if (report.columnOrder.length > 0) {
|
|
6968
|
+
queryObj.ordered_columns = report.columnOrder.map(entry => ({
|
|
6969
|
+
type: entry.type,
|
|
6970
|
+
ref: entry.key
|
|
6971
|
+
}));
|
|
6972
|
+
}
|
|
6973
|
+
|
|
6623
6974
|
// Only add titles if there are any overrides
|
|
6624
6975
|
if (Object.keys(titles).length > 0) {
|
|
6625
6976
|
queryObj.titles = titles;
|
|
@@ -6629,6 +6980,14 @@ const ReportBuilder = ({
|
|
|
6629
6980
|
if (filter) {
|
|
6630
6981
|
queryObj.filter = filter;
|
|
6631
6982
|
}
|
|
6983
|
+
return queryObj;
|
|
6984
|
+
};
|
|
6985
|
+
|
|
6986
|
+
// Convert internal report structure to API format
|
|
6987
|
+
const convertReportToApiFormat = (page = 0, size = 50) => {
|
|
6988
|
+
const queryObj = buildQueryObject();
|
|
6989
|
+
queryObj.limit = size;
|
|
6990
|
+
queryObj.offset = page * size;
|
|
6632
6991
|
|
|
6633
6992
|
// Get parameters from context if available, otherwise use default
|
|
6634
6993
|
const parameters = reportingContext?.parameters || {
|
|
@@ -6671,8 +7030,8 @@ const ReportBuilder = ({
|
|
|
6671
7030
|
const handleRunReport = async () => {
|
|
6672
7031
|
setCurrentPage(0);
|
|
6673
7032
|
await runReportWithPagination(0, pageSize);
|
|
6674
|
-
// Switch to Results tab after running report (
|
|
6675
|
-
setActiveTab(
|
|
7033
|
+
// Switch to Results tab after running report (index 4: Dimensions, Metrics, Filters, Column Order, Results)
|
|
7034
|
+
setActiveTab(4);
|
|
6676
7035
|
};
|
|
6677
7036
|
const handleDownloadReport = async () => {
|
|
6678
7037
|
try {
|
|
@@ -6722,77 +7081,7 @@ const ReportBuilder = ({
|
|
|
6722
7081
|
}
|
|
6723
7082
|
try {
|
|
6724
7083
|
setSaving(true);
|
|
6725
|
-
|
|
6726
|
-
// Build order_by array - include all dimensions, add desc property only when needed
|
|
6727
|
-
const orderBy = report.dimensions.map(dim => {
|
|
6728
|
-
const orderItem = {
|
|
6729
|
-
name: dim.fullPath
|
|
6730
|
-
};
|
|
6731
|
-
if (dim.sortOrder === "desc") {
|
|
6732
|
-
orderItem.desc = true;
|
|
6733
|
-
}
|
|
6734
|
-
// For 'asc' or null, just include { name: fullPath } without desc property
|
|
6735
|
-
return orderItem;
|
|
6736
|
-
});
|
|
6737
|
-
|
|
6738
|
-
// Build titles object - only include overridden titles
|
|
6739
|
-
const titles = {};
|
|
6740
|
-
if (Object.keys(titleOverrides.dimensions).length > 0) {
|
|
6741
|
-
titles.dimensions = titleOverrides.dimensions;
|
|
6742
|
-
}
|
|
6743
|
-
if (Object.keys(titleOverrides.metrics).length > 0) {
|
|
6744
|
-
titles.metrics = titleOverrides.metrics;
|
|
6745
|
-
}
|
|
6746
|
-
if (Object.keys(titleOverrides.filters).length > 0) {
|
|
6747
|
-
titles.filters = titleOverrides.filters;
|
|
6748
|
-
}
|
|
6749
|
-
const queryObj = {
|
|
6750
|
-
dimensions: report.dimensions.map(dim => dim.fullPath),
|
|
6751
|
-
metrics: report.metrics.map(metric => metric.fullPath),
|
|
6752
|
-
order_by: orderBy
|
|
6753
|
-
};
|
|
6754
|
-
|
|
6755
|
-
// Only add computed_dimensions if there are any
|
|
6756
|
-
if (report.computedDimensions && report.computedDimensions.length > 0) {
|
|
6757
|
-
queryObj.computed_dimensions = report.computedDimensions.map(cd => ({
|
|
6758
|
-
name: cd.name,
|
|
6759
|
-
value: cd.value
|
|
6760
|
-
}));
|
|
6761
|
-
}
|
|
6762
|
-
|
|
6763
|
-
// Only add titles if there are any overrides
|
|
6764
|
-
if (Object.keys(titles).length > 0) {
|
|
6765
|
-
queryObj.titles = titles;
|
|
6766
|
-
}
|
|
6767
|
-
|
|
6768
|
-
// Add filter if there are any - NEW API format with top-level 'and' operator
|
|
6769
|
-
// NEW format: { and: [ { "ft.currency": ["USD", "EUR"] }, { "ft_ba.created_at": {gte: "2025-05-01", lte: "2025-12-31"} } ] }
|
|
6770
|
-
if (Object.keys(report.filters).length > 0) {
|
|
6771
|
-
const conditions = [];
|
|
6772
|
-
Object.entries(report.filters).forEach(([fullPath, filterData]) => {
|
|
6773
|
-
if (filterData.values) {
|
|
6774
|
-
// Check if values is an object (date range) or array (regular filter)
|
|
6775
|
-
if (Array.isArray(filterData.values)) {
|
|
6776
|
-
// Regular filter - only add if array has values
|
|
6777
|
-
if (filterData.values.length > 0) {
|
|
6778
|
-
conditions.push({
|
|
6779
|
-
[fullPath]: filterData.values
|
|
6780
|
-
});
|
|
6781
|
-
}
|
|
6782
|
-
} else if (typeof filterData.values === "object") {
|
|
6783
|
-
// Date range filter - add the object as-is
|
|
6784
|
-
conditions.push({
|
|
6785
|
-
[fullPath]: filterData.values
|
|
6786
|
-
});
|
|
6787
|
-
}
|
|
6788
|
-
}
|
|
6789
|
-
});
|
|
6790
|
-
if (conditions.length > 0) {
|
|
6791
|
-
queryObj.filter = {
|
|
6792
|
-
and: conditions
|
|
6793
|
-
};
|
|
6794
|
-
}
|
|
6795
|
-
}
|
|
7084
|
+
const queryObj = buildQueryObject();
|
|
6796
7085
|
|
|
6797
7086
|
// Build the doc object
|
|
6798
7087
|
const docObj = {
|
|
@@ -6837,6 +7126,23 @@ const ReportBuilder = ({
|
|
|
6837
7126
|
}
|
|
6838
7127
|
};
|
|
6839
7128
|
const canSaveReport = selectedProvider && reportTitle.trim() && (report.dimensions.length > 0 || report.metrics.length > 0 || report.computedDimensions.length > 0);
|
|
7129
|
+
|
|
7130
|
+
// Resolve the unified dimensionOrder into actual dimension/computed-dimension
|
|
7131
|
+
// objects so the results grid can render columns in that same order.
|
|
7132
|
+
const dimensionByFullPath = Object.fromEntries(report.dimensions.map(d => [d.fullPath, d]));
|
|
7133
|
+
const computedDimensionByName = Object.fromEntries(report.computedDimensions.map(cd => [cd.name, cd]));
|
|
7134
|
+
const orderedDimensionItems = report.dimensionOrder.map(entry => entry.type === "dimension" ? {
|
|
7135
|
+
kind: "dimension",
|
|
7136
|
+
data: dimensionByFullPath[entry.key]
|
|
7137
|
+
} : {
|
|
7138
|
+
kind: "computed",
|
|
7139
|
+
data: computedDimensionByName[entry.key]
|
|
7140
|
+
}).filter(item => item.data);
|
|
7141
|
+
|
|
7142
|
+
// Final column list for the results grid: the explicit Column Order tab
|
|
7143
|
+
// arrangement when defined, otherwise dimensions/computed (in their own
|
|
7144
|
+
// order) followed by metrics (in their own order) — today's behavior.
|
|
7145
|
+
const columnItems = resolveColumnOrder(report.columnOrder, orderedDimensionItems, report.metrics);
|
|
6840
7146
|
return /*#__PURE__*/React__default.createElement(Box$1, {
|
|
6841
7147
|
sx: {
|
|
6842
7148
|
p: 3,
|
|
@@ -7091,9 +7397,36 @@ const ReportBuilder = ({
|
|
|
7091
7397
|
}
|
|
7092
7398
|
}
|
|
7093
7399
|
}), /*#__PURE__*/React__default.createElement(Tab, {
|
|
7094
|
-
label:
|
|
7400
|
+
label: /*#__PURE__*/React__default.createElement(Badge, {
|
|
7401
|
+
variant: "dot",
|
|
7402
|
+
invisible: report.columnOrder.length === 0,
|
|
7403
|
+
sx: {
|
|
7404
|
+
"& .MuiBadge-dot": {
|
|
7405
|
+
backgroundColor: "rgb(70, 134, 128)"
|
|
7406
|
+
}
|
|
7407
|
+
}
|
|
7408
|
+
}, /*#__PURE__*/React__default.createElement("span", {
|
|
7409
|
+
style: {
|
|
7410
|
+
marginRight: report.columnOrder.length > 0 ? "8px" : "0"
|
|
7411
|
+
}
|
|
7412
|
+
}, "Column Order")),
|
|
7095
7413
|
id: "report-tab-3",
|
|
7096
7414
|
"aria-controls": "report-tabpanel-3",
|
|
7415
|
+
sx: {
|
|
7416
|
+
height: "41px",
|
|
7417
|
+
fontFamily: "system-ui",
|
|
7418
|
+
borderRadius: "0.5rem",
|
|
7419
|
+
boxShadow: "none",
|
|
7420
|
+
textTransform: "none",
|
|
7421
|
+
color: "rgb(37, 37, 37)",
|
|
7422
|
+
"&.Mui-selected": {
|
|
7423
|
+
color: "rgb(70, 134, 128)"
|
|
7424
|
+
}
|
|
7425
|
+
}
|
|
7426
|
+
}), /*#__PURE__*/React__default.createElement(Tab, {
|
|
7427
|
+
label: reportData ? "Results" : "Results (Run report first)",
|
|
7428
|
+
id: "report-tab-4",
|
|
7429
|
+
"aria-controls": "report-tabpanel-4",
|
|
7097
7430
|
disabled: !reportData,
|
|
7098
7431
|
sx: {
|
|
7099
7432
|
height: "41px",
|
|
@@ -7115,7 +7448,7 @@ const ReportBuilder = ({
|
|
|
7115
7448
|
savedDimensions: report.dimensions,
|
|
7116
7449
|
onSaveDimension: handleSaveDimension,
|
|
7117
7450
|
onRemoveDimension: handleRemoveDimension,
|
|
7118
|
-
|
|
7451
|
+
onUpdateDimensionSortOrder: handleUpdateDimensionSortOrder,
|
|
7119
7452
|
titleOverrides: titleOverrides.dimensions,
|
|
7120
7453
|
onUpdateTitle: handleUpdateDimensionTitle,
|
|
7121
7454
|
onResetTitle: handleResetDimensionTitle,
|
|
@@ -7125,7 +7458,8 @@ const ReportBuilder = ({
|
|
|
7125
7458
|
onSaveComputedDimension: handleSaveComputedDimension,
|
|
7126
7459
|
onUpdateComputedDimension: handleUpdateComputedDimension,
|
|
7127
7460
|
onRemoveComputedDimension: handleRemoveComputedDimension,
|
|
7128
|
-
|
|
7461
|
+
dimensionOrder: report.dimensionOrder,
|
|
7462
|
+
onReorderDimensionItems: handleReorderDimensionItems
|
|
7129
7463
|
})), /*#__PURE__*/React__default.createElement(TabPanel, {
|
|
7130
7464
|
value: activeTab,
|
|
7131
7465
|
index: 1
|
|
@@ -7159,11 +7493,18 @@ const ReportBuilder = ({
|
|
|
7159
7493
|
})), /*#__PURE__*/React__default.createElement(TabPanel, {
|
|
7160
7494
|
value: activeTab,
|
|
7161
7495
|
index: 3
|
|
7496
|
+
}, /*#__PURE__*/React__default.createElement(ColumnOrder, {
|
|
7497
|
+
orderedDimensionItems: orderedDimensionItems,
|
|
7498
|
+
metrics: report.metrics,
|
|
7499
|
+
columnOrder: report.columnOrder,
|
|
7500
|
+
onReorderColumnItems: handleReorderColumnItems,
|
|
7501
|
+
titleOverrides: titleOverrides
|
|
7502
|
+
})), /*#__PURE__*/React__default.createElement(TabPanel, {
|
|
7503
|
+
value: activeTab,
|
|
7504
|
+
index: 4
|
|
7162
7505
|
}, reportData && /*#__PURE__*/React__default.createElement(ReportDataGrid, {
|
|
7163
7506
|
reportData: reportData,
|
|
7164
|
-
|
|
7165
|
-
metrics: report.metrics,
|
|
7166
|
-
computedDimensions: report.computedDimensions,
|
|
7507
|
+
columnItems: columnItems,
|
|
7167
7508
|
loading: loading,
|
|
7168
7509
|
onPageChange: handlePageChange,
|
|
7169
7510
|
totalRows: totalRows,
|
|
@@ -7226,6 +7567,50 @@ const ReportDefinitionsManager = () => {
|
|
|
7226
7567
|
});
|
|
7227
7568
|
};
|
|
7228
7569
|
|
|
7570
|
+
// TODO: replace with currencies fetched from the API once that's available.
|
|
7571
|
+
const CURRENCY_OPTIONS = [{
|
|
7572
|
+
key: "USD",
|
|
7573
|
+
value: "USD"
|
|
7574
|
+
}, {
|
|
7575
|
+
key: "EUR",
|
|
7576
|
+
value: "EUR"
|
|
7577
|
+
}, {
|
|
7578
|
+
key: "SEK",
|
|
7579
|
+
value: "SEK"
|
|
7580
|
+
}, {
|
|
7581
|
+
key: "GBP",
|
|
7582
|
+
value: "GBP"
|
|
7583
|
+
}, {
|
|
7584
|
+
key: "DKK",
|
|
7585
|
+
value: "DKK"
|
|
7586
|
+
}, {
|
|
7587
|
+
key: "NOK",
|
|
7588
|
+
value: "NOK"
|
|
7589
|
+
}];
|
|
7590
|
+
const CurrencySelector = () => {
|
|
7591
|
+
const {
|
|
7592
|
+
parameters,
|
|
7593
|
+
setParameters
|
|
7594
|
+
} = useReportingContext();
|
|
7595
|
+
|
|
7596
|
+
// Hidden by default; only shown when a `cur` query parameter is present in the top-level URL.
|
|
7597
|
+
const showCurrencySelector = new URLSearchParams(window.location.search).has("cur");
|
|
7598
|
+
if (!showCurrencySelector) {
|
|
7599
|
+
return null;
|
|
7600
|
+
}
|
|
7601
|
+
return /*#__PURE__*/React__default.createElement(SingleSelect, {
|
|
7602
|
+
items: CURRENCY_OPTIONS,
|
|
7603
|
+
value: parameters.base_currency,
|
|
7604
|
+
label: "Currency",
|
|
7605
|
+
onChange: e => setParameters({
|
|
7606
|
+
...parameters,
|
|
7607
|
+
base_currency: e.target.value
|
|
7608
|
+
}),
|
|
7609
|
+
sx: {
|
|
7610
|
+
width: 110
|
|
7611
|
+
}
|
|
7612
|
+
});
|
|
7613
|
+
};
|
|
7229
7614
|
const ReportApp = ({
|
|
7230
7615
|
params,
|
|
7231
7616
|
api
|
|
@@ -7292,7 +7677,13 @@ const ReportApp = ({
|
|
|
7292
7677
|
}
|
|
7293
7678
|
}
|
|
7294
7679
|
})
|
|
7295
|
-
}, /*#__PURE__*/React__default.createElement(
|
|
7680
|
+
}, /*#__PURE__*/React__default.createElement(Box, {
|
|
7681
|
+
sx: {
|
|
7682
|
+
display: "flex",
|
|
7683
|
+
justifyContent: "flex-end",
|
|
7684
|
+
p: 1
|
|
7685
|
+
}
|
|
7686
|
+
}, /*#__PURE__*/React__default.createElement(CurrencySelector, null)), /*#__PURE__*/React__default.createElement(ReportDefinitionsManager, null))));
|
|
7296
7687
|
};
|
|
7297
7688
|
|
|
7298
7689
|
var index = {
|