mirror of
https://github.com/Unleash/unleash.git
synced 2025-08-13 13:48:59 +02:00
Update impact metrics state (#10342)
This commit is contained in:
parent
69905185c5
commit
1948861a46
@ -9,6 +9,8 @@ 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, LayoutItem } from './types.ts';
|
||||||
|
import useToast from 'hooks/useToast';
|
||||||
|
import { formatUnknownError } from 'utils/formatUnknownError';
|
||||||
|
|
||||||
const StyledEmptyState = styled(Paper)(({ theme }) => ({
|
const StyledEmptyState = styled(Paper)(({ theme }) => ({
|
||||||
textAlign: 'center',
|
textAlign: 'center',
|
||||||
@ -21,9 +23,18 @@ const StyledEmptyState = styled(Paper)(({ theme }) => ({
|
|||||||
export const ImpactMetrics: FC = () => {
|
export const ImpactMetrics: FC = () => {
|
||||||
const [modalOpen, setModalOpen] = useState(false);
|
const [modalOpen, setModalOpen] = useState(false);
|
||||||
const [editingChart, setEditingChart] = useState<ChartConfig | undefined>();
|
const [editingChart, setEditingChart] = useState<ChartConfig | undefined>();
|
||||||
|
const { setToastApiError } = useToast();
|
||||||
|
|
||||||
const { charts, layout, addChart, updateChart, deleteChart, updateLayout } =
|
const {
|
||||||
useImpactMetricsState();
|
charts,
|
||||||
|
layout,
|
||||||
|
loading: settingsLoading,
|
||||||
|
error: settingsError,
|
||||||
|
addChart,
|
||||||
|
updateChart,
|
||||||
|
deleteChart,
|
||||||
|
updateLayout,
|
||||||
|
} = useImpactMetricsState();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
metadata,
|
metadata,
|
||||||
@ -51,20 +62,39 @@ export const ImpactMetrics: FC = () => {
|
|||||||
setModalOpen(true);
|
setModalOpen(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSaveChart = (config: Omit<ChartConfig, 'id'>) => {
|
const handleSaveChart = async (config: Omit<ChartConfig, 'id'>) => {
|
||||||
if (editingChart) {
|
try {
|
||||||
updateChart(editingChart.id, config);
|
if (editingChart) {
|
||||||
} else {
|
await updateChart(editingChart.id, config);
|
||||||
addChart(config);
|
} else {
|
||||||
|
await addChart(config);
|
||||||
|
}
|
||||||
|
setModalOpen(false);
|
||||||
|
} catch (error) {
|
||||||
|
setToastApiError(formatUnknownError(error));
|
||||||
}
|
}
|
||||||
setModalOpen(false);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleLayoutChange = useCallback(
|
const handleLayoutChange = useCallback(
|
||||||
(layout: any[]) => {
|
async (layout: any[]) => {
|
||||||
updateLayout(layout as LayoutItem[]);
|
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) {
|
||||||
|
setToastApiError(formatUnknownError(error));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[deleteChart],
|
||||||
);
|
);
|
||||||
|
|
||||||
const gridItems: GridItem[] = useMemo(
|
const gridItems: GridItem[] = useMemo(
|
||||||
@ -79,7 +109,7 @@ export const ImpactMetrics: FC = () => {
|
|||||||
<ChartItem
|
<ChartItem
|
||||||
config={config}
|
config={config}
|
||||||
onEdit={handleEditChart}
|
onEdit={handleEditChart}
|
||||||
onDelete={deleteChart}
|
onDelete={handleDeleteChart}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
w: existingLayout?.w ?? 6,
|
w: existingLayout?.w ?? 6,
|
||||||
@ -92,10 +122,11 @@ export const ImpactMetrics: FC = () => {
|
|||||||
maxH: 8,
|
maxH: 8,
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
[charts, layout, handleEditChart, deleteChart],
|
[charts, layout, handleEditChart, handleDeleteChart],
|
||||||
);
|
);
|
||||||
|
|
||||||
const hasError = metadataError;
|
const hasError = metadataError || settingsError;
|
||||||
|
const isLoading = metadataLoading || settingsLoading;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -111,14 +142,14 @@ export const ImpactMetrics: FC = () => {
|
|||||||
variant='contained'
|
variant='contained'
|
||||||
startIcon={<Add />}
|
startIcon={<Add />}
|
||||||
onClick={handleAddChart}
|
onClick={handleAddChart}
|
||||||
disabled={metadataLoading || !!hasError}
|
disabled={isLoading || !!hasError}
|
||||||
>
|
>
|
||||||
Add Chart
|
Add Chart
|
||||||
</Button>
|
</Button>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{charts.length === 0 && !metadataLoading && !hasError ? (
|
{charts.length === 0 && !isLoading && !hasError ? (
|
||||||
<StyledEmptyState>
|
<StyledEmptyState>
|
||||||
<Typography variant='h6' gutterBottom>
|
<Typography variant='h6' gutterBottom>
|
||||||
No charts configured
|
No charts configured
|
||||||
@ -135,7 +166,7 @@ export const ImpactMetrics: FC = () => {
|
|||||||
variant='contained'
|
variant='contained'
|
||||||
startIcon={<Add />}
|
startIcon={<Add />}
|
||||||
onClick={handleAddChart}
|
onClick={handleAddChart}
|
||||||
disabled={metadataLoading || !!hasError}
|
disabled={isLoading || !!hasError}
|
||||||
>
|
>
|
||||||
Add Chart
|
Add Chart
|
||||||
</Button>
|
</Button>
|
||||||
@ -153,7 +184,7 @@ export const ImpactMetrics: FC = () => {
|
|||||||
onSave={handleSaveChart}
|
onSave={handleSaveChart}
|
||||||
initialConfig={editingChart}
|
initialConfig={editingChart}
|
||||||
metricSeries={metricSeries}
|
metricSeries={metricSeries}
|
||||||
loading={metadataLoading}
|
loading={metadataLoading || settingsLoading}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
@ -184,7 +184,11 @@ export const ImpactMetricsChart: FC<ImpactMetricsChartProps> = ({
|
|||||||
background: theme.palette.background.elevation1,
|
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>
|
<code>{debug.query}</code>
|
||||||
</Typography>
|
</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
|
@ -1,123 +1,332 @@
|
|||||||
|
import { screen, waitFor } from '@testing-library/react';
|
||||||
|
import userEvent from '@testing-library/user-event';
|
||||||
import { render } from 'utils/testRenderer';
|
import { render } from 'utils/testRenderer';
|
||||||
|
import { testServerRoute, testServerSetup } from 'utils/testServer';
|
||||||
import { useImpactMetricsState } from './useImpactMetricsState.ts';
|
import { useImpactMetricsState } from './useImpactMetricsState.ts';
|
||||||
import { Route, Routes } from 'react-router-dom';
|
|
||||||
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 server = testServerSetup();
|
||||||
const { charts, layout } = useImpactMetricsState();
|
|
||||||
|
const TestComponent: FC<{
|
||||||
|
enableActions?: boolean;
|
||||||
|
}> = ({ enableActions = false }) => {
|
||||||
|
const {
|
||||||
|
charts,
|
||||||
|
layout,
|
||||||
|
loading,
|
||||||
|
error,
|
||||||
|
addChart,
|
||||||
|
updateChart,
|
||||||
|
deleteChart,
|
||||||
|
} = useImpactMetricsState();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<span data-testid='charts-count'>{charts.length}</span>
|
<span data-testid='charts-count'>{charts.length}</span>
|
||||||
<span data-testid='layout-count'>{layout.length}</span>
|
<span data-testid='layout-count'>{layout.length}</span>
|
||||||
|
<span data-testid='loading'>{loading.toString()}</span>
|
||||||
|
<span data-testid='error'>{error ? 'has-error' : 'no-error'}</span>
|
||||||
|
|
||||||
|
{enableActions && (
|
||||||
|
<button
|
||||||
|
type='button'
|
||||||
|
data-testid='add-chart'
|
||||||
|
onClick={() =>
|
||||||
|
addChart({
|
||||||
|
selectedSeries: 'test-series',
|
||||||
|
selectedRange: 'day',
|
||||||
|
beginAtZero: true,
|
||||||
|
showRate: false,
|
||||||
|
selectedLabels: {},
|
||||||
|
title: 'Test Chart',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Add Chart
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{enableActions && charts.length > 0 && (
|
||||||
|
<button
|
||||||
|
type='button'
|
||||||
|
data-testid='update-chart'
|
||||||
|
onClick={() =>
|
||||||
|
updateChart(charts[0].id, { title: 'Updated Chart' })
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Update Chart
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{enableActions && charts.length > 0 && (
|
||||||
|
<button
|
||||||
|
type='button'
|
||||||
|
data-testid='delete-chart'
|
||||||
|
onClick={() => deleteChart(charts[0].id)}
|
||||||
|
>
|
||||||
|
Delete Chart
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const TestWrapper = () => (
|
const mockSettings: ImpactMetricsState = {
|
||||||
<Routes>
|
charts: [
|
||||||
<Route path='/impact-metrics' element={<TestComponent />} />
|
{
|
||||||
</Routes>
|
id: 'test-chart',
|
||||||
);
|
selectedSeries: 'test-series',
|
||||||
|
selectedRange: 'day' as const,
|
||||||
|
beginAtZero: true,
|
||||||
|
showRate: false,
|
||||||
|
selectedLabels: {},
|
||||||
|
title: 'Test Chart',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
layout: [
|
||||||
|
{
|
||||||
|
i: 'test-chart',
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
w: 6,
|
||||||
|
h: 4,
|
||||||
|
minW: 4,
|
||||||
|
minH: 2,
|
||||||
|
maxW: 12,
|
||||||
|
maxH: 8,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const emptySettings: ImpactMetricsState = {
|
||||||
|
charts: [],
|
||||||
|
layout: [],
|
||||||
|
};
|
||||||
|
|
||||||
describe('useImpactMetricsState', () => {
|
describe('useImpactMetricsState', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
window.localStorage.clear();
|
testServerRoute(server, '/api/admin/ui-config', {});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('loads state from localStorage to the URL after opening page without URL state', async () => {
|
it('loads settings from API', async () => {
|
||||||
const { setValue } = createLocalStorage<ImpactMetricsState>(
|
testServerRoute(
|
||||||
'impact-metrics-state',
|
server,
|
||||||
{
|
'/api/admin/impact-metrics/settings',
|
||||||
charts: [],
|
mockSettings,
|
||||||
layout: [],
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
|
|
||||||
setValue({
|
render(<TestComponent />);
|
||||||
charts: [
|
|
||||||
{
|
await waitFor(() => {
|
||||||
id: 'test-chart',
|
expect(screen.getByTestId('charts-count')).toHaveTextContent('1');
|
||||||
selectedSeries: 'test-series',
|
expect(screen.getByTestId('layout-count')).toHaveTextContent('1');
|
||||||
selectedRange: 'day' as const,
|
expect(screen.getByTestId('loading')).toHaveTextContent('false');
|
||||||
beginAtZero: true,
|
expect(screen.getByTestId('error')).toHaveTextContent('no-error');
|
||||||
showRate: false,
|
|
||||||
selectedLabels: {},
|
|
||||||
title: 'Test Chart',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
layout: [
|
|
||||||
{
|
|
||||||
i: 'test-chart',
|
|
||||||
x: 0,
|
|
||||||
y: 0,
|
|
||||||
w: 6,
|
|
||||||
h: 4,
|
|
||||||
minW: 4,
|
|
||||||
minH: 2,
|
|
||||||
maxW: 12,
|
|
||||||
maxH: 8,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
});
|
||||||
|
|
||||||
render(<TestWrapper />, { route: '/impact-metrics' });
|
|
||||||
|
|
||||||
expect(window.location.href).toContain('charts=');
|
|
||||||
expect(window.location.href).toContain('layout=');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('does not modify URL when URL already contains data', async () => {
|
it('handles empty settings', async () => {
|
||||||
const { setValue } = createLocalStorage<ImpactMetricsState>(
|
testServerRoute(
|
||||||
'impact-metrics-state',
|
server,
|
||||||
|
'/api/admin/impact-metrics/settings',
|
||||||
|
emptySettings,
|
||||||
|
);
|
||||||
|
|
||||||
|
render(<TestComponent />);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByTestId('charts-count')).toHaveTextContent('0');
|
||||||
|
expect(screen.getByTestId('layout-count')).toHaveTextContent('0');
|
||||||
|
expect(screen.getByTestId('loading')).toHaveTextContent('false');
|
||||||
|
expect(screen.getByTestId('error')).toHaveTextContent('no-error');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles API errors', async () => {
|
||||||
|
testServerRoute(
|
||||||
|
server,
|
||||||
|
'/api/admin/impact-metrics/settings',
|
||||||
|
{ message: 'Server error' },
|
||||||
|
'get',
|
||||||
|
500,
|
||||||
|
);
|
||||||
|
|
||||||
|
render(<TestComponent />);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByTestId('error')).toHaveTextContent('has-error');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('adds a chart successfully', async () => {
|
||||||
|
testServerRoute(
|
||||||
|
server,
|
||||||
|
'/api/admin/impact-metrics/settings',
|
||||||
|
emptySettings,
|
||||||
|
);
|
||||||
|
|
||||||
|
render(<TestComponent enableActions />);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByTestId('charts-count')).toHaveTextContent('0');
|
||||||
|
});
|
||||||
|
|
||||||
|
testServerRoute(
|
||||||
|
server,
|
||||||
|
'/api/admin/impact-metrics/settings',
|
||||||
{
|
{
|
||||||
charts: [],
|
charts: [
|
||||||
layout: [],
|
{
|
||||||
|
id: 'new-chart-id',
|
||||||
|
selectedSeries: 'test-series',
|
||||||
|
selectedRange: 'day',
|
||||||
|
beginAtZero: true,
|
||||||
|
showRate: false,
|
||||||
|
selectedLabels: {},
|
||||||
|
title: 'Test Chart',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
layout: [
|
||||||
|
{
|
||||||
|
i: 'new-chart-id',
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
w: 6,
|
||||||
|
h: 4,
|
||||||
|
minW: 4,
|
||||||
|
minH: 2,
|
||||||
|
maxW: 12,
|
||||||
|
maxH: 8,
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
|
'put',
|
||||||
|
200,
|
||||||
);
|
);
|
||||||
|
|
||||||
setValue({
|
testServerRoute(
|
||||||
charts: [
|
server,
|
||||||
{
|
'/api/admin/impact-metrics/settings',
|
||||||
id: 'old-chart',
|
{
|
||||||
selectedSeries: 'old-series',
|
charts: [
|
||||||
selectedRange: 'day' as const,
|
{
|
||||||
beginAtZero: true,
|
id: 'new-chart-id',
|
||||||
showRate: false,
|
selectedSeries: 'test-series',
|
||||||
selectedLabels: {},
|
selectedRange: 'day',
|
||||||
title: 'Old Chart',
|
beginAtZero: true,
|
||||||
},
|
showRate: false,
|
||||||
],
|
selectedLabels: {},
|
||||||
layout: [],
|
title: 'Test Chart',
|
||||||
});
|
},
|
||||||
|
],
|
||||||
const urlCharts = btoa(
|
layout: [
|
||||||
JSON.stringify([
|
{
|
||||||
{
|
i: 'new-chart-id',
|
||||||
id: 'url-chart',
|
x: 0,
|
||||||
selectedSeries: 'url-series',
|
y: 0,
|
||||||
selectedRange: 'day',
|
w: 6,
|
||||||
beginAtZero: true,
|
h: 4,
|
||||||
showRate: false,
|
minW: 4,
|
||||||
selectedLabels: {},
|
minH: 2,
|
||||||
title: 'URL Chart',
|
maxW: 12,
|
||||||
},
|
maxH: 8,
|
||||||
]),
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
'get',
|
||||||
|
200,
|
||||||
);
|
);
|
||||||
|
|
||||||
render(<TestWrapper />, {
|
const addButton = screen.getByTestId('add-chart');
|
||||||
route: `/impact-metrics?charts=${encodeURIComponent(urlCharts)}`,
|
await userEvent.click(addButton);
|
||||||
|
|
||||||
|
await waitFor(
|
||||||
|
() => {
|
||||||
|
expect(screen.getByTestId('charts-count')).toHaveTextContent(
|
||||||
|
'1',
|
||||||
|
);
|
||||||
|
},
|
||||||
|
{ timeout: 5000 },
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('updates a chart successfully', async () => {
|
||||||
|
testServerRoute(
|
||||||
|
server,
|
||||||
|
'/api/admin/impact-metrics/settings',
|
||||||
|
mockSettings,
|
||||||
|
);
|
||||||
|
|
||||||
|
testServerRoute(
|
||||||
|
server,
|
||||||
|
'/api/admin/impact-metrics/settings',
|
||||||
|
{
|
||||||
|
charts: [
|
||||||
|
{
|
||||||
|
...mockSettings.charts[0],
|
||||||
|
title: 'Updated Chart',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
layout: mockSettings.layout,
|
||||||
|
},
|
||||||
|
'put',
|
||||||
|
200,
|
||||||
|
);
|
||||||
|
|
||||||
|
render(<TestComponent enableActions />);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByTestId('charts-count')).toHaveTextContent('1');
|
||||||
});
|
});
|
||||||
|
|
||||||
const urlParams = new URLSearchParams(window.location.search);
|
const updateButton = screen.getByTestId('update-chart');
|
||||||
const chartsParam = urlParams.get('charts');
|
await userEvent.click(updateButton);
|
||||||
|
|
||||||
expect(chartsParam).toBeTruthy();
|
await waitFor(() => {
|
||||||
|
expect(screen.getByTestId('charts-count')).toHaveTextContent('1');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
const decodedCharts = JSON.parse(atob(chartsParam!));
|
it('deletes a chart successfully', async () => {
|
||||||
expect(decodedCharts[0].id).toBe('url-chart');
|
testServerRoute(
|
||||||
expect(decodedCharts[0].id).not.toBe('old-chart');
|
server,
|
||||||
|
'/api/admin/impact-metrics/settings',
|
||||||
|
mockSettings,
|
||||||
|
);
|
||||||
|
|
||||||
|
render(<TestComponent enableActions />);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByTestId('charts-count')).toHaveTextContent('1');
|
||||||
|
});
|
||||||
|
|
||||||
|
testServerRoute(
|
||||||
|
server,
|
||||||
|
'/api/admin/impact-metrics/settings',
|
||||||
|
emptySettings,
|
||||||
|
'put',
|
||||||
|
200,
|
||||||
|
);
|
||||||
|
|
||||||
|
testServerRoute(
|
||||||
|
server,
|
||||||
|
'/api/admin/impact-metrics/settings',
|
||||||
|
emptySettings,
|
||||||
|
'get',
|
||||||
|
200,
|
||||||
|
);
|
||||||
|
|
||||||
|
const deleteButton = screen.getByTestId('delete-chart');
|
||||||
|
await userEvent.click(deleteButton);
|
||||||
|
|
||||||
|
await waitFor(
|
||||||
|
() => {
|
||||||
|
expect(screen.getByTestId('charts-count')).toHaveTextContent(
|
||||||
|
'0',
|
||||||
|
);
|
||||||
|
},
|
||||||
|
{ timeout: 5000 },
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -1,72 +1,40 @@
|
|||||||
import { useCallback, useMemo } from 'react';
|
import { useCallback } from 'react';
|
||||||
import { withDefault } from 'use-query-params';
|
import { useImpactMetricsSettings } from 'hooks/api/getters/useImpactMetricsSettings/useImpactMetricsSettings.js';
|
||||||
import { usePersistentTableState } from 'hooks/usePersistentTableState';
|
import { useImpactMetricsSettingsApi } from 'hooks/api/actions/useImpactMetricsSettingsApi/useImpactMetricsSettingsApi.js';
|
||||||
import type { ChartConfig, ImpactMetricsState, LayoutItem } from '../types.ts';
|
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 = () => {
|
export const useImpactMetricsState = () => {
|
||||||
const stateConfig = {
|
const {
|
||||||
charts: withDefault(ChartsParam, []),
|
settings,
|
||||||
layout: withDefault(LayoutParam, []),
|
loading: settingsLoading,
|
||||||
};
|
error: settingsError,
|
||||||
|
refetch,
|
||||||
|
} = useImpactMetricsSettings();
|
||||||
|
|
||||||
const [tableState, setTableState] = usePersistentTableState(
|
const {
|
||||||
'impact-metrics-state',
|
updateSettings,
|
||||||
stateConfig,
|
loading: actionLoading,
|
||||||
);
|
errors: actionErrors,
|
||||||
|
} = useImpactMetricsSettingsApi();
|
||||||
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 addChart = useCallback(
|
const addChart = useCallback(
|
||||||
(config: Omit<ChartConfig, 'id'>) => {
|
async (config: Omit<ChartConfig, 'id'>) => {
|
||||||
const newChart: ChartConfig = {
|
const newChart: ChartConfig = {
|
||||||
...config,
|
...config,
|
||||||
id: `chart-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
|
id: `chart-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
|
||||||
};
|
};
|
||||||
|
|
||||||
const maxY =
|
const maxY =
|
||||||
currentState.layout.length > 0
|
settings.layout.length > 0
|
||||||
? Math.max(
|
? Math.max(
|
||||||
...currentState.layout.map((item) => item.y + item.h),
|
...settings.layout.map((item) => item.y + item.h),
|
||||||
)
|
)
|
||||||
: 0;
|
: 0;
|
||||||
|
|
||||||
updateState({
|
const newState: ImpactMetricsState = {
|
||||||
charts: [...currentState.charts, newChart],
|
charts: [...settings.charts, newChart],
|
||||||
layout: [
|
layout: [
|
||||||
...currentState.layout,
|
...settings.layout,
|
||||||
{
|
{
|
||||||
i: newChart.id,
|
i: newChart.id,
|
||||||
x: 0,
|
x: 0,
|
||||||
@ -79,46 +47,61 @@ export const useImpactMetricsState = () => {
|
|||||||
maxH: 8,
|
maxH: 8,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
});
|
};
|
||||||
|
|
||||||
|
await updateSettings(newState);
|
||||||
|
refetch();
|
||||||
},
|
},
|
||||||
[currentState.charts, currentState.layout, updateState],
|
[settings, updateSettings, refetch],
|
||||||
);
|
);
|
||||||
|
|
||||||
const updateChart = useCallback(
|
const updateChart = useCallback(
|
||||||
(id: string, updates: Partial<ChartConfig>) => {
|
async (id: string, updates: Partial<ChartConfig>) => {
|
||||||
updateState({
|
const updatedCharts = settings.charts.map((chart) =>
|
||||||
charts: currentState.charts.map((chart) =>
|
chart.id === id ? { ...chart, ...updates } : chart,
|
||||||
chart.id === id ? { ...chart, ...updates } : chart,
|
);
|
||||||
),
|
const newState: ImpactMetricsState = {
|
||||||
layout: currentState.layout,
|
charts: updatedCharts,
|
||||||
});
|
layout: settings.layout,
|
||||||
|
};
|
||||||
|
await updateSettings(newState);
|
||||||
|
refetch();
|
||||||
},
|
},
|
||||||
[currentState.charts, currentState.layout, updateState],
|
[settings, updateSettings, refetch],
|
||||||
);
|
);
|
||||||
|
|
||||||
const deleteChart = useCallback(
|
const deleteChart = useCallback(
|
||||||
(id: string) => {
|
async (id: string) => {
|
||||||
updateState({
|
const newState: ImpactMetricsState = {
|
||||||
charts: currentState.charts.filter((chart) => chart.id !== id),
|
charts: settings.charts.filter((chart) => chart.id !== id),
|
||||||
layout: currentState.layout.filter((item) => item.i !== id),
|
layout: settings.layout.filter((item) => item.i !== id),
|
||||||
});
|
};
|
||||||
|
await updateSettings(newState);
|
||||||
|
refetch();
|
||||||
},
|
},
|
||||||
[currentState.charts, currentState.layout, updateState],
|
[settings, updateSettings, refetch],
|
||||||
);
|
);
|
||||||
|
|
||||||
const updateLayout = useCallback(
|
const updateLayout = useCallback(
|
||||||
(newLayout: LayoutItem[]) => {
|
async (newLayout: LayoutItem[]) => {
|
||||||
updateState({
|
const newState: ImpactMetricsState = {
|
||||||
charts: currentState.charts,
|
charts: settings.charts,
|
||||||
layout: newLayout,
|
layout: newLayout,
|
||||||
});
|
};
|
||||||
|
await updateSettings(newState);
|
||||||
|
refetch();
|
||||||
},
|
},
|
||||||
[currentState.charts, updateState],
|
[settings, updateSettings, refetch],
|
||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
charts: currentState.charts || [],
|
charts: settings.charts || [],
|
||||||
layout: currentState.layout || [],
|
layout: settings.layout || [],
|
||||||
|
loading: settingsLoading || actionLoading,
|
||||||
|
error:
|
||||||
|
settingsError || Object.keys(actionErrors).length > 0
|
||||||
|
? actionErrors
|
||||||
|
: undefined,
|
||||||
addChart,
|
addChart,
|
||||||
updateChart,
|
updateChart,
|
||||||
deleteChart,
|
deleteChart,
|
||||||
|
@ -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,
|
||||||
|
};
|
||||||
|
};
|
@ -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,
|
||||||
|
};
|
||||||
|
};
|
Loading…
Reference in New Issue
Block a user