1
0
mirror of https://github.com/Unleash/unleash.git synced 2024-12-22 19:07:54 +01:00

Feat/new strategy configuration header (#5655)

This PR adds more information to the header of the strategy according to
the new designs:

<img width="1298" alt="Skjermbilde 2023-12-15 kl 13 31 26"
src="https://github.com/Unleash/unleash/assets/16081982/73a3bc6b-c78b-4f24-b9f3-8a4b2c14e39c">
This commit is contained in:
Fredrik Strand Oseberg 2023-12-15 14:09:47 +01:00 committed by GitHub
parent dafec2e672
commit 864ae4530b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 352 additions and 31 deletions

View File

@ -25,6 +25,8 @@ import {
} from '@mui/material';
import { Delete, Edit, MoreVert } from '@mui/icons-material';
import { EditChange } from './EditChange';
import { useUiFlag } from 'hooks/useUiFlag';
import { NewEditChange } from './NewEditChange';
const useShowActions = (changeRequest: IChangeRequest, change: IChange) => {
const { isChangeRequestConfigured } = useChangeRequestsEnabled(
@ -66,6 +68,7 @@ export const ChangeActions: FC<{
const { showDiscard, showEdit } = useShowActions(changeRequest, change);
const { discardChange } = useChangeRequestApi();
const { setToastData, setToastApiError } = useToast();
const newStrategyConfiguration = useUiFlag('newStrategyConfiguration');
const [editOpen, setEditOpen] = useState(false);
@ -149,8 +152,13 @@ export const ChangeActions: FC<{
Edit change
</Typography>
</ListItemText>
<EditChange
changeRequestId={changeRequest.id}
<ConditionallyRender
condition={newStrategyConfiguration}
show={
<NewEditChange
changeRequestId={
changeRequest.id
}
featureId={feature}
change={
change as
@ -169,6 +177,32 @@ export const ChangeActions: FC<{
setEditOpen(false);
}}
/>
}
elseShow={
<EditChange
changeRequestId={
changeRequest.id
}
featureId={feature}
change={
change as
| IChangeRequestAddStrategy
| IChangeRequestUpdateStrategy
}
environment={
changeRequest.environment
}
open={editOpen}
onSubmit={() => {
setEditOpen(false);
onRefetch?.();
}}
onClose={() => {
setEditOpen(false);
}}
/>
}
/>
</MenuItem>
}
/>

View File

@ -0,0 +1,232 @@
import React, { useEffect, useRef, useState } from 'react';
import { FeatureStrategyForm } from 'component/feature/FeatureStrategy/FeatureStrategyForm/FeatureStrategyForm';
import FormTemplate from 'component/common/FormTemplate/FormTemplate';
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
import { formatUnknownError } from 'utils/formatUnknownError';
import useToast from 'hooks/useToast';
import { IFeatureStrategy } from 'interfaces/strategy';
import { UPDATE_FEATURE_STRATEGY } from 'component/providers/AccessProvider/permissions';
import { ISegment } from 'interfaces/segment';
import { formatStrategyName } from 'utils/strategyNames';
import { useFormErrors } from 'hooks/useFormErrors';
import { useCollaborateData } from 'hooks/useCollaborateData';
import { useFeature } from 'hooks/api/getters/useFeature/useFeature';
import { IFeatureToggle } from 'interfaces/featureToggle';
import { useChangeRequestsEnabled } from 'hooks/useChangeRequestsEnabled';
import { useChangeRequestApi } from 'hooks/api/actions/useChangeRequestApi/useChangeRequestApi';
import { comparisonModerator } from 'component/feature/FeatureStrategy/featureStrategy.utils';
import {
IChangeRequestAddStrategy,
IChangeRequestUpdateStrategy,
} from 'component/changeRequest/changeRequest.types';
import { SidebarModal } from 'component/common/SidebarModal/SidebarModal';
import { useSegments } from 'hooks/api/getters/useSegments/useSegments';
import { useUiFlag } from 'hooks/useUiFlag';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { NewFeatureStrategyForm } from 'component/feature/FeatureStrategy/FeatureStrategyForm/NewFeatureStrategyForm';
interface IEditChangeProps {
change: IChangeRequestAddStrategy | IChangeRequestUpdateStrategy;
changeRequestId: number;
featureId: string;
environment: string;
open: boolean;
onSubmit: () => void;
onClose: () => void;
}
export const NewEditChange = ({
change,
changeRequestId,
environment,
open,
onSubmit,
onClose,
featureId,
}: IEditChangeProps) => {
const projectId = useRequiredPathParam('projectId');
const { editChange } = useChangeRequestApi();
const [tab, setTab] = useState(0);
const newStrategyConfiguration = useUiFlag('newStrategyConfiguration');
const [strategy, setStrategy] = useState<Partial<IFeatureStrategy>>(
change.payload,
);
const { segments: allSegments } = useSegments();
const strategySegments = (allSegments || []).filter((segment) => {
return change.payload.segments?.includes(segment.id);
});
const [segments, setSegments] = useState<ISegment[]>(strategySegments);
const strategyDefinition = {
parameters: change.payload.parameters,
name: change.payload.name,
};
const { setToastData, setToastApiError } = useToast();
const errors = useFormErrors();
const { uiConfig } = useUiConfig();
const { unleashUrl } = uiConfig;
const { isChangeRequestConfigured } = useChangeRequestsEnabled(projectId);
const { feature, refetchFeature } = useFeature(projectId, featureId);
const ref = useRef<IFeatureToggle>(feature);
const { data, staleDataNotification, forceRefreshCache } =
useCollaborateData<IFeatureToggle>(
{
unleashGetter: useFeature,
params: [projectId, featureId],
dataKey: 'feature',
refetchFunctionKey: 'refetchFeature',
options: {},
},
feature,
{
afterSubmitAction: refetchFeature,
},
comparisonModerator,
);
useEffect(() => {
if (ref.current.name === '' && feature.name) {
forceRefreshCache(feature);
ref.current = feature;
}
}, [feature]);
const payload = {
...strategy,
segments: segments.map((segment) => segment.id),
};
const onInternalSubmit = async () => {
try {
await editChange(projectId, changeRequestId, change.id, {
action: strategy.id ? 'updateStrategy' : 'addStrategy',
feature: featureId,
payload,
});
onSubmit();
setToastData({
title: 'Change updated',
type: 'success',
});
} catch (error: unknown) {
setToastApiError(formatUnknownError(error));
}
};
if (!strategyDefinition) {
return null;
}
if (!data) return null;
return (
<SidebarModal
open={open}
onClose={onClose}
label='Edit change'
onClick={(e) => {
e.stopPropagation();
}}
>
<FormTemplate
modal
disablePadding
description={featureStrategyHelp}
documentationLink={featureStrategyDocsLink}
documentationLinkLabel={featureStrategyDocsLinkLabel}
formatApiCode={() =>
formatUpdateStrategyApiCode(
projectId,
changeRequestId,
change.id,
payload,
unleashUrl,
)
}
>
<ConditionallyRender
condition={newStrategyConfiguration}
show={
<NewFeatureStrategyForm
projectId={projectId}
feature={data}
strategy={strategy}
setStrategy={setStrategy}
segments={segments}
setSegments={setSegments}
environmentId={environment}
onSubmit={onInternalSubmit}
onCancel={onClose}
loading={false}
permission={UPDATE_FEATURE_STRATEGY}
errors={errors}
isChangeRequest={isChangeRequestConfigured(
environment,
)}
tab={tab}
setTab={setTab}
/>
}
elseShow={
<FeatureStrategyForm
projectId={projectId}
feature={data}
strategy={strategy}
setStrategy={setStrategy}
segments={segments}
setSegments={setSegments}
environmentId={environment}
onSubmit={onInternalSubmit}
onCancel={onClose}
loading={false}
permission={UPDATE_FEATURE_STRATEGY}
errors={errors}
isChangeRequest={isChangeRequestConfigured(
environment,
)}
/>
}
/>
{staleDataNotification}
</FormTemplate>
</SidebarModal>
);
};
export const formatUpdateStrategyApiCode = (
projectId: string,
changeRequestId: number,
changeId: number,
strategy: Partial<IFeatureStrategy>,
unleashUrl?: string,
): string => {
if (!unleashUrl) {
return '';
}
const url = `${unleashUrl}/api/admin/projects/${projectId}/change-requests/${changeRequestId}/changes/${changeId}`;
const payload = JSON.stringify(strategy, undefined, 2);
return `curl --location --request PUT '${url}' \\
--header 'Authorization: INSERT_API_KEY' \\
--header 'Content-Type: application/json' \\
--data-raw '${payload}'`;
};
export const featureStrategyHelp = `
An activation strategy will only run when a feature toggle is enabled and provides a way to control who will get access to the feature.
If any of a feature toggle's activation strategies returns true, the user will get access.
`;
export const featureStrategyDocsLink =
'https://docs.getunleash.io/reference/activation-strategies';
export const featureStrategyDocsLinkLabel = 'Strategies documentation';

View File

@ -1,6 +1,15 @@
import React, { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { Alert, Button, styled, Tabs, Tab, Box, Divider } from '@mui/material';
import {
Alert,
Button,
styled,
Tabs,
Tab,
Box,
Divider,
Typography,
} from '@mui/material';
import {
IFeatureStrategy,
IFeatureStrategyParameters,
@ -34,6 +43,10 @@ import { FeatureStrategyEnabledDisabled } from './FeatureStrategyEnabledDisabled
import { StrategyVariants } from 'component/feature/StrategyTypes/StrategyVariants';
import { usePlausibleTracker } from 'hooks/usePlausibleTracker';
import { formatStrategyName } from 'utils/strategyNames';
import { Badge } from 'component/common/Badge/Badge';
import EnvironmentIcon from 'component/common/EnvironmentIcon/EnvironmentIcon';
import { useProjectEnvironments } from 'hooks/api/getters/useProjectEnvironments/useProjectEnvironments';
import { useFeature } from 'hooks/api/getters/useFeature/useFeature';
interface IFeatureStrategyFormProps {
feature: IFeatureToggle;
@ -80,18 +93,12 @@ const StyledForm = styled('form')(({ theme }) => ({
height: '100%',
}));
const StyledHr = styled('hr')(({ theme }) => ({
width: '100%',
height: '1px',
margin: theme.spacing(2, 0),
border: 'none',
background: theme.palette.background.elevation2,
}));
const StyledTitle = styled('h1')(({ theme }) => ({
fontWeight: 'normal',
display: 'flex',
alignItems: 'center',
paddingTop: theme.spacing(2),
paddingBottom: theme.spacing(2),
}));
const StyledButtons = styled('div')(({ theme }) => ({
@ -133,12 +140,35 @@ const StyledTargetingHeader = styled('div')(({ theme }) => ({
const StyledHeaderBox = styled(Box)(({ theme }) => ({
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
paddingLeft: theme.spacing(6),
paddingRight: theme.spacing(6),
paddingTop: theme.spacing(2),
paddingBottom: theme.spacing(2),
}));
const StyledEnvironmentBox = styled(Box)(({ theme }) => ({
display: 'flex',
alignItems: 'center',
}));
const EnvironmentIconBox = styled(Box)(({ theme }) => ({
transform: 'scale(0.9)',
display: 'flex',
alignItems: 'center',
}));
const EnvironmentTypography = styled(Typography)<{ enabled: boolean }>(
({ theme, enabled }) => ({
fontWeight: enabled ? 'bold' : 'normal',
}),
);
const EnvironmentTypographyHeader = styled(Typography)(({ theme }) => ({
marginRight: theme.spacing(0.5),
color: theme.palette.text.secondary,
}));
export const NewFeatureStrategyForm = ({
projectId,
feature,
@ -167,6 +197,10 @@ export const NewFeatureStrategyForm = ({
);
const { strategyDefinition } = useStrategy(strategy?.name);
const foundEnvironment = feature.environments.find(
(environment) => environment.name === environmentId,
);
const { data } = usePendingChangeRequests(feature.project);
const { changeRequestInReviewOrApproved, alert } =
useChangeRequestInReviewWarning(data);
@ -180,11 +214,7 @@ export const NewFeatureStrategyForm = ({
const navigate = useNavigate();
const {
uiConfig,
error: uiConfigError,
loading: uiConfigLoading,
} = useUiConfig();
const { error: uiConfigError, loading: uiConfigLoading } = useUiConfig();
if (uiConfigError) {
throw uiConfigError;
@ -257,7 +287,32 @@ export const NewFeatureStrategyForm = ({
<StyledHeaderBox>
<StyledTitle>
{formatStrategyName(strategy.name || '')}
<ConditionallyRender
condition={strategy.name === 'flexibleRollout'}
show={
<Badge color='success' sx={{ marginLeft: '1rem' }}>
{strategy.parameters?.rollout}%
</Badge>
}
/>
</StyledTitle>
{foundEnvironment ? (
<StyledEnvironmentBox>
<EnvironmentTypographyHeader>
Environment:
</EnvironmentTypographyHeader>
<EnvironmentIconBox>
<EnvironmentIcon
enabled={foundEnvironment.enabled}
/>{' '}
<EnvironmentTypography
enabled={foundEnvironment.enabled}
>
{foundEnvironment.name}
</EnvironmentTypography>
</EnvironmentIconBox>
</StyledEnvironmentBox>
) : null}
</StyledHeaderBox>
<StyledTabs value={tab} onChange={handleChange}>
<Tab label='General' />