mirror of
https://github.com/Unleash/unleash.git
synced 2025-09-19 17:52:45 +02:00
chore: add quick filters to add strategy modal (#10641)
https://linear.app/unleash/issue/2-3868/add-strategy-filters-at-the-top Adds quick filters to our "add strategy" modal. `FeatureStrategyMenuCards.tsx` was getting increasingly complex, so this includes some refactoring. My quick filters implementation was generic enough that I added `QuickFilters.tsx` as a common component, maybe we can reuse it in the future. <img width="991" height="663" alt="image" src="https://github.com/user-attachments/assets/352b6d2d-c975-4cb1-9799-163dfb153ccb" /> <img width="985" height="358" alt="image" src="https://github.com/user-attachments/assets/a7d20dab-2774-409f-8940-f0d1a980b819" />
This commit is contained in:
parent
0ea006f72c
commit
a5adac5d8d
54
frontend/src/component/common/QuickFilters/QuickFilters.tsx
Normal file
54
frontend/src/component/common/QuickFilters/QuickFilters.tsx
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
import { Box, Chip, styled } from '@mui/material';
|
||||||
|
|
||||||
|
const StyledChip = styled(Chip, {
|
||||||
|
shouldForwardProp: (prop) => prop !== 'isActive',
|
||||||
|
})<{
|
||||||
|
isActive?: boolean;
|
||||||
|
}>(({ theme, isActive = false }) => ({
|
||||||
|
borderRadius: theme.shape.borderRadius,
|
||||||
|
padding: theme.spacing(0.5),
|
||||||
|
fontSize: theme.fontSizes.smallerBody,
|
||||||
|
height: 'auto',
|
||||||
|
...(isActive && {
|
||||||
|
backgroundColor: theme.palette.secondary.light,
|
||||||
|
fontWeight: 'bold',
|
||||||
|
borderColor: theme.palette.primary.main,
|
||||||
|
color: theme.palette.primary.main,
|
||||||
|
}),
|
||||||
|
':focus-visible': {
|
||||||
|
outline: `1px solid ${theme.palette.primary.main}`,
|
||||||
|
borderColor: theme.palette.primary.main,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
const StyledContainer = styled(Box)(({ theme }) => ({
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
flexWrap: 'wrap',
|
||||||
|
gap: theme.spacing(1),
|
||||||
|
}));
|
||||||
|
|
||||||
|
interface IQuickFiltersProps<T> {
|
||||||
|
filters: Array<{ label: string; value: T }>;
|
||||||
|
value: T;
|
||||||
|
onChange: (value: T) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const QuickFilters = <T extends string | null>({
|
||||||
|
filters,
|
||||||
|
value: currentValue,
|
||||||
|
onChange,
|
||||||
|
}: IQuickFiltersProps<T>) => (
|
||||||
|
<StyledContainer>
|
||||||
|
{filters.map(({ label, value }) => (
|
||||||
|
<StyledChip
|
||||||
|
key={label}
|
||||||
|
data-loading
|
||||||
|
label={label}
|
||||||
|
variant='outlined'
|
||||||
|
isActive={value === currentValue}
|
||||||
|
onClick={() => onChange(value)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</StyledContainer>
|
||||||
|
);
|
@ -251,7 +251,6 @@ export const FeatureStrategyMenu = ({
|
|||||||
projectId={projectId}
|
projectId={projectId}
|
||||||
featureId={featureId}
|
featureId={featureId}
|
||||||
environmentId={environmentId}
|
environmentId={environmentId}
|
||||||
onlyReleasePlans={onlyReleasePlans}
|
|
||||||
onAddReleasePlan={(template) => {
|
onAddReleasePlan={(template) => {
|
||||||
setSelectedTemplate(template);
|
setSelectedTemplate(template);
|
||||||
addReleasePlan(template);
|
addReleasePlan(template);
|
||||||
|
@ -1,18 +1,30 @@
|
|||||||
import { styled, Typography, Box, IconButton, Button } from '@mui/material';
|
import { styled, Typography, Box, IconButton } from '@mui/material';
|
||||||
import { useStrategies } from 'hooks/api/getters/useStrategies/useStrategies';
|
import { useStrategies } from 'hooks/api/getters/useStrategies/useStrategies';
|
||||||
import { FeatureStrategyMenuCard } from '../FeatureStrategyMenuCard/FeatureStrategyMenuCard.tsx';
|
import { FeatureStrategyMenuCard } from '../FeatureStrategyMenuCard/FeatureStrategyMenuCard.tsx';
|
||||||
import { useReleasePlanTemplates } from 'hooks/api/getters/useReleasePlanTemplates/useReleasePlanTemplates';
|
|
||||||
import { FeatureReleasePlanCard } from '../FeatureReleasePlanCard/FeatureReleasePlanCard.tsx';
|
|
||||||
import type { IReleasePlanTemplate } from 'interfaces/releasePlans';
|
import type { IReleasePlanTemplate } from 'interfaces/releasePlans';
|
||||||
import { Link as RouterLink } from 'react-router-dom';
|
import { Link as RouterLink } from 'react-router-dom';
|
||||||
import CloseIcon from '@mui/icons-material/Close';
|
import CloseIcon from '@mui/icons-material/Close';
|
||||||
import FactCheckOutlinedIcon from '@mui/icons-material/FactCheckOutlined';
|
|
||||||
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig.ts';
|
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig.ts';
|
||||||
import { HelpIcon } from 'component/common/HelpIcon/HelpIcon.tsx';
|
import { HelpIcon } from 'component/common/HelpIcon/HelpIcon.tsx';
|
||||||
import useProjectOverview from 'hooks/api/getters/useProjectOverview/useProjectOverview.ts';
|
import useProjectOverview from 'hooks/api/getters/useProjectOverview/useProjectOverview.ts';
|
||||||
import { useState } from 'react';
|
import { useMemo, useState } from 'react';
|
||||||
|
import {
|
||||||
|
FeatureStrategyMenuCardsSection,
|
||||||
|
StyledStrategyModalSectionHeader,
|
||||||
|
} from './FeatureStrategyMenuCardsSection.tsx';
|
||||||
|
import { FeatureStrategyMenuCardsReleaseTemplates } from './FeatureStrategyMenuCardsReleaseTemplates.tsx';
|
||||||
|
import { QuickFilters } from 'component/common/QuickFilters/QuickFilters.tsx';
|
||||||
|
|
||||||
const RELEASE_TEMPLATE_DISPLAY_LIMIT = 5;
|
const FILTERS = [
|
||||||
|
{ label: 'All', value: null },
|
||||||
|
{ label: 'Project default', value: 'default' },
|
||||||
|
{ label: 'Standard strategies', value: 'standard' },
|
||||||
|
{ label: 'Release templates', value: 'releaseTemplates' },
|
||||||
|
{ label: 'Advanced strategies', value: 'advanced' },
|
||||||
|
{ label: 'Custom strategies', value: 'custom' },
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
export type StrategyFilterValue = (typeof FILTERS)[number]['value'];
|
||||||
|
|
||||||
const StyledContainer = styled(Box)(() => ({
|
const StyledContainer = styled(Box)(() => ({
|
||||||
width: '100%',
|
width: '100%',
|
||||||
@ -31,13 +43,6 @@ const StyledScrollableContent = styled(Box)(({ theme }) => ({
|
|||||||
gap: theme.spacing(5),
|
gap: theme.spacing(5),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const StyledGrid = styled(Box)(({ theme }) => ({
|
|
||||||
display: 'grid',
|
|
||||||
gridTemplateColumns: 'repeat(3, 1fr)',
|
|
||||||
gap: theme.spacing(2),
|
|
||||||
width: '100%',
|
|
||||||
}));
|
|
||||||
|
|
||||||
const StyledHeader = styled(Box)(({ theme }) => ({
|
const StyledHeader = styled(Box)(({ theme }) => ({
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
justifyContent: 'space-between',
|
justifyContent: 'space-between',
|
||||||
@ -45,47 +50,9 @@ const StyledHeader = styled(Box)(({ theme }) => ({
|
|||||||
padding: theme.spacing(4, 4, 2, 4),
|
padding: theme.spacing(4, 4, 2, 4),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const StyledSectionHeader = styled(Box)(({ theme }) => ({
|
const StyledFiltersContainer = styled(Box)(({ theme }) => ({
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
padding: theme.spacing(0, 4, 5, 4),
|
||||||
gap: theme.spacing(0.5),
|
|
||||||
marginBottom: theme.spacing(0.5),
|
|
||||||
width: '100%',
|
|
||||||
}));
|
|
||||||
|
|
||||||
const StyledIcon = styled('span')(({ theme }) => ({
|
|
||||||
width: theme.spacing(3),
|
|
||||||
'& > svg': {
|
|
||||||
fill: theme.palette.primary.main,
|
|
||||||
width: theme.spacing(2.25),
|
|
||||||
height: theme.spacing(2.25),
|
|
||||||
},
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
}));
|
|
||||||
|
|
||||||
const StyledNoTemplatesContainer = styled(Box)(({ theme }) => ({
|
|
||||||
display: 'flex',
|
|
||||||
flexDirection: 'column',
|
|
||||||
alignItems: 'flex-start',
|
|
||||||
justifyContent: 'flex-start',
|
|
||||||
backgroundColor: theme.palette.neutral.light,
|
|
||||||
borderRadius: theme.shape.borderRadiusMedium,
|
|
||||||
padding: theme.spacing(3),
|
|
||||||
width: 'auto',
|
|
||||||
}));
|
|
||||||
|
|
||||||
const StyledNoTemplatesTitle = styled(Typography)(({ theme }) => ({
|
|
||||||
fontSize: theme.typography.caption.fontSize,
|
|
||||||
fontWeight: theme.typography.fontWeightBold,
|
|
||||||
marginBottom: theme.spacing(1),
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
}));
|
|
||||||
|
|
||||||
const StyledNoTemplatesDescription = styled(Typography)(({ theme }) => ({
|
|
||||||
fontSize: theme.typography.caption.fontSize,
|
|
||||||
color: theme.palette.text.secondary,
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const StyledLink = styled(RouterLink)({
|
const StyledLink = styled(RouterLink)({
|
||||||
@ -95,18 +62,10 @@ const StyledLink = styled(RouterLink)({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const StyledViewAllTemplates = styled(Box)({
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center',
|
|
||||||
height: '100%',
|
|
||||||
});
|
|
||||||
|
|
||||||
interface IFeatureStrategyMenuCardsProps {
|
interface IFeatureStrategyMenuCardsProps {
|
||||||
projectId: string;
|
projectId: string;
|
||||||
featureId: string;
|
featureId: string;
|
||||||
environmentId: string;
|
environmentId: string;
|
||||||
onlyReleasePlans: boolean;
|
|
||||||
onAddReleasePlan: (template: IReleasePlanTemplate) => void;
|
onAddReleasePlan: (template: IReleasePlanTemplate) => void;
|
||||||
onReviewReleasePlan: (template: IReleasePlanTemplate) => void;
|
onReviewReleasePlan: (template: IReleasePlanTemplate) => void;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
@ -123,10 +82,9 @@ export const FeatureStrategyMenuCards = ({
|
|||||||
const { isEnterprise } = useUiConfig();
|
const { isEnterprise } = useUiConfig();
|
||||||
|
|
||||||
const { strategies } = useStrategies();
|
const { strategies } = useStrategies();
|
||||||
const { templates } = useReleasePlanTemplates();
|
|
||||||
const { project } = useProjectOverview(projectId);
|
const { project } = useProjectOverview(projectId);
|
||||||
|
|
||||||
const [seeAllReleaseTemplates, setSeeAllReleaseTemplates] = useState(false);
|
const [filter, setFilter] = useState<StrategyFilterValue>(null);
|
||||||
|
|
||||||
const activeStrategies = strategies.filter(
|
const activeStrategies = strategies.filter(
|
||||||
(strategy) => !strategy.deprecated,
|
(strategy) => !strategy.deprecated,
|
||||||
@ -159,70 +117,19 @@ export const FeatureStrategyMenuCards = ({
|
|||||||
'This is the default strategy defined for this environment in the project',
|
'This is the default strategy defined for this environment in the project',
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderReleasePlanTemplates = () => {
|
const availableFilters = useMemo(
|
||||||
if (!isEnterprise()) {
|
() =>
|
||||||
return null;
|
FILTERS.filter(({ value }) => {
|
||||||
}
|
if (value === 'releaseTemplates') return isEnterprise();
|
||||||
|
if (value === 'advanced') return advancedStrategies.length > 0;
|
||||||
|
if (value === 'custom') return customStrategies.length > 0;
|
||||||
|
return true;
|
||||||
|
}),
|
||||||
|
[isEnterprise, advancedStrategies.length, customStrategies.length],
|
||||||
|
);
|
||||||
|
|
||||||
const slicedTemplates = seeAllReleaseTemplates
|
const shouldRender = (key: StrategyFilterValue) => {
|
||||||
? templates
|
return filter === null || filter === key;
|
||||||
: templates.slice(0, RELEASE_TEMPLATE_DISPLAY_LIMIT);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Box>
|
|
||||||
<StyledSectionHeader>
|
|
||||||
<Typography color='inherit' variant='body2'>
|
|
||||||
Release templates
|
|
||||||
</Typography>
|
|
||||||
</StyledSectionHeader>
|
|
||||||
{!templates.length ? (
|
|
||||||
<StyledNoTemplatesContainer>
|
|
||||||
<StyledNoTemplatesTitle>
|
|
||||||
<StyledIcon>
|
|
||||||
<FactCheckOutlinedIcon />
|
|
||||||
</StyledIcon>
|
|
||||||
Create your own release templates
|
|
||||||
</StyledNoTemplatesTitle>
|
|
||||||
<StyledNoTemplatesDescription>
|
|
||||||
Standardize your rollouts and save time by reusing
|
|
||||||
predefined strategies. Find release templates in the
|
|
||||||
side menu under{' '}
|
|
||||||
<StyledLink to='/release-templates'>
|
|
||||||
Configure > Release templates
|
|
||||||
</StyledLink>
|
|
||||||
</StyledNoTemplatesDescription>
|
|
||||||
</StyledNoTemplatesContainer>
|
|
||||||
) : (
|
|
||||||
<StyledGrid>
|
|
||||||
{slicedTemplates.map((template) => (
|
|
||||||
<FeatureReleasePlanCard
|
|
||||||
key={template.id}
|
|
||||||
template={template}
|
|
||||||
onClick={() => onAddReleasePlan(template)}
|
|
||||||
onPreviewClick={() =>
|
|
||||||
onReviewReleasePlan(template)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
{slicedTemplates.length < templates.length &&
|
|
||||||
templates.length >
|
|
||||||
RELEASE_TEMPLATE_DISPLAY_LIMIT && (
|
|
||||||
<StyledViewAllTemplates>
|
|
||||||
<Button
|
|
||||||
variant='text'
|
|
||||||
size='small'
|
|
||||||
onClick={() =>
|
|
||||||
setSeeAllReleaseTemplates(true)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
View all available templates
|
|
||||||
</Button>
|
|
||||||
</StyledViewAllTemplates>
|
|
||||||
)}
|
|
||||||
</StyledGrid>
|
|
||||||
)}
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -238,45 +145,87 @@ export const FeatureStrategyMenuCards = ({
|
|||||||
<CloseIcon fontSize='small' />
|
<CloseIcon fontSize='small' />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</StyledHeader>
|
</StyledHeader>
|
||||||
|
<StyledFiltersContainer>
|
||||||
|
<QuickFilters
|
||||||
|
filters={availableFilters}
|
||||||
|
value={filter}
|
||||||
|
onChange={setFilter}
|
||||||
|
/>
|
||||||
|
</StyledFiltersContainer>
|
||||||
<StyledScrollableContent>
|
<StyledScrollableContent>
|
||||||
<Box>
|
{(shouldRender('default') || shouldRender('standard')) && (
|
||||||
<StyledGrid>
|
<Box>
|
||||||
<StyledSectionHeader>
|
<FeatureStrategyMenuCardsSection>
|
||||||
<Typography color='inherit' variant='body2'>
|
{shouldRender('default') && (
|
||||||
Project default
|
<StyledStrategyModalSectionHeader>
|
||||||
</Typography>
|
<Typography color='inherit' variant='body2'>
|
||||||
<HelpIcon
|
Project default
|
||||||
htmlTooltip
|
</Typography>
|
||||||
tooltip={
|
<HelpIcon
|
||||||
<>
|
htmlTooltip
|
||||||
This is set per project, per
|
tooltip={
|
||||||
environment, and can be configured{' '}
|
<>
|
||||||
<StyledLink
|
This is set per project, per
|
||||||
to={`/projects/${projectId}/settings/default-strategy`}
|
environment, and can be
|
||||||
>
|
configured{' '}
|
||||||
here
|
<StyledLink
|
||||||
</StyledLink>
|
to={`/projects/${projectId}/settings/default-strategy`}
|
||||||
</>
|
>
|
||||||
}
|
here
|
||||||
size='16px'
|
</StyledLink>
|
||||||
/>
|
</>
|
||||||
</StyledSectionHeader>
|
}
|
||||||
<StyledSectionHeader>
|
size='16px'
|
||||||
<Typography color='inherit' variant='body2'>
|
/>
|
||||||
Standard strategies
|
</StyledStrategyModalSectionHeader>
|
||||||
</Typography>
|
)}
|
||||||
</StyledSectionHeader>
|
{shouldRender('standard') && (
|
||||||
</StyledGrid>
|
<StyledStrategyModalSectionHeader>
|
||||||
<StyledGrid>
|
<Typography color='inherit' variant='body2'>
|
||||||
<FeatureStrategyMenuCard
|
Standard strategies
|
||||||
projectId={projectId}
|
</Typography>
|
||||||
featureId={featureId}
|
</StyledStrategyModalSectionHeader>
|
||||||
environmentId={environmentId}
|
)}
|
||||||
strategy={defaultStrategy}
|
</FeatureStrategyMenuCardsSection>
|
||||||
defaultStrategy
|
<FeatureStrategyMenuCardsSection>
|
||||||
onClose={onClose}
|
{shouldRender('default') && (
|
||||||
/>
|
<FeatureStrategyMenuCard
|
||||||
{standardStrategies.map((strategy) => (
|
projectId={projectId}
|
||||||
|
featureId={featureId}
|
||||||
|
environmentId={environmentId}
|
||||||
|
strategy={defaultStrategy}
|
||||||
|
defaultStrategy
|
||||||
|
onClose={onClose}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{shouldRender('standard') && (
|
||||||
|
<>
|
||||||
|
{standardStrategies.map((strategy) => (
|
||||||
|
<FeatureStrategyMenuCard
|
||||||
|
key={strategy.name}
|
||||||
|
projectId={projectId}
|
||||||
|
featureId={featureId}
|
||||||
|
environmentId={environmentId}
|
||||||
|
strategy={strategy}
|
||||||
|
onClose={onClose}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</FeatureStrategyMenuCardsSection>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
{shouldRender('releaseTemplates') && (
|
||||||
|
<FeatureStrategyMenuCardsReleaseTemplates
|
||||||
|
onAddReleasePlan={onAddReleasePlan}
|
||||||
|
onReviewReleasePlan={onReviewReleasePlan}
|
||||||
|
filter={filter}
|
||||||
|
setFilter={setFilter}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{advancedStrategies.length > 0 && shouldRender('advanced') && (
|
||||||
|
<FeatureStrategyMenuCardsSection title='Advanced strategies'>
|
||||||
|
{advancedStrategies.map((strategy) => (
|
||||||
<FeatureStrategyMenuCard
|
<FeatureStrategyMenuCard
|
||||||
key={strategy.name}
|
key={strategy.name}
|
||||||
projectId={projectId}
|
projectId={projectId}
|
||||||
@ -286,50 +235,21 @@ export const FeatureStrategyMenuCards = ({
|
|||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</StyledGrid>
|
</FeatureStrategyMenuCardsSection>
|
||||||
</Box>
|
|
||||||
{renderReleasePlanTemplates()}
|
|
||||||
{advancedStrategies.length > 0 && (
|
|
||||||
<Box>
|
|
||||||
<StyledSectionHeader>
|
|
||||||
<Typography color='inherit' variant='body2'>
|
|
||||||
Advanced strategies
|
|
||||||
</Typography>
|
|
||||||
</StyledSectionHeader>
|
|
||||||
<StyledGrid>
|
|
||||||
{advancedStrategies.map((strategy) => (
|
|
||||||
<FeatureStrategyMenuCard
|
|
||||||
key={strategy.name}
|
|
||||||
projectId={projectId}
|
|
||||||
featureId={featureId}
|
|
||||||
environmentId={environmentId}
|
|
||||||
strategy={strategy}
|
|
||||||
onClose={onClose}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</StyledGrid>
|
|
||||||
</Box>
|
|
||||||
)}
|
)}
|
||||||
{customStrategies.length > 0 && (
|
{customStrategies.length > 0 && shouldRender('custom') && (
|
||||||
<Box>
|
<FeatureStrategyMenuCardsSection title='Custom strategies'>
|
||||||
<StyledSectionHeader>
|
{customStrategies.map((strategy) => (
|
||||||
<Typography color='inherit' variant='body2'>
|
<FeatureStrategyMenuCard
|
||||||
Custom strategies
|
key={strategy.name}
|
||||||
</Typography>
|
projectId={projectId}
|
||||||
</StyledSectionHeader>
|
featureId={featureId}
|
||||||
<StyledGrid>
|
environmentId={environmentId}
|
||||||
{customStrategies.map((strategy) => (
|
strategy={strategy}
|
||||||
<FeatureStrategyMenuCard
|
onClose={onClose}
|
||||||
key={strategy.name}
|
/>
|
||||||
projectId={projectId}
|
))}
|
||||||
featureId={featureId}
|
</FeatureStrategyMenuCardsSection>
|
||||||
environmentId={environmentId}
|
|
||||||
strategy={strategy}
|
|
||||||
onClose={onClose}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</StyledGrid>
|
|
||||||
</Box>
|
|
||||||
)}
|
)}
|
||||||
</StyledScrollableContent>
|
</StyledScrollableContent>
|
||||||
</StyledContainer>
|
</StyledContainer>
|
||||||
|
@ -0,0 +1,143 @@
|
|||||||
|
import { useReleasePlanTemplates } from 'hooks/api/getters/useReleasePlanTemplates/useReleasePlanTemplates';
|
||||||
|
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
|
||||||
|
import FactCheckOutlinedIcon from '@mui/icons-material/FactCheckOutlined';
|
||||||
|
import { FeatureReleasePlanCard } from '../FeatureReleasePlanCard/FeatureReleasePlanCard.tsx';
|
||||||
|
import type { IReleasePlanTemplate } from 'interfaces/releasePlans.ts';
|
||||||
|
import { Box, Button, styled, Typography } from '@mui/material';
|
||||||
|
import type { StrategyFilterValue } from './FeatureStrategyMenuCards.tsx';
|
||||||
|
import type { Dispatch, SetStateAction } from 'react';
|
||||||
|
import { Link as RouterLink } from 'react-router-dom';
|
||||||
|
import {
|
||||||
|
FeatureStrategyMenuCardsSection,
|
||||||
|
StyledStrategyModalSectionHeader,
|
||||||
|
} from './FeatureStrategyMenuCardsSection.tsx';
|
||||||
|
|
||||||
|
const RELEASE_TEMPLATE_DISPLAY_LIMIT = 5;
|
||||||
|
|
||||||
|
const StyledIcon = styled('span')(({ theme }) => ({
|
||||||
|
width: theme.spacing(3),
|
||||||
|
'& > svg': {
|
||||||
|
fill: theme.palette.primary.main,
|
||||||
|
width: theme.spacing(2.25),
|
||||||
|
height: theme.spacing(2.25),
|
||||||
|
},
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
}));
|
||||||
|
|
||||||
|
const StyledNoTemplatesContainer = styled(Box)(({ theme }) => ({
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
alignItems: 'flex-start',
|
||||||
|
justifyContent: 'flex-start',
|
||||||
|
backgroundColor: theme.palette.neutral.light,
|
||||||
|
borderRadius: theme.shape.borderRadiusMedium,
|
||||||
|
padding: theme.spacing(3),
|
||||||
|
width: 'auto',
|
||||||
|
}));
|
||||||
|
|
||||||
|
const StyledNoTemplatesTitle = styled(Typography)(({ theme }) => ({
|
||||||
|
fontSize: theme.typography.caption.fontSize,
|
||||||
|
fontWeight: theme.typography.fontWeightBold,
|
||||||
|
marginBottom: theme.spacing(1),
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
}));
|
||||||
|
|
||||||
|
const StyledNoTemplatesDescription = styled(Typography)(({ theme }) => ({
|
||||||
|
fontSize: theme.typography.caption.fontSize,
|
||||||
|
color: theme.palette.text.secondary,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const StyledViewAllTemplates = styled(Box)({
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
height: '100%',
|
||||||
|
});
|
||||||
|
|
||||||
|
const StyledLink = styled(RouterLink)({
|
||||||
|
textDecoration: 'none',
|
||||||
|
'&:hover': {
|
||||||
|
textDecoration: 'underline',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
interface IFeatureStrategyMenuCardsReleaseTemplatesProps {
|
||||||
|
onAddReleasePlan: (template: IReleasePlanTemplate) => void;
|
||||||
|
onReviewReleasePlan: (template: IReleasePlanTemplate) => void;
|
||||||
|
filter: StrategyFilterValue;
|
||||||
|
setFilter: Dispatch<SetStateAction<StrategyFilterValue>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const FeatureStrategyMenuCardsReleaseTemplates = ({
|
||||||
|
onAddReleasePlan,
|
||||||
|
onReviewReleasePlan,
|
||||||
|
filter,
|
||||||
|
setFilter,
|
||||||
|
}: IFeatureStrategyMenuCardsReleaseTemplatesProps) => {
|
||||||
|
const { isEnterprise } = useUiConfig();
|
||||||
|
const { templates } = useReleasePlanTemplates();
|
||||||
|
|
||||||
|
if (!isEnterprise()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const slicedTemplates =
|
||||||
|
filter === 'releaseTemplates'
|
||||||
|
? templates
|
||||||
|
: templates.slice(0, RELEASE_TEMPLATE_DISPLAY_LIMIT);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box>
|
||||||
|
<StyledStrategyModalSectionHeader>
|
||||||
|
<Typography color='inherit' variant='body2'>
|
||||||
|
Release templates
|
||||||
|
</Typography>
|
||||||
|
</StyledStrategyModalSectionHeader>
|
||||||
|
{!templates.length ? (
|
||||||
|
<StyledNoTemplatesContainer>
|
||||||
|
<StyledNoTemplatesTitle>
|
||||||
|
<StyledIcon>
|
||||||
|
<FactCheckOutlinedIcon />
|
||||||
|
</StyledIcon>
|
||||||
|
Create your own release templates
|
||||||
|
</StyledNoTemplatesTitle>
|
||||||
|
<StyledNoTemplatesDescription>
|
||||||
|
Standardize your rollouts and save time by reusing
|
||||||
|
predefined strategies. Find release templates in the
|
||||||
|
side menu under{' '}
|
||||||
|
<StyledLink to='/release-templates'>
|
||||||
|
Configure > Release templates
|
||||||
|
</StyledLink>
|
||||||
|
</StyledNoTemplatesDescription>
|
||||||
|
</StyledNoTemplatesContainer>
|
||||||
|
) : (
|
||||||
|
<FeatureStrategyMenuCardsSection>
|
||||||
|
{slicedTemplates.map((template) => (
|
||||||
|
<FeatureReleasePlanCard
|
||||||
|
key={template.id}
|
||||||
|
template={template}
|
||||||
|
onClick={() => onAddReleasePlan(template)}
|
||||||
|
onPreviewClick={() => onReviewReleasePlan(template)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
{slicedTemplates.length < templates.length &&
|
||||||
|
templates.length > RELEASE_TEMPLATE_DISPLAY_LIMIT && (
|
||||||
|
<StyledViewAllTemplates>
|
||||||
|
<Button
|
||||||
|
variant='text'
|
||||||
|
size='small'
|
||||||
|
onClick={() =>
|
||||||
|
setFilter('releaseTemplates')
|
||||||
|
}
|
||||||
|
>
|
||||||
|
View all available templates
|
||||||
|
</Button>
|
||||||
|
</StyledViewAllTemplates>
|
||||||
|
)}
|
||||||
|
</FeatureStrategyMenuCardsSection>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,40 @@
|
|||||||
|
import { Box, styled, Typography } from '@mui/material';
|
||||||
|
import type { ReactNode } from 'react';
|
||||||
|
|
||||||
|
export const StyledStrategyModalSectionHeader = styled(Box)(({ theme }) => ({
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: theme.spacing(0.5),
|
||||||
|
marginBottom: theme.spacing(0.5),
|
||||||
|
width: '100%',
|
||||||
|
}));
|
||||||
|
|
||||||
|
const StyledStrategyModalSectionGrid = styled(Box)(({ theme }) => ({
|
||||||
|
display: 'grid',
|
||||||
|
gridTemplateColumns: 'repeat(3, 1fr)',
|
||||||
|
gap: theme.spacing(2),
|
||||||
|
width: '100%',
|
||||||
|
}));
|
||||||
|
|
||||||
|
interface IFeatureStrategyMenuCardsSectionProps {
|
||||||
|
title?: string;
|
||||||
|
children: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const FeatureStrategyMenuCardsSection = ({
|
||||||
|
title,
|
||||||
|
children,
|
||||||
|
}: IFeatureStrategyMenuCardsSectionProps) => (
|
||||||
|
<Box>
|
||||||
|
{title && (
|
||||||
|
<StyledStrategyModalSectionHeader>
|
||||||
|
<Typography color='inherit' variant='body2'>
|
||||||
|
{title}
|
||||||
|
</Typography>
|
||||||
|
</StyledStrategyModalSectionHeader>
|
||||||
|
)}
|
||||||
|
<StyledStrategyModalSectionGrid>
|
||||||
|
{children}
|
||||||
|
</StyledStrategyModalSectionGrid>
|
||||||
|
</Box>
|
||||||
|
);
|
Loading…
Reference in New Issue
Block a user