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 { Box, styled, Typography } from '@mui/material';
|
||||
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 { ChartConfigModal } from '../../../impact-metrics/ChartConfigModal/ChartConfigModal.tsx';
|
||||
import { useImpactMetricsApi } from 'hooks/api/actions/useImpactMetricsApi/useImpactMetricsApi.ts';
|
||||
@ -49,10 +49,10 @@ export const FeatureImpactMetrics: FC = () => {
|
||||
const { setToastApiError } = useToast();
|
||||
|
||||
const {
|
||||
metadata,
|
||||
metricSeries,
|
||||
loading: metadataLoading,
|
||||
error: metadataError,
|
||||
} = useImpactMetricsMetadata();
|
||||
} = useImpactMetricsNames();
|
||||
|
||||
const handleAddChart = () => {
|
||||
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 editingChart =
|
||||
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 { useMilestoneProgressionForm } from '../hooks/useMilestoneProgressionForm.js';
|
||||
import { MilestoneProgressionTimeInput } from './MilestoneProgressionTimeInput.tsx';
|
||||
import type { ChangeMilestoneProgressionSchema } from 'openapi';
|
||||
import type { MilestoneStatus } from '../ReleasePlanMilestone/ReleasePlanMilestoneStatus.tsx';
|
||||
import { useMilestoneProgressionInfo } from '../hooks/useMilestoneProgressionInfo.ts';
|
||||
import {
|
||||
StyledFormContainer,
|
||||
StyledTopRow,
|
||||
StyledLabel,
|
||||
StyledButtonGroup,
|
||||
StyledErrorMessage,
|
||||
StyledInfoLine,
|
||||
createStyledIcon,
|
||||
} from '../shared/SharedFormComponents.tsx';
|
||||
|
||||
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',
|
||||
}));
|
||||
|
||||
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',
|
||||
}));
|
||||
const StyledIcon = createStyledIcon(BoltIcon);
|
||||
|
||||
interface IMilestoneProgressionFormProps {
|
||||
sourceMilestoneId: string;
|
||||
|
||||
@ -31,6 +31,7 @@ import { ReleasePlanMilestoneItem } from './ReleasePlanMilestoneItem/ReleasePlan
|
||||
import Add from '@mui/icons-material/Add';
|
||||
|
||||
import { StyledActionButton } from './ReleasePlanMilestoneItem/StyledActionButton.tsx';
|
||||
import { SafeguardForm } from './SafeguardForm/SafeguardForm.tsx';
|
||||
|
||||
const StyledContainer = styled('div')(({ theme }) => ({
|
||||
padding: theme.spacing(2),
|
||||
@ -194,6 +195,7 @@ export const ReleasePlan = ({
|
||||
const [milestoneToDeleteProgression, setMilestoneToDeleteProgression] =
|
||||
useState<IReleasePlanMilestone | null>(null);
|
||||
const [isDeletingProgression, setIsDeletingProgression] = useState(false);
|
||||
const [safeguardFormOpen, setSafeguardFormOpen] = useState(false);
|
||||
|
||||
const onChangeRequestConfirm = async () => {
|
||||
if (!changeRequestAction) return;
|
||||
@ -375,6 +377,21 @@ export const ReleasePlan = ({
|
||||
(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 (
|
||||
<StyledContainer>
|
||||
<StyledHeader>
|
||||
@ -406,13 +423,20 @@ export const ReleasePlan = ({
|
||||
<StyledBody safeguards={safeguardsEnabled}>
|
||||
{safeguardsEnabled ? (
|
||||
<StyledAddSafeguard>
|
||||
<StyledActionButton
|
||||
onClick={() => {}}
|
||||
color='primary'
|
||||
startIcon={<Add />}
|
||||
>
|
||||
Add safeguard
|
||||
</StyledActionButton>
|
||||
{safeguardFormOpen ? (
|
||||
<SafeguardForm
|
||||
onSubmit={handleSafeguardSubmit}
|
||||
onCancel={() => setSafeguardFormOpen(false)}
|
||||
/>
|
||||
) : (
|
||||
<StyledActionButton
|
||||
onClick={() => setSafeguardFormOpen(true)}
|
||||
color='primary'
|
||||
startIcon={<Add />}
|
||||
>
|
||||
Add safeguard
|
||||
</StyledActionButton>
|
||||
)}
|
||||
</StyledAddSafeguard>
|
||||
) : null}
|
||||
<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 { useMemo, useState, useCallback } from 'react';
|
||||
import { type FC, useMemo } from 'react';
|
||||
import { useState, useCallback } from 'react';
|
||||
import { Typography, Button, Paper, styled, Box } from '@mui/material';
|
||||
import Add from '@mui/icons-material/Add';
|
||||
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 { ChartItem } from './ChartItem.tsx';
|
||||
import { PlausibleChartItem } from './PlausibleChartItem.tsx';
|
||||
@ -54,20 +54,10 @@ export const ImpactMetrics: FC = () => {
|
||||
} = useImpactMetricsState();
|
||||
|
||||
const {
|
||||
metadata,
|
||||
metricSeries,
|
||||
loading: metadataLoading,
|
||||
error: metadataError,
|
||||
} = useImpactMetricsMetadata();
|
||||
|
||||
const metricSeries = useMemo(() => {
|
||||
if (!metadata?.series) {
|
||||
return [];
|
||||
}
|
||||
return Object.entries(metadata.series).map(([name, rest]) => ({
|
||||
name,
|
||||
...rest,
|
||||
}));
|
||||
}, [metadata]);
|
||||
} = useImpactMetricsNames();
|
||||
|
||||
const handleAddChart = () => {
|
||||
setEditingChart(undefined);
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import { useMemo } from 'react';
|
||||
import { fetcher, useApiGetter } from '../useApiGetter/useApiGetter.js';
|
||||
import { formatApiPath } from 'utils/formatPath';
|
||||
|
||||
@ -25,3 +26,23 @@ export const useImpactMetricsMetadata = () => {
|
||||
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