diff --git a/frontend/src/component/common/QuickFilters/QuickFilters.tsx b/frontend/src/component/common/QuickFilters/QuickFilters.tsx new file mode 100644 index 0000000000..441a5b69f4 --- /dev/null +++ b/frontend/src/component/common/QuickFilters/QuickFilters.tsx @@ -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 { + filters: Array<{ label: string; value: T }>; + value: T; + onChange: (value: T) => void; +} + +export const QuickFilters = ({ + filters, + value: currentValue, + onChange, +}: IQuickFiltersProps) => ( + + {filters.map(({ label, value }) => ( + onChange(value)} + /> + ))} + +); diff --git a/frontend/src/component/feature/FeatureStrategy/FeatureStrategyMenu/FeatureStrategyMenu.tsx b/frontend/src/component/feature/FeatureStrategy/FeatureStrategyMenu/FeatureStrategyMenu.tsx index ec6a774d71..0d2268df38 100644 --- a/frontend/src/component/feature/FeatureStrategy/FeatureStrategyMenu/FeatureStrategyMenu.tsx +++ b/frontend/src/component/feature/FeatureStrategy/FeatureStrategyMenu/FeatureStrategyMenu.tsx @@ -251,7 +251,6 @@ export const FeatureStrategyMenu = ({ projectId={projectId} featureId={featureId} environmentId={environmentId} - onlyReleasePlans={onlyReleasePlans} onAddReleasePlan={(template) => { setSelectedTemplate(template); addReleasePlan(template); diff --git a/frontend/src/component/feature/FeatureStrategy/FeatureStrategyMenu/FeatureStrategyMenuCards/FeatureStrategyMenuCards.tsx b/frontend/src/component/feature/FeatureStrategy/FeatureStrategyMenu/FeatureStrategyMenuCards/FeatureStrategyMenuCards.tsx index 4f50059a1c..47d0896e3d 100644 --- a/frontend/src/component/feature/FeatureStrategy/FeatureStrategyMenu/FeatureStrategyMenuCards/FeatureStrategyMenuCards.tsx +++ b/frontend/src/component/feature/FeatureStrategy/FeatureStrategyMenu/FeatureStrategyMenuCards/FeatureStrategyMenuCards.tsx @@ -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 { 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 { Link as RouterLink } from 'react-router-dom'; import CloseIcon from '@mui/icons-material/Close'; -import FactCheckOutlinedIcon from '@mui/icons-material/FactCheckOutlined'; import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig.ts'; import { HelpIcon } from 'component/common/HelpIcon/HelpIcon.tsx'; 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)(() => ({ width: '100%', @@ -31,13 +43,6 @@ const StyledScrollableContent = styled(Box)(({ theme }) => ({ 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 }) => ({ display: 'flex', justifyContent: 'space-between', @@ -45,47 +50,9 @@ const StyledHeader = styled(Box)(({ theme }) => ({ padding: theme.spacing(4, 4, 2, 4), })); -const StyledSectionHeader = styled(Box)(({ theme }) => ({ +const StyledFiltersContainer = styled(Box)(({ theme }) => ({ display: 'flex', - alignItems: 'center', - 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, + padding: theme.spacing(0, 4, 5, 4), })); 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 { projectId: string; featureId: string; environmentId: string; - onlyReleasePlans: boolean; onAddReleasePlan: (template: IReleasePlanTemplate) => void; onReviewReleasePlan: (template: IReleasePlanTemplate) => void; onClose: () => void; @@ -123,10 +82,9 @@ export const FeatureStrategyMenuCards = ({ const { isEnterprise } = useUiConfig(); const { strategies } = useStrategies(); - const { templates } = useReleasePlanTemplates(); const { project } = useProjectOverview(projectId); - const [seeAllReleaseTemplates, setSeeAllReleaseTemplates] = useState(false); + const [filter, setFilter] = useState(null); const activeStrategies = strategies.filter( (strategy) => !strategy.deprecated, @@ -159,70 +117,19 @@ export const FeatureStrategyMenuCards = ({ 'This is the default strategy defined for this environment in the project', }; - const renderReleasePlanTemplates = () => { - if (!isEnterprise()) { - return null; - } + const availableFilters = useMemo( + () => + 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 - ? templates - : templates.slice(0, RELEASE_TEMPLATE_DISPLAY_LIMIT); - - return ( - - - - Release templates - - - {!templates.length ? ( - - - - - - Create your own release templates - - - Standardize your rollouts and save time by reusing - predefined strategies. Find release templates in the - side menu under{' '} - - Configure > Release templates - - - - ) : ( - - {slicedTemplates.map((template) => ( - onAddReleasePlan(template)} - onPreviewClick={() => - onReviewReleasePlan(template) - } - /> - ))} - {slicedTemplates.length < templates.length && - templates.length > - RELEASE_TEMPLATE_DISPLAY_LIMIT && ( - - - - )} - - )} - - ); + const shouldRender = (key: StrategyFilterValue) => { + return filter === null || filter === key; }; return ( @@ -238,45 +145,87 @@ export const FeatureStrategyMenuCards = ({ + + + - - - - - Project default - - - This is set per project, per - environment, and can be configured{' '} - - here - - - } - size='16px' - /> - - - - Standard strategies - - - - - - {standardStrategies.map((strategy) => ( + {(shouldRender('default') || shouldRender('standard')) && ( + + + {shouldRender('default') && ( + + + Project default + + + This is set per project, per + environment, and can be + configured{' '} + + here + + + } + size='16px' + /> + + )} + {shouldRender('standard') && ( + + + Standard strategies + + + )} + + + {shouldRender('default') && ( + + )} + {shouldRender('standard') && ( + <> + {standardStrategies.map((strategy) => ( + + ))} + + )} + + + )} + {shouldRender('releaseTemplates') && ( + + )} + {advancedStrategies.length > 0 && shouldRender('advanced') && ( + + {advancedStrategies.map((strategy) => ( ))} - - - {renderReleasePlanTemplates()} - {advancedStrategies.length > 0 && ( - - - - Advanced strategies - - - - {advancedStrategies.map((strategy) => ( - - ))} - - + )} - {customStrategies.length > 0 && ( - - - - Custom strategies - - - - {customStrategies.map((strategy) => ( - - ))} - - + {customStrategies.length > 0 && shouldRender('custom') && ( + + {customStrategies.map((strategy) => ( + + ))} + )} diff --git a/frontend/src/component/feature/FeatureStrategy/FeatureStrategyMenu/FeatureStrategyMenuCards/FeatureStrategyMenuCardsReleaseTemplates.tsx b/frontend/src/component/feature/FeatureStrategy/FeatureStrategyMenu/FeatureStrategyMenuCards/FeatureStrategyMenuCardsReleaseTemplates.tsx new file mode 100644 index 0000000000..4e6c4de96c --- /dev/null +++ b/frontend/src/component/feature/FeatureStrategy/FeatureStrategyMenu/FeatureStrategyMenuCards/FeatureStrategyMenuCardsReleaseTemplates.tsx @@ -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>; +} + +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 ( + + + + Release templates + + + {!templates.length ? ( + + + + + + Create your own release templates + + + Standardize your rollouts and save time by reusing + predefined strategies. Find release templates in the + side menu under{' '} + + Configure > Release templates + + + + ) : ( + + {slicedTemplates.map((template) => ( + onAddReleasePlan(template)} + onPreviewClick={() => onReviewReleasePlan(template)} + /> + ))} + {slicedTemplates.length < templates.length && + templates.length > RELEASE_TEMPLATE_DISPLAY_LIMIT && ( + + + + )} + + )} + + ); +}; diff --git a/frontend/src/component/feature/FeatureStrategy/FeatureStrategyMenu/FeatureStrategyMenuCards/FeatureStrategyMenuCardsSection.tsx b/frontend/src/component/feature/FeatureStrategy/FeatureStrategyMenu/FeatureStrategyMenuCards/FeatureStrategyMenuCardsSection.tsx new file mode 100644 index 0000000000..f2ded61c2d --- /dev/null +++ b/frontend/src/component/feature/FeatureStrategy/FeatureStrategyMenu/FeatureStrategyMenuCards/FeatureStrategyMenuCardsSection.tsx @@ -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) => ( + + {title && ( + + + {title} + + + )} + + {children} + + +);