1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-09-24 17:51:14 +02:00

refactor: global impact metrics saving

This commit is contained in:
Tymoteusz Czech 2025-09-08 15:21:47 +02:00
parent 0a7b295cc8
commit e5c24117f0
No known key found for this signature in database
GPG Key ID: 133555230D88D75F
7 changed files with 90 additions and 96 deletions

View File

@ -134,8 +134,10 @@ export const GridLayoutWrapper: FC<GridLayoutWrapperProps> = ({
Number.parseInt(theme.spacing(2)), Number.parseInt(theme.spacing(2)),
]} ]}
containerPadding={[0, 0]} containerPadding={[0, 0]}
isDraggable={!isMobileBreakpoint} // isDraggable={!isMobileBreakpoint}
isResizable={!isMobileBreakpoint} // isResizable={!isMobileBreakpoint}
isDraggable={false}
isResizable={false}
onLayoutChange={handleLayoutChange} onLayoutChange={handleLayoutChange}
resizeHandles={['se']} resizeHandles={['se']}
draggableHandle='.grid-item-drag-handle' draggableHandle='.grid-item-drag-handle'

View File

@ -2,14 +2,13 @@ import type { FC } from 'react';
import { useMemo, useState, useCallback } from 'react'; import { useMemo, useState, useCallback } from 'react';
import { Typography, Button, Paper, styled, Box } from '@mui/material'; import { Typography, Button, Paper, styled, Box } from '@mui/material';
import Add from '@mui/icons-material/Add'; import Add from '@mui/icons-material/Add';
import DragHandle from '@mui/icons-material/DragHandle';
import { PageHeader } from 'component/common/PageHeader/PageHeader.tsx'; import { PageHeader } from 'component/common/PageHeader/PageHeader.tsx';
import { useImpactMetricsMetadata } from 'hooks/api/getters/useImpactMetricsMetadata/useImpactMetricsMetadata'; import { useImpactMetricsMetadata } from 'hooks/api/getters/useImpactMetricsMetadata/useImpactMetricsMetadata';
import { ChartConfigModal } from './ChartConfigModal/ChartConfigModal.tsx'; import { ChartConfigModal } from './ChartConfigModal/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 { useImpactMetricsState } from './hooks/useImpactMetricsState.ts'; import { useImpactMetricsState } from './hooks/useImpactMetricsState.ts';
import type { ChartConfig, LayoutItem } from './types.ts'; import type { ChartConfig } from './types.ts';
import useToast from 'hooks/useToast'; import useToast from 'hooks/useToast';
import { formatUnknownError } from 'utils/formatUnknownError'; import { formatUnknownError } from 'utils/formatUnknownError';
import PermissionButton from 'component/common/PermissionButton/PermissionButton.tsx'; import PermissionButton from 'component/common/PermissionButton/PermissionButton.tsx';
@ -49,7 +48,7 @@ export const ImpactMetrics: FC = () => {
addChart, addChart,
updateChart, updateChart,
deleteChart, deleteChart,
updateLayout, // updateLayout,
} = useImpactMetricsState(); } = useImpactMetricsState();
const { const {
@ -91,16 +90,16 @@ export const ImpactMetrics: FC = () => {
} }
}; };
const handleLayoutChange = useCallback( // const handleLayoutChange = useCallback(
async (layout: any[]) => { // async (layout: any[]) => {
try { // try {
await updateLayout(layout as LayoutItem[]); // await updateLayout(layout as LayoutItem[]);
} catch (error) { // } catch (error) {
setToastApiError(formatUnknownError(error)); // setToastApiError(formatUnknownError(error));
} // }
}, // },
[updateLayout, setToastApiError], // [updateLayout, setToastApiError],
); // );
const handleDeleteChart = useCallback( const handleDeleteChart = useCallback(
async (id: string) => { async (id: string) => {
@ -127,9 +126,10 @@ export const ImpactMetrics: FC = () => {
onEdit={handleEditChart} onEdit={handleEditChart}
onDelete={handleDeleteChart} onDelete={handleDeleteChart}
dragHandle={ dragHandle={
<StyledDragHandle className='grid-item-drag-handle'> null
<DragHandle fontSize='small' /> // <StyledDragHandle className='grid-item-drag-handle'>
</StyledDragHandle> // <DragHandle fontSize='small' />
// </StyledDragHandle>
} }
/> />
), ),
@ -196,7 +196,7 @@ export const ImpactMetrics: FC = () => {
) : charts.length > 0 ? ( ) : charts.length > 0 ? (
<GridLayoutWrapper <GridLayoutWrapper
items={gridItems} items={gridItems}
onLayoutChange={handleLayoutChange} // onLayoutChange={handleLayoutChange}
/> />
) : null} ) : null}

