diff --git a/statshouse-ui/package-lock.json b/statshouse-ui/package-lock.json index 983055ba2..364171dd5 100644 --- a/statshouse-ui/package-lock.json +++ b/statshouse-ui/package-lock.json @@ -49,6 +49,7 @@ "react": "^19.0.0", "react-data-grid": "^7.0.0-beta.47", "react-dom": "^19.0.0", + "react-grid-layout": "^1.5.0", "react-markdown": "^9.0.3", "react-router-dom": "^7.1.1", "react-window": "^1.8.11", @@ -69,6 +70,7 @@ "@types/node": "^20.17.12", "@types/react": "^19.0.4", "@types/react-dom": "^19.0.2", + "@types/react-grid-layout": "^1.3.5", "@typescript-eslint/eslint-plugin": "^8.19.1", "@typescript-eslint/parser": "^8.19.1", "prettier": "^3.4.2" @@ -4889,6 +4891,16 @@ "@types/react": "^19.0.0" } }, + "node_modules/@types/react-grid-layout": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/react-grid-layout/-/react-grid-layout-1.3.5.tgz", + "integrity": "sha512-WH/po1gcEcoR6y857yAnPGug+ZhkF4PaTUxgAbwfeSH/QOgVSakKHBXoPGad/sEznmkiaK3pqHk+etdWisoeBQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/react": "*" + } + }, "node_modules/@types/react-window": { "version": "1.8.8", "resolved": "https://registry.npmjs.org/@types/react-window/-/react-window-1.8.8.tgz", @@ -7053,6 +7065,12 @@ "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.2.0.tgz", "integrity": "sha512-xJuoT5+L99XlZ8twedaRf6Ax2TgQVxvgZOYoPKqZufmJib0tL2tegPBOZb1pVNgIhlqDlA0eO0c3wBvQcmzx4w==" }, + "node_modules/fast-equals": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-4.0.3.tgz", + "integrity": "sha512-G3BSX9cfKttjr+2o1O22tYMLq0DPluZnYtq1rXumE1SpL/F/SLIfHx08WYQoWSIpeMYf8sRbJ8++71+v6Pnxfg==", + "license": "MIT" + }, "node_modules/fast-glob": { "version": "3.3.3", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", @@ -12331,6 +12349,47 @@ "react": "^19.0.0" } }, + "node_modules/react-draggable": { + "version": "4.4.6", + "resolved": "https://registry.npmjs.org/react-draggable/-/react-draggable-4.4.6.tgz", + "integrity": "sha512-LtY5Xw1zTPqHkVmtM3X8MUOxNDOUhv/khTgBgrUvwaS064bwVvxT+q5El0uUFNx5IEPKXuRejr7UqLwBIg5pdw==", + "license": "MIT", + "dependencies": { + "clsx": "^1.1.1", + "prop-types": "^15.8.1" + }, + "peerDependencies": { + "react": ">= 16.3.0", + "react-dom": ">= 16.3.0" + } + }, + "node_modules/react-draggable/node_modules/clsx": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz", + "integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/react-grid-layout": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/react-grid-layout/-/react-grid-layout-1.5.0.tgz", + "integrity": "sha512-WBKX7w/LsTfI99WskSu6nX2nbJAUD7GD6nIXcwYLyPpnslojtmql2oD3I2g5C3AK8hrxIarYT8awhuDIp7iQ5w==", + "license": "MIT", + "dependencies": { + "clsx": "^2.0.0", + "fast-equals": "^4.0.3", + "prop-types": "^15.8.1", + "react-draggable": "^4.4.5", + "react-resizable": "^3.0.5", + "resize-observer-polyfill": "^1.5.1" + }, + "peerDependencies": { + "react": ">= 16.3.0", + "react-dom": ">= 16.3.0" + } + }, "node_modules/react-is": { "version": "17.0.2", "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", @@ -12362,6 +12421,19 @@ "react": ">=18" } }, + "node_modules/react-resizable": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/react-resizable/-/react-resizable-3.0.5.tgz", + "integrity": "sha512-vKpeHhI5OZvYn82kXOs1bC8aOXktGU5AmKAgaZS4F5JPburCtbmDPqE7Pzp+1kN4+Wb81LlF33VpGwWwtXem+w==", + "license": "MIT", + "dependencies": { + "prop-types": "15.x", + "react-draggable": "^4.0.3" + }, + "peerDependencies": { + "react": ">= 16.3" + } + }, "node_modules/react-router": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.1.1.tgz", @@ -12625,6 +12697,12 @@ "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==" }, + "node_modules/resize-observer-polyfill": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz", + "integrity": "sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==", + "license": "MIT" + }, "node_modules/resolve": { "version": "1.22.0", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.0.tgz", @@ -17486,6 +17564,15 @@ "devOptional": true, "requires": {} }, + "@types/react-grid-layout": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/react-grid-layout/-/react-grid-layout-1.3.5.tgz", + "integrity": "sha512-WH/po1gcEcoR6y857yAnPGug+ZhkF4PaTUxgAbwfeSH/QOgVSakKHBXoPGad/sEznmkiaK3pqHk+etdWisoeBQ==", + "dev": true, + "requires": { + "@types/react": "*" + } + }, "@types/react-window": { "version": "1.8.8", "resolved": "https://registry.npmjs.org/@types/react-window/-/react-window-1.8.8.tgz", @@ -18966,6 +19053,11 @@ "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.2.0.tgz", "integrity": "sha512-xJuoT5+L99XlZ8twedaRf6Ax2TgQVxvgZOYoPKqZufmJib0tL2tegPBOZb1pVNgIhlqDlA0eO0c3wBvQcmzx4w==" }, + "fast-equals": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-4.0.3.tgz", + "integrity": "sha512-G3BSX9cfKttjr+2o1O22tYMLq0DPluZnYtq1rXumE1SpL/F/SLIfHx08WYQoWSIpeMYf8sRbJ8++71+v6Pnxfg==" + }, "fast-glob": { "version": "3.3.3", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", @@ -22704,6 +22796,35 @@ "scheduler": "^0.25.0" } }, + "react-draggable": { + "version": "4.4.6", + "resolved": "https://registry.npmjs.org/react-draggable/-/react-draggable-4.4.6.tgz", + "integrity": "sha512-LtY5Xw1zTPqHkVmtM3X8MUOxNDOUhv/khTgBgrUvwaS064bwVvxT+q5El0uUFNx5IEPKXuRejr7UqLwBIg5pdw==", + "requires": { + "clsx": "^1.1.1", + "prop-types": "^15.8.1" + }, + "dependencies": { + "clsx": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz", + "integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==" + } + } + }, + "react-grid-layout": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/react-grid-layout/-/react-grid-layout-1.5.0.tgz", + "integrity": "sha512-WBKX7w/LsTfI99WskSu6nX2nbJAUD7GD6nIXcwYLyPpnslojtmql2oD3I2g5C3AK8hrxIarYT8awhuDIp7iQ5w==", + "requires": { + "clsx": "^2.0.0", + "fast-equals": "^4.0.3", + "prop-types": "^15.8.1", + "react-draggable": "^4.4.5", + "react-resizable": "^3.0.5", + "resize-observer-polyfill": "^1.5.1" + } + }, "react-is": { "version": "17.0.2", "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", @@ -22727,6 +22848,15 @@ "vfile": "^6.0.0" } }, + "react-resizable": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/react-resizable/-/react-resizable-3.0.5.tgz", + "integrity": "sha512-vKpeHhI5OZvYn82kXOs1bC8aOXktGU5AmKAgaZS4F5JPburCtbmDPqE7Pzp+1kN4+Wb81LlF33VpGwWwtXem+w==", + "requires": { + "prop-types": "15.x", + "react-draggable": "^4.0.3" + } + }, "react-router": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.1.1.tgz", @@ -22910,6 +23040,11 @@ "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==" }, + "resize-observer-polyfill": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz", + "integrity": "sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==" + }, "resolve": { "version": "1.22.0", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.0.tgz", diff --git a/statshouse-ui/package.json b/statshouse-ui/package.json index da678c9d1..37c8ea701 100644 --- a/statshouse-ui/package.json +++ b/statshouse-ui/package.json @@ -45,6 +45,7 @@ "react": "^19.0.0", "react-data-grid": "^7.0.0-beta.47", "react-dom": "^19.0.0", + "react-grid-layout": "^1.5.0", "react-markdown": "^9.0.3", "react-router-dom": "^7.1.1", "react-window": "^1.8.11", @@ -65,6 +66,7 @@ "@types/node": "^20.17.12", "@types/react": "^19.0.4", "@types/react-dom": "^19.0.2", + "@types/react-grid-layout": "^1.3.5", "@typescript-eslint/eslint-plugin": "^8.19.1", "@typescript-eslint/parser": "^8.19.1", "prettier": "^3.4.2" diff --git a/statshouse-ui/src/api/enum.ts b/statshouse-ui/src/api/enum.ts index 0b0c3d59f..90d04f44a 100644 --- a/statshouse-ui/src/api/enum.ts +++ b/statshouse-ui/src/api/enum.ts @@ -66,6 +66,7 @@ export const GET_PARAMS = { dashboardGroupInfoCount: 'n', dashboardGroupInfoSize: 's', dashboardGroupInfoDescription: 'd', + dashboardGroupInfoLayouts: 'ly', metricLive: 'live', theme: 'theme', avoidCache: 'ac', diff --git a/statshouse-ui/src/common/helpers.ts b/statshouse-ui/src/common/helpers.ts index e9341bd8d..e1ae7f784 100644 --- a/statshouse-ui/src/common/helpers.ts +++ b/statshouse-ui/src/common/helpers.ts @@ -7,6 +7,10 @@ import { produce } from 'immer'; import { mapKeyboardEnToRu, mapKeyboardRuToEn, toggleKeyboard } from './toggleKeyboard'; import type uPlot from 'uplot'; +import { BREAKPOINT_WIDTH } from '@/components2/Dashboard/constants'; +import { GroupInfo } from '@/url2'; +import { BreakpointKey, LayoutScheme } from '@/components2/Dashboard/types'; +import { BREAKPOINTS_SIZES } from '@/components2/Dashboard/constants'; export function isArray(item: unknown): item is unknown[] { return Array.isArray(item); @@ -427,3 +431,74 @@ export const bwd = (v: number) => { } return Math.pow(2, v) - 1; }; + +const getBreakpointKey = (width: number): BreakpointKey => { + if (width >= BREAKPOINT_WIDTH.xxxl) return BREAKPOINTS_SIZES.xxxl; + if (width >= BREAKPOINT_WIDTH.xxl) return BREAKPOINTS_SIZES.xxl; + if (width >= BREAKPOINT_WIDTH.xl) return BREAKPOINTS_SIZES.xl; + if (width >= BREAKPOINT_WIDTH.lg) return BREAKPOINTS_SIZES.lg; + if (width >= BREAKPOINT_WIDTH.md) return BREAKPOINTS_SIZES.md; + if (width >= BREAKPOINT_WIDTH.sm) return BREAKPOINTS_SIZES.sm; + if (width >= BREAKPOINT_WIDTH.xs) return BREAKPOINTS_SIZES.xs; + return BREAKPOINTS_SIZES.xxs; +}; + +export const getBreakpointConfig = () => { + const width = window.innerWidth; + return { breakpointKey: getBreakpointKey(width) }; +}; + +export const calculateMaxRows = (plots: string[], cols: number, layout?: { y: number; h: number }[]) => { + if (!layout?.length) { + return Math.ceil(plots.length / cols) + 1; + } + + const maxOccupiedRow = layout.reduce((max, item) => { + const itemLastRow = item.y + item.h; + return Math.max(max, itemLastRow); + }, 0); + + return maxOccupiedRow + 1; +}; + +export const calculateDynamicRowHeight = (width: number, baseWidth: number = 2700, baseHeight: number = 480) => { + if (width <= baseWidth) { + return baseHeight; + } + const extraWidth = width - baseWidth; + const extraBlocks = Math.floor(extraWidth / 300); + + const finalHeight = baseHeight + extraBlocks * 30; + + return finalHeight; +}; + +export const updateGroupWithLayout = (groupInfo: GroupInfo, groupKey: string, layouts?: LayoutScheme) => { + const layoutScheme = layouts?.groupKey === groupKey; + + if (layoutScheme) { + groupInfo.layouts = layouts.layout; + } + return groupInfo; +}; + +// convert size to number of columns +export function getSizeColumns(size?: string): number { + if (size === undefined) return 2; + + const numericSize = Number(size); + if (!isNaN(numericSize)) { + return numericSize; + } + + switch (size) { + case 'l': + return 2; + case 'm': + return 3; + case 's': + return 4; + default: + return 2; + } +} diff --git a/statshouse-ui/src/common/prepareItemsGroup.tsx b/statshouse-ui/src/common/prepareItemsGroup.tsx index 01348ef57..b742f4bff 100644 --- a/statshouse-ui/src/common/prepareItemsGroup.tsx +++ b/statshouse-ui/src/common/prepareItemsGroup.tsx @@ -4,7 +4,9 @@ // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. -import type { QueryParams } from '@/url2'; +import { DEFAULT_LAYOUT_COORDS } from '@/components2/Dashboard/constants'; +import type { GroupKey, PlotKey, QueryParams } from '@/url2'; +import type { Layout } from 'react-grid-layout'; export function prepareItemsGroup({ orderGroup, @@ -20,3 +22,38 @@ export function prepareItemsGroup({ }; }); } + +type PrepareItemsGroupWithLayoutProps = { + groups: QueryParams['groups']; + orderGroup: QueryParams['orderGroup']; + orderPlot: PlotKey[]; +}; + +type PrepareItemsGroupWithLayoutResult = { + itemsGroup: { groupKey: GroupKey; plots: PlotKey[] }[]; + layoutsCoords: { groupKey: GroupKey; layout: Layout[] }[]; +}; + +export function prepareItemsGroupWithLayout({ + groups, + orderGroup, + orderPlot, +}: PrepareItemsGroupWithLayoutProps): PrepareItemsGroupWithLayoutResult { + const orderP = [...orderPlot]; + const itemsGroup = orderGroup.map((groupKey) => { + const count = groups[groupKey]?.count ?? 0; + const groupPlots = orderP.splice(0, count); + + return { + groupKey, + plots: groupPlots, + }; + }); + + const layoutsCoords = orderGroup.map((groupKey) => ({ + groupKey, + layout: groups[groupKey]?.layouts ?? [DEFAULT_LAYOUT_COORDS], + })); + + return { itemsGroup, layoutsCoords }; +} diff --git a/statshouse-ui/src/components2/Dashboard/Dashboard.tsx b/statshouse-ui/src/components2/Dashboard/Dashboard.tsx index c33b6974f..29062d833 100644 --- a/statshouse-ui/src/components2/Dashboard/Dashboard.tsx +++ b/statshouse-ui/src/components2/Dashboard/Dashboard.tsx @@ -23,6 +23,7 @@ import { produce } from '~immer/dist/immer'; import { HistoryList } from '../HistoryList'; const PATH_VERSION_PARAM = '&dv'; +import { DashboardLayoutNew } from './DashboardLayoutNew'; export type DashboardProps = { className?: string; @@ -148,7 +149,7 @@ export const Dashboard = memo(function Dashboard({ className }: DashboardProps) {variablesLength > 0 && tabNum === '-1' && !tvModeEnable && ( )} - + {tabNum === '-2' && } {tabNum === '-3' && dashboardId && ( ) => { - const groupKey = e.currentTarget.getAttribute('data-group') ?? '0'; - const size = e.currentTarget.value ?? '2'; - setDashboardGroup(groupKey, (g) => { - g.size = size; - }); - }, - [setDashboardGroup] - ); - const onAddGroup = useCallback( (e: React.MouseEvent) => { const groupKey = e.currentTarget.getAttribute('data-index-group') ?? '0'; @@ -139,7 +129,7 @@ export const DashboardGroup = memo(function DashboardGroup({ children, groupKey, >