mirror of
https://github.com/Unleash/unleash.git
synced 2025-04-10 01:16:39 +02: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:
parent
dafec2e672
commit
864ae4530b
@ -25,6 +25,8 @@ import {
|
|||||||
} from '@mui/material';
|
} from '@mui/material';
|
||||||
import { Delete, Edit, MoreVert } from '@mui/icons-material';
|
import { Delete, Edit, MoreVert } from '@mui/icons-material';
|
||||||
import { EditChange } from './EditChange';
|
import { EditChange } from './EditChange';
|
||||||
|
import { useUiFlag } from 'hooks/useUiFlag';
|
||||||
|
import { NewEditChange } from './NewEditChange';
|
||||||
|
|
||||||
const useShowActions = (changeRequest: IChangeRequest, change: IChange) => {
|
const useShowActions = (changeRequest: IChangeRequest, change: IChange) => {
|
||||||
const { isChangeRequestConfigured } = useChangeRequestsEnabled(
|
const { isChangeRequestConfigured } = useChangeRequestsEnabled(
|
||||||
@ -66,6 +68,7 @@ export const ChangeActions: FC<{
|
|||||||
const { showDiscard, showEdit } = useShowActions(changeRequest, change);
|
const { showDiscard, showEdit } = useShowActions(changeRequest, change);
|
||||||
const { discardChange } = useChangeRequestApi();
|
const { discardChange } = useChangeRequestApi();
|
||||||
const { setToastData, setToastApiError } = useToast();
|
const { setToastData, setToastApiError } = useToast();
|
||||||
|
const newStrategyConfiguration = useUiFlag('newStrategyConfiguration');
|
||||||
|
|
||||||
const [editOpen, setEditOpen] = useState(false);
|
const [editOpen, setEditOpen] = useState(false);
|
||||||
|
|
||||||
@ -149,8 +152,13 @@ export const ChangeActions: FC<{
|
|||||||
Edit change
|
Edit change
|
||||||
</Typography>
|
</Typography>
|
||||||
</ListItemText>
|
</ListItemText>
|
||||||
<EditChange
|
<ConditionallyRender
|
||||||
changeRequestId={changeRequest.id}
|
condition={newStrategyConfiguration}
|
||||||
|
show={
|
||||||
|
<NewEditChange
|
||||||
|
changeRequestId={
|
||||||
|
changeRequest.id
|
||||||
|
}
|
||||||
featureId={feature}
|
featureId={feature}
|
||||||
change={
|
change={
|
||||||
change as
|
change as
|
||||||
@ -169,6 +177,32 @@ export const ChangeActions: FC<{
|
|||||||
setEditOpen(false);
|
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>
|
</MenuItem>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
@ -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';
|
@ -1,6 +1,15 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
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 {
|
import {
|
||||||
IFeatureStrategy,
|
IFeatureStrategy,
|
||||||
IFeatureStrategyParameters,
|
IFeatureStrategyParameters,
|
||||||
@ -34,6 +43,10 @@ import { FeatureStrategyEnabledDisabled } from './FeatureStrategyEnabledDisabled
|
|||||||
import { StrategyVariants } from 'component/feature/StrategyTypes/StrategyVariants';
|
import { StrategyVariants } from 'component/feature/StrategyTypes/StrategyVariants';
|
||||||
import { usePlausibleTracker } from 'hooks/usePlausibleTracker';
|
import { usePlausibleTracker } from 'hooks/usePlausibleTracker';
|
||||||
import { formatStrategyName } from 'utils/strategyNames';
|
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 {
|
interface IFeatureStrategyFormProps {
|
||||||
feature: IFeatureToggle;
|
feature: IFeatureToggle;
|
||||||
@ -80,18 +93,12 @@ const StyledForm = styled('form')(({ theme }) => ({
|
|||||||
height: '100%',
|
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 }) => ({
|
const StyledTitle = styled('h1')(({ theme }) => ({
|
||||||
fontWeight: 'normal',
|
fontWeight: 'normal',
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
|
paddingTop: theme.spacing(2),
|
||||||
|
paddingBottom: theme.spacing(2),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const StyledButtons = styled('div')(({ theme }) => ({
|
const StyledButtons = styled('div')(({ theme }) => ({
|
||||||
@ -133,12 +140,35 @@ const StyledTargetingHeader = styled('div')(({ theme }) => ({
|
|||||||
const StyledHeaderBox = styled(Box)(({ theme }) => ({
|
const StyledHeaderBox = styled(Box)(({ theme }) => ({
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
|
justifyContent: 'space-between',
|
||||||
paddingLeft: theme.spacing(6),
|
paddingLeft: theme.spacing(6),
|
||||||
paddingRight: theme.spacing(6),
|
paddingRight: theme.spacing(6),
|
||||||
paddingTop: theme.spacing(2),
|
paddingTop: theme.spacing(2),
|
||||||
paddingBottom: 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 = ({
|
export const NewFeatureStrategyForm = ({
|
||||||
projectId,
|
projectId,
|
||||||
feature,
|
feature,
|
||||||
@ -167,6 +197,10 @@ export const NewFeatureStrategyForm = ({
|
|||||||
);
|
);
|
||||||
const { strategyDefinition } = useStrategy(strategy?.name);
|
const { strategyDefinition } = useStrategy(strategy?.name);
|
||||||
|
|
||||||
|
const foundEnvironment = feature.environments.find(
|
||||||
|
(environment) => environment.name === environmentId,
|
||||||
|
);
|
||||||
|
|
||||||
const { data } = usePendingChangeRequests(feature.project);
|
const { data } = usePendingChangeRequests(feature.project);
|
||||||
const { changeRequestInReviewOrApproved, alert } =
|
const { changeRequestInReviewOrApproved, alert } =
|
||||||
useChangeRequestInReviewWarning(data);
|
useChangeRequestInReviewWarning(data);
|
||||||
@ -180,11 +214,7 @@ export const NewFeatureStrategyForm = ({
|
|||||||
|
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const {
|
const { error: uiConfigError, loading: uiConfigLoading } = useUiConfig();
|
||||||
uiConfig,
|
|
||||||
error: uiConfigError,
|
|
||||||
loading: uiConfigLoading,
|
|
||||||
} = useUiConfig();
|
|
||||||
|
|
||||||
if (uiConfigError) {
|
if (uiConfigError) {
|
||||||
throw uiConfigError;
|
throw uiConfigError;
|
||||||
@ -257,7 +287,32 @@ export const NewFeatureStrategyForm = ({
|
|||||||
<StyledHeaderBox>
|
<StyledHeaderBox>
|
||||||
<StyledTitle>
|
<StyledTitle>
|
||||||
{formatStrategyName(strategy.name || '')}
|
{formatStrategyName(strategy.name || '')}
|
||||||
|
<ConditionallyRender
|
||||||
|
condition={strategy.name === 'flexibleRollout'}
|
||||||
|
show={
|
||||||
|
<Badge color='success' sx={{ marginLeft: '1rem' }}>
|
||||||
|
{strategy.parameters?.rollout}%
|
||||||
|
</Badge>
|
||||||
|
}
|
||||||
|
/>
|
||||||
</StyledTitle>
|
</StyledTitle>
|
||||||
|
{foundEnvironment ? (
|
||||||
|
<StyledEnvironmentBox>
|
||||||
|
<EnvironmentTypographyHeader>
|
||||||
|
Environment:
|
||||||
|
</EnvironmentTypographyHeader>
|
||||||
|
<EnvironmentIconBox>
|
||||||
|
<EnvironmentIcon
|
||||||
|
enabled={foundEnvironment.enabled}
|
||||||
|
/>{' '}
|
||||||
|
<EnvironmentTypography
|
||||||
|
enabled={foundEnvironment.enabled}
|
||||||
|
>
|
||||||
|
{foundEnvironment.name}
|
||||||
|
</EnvironmentTypography>
|
||||||
|
</EnvironmentIconBox>
|
||||||
|
</StyledEnvironmentBox>
|
||||||
|
) : null}
|
||||||
</StyledHeaderBox>
|
</StyledHeaderBox>
|
||||||
<StyledTabs value={tab} onChange={handleChange}>
|
<StyledTabs value={tab} onChange={handleChange}>
|
||||||
<Tab label='General' />
|
<Tab label='General' />
|
||||||
|
Loading…
Reference in New Issue
Block a user