Skip to content

Commit d299db0

Browse files
authored
StatsHouse UI: grid dash step 3, new dashboard layout (#1963)
1 parent a775fec commit d299db0

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

56 files changed

+1225
-960
lines changed

statshouse-ui/src/api/dashboard.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,10 @@ import {
1717
} from '@tanstack/react-query';
1818
import { queryClient } from '@/common/queryClient';
1919
import { type QueryParams, urlEncode } from '@/url2';
20-
import { dashboardMigrateSaveToOld } from '@/store2/urlStore/dashboardMigrate';
20+
import { dashboardMigrateSaveToOld, fixV4forDash } from '@/store2/urlStore/dashboardMigrate';
2121
import { toNumber } from '@/common/helpers';
2222
import { API_HISTORY } from './history';
23+
import { setFullDashSave } from '@/common/migrate/migrate3to4';
2324

2425
const ApiDashboardEndpoint = '/api/dashboard';
2526

@@ -152,7 +153,9 @@ export function useApiDashboard<T = ApiDashboard>(
152153
return useQuery({ ...options, select, enabled });
153154
}
154155
export function getDashboardSaveFetchParams(params: QueryParams, remove?: boolean, copy?: boolean): DashboardInfo {
155-
const searchParams = urlEncode(params);
156+
setFullDashSave(true);
157+
const searchParams = urlEncode(fixV4forDash(params));
158+
setFullDashSave(false);
156159

157160
const oldDashboardParams = dashboardMigrateSaveToOld(params);
158161
oldDashboardParams.dashboard.data.searchParams = searchParams;

statshouse-ui/src/api/enum.ts

Lines changed: 1 addition & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ export const GET_PARAMS = {
5959
dashboardName: 'dn',
6060
dashboardDescription: 'dd',
6161
dashboardVersion: 'dv',
62+
dashboardSchemeVersion: 'ver',
6263
metricsGroupID: 'id',
6364
metricsNamespacesID: 'id',
6465
metricsListAndDisabled: 'sd',
@@ -449,20 +450,3 @@ export const TIME_RANGE_ABBREV_DESCRIPTION: Record<TimeRangeAbbrev, string> = {
449450
[TIME_RANGE_ABBREV.last1y]: 'Last year',
450451
[TIME_RANGE_ABBREV.last2y]: 'Last 2 years',
451452
};
452-
453-
export const LAYOUT_WIDGET_SIZE = Object.freeze({
454-
[PLOT_TYPE.Metric]: {
455-
minW: 2,
456-
minH: 2,
457-
maxW: 12,
458-
maxH: 10,
459-
},
460-
[PLOT_TYPE.Event]: {
461-
minW: 2,
462-
minH: 2,
463-
maxW: 12,
464-
maxH: 10,
465-
},
466-
} as const);
467-
468-
export const LAYOUT_COLUMNS = 12;
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
// Copyright 2025 V Kontakte LLC
2+
//
3+
// This Source Code Form is subject to the terms of the Mozilla Public
4+
// License, v. 2.0. If a copy of the MPL was not distributed with this
5+
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
6+
7+
import '@/testMock/matchMedia.mock';
8+
import { migrate3to4 } from './migrate3to4';
9+
import { produce } from 'immer';
10+
import { getDefaultParams, getNewGroup, getNewMetric, getNewMetricLayout, getNewVariable, QueryParams } from '@/url2';
11+
import { promQLMetric } from '@/view/promQLMetric';
12+
13+
const paramsV3: QueryParams = {
14+
...getDefaultParams(),
15+
timeRange: { absolute: true, from: 0, now: 1577826000, to: 1577826000, urlTo: 1577826000 },
16+
plots: {
17+
'0': { ...getNewMetric(), metricName: 'metric0', id: '0' },
18+
'1': { ...getNewMetric(), metricName: 'metric1', id: '1' },
19+
'2': { ...getNewMetric(), metricName: 'metric2', id: '2' },
20+
'3': {
21+
...getNewMetric(),
22+
metricName: promQLMetric,
23+
promQL: 'promQL3',
24+
id: '3',
25+
},
26+
},
27+
orderPlot: ['3', '2', '1', '0'],
28+
groups: {
29+
'0': { ...getNewGroup(), id: '0', name: 'group0', count: 2, size: '3' },
30+
'1': { ...getNewGroup(), id: '1', name: 'group1', count: 0, size: '2' },
31+
'2': { ...getNewGroup(), id: '2', name: 'group2', count: 2, size: '2' },
32+
'3': { ...getNewGroup(), id: '3', name: 'group3', count: 0, size: '2' },
33+
},
34+
orderGroup: ['3', '2', '1', '0'],
35+
variables: {
36+
'0': { ...getNewVariable(), id: '0', name: 'var0' },
37+
'1': { ...getNewVariable(), id: '1', name: 'var1' },
38+
'2': { ...getNewVariable(), id: '2', name: 'var2' },
39+
'3': { ...getNewVariable(), id: '3', name: 'var3' },
40+
},
41+
orderVariables: ['3', '2', '1', '0'],
42+
};
43+
44+
const paramsV4: QueryParams = {
45+
...getDefaultParams(),
46+
timeRange: { absolute: true, from: 0, now: 1577826000, to: 1577826000, urlTo: 1577826000 },
47+
plots: {
48+
'0': {
49+
...getNewMetric(),
50+
metricName: 'metric0',
51+
group: '0',
52+
layout: getNewMetricLayout(undefined, '3', { x: 8, y: 0 }),
53+
id: '0',
54+
},
55+
'1': {
56+
...getNewMetric(),
57+
metricName: 'metric1',
58+
group: '0',
59+
layout: getNewMetricLayout(undefined, '3', { x: 0, y: 0 }),
60+
id: '1',
61+
},
62+
'2': {
63+
...getNewMetric(),
64+
metricName: 'metric2',
65+
group: '2',
66+
layout: getNewMetricLayout(undefined, '2', { x: 12, y: 0 }),
67+
id: '2',
68+
},
69+
'3': {
70+
...getNewMetric(),
71+
metricName: promQLMetric,
72+
promQL: 'promQL3',
73+
group: '2',
74+
layout: getNewMetricLayout(undefined, '2', { x: 0, y: 0 }),
75+
id: '3',
76+
},
77+
},
78+
orderPlot: [],
79+
groups: {
80+
'0': { ...getNewGroup(), id: '0', name: 'group0', count: 2, size: '3' },
81+
'1': { ...getNewGroup(), id: '1', name: 'group1', count: 0, size: '2' },
82+
'2': { ...getNewGroup(), id: '2', name: 'group2', count: 2, size: '2' },
83+
'3': { ...getNewGroup(), id: '3', name: 'group3', count: 0, size: '2' },
84+
},
85+
orderGroup: ['3', '2', '1', '0'],
86+
variables: {
87+
'0': { ...getNewVariable(), id: '0', name: 'var0' },
88+
'1': { ...getNewVariable(), id: '1', name: 'var1' },
89+
'2': { ...getNewVariable(), id: '2', name: 'var2' },
90+
'3': { ...getNewVariable(), id: '3', name: 'var3' },
91+
},
92+
orderVariables: ['3', '2', '1', '0'],
93+
version: '4',
94+
};
95+
96+
describe('migrate3to4', () => {
97+
it('v3 input', () => {
98+
const paramsRes = produce(paramsV3, migrate3to4());
99+
expect(paramsRes).toEqual(paramsV4);
100+
});
101+
});
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
// Copyright 2025 V Kontakte LLC
2+
//
3+
// This Source Code Form is subject to the terms of the Mozilla Public
4+
// License, v. 2.0. If a copy of the MPL was not distributed with this
5+
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
6+
7+
import { getNewMetricLayout, GroupKey, PlotKey, PlotParams, QueryParams } from '@/url2';
8+
import { ProduceUpdate } from '@/store2/helpers';
9+
import { toNumber } from '@/common/helpers';
10+
11+
export function migrate3to4(): ProduceUpdate<QueryParams> {
12+
return (params) => {
13+
if (params.version !== '4') {
14+
if (params.orderPlot?.length) {
15+
getMapGroupV3(params).forEach(({ groupKey, plotKeys }) => {
16+
setGroupAutoPosition(plotKeys, params.groups[groupKey]?.size ?? '2')(params);
17+
plotKeys.forEach((plotKey) => {
18+
if (params.plots[plotKey]) {
19+
params.plots[plotKey].group ??= groupKey;
20+
}
21+
});
22+
});
23+
params.orderPlot = [];
24+
}
25+
params.version = '4';
26+
}
27+
};
28+
}
29+
30+
export function getMapGroupV3(params: Pick<QueryParams, 'orderPlot' | 'groups' | 'orderGroup'>) {
31+
const orderPlots = [...(params.orderPlot ?? [])];
32+
return params.orderGroup.map((groupKey) => ({
33+
groupKey,
34+
plotKeys: orderPlots.splice(0, params.groups[groupKey]?.count ?? 0),
35+
}));
36+
}
37+
38+
export function setLayoutAutoPosition(
39+
groupKeys: GroupKey[],
40+
sizes: string[]
41+
): ProduceUpdate<Pick<QueryParams, 'plots' | 'orderGroup' | 'groups'>> {
42+
return (params) => {
43+
const groupPlots = sortGroupPlots(groupKeys, params.plots);
44+
groupPlots.forEach(({ plotKeys }, groupIndex) => {
45+
setGroupAutoPosition(plotKeys, sizes[groupIndex])(params);
46+
});
47+
};
48+
}
49+
export function setGroupAutoPosition(
50+
plotKeys: PlotKey[],
51+
size: string
52+
): ProduceUpdate<Pick<QueryParams, 'plots' | 'orderGroup'>> {
53+
return (params) => {
54+
let row = 0;
55+
let col = 0;
56+
plotKeys.forEach((plotKey) => {
57+
if (params.plots[plotKey]) {
58+
params.plots[plotKey].layout = getNewMetricLayout(params.plots[plotKey].type, size, { x: col, y: row });
59+
60+
col = params.plots[plotKey].layout.x + params.plots[plotKey].layout.w;
61+
row = params.plots[plotKey].layout.y;
62+
}
63+
});
64+
};
65+
}
66+
67+
export function getMapGroupPlotKeys(plots: Partial<Record<PlotKey, PlotParams>>) {
68+
return Object.values(plots)
69+
.sort((a, b) => {
70+
if (a && b) {
71+
return toLayoutSort(a) - toLayoutSort(b);
72+
}
73+
return 0;
74+
})
75+
.reduce(
76+
(res, plot) => {
77+
if (plot && plot.group != null) {
78+
res[plot.group] ??= {
79+
groupKey: plot.group,
80+
plotKeys: [],
81+
};
82+
res[plot.group]?.plotKeys.push(plot.id);
83+
}
84+
return res;
85+
},
86+
{} as Partial<Record<GroupKey, { groupKey: GroupKey; plotKeys: PlotKey[] }>>
87+
);
88+
}
89+
90+
export function sortGroupPlots(orderGroup: GroupKey[], plots: Partial<Record<PlotKey, PlotParams>>) {
91+
const mapGroups = getMapGroupPlotKeys(plots);
92+
return orderGroup.map(
93+
(groupKey) =>
94+
mapGroups[groupKey] ?? {
95+
groupKey,
96+
plotKeys: [],
97+
}
98+
);
99+
}
100+
101+
export function toLayoutSort({ group, layout }: Pick<PlotParams, 'group' | 'layout'>): number {
102+
if (group && layout) {
103+
return (toNumber(group, 0) << 24) + (layout.y << 8) + layout.x;
104+
}
105+
return 0;
106+
}
107+
export function addLeadSymbols(str: string = '', numberSymbol: number = 0, symbol: string = '0'): string {
108+
return (symbol.repeat(numberSymbol) + str).substring(-numberSymbol);
109+
}
110+
111+
let fullSave = false;
112+
export function setFullDashSave(toggle?: boolean) {
113+
fullSave = toggle ?? !fullSave;
114+
}
115+
export function getFullDashSave() {
116+
return fullSave;
117+
}

statshouse-ui/src/common/prepareItemsGroup.tsx

Lines changed: 0 additions & 22 deletions
This file was deleted.

statshouse-ui/src/components/UI/Dropdown.tsx

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,23 +12,33 @@ import { useStateBoolean } from '@/hooks';
1212

1313
import { DropdownContextProvider } from '@/contexts/DropdownContextProvider';
1414

15-
export type DropdownProps = { className?: string; caption?: React.ReactNode; children?: React.ReactNode };
15+
export type DropdownProps = {
16+
className?: string;
17+
caption?: React.ReactNode;
18+
children?: React.ReactNode;
19+
autoClose?: boolean;
20+
};
1621

17-
export const Dropdown = memo(function Dropdown({ className, children, caption }: DropdownProps) {
22+
export const Dropdown = memo(function Dropdown({ className, children, caption, autoClose = true }: DropdownProps) {
1823
const [dropdown, setDropdown] = useStateBoolean(false);
1924

2025
return (
2126
<Tooltip
2227
as="button"
2328
type="button"
2429
className={cn(className, 'overflow-auto')}
25-
title={<DropdownContextProvider value={setDropdown}>{children}</DropdownContextProvider>}
30+
title={
31+
<DropdownContextProvider value={setDropdown}>
32+
<div onClick={autoClose ? setDropdown.off : undefined}>{children}</div>
33+
</DropdownContextProvider>
34+
}
2635
open={dropdown}
2736
vertical={POPPER_VERTICAL.outBottom}
2837
horizontal={POPPER_HORIZONTAL.right}
2938
onClick={setDropdown.toggle}
3039
onClickOuter={setDropdown.off}
3140
titleClassName={'p-0 m-0'}
41+
hover
3242
noStyle
3343
>
3444
{caption}

statshouse-ui/src/components/UI/Tooltip.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
1+
// Copyright 2025 V Kontakte LLC
2+
//
3+
// This Source Code Form is subject to the terms of the Mozilla Public
4+
// License, v. 2.0. If a copy of the MPL was not distributed with this
5+
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
6+
17
import React, { useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react';
28
import { Popper, POPPER_HORIZONTAL, POPPER_VERTICAL, PopperHorizontal, PopperVertical } from './Popper';
39
import type { JSX } from 'react/jsx-runtime';

0 commit comments

Comments
 (0)