From 864ae4530b20085f7af35216bc27e20fa87822a5 Mon Sep 17 00:00:00 2001 From: Fredrik Strand Oseberg Date: Fri, 15 Dec 2023 14:09:47 +0100 Subject: [PATCH] Feat/new strategy configuration header (#5655) This PR adds more information to the header of the strategy according to the new designs: Skjermbilde 2023-12-15 kl 13 31 26 --- .../Changes/Change/ChangeActions.tsx | 68 +++-- .../Changes/Change/NewEditChange.tsx | 232 ++++++++++++++++++ .../NewFeatureStrategyForm.tsx | 83 +++++-- 3 files changed, 352 insertions(+), 31 deletions(-) create mode 100644 frontend/src/component/changeRequest/ChangeRequest/Changes/Change/NewEditChange.tsx diff --git a/frontend/src/component/changeRequest/ChangeRequest/Changes/Change/ChangeActions.tsx b/frontend/src/component/changeRequest/ChangeRequest/Changes/Change/ChangeActions.tsx index 1a2dfba871..a506224075 100644 --- a/frontend/src/component/changeRequest/ChangeRequest/Changes/Change/ChangeActions.tsx +++ b/frontend/src/component/changeRequest/ChangeRequest/Changes/Change/ChangeActions.tsx @@ -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,25 +152,56 @@ export const ChangeActions: FC<{ Edit change - { + setEditOpen(false); + onRefetch?.(); + }} + onClose={() => { + setEditOpen(false); + }} + /> } - environment={ - changeRequest.environment + elseShow={ + { + setEditOpen(false); + onRefetch?.(); + }} + onClose={() => { + setEditOpen(false); + }} + /> } - open={editOpen} - onSubmit={() => { - setEditOpen(false); - onRefetch?.(); - }} - onClose={() => { - setEditOpen(false); - }} /> } diff --git a/frontend/src/component/changeRequest/ChangeRequest/Changes/Change/NewEditChange.tsx b/frontend/src/component/changeRequest/ChangeRequest/Changes/Change/NewEditChange.tsx new file mode 100644 index 0000000000..dfe8d4e7f8 --- /dev/null +++ b/frontend/src/component/changeRequest/ChangeRequest/Changes/Change/NewEditChange.tsx @@ -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>( + change.payload, + ); + + const { segments: allSegments } = useSegments(); + const strategySegments = (allSegments || []).filter((segment) => { + return change.payload.segments?.includes(segment.id); + }); + + const [segments, setSegments] = useState(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(feature); + + const { data, staleDataNotification, forceRefreshCache } = + useCollaborateData( + { + 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 ( + { + e.stopPropagation(); + }} + > + + formatUpdateStrategyApiCode( + projectId, + changeRequestId, + change.id, + payload, + unleashUrl, + ) + } + > + + } + elseShow={ + + } + /> + + {staleDataNotification} + + + ); +}; + +export const formatUpdateStrategyApiCode = ( + projectId: string, + changeRequestId: number, + changeId: number, + strategy: Partial, + 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'; diff --git a/frontend/src/component/feature/FeatureStrategy/FeatureStrategyForm/NewFeatureStrategyForm.tsx b/frontend/src/component/feature/FeatureStrategy/FeatureStrategyForm/NewFeatureStrategyForm.tsx index 3d934431b6..660a5bc427 100644 --- a/frontend/src/component/feature/FeatureStrategy/FeatureStrategyForm/NewFeatureStrategyForm.tsx +++ b/frontend/src/component/feature/FeatureStrategy/FeatureStrategyForm/NewFeatureStrategyForm.tsx @@ -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 = ({ {formatStrategyName(strategy.name || '')} + + {strategy.parameters?.rollout}% + + } + /> + {foundEnvironment ? ( + + + Environment: + + + {' '} + + {foundEnvironment.name} + + + + ) : null}