1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-08-23 13:46:45 +02:00

update impact metrics state

This commit is contained in:
Tymoteusz Czech 2025-07-10 17:15:19 +02:00
parent 69905185c5
commit a9a7bc372d
No known key found for this signature in database
GPG Key ID: 133555230D88D75F
5 changed files with 159 additions and 94 deletions

View File

@ -9,6 +9,8 @@ import { ChartItem } from './ChartItem.tsx';
import { GridLayoutWrapper, type GridItem } from './GridLayoutWrapper.tsx';
import { useImpactMetricsState } from './hooks/useImpactMetricsState.ts';
import type { ChartConfig, LayoutItem } from './types.ts';
import useToast from 'hooks/useToast';
import { formatUnknownError } from 'utils/formatUnknownError';
const StyledEmptyState = styled(Paper)(({ theme }) => ({
textAlign: 'center',
@ -21,9 +23,18 @@ const StyledEmptyState = styled(Paper)(({ theme }) => ({
export const ImpactMetrics: FC = () => {
const [modalOpen, setModalOpen] = useState(false);
const [editingChart, setEditingChart] = useState<ChartConfig | undefined>();
const { setToastApiError } = useToast();
const { charts, layout, addChart, updateChart, deleteChart, updateLayout } =
useImpactMetricsState();
const {
charts,
layout,
loading: settingsLoading,
error: settingsError,
addChart,
updateChart,
deleteChart,
updateLayout,
} = useImpactMetricsState();
const {
metadata,
@ -51,20 +62,39 @@ export const ImpactMetrics: FC = () => {
setModalOpen(true);
};
const handleSaveChart = (config: Omit<ChartConfig, 'id'>) => {
if (editingChart) {
updateChart(editingChart.id, config);
} else {
addChart(config);
const handleSaveChart = async (config: Omit<ChartConfig, 'id'>) => {
try {
if (editingChart) {
await updateChart(editingChart.id, config);
} else {
await addChart(config);
}
setModalOpen(false);
} catch (error) {
setToastApiError(formatUnknownError(error));
}
setModalOpen(false);
};
const handleLayoutChange = useCallback(
(layout: any[]) => {
updateLayout(layout as LayoutItem[]);
async (layout: any[]) => {
try {
await updateLayout(layout as LayoutItem[]);
} catch (error) {
setToastApiError(formatUnknownError(error));
}
},
[updateLayout],
[updateLayout, setToastApiError],
);
const handleDeleteChart = useCallback(
async (id: string) => {
try {
await deleteChart(id);
} catch (error) {
console.error('Failed to delete chart:', error);
}
},
[deleteChart],
);
const gridItems: GridItem[] = useMemo(
@ -79,7 +109,7 @@ export const ImpactMetrics: FC = () => {
<ChartItem
config={config}
onEdit={handleEditChart}
onDelete={deleteChart}
onDelete={handleDeleteChart}
/>
),
w: existingLayout?.w ?? 6,
@ -92,10 +122,11 @@ export const ImpactMetrics: FC = () => {
maxH: 8,
};
}),
[charts, layout, handleEditChart, deleteChart],
[charts, layout, handleEditChart, handleDeleteChart],
);
const hasError = metadataError;
const hasError = metadataError || settingsError;
const isLoading = metadataLoading || settingsLoading;
return (
<>
@ -111,14 +142,14 @@ export const ImpactMetrics: FC = () => {
variant='contained'
startIcon={<Add />}
onClick={handleAddChart}
disabled={metadataLoading || !!hasError}
disabled={isLoading || !!hasError}
>
Add Chart
</Button>
}
/>
{charts.length === 0 && !metadataLoading && !hasError ? (
{charts.length === 0 && !isLoading && !hasError ? (
<StyledEmptyState>
<Typography variant='h6' gutterBottom>
No charts configured
@ -135,7 +166,7 @@ export const ImpactMetrics: FC = () => {
variant='contained'
startIcon={<Add />}
onClick={handleAddChart}
disabled={metadataLoading || !!hasError}
disabled={isLoading || !!hasError}
>
Add Chart
</Button>
@ -153,7 +184,7 @@ export const ImpactMetrics: FC = () => {
onSave={handleSaveChart}
initialConfig={editingChart}
metricSeries={metricSeries}
loading={metadataLoading}
loading={metadataLoading || settingsLoading}
/>
</>
);

View File

@ -184,7 +184,11 @@ export const ImpactMetricsChart: FC<ImpactMetricsChartProps> = ({
background: theme.palette.background.elevation1,
})}
>
<Typography variant='caption' color='text.secondary'>
<Typography
variant='caption'
color='text.secondary'
sx={{ textWrap: 'break-all' }}
>
<code>{debug.query}</code>
</Typography>
</Box>

View File

@ -1,72 +1,40 @@
import { useCallback, useMemo } from 'react';
import { withDefault } from 'use-query-params';
import { usePersistentTableState } from 'hooks/usePersistentTableState';
import { useCallback } from 'react';
import { useImpactMetricsSettings } from 'hooks/api/getters/useImpactMetricsSettings/useImpactMetricsSettings.js';
import {useImpactMetricsSettingsApi} from 'hooks/api/actions/useImpactMetricsSettingsApi/useImpactMetricsSettingsApi.js';
import type { ChartConfig, ImpactMetricsState, LayoutItem } from '../types.ts';
const createArrayParam = <T>() => ({
encode: (items: T[]): string =>
items.length > 0 ? btoa(JSON.stringify(items)) : '',
decode: (value: string | (string | null)[] | null | undefined): T[] => {
if (typeof value !== 'string' || !value) return [];
try {
return JSON.parse(atob(value));
} catch {
return [];
}
},
});
const ChartsParam = createArrayParam<ChartConfig>();
const LayoutParam = createArrayParam<LayoutItem>();
export const useImpactMetricsState = () => {
const stateConfig = {
charts: withDefault(ChartsParam, []),
layout: withDefault(LayoutParam, []),
};
const {
settings,
loading: settingsLoading,
error: settingsError,
refetch,
} = useImpactMetricsSettings();
const [tableState, setTableState] = usePersistentTableState(
'impact-metrics-state',
stateConfig,
);
const currentState: ImpactMetricsState = useMemo(
() => ({
charts: tableState.charts || [],
layout: tableState.layout || [],
}),
[tableState.charts, tableState.layout],
);
const updateState = useCallback(
(newState: ImpactMetricsState) => {
setTableState({
charts: newState.charts,
layout: newState.layout,
});
},
[setTableState],
);
const {
updateSettings,
loading: actionLoading,
errors: actionErrors,
} = useImpactMetricsSettingsApi();
const addChart = useCallback(
(config: Omit<ChartConfig, 'id'>) => {
async (config: Omit<ChartConfig, 'id'>) => {
const newChart: ChartConfig = {
...config,
id: `chart-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
};
const maxY =
currentState.layout.length > 0
settings.layout.length > 0
? Math.max(
...currentState.layout.map((item) => item.y + item.h),
...settings.layout.map((item) => item.y + item.h),
)
: 0;
updateState({
charts: [...currentState.charts, newChart],
const newState: ImpactMetricsState = {
charts: [...settings.charts, newChart],
layout: [
...currentState.layout,
...settings.layout,
{
i: newChart.id,
x: 0,
@ -79,46 +47,58 @@ export const useImpactMetricsState = () => {
maxH: 8,
},
],
});
};
await updateSettings(newState);
refetch();
},
[currentState.charts, currentState.layout, updateState],
[settings, updateSettings, refetch],
);
const updateChart = useCallback(
(id: string, updates: Partial<ChartConfig>) => {
updateState({
charts: currentState.charts.map((chart) =>
chart.id === id ? { ...chart, ...updates } : chart,
),
layout: currentState.layout,
});
async (id: string, updates: Partial<ChartConfig>) => {
const updatedCharts = settings.charts.map((chart) =>
chart.id === id ? { ...chart, ...updates } : chart,
);
const newState: ImpactMetricsState = {
charts: updatedCharts,
layout: settings.layout,
};
await updateSettings(newState);
refetch();
},
[currentState.charts, currentState.layout, updateState],
[settings, updateSettings, refetch],
);
const deleteChart = useCallback(
(id: string) => {
updateState({
charts: currentState.charts.filter((chart) => chart.id !== id),
layout: currentState.layout.filter((item) => item.i !== id),
});
async (id: string) => {
const newState: ImpactMetricsState = {
charts: settings.charts.filter((chart) => chart.id !== id),
layout: settings.layout.filter((item) => item.i !== id),
};
await updateSettings(newState);
refetch();
},
[currentState.charts, currentState.layout, updateState],
[settings, updateSettings, refetch],
);
const updateLayout = useCallback(
(newLayout: LayoutItem[]) => {
updateState({
charts: currentState.charts,
async (newLayout: LayoutItem[]) => {
const newState: ImpactMetricsState = {
charts: settings.charts,
layout: newLayout,
});
};
await updateSettings(newState);
refetch();
},
[currentState.charts, updateState],
[settings, updateSettings, refetch],
);
return {
charts: currentState.charts || [],
layout: currentState.layout || [],
charts: settings.charts || [],
layout: settings.layout || [],
loading: settingsLoading || actionLoading,
error: settingsError || Object.keys(actionErrors).length > 0 ? actionErrors : undefined,
addChart,
updateChart,
deleteChart,

View File

@ -0,0 +1,32 @@
import { useCallback } from 'react';
import useAPI from '../useApi/useApi.js';
import type { ImpactMetricsState } from 'component/impact-metrics/types.ts';
export const useImpactMetricsSettingsApi = () => {
const { makeRequest, createRequest, errors, loading } = useAPI({
propagateErrors: true,
});
const updateSettings = useCallback(
async (settings: ImpactMetricsState) => {
const path = `api/admin/impact-metrics/settings`;
const req = createRequest(
path,
{
method: 'PUT',
body: JSON.stringify(settings)
},
'updateImpactMetricsSettings',
);
return makeRequest(req.caller, req.id);
},
[makeRequest, createRequest],
);
return {
updateSettings,
errors,
loading,
};
};

View File

@ -0,0 +1,18 @@
import { fetcher, useApiGetter } from '../useApiGetter/useApiGetter.js';
import { formatApiPath } from 'utils/formatPath';
import type { ImpactMetricsState } from 'component/impact-metrics/types.ts';
export const useImpactMetricsSettings = () => {
const PATH = `api/admin/impact-metrics/settings`;
const { data, refetch, loading, error } =
useApiGetter<ImpactMetricsState>(formatApiPath(PATH), () =>
fetcher(formatApiPath(PATH), 'Impact metrics settings'),
);
return {
settings: data || { charts: [], layout: [] },
refetch,
loading,
error,
};
};