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:
parent
dafec2e672
commit
864ae4530b
@ -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>
|
||||
}
|
||||
/>
|
||||
|
@ -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 { 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' />
|
||||
|
Loading…
Reference in New Issue
Block a user