From a9a7bc372d564c1818bcdff861337b62dfd2eddc Mon Sep 17 00:00:00 2001 From: Tymoteusz Czech <2625371+Tymek@users.noreply.github.com> Date: Thu, 10 Jul 2025 17:15:19 +0200 Subject: [PATCH] update impact metrics state --- .../impact-metrics/ImpactMetrics.tsx | 67 ++++++--- .../impact-metrics/ImpactMetricsChart.tsx | 6 +- .../hooks/useImpactMetricsState.ts | 130 ++++++++---------- .../useImpactMetricsSettingsApi.ts | 32 +++++ .../useImpactMetricsSettings.ts | 18 +++ 5 files changed, 159 insertions(+), 94 deletions(-) create mode 100644 frontend/src/hooks/api/actions/useImpactMetricsSettingsApi/useImpactMetricsSettingsApi.ts create mode 100644 frontend/src/hooks/api/getters/useImpactMetricsSettings/useImpactMetricsSettings.ts diff --git a/frontend/src/component/impact-metrics/ImpactMetrics.tsx b/frontend/src/component/impact-metrics/ImpactMetrics.tsx index 3cbbbc5027..251dd999cf 100644 --- a/frontend/src/component/impact-metrics/ImpactMetrics.tsx +++ b/frontend/src/component/impact-metrics/ImpactMetrics.tsx @@ -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(); + 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) => { - if (editingChart) { - updateChart(editingChart.id, config); - } else { - addChart(config); + const handleSaveChart = async (config: Omit) => { + 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 = () => { ), 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={} onClick={handleAddChart} - disabled={metadataLoading || !!hasError} + disabled={isLoading || !!hasError} > Add Chart } /> - {charts.length === 0 && !metadataLoading && !hasError ? ( + {charts.length === 0 && !isLoading && !hasError ? ( No charts configured @@ -135,7 +166,7 @@ export const ImpactMetrics: FC = () => { variant='contained' startIcon={} onClick={handleAddChart} - disabled={metadataLoading || !!hasError} + disabled={isLoading || !!hasError} > Add Chart @@ -153,7 +184,7 @@ export const ImpactMetrics: FC = () => { onSave={handleSaveChart} initialConfig={editingChart} metricSeries={metricSeries} - loading={metadataLoading} + loading={metadataLoading || settingsLoading} /> ); diff --git a/frontend/src/component/impact-metrics/ImpactMetricsChart.tsx b/frontend/src/component/impact-metrics/ImpactMetricsChart.tsx index eacb969c2d..27476e8829 100644 --- a/frontend/src/component/impact-metrics/ImpactMetricsChart.tsx +++ b/frontend/src/component/impact-metrics/ImpactMetricsChart.tsx @@ -184,7 +184,11 @@ export const ImpactMetricsChart: FC = ({ background: theme.palette.background.elevation1, })} > - + {debug.query} diff --git a/frontend/src/component/impact-metrics/hooks/useImpactMetricsState.ts b/frontend/src/component/impact-metrics/hooks/useImpactMetricsState.ts index 58069dba09..5904181a2e 100644 --- a/frontend/src/component/impact-metrics/hooks/useImpactMetricsState.ts +++ b/frontend/src/component/impact-metrics/hooks/useImpactMetricsState.ts @@ -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 = () => ({ - 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(); -const LayoutParam = createArrayParam(); - 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) => { + async (config: Omit) => { 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) => { - updateState({ - charts: currentState.charts.map((chart) => - chart.id === id ? { ...chart, ...updates } : chart, - ), - layout: currentState.layout, - }); + async (id: string, updates: Partial) => { + 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, diff --git a/frontend/src/hooks/api/actions/useImpactMetricsSettingsApi/useImpactMetricsSettingsApi.ts b/frontend/src/hooks/api/actions/useImpactMetricsSettingsApi/useImpactMetricsSettingsApi.ts new file mode 100644 index 0000000000..93ae166b62 --- /dev/null +++ b/frontend/src/hooks/api/actions/useImpactMetricsSettingsApi/useImpactMetricsSettingsApi.ts @@ -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, + }; +}; diff --git a/frontend/src/hooks/api/getters/useImpactMetricsSettings/useImpactMetricsSettings.ts b/frontend/src/hooks/api/getters/useImpactMetricsSettings/useImpactMetricsSettings.ts new file mode 100644 index 0000000000..73f7765de6 --- /dev/null +++ b/frontend/src/hooks/api/getters/useImpactMetricsSettings/useImpactMetricsSettings.ts @@ -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(formatApiPath(PATH), () => + fetcher(formatApiPath(PATH), 'Impact metrics settings'), + ); + + return { + settings: data || { charts: [], layout: [] }, + refetch, + loading, + error, + }; +};