View File

@ -1,7 +1,7 @@
import { useCallback } from 'react'; import { useCallback, useMemo } from 'react';
import { useImpactMetricsSettings } from 'hooks/api/getters/useImpactMetricsSettings/useImpactMetricsSettings.js'; import type { ChartConfig } from '../types.ts';
import { useImpactMetricsSettingsApi } from 'hooks/api/actions/useImpactMetricsSettingsApi/useImpactMetricsSettingsApi.js'; import { useImpactMetricsConfig } from 'hooks/api/getters/useImpactMetricsConfig/useImpactMetricsConfig.ts';
import type { ChartConfig, ImpactMetricsState, LayoutItem } from '../types.ts'; import { useImpactMetricsApi } from 'hooks/api/actions/useImpactMetricsSettingsApi/useImpactMetricsApi.ts';
/** /**
* "Select all" represents all current and future labels. * "Select all" represents all current and future labels.
@ -11,106 +11,71 @@ export const METRIC_LABELS_SELECT_ALL = '*';
export const useImpactMetricsState = () => { export const useImpactMetricsState = () => {
const { const {
settings, configs,
loading: settingsLoading, loading: configLoading,
error: settingsError, error: configError,
refetch, refetch,
} = useImpactMetricsSettings(); } = useImpactMetricsConfig();
const { layout, charts } = useMemo(
() => ({
layout: configs.map((config, index) => ({
i: config.id,
x: 0,
y: index * 4,
w: 6,
h: 4,
minW: 4,
minH: 2,
maxW: 12,
maxH: 8,
})),
charts: configs,
}),
[configs],
);
const { const {
updateSettings, createImpactMetric,
deleteImpactMetric,
loading: actionLoading, loading: actionLoading,
errors: actionErrors, errors: actionErrors,
} = useImpactMetricsSettingsApi(); } = useImpactMetricsApi();
const addChart = useCallback( const addChart = useCallback(
async (config: Omit<ChartConfig, 'id'>) => { async (config: Omit<ChartConfig, 'id'>) => {
const newChart: ChartConfig = { await createImpactMetric(config);
...config,
id: `chart-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
};
const maxY =
settings.layout.length > 0
? Math.max(
...settings.layout.map((item) => item.y + item.h),
)
: 0;
const newState: ImpactMetricsState = {
charts: [...settings.charts, newChart],
layout: [
...settings.layout,
{
i: newChart.id,
x: 0,
y: maxY,
w: 6,
h: 4,
minW: 4,
minH: 2,
maxW: 12,
maxH: 8,
},
],
};
await updateSettings(newState);
refetch(); refetch();
}, },
[settings, updateSettings, refetch], [createImpactMetric, refetch],
); );
const updateChart = useCallback( const updateChart = useCallback(
async (id: string, updates: Partial<ChartConfig>) => { async (id: string, updates: Omit<ChartConfig, 'id'>) => {
const updatedCharts = settings.charts.map((chart) => await createImpactMetric({ ...updates, id });
chart.id === id ? { ...chart, ...updates } : chart,
);
const newState: ImpactMetricsState = {
charts: updatedCharts,
layout: settings.layout,
};
await updateSettings(newState);
refetch(); refetch();
}, },
[settings, updateSettings, refetch], [configs, createImpactMetric, refetch],
); );
const deleteChart = useCallback( const deleteChart = useCallback(
async (id: string) => { async (id: string) => {
const newState: ImpactMetricsState = { await deleteImpactMetric(id);
charts: settings.charts.filter((chart) => chart.id !== id),
layout: settings.layout.filter((item) => item.i !== id),
};
await updateSettings(newState);
refetch(); refetch();
}, },
[settings, updateSettings, refetch], [configs, deleteImpactMetric, refetch],
);
const updateLayout = useCallback(
async (newLayout: LayoutItem[]) => {
const newState: ImpactMetricsState = {
charts: settings.charts,
layout: newLayout,
};
await updateSettings(newState);
refetch();
},
[settings, updateSettings, refetch],
); );
return { return {
charts: settings.charts || [], charts,
layout: settings.layout || [], layout,
loading: settingsLoading || actionLoading, loading: configLoading || actionLoading,
error: error:
settingsError || Object.keys(actionErrors).length > 0 configError || Object.keys(actionErrors).length > 0
? actionErrors ? actionErrors
: undefined, : undefined,
addChart, addChart,
updateChart, updateChart,
deleteChart, deleteChart,
updateLayout,
}; };
}; };

View File

@ -9,7 +9,7 @@ type UseImpactMetricsApiParams =
} }
| undefined; | undefined;
export const useImpactMetricsApi = (params: UseImpactMetricsApiParams) => { export const useImpactMetricsApi = (params?: UseImpactMetricsApiParams) => {
const basePath = params const basePath = params
? `api/admin/projects/${params.projectId}/features/${params.featureName}/impact-metrics/config` ? `api/admin/projects/${params.projectId}/features/${params.featureName}/impact-metrics/config`
: `api/admin/impact-metrics/config`; : `api/admin/impact-metrics/config`;
@ -46,7 +46,7 @@ export const useImpactMetricsApi = (params: UseImpactMetricsApiParams) => {
return makeRequest(req.caller, req.id); return makeRequest(req.caller, req.id);
}, },
[makeRequest, createRequest], [makeRequest, createRequest, basePath],
); );
return { return {

View File

@ -2,6 +2,9 @@ import { useCallback } from 'react';
import useAPI from '../useApi/useApi.js'; import useAPI from '../useApi/useApi.js';
import type { ImpactMetricsState } from 'component/impact-metrics/types.ts'; import type { ImpactMetricsState } from 'component/impact-metrics/types.ts';
/**
* @deprecated use `useImpactMetricsApi()` instead
*/
export const useImpactMetricsSettingsApi = () => { export const useImpactMetricsSettingsApi = () => {
const { makeRequest, createRequest, errors, loading } = useAPI({ const { makeRequest, createRequest, errors, loading } = useAPI({
propagateErrors: true, propagateErrors: true,

View File

@ -0,0 +1,21 @@
import type { ImpactMetricsConfigListSchema } from 'openapi/index.js';
import { fetcher, useApiGetter } from '../useApiGetter/useApiGetter.js';
import { formatApiPath } from 'utils/formatPath';
/**
* Get all impact metrics configurations now associated with any feature flag.
*/
export const useImpactMetricsConfig = () => {
const PATH = `api/admin/impact-metrics/config`;
const { data, refetch, loading, error } =
useApiGetter<ImpactMetricsConfigListSchema>(formatApiPath(PATH), () =>
fetcher(formatApiPath(PATH), 'impactMetricsConfig'),
);
return {
configs: data?.configs || [],
refetch,
loading,
error,
};
};

View File

@ -2,6 +2,9 @@ import { fetcher, useApiGetter } from '../useApiGetter/useApiGetter.js';
import { formatApiPath } from 'utils/formatPath'; import { formatApiPath } from 'utils/formatPath';
import type { DisplayImpactMetricsState } from 'component/impact-metrics/types.ts'; import type { DisplayImpactMetricsState } from 'component/impact-metrics/types.ts';
/**
* @deprecated use `useImpactMetricsConfig()` instead
*/
export const useImpactMetricsSettings = () => { export const useImpactMetricsSettings = () => {
const PATH = `api/admin/impact-metrics/settings`; const PATH = `api/admin/impact-metrics/settings`;
const { data, refetch, loading, error } = const { data, refetch, loading, error } =