1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-09-01 13:47:27 +02:00

Improve impact metrics layout (#10326)

- narrow screen no longer breaks
- fixed size of series indicators in tooltips
- simplified grid layout props
- updated X axis ticks
This commit is contained in:
Tymoteusz Czech 2025-07-08 12:28:04 +02:00 committed by GitHub
parent 2d83f297a1
commit 1eefede62e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 98 additions and 77 deletions

View File

@ -44,7 +44,6 @@ const StyledWidget = styled(Paper)(({ theme }) => ({
display: 'flex',
flexDirection: 'column',
height: '100%',
overflow: 'hidden',
}));
const StyledChartContent = styled(Box)({

View File

@ -1,19 +1,19 @@
import type { FC, ReactNode } from 'react';
import { useMemo, useCallback } from 'react';
import { Responsive, WidthProvider } from 'react-grid-layout';
import { styled } from '@mui/material';
import GridLayout, { WidthProvider } from 'react-grid-layout';
import { styled, useTheme, useMediaQuery } from '@mui/material';
import 'react-grid-layout/css/styles.css';
import 'react-resizable/css/styles.css';
const ResponsiveGridLayout = WidthProvider(Responsive);
const ResponsiveGridLayout = WidthProvider(GridLayout);
const StyledGridContainer = styled('div')(({ theme }) => ({
'& .react-grid-item': {
borderRadius: `${theme.shape.borderRadiusMedium}px`,
},
'& .react-resizable-handle': {
'&::after': {
opacity: 0.5,
'& .grid-item-drag-handle': {
[theme.breakpoints.down('md')]: {
display: 'none',
},
},
}));
@ -32,65 +32,74 @@ export type GridItem = {
static?: boolean;
};
type LayoutItem = {
i: string;
x: number;
y: number;
w: number;
h: number;
minW?: number;
minH?: number;
maxW?: number;
maxH?: number;
static?: boolean;
};
type GridLayoutWrapperProps = {
items: GridItem[];
onLayoutChange?: (layout: unknown[]) => void;
onLayoutChange?: (layout: LayoutItem[]) => void;
cols?: { lg: number; md: number; sm: number; xs: number; xxs: number };
rowHeight?: number;
margin?: [number, number];
isDraggable?: boolean;
isResizable?: boolean;
compactType?: 'vertical' | 'horizontal' | null;
};
export const GridLayoutWrapper: FC<GridLayoutWrapperProps> = ({
items,
onLayoutChange,
cols = { lg: 12, md: 10, sm: 6, xs: 4, xxs: 2 },
cols = { lg: 12, md: 12, sm: 6, xs: 4, xxs: 2 },
rowHeight = 180,
margin = [16, 16],
isDraggable = true,
isResizable = true,
compactType = 'vertical',
}) => {
const layouts = useMemo(() => {
const baseLayout = items.map((item, index) => ({
const theme = useTheme();
const isMobileBreakpoint = useMediaQuery(theme.breakpoints.down('md'));
const layout = useMemo(() => {
if (isMobileBreakpoint) {
let currentY = 0;
return items.map((item) => {
const layoutItem = {
i: item.id,
x: 0,
y: currentY,
w: cols.xs,
h: item.h ?? 4,
minW: cols.xs,
minH: item.minH ?? 3,
maxW: cols.xs,
maxH: item.maxH ?? 8,
static: false,
};
currentY += layoutItem.h;
return layoutItem;
});
}
return items.map((item, index) => ({
i: item.id,
x: item.x ?? (index % cols.lg) * (item.w ?? 6),
y: item.y ?? Math.floor(index / cols.lg) * (item.h ?? 4),
w: item.w ?? 6,
x:
item.x ??
(index % Math.floor(cols.lg / (item.w ?? 4))) * (item.w ?? 4),
y:
item.y ??
Math.floor(index / Math.floor(cols.lg / (item.w ?? 4))) *
(item.h ?? 4),
w: item.w ?? 4,
h: item.h ?? 4,
minW: item.minW ?? 3,
minW: item.minW ?? 4,
minH: item.minH ?? 3,
maxW: item.maxW ?? 12,
maxH: item.maxH ?? 8,
static: item.static ?? false,
}));
return {
lg: baseLayout,
md: baseLayout.map((item) => ({
...item,
w: Math.min(item.w, cols.md),
x: Math.min(item.x, cols.md - item.w),
})),
sm: baseLayout.map((item) => ({
...item,
w: Math.min(item.w, cols.sm),
x: Math.min(item.x, cols.sm - item.w),
})),
xs: baseLayout.map((item) => ({
...item,
w: Math.min(item.w, cols.xs),
x: Math.min(item.x, cols.xs - item.w),
})),
xxs: baseLayout.map((item) => ({
...item,
w: Math.min(item.w, cols.xxs),
x: Math.min(item.x, cols.xxs - item.w),
})),
};
}, [items, cols]);
}, [items, cols, isMobileBreakpoint]);
const children = useMemo(
() => items.map((item) => <div key={item.id}>{item.component}</div>),
@ -98,31 +107,36 @@ export const GridLayoutWrapper: FC<GridLayoutWrapperProps> = ({
);
const handleLayoutChange = useCallback(
(layout: unknown[], layouts: unknown) => {
onLayoutChange?.(layout);
(layout: LayoutItem[]) => {
if (!isMobileBreakpoint) {
onLayoutChange?.(layout);
}
},
[onLayoutChange],
[onLayoutChange, isMobileBreakpoint],
);
return (
<StyledGridContainer>
<ResponsiveGridLayout
className='impact-metrics-grid'
layouts={layouts}
breakpoints={{ lg: 1200, md: 996, sm: 768, xs: 480, xxs: 0 }}
cols={cols}
layout={layout}
cols={isMobileBreakpoint ? cols.xs : cols.lg}
rowHeight={rowHeight}
margin={margin}
margin={[
Number.parseInt(theme.spacing(2)),
Number.parseInt(theme.spacing(2)),
]}
containerPadding={[0, 0]}
isDraggable={isDraggable}
isResizable={isResizable}
isDraggable={!isMobileBreakpoint}
isResizable={!isMobileBreakpoint}
onLayoutChange={handleLayoutChange}
resizeHandles={['se']}
draggableHandle='.grid-item-drag-handle'
compactType={compactType}
compactType={isMobileBreakpoint ? null : 'vertical'}
preventCollision={false}
useCSSTransforms={true}
autoSize={true}
allowOverlap={false}
>
{children}
</ResponsiveGridLayout>

View File

@ -14,8 +14,8 @@ const StyledEmptyState = styled(Paper)(({ theme }) => ({
textAlign: 'center',
padding: theme.spacing(8),
backgroundColor: theme.palette.background.default,
borderRadius: theme.shape.borderRadius * 2,
border: `2px dashed ${theme.palette.divider}`,
borderRadius: `${theme.shape.borderRadiusMedium}px`,
boxShadow: 'none',
}));
export const ImpactMetrics: FC = () => {
@ -144,9 +144,6 @@ export const ImpactMetrics: FC = () => {
<GridLayoutWrapper
items={gridItems}
onLayoutChange={handleLayoutChange}
rowHeight={180}
margin={[16, 16]}
cols={{ lg: 12, md: 10, sm: 6, xs: 4, xxs: 2 }}
/>
) : null}

View File

@ -104,6 +104,11 @@ export const ImpactMetricsChart: FC<ImpactMetricsChartProps> = ({
},
tooltipFormat: 'PPpp',
},
ticks: {
maxRotation: 45,
minRotation: 45,
maxTicksLimit: 8,
},
},
y: {
beginAtZero,

View File

@ -43,15 +43,25 @@ export const useChartData = (
],
};
} else {
// Create a comprehensive timestamp range for consistent X-axis
const allTimestamps = new Set<number>();
timeSeriesData.forEach((series) => {
series.data.forEach(([timestamp]) => {
allTimestamps.add(timestamp);
});
});
if (allTimestamps.size === 0) {
return {
labels: [],
datasets: [],
};
}
const sortedTimestamps = Array.from(allTimestamps).sort(
(a, b) => a - b,
);
const labels = sortedTimestamps.map(
(timestamp) => new Date(timestamp * 1000),
);
@ -72,7 +82,6 @@ export const useChartData = (
borderColor: color,
backgroundColor: color,
fill: false,
spanGaps: false,
};
});

View File

@ -7,22 +7,24 @@ export const getTimeUnit = (selectedRange: string) => {
case 'week':
return 'day';
case 'month':
return 'day';
return 'week';
default:
return 'hour';
return 'day';
}
};
export const getDisplayFormat = (selectedRange: string) => {
switch (selectedRange) {
case 'hour':
return 'HH:mm';
case 'day':
return 'HH:mm';
case 'week':
return 'MMM dd';
case 'month':
return 'MMM dd';
default:
return 'MMM dd HH:mm';
return 'MMM dd';
}
};

View File

@ -1,5 +1,6 @@
import { Box, Paper, styled, Typography } from '@mui/material';
import type { TooltipItem } from 'chart.js';
import { Truncator } from 'component/common/Truncator/Truncator';
import type React from 'react';
import type { FC, VFC } from 'react';
import { objectId } from 'utils/objectId';
@ -32,12 +33,13 @@ const StyledItem = styled('li')(({ theme }) => ({
marginBottom: theme.spacing(0.5),
display: 'flex',
alignItems: 'center',
fontSize: theme.typography.body2.fontSize,
}));
const StyledLabelIcon = styled('span')(({ theme }) => ({
display: 'inline-block',
width: 8,
height: 8,
minWidth: 8,
minHeight: 8,
borderRadius: '50%',
marginRight: theme.spacing(1),
}));
@ -119,14 +121,7 @@ export const ChartTooltip: VFC<IChartTooltipProps> = ({ tooltip }) => (
>
{' '}
</StyledLabelIcon>
<Typography
variant='body2'
sx={{
display: 'inline-block',
}}
>
{item.title}
</Typography>
<Truncator lines={2}>{item.title}</Truncator>
</StyledItem>
))}
</StyledList>