mirror of
https://github.com/Unleash/unleash.git
synced 2025-09-19 17:52:45 +02:00
refactor: remove flagOverviewRedesign flag (#9888)
Co-authored-by: Thomas Heartman <thomas@getunleash.io>
This commit is contained in:
parent
6a2953f768
commit
af93f93836
@ -40,7 +40,6 @@ describe('demo', () => {
|
||||
res.body.flags = {
|
||||
...res.body.flags,
|
||||
demo: true,
|
||||
flagOverviewRedesign: true,
|
||||
};
|
||||
}
|
||||
});
|
||||
|
@ -44,7 +44,6 @@ describe('feature', () => {
|
||||
if (res.body) {
|
||||
res.body.flags = {
|
||||
...res.body.flags,
|
||||
flagOverviewRedesign: true,
|
||||
};
|
||||
}
|
||||
});
|
||||
|
@ -1,6 +1,6 @@
|
||||
import type React from 'react';
|
||||
import type { FC } from 'react';
|
||||
import { render, screen, within, fireEvent } from '@testing-library/react';
|
||||
import { render, screen, fireEvent } from '@testing-library/react';
|
||||
import { MemoryRouter, Routes, Route } from 'react-router-dom';
|
||||
import { ThemeProvider } from 'themes/ThemeProvider';
|
||||
import { MainLayout } from 'component/layout/MainLayout/MainLayout';
|
||||
@ -271,13 +271,6 @@ const verifyBannerForPendingChangeRequest = async () => {
|
||||
return screen.findByText('Change request mode', {}, { timeout: 5000 });
|
||||
};
|
||||
|
||||
const changeFlag = async (environment: string) => {
|
||||
const featureFlagStatusBox = screen.getByTestId('feature-flag-status');
|
||||
await within(featureFlagStatusBox).findByText(environment);
|
||||
const flag = screen.getAllByRole('checkbox')[1];
|
||||
fireEvent.click(flag);
|
||||
};
|
||||
|
||||
const verifyChangeRequestDialog = async (bannerMainText: string) => {
|
||||
await screen.findByText('Your suggestion:');
|
||||
const message = screen.getByTestId('update-enabled-message').textContent;
|
||||
@ -298,7 +291,8 @@ test('add flag change to pending change request', async () => {
|
||||
|
||||
await verifyBannerForPendingChangeRequest();
|
||||
|
||||
await changeFlag('production');
|
||||
const flag = screen.getByLabelText('production');
|
||||
fireEvent.click(flag);
|
||||
|
||||
await verifyChangeRequestDialog('Enable feature flag test in production');
|
||||
}, 10000);
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { screen } from '@testing-library/react';
|
||||
import { screen, waitFor } from '@testing-library/react';
|
||||
|
||||
import { Route, Routes } from 'react-router-dom';
|
||||
|
||||
@ -226,7 +226,7 @@ test('Display default disable feature', async () => {
|
||||
expect(screen.getByText('Feature status will change')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('Displays feature strategy variants table when addStrategy action with variants', async () => {
|
||||
test('Displays strategy variant table when addStrategy action with variants', async () => {
|
||||
render(
|
||||
<Routes>
|
||||
<Route
|
||||
@ -269,7 +269,7 @@ test('Displays feature strategy variants table when addStrategy action with vari
|
||||
},
|
||||
);
|
||||
|
||||
await screen.findByText('Setting strategy variants to:');
|
||||
await screen.findByText('Adding strategy variants:');
|
||||
});
|
||||
|
||||
test('Displays feature strategy variants table when there is a change in the variants array', async () => {
|
||||
@ -297,7 +297,10 @@ test('Displays feature strategy variants table when there is a change in the var
|
||||
route: '/projects/default/change-requests/27',
|
||||
},
|
||||
);
|
||||
await screen.findByText('Updating strategy variants to:');
|
||||
|
||||
waitFor(async () => {
|
||||
await screen.findByText('Updating strategy variants to:');
|
||||
});
|
||||
});
|
||||
|
||||
test('Displays feature strategy variants table when existing strategy does not have variants and change does', async () => {
|
||||
@ -325,5 +328,5 @@ test('Displays feature strategy variants table when existing strategy does not h
|
||||
route: '/projects/default/change-requests/27',
|
||||
},
|
||||
);
|
||||
await screen.findByText('Updating strategy variants to:');
|
||||
await screen.findByText('Adding strategy variants:');
|
||||
});
|
||||
|
@ -31,6 +31,7 @@ const StyledSingleChangeBox = styled(Box, {
|
||||
$isAfterWarning,
|
||||
$isLast,
|
||||
}) => ({
|
||||
overflow: 'hidden',
|
||||
borderLeft: '1px solid',
|
||||
borderRight: '1px solid',
|
||||
borderTop: '1px solid',
|
||||
|
@ -12,7 +12,7 @@ import { useReleasePlans } from 'hooks/api/getters/useReleasePlans/useReleasePla
|
||||
import { TooltipLink } from 'component/common/TooltipLink/TooltipLink';
|
||||
import EventDiff from 'component/events/EventDiff/EventDiff';
|
||||
import { ReleasePlan } from 'component/feature/FeatureView/FeatureOverview/ReleasePlan/ReleasePlan';
|
||||
import { ReleasePlanMilestone } from 'component/feature/FeatureView/FeatureOverview/ReleasePlan/ReleasePlanMilestone/LegacyReleasePlanMilestone';
|
||||
import { ReleasePlanMilestone } from 'component/feature/FeatureView/FeatureOverview/ReleasePlan/ReleasePlanMilestone/ReleasePlanMilestone';
|
||||
import type { IReleasePlan } from 'interfaces/releasePlans';
|
||||
|
||||
export const ChangeItemWrapper = styled(Box)({
|
||||
|
@ -119,7 +119,8 @@ test('Editing strategy before change request is applied diffs against current st
|
||||
await screen.findByText('+ variants.0.name: "change_variant"');
|
||||
|
||||
await screen.findByText('Updating strategy variants to:');
|
||||
await screen.findByText('change_variant');
|
||||
const variants = await screen.findAllByText('change_variant');
|
||||
expect(variants).toHaveLength(2);
|
||||
});
|
||||
|
||||
test('Editing strategy after change request is applied diffs against the snapshot', async () => {
|
||||
@ -189,7 +190,8 @@ test('Editing strategy after change request is applied diffs against the snapsho
|
||||
await screen.findByText('+ variants.0.name: "change_variant"');
|
||||
|
||||
await screen.findByText('Updating strategy variants to:');
|
||||
await screen.findByText('change_variant');
|
||||
const variants = await screen.findAllByText('change_variant');
|
||||
expect(variants).toHaveLength(2);
|
||||
});
|
||||
|
||||
test('Deleting strategy before change request is applied diffs against current strategy', async () => {
|
||||
@ -226,7 +228,6 @@ test('Deleting strategy before change request is applied diffs against current s
|
||||
await userEvent.hover(viewDiff);
|
||||
await screen.findByText('- constraints (deleted)');
|
||||
|
||||
await screen.findByText('Deleting strategy variants:');
|
||||
await screen.findByText('current_variant');
|
||||
});
|
||||
|
||||
@ -281,7 +282,6 @@ test('Deleting strategy after change request is applied diffs against the snapsh
|
||||
await userEvent.hover(viewDiff);
|
||||
await screen.findByText('- constraints (deleted)');
|
||||
|
||||
await screen.findByText('Deleting strategy variants:');
|
||||
await screen.findByText('snapshot_variant');
|
||||
});
|
||||
|
||||
@ -329,9 +329,8 @@ test('Adding strategy always diffs against undefined strategy', async () => {
|
||||
const viewDiff = await screen.findByText('View Diff');
|
||||
await userEvent.hover(viewDiff);
|
||||
await screen.findByText(`+ name: "flexibleRollout"`);
|
||||
|
||||
await screen.findByText('Setting strategy variants to:');
|
||||
await screen.findByText('change_variant');
|
||||
const variants = await screen.findAllByText('change_variant');
|
||||
expect(variants).toHaveLength(2);
|
||||
});
|
||||
|
||||
test('Segments order does not matter for diff calculation', async () => {
|
||||
|
@ -21,7 +21,6 @@ import { flexRow } from 'themes/themeStyles';
|
||||
import { EnvironmentVariantsTable } from 'component/feature/FeatureView/FeatureVariants/FeatureEnvironmentVariants/EnvironmentVariantsCard/EnvironmentVariantsTable/EnvironmentVariantsTable';
|
||||
import { ChangeOverwriteWarning } from './ChangeOverwriteWarning/ChangeOverwriteWarning';
|
||||
import type { IFeatureStrategy } from 'interfaces/strategy';
|
||||
import { useUiFlag } from 'hooks/useUiFlag';
|
||||
|
||||
export const ChangeItemWrapper = styled(Box)({
|
||||
display: 'flex',
|
||||
@ -163,21 +162,6 @@ const DeleteStrategy: FC<{
|
||||
{referenceStrategy && (
|
||||
<StrategyExecution strategy={referenceStrategy} />
|
||||
)}
|
||||
<ConditionallyRender
|
||||
condition={Boolean(referenceStrategy?.variants?.length)}
|
||||
show={
|
||||
referenceStrategy?.variants && (
|
||||
<StyledBox>
|
||||
<StyledTypography>
|
||||
Deleting strategy variants:
|
||||
</StyledTypography>
|
||||
<EnvironmentVariantsTable
|
||||
variants={referenceStrategy.variants}
|
||||
/>
|
||||
</StyledBox>
|
||||
)
|
||||
}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@ -251,19 +235,26 @@ const UpdateStrategy: FC<{
|
||||
}
|
||||
/>
|
||||
<StrategyExecution strategy={change.payload} />
|
||||
<ConditionallyRender
|
||||
condition={Boolean(hasVariantDiff)}
|
||||
show={
|
||||
<StyledBox>
|
||||
{hasVariantDiff ? (
|
||||
<StyledBox>
|
||||
{change.payload.variants?.length ? (
|
||||
<>
|
||||
<StyledTypography>
|
||||
{currentStrategy?.variants?.length
|
||||
? 'Updating strategy variants to:'
|
||||
: 'Adding strategy variants:'}
|
||||
</StyledTypography>
|
||||
<EnvironmentVariantsTable
|
||||
variants={change.payload.variants || []}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<StyledTypography>
|
||||
Updating strategy variants to:
|
||||
Removed all strategy variants.
|
||||
</StyledTypography>
|
||||
<EnvironmentVariantsTable
|
||||
variants={change.payload.variants || []}
|
||||
/>
|
||||
</StyledBox>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</StyledBox>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@ -271,55 +262,45 @@ const UpdateStrategy: FC<{
|
||||
const AddStrategy: FC<{
|
||||
change: IChangeRequestAddStrategy;
|
||||
actions?: ReactNode;
|
||||
}> = ({ change, actions }) => {
|
||||
const showOldStrategyVariants = !useUiFlag('flagOverviewRedesign');
|
||||
return (
|
||||
<>
|
||||
<ChangeItemCreateEditDeleteWrapper>
|
||||
<ChangeItemInfo>
|
||||
<Typography
|
||||
color={
|
||||
change.payload?.disabled
|
||||
? 'action.disabled'
|
||||
: 'success.dark'
|
||||
}
|
||||
>
|
||||
+ Adding strategy:
|
||||
</Typography>
|
||||
<StrategyTooltipLink
|
||||
name={change.payload.name}
|
||||
title={change.payload.title}
|
||||
>
|
||||
<StrategyDiff
|
||||
change={change}
|
||||
currentStrategy={undefined}
|
||||
/>
|
||||
</StrategyTooltipLink>
|
||||
<div>
|
||||
<DisabledEnabledState
|
||||
disabled
|
||||
show={change.payload?.disabled === true}
|
||||
/>
|
||||
</div>
|
||||
</ChangeItemInfo>
|
||||
<div>{actions}</div>
|
||||
</ChangeItemCreateEditDeleteWrapper>
|
||||
<StrategyExecution strategy={change.payload} />
|
||||
{showOldStrategyVariants &&
|
||||
change.payload.variants &&
|
||||
change.payload.variants.length > 0 && (
|
||||
<StyledBox>
|
||||
<StyledTypography>
|
||||
Setting strategy variants to:
|
||||
</StyledTypography>
|
||||
<EnvironmentVariantsTable
|
||||
variants={change.payload.variants}
|
||||
/>
|
||||
</StyledBox>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
}> = ({ change, actions }) => (
|
||||
<>
|
||||
<ChangeItemCreateEditDeleteWrapper>
|
||||
<ChangeItemInfo>
|
||||
<Typography
|
||||
color={
|
||||
change.payload?.disabled
|
||||
? 'action.disabled'
|
||||
: 'success.dark'
|
||||
}
|
||||
>
|
||||
+ Adding strategy:
|
||||
</Typography>
|
||||
<StrategyTooltipLink
|
||||
name={change.payload.name}
|
||||
title={change.payload.title}
|
||||
>
|
||||
<StrategyDiff change={change} currentStrategy={undefined} />
|
||||
</StrategyTooltipLink>
|
||||
<div>
|
||||
<DisabledEnabledState
|
||||
disabled
|
||||
show={change.payload?.disabled === true}
|
||||
/>
|
||||
</div>
|
||||
</ChangeItemInfo>
|
||||
<div>{actions}</div>
|
||||
</ChangeItemCreateEditDeleteWrapper>
|
||||
<StrategyExecution strategy={change.payload} />
|
||||
{change.payload.variants?.length ? (
|
||||
<StyledBox>
|
||||
<StyledTypography>Adding strategy variants:</StyledTypography>
|
||||
<EnvironmentVariantsTable
|
||||
variants={change.payload.variants || []}
|
||||
/>
|
||||
</StyledBox>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
|
||||
export const StrategyChange: FC<{
|
||||
actions?: ReactNode;
|
||||
|
@ -287,7 +287,10 @@ const copyButtonsActiveInOtherEnv = async () => {
|
||||
|
||||
const openEnvironments = async (envNames: string[]) => {
|
||||
for (const env of envNames) {
|
||||
(await screen.findAllByText(env))[1].click();
|
||||
const environmentHeader = await screen.findByRole('heading', {
|
||||
name: env,
|
||||
});
|
||||
fireEvent.click(environmentHeader);
|
||||
}
|
||||
};
|
||||
|
||||
@ -313,7 +316,6 @@ test('open mode + non-project member can perform basic change request actions',
|
||||
<FeatureView />
|
||||
</UnleashUiSetup>,
|
||||
);
|
||||
|
||||
await openEnvironments(['development', 'production', 'custom']);
|
||||
|
||||
await strategiesAreDisplayed('UserIDs', 'Standard');
|
||||
|
@ -18,14 +18,12 @@ import {
|
||||
StyledFlexAlignCenterBox,
|
||||
StyledSuccessIcon,
|
||||
} from '../ChangeRequestSidebar';
|
||||
import CloudCircle from '@mui/icons-material/CloudCircle';
|
||||
import { AddCommentField } from '../../ChangeRequestOverview/ChangeRequestComments/AddCommentField';
|
||||
import { useAuthUser } from 'hooks/api/getters/useAuth/useAuthUser';
|
||||
import Input from 'component/common/Input/Input';
|
||||
import { ChangeRequestTitle } from './ChangeRequestTitle';
|
||||
import { UpdateCount } from 'component/changeRequest/UpdateCount';
|
||||
import { useChangeRequestApi } from 'hooks/api/actions/useChangeRequestApi/useChangeRequestApi';
|
||||
import { useUiFlag } from 'hooks/useUiFlag';
|
||||
|
||||
const SubmitChangeRequestButton: FC<{
|
||||
onClick: () => void;
|
||||
@ -70,7 +68,6 @@ export const EnvironmentChangeRequest: FC<{
|
||||
children?: React.ReactNode;
|
||||
}> = ({ environmentChangeRequest, onClose, onReview, onDiscard, children }) => {
|
||||
const theme = useTheme();
|
||||
const showCloudIcon = !useUiFlag('flagOverviewRedesign');
|
||||
const [commentText, setCommentText] = useState('');
|
||||
const { user } = useAuthUser();
|
||||
const [title, setTitle] = useState(environmentChangeRequest.title);
|
||||
@ -98,14 +95,6 @@ export const EnvironmentChangeRequest: FC<{
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
{showCloudIcon ? (
|
||||
<CloudCircle
|
||||
sx={(theme) => ({
|
||||
color: theme.palette.primary.light,
|
||||
mr: 0.5,
|
||||
})}
|
||||
/>
|
||||
) : null}
|
||||
<Typography component='span' variant='h2'>
|
||||
{environmentChangeRequest.environment}
|
||||
</Typography>
|
||||
|
@ -1,10 +1,7 @@
|
||||
import { ConstraintIcon } from 'component/common/LegacyConstraintAccordion/ConstraintIcon';
|
||||
import type { IConstraint } from 'interfaces/strategy';
|
||||
import { ConstraintAccordionViewHeaderInfo } from './ConstraintAccordionViewHeaderInfo';
|
||||
import { ConstraintAccordionViewHeaderInfo as LegacyConstraintAccordionViewHeaderInfo } from './LegacyConstraintAccordionViewHeaderInfo';
|
||||
import { styled } from '@mui/system';
|
||||
import useUnleashContext from 'hooks/api/getters/useUnleashContext/useUnleashContext';
|
||||
import { useUiFlag } from 'hooks/useUiFlag';
|
||||
import { ConstraintAccordionViewActions } from '../../ConstraintAccordionViewActions/ConstraintAccordionViewActions';
|
||||
import { ConstraintAccordionEditActions } from '../../ConstraintAccordionEditActions/ConstraintAccordionEditActions';
|
||||
|
||||
@ -13,9 +10,15 @@ interface IConstraintAccordionViewHeaderProps {
|
||||
onDelete?: () => void;
|
||||
onEdit?: () => void;
|
||||
onUse?: () => void;
|
||||
/**
|
||||
* @deprecated
|
||||
*/
|
||||
singleValue: boolean;
|
||||
expanded: boolean;
|
||||
allowExpand: (shouldExpand: boolean) => void;
|
||||
/**
|
||||
* @deprecated
|
||||
*/
|
||||
compact?: boolean;
|
||||
disabled?: boolean;
|
||||
}
|
||||
@ -43,7 +46,6 @@ export const ConstraintAccordionViewHeader = ({
|
||||
disabled,
|
||||
}: IConstraintAccordionViewHeaderProps) => {
|
||||
const { context } = useUnleashContext();
|
||||
const flagOverviewRedesign = useUiFlag('flagOverviewRedesign');
|
||||
const { contextName } = constraint;
|
||||
|
||||
const disableEdit = !context
|
||||
@ -52,25 +54,12 @@ export const ConstraintAccordionViewHeader = ({
|
||||
|
||||
return (
|
||||
<StyledContainer>
|
||||
{!flagOverviewRedesign ? (
|
||||
<ConstraintIcon compact={compact} disabled={disabled} />
|
||||
) : null}
|
||||
{flagOverviewRedesign ? (
|
||||
<ConstraintAccordionViewHeaderInfo
|
||||
constraint={constraint}
|
||||
allowExpand={allowExpand}
|
||||
expanded={expanded}
|
||||
disabled={disabled}
|
||||
/>
|
||||
) : (
|
||||
<LegacyConstraintAccordionViewHeaderInfo
|
||||
constraint={constraint}
|
||||
singleValue={singleValue}
|
||||
allowExpand={allowExpand}
|
||||
expanded={expanded}
|
||||
disabled={disabled}
|
||||
/>
|
||||
)}
|
||||
<ConstraintAccordionViewHeaderInfo
|
||||
constraint={constraint}
|
||||
allowExpand={allowExpand}
|
||||
expanded={expanded}
|
||||
disabled={disabled}
|
||||
/>
|
||||
{onUse ? (
|
||||
<ConstraintAccordionViewActions onUse={onUse} />
|
||||
) : (
|
||||
|
@ -1,104 +0,0 @@
|
||||
import { styled, Tooltip } from '@mui/material';
|
||||
import { ConstraintViewHeaderOperator } from './ConstraintViewHeaderOperator';
|
||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||
import { ConstraintAccordionViewHeaderSingleValue } from './ConstraintAccordionViewHeaderSingleValue';
|
||||
import { ConstraintAccordionViewHeaderMultipleValues } from './ConstraintAccordionViewHeaderMultipleValues';
|
||||
import type { IConstraint } from 'interfaces/strategy';
|
||||
|
||||
const StyledHeaderText = styled('span')(({ theme }) => ({
|
||||
display: '-webkit-box',
|
||||
WebkitLineClamp: 3,
|
||||
WebkitBoxOrient: 'vertical',
|
||||
overflow: 'hidden',
|
||||
maxWidth: '100px',
|
||||
minWidth: '100px',
|
||||
marginRight: '10px',
|
||||
marginTop: 'auto',
|
||||
marginBottom: 'auto',
|
||||
wordBreak: 'break-word',
|
||||
fontSize: theme.fontSizes.smallBody,
|
||||
[theme.breakpoints.down(710)]: {
|
||||
textAlign: 'center',
|
||||
padding: theme.spacing(1, 0),
|
||||
marginRight: 'inherit',
|
||||
maxWidth: 'inherit',
|
||||
},
|
||||
}));
|
||||
|
||||
const StyledHeaderWrapper = styled('div')(({ theme }) => ({
|
||||
display: 'flex',
|
||||
width: '100%',
|
||||
justifyContent: 'space-between',
|
||||
borderRadius: theme.spacing(1),
|
||||
}));
|
||||
|
||||
const StyledHeaderMetaInfo = styled('div')(({ theme }) => ({
|
||||
display: 'flex',
|
||||
alignItems: 'stretch',
|
||||
marginLeft: theme.spacing(1),
|
||||
[theme.breakpoints.down('sm')]: {
|
||||
marginLeft: 0,
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
width: '100%',
|
||||
},
|
||||
}));
|
||||
|
||||
interface ConstraintAccordionViewHeaderMetaInfoProps {
|
||||
constraint: IConstraint;
|
||||
singleValue: boolean;
|
||||
expanded: boolean;
|
||||
allowExpand: (shouldExpand: boolean) => void;
|
||||
disabled?: boolean;
|
||||
maxLength?: number;
|
||||
}
|
||||
|
||||
export const ConstraintAccordionViewHeaderInfo = ({
|
||||
constraint,
|
||||
singleValue,
|
||||
allowExpand,
|
||||
expanded,
|
||||
disabled = false,
|
||||
maxLength = 112, //The max number of characters in the values text for NOT allowing expansion
|
||||
}: ConstraintAccordionViewHeaderMetaInfoProps) => {
|
||||
return (
|
||||
<StyledHeaderWrapper>
|
||||
<StyledHeaderMetaInfo>
|
||||
<Tooltip title={constraint.contextName} arrow>
|
||||
<StyledHeaderText
|
||||
sx={(theme) => ({
|
||||
color: disabled
|
||||
? theme.palette.text.secondary
|
||||
: 'inherit',
|
||||
})}
|
||||
>
|
||||
{constraint.contextName}
|
||||
</StyledHeaderText>
|
||||
</Tooltip>
|
||||
<ConstraintViewHeaderOperator
|
||||
constraint={constraint}
|
||||
disabled={disabled}
|
||||
/>
|
||||
<ConditionallyRender
|
||||
condition={singleValue}
|
||||
show={
|
||||
<ConstraintAccordionViewHeaderSingleValue
|
||||
constraint={constraint}
|
||||
allowExpand={allowExpand}
|
||||
disabled={disabled}
|
||||
/>
|
||||
}
|
||||
elseShow={
|
||||
<ConstraintAccordionViewHeaderMultipleValues
|
||||
constraint={constraint}
|
||||
expanded={expanded}
|
||||
allowExpand={allowExpand}
|
||||
maxLength={maxLength}
|
||||
disabled={disabled}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</StyledHeaderMetaInfo>
|
||||
</StyledHeaderWrapper>
|
||||
);
|
||||
};
|
@ -1,38 +0,0 @@
|
||||
import type { VFC } from 'react';
|
||||
import { Box } from '@mui/material';
|
||||
import TrackChanges from '@mui/icons-material/TrackChanges';
|
||||
|
||||
interface IConstraintIconProps {
|
||||
compact?: boolean;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated remove with `flagOverviewRedesign`
|
||||
*/
|
||||
export const ConstraintIcon: VFC<IConstraintIconProps> = ({
|
||||
compact,
|
||||
disabled,
|
||||
}) => (
|
||||
<Box
|
||||
sx={(theme) => ({
|
||||
backgroundColor: disabled
|
||||
? theme.palette.neutral.border
|
||||
: 'primary.light',
|
||||
p: compact ? '1px' : '2px',
|
||||
borderRadius: '50%',
|
||||
width: compact ? '18px' : '24px',
|
||||
height: compact ? '18px' : '24px',
|
||||
marginRight: '13px',
|
||||
})}
|
||||
>
|
||||
<TrackChanges
|
||||
sx={(theme) => ({
|
||||
fill: theme.palette.common.white,
|
||||
display: 'block',
|
||||
width: compact ? theme.spacing(2) : theme.spacing(2.5),
|
||||
height: compact ? theme.spacing(2) : theme.spacing(2.5),
|
||||
})}
|
||||
/>
|
||||
</Box>
|
||||
);
|
@ -1,5 +1,5 @@
|
||||
import type React from 'react';
|
||||
import { forwardRef, Fragment, useImperativeHandle } from 'react';
|
||||
import { forwardRef, useImperativeHandle } from 'react';
|
||||
import { styled } from '@mui/material';
|
||||
import type { IConstraint } from 'interfaces/strategy';
|
||||
import produce from 'immer';
|
||||
@ -9,8 +9,6 @@ import {
|
||||
constraintId,
|
||||
createEmptyConstraint,
|
||||
} from 'component/common/LegacyConstraintAccordion/ConstraintAccordionList/createEmptyConstraint';
|
||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||
import { StrategySeparator } from 'component/common/StrategySeparator/LegacyStrategySeparator';
|
||||
import { NewConstraintAccordion } from 'component/common/NewConstraintAccordion/NewConstraintAccordion';
|
||||
import { ConstraintsList } from 'component/common/ConstraintsList/ConstraintsList';
|
||||
import { useUiFlag } from 'hooks/useUiFlag';
|
||||
@ -87,7 +85,6 @@ export const NewConstraintAccordionList = forwardRef<
|
||||
IConstraintList
|
||||
>(({ constraints, setConstraints, state }, ref) => {
|
||||
const { context } = useUnleashContext();
|
||||
const flagOverviewRedesign = useUiFlag('flagOverviewRedesign');
|
||||
const addEditStrategy = useUiFlag('addEditStrategy');
|
||||
|
||||
const onEdit =
|
||||
@ -145,77 +142,43 @@ export const NewConstraintAccordionList = forwardRef<
|
||||
return null;
|
||||
}
|
||||
|
||||
if (flagOverviewRedesign) {
|
||||
return (
|
||||
<StyledContainer id={constraintAccordionListId}>
|
||||
<ConstraintsList>
|
||||
{constraints.map((constraint, index) =>
|
||||
addEditStrategy ? (
|
||||
state.get(constraint)?.editing ? (
|
||||
<EditableConstraintWrapper
|
||||
key={constraint[constraintId]}
|
||||
constraint={constraint}
|
||||
onCancel={onCancel?.bind(null, index)}
|
||||
onDelete={onRemove?.bind(null, index)}
|
||||
onSave={onSave!.bind(null, index)}
|
||||
onAutoSave={onAutoSave?.(
|
||||
constraint[constraintId],
|
||||
)}
|
||||
/>
|
||||
) : (
|
||||
<ConstraintAccordionView
|
||||
key={constraint[constraintId]}
|
||||
constraint={constraint}
|
||||
/>
|
||||
)
|
||||
) : (
|
||||
<NewConstraintAccordion
|
||||
return (
|
||||
<StyledContainer id={constraintAccordionListId}>
|
||||
<ConstraintsList>
|
||||
{constraints.map((constraint, index) =>
|
||||
addEditStrategy ? (
|
||||
state.get(constraint)?.editing ? (
|
||||
<EditableConstraintWrapper
|
||||
key={constraint[constraintId]}
|
||||
constraint={constraint}
|
||||
onEdit={onEdit?.bind(null, constraint)}
|
||||
onCancel={onCancel.bind(null, index)}
|
||||
onCancel={onCancel?.bind(null, index)}
|
||||
onDelete={onRemove?.bind(null, index)}
|
||||
onSave={onSave?.bind(null, index)}
|
||||
onSave={onSave!.bind(null, index)}
|
||||
onAutoSave={onAutoSave?.(
|
||||
constraint[constraintId],
|
||||
)}
|
||||
editing={Boolean(
|
||||
state.get(constraint)?.editing,
|
||||
)}
|
||||
compact
|
||||
/>
|
||||
),
|
||||
)}
|
||||
</ConstraintsList>
|
||||
</StyledContainer>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<StyledContainer id={constraintAccordionListId}>
|
||||
{constraints.map((constraint, index) => {
|
||||
const id = constraint[constraintId];
|
||||
|
||||
return (
|
||||
<Fragment key={id}>
|
||||
<ConditionallyRender
|
||||
condition={index > 0}
|
||||
show={<StrategySeparator text='AND' />}
|
||||
/>
|
||||
|
||||
) : (
|
||||
<ConstraintAccordionView
|
||||
key={constraint[constraintId]}
|
||||
constraint={constraint}
|
||||
/>
|
||||
)
|
||||
) : (
|
||||
<NewConstraintAccordion
|
||||
key={constraint[constraintId]}
|
||||
constraint={constraint}
|
||||
onEdit={onEdit?.bind(null, constraint)}
|
||||
onCancel={onCancel.bind(null, index)}
|
||||
onDelete={onRemove?.bind(null, index)}
|
||||
onSave={onSave?.bind(null, index)}
|
||||
onAutoSave={onAutoSave?.(id)}
|
||||
onAutoSave={onAutoSave?.(constraint[constraintId])}
|
||||
editing={Boolean(state.get(constraint)?.editing)}
|
||||
compact
|
||||
/>
|
||||
</Fragment>
|
||||
);
|
||||
})}
|
||||
),
|
||||
)}
|
||||
</ConstraintsList>
|
||||
</StyledContainer>
|
||||
);
|
||||
});
|
||||
|
@ -1,205 +0,0 @@
|
||||
// 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>
|
||||
);
|
||||
};
|
@ -1,31 +1,8 @@
|
||||
import { screen } from '@testing-library/react';
|
||||
import { render } from 'utils/testRenderer';
|
||||
import { StrategyItemContainer as LegacyStrategyItemContainer } from './LegacyStrategyItemContainer';
|
||||
import type { IFeatureStrategy } from 'interfaces/strategy';
|
||||
import { StrategyItemContainer } from './StrategyItemContainer';
|
||||
|
||||
// todo: remove this test along with the flag flagOverviewRedesign
|
||||
test('(deprecated) should render strategy name, custom title and description', async () => {
|
||||
const strategy: IFeatureStrategy = {
|
||||
id: 'irrelevant',
|
||||
name: 'strategy name',
|
||||
title: 'custom title',
|
||||
constraints: [],
|
||||
parameters: {},
|
||||
};
|
||||
|
||||
render(
|
||||
<LegacyStrategyItemContainer
|
||||
strategy={strategy}
|
||||
description={'description'}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.getByText('strategy name')).toBeInTheDocument();
|
||||
expect(screen.getByText('description')).toBeInTheDocument();
|
||||
await screen.findByText('custom title'); // behind async flag
|
||||
});
|
||||
|
||||
test('should render strategy name, custom title and description', async () => {
|
||||
const strategy: IFeatureStrategy = {
|
||||
id: 'irrelevant',
|
||||
|
@ -29,6 +29,9 @@ const StyledCenteredContent = styled(StyledContent)(({ theme }) => ({
|
||||
padding: theme.spacing(0.75, 1.5),
|
||||
}));
|
||||
|
||||
/**
|
||||
* @deprecated remove with 'flagOverviewRedesign' flag. This pollutes a lot of places in the codebase 😞
|
||||
*/
|
||||
export const StrategySeparator = ({ text }: IStrategySeparatorProps) => {
|
||||
const theme = useTheme();
|
||||
|
||||
|
@ -1,106 +0,0 @@
|
||||
import { type MouseEvent, useContext, useState, type VFC } from 'react';
|
||||
import {
|
||||
Button,
|
||||
ListItemIcon,
|
||||
ListItemText,
|
||||
Menu,
|
||||
MenuItem,
|
||||
Tooltip,
|
||||
} from '@mui/material';
|
||||
import Lock from '@mui/icons-material/Lock';
|
||||
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
|
||||
import type { IFeatureEnvironment } from 'interfaces/featureToggle';
|
||||
import AccessContext from 'contexts/AccessContext';
|
||||
import { CREATE_FEATURE_STRATEGY } from 'component/providers/AccessProvider/permissions';
|
||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||
|
||||
interface ICopyButtonProps {
|
||||
environmentId: IFeatureEnvironment['name'];
|
||||
environments: IFeatureEnvironment['name'][];
|
||||
onClick: (environmentId: string) => void;
|
||||
}
|
||||
|
||||
export const CopyButton: VFC<ICopyButtonProps> = ({
|
||||
environmentId,
|
||||
environments,
|
||||
onClick,
|
||||
}) => {
|
||||
const projectId = useRequiredPathParam('projectId');
|
||||
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
|
||||
const open = Boolean(anchorEl);
|
||||
const { hasAccess } = useContext(AccessContext);
|
||||
const enabled = environments.some((environment) =>
|
||||
hasAccess(CREATE_FEATURE_STRATEGY, projectId, environment),
|
||||
);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Tooltip title={enabled ? '' : '(Access denied)'}>
|
||||
<div>
|
||||
<Button
|
||||
id={`copy-all-strategies-${environmentId}`}
|
||||
aria-controls={open ? 'basic-menu' : undefined}
|
||||
aria-haspopup='true'
|
||||
aria-expanded={open ? 'true' : undefined}
|
||||
onClick={(event: MouseEvent<HTMLButtonElement>) => {
|
||||
setAnchorEl(event.currentTarget);
|
||||
}}
|
||||
disabled={!enabled}
|
||||
variant='outlined'
|
||||
>
|
||||
Copy from another environment
|
||||
</Button>
|
||||
</div>
|
||||
</Tooltip>
|
||||
<Menu
|
||||
id='basic-menu'
|
||||
anchorEl={anchorEl}
|
||||
open={open}
|
||||
onClose={() => {
|
||||
setAnchorEl(null);
|
||||
}}
|
||||
MenuListProps={{
|
||||
'aria-labelledby': `copy-all-strategies-${environmentId}`,
|
||||
}}
|
||||
>
|
||||
{environments.map((environment) => {
|
||||
const access = hasAccess(
|
||||
CREATE_FEATURE_STRATEGY,
|
||||
projectId,
|
||||
environment,
|
||||
);
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
title={
|
||||
access
|
||||
? ''
|
||||
: "You don't have access to add a strategy to this environment"
|
||||
}
|
||||
key={environment}
|
||||
>
|
||||
<div>
|
||||
<MenuItem
|
||||
onClick={() => onClick(environment)}
|
||||
disabled={!access}
|
||||
>
|
||||
<ConditionallyRender
|
||||
condition={!access}
|
||||
show={
|
||||
<ListItemIcon>
|
||||
<Lock fontSize='small' />
|
||||
</ListItemIcon>
|
||||
}
|
||||
/>
|
||||
<ListItemText>
|
||||
Copy from {environment}
|
||||
</ListItemText>
|
||||
</MenuItem>
|
||||
</div>
|
||||
</Tooltip>
|
||||
);
|
||||
})}
|
||||
</Menu>
|
||||
</div>
|
||||
);
|
||||
};
|
@ -1,187 +0,0 @@
|
||||
// deprecated; remove with the `flagOverviewRedesign` flag
|
||||
import { Link } from 'react-router-dom';
|
||||
import { Box, styled } from '@mui/material';
|
||||
import useFeatureStrategyApi from 'hooks/api/actions/useFeatureStrategyApi/useFeatureStrategyApi';
|
||||
import useToast from 'hooks/useToast';
|
||||
import { useFeature } from 'hooks/api/getters/useFeature/useFeature';
|
||||
import { formatUnknownError } from 'utils/formatUnknownError';
|
||||
import { useFeatureImmutable } from 'hooks/api/getters/useFeature/useFeatureImmutable';
|
||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||
import { CopyButton } from './CopyButton/CopyButton';
|
||||
import { useChangeRequestAddStrategy } from 'hooks/useChangeRequestAddStrategy';
|
||||
import { ChangeRequestDialogue } from 'component/changeRequest/ChangeRequestConfirmDialog/ChangeRequestConfirmDialog';
|
||||
import { CopyStrategiesMessage } from 'component/changeRequest/ChangeRequestConfirmDialog/ChangeRequestMessages/CopyStrategiesMessage';
|
||||
import { useChangeRequestsEnabled } from 'hooks/useChangeRequestsEnabled';
|
||||
import { FeatureStrategyMenuWrapper } from '../FeatureStrategyMenu/FeatureStrategyMenu';
|
||||
|
||||
interface IFeatureStrategyEmptyProps {
|
||||
projectId: string;
|
||||
featureId: string;
|
||||
environmentId: string;
|
||||
}
|
||||
|
||||
const StyledContainer = styled('div')(({ theme }) => ({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
paddingTop: theme.spacing(2),
|
||||
}));
|
||||
|
||||
const StyledTitle = styled('div')(({ theme }) => ({
|
||||
fontSize: theme.fontSizes.bodySize,
|
||||
textAlign: 'center',
|
||||
color: theme.palette.text.primary,
|
||||
marginBottom: theme.spacing(1),
|
||||
}));
|
||||
|
||||
const StyledDescription = styled('p')(({ theme }) => ({
|
||||
color: theme.palette.text.secondary,
|
||||
fontSize: theme.fontSizes.smallBody,
|
||||
textAlign: 'center',
|
||||
marginBottom: theme.spacing(3),
|
||||
|
||||
a: {
|
||||
color: theme.palette.links,
|
||||
},
|
||||
}));
|
||||
|
||||
export const FeatureStrategyEmpty = ({
|
||||
projectId,
|
||||
featureId,
|
||||
environmentId,
|
||||
}: IFeatureStrategyEmptyProps) => {
|
||||
const { addStrategyToFeature } = useFeatureStrategyApi();
|
||||
const { setToastData, setToastApiError } = useToast();
|
||||
const { refetchFeature } = useFeature(projectId, featureId);
|
||||
const { refetchFeature: refetchFeatureImmutable } = useFeatureImmutable(
|
||||
projectId,
|
||||
featureId,
|
||||
);
|
||||
const { feature } = useFeature(projectId, featureId);
|
||||
const otherAvailableEnvironments = feature?.environments.filter(
|
||||
(environment) =>
|
||||
environment.name !== environmentId &&
|
||||
environment.strategies &&
|
||||
environment.strategies.length > 0,
|
||||
);
|
||||
const { isChangeRequestConfigured } = useChangeRequestsEnabled(projectId);
|
||||
|
||||
const {
|
||||
changeRequestDialogDetails,
|
||||
onChangeRequestAddStrategies,
|
||||
onChangeRequestAddStrategiesConfirm,
|
||||
onChangeRequestAddStrategyClose,
|
||||
} = useChangeRequestAddStrategy(projectId, featureId, 'addStrategy');
|
||||
|
||||
const onAfterAddStrategy = (multiple = false) => {
|
||||
refetchFeature();
|
||||
refetchFeatureImmutable();
|
||||
|
||||
setToastData({
|
||||
text: multiple ? 'Strategies created' : 'Strategy created',
|
||||
type: 'success',
|
||||
});
|
||||
};
|
||||
|
||||
const onCopyStrategies = async (fromEnvironmentName: string) => {
|
||||
const strategies =
|
||||
otherAvailableEnvironments?.find(
|
||||
(environment) => environment.name === fromEnvironmentName,
|
||||
)?.strategies || [];
|
||||
|
||||
if (isChangeRequestConfigured(environmentId)) {
|
||||
await onChangeRequestAddStrategies(
|
||||
environmentId,
|
||||
strategies,
|
||||
fromEnvironmentName,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await Promise.all(
|
||||
strategies.map((strategy) => {
|
||||
const { id, ...strategyCopy } = {
|
||||
...strategy,
|
||||
environment: environmentId,
|
||||
};
|
||||
|
||||
return addStrategyToFeature(
|
||||
projectId,
|
||||
featureId,
|
||||
environmentId,
|
||||
strategyCopy,
|
||||
);
|
||||
}),
|
||||
);
|
||||
onAfterAddStrategy(true);
|
||||
} catch (error) {
|
||||
setToastApiError(formatUnknownError(error));
|
||||
}
|
||||
};
|
||||
|
||||
const canCopyFromOtherEnvironment =
|
||||
otherAvailableEnvironments && otherAvailableEnvironments.length > 0;
|
||||
|
||||
return (
|
||||
<>
|
||||
<ChangeRequestDialogue
|
||||
isOpen={changeRequestDialogDetails.isOpen}
|
||||
onClose={onChangeRequestAddStrategyClose}
|
||||
environment={changeRequestDialogDetails?.environment}
|
||||
onConfirm={onChangeRequestAddStrategiesConfirm}
|
||||
messageComponent={
|
||||
<CopyStrategiesMessage
|
||||
fromEnvironment={
|
||||
changeRequestDialogDetails.fromEnvironment!
|
||||
}
|
||||
payload={changeRequestDialogDetails.strategies!}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
||||
<StyledContainer>
|
||||
<StyledTitle>
|
||||
You have not defined any strategies yet.
|
||||
</StyledTitle>
|
||||
<StyledDescription>
|
||||
Strategies added in this environment will only be executed
|
||||
if the SDK is using an{' '}
|
||||
<Link to='/admin/api'>API key configured</Link> for this
|
||||
environment.
|
||||
</StyledDescription>
|
||||
<Box
|
||||
sx={{
|
||||
w: '100%',
|
||||
display: 'flex',
|
||||
flexWrap: 'wrap',
|
||||
gap: 2,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
}}
|
||||
>
|
||||
<FeatureStrategyMenuWrapper
|
||||
label='Add your first strategy'
|
||||
projectId={projectId}
|
||||
featureId={featureId}
|
||||
environmentId={environmentId}
|
||||
matchWidth={canCopyFromOtherEnvironment}
|
||||
/>
|
||||
<ConditionallyRender
|
||||
condition={canCopyFromOtherEnvironment}
|
||||
show={
|
||||
<CopyButton
|
||||
environmentId={environmentId}
|
||||
environments={otherAvailableEnvironments.map(
|
||||
(environment) => environment.name,
|
||||
)}
|
||||
onClick={onCopyStrategies}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</Box>
|
||||
</StyledContainer>
|
||||
</>
|
||||
);
|
||||
};
|
@ -13,7 +13,6 @@ import { useEffect, useState } from 'react';
|
||||
import { useLastViewedFlags } from 'hooks/useLastViewedFlags';
|
||||
import { useUiFlag } from 'hooks/useUiFlag';
|
||||
import { FeatureOverviewEnvironments } from './FeatureOverviewEnvironments/FeatureOverviewEnvironments';
|
||||
import { default as LegacyFleatureOverview } from './LegacyFeatureOverview';
|
||||
import { useEnvironmentVisibility } from './FeatureOverviewMetaData/EnvironmentVisibilityMenu/hooks/useEnvironmentVisibility';
|
||||
import useSplashApi from 'hooks/api/actions/useSplashApi/useSplashApi';
|
||||
import { useAuthSplash } from 'hooks/api/getters/useAuth/useAuthSplash';
|
||||
@ -50,18 +49,12 @@ export const FeatureOverview = () => {
|
||||
useEffect(() => {
|
||||
setLastViewed({ featureId, projectId });
|
||||
}, [featureId]);
|
||||
const flagOverviewRedesign = useUiFlag('flagOverviewRedesign');
|
||||
const { setSplashSeen } = useSplashApi();
|
||||
const { splash } = useAuthSplash();
|
||||
const [showTooltip, setShowTooltip] = useState(false);
|
||||
const [hasClosedTooltip, setHasClosedTooltip] = useState(false);
|
||||
const { feature, refetchFeature } = useFeature(projectId, featureId);
|
||||
const cleanupReminderEnabled = useUiFlag('cleanupReminder');
|
||||
|
||||
if (!flagOverviewRedesign) {
|
||||
return <LegacyFleatureOverview />;
|
||||
}
|
||||
|
||||
const dragTooltipSplashId = 'strategy-drag-tooltip';
|
||||
const shouldShowStrategyDragTooltip = !splash?.[dragTooltipSplashId];
|
||||
const toggleShowTooltip = (envIsOpen: boolean) => {
|
||||
|
@ -1,354 +0,0 @@
|
||||
// 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/LegacyReleasePlan';
|
||||
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');
|
||||
}
|
||||
}, []);
|
||||
|
||||
const pageSize = 20;
|
||||
const { page, pages, setPageIndex, pageIndex } =
|
||||
usePagination<IFeatureStrategy>(strategies, pageSize);
|
||||
|
||||
if (!featureEnvironment) {
|
||||
return null;
|
||||
}
|
||||
|
||||
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;
|
@ -1,150 +0,0 @@
|
||||
// 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;
|
||||
};
|
@ -1,103 +0,0 @@
|
||||
// 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 { VariantsSplitPreview } from 'component/common/VariantsSplitPreview/VariantsSplitPreview';
|
||||
import { Box } from '@mui/material';
|
||||
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' }}>
|
||||
<VariantsSplitPreview variants={strategy.variants} />
|
||||
</Box>
|
||||
) : (
|
||||
<VariantsSplitPreview variants={strategy.variants} />
|
||||
))}
|
||||
</StrategyItemContainer>
|
||||
);
|
||||
};
|
@ -1,372 +0,0 @@
|
||||
import { type FC, Fragment, useMemo } from 'react';
|
||||
import { Alert, Box, Chip, Link, styled } from '@mui/material';
|
||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||
import PercentageCircle from 'component/common/PercentageCircle/PercentageCircle';
|
||||
import { StrategySeparator } from 'component/common/StrategySeparator/LegacyStrategySeparator';
|
||||
import { ConstraintItem } from './ConstraintItem/LegacyConstraintItem';
|
||||
import { useStrategies } from 'hooks/api/getters/useStrategies/useStrategies';
|
||||
import { useSegments } from 'hooks/api/getters/useSegments/useSegments';
|
||||
import { FeatureOverviewSegment } from 'component/feature/FeatureView/FeatureOverview/FeatureOverviewSegment/FeatureOverviewSegment';
|
||||
import { ConstraintAccordionList } from 'component/common/LegacyConstraintAccordion/ConstraintAccordionList/ConstraintAccordionList';
|
||||
import {
|
||||
parseParameterNumber,
|
||||
parseParameterString,
|
||||
parseParameterStrings,
|
||||
} from 'utils/parseParameter';
|
||||
import StringTruncator from 'component/common/StringTruncator/StringTruncator';
|
||||
import { Badge } from 'component/common/Badge/Badge';
|
||||
import type { CreateFeatureStrategySchema } from 'openapi';
|
||||
import type { IFeatureStrategyPayload } from 'interfaces/strategy';
|
||||
import { BuiltInStrategies } from 'utils/strategyNames';
|
||||
|
||||
interface IStrategyExecutionProps {
|
||||
strategy: IFeatureStrategyPayload | CreateFeatureStrategySchema;
|
||||
displayGroupId?: boolean;
|
||||
}
|
||||
|
||||
const StyledContainer = styled(Box, {
|
||||
shouldForwardProp: (prop) => prop !== 'disabled',
|
||||
})<{ disabled?: boolean | null }>(({ theme, disabled }) => ({
|
||||
'& p, & span, & h1, & h2, & h3, & h4, & h5, & h6': {
|
||||
color: disabled ? theme.palette.neutral.main : 'inherit',
|
||||
},
|
||||
'.constraint-icon-container': {
|
||||
backgroundColor: disabled
|
||||
? theme.palette.neutral.border
|
||||
: theme.palette.primary.light,
|
||||
borderRadius: '50%',
|
||||
},
|
||||
'.constraint-icon': {
|
||||
fill: disabled
|
||||
? theme.palette.neutral.light
|
||||
: theme.palette.common.white,
|
||||
},
|
||||
}));
|
||||
|
||||
const CustomStrategyDeprecationWarning = () => (
|
||||
<Alert severity='warning' sx={{ mb: 2 }}>
|
||||
Custom strategies are deprecated and may be removed in a future major
|
||||
version. Consider rewriting this strategy as a predefined strategy with{' '}
|
||||
<Link
|
||||
href={
|
||||
'https://docs.getunleash.io/reference/activation-strategies#constraints'
|
||||
}
|
||||
target='_blank'
|
||||
variant='body2'
|
||||
>
|
||||
constraints.
|
||||
</Link>
|
||||
</Alert>
|
||||
);
|
||||
|
||||
const NoItems: FC = () => (
|
||||
<Box sx={{ px: 3, color: 'text.disabled' }}>
|
||||
This strategy does not have constraints or parameters.
|
||||
</Box>
|
||||
);
|
||||
|
||||
const StyledValueContainer = styled(Box)(({ theme }) => ({
|
||||
padding: theme.spacing(2, 3),
|
||||
border: `1px solid ${theme.palette.divider}`,
|
||||
borderRadius: theme.shape.borderRadiusMedium,
|
||||
background: theme.palette.background.default,
|
||||
}));
|
||||
|
||||
const StyledValueSeparator = styled('span')(({ theme }) => ({
|
||||
color: theme.palette.neutral.main,
|
||||
}));
|
||||
|
||||
export const StrategyExecution: FC<IStrategyExecutionProps> = ({
|
||||
strategy,
|
||||
displayGroupId = false,
|
||||
}) => {
|
||||
const { parameters, constraints = [] } = strategy;
|
||||
const stickiness = parameters?.stickiness;
|
||||
const explainStickiness =
|
||||
typeof stickiness === 'string' && stickiness !== 'default';
|
||||
const { strategies } = useStrategies();
|
||||
const { segments } = useSegments();
|
||||
const strategySegments = segments?.filter((segment) => {
|
||||
return strategy.segments?.includes(segment.id);
|
||||
});
|
||||
|
||||
const definition = strategies.find((strategyDefinition) => {
|
||||
return strategyDefinition.name === strategy.name;
|
||||
});
|
||||
|
||||
const parametersList = useMemo(() => {
|
||||
if (!parameters || definition?.editable) return null;
|
||||
|
||||
return Object.keys(parameters).map((key) => {
|
||||
switch (key) {
|
||||
case 'rollout':
|
||||
case 'Rollout': {
|
||||
const percentage = parseParameterNumber(parameters[key]);
|
||||
|
||||
const badgeType = strategy.disabled ? 'neutral' : 'success';
|
||||
|
||||
return (
|
||||
<StyledValueContainer
|
||||
sx={{ display: 'flex', alignItems: 'center' }}
|
||||
>
|
||||
<Box sx={{ mr: 2 }}>
|
||||
<PercentageCircle
|
||||
percentage={percentage}
|
||||
size='2rem'
|
||||
disabled={strategy.disabled}
|
||||
/>
|
||||
</Box>
|
||||
<div>
|
||||
<Badge color={badgeType}>{percentage}%</Badge>{' '}
|
||||
<span>of your base</span>{' '}
|
||||
<span>
|
||||
{explainStickiness ? (
|
||||
<>
|
||||
with <strong>{stickiness}</strong>
|
||||
</>
|
||||
) : (
|
||||
''
|
||||
)}{' '}
|
||||
</span>
|
||||
<span>
|
||||
{constraints.length > 0
|
||||
? 'who match constraints'
|
||||
: ''}{' '}
|
||||
is included.
|
||||
</span>
|
||||
</div>
|
||||
{displayGroupId && parameters.groupId && (
|
||||
<Box
|
||||
sx={(theme) => ({
|
||||
ml: 1,
|
||||
color: theme.palette.info.contrastText,
|
||||
})}
|
||||
>
|
||||
<Badge color='info'>
|
||||
GroupId: {parameters.groupId}
|
||||
</Badge>
|
||||
</Box>
|
||||
)}
|
||||
</StyledValueContainer>
|
||||
);
|
||||
}
|
||||
case 'userIds':
|
||||
case 'UserIds': {
|
||||
const users = parseParameterStrings(parameters[key]);
|
||||
return (
|
||||
<ConstraintItem key={key} value={users} text='user' />
|
||||
);
|
||||
}
|
||||
case 'hostNames':
|
||||
case 'HostNames': {
|
||||
const hosts = parseParameterStrings(parameters[key]);
|
||||
return (
|
||||
<ConstraintItem key={key} value={hosts} text={'host'} />
|
||||
);
|
||||
}
|
||||
case 'IPs': {
|
||||
const IPs = parseParameterStrings(parameters[key]);
|
||||
return <ConstraintItem key={key} value={IPs} text={'IP'} />;
|
||||
}
|
||||
case 'stickiness':
|
||||
case 'groupId':
|
||||
return null;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
});
|
||||
}, [parameters, definition, constraints, strategy.disabled]);
|
||||
|
||||
const customStrategyList = useMemo(() => {
|
||||
if (!parameters || !definition?.editable) return null;
|
||||
const isSetTo = (
|
||||
<StyledValueSeparator>{' is set to '}</StyledValueSeparator>
|
||||
);
|
||||
|
||||
return definition?.parameters.map((param) => {
|
||||
const { type, name } = { ...param };
|
||||
if (!type || !name || parameters[name] === undefined) {
|
||||
return null;
|
||||
}
|
||||
const nameItem = (
|
||||
<StringTruncator maxLength={15} maxWidth='150' text={name} />
|
||||
);
|
||||
|
||||
switch (param?.type) {
|
||||
case 'list': {
|
||||
const values = parseParameterStrings(parameters[name]);
|
||||
|
||||
return values.length > 0 ? (
|
||||
<StyledValueContainer>
|
||||
{nameItem}{' '}
|
||||
<StyledValueSeparator>
|
||||
has {values.length}{' '}
|
||||
{values.length > 1 ? `items` : 'item'}:{' '}
|
||||
{values.map((item: string) => (
|
||||
<Chip
|
||||
key={item}
|
||||
label={
|
||||
<StringTruncator
|
||||
maxWidth='300'
|
||||
text={item}
|
||||
maxLength={50}
|
||||
/>
|
||||
}
|
||||
sx={{ mr: 0.5 }}
|
||||
/>
|
||||
))}
|
||||
</StyledValueSeparator>
|
||||
</StyledValueContainer>
|
||||
) : null;
|
||||
}
|
||||
|
||||
case 'percentage': {
|
||||
const percentage = parseParameterNumber(parameters[name]);
|
||||
return parameters[name] !== '' ? (
|
||||
<StyledValueContainer
|
||||
sx={{ display: 'flex', alignItems: 'center' }}
|
||||
>
|
||||
<Box sx={{ mr: 2 }}>
|
||||
<PercentageCircle
|
||||
percentage={percentage}
|
||||
size='2rem'
|
||||
/>
|
||||
</Box>
|
||||
<div>
|
||||
{nameItem}
|
||||
{isSetTo}
|
||||
<Badge color='success'>{percentage}%</Badge>
|
||||
</div>
|
||||
</StyledValueContainer>
|
||||
) : null;
|
||||
}
|
||||
|
||||
case 'boolean':
|
||||
return parameters[name] === 'true' ||
|
||||
parameters[name] === 'false' ? (
|
||||
<StyledValueContainer>
|
||||
<StringTruncator
|
||||
maxLength={15}
|
||||
maxWidth='150'
|
||||
text={name}
|
||||
/>
|
||||
{isSetTo}
|
||||
<Badge
|
||||
color={
|
||||
parameters[name] === 'true'
|
||||
? 'success'
|
||||
: 'error'
|
||||
}
|
||||
>
|
||||
{parameters[name]}
|
||||
</Badge>
|
||||
</StyledValueContainer>
|
||||
) : null;
|
||||
|
||||
case 'string': {
|
||||
const value = parseParameterString(parameters[name]);
|
||||
return typeof parameters[name] !== 'undefined' ? (
|
||||
<StyledValueContainer>
|
||||
{nameItem}
|
||||
<ConditionallyRender
|
||||
condition={value === ''}
|
||||
show={
|
||||
<StyledValueSeparator>
|
||||
{' is an empty string'}
|
||||
</StyledValueSeparator>
|
||||
}
|
||||
elseShow={
|
||||
<>
|
||||
{isSetTo}
|
||||
<StringTruncator
|
||||
maxWidth='300'
|
||||
text={value}
|
||||
maxLength={50}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
</StyledValueContainer>
|
||||
) : null;
|
||||
}
|
||||
|
||||
case 'number': {
|
||||
const number = parseParameterNumber(parameters[name]);
|
||||
return parameters[name] !== '' && number !== undefined ? (
|
||||
<StyledValueContainer>
|
||||
{nameItem}
|
||||
{isSetTo}
|
||||
<StringTruncator
|
||||
maxWidth='300'
|
||||
text={String(number)}
|
||||
maxLength={50}
|
||||
/>
|
||||
</StyledValueContainer>
|
||||
) : null;
|
||||
}
|
||||
case 'default':
|
||||
return null;
|
||||
}
|
||||
|
||||
return null;
|
||||
});
|
||||
}, [parameters, definition]);
|
||||
|
||||
if (!parameters) {
|
||||
return <NoItems />;
|
||||
}
|
||||
|
||||
const listItems = [
|
||||
strategySegments && strategySegments.length > 0 && (
|
||||
<FeatureOverviewSegment
|
||||
segments={strategySegments}
|
||||
disabled={strategy.disabled}
|
||||
/>
|
||||
),
|
||||
constraints.length > 0 && (
|
||||
<ConstraintAccordionList
|
||||
constraints={constraints}
|
||||
showLabel={false}
|
||||
/>
|
||||
),
|
||||
strategy.name === 'default' && (
|
||||
<>
|
||||
<StyledValueContainer sx={{ width: '100%' }}>
|
||||
The standard strategy is <Badge color='success'>ON</Badge>{' '}
|
||||
for all users.
|
||||
</StyledValueContainer>
|
||||
</>
|
||||
),
|
||||
...(parametersList ?? []),
|
||||
...(customStrategyList ?? []),
|
||||
].filter(Boolean);
|
||||
|
||||
return (
|
||||
<>
|
||||
<ConditionallyRender
|
||||
condition={
|
||||
!BuiltInStrategies.includes(strategy.name || 'default')
|
||||
}
|
||||
show={<CustomStrategyDeprecationWarning />}
|
||||
/>
|
||||
|
||||
<ConditionallyRender
|
||||
condition={listItems.length > 0}
|
||||
show={
|
||||
<StyledContainer disabled={Boolean(strategy.disabled)}>
|
||||
{listItems.map((item, index) => (
|
||||
<Fragment key={index}>
|
||||
<ConditionallyRender
|
||||
condition={index > 0}
|
||||
show={<StrategySeparator text='AND' />}
|
||||
/>
|
||||
{item}
|
||||
</Fragment>
|
||||
))}
|
||||
</StyledContainer>
|
||||
}
|
||||
elseShow={<NoItems />}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
@ -1,8 +1,6 @@
|
||||
import type { FC } from 'react';
|
||||
import type { FeatureStrategySchema } from 'openapi';
|
||||
import type { IFeatureStrategyPayload } from 'interfaces/strategy';
|
||||
import { useUiFlag } from 'hooks/useUiFlag';
|
||||
import { StrategyExecution as LegacyStrategyExecution } from './LegacyStrategyExecution';
|
||||
import { ConstraintAccordionView } from 'component/common/NewConstraintAccordion/ConstraintAccordionView/ConstraintAccordionView';
|
||||
import { useStrategies } from 'hooks/api/getters/useStrategies/useStrategies';
|
||||
import { objectId } from 'utils/objectId';
|
||||
@ -37,16 +35,6 @@ export const StrategyExecution: FC<StrategyExecutionProps> = ({
|
||||
const strategySegments = segments?.filter((segment) =>
|
||||
strategy.segments?.includes(segment.id),
|
||||
);
|
||||
const flagOverviewRedesign = useUiFlag('flagOverviewRedesign');
|
||||
|
||||
if (!flagOverviewRedesign) {
|
||||
return (
|
||||
<LegacyStrategyExecution
|
||||
strategy={strategy}
|
||||
displayGroupId={displayGroupId}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
|
@ -1,131 +0,0 @@
|
||||
import FiberManualRecord from '@mui/icons-material/FiberManualRecord';
|
||||
import { useTheme } from '@mui/system';
|
||||
import type { IFeatureEnvironmentMetrics } from 'interfaces/featureToggle';
|
||||
import { calculatePercentage } from 'utils/calculatePercentage';
|
||||
import PercentageCircle from 'component/common/PercentageCircle/PercentageCircle';
|
||||
import { PrettifyLargeNumber } from 'component/common/PrettifyLargeNumber/PrettifyLargeNumber';
|
||||
import { styled } from '@mui/material';
|
||||
|
||||
interface IFeatureOverviewEnvironmentMetrics {
|
||||
environmentMetric?: IFeatureEnvironmentMetrics;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
const StyledContainer = styled('div')({
|
||||
marginLeft: 'auto',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
});
|
||||
|
||||
const StyledInfo = styled('div')(({ theme }) => ({
|
||||
marginRight: theme.spacing(1),
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
}));
|
||||
|
||||
const StyledPercentage = styled('p')(({ theme }) => ({
|
||||
color: theme.palette.primary.main,
|
||||
textAlign: 'right',
|
||||
fontSize: theme.fontSizes.bodySize,
|
||||
}));
|
||||
|
||||
const StyledInfoParagraph = styled('p')(({ theme }) => ({
|
||||
maxWidth: '270px',
|
||||
marginTop: theme.spacing(0.5),
|
||||
fontSize: theme.fontSizes.smallBody,
|
||||
textAlign: 'right',
|
||||
[theme.breakpoints.down(700)]: {
|
||||
display: 'none',
|
||||
},
|
||||
}));
|
||||
|
||||
const StyledIcon = styled(FiberManualRecord)(({ theme }) => ({
|
||||
fill: theme.palette.background.elevation2,
|
||||
height: '75px',
|
||||
width: '75px',
|
||||
[theme.breakpoints.down(500)]: {
|
||||
display: 'none',
|
||||
},
|
||||
}));
|
||||
|
||||
const StyledPercentageCircle = styled('div')(({ theme }) => ({
|
||||
margin: theme.spacing(0, 2),
|
||||
[theme.breakpoints.down(500)]: {
|
||||
display: 'none',
|
||||
},
|
||||
}));
|
||||
|
||||
/**
|
||||
* @deprecated remove with `flagOverviewRedesign` flag
|
||||
*/
|
||||
const FeatureOverviewEnvironmentMetrics = ({
|
||||
environmentMetric,
|
||||
disabled = false,
|
||||
}: IFeatureOverviewEnvironmentMetrics) => {
|
||||
const theme = useTheme();
|
||||
|
||||
if (!environmentMetric) return null;
|
||||
|
||||
const total = environmentMetric.yes + environmentMetric.no;
|
||||
const percentage = calculatePercentage(total, environmentMetric?.yes);
|
||||
|
||||
if (
|
||||
!environmentMetric ||
|
||||
(environmentMetric.yes === 0 && environmentMetric.no === 0)
|
||||
) {
|
||||
return (
|
||||
<StyledContainer>
|
||||
<StyledInfo>
|
||||
<StyledPercentage
|
||||
style={{
|
||||
color: disabled
|
||||
? theme.palette.text.secondary
|
||||
: undefined,
|
||||
}}
|
||||
data-loading
|
||||
>
|
||||
{percentage}%
|
||||
</StyledPercentage>
|
||||
<StyledInfoParagraph
|
||||
style={{
|
||||
color: disabled
|
||||
? theme.palette.text.secondary
|
||||
: theme.palette.text.primary,
|
||||
}}
|
||||
data-loading
|
||||
>
|
||||
The feature has been requested <b>0 times</b> and
|
||||
exposed
|
||||
<b> 0 times</b> in the last hour
|
||||
</StyledInfoParagraph>
|
||||
</StyledInfo>
|
||||
<StyledIcon style={{ transform: 'scale(1.1)' }} data-loading />
|
||||
</StyledContainer>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<StyledContainer>
|
||||
<StyledInfo>
|
||||
<StyledPercentage>{percentage}%</StyledPercentage>
|
||||
<StyledInfoParagraph>
|
||||
The feature has been requested{' '}
|
||||
<b>
|
||||
<PrettifyLargeNumber value={total} /> times
|
||||
</b>{' '}
|
||||
and exposed{' '}
|
||||
<b>
|
||||
<PrettifyLargeNumber value={environmentMetric.yes} />{' '}
|
||||
times
|
||||
</b>{' '}
|
||||
in the last hour
|
||||
</StyledInfoParagraph>
|
||||
</StyledInfo>
|
||||
<StyledPercentageCircle data-loading>
|
||||
<PercentageCircle percentage={percentage} size='3rem' />
|
||||
</StyledPercentageCircle>
|
||||
</StyledContainer>
|
||||
);
|
||||
};
|
||||
|
||||
export default FeatureOverviewEnvironmentMetrics;
|
@ -1,252 +0,0 @@
|
||||
import {
|
||||
Accordion,
|
||||
AccordionDetails,
|
||||
AccordionSummary,
|
||||
Box,
|
||||
styled,
|
||||
} from '@mui/material';
|
||||
import ExpandMore from '@mui/icons-material/ExpandMore';
|
||||
import { useFeature } from 'hooks/api/getters/useFeature/useFeature';
|
||||
import useFeatureMetrics from 'hooks/api/getters/useFeatureMetrics/useFeatureMetrics';
|
||||
import type { IFeatureEnvironment } from 'interfaces/featureToggle';
|
||||
import { getFeatureMetrics } from 'utils/getFeatureMetrics';
|
||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||
import EnvironmentIcon from 'component/common/EnvironmentIcon/EnvironmentIcon';
|
||||
import StringTruncator from 'component/common/StringTruncator/StringTruncator';
|
||||
import EnvironmentAccordionBody from './EnvironmentAccordionBody/LegacyEnvironmentAccordionBody';
|
||||
import { EnvironmentFooter } from './EnvironmentFooter/EnvironmentFooter';
|
||||
import FeatureOverviewEnvironmentMetrics from './EnvironmentHeader/FeatureOverviewEnvironmentMetrics/LegacyFeatureOverviewEnvironmentMetrics';
|
||||
import { FeatureStrategyMenuWrapper } from 'component/feature/FeatureStrategy/FeatureStrategyMenu/FeatureStrategyMenu';
|
||||
import { FEATURE_ENVIRONMENT_ACCORDION } from 'utils/testIds';
|
||||
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
|
||||
import { FeatureStrategyIcons } from 'component/feature/FeatureStrategy/FeatureStrategyIcons/FeatureStrategyIcons';
|
||||
import { useGlobalLocalStorage } from 'hooks/useGlobalLocalStorage';
|
||||
import { Badge } from 'component/common/Badge/Badge';
|
||||
import { UpgradeChangeRequests } from './UpgradeChangeRequests/UpgradeChangeRequests';
|
||||
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
|
||||
|
||||
interface IFeatureOverviewEnvironmentProps {
|
||||
env: IFeatureEnvironment;
|
||||
}
|
||||
|
||||
const StyledFeatureOverviewEnvironment = styled('div', {
|
||||
shouldForwardProp: (prop) => prop !== 'enabled',
|
||||
})<{ enabled: boolean }>(({ theme, enabled }) => ({
|
||||
borderRadius: theme.shape.borderRadiusLarge,
|
||||
marginBottom: theme.spacing(2),
|
||||
backgroundColor: enabled
|
||||
? theme.palette.background.paper
|
||||
: theme.palette.envAccordion.disabled,
|
||||
}));
|
||||
|
||||
const StyledAccordion = styled(Accordion)({
|
||||
boxShadow: 'none',
|
||||
background: 'none',
|
||||
});
|
||||
|
||||
const StyledAccordionSummary = styled(AccordionSummary)(({ theme }) => ({
|
||||
boxShadow: 'none',
|
||||
padding: theme.spacing(2, 4),
|
||||
[theme.breakpoints.down(400)]: {
|
||||
padding: theme.spacing(1, 2),
|
||||
},
|
||||
}));
|
||||
|
||||
const StyledAccordionDetails = styled(AccordionDetails, {
|
||||
shouldForwardProp: (prop) => prop !== 'enabled',
|
||||
})<{ enabled: boolean }>(({ theme }) => ({
|
||||
padding: theme.spacing(3),
|
||||
background: theme.palette.envAccordion.expanded,
|
||||
borderBottomLeftRadius: theme.shape.borderRadiusLarge,
|
||||
borderBottomRightRadius: theme.shape.borderRadiusLarge,
|
||||
boxShadow: theme.boxShadows.accordionFooter,
|
||||
|
||||
[theme.breakpoints.down('md')]: {
|
||||
padding: theme.spacing(2, 1),
|
||||
},
|
||||
}));
|
||||
|
||||
const StyledEnvironmentAccordionBody = styled(EnvironmentAccordionBody)(
|
||||
({ theme }) => ({
|
||||
width: '100%',
|
||||
position: 'relative',
|
||||
paddingBottom: theme.spacing(2),
|
||||
}),
|
||||
);
|
||||
|
||||
const StyledHeader = styled('div', {
|
||||
shouldForwardProp: (prop) => prop !== 'enabled',
|
||||
})<{ enabled: boolean }>(({ theme, enabled }) => ({
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
flexDirection: 'column',
|
||||
color: enabled ? theme.palette.text.primary : theme.palette.text.secondary,
|
||||
}));
|
||||
|
||||
const StyledHeaderTitle = styled('div')(({ theme }) => ({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
[theme.breakpoints.down(560)]: {
|
||||
flexDirection: 'column',
|
||||
textAlign: 'center',
|
||||
},
|
||||
}));
|
||||
|
||||
const StyledEnvironmentIcon = styled(EnvironmentIcon)(({ theme }) => ({
|
||||
[theme.breakpoints.down(560)]: {
|
||||
marginBottom: '0.5rem',
|
||||
},
|
||||
}));
|
||||
|
||||
const StyledStringTruncator = styled(StringTruncator)(({ theme }) => ({
|
||||
fontSize: theme.fontSizes.bodySize,
|
||||
fontWeight: theme.typography.fontWeightMedium,
|
||||
[theme.breakpoints.down(560)]: {
|
||||
textAlign: 'center',
|
||||
},
|
||||
}));
|
||||
|
||||
const StyledButtonContainer = styled('div')(({ theme }) => ({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
marginTop: theme.spacing(2),
|
||||
gap: theme.spacing(2),
|
||||
flexWrap: 'wrap',
|
||||
[theme.breakpoints.down(560)]: {
|
||||
flexDirection: 'column',
|
||||
},
|
||||
}));
|
||||
|
||||
/**
|
||||
* @deprecated remove this file with `flagOverviewRedesign`
|
||||
*/
|
||||
const FeatureOverviewEnvironment = ({
|
||||
env,
|
||||
}: IFeatureOverviewEnvironmentProps) => {
|
||||
const projectId = useRequiredPathParam('projectId');
|
||||
const featureId = useRequiredPathParam('featureId');
|
||||
const { metrics } = useFeatureMetrics(projectId, featureId);
|
||||
const { feature } = useFeature(projectId, featureId);
|
||||
const { value: globalStore } = useGlobalLocalStorage();
|
||||
|
||||
const featureMetrics = getFeatureMetrics(feature?.environments, metrics);
|
||||
const environmentMetric = featureMetrics.find(
|
||||
(featureMetric) => featureMetric.environment === env.name,
|
||||
);
|
||||
const featureEnvironment = feature?.environments.find(
|
||||
(featureEnvironment) => featureEnvironment.name === env.name,
|
||||
);
|
||||
const { isOss } = useUiConfig();
|
||||
const showChangeRequestUpgrade = env.type === 'production' && isOss();
|
||||
|
||||
return (
|
||||
<ConditionallyRender
|
||||
condition={!new Set(globalStore.hiddenEnvironments).has(env.name)}
|
||||
show={
|
||||
<StyledFeatureOverviewEnvironment enabled={env.enabled}>
|
||||
<StyledAccordion
|
||||
TransitionProps={{ mountOnEnter: true }}
|
||||
data-testid={`${FEATURE_ENVIRONMENT_ACCORDION}_${env.name}`}
|
||||
className={`environment-accordion ${
|
||||
env.enabled ? '' : 'accordion-disabled'
|
||||
}`}
|
||||
>
|
||||
<StyledAccordionSummary
|
||||
expandIcon={<ExpandMore titleAccess='Toggle' />}
|
||||
>
|
||||
<StyledHeader data-loading enabled={env.enabled}>
|
||||
<StyledHeaderTitle>
|
||||
<StyledEnvironmentIcon
|
||||
enabled={env.enabled}
|
||||
/>
|
||||
<div>
|
||||
<StyledStringTruncator
|
||||
text={env.name}
|
||||
maxWidth='100'
|
||||
maxLength={15}
|
||||
/>
|
||||
</div>
|
||||
<ConditionallyRender
|
||||
condition={!env.enabled}
|
||||
show={
|
||||
<Badge
|
||||
color='neutral'
|
||||
sx={{ ml: 1 }}
|
||||
>
|
||||
Disabled
|
||||
</Badge>
|
||||
}
|
||||
/>
|
||||
</StyledHeaderTitle>
|
||||
<StyledButtonContainer>
|
||||
<FeatureStrategyMenuWrapper
|
||||
label='Add strategy'
|
||||
projectId={projectId}
|
||||
featureId={featureId}
|
||||
environmentId={env.name}
|
||||
variant='outlined'
|
||||
size='small'
|
||||
/>
|
||||
<FeatureStrategyIcons
|
||||
strategies={
|
||||
featureEnvironment?.strategies
|
||||
}
|
||||
/>
|
||||
</StyledButtonContainer>
|
||||
</StyledHeader>
|
||||
|
||||
<FeatureOverviewEnvironmentMetrics
|
||||
environmentMetric={environmentMetric}
|
||||
disabled={!env.enabled}
|
||||
/>
|
||||
</StyledAccordionSummary>
|
||||
|
||||
<StyledAccordionDetails enabled={env.enabled}>
|
||||
<StyledEnvironmentAccordionBody
|
||||
featureEnvironment={featureEnvironment}
|
||||
isDisabled={!env.enabled}
|
||||
otherEnvironments={feature?.environments
|
||||
.map(({ name }) => name)
|
||||
.filter((name) => name !== env.name)}
|
||||
/>
|
||||
<ConditionallyRender
|
||||
condition={
|
||||
(featureEnvironment?.strategies?.length ||
|
||||
0) > 0
|
||||
}
|
||||
show={
|
||||
<>
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
py: 1,
|
||||
}}
|
||||
>
|
||||
<FeatureStrategyMenuWrapper
|
||||
label='Add strategy'
|
||||
projectId={projectId}
|
||||
featureId={featureId}
|
||||
environmentId={env.name}
|
||||
/>
|
||||
</Box>
|
||||
<EnvironmentFooter
|
||||
environmentMetric={
|
||||
environmentMetric
|
||||
}
|
||||
/>
|
||||
{showChangeRequestUpgrade ? (
|
||||
<UpgradeChangeRequests />
|
||||
) : null}
|
||||
</>
|
||||
}
|
||||
/>
|
||||
</StyledAccordionDetails>
|
||||
</StyledAccordion>
|
||||
</StyledFeatureOverviewEnvironment>
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default FeatureOverviewEnvironment;
|
@ -1,8 +1,6 @@
|
||||
import type { ComponentProps, FC } from 'react';
|
||||
import { useUiFlag } from 'hooks/useUiFlag';
|
||||
import { useFeature } from 'hooks/api/getters/useFeature/useFeature';
|
||||
import { FeatureOverviewEnvironment } from './FeatureOverviewEnvironment/FeatureOverviewEnvironment';
|
||||
import LegacyFeatureOverviewEnvironment from './FeatureOverviewEnvironment/LegacyFeatureOverviewEnvironment';
|
||||
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
|
||||
import useFeatureMetrics from 'hooks/api/getters/useFeatureMetrics/useFeatureMetrics';
|
||||
import { getFeatureMetrics } from 'utils/getFeatureMetrics';
|
||||
@ -40,23 +38,9 @@ export const FeatureOverviewEnvironments: FC<
|
||||
const { feature } = useFeature(projectId, featureId);
|
||||
const { metrics } = useFeatureMetrics(projectId, featureId);
|
||||
const featureMetrics = getFeatureMetrics(feature?.environments, metrics);
|
||||
const flagOverviewRedesign = useUiFlag('flagOverviewRedesign');
|
||||
|
||||
if (!feature) return null;
|
||||
|
||||
if (!flagOverviewRedesign) {
|
||||
return (
|
||||
<>
|
||||
{feature.environments?.map((env) => (
|
||||
<LegacyFeatureOverviewEnvironment
|
||||
env={env}
|
||||
key={env.name}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return feature.environments
|
||||
?.filter((env) => !hiddenEnvironments.includes(env.name))
|
||||
.map((env) => (
|
||||
|
@ -1,104 +0,0 @@
|
||||
import type React from 'react';
|
||||
import { type FC, useState } from 'react';
|
||||
import {
|
||||
IconButton,
|
||||
ListItemIcon,
|
||||
ListItemText,
|
||||
MenuItem,
|
||||
MenuList,
|
||||
Popover,
|
||||
styled,
|
||||
Tooltip,
|
||||
Typography,
|
||||
Box,
|
||||
} from '@mui/material';
|
||||
import Delete from '@mui/icons-material/Delete';
|
||||
import Edit from '@mui/icons-material/Edit';
|
||||
import MoreVert from '@mui/icons-material/MoreVert';
|
||||
|
||||
const StyledPopover = styled(Popover)(({ theme }) => ({
|
||||
borderRadius: theme.shape.borderRadiusLarge,
|
||||
padding: theme.spacing(1, 1.5),
|
||||
}));
|
||||
|
||||
export const OldDependencyActions: FC<{
|
||||
feature: string;
|
||||
onEdit: () => void;
|
||||
onDelete: () => void;
|
||||
}> = ({ feature, onEdit, onDelete }) => {
|
||||
const id = `dependency-${feature}-actions`;
|
||||
const menuId = `${id}-menu`;
|
||||
|
||||
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
|
||||
|
||||
const open = Boolean(anchorEl);
|
||||
const openActions = (event: React.MouseEvent<HTMLButtonElement>) => {
|
||||
setAnchorEl(event.currentTarget);
|
||||
};
|
||||
const closeActions = () => {
|
||||
setAnchorEl(null);
|
||||
};
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Tooltip title='Dependency actions' arrow describeChild>
|
||||
<IconButton
|
||||
sx={{ mr: 0.25 }}
|
||||
id={id}
|
||||
aria-controls={open ? menuId : undefined}
|
||||
aria-haspopup='true'
|
||||
aria-expanded={open ? 'true' : undefined}
|
||||
onClick={openActions}
|
||||
type='button'
|
||||
>
|
||||
<MoreVert />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<StyledPopover
|
||||
id={menuId}
|
||||
anchorEl={anchorEl}
|
||||
open={open}
|
||||
onClose={closeActions}
|
||||
transformOrigin={{
|
||||
horizontal: 'right',
|
||||
vertical: 'top',
|
||||
}}
|
||||
anchorOrigin={{
|
||||
horizontal: 'right',
|
||||
vertical: 'bottom',
|
||||
}}
|
||||
disableScrollLock={true}
|
||||
>
|
||||
<MenuList aria-labelledby={id}>
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
onEdit();
|
||||
closeActions();
|
||||
}}
|
||||
>
|
||||
<ListItemIcon>
|
||||
<Edit />
|
||||
</ListItemIcon>
|
||||
<ListItemText>
|
||||
<Typography variant='body2'>Edit</Typography>
|
||||
</ListItemText>
|
||||
</MenuItem>
|
||||
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
onDelete();
|
||||
closeActions();
|
||||
}}
|
||||
>
|
||||
<ListItemIcon>
|
||||
<Delete />
|
||||
</ListItemIcon>
|
||||
<ListItemText>
|
||||
<Typography variant='body2'>Delete</Typography>
|
||||
</ListItemText>
|
||||
</MenuItem>
|
||||
</MenuList>
|
||||
</StyledPopover>
|
||||
</Box>
|
||||
);
|
||||
};
|
@ -1,218 +0,0 @@
|
||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||
import { AddDependencyDialogue } from 'component/feature/Dependencies/AddDependencyDialogue';
|
||||
import type { IFeatureToggle } from 'interfaces/featureToggle';
|
||||
import { type FC, useState } from 'react';
|
||||
import {
|
||||
FlexRow,
|
||||
StyledDetail,
|
||||
StyledLabel,
|
||||
StyledLink,
|
||||
} from '../FeatureOverviewSidePanel/FeatureOverviewSidePanelDetails/StyledRow';
|
||||
import { OldDependencyActions } from './OldDependencyActions';
|
||||
import { useDependentFeaturesApi } from 'hooks/api/actions/useDependentFeaturesApi/useDependentFeaturesApi';
|
||||
import { useFeature } from 'hooks/api/getters/useFeature/useFeature';
|
||||
import { ChildrenTooltip } from './ChildrenTooltip';
|
||||
import PermissionButton from 'component/common/PermissionButton/PermissionButton';
|
||||
import { UPDATE_FEATURE_DEPENDENCY } from 'component/providers/AccessProvider/permissions';
|
||||
import { useCheckProjectAccess } from 'hooks/useHasAccess';
|
||||
import { useChangeRequestApi } from 'hooks/api/actions/useChangeRequestApi/useChangeRequestApi';
|
||||
import { usePendingChangeRequests } from 'hooks/api/getters/usePendingChangeRequests/usePendingChangeRequests';
|
||||
import useToast from 'hooks/useToast';
|
||||
import { useHighestPermissionChangeRequestEnvironment } from 'hooks/useHighestPermissionChangeRequestEnvironment';
|
||||
import { useChangeRequestsEnabled } from 'hooks/useChangeRequestsEnabled';
|
||||
import { formatUnknownError } from 'utils/formatUnknownError';
|
||||
import { usePlausibleTracker } from 'hooks/usePlausibleTracker';
|
||||
import { VariantsTooltip } from './VariantsTooltip';
|
||||
|
||||
const useDeleteDependency = (project: string, featureId: string) => {
|
||||
const { trackEvent } = usePlausibleTracker();
|
||||
const { addChange } = useChangeRequestApi();
|
||||
const { refetch: refetchChangeRequests } =
|
||||
usePendingChangeRequests(project);
|
||||
const { setToastData, setToastApiError } = useToast();
|
||||
const { refetchFeature } = useFeature(project, featureId);
|
||||
const environment = useHighestPermissionChangeRequestEnvironment(project)();
|
||||
const { isChangeRequestConfiguredInAnyEnv } =
|
||||
useChangeRequestsEnabled(project);
|
||||
const { removeDependencies } = useDependentFeaturesApi(project);
|
||||
|
||||
const handleAddChange = async () => {
|
||||
if (!environment) {
|
||||
console.error('No change request environment');
|
||||
return;
|
||||
}
|
||||
await addChange(project, environment, [
|
||||
{
|
||||
action: 'deleteDependency',
|
||||
feature: featureId,
|
||||
payload: undefined,
|
||||
},
|
||||
]);
|
||||
};
|
||||
|
||||
const deleteDependency = async () => {
|
||||
try {
|
||||
if (isChangeRequestConfiguredInAnyEnv()) {
|
||||
await handleAddChange();
|
||||
trackEvent('dependent_features', {
|
||||
props: {
|
||||
eventType: 'delete dependency added to change request',
|
||||
},
|
||||
});
|
||||
setToastData({
|
||||
type: 'success',
|
||||
text: 'Change added to draft',
|
||||
});
|
||||
await refetchChangeRequests();
|
||||
} else {
|
||||
await removeDependencies(featureId);
|
||||
trackEvent('dependent_features', {
|
||||
props: {
|
||||
eventType: 'dependency removed',
|
||||
},
|
||||
});
|
||||
setToastData({ text: 'Dependency removed', type: 'success' });
|
||||
await refetchFeature();
|
||||
}
|
||||
} catch (error) {
|
||||
setToastApiError(formatUnknownError(error));
|
||||
}
|
||||
};
|
||||
|
||||
return deleteDependency;
|
||||
};
|
||||
|
||||
export const OldDependencyRow: FC<{ feature: IFeatureToggle }> = ({
|
||||
feature,
|
||||
}) => {
|
||||
const [showDependencyDialogue, setShowDependencyDialogue] = useState(false);
|
||||
const canAddParentDependency =
|
||||
Boolean(feature.project) &&
|
||||
feature.dependencies.length === 0 &&
|
||||
feature.children.length === 0;
|
||||
const hasParentDependency =
|
||||
Boolean(feature.project) && Boolean(feature.dependencies.length > 0);
|
||||
const hasChildren = Boolean(feature.project) && feature.children.length > 0;
|
||||
const environment = useHighestPermissionChangeRequestEnvironment(
|
||||
feature.project,
|
||||
)();
|
||||
const checkAccess = useCheckProjectAccess(feature.project);
|
||||
const deleteDependency = useDeleteDependency(feature.project, feature.name);
|
||||
|
||||
return (
|
||||
<>
|
||||
<ConditionallyRender
|
||||
condition={canAddParentDependency}
|
||||
show={
|
||||
<FlexRow>
|
||||
<StyledDetail>
|
||||
<StyledLabel>Dependency:</StyledLabel>
|
||||
<PermissionButton
|
||||
size='small'
|
||||
permission={UPDATE_FEATURE_DEPENDENCY}
|
||||
projectId={feature.project}
|
||||
variant='text'
|
||||
onClick={() => {
|
||||
setShowDependencyDialogue(true);
|
||||
}}
|
||||
sx={(theme) => ({
|
||||
marginBottom: theme.spacing(0.4),
|
||||
})}
|
||||
>
|
||||
Add parent flag
|
||||
</PermissionButton>
|
||||
</StyledDetail>
|
||||
</FlexRow>
|
||||
}
|
||||
/>
|
||||
<ConditionallyRender
|
||||
condition={hasParentDependency}
|
||||
show={
|
||||
<FlexRow>
|
||||
<StyledDetail>
|
||||
<StyledLabel>Dependency:</StyledLabel>
|
||||
<StyledLink
|
||||
to={`/projects/${feature.project}/features/${feature.dependencies[0]?.feature}`}
|
||||
>
|
||||
{feature.dependencies[0]?.feature}
|
||||
</StyledLink>
|
||||
</StyledDetail>
|
||||
<ConditionallyRender
|
||||
condition={checkAccess(
|
||||
UPDATE_FEATURE_DEPENDENCY,
|
||||
environment,
|
||||
)}
|
||||
show={
|
||||
<OldDependencyActions
|
||||
feature={feature.name}
|
||||
onEdit={() =>
|
||||
setShowDependencyDialogue(true)
|
||||
}
|
||||
onDelete={deleteDependency}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</FlexRow>
|
||||
}
|
||||
/>
|
||||
<ConditionallyRender
|
||||
condition={
|
||||
hasParentDependency && !feature.dependencies[0]?.enabled
|
||||
}
|
||||
show={
|
||||
<FlexRow>
|
||||
<StyledDetail>
|
||||
<StyledLabel>Dependency value:</StyledLabel>
|
||||
<span>disabled</span>
|
||||
</StyledDetail>
|
||||
</FlexRow>
|
||||
}
|
||||
/>
|
||||
<ConditionallyRender
|
||||
condition={
|
||||
hasParentDependency &&
|
||||
Boolean(feature.dependencies[0]?.variants?.length)
|
||||
}
|
||||
show={
|
||||
<FlexRow>
|
||||
<StyledDetail>
|
||||
<StyledLabel>Dependency value:</StyledLabel>
|
||||
<VariantsTooltip
|
||||
variants={
|
||||
feature.dependencies[0]?.variants || []
|
||||
}
|
||||
/>
|
||||
</StyledDetail>
|
||||
</FlexRow>
|
||||
}
|
||||
/>
|
||||
<ConditionallyRender
|
||||
condition={hasChildren}
|
||||
show={
|
||||
<FlexRow>
|
||||
<StyledDetail>
|
||||
<StyledLabel>Children:</StyledLabel>
|
||||
<ChildrenTooltip
|
||||
childFeatures={feature.children}
|
||||
project={feature.project}
|
||||
/>
|
||||
</StyledDetail>
|
||||
</FlexRow>
|
||||
}
|
||||
/>
|
||||
|
||||
<ConditionallyRender
|
||||
condition={Boolean(feature.project)}
|
||||
show={
|
||||
<AddDependencyDialogue
|
||||
project={feature.project}
|
||||
featureId={feature.name}
|
||||
parentDependency={feature.dependencies[0]}
|
||||
onClose={() => setShowDependencyDialogue(false)}
|
||||
showDependencyDialogue={showDependencyDialogue}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
@ -1,281 +0,0 @@
|
||||
import { Box, capitalize, styled } from '@mui/material';
|
||||
import { Link, useNavigate } from 'react-router-dom';
|
||||
import { useFeature } from 'hooks/api/getters/useFeature/useFeature';
|
||||
import { getFeatureTypeIcons } from 'utils/getFeatureTypeIcons';
|
||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||
import Edit from '@mui/icons-material/Edit';
|
||||
import PermissionIconButton from 'component/common/PermissionIconButton/PermissionIconButton';
|
||||
import { UPDATE_FEATURE } from 'component/providers/AccessProvider/permissions';
|
||||
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
|
||||
import { FeatureArchiveDialog } from 'component/common/FeatureArchiveDialog/FeatureArchiveDialog';
|
||||
import { useState } from 'react';
|
||||
import { FeatureArchiveNotAllowedDialog } from 'component/common/FeatureArchiveDialog/FeatureArchiveNotAllowedDialog';
|
||||
import { StyledDetail } from '../FeatureOverviewSidePanel/FeatureOverviewSidePanelDetails/StyledRow';
|
||||
import { formatDateYMD } from 'utils/formatDate';
|
||||
import { parseISO } from 'date-fns';
|
||||
import { FeatureEnvironmentSeen } from '../../FeatureEnvironmentSeen/FeatureEnvironmentSeen';
|
||||
import { OldDependencyRow } from './OldDependencyRow';
|
||||
import { useLocationSettings } from 'hooks/useLocationSettings';
|
||||
import { useShowDependentFeatures } from './useShowDependentFeatures';
|
||||
import type { ILastSeenEnvironments } from 'interfaces/featureToggle';
|
||||
import { FeatureLifecycle } from '../FeatureLifecycle/FeatureLifecycle';
|
||||
import { MarkCompletedDialogue } from '../FeatureLifecycle/MarkCompletedDialogue';
|
||||
import { UserAvatar } from 'component/common/UserAvatar/UserAvatar';
|
||||
|
||||
const StyledContainer = styled('div')(({ theme }) => ({
|
||||
borderRadius: theme.shape.borderRadiusLarge,
|
||||
backgroundColor: theme.palette.background.paper,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
maxWidth: '350px',
|
||||
minWidth: '350px',
|
||||
marginRight: theme.spacing(2),
|
||||
[theme.breakpoints.down(1000)]: {
|
||||
width: '100%',
|
||||
maxWidth: 'none',
|
||||
minWidth: 'auto',
|
||||
},
|
||||
}));
|
||||
|
||||
const StyledPaddingContainerTop = styled('div')({
|
||||
padding: '1.5rem 1.5rem 0 1.5rem',
|
||||
});
|
||||
|
||||
const StyledMetaDataHeader = styled('div')({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
});
|
||||
|
||||
const StyledHeader = styled('h2')(({ theme }) => ({
|
||||
fontSize: theme.fontSizes.mainHeader,
|
||||
fontWeight: 'normal',
|
||||
margin: 0,
|
||||
}));
|
||||
|
||||
const StyledBody = styled('div')(({ theme }) => ({
|
||||
margin: theme.spacing(2, 0),
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
fontSize: theme.fontSizes.smallBody,
|
||||
}));
|
||||
|
||||
const BodyItemWithIcon = styled('div')(({ theme }) => ({}));
|
||||
|
||||
const SpacedBodyItem = styled('div')(({ theme }) => ({
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
padding: theme.spacing(1, 0),
|
||||
}));
|
||||
|
||||
const StyledDescriptionContainer = styled('div')(({ theme }) => ({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
}));
|
||||
|
||||
const StyledDetailsContainer = styled('div')(({ theme }) => ({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
}));
|
||||
|
||||
const StyledDescription = styled('p')({
|
||||
wordBreak: 'break-word',
|
||||
});
|
||||
|
||||
const StyledUserAvatar = styled(UserAvatar)(({ theme }) => ({
|
||||
margin: theme.spacing(1),
|
||||
}));
|
||||
|
||||
export const StyledLabel = styled('span')(({ theme }) => ({
|
||||
color: theme.palette.text.secondary,
|
||||
marginRight: theme.spacing(1),
|
||||
}));
|
||||
|
||||
const OldFeatureOverviewMetaData = () => {
|
||||
const projectId = useRequiredPathParam('projectId');
|
||||
const featureId = useRequiredPathParam('featureId');
|
||||
const { feature, refetchFeature } = useFeature(projectId, featureId);
|
||||
const { project, description, type } = feature;
|
||||
const navigate = useNavigate();
|
||||
const [showDelDialog, setShowDelDialog] = useState(false);
|
||||
const [showMarkCompletedDialogue, setShowMarkCompletedDialogue] =
|
||||
useState(false);
|
||||
|
||||
const { locationSettings } = useLocationSettings();
|
||||
const showDependentFeatures = useShowDependentFeatures(feature.project);
|
||||
|
||||
const lastSeenEnvironments: ILastSeenEnvironments[] =
|
||||
feature.environments?.map((env) => ({
|
||||
name: env.name,
|
||||
lastSeenAt: env.lastSeenAt,
|
||||
enabled: env.enabled,
|
||||
yes: env.yes,
|
||||
no: env.no,
|
||||
}));
|
||||
|
||||
const IconComponent = getFeatureTypeIcons(type);
|
||||
|
||||
return (
|
||||
<StyledContainer>
|
||||
<StyledPaddingContainerTop>
|
||||
<StyledMetaDataHeader data-loading>
|
||||
<IconComponent
|
||||
sx={(theme) => ({
|
||||
marginRight: theme.spacing(2),
|
||||
height: '40px',
|
||||
width: '40px',
|
||||
padding: theme.spacing(0.5),
|
||||
backgroundColor:
|
||||
theme.palette.background.alternative,
|
||||
fill: theme.palette.primary.contrastText,
|
||||
borderRadius: `${theme.shape.borderRadiusMedium}px`,
|
||||
})}
|
||||
/>{' '}
|
||||
<StyledHeader>{capitalize(type || '')} toggle</StyledHeader>
|
||||
</StyledMetaDataHeader>
|
||||
<StyledBody>
|
||||
<SpacedBodyItem data-loading>
|
||||
<StyledLabel>Project:</StyledLabel>
|
||||
<Box sx={{ wordBreak: 'break-all' }}>{project}</Box>
|
||||
</SpacedBodyItem>
|
||||
<ConditionallyRender
|
||||
condition={Boolean(feature.lifecycle)}
|
||||
show={
|
||||
<SpacedBodyItem data-loading>
|
||||
<StyledLabel>Lifecycle:</StyledLabel>
|
||||
<FeatureLifecycle
|
||||
feature={feature}
|
||||
onArchive={() => setShowDelDialog(true)}
|
||||
onComplete={() =>
|
||||
setShowMarkCompletedDialogue(true)
|
||||
}
|
||||
onUncomplete={refetchFeature}
|
||||
/>
|
||||
</SpacedBodyItem>
|
||||
}
|
||||
/>
|
||||
|
||||
<ConditionallyRender
|
||||
condition={Boolean(description)}
|
||||
show={
|
||||
<BodyItemWithIcon data-loading sx={{ pt: 1 }}>
|
||||
<StyledLabel>Description:</StyledLabel>
|
||||
<StyledDescriptionContainer>
|
||||
<StyledDescription>
|
||||
{description}
|
||||
</StyledDescription>
|
||||
<PermissionIconButton
|
||||
size='medium'
|
||||
projectId={projectId}
|
||||
permission={UPDATE_FEATURE}
|
||||
component={Link}
|
||||
to={`/projects/${projectId}/features/${featureId}/settings`}
|
||||
tooltipProps={{
|
||||
title: 'Edit description',
|
||||
}}
|
||||
>
|
||||
<Edit />
|
||||
</PermissionIconButton>
|
||||
</StyledDescriptionContainer>
|
||||
</BodyItemWithIcon>
|
||||
}
|
||||
elseShow={
|
||||
<div data-loading>
|
||||
<StyledDescriptionContainer>
|
||||
No description.{' '}
|
||||
<PermissionIconButton
|
||||
size='medium'
|
||||
projectId={projectId}
|
||||
permission={UPDATE_FEATURE}
|
||||
component={Link}
|
||||
to={`/projects/${projectId}/features/${featureId}/settings`}
|
||||
tooltipProps={{
|
||||
title: 'Edit description',
|
||||
}}
|
||||
>
|
||||
<Edit />
|
||||
</PermissionIconButton>
|
||||
</StyledDescriptionContainer>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
<BodyItemWithIcon>
|
||||
<StyledDetailsContainer>
|
||||
<StyledDetail>
|
||||
<StyledLabel>Created at:</StyledLabel>
|
||||
<span>
|
||||
{formatDateYMD(
|
||||
parseISO(feature.createdAt),
|
||||
locationSettings.locale,
|
||||
)}
|
||||
</span>
|
||||
</StyledDetail>
|
||||
|
||||
<FeatureEnvironmentSeen
|
||||
featureLastSeen={feature.lastSeenAt}
|
||||
environments={lastSeenEnvironments}
|
||||
/>
|
||||
</StyledDetailsContainer>
|
||||
</BodyItemWithIcon>
|
||||
<ConditionallyRender
|
||||
condition={Boolean(feature.createdBy)}
|
||||
show={() => (
|
||||
<BodyItemWithIcon>
|
||||
<StyledDetailsContainer>
|
||||
<StyledDetail>
|
||||
<StyledLabel>Created by:</StyledLabel>
|
||||
<span>{feature.createdBy?.name}</span>
|
||||
</StyledDetail>
|
||||
<StyledUserAvatar
|
||||
user={feature.createdBy}
|
||||
/>
|
||||
</StyledDetailsContainer>
|
||||
</BodyItemWithIcon>
|
||||
)}
|
||||
/>
|
||||
<ConditionallyRender
|
||||
condition={showDependentFeatures}
|
||||
show={<OldDependencyRow feature={feature} />}
|
||||
/>
|
||||
</StyledBody>
|
||||
</StyledPaddingContainerTop>
|
||||
<ConditionallyRender
|
||||
condition={feature.children.length > 0}
|
||||
show={
|
||||
<FeatureArchiveNotAllowedDialog
|
||||
features={feature.children}
|
||||
project={projectId}
|
||||
isOpen={showDelDialog}
|
||||
onClose={() => setShowDelDialog(false)}
|
||||
/>
|
||||
}
|
||||
elseShow={
|
||||
<FeatureArchiveDialog
|
||||
isOpen={showDelDialog}
|
||||
onConfirm={() => {
|
||||
navigate(`/projects/${projectId}`);
|
||||
}}
|
||||
onClose={() => setShowDelDialog(false)}
|
||||
projectId={projectId}
|
||||
featureIds={[featureId]}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<ConditionallyRender
|
||||
condition={Boolean(feature.project)}
|
||||
show={
|
||||
<MarkCompletedDialogue
|
||||
isOpen={showMarkCompletedDialogue}
|
||||
setIsOpen={setShowMarkCompletedDialogue}
|
||||
projectId={feature.project}
|
||||
featureId={feature.name}
|
||||
onComplete={refetchFeature}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</StyledContainer>
|
||||
);
|
||||
};
|
||||
|
||||
export default OldFeatureOverviewMetaData;
|
@ -1,84 +0,0 @@
|
||||
import { Box, styled } from '@mui/material';
|
||||
import { useFeature } from 'hooks/api/getters/useFeature/useFeature';
|
||||
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
|
||||
import { Sticky } from 'component/common/Sticky/Sticky';
|
||||
import {
|
||||
type ITab,
|
||||
VerticalTabs,
|
||||
} from 'component/common/VerticalTabs/VerticalTabs';
|
||||
import EnvironmentIcon from 'component/common/EnvironmentIcon/EnvironmentIcon';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
const StyledContainer = styled(Box)(({ theme }) => ({
|
||||
margin: theme.spacing(2),
|
||||
marginLeft: 0,
|
||||
padding: theme.spacing(3),
|
||||
borderRadius: theme.shape.borderRadiusLarge,
|
||||
backgroundColor: theme.palette.background.paper,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: theme.spacing(2),
|
||||
width: '350px',
|
||||
[theme.breakpoints.down('md')]: {
|
||||
width: '100%',
|
||||
},
|
||||
}));
|
||||
|
||||
const StyledHeader = styled('h3')(({ theme }) => ({
|
||||
display: 'flex',
|
||||
fontSize: theme.fontSizes.bodySize,
|
||||
margin: 0,
|
||||
marginBottom: theme.spacing(1),
|
||||
}));
|
||||
|
||||
const StyledVerticalTabs = styled(VerticalTabs)(({ theme }) => ({
|
||||
'&&& .selected': {
|
||||
backgroundColor: theme.palette.neutral.light,
|
||||
},
|
||||
}));
|
||||
|
||||
interface IFeatureOverviewSidePanelProps {
|
||||
environmentId: string;
|
||||
setEnvironmentId: React.Dispatch<React.SetStateAction<string>>;
|
||||
}
|
||||
|
||||
export const FeatureOverviewSidePanel = ({
|
||||
environmentId,
|
||||
setEnvironmentId,
|
||||
}: IFeatureOverviewSidePanelProps) => {
|
||||
const projectId = useRequiredPathParam('projectId');
|
||||
const featureId = useRequiredPathParam('featureId');
|
||||
const { feature } = useFeature(projectId, featureId);
|
||||
const isSticky = feature.environments?.length <= 3;
|
||||
|
||||
const tabs: ITab[] = feature.environments.map(
|
||||
({ name, enabled, strategies }) => ({
|
||||
id: name,
|
||||
label: name,
|
||||
description:
|
||||
strategies.length === 1
|
||||
? '1 strategy'
|
||||
: `${strategies.length || 'No'} strategies`,
|
||||
startIcon: <EnvironmentIcon enabled={enabled} />,
|
||||
}),
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!environmentId) {
|
||||
setEnvironmentId(tabs[0]?.id);
|
||||
}
|
||||
}, [tabs]);
|
||||
|
||||
return (
|
||||
<StyledContainer as={isSticky ? Sticky : Box}>
|
||||
<StyledHeader data-loading>
|
||||
Environments ({feature.environments.length})
|
||||
</StyledHeader>
|
||||
<StyledVerticalTabs
|
||||
tabs={tabs}
|
||||
value={environmentId}
|
||||
onChange={({ id }) => setEnvironmentId(id)}
|
||||
/>
|
||||
</StyledContainer>
|
||||
);
|
||||
};
|
@ -1,50 +0,0 @@
|
||||
import type { IFeatureEnvironment } from 'interfaces/featureToggle';
|
||||
import { IconButton, styled } from '@mui/material';
|
||||
import Visibility from '@mui/icons-material/Visibility';
|
||||
import VisibilityOff from '@mui/icons-material/VisibilityOff';
|
||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||
|
||||
const StyledVisibilityToggle = styled(IconButton, {
|
||||
shouldForwardProp: (prop) => prop !== 'visibilityOff',
|
||||
})<{ visibilityOff: boolean }>(({ theme, visibilityOff }) => ({
|
||||
marginLeft: 'auto',
|
||||
marginRight: theme.spacing(-1),
|
||||
color: visibilityOff
|
||||
? theme.palette.action.active
|
||||
: theme.palette.action.focus,
|
||||
'&:hover': {
|
||||
color: theme.palette.action.active,
|
||||
},
|
||||
}));
|
||||
|
||||
interface IFeatureOverviewSidePanelEnvironmentHiderProps {
|
||||
environment: IFeatureEnvironment;
|
||||
hiddenEnvironments: Set<String>;
|
||||
setHiddenEnvironments: (environment: string) => void;
|
||||
}
|
||||
|
||||
export const FeatureOverviewSidePanelEnvironmentHider = ({
|
||||
environment,
|
||||
hiddenEnvironments,
|
||||
setHiddenEnvironments,
|
||||
}: IFeatureOverviewSidePanelEnvironmentHiderProps) => {
|
||||
const toggleHiddenEnvironments = () => {
|
||||
setHiddenEnvironments(environment.name);
|
||||
};
|
||||
|
||||
const isHidden = hiddenEnvironments.has(environment.name);
|
||||
|
||||
return (
|
||||
<StyledVisibilityToggle
|
||||
onClick={toggleHiddenEnvironments}
|
||||
visibilityOff={isHidden}
|
||||
aria-label={`${isHidden ? 'Show' : 'Hide'} environment "${environment.name}"`}
|
||||
>
|
||||
<ConditionallyRender
|
||||
condition={isHidden}
|
||||
show={<VisibilityOff />}
|
||||
elseShow={<Visibility />}
|
||||
/>
|
||||
</StyledVisibilityToggle>
|
||||
);
|
||||
};
|
@ -1,101 +0,0 @@
|
||||
import { useFeature } from 'hooks/api/getters/useFeature/useFeature';
|
||||
import type { IFeatureEnvironment } from 'interfaces/featureToggle';
|
||||
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
|
||||
import { styled } from '@mui/material';
|
||||
import StringTruncator from 'component/common/StringTruncator/StringTruncator';
|
||||
import { FeatureOverviewSidePanelEnvironmentHider } from './FeatureOverviewSidePanelEnvironmentHider';
|
||||
import { FeatureToggleSwitch } from 'component/project/Project/ProjectFeatureToggles/FeatureToggleSwitch/FeatureToggleSwitch';
|
||||
import { useFeatureToggleSwitch } from 'component/project/Project/ProjectFeatureToggles/FeatureToggleSwitch/useFeatureToggleSwitch';
|
||||
import { useChangeRequestsEnabled } from 'hooks/useChangeRequestsEnabled';
|
||||
|
||||
const StyledContainer = styled('div')(({ theme }) => ({
|
||||
marginLeft: theme.spacing(-1.5),
|
||||
'&:not(:last-of-type)': {
|
||||
marginBottom: theme.spacing(2),
|
||||
},
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
}));
|
||||
|
||||
const StyledLabel = styled('label')(() => ({
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
cursor: 'pointer',
|
||||
}));
|
||||
|
||||
interface IFeatureOverviewSidePanelEnvironmentSwitchProps {
|
||||
environment: IFeatureEnvironment;
|
||||
callback?: () => void;
|
||||
children?: React.ReactNode;
|
||||
hiddenEnvironments: Set<String>;
|
||||
setHiddenEnvironments: (environment: string) => void;
|
||||
}
|
||||
|
||||
export const FeatureOverviewSidePanelEnvironmentSwitch = ({
|
||||
environment,
|
||||
callback,
|
||||
children,
|
||||
hiddenEnvironments,
|
||||
setHiddenEnvironments,
|
||||
}: IFeatureOverviewSidePanelEnvironmentSwitchProps) => {
|
||||
const projectId = useRequiredPathParam('projectId');
|
||||
const featureId = useRequiredPathParam('featureId');
|
||||
const { feature, refetchFeature } = useFeature(projectId, featureId);
|
||||
const { isChangeRequestConfigured } = useChangeRequestsEnabled(projectId);
|
||||
|
||||
const defaultContent = (
|
||||
<>
|
||||
{' '}
|
||||
<span data-loading>
|
||||
{environment.enabled ? 'enabled' : 'disabled'} in
|
||||
</span>
|
||||
|
||||
<StringTruncator
|
||||
text={environment.name}
|
||||
maxWidth='120'
|
||||
maxLength={15}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
const { onToggle: onFeatureToggle, modals: featureToggleModals } =
|
||||
useFeatureToggleSwitch(projectId);
|
||||
|
||||
const handleToggle = (newState: boolean, onRollback: () => void) =>
|
||||
onFeatureToggle(newState, {
|
||||
projectId,
|
||||
featureId,
|
||||
environmentName: environment.name,
|
||||
environmentType: environment.type,
|
||||
hasStrategies: environment.strategies.length > 0,
|
||||
hasEnabledStrategies: environment.strategies.some(
|
||||
(strategy) => !strategy.disabled,
|
||||
),
|
||||
isChangeRequestEnabled: isChangeRequestConfigured(environment.name),
|
||||
onRollback,
|
||||
onSuccess: () => {
|
||||
if (callback) callback();
|
||||
refetchFeature();
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<StyledContainer>
|
||||
<StyledLabel>
|
||||
<FeatureToggleSwitch
|
||||
featureId={feature.name}
|
||||
projectId={projectId}
|
||||
environmentName={environment.name}
|
||||
onToggle={handleToggle}
|
||||
value={environment.enabled}
|
||||
/>
|
||||
{children ?? defaultContent}
|
||||
</StyledLabel>
|
||||
<FeatureOverviewSidePanelEnvironmentHider
|
||||
environment={environment}
|
||||
hiddenEnvironments={hiddenEnvironments}
|
||||
setHiddenEnvironments={setHiddenEnvironments}
|
||||
/>
|
||||
{featureToggleModals}
|
||||
</StyledContainer>
|
||||
);
|
||||
};
|
@ -1,117 +0,0 @@
|
||||
import type { IFeatureToggle } from 'interfaces/featureToggle';
|
||||
import { FeatureOverviewSidePanelEnvironmentSwitch } from 'component/feature/FeatureView/FeatureOverview/FeatureOverviewSidePanel/FeatureOverviewSidePanelEnvironmentSwitches/FeatureOverviewSidePanelEnvironmentSwitch/FeatureOverviewSidePanelEnvironmentSwitch';
|
||||
import { Link, styled, Tooltip } from '@mui/material';
|
||||
import { Link as RouterLink } from 'react-router-dom';
|
||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||
import VariantsWarningTooltip from 'component/feature/FeatureView/FeatureVariants/VariantsTooltipWarning';
|
||||
|
||||
const StyledContainer = styled('div')(({ theme }) => ({
|
||||
padding: theme.spacing(3),
|
||||
}));
|
||||
|
||||
const StyledSwitchLabel = styled('div')(() => ({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
}));
|
||||
|
||||
const StyledLabel = styled('p')(({ theme }) => ({
|
||||
fontSize: theme.fontSizes.bodySize,
|
||||
}));
|
||||
|
||||
const StyledSubLabel = styled('p')(({ theme }) => ({
|
||||
fontSize: theme.fontSizes.smallBody,
|
||||
color: theme.palette.text.secondary,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
}));
|
||||
|
||||
const StyledSeparator = styled('span')(({ theme }) => ({
|
||||
padding: theme.spacing(0, 0.5),
|
||||
'::after': {
|
||||
content: '"-"',
|
||||
},
|
||||
}));
|
||||
|
||||
const StyledLink = styled(Link<typeof RouterLink | 'a'>)(() => ({
|
||||
'&:hover, &:focus': {
|
||||
textDecoration: 'underline',
|
||||
},
|
||||
}));
|
||||
|
||||
interface IFeatureOverviewSidePanelEnvironmentSwitchesProps {
|
||||
feature: IFeatureToggle;
|
||||
header: React.ReactNode;
|
||||
hiddenEnvironments: Set<String>;
|
||||
setHiddenEnvironments: (environment: string) => void;
|
||||
}
|
||||
|
||||
export const FeatureOverviewSidePanelEnvironmentSwitches = ({
|
||||
feature,
|
||||
header,
|
||||
hiddenEnvironments,
|
||||
setHiddenEnvironments,
|
||||
}: IFeatureOverviewSidePanelEnvironmentSwitchesProps) => {
|
||||
const someEnabledEnvironmentHasVariants = feature.environments.some(
|
||||
(environment) => environment.enabled && environment.variants?.length,
|
||||
);
|
||||
return (
|
||||
<StyledContainer data-testid='feature-flag-status'>
|
||||
{header}
|
||||
{feature.environments.map((environment) => {
|
||||
const strategiesLabel =
|
||||
environment.strategies.length === 1
|
||||
? '1 strategy'
|
||||
: `${environment.strategies.length} strategies`;
|
||||
|
||||
const variants = environment.variants ?? [];
|
||||
|
||||
const variantsLink = variants.length > 0 && (
|
||||
<>
|
||||
<StyledSeparator />
|
||||
<Tooltip title='View variants' arrow describeChild>
|
||||
<StyledLink
|
||||
component={RouterLink}
|
||||
to={`/projects/${feature.project}/features/${feature.name}/variants`}
|
||||
underline='hover'
|
||||
>
|
||||
{variants.length === 1
|
||||
? '1 variant'
|
||||
: `${variants.length} variants`}
|
||||
</StyledLink>
|
||||
</Tooltip>
|
||||
</>
|
||||
);
|
||||
|
||||
const hasWarning =
|
||||
environment.enabled &&
|
||||
variants.length === 0 &&
|
||||
someEnabledEnvironmentHasVariants;
|
||||
return (
|
||||
<FeatureOverviewSidePanelEnvironmentSwitch
|
||||
key={environment.name}
|
||||
environment={environment}
|
||||
hiddenEnvironments={hiddenEnvironments}
|
||||
setHiddenEnvironments={setHiddenEnvironments}
|
||||
>
|
||||
<StyledSwitchLabel>
|
||||
<StyledLabel>{environment.name}</StyledLabel>
|
||||
<StyledSubLabel>
|
||||
{strategiesLabel}
|
||||
{variantsLink}
|
||||
<ConditionallyRender
|
||||
condition={hasWarning}
|
||||
show={
|
||||
<>
|
||||
<StyledSeparator />
|
||||
<VariantsWarningTooltip />
|
||||
</>
|
||||
}
|
||||
/>
|
||||
</StyledSubLabel>
|
||||
</StyledSwitchLabel>
|
||||
</FeatureOverviewSidePanelEnvironmentSwitch>
|
||||
);
|
||||
})}
|
||||
</StyledContainer>
|
||||
);
|
||||
};
|
@ -1,89 +0,0 @@
|
||||
import { Box, Divider, styled } from '@mui/material';
|
||||
import { HelpIcon } from 'component/common/HelpIcon/HelpIcon';
|
||||
import { useFeature } from 'hooks/api/getters/useFeature/useFeature';
|
||||
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
|
||||
import { FeatureOverviewSidePanelEnvironmentSwitches } from './FeatureOverviewSidePanelEnvironmentSwitches/FeatureOverviewSidePanelEnvironmentSwitches';
|
||||
import { FeatureOverviewSidePanelTags } from './FeatureOverviewSidePanelTags/FeatureOverviewSidePanelTags';
|
||||
import { Sticky } from 'component/common/Sticky/Sticky';
|
||||
|
||||
const StyledContainer = styled(Box)(({ theme }) => ({
|
||||
top: theme.spacing(2),
|
||||
borderRadius: theme.shape.borderRadiusLarge,
|
||||
backgroundColor: theme.palette.background.paper,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
maxWidth: '350px',
|
||||
minWidth: '350px',
|
||||
marginRight: '1rem',
|
||||
marginTop: '1rem',
|
||||
[theme.breakpoints.down(1000)]: {
|
||||
marginBottom: '1rem',
|
||||
width: '100%',
|
||||
maxWidth: 'none',
|
||||
minWidth: 'auto',
|
||||
},
|
||||
}));
|
||||
|
||||
const StyledHeader = styled('h3')(({ theme }) => ({
|
||||
display: 'flex',
|
||||
gap: theme.spacing(1),
|
||||
alignItems: 'center',
|
||||
fontSize: theme.fontSizes.bodySize,
|
||||
margin: 0,
|
||||
marginBottom: theme.spacing(3),
|
||||
|
||||
// Make the help icon align with the text.
|
||||
'& > :last-child': {
|
||||
position: 'relative',
|
||||
top: 1,
|
||||
},
|
||||
}));
|
||||
|
||||
interface IFeatureOverviewSidePanelProps {
|
||||
hiddenEnvironments: Set<String>;
|
||||
setHiddenEnvironments: (environment: string) => void;
|
||||
}
|
||||
|
||||
export const OldFeatureOverviewSidePanel = ({
|
||||
hiddenEnvironments,
|
||||
setHiddenEnvironments,
|
||||
}: IFeatureOverviewSidePanelProps) => {
|
||||
const projectId = useRequiredPathParam('projectId');
|
||||
const featureId = useRequiredPathParam('featureId');
|
||||
const { feature } = useFeature(projectId, featureId);
|
||||
const isSticky = feature.environments?.length <= 3;
|
||||
|
||||
return (
|
||||
<StyledContainer as={isSticky ? Sticky : Box}>
|
||||
<FeatureOverviewSidePanelEnvironmentSwitches
|
||||
header={
|
||||
<StyledHeader data-loading>
|
||||
Enabled in environments (
|
||||
{
|
||||
feature.environments.filter(
|
||||
({ enabled }) => enabled,
|
||||
).length
|
||||
}
|
||||
)
|
||||
<HelpIcon
|
||||
tooltip='When a feature is switched off in an environment, it will always return false. When switched on, it will return true or false depending on its strategies.'
|
||||
placement='top'
|
||||
/>
|
||||
</StyledHeader>
|
||||
}
|
||||
feature={feature}
|
||||
hiddenEnvironments={hiddenEnvironments}
|
||||
setHiddenEnvironments={setHiddenEnvironments}
|
||||
/>
|
||||
<Divider />
|
||||
<FeatureOverviewSidePanelTags
|
||||
header={
|
||||
<StyledHeader data-loading>
|
||||
Tags for this feature flag
|
||||
</StyledHeader>
|
||||
}
|
||||
feature={feature}
|
||||
/>
|
||||
</StyledContainer>
|
||||
);
|
||||
};
|
@ -1,91 +0,0 @@
|
||||
import { FeatureOverviewEnvironments } from './FeatureOverviewEnvironments/FeatureOverviewEnvironments';
|
||||
import { Route, Routes, useNavigate } from 'react-router-dom';
|
||||
import { SidebarModal } from 'component/common/SidebarModal/SidebarModal';
|
||||
import {
|
||||
FeatureStrategyEdit,
|
||||
formatFeaturePath,
|
||||
} from 'component/feature/FeatureStrategy/FeatureStrategyEdit/FeatureStrategyEdit';
|
||||
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
|
||||
import { usePageTitle } from 'hooks/usePageTitle';
|
||||
import { useHiddenEnvironments } from 'hooks/useHiddenEnvironments';
|
||||
import { styled } from '@mui/material';
|
||||
import { FeatureStrategyCreate } from 'component/feature/FeatureStrategy/FeatureStrategyCreate/FeatureStrategyCreate';
|
||||
import { useEffect } from 'react';
|
||||
import { useLastViewedFlags } from 'hooks/useLastViewedFlags';
|
||||
import OldFeatureOverviewMetaData from './FeatureOverviewMetaData/OldFeatureOverviewMetaData';
|
||||
import { OldFeatureOverviewSidePanel } from 'component/feature/FeatureView/FeatureOverview/FeatureOverviewSidePanel/OldFeatureOverviewSidePanel';
|
||||
|
||||
const StyledContainer = styled('div')(({ theme }) => ({
|
||||
display: 'flex',
|
||||
width: '100%',
|
||||
[theme.breakpoints.down(1000)]: {
|
||||
flexDirection: 'column',
|
||||
},
|
||||
}));
|
||||
|
||||
const StyledMainContent = styled('div')(({ theme }) => ({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
width: `calc(100% - (350px + 1rem))`,
|
||||
[theme.breakpoints.down(1000)]: {
|
||||
width: '100%',
|
||||
},
|
||||
}));
|
||||
|
||||
const FeatureOverview = () => {
|
||||
const navigate = useNavigate();
|
||||
const projectId = useRequiredPathParam('projectId');
|
||||
const featureId = useRequiredPathParam('featureId');
|
||||
const featurePath = formatFeaturePath(projectId, featureId);
|
||||
const { hiddenEnvironments, setHiddenEnvironments } =
|
||||
useHiddenEnvironments();
|
||||
const onSidebarClose = () => navigate(featurePath);
|
||||
usePageTitle(featureId);
|
||||
const { setLastViewed } = useLastViewedFlags();
|
||||
useEffect(() => {
|
||||
setLastViewed({ featureId, projectId });
|
||||
}, [featureId]);
|
||||
|
||||
return (
|
||||
<StyledContainer>
|
||||
<div>
|
||||
<OldFeatureOverviewMetaData />
|
||||
<OldFeatureOverviewSidePanel
|
||||
hiddenEnvironments={hiddenEnvironments}
|
||||
setHiddenEnvironments={setHiddenEnvironments}
|
||||
/>
|
||||
</div>
|
||||
<StyledMainContent>
|
||||
<FeatureOverviewEnvironments />
|
||||
</StyledMainContent>
|
||||
<Routes>
|
||||
<Route
|
||||
path='strategies/create'
|
||||
element={
|
||||
<SidebarModal
|
||||
label='Create feature strategy'
|
||||
onClose={onSidebarClose}
|
||||
open
|
||||
>
|
||||
<FeatureStrategyCreate />
|
||||
</SidebarModal>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path='strategies/edit'
|
||||
element={
|
||||
<SidebarModal
|
||||
label='Edit feature strategy'
|
||||
onClose={onSidebarClose}
|
||||
open
|
||||
>
|
||||
<FeatureStrategyEdit />
|
||||
</SidebarModal>
|
||||
}
|
||||
/>
|
||||
</Routes>
|
||||
</StyledContainer>
|
||||
);
|
||||
};
|
||||
|
||||
export default FeatureOverview;
|
@ -1,316 +0,0 @@
|
||||
import Delete from '@mui/icons-material/Delete';
|
||||
import { styled } from '@mui/material';
|
||||
import { DELETE_FEATURE_STRATEGY } from '@server/types/permissions';
|
||||
import PermissionIconButton from 'component/common/PermissionIconButton/PermissionIconButton';
|
||||
import { useReleasePlansApi } from 'hooks/api/actions/useReleasePlansApi/useReleasePlansApi';
|
||||
import { useReleasePlans } from 'hooks/api/getters/useReleasePlans/useReleasePlans';
|
||||
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
|
||||
import useToast from 'hooks/useToast';
|
||||
import type {
|
||||
IReleasePlan,
|
||||
IReleasePlanMilestone,
|
||||
} from 'interfaces/releasePlans';
|
||||
import { useState } from 'react';
|
||||
import { formatUnknownError } from 'utils/formatUnknownError';
|
||||
import { ReleasePlanRemoveDialog } from './ReleasePlanRemoveDialog';
|
||||
import { ReleasePlanMilestone } from './ReleasePlanMilestone/LegacyReleasePlanMilestone';
|
||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||
import { useChangeRequestsEnabled } from 'hooks/useChangeRequestsEnabled';
|
||||
import { useUiFlag } from 'hooks/useUiFlag';
|
||||
import { useChangeRequestApi } from 'hooks/api/actions/useChangeRequestApi/useChangeRequestApi';
|
||||
import { usePendingChangeRequests } from 'hooks/api/getters/usePendingChangeRequests/usePendingChangeRequests';
|
||||
import { RemoveReleasePlanChangeRequestDialog } from './ChangeRequest/RemoveReleasePlanChangeRequestDialog';
|
||||
import { StartMilestoneChangeRequestDialog } from './ChangeRequest/StartMilestoneChangeRequestDialog';
|
||||
import { usePlausibleTracker } from 'hooks/usePlausibleTracker';
|
||||
import { Truncator } from 'component/common/Truncator/Truncator';
|
||||
|
||||
const StyledContainer = styled('div', {
|
||||
shouldForwardProp: (prop) => prop !== 'readonly',
|
||||
})<{ readonly?: boolean }>(({ theme, readonly }) => ({
|
||||
padding: theme.spacing(2),
|
||||
borderRadius: theme.shape.borderRadiusMedium,
|
||||
'& + &': {
|
||||
marginTop: theme.spacing(2),
|
||||
},
|
||||
background: readonly
|
||||
? theme.palette.background.elevation1
|
||||
: theme.palette.background.paper,
|
||||
}));
|
||||
|
||||
const StyledHeader = styled('div')(({ theme }) => ({
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
color: theme.palette.text.primary,
|
||||
}));
|
||||
|
||||
const StyledHeaderTitleContainer = styled('div')(({ theme }) => ({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'center',
|
||||
gap: theme.spacing(1),
|
||||
}));
|
||||
|
||||
const StyledHeaderTitleLabel = styled('span')(({ theme }) => ({
|
||||
fontSize: theme.fontSizes.smallerBody,
|
||||
lineHeight: 0.5,
|
||||
color: theme.palette.text.secondary,
|
||||
marginBottom: theme.spacing(0.5),
|
||||
}));
|
||||
|
||||
const StyledHeaderDescription = styled('span')(({ theme }) => ({
|
||||
fontSize: theme.fontSizes.smallBody,
|
||||
color: theme.palette.text.secondary,
|
||||
}));
|
||||
|
||||
const StyledBody = styled('div')(({ theme }) => ({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
marginTop: theme.spacing(3),
|
||||
}));
|
||||
|
||||
const StyledConnection = styled('div')(({ theme }) => ({
|
||||
width: 4,
|
||||
height: theme.spacing(2),
|
||||
backgroundColor: theme.palette.divider,
|
||||
marginLeft: theme.spacing(3.25),
|
||||
}));
|
||||
|
||||
interface IReleasePlanProps {
|
||||
plan: IReleasePlan;
|
||||
environmentIsDisabled?: boolean;
|
||||
readonly?: boolean;
|
||||
}
|
||||
|
||||
export const ReleasePlan = ({
|
||||
plan,
|
||||
environmentIsDisabled,
|
||||
readonly,
|
||||
}: IReleasePlanProps) => {
|
||||
const {
|
||||
id,
|
||||
name,
|
||||
description,
|
||||
activeMilestoneId,
|
||||
featureName,
|
||||
environment,
|
||||
milestones,
|
||||
} = plan;
|
||||
|
||||
const projectId = useRequiredPathParam('projectId');
|
||||
const { refetch } = useReleasePlans(projectId, featureName, environment);
|
||||
const { removeReleasePlanFromFeature, startReleasePlanMilestone } =
|
||||
useReleasePlansApi();
|
||||
const { setToastData, setToastApiError } = useToast();
|
||||
const { trackEvent } = usePlausibleTracker();
|
||||
|
||||
const [removeOpen, setRemoveOpen] = useState(false);
|
||||
const [changeRequestDialogRemoveOpen, setChangeRequestDialogRemoveOpen] =
|
||||
useState(false);
|
||||
const [
|
||||
changeRequestDialogStartMilestoneOpen,
|
||||
setChangeRequestDialogStartMilestoneOpen,
|
||||
] = useState(false);
|
||||
const [
|
||||
milestoneForChangeRequestDialog,
|
||||
setMilestoneForChangeRequestDialog,
|
||||
] = useState<IReleasePlanMilestone>();
|
||||
const { isChangeRequestConfigured } = useChangeRequestsEnabled(projectId);
|
||||
const { addChange } = useChangeRequestApi();
|
||||
const { refetch: refetchChangeRequests } =
|
||||
usePendingChangeRequests(projectId);
|
||||
|
||||
const releasePlansEnabled = useUiFlag('releasePlans');
|
||||
|
||||
const onAddRemovePlanChangesConfirm = async () => {
|
||||
await addChange(projectId, environment, {
|
||||
feature: featureName,
|
||||
action: 'deleteReleasePlan',
|
||||
payload: {
|
||||
planId: plan.id,
|
||||
},
|
||||
});
|
||||
|
||||
await refetchChangeRequests();
|
||||
|
||||
setToastData({
|
||||
type: 'success',
|
||||
text: 'Added to draft',
|
||||
});
|
||||
|
||||
setChangeRequestDialogRemoveOpen(false);
|
||||
};
|
||||
|
||||
const onAddStartMilestoneChangesConfirm = async () => {
|
||||
await addChange(projectId, environment, {
|
||||
feature: featureName,
|
||||
action: 'startMilestone',
|
||||
payload: {
|
||||
planId: plan.id,
|
||||
milestoneId: milestoneForChangeRequestDialog?.id,
|
||||
},
|
||||
});
|
||||
|
||||
await refetchChangeRequests();
|
||||
|
||||
setToastData({
|
||||
type: 'success',
|
||||
text: 'Added to draft',
|
||||
});
|
||||
|
||||
setChangeRequestDialogStartMilestoneOpen(false);
|
||||
};
|
||||
|
||||
const confirmRemoveReleasePlan = () => {
|
||||
if (releasePlansEnabled && isChangeRequestConfigured(environment)) {
|
||||
setChangeRequestDialogRemoveOpen(true);
|
||||
} else {
|
||||
setRemoveOpen(true);
|
||||
}
|
||||
|
||||
trackEvent('release-management', {
|
||||
props: {
|
||||
eventType: 'remove-plan',
|
||||
plan: name,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const onRemoveConfirm = async () => {
|
||||
try {
|
||||
await removeReleasePlanFromFeature(
|
||||
projectId,
|
||||
featureName,
|
||||
environment,
|
||||
id,
|
||||
);
|
||||
setToastData({
|
||||
text: `Release plan "${name}" has been removed from ${featureName} in ${environment}`,
|
||||
type: 'success',
|
||||
});
|
||||
|
||||
refetch();
|
||||
setRemoveOpen(false);
|
||||
} catch (error: unknown) {
|
||||
setToastApiError(formatUnknownError(error));
|
||||
}
|
||||
};
|
||||
|
||||
const onStartMilestone = async (milestone: IReleasePlanMilestone) => {
|
||||
if (releasePlansEnabled && isChangeRequestConfigured(environment)) {
|
||||
setMilestoneForChangeRequestDialog(milestone);
|
||||
setChangeRequestDialogStartMilestoneOpen(true);
|
||||
} else {
|
||||
try {
|
||||
await startReleasePlanMilestone(
|
||||
projectId,
|
||||
featureName,
|
||||
environment,
|
||||
id,
|
||||
milestone.id,
|
||||
);
|
||||
setToastData({
|
||||
text: `Milestone "${milestone.name}" has started`,
|
||||
type: 'success',
|
||||
});
|
||||
refetch();
|
||||
} catch (error: unknown) {
|
||||
setToastApiError(formatUnknownError(error));
|
||||
}
|
||||
}
|
||||
|
||||
trackEvent('release-management', {
|
||||
props: {
|
||||
eventType: 'start-milestone',
|
||||
plan: name,
|
||||
milestone: milestone.name,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const activeIndex = milestones.findIndex(
|
||||
(milestone) => milestone.id === activeMilestoneId,
|
||||
);
|
||||
|
||||
return (
|
||||
<StyledContainer readonly={readonly}>
|
||||
<StyledHeader>
|
||||
<StyledHeaderTitleContainer>
|
||||
<StyledHeaderTitleLabel>
|
||||
Release plan
|
||||
</StyledHeaderTitleLabel>
|
||||
<span>{name}</span>
|
||||
<StyledHeaderDescription>
|
||||
<Truncator lines={2} title={description}>
|
||||
{description}
|
||||
</Truncator>
|
||||
</StyledHeaderDescription>
|
||||
</StyledHeaderTitleContainer>
|
||||
{!readonly && (
|
||||
<PermissionIconButton
|
||||
onClick={confirmRemoveReleasePlan}
|
||||
permission={DELETE_FEATURE_STRATEGY}
|
||||
environmentId={environment}
|
||||
projectId={projectId}
|
||||
tooltipProps={{
|
||||
title: 'Remove release plan',
|
||||
}}
|
||||
>
|
||||
<Delete />
|
||||
</PermissionIconButton>
|
||||
)}
|
||||
</StyledHeader>
|
||||
<StyledBody>
|
||||
{milestones.map((milestone, index) => (
|
||||
<div key={milestone.id}>
|
||||
<ReleasePlanMilestone
|
||||
readonly={readonly}
|
||||
milestone={milestone}
|
||||
status={
|
||||
milestone.id === activeMilestoneId
|
||||
? environmentIsDisabled
|
||||
? 'paused'
|
||||
: 'active'
|
||||
: index < activeIndex
|
||||
? 'completed'
|
||||
: 'not-started'
|
||||
}
|
||||
onStartMilestone={onStartMilestone}
|
||||
/>
|
||||
<ConditionallyRender
|
||||
condition={index < milestones.length - 1}
|
||||
show={<StyledConnection />}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</StyledBody>
|
||||
<ReleasePlanRemoveDialog
|
||||
plan={plan}
|
||||
open={removeOpen}
|
||||
setOpen={setRemoveOpen}
|
||||
onConfirm={onRemoveConfirm}
|
||||
environmentActive={!environmentIsDisabled}
|
||||
/>
|
||||
<RemoveReleasePlanChangeRequestDialog
|
||||
environmentId={environment}
|
||||
featureId={featureName}
|
||||
isOpen={changeRequestDialogRemoveOpen}
|
||||
onConfirm={onAddRemovePlanChangesConfirm}
|
||||
onClosing={() => setChangeRequestDialogRemoveOpen(false)}
|
||||
releasePlan={plan}
|
||||
environmentActive={!environmentIsDisabled}
|
||||
/>
|
||||
<StartMilestoneChangeRequestDialog
|
||||
environmentId={environment}
|
||||
featureId={featureName}
|
||||
isOpen={changeRequestDialogStartMilestoneOpen}
|
||||
onConfirm={onAddStartMilestoneChangesConfirm}
|
||||
onClosing={() => {
|
||||
setMilestoneForChangeRequestDialog(undefined);
|
||||
setChangeRequestDialogStartMilestoneOpen(false);
|
||||
}}
|
||||
releasePlan={plan}
|
||||
milestone={milestoneForChangeRequestDialog}
|
||||
/>
|
||||
</StyledContainer>
|
||||
);
|
||||
};
|
@ -1,130 +0,0 @@
|
||||
// deprecated; remove with `flagOverviewRedesign` flag
|
||||
import ExpandMore from '@mui/icons-material/ExpandMore';
|
||||
import {
|
||||
Accordion,
|
||||
AccordionDetails,
|
||||
AccordionSummary,
|
||||
styled,
|
||||
} from '@mui/material';
|
||||
import type { IReleasePlanMilestone } from 'interfaces/releasePlans';
|
||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||
import { ReleasePlanMilestoneStrategy } from './ReleasePlanMilestoneStrategy';
|
||||
import { StrategySeparator } from 'component/common/StrategySeparator/LegacyStrategySeparator';
|
||||
import {
|
||||
ReleasePlanMilestoneStatus,
|
||||
type MilestoneStatus,
|
||||
} from './ReleasePlanMilestoneStatus';
|
||||
import { useState } from 'react';
|
||||
|
||||
const StyledAccordion = styled(Accordion, {
|
||||
shouldForwardProp: (prop) => prop !== 'status',
|
||||
})<{ status: MilestoneStatus }>(({ theme, status }) => ({
|
||||
border: `1px solid ${status === 'active' ? theme.palette.success.border : theme.palette.divider}`,
|
||||
boxShadow: 'none',
|
||||
margin: 0,
|
||||
backgroundColor: theme.palette.background.paper,
|
||||
'&:before': {
|
||||
display: 'none',
|
||||
},
|
||||
}));
|
||||
|
||||
const StyledAccordionSummary = styled(AccordionSummary)({
|
||||
'& .MuiAccordionSummary-content': {
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
minHeight: '30px',
|
||||
},
|
||||
});
|
||||
|
||||
const StyledTitleContainer = styled('div')(({ theme }) => ({
|
||||
display: 'flex',
|
||||
alignItems: 'start',
|
||||
flexDirection: 'column',
|
||||
gap: theme.spacing(0.5),
|
||||
}));
|
||||
|
||||
const StyledTitle = styled('span')(({ theme }) => ({
|
||||
fontWeight: theme.fontWeight.bold,
|
||||
}));
|
||||
|
||||
const StyledSecondaryLabel = styled('span')(({ theme }) => ({
|
||||
color: theme.palette.text.secondary,
|
||||
fontSize: theme.fontSizes.smallBody,
|
||||
}));
|
||||
|
||||
const StyledAccordionDetails = styled(AccordionDetails)(({ theme }) => ({
|
||||
borderBottomLeftRadius: theme.shape.borderRadiusLarge,
|
||||
borderBottomRightRadius: theme.shape.borderRadiusLarge,
|
||||
}));
|
||||
|
||||
interface IReleasePlanMilestoneProps {
|
||||
milestone: IReleasePlanMilestone;
|
||||
status?: MilestoneStatus;
|
||||
onStartMilestone?: (milestone: IReleasePlanMilestone) => void;
|
||||
readonly?: boolean;
|
||||
}
|
||||
|
||||
export const ReleasePlanMilestone = ({
|
||||
milestone,
|
||||
status = 'not-started',
|
||||
onStartMilestone,
|
||||
readonly,
|
||||
}: IReleasePlanMilestoneProps) => {
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
|
||||
if (!milestone.strategies.length) {
|
||||
return (
|
||||
<StyledAccordion status={status}>
|
||||
<StyledAccordionSummary>
|
||||
<StyledTitleContainer>
|
||||
<StyledTitle>{milestone.name}</StyledTitle>
|
||||
{!readonly && onStartMilestone && (
|
||||
<ReleasePlanMilestoneStatus
|
||||
status={status}
|
||||
onStartMilestone={() =>
|
||||
onStartMilestone(milestone)
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</StyledTitleContainer>
|
||||
<StyledSecondaryLabel>No strategies</StyledSecondaryLabel>
|
||||
</StyledAccordionSummary>
|
||||
</StyledAccordion>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<StyledAccordion
|
||||
status={status}
|
||||
onChange={(evt, expanded) => setExpanded(expanded)}
|
||||
>
|
||||
<StyledAccordionSummary expandIcon={<ExpandMore />}>
|
||||
<StyledTitleContainer>
|
||||
<StyledTitle>{milestone.name}</StyledTitle>
|
||||
{!readonly && onStartMilestone && (
|
||||
<ReleasePlanMilestoneStatus
|
||||
status={status}
|
||||
onStartMilestone={() => onStartMilestone(milestone)}
|
||||
/>
|
||||
)}
|
||||
</StyledTitleContainer>
|
||||
<StyledSecondaryLabel>
|
||||
{milestone.strategies.length === 1
|
||||
? `${expanded ? 'Hide' : 'View'} strategy`
|
||||
: `${expanded ? 'Hide' : 'View'} ${milestone.strategies.length} strategies`}
|
||||
</StyledSecondaryLabel>
|
||||
</StyledAccordionSummary>
|
||||
<StyledAccordionDetails>
|
||||
{milestone.strategies.map((strategy, index) => (
|
||||
<div key={strategy.id}>
|
||||
<ConditionallyRender
|
||||
condition={index > 0}
|
||||
show={<StrategySeparator text='OR' />}
|
||||
/>
|
||||
<ReleasePlanMilestoneStrategy strategy={strategy} />
|
||||
</div>
|
||||
))}
|
||||
</StyledAccordionDetails>
|
||||
</StyledAccordion>
|
||||
);
|
||||
};
|
@ -5,19 +5,11 @@ import {
|
||||
Tab,
|
||||
Tabs,
|
||||
type Theme,
|
||||
Tooltip,
|
||||
Typography,
|
||||
useMediaQuery,
|
||||
} from '@mui/material';
|
||||
import Archive from '@mui/icons-material/Archive';
|
||||
import ArchiveOutlined from '@mui/icons-material/ArchiveOutlined';
|
||||
import FileCopy from '@mui/icons-material/FileCopy';
|
||||
import Label from '@mui/icons-material/Label';
|
||||
import WatchLater from '@mui/icons-material/WatchLater';
|
||||
import WatchLaterOutlined from '@mui/icons-material/WatchLaterOutlined';
|
||||
import LibraryAdd from '@mui/icons-material/LibraryAdd';
|
||||
import LibraryAddOutlined from '@mui/icons-material/LibraryAddOutlined';
|
||||
import Check from '@mui/icons-material/Check';
|
||||
import Star from '@mui/icons-material/Star';
|
||||
import { Link, useLocation, useNavigate } from 'react-router-dom';
|
||||
import { useFeature } from 'hooks/api/getters/useFeature/useFeature';
|
||||
@ -27,17 +19,12 @@ import {
|
||||
UPDATE_FEATURE,
|
||||
} from 'component/providers/AccessProvider/permissions';
|
||||
import PermissionIconButton from 'component/common/PermissionIconButton/PermissionIconButton';
|
||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||
import { FeatureStatusChip } from 'component/common/FeatureStatusChip/FeatureStatusChip';
|
||||
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
|
||||
import { useFavoriteFeaturesApi } from 'hooks/api/actions/useFavoriteFeaturesApi/useFavoriteFeaturesApi';
|
||||
import { FavoriteIconButton } from 'component/common/FavoriteIconButton/FavoriteIconButton';
|
||||
import { ChildrenTooltip } from './FeatureOverview/FeatureOverviewMetaData/ChildrenTooltip';
|
||||
import copy from 'copy-to-clipboard';
|
||||
import useToast from 'hooks/useToast';
|
||||
import { useUiFlag } from 'hooks/useUiFlag';
|
||||
import type { IFeatureToggle } from 'interfaces/featureToggle';
|
||||
import { Collaborators } from './Collaborators';
|
||||
import StarBorder from '@mui/icons-material/StarBorder';
|
||||
import { TooltipResolver } from 'component/common/TooltipResolver/TooltipResolver';
|
||||
import { ManageTagsDialog } from './FeatureOverview/ManageTagsDialog/ManageTagsDialog';
|
||||
@ -46,7 +33,7 @@ import { FeatureArchiveDialog } from 'component/common/FeatureArchiveDialog/Feat
|
||||
import { FeatureArchiveNotAllowedDialog } from 'component/common/FeatureArchiveDialog/FeatureArchiveNotAllowedDialog';
|
||||
import { FeatureCopyName } from './FeatureCopyName/FeatureCopyName';
|
||||
|
||||
const NewStyledHeader = styled('div')(({ theme }) => ({
|
||||
const StyledHeader = styled('div')(({ theme }) => ({
|
||||
backgroundColor: 'none',
|
||||
marginBottom: theme.spacing(2),
|
||||
borderBottom: `1px solid ${theme.palette.divider}`,
|
||||
@ -115,67 +102,6 @@ const IconButtonWithTooltip: FC<
|
||||
);
|
||||
};
|
||||
|
||||
const StyledHeader = styled('div')(({ theme }) => ({
|
||||
backgroundColor: theme.palette.background.paper,
|
||||
borderRadius: theme.shape.borderRadiusLarge,
|
||||
marginBottom: theme.spacing(2),
|
||||
}));
|
||||
|
||||
const StyledInnerContainer = styled('div')(({ theme }) => ({
|
||||
padding: theme.spacing(2, 4, 2, 2),
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
[theme.breakpoints.down(500)]: {
|
||||
flexDirection: 'column',
|
||||
},
|
||||
}));
|
||||
|
||||
const StyledFlagInfoContainer = styled('div')(({ theme }) => ({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
columnGap: theme.spacing(1),
|
||||
}));
|
||||
|
||||
const StyledDependency = styled('div')(({ theme }) => ({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: theme.spacing(1),
|
||||
marginTop: theme.spacing(1),
|
||||
fontSize: theme.fontSizes.smallBody,
|
||||
padding: theme.spacing(0.75, 1.5),
|
||||
backgroundColor: theme.palette.background.elevation2,
|
||||
borderRadius: `${theme.shape.borderRadiusMedium}px`,
|
||||
width: 'max-content',
|
||||
}));
|
||||
|
||||
const StyledFeatureViewHeader = styled('h1')(({ theme }) => ({
|
||||
fontSize: theme.fontSizes.mainHeader,
|
||||
fontWeight: 'normal',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
wordBreak: 'break-all',
|
||||
}));
|
||||
|
||||
const StyledToolbarContainer = styled('div')({
|
||||
flexShrink: 0,
|
||||
display: 'flex',
|
||||
});
|
||||
|
||||
const StyledSeparator = styled('div')(({ theme }) => ({
|
||||
width: '100%',
|
||||
backgroundColor: theme.palette.divider,
|
||||
height: '1px',
|
||||
}));
|
||||
|
||||
const StyledTabRow = styled('div')(({ theme }) => ({
|
||||
display: 'flex',
|
||||
flexFlow: 'row nowrap',
|
||||
gap: theme.spacing(4),
|
||||
paddingInline: theme.spacing(4),
|
||||
justifyContent: 'space-between',
|
||||
}));
|
||||
|
||||
const StyledTabs = styled(Tabs)({
|
||||
minWidth: 0,
|
||||
maxWidth: '100%',
|
||||
@ -280,7 +206,6 @@ type Props = {
|
||||
export const FeatureViewHeader: FC<Props> = ({ feature }) => {
|
||||
const projectId = useRequiredPathParam('projectId');
|
||||
const featureId = useRequiredPathParam('featureId');
|
||||
const flagOverviewRedesign = useUiFlag('flagOverviewRedesign');
|
||||
const { favorite, unfavorite } = useFavoriteFeaturesApi();
|
||||
const { refetchFeature } = useFeature(projectId, featureId);
|
||||
const { setToastData, setToastApiError } = useToast();
|
||||
@ -289,9 +214,6 @@ export const FeatureViewHeader: FC<Props> = ({ feature }) => {
|
||||
const [showDelDialog, setShowDelDialog] = useState(false);
|
||||
const [openStaleDialog, setOpenStaleDialog] = useState(false);
|
||||
|
||||
const [isFeatureNameCopied, setIsFeatureNameCopied] = useState(false);
|
||||
const smallScreen = useMediaQuery(`(max-width:${500}px)`);
|
||||
|
||||
const navigate = useNavigate();
|
||||
|
||||
const { pathname } = useLocation();
|
||||
@ -344,21 +266,6 @@ export const FeatureViewHeader: FC<Props> = ({ feature }) => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleCopyToClipboard = () => {
|
||||
try {
|
||||
copy(feature.name);
|
||||
setIsFeatureNameCopied(true);
|
||||
setTimeout(() => {
|
||||
setIsFeatureNameCopied(false);
|
||||
}, 3000);
|
||||
} catch (error: unknown) {
|
||||
setToastData({
|
||||
type: 'error',
|
||||
text: 'Could not copy feature name',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const HeaderActionsInner: FC<{ showOnNarrowScreens?: boolean }> = ({
|
||||
showOnNarrowScreens,
|
||||
}) => {
|
||||
@ -375,184 +282,36 @@ export const FeatureViewHeader: FC<Props> = ({ feature }) => {
|
||||
|
||||
return (
|
||||
<>
|
||||
{flagOverviewRedesign ? (
|
||||
<NewStyledHeader>
|
||||
<UpperHeaderRow>
|
||||
<StyledTitle>
|
||||
<Typography variant='h1'>{feature.name}</Typography>
|
||||
<FeatureCopyName name={feature.name} />
|
||||
</StyledTitle>
|
||||
{feature.stale ? (
|
||||
<FeatureStatusChip stale={true} />
|
||||
) : null}
|
||||
</UpperHeaderRow>
|
||||
<LowerHeaderRow>
|
||||
<HeaderActionsInner showOnNarrowScreens />
|
||||
<StyledTabs
|
||||
value={activeTab.path}
|
||||
indicatorColor='primary'
|
||||
textColor='primary'
|
||||
aria-label='Feature flag tabs'
|
||||
variant='scrollable'
|
||||
>
|
||||
{tabData.map((tab) => (
|
||||
<StyledTabButton
|
||||
key={tab.title}
|
||||
label={tab.title}
|
||||
value={tab.path}
|
||||
onClick={() => navigate(tab.path)}
|
||||
data-testid={`TAB-${tab.title}`}
|
||||
/>
|
||||
))}
|
||||
</StyledTabs>
|
||||
<HeaderActionsInner />
|
||||
</LowerHeaderRow>
|
||||
</NewStyledHeader>
|
||||
) : (
|
||||
<StyledHeader>
|
||||
<StyledInnerContainer>
|
||||
<StyledFlagInfoContainer>
|
||||
<FavoriteIconButton
|
||||
onClick={onFavorite}
|
||||
isFavorite={feature.favorite}
|
||||
<StyledHeader>
|
||||
<UpperHeaderRow>
|
||||
<StyledTitle>
|
||||
<Typography variant='h1'>{feature.name}</Typography>
|
||||
<FeatureCopyName name={feature.name} />
|
||||
</StyledTitle>
|
||||
{feature.stale ? <FeatureStatusChip stale={true} /> : null}
|
||||
</UpperHeaderRow>
|
||||
<LowerHeaderRow>
|
||||
<HeaderActionsInner showOnNarrowScreens />
|
||||
<StyledTabs
|
||||
value={activeTab.path}
|
||||
indicatorColor='primary'
|
||||
textColor='primary'
|
||||
aria-label='Feature flag tabs'
|
||||
variant='scrollable'
|
||||
>
|
||||
{tabData.map((tab) => (
|
||||
<StyledTabButton
|
||||
key={tab.title}
|
||||
label={tab.title}
|
||||
value={tab.path}
|
||||
onClick={() => navigate(tab.path)}
|
||||
data-testid={`TAB-${tab.title}`}
|
||||
/>
|
||||
<div>
|
||||
<StyledFlagInfoContainer>
|
||||
<StyledFeatureViewHeader data-loading>
|
||||
{feature.name}
|
||||
</StyledFeatureViewHeader>
|
||||
<Tooltip
|
||||
title={
|
||||
isFeatureNameCopied
|
||||
? 'Copied!'
|
||||
: 'Copy name'
|
||||
}
|
||||
arrow
|
||||
>
|
||||
<IconButton
|
||||
onClick={handleCopyToClipboard}
|
||||
>
|
||||
{isFeatureNameCopied ? (
|
||||
<Check
|
||||
style={{ fontSize: 16 }}
|
||||
/>
|
||||
) : (
|
||||
<FileCopy
|
||||
style={{ fontSize: 16 }}
|
||||
/>
|
||||
)}
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<ConditionallyRender
|
||||
condition={!smallScreen}
|
||||
show={
|
||||
<FeatureStatusChip
|
||||
stale={feature.stale}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</StyledFlagInfoContainer>
|
||||
<ConditionallyRender
|
||||
condition={feature.dependencies.length > 0}
|
||||
show={
|
||||
<StyledDependency>
|
||||
<b>Has parent: </b>
|
||||
<StyledLink
|
||||
to={`/projects/${feature.project}/features/${feature.dependencies[0]?.feature}`}
|
||||
>
|
||||
{
|
||||
feature.dependencies[0]
|
||||
?.feature
|
||||
}
|
||||
</StyledLink>
|
||||
</StyledDependency>
|
||||
}
|
||||
/>
|
||||
<ConditionallyRender
|
||||
condition={feature.children.length > 0}
|
||||
show={
|
||||
<StyledDependency>
|
||||
<b>Has children:</b>
|
||||
<ChildrenTooltip
|
||||
childFeatures={feature.children}
|
||||
project={feature.project}
|
||||
/>
|
||||
</StyledDependency>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</StyledFlagInfoContainer>
|
||||
|
||||
<StyledToolbarContainer>
|
||||
<PermissionIconButton
|
||||
permission={CREATE_FEATURE}
|
||||
projectId={projectId}
|
||||
data-loading
|
||||
component={Link}
|
||||
to={`/projects/${projectId}/features/${featureId}/copy`}
|
||||
tooltipProps={{
|
||||
title: 'Clone',
|
||||
}}
|
||||
>
|
||||
<LibraryAdd />
|
||||
</PermissionIconButton>
|
||||
<PermissionIconButton
|
||||
permission={DELETE_FEATURE}
|
||||
projectId={projectId}
|
||||
tooltipProps={{
|
||||
title: 'Archive feature flag',
|
||||
}}
|
||||
data-loading
|
||||
onClick={() => setShowDelDialog(true)}
|
||||
>
|
||||
<Archive />
|
||||
</PermissionIconButton>
|
||||
<PermissionIconButton
|
||||
onClick={() => setOpenStaleDialog(true)}
|
||||
permission={UPDATE_FEATURE}
|
||||
projectId={projectId}
|
||||
tooltipProps={{
|
||||
title: 'Toggle stale state',
|
||||
}}
|
||||
data-loading
|
||||
>
|
||||
<WatchLater />
|
||||
</PermissionIconButton>
|
||||
<PermissionIconButton
|
||||
onClick={() => setOpenTagDialog(true)}
|
||||
permission={UPDATE_FEATURE}
|
||||
projectId={projectId}
|
||||
tooltipProps={{ title: 'Add tag' }}
|
||||
data-loading
|
||||
>
|
||||
<Label />
|
||||
</PermissionIconButton>
|
||||
</StyledToolbarContainer>
|
||||
</StyledInnerContainer>
|
||||
<StyledSeparator />
|
||||
<StyledTabRow>
|
||||
<Tabs
|
||||
value={activeTab.path}
|
||||
indicatorColor='primary'
|
||||
textColor='primary'
|
||||
>
|
||||
{tabData.map((tab) => (
|
||||
<StyledTabButton
|
||||
key={tab.title}
|
||||
label={tab.title}
|
||||
value={tab.path}
|
||||
onClick={() => navigate(tab.path)}
|
||||
data-testid={`TAB-${tab.title}`}
|
||||
/>
|
||||
))}
|
||||
</Tabs>
|
||||
<Collaborators
|
||||
collaborators={feature.collaborators?.users}
|
||||
/>
|
||||
</StyledTabRow>
|
||||
</StyledHeader>
|
||||
)}
|
||||
|
||||
))}
|
||||
</StyledTabs>
|
||||
<HeaderActionsInner />
|
||||
</LowerHeaderRow>
|
||||
</StyledHeader>
|
||||
{feature.children.length > 0 ? (
|
||||
<FeatureArchiveNotAllowedDialog
|
||||
features={feature.children}
|
||||
@ -571,7 +330,6 @@ export const FeatureViewHeader: FC<Props> = ({ feature }) => {
|
||||
featureIds={[featureId]}
|
||||
/>
|
||||
)}
|
||||
|
||||
<FeatureStaleDialog
|
||||
isStale={feature.stale}
|
||||
isOpen={openStaleDialog}
|
||||
|
@ -1,6 +1,5 @@
|
||||
import { screen } from '@testing-library/react';
|
||||
import { render } from 'utils/testRenderer';
|
||||
import { FeatureDetails as LegacyFeatureDetails } from './LegacyFeatureDetails';
|
||||
import type { PlaygroundFeatureSchema, PlaygroundRequestSchema } from 'openapi';
|
||||
import { FeatureDetails } from './FeatureDetails';
|
||||
|
||||
@ -80,7 +79,7 @@ const testCases = [
|
||||
testCases.forEach(({ name, feature, expectedText1, expectedText2 }) => {
|
||||
test(`${name} (legacy)`, async () => {
|
||||
render(
|
||||
<LegacyFeatureDetails
|
||||
<FeatureDetails
|
||||
feature={feature}
|
||||
input={
|
||||
{ environment: 'development' } as PlaygroundRequestSchema
|
||||
|
@ -2,9 +2,6 @@ import { useRef, useState } from 'react';
|
||||
import type { PlaygroundFeatureSchema, PlaygroundRequestSchema } from 'openapi';
|
||||
import { IconButton, Popover, styled } from '@mui/material';
|
||||
import InfoOutlined from '@mui/icons-material/InfoOutlined';
|
||||
import { FeatureDetails as LegacyFeatureDetails } from './FeatureDetails/LegacyFeatureDetails';
|
||||
import { PlaygroundResultFeatureStrategyList as LegacyPlaygroundResultFeatureStrategyList } from './FeatureStrategyList/LegacyPlaygroundResultFeatureStrategyList';
|
||||
import { useUiFlag } from 'hooks/useUiFlag';
|
||||
import { FeatureDetails } from './FeatureDetails/FeatureDetails';
|
||||
import { PlaygroundResultFeatureStrategyList } from './FeatureStrategyList/PlaygroundResultsFeatureStrategyList';
|
||||
|
||||
@ -18,62 +15,6 @@ const FeatureResultPopoverWrapper = styled('div')(({ theme }) => ({
|
||||
color: theme.palette.divider,
|
||||
}));
|
||||
|
||||
const LegacyFeatureResultInfoPopoverCell = ({
|
||||
feature,
|
||||
input,
|
||||
}: FeatureResultInfoPopoverCellProps) => {
|
||||
const [open, setOpen] = useState(false);
|
||||
const ref = useRef(null);
|
||||
|
||||
const togglePopover = () => {
|
||||
setOpen(!open);
|
||||
};
|
||||
|
||||
return (
|
||||
<FeatureResultPopoverWrapper>
|
||||
<IconButton onClick={togglePopover}>
|
||||
<InfoOutlined ref={ref} />
|
||||
</IconButton>
|
||||
<Popover
|
||||
open={open}
|
||||
onClose={() => setOpen(false)}
|
||||
anchorEl={ref.current}
|
||||
PaperProps={{
|
||||
sx: (theme) => ({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
padding: theme.spacing(6),
|
||||
width: 728,
|
||||
maxWidth: '100%',
|
||||
height: 'auto',
|
||||
overflowY: 'auto',
|
||||
backgroundColor: theme.palette.background.elevation2,
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
}),
|
||||
}}
|
||||
anchorOrigin={{
|
||||
vertical: 'top',
|
||||
horizontal: 'right',
|
||||
}}
|
||||
transformOrigin={{
|
||||
vertical: 'center',
|
||||
horizontal: 'left',
|
||||
}}
|
||||
>
|
||||
<LegacyFeatureDetails
|
||||
feature={feature}
|
||||
input={input}
|
||||
onClose={() => setOpen(false)}
|
||||
/>
|
||||
<LegacyPlaygroundResultFeatureStrategyList
|
||||
feature={feature}
|
||||
input={input}
|
||||
/>
|
||||
</Popover>
|
||||
</FeatureResultPopoverWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export const NewFeatureResultInfoPopoverCell = ({
|
||||
feature,
|
||||
input,
|
||||
@ -136,15 +77,9 @@ export const NewFeatureResultInfoPopoverCell = ({
|
||||
export const FeatureResultInfoPopoverCell = (
|
||||
props: FeatureResultInfoPopoverCellProps,
|
||||
) => {
|
||||
const useNewStrategyDesign = useUiFlag('flagOverviewRedesign');
|
||||
|
||||
if (!props.feature) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return useNewStrategyDesign ? (
|
||||
<NewFeatureResultInfoPopoverCell {...props} />
|
||||
) : (
|
||||
<LegacyFeatureResultInfoPopoverCell {...props} />
|
||||
);
|
||||
return <NewFeatureResultInfoPopoverCell {...props} />;
|
||||
};
|
||||
|
@ -1,77 +0,0 @@
|
||||
import {
|
||||
PlaygroundResultStrategyLists,
|
||||
WrappedPlaygroundResultStrategyList,
|
||||
} from './StrategyList/LegacyPlaygroundResultStrategyLists';
|
||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||
import type { PlaygroundFeatureSchema, PlaygroundRequestSchema } from 'openapi';
|
||||
import { Alert } from '@mui/material';
|
||||
|
||||
interface PlaygroundResultFeatureStrategyListProps {
|
||||
feature: PlaygroundFeatureSchema;
|
||||
input?: PlaygroundRequestSchema;
|
||||
}
|
||||
|
||||
export const PlaygroundResultFeatureStrategyList = ({
|
||||
feature,
|
||||
input,
|
||||
}: PlaygroundResultFeatureStrategyListProps) => {
|
||||
const enabledStrategies = feature.strategies?.data?.filter(
|
||||
(strategy) => !strategy.disabled,
|
||||
);
|
||||
const disabledStrategies = feature.strategies?.data?.filter(
|
||||
(strategy) => strategy.disabled,
|
||||
);
|
||||
|
||||
const showDisabledStrategies = disabledStrategies?.length > 0;
|
||||
|
||||
return (
|
||||
<>
|
||||
<ConditionallyRender
|
||||
condition={feature?.strategies?.data?.length === 0}
|
||||
show={
|
||||
<Alert severity='warning' sx={{ mt: 2 }}>
|
||||
There are no strategies added to this feature flag in
|
||||
selected environment.
|
||||
</Alert>
|
||||
}
|
||||
/>
|
||||
<ConditionallyRender
|
||||
condition={
|
||||
(feature.hasUnsatisfiedDependency ||
|
||||
!feature.isEnabledInCurrentEnvironment) &&
|
||||
Boolean(feature?.strategies?.data)
|
||||
}
|
||||
show={
|
||||
<WrappedPlaygroundResultStrategyList
|
||||
feature={feature}
|
||||
input={input}
|
||||
/>
|
||||
}
|
||||
elseShow={
|
||||
<>
|
||||
<PlaygroundResultStrategyLists
|
||||
strategies={enabledStrategies || []}
|
||||
input={input}
|
||||
titlePrefix={
|
||||
showDisabledStrategies ? 'Enabled' : ''
|
||||
}
|
||||
/>
|
||||
<ConditionallyRender
|
||||
condition={showDisabledStrategies}
|
||||
show={
|
||||
<PlaygroundResultStrategyLists
|
||||
strategies={disabledStrategies}
|
||||
input={input}
|
||||
titlePrefix={'Disabled'}
|
||||
infoText={
|
||||
'Disabled strategies are not evaluated for the overall result.'
|
||||
}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
@ -1,8 +1,7 @@
|
||||
import { vi } from 'vitest';
|
||||
import { screen } from '@testing-library/react';
|
||||
import { render } from 'utils/testRenderer';
|
||||
import type { PlaygroundFeatureSchema, PlaygroundRequestSchema } from 'openapi';
|
||||
import { PlaygroundResultFeatureStrategyList as LegacyPlaygroundResultFeatureStrategyList } from './LegacyPlaygroundResultFeatureStrategyList';
|
||||
import { vi } from 'vitest';
|
||||
import { PlaygroundResultFeatureStrategyList } from './PlaygroundResultsFeatureStrategyList';
|
||||
|
||||
const testCases = [
|
||||
@ -139,7 +138,7 @@ afterAll(() => {
|
||||
testCases.forEach(({ name, feature, expectedText }) => {
|
||||
test(`${name} (legacy)`, async () => {
|
||||
render(
|
||||
<LegacyPlaygroundResultFeatureStrategyList
|
||||
<PlaygroundResultFeatureStrategyList
|
||||
feature={feature}
|
||||
input={
|
||||
{ environment: 'development' } as PlaygroundRequestSchema
|
||||
|
@ -1,152 +0,0 @@
|
||||
import { Fragment } from 'react';
|
||||
import { Alert, Box, styled, Typography } from '@mui/material';
|
||||
import type {
|
||||
PlaygroundStrategySchema,
|
||||
PlaygroundRequestSchema,
|
||||
PlaygroundFeatureSchema,
|
||||
} from 'openapi';
|
||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||
import { FeatureStrategyItem } from './StrategyItem/LegacyFeatureStrategyItem';
|
||||
import { StrategySeparator } from 'component/common/StrategySeparator/LegacyStrategySeparator';
|
||||
|
||||
const StyledAlertWrapper = styled('div')(({ theme }) => ({
|
||||
display: 'flex',
|
||||
padding: `0, 4px`,
|
||||
flexDirection: 'column',
|
||||
borderRadius: theme.shape.borderRadiusMedium,
|
||||
border: `1px solid ${theme.palette.warning.border}`,
|
||||
}));
|
||||
|
||||
const StyledListWrapper = styled('div')(({ theme }) => ({
|
||||
padding: theme.spacing(1, 0.5),
|
||||
}));
|
||||
|
||||
const StyledAlert = styled(Alert)(({ theme }) => ({
|
||||
border: '0!important',
|
||||
borderBottomLeftRadius: 0,
|
||||
borderBottomRightRadius: 0,
|
||||
borderBottom: `1px solid ${theme.palette.warning.border}!important`,
|
||||
}));
|
||||
|
||||
interface PlaygroundResultStrategyListProps {
|
||||
strategies: PlaygroundStrategySchema[];
|
||||
input?: PlaygroundRequestSchema;
|
||||
titlePrefix?: string;
|
||||
infoText?: string;
|
||||
}
|
||||
|
||||
const StyledSubtitle = styled(Typography)(({ theme }) => ({
|
||||
margin: theme.spacing(2, 1, 2, 0),
|
||||
color: 'text.secondary',
|
||||
}));
|
||||
|
||||
export const PlaygroundResultStrategyLists = ({
|
||||
strategies,
|
||||
input,
|
||||
titlePrefix,
|
||||
infoText,
|
||||
}: PlaygroundResultStrategyListProps) => (
|
||||
<ConditionallyRender
|
||||
condition={strategies.length > 0}
|
||||
show={
|
||||
<>
|
||||
<StyledSubtitle variant={'subtitle1'}>{`${
|
||||
titlePrefix
|
||||
? titlePrefix.concat(' strategies')
|
||||
: 'Strategies'
|
||||
} (${strategies?.length})`}</StyledSubtitle>
|
||||
<ConditionallyRender
|
||||
condition={Boolean(infoText)}
|
||||
show={
|
||||
<StyledSubtitle variant={'subtitle2'}>
|
||||
{infoText}
|
||||
</StyledSubtitle>
|
||||
}
|
||||
/>
|
||||
<Box sx={{ width: '100%' }}>
|
||||
{strategies?.map((strategy, index) => (
|
||||
<Fragment key={strategy.id}>
|
||||
<ConditionallyRender
|
||||
condition={index > 0}
|
||||
show={<StrategySeparator text='OR' />}
|
||||
/>
|
||||
<FeatureStrategyItem
|
||||
key={strategy.id}
|
||||
strategy={strategy}
|
||||
index={index}
|
||||
input={input}
|
||||
/>
|
||||
</Fragment>
|
||||
))}
|
||||
</Box>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
);
|
||||
|
||||
interface IWrappedPlaygroundResultStrategyListProps {
|
||||
feature: PlaygroundFeatureSchema;
|
||||
input?: PlaygroundRequestSchema;
|
||||
}
|
||||
|
||||
const resolveHintText = (feature: PlaygroundFeatureSchema) => {
|
||||
if (
|
||||
feature.hasUnsatisfiedDependency &&
|
||||
!feature.isEnabledInCurrentEnvironment
|
||||
) {
|
||||
return 'If the environment was enabled and parent dependencies were satisfied';
|
||||
}
|
||||
if (feature.hasUnsatisfiedDependency) {
|
||||
return 'If parent dependencies were satisfied';
|
||||
}
|
||||
if (!feature.isEnabledInCurrentEnvironment) {
|
||||
return 'If the environment was enabled';
|
||||
}
|
||||
return '';
|
||||
};
|
||||
|
||||
export const WrappedPlaygroundResultStrategyList = ({
|
||||
feature,
|
||||
input,
|
||||
}: IWrappedPlaygroundResultStrategyListProps) => {
|
||||
const enabledStrategies = feature.strategies?.data?.filter(
|
||||
(strategy) => !strategy.disabled,
|
||||
);
|
||||
const disabledStrategies = feature.strategies?.data?.filter(
|
||||
(strategy) => strategy.disabled,
|
||||
);
|
||||
|
||||
const showDisabledStrategies = disabledStrategies?.length > 0;
|
||||
|
||||
return (
|
||||
<StyledAlertWrapper sx={{ pb: 1, mt: 2 }}>
|
||||
<StyledAlert severity={'info'} color={'warning'}>
|
||||
{resolveHintText(feature)}, then this feature flag would be{' '}
|
||||
{feature.strategies?.result ? 'TRUE' : 'FALSE'} with strategies
|
||||
evaluated like this:{' '}
|
||||
</StyledAlert>
|
||||
<StyledListWrapper sx={{ p: 2.5 }}>
|
||||
<PlaygroundResultStrategyLists
|
||||
strategies={enabledStrategies || []}
|
||||
input={input}
|
||||
titlePrefix={showDisabledStrategies ? 'Enabled' : ''}
|
||||
/>
|
||||
</StyledListWrapper>
|
||||
<ConditionallyRender
|
||||
condition={showDisabledStrategies}
|
||||
show={
|
||||
<StyledListWrapper sx={{ p: 2.5 }}>
|
||||
<PlaygroundResultStrategyLists
|
||||
strategies={disabledStrategies}
|
||||
input={input}
|
||||
titlePrefix={'Disabled'}
|
||||
infoText={
|
||||
'Disabled strategies are not evaluated for the overall result.'
|
||||
}
|
||||
/>
|
||||
</StyledListWrapper>
|
||||
}
|
||||
/>
|
||||
</StyledAlertWrapper>
|
||||
);
|
||||
};
|
@ -1,70 +0,0 @@
|
||||
import { useTheme } from '@mui/material';
|
||||
import { PlaygroundResultChip } from '../../../../PlaygroundResultChip/LegacyPlaygroundResultChip';
|
||||
import type {
|
||||
PlaygroundStrategySchema,
|
||||
PlaygroundRequestSchema,
|
||||
} from 'openapi';
|
||||
import { StrategyExecution } from './PlaygroundStrategyExecution/LegacyStrategyExecution';
|
||||
import { objectId } from 'utils/objectId';
|
||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||
import { DisabledStrategyExecution } from './PlaygroundStrategyExecution/DisabledStrategyExecution';
|
||||
import { StrategyItemContainer } from 'component/common/StrategyItemContainer/LegacyStrategyItemContainer';
|
||||
|
||||
interface IFeatureStrategyItemProps {
|
||||
strategy: PlaygroundStrategySchema;
|
||||
index: number;
|
||||
input?: PlaygroundRequestSchema;
|
||||
}
|
||||
|
||||
export const FeatureStrategyItem = ({
|
||||
strategy,
|
||||
input,
|
||||
index,
|
||||
}: IFeatureStrategyItemProps) => {
|
||||
const { result } = strategy;
|
||||
const theme = useTheme();
|
||||
const label =
|
||||
result.evaluationStatus === 'incomplete' ||
|
||||
result.evaluationStatus === 'unevaluated'
|
||||
? 'Unevaluated'
|
||||
: result.enabled
|
||||
? 'True'
|
||||
: 'False';
|
||||
|
||||
return (
|
||||
<StrategyItemContainer
|
||||
style={{
|
||||
borderColor:
|
||||
result.enabled && result.evaluationStatus === 'complete'
|
||||
? theme.palette.success.main
|
||||
: 'none',
|
||||
}}
|
||||
strategy={{ ...strategy, id: `${objectId(strategy)}` }}
|
||||
orderNumber={index + 1}
|
||||
actions={
|
||||
<PlaygroundResultChip
|
||||
showIcon={false}
|
||||
enabled={result.enabled}
|
||||
label={label}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<ConditionallyRender
|
||||
condition={Boolean(strategy.disabled)}
|
||||
show={
|
||||
<DisabledStrategyExecution
|
||||
strategyResult={strategy}
|
||||
input={input}
|
||||
/>
|
||||
}
|
||||
elseShow={
|
||||
<StrategyExecution
|
||||
strategyResult={strategy}
|
||||
input={input}
|
||||
percentageFill={theme.palette.background.elevation2}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</StrategyItemContainer>
|
||||
);
|
||||
};
|
@ -75,7 +75,7 @@ export const PlaygroundStrategyExecution: FC<StrategyExecutionProps> = ({
|
||||
<ConstraintListItem key={index}>{param}</ConstraintListItem>
|
||||
)),
|
||||
name === 'default' && (
|
||||
<StyledBoxSummary sx={{ width: '100%' }}>
|
||||
<StyledBoxSummary sx={{ width: '100%' }} key='default-on'>
|
||||
The standard strategy is <Badge color='success'>ON</Badge> for
|
||||
all users.
|
||||
</StyledBoxSummary>
|
||||
|
@ -1,12 +1,4 @@
|
||||
// deprecated; remove with 'flagOverviewRedesign' flag
|
||||
import type { VFC } from 'react';
|
||||
import { useTheme } from '@mui/material';
|
||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||
import { ReactComponent as FeatureEnabledIcon } from 'assets/icons/isenabled-true.svg';
|
||||
import { ReactComponent as FeatureDisabledIcon } from 'assets/icons/isenabled-false.svg';
|
||||
import WarningOutlined from '@mui/icons-material/WarningOutlined';
|
||||
import { Badge } from 'component/common/Badge/Badge';
|
||||
import { useUiFlag } from 'hooks/useUiFlag';
|
||||
import { PlaygroundResultChip as NewPlaygroundResultChip } from './PlaygroundResultChip';
|
||||
|
||||
interface IResultChipProps {
|
||||
@ -16,74 +8,17 @@ interface IResultChipProps {
|
||||
showIcon?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated remove with 'flagOverviewRedesign' flag. This pollutes a lot of places in the codebase 😞
|
||||
*/
|
||||
export const PlaygroundResultChip: VFC<IResultChipProps> = ({
|
||||
enabled,
|
||||
label,
|
||||
showIcon = true,
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
const flagOverviewRedesign = useUiFlag('flagOverviewRedesign');
|
||||
if (flagOverviewRedesign) {
|
||||
return (
|
||||
<NewPlaygroundResultChip
|
||||
enabled={enabled}
|
||||
label={label}
|
||||
showIcon={showIcon}
|
||||
/>
|
||||
);
|
||||
}
|
||||
const icon = (
|
||||
<ConditionallyRender
|
||||
condition={enabled === 'unknown' || enabled === 'unevaluated'}
|
||||
show={<WarningOutlined color={'warning'} fontSize='inherit' />}
|
||||
elseShow={
|
||||
<ConditionallyRender
|
||||
condition={typeof enabled === 'boolean' && Boolean(enabled)}
|
||||
show={
|
||||
<FeatureEnabledIcon
|
||||
aria-hidden
|
||||
color={theme.palette.success.main}
|
||||
strokeWidth='0.25'
|
||||
/>
|
||||
}
|
||||
elseShow={
|
||||
<FeatureDisabledIcon
|
||||
aria-hidden
|
||||
color={theme.palette.error.main}
|
||||
strokeWidth='0.25'
|
||||
/>
|
||||
}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<ConditionallyRender
|
||||
condition={enabled === 'unknown' || enabled === 'unevaluated'}
|
||||
show={
|
||||
<Badge icon={showIcon ? icon : undefined} color='warning'>
|
||||
{label}
|
||||
</Badge>
|
||||
}
|
||||
elseShow={
|
||||
<ConditionallyRender
|
||||
condition={typeof enabled === 'boolean' && Boolean(enabled)}
|
||||
show={
|
||||
<Badge
|
||||
color='success'
|
||||
icon={showIcon ? icon : undefined}
|
||||
>
|
||||
{label}
|
||||
</Badge>
|
||||
}
|
||||
elseShow={
|
||||
<Badge color='error' icon={showIcon ? icon : undefined}>
|
||||
{label}
|
||||
</Badge>
|
||||
}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
}) => (
|
||||
<NewPlaygroundResultChip
|
||||
enabled={enabled}
|
||||
label={label}
|
||||
showIcon={showIcon}
|
||||
/>
|
||||
);
|
||||
|
@ -46,8 +46,8 @@ export const FeatureToggleSwitch: VFC<FeatureToggleSwitchProps> = ({
|
||||
<PermissionSwitch
|
||||
tooltip={
|
||||
isChecked
|
||||
? `Disable feature in ${environmentName}`
|
||||
: `Enable feature in ${environmentName}`
|
||||
? `Disable flag in ${environmentName}`
|
||||
: `Enable flag in ${environmentName}`
|
||||
}
|
||||
checked={value}
|
||||
environmentId={environmentName}
|
||||
|
@ -10,14 +10,12 @@ import {
|
||||
UPDATE_PROJECT,
|
||||
} from 'component/providers/AccessProvider/permissions';
|
||||
import { Alert, styled } from '@mui/material';
|
||||
import LegacyProjectEnvironment from './ProjectEnvironment/LegacyProjectEnvironment';
|
||||
import { Route, Routes, useNavigate } from 'react-router-dom';
|
||||
import { SidebarModal } from 'component/common/SidebarModal/SidebarModal';
|
||||
import EditDefaultStrategy from './ProjectEnvironment/ProjectEnvironmentDefaultStrategy/EditDefaultStrategy';
|
||||
import useProjectOverview, {
|
||||
useProjectOverviewNameOrId,
|
||||
} from 'hooks/api/getters/useProjectOverview/useProjectOverview';
|
||||
import { useUiFlag } from 'hooks/useUiFlag';
|
||||
import { ProjectEnvironment } from './ProjectEnvironment/ProjectEnvironment';
|
||||
|
||||
const StyledAlert = styled(Alert)(({ theme }) => ({
|
||||
@ -30,7 +28,6 @@ export const ProjectDefaultStrategySettings = () => {
|
||||
const { project } = useProjectOverview(projectId);
|
||||
const navigate = useNavigate();
|
||||
usePageTitle(`Project default strategy configuration – ${projectName}`);
|
||||
const flagOverviewRedesign = useUiFlag('flagOverviewRedesign');
|
||||
|
||||
if (
|
||||
!hasAccess(
|
||||
@ -64,19 +61,12 @@ export const ProjectDefaultStrategySettings = () => {
|
||||
specific environment. These will be used when you enable a
|
||||
toggle environment that has no strategies defined
|
||||
</StyledAlert>
|
||||
{project?.environments.map((environment) =>
|
||||
flagOverviewRedesign ? (
|
||||
<ProjectEnvironment
|
||||
environment={environment}
|
||||
key={environment.environment}
|
||||
/>
|
||||
) : (
|
||||
<LegacyProjectEnvironment
|
||||
environment={environment}
|
||||
key={environment.environment}
|
||||
/>
|
||||
),
|
||||
)}
|
||||
{project?.environments.map((environment) => (
|
||||
<ProjectEnvironment
|
||||
environment={environment}
|
||||
key={environment.environment}
|
||||
/>
|
||||
))}
|
||||
</PageContent>
|
||||
<Routes>
|
||||
<Route
|
||||
|
@ -1,149 +0,0 @@
|
||||
import {
|
||||
Accordion,
|
||||
AccordionDetails,
|
||||
AccordionSummary,
|
||||
styled,
|
||||
useTheme,
|
||||
} from '@mui/material';
|
||||
import EnvironmentIcon from 'component/common/EnvironmentIcon/EnvironmentIcon';
|
||||
import StringTruncator from 'component/common/StringTruncator/StringTruncator';
|
||||
import { PROJECT_ENVIRONMENT_ACCORDION } from 'utils/testIds';
|
||||
import type { ProjectEnvironmentType } from '../../../../../../interfaces/environments';
|
||||
import LegacyProjectEnvironmentDefaultStrategy from './ProjectEnvironmentDefaultStrategy/LegacyProjectEnvironmentDefaultStrategy';
|
||||
|
||||
interface IProjectEnvironmentProps {
|
||||
environment: ProjectEnvironmentType;
|
||||
}
|
||||
|
||||
const StyledProjectEnvironmentOverview = styled('div', {
|
||||
shouldForwardProp: (prop) => prop !== 'enabled',
|
||||
})<{ enabled: boolean }>(({ theme, enabled }) => ({
|
||||
borderRadius: theme.shape.borderRadiusLarge,
|
||||
marginBottom: theme.spacing(2),
|
||||
backgroundColor: enabled
|
||||
? theme.palette.background.paper
|
||||
: theme.palette.envAccordion.disabled,
|
||||
}));
|
||||
|
||||
const StyledAccordion = styled(Accordion)({
|
||||
boxShadow: 'none',
|
||||
background: 'none',
|
||||
});
|
||||
|
||||
const StyledAccordionSummary = styled(AccordionSummary)(({ theme }) => ({
|
||||
boxShadow: 'none',
|
||||
padding: theme.spacing(2, 4),
|
||||
pointerEvents: 'none',
|
||||
[theme.breakpoints.down(400)]: {
|
||||
padding: theme.spacing(1, 2),
|
||||
},
|
||||
}));
|
||||
|
||||
const StyledAccordionDetails = styled(AccordionDetails)(({ theme }) => ({
|
||||
padding: theme.spacing(3),
|
||||
background: theme.palette.envAccordion.expanded,
|
||||
borderBottomLeftRadius: theme.shape.borderRadiusLarge,
|
||||
borderBottomRightRadius: theme.shape.borderRadiusLarge,
|
||||
boxShadow: theme.boxShadows.accordionFooter,
|
||||
|
||||
[theme.breakpoints.down('md')]: {
|
||||
padding: theme.spacing(2, 1),
|
||||
},
|
||||
}));
|
||||
|
||||
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 StyledHeader = styled('div', {
|
||||
shouldForwardProp: (prop) => prop !== 'enabled',
|
||||
})<{ enabled: boolean }>(({ theme, enabled }) => ({
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
flexDirection: 'column',
|
||||
color: enabled ? theme.palette.text.primary : theme.palette.text.secondary,
|
||||
}));
|
||||
|
||||
const StyledHeaderTitle = styled('div')(({ theme }) => ({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
fontWeight: 'bold',
|
||||
[theme.breakpoints.down(560)]: {
|
||||
flexDirection: 'column',
|
||||
textAlign: 'center',
|
||||
},
|
||||
}));
|
||||
|
||||
const StyledEnvironmentIcon = styled(EnvironmentIcon)(({ theme }) => ({
|
||||
[theme.breakpoints.down(560)]: {
|
||||
marginBottom: '0.5rem',
|
||||
},
|
||||
}));
|
||||
|
||||
const StyledStringTruncator = styled(StringTruncator)(({ theme }) => ({
|
||||
fontSize: theme.fontSizes.bodySize,
|
||||
fontWeight: theme.typography.fontWeightMedium,
|
||||
[theme.breakpoints.down(560)]: {
|
||||
textAlign: 'center',
|
||||
},
|
||||
}));
|
||||
|
||||
const ProjectEnvironment = ({ environment }: IProjectEnvironmentProps) => {
|
||||
const { environment: name } = environment;
|
||||
const description = `Default strategy configuration in the ${name} environment`;
|
||||
const theme = useTheme();
|
||||
const enabled = false;
|
||||
|
||||
return (
|
||||
<StyledProjectEnvironmentOverview enabled={false}>
|
||||
<StyledAccordion
|
||||
expanded={true}
|
||||
onChange={(e) => e.stopPropagation()}
|
||||
data-testid={`${PROJECT_ENVIRONMENT_ACCORDION}_${name}`}
|
||||
className={`environment-accordion ${
|
||||
enabled ? '' : 'accordion-disabled'
|
||||
}`}
|
||||
style={{
|
||||
outline: `2px solid ${theme.palette.divider}`,
|
||||
backgroundColor: theme.palette.background.paper,
|
||||
}}
|
||||
>
|
||||
<StyledAccordionSummary>
|
||||
<StyledHeader data-loading enabled={enabled}>
|
||||
<StyledHeaderTitle>
|
||||
<StyledEnvironmentIcon enabled />
|
||||
<div>
|
||||
<StyledStringTruncator
|
||||
text={name}
|
||||
maxWidth='100'
|
||||
maxLength={15}
|
||||
/>
|
||||
</div>
|
||||
</StyledHeaderTitle>
|
||||
</StyledHeader>
|
||||
</StyledAccordionSummary>
|
||||
|
||||
<StyledAccordionDetails>
|
||||
<StyledAccordionBody>
|
||||
<StyledAccordionBodyInnerContainer>
|
||||
<LegacyProjectEnvironmentDefaultStrategy
|
||||
environment={environment}
|
||||
description={description}
|
||||
/>
|
||||
</StyledAccordionBodyInnerContainer>
|
||||
</StyledAccordionBody>
|
||||
</StyledAccordionDetails>
|
||||
</StyledAccordion>
|
||||
</StyledProjectEnvironmentOverview>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProjectEnvironment;
|
@ -1,94 +0,0 @@
|
||||
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
|
||||
import PermissionIconButton from 'component/common/PermissionIconButton/PermissionIconButton';
|
||||
import { Link } from 'react-router-dom';
|
||||
import Edit from '@mui/icons-material/Edit';
|
||||
import { StrategyExecution } from 'component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/EnvironmentAccordionBody/StrategyDraggableItem/StrategyItem/StrategyExecution/StrategyExecution';
|
||||
import type { ProjectEnvironmentType } from 'interfaces/environments';
|
||||
import { useMemo } from 'react';
|
||||
import type { CreateFeatureStrategySchema } from 'openapi';
|
||||
import {
|
||||
PROJECT_DEFAULT_STRATEGY_WRITE,
|
||||
UPDATE_PROJECT,
|
||||
} from '@server/types/permissions';
|
||||
import { VariantsSplitPreview } from 'component/common/VariantsSplitPreview/VariantsSplitPreview';
|
||||
import { StrategyItemContainer } from 'component/common/StrategyItemContainer/LegacyStrategyItemContainer';
|
||||
|
||||
interface ProjectEnvironmentDefaultStrategyProps {
|
||||
environment: ProjectEnvironmentType;
|
||||
description: string;
|
||||
}
|
||||
|
||||
export const formatEditProjectEnvironmentStrategyPath = (
|
||||
projectId: string,
|
||||
environmentId: string,
|
||||
): string => {
|
||||
const params = new URLSearchParams({ environmentId });
|
||||
|
||||
return `/projects/${projectId}/settings/default-strategy/edit?${params}`;
|
||||
};
|
||||
|
||||
const DEFAULT_STRATEGY: CreateFeatureStrategySchema = {
|
||||
name: 'flexibleRollout',
|
||||
disabled: false,
|
||||
constraints: [],
|
||||
title: '',
|
||||
parameters: {
|
||||
rollout: '100',
|
||||
stickiness: 'default',
|
||||
groupId: '',
|
||||
},
|
||||
};
|
||||
|
||||
const ProjectEnvironmentDefaultStrategy = ({
|
||||
environment,
|
||||
description,
|
||||
}: ProjectEnvironmentDefaultStrategyProps) => {
|
||||
const projectId = useRequiredPathParam('projectId');
|
||||
const { environment: environmentId, defaultStrategy } = environment;
|
||||
|
||||
const editStrategyPath = formatEditProjectEnvironmentStrategyPath(
|
||||
projectId,
|
||||
environmentId,
|
||||
);
|
||||
|
||||
const strategy: CreateFeatureStrategySchema = useMemo(() => {
|
||||
return defaultStrategy ? defaultStrategy : DEFAULT_STRATEGY;
|
||||
}, [JSON.stringify(defaultStrategy)]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<StrategyItemContainer
|
||||
strategy={strategy as any}
|
||||
description={description}
|
||||
actions={
|
||||
<>
|
||||
<PermissionIconButton
|
||||
permission={[
|
||||
PROJECT_DEFAULT_STRATEGY_WRITE,
|
||||
UPDATE_PROJECT,
|
||||
]}
|
||||
environmentId={environmentId}
|
||||
projectId={projectId}
|
||||
component={Link}
|
||||
to={editStrategyPath}
|
||||
tooltipProps={{
|
||||
title: `Edit default strategy for "${environmentId}"`,
|
||||
}}
|
||||
data-testid={`STRATEGY_EDIT-${strategy?.name}`}
|
||||
>
|
||||
<Edit />
|
||||
</PermissionIconButton>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<StrategyExecution strategy={strategy} />
|
||||
|
||||
{strategy.variants && strategy.variants.length > 0 ? (
|
||||
<VariantsSplitPreview variants={strategy.variants} />
|
||||
) : null}
|
||||
</StrategyItemContainer>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProjectEnvironmentDefaultStrategy;
|
@ -1,528 +0,0 @@
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Card,
|
||||
Grid,
|
||||
Popover,
|
||||
styled,
|
||||
Accordion,
|
||||
AccordionSummary,
|
||||
AccordionDetails,
|
||||
IconButton,
|
||||
FormHelperText,
|
||||
} from '@mui/material';
|
||||
import Delete from '@mui/icons-material/DeleteOutlined';
|
||||
import type { IReleasePlanMilestoneStrategy } from 'interfaces/releasePlans';
|
||||
import { type DragEventHandler, type RefObject, useRef, useState } from 'react';
|
||||
import ExpandMore from '@mui/icons-material/ExpandMore';
|
||||
import { MilestoneCardName } from './MilestoneCardName';
|
||||
import { MilestoneStrategyMenuCards } from './MilestoneStrategyMenu/MilestoneStrategyMenuCards';
|
||||
import { MilestoneStrategyDraggableItem } from './MilestoneStrategyDraggableItem';
|
||||
import { SidebarModal } from 'component/common/SidebarModal/SidebarModal';
|
||||
import { ReleasePlanTemplateAddStrategyForm } from '../../MilestoneStrategy/ReleasePlanTemplateAddStrategyForm';
|
||||
import DragIndicator from '@mui/icons-material/DragIndicator';
|
||||
import { type OnMoveItem, useDragItem } from 'hooks/useDragItem';
|
||||
import type { IExtendedMilestonePayload } from 'component/releases/hooks/useTemplateForm';
|
||||
|
||||
const StyledMilestoneCard = styled(Card, {
|
||||
shouldForwardProp: (prop) => prop !== 'hasError',
|
||||
})<{ hasError: boolean }>(({ theme, hasError }) => ({
|
||||
marginTop: theme.spacing(2),
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'space-between',
|
||||
boxShadow: 'none',
|
||||
border: `1px solid ${hasError ? theme.palette.error.border : theme.palette.divider}`,
|
||||
borderRadius: theme.shape.borderRadiusMedium,
|
||||
[theme.breakpoints.down('sm')]: {
|
||||
justifyContent: 'center',
|
||||
},
|
||||
transition: 'background-color 0.2s ease-in-out',
|
||||
backgroundColor: theme.palette.background.default,
|
||||
}));
|
||||
|
||||
const StyledMilestoneCardBody = styled(Box)(({ theme }) => ({
|
||||
padding: theme.spacing(2, 2),
|
||||
}));
|
||||
|
||||
const StyledGridItem = styled(Grid)(({ theme }) => ({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
}));
|
||||
|
||||
const StyledAddStrategyButton = styled(Button)(({ theme }) => ({}));
|
||||
|
||||
const StyledAccordion = styled(Accordion)(({ theme }) => ({
|
||||
marginTop: theme.spacing(2),
|
||||
boxShadow: 'none',
|
||||
background: 'none',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'space-between',
|
||||
border: `1px solid ${theme.palette.divider}`,
|
||||
borderRadius: theme.shape.borderRadiusMedium,
|
||||
[theme.breakpoints.down('sm')]: {
|
||||
justifyContent: 'center',
|
||||
},
|
||||
backgroundColor: theme.palette.background.default,
|
||||
'&:before': {
|
||||
opacity: '0 !important',
|
||||
},
|
||||
'&.Mui-expanded': { marginTop: `${theme.spacing(2)} !important` },
|
||||
}));
|
||||
|
||||
const StyledAccordionSummary = styled(AccordionSummary)(({ theme }) => ({
|
||||
boxShadow: 'none',
|
||||
padding: theme.spacing(1.5, 2),
|
||||
borderRadius: theme.shape.borderRadiusMedium,
|
||||
[theme.breakpoints.down(400)]: {
|
||||
padding: theme.spacing(1, 2),
|
||||
},
|
||||
'&.Mui-focusVisible': {
|
||||
backgroundColor: theme.palette.background.paper,
|
||||
padding: theme.spacing(0.5, 2, 0.3, 2),
|
||||
},
|
||||
}));
|
||||
|
||||
const StyledAccordionDetails = styled(AccordionDetails)(({ theme }) => ({
|
||||
borderBottomLeftRadius: theme.shape.borderRadiusMedium,
|
||||
borderBottomRightRadius: theme.shape.borderRadiusMedium,
|
||||
padding: theme.spacing(0),
|
||||
[theme.breakpoints.down('md')]: {
|
||||
padding: theme.spacing(2, 1),
|
||||
},
|
||||
backgroundColor: theme.palette.neutral.light,
|
||||
}));
|
||||
|
||||
const StyledAccordionFooter = styled(Grid)(({ theme }) => ({
|
||||
padding: theme.spacing(2),
|
||||
paddingTop: 0,
|
||||
backgroundColor: theme.palette.background.default,
|
||||
borderRadius: theme.shape.borderRadiusMedium,
|
||||
}));
|
||||
|
||||
const StyledMilestoneActionGrid = styled(Grid)(({ theme }) => ({
|
||||
display: 'flex',
|
||||
justifyContent: 'flex-end',
|
||||
}));
|
||||
|
||||
const StyledIconButton = styled(IconButton)(({ theme }) => ({
|
||||
marginLeft: theme.spacing(1),
|
||||
color: theme.palette.primary.main,
|
||||
}));
|
||||
|
||||
const StyledDragIcon = styled(IconButton)(({ theme }) => ({
|
||||
padding: 0,
|
||||
cursor: 'grab',
|
||||
transition: 'color 0.2s ease-in-out',
|
||||
marginRight: theme.spacing(1),
|
||||
'& > svg': {
|
||||
color: 'action.active',
|
||||
},
|
||||
}));
|
||||
|
||||
export interface IMilestoneCardProps {
|
||||
milestone: IExtendedMilestonePayload;
|
||||
milestoneChanged: (milestone: IExtendedMilestonePayload) => void;
|
||||
errors: { [key: string]: string };
|
||||
clearErrors: () => void;
|
||||
removable: boolean;
|
||||
onDeleteMilestone: () => void;
|
||||
index: number;
|
||||
onMoveItem: OnMoveItem;
|
||||
}
|
||||
|
||||
export const MilestoneCard = ({
|
||||
milestone,
|
||||
milestoneChanged,
|
||||
errors,
|
||||
clearErrors,
|
||||
removable,
|
||||
onDeleteMilestone,
|
||||
index,
|
||||
onMoveItem,
|
||||
}: IMilestoneCardProps) => {
|
||||
const [anchor, setAnchor] = useState<Element>();
|
||||
const [dragItem, setDragItem] = useState<{
|
||||
id: string;
|
||||
index: number;
|
||||
height: number;
|
||||
} | null>(null);
|
||||
const [addUpdateStrategyOpen, setAddUpdateStrategyOpen] = useState(false);
|
||||
const [strategyModeEdit, setStrategyModeEdit] = useState(false);
|
||||
const [expanded, setExpanded] = useState(Boolean(milestone.startExpanded));
|
||||
const isPopoverOpen = Boolean(anchor);
|
||||
const popoverId = isPopoverOpen
|
||||
? 'MilestoneStrategyMenuPopover'
|
||||
: undefined;
|
||||
|
||||
const dragHandleRef = useRef(null);
|
||||
|
||||
const dragItemRef = useDragItem<HTMLTableRowElement>(
|
||||
index,
|
||||
onMoveItem,
|
||||
dragHandleRef,
|
||||
);
|
||||
|
||||
const dragHandle = (
|
||||
<StyledDragIcon ref={dragHandleRef} disableRipple size='small'>
|
||||
<DragIndicator titleAccess='Drag to reorder' />
|
||||
</StyledDragIcon>
|
||||
);
|
||||
|
||||
const onClose = () => {
|
||||
setAnchor(undefined);
|
||||
};
|
||||
|
||||
const [currentStrategy, setCurrentStrategy] = useState<
|
||||
Omit<IReleasePlanMilestoneStrategy, 'milestoneId'>
|
||||
>({
|
||||
name: 'flexibleRollout',
|
||||
parameters: { rollout: '50' },
|
||||
constraints: [],
|
||||
title: '',
|
||||
id: 'temp',
|
||||
});
|
||||
|
||||
const milestoneStrategyChanged = (
|
||||
strategy: Omit<IReleasePlanMilestoneStrategy, 'milestoneId'>,
|
||||
) => {
|
||||
const strategies = milestone.strategies || [];
|
||||
milestoneChanged({
|
||||
...milestone,
|
||||
strategies: [
|
||||
...strategies.map((strat) =>
|
||||
strat.id === strategy.id ? strategy : strat,
|
||||
),
|
||||
],
|
||||
});
|
||||
};
|
||||
|
||||
const milestoneStrategyAdded = (
|
||||
strategy: Omit<IReleasePlanMilestoneStrategy, 'milestoneId'>,
|
||||
) => {
|
||||
milestoneChanged({
|
||||
...milestone,
|
||||
strategies: [
|
||||
...(milestone.strategies || []),
|
||||
{
|
||||
...strategy,
|
||||
strategyName: strategy.strategyName,
|
||||
sortOrder: milestone.strategies?.length || 0,
|
||||
},
|
||||
],
|
||||
});
|
||||
};
|
||||
|
||||
const addUpdateStrategy = (
|
||||
strategy: Omit<IReleasePlanMilestoneStrategy, 'milestoneId'>,
|
||||
) => {
|
||||
const existingStrategy = milestone.strategies?.find(
|
||||
(strat) => strat.id === strategy.id,
|
||||
);
|
||||
if (existingStrategy) {
|
||||
milestoneStrategyChanged(strategy);
|
||||
} else {
|
||||
milestoneStrategyAdded(strategy);
|
||||
setExpanded(true);
|
||||
}
|
||||
setAddUpdateStrategyOpen(false);
|
||||
setStrategyModeEdit(false);
|
||||
setCurrentStrategy({
|
||||
name: 'flexibleRollout',
|
||||
parameters: { rollout: '50' },
|
||||
constraints: [],
|
||||
title: '',
|
||||
id: 'temp',
|
||||
});
|
||||
clearErrors();
|
||||
};
|
||||
|
||||
const openAddUpdateStrategyForm = (
|
||||
strategy: Omit<IReleasePlanMilestoneStrategy, 'milestoneId'>,
|
||||
editing: boolean,
|
||||
) => {
|
||||
setStrategyModeEdit(editing);
|
||||
setCurrentStrategy(strategy);
|
||||
setAddUpdateStrategyOpen(true);
|
||||
};
|
||||
|
||||
const onStrategyDragOver =
|
||||
(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 oldStrategies = milestone.strategies || [];
|
||||
const newStrategies = [...oldStrategies];
|
||||
const movedStrategy = newStrategies.splice(
|
||||
dragItem.index,
|
||||
1,
|
||||
)[0];
|
||||
newStrategies.splice(targetIndex, 0, movedStrategy);
|
||||
milestoneChanged({ ...milestone, strategies: newStrategies });
|
||||
setDragItem({
|
||||
...dragItem,
|
||||
index: targetIndex,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const onStrategyDragStartRef =
|
||||
(
|
||||
ref: RefObject<HTMLDivElement>,
|
||||
index: number,
|
||||
): DragEventHandler<HTMLButtonElement> =>
|
||||
(event) => {
|
||||
if (!ref.current || !milestone.strategies) {
|
||||
return;
|
||||
}
|
||||
|
||||
setDragItem({
|
||||
id: milestone.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 onStrategyDragEnd = () => {
|
||||
setDragItem(null);
|
||||
onReOrderStrategies();
|
||||
};
|
||||
|
||||
const onReOrderStrategies = () => {
|
||||
if (!milestone.strategies) {
|
||||
return;
|
||||
}
|
||||
const newStrategies = [...milestone.strategies];
|
||||
newStrategies.forEach((strategy, index) => {
|
||||
strategy.sortOrder = index;
|
||||
});
|
||||
milestoneChanged({ ...milestone, strategies: newStrategies });
|
||||
};
|
||||
|
||||
const milestoneStrategyDeleted = (strategyId: string) => {
|
||||
const strategies = milestone.strategies || [];
|
||||
milestoneChanged({
|
||||
...milestone,
|
||||
strategies: [
|
||||
...strategies.filter((strat) => strat.id !== strategyId),
|
||||
],
|
||||
});
|
||||
};
|
||||
|
||||
const milestoneNameChanged = (name: string) => {
|
||||
milestoneChanged({ ...milestone, name });
|
||||
};
|
||||
|
||||
if (!milestone.strategies || milestone.strategies.length === 0) {
|
||||
return (
|
||||
<>
|
||||
<StyledMilestoneCard
|
||||
hasError={
|
||||
Boolean(errors?.[milestone.id]) ||
|
||||
Boolean(errors?.[`${milestone.id}_name`])
|
||||
}
|
||||
ref={dragItemRef}
|
||||
>
|
||||
<StyledMilestoneCardBody>
|
||||
<Grid container>
|
||||
<StyledGridItem item xs={6} md={6}>
|
||||
{dragHandle}
|
||||
<MilestoneCardName
|
||||
milestone={milestone}
|
||||
errors={errors}
|
||||
clearErrors={clearErrors}
|
||||
milestoneNameChanged={milestoneNameChanged}
|
||||
/>
|
||||
</StyledGridItem>
|
||||
<StyledMilestoneActionGrid item xs={6} md={6}>
|
||||
<Button
|
||||
variant='outlined'
|
||||
color='primary'
|
||||
onClick={(ev) =>
|
||||
setAnchor(ev.currentTarget)
|
||||
}
|
||||
>
|
||||
Add strategy
|
||||
</Button>
|
||||
<StyledIconButton
|
||||
title='Remove milestone'
|
||||
onClick={onDeleteMilestone}
|
||||
disabled={!removable}
|
||||
>
|
||||
<Delete />
|
||||
</StyledIconButton>
|
||||
|
||||
<Popover
|
||||
id={popoverId}
|
||||
open={isPopoverOpen}
|
||||
anchorEl={anchor}
|
||||
onClose={onClose}
|
||||
onClick={onClose}
|
||||
PaperProps={{
|
||||
sx: (theme) => ({
|
||||
paddingBottom: theme.spacing(1),
|
||||
}),
|
||||
}}
|
||||
>
|
||||
<MilestoneStrategyMenuCards
|
||||
openEditAddStrategy={(strategy) => {
|
||||
openAddUpdateStrategyForm(
|
||||
strategy,
|
||||
false,
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</Popover>
|
||||
</StyledMilestoneActionGrid>
|
||||
</Grid>
|
||||
</StyledMilestoneCardBody>
|
||||
</StyledMilestoneCard>
|
||||
|
||||
<FormHelperText error={Boolean(errors?.[milestone.id])}>
|
||||
{errors?.[milestone.id]}
|
||||
</FormHelperText>
|
||||
|
||||
<SidebarModal
|
||||
label='Add strategy to template milestone'
|
||||
onClose={() => {
|
||||
setAddUpdateStrategyOpen(false);
|
||||
setStrategyModeEdit(false);
|
||||
}}
|
||||
open={addUpdateStrategyOpen}
|
||||
>
|
||||
<ReleasePlanTemplateAddStrategyForm
|
||||
strategy={currentStrategy}
|
||||
onAddUpdateStrategy={addUpdateStrategy}
|
||||
onCancel={() => {
|
||||
setAddUpdateStrategyOpen(false);
|
||||
setStrategyModeEdit(false);
|
||||
}}
|
||||
editMode={strategyModeEdit}
|
||||
/>
|
||||
</SidebarModal>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<StyledAccordion
|
||||
expanded={expanded}
|
||||
onChange={(e, change) => setExpanded(change)}
|
||||
>
|
||||
<StyledAccordionSummary
|
||||
expandIcon={<ExpandMore titleAccess='Toggle' />}
|
||||
ref={dragItemRef}
|
||||
>
|
||||
{dragHandle}
|
||||
<MilestoneCardName
|
||||
milestone={milestone}
|
||||
errors={errors}
|
||||
clearErrors={clearErrors}
|
||||
milestoneNameChanged={milestoneNameChanged}
|
||||
/>
|
||||
</StyledAccordionSummary>
|
||||
<StyledAccordionDetails>
|
||||
{milestone.strategies.map((strg, index) => (
|
||||
<div key={strg.id}>
|
||||
<MilestoneStrategyDraggableItem
|
||||
index={index}
|
||||
onDragEnd={onStrategyDragEnd}
|
||||
onDragStartRef={onStrategyDragStartRef}
|
||||
onDragOver={onStrategyDragOver(strg.id)}
|
||||
onDeleteClick={() =>
|
||||
milestoneStrategyDeleted(strg.id)
|
||||
}
|
||||
onEditClick={() => {
|
||||
openAddUpdateStrategyForm(strg, true);
|
||||
}}
|
||||
isDragging={dragItem?.id === strg.id}
|
||||
strategy={strg}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
<StyledAccordionFooter>
|
||||
<StyledAddStrategyButton
|
||||
variant='outlined'
|
||||
color='primary'
|
||||
onClick={(ev) => setAnchor(ev.currentTarget)}
|
||||
>
|
||||
Add strategy
|
||||
</StyledAddStrategyButton>
|
||||
<Button
|
||||
variant='text'
|
||||
color='primary'
|
||||
onClick={onDeleteMilestone}
|
||||
disabled={!removable}
|
||||
>
|
||||
<Delete /> Remove milestone
|
||||
</Button>
|
||||
<Popover
|
||||
id={popoverId}
|
||||
open={isPopoverOpen}
|
||||
anchorEl={anchor}
|
||||
onClose={onClose}
|
||||
onClick={onClose}
|
||||
PaperProps={{
|
||||
sx: (theme) => ({
|
||||
paddingBottom: theme.spacing(1),
|
||||
}),
|
||||
}}
|
||||
>
|
||||
<MilestoneStrategyMenuCards
|
||||
openEditAddStrategy={(strategy) => {
|
||||
openAddUpdateStrategyForm(strategy, false);
|
||||
}}
|
||||
/>
|
||||
</Popover>
|
||||
</StyledAccordionFooter>
|
||||
</StyledAccordionDetails>
|
||||
</StyledAccordion>
|
||||
|
||||
<FormHelperText error={Boolean(errors?.[milestone.id])}>
|
||||
{errors?.[milestone.id]}
|
||||
</FormHelperText>
|
||||
|
||||
<SidebarModal
|
||||
label='Add strategy to template milestone'
|
||||
onClose={() => {
|
||||
setAddUpdateStrategyOpen(false);
|
||||
setStrategyModeEdit(false);
|
||||
}}
|
||||
open={addUpdateStrategyOpen}
|
||||
>
|
||||
<ReleasePlanTemplateAddStrategyForm
|
||||
strategy={currentStrategy}
|
||||
onAddUpdateStrategy={addUpdateStrategy}
|
||||
onCancel={() => {
|
||||
setAddUpdateStrategyOpen(false);
|
||||
setStrategyModeEdit(false);
|
||||
}}
|
||||
editMode={strategyModeEdit}
|
||||
/>
|
||||
</SidebarModal>
|
||||
</>
|
||||
);
|
||||
};
|
@ -4,8 +4,6 @@ import Add from '@mui/icons-material/Add';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { useCallback } from 'react';
|
||||
import type { OnMoveItem } from 'hooks/useDragItem';
|
||||
import { MilestoneCard as LegacyMilestoneCard } from './MilestoneCard/LegacyMilestoneCard';
|
||||
import { useUiFlag } from 'hooks/useUiFlag';
|
||||
import { MilestoneCard } from './MilestoneCard/MilestoneCard';
|
||||
|
||||
interface IMilestoneListProps {
|
||||
@ -30,10 +28,9 @@ export const MilestoneList = ({
|
||||
clearErrors,
|
||||
milestoneChanged,
|
||||
}: IMilestoneListProps) => {
|
||||
const useNewMilestoneCard = useUiFlag('flagOverviewRedesign');
|
||||
const onMoveItem: OnMoveItem = useCallback(
|
||||
async ({ dragIndex, dropIndex, event, draggedElement }) => {
|
||||
if (useNewMilestoneCard && event.type === 'drop') {
|
||||
if (event.type === 'drop') {
|
||||
return; // the user has let go, we should leave the current sort order as it is currently visually displayed
|
||||
}
|
||||
|
||||
@ -83,12 +80,10 @@ export const MilestoneList = ({
|
||||
);
|
||||
};
|
||||
|
||||
const Card = useNewMilestoneCard ? MilestoneCard : LegacyMilestoneCard;
|
||||
|
||||
return (
|
||||
<>
|
||||
{milestones.map((milestone, index) => (
|
||||
<Card
|
||||
<MilestoneCard
|
||||
key={milestone.id}
|
||||
index={index}
|
||||
onMoveItem={onMoveItem}
|
||||
|
@ -1,45 +0,0 @@
|
||||
import { useGlobalLocalStorage } from './useGlobalLocalStorage';
|
||||
import { useState } from 'react';
|
||||
import { usePlausibleTracker } from 'hooks/usePlausibleTracker';
|
||||
|
||||
/**
|
||||
* @deprecated remove with `flagOverviewRedesign`
|
||||
*/
|
||||
export const useHiddenEnvironments = () => {
|
||||
const { trackEvent } = usePlausibleTracker();
|
||||
|
||||
const { value: globalStore, setValue: setGlobalStore } =
|
||||
useGlobalLocalStorage();
|
||||
const [hiddenEnvironments, setStoredHiddenEnvironments] = useState<
|
||||
Set<string>
|
||||
>(new Set(globalStore.hiddenEnvironments));
|
||||
|
||||
const setHiddenEnvironments = (environment: string) => {
|
||||
setGlobalStore((params) => {
|
||||
const hiddenEnvironments = new Set(
|
||||
Array.from(params.hiddenEnvironments || []),
|
||||
);
|
||||
if (hiddenEnvironments.has(environment)) {
|
||||
hiddenEnvironments.delete(environment);
|
||||
trackEvent('hidden_environment', {
|
||||
props: {
|
||||
eventType: `environment unhidden`,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
hiddenEnvironments.add(environment);
|
||||
}
|
||||
setStoredHiddenEnvironments(hiddenEnvironments);
|
||||
|
||||
return {
|
||||
...globalStore,
|
||||
hiddenEnvironments: [...hiddenEnvironments],
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
hiddenEnvironments,
|
||||
setHiddenEnvironments,
|
||||
};
|
||||
};
|
@ -87,7 +87,6 @@ export type UiFlags = {
|
||||
'enterprise-payg'?: boolean;
|
||||
productivityReportEmail?: boolean;
|
||||
showUserDeviceCount?: boolean;
|
||||
flagOverviewRedesign?: boolean;
|
||||
consumptionModel?: boolean;
|
||||
edgeObservability?: boolean;
|
||||
adminNavUI?: boolean;
|
||||
|
@ -1,6 +1,5 @@
|
||||
import { createTheme } from '@mui/material/styles';
|
||||
import { colors } from './colors';
|
||||
import { alpha } from '@mui/material';
|
||||
import { focusable } from 'themes/themeStyles';
|
||||
|
||||
export const baseTheme = {
|
||||
@ -517,17 +516,6 @@ export const lightTheme = createTheme({
|
||||
'&:first-of-type, &:last-of-type': {
|
||||
borderRadius: theme.shape.borderRadiusLarge,
|
||||
},
|
||||
// Environment accordion -- remove with `flagOverviewRedesign` flag
|
||||
'&.environment-accordion.Mui-expanded': {
|
||||
outline: `2px solid ${alpha(
|
||||
theme.palette.background.alternative,
|
||||
0.6,
|
||||
)}`,
|
||||
boxShadow: `0px 2px 8px ${alpha(
|
||||
theme.palette.primary.main,
|
||||
0.2,
|
||||
)}`,
|
||||
},
|
||||
}),
|
||||
},
|
||||
},
|
||||
|
@ -51,7 +51,6 @@ export type IFlagKey =
|
||||
| 'productivityReportEmail'
|
||||
| 'productivityReportUnsubscribers'
|
||||
| 'enterprise-payg'
|
||||
| 'flagOverviewRedesign'
|
||||
| 'showUserDeviceCount'
|
||||
| 'memorizeStats'
|
||||
| 'streaming'
|
||||
@ -262,10 +261,6 @@ const flags: IFlags = {
|
||||
process.env.UNLEASH_EXPERIMENTAL_SHOW_USER_DEVICE_COUNT,
|
||||
false,
|
||||
),
|
||||
flagOverviewRedesign: parseEnvVarBoolean(
|
||||
process.env.UNLEASH_EXPERIMENTAL_FLAG_OVERVIEW_REDESIGN,
|
||||
false,
|
||||
),
|
||||
streaming: parseEnvVarBoolean(
|
||||
process.env.UNLEASH_EXPERIMENTAL_STREAMING,
|
||||
false,
|
||||
|
@ -50,7 +50,6 @@ process.nextTick(async () => {
|
||||
webhookDomainLogging: true,
|
||||
releasePlans: false,
|
||||
showUserDeviceCount: true,
|
||||
flagOverviewRedesign: true,
|
||||
deltaApi: true,
|
||||
uniqueSdkTracking: true,
|
||||
filterExistingFlagNames: true,
|
||||
|
Loading…
Reference in New Issue
Block a user