mirror of
https://github.com/Unleash/unleash.git
synced 2025-09-01 13:47:27 +02:00
refactor: apply previous pr comments
This commit is contained in:
parent
75d3a5d8d6
commit
41e0c3a653
@ -1,5 +1,5 @@
|
|||||||
import type { FC } from 'react';
|
import type { FC } from 'react';
|
||||||
import { useState, useEffect, useMemo } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
@ -8,21 +8,11 @@ import {
|
|||||||
Button,
|
Button,
|
||||||
TextField,
|
TextField,
|
||||||
Box,
|
Box,
|
||||||
Typography,
|
|
||||||
Alert,
|
|
||||||
styled,
|
styled,
|
||||||
} from '@mui/material';
|
} from '@mui/material';
|
||||||
import { ImpactMetricsControls } from './ImpactMetricsControls/ImpactMetricsControls.tsx';
|
import { ImpactMetricsControls } from './ImpactMetricsControls/ImpactMetricsControls.tsx';
|
||||||
import {
|
import { ImpactMetricsChartPreview } from './ImpactMetricsChartPreview.tsx';
|
||||||
LineChart,
|
|
||||||
NotEnoughData,
|
|
||||||
} from '../insights/components/LineChart/LineChart.tsx';
|
|
||||||
import { StyledChartContainer } from 'component/insights/InsightsCharts.styles';
|
|
||||||
import { useImpactMetricsData } from 'hooks/api/getters/useImpactMetricsData/useImpactMetricsData';
|
import { useImpactMetricsData } from 'hooks/api/getters/useImpactMetricsData/useImpactMetricsData';
|
||||||
import { usePlaceholderData } from '../insights/hooks/usePlaceholderData.js';
|
|
||||||
import { getDisplayFormat, getTimeUnit, formatLargeNumbers } from './utils.ts';
|
|
||||||
import { fromUnixTime } from 'date-fns';
|
|
||||||
import { useChartData } from './hooks/useChartData.ts';
|
|
||||||
import type { ChartConfig } from './types.ts';
|
import type { ChartConfig } from './types.ts';
|
||||||
import type { ImpactMetricsSeries } from 'hooks/api/getters/useImpactMetricsMetadata/useImpactMetricsMetadata';
|
import type { ImpactMetricsSeries } from 'hooks/api/getters/useImpactMetricsMetadata/useImpactMetricsMetadata';
|
||||||
|
|
||||||
@ -82,25 +72,6 @@ export const ChartConfigModal: FC<ChartConfigModalProps> = ({
|
|||||||
Record<string, string[]>
|
Record<string, string[]>
|
||||||
>(initialConfig?.selectedLabels || {});
|
>(initialConfig?.selectedLabels || {});
|
||||||
|
|
||||||
// Data for preview
|
|
||||||
const {
|
|
||||||
data: { start, end, series: timeSeriesData },
|
|
||||||
loading: dataLoading,
|
|
||||||
error: dataError,
|
|
||||||
} = useImpactMetricsData(
|
|
||||||
selectedSeries
|
|
||||||
? {
|
|
||||||
series: selectedSeries,
|
|
||||||
range: selectedRange,
|
|
||||||
labels:
|
|
||||||
Object.keys(selectedLabels).length > 0
|
|
||||||
? selectedLabels
|
|
||||||
: undefined,
|
|
||||||
}
|
|
||||||
: undefined,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Fetch available labels for the currently selected series
|
|
||||||
const {
|
const {
|
||||||
data: { labels: currentAvailableLabels },
|
data: { labels: currentAvailableLabels },
|
||||||
} = useImpactMetricsData(
|
} = useImpactMetricsData(
|
||||||
@ -112,40 +83,6 @@ export const ChartConfigModal: FC<ChartConfigModalProps> = ({
|
|||||||
: undefined,
|
: undefined,
|
||||||
);
|
);
|
||||||
|
|
||||||
const placeholderData = usePlaceholderData({
|
|
||||||
fill: true,
|
|
||||||
type: 'constant',
|
|
||||||
});
|
|
||||||
|
|
||||||
const data = useChartData(timeSeriesData);
|
|
||||||
|
|
||||||
const hasError = !!dataError;
|
|
||||||
const isLoading = dataLoading;
|
|
||||||
const shouldShowPlaceholder = !selectedSeries || isLoading || hasError;
|
|
||||||
const notEnoughData = useMemo(
|
|
||||||
() =>
|
|
||||||
!isLoading &&
|
|
||||||
(!timeSeriesData ||
|
|
||||||
timeSeriesData.length === 0 ||
|
|
||||||
!data.datasets.some((d) => d.data.length > 1)),
|
|
||||||
[data, isLoading, timeSeriesData],
|
|
||||||
);
|
|
||||||
|
|
||||||
const minTime = start
|
|
||||||
? fromUnixTime(Number.parseInt(start, 10))
|
|
||||||
: undefined;
|
|
||||||
const maxTime = end ? fromUnixTime(Number.parseInt(end, 10)) : undefined;
|
|
||||||
|
|
||||||
const placeholder = selectedSeries ? (
|
|
||||||
<NotEnoughData description='Send impact metrics using Unleash SDK and select data series to view the chart.' />
|
|
||||||
) : (
|
|
||||||
<NotEnoughData
|
|
||||||
title='Select a metric series to view the chart.'
|
|
||||||
description=''
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
const cover = notEnoughData ? placeholder : isLoading;
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (open && initialConfig) {
|
if (open && initialConfig) {
|
||||||
setTitle(initialConfig.title || '');
|
setTitle(initialConfig.title || '');
|
||||||
@ -232,100 +169,13 @@ export const ChartConfigModal: FC<ChartConfigModalProps> = ({
|
|||||||
availableLabels={currentAvailableLabels}
|
availableLabels={currentAvailableLabels}
|
||||||
/>
|
/>
|
||||||
</StyledConfigPanel>
|
</StyledConfigPanel>
|
||||||
|
|
||||||
{/* Preview Panel */}
|
|
||||||
<StyledPreviewPanel>
|
<StyledPreviewPanel>
|
||||||
<Typography variant='h6' color='text.secondary'>
|
<ImpactMetricsChartPreview
|
||||||
Preview
|
selectedSeries={selectedSeries}
|
||||||
</Typography>
|
selectedRange={selectedRange}
|
||||||
|
selectedLabels={selectedLabels}
|
||||||
{!selectedSeries && !isLoading ? (
|
beginAtZero={beginAtZero}
|
||||||
<Typography variant='body2' color='text.secondary'>
|
/>
|
||||||
Select a metric series to view the preview
|
|
||||||
</Typography>
|
|
||||||
) : null}
|
|
||||||
|
|
||||||
<StyledChartContainer>
|
|
||||||
{hasError ? (
|
|
||||||
<Alert severity='error'>
|
|
||||||
Failed to load impact metrics. Please check
|
|
||||||
if Prometheus is configured and the feature
|
|
||||||
flag is enabled.
|
|
||||||
</Alert>
|
|
||||||
) : null}
|
|
||||||
<LineChart
|
|
||||||
data={
|
|
||||||
notEnoughData || isLoading
|
|
||||||
? placeholderData
|
|
||||||
: data
|
|
||||||
}
|
|
||||||
overrideOptions={
|
|
||||||
shouldShowPlaceholder
|
|
||||||
? {}
|
|
||||||
: {
|
|
||||||
scales: {
|
|
||||||
x: {
|
|
||||||
type: 'time',
|
|
||||||
min: minTime?.getTime(),
|
|
||||||
max: maxTime?.getTime(),
|
|
||||||
time: {
|
|
||||||
unit: getTimeUnit(
|
|
||||||
selectedRange,
|
|
||||||
),
|
|
||||||
displayFormats: {
|
|
||||||
[getTimeUnit(
|
|
||||||
selectedRange,
|
|
||||||
)]:
|
|
||||||
getDisplayFormat(
|
|
||||||
selectedRange,
|
|
||||||
),
|
|
||||||
},
|
|
||||||
tooltipFormat: 'PPpp',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
y: {
|
|
||||||
beginAtZero,
|
|
||||||
title: {
|
|
||||||
display: false,
|
|
||||||
},
|
|
||||||
ticks: {
|
|
||||||
precision: 0,
|
|
||||||
callback: (
|
|
||||||
value: unknown,
|
|
||||||
): string | number =>
|
|
||||||
typeof value ===
|
|
||||||
'number'
|
|
||||||
? formatLargeNumbers(
|
|
||||||
value,
|
|
||||||
)
|
|
||||||
: (value as number),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
plugins: {
|
|
||||||
legend: {
|
|
||||||
display:
|
|
||||||
timeSeriesData &&
|
|
||||||
timeSeriesData.length >
|
|
||||||
1,
|
|
||||||
position:
|
|
||||||
'bottom' as const,
|
|
||||||
labels: {
|
|
||||||
usePointStyle: true,
|
|
||||||
boxWidth: 8,
|
|
||||||
padding: 12,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
animations: {
|
|
||||||
x: { duration: 0 },
|
|
||||||
y: { duration: 0 },
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
cover={cover}
|
|
||||||
/>
|
|
||||||
</StyledChartContainer>
|
|
||||||
</StyledPreviewPanel>
|
</StyledPreviewPanel>
|
||||||
</Box>
|
</Box>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
|
@ -1,25 +1,9 @@
|
|||||||
import type { FC } from 'react';
|
import type { FC } from 'react';
|
||||||
import { useMemo } from 'react';
|
import { Box, Typography, IconButton, styled, Paper } from '@mui/material';
|
||||||
import {
|
|
||||||
Box,
|
|
||||||
Typography,
|
|
||||||
IconButton,
|
|
||||||
Alert,
|
|
||||||
styled,
|
|
||||||
Paper,
|
|
||||||
} from '@mui/material';
|
|
||||||
import Edit from '@mui/icons-material/Edit';
|
import Edit from '@mui/icons-material/Edit';
|
||||||
import Delete from '@mui/icons-material/Delete';
|
import Delete from '@mui/icons-material/Delete';
|
||||||
import DragHandle from '@mui/icons-material/DragHandle';
|
import DragHandle from '@mui/icons-material/DragHandle';
|
||||||
import {
|
import { ImpactMetricsChart } from './ImpactMetricsChart.tsx';
|
||||||
LineChart,
|
|
||||||
NotEnoughData,
|
|
||||||
} from '../insights/components/LineChart/LineChart.tsx';
|
|
||||||
import { useImpactMetricsData } from 'hooks/api/getters/useImpactMetricsData/useImpactMetricsData';
|
|
||||||
import { usePlaceholderData } from '../insights/hooks/usePlaceholderData.js';
|
|
||||||
import { getDisplayFormat, getTimeUnit, formatLargeNumbers } from './utils.ts';
|
|
||||||
import { fromUnixTime } from 'date-fns';
|
|
||||||
import { useChartData } from './hooks/useChartData.ts';
|
|
||||||
import type { ChartConfig } from './types.ts';
|
import type { ChartConfig } from './types.ts';
|
||||||
|
|
||||||
export interface ChartItemProps {
|
export interface ChartItemProps {
|
||||||
@ -32,46 +16,30 @@ const getConfigDescription = (config: ChartConfig): string => {
|
|||||||
const parts: string[] = [];
|
const parts: string[] = [];
|
||||||
|
|
||||||
if (config.selectedSeries) {
|
if (config.selectedSeries) {
|
||||||
parts.push(`Series: ${config.selectedSeries}`);
|
parts.push(`${config.selectedSeries}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
parts.push(`Time range: last ${config.selectedRange}`);
|
parts.push(`last ${config.selectedRange}`);
|
||||||
|
|
||||||
if (config.beginAtZero) {
|
|
||||||
parts.push('Begin at zero');
|
|
||||||
}
|
|
||||||
|
|
||||||
const labelCount = Object.keys(config.selectedLabels).length;
|
const labelCount = Object.keys(config.selectedLabels).length;
|
||||||
if (labelCount > 0) {
|
if (labelCount > 0) {
|
||||||
parts.push(`${labelCount} label filter${labelCount > 1 ? 's' : ''}`);
|
parts.push(`${labelCount} filter${labelCount > 1 ? 's' : ''}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
return parts.join(' • ');
|
return parts.join(' • ');
|
||||||
};
|
};
|
||||||
|
|
||||||
const StyledHeader = styled(Box)(({ theme }) => ({
|
const StyledChartWrapper = styled(Box)({
|
||||||
display: 'flex',
|
height: '100%',
|
||||||
justifyContent: 'space-between',
|
width: '100%',
|
||||||
alignItems: 'flex-start',
|
'& > div': {
|
||||||
padding: theme.spacing(2, 3),
|
height: '100% !important',
|
||||||
borderBottom: `1px solid ${theme.palette.divider}`,
|
width: '100% !important',
|
||||||
}));
|
|
||||||
|
|
||||||
const StyledDragHandle = styled(Box)(({ theme }) => ({
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
cursor: 'move',
|
|
||||||
padding: theme.spacing(0.5),
|
|
||||||
borderRadius: theme.shape.borderRadius,
|
|
||||||
color: theme.palette.text.secondary,
|
|
||||||
'&:hover': {
|
|
||||||
backgroundColor: theme.palette.action.hover,
|
|
||||||
color: theme.palette.text.primary,
|
|
||||||
},
|
},
|
||||||
}));
|
});
|
||||||
|
|
||||||
const StyledWidget = styled(Paper)(({ theme }) => ({
|
const StyledWidget = styled(Paper)(({ theme }) => ({
|
||||||
borderRadius: `${theme.shape.borderRadiusLarge}px`,
|
borderRadius: `${theme.shape.borderRadiusMedium}px`,
|
||||||
boxShadow: 'none',
|
boxShadow: 'none',
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
@ -97,182 +65,81 @@ const StyledImpactChartContainer = styled(Box)(({ theme }) => ({
|
|||||||
padding: theme.spacing(3),
|
padding: theme.spacing(3),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const StyledChartWrapper = styled(Box)({
|
const StyledHeader = styled(Box)(({ theme }) => ({
|
||||||
height: '100%',
|
display: 'flex',
|
||||||
width: '100%',
|
gap: theme.spacing(2),
|
||||||
'& > div': {
|
alignItems: 'center',
|
||||||
height: '100% !important',
|
padding: theme.spacing(1.5, 2),
|
||||||
width: '100% !important',
|
borderBottom: `1px solid ${theme.palette.divider}`,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const StyledDragHandle = styled(Box)(({ theme }) => ({
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
cursor: 'move',
|
||||||
|
padding: theme.spacing(0.5),
|
||||||
|
borderRadius: theme.shape.borderRadius,
|
||||||
|
color: theme.palette.text.secondary,
|
||||||
|
'&:hover': {
|
||||||
|
backgroundColor: theme.palette.action.hover,
|
||||||
|
color: theme.palette.text.primary,
|
||||||
},
|
},
|
||||||
});
|
}));
|
||||||
|
|
||||||
export const ChartItem: FC<ChartItemProps> = ({ config, onEdit, onDelete }) => {
|
const StyledChartTitle = styled(Box)(({ theme }) => ({
|
||||||
const {
|
display: 'flex',
|
||||||
data: { start, end, series: timeSeriesData },
|
flexDirection: 'column',
|
||||||
loading: dataLoading,
|
justifyContent: 'flex-end',
|
||||||
error: dataError,
|
flexGrow: 1,
|
||||||
} = useImpactMetricsData({
|
overflow: 'hidden',
|
||||||
series: config.selectedSeries,
|
textOverflow: 'ellipsis',
|
||||||
range: config.selectedRange,
|
}));
|
||||||
labels:
|
|
||||||
Object.keys(config.selectedLabels).length > 0
|
|
||||||
? config.selectedLabels
|
|
||||||
: undefined,
|
|
||||||
});
|
|
||||||
|
|
||||||
const placeholderData = usePlaceholderData({
|
const StyledChartActions = styled(Box)(({ theme }) => ({
|
||||||
fill: true,
|
marginLeft: 'auto',
|
||||||
type: 'constant',
|
display: 'flex',
|
||||||
});
|
alignItems: 'center',
|
||||||
|
gap: theme.spacing(0.5),
|
||||||
|
}));
|
||||||
|
|
||||||
const data = useChartData(timeSeriesData);
|
export const ChartItem: FC<ChartItemProps> = ({ config, onEdit, onDelete }) => (
|
||||||
|
<StyledWidget>
|
||||||
|
<StyledHeader>
|
||||||
|
<StyledDragHandle className='grid-item-drag-handle'>
|
||||||
|
<DragHandle fontSize='small' />
|
||||||
|
</StyledDragHandle>
|
||||||
|
<StyledChartTitle>
|
||||||
|
{config.title && (
|
||||||
|
<Typography variant='h6'>{config.title}</Typography>
|
||||||
|
)}
|
||||||
|
<Typography variant='body2' color='text.secondary'>
|
||||||
|
{getConfigDescription(config)}
|
||||||
|
</Typography>
|
||||||
|
</StyledChartTitle>
|
||||||
|
<StyledChartActions>
|
||||||
|
<IconButton onClick={() => onEdit(config)}>
|
||||||
|
<Edit />
|
||||||
|
</IconButton>
|
||||||
|
<IconButton onClick={() => onDelete(config.id)}>
|
||||||
|
<Delete />
|
||||||
|
</IconButton>
|
||||||
|
</StyledChartActions>
|
||||||
|
</StyledHeader>
|
||||||
|
|
||||||
const hasError = !!dataError;
|
<StyledChartContent>
|
||||||
const isLoading = dataLoading;
|
<StyledImpactChartContainer>
|
||||||
const shouldShowPlaceholder = isLoading || hasError;
|
<StyledChartWrapper>
|
||||||
const notEnoughData = useMemo(
|
<ImpactMetricsChart
|
||||||
() =>
|
selectedSeries={config.selectedSeries}
|
||||||
!isLoading &&
|
selectedRange={config.selectedRange}
|
||||||
(!timeSeriesData ||
|
selectedLabels={config.selectedLabels}
|
||||||
timeSeriesData.length === 0 ||
|
beginAtZero={config.beginAtZero}
|
||||||
!data.datasets.some((d) => d.data.length > 1)),
|
aspectRatio={1.5}
|
||||||
[data, isLoading, timeSeriesData],
|
overrideOptions={{ maintainAspectRatio: false }}
|
||||||
);
|
emptyDataDescription='Send impact metrics using Unleash SDK for this series to view the chart.'
|
||||||
|
/>
|
||||||
const minTime = start
|
</StyledChartWrapper>
|
||||||
? fromUnixTime(Number.parseInt(start, 10))
|
</StyledImpactChartContainer>
|
||||||
: undefined;
|
</StyledChartContent>
|
||||||
const maxTime = end ? fromUnixTime(Number.parseInt(end, 10)) : undefined;
|
</StyledWidget>
|
||||||
|
);
|
||||||
const placeholder = (
|
|
||||||
<NotEnoughData description='Send impact metrics using Unleash SDK for this series to view the chart.' />
|
|
||||||
);
|
|
||||||
const cover = notEnoughData ? placeholder : isLoading;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<StyledWidget>
|
|
||||||
<StyledHeader>
|
|
||||||
<Box sx={{ display: 'flex', alignItems: 'flex-start', gap: 1 }}>
|
|
||||||
<StyledDragHandle className='grid-item-drag-handle'>
|
|
||||||
<DragHandle fontSize='small' />
|
|
||||||
</StyledDragHandle>
|
|
||||||
<Box>
|
|
||||||
{config.title && (
|
|
||||||
<Typography variant='h6' gutterBottom>
|
|
||||||
{config.title}
|
|
||||||
</Typography>
|
|
||||||
)}
|
|
||||||
<Typography
|
|
||||||
variant='body2'
|
|
||||||
color='text.secondary'
|
|
||||||
sx={{ mb: 1 }}
|
|
||||||
>
|
|
||||||
{getConfigDescription(config)}
|
|
||||||
</Typography>
|
|
||||||
</Box>
|
|
||||||
</Box>
|
|
||||||
<Box>
|
|
||||||
<IconButton
|
|
||||||
onClick={() => onEdit(config)}
|
|
||||||
size='small'
|
|
||||||
sx={{ mr: 1 }}
|
|
||||||
>
|
|
||||||
<Edit fontSize='small' />
|
|
||||||
</IconButton>
|
|
||||||
<IconButton
|
|
||||||
onClick={() => onDelete(config.id)}
|
|
||||||
size='small'
|
|
||||||
>
|
|
||||||
<Delete fontSize='small' />
|
|
||||||
</IconButton>
|
|
||||||
</Box>
|
|
||||||
</StyledHeader>
|
|
||||||
|
|
||||||
<StyledChartContent>
|
|
||||||
<StyledImpactChartContainer>
|
|
||||||
{hasError ? (
|
|
||||||
<Alert severity='error'>
|
|
||||||
Failed to load impact metrics. Please check if
|
|
||||||
Prometheus is configured and the feature flag is
|
|
||||||
enabled.
|
|
||||||
</Alert>
|
|
||||||
) : null}
|
|
||||||
<StyledChartWrapper>
|
|
||||||
<LineChart
|
|
||||||
data={
|
|
||||||
notEnoughData || isLoading
|
|
||||||
? placeholderData
|
|
||||||
: data
|
|
||||||
}
|
|
||||||
aspectRatio={1.5}
|
|
||||||
overrideOptions={
|
|
||||||
shouldShowPlaceholder
|
|
||||||
? { maintainAspectRatio: false }
|
|
||||||
: {
|
|
||||||
maintainAspectRatio: false,
|
|
||||||
scales: {
|
|
||||||
x: {
|
|
||||||
type: 'time',
|
|
||||||
min: minTime?.getTime(),
|
|
||||||
max: maxTime?.getTime(),
|
|
||||||
time: {
|
|
||||||
unit: getTimeUnit(
|
|
||||||
config.selectedRange,
|
|
||||||
),
|
|
||||||
displayFormats: {
|
|
||||||
[getTimeUnit(
|
|
||||||
config.selectedRange,
|
|
||||||
)]: getDisplayFormat(
|
|
||||||
config.selectedRange,
|
|
||||||
),
|
|
||||||
},
|
|
||||||
tooltipFormat: 'PPpp',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
y: {
|
|
||||||
beginAtZero:
|
|
||||||
config.beginAtZero,
|
|
||||||
title: {
|
|
||||||
display: false,
|
|
||||||
},
|
|
||||||
ticks: {
|
|
||||||
precision: 0,
|
|
||||||
callback: (
|
|
||||||
value: unknown,
|
|
||||||
): string | number =>
|
|
||||||
typeof value ===
|
|
||||||
'number'
|
|
||||||
? formatLargeNumbers(
|
|
||||||
value,
|
|
||||||
)
|
|
||||||
: (value as number),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
plugins: {
|
|
||||||
legend: {
|
|
||||||
display:
|
|
||||||
timeSeriesData &&
|
|
||||||
timeSeriesData.length > 1,
|
|
||||||
position: 'bottom' as const,
|
|
||||||
labels: {
|
|
||||||
usePointStyle: true,
|
|
||||||
boxWidth: 8,
|
|
||||||
padding: 12,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
animations: {
|
|
||||||
x: { duration: 0 },
|
|
||||||
y: { duration: 0 },
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
cover={cover}
|
|
||||||
/>
|
|
||||||
</StyledChartWrapper>
|
|
||||||
</StyledImpactChartContainer>
|
|
||||||
</StyledChartContent>
|
|
||||||
</StyledWidget>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
@ -15,7 +15,7 @@ const StyledGridContainer = styled('div')(({ theme }) => ({
|
|||||||
'& .react-grid-item': {
|
'& .react-grid-item': {
|
||||||
transition: 'all 200ms ease',
|
transition: 'all 200ms ease',
|
||||||
border: `1px solid ${theme.palette.divider}`,
|
border: `1px solid ${theme.palette.divider}`,
|
||||||
borderRadius: theme.shape.borderRadius,
|
borderRadius: `${theme.shape.borderRadiusMedium}px`,
|
||||||
backgroundColor: theme.palette.background.paper,
|
backgroundColor: theme.palette.background.paper,
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
'&.react-grid-item--placeholder': {
|
'&.react-grid-item--placeholder': {
|
||||||
@ -44,16 +44,15 @@ const StyledGridContainer = styled('div')(({ theme }) => ({
|
|||||||
},
|
},
|
||||||
'& .react-resizable-handle': {
|
'& .react-resizable-handle': {
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
width: '20px',
|
width: theme.spacing(3),
|
||||||
height: '20px',
|
height: theme.spacing(3),
|
||||||
bottom: '0px',
|
bottom: '0px',
|
||||||
right: '0px',
|
right: '0px',
|
||||||
cursor: 'se-resize',
|
cursor: 'se-resize',
|
||||||
backgroundImage: `url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjAiIGhlaWdodD0iMjAiIHZpZXdCb3g9IjAgMCAyMCAyMCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTMgMTdMMTcgM00zIDEzTDEzIDNNNyAxN0wxNyA3IiBzdHJva2U9IiM5OTkiIHN0cm9rZS13aWR0aD0iMiIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIi8+Cjwvc3ZnPgo=')`,
|
|
||||||
backgroundRepeat: 'no-repeat',
|
backgroundRepeat: 'no-repeat',
|
||||||
backgroundPosition: 'center',
|
backgroundPosition: 'center',
|
||||||
'&:hover': {
|
'&::after': {
|
||||||
backgroundImage: `url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjAiIGhlaWdodD0iMjAiIHZpZXdCb3g9IjAgMCAyMCAyMCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTMgMTdMMTcgM00zIDEzTDEzIDNNNyAxN0wxNyA3IiBzdHJva2U9IiMzMzMiIHN0cm9rZS13aWR0aD0iMiIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIi8+Cjwvc3ZnPgo=')`,
|
display: 'none',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
@ -93,7 +92,6 @@ export const GridLayoutWrapper: FC<GridLayoutWrapperProps> = ({
|
|||||||
isResizable = true,
|
isResizable = true,
|
||||||
compactType = 'vertical',
|
compactType = 'vertical',
|
||||||
}) => {
|
}) => {
|
||||||
// Memoize layouts to prevent unnecessary re-renders
|
|
||||||
const layouts = useMemo(() => {
|
const layouts = useMemo(() => {
|
||||||
const baseLayout = items.map((item, index) => ({
|
const baseLayout = items.map((item, index) => ({
|
||||||
i: item.id,
|
i: item.id,
|
||||||
@ -133,7 +131,6 @@ export const GridLayoutWrapper: FC<GridLayoutWrapperProps> = ({
|
|||||||
};
|
};
|
||||||
}, [items, cols]);
|
}, [items, cols]);
|
||||||
|
|
||||||
// Memoize children to improve performance
|
|
||||||
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>),
|
||||||
[items],
|
[items],
|
||||||
|
@ -7,7 +7,7 @@ import { useImpactMetricsMetadata } from 'hooks/api/getters/useImpactMetricsMeta
|
|||||||
import { ChartConfigModal } from './ChartConfigModal.tsx';
|
import { ChartConfigModal } from './ChartConfigModal.tsx';
|
||||||
import { ChartItem } from './ChartItem.tsx';
|
import { ChartItem } from './ChartItem.tsx';
|
||||||
import { GridLayoutWrapper, type GridItem } from './GridLayoutWrapper.tsx';
|
import { GridLayoutWrapper, type GridItem } from './GridLayoutWrapper.tsx';
|
||||||
import { useUrlState } from './hooks/useUrlState.ts';
|
import { useImpactMetricsState } from './hooks/useImpactMetricsState.ts';
|
||||||
import type { ChartConfig, LayoutItem } from './types.ts';
|
import type { ChartConfig, LayoutItem } from './types.ts';
|
||||||
|
|
||||||
const StyledEmptyState = styled(Paper)(({ theme }) => ({
|
const StyledEmptyState = styled(Paper)(({ theme }) => ({
|
||||||
@ -23,7 +23,7 @@ export const ImpactMetrics: FC = () => {
|
|||||||
const [editingChart, setEditingChart] = useState<ChartConfig | undefined>();
|
const [editingChart, setEditingChart] = useState<ChartConfig | undefined>();
|
||||||
|
|
||||||
const { charts, layout, addChart, updateChart, deleteChart, updateLayout } =
|
const { charts, layout, addChart, updateChart, deleteChart, updateLayout } =
|
||||||
useUrlState();
|
useImpactMetricsState();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
metadata,
|
metadata,
|
||||||
|
150
frontend/src/component/impact-metrics/ImpactMetricsChart.tsx
Normal file
150
frontend/src/component/impact-metrics/ImpactMetricsChart.tsx
Normal file
@ -0,0 +1,150 @@
|
|||||||
|
import type { FC, ReactNode } from 'react';
|
||||||
|
import { useMemo } from 'react';
|
||||||
|
import { Alert } from '@mui/material';
|
||||||
|
import {
|
||||||
|
LineChart,
|
||||||
|
NotEnoughData,
|
||||||
|
} from '../insights/components/LineChart/LineChart.tsx';
|
||||||
|
import { useImpactMetricsData } from 'hooks/api/getters/useImpactMetricsData/useImpactMetricsData';
|
||||||
|
import { usePlaceholderData } from '../insights/hooks/usePlaceholderData.js';
|
||||||
|
import { getDisplayFormat, getTimeUnit, formatLargeNumbers } from './utils.ts';
|
||||||
|
import { fromUnixTime } from 'date-fns';
|
||||||
|
import { useChartData } from './hooks/useChartData.ts';
|
||||||
|
|
||||||
|
type ImpactMetricsChartProps = {
|
||||||
|
selectedSeries: string;
|
||||||
|
selectedRange: 'hour' | 'day' | 'week' | 'month';
|
||||||
|
selectedLabels: Record<string, string[]>;
|
||||||
|
beginAtZero: boolean;
|
||||||
|
aspectRatio?: number;
|
||||||
|
overrideOptions?: Record<string, unknown>;
|
||||||
|
errorTitle?: string;
|
||||||
|
emptyDataDescription?: string;
|
||||||
|
noSeriesPlaceholder?: ReactNode;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ImpactMetricsChart: FC<ImpactMetricsChartProps> = ({
|
||||||
|
selectedSeries,
|
||||||
|
selectedRange,
|
||||||
|
selectedLabels,
|
||||||
|
beginAtZero,
|
||||||
|
aspectRatio,
|
||||||
|
overrideOptions = {},
|
||||||
|
errorTitle = 'Failed to load impact metrics. Please check if Prometheus is configured and the feature flag is enabled.',
|
||||||
|
emptyDataDescription = 'Send impact metrics using Unleash SDK and select data series to view the chart.',
|
||||||
|
noSeriesPlaceholder,
|
||||||
|
}) => {
|
||||||
|
const {
|
||||||
|
data: { start, end, series: timeSeriesData },
|
||||||
|
loading: dataLoading,
|
||||||
|
error: dataError,
|
||||||
|
} = useImpactMetricsData(
|
||||||
|
selectedSeries
|
||||||
|
? {
|
||||||
|
series: selectedSeries,
|
||||||
|
range: selectedRange,
|
||||||
|
labels:
|
||||||
|
Object.keys(selectedLabels).length > 0
|
||||||
|
? selectedLabels
|
||||||
|
: undefined,
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
|
);
|
||||||
|
|
||||||
|
const placeholderData = usePlaceholderData({
|
||||||
|
fill: true,
|
||||||
|
type: 'constant',
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = useChartData(timeSeriesData);
|
||||||
|
|
||||||
|
const hasError = !!dataError;
|
||||||
|
const isLoading = dataLoading;
|
||||||
|
const shouldShowPlaceholder = !selectedSeries || isLoading || hasError;
|
||||||
|
const notEnoughData = useMemo(
|
||||||
|
() =>
|
||||||
|
!isLoading &&
|
||||||
|
(!timeSeriesData ||
|
||||||
|
timeSeriesData.length === 0 ||
|
||||||
|
!data.datasets.some((d) => d.data.length > 1)),
|
||||||
|
[data, isLoading, timeSeriesData],
|
||||||
|
);
|
||||||
|
|
||||||
|
const minTime = start
|
||||||
|
? fromUnixTime(Number.parseInt(start, 10))
|
||||||
|
: undefined;
|
||||||
|
const maxTime = end ? fromUnixTime(Number.parseInt(end, 10)) : undefined;
|
||||||
|
|
||||||
|
const placeholder = selectedSeries ? (
|
||||||
|
<NotEnoughData description={emptyDataDescription} />
|
||||||
|
) : noSeriesPlaceholder ? (
|
||||||
|
noSeriesPlaceholder
|
||||||
|
) : (
|
||||||
|
<NotEnoughData
|
||||||
|
title='Select a metric series to view the chart.'
|
||||||
|
description=''
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
const cover = notEnoughData ? placeholder : isLoading;
|
||||||
|
|
||||||
|
const chartOptions = shouldShowPlaceholder
|
||||||
|
? overrideOptions
|
||||||
|
: {
|
||||||
|
...overrideOptions,
|
||||||
|
scales: {
|
||||||
|
x: {
|
||||||
|
type: 'time',
|
||||||
|
min: minTime?.getTime(),
|
||||||
|
max: maxTime?.getTime(),
|
||||||
|
time: {
|
||||||
|
unit: getTimeUnit(selectedRange),
|
||||||
|
displayFormats: {
|
||||||
|
[getTimeUnit(selectedRange)]:
|
||||||
|
getDisplayFormat(selectedRange),
|
||||||
|
},
|
||||||
|
tooltipFormat: 'PPpp',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
y: {
|
||||||
|
beginAtZero,
|
||||||
|
title: {
|
||||||
|
display: false,
|
||||||
|
},
|
||||||
|
ticks: {
|
||||||
|
precision: 0,
|
||||||
|
callback: (value: unknown): string | number =>
|
||||||
|
typeof value === 'number'
|
||||||
|
? formatLargeNumbers(value)
|
||||||
|
: (value as number),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
plugins: {
|
||||||
|
legend: {
|
||||||
|
display: timeSeriesData && timeSeriesData.length > 1,
|
||||||
|
position: 'bottom' as const,
|
||||||
|
labels: {
|
||||||
|
usePointStyle: true,
|
||||||
|
boxWidth: 8,
|
||||||
|
padding: 12,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
animations: {
|
||||||
|
x: { duration: 0 },
|
||||||
|
y: { duration: 0 },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{hasError ? <Alert severity='error'>{errorTitle}</Alert> : null}
|
||||||
|
<LineChart
|
||||||
|
data={notEnoughData || isLoading ? placeholderData : data}
|
||||||
|
aspectRatio={aspectRatio}
|
||||||
|
overrideOptions={chartOptions}
|
||||||
|
cover={cover}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,39 @@
|
|||||||
|
import type { FC } from 'react';
|
||||||
|
import { Typography } from '@mui/material';
|
||||||
|
import { StyledChartContainer } from 'component/insights/InsightsCharts.styles';
|
||||||
|
import { ImpactMetricsChart } from './ImpactMetricsChart.tsx';
|
||||||
|
|
||||||
|
type ImpactMetricsChartPreviewProps = {
|
||||||
|
selectedSeries: string;
|
||||||
|
selectedRange: 'hour' | 'day' | 'week' | 'month';
|
||||||
|
selectedLabels: Record<string, string[]>;
|
||||||
|
beginAtZero: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ImpactMetricsChartPreview: FC<ImpactMetricsChartPreviewProps> = ({
|
||||||
|
selectedSeries,
|
||||||
|
selectedRange,
|
||||||
|
selectedLabels,
|
||||||
|
beginAtZero,
|
||||||
|
}) => (
|
||||||
|
<>
|
||||||
|
<Typography variant='h6' color='text.secondary'>
|
||||||
|
Preview
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
{!selectedSeries ? (
|
||||||
|
<Typography variant='body2' color='text.secondary'>
|
||||||
|
Select a metric series to view the preview
|
||||||
|
</Typography>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
<StyledChartContainer>
|
||||||
|
<ImpactMetricsChart
|
||||||
|
selectedSeries={selectedSeries}
|
||||||
|
selectedRange={selectedRange}
|
||||||
|
selectedLabels={selectedLabels}
|
||||||
|
beginAtZero={beginAtZero}
|
||||||
|
/>
|
||||||
|
</StyledChartContainer>
|
||||||
|
</>
|
||||||
|
);
|
@ -1,203 +0,0 @@
|
|||||||
import type { FC } from 'react';
|
|
||||||
import {
|
|
||||||
FormControl,
|
|
||||||
InputLabel,
|
|
||||||
Select,
|
|
||||||
MenuItem,
|
|
||||||
FormControlLabel,
|
|
||||||
Checkbox,
|
|
||||||
Box,
|
|
||||||
Autocomplete,
|
|
||||||
TextField,
|
|
||||||
Typography,
|
|
||||||
Chip,
|
|
||||||
} from '@mui/material';
|
|
||||||
import type { ImpactMetricsSeries } from 'hooks/api/getters/useImpactMetricsMetadata/useImpactMetricsMetadata';
|
|
||||||
import type { ImpactMetricsLabels } from 'hooks/api/getters/useImpactMetricsData/useImpactMetricsData';
|
|
||||||
import { Highlighter } from 'component/common/Highlighter/Highlighter';
|
|
||||||
|
|
||||||
export interface ImpactMetricsControlsProps {
|
|
||||||
selectedSeries: string;
|
|
||||||
onSeriesChange: (series: string) => void;
|
|
||||||
selectedRange: 'hour' | 'day' | 'week' | 'month';
|
|
||||||
onRangeChange: (range: 'hour' | 'day' | 'week' | 'month') => void;
|
|
||||||
beginAtZero: boolean;
|
|
||||||
onBeginAtZeroChange: (beginAtZero: boolean) => void;
|
|
||||||
metricSeries: (ImpactMetricsSeries & { name: string })[];
|
|
||||||
loading?: boolean;
|
|
||||||
selectedLabels: Record<string, string[]>;
|
|
||||||
onLabelsChange: (labels: Record<string, string[]>) => void;
|
|
||||||
availableLabels?: ImpactMetricsLabels;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const ImpactMetricsControls: FC<ImpactMetricsControlsProps> = ({
|
|
||||||
selectedSeries,
|
|
||||||
onSeriesChange,
|
|
||||||
selectedRange,
|
|
||||||
onRangeChange,
|
|
||||||
beginAtZero,
|
|
||||||
onBeginAtZeroChange,
|
|
||||||
metricSeries,
|
|
||||||
loading = false,
|
|
||||||
selectedLabels,
|
|
||||||
onLabelsChange,
|
|
||||||
availableLabels,
|
|
||||||
}) => {
|
|
||||||
const handleLabelChange = (labelKey: string, values: string[]) => {
|
|
||||||
const newLabels = { ...selectedLabels };
|
|
||||||
if (values.length === 0) {
|
|
||||||
delete newLabels[labelKey];
|
|
||||||
} else {
|
|
||||||
newLabels[labelKey] = values;
|
|
||||||
}
|
|
||||||
onLabelsChange(newLabels);
|
|
||||||
};
|
|
||||||
|
|
||||||
const clearAllLabels = () => {
|
|
||||||
onLabelsChange({});
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Box
|
|
||||||
sx={(theme) => ({
|
|
||||||
display: 'flex',
|
|
||||||
flexDirection: 'column',
|
|
||||||
gap: theme.spacing(3),
|
|
||||||
maxWidth: 400,
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
<Typography variant='body2' color='text.secondary'>
|
|
||||||
Select a custom metric to see its value over time. This can help
|
|
||||||
you understand the impact of your feature rollout on key
|
|
||||||
outcomes, such as system performance, usage patterns or error
|
|
||||||
rates.
|
|
||||||
</Typography>
|
|
||||||
|
|
||||||
<Autocomplete
|
|
||||||
options={metricSeries}
|
|
||||||
getOptionLabel={(option) => option.name}
|
|
||||||
value={
|
|
||||||
metricSeries.find(
|
|
||||||
(option) => option.name === selectedSeries,
|
|
||||||
) || null
|
|
||||||
}
|
|
||||||
onChange={(_, newValue) => onSeriesChange(newValue?.name || '')}
|
|
||||||
disabled={loading}
|
|
||||||
renderOption={(props, option, { inputValue }) => (
|
|
||||||
<Box component='li' {...props}>
|
|
||||||
<Box sx={{ display: 'flex', flexDirection: 'column' }}>
|
|
||||||
<Typography variant='body2'>
|
|
||||||
<Highlighter search={inputValue}>
|
|
||||||
{option.name}
|
|
||||||
</Highlighter>
|
|
||||||
</Typography>
|
|
||||||
<Typography
|
|
||||||
variant='caption'
|
|
||||||
color='text.secondary'
|
|
||||||
>
|
|
||||||
<Highlighter search={inputValue}>
|
|
||||||
{option.help}
|
|
||||||
</Highlighter>
|
|
||||||
</Typography>
|
|
||||||
</Box>
|
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
renderInput={(params) => (
|
|
||||||
<TextField
|
|
||||||
{...params}
|
|
||||||
label='Data series'
|
|
||||||
placeholder='Search for a metric…'
|
|
||||||
variant='outlined'
|
|
||||||
size='small'
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
noOptionsText='No metrics available'
|
|
||||||
sx={{ minWidth: 300 }}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<FormControl variant='outlined' size='small' sx={{ minWidth: 200 }}>
|
|
||||||
<InputLabel id='range-select-label'>Time</InputLabel>
|
|
||||||
<Select
|
|
||||||
labelId='range-select-label'
|
|
||||||
value={selectedRange}
|
|
||||||
onChange={(e) =>
|
|
||||||
onRangeChange(
|
|
||||||
e.target.value as 'hour' | 'day' | 'week' | 'month',
|
|
||||||
)
|
|
||||||
}
|
|
||||||
label='Time Range'
|
|
||||||
>
|
|
||||||
<MenuItem value='hour'>Last hour</MenuItem>
|
|
||||||
<MenuItem value='day'>Last 24 hours</MenuItem>
|
|
||||||
<MenuItem value='week'>Last 7 days</MenuItem>
|
|
||||||
<MenuItem value='month'>Last 30 days</MenuItem>
|
|
||||||
</Select>
|
|
||||||
</FormControl>
|
|
||||||
|
|
||||||
<FormControlLabel
|
|
||||||
control={
|
|
||||||
<Checkbox
|
|
||||||
checked={beginAtZero}
|
|
||||||
onChange={(e) => onBeginAtZeroChange(e.target.checked)}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
label='Begin at zero'
|
|
||||||
/>
|
|
||||||
{availableLabels && Object.keys(availableLabels).length > 0 ? (
|
|
||||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
|
|
||||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
|
||||||
<Typography variant='subtitle2'>
|
|
||||||
Filter by labels
|
|
||||||
</Typography>
|
|
||||||
{Object.keys(selectedLabels).length > 0 && (
|
|
||||||
<Chip
|
|
||||||
label='Clear all'
|
|
||||||
size='small'
|
|
||||||
variant='outlined'
|
|
||||||
onClick={clearAllLabels}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
{Object.entries(availableLabels).map(
|
|
||||||
([labelKey, values]) => (
|
|
||||||
<Autocomplete
|
|
||||||
key={labelKey}
|
|
||||||
multiple
|
|
||||||
options={values}
|
|
||||||
value={selectedLabels[labelKey] || []}
|
|
||||||
onChange={(_, newValues) =>
|
|
||||||
handleLabelChange(labelKey, newValues)
|
|
||||||
}
|
|
||||||
renderTags={(value, getTagProps) =>
|
|
||||||
value.map((option, index) => {
|
|
||||||
const { key, ...chipProps } =
|
|
||||||
getTagProps({ index });
|
|
||||||
return (
|
|
||||||
<Chip
|
|
||||||
{...chipProps}
|
|
||||||
key={key}
|
|
||||||
label={option}
|
|
||||||
size='small'
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
renderInput={(params) => (
|
|
||||||
<TextField
|
|
||||||
{...params}
|
|
||||||
label={labelKey}
|
|
||||||
placeholder='Select values...'
|
|
||||||
variant='outlined'
|
|
||||||
size='small'
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
sx={{ minWidth: 300 }}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
)}
|
|
||||||
</Box>
|
|
||||||
) : null}
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
};
|
|
@ -1,5 +1,7 @@
|
|||||||
import { lazy } from 'react';
|
import { lazy } from 'react';
|
||||||
|
|
||||||
export const LazyImpactMetricsPage = lazy(
|
export const LazyImpactMetricsPage = lazy(() =>
|
||||||
() => import('./LazyImpactMetricsPageExport.tsx'),
|
import('./ImpactMetricsPage.tsx').then((module) => ({
|
||||||
|
default: module.ImpactMetricsPage,
|
||||||
|
})),
|
||||||
);
|
);
|
||||||
|
@ -1,3 +0,0 @@
|
|||||||
import { ImpactMetricsPage } from './ImpactMetricsPage.tsx';
|
|
||||||
|
|
||||||
export default ImpactMetricsPage;
|
|
@ -20,7 +20,7 @@ const createArrayParam = <T>() => ({
|
|||||||
const ChartsParam = createArrayParam<ChartConfig>();
|
const ChartsParam = createArrayParam<ChartConfig>();
|
||||||
const LayoutParam = createArrayParam<LayoutItem>();
|
const LayoutParam = createArrayParam<LayoutItem>();
|
||||||
|
|
||||||
export const useUrlState = () => {
|
export const useImpactMetricsState = () => {
|
||||||
const stateConfig = {
|
const stateConfig = {
|
||||||
charts: withDefault(ChartsParam, []),
|
charts: withDefault(ChartsParam, []),
|
||||||
layout: withDefault(LayoutParam, []),
|
layout: withDefault(LayoutParam, []),
|
@ -1,12 +1,12 @@
|
|||||||
import { render } from 'utils/testRenderer';
|
import { render } from 'utils/testRenderer';
|
||||||
import { useUrlState } from './useUrlState.ts';
|
import { useImpactMetricsState } from './useImpactMetricsState.ts';
|
||||||
import { Route, Routes } from 'react-router-dom';
|
import { Route, Routes } from 'react-router-dom';
|
||||||
import { createLocalStorage } from '../../../utils/createLocalStorage.ts';
|
import { createLocalStorage } from '../../../utils/createLocalStorage.ts';
|
||||||
import type { FC } from 'react';
|
import type { FC } from 'react';
|
||||||
import type { ImpactMetricsState } from '../types.ts';
|
import type { ImpactMetricsState } from '../types.ts';
|
||||||
|
|
||||||
const TestComponent: FC = () => {
|
const TestComponent: FC = () => {
|
||||||
const { charts, layout } = useUrlState();
|
const { charts, layout } = useImpactMetricsState();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
|
Loading…
Reference in New Issue
Block a user