mirror of
https://github.com/Unleash/unleash.git
synced 2025-11-24 20:06:55 +01:00
feat: safeguard form draft (#10954)
This commit is contained in:
parent
142b5a5d95
commit
529726decf
@ -2,7 +2,7 @@ import { PageContent } from 'component/common/PageContent/PageContent.tsx';
|
|||||||
import { PageHeader } from '../../../common/PageHeader/PageHeader.tsx';
|
import { PageHeader } from '../../../common/PageHeader/PageHeader.tsx';
|
||||||
import { Box, styled, Typography } from '@mui/material';
|
import { Box, styled, Typography } from '@mui/material';
|
||||||
import Add from '@mui/icons-material/Add';
|
import Add from '@mui/icons-material/Add';
|
||||||
import { useImpactMetricsMetadata } from 'hooks/api/getters/useImpactMetricsMetadata/useImpactMetricsMetadata.ts';
|
import { useImpactMetricsNames } from 'hooks/api/getters/useImpactMetricsMetadata/useImpactMetricsMetadata.ts';
|
||||||
import { type FC, useMemo, useState } from 'react';
|
import { type FC, useMemo, useState } from 'react';
|
||||||
import { ChartConfigModal } from '../../../impact-metrics/ChartConfigModal/ChartConfigModal.tsx';
|
import { ChartConfigModal } from '../../../impact-metrics/ChartConfigModal/ChartConfigModal.tsx';
|
||||||
import { useImpactMetricsApi } from 'hooks/api/actions/useImpactMetricsApi/useImpactMetricsApi.ts';
|
import { useImpactMetricsApi } from 'hooks/api/actions/useImpactMetricsApi/useImpactMetricsApi.ts';
|
||||||
@ -49,10 +49,10 @@ export const FeatureImpactMetrics: FC = () => {
|
|||||||
const { setToastApiError } = useToast();
|
const { setToastApiError } = useToast();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
metadata,
|
metricSeries,
|
||||||
loading: metadataLoading,
|
loading: metadataLoading,
|
||||||
error: metadataError,
|
error: metadataError,
|
||||||
} = useImpactMetricsMetadata();
|
} = useImpactMetricsNames();
|
||||||
|
|
||||||
const handleAddChart = () => {
|
const handleAddChart = () => {
|
||||||
setModalState({ type: 'creating' });
|
setModalState({ type: 'creating' });
|
||||||
@ -94,16 +94,6 @@ export const FeatureImpactMetrics: FC = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const metricSeries = useMemo(() => {
|
|
||||||
if (!metadata?.series) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
return Object.entries(metadata.series).map(([name, rest]) => ({
|
|
||||||
name,
|
|
||||||
...rest,
|
|
||||||
}));
|
|
||||||
}, [metadata]);
|
|
||||||
|
|
||||||
const isModalOpen = modalState.type !== 'closed';
|
const isModalOpen = modalState.type !== 'closed';
|
||||||
const editingChart =
|
const editingChart =
|
||||||
modalState.type === 'editing' ? modalState.config : undefined;
|
modalState.type === 'editing' ? modalState.config : undefined;
|
||||||
|
|||||||
@ -1,66 +1,21 @@
|
|||||||
import { Button, styled } from '@mui/material';
|
import { Button } from '@mui/material';
|
||||||
import BoltIcon from '@mui/icons-material/Bolt';
|
import BoltIcon from '@mui/icons-material/Bolt';
|
||||||
import { useMilestoneProgressionForm } from '../hooks/useMilestoneProgressionForm.js';
|
import { useMilestoneProgressionForm } from '../hooks/useMilestoneProgressionForm.js';
|
||||||
import { MilestoneProgressionTimeInput } from './MilestoneProgressionTimeInput.tsx';
|
import { MilestoneProgressionTimeInput } from './MilestoneProgressionTimeInput.tsx';
|
||||||
import type { ChangeMilestoneProgressionSchema } from 'openapi';
|
import type { ChangeMilestoneProgressionSchema } from 'openapi';
|
||||||
import type { MilestoneStatus } from '../ReleasePlanMilestone/ReleasePlanMilestoneStatus.tsx';
|
import type { MilestoneStatus } from '../ReleasePlanMilestone/ReleasePlanMilestoneStatus.tsx';
|
||||||
import { useMilestoneProgressionInfo } from '../hooks/useMilestoneProgressionInfo.ts';
|
import { useMilestoneProgressionInfo } from '../hooks/useMilestoneProgressionInfo.ts';
|
||||||
|
import {
|
||||||
|
StyledFormContainer,
|
||||||
|
StyledTopRow,
|
||||||
|
StyledLabel,
|
||||||
|
StyledButtonGroup,
|
||||||
|
StyledErrorMessage,
|
||||||
|
StyledInfoLine,
|
||||||
|
createStyledIcon,
|
||||||
|
} from '../shared/SharedFormComponents.tsx';
|
||||||
|
|
||||||
const StyledFormContainer = styled('form')(({ theme }) => ({
|
const StyledIcon = createStyledIcon(BoltIcon);
|
||||||
display: 'flex',
|
|
||||||
flexDirection: 'column',
|
|
||||||
gap: theme.spacing(1.5),
|
|
||||||
padding: theme.spacing(1.5, 2),
|
|
||||||
backgroundColor: theme.palette.background.elevation1,
|
|
||||||
border: `1px solid ${theme.palette.divider}`,
|
|
||||||
width: '100%',
|
|
||||||
borderRadius: `${theme.shape.borderRadiusLarge}px`,
|
|
||||||
position: 'relative',
|
|
||||||
}));
|
|
||||||
|
|
||||||
const StyledTopRow = styled('div')(({ theme }) => ({
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
gap: theme.spacing(1),
|
|
||||||
}));
|
|
||||||
|
|
||||||
const StyledIcon = styled(BoltIcon)(({ theme }) => ({
|
|
||||||
color: theme.palette.common.white,
|
|
||||||
fontSize: 18,
|
|
||||||
flexShrink: 0,
|
|
||||||
backgroundColor: theme.palette.primary.main,
|
|
||||||
borderRadius: '50%',
|
|
||||||
padding: theme.spacing(0.25),
|
|
||||||
}));
|
|
||||||
|
|
||||||
const StyledLabel = styled('span')(({ theme }) => ({
|
|
||||||
color: theme.palette.text.primary,
|
|
||||||
fontSize: theme.typography.body2.fontSize,
|
|
||||||
flexShrink: 0,
|
|
||||||
}));
|
|
||||||
|
|
||||||
const StyledButtonGroup = styled('div')(({ theme }) => ({
|
|
||||||
display: 'flex',
|
|
||||||
gap: theme.spacing(1),
|
|
||||||
justifyContent: 'flex-end',
|
|
||||||
alignItems: 'center',
|
|
||||||
paddingTop: theme.spacing(1),
|
|
||||||
marginTop: theme.spacing(0.5),
|
|
||||||
borderTop: `1px solid ${theme.palette.divider}`,
|
|
||||||
}));
|
|
||||||
|
|
||||||
const StyledErrorMessage = styled('span')(({ theme }) => ({
|
|
||||||
color: theme.palette.error.main,
|
|
||||||
fontSize: theme.typography.body2.fontSize,
|
|
||||||
paddingLeft: theme.spacing(3.25),
|
|
||||||
}));
|
|
||||||
|
|
||||||
const StyledInfoLine = styled('span')(({ theme }) => ({
|
|
||||||
color: theme.palette.text.secondary,
|
|
||||||
fontSize: theme.typography.caption.fontSize,
|
|
||||||
paddingLeft: theme.spacing(3.25),
|
|
||||||
fontStyle: 'italic',
|
|
||||||
}));
|
|
||||||
|
|
||||||
interface IMilestoneProgressionFormProps {
|
interface IMilestoneProgressionFormProps {
|
||||||
sourceMilestoneId: string;
|
sourceMilestoneId: string;
|
||||||
|
|||||||
@ -31,6 +31,7 @@ import { ReleasePlanMilestoneItem } from './ReleasePlanMilestoneItem/ReleasePlan
|
|||||||
import Add from '@mui/icons-material/Add';
|
import Add from '@mui/icons-material/Add';
|
||||||
|
|
||||||
import { StyledActionButton } from './ReleasePlanMilestoneItem/StyledActionButton.tsx';
|
import { StyledActionButton } from './ReleasePlanMilestoneItem/StyledActionButton.tsx';
|
||||||
|
import { SafeguardForm } from './SafeguardForm/SafeguardForm.tsx';
|
||||||
|
|
||||||
const StyledContainer = styled('div')(({ theme }) => ({
|
const StyledContainer = styled('div')(({ theme }) => ({
|
||||||
padding: theme.spacing(2),
|
padding: theme.spacing(2),
|
||||||
@ -194,6 +195,7 @@ export const ReleasePlan = ({
|
|||||||
const [milestoneToDeleteProgression, setMilestoneToDeleteProgression] =
|
const [milestoneToDeleteProgression, setMilestoneToDeleteProgression] =
|
||||||
useState<IReleasePlanMilestone | null>(null);
|
useState<IReleasePlanMilestone | null>(null);
|
||||||
const [isDeletingProgression, setIsDeletingProgression] = useState(false);
|
const [isDeletingProgression, setIsDeletingProgression] = useState(false);
|
||||||
|
const [safeguardFormOpen, setSafeguardFormOpen] = useState(false);
|
||||||
|
|
||||||
const onChangeRequestConfirm = async () => {
|
const onChangeRequestConfirm = async () => {
|
||||||
if (!changeRequestAction) return;
|
if (!changeRequestAction) return;
|
||||||
@ -375,6 +377,21 @@ export const ReleasePlan = ({
|
|||||||
(milestone) => milestone.id === activeMilestoneId,
|
(milestone) => milestone.id === activeMilestoneId,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const handleSafeguardSubmit = (data: {
|
||||||
|
metric: string;
|
||||||
|
application: string;
|
||||||
|
aggregation: string;
|
||||||
|
comparison: string;
|
||||||
|
threshold: number;
|
||||||
|
}) => {
|
||||||
|
console.log('Safeguard data:', data);
|
||||||
|
setSafeguardFormOpen(false);
|
||||||
|
setToastData({
|
||||||
|
type: 'success',
|
||||||
|
text: 'Safeguard added successfully',
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledContainer>
|
<StyledContainer>
|
||||||
<StyledHeader>
|
<StyledHeader>
|
||||||
@ -406,13 +423,20 @@ export const ReleasePlan = ({
|
|||||||
<StyledBody safeguards={safeguardsEnabled}>
|
<StyledBody safeguards={safeguardsEnabled}>
|
||||||
{safeguardsEnabled ? (
|
{safeguardsEnabled ? (
|
||||||
<StyledAddSafeguard>
|
<StyledAddSafeguard>
|
||||||
<StyledActionButton
|
{safeguardFormOpen ? (
|
||||||
onClick={() => {}}
|
<SafeguardForm
|
||||||
color='primary'
|
onSubmit={handleSafeguardSubmit}
|
||||||
startIcon={<Add />}
|
onCancel={() => setSafeguardFormOpen(false)}
|
||||||
>
|
/>
|
||||||
Add safeguard
|
) : (
|
||||||
</StyledActionButton>
|
<StyledActionButton
|
||||||
|
onClick={() => setSafeguardFormOpen(true)}
|
||||||
|
color='primary'
|
||||||
|
startIcon={<Add />}
|
||||||
|
>
|
||||||
|
Add safeguard
|
||||||
|
</StyledActionButton>
|
||||||
|
)}
|
||||||
</StyledAddSafeguard>
|
</StyledAddSafeguard>
|
||||||
) : null}
|
) : null}
|
||||||
<StyledMilestones safeguards={safeguardsEnabled}>
|
<StyledMilestones safeguards={safeguardsEnabled}>
|
||||||
|
|||||||
@ -0,0 +1,148 @@
|
|||||||
|
import { Button } from '@mui/material';
|
||||||
|
import ShieldIcon from '@mui/icons-material/Shield';
|
||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
import type { FormEvent } from 'react';
|
||||||
|
import { useImpactMetricsNames } from 'hooks/api/getters/useImpactMetricsMetadata/useImpactMetricsMetadata';
|
||||||
|
import {
|
||||||
|
StyledFormContainer,
|
||||||
|
StyledTopRow,
|
||||||
|
StyledLabel,
|
||||||
|
StyledButtonGroup,
|
||||||
|
StyledSelect,
|
||||||
|
StyledMenuItem,
|
||||||
|
StyledTextField,
|
||||||
|
createStyledIcon,
|
||||||
|
} from '../shared/SharedFormComponents.tsx';
|
||||||
|
|
||||||
|
const StyledIcon = createStyledIcon(ShieldIcon);
|
||||||
|
|
||||||
|
interface ISafeguardFormProps {
|
||||||
|
onSubmit: (data: {
|
||||||
|
metric: string;
|
||||||
|
application: string;
|
||||||
|
aggregation: string;
|
||||||
|
comparison: string;
|
||||||
|
threshold: number;
|
||||||
|
}) => void;
|
||||||
|
onCancel: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SafeguardForm = ({ onSubmit, onCancel }: ISafeguardFormProps) => {
|
||||||
|
const { metricSeries, loading } = useImpactMetricsNames();
|
||||||
|
|
||||||
|
// Hardcoded values for now
|
||||||
|
const aggregationModes = ['rps', 'count'];
|
||||||
|
const applicationNames = ['web', 'mobile', 'api', 'backend'];
|
||||||
|
|
||||||
|
const [selectedMetric, setSelectedMetric] = useState('');
|
||||||
|
const [application, setApplication] = useState('web');
|
||||||
|
const [aggregationMode, setAggregationMode] = useState('rps');
|
||||||
|
const [operator, setOperator] = useState('>');
|
||||||
|
const [threshold, setThreshold] = useState(0);
|
||||||
|
|
||||||
|
// Set initial metric when data loads
|
||||||
|
useEffect(() => {
|
||||||
|
if (metricSeries.length > 0 && !selectedMetric) {
|
||||||
|
setSelectedMetric(metricSeries[0].name);
|
||||||
|
}
|
||||||
|
}, [metricSeries, selectedMetric]);
|
||||||
|
|
||||||
|
const handleSubmit = (e: FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (threshold && !Number.isNaN(Number(threshold))) {
|
||||||
|
const metric = metricSeries.find((m) => m.name === selectedMetric);
|
||||||
|
onSubmit({
|
||||||
|
metric: metric?.displayName || selectedMetric,
|
||||||
|
application,
|
||||||
|
aggregation: aggregationMode,
|
||||||
|
comparison: operator,
|
||||||
|
threshold: Number(threshold),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<StyledFormContainer onSubmit={handleSubmit}>
|
||||||
|
<StyledTopRow>
|
||||||
|
<StyledIcon />
|
||||||
|
<StyledSelect
|
||||||
|
value={selectedMetric}
|
||||||
|
onChange={(e) =>
|
||||||
|
setSelectedMetric(e.target.value as string)
|
||||||
|
}
|
||||||
|
variant='outlined'
|
||||||
|
size='small'
|
||||||
|
>
|
||||||
|
{metricSeries.map((metric) => (
|
||||||
|
<StyledMenuItem key={metric.name} value={metric.name}>
|
||||||
|
{metric.displayName}
|
||||||
|
</StyledMenuItem>
|
||||||
|
))}
|
||||||
|
</StyledSelect>
|
||||||
|
|
||||||
|
<StyledLabel>filtered by</StyledLabel>
|
||||||
|
<StyledSelect
|
||||||
|
value={application}
|
||||||
|
onChange={(e) => setApplication(String(e.target.value))}
|
||||||
|
variant='outlined'
|
||||||
|
size='small'
|
||||||
|
>
|
||||||
|
{applicationNames.map((app) => (
|
||||||
|
<StyledMenuItem key={app} value={app}>
|
||||||
|
{app}
|
||||||
|
</StyledMenuItem>
|
||||||
|
))}
|
||||||
|
</StyledSelect>
|
||||||
|
|
||||||
|
<StyledLabel>aggregated by</StyledLabel>
|
||||||
|
<StyledSelect
|
||||||
|
value={aggregationMode}
|
||||||
|
onChange={(e) => setAggregationMode(String(e.target.value))}
|
||||||
|
variant='outlined'
|
||||||
|
size='small'
|
||||||
|
>
|
||||||
|
{aggregationModes.map((mode) => (
|
||||||
|
<StyledMenuItem key={mode} value={mode}>
|
||||||
|
{mode}
|
||||||
|
</StyledMenuItem>
|
||||||
|
))}
|
||||||
|
</StyledSelect>
|
||||||
|
|
||||||
|
<StyledLabel>is</StyledLabel>
|
||||||
|
<StyledSelect
|
||||||
|
value={operator}
|
||||||
|
onChange={(e) => setOperator(String(e.target.value))}
|
||||||
|
variant='outlined'
|
||||||
|
size='small'
|
||||||
|
>
|
||||||
|
<StyledMenuItem value='>'>More than</StyledMenuItem>
|
||||||
|
<StyledMenuItem value='<'>Less than</StyledMenuItem>
|
||||||
|
</StyledSelect>
|
||||||
|
|
||||||
|
<StyledTextField
|
||||||
|
type='number'
|
||||||
|
value={threshold}
|
||||||
|
onChange={(e) => setThreshold(Number(e.target.value))}
|
||||||
|
placeholder='Value'
|
||||||
|
variant='outlined'
|
||||||
|
size='small'
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</StyledTopRow>
|
||||||
|
<StyledButtonGroup>
|
||||||
|
<Button variant='outlined' onClick={onCancel} size='small'>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant='contained'
|
||||||
|
color='primary'
|
||||||
|
size='small'
|
||||||
|
type='submit'
|
||||||
|
disabled={!threshold || Number.isNaN(Number(threshold))}
|
||||||
|
>
|
||||||
|
Save
|
||||||
|
</Button>
|
||||||
|
</StyledButtonGroup>
|
||||||
|
</StyledFormContainer>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -0,0 +1,82 @@
|
|||||||
|
import { styled, Select, MenuItem, TextField } from '@mui/material';
|
||||||
|
|
||||||
|
export const StyledFormContainer = styled('form')(({ theme }) => ({
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
gap: theme.spacing(1.5),
|
||||||
|
padding: theme.spacing(1.5, 2),
|
||||||
|
backgroundColor: theme.palette.background.elevation1,
|
||||||
|
border: `1px solid ${theme.palette.divider}`,
|
||||||
|
width: '100%',
|
||||||
|
borderRadius: `${theme.shape.borderRadiusLarge}px`,
|
||||||
|
position: 'relative',
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const StyledTopRow = styled('div')(({ theme }) => ({
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: theme.spacing(1),
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const StyledLabel = styled('span')(({ theme }) => ({
|
||||||
|
color: theme.palette.text.primary,
|
||||||
|
fontSize: theme.typography.body2.fontSize,
|
||||||
|
flexShrink: 0,
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const StyledButtonGroup = styled('div')(({ theme }) => ({
|
||||||
|
display: 'flex',
|
||||||
|
gap: theme.spacing(1),
|
||||||
|
justifyContent: 'flex-end',
|
||||||
|
alignItems: 'center',
|
||||||
|
paddingTop: theme.spacing(1),
|
||||||
|
marginTop: theme.spacing(0.5),
|
||||||
|
borderTop: `1px solid ${theme.palette.divider}`,
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const StyledSelect = styled(Select)(({ theme }) => ({
|
||||||
|
minWidth: 120,
|
||||||
|
maxWidth: 120,
|
||||||
|
'& .MuiSelect-select': {
|
||||||
|
fontSize: theme.typography.body2.fontSize,
|
||||||
|
padding: theme.spacing(0.5, 1),
|
||||||
|
overflow: 'hidden',
|
||||||
|
textOverflow: 'ellipsis',
|
||||||
|
whiteSpace: 'nowrap',
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const StyledMenuItem = styled(MenuItem)(({ theme }) => ({
|
||||||
|
fontSize: theme.typography.body2.fontSize,
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const StyledTextField = styled(TextField)(({ theme }) => ({
|
||||||
|
width: 80,
|
||||||
|
'& .MuiInputBase-input': {
|
||||||
|
fontSize: theme.typography.body2.fontSize,
|
||||||
|
padding: theme.spacing(0.5, 1),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const createStyledIcon = (IconComponent: React.ComponentType<any>) =>
|
||||||
|
styled(IconComponent)(({ theme }) => ({
|
||||||
|
color: theme.palette.common.white,
|
||||||
|
fontSize: 18,
|
||||||
|
flexShrink: 0,
|
||||||
|
backgroundColor: theme.palette.primary.main,
|
||||||
|
borderRadius: '50%',
|
||||||
|
padding: theme.spacing(0.25),
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const StyledErrorMessage = styled('span')(({ theme }) => ({
|
||||||
|
color: theme.palette.error.main,
|
||||||
|
fontSize: theme.typography.body2.fontSize,
|
||||||
|
paddingLeft: theme.spacing(3.25),
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const StyledInfoLine = styled('span')(({ theme }) => ({
|
||||||
|
color: theme.palette.text.secondary,
|
||||||
|
fontSize: theme.typography.caption.fontSize,
|
||||||
|
paddingLeft: theme.spacing(3.25),
|
||||||
|
fontStyle: 'italic',
|
||||||
|
}));
|
||||||
@ -1,9 +1,9 @@
|
|||||||
import type { FC } from 'react';
|
import { type FC, useMemo } from 'react';
|
||||||
import { useMemo, useState, useCallback } from 'react';
|
import { 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 { PageHeader } from 'component/common/PageHeader/PageHeader.tsx';
|
import { PageHeader } from 'component/common/PageHeader/PageHeader.tsx';
|
||||||
import { useImpactMetricsMetadata } from 'hooks/api/getters/useImpactMetricsMetadata/useImpactMetricsMetadata';
|
import { useImpactMetricsNames } 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 { PlausibleChartItem } from './PlausibleChartItem.tsx';
|
import { PlausibleChartItem } from './PlausibleChartItem.tsx';
|
||||||
@ -54,20 +54,10 @@ export const ImpactMetrics: FC = () => {
|
|||||||
} = useImpactMetricsState();
|
} = useImpactMetricsState();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
metadata,
|
metricSeries,
|
||||||
loading: metadataLoading,
|
loading: metadataLoading,
|
||||||
error: metadataError,
|
error: metadataError,
|
||||||
} = useImpactMetricsMetadata();
|
} = useImpactMetricsNames();
|
||||||
|
|
||||||
const metricSeries = useMemo(() => {
|
|
||||||
if (!metadata?.series) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
return Object.entries(metadata.series).map(([name, rest]) => ({
|
|
||||||
name,
|
|
||||||
...rest,
|
|
||||||
}));
|
|
||||||
}, [metadata]);
|
|
||||||
|
|
||||||
const handleAddChart = () => {
|
const handleAddChart = () => {
|
||||||
setEditingChart(undefined);
|
setEditingChart(undefined);
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
import { useMemo } from 'react';
|
||||||
import { fetcher, useApiGetter } from '../useApiGetter/useApiGetter.js';
|
import { fetcher, useApiGetter } from '../useApiGetter/useApiGetter.js';
|
||||||
import { formatApiPath } from 'utils/formatPath';
|
import { formatApiPath } from 'utils/formatPath';
|
||||||
|
|
||||||
@ -25,3 +26,23 @@ export const useImpactMetricsMetadata = () => {
|
|||||||
error,
|
error,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const useImpactMetricsNames = () => {
|
||||||
|
const { metadata, loading, error } = useImpactMetricsMetadata();
|
||||||
|
|
||||||
|
const metricSeries = useMemo(() => {
|
||||||
|
if (!metadata?.series) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return Object.entries(metadata.series).map(([name, rest]) => ({
|
||||||
|
name,
|
||||||
|
...rest,
|
||||||
|
}));
|
||||||
|
}, [metadata]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
metricSeries,
|
||||||
|
loading,
|
||||||
|
error,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user