1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-06-18 01:18:23 +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,6 +171,9 @@ export const FeatureStrategyEmpty = ({
justifyContent: 'center', justifyContent: 'center',
}} }}
> >
<ConditionallyRender
condition={Boolean(strategySplittedButton)}
show={
<FeatureStrategyMenu <FeatureStrategyMenu
label="Add your first strategy" label="Add your first strategy"
projectId={projectId} projectId={projectId}
@ -173,6 +181,17 @@ export const FeatureStrategyEmpty = ({
environmentId={environmentId} environmentId={environmentId}
matchWidth={canCopyFromOtherEnvironment} matchWidth={canCopyFromOtherEnvironment}
/> />
}
elseShow={
<LegacyFeatureStrategyMenu
label="Add your first strategy"
projectId={projectId}
featureId={featureId}
environmentId={environmentId}
matchWidth={canCopyFromOtherEnvironment}
/>
}
/>
<ConditionallyRender <ConditionallyRender
condition={canCopyFromOtherEnvironment} condition={canCopyFromOtherEnvironment}
show={ show={
@ -186,6 +205,10 @@ export const FeatureStrategyEmpty = ({
} }
/> />
</Box> </Box>
<ConditionallyRender
condition={strategySplittedButton === false}
show={
<>
<Box sx={{ width: '100%', mt: 3 }}> <Box sx={{ width: '100%', mt: 3 }}>
<SectionSeparator> <SectionSeparator>
Or use a strategy template Or use a strategy template
@ -196,7 +219,10 @@ export const FeatureStrategyEmpty = ({
display: 'grid', display: 'grid',
width: '100%', width: '100%',
gap: 2, gap: 2,
gridTemplateColumns: { xs: '1fr', sm: '1fr 1fr' }, gridTemplateColumns: {
xs: '1fr',
sm: '1fr 1fr',
},
}} }}
> >
<AddFromTemplateCard <AddFromTemplateCard
@ -212,8 +238,8 @@ export const FeatureStrategyEmpty = ({
constraints: [], constraints: [],
}} }}
> >
The standard strategy is strictly on/off for your entire The standard strategy is strictly on/off for
userbase. your entire userbase.
</AddFromTemplateCard> </AddFromTemplateCard>
<AddFromTemplateCard <AddFromTemplateCard
title="Gradual rollout" title="Gradual rollout"
@ -221,7 +247,9 @@ export const FeatureStrategyEmpty = ({
featureId={featureId} featureId={featureId}
environmentId={environmentId} environmentId={environmentId}
onAfterAddStrategy={onAfterAddStrategy} onAfterAddStrategy={onAfterAddStrategy}
Icon={getFeatureStrategyIcon('flexibleRollout')} Icon={getFeatureStrategyIcon(
'flexibleRollout'
)}
strategy={{ strategy={{
name: 'flexibleRollout', name: 'flexibleRollout',
parameters: { parameters: {
@ -235,6 +263,9 @@ export const FeatureStrategyEmpty = ({
Roll out to a percentage of your userbase. Roll out to a percentage of your userbase.
</AddFromTemplateCard> </AddFromTemplateCard>
</Box> </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,8 +187,23 @@ const FeatureOverviewEnvironment = ({
} }
/> />
</StyledHeaderTitle> </StyledHeaderTitle>
<StyledContainer> <ConditionallyRender
condition={Boolean(strategySplittedButton)}
show={
<StyledButtonContainer>
<FeatureStrategyMenu <FeatureStrategyMenu
label="Add strategy"
projectId={projectId}
featureId={featureId}
environmentId={env.name}
variant="outlined"
size="small"
/>
</StyledButtonContainer>
}
elseShow={
<LegacyStyledButtonContainer>
<LegacyFeatureStrategyMenu
label="Add strategy" label="Add strategy"
projectId={projectId} projectId={projectId}
featureId={featureId} featureId={featureId}
@ -184,7 +215,9 @@ const FeatureOverviewEnvironment = ({
featureEnvironment?.strategies featureEnvironment?.strategies
} }
/> />
</StyledContainer> </LegacyStyledButtonContainer>
}
/>
</StyledHeader> </StyledHeader>
<FeatureOverviewEnvironmentMetrics <FeatureOverviewEnvironmentMetrics
@ -215,12 +248,27 @@ const FeatureOverviewEnvironment = ({
py: 1, py: 1,
}} }}
> >
<ConditionallyRender
condition={Boolean(
strategySplittedButton
)}
show={
<FeatureStrategyMenu <FeatureStrategyMenu
label="Add strategy" label="Add strategy"
projectId={projectId} projectId={projectId}
featureId={featureId} featureId={featureId}
environmentId={env.name} environmentId={env.name}
/> />
}
elseShow={
<LegacyFeatureStrategyMenu
label="Add strategy"
projectId={projectId}
featureId={featureId}
environmentId={env.name}
/>
}
/>
</Box> </Box>
<EnvironmentFooter <EnvironmentFooter
environmentMetric={ environmentMetric={

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,