@dashadmin/dash-reports 1.3.72 → 1.3.74-srcfree.1
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/ReportFilters.d.ts +9 -0
- package/dist/ReportFiltersDrawer.d.ts +12 -0
- package/dist/ReportPage.d.ts +9 -0
- package/dist/ReportsDashboard.d.ts +7 -0
- package/dist/ReportsIndex.d.ts +7 -0
- package/dist/ReportsModule.d.ts +7 -0
- package/dist/TenantDashboard.d.ts +12 -0
- package/dist/adapters/mui/BarWidget.d.ts +3 -0
- package/dist/adapters/mui/DoughnutWidget.d.ts +3 -0
- package/dist/adapters/mui/FunnelWidget.d.ts +3 -0
- package/dist/adapters/mui/KpiWidget.d.ts +3 -0
- package/dist/adapters/mui/LineWidget.d.ts +3 -0
- package/dist/adapters/mui/PieWidget.d.ts +3 -0
- package/dist/adapters/mui/PreviewWidget.d.ts +10 -0
- package/dist/adapters/mui/TableWidget.d.ts +16 -0
- package/dist/adapters/mui/chartSetup.d.ts +2 -0
- package/dist/adapters/mui/chartTheme.d.ts +71 -0
- package/dist/adapters/mui/index.d.ts +12 -0
- package/dist/adapters/mui/palette.d.ts +9 -0
- package/dist/adapters/mui/useChartData.d.ts +14 -0
- package/dist/core/formatBucket.d.ts +4 -0
- package/dist/core/label.d.ts +3 -0
- package/dist/core/queryParams.d.ts +3 -0
- package/dist/core/types.d.ts +94 -0
- package/dist/core/useDashboard.d.ts +50 -0
- package/dist/core/useReportExport.d.ts +7 -0
- package/dist/core/useReportList.d.ts +7 -0
- package/dist/core/useReportPreview.d.ts +7 -0
- package/dist/core/useReportSeries.d.ts +8 -0
- package/dist/core/useReportSpec.d.ts +9 -0
- package/dist/core/useReportTable.d.ts +10 -0
- package/dist/core/visualizationLayout.d.ts +13 -0
- package/dist/core/visualizationRegistry.d.ts +14 -0
- package/dist/createReportsResource.d.ts +40 -0
- package/dist/i18n/en.d.ts +83 -0
- package/dist/i18n/es.d.ts +83 -0
- package/{src/index.tsx → dist/index.d.ts} +3 -31
- package/package.json +13 -10
- package/src/ReportFilters.tsx +0 -145
- package/src/ReportFiltersDrawer.tsx +0 -73
- package/src/ReportPage.tsx +0 -280
- package/src/ReportsDashboard.tsx +0 -61
- package/src/ReportsIndex.tsx +0 -113
- package/src/ReportsModule.tsx +0 -42
- package/src/TenantDashboard.tsx +0 -290
- package/src/adapters/mui/BarWidget.tsx +0 -66
- package/src/adapters/mui/DoughnutWidget.tsx +0 -112
- package/src/adapters/mui/FunnelWidget.tsx +0 -108
- package/src/adapters/mui/KpiWidget.tsx +0 -123
- package/src/adapters/mui/LineWidget.tsx +0 -70
- package/src/adapters/mui/PieWidget.tsx +0 -16
- package/src/adapters/mui/PreviewWidget.tsx +0 -123
- package/src/adapters/mui/TableWidget.tsx +0 -141
- package/src/adapters/mui/chartSetup.ts +0 -53
- package/src/adapters/mui/chartTheme.ts +0 -84
- package/src/adapters/mui/index.ts +0 -29
- package/src/adapters/mui/palette.ts +0 -85
- package/src/adapters/mui/useChartData.ts +0 -62
- package/src/core/formatBucket.ts +0 -73
- package/src/core/label.ts +0 -28
- package/src/core/queryParams.ts +0 -27
- package/src/core/types.ts +0 -157
- package/src/core/useDashboard.ts +0 -161
- package/src/core/useReportExport.ts +0 -68
- package/src/core/useReportList.ts +0 -51
- package/src/core/useReportPreview.ts +0 -68
- package/src/core/useReportSeries.ts +0 -59
- package/src/core/useReportSpec.ts +0 -79
- package/src/core/useReportTable.ts +0 -71
- package/src/core/visualizationLayout.ts +0 -34
- package/src/core/visualizationRegistry.ts +0 -39
- package/src/createReportsResource.tsx +0 -81
- package/src/i18n/en.ts +0 -95
- package/src/i18n/es.ts +0 -84
- /package/{src/i18n/index.ts → dist/i18n/index.d.ts} +0 -0
|
@@ -1,108 +0,0 @@
|
|
|
1
|
-
import { Box, LinearProgress, Stack, Typography, useTheme } from '@mui/material';
|
|
2
|
-
import { colorForSlot } from './palette';
|
|
3
|
-
import { label as renderLabel } from '../../core/label';
|
|
4
|
-
import type { VisualizationProps } from '../../core/visualizationRegistry';
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* Stage-by-stage drop-off.
|
|
8
|
-
*
|
|
9
|
-
* Rendered as proportional bars rather than the tapering-trapezoid shape a
|
|
10
|
-
* "funnel" usually implies: a trapezoid encodes each stage in an area whose
|
|
11
|
-
* width AND height both change, which overstates the drop. A bar per stage
|
|
12
|
-
* encodes one quantity in one dimension and is simply readable.
|
|
13
|
-
*
|
|
14
|
-
* The stage order is the METRIC order the backend declared, which is the flow
|
|
15
|
-
* order — so this widget renders KitchnTabs' tab lifecycle without knowing
|
|
16
|
-
* anything about it. Percentages are shown against the first stage (overall
|
|
17
|
-
* conversion) and against the previous one (step conversion), because the two
|
|
18
|
-
* answer different questions and reading either off a bar length is guesswork.
|
|
19
|
-
*/
|
|
20
|
-
export function FunnelWidget({ result, spec, translate }: VisualizationProps) {
|
|
21
|
-
const theme = useTheme();
|
|
22
|
-
|
|
23
|
-
const stages = spec.metrics
|
|
24
|
-
.map((metric, index) => ({
|
|
25
|
-
key: metric.key,
|
|
26
|
-
label: renderLabel(translate, metric.label, metric.labelText),
|
|
27
|
-
value: result.totals[metric.key] ?? 0,
|
|
28
|
-
slot: index,
|
|
29
|
-
}))
|
|
30
|
-
.filter((s) => s.value !== null);
|
|
31
|
-
|
|
32
|
-
if (stages.length === 0) return null;
|
|
33
|
-
|
|
34
|
-
const first = stages[0]?.value || 0;
|
|
35
|
-
|
|
36
|
-
return (
|
|
37
|
-
<Stack spacing={1.75} sx={{ px: 0.5, py: 1 }}>
|
|
38
|
-
{stages.map((stage, index) => {
|
|
39
|
-
const previous = index === 0 ? stage.value : stages[index - 1].value;
|
|
40
|
-
const ofFirst = first > 0 ? (stage.value / first) * 100 : 0;
|
|
41
|
-
const ofPrevious = previous > 0 ? (stage.value / previous) * 100 : 0;
|
|
42
|
-
const color = colorForSlot(stage.slot, theme.palette.mode === 'dark');
|
|
43
|
-
|
|
44
|
-
return (
|
|
45
|
-
<Box key={stage.key}>
|
|
46
|
-
<Stack
|
|
47
|
-
direction="row"
|
|
48
|
-
sx={{ justifyContent: 'space-between', alignItems: 'baseline', gap: 1, mb: 0.5 }}
|
|
49
|
-
>
|
|
50
|
-
{/* The label is the only part allowed to shrink; the figures stay
|
|
51
|
-
on one line. Previously the percentage column wrapped to three
|
|
52
|
-
lines ("100%", "etapa", "833.3%") and the row read as noise. */}
|
|
53
|
-
<Typography
|
|
54
|
-
variant="body2"
|
|
55
|
-
sx={{
|
|
56
|
-
color: theme.palette.text.primary,
|
|
57
|
-
overflow: 'hidden',
|
|
58
|
-
textOverflow: 'ellipsis',
|
|
59
|
-
whiteSpace: 'nowrap',
|
|
60
|
-
minWidth: 0,
|
|
61
|
-
}}
|
|
62
|
-
>
|
|
63
|
-
{stage.label}
|
|
64
|
-
</Typography>
|
|
65
|
-
<Stack
|
|
66
|
-
direction="row"
|
|
67
|
-
spacing={1.5}
|
|
68
|
-
sx={{ alignItems: 'baseline', flexShrink: 0, whiteSpace: 'nowrap' }}
|
|
69
|
-
>
|
|
70
|
-
<Typography variant="body2" sx={{ color: theme.palette.text.primary, fontWeight: 500 }}>
|
|
71
|
-
{stage.value.toLocaleString()}
|
|
72
|
-
</Typography>
|
|
73
|
-
<Typography variant="caption" sx={{ color: theme.palette.text.secondary }}>
|
|
74
|
-
{Math.round(ofFirst * 10) / 10}%
|
|
75
|
-
{/* A step above 100% is not a conversion — it means tabs
|
|
76
|
-
reached this stage without passing through the previous
|
|
77
|
-
one (a tab closed straight from CREATED, say). Printing
|
|
78
|
-
"833% conversion" reads as a broken number; naming the
|
|
79
|
-
skip states what actually happened. */}
|
|
80
|
-
{index > 0 && (
|
|
81
|
-
ofPrevious > 100
|
|
82
|
-
? ` · ${translate('dash_reports.funnel.skipped', { _: 'stage skipped' })}`
|
|
83
|
-
: ` · ${translate('dash_reports.funnel.step', { _: 'step' })} ${Math.round(ofPrevious * 10) / 10}%`
|
|
84
|
-
)}
|
|
85
|
-
</Typography>
|
|
86
|
-
</Stack>
|
|
87
|
-
</Stack>
|
|
88
|
-
<LinearProgress
|
|
89
|
-
variant="determinate"
|
|
90
|
-
value={Math.min(100, ofFirst)}
|
|
91
|
-
aria-label={stage.label}
|
|
92
|
-
sx={{
|
|
93
|
-
height: 10,
|
|
94
|
-
borderRadius: 1,
|
|
95
|
-
backgroundColor: theme.palette.mode === 'dark'
|
|
96
|
-
? 'rgba(255,255,255,0.08)'
|
|
97
|
-
: 'rgba(0,0,0,0.06)',
|
|
98
|
-
'& .MuiLinearProgress-bar': { backgroundColor: color, borderRadius: 1 },
|
|
99
|
-
}}
|
|
100
|
-
/>
|
|
101
|
-
</Box>
|
|
102
|
-
);
|
|
103
|
-
})}
|
|
104
|
-
</Stack>
|
|
105
|
-
);
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
export default FunnelWidget;
|
|
@@ -1,123 +0,0 @@
|
|
|
1
|
-
import { Box, Typography, useTheme } from '@mui/material';
|
|
2
|
-
import { label as renderLabel } from '../../core/label';
|
|
3
|
-
import type { VisualizationProps } from '../../core/visualizationRegistry';
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* The headline numbers, with no plot.
|
|
7
|
-
*
|
|
8
|
-
* The window totals come straight from the backend's ungrouped aggregate — not
|
|
9
|
-
* a sum of the series — because averages and extrema do not compose across
|
|
10
|
-
* buckets: averaging twelve monthly averages is not the yearly average.
|
|
11
|
-
*
|
|
12
|
-
* Figures use the body face at a normal weight, and deliberately NOT
|
|
13
|
-
* `tabular-nums`: equal-width digits make a large standalone number look gappy
|
|
14
|
-
* and mechanical. Tabular figures belong in a column of numbers, which is what
|
|
15
|
-
* the table view is for.
|
|
16
|
-
*/
|
|
17
|
-
/**
|
|
18
|
-
* A figure, in the unit its metric is measured in.
|
|
19
|
-
*
|
|
20
|
-
* Durations are minutes, and are shown as minutes rather than a bare number —
|
|
21
|
-
* "643" beside "Entregado → Cerrado" is ambiguous in a way "643 min" is not.
|
|
22
|
-
* Sub-minute averages keep two decimals so a fast hop reads as 0.26 min rather
|
|
23
|
-
* than rounding to a flat 0, which would look like "no data".
|
|
24
|
-
*/
|
|
25
|
-
function formatMetricValue(
|
|
26
|
-
value: number | null | undefined,
|
|
27
|
-
metric: { decimal: boolean; unit?: string | null },
|
|
28
|
-
translate: (key: string, options?: Record<string, unknown>) => string,
|
|
29
|
-
): string {
|
|
30
|
-
if (value === null || value === undefined) return '—';
|
|
31
|
-
|
|
32
|
-
const digits = metric.decimal ? 2 : 0;
|
|
33
|
-
const text = value.toLocaleString(undefined, {
|
|
34
|
-
minimumFractionDigits: digits,
|
|
35
|
-
maximumFractionDigits: digits,
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
if (metric.unit === 'minutes') {
|
|
39
|
-
return formatMinutes(value, translate);
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
return text;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
/**
|
|
46
|
-
* A duration in the unit a person would say it in.
|
|
47
|
-
*
|
|
48
|
-
* "776.40 min" is technically right and practically unreadable — nobody holds
|
|
49
|
-
* thirteen hours in their head as a minute count. The scale switches at the
|
|
50
|
-
* points where the smaller unit stops being informative: under a minute,
|
|
51
|
-
* seconds; under 90 minutes, minutes; beyond that, hours.
|
|
52
|
-
*/
|
|
53
|
-
function formatMinutes(
|
|
54
|
-
minutes: number,
|
|
55
|
-
translate: (key: string, options?: Record<string, unknown>) => string,
|
|
56
|
-
): string {
|
|
57
|
-
const abs = Math.abs(minutes);
|
|
58
|
-
|
|
59
|
-
if (abs < 1) {
|
|
60
|
-
const seconds = Math.round(minutes * 60);
|
|
61
|
-
return `${seconds} ${translate('dash_reports.units.seconds', { _: 's' })}`;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
if (abs < 90) {
|
|
65
|
-
const shown = abs < 10 ? Math.round(minutes * 10) / 10 : Math.round(minutes);
|
|
66
|
-
return `${shown} ${translate('dash_reports.units.minutes', { _: 'min' })}`;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
const hours = Math.round((minutes / 60) * 10) / 10;
|
|
70
|
-
return `${hours} ${translate('dash_reports.units.hours', { _: 'h' })}`;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
export function KpiWidget({ result, spec, translate }: VisualizationProps) {
|
|
74
|
-
const theme = useTheme();
|
|
75
|
-
|
|
76
|
-
const metrics = spec.metrics.filter((m) => result.totals[m.key] !== undefined);
|
|
77
|
-
|
|
78
|
-
if (metrics.length === 0) return null;
|
|
79
|
-
|
|
80
|
-
// A grid, not a wrapping flex row: with flex-wrap the figures landed at
|
|
81
|
-
// arbitrary x-positions on each line, so nothing lined up vertically and a
|
|
82
|
-
// six-metric tile read as scattered rather than tabulated.
|
|
83
|
-
return (
|
|
84
|
-
<Box
|
|
85
|
-
sx={{
|
|
86
|
-
display: 'grid',
|
|
87
|
-
gridTemplateColumns: 'repeat(auto-fit, minmax(140px, 1fr))',
|
|
88
|
-
gap: 2,
|
|
89
|
-
px: 0.5,
|
|
90
|
-
py: 1,
|
|
91
|
-
}}
|
|
92
|
-
role="list"
|
|
93
|
-
>
|
|
94
|
-
{metrics.map((metric) => {
|
|
95
|
-
const value = result.totals[metric.key];
|
|
96
|
-
|
|
97
|
-
return (
|
|
98
|
-
<Box key={metric.key} role="listitem" sx={{ minWidth: 140 }}>
|
|
99
|
-
<Typography
|
|
100
|
-
variant="body2"
|
|
101
|
-
sx={{ color: theme.palette.text.secondary, mb: 0.5 }}
|
|
102
|
-
>
|
|
103
|
-
{renderLabel(translate, metric.label, metric.labelText)}
|
|
104
|
-
</Typography>
|
|
105
|
-
<Typography
|
|
106
|
-
component="p"
|
|
107
|
-
sx={{
|
|
108
|
-
fontSize: '1.75rem',
|
|
109
|
-
lineHeight: 1.15,
|
|
110
|
-
fontWeight: 500,
|
|
111
|
-
color: theme.palette.text.primary,
|
|
112
|
-
}}
|
|
113
|
-
>
|
|
114
|
-
{formatMetricValue(value, metric, translate)}
|
|
115
|
-
</Typography>
|
|
116
|
-
</Box>
|
|
117
|
-
);
|
|
118
|
-
})}
|
|
119
|
-
</Box>
|
|
120
|
-
);
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
export default KpiWidget;
|
|
@@ -1,70 +0,0 @@
|
|
|
1
|
-
import { Line } from 'react-chartjs-2';
|
|
2
|
-
import { ensureChartsRegistered } from './chartSetup';
|
|
3
|
-
import { baseChartOptions } from './chartTheme';
|
|
4
|
-
import { useChartData } from './useChartData';
|
|
5
|
-
import type { VisualizationProps } from '../../core/visualizationRegistry';
|
|
6
|
-
|
|
7
|
-
ensureChartsRegistered();
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* Change over time.
|
|
11
|
-
*
|
|
12
|
-
* 2px strokes and points that only appear on hover — a marker on every bucket
|
|
13
|
-
* of a 90-day series is noise, but the hover target still has to be reachable,
|
|
14
|
-
* so hitRadius stays generous while the drawn radius is zero. Lines are not
|
|
15
|
-
* filled by default: overlapping translucent areas muddy every colour beneath
|
|
16
|
-
* them, and this chart is for comparing shapes.
|
|
17
|
-
*
|
|
18
|
-
* `spanGaps` is deliberately off. A null is "no data" (an average with nothing
|
|
19
|
-
* to average), and bridging it would draw a straight line through a period the
|
|
20
|
-
* report is explicitly saying nothing about.
|
|
21
|
-
*/
|
|
22
|
-
export function LineWidget(props: VisualizationProps) {
|
|
23
|
-
const { chrome, datasets, labels, longLabels } = useChartData(props);
|
|
24
|
-
const base = baseChartOptions(chrome, datasets.length > 1);
|
|
25
|
-
|
|
26
|
-
const data = {
|
|
27
|
-
labels,
|
|
28
|
-
datasets: datasets.map((d) => ({
|
|
29
|
-
label: d.label,
|
|
30
|
-
data: d.data,
|
|
31
|
-
borderColor: d.color,
|
|
32
|
-
backgroundColor: d.color,
|
|
33
|
-
borderWidth: 2,
|
|
34
|
-
tension: 0.25,
|
|
35
|
-
pointRadius: 0,
|
|
36
|
-
pointHoverRadius: 5,
|
|
37
|
-
// A 12px hit radius on an invisible point: the value stays reachable
|
|
38
|
-
// without asking anyone to land on a 2px line.
|
|
39
|
-
pointHitRadius: 12,
|
|
40
|
-
pointBorderWidth: 2,
|
|
41
|
-
// A surface-coloured ring keeps an active point legible where series
|
|
42
|
-
// overlap, without drawing a border on every mark.
|
|
43
|
-
pointHoverBorderColor: chrome.surface,
|
|
44
|
-
pointHoverBorderWidth: 2,
|
|
45
|
-
spanGaps: false,
|
|
46
|
-
fill: false,
|
|
47
|
-
})),
|
|
48
|
-
};
|
|
49
|
-
|
|
50
|
-
return (
|
|
51
|
-
<Line
|
|
52
|
-
data={data}
|
|
53
|
-
height={props.height ?? 320}
|
|
54
|
-
options={{
|
|
55
|
-
...base,
|
|
56
|
-
plugins: {
|
|
57
|
-
...base.plugins,
|
|
58
|
-
tooltip: {
|
|
59
|
-
...base.plugins.tooltip,
|
|
60
|
-
callbacks: {
|
|
61
|
-
title: (items: Array<{ dataIndex: number }>) => longLabels[items[0]?.dataIndex] ?? '',
|
|
62
|
-
},
|
|
63
|
-
},
|
|
64
|
-
},
|
|
65
|
-
}}
|
|
66
|
-
/>
|
|
67
|
-
);
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
export default LineWidget;
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
import { DoughnutWidget } from './DoughnutWidget';
|
|
2
|
-
import type { VisualizationProps } from '../../core/visualizationRegistry';
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* A doughnut, not a true pie.
|
|
6
|
-
*
|
|
7
|
-
* Kept as a distinct registered type so a report that declares `Pie` renders,
|
|
8
|
-
* but drawn with a centre hole: the hole removes the wedge apexes, where a
|
|
9
|
-
* pie's area is least readable, and gives the legend somewhere to breathe.
|
|
10
|
-
* Nothing is lost — both encode a part-of-whole share by angle.
|
|
11
|
-
*/
|
|
12
|
-
export function PieWidget(props: VisualizationProps) {
|
|
13
|
-
return <DoughnutWidget {...props} />;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export default PieWidget;
|
|
@@ -1,123 +0,0 @@
|
|
|
1
|
-
import { Box, Skeleton, Stack, Typography, useTheme } from '@mui/material';
|
|
2
|
-
import { Bar, Line } from 'react-chartjs-2';
|
|
3
|
-
import { ensureChartsRegistered } from './chartSetup';
|
|
4
|
-
import { chartChrome } from './chartTheme';
|
|
5
|
-
import { buildSlotMap, colorForSlot, MAX_SERIES } from './palette';
|
|
6
|
-
import { label as renderLabel } from '../../core/label';
|
|
7
|
-
import type { ReportMetric, ReportResult, TranslateFn, VisualizationType } from '../../core/types';
|
|
8
|
-
|
|
9
|
-
ensureChartsRegistered();
|
|
10
|
-
|
|
11
|
-
export interface PreviewWidgetProps {
|
|
12
|
-
result: ReportResult | null;
|
|
13
|
-
loading: boolean;
|
|
14
|
-
type: VisualizationType;
|
|
15
|
-
translate: TranslateFn;
|
|
16
|
-
/** From the list payload; without it the card would print raw metric keys. */
|
|
17
|
-
metrics?: ReportMetric[];
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
const PREVIEW_HEIGHT = 96;
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* The card-sized rendering of a report.
|
|
24
|
-
*
|
|
25
|
-
* Deliberately not the full widget shrunk down. At this size axes, legends and
|
|
26
|
-
* gridlines are unreadable ink — so the preview keeps only the shape (or, for a
|
|
27
|
-
* report whose answer is a number, just the number). Everything is stripped:
|
|
28
|
-
* no axes, no legend, no tooltip. Reading a value is what opening the report is
|
|
29
|
-
* for; this is only enough to tell one report from another and see whether
|
|
30
|
-
* anything is happening.
|
|
31
|
-
*/
|
|
32
|
-
export function PreviewWidget({ result, loading, type, translate, metrics }: PreviewWidgetProps) {
|
|
33
|
-
const theme = useTheme();
|
|
34
|
-
|
|
35
|
-
if (loading) {
|
|
36
|
-
return <Skeleton variant="rounded" height={PREVIEW_HEIGHT} sx={{ mt: 1 }} />;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
if (!result || result.series.length === 0) {
|
|
40
|
-
return (
|
|
41
|
-
<Box sx={{ height: PREVIEW_HEIGHT, display: 'grid', placeItems: 'center', mt: 1 }}>
|
|
42
|
-
<Typography variant="caption" sx={{ color: theme.palette.text.secondary }}>
|
|
43
|
-
{translate('dash_reports.no_data', { _: 'No data for this range.' })}
|
|
44
|
-
</Typography>
|
|
45
|
-
</Box>
|
|
46
|
-
);
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
const chrome = chartChrome(theme);
|
|
50
|
-
|
|
51
|
-
// Kpi and Funnel both answer with numbers rather than a shape, so at card
|
|
52
|
-
// size they render as figures instead of a chart nobody could read.
|
|
53
|
-
if (type === 'Kpi' || type === 'Funnel') {
|
|
54
|
-
const entries = Object.entries(result.totals).slice(0, 3);
|
|
55
|
-
|
|
56
|
-
return (
|
|
57
|
-
<Stack direction="row" sx={{ gap: 2.5, mt: 1.5, minHeight: PREVIEW_HEIGHT, alignItems: 'center', flexWrap: 'wrap' }}>
|
|
58
|
-
{entries.map(([key, value]) => {
|
|
59
|
-
const metric = metrics?.find((m) => m.key === key);
|
|
60
|
-
|
|
61
|
-
return (
|
|
62
|
-
<Box key={key}>
|
|
63
|
-
<Typography variant="caption" sx={{ color: theme.palette.text.secondary, display: 'block' }}>
|
|
64
|
-
{metric ? renderLabel(translate, metric.label, metric.labelText) : key}
|
|
65
|
-
</Typography>
|
|
66
|
-
<Typography sx={{ fontSize: '1.35rem', lineHeight: 1.2, color: theme.palette.text.primary }}>
|
|
67
|
-
{value === null || value === undefined ? '—' : value.toLocaleString()}
|
|
68
|
-
</Typography>
|
|
69
|
-
</Box>
|
|
70
|
-
);
|
|
71
|
-
})}
|
|
72
|
-
</Stack>
|
|
73
|
-
);
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
const slots = buildSlotMap(null, result.meta?.pivot ?? undefined, result.series.map((s) => s.key));
|
|
77
|
-
const datasets = result.series
|
|
78
|
-
.filter((s) => (slots.get(s.key) ?? 0) < MAX_SERIES)
|
|
79
|
-
.map((s) => ({
|
|
80
|
-
label: s.label,
|
|
81
|
-
data: s.data,
|
|
82
|
-
color: colorForSlot(slots.get(s.key) ?? 0, chrome.dark),
|
|
83
|
-
}));
|
|
84
|
-
|
|
85
|
-
const options = {
|
|
86
|
-
responsive: true,
|
|
87
|
-
maintainAspectRatio: false,
|
|
88
|
-
// Every affordance off: at 96px these are ink, not information.
|
|
89
|
-
plugins: { legend: { display: false }, tooltip: { enabled: false } },
|
|
90
|
-
scales: {
|
|
91
|
-
x: { display: false },
|
|
92
|
-
y: { display: false, beginAtZero: true },
|
|
93
|
-
},
|
|
94
|
-
elements: { point: { radius: 0 } },
|
|
95
|
-
};
|
|
96
|
-
|
|
97
|
-
const data = {
|
|
98
|
-
labels: result.buckets.map(() => ''),
|
|
99
|
-
datasets: datasets.map((d) => ({
|
|
100
|
-
label: d.label,
|
|
101
|
-
data: d.data,
|
|
102
|
-
backgroundColor: d.color,
|
|
103
|
-
borderColor: d.color,
|
|
104
|
-
borderWidth: type === 'Line' ? 2 : 0,
|
|
105
|
-
borderRadius: 3,
|
|
106
|
-
borderSkipped: 'bottom' as const,
|
|
107
|
-
maxBarThickness: 14,
|
|
108
|
-
tension: 0.3,
|
|
109
|
-
fill: false,
|
|
110
|
-
pointRadius: 0,
|
|
111
|
-
})),
|
|
112
|
-
};
|
|
113
|
-
|
|
114
|
-
return (
|
|
115
|
-
<Box sx={{ height: PREVIEW_HEIGHT, mt: 1 }}>
|
|
116
|
-
{type === 'Line'
|
|
117
|
-
? <Line data={data} options={options} height={PREVIEW_HEIGHT} />
|
|
118
|
-
: <Bar data={data} options={options} height={PREVIEW_HEIGHT} />}
|
|
119
|
-
</Box>
|
|
120
|
-
);
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
export default PreviewWidget;
|
|
@@ -1,141 +0,0 @@
|
|
|
1
|
-
import { useMemo } from 'react';
|
|
2
|
-
import {
|
|
3
|
-
Box, Table, TableBody, TableCell, TableContainer, TableHead, TablePagination, TableRow, Typography, useTheme,
|
|
4
|
-
} from '@mui/material';
|
|
5
|
-
import { formatBucketLong } from '../../core/formatBucket';
|
|
6
|
-
import { label as renderLabel } from '../../core/label';
|
|
7
|
-
import type { VisualizationProps } from '../../core/visualizationRegistry';
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* The series as numbers.
|
|
11
|
-
*
|
|
12
|
-
* Not optional decoration. Three of the light-mode series hues sit below 3:1
|
|
13
|
-
* contrast against a light surface, which is permitted only where a
|
|
14
|
-
* non-colour path to every value exists — this is that path. It is also the
|
|
15
|
-
* answer to "a tooltip is the only way to read a value".
|
|
16
|
-
*
|
|
17
|
-
* Figures are tabular here, unlike the KPI tile: in a column of numbers,
|
|
18
|
-
* equal-width digits are what makes magnitudes line up and scan.
|
|
19
|
-
*/
|
|
20
|
-
export function SeriesTableWidget({ result, translate }: VisualizationProps) {
|
|
21
|
-
const theme = useTheme();
|
|
22
|
-
|
|
23
|
-
const rows = useMemo(
|
|
24
|
-
() => result.buckets.map((bucket, index) => ({
|
|
25
|
-
bucket: formatBucketLong(bucket, result.granularity, translate),
|
|
26
|
-
values: result.series.map((s) => s.data[index]),
|
|
27
|
-
})),
|
|
28
|
-
[result, translate],
|
|
29
|
-
);
|
|
30
|
-
|
|
31
|
-
return (
|
|
32
|
-
<TableContainer sx={{ maxHeight: 420 }}>
|
|
33
|
-
<Table size="small" stickyHeader>
|
|
34
|
-
<TableHead>
|
|
35
|
-
<TableRow>
|
|
36
|
-
<TableCell sx={{ color: theme.palette.text.secondary }}>
|
|
37
|
-
{translate('dash_reports.table.period', { _: 'Period' })}
|
|
38
|
-
</TableCell>
|
|
39
|
-
{result.series.map((s) => (
|
|
40
|
-
<TableCell key={s.key} align="right" sx={{ color: theme.palette.text.secondary }}>
|
|
41
|
-
{renderLabel(translate, s.label, s.labelText)}
|
|
42
|
-
</TableCell>
|
|
43
|
-
))}
|
|
44
|
-
</TableRow>
|
|
45
|
-
</TableHead>
|
|
46
|
-
<TableBody>
|
|
47
|
-
{rows.map((row) => (
|
|
48
|
-
<TableRow key={row.bucket} hover>
|
|
49
|
-
<TableCell sx={{ color: theme.palette.text.primary }}>{row.bucket}</TableCell>
|
|
50
|
-
{row.values.map((value, i) => (
|
|
51
|
-
<TableCell
|
|
52
|
-
key={i}
|
|
53
|
-
align="right"
|
|
54
|
-
sx={{ color: theme.palette.text.primary, fontVariantNumeric: 'tabular-nums' }}
|
|
55
|
-
>
|
|
56
|
-
{value === null || value === undefined ? '—' : value.toLocaleString()}
|
|
57
|
-
</TableCell>
|
|
58
|
-
))}
|
|
59
|
-
</TableRow>
|
|
60
|
-
))}
|
|
61
|
-
</TableBody>
|
|
62
|
-
</Table>
|
|
63
|
-
</TableContainer>
|
|
64
|
-
);
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
export interface DetailTableProps {
|
|
68
|
-
page: {
|
|
69
|
-
data: Array<Record<string, unknown>>;
|
|
70
|
-
total: number;
|
|
71
|
-
page: number;
|
|
72
|
-
perPage: number;
|
|
73
|
-
} | null;
|
|
74
|
-
loading: boolean;
|
|
75
|
-
currentPage: number;
|
|
76
|
-
onPageChange: (page: number) => void;
|
|
77
|
-
translate: (key: string, options?: Record<string, unknown>) => string;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
/** The paginated source rows behind the chart. */
|
|
81
|
-
export function DetailTable({ page, loading, currentPage, onPageChange, translate }: DetailTableProps) {
|
|
82
|
-
const theme = useTheme();
|
|
83
|
-
const rows = page?.data ?? [];
|
|
84
|
-
const columns = rows.length > 0 ? Object.keys(rows[0]) : [];
|
|
85
|
-
|
|
86
|
-
if (!loading && rows.length === 0) {
|
|
87
|
-
return (
|
|
88
|
-
<Box sx={{ p: 3 }}>
|
|
89
|
-
<Typography variant="body2" sx={{ color: theme.palette.text.secondary }}>
|
|
90
|
-
{translate('dash_reports.no_rows', { _: 'No rows for this range.' })}
|
|
91
|
-
</Typography>
|
|
92
|
-
</Box>
|
|
93
|
-
);
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
return (
|
|
97
|
-
<Box>
|
|
98
|
-
<TableContainer sx={{ maxHeight: 420 }}>
|
|
99
|
-
<Table size="small" stickyHeader>
|
|
100
|
-
<TableHead>
|
|
101
|
-
<TableRow>
|
|
102
|
-
{columns.map((column) => (
|
|
103
|
-
<TableCell key={column} sx={{ color: theme.palette.text.secondary, whiteSpace: 'nowrap' }}>
|
|
104
|
-
{translate(`dash_reports.columns.${column}`, { _: column })}
|
|
105
|
-
</TableCell>
|
|
106
|
-
))}
|
|
107
|
-
</TableRow>
|
|
108
|
-
</TableHead>
|
|
109
|
-
<TableBody>
|
|
110
|
-
{rows.map((row, index) => (
|
|
111
|
-
<TableRow key={index} hover>
|
|
112
|
-
{columns.map((column) => (
|
|
113
|
-
<TableCell key={column} sx={{ color: theme.palette.text.primary, whiteSpace: 'nowrap' }}>
|
|
114
|
-
{formatCell(row[column])}
|
|
115
|
-
</TableCell>
|
|
116
|
-
))}
|
|
117
|
-
</TableRow>
|
|
118
|
-
))}
|
|
119
|
-
</TableBody>
|
|
120
|
-
</Table>
|
|
121
|
-
</TableContainer>
|
|
122
|
-
<TablePagination
|
|
123
|
-
component="div"
|
|
124
|
-
count={page?.total ?? 0}
|
|
125
|
-
page={Math.max(0, currentPage - 1)}
|
|
126
|
-
onPageChange={(_, next) => onPageChange(next + 1)}
|
|
127
|
-
rowsPerPage={page?.perPage ?? 50}
|
|
128
|
-
rowsPerPageOptions={[page?.perPage ?? 50]}
|
|
129
|
-
/>
|
|
130
|
-
</Box>
|
|
131
|
-
);
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
function formatCell(value: unknown): string {
|
|
135
|
-
if (value === null || value === undefined) return '—';
|
|
136
|
-
if (typeof value === 'boolean') return value ? '✓' : '—';
|
|
137
|
-
if (typeof value === 'object') return JSON.stringify(value);
|
|
138
|
-
return String(value);
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
export default SeriesTableWidget;
|
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
BarController,
|
|
3
|
-
BarElement,
|
|
4
|
-
CategoryScale,
|
|
5
|
-
Chart as ChartJS,
|
|
6
|
-
Filler,
|
|
7
|
-
Legend,
|
|
8
|
-
LineController,
|
|
9
|
-
LineElement,
|
|
10
|
-
LinearScale,
|
|
11
|
-
ArcElement,
|
|
12
|
-
DoughnutController,
|
|
13
|
-
PieController,
|
|
14
|
-
PointElement,
|
|
15
|
-
Tooltip,
|
|
16
|
-
} from 'chart.js';
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* Register only the Chart.js pieces these widgets use.
|
|
20
|
-
*
|
|
21
|
-
* Explicit registration rather than `registerables`, which pulls in every
|
|
22
|
-
* controller, scale and plugin Chart.js ships. The legacy Stats page imported
|
|
23
|
-
* the lot for two chart types.
|
|
24
|
-
*
|
|
25
|
-
* Note what is absent: the datalabels plugin. A value printed on every point is
|
|
26
|
-
* an anti-pattern — it is unreadable at any real series count and goes unread.
|
|
27
|
-
* Values are reachable through the tooltip and, always, the table view.
|
|
28
|
-
*/
|
|
29
|
-
let registered = false;
|
|
30
|
-
|
|
31
|
-
export function ensureChartsRegistered(): void {
|
|
32
|
-
if (registered) return;
|
|
33
|
-
|
|
34
|
-
ChartJS.register(
|
|
35
|
-
BarController,
|
|
36
|
-
BarElement,
|
|
37
|
-
LineController,
|
|
38
|
-
LineElement,
|
|
39
|
-
PointElement,
|
|
40
|
-
ArcElement,
|
|
41
|
-
DoughnutController,
|
|
42
|
-
PieController,
|
|
43
|
-
CategoryScale,
|
|
44
|
-
LinearScale,
|
|
45
|
-
Filler,
|
|
46
|
-
Legend,
|
|
47
|
-
Tooltip,
|
|
48
|
-
);
|
|
49
|
-
|
|
50
|
-
registered = true;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
export default ensureChartsRegistered;
|