1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-11-24 20:06:55 +01:00

feat: safeguards api frontend (#10959)

This commit is contained in:
Mateusz Kwasniewski 2025-11-10 16:52:07 +01:00 committed by GitHub
parent 4479d0478e
commit 34a34364fb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
30 changed files with 327 additions and 55 deletions

View File

@ -32,6 +32,8 @@ import Add from '@mui/icons-material/Add';
import { StyledActionButton } from './ReleasePlanMilestoneItem/StyledActionButton.tsx';
import { SafeguardForm } from './SafeguardForm/SafeguardForm.tsx';
import { useSafeguardsApi } from 'hooks/api/actions/useSafeguardsApi/useSafeguardsApi';
import type { CreateSafeguardSchema } from 'openapi/models/createSafeguardSchema';
const StyledContainer = styled('div')(({ theme }) => ({
padding: theme.spacing(2),
@ -129,6 +131,7 @@ export const ReleasePlan = ({
const { removeReleasePlanFromFeature, startReleasePlanMilestone } =
useReleasePlansApi();
const { deleteMilestoneProgression } = useMilestoneProgressionsApi();
const { createOrUpdateSafeguard } = useSafeguardsApi();
const { setToastData, setToastApiError } = useToast();
const { trackEvent } = usePlausibleTracker();
@ -377,24 +380,24 @@ export const ReleasePlan = ({
(milestone) => milestone.id === activeMilestoneId,
);
const handleSafeguardSubmit = (data: {
impactMetric: {
metricName: string;
timeRange: string;
aggregationMode: string;
labelSelectors: {
appName: string[];
};
};
operator: string;
threshold: number;
}) => {
console.log('Safeguard data:', data);
setSafeguardFormOpen(false);
setToastData({
type: 'success',
text: 'Safeguard added successfully',
});
const handleSafeguardSubmit = async (data: CreateSafeguardSchema) => {
try {
await createOrUpdateSafeguard({
projectId,
featureName,
environment,
planId: id,
body: data,
});
setSafeguardFormOpen(false);
setToastData({
type: 'success',
text: 'Safeguard added successfully',
});
refetch();
} catch (error: unknown) {
setToastApiError(formatUnknownError(error));
}
};
return (

View File

@ -4,13 +4,13 @@ import type { FormEvent } from 'react';
import { useEffect, useMemo, useState } from 'react';
import { useImpactMetricsNames } from 'hooks/api/getters/useImpactMetricsMetadata/useImpactMetricsMetadata';
import { useImpactMetricsData } from 'hooks/api/getters/useImpactMetricsData/useImpactMetricsData';
import {
RangeSelector,
type TimeRange,
} 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 { SeriesSelector } from 'component/impact-metrics/ChartConfigModal/ImpactMetricsControls/SeriesSelector/SeriesSelector';
import type { AggregationMode } from 'component/impact-metrics/types';
import type { CreateSafeguardSchema } from 'openapi/models/createSafeguardSchema';
import type { MetricQuerySchemaTimeRange } from 'openapi/models/metricQuerySchemaTimeRange';
import type { MetricQuerySchemaAggregationMode } from 'openapi/models/metricQuerySchemaAggregationMode';
import type { CreateSafeguardSchemaOperator } from 'openapi/models/createSafeguardSchemaOperator';
import {
createStyledIcon,
StyledButtonGroup,
@ -25,18 +25,7 @@ import {
const StyledIcon = createStyledIcon(ShieldIcon);
interface ISafeguardFormProps {
onSubmit: (data: {
impactMetric: {
metricName: string;
timeRange: TimeRange;
aggregationMode: AggregationMode;
labelSelectors: {
appName: string[];
};
};
operator: string;
threshold: number;
}) => void;
onSubmit: (data: CreateSafeguardSchema) => void;
onCancel: () => void;
}
@ -46,10 +35,12 @@ export const SafeguardForm = ({ onSubmit, onCancel }: ISafeguardFormProps) => {
const [selectedMetric, setSelectedMetric] = useState('');
const [application, setApplication] = useState('*');
const [aggregationMode, setAggregationMode] =
useState<AggregationMode>('rps');
const [operator, setOperator] = useState('>');
useState<MetricQuerySchemaAggregationMode>('rps');
const [operator, setOperator] =
useState<CreateSafeguardSchemaOperator>('>');
const [threshold, setThreshold] = useState(0);
const [timeRange, setTimeRange] = useState<TimeRange>('day');
const [timeRange, setTimeRange] =
useState<MetricQuerySchemaTimeRange>('day');
const { data: metricsData } = useImpactMetricsData(
selectedMetric
@ -96,7 +87,7 @@ export const SafeguardForm = ({ onSubmit, onCancel }: ISafeguardFormProps) => {
const handleSubmit = (e: FormEvent) => {
e.preventDefault();
if (!Number.isNaN(Number(threshold))) {
onSubmit({
const data: CreateSafeguardSchema = {
impactMetric: {
metricName: selectedMetric,
timeRange,
@ -107,7 +98,8 @@ export const SafeguardForm = ({ onSubmit, onCancel }: ISafeguardFormProps) => {
},
operator,
threshold: Number(threshold),
});
};
onSubmit(data);
}
};
@ -146,7 +138,11 @@ export const SafeguardForm = ({ onSubmit, onCancel }: ISafeguardFormProps) => {
<StyledLabel>is</StyledLabel>
<StyledSelect
value={operator}
onChange={(e) => setOperator(String(e.target.value))}
onChange={(e) =>
setOperator(
e.target.value as CreateSafeguardSchemaOperator,
)
}
variant='outlined'
size='small'
>

View File

@ -0,0 +1,43 @@
import useAPI from '../useApi/useApi.js';
import type { CreateSafeguardSchema } from 'openapi/models/createSafeguardSchema';
export interface CreateSafeguardParams {
projectId: string;
featureName: string;
environment: string;
planId: string;
body: CreateSafeguardSchema;
}
export const useSafeguardsApi = () => {
const { makeRequest, createRequest, errors, loading } = useAPI({
propagateErrors: true,
});
const createOrUpdateSafeguard = async ({
projectId,
featureName,
environment,
planId,
body,
}: CreateSafeguardParams): Promise<void> => {
const requestId = 'createOrUpdateSafeguard';
const path = `api/admin/projects/${projectId}/features/${featureName}/environments/${environment}/release_plans/${planId}/safeguards`;
const req = createRequest(
path,
{
method: 'PUT',
body: JSON.stringify(body),
},
requestId,
);
await makeRequest(req.caller, req.id);
};
return {
createOrUpdateSafeguard,
errors,
loading,
};
};

View File

@ -19,7 +19,7 @@ export interface ApplicationSchema {
icon?: string;
/** Which SDK and version the application reporting uses. Typically represented as `<identifier>:<version>` */
sdkVersion?: string;
/** Which [strategies](https://docs.getunleash.io/topics/the-anatomy-of-unleash#activation-strategies) the application has loaded. Useful when trying to figure out if your [custom strategy](https://docs.getunleash.io/reference/custom-activation-strategies) has been loaded in the SDK */
/** Which [strategies](https://docs.getunleash.io/topics/the-anatomy-of-unleash#activation-strategies) the application has loaded. Useful when trying to figure out if your [custom strategy](https://docs.getunleash.io/reference/activation-strategies#custom-strategies) has been loaded in the SDK */
strategies?: string[];
/** A link to reference the application reporting the metrics. Could for instance be a GitHub link to the repository of the application */
url?: string;

View File

@ -0,0 +1,14 @@
/**
* Generated by Orval
* Do not edit manually.
* See `gen:api` script in package.json
*/
export type ChangeSafeguard401 = {
/** The ID of the error instance */
id?: string;
/** A description of what went wrong. */
message?: string;
/** The name of the error kind */
name?: string;
};

View File

@ -0,0 +1,14 @@
/**
* Generated by Orval
* Do not edit manually.
* See `gen:api` script in package.json
*/
export type ChangeSafeguard403 = {
/** The ID of the error instance */
id?: string;
/** A description of what went wrong. */
message?: string;
/** The name of the error kind */
name?: string;
};

View File

@ -12,7 +12,7 @@ export interface CreateApplicationSchema {
color?: string;
/** An URL to an icon file to be used for the applications's entry in the application list */
icon?: string;
/** Which [strategies](https://docs.getunleash.io/topics/the-anatomy-of-unleash#activation-strategies) the application has loaded. Useful when trying to figure out if your [custom strategy](https://docs.getunleash.io/reference/custom-activation-strategies) has been loaded in the SDK */
/** Which [strategies](https://docs.getunleash.io/topics/the-anatomy-of-unleash#activation-strategies) the application has loaded. Useful when trying to figure out if your [custom strategy](https://docs.getunleash.io/reference/activation-strategies#custom-strategies) has been loaded in the SDK */
strategies?: string[];
/** A link to reference the application reporting the metrics. Could for instance be a GitHub link to the repository of the application */
url?: string;

View File

@ -18,7 +18,7 @@ export interface CreateImpactMetricsConfigSchema {
id?: string;
/** The selected labels and their values for filtering the metric data. */
labelSelectors: CreateImpactMetricsConfigSchemaLabelSelectors;
/** The Prometheus metric series to display. It includes both unleash prefix and metric type and display name */
/** The Prometheus metric series to query. It includes both unleash prefix and metric type and display name */
metricName: string;
/** The time range for the metric data. */
timeRange: CreateImpactMetricsConfigSchemaTimeRange;

View File

@ -0,0 +1,19 @@
/**
* Generated by Orval
* Do not edit manually.
* See `gen:api` script in package.json
*/
import type { MetricQuerySchema } from './metricQuerySchema.js';
import type { CreateSafeguardSchemaOperator } from './createSafeguardSchemaOperator.js';
/**
* Request body to create a safeguard with metric-based alert condition.
*/
export interface CreateSafeguardSchema {
/** Metric configuration that should be evaluated for the safeguard. */
impactMetric: MetricQuerySchema;
/** The comparison operator for the threshold check. */
operator: CreateSafeguardSchemaOperator;
/** The threshold value to compare against. */
threshold: number;
}

View File

@ -0,0 +1,17 @@
/**
* Generated by Orval
* Do not edit manually.
* See `gen:api` script in package.json
*/
/**
* The comparison operator for the threshold check.
*/
export type CreateSafeguardSchemaOperator =
(typeof CreateSafeguardSchemaOperator)[keyof typeof CreateSafeguardSchemaOperator];
// eslint-disable-next-line @typescript-eslint/no-redeclare
export const CreateSafeguardSchemaOperator = {
'>': '>',
'<': '<',
} as const;

View File

@ -6,7 +6,7 @@
import type { CreateStrategySchemaParametersItem } from './createStrategySchemaParametersItem.js';
/**
* The data required to create a strategy type. Refer to the docs on [custom strategy types](https://docs.getunleash.io/reference/custom-activation-strategies) for more information.
* The data required to create a strategy type. Refer to the docs on [custom strategy types](https://docs.getunleash.io/reference/activation-strategies#custom-strategies) for more information.
*/
export interface CreateStrategySchema {
/** Whether the strategy type is deprecated or not. Defaults to `false`. */

View File

@ -12,6 +12,6 @@ export type CreateStrategySchemaParametersItem = {
name: string;
/** Whether this parameter must be configured when using the strategy. Defaults to `false` */
required?: boolean;
/** The [type of the parameter](https://docs.getunleash.io/reference/custom-activation-strategies#parameter-types) */
/** The [type of the parameter](https://docs.getunleash.io/reference/activation-strategies#parameters) */
type: CreateStrategySchemaParametersItemType;
};

View File

@ -5,7 +5,7 @@
*/
/**
* The [type of the parameter](https://docs.getunleash.io/reference/custom-activation-strategies#parameter-types)
* The [type of the parameter](https://docs.getunleash.io/reference/activation-strategies#parameters)
*/
export type CreateStrategySchemaParametersItemType =
(typeof CreateStrategySchemaParametersItemType)[keyof typeof CreateStrategySchemaParametersItemType];

View File

@ -28,7 +28,7 @@ export interface EdgeInstanceDataSchema {
connectionConsumptionSinceLastReport?: ConnectionConsumptionSchema;
/** Which version (semver) of Edge is the Edge instance running. */
edgeVersion: string;
/** A marker that tells Unleash whether this Edge instance is self-hosted or hosted by Unleash. */
/** A marker that tells Unleash whether this Edge instance is self-hosted, enterprise self-hosted, or hosted by Unleash. */
hosting?: EdgeInstanceDataSchemaHosting;
/** The ID of the Edge process, typically a ULID. Newly generated for each restart of the instance. */
identifier: string;

View File

@ -5,7 +5,7 @@
*/
/**
* A marker that tells Unleash whether this Edge instance is self-hosted or hosted by Unleash.
* A marker that tells Unleash whether this Edge instance is self-hosted, enterprise self-hosted, or hosted by Unleash.
*/
export type EdgeInstanceDataSchemaHosting =
(typeof EdgeInstanceDataSchemaHosting)[keyof typeof EdgeInstanceDataSchemaHosting];
@ -14,4 +14,5 @@ export type EdgeInstanceDataSchemaHosting =
export const EdgeInstanceDataSchemaHosting = {
hosted: 'hosted',
'self-hosted': 'self-hosted',
'enterprise-self-hosted': 'enterprise-self-hosted',
} as const;

View File

@ -21,7 +21,7 @@ export interface ImpactMetricsConfigSchema {
id: string;
/** The selected labels and their values for filtering the metric data. */
labelSelectors: ImpactMetricsConfigSchemaLabelSelectors;
/** The Prometheus metric series to display. It includes both unleash prefix and metric type and display name */
/** The Prometheus metric series to query. It includes both unleash prefix and metric type and display name */
metricName: string;
/** The time range for the metric data. */
timeRange: ImpactMetricsConfigSchemaTimeRange;

View File

@ -349,6 +349,8 @@ export * from './changeRequestsSchemaItemOneOfFour.js';
export * from './changeRequestsSchemaItemOneOfFourCreatedBy.js';
export * from './changeRequestsSchemaItemOneOfFourState.js';
export * from './changeRequestsSchemaItemOneOfState.js';
export * from './changeSafeguard401.js';
export * from './changeSafeguard403.js';
export * from './changeUserPassword400.js';
export * from './changeUserPassword401.js';
export * from './changeUserPassword403.js';
@ -496,6 +498,8 @@ export * from './createRoleWithPermissionsSchemaAnyOfFourPermissionsItem.js';
export * from './createRoleWithPermissionsSchemaAnyOfFourType.js';
export * from './createRoleWithPermissionsSchemaAnyOfPermissionsItem.js';
export * from './createRoleWithPermissionsSchemaAnyOfType.js';
export * from './createSafeguardSchema.js';
export * from './createSafeguardSchemaOperator.js';
export * from './createSegment400.js';
export * from './createSegment401.js';
export * from './createSegment403.js';
@ -1039,6 +1043,10 @@ export * from './meteredRequestsSchemaApiDataItem.js';
export * from './meteredRequestsSchemaApiDataItemDataPointsItem.js';
export * from './meteredRequestsSchemaDateRange.js';
export * from './meteredRequestsSchemaGrouping.js';
export * from './metricQuerySchema.js';
export * from './metricQuerySchemaAggregationMode.js';
export * from './metricQuerySchemaLabelSelectors.js';
export * from './metricQuerySchemaTimeRange.js';
export * from './milestoneProgressionSchema.js';
export * from './nameSchema.js';
export * from './notificationsSchema.js';
@ -1206,6 +1214,8 @@ export * from './pushVariantsSchema.js';
export * from './reactivateStrategy401.js';
export * from './reactivateStrategy403.js';
export * from './reactivateStrategy404.js';
export * from './readyCheckSchema.js';
export * from './readyCheckSchemaHealth.js';
export * from './recordUiErrorSchema.js';
export * from './registerClientMetrics400.js';
export * from './registerFrontendClient400.js';
@ -1283,6 +1293,9 @@ export * from './roleWithPermissionsSchema.js';
export * from './roleWithVersionSchema.js';
export * from './rolesSchema.js';
export * from './rolesWithVersionSchema.js';
export * from './safeguardSchema.js';
export * from './safeguardSchemaTriggerCondition.js';
export * from './safeguardSchemaTriggerConditionOperator.js';
export * from './samlSettingsResponseSchema.js';
export * from './samlSettingsResponseSchemaDefaultRootRole.js';
export * from './samlSettingsSchema.js';

View File

@ -0,0 +1,22 @@
/**
* Generated by Orval
* Do not edit manually.
* See `gen:api` script in package.json
*/
import type { MetricQuerySchemaAggregationMode } from './metricQuerySchemaAggregationMode.js';
import type { MetricQuerySchemaLabelSelectors } from './metricQuerySchemaLabelSelectors.js';
import type { MetricQuerySchemaTimeRange } from './metricQuerySchemaTimeRange.js';
/**
* Common metric query configuration for selecting and filtering metric data.
*/
export interface MetricQuerySchema {
/** The aggregation mode for the metric data. */
aggregationMode: MetricQuerySchemaAggregationMode;
/** The selected labels and their values for filtering the metric data. */
labelSelectors: MetricQuerySchemaLabelSelectors;
/** The Prometheus metric series to query. It includes both unleash prefix and metric type and display name */
metricName: string;
/** The time range for the metric data. */
timeRange: MetricQuerySchemaTimeRange;
}

View File

@ -0,0 +1,22 @@
/**
* Generated by Orval
* Do not edit manually.
* See `gen:api` script in package.json
*/
/**
* The aggregation mode for the metric data.
*/
export type MetricQuerySchemaAggregationMode =
(typeof MetricQuerySchemaAggregationMode)[keyof typeof MetricQuerySchemaAggregationMode];
// eslint-disable-next-line @typescript-eslint/no-redeclare
export const MetricQuerySchemaAggregationMode = {
rps: 'rps',
count: 'count',
avg: 'avg',
sum: 'sum',
p95: 'p95',
p99: 'p99',
p50: 'p50',
} as const;

View File

@ -0,0 +1,10 @@
/**
* Generated by Orval
* Do not edit manually.
* See `gen:api` script in package.json
*/
/**
* The selected labels and their values for filtering the metric data.
*/
export type MetricQuerySchemaLabelSelectors = { [key: string]: string[] };

View File

@ -0,0 +1,19 @@
/**
* Generated by Orval
* Do not edit manually.
* See `gen:api` script in package.json
*/
/**
* The time range for the metric data.
*/
export type MetricQuerySchemaTimeRange =
(typeof MetricQuerySchemaTimeRange)[keyof typeof MetricQuerySchemaTimeRange];
// eslint-disable-next-line @typescript-eslint/no-redeclare
export const MetricQuerySchemaTimeRange = {
hour: 'hour',
day: 'day',
week: 'week',
month: 'month',
} as const;

View File

@ -9,8 +9,6 @@ import type { TransitionConditionSchema } from './transitionConditionSchema.js';
* A milestone progression configuration
*/
export interface MilestoneProgressionSchema {
/** The unique identifier for this progression */
id: string;
/** The ID of the source milestone */
sourceMilestone: string;
/** The ID of the target milestone */

View File

@ -10,7 +10,7 @@
export interface PermissionSchema {
/** The environment this permission applies to */
environment?: string;
/** [Project](https://docs.getunleash.io/reference/rbac#project-permissions) or [environment](https://docs.getunleash.io/reference/rbac#environment-permissions) permission name */
/** [Project](https://docs.getunleash.io/reference/rbac#project-level-permissions) or [environment](https://docs.getunleash.io/reference/rbac#environment-level-permissions) permission name */
permission: string;
/** The project this permission applies to */
project?: string;

View File

@ -0,0 +1,14 @@
/**
* Generated by Orval
* Do not edit manually.
* See `gen:api` script in package.json
*/
import type { ReadyCheckSchemaHealth } from './readyCheckSchemaHealth.js';
/**
* Used by service orchestrators to decide whether this Unleash instance should be considered ready to serve requests.
*/
export interface ReadyCheckSchema {
/** The readiness state this Unleash instance is in. GOOD if the server is up and running. If the server is unhealthy you will get an unsuccessful http response. */
health: ReadyCheckSchemaHealth;
}

View File

@ -0,0 +1,16 @@
/**
* Generated by Orval
* Do not edit manually.
* See `gen:api` script in package.json
*/
/**
* The readiness state this Unleash instance is in. GOOD if the server is up and running. If the server is unhealthy you will get an unsuccessful http response.
*/
export type ReadyCheckSchemaHealth =
(typeof ReadyCheckSchemaHealth)[keyof typeof ReadyCheckSchemaHealth];
// eslint-disable-next-line @typescript-eslint/no-redeclare
export const ReadyCheckSchemaHealth = {
GOOD: 'GOOD',
} as const;

View File

@ -0,0 +1,18 @@
/**
* Generated by Orval
* Do not edit manually.
* See `gen:api` script in package.json
*/
import type { SafeguardSchemaTriggerCondition } from './safeguardSchemaTriggerCondition.js';
/**
* A safeguard configuration for release plan automation
*/
export interface SafeguardSchema {
/** The unique ULID identifier for this safeguard */
id: string;
/** The impact metric id */
impactMetric: string;
/** The condition that triggers the safeguard action */
triggerCondition: SafeguardSchemaTriggerCondition;
}

View File

@ -0,0 +1,16 @@
/**
* Generated by Orval
* Do not edit manually.
* See `gen:api` script in package.json
*/
import type { SafeguardSchemaTriggerConditionOperator } from './safeguardSchemaTriggerConditionOperator.js';
/**
* The condition that triggers the safeguard action
*/
export type SafeguardSchemaTriggerCondition = {
/** The comparison operator for the threshold check. */
operator: SafeguardSchemaTriggerConditionOperator;
/** The threshold value to compare against. */
threshold: number;
};

View File

@ -0,0 +1,17 @@
/**
* Generated by Orval
* Do not edit manually.
* See `gen:api` script in package.json
*/
/**
* The comparison operator for the threshold check.
*/
export type SafeguardSchemaTriggerConditionOperator =
(typeof SafeguardSchemaTriggerConditionOperator)[keyof typeof SafeguardSchemaTriggerConditionOperator];
// eslint-disable-next-line @typescript-eslint/no-redeclare
export const SafeguardSchemaTriggerConditionOperator = {
'>': '>',
'<': '<',
} as const;

View File

@ -12,6 +12,6 @@ export type UpdateStrategySchemaParametersItem = {
name: string;
/** Whether this parameter must be configured when using the strategy. Defaults to `false` */
required?: boolean;
/** The [type of the parameter](https://docs.getunleash.io/reference/custom-activation-strategies#parameter-types) */
/** The [type of the parameter](https://docs.getunleash.io/reference/activation-strategies#parameters) */
type: UpdateStrategySchemaParametersItemType;
};

View File

@ -5,7 +5,7 @@
*/
/**
* The [type of the parameter](https://docs.getunleash.io/reference/custom-activation-strategies#parameter-types)
* The [type of the parameter](https://docs.getunleash.io/reference/activation-strategies#parameters)
*/
export type UpdateStrategySchemaParametersItemType =
(typeof UpdateStrategySchemaParametersItemType)[keyof typeof UpdateStrategySchemaParametersItemType];