mirror of
https://github.com/Unleash/unleash.git
synced 2025-11-24 20:06:55 +01:00
feat: safeguards form edit and display (#10967)
This commit is contained in:
parent
89a3578826
commit
3b07b66712
@ -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 { useImpactMetricsNames } from 'hooks/api/getters/useImpactMetricsMetadata/useImpactMetricsMetadata.ts';
|
import { useImpactMetricsOptions } 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 {
|
||||||
metricSeries,
|
metricOptions,
|
||||||
loading: metadataLoading,
|
loading: metadataLoading,
|
||||||
error: metadataError,
|
error: metadataError,
|
||||||
} = useImpactMetricsNames();
|
} = useImpactMetricsOptions();
|
||||||
|
|
||||||
const handleAddChart = () => {
|
const handleAddChart = () => {
|
||||||
setModalState({ type: 'creating' });
|
setModalState({ type: 'creating' });
|
||||||
@ -151,7 +151,7 @@ export const FeatureImpactMetrics: FC = () => {
|
|||||||
onClose={handleCloseModal}
|
onClose={handleCloseModal}
|
||||||
onSave={handleSaveChart}
|
onSave={handleSaveChart}
|
||||||
initialConfig={editingChart}
|
initialConfig={editingChart}
|
||||||
metricSeries={metricSeries}
|
metricSeries={metricOptions}
|
||||||
loading={metadataLoading}
|
loading={metadataLoading}
|
||||||
/>
|
/>
|
||||||
</PageContent>
|
</PageContent>
|
||||||
|
|||||||
@ -120,6 +120,7 @@ export const ReleasePlan = ({
|
|||||||
featureName,
|
featureName,
|
||||||
environment,
|
environment,
|
||||||
milestones,
|
milestones,
|
||||||
|
safeguards,
|
||||||
} = plan;
|
} = plan;
|
||||||
|
|
||||||
const projectId = useRequiredPathParam('projectId');
|
const projectId = useRequiredPathParam('projectId');
|
||||||
@ -389,7 +390,6 @@ export const ReleasePlan = ({
|
|||||||
planId: id,
|
planId: id,
|
||||||
body: data,
|
body: data,
|
||||||
});
|
});
|
||||||
setSafeguardFormOpen(false);
|
|
||||||
setToastData({
|
setToastData({
|
||||||
type: 'success',
|
type: 'success',
|
||||||
text: 'Safeguard added successfully',
|
text: 'Safeguard added successfully',
|
||||||
@ -431,11 +431,20 @@ export const ReleasePlan = ({
|
|||||||
<StyledBody safeguards={safeguardsEnabled}>
|
<StyledBody safeguards={safeguardsEnabled}>
|
||||||
{safeguardsEnabled ? (
|
{safeguardsEnabled ? (
|
||||||
<StyledAddSafeguard>
|
<StyledAddSafeguard>
|
||||||
{safeguardFormOpen ? (
|
{safeguards.length > 0 ? (
|
||||||
<SafeguardForm
|
<SafeguardForm
|
||||||
|
safeguard={safeguards[0]}
|
||||||
onSubmit={handleSafeguardSubmit}
|
onSubmit={handleSafeguardSubmit}
|
||||||
onCancel={() => setSafeguardFormOpen(false)}
|
onCancel={() => setSafeguardFormOpen(false)}
|
||||||
/>
|
/>
|
||||||
|
) : safeguardFormOpen ? (
|
||||||
|
<SafeguardForm
|
||||||
|
onSubmit={(data) => {
|
||||||
|
handleSafeguardSubmit(data);
|
||||||
|
setSafeguardFormOpen(false);
|
||||||
|
}}
|
||||||
|
onCancel={() => setSafeguardFormOpen(false)}
|
||||||
|
/>
|
||||||
) : (
|
) : (
|
||||||
<StyledActionButton
|
<StyledActionButton
|
||||||
onClick={() => setSafeguardFormOpen(true)}
|
onClick={() => setSafeguardFormOpen(true)}
|
||||||
|
|||||||
@ -2,11 +2,11 @@ import { Button } from '@mui/material';
|
|||||||
import ShieldIcon from '@mui/icons-material/Shield';
|
import ShieldIcon from '@mui/icons-material/Shield';
|
||||||
import type { FormEvent } from 'react';
|
import type { FormEvent } from 'react';
|
||||||
import { useEffect, useMemo, useState } from 'react';
|
import { useEffect, useMemo, useState } from 'react';
|
||||||
import { useImpactMetricsNames } from 'hooks/api/getters/useImpactMetricsMetadata/useImpactMetricsMetadata';
|
import { useImpactMetricsOptions } from 'hooks/api/getters/useImpactMetricsMetadata/useImpactMetricsMetadata';
|
||||||
import { useImpactMetricsData } from 'hooks/api/getters/useImpactMetricsData/useImpactMetricsData';
|
import { useImpactMetricsData } from 'hooks/api/getters/useImpactMetricsData/useImpactMetricsData';
|
||||||
import { RangeSelector } from 'component/impact-metrics/ChartConfigModal/ImpactMetricsControls/RangeSelector/RangeSelector';
|
import { RangeSelector } from 'component/impact-metrics/ChartConfigModal/ImpactMetricsControls/RangeSelector/RangeSelector';
|
||||||
import { ModeSelector } from 'component/impact-metrics/ChartConfigModal/ImpactMetricsControls/ModeSelector/ModeSelector';
|
import { ModeSelector } from 'component/impact-metrics/ChartConfigModal/ImpactMetricsControls/ModeSelector/ModeSelector';
|
||||||
import { SeriesSelector } from 'component/impact-metrics/ChartConfigModal/ImpactMetricsControls/SeriesSelector/SeriesSelector';
|
import { MetricSelector } from 'component/impact-metrics/ChartConfigModal/ImpactMetricsControls/SeriesSelector/MetricSelector.tsx';
|
||||||
import type { CreateSafeguardSchema } from 'openapi/models/createSafeguardSchema';
|
import type { CreateSafeguardSchema } from 'openapi/models/createSafeguardSchema';
|
||||||
import type { MetricQuerySchemaTimeRange } from 'openapi/models/metricQuerySchemaTimeRange';
|
import type { MetricQuerySchemaTimeRange } from 'openapi/models/metricQuerySchemaTimeRange';
|
||||||
import type { MetricQuerySchemaAggregationMode } from 'openapi/models/metricQuerySchemaAggregationMode';
|
import type { MetricQuerySchemaAggregationMode } from 'openapi/models/metricQuerySchemaAggregationMode';
|
||||||
@ -21,31 +21,80 @@ import {
|
|||||||
StyledTextField,
|
StyledTextField,
|
||||||
StyledTopRow,
|
StyledTopRow,
|
||||||
} from '../shared/SharedFormComponents.tsx';
|
} from '../shared/SharedFormComponents.tsx';
|
||||||
|
import type { ISafeguard } from 'interfaces/releasePlans.ts';
|
||||||
|
|
||||||
const StyledIcon = createStyledIcon(ShieldIcon);
|
const StyledIcon = createStyledIcon(ShieldIcon);
|
||||||
|
|
||||||
interface ISafeguardFormProps {
|
interface ISafeguardFormProps {
|
||||||
onSubmit: (data: CreateSafeguardSchema) => void;
|
onSubmit: (data: CreateSafeguardSchema) => void;
|
||||||
onCancel: () => void;
|
onCancel: () => void;
|
||||||
|
safeguard?: ISafeguard;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const SafeguardForm = ({ onSubmit, onCancel }: ISafeguardFormProps) => {
|
type FormMode = 'create' | 'edit' | 'display';
|
||||||
const { metricSeries, loading } = useImpactMetricsNames();
|
|
||||||
|
|
||||||
const [selectedMetric, setSelectedMetric] = useState('');
|
const getInitialValues = (safeguard?: ISafeguard) => ({
|
||||||
const [application, setApplication] = useState('*');
|
metricName: safeguard?.impactMetric.metricName || '',
|
||||||
|
appName: safeguard?.impactMetric.labelSelectors.appName[0] || '*',
|
||||||
|
aggregationMode: (safeguard?.impactMetric.aggregationMode ||
|
||||||
|
'rps') as MetricQuerySchemaAggregationMode,
|
||||||
|
operator: (safeguard?.triggerCondition.operator ||
|
||||||
|
'>') as CreateSafeguardSchemaOperator,
|
||||||
|
threshold: safeguard?.triggerCondition?.threshold || 0,
|
||||||
|
timeRange: (safeguard?.impactMetric.timeRange ||
|
||||||
|
'day') as MetricQuerySchemaTimeRange,
|
||||||
|
});
|
||||||
|
|
||||||
|
const getDefaultAggregationMode = (
|
||||||
|
metricType: string,
|
||||||
|
fallback: MetricQuerySchemaAggregationMode = 'rps',
|
||||||
|
): MetricQuerySchemaAggregationMode => {
|
||||||
|
switch (metricType) {
|
||||||
|
case 'counter':
|
||||||
|
return 'count';
|
||||||
|
case 'gauge':
|
||||||
|
return 'avg';
|
||||||
|
case 'histogram':
|
||||||
|
return 'p50';
|
||||||
|
default:
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SafeguardForm = ({
|
||||||
|
onSubmit,
|
||||||
|
onCancel,
|
||||||
|
safeguard,
|
||||||
|
}: ISafeguardFormProps) => {
|
||||||
|
const { metricOptions, loading } = useImpactMetricsOptions();
|
||||||
|
|
||||||
|
const initialValues = useMemo(
|
||||||
|
() => getInitialValues(safeguard),
|
||||||
|
[safeguard],
|
||||||
|
);
|
||||||
|
|
||||||
|
const [metricName, setMetricName] = useState(initialValues.metricName);
|
||||||
|
const [appName, setAppName] = useState(initialValues.appName);
|
||||||
const [aggregationMode, setAggregationMode] =
|
const [aggregationMode, setAggregationMode] =
|
||||||
useState<MetricQuerySchemaAggregationMode>('rps');
|
useState<MetricQuerySchemaAggregationMode>(
|
||||||
const [operator, setOperator] =
|
initialValues.aggregationMode,
|
||||||
useState<CreateSafeguardSchemaOperator>('>');
|
);
|
||||||
const [threshold, setThreshold] = useState(0);
|
const [operator, setOperator] = useState<CreateSafeguardSchemaOperator>(
|
||||||
const [timeRange, setTimeRange] =
|
initialValues.operator,
|
||||||
useState<MetricQuerySchemaTimeRange>('day');
|
);
|
||||||
|
const [threshold, setThreshold] = useState(initialValues.threshold);
|
||||||
|
const [timeRange, setTimeRange] = useState<MetricQuerySchemaTimeRange>(
|
||||||
|
initialValues.timeRange,
|
||||||
|
);
|
||||||
|
|
||||||
|
const [mode, setMode] = useState<FormMode>(
|
||||||
|
safeguard ? 'display' : 'create',
|
||||||
|
);
|
||||||
|
|
||||||
const { data: metricsData } = useImpactMetricsData(
|
const { data: metricsData } = useImpactMetricsData(
|
||||||
selectedMetric
|
metricName
|
||||||
? {
|
? {
|
||||||
series: selectedMetric,
|
series: metricName,
|
||||||
range: timeRange,
|
range: timeRange,
|
||||||
aggregationMode: aggregationMode,
|
aggregationMode: aggregationMode,
|
||||||
}
|
}
|
||||||
@ -58,66 +107,125 @@ export const SafeguardForm = ({ onSubmit, onCancel }: ISafeguardFormProps) => {
|
|||||||
}, [metricsData?.labels?.appName]);
|
}, [metricsData?.labels?.appName]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (metricSeries.length > 0 && !selectedMetric) {
|
if (metricOptions.length > 0 && !metricName) {
|
||||||
setSelectedMetric(metricSeries[0].name);
|
setMetricName(metricOptions[0].name);
|
||||||
}
|
}
|
||||||
}, [metricSeries, selectedMetric]);
|
}, [metricOptions, metricName]);
|
||||||
|
|
||||||
const selectedMetricData = metricSeries.find(
|
const selectedMetricData = metricOptions.find((m) => m.name === metricName);
|
||||||
(m) => m.name === selectedMetric,
|
|
||||||
);
|
|
||||||
const metricType = selectedMetricData?.type || 'unknown';
|
const metricType = selectedMetricData?.type || 'unknown';
|
||||||
|
|
||||||
const handleMetricChange = (metricName: string) => {
|
const enterEditMode = () => {
|
||||||
setSelectedMetric(metricName);
|
if (mode === 'display') {
|
||||||
setApplication('*');
|
setMode('edit');
|
||||||
|
|
||||||
const metric = metricSeries.find((m) => m.name === metricName);
|
|
||||||
const type = metric?.type || 'unknown';
|
|
||||||
|
|
||||||
if (type === 'counter') {
|
|
||||||
setAggregationMode('count');
|
|
||||||
} else if (type === 'gauge') {
|
|
||||||
setAggregationMode('avg');
|
|
||||||
} else if (type === 'histogram') {
|
|
||||||
setAggregationMode('p50');
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleMetricChange = (value: string) => {
|
||||||
|
enterEditMode();
|
||||||
|
setMetricName(value);
|
||||||
|
setAppName('*');
|
||||||
|
|
||||||
|
const metric = metricOptions.find((m) => m.name === value);
|
||||||
|
if (metric?.type) {
|
||||||
|
setAggregationMode(
|
||||||
|
getDefaultAggregationMode(metric.type, aggregationMode),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleApplicationChange = (value: string) => {
|
||||||
|
enterEditMode();
|
||||||
|
setAppName(value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAggregationModeChange = (
|
||||||
|
value: MetricQuerySchemaAggregationMode,
|
||||||
|
) => {
|
||||||
|
enterEditMode();
|
||||||
|
setAggregationMode(value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleOperatorChange = (value: CreateSafeguardSchemaOperator) => {
|
||||||
|
enterEditMode();
|
||||||
|
setOperator(value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleThresholdChange = (value: number) => {
|
||||||
|
enterEditMode();
|
||||||
|
setThreshold(value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleTimeRangeChange = (value: MetricQuerySchemaTimeRange) => {
|
||||||
|
enterEditMode();
|
||||||
|
setTimeRange(value);
|
||||||
|
};
|
||||||
|
|
||||||
const handleSubmit = (e: FormEvent) => {
|
const handleSubmit = (e: FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (!Number.isNaN(Number(threshold))) {
|
|
||||||
const data: CreateSafeguardSchema = {
|
if (Number.isNaN(Number(threshold))) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
onSubmit({
|
||||||
impactMetric: {
|
impactMetric: {
|
||||||
metricName: selectedMetric,
|
metricName,
|
||||||
timeRange,
|
timeRange,
|
||||||
aggregationMode,
|
aggregationMode,
|
||||||
labelSelectors: {
|
labelSelectors: {
|
||||||
appName: [application],
|
appName: [appName],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
operator,
|
operator,
|
||||||
threshold: Number(threshold),
|
threshold: Number(threshold),
|
||||||
};
|
});
|
||||||
onSubmit(data);
|
|
||||||
|
if (mode === 'edit') {
|
||||||
|
setMode('display');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const resetToOriginalValues = () => {
|
||||||
|
if (!safeguard) return;
|
||||||
|
|
||||||
|
setMetricName(initialValues.metricName);
|
||||||
|
setAppName(initialValues.appName);
|
||||||
|
setAggregationMode(initialValues.aggregationMode);
|
||||||
|
setOperator(initialValues.operator);
|
||||||
|
setThreshold(initialValues.threshold);
|
||||||
|
setTimeRange(initialValues.timeRange);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCancel = () => {
|
||||||
|
if (mode === 'create') {
|
||||||
|
onCancel();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
resetToOriginalValues();
|
||||||
|
setMode('display');
|
||||||
|
};
|
||||||
|
|
||||||
|
const showButtons = mode === 'create' || mode === 'edit';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledFormContainer onSubmit={handleSubmit}>
|
<StyledFormContainer onSubmit={handleSubmit}>
|
||||||
<StyledTopRow>
|
<StyledTopRow>
|
||||||
<StyledIcon />
|
<StyledIcon />
|
||||||
<SeriesSelector
|
<MetricSelector
|
||||||
value={selectedMetric}
|
value={metricName}
|
||||||
onChange={handleMetricChange}
|
onChange={handleMetricChange}
|
||||||
options={metricSeries}
|
options={metricOptions}
|
||||||
loading={loading}
|
loading={loading}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<StyledLabel>filtered by</StyledLabel>
|
<StyledLabel>filtered by</StyledLabel>
|
||||||
<StyledSelect
|
<StyledSelect
|
||||||
value={application}
|
value={appName}
|
||||||
onChange={(e) => setApplication(String(e.target.value))}
|
onChange={(e) =>
|
||||||
|
handleApplicationChange(String(e.target.value))
|
||||||
|
}
|
||||||
variant='outlined'
|
variant='outlined'
|
||||||
size='small'
|
size='small'
|
||||||
>
|
>
|
||||||
@ -131,7 +239,7 @@ export const SafeguardForm = ({ onSubmit, onCancel }: ISafeguardFormProps) => {
|
|||||||
<StyledLabel>aggregated by</StyledLabel>
|
<StyledLabel>aggregated by</StyledLabel>
|
||||||
<ModeSelector
|
<ModeSelector
|
||||||
value={aggregationMode}
|
value={aggregationMode}
|
||||||
onChange={setAggregationMode}
|
onChange={handleAggregationModeChange}
|
||||||
metricType={metricType}
|
metricType={metricType}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@ -139,7 +247,7 @@ export const SafeguardForm = ({ onSubmit, onCancel }: ISafeguardFormProps) => {
|
|||||||
<StyledSelect
|
<StyledSelect
|
||||||
value={operator}
|
value={operator}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
setOperator(
|
handleOperatorChange(
|
||||||
e.target.value as CreateSafeguardSchemaOperator,
|
e.target.value as CreateSafeguardSchemaOperator,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -153,7 +261,9 @@ export const SafeguardForm = ({ onSubmit, onCancel }: ISafeguardFormProps) => {
|
|||||||
<StyledTextField
|
<StyledTextField
|
||||||
type='number'
|
type='number'
|
||||||
value={threshold}
|
value={threshold}
|
||||||
onChange={(e) => setThreshold(Number(e.target.value))}
|
onChange={(e) =>
|
||||||
|
handleThresholdChange(Number(e.target.value))
|
||||||
|
}
|
||||||
placeholder='Value'
|
placeholder='Value'
|
||||||
variant='outlined'
|
variant='outlined'
|
||||||
size='small'
|
size='small'
|
||||||
@ -161,10 +271,18 @@ export const SafeguardForm = ({ onSubmit, onCancel }: ISafeguardFormProps) => {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<StyledLabel>over</StyledLabel>
|
<StyledLabel>over</StyledLabel>
|
||||||
<RangeSelector value={timeRange} onChange={setTimeRange} />
|
<RangeSelector
|
||||||
|
value={timeRange}
|
||||||
|
onChange={handleTimeRangeChange}
|
||||||
|
/>
|
||||||
</StyledTopRow>
|
</StyledTopRow>
|
||||||
|
{showButtons && (
|
||||||
<StyledButtonGroup>
|
<StyledButtonGroup>
|
||||||
<Button variant='outlined' onClick={onCancel} size='small'>
|
<Button
|
||||||
|
variant='outlined'
|
||||||
|
onClick={handleCancel}
|
||||||
|
size='small'
|
||||||
|
>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
@ -177,6 +295,7 @@ export const SafeguardForm = ({ onSubmit, onCancel }: ISafeguardFormProps) => {
|
|||||||
Save
|
Save
|
||||||
</Button>
|
</Button>
|
||||||
</StyledButtonGroup>
|
</StyledButtonGroup>
|
||||||
|
)}
|
||||||
</StyledFormContainer>
|
</StyledFormContainer>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import type { FC } from 'react';
|
import type { FC } from 'react';
|
||||||
import { Box, Typography, FormControlLabel, Checkbox } from '@mui/material';
|
import { Box, Typography, FormControlLabel, Checkbox } from '@mui/material';
|
||||||
import type { ImpactMetricsSeries } from 'hooks/api/getters/useImpactMetricsMetadata/useImpactMetricsMetadata';
|
import type { ImpactMetricsSeries } from 'hooks/api/getters/useImpactMetricsMetadata/useImpactMetricsMetadata';
|
||||||
import { SeriesSelector } from './SeriesSelector/SeriesSelector.tsx';
|
import { MetricSelector } from './SeriesSelector/MetricSelector.tsx';
|
||||||
import { RangeSelector } from './RangeSelector/RangeSelector.tsx';
|
import { RangeSelector } from './RangeSelector/RangeSelector.tsx';
|
||||||
import { ModeSelector } from './ModeSelector/ModeSelector.tsx';
|
import { ModeSelector } from './ModeSelector/ModeSelector.tsx';
|
||||||
import type { ChartFormState } from '../../hooks/useChartFormState.ts';
|
import type { ChartFormState } from '../../hooks/useChartFormState.ts';
|
||||||
@ -42,7 +42,7 @@ export const ImpactMetricsControls: FC<ImpactMetricsControlsProps> = ({
|
|||||||
rates.
|
rates.
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|
||||||
<SeriesSelector
|
<MetricSelector
|
||||||
value={formData.metricName}
|
value={formData.metricName}
|
||||||
onChange={actions.handleSeriesChange}
|
onChange={actions.handleSeriesChange}
|
||||||
options={metricSeries}
|
options={metricSeries}
|
||||||
|
|||||||
@ -1,9 +1,8 @@
|
|||||||
import type { FC } from 'react';
|
import type { FC } from 'react';
|
||||||
import { Autocomplete, TextField, Typography, Box } from '@mui/material';
|
import { Autocomplete, TextField, Typography, Box } from '@mui/material';
|
||||||
import type { ImpactMetricsSeries } from 'hooks/api/getters/useImpactMetricsMetadata/useImpactMetricsMetadata';
|
|
||||||
import { Highlighter } from 'component/common/Highlighter/Highlighter';
|
import { Highlighter } from 'component/common/Highlighter/Highlighter';
|
||||||
|
|
||||||
type SeriesOption = ImpactMetricsSeries & { name: string; displayName: string };
|
type SeriesOption = { name: string; displayName: string; help: string };
|
||||||
|
|
||||||
export type SeriesSelectorProps = {
|
export type SeriesSelectorProps = {
|
||||||
value: string;
|
value: string;
|
||||||
@ -12,7 +11,7 @@ export type SeriesSelectorProps = {
|
|||||||
loading?: boolean;
|
loading?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const SeriesSelector: FC<SeriesSelectorProps> = ({
|
export const MetricSelector: FC<SeriesSelectorProps> = ({
|
||||||
value,
|
value,
|
||||||
onChange,
|
onChange,
|
||||||
options,
|
options,
|
||||||
@ -3,7 +3,7 @@ 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 { useImpactMetricsNames } from 'hooks/api/getters/useImpactMetricsMetadata/useImpactMetricsMetadata';
|
import { useImpactMetricsOptions } 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,10 +54,10 @@ export const ImpactMetrics: FC = () => {
|
|||||||
} = useImpactMetricsState();
|
} = useImpactMetricsState();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
metricSeries,
|
metricOptions,
|
||||||
loading: metadataLoading,
|
loading: metadataLoading,
|
||||||
error: metadataError,
|
error: metadataError,
|
||||||
} = useImpactMetricsNames();
|
} = useImpactMetricsOptions();
|
||||||
|
|
||||||
const handleAddChart = () => {
|
const handleAddChart = () => {
|
||||||
setEditingChart(undefined);
|
setEditingChart(undefined);
|
||||||
@ -193,7 +193,7 @@ export const ImpactMetrics: FC = () => {
|
|||||||
onClose={() => setModalOpen(false)}
|
onClose={() => setModalOpen(false)}
|
||||||
onSave={handleSaveChart}
|
onSave={handleSaveChart}
|
||||||
initialConfig={editingChart}
|
initialConfig={editingChart}
|
||||||
metricSeries={metricSeries}
|
metricSeries={metricOptions}
|
||||||
loading={metadataLoading || settingsLoading}
|
loading={metadataLoading || settingsLoading}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@ -27,10 +27,10 @@ export const useImpactMetricsMetadata = () => {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useImpactMetricsNames = () => {
|
export const useImpactMetricsOptions = () => {
|
||||||
const { metadata, loading, error } = useImpactMetricsMetadata();
|
const { metadata, loading, error } = useImpactMetricsMetadata();
|
||||||
|
|
||||||
const metricSeries = useMemo(() => {
|
const metricOptions = useMemo(() => {
|
||||||
if (!metadata?.series) {
|
if (!metadata?.series) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
@ -41,7 +41,7 @@ export const useImpactMetricsNames = () => {
|
|||||||
}, [metadata]);
|
}, [metadata]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
metricSeries,
|
metricOptions,
|
||||||
loading,
|
loading,
|
||||||
error,
|
error,
|
||||||
};
|
};
|
||||||
|
|||||||
@ -12,6 +12,7 @@ export const useReleasePlanPreview = (
|
|||||||
...template,
|
...template,
|
||||||
featureName,
|
featureName,
|
||||||
environment,
|
environment,
|
||||||
|
safeguards: [],
|
||||||
milestones: template.milestones.map((milestone) => ({
|
milestones: template.milestones.map((milestone) => ({
|
||||||
...milestone,
|
...milestone,
|
||||||
releasePlanDefinitionId: template.id,
|
releasePlanDefinitionId: template.id,
|
||||||
|
|||||||
@ -1,4 +1,7 @@
|
|||||||
import type { IFeatureStrategy } from './strategy.js';
|
import type { IFeatureStrategy } from './strategy.js';
|
||||||
|
import type { MetricQuerySchemaTimeRange } from 'openapi/models/metricQuerySchemaTimeRange';
|
||||||
|
import type { MetricQuerySchemaAggregationMode } from 'openapi/models/metricQuerySchemaAggregationMode';
|
||||||
|
import type { CreateSafeguardSchemaOperator } from 'openapi/models/createSafeguardSchemaOperator';
|
||||||
|
|
||||||
export interface IReleasePlanTemplate {
|
export interface IReleasePlanTemplate {
|
||||||
id: string;
|
id: string;
|
||||||
@ -18,6 +21,18 @@ export interface IReleasePlanTemplate {
|
|||||||
archivedAt?: string;
|
archivedAt?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ISafeguard {
|
||||||
|
impactMetric: {
|
||||||
|
aggregationMode: MetricQuerySchemaAggregationMode;
|
||||||
|
metricName: string;
|
||||||
|
timeRange: MetricQuerySchemaTimeRange;
|
||||||
|
labelSelectors: { appName: [string] };
|
||||||
|
};
|
||||||
|
triggerCondition: {
|
||||||
|
operator: CreateSafeguardSchemaOperator;
|
||||||
|
threshold: number;
|
||||||
|
};
|
||||||
|
}
|
||||||
export interface IReleasePlan {
|
export interface IReleasePlan {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
@ -28,6 +43,7 @@ export interface IReleasePlan {
|
|||||||
featureName: string;
|
featureName: string;
|
||||||
environment: string;
|
environment: string;
|
||||||
milestones: IReleasePlanMilestone[];
|
milestones: IReleasePlanMilestone[];
|
||||||
|
safeguards: ISafeguard[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IReleasePlanMilestone {
|
export interface IReleasePlanMilestone {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user