diff --git a/frontend/src/component/common/QuickFilters/QuickFilters.tsx b/frontend/src/component/common/QuickFilters/QuickFilters.tsx index 441a5b69f4..c61d01af40 100644 --- a/frontend/src/component/common/QuickFilters/QuickFilters.tsx +++ b/frontend/src/component/common/QuickFilters/QuickFilters.tsx @@ -47,6 +47,7 @@ export const QuickFilters = ({ label={label} variant='outlined' isActive={value === currentValue} + aria-pressed={value === currentValue} onClick={() => onChange(value)} /> ))} diff --git a/frontend/src/component/feature/FeatureStrategy/FeatureStrategyMenu/FeatureStrategyMenu.tsx b/frontend/src/component/feature/FeatureStrategy/FeatureStrategyMenu/FeatureStrategyMenu.tsx index cc06ba57d5..0a2c08b8e4 100644 --- a/frontend/src/component/feature/FeatureStrategy/FeatureStrategyMenu/FeatureStrategyMenu.tsx +++ b/frontend/src/component/feature/FeatureStrategy/FeatureStrategyMenu/FeatureStrategyMenu.tsx @@ -1,9 +1,4 @@ -import type React from 'react'; import { useEffect, useState } from 'react'; -import PermissionButton, { - type IPermissionButtonProps, -} from 'component/common/PermissionButton/PermissionButton'; -import { CREATE_FEATURE_STRATEGY } from 'component/providers/AccessProvider/permissions'; import { Box, Dialog, IconButton, styled, Typography } from '@mui/material'; import CloseIcon from '@mui/icons-material/Close'; import { usePlausibleTracker } from 'hooks/usePlausibleTracker'; @@ -23,22 +18,14 @@ import { import { ReleasePlanConfirmationDialog } from './ReleasePlanConfirmationDialog.tsx'; interface IFeatureStrategyMenuProps { - label: string; projectId: string; featureId: string; environmentId: string; - variant?: IPermissionButtonProps['variant']; - matchWidth?: boolean; - disableReason?: string; + isStrategyMenuDialogOpen: boolean; + onClose: any; + defaultFilter?: StrategyFilterValue; } -const StyledStrategyMenu = styled('div')(({ theme }) => ({ - display: 'flex', - flexFlow: 'row', - justifyContent: 'flex-end', - gap: theme.spacing(1), -})); - const StyledHeader = styled(Box)(({ theme }) => ({ display: 'flex', justifyContent: 'space-between', @@ -47,26 +34,20 @@ const StyledHeader = styled(Box)(({ theme }) => ({ })); export const FeatureStrategyMenu = ({ - label, projectId, featureId, environmentId, - variant, - matchWidth, - disableReason, + isStrategyMenuDialogOpen, + onClose, + defaultFilter = null, }: IFeatureStrategyMenuProps) => { - const [isStrategyMenuDialogOpen, setIsStrategyMenuDialogOpen] = - useState(false); - const [filter, setFilter] = useState(null); + const [filter, setFilter] = useState(defaultFilter); const { trackEvent } = usePlausibleTracker(); const [selectedTemplate, setSelectedTemplate] = useState(); const [releasePlanPreview, setReleasePlanPreview] = useState(false); const [addReleasePlanConfirmationOpen, setAddReleasePlanConfirmationOpen] = useState(false); - const dialogId = isStrategyMenuDialogOpen - ? 'FeatureStrategyMenuDialog' - : undefined; const { setToastApiError, setToastData } = useToast(); const { isChangeRequestConfigured } = useChangeRequestsEnabled(projectId); const { addChange } = useChangeRequestApi(); @@ -82,18 +63,11 @@ export const FeatureStrategyMenu = ({ const activeReleasePlan = releasePlans[0]; - const onClose = () => { - setIsStrategyMenuDialogOpen(false); - }; - useEffect(() => { if (!isStrategyMenuDialogOpen) return; setReleasePlanPreview(false); - }, [isStrategyMenuDialogOpen]); - - const openMoreStrategies = (_event: React.SyntheticEvent) => { - setIsStrategyMenuDialogOpen(true); - }; + setFilter(defaultFilter); + }, [isStrategyMenuDialogOpen, defaultFilter]); const addReleasePlan = async ( template: IReleasePlanTemplate, @@ -150,23 +124,7 @@ export const FeatureStrategyMenu = ({ }; return ( - event.stopPropagation()}> - - Add strategy - + <> )} - + ); }; diff --git a/frontend/src/component/feature/FeatureStrategy/FeatureStrategyMenu/FeatureStrategyMenuButton.tsx b/frontend/src/component/feature/FeatureStrategy/FeatureStrategyMenu/FeatureStrategyMenuButton.tsx new file mode 100644 index 0000000000..d4d5a7d1e3 --- /dev/null +++ b/frontend/src/component/feature/FeatureStrategy/FeatureStrategyMenu/FeatureStrategyMenuButton.tsx @@ -0,0 +1,55 @@ +import PermissionButton, { + type IPermissionButtonProps, +} from 'component/common/PermissionButton/PermissionButton'; +import { CREATE_FEATURE_STRATEGY } from 'component/providers/AccessProvider/permissions'; +import { styled } from '@mui/material'; + +interface IFeatureStrategyMenuButtonProps { + label: string; + projectId: string; + environmentId: string; + dialogId?: string; + onClick: any; + variant?: IPermissionButtonProps['variant']; + matchWidth?: boolean; + disableReason?: string; +} + +const StyledStrategyMenu = styled('div')(({ theme }) => ({ + display: 'flex', + flexFlow: 'row', + justifyContent: 'flex-end', + gap: theme.spacing(1), +})); + +export const FeatureStrategyMenuButton = ({ + label, + projectId, + environmentId, + dialogId, + onClick, + variant, + matchWidth, + disableReason, +}: IFeatureStrategyMenuButtonProps) => { + return ( + event.stopPropagation()}> + + {label} + + + ); +}; diff --git a/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/EnvironmentHeader/EnvironmentHeader.styles.tsx b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/EnvironmentHeader/EnvironmentHeader.styles.tsx new file mode 100644 index 0000000000..ba2ee9492a --- /dev/null +++ b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/EnvironmentHeader/EnvironmentHeader.styles.tsx @@ -0,0 +1,35 @@ +import { Box, styled } from '@mui/material'; + +export const StyledSuggestion = styled('div')(({ theme }) => ({ + width: '100%', + display: 'flex', + alignItems: 'center', + padding: theme.spacing(0.5, 3), + background: theme.palette.secondary.light, + borderBottomLeftRadius: theme.shape.borderRadiusLarge, + borderBottomRightRadius: theme.shape.borderRadiusLarge, + color: theme.palette.primary.main, + fontSize: theme.fontSizes.smallerBody, +})); + +export const StyledBold = styled('b')(({ theme }) => ({ + fontWeight: theme.typography.fontWeightBold, +})); + +export const StyledSpan = styled('span')(({ theme }) => ({ + fontWeight: theme.typography.fontWeightBold, + textDecoration: 'underline', +})); + +export const TooltipHeader = styled('div')(({ theme }) => ({ + fontWeight: theme.typography.fontWeightBold, +})); + +export const TooltipDescription = styled('div')(({ theme }) => ({ + fontSize: theme.fontSizes.smallerBody, + paddingBottom: theme.spacing(1.5), +})); + +export const StyledBox = styled(Box)(({ theme }) => ({ + padding: theme.spacing(1.5), +})); diff --git a/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/EnvironmentHeader/EnvironmentHeader.tsx b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/EnvironmentHeader/EnvironmentHeader.tsx index f5058f686e..aee264552d 100644 --- a/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/EnvironmentHeader/EnvironmentHeader.tsx +++ b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/EnvironmentHeader/EnvironmentHeader.tsx @@ -10,6 +10,7 @@ import { useId } from 'hooks/useId'; import { EnvironmentStrategySuggestion } from './EnvironmentStrategySuggestion/EnvironmentStrategySuggestion.js'; import type { IFeatureStrategy } from 'interfaces/strategy'; import { useProjectEnvironments } from 'hooks/api/getters/useProjectEnvironments/useProjectEnvironments'; +import { EnvironmentTemplateSuggestion } from './EnvironmentTemplateSuggestion/EnvironmentTemplateSuggestion'; const StyledAccordionSummary = styled(AccordionSummary, { shouldForwardProp: (prop) => prop !== 'expandable' && prop !== 'empty', @@ -109,6 +110,7 @@ type EnvironmentHeaderProps = { expandable?: boolean; environmentMetadata?: EnvironmentMetadata; hasActivations?: boolean; + onOpenReleaseTemplates?: any; } & AccordionSummaryProps; const MetadataChip = ({ @@ -162,13 +164,14 @@ export const EnvironmentHeader: FC< expandable = true, environmentMetadata, hasActivations = false, + onOpenReleaseTemplates, ...props }) => { const id = useId(); const { environments } = useProjectEnvironments(projectId); - const defaultStrategy = environments.find( - (env) => env.name === environmentId, - )?.defaultStrategy; + const environment = environments.find((env) => env.name === environmentId); + const defaultStrategy = environment?.defaultStrategy; + const environmentType = environment?.type; const strategy: Omit = useMemo(() => { const baseDefaultStrategy = { @@ -211,7 +214,7 @@ export const EnvironmentHeader: FC< {children} - {!hasActivations && ( + {!hasActivations && environmentType !== 'production' && ( )} + {!hasActivations && + environmentType === 'production' && + onOpenReleaseTemplates && ( + + )} ); }; diff --git a/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/EnvironmentHeader/EnvironmentStrategySuggestion/EnvironmentStrategySuggestion.tsx b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/EnvironmentHeader/EnvironmentStrategySuggestion/EnvironmentStrategySuggestion.tsx index 73bb074830..308ab2fb33 100644 --- a/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/EnvironmentHeader/EnvironmentStrategySuggestion/EnvironmentStrategySuggestion.tsx +++ b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/EnvironmentHeader/EnvironmentStrategySuggestion/EnvironmentStrategySuggestion.tsx @@ -1,4 +1,3 @@ -import { Box, styled } from '@mui/material'; import { HtmlTooltip } from 'component/common/HtmlTooltip/HtmlTooltip'; import { Link } from 'react-router-dom'; import { StrategyExecution } from '../../EnvironmentAccordionBody/StrategyDraggableItem/StrategyItem/StrategyExecution/StrategyExecution.js'; @@ -12,40 +11,14 @@ import useToast from 'hooks/useToast.js'; import { usePendingChangeRequests } from 'hooks/api/getters/usePendingChangeRequests/usePendingChangeRequests.js'; import { useFeature } from 'hooks/api/getters/useFeature/useFeature.js'; import useFeatureStrategyApi from 'hooks/api/actions/useFeatureStrategyApi/useFeatureStrategyApi.js'; - -const StyledSuggestion = styled('div')(({ theme }) => ({ - width: '100%', - display: 'flex', - alignItems: 'center', - padding: theme.spacing(0.5, 3), - background: theme.palette.secondary.light, - borderBottomLeftRadius: theme.shape.borderRadiusLarge, - borderBottomRightRadius: theme.shape.borderRadiusLarge, - color: theme.palette.primary.main, - fontSize: theme.fontSizes.smallerBody, -})); - -const StyledBold = styled('b')(({ theme }) => ({ - fontWeight: theme.typography.fontWeightBold, -})); - -const StyledSpan = styled('span')(({ theme }) => ({ - fontWeight: theme.typography.fontWeightBold, - textDecoration: 'underline', -})); - -const TooltipHeader = styled('div')(({ theme }) => ({ - fontWeight: theme.typography.fontWeightBold, -})); - -const TooltipDescription = styled('div')(({ theme }) => ({ - fontSize: theme.fontSizes.smallerBody, - paddingBottom: theme.spacing(1.5), -})); - -const StyledBox = styled(Box)(({ theme }) => ({ - padding: theme.spacing(1.5), -})); +import { + StyledBold, + StyledBox, + StyledSpan, + StyledSuggestion, + TooltipDescription, + TooltipHeader, +} from '../EnvironmentHeader.styles'; type DefaultStrategySuggestionProps = { projectId: string; diff --git a/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/EnvironmentHeader/EnvironmentTemplateSuggestion/EnvironmentTemplateSuggestion.tsx b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/EnvironmentHeader/EnvironmentTemplateSuggestion/EnvironmentTemplateSuggestion.tsx new file mode 100644 index 0000000000..232ad1a56b --- /dev/null +++ b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/EnvironmentHeader/EnvironmentTemplateSuggestion/EnvironmentTemplateSuggestion.tsx @@ -0,0 +1,48 @@ +import { Button } from '@mui/material'; +import { Link } from 'react-router-dom'; +import { + StyledBold, + StyledBox, + StyledSpan, + StyledSuggestion, + TooltipDescription, +} from '../EnvironmentHeader.styles'; +import { HtmlTooltip } from 'component/common/HtmlTooltip/HtmlTooltip'; + +type EnvironmentTemplateSuggestionProps = { + onClick: () => void; +}; + +export const EnvironmentTemplateSuggestion = ({ + onClick, +}: EnvironmentTemplateSuggestionProps) => { + return ( + + Suggestion: +  Add a  + + + Release templates are defined globally  + + here + + + + } + maxWidth='200' + arrow + > + release template + +  to this environment  + + + ); +}; diff --git a/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/FeatureOverviewEnvironment.test.tsx b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/FeatureOverviewEnvironment.test.tsx index e30674ed52..4c46d9e8b6 100644 --- a/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/FeatureOverviewEnvironment.test.tsx +++ b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/FeatureOverviewEnvironment.test.tsx @@ -4,6 +4,10 @@ import { render } from 'utils/testRenderer'; import { FeatureOverviewEnvironment } from './FeatureOverviewEnvironment.tsx'; import { Route, Routes } from 'react-router-dom'; import { CREATE_FEATURE_STRATEGY } from 'component/providers/AccessProvider/permissions'; +import { testServerRoute, testServerSetup } from 'utils/testServer'; +import userEvent from '@testing-library/user-event'; + +const server = testServerSetup(); const renderRoute = (element: ReactNode, permissions: any[] = []) => render( @@ -19,6 +23,54 @@ const renderRoute = (element: ReactNode, permissions: any[] = []) => }, ); +const setupEnterpriseEndpoints = () => { + testServerRoute(server, '/api/admin/ui-config', { + versionInfo: { + current: { + enterprise: '1.0.0', + }, + }, + environment: 'enterprise', + }); + testServerRoute(server, '/api/admin/release-plan-templates', [ + { + id: 'template-1', + name: 'Test Template', + description: 'A test release template', + }, + ]); + testServerRoute(server, '/api/admin/environments/project/default', { + environments: [ + { + name: 'production', + enabled: true, + type: 'production', + }, + ], + }); +}; + +const setupOssEndpoints = () => { + testServerRoute(server, '/api/admin/ui-config', { + versionInfo: { + current: {}, + }, + flags: {}, + resourceLimits: { + featureEnvironmentStrategies: 30, + }, + }); + testServerRoute(server, '/api/admin/environments/project/default', { + environments: [ + { + name: 'production', + enabled: true, + type: 'production', + }, + ], + }); +}; + describe('FeatureOverviewEnvironment', () => { test('should allow to add strategy', async () => { renderRoute( @@ -63,4 +115,131 @@ describe('FeatureOverviewEnvironment', () => { const button = await screen.findByText('Add strategy'); expect(button).toHaveAttribute('aria-disabled', 'true'); }); + + test('shows release template suggestion for production environment on enterprise', async () => { + setupEnterpriseEndpoints(); + renderRoute( + , + [{ permission: CREATE_FEATURE_STRATEGY }], + ); + + expect( + await screen.findByText('Choose a release template'), + ).toBeInTheDocument(); + }); + + test('does not show release template suggestion for non-production environment on enterprise', async () => { + setupEnterpriseEndpoints(); + testServerRoute(server, '/api/admin/environments/project/default', { + environments: [ + { + name: 'development', + enabled: true, + type: 'development', + sortOrder: 0, + }, + ], + }); + + renderRoute( + , + [{ permission: CREATE_FEATURE_STRATEGY }], + ); + + expect( + await screen.findByText(/default strategy/i), + ).toBeInTheDocument(); + expect( + screen.queryByText('Choose a release template'), + ).not.toBeInTheDocument(); + }); + + test('does not show release template suggestion for production environment on OSS', async () => { + setupOssEndpoints(); + renderRoute( + , + [{ permission: CREATE_FEATURE_STRATEGY }], + ); + + expect(await screen.findByText('production')).toBeInTheDocument(); + expect( + screen.queryByText('Choose a release template'), + ).not.toBeInTheDocument(); + }); + + test('does not show release template suggestion when environment has activations', async () => { + setupEnterpriseEndpoints(); + renderRoute( + , + [{ permission: CREATE_FEATURE_STRATEGY }], + ); + + expect(await screen.findByText('production')).toBeInTheDocument(); + + expect( + screen.queryByText('Choose a release template'), + ).not.toBeInTheDocument(); + }); + + test('opens strategy menu dialog with release templates filter when clicking release template suggestion', async () => { + const user = userEvent.setup(); + setupEnterpriseEndpoints(); + renderRoute( + , + [{ permission: CREATE_FEATURE_STRATEGY }], + ); + + const releaseTemplateButton = await screen.findByText( + 'Choose a release template', + ); + await user.click(releaseTemplateButton); + const releaseTemplatesFilter = screen.queryByRole('button', { + name: /release templates/i, + }); + + expect(releaseTemplatesFilter).toBeInTheDocument(); + expect(releaseTemplatesFilter).toHaveAttribute('aria-pressed', 'true'); + }); }); diff --git a/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/FeatureOverviewEnvironment.tsx b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/FeatureOverviewEnvironment.tsx index f689898bfa..93bfb20b08 100644 --- a/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/FeatureOverviewEnvironment.tsx +++ b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/FeatureOverviewEnvironment.tsx @@ -1,4 +1,5 @@ -import { Accordion, AccordionDetails, styled } from '@mui/material'; +import { useState } from 'react'; +import { Accordion, AccordionDetails, Box, styled } from '@mui/material'; import type { IFeatureEnvironment, IFeatureEnvironmentMetrics, @@ -14,10 +15,10 @@ import { } from './EnvironmentHeader/EnvironmentHeader.tsx'; import FeatureOverviewEnvironmentMetrics from './EnvironmentHeader/FeatureOverviewEnvironmentMetrics/FeatureOverviewEnvironmentMetrics.tsx'; import { FeatureOverviewEnvironmentToggle } from './EnvironmentHeader/FeatureOverviewEnvironmentToggle/FeatureOverviewEnvironmentToggle.tsx'; -import { useState } from 'react'; import type { IReleasePlan } from 'interfaces/releasePlans'; import { EnvironmentAccordionBody } from './EnvironmentAccordionBody/EnvironmentAccordionBody.tsx'; -import { Box } from '@mui/material'; +import type { StrategyFilterValue } from 'component/feature/FeatureStrategy/FeatureStrategyMenu/FeatureStrategyMenuCards/FeatureStrategyMenuCards'; +import { FeatureStrategyMenuButton } from 'component/feature/FeatureStrategy/FeatureStrategyMenu/FeatureStrategyMenuButton.tsx'; const StyledFeatureOverviewEnvironment = styled('div')(({ theme }) => ({ borderRadius: theme.shape.borderRadiusLarge, @@ -73,13 +74,30 @@ export const FeatureOverviewEnvironment = ({ const [isOpen, setIsOpen] = useState(false); const projectId = useRequiredPathParam('projectId'); const featureId = useRequiredPathParam('featureId'); - const { isOss } = useUiConfig(); + const { isOss, isEnterprise } = useUiConfig(); const hasActivations = Boolean( environment?.enabled || (environment?.strategies && environment?.strategies.length > 0) || (environment?.releasePlans && environment?.releasePlans.length > 0), ); + const [filter, setFilter] = useState(null); + const [isStrategyMenuDialogOpen, setIsStrategyMenuDialogOpen] = + useState(false); + + const dialogId = isStrategyMenuDialogOpen + ? 'FeatureStrategyMenuDialog' + : undefined; + + const openMoreStrategies = (_event: React.SyntheticEvent) => { + setFilter(null); + setIsStrategyMenuDialogOpen(true); + }; + + const onClose = () => { + setIsStrategyMenuDialogOpen(false); + }; + return ( { + setFilter('releaseTemplates'); + setIsStrategyMenuDialogOpen(true); + } + : undefined + } > {!hasActivations ? ( - + <> + + + ) : ( - +