mirror of
https://github.com/Unleash/unleash.git
synced 2025-06-04 01:18:20 +02:00
chore: release plans flow in flag environments (#8843)
https://linear.app/unleash/issue/2-2816/add-release-plan-to-feature-flag-from-release-template https://linear.app/unleash/issue/2-2818/list-release-plan-with-milestones-in-feature-flag-environment-section https://linear.app/unleash/issue/2-2819/removing-release-plan-from-feature Implements the release plan flow in the feature flag environment. You can now manage release plans in a feature flag environment by adding or removing them, as well as start milestones. https://github.com/user-attachments/assets/24db9db4-7c3a-463e-b48a-611358f2b212
This commit is contained in:
parent
26d96a0002
commit
14403d7836
@ -6,6 +6,7 @@ import type { IReleasePlanTemplate } from 'interfaces/releasePlans';
|
|||||||
import { useReleasePlansApi } from 'hooks/api/actions/useReleasePlansApi/useReleasePlansApi';
|
import { useReleasePlansApi } from 'hooks/api/actions/useReleasePlansApi/useReleasePlansApi';
|
||||||
import useToast from 'hooks/useToast';
|
import useToast from 'hooks/useToast';
|
||||||
import { formatUnknownError } from 'utils/formatUnknownError';
|
import { formatUnknownError } from 'utils/formatUnknownError';
|
||||||
|
import { useReleasePlans } from 'hooks/api/getters/useReleasePlans/useReleasePlans';
|
||||||
|
|
||||||
const StyledIcon = styled('div')(({ theme }) => ({
|
const StyledIcon = styled('div')(({ theme }) => ({
|
||||||
width: theme.spacing(4),
|
width: theme.spacing(4),
|
||||||
@ -60,6 +61,7 @@ export const FeatureReleasePlanCard = ({
|
|||||||
}: IFeatureReleasePlanCardProps) => {
|
}: IFeatureReleasePlanCardProps) => {
|
||||||
const Icon = getFeatureStrategyIcon('releasePlanTemplate');
|
const Icon = getFeatureStrategyIcon('releasePlanTemplate');
|
||||||
const { trackEvent } = usePlausibleTracker();
|
const { trackEvent } = usePlausibleTracker();
|
||||||
|
const { refetch } = useReleasePlans(projectId, featureId, environmentId);
|
||||||
const { addReleasePlanToFeature } = useReleasePlansApi();
|
const { addReleasePlanToFeature } = useReleasePlansApi();
|
||||||
const { setToastApiError, setToastData } = useToast();
|
const { setToastApiError, setToastData } = useToast();
|
||||||
|
|
||||||
@ -75,6 +77,7 @@ export const FeatureReleasePlanCard = ({
|
|||||||
type: 'success',
|
type: 'success',
|
||||||
title: 'Release plan added',
|
title: 'Release plan added',
|
||||||
});
|
});
|
||||||
|
refetch();
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
setToastApiError(formatUnknownError(error));
|
setToastApiError(formatUnknownError(error));
|
||||||
}
|
}
|
||||||
|
@ -21,6 +21,10 @@ import usePagination from 'hooks/usePagination';
|
|||||||
import type { IFeatureStrategy } from 'interfaces/strategy';
|
import type { IFeatureStrategy } from 'interfaces/strategy';
|
||||||
import { usePlausibleTracker } from 'hooks/usePlausibleTracker';
|
import { usePlausibleTracker } from 'hooks/usePlausibleTracker';
|
||||||
import { useUiFlag } from 'hooks/useUiFlag';
|
import { useUiFlag } from 'hooks/useUiFlag';
|
||||||
|
import { useReleasePlans } from 'hooks/api/getters/useReleasePlans/useReleasePlans';
|
||||||
|
import { ReleasePlan } from '../../../ReleasePlan/ReleasePlan';
|
||||||
|
import { Badge } from 'component/common/Badge/Badge';
|
||||||
|
import { SectionSeparator } from '../SectionSeparator/SectionSeparator';
|
||||||
|
|
||||||
interface IEnvironmentAccordionBodyProps {
|
interface IEnvironmentAccordionBodyProps {
|
||||||
isDisabled: boolean;
|
isDisabled: boolean;
|
||||||
@ -40,6 +44,14 @@ const StyledAccordionBodyInnerContainer = styled('div')(({ theme }) => ({
|
|||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
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 EnvironmentAccordionBody = ({
|
const EnvironmentAccordionBody = ({
|
||||||
featureEnvironment,
|
featureEnvironment,
|
||||||
isDisabled,
|
isDisabled,
|
||||||
@ -58,6 +70,11 @@ const EnvironmentAccordionBody = ({
|
|||||||
const [strategies, setStrategies] = useState(
|
const [strategies, setStrategies] = useState(
|
||||||
featureEnvironment?.strategies || [],
|
featureEnvironment?.strategies || [],
|
||||||
);
|
);
|
||||||
|
const { releasePlans } = useReleasePlans(
|
||||||
|
projectId,
|
||||||
|
featureId,
|
||||||
|
featureEnvironment?.name,
|
||||||
|
);
|
||||||
const { trackEvent } = usePlausibleTracker();
|
const { trackEvent } = usePlausibleTracker();
|
||||||
|
|
||||||
const [dragItem, setDragItem] = useState<{
|
const [dragItem, setDragItem] = useState<{
|
||||||
@ -201,7 +218,10 @@ const EnvironmentAccordionBody = ({
|
|||||||
<StyledAccordionBody>
|
<StyledAccordionBody>
|
||||||
<StyledAccordionBodyInnerContainer>
|
<StyledAccordionBodyInnerContainer>
|
||||||
<ConditionallyRender
|
<ConditionallyRender
|
||||||
condition={strategies.length > 0 && isDisabled}
|
condition={
|
||||||
|
(releasePlans.length > 0 || strategies.length > 0) &&
|
||||||
|
isDisabled
|
||||||
|
}
|
||||||
show={() => (
|
show={() => (
|
||||||
<Alert severity='warning' sx={{ mb: 2 }}>
|
<Alert severity='warning' sx={{ mb: 2 }}>
|
||||||
This environment is disabled, which means that none
|
This environment is disabled, which means that none
|
||||||
@ -210,8 +230,23 @@ const EnvironmentAccordionBody = ({
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<ConditionallyRender
|
<ConditionallyRender
|
||||||
condition={strategies.length > 0}
|
condition={releasePlans.length > 0 || strategies.length > 0}
|
||||||
show={
|
show={
|
||||||
|
<>
|
||||||
|
{releasePlans.map((plan) => (
|
||||||
|
<ReleasePlan key={plan.id} plan={plan} />
|
||||||
|
))}
|
||||||
|
<ConditionallyRender
|
||||||
|
condition={
|
||||||
|
releasePlans.length > 0 &&
|
||||||
|
strategies.length > 0
|
||||||
|
}
|
||||||
|
show={
|
||||||
|
<SectionSeparator>
|
||||||
|
<StyledBadge>OR</StyledBadge>
|
||||||
|
</SectionSeparator>
|
||||||
|
}
|
||||||
|
/>
|
||||||
<ConditionallyRender
|
<ConditionallyRender
|
||||||
condition={
|
condition={
|
||||||
strategies.length < 50 ||
|
strategies.length < 50 ||
|
||||||
@ -234,7 +269,9 @@ const EnvironmentAccordionBody = ({
|
|||||||
dragItem?.id === strategy.id
|
dragItem?.id === strategy.id
|
||||||
}
|
}
|
||||||
onDragStartRef={onDragStartRef}
|
onDragStartRef={onDragStartRef}
|
||||||
onDragOver={onDragOver(strategy.id)}
|
onDragOver={onDragOver(
|
||||||
|
strategy.id,
|
||||||
|
)}
|
||||||
onDragEnd={onDragEnd}
|
onDragEnd={onDragEnd}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
@ -243,17 +280,20 @@ const EnvironmentAccordionBody = ({
|
|||||||
elseShow={
|
elseShow={
|
||||||
<>
|
<>
|
||||||
<Alert severity='error'>
|
<Alert severity='error'>
|
||||||
We noticed you're using a high number of
|
We noticed you're using a high
|
||||||
activation strategies. To ensure a more
|
number of activation strategies. To
|
||||||
targeted approach, consider leveraging
|
ensure a more targeted approach,
|
||||||
constraints or segments.
|
consider leveraging constraints or
|
||||||
|
segments.
|
||||||
</Alert>
|
</Alert>
|
||||||
<br />
|
<br />
|
||||||
{page.map((strategy, index) => (
|
{page.map((strategy, index) => (
|
||||||
<StrategyDraggableItem
|
<StrategyDraggableItem
|
||||||
key={strategy.id}
|
key={strategy.id}
|
||||||
strategy={strategy}
|
strategy={strategy}
|
||||||
index={index + pageIndex * pageSize}
|
index={
|
||||||
|
index + pageIndex * pageSize
|
||||||
|
}
|
||||||
environmentName={
|
environmentName={
|
||||||
featureEnvironment.name
|
featureEnvironment.name
|
||||||
}
|
}
|
||||||
@ -261,7 +301,9 @@ const EnvironmentAccordionBody = ({
|
|||||||
otherEnvironments
|
otherEnvironments
|
||||||
}
|
}
|
||||||
isDragging={false}
|
isDragging={false}
|
||||||
onDragStartRef={(() => {}) as any}
|
onDragStartRef={
|
||||||
|
(() => {}) as any
|
||||||
|
}
|
||||||
onDragOver={(() => {}) as any}
|
onDragOver={(() => {}) as any}
|
||||||
onDragEnd={(() => {}) as any}
|
onDragEnd={(() => {}) as any}
|
||||||
/>
|
/>
|
||||||
@ -278,6 +320,7 @@ const EnvironmentAccordionBody = ({
|
|||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
</>
|
||||||
}
|
}
|
||||||
elseShow={
|
elseShow={
|
||||||
<FeatureStrategyEmpty
|
<FeatureStrategyEmpty
|
||||||
|
@ -1,6 +1,12 @@
|
|||||||
import type { IFeatureEnvironmentMetrics } from 'interfaces/featureToggle';
|
import type { IFeatureEnvironmentMetrics } from 'interfaces/featureToggle';
|
||||||
import { FeatureMetricsStats } from 'component/feature/FeatureView/FeatureMetrics/FeatureMetricsStats/FeatureMetricsStats';
|
import { FeatureMetricsStats } from 'component/feature/FeatureView/FeatureMetrics/FeatureMetricsStats/FeatureMetricsStats';
|
||||||
import { SectionSeparator } from '../SectionSeparator/SectionSeparator';
|
import { SectionSeparator } from '../SectionSeparator/SectionSeparator';
|
||||||
|
import { styled } from '@mui/material';
|
||||||
|
|
||||||
|
const StyledLabel = styled('span')(({ theme }) => ({
|
||||||
|
background: theme.palette.envAccordion.expanded,
|
||||||
|
padding: theme.spacing(0, 2),
|
||||||
|
}));
|
||||||
|
|
||||||
interface IEnvironmentFooterProps {
|
interface IEnvironmentFooterProps {
|
||||||
environmentMetric?: IFeatureEnvironmentMetrics;
|
environmentMetric?: IFeatureEnvironmentMetrics;
|
||||||
@ -15,7 +21,9 @@ export const EnvironmentFooter = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<SectionSeparator>Feature flag exposure</SectionSeparator>
|
<SectionSeparator>
|
||||||
|
<StyledLabel>Feature flag exposure</StyledLabel>
|
||||||
|
</SectionSeparator>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<FeatureMetricsStats
|
<FeatureMetricsStats
|
||||||
|
@ -23,7 +23,6 @@ const SeparatorContent = styled('span')(({ theme }) => ({
|
|||||||
fontSize: theme.fontSizes.bodySize,
|
fontSize: theme.fontSizes.bodySize,
|
||||||
textAlign: 'center',
|
textAlign: 'center',
|
||||||
padding: '0 1rem',
|
padding: '0 1rem',
|
||||||
background: theme.palette.envAccordion.expanded,
|
|
||||||
position: 'relative',
|
position: 'relative',
|
||||||
maxWidth: '80%',
|
maxWidth: '80%',
|
||||||
color: theme.palette.text.primary,
|
color: theme.palette.text.primary,
|
||||||
|
@ -22,6 +22,10 @@ import type { IFeatureStrategy } from 'interfaces/strategy';
|
|||||||
import { usePlausibleTracker } from 'hooks/usePlausibleTracker';
|
import { usePlausibleTracker } from 'hooks/usePlausibleTracker';
|
||||||
import { useUiFlag } from 'hooks/useUiFlag';
|
import { useUiFlag } from 'hooks/useUiFlag';
|
||||||
import isEqual from 'lodash/isEqual';
|
import isEqual from 'lodash/isEqual';
|
||||||
|
import { useReleasePlans } from 'hooks/api/getters/useReleasePlans/useReleasePlans';
|
||||||
|
import { ReleasePlan } from '../ReleasePlan/ReleasePlan';
|
||||||
|
import { SectionSeparator } from '../FeatureOverviewEnvironments/FeatureOverviewEnvironment/SectionSeparator/SectionSeparator';
|
||||||
|
import { Badge } from 'component/common/Badge/Badge';
|
||||||
|
|
||||||
interface IEnvironmentAccordionBodyProps {
|
interface IEnvironmentAccordionBodyProps {
|
||||||
isDisabled: boolean;
|
isDisabled: boolean;
|
||||||
@ -41,6 +45,14 @@ const StyledAccordionBodyInnerContainer = styled('div')(({ theme }) => ({
|
|||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
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,
|
||||||
|
}));
|
||||||
|
|
||||||
export const FeatureOverviewEnvironmentBody = ({
|
export const FeatureOverviewEnvironmentBody = ({
|
||||||
featureEnvironment,
|
featureEnvironment,
|
||||||
isDisabled,
|
isDisabled,
|
||||||
@ -59,6 +71,11 @@ export const FeatureOverviewEnvironmentBody = ({
|
|||||||
const [strategies, setStrategies] = useState(
|
const [strategies, setStrategies] = useState(
|
||||||
featureEnvironment?.strategies || [],
|
featureEnvironment?.strategies || [],
|
||||||
);
|
);
|
||||||
|
const { releasePlans } = useReleasePlans(
|
||||||
|
projectId,
|
||||||
|
featureId,
|
||||||
|
featureEnvironment?.name,
|
||||||
|
);
|
||||||
const { trackEvent } = usePlausibleTracker();
|
const { trackEvent } = usePlausibleTracker();
|
||||||
|
|
||||||
const [dragItem, setDragItem] = useState<{
|
const [dragItem, setDragItem] = useState<{
|
||||||
@ -215,7 +232,11 @@ export const FeatureOverviewEnvironmentBody = ({
|
|||||||
<StyledAccordionBody>
|
<StyledAccordionBody>
|
||||||
<StyledAccordionBodyInnerContainer>
|
<StyledAccordionBodyInnerContainer>
|
||||||
<ConditionallyRender
|
<ConditionallyRender
|
||||||
condition={strategiesToDisplay.length > 0 && isDisabled}
|
condition={
|
||||||
|
(releasePlans.length > 0 ||
|
||||||
|
strategiesToDisplay.length > 0) &&
|
||||||
|
isDisabled
|
||||||
|
}
|
||||||
show={() => (
|
show={() => (
|
||||||
<Alert severity='warning' sx={{ mb: 2 }}>
|
<Alert severity='warning' sx={{ mb: 2 }}>
|
||||||
This environment is disabled, which means that none
|
This environment is disabled, which means that none
|
||||||
@ -224,8 +245,26 @@ export const FeatureOverviewEnvironmentBody = ({
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<ConditionallyRender
|
<ConditionallyRender
|
||||||
condition={strategiesToDisplay.length > 0}
|
condition={
|
||||||
|
releasePlans.length > 0 ||
|
||||||
|
strategiesToDisplay.length > 0
|
||||||
|
}
|
||||||
show={
|
show={
|
||||||
|
<>
|
||||||
|
{releasePlans.map((plan) => (
|
||||||
|
<ReleasePlan key={plan.id} plan={plan} />
|
||||||
|
))}
|
||||||
|
<ConditionallyRender
|
||||||
|
condition={
|
||||||
|
releasePlans.length > 0 &&
|
||||||
|
strategies.length > 0
|
||||||
|
}
|
||||||
|
show={
|
||||||
|
<SectionSeparator>
|
||||||
|
<StyledBadge>OR</StyledBadge>
|
||||||
|
</SectionSeparator>
|
||||||
|
}
|
||||||
|
/>
|
||||||
<ConditionallyRender
|
<ConditionallyRender
|
||||||
condition={
|
condition={
|
||||||
strategiesToDisplay.length < 50 ||
|
strategiesToDisplay.length < 50 ||
|
||||||
@ -246,9 +285,12 @@ export const FeatureOverviewEnvironmentBody = ({
|
|||||||
otherEnvironments
|
otherEnvironments
|
||||||
}
|
}
|
||||||
isDragging={
|
isDragging={
|
||||||
dragItem?.id === strategy.id
|
dragItem?.id ===
|
||||||
|
strategy.id
|
||||||
|
}
|
||||||
|
onDragStartRef={
|
||||||
|
onDragStartRef
|
||||||
}
|
}
|
||||||
onDragStartRef={onDragStartRef}
|
|
||||||
onDragOver={onDragOver(
|
onDragOver={onDragOver(
|
||||||
strategy.id,
|
strategy.id,
|
||||||
)}
|
)}
|
||||||
@ -261,17 +303,20 @@ export const FeatureOverviewEnvironmentBody = ({
|
|||||||
elseShow={
|
elseShow={
|
||||||
<>
|
<>
|
||||||
<Alert severity='error'>
|
<Alert severity='error'>
|
||||||
We noticed you're using a high number of
|
We noticed you're using a high
|
||||||
activation strategies. To ensure a more
|
number of activation strategies. To
|
||||||
targeted approach, consider leveraging
|
ensure a more targeted approach,
|
||||||
constraints or segments.
|
consider leveraging constraints or
|
||||||
|
segments.
|
||||||
</Alert>
|
</Alert>
|
||||||
<br />
|
<br />
|
||||||
{page.map((strategy, index) => (
|
{page.map((strategy, index) => (
|
||||||
<StrategyDraggableItem
|
<StrategyDraggableItem
|
||||||
key={strategy.id}
|
key={strategy.id}
|
||||||
strategy={strategy}
|
strategy={strategy}
|
||||||
index={index + pageIndex * pageSize}
|
index={
|
||||||
|
index + pageIndex * pageSize
|
||||||
|
}
|
||||||
environmentName={
|
environmentName={
|
||||||
featureEnvironment.name
|
featureEnvironment.name
|
||||||
}
|
}
|
||||||
@ -279,7 +324,9 @@ export const FeatureOverviewEnvironmentBody = ({
|
|||||||
otherEnvironments
|
otherEnvironments
|
||||||
}
|
}
|
||||||
isDragging={false}
|
isDragging={false}
|
||||||
onDragStartRef={(() => {}) as any}
|
onDragStartRef={
|
||||||
|
(() => {}) as any
|
||||||
|
}
|
||||||
onDragOver={(() => {}) as any}
|
onDragOver={(() => {}) as any}
|
||||||
onDragEnd={(() => {}) as any}
|
onDragEnd={(() => {}) as any}
|
||||||
/>
|
/>
|
||||||
@ -296,6 +343,7 @@ export const FeatureOverviewEnvironmentBody = ({
|
|||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
</>
|
||||||
}
|
}
|
||||||
elseShow={
|
elseShow={
|
||||||
<FeatureStrategyEmpty
|
<FeatureStrategyEmpty
|
||||||
|
@ -0,0 +1,193 @@
|
|||||||
|
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';
|
||||||
|
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||||
|
|
||||||
|
const StyledContainer = styled('div', {
|
||||||
|
shouldForwardProp: (prop) => prop !== 'disabled',
|
||||||
|
})<{ disabled?: boolean }>(({ theme, disabled }) => ({
|
||||||
|
padding: theme.spacing(2),
|
||||||
|
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 !== 'disabled',
|
||||||
|
})<{ disabled?: boolean }>(({ theme, disabled }) => ({
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
color: disabled ? theme.palette.text.secondary : 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,
|
||||||
|
lineHeight: 0.5,
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ReleasePlan = ({ plan }: 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 [removeOpen, setRemoveOpen] = useState(false);
|
||||||
|
|
||||||
|
const onRemoveConfirm = async () => {
|
||||||
|
try {
|
||||||
|
await removeReleasePlanFromFeature(
|
||||||
|
projectId,
|
||||||
|
featureName,
|
||||||
|
environment,
|
||||||
|
id,
|
||||||
|
);
|
||||||
|
setToastData({
|
||||||
|
title: `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) => {
|
||||||
|
try {
|
||||||
|
await startReleasePlanMilestone(
|
||||||
|
projectId,
|
||||||
|
featureName,
|
||||||
|
environment,
|
||||||
|
id,
|
||||||
|
milestone.id,
|
||||||
|
);
|
||||||
|
setToastData({
|
||||||
|
title: `Milestone "${milestone.name}" has started`,
|
||||||
|
type: 'success',
|
||||||
|
});
|
||||||
|
refetch();
|
||||||
|
} catch (error: unknown) {
|
||||||
|
setToastApiError(formatUnknownError(error));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const disabled = !activeMilestoneId;
|
||||||
|
const activeIndex = milestones.findIndex(
|
||||||
|
(milestone) => milestone.id === activeMilestoneId,
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<StyledContainer disabled={disabled}>
|
||||||
|
<StyledHeader disabled={disabled}>
|
||||||
|
<StyledHeaderTitleContainer>
|
||||||
|
<StyledHeaderTitleLabel>
|
||||||
|
Release plan
|
||||||
|
</StyledHeaderTitleLabel>
|
||||||
|
<span>{name}</span>
|
||||||
|
<StyledHeaderDescription>
|
||||||
|
{description}
|
||||||
|
</StyledHeaderDescription>
|
||||||
|
</StyledHeaderTitleContainer>
|
||||||
|
<PermissionIconButton
|
||||||
|
onClick={() => setRemoveOpen(true)}
|
||||||
|
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
|
||||||
|
milestone={milestone}
|
||||||
|
status={
|
||||||
|
milestone.id === activeMilestoneId
|
||||||
|
? '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}
|
||||||
|
/>
|
||||||
|
</StyledContainer>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,146 @@
|
|||||||
|
import ExpandMore from '@mui/icons-material/ExpandMore';
|
||||||
|
import {
|
||||||
|
Accordion,
|
||||||
|
AccordionDetails,
|
||||||
|
AccordionSummary,
|
||||||
|
Link,
|
||||||
|
styled,
|
||||||
|
} from '@mui/material';
|
||||||
|
import PlayCircleIcon from '@mui/icons-material/PlayCircle';
|
||||||
|
import TripOriginIcon from '@mui/icons-material/TripOrigin';
|
||||||
|
import type { IReleasePlanMilestone } from 'interfaces/releasePlans';
|
||||||
|
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||||
|
import { ReleasePlanMilestoneStrategy } from './ReleasePlanMilestoneStrategy';
|
||||||
|
import { StrategySeparator } from 'component/common/StrategySeparator/StrategySeparator';
|
||||||
|
|
||||||
|
type MilestoneStatus = 'not-started' | 'active' | 'completed';
|
||||||
|
|
||||||
|
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',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
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 StyledStatus = styled('div', {
|
||||||
|
shouldForwardProp: (prop) => prop !== 'status',
|
||||||
|
})<{ status: MilestoneStatus }>(({ theme, status }) => ({
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: theme.spacing(1),
|
||||||
|
paddingRight: theme.spacing(1),
|
||||||
|
fontSize: theme.fontSizes.smallerBody,
|
||||||
|
borderRadius: theme.shape.borderRadiusMedium,
|
||||||
|
backgroundColor:
|
||||||
|
status === 'active' ? theme.palette.success.light : 'transparent',
|
||||||
|
color:
|
||||||
|
status === 'active'
|
||||||
|
? theme.palette.success.contrastText
|
||||||
|
: status === 'completed'
|
||||||
|
? theme.palette.text.secondary
|
||||||
|
: theme.palette.text.primary,
|
||||||
|
'& svg': {
|
||||||
|
color:
|
||||||
|
status === 'active'
|
||||||
|
? theme.palette.success.main
|
||||||
|
: status === 'completed'
|
||||||
|
? theme.palette.neutral.border
|
||||||
|
: theme.palette.primary.main,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
const StyledSecondaryLabel = styled('span')(({ theme }) => ({
|
||||||
|
color: theme.palette.text.secondary,
|
||||||
|
fontSize: theme.fontSizes.smallBody,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const StyledAccordionDetails = styled(AccordionDetails)(({ theme }) => ({
|
||||||
|
backgroundColor: theme.palette.envAccordion.expanded,
|
||||||
|
borderBottomLeftRadius: theme.shape.borderRadiusLarge,
|
||||||
|
borderBottomRightRadius: theme.shape.borderRadiusLarge,
|
||||||
|
}));
|
||||||
|
|
||||||
|
interface IReleasePlanMilestoneProps {
|
||||||
|
milestone: IReleasePlanMilestone;
|
||||||
|
status: MilestoneStatus;
|
||||||
|
onStartMilestone: (milestone: IReleasePlanMilestone) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ReleasePlanMilestone = ({
|
||||||
|
milestone,
|
||||||
|
status,
|
||||||
|
onStartMilestone,
|
||||||
|
}: IReleasePlanMilestoneProps) => {
|
||||||
|
const statusText =
|
||||||
|
status === 'active'
|
||||||
|
? 'Running'
|
||||||
|
: status === 'completed'
|
||||||
|
? 'Restart'
|
||||||
|
: 'Start';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<StyledAccordion status={status}>
|
||||||
|
<StyledAccordionSummary expandIcon={<ExpandMore />}>
|
||||||
|
<StyledTitleContainer>
|
||||||
|
<StyledTitle>{milestone.name}</StyledTitle>
|
||||||
|
<StyledStatus status={status}>
|
||||||
|
<ConditionallyRender
|
||||||
|
condition={status === 'active'}
|
||||||
|
show={<TripOriginIcon />}
|
||||||
|
elseShow={<PlayCircleIcon />}
|
||||||
|
/>
|
||||||
|
<ConditionallyRender
|
||||||
|
condition={status === 'active'}
|
||||||
|
show={<span>{statusText}</span>}
|
||||||
|
elseShow={
|
||||||
|
<Link
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
onStartMilestone(milestone);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{statusText}
|
||||||
|
</Link>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</StyledStatus>
|
||||||
|
</StyledTitleContainer>
|
||||||
|
<StyledSecondaryLabel>View 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>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,55 @@
|
|||||||
|
import { Box, styled } from '@mui/material';
|
||||||
|
import { StrategyExecution } from '../FeatureOverviewEnvironments/FeatureOverviewEnvironment/EnvironmentAccordionBody/StrategyDraggableItem/StrategyItem/StrategyExecution/StrategyExecution';
|
||||||
|
import SplitPreviewSlider from 'component/feature/StrategyTypes/SplitPreviewSlider/SplitPreviewSlider';
|
||||||
|
import {
|
||||||
|
formatStrategyName,
|
||||||
|
getFeatureStrategyIcon,
|
||||||
|
} from 'utils/strategyNames';
|
||||||
|
import type { IFeatureStrategy } from 'interfaces/strategy';
|
||||||
|
|
||||||
|
const StyledStrategy = styled('div')(({ theme }) => ({
|
||||||
|
background: theme.palette.background.paper,
|
||||||
|
border: `1px solid ${theme.palette.divider}`,
|
||||||
|
borderRadius: theme.shape.borderRadiusMedium,
|
||||||
|
padding: theme.spacing(2),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const StyledHeader = styled('div')(({ theme }) => ({
|
||||||
|
display: 'flex',
|
||||||
|
gap: theme.spacing(1),
|
||||||
|
alignItems: 'center',
|
||||||
|
color: theme.palette.text.primary,
|
||||||
|
'& > svg': {
|
||||||
|
fill: theme.palette.action.disabled,
|
||||||
|
},
|
||||||
|
marginBottom: theme.spacing(1),
|
||||||
|
}));
|
||||||
|
|
||||||
|
interface IReleasePlanMilestoneStrategyProps {
|
||||||
|
strategy: IFeatureStrategy;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ReleasePlanMilestoneStrategy = ({
|
||||||
|
strategy,
|
||||||
|
}: IReleasePlanMilestoneStrategyProps) => {
|
||||||
|
const Icon = getFeatureStrategyIcon(strategy.strategyName);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<StyledStrategy>
|
||||||
|
<StyledHeader>
|
||||||
|
<Icon />
|
||||||
|
{`${formatStrategyName(String(strategy.strategyName))}${strategy.title ? `: ${strategy.title}` : ''}`}
|
||||||
|
</StyledHeader>
|
||||||
|
<StrategyExecution strategy={strategy} />
|
||||||
|
{strategy.variants &&
|
||||||
|
strategy.variants.length > 0 &&
|
||||||
|
(strategy.disabled ? (
|
||||||
|
<Box sx={{ opacity: '0.5' }}>
|
||||||
|
<SplitPreviewSlider variants={strategy.variants} />
|
||||||
|
</Box>
|
||||||
|
) : (
|
||||||
|
<SplitPreviewSlider variants={strategy.variants} />
|
||||||
|
))}
|
||||||
|
</StyledStrategy>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,43 @@
|
|||||||
|
import { Alert } from '@mui/material';
|
||||||
|
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||||
|
import { Dialogue } from 'component/common/Dialogue/Dialogue';
|
||||||
|
import type { IReleasePlan } from 'interfaces/releasePlans';
|
||||||
|
|
||||||
|
interface IReleasePlanRemoveDialogProps {
|
||||||
|
plan: IReleasePlan;
|
||||||
|
open: boolean;
|
||||||
|
setOpen: React.Dispatch<React.SetStateAction<boolean>>;
|
||||||
|
onConfirm: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ReleasePlanRemoveDialog = ({
|
||||||
|
plan,
|
||||||
|
open,
|
||||||
|
setOpen,
|
||||||
|
onConfirm,
|
||||||
|
}: IReleasePlanRemoveDialogProps) => (
|
||||||
|
<Dialogue
|
||||||
|
title='Remove release plan?'
|
||||||
|
open={open}
|
||||||
|
primaryButtonText='Remove release plan'
|
||||||
|
secondaryButtonText='Cancel'
|
||||||
|
onClick={onConfirm}
|
||||||
|
onClose={() => setOpen(false)}
|
||||||
|
>
|
||||||
|
<ConditionallyRender
|
||||||
|
condition={Boolean(plan.activeMilestoneId)}
|
||||||
|
show={
|
||||||
|
<Alert severity='error' sx={{ mb: 2 }}>
|
||||||
|
This release plan currently has one active milestone.
|
||||||
|
Removing the release plan will change which users receive
|
||||||
|
access to the feature.
|
||||||
|
</Alert>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<p>
|
||||||
|
You are about to remove release plan <strong>{plan.name}</strong>{' '}
|
||||||
|
from <strong>{plan.featureName}</strong> in{' '}
|
||||||
|
<strong>{plan.environment}</strong>
|
||||||
|
</p>
|
||||||
|
</Dialogue>
|
||||||
|
);
|
@ -58,7 +58,7 @@ interface IMilestoneCardProps {
|
|||||||
milestoneNameChanged: (milestoneId: string, name: string) => void;
|
milestoneNameChanged: (milestoneId: string, name: string) => void;
|
||||||
showAddStrategyDialog: (
|
showAddStrategyDialog: (
|
||||||
milestoneId: string,
|
milestoneId: string,
|
||||||
strategy: IReleasePlanMilestoneStrategy,
|
strategy: Omit<IReleasePlanMilestoneStrategy, 'milestoneId'>,
|
||||||
) => void;
|
) => void;
|
||||||
errors: { [key: string]: string };
|
errors: { [key: string]: string };
|
||||||
clearErrors: () => void;
|
clearErrors: () => void;
|
||||||
@ -84,7 +84,7 @@ export const MilestoneCard = ({
|
|||||||
|
|
||||||
const onSelectStrategy = (
|
const onSelectStrategy = (
|
||||||
milestoneId: string,
|
milestoneId: string,
|
||||||
strategy: IReleasePlanMilestoneStrategy,
|
strategy: Omit<IReleasePlanMilestoneStrategy, 'milestoneId'>,
|
||||||
) => {
|
) => {
|
||||||
showAddStrategyDialog(milestone.id, strategy);
|
showAddStrategyDialog(milestone.id, strategy);
|
||||||
};
|
};
|
||||||
|
@ -15,7 +15,7 @@ interface IMilestoneListProps {
|
|||||||
>;
|
>;
|
||||||
openAddStrategyForm: (
|
openAddStrategyForm: (
|
||||||
milestoneId: string,
|
milestoneId: string,
|
||||||
strategy: IReleasePlanMilestoneStrategy,
|
strategy: Omit<IReleasePlanMilestoneStrategy, 'milestoneId'>,
|
||||||
) => void;
|
) => void;
|
||||||
errors: { [key: string]: string };
|
errors: { [key: string]: string };
|
||||||
clearErrors: () => void;
|
clearErrors: () => void;
|
||||||
|
@ -51,7 +51,9 @@ const StyledCard = styled('div')(({ theme }) => ({
|
|||||||
|
|
||||||
interface IMilestoneStrategyMenuCardProps {
|
interface IMilestoneStrategyMenuCardProps {
|
||||||
strategy: IStrategy;
|
strategy: IStrategy;
|
||||||
onClick: (strategy: IReleasePlanMilestoneStrategy) => void;
|
onClick: (
|
||||||
|
strategy: Omit<IReleasePlanMilestoneStrategy, 'milestoneId'>,
|
||||||
|
) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const MilestoneStrategyMenuCard = ({
|
export const MilestoneStrategyMenuCard = ({
|
||||||
|
@ -12,7 +12,7 @@ interface IMilestoneStrategyMenuCardsProps {
|
|||||||
milestoneId: string;
|
milestoneId: string;
|
||||||
openAddStrategy: (
|
openAddStrategy: (
|
||||||
milestoneId: string,
|
milestoneId: string,
|
||||||
strategy: IReleasePlanMilestoneStrategy,
|
strategy: Omit<IReleasePlanMilestoneStrategy, 'milestoneId'>,
|
||||||
) => void;
|
) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -26,7 +26,9 @@ export const MilestoneStrategyMenuCards = ({
|
|||||||
(strategy) => !strategy.deprecated && !strategy.editable,
|
(strategy) => !strategy.deprecated && !strategy.editable,
|
||||||
);
|
);
|
||||||
|
|
||||||
const onClick = (strategy: IReleasePlanMilestoneStrategy) => {
|
const onClick = (
|
||||||
|
strategy: Omit<IReleasePlanMilestoneStrategy, 'milestoneId'>,
|
||||||
|
) => {
|
||||||
openAddStrategy(milestoneId, strategy);
|
openAddStrategy(milestoneId, strategy);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -66,10 +66,10 @@ const StyledTargetingHeader = styled('div')(({ theme }) => ({
|
|||||||
interface IReleasePlanTemplateAddStrategyFormProps {
|
interface IReleasePlanTemplateAddStrategyFormProps {
|
||||||
milestoneId: string | undefined;
|
milestoneId: string | undefined;
|
||||||
onCancel: () => void;
|
onCancel: () => void;
|
||||||
strategy: IReleasePlanMilestoneStrategy;
|
strategy: Omit<IReleasePlanMilestoneStrategy, 'milestoneId'>;
|
||||||
onAddStrategy: (
|
onAddStrategy: (
|
||||||
milestoneId: string,
|
milestoneId: string,
|
||||||
strategy: IReleasePlanMilestoneStrategy,
|
strategy: Omit<IReleasePlanMilestoneStrategy, 'milestoneId'>,
|
||||||
) => void;
|
) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -135,7 +135,7 @@ export const ReleasePlanTemplateAddStrategyForm = ({
|
|||||||
{activeTab === 0 && (
|
{activeTab === 0 && (
|
||||||
<>
|
<>
|
||||||
<MilestoneStrategyTitle
|
<MilestoneStrategyTitle
|
||||||
title={addStrategy.title}
|
title={addStrategy.title || ''}
|
||||||
setTitle={(title) =>
|
setTitle={(title) =>
|
||||||
updateParameter('title', title)
|
updateParameter('title', title)
|
||||||
}
|
}
|
||||||
|
@ -62,7 +62,9 @@ export const TemplateForm: React.FC<ITemplateFormProps> = ({
|
|||||||
const [activeMilestoneId, setActiveMilestoneId] = useState<
|
const [activeMilestoneId, setActiveMilestoneId] = useState<
|
||||||
string | undefined
|
string | undefined
|
||||||
>();
|
>();
|
||||||
const [strategy, setStrategy] = useState<IReleasePlanMilestoneStrategy>({
|
const [strategy, setStrategy] = useState<
|
||||||
|
Omit<IReleasePlanMilestoneStrategy, 'milestoneId'>
|
||||||
|
>({
|
||||||
name: 'flexibleRollout',
|
name: 'flexibleRollout',
|
||||||
parameters: { rollout: '50' },
|
parameters: { rollout: '50' },
|
||||||
constraints: [],
|
constraints: [],
|
||||||
@ -71,7 +73,7 @@ export const TemplateForm: React.FC<ITemplateFormProps> = ({
|
|||||||
});
|
});
|
||||||
const openAddStrategyForm = (
|
const openAddStrategyForm = (
|
||||||
milestoneId: string,
|
milestoneId: string,
|
||||||
strategy: IReleasePlanMilestoneStrategy,
|
strategy: Omit<IReleasePlanMilestoneStrategy, 'milestoneId'>,
|
||||||
) => {
|
) => {
|
||||||
setActiveMilestoneId(milestoneId);
|
setActiveMilestoneId(milestoneId);
|
||||||
setStrategy(strategy);
|
setStrategy(strategy);
|
||||||
@ -80,7 +82,7 @@ export const TemplateForm: React.FC<ITemplateFormProps> = ({
|
|||||||
|
|
||||||
const addStrategy = (
|
const addStrategy = (
|
||||||
milestoneId: string,
|
milestoneId: string,
|
||||||
strategy: IReleasePlanMilestoneStrategy,
|
strategy: Omit<IReleasePlanMilestoneStrategy, 'milestoneId'>,
|
||||||
) => {
|
) => {
|
||||||
setMilestones((prev) =>
|
setMilestones((prev) =>
|
||||||
prev.map((milestone, i) =>
|
prev.map((milestone, i) =>
|
||||||
|
@ -25,7 +25,36 @@ export const useReleasePlansApi = () => {
|
|||||||
await makeRequest(req.caller, req.id);
|
await makeRequest(req.caller, req.id);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const removeReleasePlanFromFeature = async (
|
||||||
|
projectId: string,
|
||||||
|
featureName: string,
|
||||||
|
environment: string,
|
||||||
|
releasePlanId: string,
|
||||||
|
): Promise<void> => {
|
||||||
|
const requestId = 'removeReleasePlanFromFeature';
|
||||||
|
const path = `api/admin/projects/${projectId}/features/${featureName}/environments/${environment}/release_plans/${releasePlanId}`;
|
||||||
|
const req = createRequest(path, { method: 'DELETE' }, requestId);
|
||||||
|
|
||||||
|
await makeRequest(req.caller, req.id);
|
||||||
|
};
|
||||||
|
|
||||||
|
const startReleasePlanMilestone = async (
|
||||||
|
projectId: string,
|
||||||
|
featureName: string,
|
||||||
|
environment: string,
|
||||||
|
releasePlanId: string,
|
||||||
|
milestoneId: string,
|
||||||
|
): Promise<void> => {
|
||||||
|
const requestId = 'startReleasePlanMilestone';
|
||||||
|
const path = `api/admin/projects/${projectId}/features/${featureName}/environments/${environment}/release_plans/${releasePlanId}/milestones/${milestoneId}/start`;
|
||||||
|
const req = createRequest(path, { method: 'POST' }, requestId);
|
||||||
|
|
||||||
|
await makeRequest(req.caller, req.id);
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
addReleasePlanToFeature,
|
addReleasePlanToFeature,
|
||||||
|
removeReleasePlanFromFeature,
|
||||||
|
startReleasePlanMilestone,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -0,0 +1,43 @@
|
|||||||
|
import { useMemo } from 'react';
|
||||||
|
import useUiConfig from '../useUiConfig/useUiConfig';
|
||||||
|
import { formatApiPath } from 'utils/formatPath';
|
||||||
|
import handleErrorResponses from '../httpErrorResponseHandler';
|
||||||
|
import { useConditionalSWR } from '../useConditionalSWR/useConditionalSWR';
|
||||||
|
import { useUiFlag } from 'hooks/useUiFlag';
|
||||||
|
import type { IReleasePlan } from 'interfaces/releasePlans';
|
||||||
|
|
||||||
|
const DEFAULT_DATA: IReleasePlan[] = [];
|
||||||
|
|
||||||
|
export const useReleasePlans = (
|
||||||
|
projectId: string,
|
||||||
|
featureName: string,
|
||||||
|
environment?: string,
|
||||||
|
) => {
|
||||||
|
const { isEnterprise } = useUiConfig();
|
||||||
|
const releasePlansEnabled = useUiFlag('releasePlans');
|
||||||
|
|
||||||
|
const { data, error, mutate } = useConditionalSWR<IReleasePlan[]>(
|
||||||
|
isEnterprise() && releasePlansEnabled && Boolean(environment),
|
||||||
|
DEFAULT_DATA,
|
||||||
|
formatApiPath(
|
||||||
|
`api/admin/projects/${projectId}/features/${featureName}/environments/${environment}/release_plans`,
|
||||||
|
),
|
||||||
|
fetcher,
|
||||||
|
);
|
||||||
|
|
||||||
|
return useMemo(
|
||||||
|
() => ({
|
||||||
|
releasePlans: data ?? [],
|
||||||
|
loading: !error && !data,
|
||||||
|
refetch: () => mutate(),
|
||||||
|
error,
|
||||||
|
}),
|
||||||
|
[data, error, mutate],
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetcher = (path: string) => {
|
||||||
|
return fetch(path)
|
||||||
|
.then(handleErrorResponses('Release plans'))
|
||||||
|
.then((res) => res.json());
|
||||||
|
};
|
@ -1,5 +1,4 @@
|
|||||||
import type { IFeatureVariant } from './featureToggle';
|
import type { IFeatureStrategy } from './strategy';
|
||||||
import type { IConstraint, IFeatureStrategyParameters } from './strategy';
|
|
||||||
|
|
||||||
export interface IReleasePlanTemplate {
|
export interface IReleasePlanTemplate {
|
||||||
id: string;
|
id: string;
|
||||||
@ -18,19 +17,27 @@ export interface IReleasePlanTemplate {
|
|||||||
milestones: IReleasePlanMilestonePayload[];
|
milestones: IReleasePlanMilestonePayload[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IReleasePlanMilestoneStrategy {
|
export interface IReleasePlan {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
title: string;
|
description: string;
|
||||||
disabled?: boolean;
|
createdAt: string;
|
||||||
constraints: IConstraint[];
|
createdByUserId: number;
|
||||||
parameters: IFeatureStrategyParameters;
|
activeMilestoneId?: string;
|
||||||
variants?: IFeatureVariant[];
|
featureName: string;
|
||||||
|
environment: string;
|
||||||
|
milestones: IReleasePlanMilestone[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IReleasePlanMilestone {
|
export interface IReleasePlanMilestone {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
|
releasePlanDefinitionId: string;
|
||||||
|
strategies: IReleasePlanMilestoneStrategy[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IReleasePlanMilestoneStrategy extends IFeatureStrategy {
|
||||||
|
milestoneId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IReleasePlanTemplatePayload {
|
export interface IReleasePlanTemplatePayload {
|
||||||
|
@ -20,7 +20,7 @@ const RolloutSvgIcon: FC = (props) => (
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
export const getFeatureStrategyIcon = (strategyName: string) => {
|
export const getFeatureStrategyIcon = (strategyName?: string) => {
|
||||||
switch (strategyName) {
|
switch (strategyName) {
|
||||||
case 'default':
|
case 'default':
|
||||||
return PowerSettingsNewIcon;
|
return PowerSettingsNewIcon;
|
||||||
|
Loading…
Reference in New Issue
Block a user