1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-02-23 00:22:19 +01:00

feat: add initial setup for tabs (#5586)

This PR sets up the initial tab structure for the new strategy form
This commit is contained in:
Fredrik Strand Oseberg 2023-12-11 13:39:21 +01:00 committed by GitHub
parent d11aedc12f
commit 9dbb7ea9a9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 407 additions and 18 deletions

View File

@ -22,6 +22,9 @@ import {
} from 'component/changeRequest/changeRequest.types'; } from 'component/changeRequest/changeRequest.types';
import { SidebarModal } from 'component/common/SidebarModal/SidebarModal'; import { SidebarModal } from 'component/common/SidebarModal/SidebarModal';
import { useSegments } from 'hooks/api/getters/useSegments/useSegments'; 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 { interface IEditChangeProps {
change: IChangeRequestAddStrategy | IChangeRequestUpdateStrategy; change: IChangeRequestAddStrategy | IChangeRequestUpdateStrategy;
@ -44,6 +47,8 @@ export const EditChange = ({
}: IEditChangeProps) => { }: IEditChangeProps) => {
const projectId = useRequiredPathParam('projectId'); const projectId = useRequiredPathParam('projectId');
const { editChange } = useChangeRequestApi(); const { editChange } = useChangeRequestApi();
const [tab, setTab] = useState(0);
const newStrategyConfiguration = useUiFlag('newStrategyConfiguration');
const [strategy, setStrategy] = useState<Partial<IFeatureStrategy>>( const [strategy, setStrategy] = useState<Partial<IFeatureStrategy>>(
change.payload, change.payload,
@ -146,6 +151,30 @@ export const EditChange = ({
) )
} }
> >
<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 <FeatureStrategyForm
projectId={projectId} projectId={projectId}
feature={data} feature={data}
@ -159,8 +188,13 @@ export const EditChange = ({
loading={false} loading={false}
permission={UPDATE_FEATURE_STRATEGY} permission={UPDATE_FEATURE_STRATEGY}
errors={errors} errors={errors}
isChangeRequest={isChangeRequestConfigured(environment)} isChangeRequest={isChangeRequestConfigured(
environment,
)}
/> />
}
/>
{staleDataNotification} {staleDataNotification}
</FormTemplate> </FormTemplate>
</SidebarModal> </SidebarModal>

View File

@ -0,0 +1,349 @@
import React, { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { Alert, Button, styled, Tabs, Tab } from '@mui/material';
import {
IFeatureStrategy,
IFeatureStrategyParameters,
IStrategyParameter,
} from 'interfaces/strategy';
import { FeatureStrategyType } from '../FeatureStrategyType/FeatureStrategyType';
import { FeatureStrategyEnabled } from './FeatureStrategyEnabled/FeatureStrategyEnabled';
import { FeatureStrategyConstraints } from '../FeatureStrategyConstraints/FeatureStrategyConstraints';
import { IFeatureToggle } from 'interfaces/featureToggle';
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { STRATEGY_FORM_SUBMIT_ID } from 'utils/testIds';
import { useConstraintsValidation } from 'hooks/api/getters/useConstraintsValidation/useConstraintsValidation';
import PermissionButton from 'component/common/PermissionButton/PermissionButton';
import { FeatureStrategySegment } from 'component/feature/FeatureStrategy/FeatureStrategySegment/FeatureStrategySegment';
import { ISegment } from 'interfaces/segment';
import { IFormErrors } from 'hooks/useFormErrors';
import { validateParameterValue } from 'utils/validateParameterValue';
import { useStrategy } from 'hooks/api/getters/useStrategy/useStrategy';
import { FeatureStrategyChangeRequestAlert } from './FeatureStrategyChangeRequestAlert/FeatureStrategyChangeRequestAlert';
import {
FeatureStrategyProdGuard,
useFeatureStrategyProdGuard,
} from '../FeatureStrategyProdGuard/FeatureStrategyProdGuard';
import { formatFeaturePath } from '../FeatureStrategyEdit/FeatureStrategyEdit';
import { useChangeRequestInReviewWarning } from 'hooks/useChangeRequestInReviewWarning';
import { usePendingChangeRequests } from 'hooks/api/getters/usePendingChangeRequests/usePendingChangeRequests';
import { useHasProjectEnvironmentAccess } from 'hooks/useHasAccess';
import { FeatureStrategyTitle } from './FeatureStrategyTitle/FeatureStrategyTitle';
import { FeatureStrategyEnabledDisabled } from './FeatureStrategyEnabledDisabled/FeatureStrategyEnabledDisabled';
import { StrategyVariants } from 'component/feature/StrategyTypes/StrategyVariants';
import { usePlausibleTracker } from 'hooks/usePlausibleTracker';
interface IFeatureStrategyFormProps {
feature: IFeatureToggle;
projectId: string;
environmentId: string;
permission: string;
onSubmit: () => void;
onCancel?: () => void;
loading: boolean;
isChangeRequest?: boolean;
strategy: Partial<IFeatureStrategy>;
setStrategy: React.Dispatch<
React.SetStateAction<Partial<IFeatureStrategy>>
>;
segments: ISegment[];
setSegments: React.Dispatch<React.SetStateAction<ISegment[]>>;
errors: IFormErrors;
tab: number;
setTab: React.Dispatch<React.SetStateAction<number>>;
}
const StyledForm = styled('form')(({ theme }) => ({
display: 'grid',
gap: theme.spacing(2),
}));
const StyledHr = styled('hr')(({ theme }) => ({
width: '100%',
height: '1px',
margin: theme.spacing(2, 0),
border: 'none',
background: theme.palette.background.elevation2,
}));
const StyledButtons = styled('div')(({ theme }) => ({
display: 'flex',
justifyContent: 'end',
gap: theme.spacing(2),
paddingBottom: theme.spacing(10),
}));
export const NewFeatureStrategyForm = ({
projectId,
feature,
environmentId,
permission,
onSubmit,
onCancel,
loading,
strategy,
setStrategy,
segments,
setSegments,
errors,
isChangeRequest,
tab,
setTab,
}: IFeatureStrategyFormProps) => {
const { trackEvent } = usePlausibleTracker();
const [showProdGuard, setShowProdGuard] = useState(false);
const hasValidConstraints = useConstraintsValidation(strategy.constraints);
const enableProdGuard = useFeatureStrategyProdGuard(feature, environmentId);
const access = useHasProjectEnvironmentAccess(
permission,
projectId,
environmentId,
);
const { strategyDefinition } = useStrategy(strategy?.name);
const { data } = usePendingChangeRequests(feature.project);
const { changeRequestInReviewOrApproved, alert } =
useChangeRequestInReviewWarning(data);
const hasChangeRequestInReviewForEnvironment =
changeRequestInReviewOrApproved(environmentId || '');
const changeRequestButtonText = hasChangeRequestInReviewForEnvironment
? 'Add to existing change request'
: 'Add change to draft';
const navigate = useNavigate();
const {
uiConfig,
error: uiConfigError,
loading: uiConfigLoading,
} = useUiConfig();
if (uiConfigError) {
throw uiConfigError;
}
if (uiConfigLoading || !strategyDefinition) {
return null;
}
const findParameterDefinition = (name: string): IStrategyParameter => {
return strategyDefinition.parameters.find((parameterDefinition) => {
return parameterDefinition.name === name;
})!;
};
const validateParameter = (
name: string,
value: IFeatureStrategyParameters[string],
): boolean => {
const parameterValueError = validateParameterValue(
findParameterDefinition(name),
value,
);
if (parameterValueError) {
errors.setFormError(name, parameterValueError);
return false;
} else {
errors.removeFormError(name);
return true;
}
};
const validateAllParameters = (): boolean => {
return strategyDefinition.parameters
.map((parameter) => parameter.name)
.map((name) => validateParameter(name, strategy.parameters?.[name]))
.every(Boolean);
};
const onDefaultCancel = () => {
navigate(formatFeaturePath(feature.project, feature.name));
};
const onSubmitWithValidation = async (event: React.FormEvent) => {
if (Array.isArray(strategy.variants) && strategy.variants?.length > 0) {
trackEvent('strategy-variants', {
props: {
eventType: 'submitted',
},
});
}
event.preventDefault();
if (!validateAllParameters()) {
return;
}
if (enableProdGuard && !isChangeRequest) {
setShowProdGuard(true);
} else {
onSubmit();
}
};
const handleChange = (event: React.ChangeEvent<{}>, newValue: number) => {
setTab(newValue);
};
return (
<StyledForm onSubmit={onSubmitWithValidation}>
<Tabs value={tab} onChange={handleChange}>
<Tab label='General' />
<Tab label='Targeting' />
<Tab label='Variants' />
</Tabs>
<ConditionallyRender
condition={tab === 0}
show={
<>
<ConditionallyRender
condition={hasChangeRequestInReviewForEnvironment}
show={alert}
elseShow={
<ConditionallyRender
condition={Boolean(isChangeRequest)}
show={
<FeatureStrategyChangeRequestAlert
environment={environmentId}
/>
}
/>
}
/>
<FeatureStrategyEnabled
projectId={feature.project}
featureId={feature.name}
environmentId={environmentId}
>
<ConditionallyRender
condition={Boolean(isChangeRequest)}
show={
<Alert severity='success'>
This feature toggle is currently enabled
in the <strong>{environmentId}</strong>{' '}
environment. Any changes made here will
be available to users as soon as these
changes are approved and applied.
</Alert>
}
elseShow={
<Alert severity='success'>
This feature toggle is currently enabled
in the <strong>{environmentId}</strong>{' '}
environment. Any changes made here will
be available to users as soon as you hit{' '}
<strong>save</strong>.
</Alert>
}
/>
</FeatureStrategyEnabled>
<StyledHr />
<FeatureStrategyTitle
title={strategy.title || ''}
setTitle={(title) => {
setStrategy((prev) => ({
...prev,
title,
}));
}}
/>
<FeatureStrategyType
strategy={strategy}
strategyDefinition={strategyDefinition}
setStrategy={setStrategy}
validateParameter={validateParameter}
errors={errors}
hasAccess={access}
/>
<FeatureStrategyEnabledDisabled
enabled={!strategy?.disabled}
onToggleEnabled={() =>
setStrategy((strategyState) => ({
...strategyState,
disabled: !strategyState.disabled,
}))
}
/>
</>
}
/>
<ConditionallyRender
condition={tab === 1}
show={
<>
<FeatureStrategySegment
segments={segments}
setSegments={setSegments}
projectId={projectId}
/>
<FeatureStrategyConstraints
projectId={feature.project}
environmentId={environmentId}
strategy={strategy}
setStrategy={setStrategy}
/>
</>
}
/>
<ConditionallyRender
condition={tab === 2}
show={
<ConditionallyRender
condition={
strategy.parameters != null &&
'stickiness' in strategy.parameters
}
show={
<StrategyVariants
strategy={strategy}
setStrategy={setStrategy}
environment={environmentId}
projectId={projectId}
/>
}
/>
}
/>
<StyledButtons>
<PermissionButton
permission={permission}
projectId={feature.project}
environmentId={environmentId}
variant='contained'
color='primary'
type='submit'
disabled={
loading ||
!hasValidConstraints ||
errors.hasFormErrors()
}
data-testid={STRATEGY_FORM_SUBMIT_ID}
>
{isChangeRequest
? changeRequestButtonText
: 'Save strategy'}
</PermissionButton>
<Button
type='button'
color='primary'
onClick={onCancel ? onCancel : onDefaultCancel}
disabled={loading}
>
Cancel
</Button>
<FeatureStrategyProdGuard
open={showProdGuard}
onClose={() => setShowProdGuard(false)}
onClick={onSubmit}
loading={loading}
label='Save strategy'
/>
</StyledButtons>
</StyledForm>
);
};

View File

@ -33,8 +33,10 @@ import { usePlausibleTracker } from 'hooks/usePlausibleTracker';
import useQueryParams from 'hooks/useQueryParams'; import useQueryParams from 'hooks/useQueryParams';
import { useSegments } from 'hooks/api/getters/useSegments/useSegments'; import { useSegments } from 'hooks/api/getters/useSegments/useSegments';
import { useDefaultStrategy } from '../../../project/Project/ProjectSettings/ProjectDefaultStrategySettings/ProjectEnvironment/ProjectEnvironmentDefaultStrategy/EditDefaultStrategy'; import { useDefaultStrategy } from '../../../project/Project/ProjectSettings/ProjectDefaultStrategySettings/ProjectEnvironment/ProjectEnvironmentDefaultStrategy/EditDefaultStrategy';
import { NewFeatureStrategyForm } from 'component/feature/FeatureStrategy/FeatureStrategyForm/NewFeatureStrategyForm';
export const NewFeatureStrategyCreate = () => { export const NewFeatureStrategyCreate = () => {
const [tab, setTab] = useState(0);
const projectId = useRequiredPathParam('projectId'); const projectId = useRequiredPathParam('projectId');
const featureId = useRequiredPathParam('featureId'); const featureId = useRequiredPathParam('featureId');
const environmentId = useRequiredQueryParam('environmentId'); const environmentId = useRequiredQueryParam('environmentId');
@ -192,8 +194,7 @@ export const NewFeatureStrategyCreate = () => {
) )
} }
> >
<h1>NEW CREATE FORM</h1> <NewFeatureStrategyForm
<FeatureStrategyForm
projectId={projectId} projectId={projectId}
feature={data} feature={data}
strategy={strategy} strategy={strategy}
@ -206,6 +207,8 @@ export const NewFeatureStrategyCreate = () => {
permission={CREATE_FEATURE_STRATEGY} permission={CREATE_FEATURE_STRATEGY}
errors={errors} errors={errors}
isChangeRequest={isChangeRequestConfigured(environmentId)} isChangeRequest={isChangeRequestConfigured(environmentId)}
tab={tab}
setTab={setTab}
/> />
{staleDataNotification} {staleDataNotification}
</FormTemplate> </FormTemplate>

View File

@ -28,6 +28,7 @@ import { useChangeRequestsEnabled } from 'hooks/useChangeRequestsEnabled';
import { useChangeRequestApi } from 'hooks/api/actions/useChangeRequestApi/useChangeRequestApi'; import { useChangeRequestApi } from 'hooks/api/actions/useChangeRequestApi/useChangeRequestApi';
import { usePendingChangeRequests } from 'hooks/api/getters/usePendingChangeRequests/usePendingChangeRequests'; import { usePendingChangeRequests } from 'hooks/api/getters/usePendingChangeRequests/usePendingChangeRequests';
import { usePlausibleTracker } from 'hooks/usePlausibleTracker'; import { usePlausibleTracker } from 'hooks/usePlausibleTracker';
import { NewFeatureStrategyForm } from 'component/feature/FeatureStrategy/FeatureStrategyForm/NewFeatureStrategyForm';
const useTitleTracking = () => { const useTitleTracking = () => {
const [previousTitle, setPreviousTitle] = useState<string>(''); const [previousTitle, setPreviousTitle] = useState<string>('');
@ -80,6 +81,7 @@ export const NewFeatureStrategyEdit = () => {
const featureId = useRequiredPathParam('featureId'); const featureId = useRequiredPathParam('featureId');
const environmentId = useRequiredQueryParam('environmentId'); const environmentId = useRequiredQueryParam('environmentId');
const strategyId = useRequiredQueryParam('strategyId'); const strategyId = useRequiredQueryParam('strategyId');
const [tab, setTab] = useState(0);
const [strategy, setStrategy] = useState<Partial<IFeatureStrategy>>({}); const [strategy, setStrategy] = useState<Partial<IFeatureStrategy>>({});
const [segments, setSegments] = useState<ISegment[]>([]); const [segments, setSegments] = useState<ISegment[]>([]);
@ -214,8 +216,7 @@ export const NewFeatureStrategyEdit = () => {
) )
} }
> >
<h1>NEW EDIT FORM</h1> <NewFeatureStrategyForm
<FeatureStrategyForm
projectId={projectId} projectId={projectId}
feature={data} feature={data}
strategy={strategy} strategy={strategy}
@ -228,6 +229,8 @@ export const NewFeatureStrategyEdit = () => {
permission={UPDATE_FEATURE_STRATEGY} permission={UPDATE_FEATURE_STRATEGY}
errors={errors} errors={errors}
isChangeRequest={isChangeRequestConfigured(environmentId)} isChangeRequest={isChangeRequestConfigured(environmentId)}
tab={tab}
setTab={setTab}
/> />
{staleDataNotification} {staleDataNotification}
</FormTemplate> </FormTemplate>