1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-04-19 01:17:18 +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; projectId?: string;
environmentId?: string; environmentId?: string;
tooltipProps?: Omit<ITooltipResolverProps, 'children'>; tooltipProps?: Omit<ITooltipResolverProps, 'children'>;
hideLockIcon?: boolean;
} }
interface IPermissionBaseButtonProps extends IPermissionButtonProps { interface IPermissionBaseButtonProps extends IPermissionButtonProps {
@ -68,6 +69,7 @@ const BasePermissionButton: React.FC<IPermissionBaseButtonProps> =
projectId, projectId,
environmentId, environmentId,
tooltipProps, tooltipProps,
hideLockIcon,
...rest ...rest
}, },
ref ref
@ -92,7 +94,7 @@ const BasePermissionButton: React.FC<IPermissionBaseButtonProps> =
endIcon={ endIcon={
<> <>
<ConditionallyRender <ConditionallyRender
condition={!access} condition={!access && !hideLockIcon}
show={<Lock titleAccess="Locked" />} show={<Lock titleAccess="Locked" />}
elseShow={ elseShow={
Boolean(rest.endIcon) && Boolean(rest.endIcon) &&

View File

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

View File

@ -1,10 +1,13 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import PermissionButton, { import PermissionButton, {
IPermissionButtonProps, IPermissionButtonProps,
} from 'component/common/PermissionButton/PermissionButton'; } from 'component/common/PermissionButton/PermissionButton';
import { CREATE_FEATURE_STRATEGY } from 'component/providers/AccessProvider/permissions'; 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 { FeatureStrategyMenuCards } from './FeatureStrategyMenuCards/FeatureStrategyMenuCards';
import { formatCreateStrategyPath } from '../FeatureStrategyCreate/FeatureStrategyCreate';
import { MoreVert } from '@mui/icons-material';
interface IFeatureStrategyMenuProps { interface IFeatureStrategyMenuProps {
label: string; label: string;
@ -13,17 +16,30 @@ interface IFeatureStrategyMenuProps {
environmentId: string; environmentId: string;
variant?: IPermissionButtonProps['variant']; variant?: IPermissionButtonProps['variant'];
matchWidth?: boolean; 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 = ({ export const FeatureStrategyMenu = ({
label, label,
projectId, projectId,
featureId, featureId,
environmentId, environmentId,
variant, variant,
size,
matchWidth, matchWidth,
}: IFeatureStrategyMenuProps) => { }: IFeatureStrategyMenuProps) => {
const [anchor, setAnchor] = useState<Element>(); const [anchor, setAnchor] = useState<Element>();
const navigate = useNavigate();
const isPopoverOpen = Boolean(anchor); const isPopoverOpen = Boolean(anchor);
const popoverId = isPopoverOpen ? 'FeatureStrategyMenuPopover' : undefined; const popoverId = isPopoverOpen ? 'FeatureStrategyMenuPopover' : undefined;
@ -35,25 +51,55 @@ export const FeatureStrategyMenu = ({
setAnchor(event.currentTarget); setAnchor(event.currentTarget);
}; };
const createStrategyPath = formatCreateStrategyPath(
projectId,
featureId,
environmentId,
'flexibleRollout',
true
);
return ( return (
<div onClick={event => event.stopPropagation()}> <div onClick={event => event.stopPropagation()}>
<PermissionButton <PermissionButton
permission={CREATE_FEATURE_STRATEGY} permission={CREATE_FEATURE_STRATEGY}
projectId={projectId} projectId={projectId}
environmentId={environmentId} environmentId={environmentId}
onClick={onClick} onClick={() => navigate(createStrategyPath)}
aria-labelledby={popoverId} aria-labelledby={popoverId}
variant={variant} variant={variant}
size={size}
sx={{ minWidth: matchWidth ? '282px' : 'auto' }} sx={{ minWidth: matchWidth ? '282px' : 'auto' }}
> >
{label} {label}
</PermissionButton> </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 <Popover
id={popoverId} id={popoverId}
open={isPopoverOpen} open={isPopoverOpen}
anchorEl={anchor} anchorEl={anchor}
onClose={onClose} onClose={onClose}
onClick={onClose} onClick={onClose}
PaperProps={{
sx: theme => ({
paddingBottom: theme.spacing(1),
}),
}}
> >
<FeatureStrategyMenuCards <FeatureStrategyMenuCards
projectId={projectId} 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 { EnvironmentFooter } from './EnvironmentFooter/EnvironmentFooter';
import FeatureOverviewEnvironmentMetrics from './FeatureOverviewEnvironmentMetrics/FeatureOverviewEnvironmentMetrics'; import FeatureOverviewEnvironmentMetrics from './FeatureOverviewEnvironmentMetrics/FeatureOverviewEnvironmentMetrics';
import { FeatureStrategyMenu } from 'component/feature/FeatureStrategy/FeatureStrategyMenu/FeatureStrategyMenu'; import { FeatureStrategyMenu } from 'component/feature/FeatureStrategy/FeatureStrategyMenu/FeatureStrategyMenu';
import { LegacyFeatureStrategyMenu } from 'component/feature/FeatureStrategy/FeatureStrategyMenu/LegacyFeatureStrategyMenu';
import { FEATURE_ENVIRONMENT_ACCORDION } from 'utils/testIds'; import { FEATURE_ENVIRONMENT_ACCORDION } from 'utils/testIds';
import { useRequiredPathParam } from 'hooks/useRequiredPathParam'; import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
import { FeatureStrategyIcons } from 'component/feature/FeatureStrategy/FeatureStrategyIcons/FeatureStrategyIcons'; import { FeatureStrategyIcons } from 'component/feature/FeatureStrategy/FeatureStrategyIcons/FeatureStrategyIcons';
import { useGlobalLocalStorage } from 'hooks/useGlobalLocalStorage'; import { useGlobalLocalStorage } from 'hooks/useGlobalLocalStorage';
import { Badge } from 'component/common/Badge/Badge'; import { Badge } from 'component/common/Badge/Badge';
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
interface IFeatureOverviewEnvironmentProps { interface IFeatureOverviewEnvironmentProps {
env: IFeatureEnvironment; env: IFeatureEnvironment;
@ -104,7 +106,10 @@ const StyledStringTruncator = styled(StringTruncator)(({ theme }) => ({
}, },
})); }));
const StyledContainer = styled('div')(({ theme }) => ({ /**
* @deprecated
*/
const LegacyStyledButtonContainer = styled('div')(({ theme }) => ({
display: 'flex', display: 'flex',
alignItems: 'center', alignItems: 'center',
marginLeft: '1.8rem', 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 = ({ const FeatureOverviewEnvironment = ({
env, env,
}: IFeatureOverviewEnvironmentProps) => { }: IFeatureOverviewEnvironmentProps) => {
@ -122,6 +136,8 @@ const FeatureOverviewEnvironment = ({
const { metrics } = useFeatureMetrics(projectId, featureId); const { metrics } = useFeatureMetrics(projectId, featureId);
const { feature } = useFeature(projectId, featureId); const { feature } = useFeature(projectId, featureId);
const { value: globalStore } = useGlobalLocalStorage(); const { value: globalStore } = useGlobalLocalStorage();
const { uiConfig } = useUiConfig();
const strategySplittedButton = uiConfig?.flags?.strategySplittedButton;
const featureMetrics = getFeatureMetrics(feature?.environments, metrics); const featureMetrics = getFeatureMetrics(feature?.environments, metrics);
const environmentMetric = featureMetrics.find( const environmentMetric = featureMetrics.find(
@ -171,20 +187,37 @@ const FeatureOverviewEnvironment = ({
} }
/> />
</StyledHeaderTitle> </StyledHeaderTitle>
<StyledContainer> <ConditionallyRender
<FeatureStrategyMenu condition={Boolean(strategySplittedButton)}
label="Add strategy" show={
projectId={projectId} <StyledButtonContainer>
featureId={featureId} <FeatureStrategyMenu
environmentId={env.name} label="Add strategy"
variant="text" projectId={projectId}
/> featureId={featureId}
<FeatureStrategyIcons environmentId={env.name}
strategies={ variant="outlined"
featureEnvironment?.strategies size="small"
} />
/> </StyledButtonContainer>
</StyledContainer> }
elseShow={
<LegacyStyledButtonContainer>
<LegacyFeatureStrategyMenu
label="Add strategy"
projectId={projectId}
featureId={featureId}
environmentId={env.name}
variant="text"
/>
<FeatureStrategyIcons
strategies={
featureEnvironment?.strategies
}
/>
</LegacyStyledButtonContainer>
}
/>
</StyledHeader> </StyledHeader>
<FeatureOverviewEnvironmentMetrics <FeatureOverviewEnvironmentMetrics
@ -215,11 +248,26 @@ const FeatureOverviewEnvironment = ({
py: 1, py: 1,
}} }}
> >
<FeatureStrategyMenu <ConditionallyRender
label="Add strategy" condition={Boolean(
projectId={projectId} strategySplittedButton
featureId={featureId} )}
environmentId={env.name} 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> </Box>
<EnvironmentFooter <EnvironmentFooter

View File

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

View File

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

View File

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