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', display: 'flex',
flexDirection: 'column', flexDirection: 'column',
height: '100%', height: '100%',
overflow: 'hidden',
})); }));
const StyledChartContent = styled(Box)({ const StyledChartContent = styled(Box)({

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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