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);
});
-};