1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-04-15 01:16:22 +02:00
Tymoteusz Czech 2023-06-21 15:26:07 +02:00 committed by GitHub
parent 71d242a299
commit 02ca60511f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 284 additions and 79 deletions

View File

@ -20,6 +20,7 @@ export interface IPermissionButtonProps extends Omit<ButtonProps, 'title'> {
projectId?: string;
environmentId?: string;
tooltipProps?: Omit<ITooltipResolverProps, 'children'>;
hideLockIcon?: boolean;
}
interface IPermissionBaseButtonProps extends IPermissionButtonProps {
@ -68,6 +69,7 @@ const BasePermissionButton: React.FC<IPermissionBaseButtonProps> =
projectId,
environmentId,
tooltipProps,
hideLockIcon,
...rest
},
ref
@ -92,7 +94,7 @@ const BasePermissionButton: React.FC<IPermissionBaseButtonProps> =
endIcon={
<>
<ConditionallyRender
condition={!access}
condition={!access && !hideLockIcon}
show={<Lock titleAccess="Locked" />}
elseShow={
Boolean(rest.endIcon) &&

View File

@ -15,6 +15,8 @@ import { useChangeRequestsEnabled } from 'hooks/useChangeRequestsEnabled';
import { getFeatureStrategyIcon } from 'utils/strategyNames';
import { AddFromTemplateCard } from './AddFromTemplateCard/AddFromTemplateCard';
import { FeatureStrategyMenu } from '../FeatureStrategyMenu/FeatureStrategyMenu';
import { LegacyFeatureStrategyMenu } from '../FeatureStrategyMenu/LegacyFeatureStrategyMenu';
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
interface IFeatureStrategyEmptyProps {
projectId: string;
@ -76,6 +78,9 @@ export const FeatureStrategyEmpty = ({
onChangeRequestAddStrategyClose,
} = useChangeRequestAddStrategy(projectId, featureId, 'addStrategy');
const { uiConfig } = useUiConfig();
const strategySplittedButton = uiConfig?.flags?.strategySplittedButton;
const onAfterAddStrategy = (multiple = false) => {
refetchFeature();
refetchFeatureImmutable();
@ -166,12 +171,26 @@ export const FeatureStrategyEmpty = ({
justifyContent: 'center',
}}
>
<FeatureStrategyMenu
label="Add your first strategy"
projectId={projectId}
featureId={featureId}
environmentId={environmentId}
matchWidth={canCopyFromOtherEnvironment}
<ConditionallyRender
condition={Boolean(strategySplittedButton)}
show={
<FeatureStrategyMenu
label="Add your first strategy"
projectId={projectId}
featureId={featureId}
environmentId={environmentId}
matchWidth={canCopyFromOtherEnvironment}
/>
}
elseShow={
<LegacyFeatureStrategyMenu
label="Add your first strategy"
projectId={projectId}
featureId={featureId}
environmentId={environmentId}
matchWidth={canCopyFromOtherEnvironment}
/>
}
/>
<ConditionallyRender
condition={canCopyFromOtherEnvironment}
@ -186,55 +205,67 @@ export const FeatureStrategyEmpty = ({
}
/>
</Box>
<Box sx={{ width: '100%', mt: 3 }}>
<SectionSeparator>
Or use a strategy template
</SectionSeparator>
</Box>
<Box
sx={{
display: 'grid',
width: '100%',
gap: 2,
gridTemplateColumns: { xs: '1fr', sm: '1fr 1fr' },
}}
>
<AddFromTemplateCard
title="Standard strategy"
projectId={projectId}
featureId={featureId}
environmentId={environmentId}
onAfterAddStrategy={onAfterAddStrategy}
Icon={getFeatureStrategyIcon('default')}
strategy={{
name: 'default',
parameters: {},
constraints: [],
}}
>
The standard strategy is strictly on/off for your entire
userbase.
</AddFromTemplateCard>
<AddFromTemplateCard
title="Gradual rollout"
projectId={projectId}
featureId={featureId}
environmentId={environmentId}
onAfterAddStrategy={onAfterAddStrategy}
Icon={getFeatureStrategyIcon('flexibleRollout')}
strategy={{
name: 'flexibleRollout',
parameters: {
rollout: '50',
stickiness: 'default',
groupId: feature.name,
},
constraints: [],
}}
>
Roll out to a percentage of your userbase.
</AddFromTemplateCard>
</Box>
<ConditionallyRender
condition={strategySplittedButton === false}
show={
<>
<Box sx={{ width: '100%', mt: 3 }}>
<SectionSeparator>
Or use a strategy template
</SectionSeparator>
</Box>
<Box
sx={{
display: 'grid',
width: '100%',
gap: 2,
gridTemplateColumns: {
xs: '1fr',
sm: '1fr 1fr',
},
}}
>
<AddFromTemplateCard
title="Standard strategy"
projectId={projectId}
featureId={featureId}
environmentId={environmentId}
onAfterAddStrategy={onAfterAddStrategy}
Icon={getFeatureStrategyIcon('default')}
strategy={{
name: 'default',
parameters: {},
constraints: [],
}}
>
The standard strategy is strictly on/off for
your entire userbase.
</AddFromTemplateCard>
<AddFromTemplateCard
title="Gradual rollout"
projectId={projectId}
featureId={featureId}
environmentId={environmentId}
onAfterAddStrategy={onAfterAddStrategy}
Icon={getFeatureStrategyIcon(
'flexibleRollout'
)}
strategy={{
name: 'flexibleRollout',
parameters: {
rollout: '50',
stickiness: 'default',
groupId: feature.name,
},
constraints: [],
}}
>
Roll out to a percentage of your userbase.
</AddFromTemplateCard>
</Box>
</>
}
/>
</StyledContainer>
</>
);

View File

@ -1,10 +1,13 @@
import React, { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import PermissionButton, {
IPermissionButtonProps,
} from 'component/common/PermissionButton/PermissionButton';
import { CREATE_FEATURE_STRATEGY } from 'component/providers/AccessProvider/permissions';
import { Popover } from '@mui/material';
import { Popover, styled } from '@mui/material';
import { FeatureStrategyMenuCards } from './FeatureStrategyMenuCards/FeatureStrategyMenuCards';
import { formatCreateStrategyPath } from '../FeatureStrategyCreate/FeatureStrategyCreate';
import { MoreVert } from '@mui/icons-material';
interface IFeatureStrategyMenuProps {
label: string;
@ -13,17 +16,30 @@ interface IFeatureStrategyMenuProps {
environmentId: string;
variant?: IPermissionButtonProps['variant'];
matchWidth?: boolean;
size?: IPermissionButtonProps['size'];
}
const StyledAdditionalMenuButton = styled(PermissionButton)(({ theme }) => ({
minWidth: 0,
width: theme.spacing(4.5),
alignItems: 'center',
justifyContent: 'center',
align: 'center',
flexDirection: 'column',
marginLeft: theme.spacing(1),
}));
export const FeatureStrategyMenu = ({
label,
projectId,
featureId,
environmentId,
variant,
size,
matchWidth,
}: IFeatureStrategyMenuProps) => {
const [anchor, setAnchor] = useState<Element>();
const navigate = useNavigate();
const isPopoverOpen = Boolean(anchor);
const popoverId = isPopoverOpen ? 'FeatureStrategyMenuPopover' : undefined;
@ -35,25 +51,55 @@ export const FeatureStrategyMenu = ({
setAnchor(event.currentTarget);
};
const createStrategyPath = formatCreateStrategyPath(
projectId,
featureId,
environmentId,
'flexibleRollout',
true
);
return (
<div onClick={event => event.stopPropagation()}>
<PermissionButton
permission={CREATE_FEATURE_STRATEGY}
projectId={projectId}
environmentId={environmentId}
onClick={onClick}
onClick={() => navigate(createStrategyPath)}
aria-labelledby={popoverId}
variant={variant}
size={size}
sx={{ minWidth: matchWidth ? '282px' : 'auto' }}
>
{label}
</PermissionButton>
<StyledAdditionalMenuButton
permission={CREATE_FEATURE_STRATEGY}
projectId={projectId}
environmentId={environmentId}
onClick={onClick}
aria-labelledby={popoverId}
variant="outlined"
size={size}
hideLockIcon
tooltipProps={{
title: 'More strategies',
}}
>
<MoreVert sx={theme => ({ margin: theme.spacing(0.25, 0) })} />
</StyledAdditionalMenuButton>
<Popover
id={popoverId}
open={isPopoverOpen}
anchorEl={anchor}
onClose={onClose}
onClick={onClose}
PaperProps={{
sx: theme => ({
paddingBottom: theme.spacing(1),
}),
}}
>
<FeatureStrategyMenuCards
projectId={projectId}

View File

@ -0,0 +1,70 @@
import React, { useState } from 'react';
import PermissionButton, {
IPermissionButtonProps,
} from 'component/common/PermissionButton/PermissionButton';
import { CREATE_FEATURE_STRATEGY } from 'component/providers/AccessProvider/permissions';
import { Popover } from '@mui/material';
import { FeatureStrategyMenuCards } from './FeatureStrategyMenuCards/FeatureStrategyMenuCards';
interface IFeatureStrategyMenuProps {
label: string;
projectId: string;
featureId: string;
environmentId: string;
variant?: IPermissionButtonProps['variant'];
matchWidth?: boolean;
}
/**
* Remove when removing feature flag strategySplittedButton
* @deprecated
*/
export const LegacyFeatureStrategyMenu = ({
label,
projectId,
featureId,
environmentId,
variant,
matchWidth,
}: IFeatureStrategyMenuProps) => {
const [anchor, setAnchor] = useState<Element>();
const isPopoverOpen = Boolean(anchor);
const popoverId = isPopoverOpen ? 'FeatureStrategyMenuPopover' : undefined;
const onClose = () => {
setAnchor(undefined);
};
const onClick = (event: React.SyntheticEvent) => {
setAnchor(event.currentTarget);
};
return (
<div onClick={event => event.stopPropagation()}>
<PermissionButton
permission={CREATE_FEATURE_STRATEGY}
projectId={projectId}
environmentId={environmentId}
onClick={onClick}
aria-labelledby={popoverId}
variant={variant}
sx={{ minWidth: matchWidth ? '282px' : 'auto' }}
>
{label}
</PermissionButton>
<Popover
id={popoverId}
open={isPopoverOpen}
anchorEl={anchor}
onClose={onClose}
onClick={onClose}
>
<FeatureStrategyMenuCards
projectId={projectId}
featureId={featureId}
environmentId={environmentId}
/>
</Popover>
</div>
);
};

View File

@ -17,11 +17,13 @@ import EnvironmentAccordionBody from './EnvironmentAccordionBody/EnvironmentAcco
import { EnvironmentFooter } from './EnvironmentFooter/EnvironmentFooter';
import FeatureOverviewEnvironmentMetrics from './FeatureOverviewEnvironmentMetrics/FeatureOverviewEnvironmentMetrics';
import { FeatureStrategyMenu } from 'component/feature/FeatureStrategy/FeatureStrategyMenu/FeatureStrategyMenu';
import { LegacyFeatureStrategyMenu } from 'component/feature/FeatureStrategy/FeatureStrategyMenu/LegacyFeatureStrategyMenu';
import { FEATURE_ENVIRONMENT_ACCORDION } from 'utils/testIds';
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
import { FeatureStrategyIcons } from 'component/feature/FeatureStrategy/FeatureStrategyIcons/FeatureStrategyIcons';
import { useGlobalLocalStorage } from 'hooks/useGlobalLocalStorage';
import { Badge } from 'component/common/Badge/Badge';
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
interface IFeatureOverviewEnvironmentProps {
env: IFeatureEnvironment;
@ -104,7 +106,10 @@ const StyledStringTruncator = styled(StringTruncator)(({ theme }) => ({
},
}));
const StyledContainer = styled('div')(({ theme }) => ({
/**
* @deprecated
*/
const LegacyStyledButtonContainer = styled('div')(({ theme }) => ({
display: 'flex',
alignItems: 'center',
marginLeft: '1.8rem',
@ -114,6 +119,15 @@ const StyledContainer = styled('div')(({ theme }) => ({
},
}));
const StyledButtonContainer = styled('div')(({ theme }) => ({
display: 'flex',
alignItems: 'center',
marginTop: theme.spacing(2),
[theme.breakpoints.down(560)]: {
flexDirection: 'column',
},
}));
const FeatureOverviewEnvironment = ({
env,
}: IFeatureOverviewEnvironmentProps) => {
@ -122,6 +136,8 @@ const FeatureOverviewEnvironment = ({
const { metrics } = useFeatureMetrics(projectId, featureId);
const { feature } = useFeature(projectId, featureId);
const { value: globalStore } = useGlobalLocalStorage();
const { uiConfig } = useUiConfig();
const strategySplittedButton = uiConfig?.flags?.strategySplittedButton;
const featureMetrics = getFeatureMetrics(feature?.environments, metrics);
const environmentMetric = featureMetrics.find(
@ -171,20 +187,37 @@ const FeatureOverviewEnvironment = ({
}
/>
</StyledHeaderTitle>
<StyledContainer>
<FeatureStrategyMenu
label="Add strategy"
projectId={projectId}
featureId={featureId}
environmentId={env.name}
variant="text"
/>
<FeatureStrategyIcons
strategies={
featureEnvironment?.strategies
}
/>
</StyledContainer>
<ConditionallyRender
condition={Boolean(strategySplittedButton)}
show={
<StyledButtonContainer>
<FeatureStrategyMenu
label="Add strategy"
projectId={projectId}
featureId={featureId}
environmentId={env.name}
variant="outlined"
size="small"
/>
</StyledButtonContainer>
}
elseShow={
<LegacyStyledButtonContainer>
<LegacyFeatureStrategyMenu
label="Add strategy"
projectId={projectId}
featureId={featureId}
environmentId={env.name}
variant="text"
/>
<FeatureStrategyIcons
strategies={
featureEnvironment?.strategies
}
/>
</LegacyStyledButtonContainer>
}
/>
</StyledHeader>
<FeatureOverviewEnvironmentMetrics
@ -215,11 +248,26 @@ const FeatureOverviewEnvironment = ({
py: 1,
}}
>
<FeatureStrategyMenu
label="Add strategy"
projectId={projectId}
featureId={featureId}
environmentId={env.name}
<ConditionallyRender
condition={Boolean(
strategySplittedButton
)}
show={
<FeatureStrategyMenu
label="Add strategy"
projectId={projectId}
featureId={featureId}
environmentId={env.name}
/>
}
elseShow={
<LegacyFeatureStrategyMenu
label="Add strategy"
projectId={projectId}
featureId={featureId}
environmentId={env.name}
/>
}
/>
</Box>
<EnvironmentFooter

View File

@ -54,6 +54,7 @@ export interface IFlags {
disableNotifications?: boolean;
advancedPlayground?: boolean;
customRootRoles?: boolean;
strategySplittedButton?: boolean;
}
export interface IVersionInfo {

View File

@ -96,6 +96,7 @@ exports[`should create default config 1`] = `
"responseTimeWithAppNameKillSwitch": false,
"segmentContextFieldUsage": false,
"strategyImprovements": false,
"strategySplittedButton": false,
"strictSchemaValidation": false,
},
},
@ -130,6 +131,7 @@ exports[`should create default config 1`] = `
"responseTimeWithAppNameKillSwitch": false,
"segmentContextFieldUsage": false,
"strategyImprovements": false,
"strategySplittedButton": false,
"strictSchemaValidation": false,
},
"externalResolver": {

View File

@ -25,7 +25,8 @@ export type IFlagKey =
| 'segmentContextFieldUsage'
| 'disableNotifications'
| 'advancedPlayground'
| 'customRootRoles';
| 'customRootRoles'
| 'strategySplittedButton';
export type IFlags = Partial<{ [key in IFlagKey]: boolean | Variant }>;
@ -90,6 +91,10 @@ const flags: IFlags = {
process.env.UNLEASH_STRATEGY_IMPROVEMENTS,
false,
),
strategySplittedButton: parseEnvVarBoolean(
process.env.UNLEASH_STRATEGY_SPLITTED_BUTTON,
false,
),
googleAuthEnabled: parseEnvVarBoolean(
process.env.GOOGLE_AUTH_ENABLED,
false,