mirror of
https://github.com/umami-software/umami.git
synced 2026-02-16 02:25:35 +01:00
Prevent unnecessary chart and board component re-renders
This commit is contained in:
parent
1f0de47c01
commit
400a35d7af
4 changed files with 104 additions and 22 deletions
17
src/app/(main)/boards/[boardId]/BoardColumn.module.css
Normal file
17
src/app/(main)/boards/[boardId]/BoardColumn.module.css
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
.column {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.columnAction {
|
||||||
|
opacity: 0;
|
||||||
|
visibility: hidden;
|
||||||
|
pointer-events: none;
|
||||||
|
transition: opacity 120ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.column:hover .columnAction,
|
||||||
|
.column:focus-within .columnAction {
|
||||||
|
opacity: 1;
|
||||||
|
visibility: visible;
|
||||||
|
pointer-events: auto;
|
||||||
|
}
|
||||||
|
|
@ -8,10 +8,11 @@ import {
|
||||||
Tooltip,
|
Tooltip,
|
||||||
TooltipTrigger,
|
TooltipTrigger,
|
||||||
} from '@umami/react-zen';
|
} from '@umami/react-zen';
|
||||||
import { useState } from 'react';
|
import { useMemo, useState } from 'react';
|
||||||
import { useBoard, useMessages } from '@/components/hooks';
|
import { useBoard, useMessages } from '@/components/hooks';
|
||||||
import { Pencil, Plus, X } from '@/components/icons';
|
import { Pencil, Plus, X } from '@/components/icons';
|
||||||
import type { BoardComponentConfig } from '@/lib/types';
|
import type { BoardComponentConfig } from '@/lib/types';
|
||||||
|
import styles from './BoardColumn.module.css';
|
||||||
import { BoardComponentRenderer } from './BoardComponentRenderer';
|
import { BoardComponentRenderer } from './BoardComponentRenderer';
|
||||||
import { BoardComponentSelect } from './BoardComponentSelect';
|
import { BoardComponentSelect } from './BoardComponentSelect';
|
||||||
|
|
||||||
|
|
@ -34,6 +35,13 @@ export function BoardColumn({
|
||||||
const { board } = useBoard();
|
const { board } = useBoard();
|
||||||
const { t, labels } = useMessages();
|
const { t, labels } = useMessages();
|
||||||
const websiteId = board?.parameters?.websiteId;
|
const websiteId = board?.parameters?.websiteId;
|
||||||
|
const renderedComponent = useMemo(() => {
|
||||||
|
if (!component || !websiteId) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <BoardComponentRenderer config={component} websiteId={websiteId} />;
|
||||||
|
}, [component, websiteId]);
|
||||||
|
|
||||||
const handleSelect = (config: BoardComponentConfig) => {
|
const handleSelect = (config: BoardComponentConfig) => {
|
||||||
onSetComponent?.(id, config);
|
onSetComponent?.(id, config);
|
||||||
|
|
@ -50,11 +58,18 @@ export function BoardColumn({
|
||||||
justifyContent="center"
|
justifyContent="center"
|
||||||
backgroundColor="surface-sunken"
|
backgroundColor="surface-sunken"
|
||||||
position="relative"
|
position="relative"
|
||||||
|
className={styles.column}
|
||||||
>
|
>
|
||||||
{editing && canRemove && (
|
{editing && canRemove && (
|
||||||
<Box position="absolute" top="10px" right="20px" zIndex={100}>
|
<Box
|
||||||
|
className={styles.columnAction}
|
||||||
|
position="absolute"
|
||||||
|
top="10px"
|
||||||
|
right="20px"
|
||||||
|
zIndex={100}
|
||||||
|
>
|
||||||
<TooltipTrigger delay={0}>
|
<TooltipTrigger delay={0}>
|
||||||
<Button variant="quiet" onPress={() => onRemove?.(id)}>
|
<Button variant="outline" onPress={() => onRemove?.(id)}>
|
||||||
<Icon size="sm">
|
<Icon size="sm">
|
||||||
<X />
|
<X />
|
||||||
</Icon>
|
</Icon>
|
||||||
|
|
@ -63,15 +78,21 @@ export function BoardColumn({
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
{component && websiteId ? (
|
{renderedComponent ? (
|
||||||
<>
|
<>
|
||||||
<Box width="100%" height="100%" overflow="auto">
|
<Box width="100%" height="100%" overflow="auto">
|
||||||
<BoardComponentRenderer config={component} websiteId={websiteId} />
|
{renderedComponent}
|
||||||
</Box>
|
</Box>
|
||||||
{editing && (
|
{editing && (
|
||||||
<Box position="absolute" bottom="10px" right="20px" zIndex={100}>
|
<Box
|
||||||
|
className={styles.columnAction}
|
||||||
|
position="absolute"
|
||||||
|
bottom="10px"
|
||||||
|
right="20px"
|
||||||
|
zIndex={100}
|
||||||
|
>
|
||||||
<TooltipTrigger delay={0}>
|
<TooltipTrigger delay={0}>
|
||||||
<Button variant="quiet" onPress={() => setShowSelect(true)}>
|
<Button variant="outline" onPress={() => setShowSelect(true)}>
|
||||||
<Icon size="sm">
|
<Icon size="sm">
|
||||||
<Pencil />
|
<Pencil />
|
||||||
</Icon>
|
</Icon>
|
||||||
|
|
@ -94,7 +115,7 @@ export function BoardColumn({
|
||||||
<Dialog
|
<Dialog
|
||||||
title={t(labels.selectComponent)}
|
title={t(labels.selectComponent)}
|
||||||
style={{
|
style={{
|
||||||
width: '750px',
|
width: '1200px',
|
||||||
maxWidth: 'calc(100vw - 40px)',
|
maxWidth: 'calc(100vw - 40px)',
|
||||||
maxHeight: 'calc(100dvh - 40px)',
|
maxHeight: 'calc(100dvh - 40px)',
|
||||||
padding: '32px',
|
padding: '32px',
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,9 @@
|
||||||
import { Column, Text } from '@umami/react-zen';
|
import { Column, Text } from '@umami/react-zen';
|
||||||
|
import { memo } from 'react';
|
||||||
import type { BoardComponentConfig } from '@/lib/types';
|
import type { BoardComponentConfig } from '@/lib/types';
|
||||||
import { getComponentDefinition } from '../boardComponentRegistry';
|
import { getComponentDefinition } from '../boardComponentRegistry';
|
||||||
|
|
||||||
export function BoardComponentRenderer({
|
function BoardComponentRendererComponent({
|
||||||
config,
|
config,
|
||||||
websiteId,
|
websiteId,
|
||||||
}: {
|
}: {
|
||||||
|
|
@ -23,3 +24,11 @@ export function BoardComponentRenderer({
|
||||||
|
|
||||||
return <Component websiteId={websiteId} {...config.props} />;
|
return <Component websiteId={websiteId} {...config.props} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const BoardComponentRenderer = memo(
|
||||||
|
BoardComponentRendererComponent,
|
||||||
|
(prevProps, nextProps) =>
|
||||||
|
prevProps.websiteId === nextProps.websiteId && prevProps.config === nextProps.config,
|
||||||
|
);
|
||||||
|
|
||||||
|
BoardComponentRenderer.displayName = 'BoardComponentRenderer';
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { useTheme } from '@umami/react-zen';
|
import { useTheme } from '@umami/react-zen';
|
||||||
import { useMemo, useState } from 'react';
|
import { memo, useCallback, useMemo, useState } from 'react';
|
||||||
import { Chart, type ChartProps } from '@/components/charts/Chart';
|
import { Chart, type ChartProps } from '@/components/charts/Chart';
|
||||||
import { ChartTooltip } from '@/components/charts/ChartTooltip';
|
import { ChartTooltip } from '@/components/charts/ChartTooltip';
|
||||||
import { useLocale } from '@/components/hooks';
|
import { useLocale } from '@/components/hooks';
|
||||||
|
|
@ -8,6 +8,8 @@ import { getThemeColors } from '@/lib/colors';
|
||||||
import { DATE_FORMATS, formatDate } from '@/lib/date';
|
import { DATE_FORMATS, formatDate } from '@/lib/date';
|
||||||
import { formatLongCurrency, formatLongNumber } from '@/lib/format';
|
import { formatLongCurrency, formatLongNumber } from '@/lib/format';
|
||||||
|
|
||||||
|
const MemoChart = memo(Chart);
|
||||||
|
|
||||||
const dateFormats = {
|
const dateFormats = {
|
||||||
millisecond: 'T',
|
millisecond: 'T',
|
||||||
second: 'pp',
|
second: 'pp',
|
||||||
|
|
@ -32,7 +34,13 @@ export interface BarChartProps extends ChartProps {
|
||||||
maxDate?: Date;
|
maxDate?: Date;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function BarChart({
|
interface TooltipState {
|
||||||
|
title: string;
|
||||||
|
color?: string;
|
||||||
|
value: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function BarChartComponent({
|
||||||
chartData,
|
chartData,
|
||||||
renderXLabel,
|
renderXLabel,
|
||||||
renderYLabel,
|
renderYLabel,
|
||||||
|
|
@ -45,7 +53,7 @@ export function BarChart({
|
||||||
currency,
|
currency,
|
||||||
...props
|
...props
|
||||||
}: BarChartProps) {
|
}: BarChartProps) {
|
||||||
const [tooltip, setTooltip] = useState(null);
|
const [tooltip, setTooltip] = useState<TooltipState | null>(null);
|
||||||
const { theme } = useTheme();
|
const { theme } = useTheme();
|
||||||
const { locale } = useLocale();
|
const { locale } = useLocale();
|
||||||
const { colors } = useMemo(() => getThemeColors(theme), [theme]);
|
const { colors } = useMemo(() => getThemeColors(theme), [theme]);
|
||||||
|
|
@ -94,13 +102,23 @@ export function BarChart({
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}, [chartData, colors, unit, stacked, renderXLabel, renderYLabel]);
|
}, [
|
||||||
|
colors,
|
||||||
|
unit,
|
||||||
|
stacked,
|
||||||
|
renderXLabel,
|
||||||
|
renderYLabel,
|
||||||
|
minDate,
|
||||||
|
maxDate,
|
||||||
|
locale,
|
||||||
|
XAxisType,
|
||||||
|
YAxisType,
|
||||||
|
]);
|
||||||
|
|
||||||
const handleTooltip = ({ tooltip }: { tooltip: any }) => {
|
const handleTooltip = useCallback(
|
||||||
|
({ tooltip }: { tooltip: any }) => {
|
||||||
const { opacity, labelColors, dataPoints } = tooltip;
|
const { opacity, labelColors, dataPoints } = tooltip;
|
||||||
|
const nextTooltip = opacity
|
||||||
setTooltip(
|
|
||||||
opacity
|
|
||||||
? {
|
? {
|
||||||
title: formatDate(
|
title: formatDate(
|
||||||
new Date(dataPoints[0].raw?.d || dataPoints[0].raw?.x || dataPoints[0].raw),
|
new Date(dataPoints[0].raw?.d || dataPoints[0].raw?.x || dataPoints[0].raw),
|
||||||
|
|
@ -112,13 +130,26 @@ export function BarChart({
|
||||||
? formatLongCurrency(dataPoints[0].raw.y, currency)
|
? formatLongCurrency(dataPoints[0].raw.y, currency)
|
||||||
: `${formatLongNumber(dataPoints[0].raw.y)} ${dataPoints[0].dataset.label}`,
|
: `${formatLongNumber(dataPoints[0].raw.y)} ${dataPoints[0].dataset.label}`,
|
||||||
}
|
}
|
||||||
: null,
|
: null;
|
||||||
|
|
||||||
|
setTooltip(prev => {
|
||||||
|
if (
|
||||||
|
prev?.title === nextTooltip?.title &&
|
||||||
|
prev?.color === nextTooltip?.color &&
|
||||||
|
prev?.value === nextTooltip?.value
|
||||||
|
) {
|
||||||
|
return prev;
|
||||||
|
}
|
||||||
|
|
||||||
|
return nextTooltip;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[currency, locale, unit],
|
||||||
);
|
);
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Chart
|
<MemoChart
|
||||||
{...props}
|
{...props}
|
||||||
type="bar"
|
type="bar"
|
||||||
chartData={chartData}
|
chartData={chartData}
|
||||||
|
|
@ -129,3 +160,7 @@ export function BarChart({
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const BarChart = memo(BarChartComponent);
|
||||||
|
|
||||||
|
BarChart.displayName = 'BarChart';
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue