diff --git a/frontend/cypress/support/UI.ts b/frontend/cypress/support/UI.ts index 160166083f..66e0748976 100644 --- a/frontend/cypress/support/UI.ts +++ b/frontend/cypress/support/UI.ts @@ -230,6 +230,9 @@ export const deleteFeatureStrategy_UI = ( }, ).as('deleteUserStrategy'); cy.visit(`/projects/${project}/features/${featureToggleName}`); + cy.get('[data-testid=FEATURE_ENVIRONMENT_ACCORDION_development]') + .first() + .click(); cy.get('[data-testid=STRATEGY_REMOVE_MENU_BTN]').first().click(); cy.get('[data-testid=STRATEGY_FORM_REMOVE_ID]').first().click(); if (!shouldWait) return cy.get('[data-testid=DIALOGUE_CONFIRM_ID]').click(); diff --git a/frontend/src/component/common/PercentageCircle/PercentageCircle.tsx b/frontend/src/component/common/PercentageCircle/PercentageCircle.tsx index e7b94d2b29..34e5686b7c 100644 --- a/frontend/src/component/common/PercentageCircle/PercentageCircle.tsx +++ b/frontend/src/component/common/PercentageCircle/PercentageCircle.tsx @@ -1,17 +1,17 @@ import { useTheme } from '@mui/material'; import type { CSSProperties } from 'react'; -interface IPercentageCircleProps { +type PercentageCircleProps = { percentage: number; size?: `${number}rem`; disabled?: boolean | null; -} +}; const PercentageCircle = ({ percentage, size = '4rem', disabled = false, -}: IPercentageCircleProps) => { +}: PercentageCircleProps) => { const theme = useTheme(); const style: CSSProperties = { diff --git a/frontend/src/component/common/PercentageCircle/PercentageDonut.tsx b/frontend/src/component/common/PercentageCircle/PercentageDonut.tsx new file mode 100644 index 0000000000..1598e51cd5 --- /dev/null +++ b/frontend/src/component/common/PercentageCircle/PercentageDonut.tsx @@ -0,0 +1,84 @@ +import { styled, useTheme } from '@mui/material'; +import type { CSSProperties, ReactNode } from 'react'; + +const StyledContainer = styled('div')(() => ({ + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + position: 'relative', + margin: 0, +})); + +const StyledContent = styled('div', { + shouldForwardProp: (prop) => prop !== 'color', +})<{ color?: string }>(({ theme, color }) => ({ + position: 'absolute', + top: '50%', + left: '50%', + transform: 'translate(-50%, -50%)', + color, + fontSize: theme.fontSizes.smallerBody, + margin: 0, +})); + +type PercentageDonutProps = { + percentage: number; + size?: `${number}rem`; + disabled?: boolean | null; + donut?: boolean; + children?: ReactNode; +}; + +export const PercentageDonut = ({ + percentage, + size = '4rem', + disabled = false, + children, +}: PercentageDonutProps) => { + const theme = useTheme(); + + const style: CSSProperties = { + display: 'block', + borderRadius: '100%', + transform: 'rotate(-90deg)', + height: size, + width: size, + }; + + // The percentage circle used to be drawn by CSS with a conic-gradient, + // but the result was either jagged or blurry. SVG seems to look better. + // See https://stackoverflow.com/a/70659532. + const r = 100 / (2 * Math.PI); + const d = 2 * r; + const strokeWidth = d * 0.2; + + const color = disabled + ? theme.palette.neutral.border + : theme.palette.primary.light; + + return ( + + {/* biome-ignore lint/a11y/noSvgWithoutTitle: should be in a figure with figcaption */} + + + + + {children} + + ); +}; diff --git a/frontend/src/component/feature/FeatureStrategy/FeatureStrategyForm/FeatureStrategyForm.tsx b/frontend/src/component/feature/FeatureStrategy/FeatureStrategyForm/FeatureStrategyForm.tsx index 68ed174b75..5d34e6d6f2 100644 --- a/frontend/src/component/feature/FeatureStrategy/FeatureStrategyForm/FeatureStrategyForm.tsx +++ b/frontend/src/component/feature/FeatureStrategy/FeatureStrategyForm/FeatureStrategyForm.tsx @@ -46,7 +46,7 @@ import { usePlausibleTracker } from 'hooks/usePlausibleTracker'; import { BuiltInStrategies, formatStrategyName } from 'utils/strategyNames'; import { Badge } from 'component/common/Badge/Badge'; import EnvironmentIcon from 'component/common/EnvironmentIcon/EnvironmentIcon'; -import { UpgradeChangeRequests } from '../../FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/UpgradeChangeRequests'; +import { UpgradeChangeRequests } from '../../FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/UpgradeChangeRequests/UpgradeChangeRequests'; interface IFeatureStrategyFormProps { feature: IFeatureToggle; diff --git a/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverview.tsx b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverview.tsx index cf0ff0e10b..c7c6e28141 100644 --- a/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverview.tsx +++ b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverview.tsx @@ -12,7 +12,7 @@ import { FeatureStrategyCreate } from 'component/feature/FeatureStrategy/Feature import { useEffect } from 'react'; import { useLastViewedFlags } from 'hooks/useLastViewedFlags'; import { useUiFlag } from 'hooks/useUiFlag'; -import { FeatureOverviewEnvironment } from './NewFeatureOverviewEnvironment/NewFeatureOverviewEnvironment'; +import { FeatureOverviewEnvironments } from './FeatureOverviewEnvironments/FeatureOverviewEnvironments'; import { default as LegacyFleatureOverview } from './LegacyFeatureOverview'; import { useEnvironmentVisibility } from './FeatureOverviewMetaData/EnvironmentVisibilityMenu/hooks/useEnvironmentVisibility'; @@ -62,7 +62,7 @@ export const FeatureOverview = () => { /> - diff --git a/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/EnvironmentFooter/EnvironmentFooter.tsx b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/EnvironmentFooter/EnvironmentFooter.tsx index df50f3dca0..67f626ed05 100644 --- a/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/EnvironmentFooter/EnvironmentFooter.tsx +++ b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/EnvironmentFooter/EnvironmentFooter.tsx @@ -12,6 +12,9 @@ interface IEnvironmentFooterProps { environmentMetric?: IFeatureEnvironmentMetrics; } +/** + * @deprecated remove with `featureOverviewRedesign` flag + */ export const EnvironmentFooter = ({ environmentMetric, }: IEnvironmentFooterProps) => { 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 new file mode 100644 index 0000000000..f499b2686a --- /dev/null +++ b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/EnvironmentHeader/EnvironmentHeader.tsx @@ -0,0 +1,87 @@ +import type { FC, ReactNode } from 'react'; +import { + AccordionSummary, + type AccordionSummaryProps, + styled, +} from '@mui/material'; +import ExpandMore from '@mui/icons-material/ExpandMore'; +import { Truncator } from 'component/common/Truncator/Truncator'; + +const StyledAccordionSummary = styled(AccordionSummary, { + shouldForwardProp: (prop) => prop !== 'expandable', +})<{ + expandable?: boolean; +}>(({ theme, expandable }) => ({ + boxShadow: 'none', + padding: theme.spacing(0.5, 3, 0.5, 2), + display: 'flex', + alignItems: 'center', + [theme.breakpoints.down('sm')]: { + fontWeight: 'bold', + }, + '&&&': { + cursor: expandable ? 'pointer' : 'default', + }, +})); + +const StyledHeader = styled('header')(({ theme }) => ({ + display: 'flex', + columnGap: theme.spacing(1), + paddingRight: theme.spacing(1), + width: '100%', + color: theme.palette.text.primary, + alignItems: 'center', + minHeight: theme.spacing(8), +})); + +const StyledHeaderTitle = styled('hgroup')(({ theme }) => ({ + display: 'flex', + flexDirection: 'column', + flex: 1, +})); + +const StyledHeaderTitleLabel = styled('p')(({ theme }) => ({ + fontSize: theme.fontSizes.smallerBody, + color: theme.palette.text.secondary, + margin: 0, +})); + +const StyledTruncator = styled(Truncator)(({ theme }) => ({ + fontSize: theme.typography.body1.fontSize, + fontWeight: theme.typography.fontWeightMedium, +})); + +type EnvironmentHeaderProps = { + environmentId: string; + children: ReactNode; + expandable?: boolean; +} & AccordionSummaryProps; + +export const EnvironmentHeader: FC = ({ + environmentId, + children, + expandable = true, + ...props +}) => { + return ( + + } + expandable={expandable} + > + + + Environment + + {environmentId} + + + {children} + + + ); +}; diff --git a/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/EnvironmentHeader/FeatureOverviewEnvironmentMetrics/FeatureOverviewEnvironmentMetrics.tsx b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/EnvironmentHeader/FeatureOverviewEnvironmentMetrics/FeatureOverviewEnvironmentMetrics.tsx new file mode 100644 index 0000000000..c250458f53 --- /dev/null +++ b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/EnvironmentHeader/FeatureOverviewEnvironmentMetrics/FeatureOverviewEnvironmentMetrics.tsx @@ -0,0 +1,108 @@ +import type { IFeatureEnvironmentMetrics } from 'interfaces/featureToggle'; +import { calculatePercentage } from 'utils/calculatePercentage'; +import { PrettifyLargeNumber } from 'component/common/PrettifyLargeNumber/PrettifyLargeNumber'; +import { + styled, + Tooltip, + tooltipClasses, + type TooltipProps, +} from '@mui/material'; +import { PercentageDonut } from 'component/common/PercentageCircle/PercentageDonut'; + +const StyledContainer = styled('figure')(({ theme }) => ({ + display: 'flex', + alignItems: 'center', + justifyContent: 'flex-end', + margin: 0, + padding: 0, +})); + +const StyledInfo = styled('figcaption')(({ theme }) => ({ + fontSize: theme.typography.body2.fontSize, + textAlign: 'right', + [theme.breakpoints.down('xl')]: { + display: 'none', + }, + display: 'flex', + flexDirection: 'column', + flexGrow: 1, + margin: 0, + padding: 0, + span: { + textWrap: 'nowrap', + }, +})); + +const StyledPercentageCircle = styled('div')(({ theme }) => ({ + marginRight: theme.spacing(1), + marginLeft: theme.spacing(1.5), + [theme.breakpoints.down(500)]: { + display: 'none', + }, +})); + +const StyledTooltip = styled(({ className, ...props }: TooltipProps) => ( + +))({ + [`& .${tooltipClasses.tooltip}`]: { + maxWidth: 200, + }, +}); + +type FeatureOverviewEnvironmentMetrics = { + environmentMetric?: Pick; + collapsed?: boolean; +}; + +const FeatureOverviewEnvironmentMetrics = ({ + environmentMetric, + collapsed, +}: FeatureOverviewEnvironmentMetrics) => { + if (!environmentMetric) return null; + + const total = environmentMetric.yes + environmentMetric.no; + const percentage = calculatePercentage(total, environmentMetric?.yes); + + const isEmpty = + !environmentMetric || + (environmentMetric.yes === 0 && environmentMetric.no === 0); + + const content = isEmpty ? ( + <> + No evaluation metrics +
+ received in the last hour + + ) : ( + <> + + The flag has been evaluated{' '} + + times + + {' '} + + and enabled{' '} + + times + {' '} + in the last hour + + + ); + + return ( + + {!collapsed ? {content} : null} + + + + {!isEmpty ? `${percentage}%` : null} + + + + + ); +}; + +export default FeatureOverviewEnvironmentMetrics; diff --git a/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/FeatureOverviewEnvironmentMetrics/FeatureOverviewEnvironmentMetrics.tsx b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/EnvironmentHeader/FeatureOverviewEnvironmentMetrics/LegacyFeatureOverviewEnvironmentMetrics.tsx similarity index 98% rename from frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/FeatureOverviewEnvironmentMetrics/FeatureOverviewEnvironmentMetrics.tsx rename to frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/EnvironmentHeader/FeatureOverviewEnvironmentMetrics/LegacyFeatureOverviewEnvironmentMetrics.tsx index 10f494d81e..9581355e0c 100644 --- a/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/FeatureOverviewEnvironmentMetrics/FeatureOverviewEnvironmentMetrics.tsx +++ b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/EnvironmentHeader/FeatureOverviewEnvironmentMetrics/LegacyFeatureOverviewEnvironmentMetrics.tsx @@ -55,6 +55,9 @@ const StyledPercentageCircle = styled('div')(({ theme }) => ({ }, })); +/** + * @deprecated remove with `flagOverviewRedesign` flag + */ const FeatureOverviewEnvironmentMetrics = ({ environmentMetric, disabled = false, diff --git a/frontend/src/component/feature/FeatureView/FeatureOverview/NewFeatureOverviewEnvironment/FeatureOverviewEnvironmentToggle.tsx b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/EnvironmentHeader/FeatureOverviewEnvironmentToggle/FeatureOverviewEnvironmentToggle.tsx similarity index 80% rename from frontend/src/component/feature/FeatureView/FeatureOverview/NewFeatureOverviewEnvironment/FeatureOverviewEnvironmentToggle.tsx rename to frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/EnvironmentHeader/FeatureOverviewEnvironmentToggle/FeatureOverviewEnvironmentToggle.tsx index aad7cf5769..b55994b957 100644 --- a/frontend/src/component/feature/FeatureView/FeatureOverview/NewFeatureOverviewEnvironment/FeatureOverviewEnvironmentToggle.tsx +++ b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/EnvironmentHeader/FeatureOverviewEnvironmentToggle/FeatureOverviewEnvironmentToggle.tsx @@ -1,3 +1,4 @@ +import { styled } from '@mui/material'; import { useFeatureToggleSwitch } from 'component/project/Project/ProjectFeatureToggles/FeatureToggleSwitch/useFeatureToggleSwitch'; import { FeatureToggleSwitch } from 'component/project/Project/ProjectFeatureToggles/FeatureToggleSwitch/FeatureToggleSwitch'; import { useChangeRequestsEnabled } from 'hooks/useChangeRequestsEnabled'; @@ -5,13 +6,21 @@ import { useFeature } from 'hooks/api/getters/useFeature/useFeature'; import { useRequiredPathParam } from 'hooks/useRequiredPathParam'; import type { IFeatureEnvironment } from 'interfaces/featureToggle'; -interface IFeatureOverviewEnvironmentToggleProps { - environment: IFeatureEnvironment; -} +const StyledContainer = styled('div')(({ theme }) => ({ + order: -1, + flex: 0, +})); + +type FeatureOverviewEnvironmentToggleProps = { + environment: Pick< + IFeatureEnvironment, + 'name' | 'type' | 'strategies' | 'enabled' + >; +}; export const FeatureOverviewEnvironmentToggle = ({ environment: { name, type, strategies, enabled }, -}: IFeatureOverviewEnvironmentToggleProps) => { +}: FeatureOverviewEnvironmentToggleProps) => { const projectId = useRequiredPathParam('projectId'); const featureId = useRequiredPathParam('featureId'); const { refetchFeature } = useFeature(projectId, featureId); @@ -37,7 +46,7 @@ export const FeatureOverviewEnvironmentToggle = ({ }); return ( - <> + event.stopPropagation()}> {featureToggleModals} - + ); }; 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 71ed62ce9b..8293f9a2db 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 @@ -1,34 +1,66 @@ +import type { ReactNode } from 'react'; import { screen } from '@testing-library/react'; import { render } from 'utils/testRenderer'; -import FeatureOverviewEnvironment from './FeatureOverviewEnvironment'; +import { FeatureOverviewEnvironment } from './FeatureOverviewEnvironment'; import { Route, Routes } from 'react-router-dom'; import { CREATE_FEATURE_STRATEGY } from 'component/providers/AccessProvider/permissions'; -const environmentWithoutStrategies = { - name: 'production', - enabled: true, - type: 'production', - strategies: [], -}; - -test('should allow to add strategy', async () => { +const renderRoute = (element: ReactNode, permissions: any[] = []) => render( - } + element={element} /> , { - route: '/projects/default/features/featureWithoutStrategies/strategies/create', - permissions: [{ permission: CREATE_FEATURE_STRATEGY }], + route: '/projects/default/features/featureId/strategies/create', + permissions, }, ); - const button = await screen.findByText('Add strategy'); - expect(button).toBeEnabled(); +describe('FeatureOverviewEnvironment', () => { + test('should allow to add strategy', async () => { + renderRoute( + , + [{ permission: CREATE_FEATURE_STRATEGY }], + ); + + const button = await screen.findByText('Add strategy'); + expect(button).toBeEnabled(); + }); + + test("should disable add button if permissions don't allow for it", async () => { + render( + + + } + /> + , + { + route: '/projects/default/features/featureWithoutStrategies/strategies/create', + permissions: [], + }, + ); + + const button = await screen.findByText('Add strategy'); + expect(button).toBeDisabled(); + }); }); 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 b813df9b6e..60687a70c5 100644 --- a/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/FeatureOverviewEnvironment.tsx +++ b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/FeatureOverviewEnvironment.tsx @@ -1,249 +1,138 @@ -import { - Accordion, - AccordionDetails, - AccordionSummary, - Box, - styled, -} from '@mui/material'; -import ExpandMore from '@mui/icons-material/ExpandMore'; -import { useFeature } from 'hooks/api/getters/useFeature/useFeature'; -import useFeatureMetrics from 'hooks/api/getters/useFeatureMetrics/useFeatureMetrics'; -import type { IFeatureEnvironment } from 'interfaces/featureToggle'; -import { getFeatureMetrics } from 'utils/getFeatureMetrics'; -import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; -import EnvironmentIcon from 'component/common/EnvironmentIcon/EnvironmentIcon'; -import StringTruncator from 'component/common/StringTruncator/StringTruncator'; +import { Accordion, AccordionDetails, styled } from '@mui/material'; +import type { + IFeatureEnvironment, + IFeatureEnvironmentMetrics, +} from 'interfaces/featureToggle'; import EnvironmentAccordionBody from './EnvironmentAccordionBody/EnvironmentAccordionBody'; -import { EnvironmentFooter } from './EnvironmentFooter/EnvironmentFooter'; -import FeatureOverviewEnvironmentMetrics from './FeatureOverviewEnvironmentMetrics/FeatureOverviewEnvironmentMetrics'; import { FeatureStrategyMenu } from 'component/feature/FeatureStrategy/FeatureStrategyMenu/FeatureStrategyMenu'; import { FEATURE_ENVIRONMENT_ACCORDION } from 'utils/testIds'; import { useRequiredPathParam } from 'hooks/useRequiredPathParam'; -import { FeatureStrategyIcons } from 'component/feature/FeatureStrategy/FeatureStrategyIcons/FeatureStrategyIcons'; -import { useGlobalLocalStorage } from 'hooks/useGlobalLocalStorage'; -import { Badge } from 'component/common/Badge/Badge'; -import { UpgradeChangeRequests } from './UpgradeChangeRequests'; +import { UpgradeChangeRequests } from './UpgradeChangeRequests/UpgradeChangeRequests'; import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig'; +import { EnvironmentHeader } from './EnvironmentHeader/EnvironmentHeader'; +import FeatureOverviewEnvironmentMetrics from './EnvironmentHeader/FeatureOverviewEnvironmentMetrics/FeatureOverviewEnvironmentMetrics'; +import { FeatureOverviewEnvironmentToggle } from './EnvironmentHeader/FeatureOverviewEnvironmentToggle/FeatureOverviewEnvironmentToggle'; +import { useState } from 'react'; +import type { IReleasePlan } from 'interfaces/releasePlans'; -interface IFeatureOverviewEnvironmentProps { - env: IFeatureEnvironment; -} - -const StyledFeatureOverviewEnvironment = styled('div', { - shouldForwardProp: (prop) => prop !== 'enabled', -})<{ enabled: boolean }>(({ theme, enabled }) => ({ +const StyledFeatureOverviewEnvironment = styled('div')(({ theme }) => ({ borderRadius: theme.shape.borderRadiusLarge, - marginBottom: theme.spacing(2), - backgroundColor: enabled - ? theme.palette.background.paper - : theme.palette.envAccordion.disabled, + backgroundColor: theme.palette.background.paper, + border: `1px solid ${theme.palette.divider}`, })); -const StyledAccordion = styled(Accordion)({ +const StyledAccordion = styled(Accordion)(({ theme }) => ({ boxShadow: 'none', background: 'none', -}); - -const StyledAccordionSummary = styled(AccordionSummary)(({ theme }) => ({ - boxShadow: 'none', - padding: theme.spacing(2, 4), - [theme.breakpoints.down(400)]: { - padding: theme.spacing(1, 2), + '&&& .MuiAccordionSummary-root': { + borderRadius: theme.shape.borderRadiusLarge, + pointerEvents: 'auto', + opacity: 1, + backgroundColor: theme.palette.background.paper, }, })); -const StyledAccordionDetails = styled(AccordionDetails, { - shouldForwardProp: (prop) => prop !== 'enabled', -})<{ enabled: boolean }>(({ theme }) => ({ - padding: theme.spacing(3), +const StyledAccordionDetails = styled(AccordionDetails)(({ theme }) => ({ + padding: 0, background: theme.palette.envAccordion.expanded, borderBottomLeftRadius: theme.shape.borderRadiusLarge, borderBottomRightRadius: theme.shape.borderRadiusLarge, boxShadow: theme.boxShadows.accordionFooter, - [theme.breakpoints.down('md')]: { padding: theme.spacing(2, 1), }, })); -const StyledEnvironmentAccordionBody = styled(EnvironmentAccordionBody)( - ({ theme }) => ({ - width: '100%', - position: 'relative', - paddingBottom: theme.spacing(2), - }), -); - -const StyledHeader = styled('div', { - shouldForwardProp: (prop) => prop !== 'enabled', -})<{ enabled: boolean }>(({ theme, enabled }) => ({ +const StyledAccordionFooter = styled('footer')(({ theme }) => ({ + padding: theme.spacing(2, 3, 3), display: 'flex', - justifyContent: 'center', flexDirection: 'column', - color: enabled ? theme.palette.text.primary : theme.palette.text.secondary, -})); - -const StyledHeaderTitle = styled('div')(({ theme }) => ({ - display: 'flex', - alignItems: 'center', - [theme.breakpoints.down(560)]: { - flexDirection: 'column', - textAlign: 'center', - }, -})); - -const StyledEnvironmentIcon = styled(EnvironmentIcon)(({ theme }) => ({ - [theme.breakpoints.down(560)]: { - marginBottom: '0.5rem', - }, -})); - -const StyledStringTruncator = styled(StringTruncator)(({ theme }) => ({ - fontSize: theme.fontSizes.bodySize, - fontWeight: theme.typography.fontWeightMedium, - [theme.breakpoints.down(560)]: { - textAlign: 'center', - }, -})); - -const StyledButtonContainer = styled('div')(({ theme }) => ({ - display: 'flex', - alignItems: 'center', - marginTop: theme.spacing(2), + alignItems: 'flex-end', gap: theme.spacing(2), - flexWrap: 'wrap', - [theme.breakpoints.down(560)]: { - flexDirection: 'column', - }, + borderTop: `1px solid ${theme.palette.divider}`, })); -const FeatureOverviewEnvironment = ({ - env, -}: IFeatureOverviewEnvironmentProps) => { - const projectId = useRequiredPathParam('projectId'); - const featureId = useRequiredPathParam('featureId'); - const { metrics } = useFeatureMetrics(projectId, featureId); - const { feature } = useFeature(projectId, featureId); - const { value: globalStore } = useGlobalLocalStorage(); +const StyledEnvironmentAccordionContainer = styled('div')(({ theme }) => ({ + width: '100%', + position: 'relative', + padding: theme.spacing(3, 3, 1), +})); - const featureMetrics = getFeatureMetrics(feature?.environments, metrics); - const environmentMetric = featureMetrics.find( - (featureMetric) => featureMetric.environment === env.name, - ); - const featureEnvironment = feature?.environments.find( - (featureEnvironment) => featureEnvironment.name === env.name, - ); - const { isOss } = useUiConfig(); - const showChangeRequestUpgrade = env.type === 'production' && isOss(); - - return ( - - - } - > - - - -
- -
- - Disabled - - } - /> -
- - - - -
- - -
- - - name) - .filter((name) => name !== env.name)} - /> - 0 - } - show={ - <> - - - - - {showChangeRequestUpgrade ? ( - - ) : null} - - } - /> - -
- - } - /> - ); +type FeatureOverviewEnvironmentProps = { + environment: IFeatureEnvironment & { + releasePlans?: IReleasePlan[]; + }; + metrics?: Pick; + otherEnvironments?: string[]; }; -export default FeatureOverviewEnvironment; +export const FeatureOverviewEnvironment = ({ + environment, + metrics = { yes: 0, no: 0 }, + otherEnvironments = [], +}: FeatureOverviewEnvironmentProps) => { + const [isOpen, setIsOopen] = useState(false); + const projectId = useRequiredPathParam('projectId'); + const featureId = useRequiredPathParam('featureId'); + const { isOss } = useUiConfig(); + const hasActivations = Boolean( + environment?.enabled || + (environment?.strategies && environment?.strategies.length > 0) || + (environment?.releasePlans && environment?.releasePlans.length > 0), + ); + + return ( + + setIsOopen(isOpen ? !isOpen : hasActivations)} + > + + + {!hasActivations ? ( + + ) : null} + + + + + + + + + {isOss() && environment?.type === 'production' ? ( + + ) : null} + + + + + ); +}; diff --git a/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/LegacyFeatureOverviewEnvironment.tsx b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/LegacyFeatureOverviewEnvironment.tsx new file mode 100644 index 0000000000..6fcf87eb84 --- /dev/null +++ b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/LegacyFeatureOverviewEnvironment.tsx @@ -0,0 +1,252 @@ +import { + Accordion, + AccordionDetails, + AccordionSummary, + Box, + styled, +} from '@mui/material'; +import ExpandMore from '@mui/icons-material/ExpandMore'; +import { useFeature } from 'hooks/api/getters/useFeature/useFeature'; +import useFeatureMetrics from 'hooks/api/getters/useFeatureMetrics/useFeatureMetrics'; +import type { IFeatureEnvironment } from 'interfaces/featureToggle'; +import { getFeatureMetrics } from 'utils/getFeatureMetrics'; +import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; +import EnvironmentIcon from 'component/common/EnvironmentIcon/EnvironmentIcon'; +import StringTruncator from 'component/common/StringTruncator/StringTruncator'; +import EnvironmentAccordionBody from './EnvironmentAccordionBody/EnvironmentAccordionBody'; +import { EnvironmentFooter } from './EnvironmentFooter/EnvironmentFooter'; +import FeatureOverviewEnvironmentMetrics from './EnvironmentHeader/FeatureOverviewEnvironmentMetrics/LegacyFeatureOverviewEnvironmentMetrics'; +import { FeatureStrategyMenu } from 'component/feature/FeatureStrategy/FeatureStrategyMenu/FeatureStrategyMenu'; +import { FEATURE_ENVIRONMENT_ACCORDION } from 'utils/testIds'; +import { useRequiredPathParam } from 'hooks/useRequiredPathParam'; +import { FeatureStrategyIcons } from 'component/feature/FeatureStrategy/FeatureStrategyIcons/FeatureStrategyIcons'; +import { useGlobalLocalStorage } from 'hooks/useGlobalLocalStorage'; +import { Badge } from 'component/common/Badge/Badge'; +import { UpgradeChangeRequests } from './UpgradeChangeRequests/UpgradeChangeRequests'; +import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig'; + +interface IFeatureOverviewEnvironmentProps { + env: IFeatureEnvironment; +} + +const StyledFeatureOverviewEnvironment = styled('div', { + shouldForwardProp: (prop) => prop !== 'enabled', +})<{ enabled: boolean }>(({ theme, enabled }) => ({ + borderRadius: theme.shape.borderRadiusLarge, + marginBottom: theme.spacing(2), + backgroundColor: enabled + ? theme.palette.background.paper + : theme.palette.envAccordion.disabled, +})); + +const StyledAccordion = styled(Accordion)({ + boxShadow: 'none', + background: 'none', +}); + +const StyledAccordionSummary = styled(AccordionSummary)(({ theme }) => ({ + boxShadow: 'none', + padding: theme.spacing(2, 4), + [theme.breakpoints.down(400)]: { + padding: theme.spacing(1, 2), + }, +})); + +const StyledAccordionDetails = styled(AccordionDetails, { + shouldForwardProp: (prop) => prop !== 'enabled', +})<{ enabled: boolean }>(({ theme }) => ({ + padding: theme.spacing(3), + background: theme.palette.envAccordion.expanded, + borderBottomLeftRadius: theme.shape.borderRadiusLarge, + borderBottomRightRadius: theme.shape.borderRadiusLarge, + boxShadow: theme.boxShadows.accordionFooter, + + [theme.breakpoints.down('md')]: { + padding: theme.spacing(2, 1), + }, +})); + +const StyledEnvironmentAccordionBody = styled(EnvironmentAccordionBody)( + ({ theme }) => ({ + width: '100%', + position: 'relative', + paddingBottom: theme.spacing(2), + }), +); + +const StyledHeader = styled('div', { + shouldForwardProp: (prop) => prop !== 'enabled', +})<{ enabled: boolean }>(({ theme, enabled }) => ({ + display: 'flex', + justifyContent: 'center', + flexDirection: 'column', + color: enabled ? theme.palette.text.primary : theme.palette.text.secondary, +})); + +const StyledHeaderTitle = styled('div')(({ theme }) => ({ + display: 'flex', + alignItems: 'center', + [theme.breakpoints.down(560)]: { + flexDirection: 'column', + textAlign: 'center', + }, +})); + +const StyledEnvironmentIcon = styled(EnvironmentIcon)(({ theme }) => ({ + [theme.breakpoints.down(560)]: { + marginBottom: '0.5rem', + }, +})); + +const StyledStringTruncator = styled(StringTruncator)(({ theme }) => ({ + fontSize: theme.fontSizes.bodySize, + fontWeight: theme.typography.fontWeightMedium, + [theme.breakpoints.down(560)]: { + textAlign: 'center', + }, +})); + +const StyledButtonContainer = styled('div')(({ theme }) => ({ + display: 'flex', + alignItems: 'center', + marginTop: theme.spacing(2), + gap: theme.spacing(2), + flexWrap: 'wrap', + [theme.breakpoints.down(560)]: { + flexDirection: 'column', + }, +})); + +/** + * @deprecated remove this file with `flagOverviewRedesign` + */ +const FeatureOverviewEnvironment = ({ + env, +}: IFeatureOverviewEnvironmentProps) => { + const projectId = useRequiredPathParam('projectId'); + const featureId = useRequiredPathParam('featureId'); + const { metrics } = useFeatureMetrics(projectId, featureId); + const { feature } = useFeature(projectId, featureId); + const { value: globalStore } = useGlobalLocalStorage(); + + const featureMetrics = getFeatureMetrics(feature?.environments, metrics); + const environmentMetric = featureMetrics.find( + (featureMetric) => featureMetric.environment === env.name, + ); + const featureEnvironment = feature?.environments.find( + (featureEnvironment) => featureEnvironment.name === env.name, + ); + const { isOss } = useUiConfig(); + const showChangeRequestUpgrade = env.type === 'production' && isOss(); + + return ( + + + } + > + + + +
+ +
+ + Disabled + + } + /> +
+ + + + +
+ + +
+ + + name) + .filter((name) => name !== env.name)} + /> + 0 + } + show={ + <> + + + + + {showChangeRequestUpgrade ? ( + + ) : null} + + } + /> + +
+ + } + /> + ); +}; + +export default FeatureOverviewEnvironment; diff --git a/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/UpgradeChangeRequests.tsx b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/UpgradeChangeRequests/UpgradeChangeRequests.tsx similarity index 100% rename from frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/UpgradeChangeRequests.tsx rename to frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/UpgradeChangeRequests/UpgradeChangeRequests.tsx diff --git a/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironments.tsx b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironments.tsx index dc72ad5fc7..ad011a1290 100644 --- a/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironments.tsx +++ b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironments.tsx @@ -1,23 +1,79 @@ +import type { ComponentProps, FC } from 'react'; +import { useUiFlag } from 'hooks/useUiFlag'; import { useFeature } from 'hooks/api/getters/useFeature/useFeature'; -import FeatureOverviewEnvironment from './FeatureOverviewEnvironment/FeatureOverviewEnvironment'; +import { FeatureOverviewEnvironment } from './FeatureOverviewEnvironment/FeatureOverviewEnvironment'; +import LegacyFeatureOverviewEnvironment from './FeatureOverviewEnvironment/LegacyFeatureOverviewEnvironment'; import { useRequiredPathParam } from 'hooks/useRequiredPathParam'; +import useFeatureMetrics from 'hooks/api/getters/useFeatureMetrics/useFeatureMetrics'; +import { getFeatureMetrics } from 'utils/getFeatureMetrics'; +import { useReleasePlans } from 'hooks/api/getters/useReleasePlans/useReleasePlans'; -const FeatureOverviewEnvironments = () => { +type FeatureOverviewEnvironmentsProps = { + hiddenEnvironments?: string[]; +}; + +const FeatureOverviewWithReleasePlans: FC< + ComponentProps +> = ({ environment, ...props }) => { const projectId = useRequiredPathParam('projectId'); const featureId = useRequiredPathParam('featureId'); - const { feature } = useFeature(projectId, featureId); - - if (!feature) return null; - - const { environments } = feature; + const { releasePlans } = useReleasePlans( + projectId, + featureId, + environment?.name, + ); return ( - <> - {environments?.map((env) => ( - - ))} - + ); }; -export default FeatureOverviewEnvironments; +export const FeatureOverviewEnvironments: FC< + FeatureOverviewEnvironmentsProps +> = ({ hiddenEnvironments = [] }) => { + const projectId = useRequiredPathParam('projectId'); + const featureId = useRequiredPathParam('featureId'); + const { feature } = useFeature(projectId, featureId); + const { metrics } = useFeatureMetrics(projectId, featureId); + const featureMetrics = getFeatureMetrics(feature?.environments, metrics); + const flagOverviewRedesign = useUiFlag('flagOverviewRedesign'); + + if (!feature) return null; + + if (!flagOverviewRedesign) { + return ( + <> + {feature.environments?.map((env) => ( + + ))} + + ); + } + + return feature.environments + ?.filter((env) => !hiddenEnvironments.includes(env.name)) + .map((env) => ( + featureMetric.environment === env?.name, + )} + otherEnvironments={ + feature.environments + ?.map((e) => e.name) + .filter( + (name) => + name !== env.name && + !hiddenEnvironments.includes(name), + ) || [] + } + /> + )); +}; diff --git a/frontend/src/component/feature/FeatureView/FeatureOverview/LegacyFeatureOverview.tsx b/frontend/src/component/feature/FeatureView/FeatureOverview/LegacyFeatureOverview.tsx index c3762c4fd8..c44fdda5fd 100644 --- a/frontend/src/component/feature/FeatureView/FeatureOverview/LegacyFeatureOverview.tsx +++ b/frontend/src/component/feature/FeatureView/FeatureOverview/LegacyFeatureOverview.tsx @@ -1,4 +1,4 @@ -import FeatureOverviewEnvironments from './FeatureOverviewEnvironments/FeatureOverviewEnvironments'; +import { FeatureOverviewEnvironments } from './FeatureOverviewEnvironments/FeatureOverviewEnvironments'; import { Route, Routes, useNavigate } from 'react-router-dom'; import { SidebarModal } from 'component/common/SidebarModal/SidebarModal'; import { diff --git a/frontend/src/component/feature/FeatureView/FeatureOverview/NewFeatureOverviewEnvironment/FeatureOverviewEnvironmentBody.tsx b/frontend/src/component/feature/FeatureView/FeatureOverview/NewFeatureOverviewEnvironment/FeatureOverviewEnvironmentBody.tsx index 720c2d5fa9..93e4422260 100644 --- a/frontend/src/component/feature/FeatureView/FeatureOverview/NewFeatureOverviewEnvironment/FeatureOverviewEnvironmentBody.tsx +++ b/frontend/src/component/feature/FeatureView/FeatureOverview/NewFeatureOverviewEnvironment/FeatureOverviewEnvironmentBody.tsx @@ -51,6 +51,9 @@ const StyledBadge = styled(Badge)(({ theme }) => ({ color: theme.palette.common.white, })); +/** + * @deprecated initial version, clean up after done with `flagOverviewRedesign` + */ export const FeatureOverviewEnvironmentBody = ({ featureEnvironment, isDisabled, diff --git a/frontend/src/component/feature/FeatureView/FeatureOverview/NewFeatureOverviewEnvironment/NewFeatureOverviewEnvironment.tsx b/frontend/src/component/feature/FeatureView/FeatureOverview/NewFeatureOverviewEnvironment/NewFeatureOverviewEnvironment.tsx index 8392df574b..6098bf87cc 100644 --- a/frontend/src/component/feature/FeatureView/FeatureOverview/NewFeatureOverviewEnvironment/NewFeatureOverviewEnvironment.tsx +++ b/frontend/src/component/feature/FeatureView/FeatureOverview/NewFeatureOverviewEnvironment/NewFeatureOverviewEnvironment.tsx @@ -3,10 +3,10 @@ import { useFeature } from 'hooks/api/getters/useFeature/useFeature'; import useFeatureMetrics from 'hooks/api/getters/useFeatureMetrics/useFeatureMetrics'; import { getFeatureMetrics } from 'utils/getFeatureMetrics'; import { FeatureOverviewEnvironmentBody } from './FeatureOverviewEnvironmentBody'; -import FeatureOverviewEnvironmentMetrics from '../FeatureOverviewEnvironments/FeatureOverviewEnvironment/FeatureOverviewEnvironmentMetrics/FeatureOverviewEnvironmentMetrics'; +import FeatureOverviewEnvironmentMetrics from '../FeatureOverviewEnvironments/FeatureOverviewEnvironment/EnvironmentHeader/FeatureOverviewEnvironmentMetrics/LegacyFeatureOverviewEnvironmentMetrics'; import { FeatureStrategyMenu } from 'component/feature/FeatureStrategy/FeatureStrategyMenu/FeatureStrategyMenu'; import { useRequiredPathParam } from 'hooks/useRequiredPathParam'; -import { FeatureOverviewEnvironmentToggle } from './FeatureOverviewEnvironmentToggle'; +import { FeatureOverviewEnvironmentToggle } from '../FeatureOverviewEnvironments/FeatureOverviewEnvironment/EnvironmentHeader/FeatureOverviewEnvironmentToggle/FeatureOverviewEnvironmentToggle'; const StyledFeatureOverviewEnvironment = styled('div')(({ theme }) => ({ padding: theme.spacing(1, 3), @@ -55,6 +55,9 @@ interface INewFeatureOverviewEnvironmentProps { hiddenEnvironments: string[]; } +/** + * @deprecated initial version, clean up after done with `flagOverviewRedesign` + */ export const FeatureOverviewEnvironment = ({ hiddenEnvironments, }: INewFeatureOverviewEnvironmentProps) => { diff --git a/frontend/src/themes/theme.ts b/frontend/src/themes/theme.ts index 05233a7f6c..82d57a640d 100644 --- a/frontend/src/themes/theme.ts +++ b/frontend/src/themes/theme.ts @@ -509,13 +509,13 @@ export const lightTheme = createTheme({ }, }, - // Environment accordion MuiAccordion: { styleOverrides: { root: ({ theme }) => ({ '&:first-of-type, &:last-of-type': { borderRadius: theme.shape.borderRadiusLarge, }, + // Environment accordion -- remove with `flagOverviewRedesign` flag '&.environment-accordion.Mui-expanded': { outline: `2px solid ${alpha( theme.palette.background.alternative, diff --git a/frontend/src/utils/getFeatureMetrics.ts b/frontend/src/utils/getFeatureMetrics.ts index edb6d97ea5..5164ed409c 100644 --- a/frontend/src/utils/getFeatureMetrics.ts +++ b/frontend/src/utils/getFeatureMetrics.ts @@ -13,11 +13,10 @@ const emptyMetric = (environment: string) => ({ export const getFeatureMetrics = ( environments: IFeatureEnvironment[], metrics: IFeatureMetrics, -) => { - return environments.map((env) => { +) => + environments.map((env) => { const envMetric = metrics.lastHourUsage.find( (metric) => metric.environment === env.name, ); return envMetric || emptyMetric(env.name); }); -};