1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-09-10 17:53:36 +02:00

chore(1-3389): new env strategy containers (#9361)

Updates the strategy list based on the new designs and moves the current
versions of the touched components into `Legacy...` files (the vast
majority of changes are that and updating imports). The relevant changes
to the components are listed in their original files.

Flag on:

![image](https://github.com/user-attachments/assets/cd49c283-6044-46d4-bcef-182cb6a1de4e)

Flag off:

![image](https://github.com/user-attachments/assets/7ef92b6d-31e5-4218-90b2-dedd5e6cc6de)

## Next steps

There's two items to review for improving these current comments (also
noted inline):
- Whether to aria-hide the "or" separator or not (I need to read up a
bit and think whether it makes sense to show that or not)
- Changing the list of strategies into an actual ordered list (`ol`).
That'd reflect the semantics better.

Next would be checking the other places we use strategy lists and then
updating those too. In doing so, I might find that some things need to
be updated, but I'll handle those when I get there.

There's also handling release plans.
This commit is contained in:
Thomas Heartman 2025-02-26 16:24:50 +01:00 committed by GitHub
parent 192bd83fa6
commit e25fb9f7c0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
27 changed files with 1009 additions and 105 deletions

View File

@ -15,7 +15,7 @@ import { type IUseWeakMap, useWeakMap } from 'hooks/useWeakMap';
import { objectId } from 'utils/objectId'; import { objectId } from 'utils/objectId';
import { createEmptyConstraint } from 'component/common/ConstraintAccordion/ConstraintAccordionList/createEmptyConstraint'; import { createEmptyConstraint } from 'component/common/ConstraintAccordion/ConstraintAccordionList/createEmptyConstraint';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { StrategySeparator } from 'component/common/StrategySeparator/StrategySeparator'; import { StrategySeparator } from 'component/common/StrategySeparator/LegacyStrategySeparator';
export interface IConstraintAccordionListProps { export interface IConstraintAccordionListProps {
constraints: IConstraint[]; constraints: IConstraint[];

View File

@ -11,7 +11,7 @@ import {
createEmptyConstraint, createEmptyConstraint,
} from 'component/common/ConstraintAccordion/ConstraintAccordionList/createEmptyConstraint'; } from 'component/common/ConstraintAccordion/ConstraintAccordionList/createEmptyConstraint';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { StrategySeparator } from 'component/common/StrategySeparator/StrategySeparator'; import { StrategySeparator } from 'component/common/StrategySeparator/LegacyStrategySeparator';
import { NewConstraintAccordion } from 'component/common/NewConstraintAccordion/NewConstraintAccordion'; import { NewConstraintAccordion } from 'component/common/NewConstraintAccordion/NewConstraintAccordion';
export interface IConstraintAccordionListProps { export interface IConstraintAccordionListProps {

View File

@ -0,0 +1,205 @@
// deprecated; remove with the `flagOverviewRedesign` flag
import type React from 'react';
import type { DragEventHandler, FC, ReactNode } from 'react';
import DragIndicator from '@mui/icons-material/DragIndicator';
import { Box, IconButton, styled } from '@mui/material';
import type { IFeatureStrategy } from 'interfaces/strategy';
import {
formatStrategyName,
getFeatureStrategyIcon,
} from 'utils/strategyNames';
import StringTruncator from 'component/common/StringTruncator/StringTruncator';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import type { PlaygroundStrategySchema } from 'openapi';
import { Badge } from '../Badge/Badge';
import { Link } from 'react-router-dom';
interface IStrategyItemContainerProps {
strategy: IFeatureStrategy | PlaygroundStrategySchema;
onDragStart?: DragEventHandler<HTMLButtonElement>;
onDragEnd?: DragEventHandler<HTMLButtonElement>;
actions?: ReactNode;
orderNumber?: number;
className?: string;
style?: React.CSSProperties;
description?: string;
children?: React.ReactNode;
}
const DragIcon = styled(IconButton)({
padding: 0,
cursor: 'inherit',
transition: 'color 0.2s ease-in-out',
});
const StyledIndexLabel = styled('div')(({ theme }) => ({
fontSize: theme.typography.fontSize,
color: theme.palette.text.secondary,
position: 'absolute',
display: 'none',
right: 'calc(100% + 6px)',
top: theme.spacing(2.5),
[theme.breakpoints.up('md')]: {
display: 'block',
},
}));
const StyledDescription = styled('div')(({ theme }) => ({
fontSize: theme.typography.fontSize,
fontWeight: 'normal',
color: theme.palette.text.secondary,
display: 'none',
top: theme.spacing(2.5),
[theme.breakpoints.up('md')]: {
display: 'block',
},
}));
const StyledCustomTitle = styled('div')(({ theme }) => ({
fontWeight: 'normal',
display: 'none',
[theme.breakpoints.up('md')]: {
display: 'block',
},
}));
const StyledHeaderContainer = styled('div')({
flexDirection: 'column',
justifyContent: 'center',
verticalAlign: 'middle',
});
const StyledContainer = styled(Box, {
shouldForwardProp: (prop) => prop !== 'disabled',
})<{ disabled?: boolean }>(({ theme, disabled }) => ({
borderRadius: theme.shape.borderRadiusMedium,
border: `1px solid ${theme.palette.divider}`,
'& + &': {
marginTop: theme.spacing(2),
},
background: disabled
? theme.palette.envAccordion.disabled
: theme.palette.background.paper,
}));
const StyledHeader = styled('div', {
shouldForwardProp: (prop) => prop !== 'draggable' && prop !== 'disabled',
})<{ draggable: boolean; disabled: boolean }>(
({ theme, draggable, disabled }) => ({
padding: theme.spacing(0.5, 2),
display: 'flex',
gap: theme.spacing(1),
alignItems: 'center',
borderBottom: `1px solid ${theme.palette.divider}`,
fontWeight: theme.typography.fontWeightMedium,
paddingLeft: draggable ? theme.spacing(1) : theme.spacing(2),
color: disabled
? theme.palette.text.secondary
: theme.palette.text.primary,
}),
);
export const StrategyItemContainer: FC<IStrategyItemContainerProps> = ({
strategy,
onDragStart,
onDragEnd,
actions,
children,
orderNumber,
style = {},
description,
}) => {
const Icon = getFeatureStrategyIcon(strategy.name);
const StrategyHeaderLink: React.FC<{ children?: React.ReactNode }> =
'links' in strategy
? ({ children }) => <Link to={strategy.links.edit}>{children}</Link>
: ({ children }) => <> {children} </>;
return (
<Box sx={{ position: 'relative' }}>
<ConditionallyRender
condition={orderNumber !== undefined}
show={<StyledIndexLabel>{orderNumber}</StyledIndexLabel>}
/>
<StyledContainer
disabled={strategy?.disabled || false}
style={style}
>
<StyledHeader
draggable={Boolean(onDragStart)}
disabled={Boolean(strategy?.disabled)}
>
<ConditionallyRender
condition={Boolean(onDragStart)}
show={() => (
<DragIcon
draggable
disableRipple
size='small'
onDragStart={onDragStart}
onDragEnd={onDragEnd}
sx={{ cursor: 'move' }}
>
<DragIndicator
titleAccess='Drag to reorder'
cursor='grab'
sx={{ color: 'action.active' }}
/>
</DragIcon>
)}
/>
<Icon
sx={{
fill: (theme) => theme.palette.action.disabled,
}}
/>
<StyledHeaderContainer>
<StrategyHeaderLink>
<StringTruncator
maxWidth='400'
maxLength={15}
text={formatStrategyName(String(strategy.name))}
/>
<ConditionallyRender
condition={Boolean(strategy.title)}
show={
<StyledCustomTitle>
{formatStrategyName(
String(strategy.title),
)}
</StyledCustomTitle>
}
/>
</StrategyHeaderLink>
<ConditionallyRender
condition={Boolean(description)}
show={
<StyledDescription>
{description}
</StyledDescription>
}
/>
</StyledHeaderContainer>
<ConditionallyRender
condition={Boolean(strategy?.disabled)}
show={() => (
<>
<Badge color='disabled'>Disabled</Badge>
</>
)}
/>
<Box
sx={{
marginLeft: 'auto',
display: 'flex',
minHeight: (theme) => theme.spacing(6),
alignItems: 'center',
}}
>
{actions}
</Box>
</StyledHeader>
<Box sx={{ p: 2 }}>{children}</Box>
</StyledContainer>
</Box>
);
};

View File

@ -1,6 +1,6 @@
import { screen } from '@testing-library/react'; import { screen } from '@testing-library/react';
import { render } from 'utils/testRenderer'; import { render } from 'utils/testRenderer';
import { StrategyItemContainer } from './StrategyItemContainer'; import { StrategyItemContainer } from './LegacyStrategyItemContainer';
import type { IFeatureStrategy } from 'interfaces/strategy'; import type { IFeatureStrategy } from 'interfaces/strategy';
test('should render strategy name, custom title and description', async () => { test('should render strategy name, custom title and description', async () => {

View File

@ -31,17 +31,6 @@ const DragIcon = styled(IconButton)({
transition: 'color 0.2s ease-in-out', transition: 'color 0.2s ease-in-out',
}); });
const StyledIndexLabel = styled('div')(({ theme }) => ({
fontSize: theme.typography.fontSize,
color: theme.palette.text.secondary,
position: 'absolute',
display: 'none',
right: 'calc(100% + 6px)',
top: theme.spacing(2.5),
[theme.breakpoints.up('md')]: {
display: 'block',
},
}));
const StyledDescription = styled('div')(({ theme }) => ({ const StyledDescription = styled('div')(({ theme }) => ({
fontSize: theme.typography.fontSize, fontSize: theme.typography.fontSize,
fontWeight: 'normal', fontWeight: 'normal',
@ -65,20 +54,13 @@ const StyledHeaderContainer = styled('div')({
verticalAlign: 'middle', verticalAlign: 'middle',
}); });
const StyledContainer = styled(Box, { const NewStyledContainer = styled(Box, {
shouldForwardProp: (prop) => prop !== 'disabled', shouldForwardProp: (prop) => prop !== 'disabled',
})<{ disabled?: boolean }>(({ theme, disabled }) => ({ })({
borderRadius: theme.shape.borderRadiusMedium, background: 'inherit',
border: `1px solid ${theme.palette.divider}`, });
'& + &': {
marginTop: theme.spacing(2),
},
background: disabled
? theme.palette.envAccordion.disabled
: theme.palette.background.paper,
}));
const StyledHeader = styled('div', { const NewStyledHeader = styled('div', {
shouldForwardProp: (prop) => prop !== 'draggable' && prop !== 'disabled', shouldForwardProp: (prop) => prop !== 'draggable' && prop !== 'disabled',
})<{ draggable: boolean; disabled: boolean }>( })<{ draggable: boolean; disabled: boolean }>(
({ theme, draggable, disabled }) => ({ ({ theme, draggable, disabled }) => ({
@ -86,7 +68,6 @@ const StyledHeader = styled('div', {
display: 'flex', display: 'flex',
gap: theme.spacing(1), gap: theme.spacing(1),
alignItems: 'center', alignItems: 'center',
borderBottom: `1px solid ${theme.palette.divider}`,
fontWeight: theme.typography.fontWeightMedium, fontWeight: theme.typography.fontWeightMedium,
paddingLeft: draggable ? theme.spacing(1) : theme.spacing(2), paddingLeft: draggable ? theme.spacing(1) : theme.spacing(2),
color: disabled color: disabled
@ -101,7 +82,6 @@ export const StrategyItemContainer: FC<IStrategyItemContainerProps> = ({
onDragEnd, onDragEnd,
actions, actions,
children, children,
orderNumber,
style = {}, style = {},
description, description,
}) => { }) => {
@ -114,15 +94,8 @@ export const StrategyItemContainer: FC<IStrategyItemContainerProps> = ({
return ( return (
<Box sx={{ position: 'relative' }}> <Box sx={{ position: 'relative' }}>
<ConditionallyRender <NewStyledContainer style={style}>
condition={orderNumber !== undefined} <NewStyledHeader
show={<StyledIndexLabel>{orderNumber}</StyledIndexLabel>}
/>
<StyledContainer
disabled={strategy?.disabled || false}
style={style}
>
<StyledHeader
draggable={Boolean(onDragStart)} draggable={Boolean(onDragStart)}
disabled={Boolean(strategy?.disabled)} disabled={Boolean(strategy?.disabled)}
> >
@ -196,9 +169,9 @@ export const StrategyItemContainer: FC<IStrategyItemContainerProps> = ({
> >
{actions} {actions}
</Box> </Box>
</StyledHeader> </NewStyledHeader>
<Box sx={{ p: 2 }}>{children}</Box> <Box sx={{ p: 2, pt: 0 }}>{children}</Box>
</StyledContainer> </NewStyledContainer>
</Box> </Box>
); );
}; };

View File

@ -0,0 +1,52 @@
// deprecated; remove with the `flagOverviewRedesign` flag
import { Box, styled, useTheme } from '@mui/material';
import { ConditionallyRender } from '../ConditionallyRender/ConditionallyRender';
interface IStrategySeparatorProps {
text: 'AND' | 'OR';
}
const StyledContent = styled('div')(({ theme }) => ({
padding: theme.spacing(0.75, 1),
color: theme.palette.text.primary,
fontSize: theme.fontSizes.smallerBody,
backgroundColor: theme.palette.background.elevation2,
borderRadius: theme.shape.borderRadius,
position: 'absolute',
zIndex: theme.zIndex.fab,
top: '50%',
left: theme.spacing(2),
transform: 'translateY(-50%)',
lineHeight: 1,
}));
const StyledCenteredContent = styled(StyledContent)(({ theme }) => ({
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
backgroundColor: theme.palette.seen.primary,
borderRadius: theme.shape.borderRadiusLarge,
padding: theme.spacing(0.75, 1.5),
}));
export const StrategySeparator = ({ text }: IStrategySeparatorProps) => {
const theme = useTheme();
return (
<Box
sx={{
height: theme.spacing(text === 'AND' ? 1 : 1.5),
position: 'relative',
width: '100%',
}}
>
<ConditionallyRender
condition={text === 'AND'}
show={() => <StyledContent>{text}</StyledContent>}
elseShow={() => (
<StyledCenteredContent>{text}</StyledCenteredContent>
)}
/>
</Box>
);
};

View File

@ -1,51 +1,57 @@
import { Box, styled, useTheme } from '@mui/material'; import { Box, styled, useTheme } from '@mui/material';
import { ConditionallyRender } from '../ConditionallyRender/ConditionallyRender';
interface IStrategySeparatorProps { interface IStrategySeparatorProps {
text: 'AND' | 'OR'; text: 'AND' | 'OR';
} }
const StyledContent = styled('div')(({ theme }) => ({ const StyledAnd = styled('div')(({ theme }) => ({
padding: theme.spacing(0.75, 1), padding: theme.spacing(0.75, 1),
color: theme.palette.text.primary, color: theme.palette.text.primary,
fontSize: theme.fontSizes.smallerBody, fontSize: theme.fontSizes.smallerBody,
backgroundColor: theme.palette.background.elevation2, backgroundColor: theme.palette.background.elevation2,
borderRadius: theme.shape.borderRadius,
position: 'absolute', position: 'absolute',
zIndex: theme.zIndex.fab, zIndex: theme.zIndex.fab,
top: '50%', top: '50%',
left: theme.spacing(2), left: theme.spacing(2),
transform: 'translateY(-50%)', transform: 'translateY(-50%)',
lineHeight: 1, lineHeight: 1,
borderRadius: theme.shape.borderRadiusLarge,
})); }));
const StyledCenteredContent = styled(StyledContent)(({ theme }) => ({ const StyledOr = styled(StyledAnd)(({ theme }) => ({
fontWeight: 'bold',
backgroundColor: theme.palette.background.alternative,
color: theme.palette.primary.contrastText,
left: theme.spacing(4),
}));
const StyledSeparator = styled('hr')(({ theme }) => ({
border: 0,
borderTop: `1px solid ${theme.palette.divider}`,
margin: 0,
position: 'absolute',
top: '50%', top: '50%',
left: '50%', width: '100%',
transform: 'translate(-50%, -50%)',
backgroundColor: theme.palette.seen.primary,
borderRadius: theme.shape.borderRadiusLarge,
padding: theme.spacing(0.75, 1.5),
})); }));
export const StrategySeparator = ({ text }: IStrategySeparatorProps) => { export const StrategySeparator = ({ text }: IStrategySeparatorProps) => {
const theme = useTheme(); const theme = useTheme();
return ( return (
<Box <Box
sx={{ sx={{
height: theme.spacing(text === 'AND' ? 1 : 1.5), height: theme.spacing(text === 'AND' ? 1 : 1.5),
position: 'relative', position: 'relative',
width: '100%',
}} }}
aria-hidden={true}
> >
<ConditionallyRender {text === 'AND' ? (
condition={text === 'AND'} <StyledAnd>{text}</StyledAnd>
show={() => <StyledContent>{text}</StyledContent>} ) : (
elseShow={() => ( <>
<StyledCenteredContent>{text}</StyledCenteredContent> <StyledSeparator />
<StyledOr>{text}</StyledOr>
</>
)} )}
/>
</Box> </Box>
); );
}; };

View File

@ -9,7 +9,7 @@ import useFeatureStrategyApi from 'hooks/api/actions/useFeatureStrategyApi/useFe
import { formatUnknownError } from 'utils/formatUnknownError'; import { formatUnknownError } from 'utils/formatUnknownError';
import useToast from 'hooks/useToast'; import useToast from 'hooks/useToast';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { StrategyDraggableItem } from './StrategyDraggableItem/StrategyDraggableItem'; import { StrategyDraggableItem } from './StrategyDraggableItem/LegacyStrategyDraggableItem';
import type { IFeatureEnvironment } from 'interfaces/featureToggle'; import type { IFeatureEnvironment } from 'interfaces/featureToggle';
import { FeatureStrategyEmpty } from 'component/feature/FeatureStrategy/FeatureStrategyEmpty/FeatureStrategyEmpty'; import { FeatureStrategyEmpty } from 'component/feature/FeatureStrategy/FeatureStrategyEmpty/FeatureStrategyEmpty';
import { useRequiredPathParam } from 'hooks/useRequiredPathParam'; import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
@ -25,6 +25,7 @@ import { useReleasePlans } from 'hooks/api/getters/useReleasePlans/useReleasePla
import { ReleasePlan } from '../../../ReleasePlan/ReleasePlan'; import { ReleasePlan } from '../../../ReleasePlan/ReleasePlan';
import { Badge } from 'component/common/Badge/Badge'; import { Badge } from 'component/common/Badge/Badge';
import { SectionSeparator } from '../SectionSeparator/SectionSeparator'; import { SectionSeparator } from '../SectionSeparator/SectionSeparator';
import { StrategyDraggableItem as NewStrategyDraggableItem } from './StrategyDraggableItem/StrategyDraggableItem';
interface IEnvironmentAccordionBodyProps { interface IEnvironmentAccordionBodyProps {
isDisabled: boolean; isDisabled: boolean;
@ -59,7 +60,7 @@ const AdditionalStrategiesDiv = styled('div')(({ theme }) => ({
marginBottom: theme.spacing(2), marginBottom: theme.spacing(2),
})); }));
const EnvironmentAccordionBody = ({ export const EnvironmentAccordionBody = ({
featureEnvironment, featureEnvironment,
isDisabled, isDisabled,
otherEnvironments, otherEnvironments,
@ -223,18 +224,6 @@ const EnvironmentAccordionBody = ({
return ( return (
<StyledAccordionBody> <StyledAccordionBody>
<StyledAccordionBodyInnerContainer> <StyledAccordionBodyInnerContainer>
<ConditionallyRender
condition={
(releasePlans.length > 0 || strategies.length > 0) &&
isDisabled
}
show={() => (
<Alert severity='warning' sx={{ mb: 2 }}>
This environment is disabled, which means that none
of your strategies are executing.
</Alert>
)}
/>
<ConditionallyRender <ConditionallyRender
condition={releasePlans.length > 0 || strategies.length > 0} condition={releasePlans.length > 0 || strategies.length > 0}
show={ show={
@ -270,7 +259,7 @@ const EnvironmentAccordionBody = ({
show={ show={
<> <>
{strategies.map((strategy, index) => ( {strategies.map((strategy, index) => (
<StrategyDraggableItem <NewStrategyDraggableItem
key={strategy.id} key={strategy.id}
strategy={strategy} strategy={strategy}
index={index} index={index}
@ -349,5 +338,3 @@ const EnvironmentAccordionBody = ({
</StyledAccordionBody> </StyledAccordionBody>
); );
}; };
export default EnvironmentAccordionBody;

View File

@ -0,0 +1,354 @@
// deprecated; remove with the `flagOverviewRedesign` flag
import {
type DragEventHandler,
type RefObject,
useEffect,
useState,
} from 'react';
import { Alert, Pagination, styled } from '@mui/material';
import useFeatureStrategyApi from 'hooks/api/actions/useFeatureStrategyApi/useFeatureStrategyApi';
import { formatUnknownError } from 'utils/formatUnknownError';
import useToast from 'hooks/useToast';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { StrategyDraggableItem } from './StrategyDraggableItem/LegacyStrategyDraggableItem';
import type { IFeatureEnvironment } from 'interfaces/featureToggle';
import { FeatureStrategyEmpty } from 'component/feature/FeatureStrategy/FeatureStrategyEmpty/FeatureStrategyEmpty';
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
import { useFeature } from 'hooks/api/getters/useFeature/useFeature';
import { useChangeRequestApi } from 'hooks/api/actions/useChangeRequestApi/useChangeRequestApi';
import { useChangeRequestsEnabled } from 'hooks/useChangeRequestsEnabled';
import { usePendingChangeRequests } from 'hooks/api/getters/usePendingChangeRequests/usePendingChangeRequests';
import usePagination from 'hooks/usePagination';
import type { IFeatureStrategy } from 'interfaces/strategy';
import { usePlausibleTracker } from 'hooks/usePlausibleTracker';
import { useUiFlag } from 'hooks/useUiFlag';
import { useReleasePlans } from 'hooks/api/getters/useReleasePlans/useReleasePlans';
import { ReleasePlan } from '../../../ReleasePlan/ReleasePlan';
import { Badge } from 'component/common/Badge/Badge';
import { SectionSeparator } from '../SectionSeparator/SectionSeparator';
interface IEnvironmentAccordionBodyProps {
isDisabled: boolean;
featureEnvironment?: IFeatureEnvironment;
otherEnvironments?: IFeatureEnvironment['name'][];
}
const StyledAccordionBody = styled('div')(({ theme }) => ({
width: '100%',
position: 'relative',
paddingBottom: theme.spacing(2),
}));
const StyledAccordionBodyInnerContainer = styled('div')(({ theme }) => ({
[theme.breakpoints.down(400)]: {
padding: theme.spacing(1),
},
}));
const StyledBadge = styled(Badge)(({ theme }) => ({
backgroundColor: theme.palette.primary.light,
border: 'none',
padding: theme.spacing(0.75, 1.5),
borderRadius: theme.shape.borderRadiusLarge,
color: theme.palette.common.white,
}));
const AdditionalStrategiesDiv = styled('div')(({ theme }) => ({
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
marginBottom: theme.spacing(2),
}));
const EnvironmentAccordionBody = ({
featureEnvironment,
isDisabled,
otherEnvironments,
}: IEnvironmentAccordionBodyProps) => {
const projectId = useRequiredPathParam('projectId');
const featureId = useRequiredPathParam('featureId');
const { setStrategiesSortOrder } = useFeatureStrategyApi();
const { addChange } = useChangeRequestApi();
const { isChangeRequestConfigured } = useChangeRequestsEnabled(projectId);
const { refetch: refetchChangeRequests } =
usePendingChangeRequests(projectId);
const { setToastData, setToastApiError } = useToast();
const { refetchFeature } = useFeature(projectId, featureId);
const manyStrategiesPagination = useUiFlag('manyStrategiesPagination');
const [strategies, setStrategies] = useState(
featureEnvironment?.strategies || [],
);
const { releasePlans } = useReleasePlans(
projectId,
featureId,
featureEnvironment?.name,
);
const { trackEvent } = usePlausibleTracker();
const [dragItem, setDragItem] = useState<{
id: string;
index: number;
height: number;
} | null>(null);
useEffect(() => {
// Use state to enable drag and drop, but switch to API output when it arrives
setStrategies(featureEnvironment?.strategies || []);
}, [featureEnvironment?.strategies]);
useEffect(() => {
if (strategies.length > 50) {
trackEvent('many-strategies');
}
}, []);
if (!featureEnvironment) {
return null;
}
const pageSize = 20;
const { page, pages, setPageIndex, pageIndex } =
usePagination<IFeatureStrategy>(strategies, pageSize);
const onReorder = async (payload: { id: string; sortOrder: number }[]) => {
try {
await setStrategiesSortOrder(
projectId,
featureId,
featureEnvironment.name,
payload,
);
refetchFeature();
setToastData({
text: 'Order of strategies updated',
type: 'success',
});
} catch (error: unknown) {
setToastApiError(formatUnknownError(error));
}
};
const onChangeRequestReorder = async (
payload: { id: string; sortOrder: number }[],
) => {
await addChange(projectId, featureEnvironment.name, {
action: 'reorderStrategy',
feature: featureId,
payload,
});
setToastData({
text: 'Strategy execution order added to draft',
type: 'success',
});
refetchChangeRequests();
};
const onStrategyReorder = async (
payload: { id: string; sortOrder: number }[],
) => {
try {
if (isChangeRequestConfigured(featureEnvironment.name)) {
await onChangeRequestReorder(payload);
} else {
await onReorder(payload);
}
} catch (error: unknown) {
setToastApiError(formatUnknownError(error));
}
};
const onDragStartRef =
(
ref: RefObject<HTMLDivElement>,
index: number,
): DragEventHandler<HTMLButtonElement> =>
(event) => {
setDragItem({
id: strategies[index].id,
index,
height: ref.current?.offsetHeight || 0,
});
if (ref?.current) {
event.dataTransfer.effectAllowed = 'move';
event.dataTransfer.setData('text/html', ref.current.outerHTML);
event.dataTransfer.setDragImage(ref.current, 20, 20);
}
};
const onDragOver =
(targetId: string) =>
(
ref: RefObject<HTMLDivElement>,
targetIndex: number,
): DragEventHandler<HTMLDivElement> =>
(event) => {
if (dragItem === null || ref.current === null) return;
if (dragItem.index === targetIndex || targetId === dragItem.id)
return;
const { top, bottom } = ref.current.getBoundingClientRect();
const overTargetTop = event.clientY - top < dragItem.height;
const overTargetBottom = bottom - event.clientY < dragItem.height;
const draggingUp = dragItem.index > targetIndex;
// prevent oscillating by only reordering if there is sufficient space
if (
(overTargetTop && draggingUp) ||
(overTargetBottom && !draggingUp)
) {
const newStrategies = [...strategies];
const movedStrategy = newStrategies.splice(
dragItem.index,
1,
)[0];
newStrategies.splice(targetIndex, 0, movedStrategy);
setStrategies(newStrategies);
setDragItem({
...dragItem,
index: targetIndex,
});
}
};
const onDragEnd = () => {
setDragItem(null);
onStrategyReorder(
strategies.map((strategy, sortOrder) => ({
id: strategy.id,
sortOrder,
})),
);
};
return (
<StyledAccordionBody>
<StyledAccordionBodyInnerContainer>
<ConditionallyRender
condition={
(releasePlans.length > 0 || strategies.length > 0) &&
isDisabled
}
show={() => (
<Alert severity='warning' sx={{ mb: 2 }}>
This environment is disabled, which means that none
of your strategies are executing.
</Alert>
)}
/>
<ConditionallyRender
condition={releasePlans.length > 0 || strategies.length > 0}
show={
<>
{releasePlans.map((plan) => (
<ReleasePlan
key={plan.id}
plan={plan}
environmentIsDisabled={isDisabled}
/>
))}
<ConditionallyRender
condition={
releasePlans.length > 0 &&
strategies.length > 0
}
show={
<>
<SectionSeparator>
<StyledBadge>OR</StyledBadge>
</SectionSeparator>
<AdditionalStrategiesDiv>
Additional strategies
</AdditionalStrategiesDiv>
</>
}
/>
<ConditionallyRender
condition={
strategies.length < 50 ||
!manyStrategiesPagination
}
show={
<>
{strategies.map((strategy, index) => (
<StrategyDraggableItem
key={strategy.id}
strategy={strategy}
index={index}
environmentName={
featureEnvironment.name
}
otherEnvironments={
otherEnvironments
}
isDragging={
dragItem?.id === strategy.id
}
onDragStartRef={onDragStartRef}
onDragOver={onDragOver(
strategy.id,
)}
onDragEnd={onDragEnd}
/>
))}
</>
}
elseShow={
<>
<Alert severity='error'>
We noticed you're using a high
number of activation strategies. To
ensure a more targeted approach,
consider leveraging constraints or
segments.
</Alert>
<br />
{page.map((strategy, index) => (
<StrategyDraggableItem
key={strategy.id}
strategy={strategy}
index={
index + pageIndex * pageSize
}
environmentName={
featureEnvironment.name
}
otherEnvironments={
otherEnvironments
}
isDragging={false}
onDragStartRef={
(() => {}) as any
}
onDragOver={(() => {}) as any}
onDragEnd={(() => {}) as any}
/>
))}
<br />
<Pagination
count={pages.length}
shape='rounded'
page={pageIndex + 1}
onChange={(_, page) =>
setPageIndex(page - 1)
}
/>
</>
}
/>
</>
}
elseShow={
<FeatureStrategyEmpty
projectId={projectId}
featureId={featureId}
environmentId={featureEnvironment.name}
/>
}
/>
</StyledAccordionBodyInnerContainer>
</StyledAccordionBody>
);
};
export default EnvironmentAccordionBody;

View File

@ -0,0 +1,150 @@
// deprecated; remove with the `flagOverviewRedesign` flag
import { type DragEventHandler, type RefObject, useRef } from 'react';
import { Box, useMediaQuery, useTheme } from '@mui/material';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { StrategySeparator } from 'component/common/StrategySeparator/LegacyStrategySeparator';
import type { IFeatureEnvironment } from 'interfaces/featureToggle';
import type { IFeatureStrategy } from 'interfaces/strategy';
import { StrategyItem } from './StrategyItem/LegacyStrategyItem';
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
import {
useStrategyChangesFromRequest,
type UseStrategyChangeFromRequestResult,
} from './StrategyItem/useStrategyChangesFromRequest';
import { ChangesScheduledBadge } from 'component/changeRequest/ModifiedInChangeRequestStatusBadge/ChangesScheduledBadge';
import type { IFeatureChange } from 'component/changeRequest/changeRequest.types';
import { Badge } from 'component/common/Badge/Badge';
import {
type ScheduledChangeRequestViewModel,
useScheduledChangeRequestsWithStrategy,
} from 'hooks/api/getters/useScheduledChangeRequestsWithStrategy/useScheduledChangeRequestsWithStrategy';
interface IStrategyDraggableItemProps {
strategy: IFeatureStrategy;
environmentName: string;
index: number;
otherEnvironments?: IFeatureEnvironment['name'][];
isDragging?: boolean;
onDragStartRef: (
ref: RefObject<HTMLDivElement>,
index: number,
) => DragEventHandler<HTMLButtonElement>;
onDragOver: (
ref: RefObject<HTMLDivElement>,
index: number,
) => DragEventHandler<HTMLDivElement>;
onDragEnd: () => void;
}
export const StrategyDraggableItem = ({
strategy,
index,
environmentName,
otherEnvironments,
isDragging,
onDragStartRef,
onDragOver,
onDragEnd,
}: IStrategyDraggableItemProps) => {
const projectId = useRequiredPathParam('projectId');
const featureId = useRequiredPathParam('featureId');
const ref = useRef<HTMLDivElement>(null);
const strategyChangesFromRequest = useStrategyChangesFromRequest(
projectId,
featureId,
environmentName,
strategy.id,
);
const { changeRequests: scheduledChangesUsingStrategy } =
useScheduledChangeRequestsWithStrategy(projectId, strategy.id);
return (
<Box
key={strategy.id}
ref={ref}
onDragOver={onDragOver(ref, index)}
sx={{ opacity: isDragging ? '0.5' : '1' }}
>
<ConditionallyRender
condition={index > 0}
show={<StrategySeparator text='OR' />}
/>
<StrategyItem
strategy={strategy}
environmentId={environmentName}
otherEnvironments={otherEnvironments}
onDragStart={onDragStartRef(ref, index)}
onDragEnd={onDragEnd}
orderNumber={index + 1}
headerChildren={renderHeaderChildren(
strategyChangesFromRequest,
scheduledChangesUsingStrategy,
)}
/>
</Box>
);
};
const ChangeRequestStatusBadge = ({
change,
}: {
change: IFeatureChange | undefined;
}) => {
const theme = useTheme();
const isSmallScreen = useMediaQuery(theme.breakpoints.down('sm'));
if (isSmallScreen) {
return null;
}
return (
<Box sx={{ mr: 1.5 }}>
<ConditionallyRender
condition={change?.action === 'updateStrategy'}
show={<Badge color='warning'>Modified in draft</Badge>}
/>
<ConditionallyRender
condition={change?.action === 'deleteStrategy'}
show={<Badge color='error'>Deleted in draft</Badge>}
/>
</Box>
);
};
const renderHeaderChildren = (
changes?: UseStrategyChangeFromRequestResult,
scheduledChanges?: ScheduledChangeRequestViewModel[],
): JSX.Element[] => {
const badges: JSX.Element[] = [];
if (changes?.length === 0 && scheduledChanges?.length === 0) {
return [];
}
const draftChange = changes?.find(
({ isScheduledChange }) => !isScheduledChange,
);
if (draftChange) {
badges.push(
<ChangeRequestStatusBadge
key={`draft-change#${draftChange.change.id}`}
change={draftChange.change}
/>,
);
}
if (scheduledChanges && scheduledChanges.length > 0) {
badges.push(
<ChangesScheduledBadge
key='scheduled-changes'
scheduledChangeRequestIds={scheduledChanges.map(
(scheduledChange) => scheduledChange.id,
)}
/>,
);
}
return badges;
};

View File

@ -1,10 +1,8 @@
import { type DragEventHandler, type RefObject, useRef } from 'react'; import { type DragEventHandler, type RefObject, useRef } from 'react';
import { Box, useMediaQuery, useTheme } from '@mui/material'; import { Box, useMediaQuery, useTheme } from '@mui/material';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { StrategySeparator } from 'component/common/StrategySeparator/StrategySeparator';
import type { IFeatureEnvironment } from 'interfaces/featureToggle'; import type { IFeatureEnvironment } from 'interfaces/featureToggle';
import type { IFeatureStrategy } from 'interfaces/strategy'; import type { IFeatureStrategy } from 'interfaces/strategy';
import { StrategyItem } from './StrategyItem/StrategyItem';
import { useRequiredPathParam } from 'hooks/useRequiredPathParam'; import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
import { import {
useStrategyChangesFromRequest, useStrategyChangesFromRequest,
@ -17,6 +15,8 @@ import {
type ScheduledChangeRequestViewModel, type ScheduledChangeRequestViewModel,
useScheduledChangeRequestsWithStrategy, useScheduledChangeRequestsWithStrategy,
} from 'hooks/api/getters/useScheduledChangeRequestsWithStrategy/useScheduledChangeRequestsWithStrategy'; } from 'hooks/api/getters/useScheduledChangeRequestsWithStrategy/useScheduledChangeRequestsWithStrategy';
import { StrategySeparator as NewStrategySeparator } from 'component/common/StrategySeparator/StrategySeparator';
import { StrategyItem as NewStrategyItem } from './StrategyItem/StrategyItem';
interface IStrategyDraggableItemProps { interface IStrategyDraggableItemProps {
strategy: IFeatureStrategy; strategy: IFeatureStrategy;
@ -67,10 +67,10 @@ export const StrategyDraggableItem = ({
> >
<ConditionallyRender <ConditionallyRender
condition={index > 0} condition={index > 0}
show={<StrategySeparator text='OR' />} show={<NewStrategySeparator text='OR' />}
/> />
<StrategyItem <NewStrategyItem
strategy={strategy} strategy={strategy}
environmentId={environmentName} environmentId={environmentName}
otherEnvironments={otherEnvironments} otherEnvironments={otherEnvironments}

View File

@ -0,0 +1,181 @@
// deprecated; remove with the `flagOverviewRedesign` flag
import type { DragEventHandler, FC } from 'react';
import Edit from '@mui/icons-material/Edit';
import { Link } from 'react-router-dom';
import type { IFeatureEnvironment } from 'interfaces/featureToggle';
import type { IFeatureStrategy } from 'interfaces/strategy';
import PermissionIconButton from 'component/common/PermissionIconButton/PermissionIconButton';
import { UPDATE_FEATURE_STRATEGY } from 'component/providers/AccessProvider/permissions';
import { formatEditStrategyPath } from 'component/feature/FeatureStrategy/FeatureStrategyEdit/FeatureStrategyEdit';
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
import { StrategyExecution } from './StrategyExecution/StrategyExecution';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { CopyStrategyIconMenu } from './CopyStrategyIconMenu/CopyStrategyIconMenu';
import { StrategyItemContainer } from 'component/common/StrategyItemContainer/LegacyStrategyItemContainer';
import MenuStrategyRemove from './MenuStrategyRemove/MenuStrategyRemove';
import SplitPreviewSlider from 'component/feature/StrategyTypes/SplitPreviewSlider/SplitPreviewSlider';
import { Box } from '@mui/material';
import { StrategyItemContainer as NewStrategyItemContainer } from 'component/common/StrategyItemContainer/StrategyItemContainer';
interface IStrategyItemProps {
environmentId: string;
strategy: IFeatureStrategy;
onDragStart?: DragEventHandler<HTMLButtonElement>;
onDragEnd?: DragEventHandler<HTMLButtonElement>;
otherEnvironments?: IFeatureEnvironment['name'][];
orderNumber?: number;
headerChildren?: JSX.Element[] | JSX.Element;
}
export const StrategyItem: FC<IStrategyItemProps> = ({
environmentId,
strategy,
onDragStart,
onDragEnd,
otherEnvironments,
orderNumber,
headerChildren,
}) => {
const projectId = useRequiredPathParam('projectId');
const featureId = useRequiredPathParam('featureId');
const editStrategyPath = formatEditStrategyPath(
projectId,
featureId,
environmentId,
strategy.id,
);
return (
<StrategyItemContainer
strategy={strategy}
onDragStart={onDragStart}
onDragEnd={onDragEnd}
orderNumber={orderNumber}
actions={
<>
{headerChildren}
<ConditionallyRender
condition={Boolean(
otherEnvironments && otherEnvironments?.length > 0,
)}
show={() => (
<CopyStrategyIconMenu
environmentId={environmentId}
environments={otherEnvironments as string[]}
strategy={strategy}
/>
)}
/>
<PermissionIconButton
permission={UPDATE_FEATURE_STRATEGY}
environmentId={environmentId}
projectId={projectId}
component={Link}
to={editStrategyPath}
tooltipProps={{
title: 'Edit strategy',
}}
data-testid={`STRATEGY_EDIT-${strategy.name}`}
>
<Edit />
</PermissionIconButton>
<MenuStrategyRemove
projectId={projectId}
featureId={featureId}
environmentId={environmentId}
strategy={strategy}
/>
</>
}
>
<StrategyExecution strategy={strategy} />
{strategy.variants &&
strategy.variants.length > 0 &&
(strategy.disabled ? (
<Box sx={{ opacity: '0.5' }}>
<SplitPreviewSlider variants={strategy.variants} />
</Box>
) : (
<SplitPreviewSlider variants={strategy.variants} />
))}
</StrategyItemContainer>
);
};
export const NewStrategyItem: FC<IStrategyItemProps> = ({
environmentId,
strategy,
onDragStart,
onDragEnd,
otherEnvironments,
orderNumber,
headerChildren,
}) => {
const projectId = useRequiredPathParam('projectId');
const featureId = useRequiredPathParam('featureId');
const editStrategyPath = formatEditStrategyPath(
projectId,
featureId,
environmentId,
strategy.id,
);
return (
<NewStrategyItemContainer
strategy={strategy}
onDragStart={onDragStart}
onDragEnd={onDragEnd}
orderNumber={orderNumber}
actions={
<>
{headerChildren}
<ConditionallyRender
condition={Boolean(
otherEnvironments && otherEnvironments?.length > 0,
)}
show={() => (
<CopyStrategyIconMenu
environmentId={environmentId}
environments={otherEnvironments as string[]}
strategy={strategy}
/>
)}
/>
<PermissionIconButton
permission={UPDATE_FEATURE_STRATEGY}
environmentId={environmentId}
projectId={projectId}
component={Link}
to={editStrategyPath}
tooltipProps={{
title: 'Edit strategy',
}}
data-testid={`STRATEGY_EDIT-${strategy.name}`}
>
<Edit />
</PermissionIconButton>
<MenuStrategyRemove
projectId={projectId}
featureId={featureId}
environmentId={environmentId}
strategy={strategy}
/>
</>
}
>
<StrategyExecution strategy={strategy} />
{strategy.variants &&
strategy.variants.length > 0 &&
(strategy.disabled ? (
<Box sx={{ opacity: '0.5' }}>
<SplitPreviewSlider variants={strategy.variants} />
</Box>
) : (
<SplitPreviewSlider variants={strategy.variants} />
))}
</NewStrategyItemContainer>
);
};

View File

@ -2,7 +2,7 @@ import { type FC, Fragment, useMemo } from 'react';
import { Alert, Box, Chip, Link, styled } from '@mui/material'; import { Alert, Box, Chip, Link, styled } from '@mui/material';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import PercentageCircle from 'component/common/PercentageCircle/PercentageCircle'; import PercentageCircle from 'component/common/PercentageCircle/PercentageCircle';
import { StrategySeparator } from 'component/common/StrategySeparator/StrategySeparator'; import { StrategySeparator } from 'component/common/StrategySeparator/LegacyStrategySeparator';
import { ConstraintItem } from './ConstraintItem/ConstraintItem'; import { ConstraintItem } from './ConstraintItem/ConstraintItem';
import { useStrategies } from 'hooks/api/getters/useStrategies/useStrategies'; import { useStrategies } from 'hooks/api/getters/useStrategies/useStrategies';
import { useSegments } from 'hooks/api/getters/useSegments/useSegments'; import { useSegments } from 'hooks/api/getters/useSegments/useSegments';

View File

@ -10,10 +10,10 @@ import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
import { StrategyExecution } from './StrategyExecution/StrategyExecution'; import { StrategyExecution } from './StrategyExecution/StrategyExecution';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { CopyStrategyIconMenu } from './CopyStrategyIconMenu/CopyStrategyIconMenu'; import { CopyStrategyIconMenu } from './CopyStrategyIconMenu/CopyStrategyIconMenu';
import { StrategyItemContainer } from 'component/common/StrategyItemContainer/StrategyItemContainer';
import MenuStrategyRemove from './MenuStrategyRemove/MenuStrategyRemove'; import MenuStrategyRemove from './MenuStrategyRemove/MenuStrategyRemove';
import SplitPreviewSlider from 'component/feature/StrategyTypes/SplitPreviewSlider/SplitPreviewSlider'; import SplitPreviewSlider from 'component/feature/StrategyTypes/SplitPreviewSlider/SplitPreviewSlider';
import { Box } from '@mui/material'; import { Box } from '@mui/material';
import { StrategyItemContainer as NewStrategyItemContainer } from 'component/common/StrategyItemContainer/StrategyItemContainer';
interface IStrategyItemProps { interface IStrategyItemProps {
environmentId: string; environmentId: string;
strategy: IFeatureStrategy; strategy: IFeatureStrategy;
@ -44,7 +44,7 @@ export const StrategyItem: FC<IStrategyItemProps> = ({
); );
return ( return (
<StrategyItemContainer <NewStrategyItemContainer
strategy={strategy} strategy={strategy}
onDragStart={onDragStart} onDragStart={onDragStart}
onDragEnd={onDragEnd} onDragEnd={onDragEnd}
@ -97,6 +97,6 @@ export const StrategyItem: FC<IStrategyItemProps> = ({
) : ( ) : (
<SplitPreviewSlider variants={strategy.variants} /> <SplitPreviewSlider variants={strategy.variants} />
))} ))}
</StrategyItemContainer> </NewStrategyItemContainer>
); );
}; };

View File

@ -3,7 +3,6 @@ import type {
IFeatureEnvironment, IFeatureEnvironment,
IFeatureEnvironmentMetrics, IFeatureEnvironmentMetrics,
} from 'interfaces/featureToggle'; } from 'interfaces/featureToggle';
import EnvironmentAccordionBody from './EnvironmentAccordionBody/EnvironmentAccordionBody';
import { FeatureStrategyMenu } from 'component/feature/FeatureStrategy/FeatureStrategyMenu/FeatureStrategyMenu'; import { FeatureStrategyMenu } from 'component/feature/FeatureStrategy/FeatureStrategyMenu/FeatureStrategyMenu';
import { FEATURE_ENVIRONMENT_ACCORDION } from 'utils/testIds'; import { FEATURE_ENVIRONMENT_ACCORDION } from 'utils/testIds';
import { useRequiredPathParam } from 'hooks/useRequiredPathParam'; import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
@ -14,6 +13,7 @@ import FeatureOverviewEnvironmentMetrics from './EnvironmentHeader/FeatureOvervi
import { FeatureOverviewEnvironmentToggle } from './EnvironmentHeader/FeatureOverviewEnvironmentToggle/FeatureOverviewEnvironmentToggle'; import { FeatureOverviewEnvironmentToggle } from './EnvironmentHeader/FeatureOverviewEnvironmentToggle/FeatureOverviewEnvironmentToggle';
import { useState } from 'react'; import { useState } from 'react';
import type { IReleasePlan } from 'interfaces/releasePlans'; import type { IReleasePlan } from 'interfaces/releasePlans';
import { EnvironmentAccordionBody as NewEnvironmentAccordionBody } from './EnvironmentAccordionBody/EnvironmentAccordionBody';
const StyledFeatureOverviewEnvironment = styled('div')(({ theme }) => ({ const StyledFeatureOverviewEnvironment = styled('div')(({ theme }) => ({
borderRadius: theme.shape.borderRadiusLarge, borderRadius: theme.shape.borderRadiusLarge,
@ -32,15 +32,12 @@ const StyledAccordion = styled(Accordion)(({ theme }) => ({
}, },
})); }));
const StyledAccordionDetails = styled(AccordionDetails)(({ theme }) => ({ const NewStyledAccordionDetails = styled(AccordionDetails)(({ theme }) => ({
padding: 0, padding: 0,
background: theme.palette.envAccordion.expanded, background: theme.palette.background.elevation1,
borderBottomLeftRadius: theme.shape.borderRadiusLarge, borderBottomLeftRadius: theme.shape.borderRadiusLarge,
borderBottomRightRadius: theme.shape.borderRadiusLarge, borderBottomRightRadius: theme.shape.borderRadiusLarge,
boxShadow: theme.boxShadows.accordionFooter, boxShadow: theme.boxShadows.accordionFooter,
[theme.breakpoints.down('md')]: {
padding: theme.spacing(2, 1),
},
})); }));
const StyledAccordionFooter = styled('footer')(({ theme }) => ({ const StyledAccordionFooter = styled('footer')(({ theme }) => ({
@ -55,7 +52,6 @@ const StyledAccordionFooter = styled('footer')(({ theme }) => ({
const StyledEnvironmentAccordionContainer = styled('div')(({ theme }) => ({ const StyledEnvironmentAccordionContainer = styled('div')(({ theme }) => ({
width: '100%', width: '100%',
position: 'relative', position: 'relative',
padding: theme.spacing(3, 3, 1),
})); }));
type FeatureOverviewEnvironmentProps = { type FeatureOverviewEnvironmentProps = {
@ -112,9 +108,9 @@ export const FeatureOverviewEnvironment = ({
collapsed={!hasActivations} collapsed={!hasActivations}
/> />
</EnvironmentHeader> </EnvironmentHeader>
<StyledAccordionDetails> <NewStyledAccordionDetails>
<StyledEnvironmentAccordionContainer> <StyledEnvironmentAccordionContainer>
<EnvironmentAccordionBody <NewEnvironmentAccordionBody
featureEnvironment={environment} featureEnvironment={environment}
isDisabled={!environment.enabled} isDisabled={!environment.enabled}
otherEnvironments={otherEnvironments} otherEnvironments={otherEnvironments}
@ -131,7 +127,7 @@ export const FeatureOverviewEnvironment = ({
<UpgradeChangeRequests /> <UpgradeChangeRequests />
) : null} ) : null}
</StyledAccordionFooter> </StyledAccordionFooter>
</StyledAccordionDetails> </NewStyledAccordionDetails>
</StyledAccordion> </StyledAccordion>
</StyledFeatureOverviewEnvironment> </StyledFeatureOverviewEnvironment>
); );

View File

@ -13,7 +13,7 @@ import { getFeatureMetrics } from 'utils/getFeatureMetrics';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import EnvironmentIcon from 'component/common/EnvironmentIcon/EnvironmentIcon'; import EnvironmentIcon from 'component/common/EnvironmentIcon/EnvironmentIcon';
import StringTruncator from 'component/common/StringTruncator/StringTruncator'; import StringTruncator from 'component/common/StringTruncator/StringTruncator';
import EnvironmentAccordionBody from './EnvironmentAccordionBody/EnvironmentAccordionBody'; import EnvironmentAccordionBody from './EnvironmentAccordionBody/LegacyEnvironmentAccordionBody';
import { EnvironmentFooter } from './EnvironmentFooter/EnvironmentFooter'; import { EnvironmentFooter } from './EnvironmentFooter/EnvironmentFooter';
import FeatureOverviewEnvironmentMetrics from './EnvironmentHeader/FeatureOverviewEnvironmentMetrics/LegacyFeatureOverviewEnvironmentMetrics'; import FeatureOverviewEnvironmentMetrics from './EnvironmentHeader/FeatureOverviewEnvironmentMetrics/LegacyFeatureOverviewEnvironmentMetrics';
import { FeatureStrategyMenu } from 'component/feature/FeatureStrategy/FeatureStrategyMenu/FeatureStrategyMenu'; import { FeatureStrategyMenu } from 'component/feature/FeatureStrategy/FeatureStrategyMenu/FeatureStrategyMenu';

View File

@ -1,6 +1,6 @@
import { Fragment } from 'react'; import { Fragment } from 'react';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { StrategySeparator } from 'component/common/StrategySeparator/StrategySeparator'; import { StrategySeparator } from 'component/common/StrategySeparator/LegacyStrategySeparator';
import { SegmentItem } from '../../../../common/SegmentItem/SegmentItem'; import { SegmentItem } from '../../../../common/SegmentItem/SegmentItem';
import type { ISegment } from 'interfaces/segment'; import type { ISegment } from 'interfaces/segment';

View File

@ -8,7 +8,7 @@ import {
import type { IReleasePlanMilestone } from 'interfaces/releasePlans'; import type { IReleasePlanMilestone } from 'interfaces/releasePlans';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { ReleasePlanMilestoneStrategy } from './ReleasePlanMilestoneStrategy'; import { ReleasePlanMilestoneStrategy } from './ReleasePlanMilestoneStrategy';
import { StrategySeparator } from 'component/common/StrategySeparator/StrategySeparator'; import { StrategySeparator } from 'component/common/StrategySeparator/LegacyStrategySeparator';
import { import {
ReleasePlanMilestoneStatus, ReleasePlanMilestoneStatus,
type MilestoneStatus, type MilestoneStatus,

View File

@ -5,7 +5,7 @@ import type {
} from 'openapi'; } from 'openapi';
import { objectId } from 'utils/objectId'; import { objectId } from 'utils/objectId';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { StrategySeparator } from 'component/common/StrategySeparator/StrategySeparator'; import { StrategySeparator } from 'component/common/StrategySeparator/LegacyStrategySeparator';
import { styled } from '@mui/material'; import { styled } from '@mui/material';
import { ConstraintAccordionView } from 'component/common/ConstraintAccordion/ConstraintAccordionView/ConstraintAccordionView'; import { ConstraintAccordionView } from 'component/common/ConstraintAccordion/ConstraintAccordionView/ConstraintAccordionView';
import { ConstraintError } from './ConstraintError/ConstraintError'; import { ConstraintError } from './ConstraintError/ConstraintError';

View File

@ -2,7 +2,7 @@ import { Fragment, type VFC } from 'react';
import type { PlaygroundConstraintSchema } from 'openapi'; import type { PlaygroundConstraintSchema } from 'openapi';
import { objectId } from 'utils/objectId'; import { objectId } from 'utils/objectId';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { StrategySeparator } from 'component/common/StrategySeparator/StrategySeparator'; import { StrategySeparator } from 'component/common/StrategySeparator/LegacyStrategySeparator';
import { styled } from '@mui/material'; import { styled } from '@mui/material';
import { ConstraintAccordionView } from 'component/common/ConstraintAccordion/ConstraintAccordionView/ConstraintAccordionView'; import { ConstraintAccordionView } from 'component/common/ConstraintAccordion/ConstraintAccordionView/ConstraintAccordionView';

View File

@ -5,7 +5,7 @@ import {
parseParameterStrings, parseParameterStrings,
} from 'utils/parseParameter'; } from 'utils/parseParameter';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { StrategySeparator } from 'component/common/StrategySeparator/StrategySeparator'; import { StrategySeparator } from 'component/common/StrategySeparator/LegacyStrategySeparator';
import { useStrategies } from 'hooks/api/getters/useStrategies/useStrategies'; import { useStrategies } from 'hooks/api/getters/useStrategies/useStrategies';
import { CustomParameterItem } from './CustomParameterItem/CustomParameterItem'; import { CustomParameterItem } from './CustomParameterItem/CustomParameterItem';

View File

@ -1,6 +1,6 @@
import { Fragment, type VFC } from 'react'; import { Fragment, type VFC } from 'react';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { StrategySeparator } from 'component/common/StrategySeparator/StrategySeparator'; import { StrategySeparator } from 'component/common/StrategySeparator/LegacyStrategySeparator';
import { styled } from '@mui/material'; import { styled } from '@mui/material';
import type { import type {
PlaygroundRequestSchema, PlaygroundRequestSchema,

View File

@ -2,7 +2,7 @@ import { Fragment, type VFC } from 'react';
import type { PlaygroundSegmentSchema, PlaygroundRequestSchema } from 'openapi'; import type { PlaygroundSegmentSchema, PlaygroundRequestSchema } from 'openapi';
import { ConstraintExecution } from '../ConstraintExecution/ConstraintExecution'; import { ConstraintExecution } from '../ConstraintExecution/ConstraintExecution';
import CancelOutlined from '@mui/icons-material/CancelOutlined'; import CancelOutlined from '@mui/icons-material/CancelOutlined';
import { StrategySeparator } from 'component/common/StrategySeparator/StrategySeparator'; import { StrategySeparator } from 'component/common/StrategySeparator/LegacyStrategySeparator';
import { styled, Typography } from '@mui/material'; import { styled, Typography } from '@mui/material';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { SegmentItem } from 'component/common/SegmentItem/SegmentItem'; import { SegmentItem } from 'component/common/SegmentItem/SegmentItem';

View File

@ -1,6 +1,6 @@
import { Fragment, type VFC } from 'react'; import { Fragment, type VFC } from 'react';
import type { PlaygroundSegmentSchema } from 'openapi'; import type { PlaygroundSegmentSchema } from 'openapi';
import { StrategySeparator } from 'component/common/StrategySeparator/StrategySeparator'; import { StrategySeparator } from 'component/common/StrategySeparator/LegacyStrategySeparator';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { SegmentItem } from 'component/common/SegmentItem/SegmentItem'; import { SegmentItem } from 'component/common/SegmentItem/SegmentItem';
import { ConstraintExecutionWithoutResults } from '../ConstraintExecution/ConstraintExecutionWithoutResults'; import { ConstraintExecutionWithoutResults } from '../ConstraintExecution/ConstraintExecutionWithoutResults';

View File

@ -1,6 +1,6 @@
import { Fragment, type VFC } from 'react'; import { Fragment, type VFC } from 'react';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { StrategySeparator } from 'component/common/StrategySeparator/StrategySeparator'; import { StrategySeparator } from 'component/common/StrategySeparator/LegacyStrategySeparator';
import { styled } from '@mui/material'; import { styled } from '@mui/material';
import type { import type {
PlaygroundRequestSchema, PlaygroundRequestSchema,

View File

@ -7,7 +7,7 @@ import type {
} from 'openapi'; } from 'openapi';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { FeatureStrategyItem } from './StrategyItem/FeatureStrategyItem'; import { FeatureStrategyItem } from './StrategyItem/FeatureStrategyItem';
import { StrategySeparator } from 'component/common/StrategySeparator/StrategySeparator'; import { StrategySeparator } from 'component/common/StrategySeparator/LegacyStrategySeparator';
const StyledAlertWrapper = styled('div')(({ theme }) => ({ const StyledAlertWrapper = styled('div')(({ theme }) => ({
display: 'flex', display: 'flex',

View File

@ -3,7 +3,7 @@ import { type DragEventHandler, type RefObject, useRef } from 'react';
import { Box, IconButton } from '@mui/material'; import { Box, IconButton } from '@mui/material';
import Edit from '@mui/icons-material/Edit'; import Edit from '@mui/icons-material/Edit';
import Delete from '@mui/icons-material/DeleteOutlined'; import Delete from '@mui/icons-material/DeleteOutlined';
import { StrategySeparator } from 'component/common/StrategySeparator/StrategySeparator'; import { StrategySeparator } from 'component/common/StrategySeparator/LegacyStrategySeparator';
import { MilestoneStrategyItem } from './MilestoneStrategyItem'; import { MilestoneStrategyItem } from './MilestoneStrategyItem';
interface IMilestoneStrategyDraggableItemProps { interface IMilestoneStrategyDraggableItemProps {