mirror of
				https://github.com/Unleash/unleash.git
				synced 2025-10-27 11:02:16 +01:00 
			
		
		
		
	chore: flag overview page redesign - environments (#8683)
https://linear.app/unleash/issue/2-2826/enabling-environment-via-feature-flag-environment-section-header https://linear.app/unleash/issue/2-2825/feature-flag-list-bottom-left-to-be-a-nav-section Follow-up to: https://github.com/Unleash/unleash/pull/8663 Implements most of the remaining work for our flag overview page redesign. Most of the code you see is a straight copy/paste from our older existing components, with the slight improvement here and there. Includes some improvements to our vertical tabs component to suit our use case. Also updates the Demo flow accordingly. I did some manual tests and it seems to work decently in both scenarios, whether `flagOverviewRedesign` is enabled or not. The demo needs some love but that's a story for a different PR and a different time. Once again, due to the duplicate file pattern, we should remember to clean this up if we decide to remove the flag. <img width="1086" alt="image" src="https://github.com/user-attachments/assets/0c375e34-cbb5-4ac4-a764-39a36b6c6781">
This commit is contained in:
		
							parent
							
								
									7597bb91ac
								
							
						
					
					
						commit
						b4fde58fa0
					
				@ -40,6 +40,7 @@ describe('demo', () => {
 | 
			
		||||
                    res.body.flags = {
 | 
			
		||||
                        ...res.body.flags,
 | 
			
		||||
                        demo: true,
 | 
			
		||||
                        flagOverviewRedesign: true,
 | 
			
		||||
                    };
 | 
			
		||||
                }
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
@ -1,6 +1,7 @@
 | 
			
		||||
///<reference path="../../global.d.ts" />
 | 
			
		||||
 | 
			
		||||
describe('feature', () => {
 | 
			
		||||
    const baseUrl = Cypress.config().baseUrl;
 | 
			
		||||
    const randomId = String(Math.random()).split('.')[1];
 | 
			
		||||
    const featureToggleName = `unleash-e2e-${randomId}`;
 | 
			
		||||
    const projectName = `unleash-e2e-project-${randomId}`;
 | 
			
		||||
@ -35,6 +36,19 @@ describe('feature', () => {
 | 
			
		||||
    beforeEach(() => {
 | 
			
		||||
        cy.login_UI();
 | 
			
		||||
        cy.visit('/features');
 | 
			
		||||
 | 
			
		||||
        cy.intercept('GET', `${baseUrl}/api/admin/ui-config`, (req) => {
 | 
			
		||||
            req.headers['cache-control'] =
 | 
			
		||||
                'no-cache, no-store, must-revalidate';
 | 
			
		||||
            req.on('response', (res) => {
 | 
			
		||||
                if (res.body) {
 | 
			
		||||
                    res.body.flags = {
 | 
			
		||||
                        ...res.body.flags,
 | 
			
		||||
                        flagOverviewRedesign: true,
 | 
			
		||||
                    };
 | 
			
		||||
                }
 | 
			
		||||
            });
 | 
			
		||||
        });
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('can create a feature flag', () => {
 | 
			
		||||
 | 
			
		||||
@ -227,7 +227,6 @@ export const deleteFeatureStrategy_UI = (
 | 
			
		||||
        },
 | 
			
		||||
    ).as('deleteUserStrategy');
 | 
			
		||||
    cy.visit(`/projects/${project}/features/${featureToggleName}`);
 | 
			
		||||
    cy.get('[data-testid=FEATURE_ENVIRONMENT_ACCORDION_development]').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();
 | 
			
		||||
 | 
			
		||||
@ -1,4 +1,5 @@
 | 
			
		||||
import { Button, styled } from '@mui/material';
 | 
			
		||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
 | 
			
		||||
 | 
			
		||||
const StyledTab = styled(Button)<{ selected: boolean }>(
 | 
			
		||||
    ({ theme, selected }) => ({
 | 
			
		||||
@ -17,7 +18,8 @@ const StyledTab = styled(Button)<{ selected: boolean }>(
 | 
			
		||||
            transition: 'background-color 0.2s ease',
 | 
			
		||||
            color: theme.palette.text.primary,
 | 
			
		||||
            textAlign: 'left',
 | 
			
		||||
            padding: theme.spacing(2, 4),
 | 
			
		||||
            padding: theme.spacing(0, 2),
 | 
			
		||||
            gap: theme.spacing(1),
 | 
			
		||||
            fontSize: theme.fontSizes.bodySize,
 | 
			
		||||
            fontWeight: selected
 | 
			
		||||
                ? theme.fontWeight.bold
 | 
			
		||||
@ -41,27 +43,53 @@ const StyledTab = styled(Button)<{ selected: boolean }>(
 | 
			
		||||
    }),
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
const StyledTabLabel = styled('div')(({ theme }) => ({
 | 
			
		||||
    display: 'flex',
 | 
			
		||||
    flexDirection: 'column',
 | 
			
		||||
    gap: theme.spacing(0.5),
 | 
			
		||||
}));
 | 
			
		||||
 | 
			
		||||
const StyledTabDescription = styled('div')(({ theme }) => ({
 | 
			
		||||
    fontWeight: theme.fontWeight.medium,
 | 
			
		||||
    fontSize: theme.fontSizes.smallBody,
 | 
			
		||||
    color: theme.palette.text.secondary,
 | 
			
		||||
}));
 | 
			
		||||
 | 
			
		||||
interface IVerticalTabProps {
 | 
			
		||||
    label: string;
 | 
			
		||||
    description?: string;
 | 
			
		||||
    selected?: boolean;
 | 
			
		||||
    onClick: () => void;
 | 
			
		||||
    icon?: React.ReactNode;
 | 
			
		||||
    startIcon?: React.ReactNode;
 | 
			
		||||
    endIcon?: React.ReactNode;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const VerticalTab = ({
 | 
			
		||||
    label,
 | 
			
		||||
    description,
 | 
			
		||||
    selected,
 | 
			
		||||
    onClick,
 | 
			
		||||
    icon,
 | 
			
		||||
    startIcon,
 | 
			
		||||
    endIcon,
 | 
			
		||||
}: IVerticalTabProps) => (
 | 
			
		||||
    <StyledTab
 | 
			
		||||
        selected={Boolean(selected)}
 | 
			
		||||
        className={selected ? 'selected' : ''}
 | 
			
		||||
        onClick={onClick}
 | 
			
		||||
        disableElevation
 | 
			
		||||
        disableFocusRipple
 | 
			
		||||
        fullWidth
 | 
			
		||||
    >
 | 
			
		||||
        {label}
 | 
			
		||||
        {icon}
 | 
			
		||||
        {startIcon}
 | 
			
		||||
        <StyledTabLabel>
 | 
			
		||||
            {label}
 | 
			
		||||
            <ConditionallyRender
 | 
			
		||||
                condition={Boolean(description)}
 | 
			
		||||
                show={
 | 
			
		||||
                    <StyledTabDescription>{description}</StyledTabDescription>
 | 
			
		||||
                }
 | 
			
		||||
            />
 | 
			
		||||
        </StyledTabLabel>
 | 
			
		||||
        {endIcon}
 | 
			
		||||
    </StyledTab>
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
@ -1,5 +1,6 @@
 | 
			
		||||
import { styled } from '@mui/material';
 | 
			
		||||
import { VerticalTab } from './VerticalTab/VerticalTab';
 | 
			
		||||
import type { HTMLAttributes } from 'react';
 | 
			
		||||
 | 
			
		||||
const StyledTabPage = styled('div')(({ theme }) => ({
 | 
			
		||||
    display: 'flex',
 | 
			
		||||
@ -15,11 +16,13 @@ const StyledTabPageContent = styled('div')(() => ({
 | 
			
		||||
    flexDirection: 'column',
 | 
			
		||||
}));
 | 
			
		||||
 | 
			
		||||
const StyledTabs = styled('div')(({ theme }) => ({
 | 
			
		||||
const StyledTabs = styled('div', {
 | 
			
		||||
    shouldForwardProp: (prop) => prop !== 'fullWidth',
 | 
			
		||||
})<{ fullWidth?: boolean }>(({ theme, fullWidth }) => ({
 | 
			
		||||
    display: 'flex',
 | 
			
		||||
    flexDirection: 'column',
 | 
			
		||||
    gap: theme.spacing(1),
 | 
			
		||||
    width: theme.spacing(30),
 | 
			
		||||
    width: fullWidth ? '100%' : theme.spacing(30),
 | 
			
		||||
    flexShrink: 0,
 | 
			
		||||
    [theme.breakpoints.down('xl')]: {
 | 
			
		||||
        width: '100%',
 | 
			
		||||
@ -29,16 +32,19 @@ const StyledTabs = styled('div')(({ theme }) => ({
 | 
			
		||||
export interface ITab {
 | 
			
		||||
    id: string;
 | 
			
		||||
    label: string;
 | 
			
		||||
    description?: string;
 | 
			
		||||
    path?: string;
 | 
			
		||||
    hidden?: boolean;
 | 
			
		||||
    icon?: React.ReactNode;
 | 
			
		||||
    startIcon?: React.ReactNode;
 | 
			
		||||
    endIcon?: React.ReactNode;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
interface IVerticalTabsProps {
 | 
			
		||||
interface IVerticalTabsProps
 | 
			
		||||
    extends Omit<HTMLAttributes<HTMLDivElement>, 'onChange'> {
 | 
			
		||||
    tabs: ITab[];
 | 
			
		||||
    value: string;
 | 
			
		||||
    onChange: (tab: ITab) => void;
 | 
			
		||||
    children: React.ReactNode;
 | 
			
		||||
    children?: React.ReactNode;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const VerticalTabs = ({
 | 
			
		||||
@ -46,21 +52,33 @@ export const VerticalTabs = ({
 | 
			
		||||
    value,
 | 
			
		||||
    onChange,
 | 
			
		||||
    children,
 | 
			
		||||
}: IVerticalTabsProps) => (
 | 
			
		||||
    <StyledTabPage>
 | 
			
		||||
        <StyledTabs>
 | 
			
		||||
            {tabs
 | 
			
		||||
                .filter((tab) => !tab.hidden)
 | 
			
		||||
                .map((tab) => (
 | 
			
		||||
                    <VerticalTab
 | 
			
		||||
                        key={tab.id}
 | 
			
		||||
                        label={tab.label}
 | 
			
		||||
                        selected={tab.id === value}
 | 
			
		||||
                        onClick={() => onChange(tab)}
 | 
			
		||||
                        icon={tab.icon}
 | 
			
		||||
                    />
 | 
			
		||||
                ))}
 | 
			
		||||
        </StyledTabs>
 | 
			
		||||
        <StyledTabPageContent>{children}</StyledTabPageContent>
 | 
			
		||||
    </StyledTabPage>
 | 
			
		||||
);
 | 
			
		||||
    ...props
 | 
			
		||||
}: IVerticalTabsProps) => {
 | 
			
		||||
    const verticalTabs = tabs
 | 
			
		||||
        .filter((tab) => !tab.hidden)
 | 
			
		||||
        .map((tab) => (
 | 
			
		||||
            <VerticalTab
 | 
			
		||||
                key={tab.id}
 | 
			
		||||
                label={tab.label}
 | 
			
		||||
                description={tab.description}
 | 
			
		||||
                selected={tab.id === value}
 | 
			
		||||
                onClick={() => onChange(tab)}
 | 
			
		||||
                startIcon={tab.startIcon}
 | 
			
		||||
                endIcon={tab.endIcon}
 | 
			
		||||
            />
 | 
			
		||||
        ));
 | 
			
		||||
 | 
			
		||||
    if (!children) {
 | 
			
		||||
        return (
 | 
			
		||||
            <StyledTabs fullWidth {...props}>
 | 
			
		||||
                {verticalTabs}
 | 
			
		||||
            </StyledTabs>
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
    return (
 | 
			
		||||
        <StyledTabPage>
 | 
			
		||||
            <StyledTabs {...props}>{verticalTabs}</StyledTabs>
 | 
			
		||||
            <StyledTabPageContent>{children}</StyledTabPageContent>
 | 
			
		||||
        </StyledTabPage>
 | 
			
		||||
    );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
@ -131,7 +131,7 @@ export const TOPICS: ITutorialTopic[] = [
 | 
			
		||||
            },
 | 
			
		||||
            {
 | 
			
		||||
                href: `/projects/${PROJECT}/features/demoApp.step2`,
 | 
			
		||||
                target: `div[data-testid="FEATURE_ENVIRONMENT_ACCORDION_${ENVIRONMENT}"] button`,
 | 
			
		||||
                target: 'button[data-testid="ADD_STRATEGY_BUTTON"]',
 | 
			
		||||
                content: (
 | 
			
		||||
                    <Description>
 | 
			
		||||
                        Add a new strategy to this environment by using this
 | 
			
		||||
@ -363,9 +363,10 @@ export const TOPICS: ITutorialTopic[] = [
 | 
			
		||||
                        strategies by using the arrow button.
 | 
			
		||||
                    </Description>
 | 
			
		||||
                ),
 | 
			
		||||
                optional: true,
 | 
			
		||||
            },
 | 
			
		||||
            {
 | 
			
		||||
                target: `div[data-testid="FEATURE_ENVIRONMENT_ACCORDION_${ENVIRONMENT}"].Mui-expanded a[data-testid="STRATEGY_EDIT-flexibleRollout"]`,
 | 
			
		||||
                target: `a[data-testid="STRATEGY_EDIT-flexibleRollout"]`,
 | 
			
		||||
                content: (
 | 
			
		||||
                    <Description>
 | 
			
		||||
                        Edit the existing gradual rollout strategy by using the
 | 
			
		||||
@ -471,7 +472,7 @@ export const TOPICS: ITutorialTopic[] = [
 | 
			
		||||
            },
 | 
			
		||||
            {
 | 
			
		||||
                href: `/projects/${PROJECT}/features/demoApp.step4`,
 | 
			
		||||
                target: `div[data-testid="FEATURE_ENVIRONMENT_ACCORDION_${ENVIRONMENT}"] button`,
 | 
			
		||||
                target: 'button[data-testid="ADD_STRATEGY_BUTTON"]',
 | 
			
		||||
                content: (
 | 
			
		||||
                    <Description>
 | 
			
		||||
                        Add a new strategy to this environment by using this
 | 
			
		||||
 | 
			
		||||
@ -80,6 +80,7 @@ export const FeatureStrategyMenu = ({
 | 
			
		||||
    return (
 | 
			
		||||
        <StyledStrategyMenu onClick={(event) => event.stopPropagation()}>
 | 
			
		||||
            <PermissionButton
 | 
			
		||||
                data-testid='ADD_STRATEGY_BUTTON'
 | 
			
		||||
                permission={CREATE_FEATURE_STRATEGY}
 | 
			
		||||
                projectId={projectId}
 | 
			
		||||
                environmentId={environmentId}
 | 
			
		||||
 | 
			
		||||
@ -12,11 +12,13 @@ import { FeatureOverviewSidePanel as NewFeatureOverviewSidePanel } from 'compone
 | 
			
		||||
import { useHiddenEnvironments } from 'hooks/useHiddenEnvironments';
 | 
			
		||||
import { styled } from '@mui/material';
 | 
			
		||||
import { FeatureStrategyCreate } from 'component/feature/FeatureStrategy/FeatureStrategyCreate/FeatureStrategyCreate';
 | 
			
		||||
import { useEffect } from 'react';
 | 
			
		||||
import { useEffect, useState } from 'react';
 | 
			
		||||
import { useLastViewedFlags } from 'hooks/useLastViewedFlags';
 | 
			
		||||
import { useUiFlag } from 'hooks/useUiFlag';
 | 
			
		||||
import OldFeatureOverviewMetaData from './FeatureOverviewMetaData/OldFeatureOverviewMetaData';
 | 
			
		||||
import { OldFeatureOverviewSidePanel } from 'component/feature/FeatureView/FeatureOverview/FeatureOverviewSidePanel/OldFeatureOverviewSidePanel';
 | 
			
		||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
 | 
			
		||||
import { NewFeatureOverviewEnvironment } from './NewFeatureOverviewEnvironment/NewFeatureOverviewEnvironment';
 | 
			
		||||
 | 
			
		||||
const StyledContainer = styled('div')(({ theme }) => ({
 | 
			
		||||
    display: 'flex',
 | 
			
		||||
@ -48,26 +50,40 @@ const FeatureOverview = () => {
 | 
			
		||||
    useEffect(() => {
 | 
			
		||||
        setLastViewed({ featureId, projectId });
 | 
			
		||||
    }, [featureId]);
 | 
			
		||||
    const [environmentId, setEnvironmentId] = useState('');
 | 
			
		||||
 | 
			
		||||
    const flagOverviewRedesign = useUiFlag('flagOverviewRedesign');
 | 
			
		||||
    const FeatureOverviewMetaData = flagOverviewRedesign
 | 
			
		||||
        ? NewFeatureOverviewMetaData
 | 
			
		||||
        : OldFeatureOverviewMetaData;
 | 
			
		||||
    const FeatureOverviewSidePanel = flagOverviewRedesign
 | 
			
		||||
        ? NewFeatureOverviewSidePanel
 | 
			
		||||
        : OldFeatureOverviewSidePanel;
 | 
			
		||||
    const FeatureOverviewSidePanel = flagOverviewRedesign ? (
 | 
			
		||||
        <NewFeatureOverviewSidePanel
 | 
			
		||||
            environmentId={environmentId}
 | 
			
		||||
            setEnvironmentId={setEnvironmentId}
 | 
			
		||||
        />
 | 
			
		||||
    ) : (
 | 
			
		||||
        <OldFeatureOverviewSidePanel
 | 
			
		||||
            hiddenEnvironments={hiddenEnvironments}
 | 
			
		||||
            setHiddenEnvironments={setHiddenEnvironments}
 | 
			
		||||
        />
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
        <StyledContainer>
 | 
			
		||||
            <div>
 | 
			
		||||
                <FeatureOverviewMetaData />
 | 
			
		||||
                <FeatureOverviewSidePanel
 | 
			
		||||
                    hiddenEnvironments={hiddenEnvironments}
 | 
			
		||||
                    setHiddenEnvironments={setHiddenEnvironments}
 | 
			
		||||
                />
 | 
			
		||||
                {FeatureOverviewSidePanel}
 | 
			
		||||
            </div>
 | 
			
		||||
            <StyledMainContent>
 | 
			
		||||
                <FeatureOverviewEnvironments />
 | 
			
		||||
                <ConditionallyRender
 | 
			
		||||
                    condition={flagOverviewRedesign}
 | 
			
		||||
                    show={
 | 
			
		||||
                        <NewFeatureOverviewEnvironment
 | 
			
		||||
                            environmentId={environmentId}
 | 
			
		||||
                        />
 | 
			
		||||
                    }
 | 
			
		||||
                    elseShow={<FeatureOverviewEnvironments />}
 | 
			
		||||
                />
 | 
			
		||||
            </StyledMainContent>
 | 
			
		||||
            <Routes>
 | 
			
		||||
                <Route
 | 
			
		||||
 | 
			
		||||
@ -1,78 +1,83 @@
 | 
			
		||||
import { Box, styled } from '@mui/material';
 | 
			
		||||
import { HelpIcon } from 'component/common/HelpIcon/HelpIcon';
 | 
			
		||||
import { useFeature } from 'hooks/api/getters/useFeature/useFeature';
 | 
			
		||||
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
 | 
			
		||||
import { FeatureOverviewSidePanelEnvironmentSwitches } from './FeatureOverviewSidePanelEnvironmentSwitches/FeatureOverviewSidePanelEnvironmentSwitches';
 | 
			
		||||
import { Sticky } from 'component/common/Sticky/Sticky';
 | 
			
		||||
import {
 | 
			
		||||
    type ITab,
 | 
			
		||||
    VerticalTabs,
 | 
			
		||||
} from 'component/common/VerticalTabs/VerticalTabs';
 | 
			
		||||
import EnvironmentIcon from 'component/common/EnvironmentIcon/EnvironmentIcon';
 | 
			
		||||
import { useEffect } from 'react';
 | 
			
		||||
 | 
			
		||||
const StyledContainer = styled(Box)(({ theme }) => ({
 | 
			
		||||
    top: theme.spacing(2),
 | 
			
		||||
    margin: theme.spacing(2),
 | 
			
		||||
    marginLeft: 0,
 | 
			
		||||
    padding: theme.spacing(3),
 | 
			
		||||
    borderRadius: theme.shape.borderRadiusLarge,
 | 
			
		||||
    backgroundColor: theme.palette.background.paper,
 | 
			
		||||
    display: 'flex',
 | 
			
		||||
    flexDirection: 'column',
 | 
			
		||||
    maxWidth: '350px',
 | 
			
		||||
    minWidth: '350px',
 | 
			
		||||
    marginRight: '1rem',
 | 
			
		||||
    marginTop: '1rem',
 | 
			
		||||
    gap: theme.spacing(2),
 | 
			
		||||
    width: '350px',
 | 
			
		||||
    [theme.breakpoints.down(1000)]: {
 | 
			
		||||
        marginBottom: '1rem',
 | 
			
		||||
        width: '100%',
 | 
			
		||||
        maxWidth: 'none',
 | 
			
		||||
        minWidth: 'auto',
 | 
			
		||||
    },
 | 
			
		||||
}));
 | 
			
		||||
 | 
			
		||||
const StyledHeader = styled('h3')(({ theme }) => ({
 | 
			
		||||
    display: 'flex',
 | 
			
		||||
    gap: theme.spacing(1),
 | 
			
		||||
    alignItems: 'center',
 | 
			
		||||
    fontSize: theme.fontSizes.bodySize,
 | 
			
		||||
    margin: 0,
 | 
			
		||||
    marginBottom: theme.spacing(3),
 | 
			
		||||
    marginBottom: theme.spacing(1),
 | 
			
		||||
}));
 | 
			
		||||
 | 
			
		||||
    // Make the help icon align with the text.
 | 
			
		||||
    '& > :last-child': {
 | 
			
		||||
        position: 'relative',
 | 
			
		||||
        top: 1,
 | 
			
		||||
const StyledVerticalTabs = styled(VerticalTabs)(({ theme }) => ({
 | 
			
		||||
    '&&& .selected': {
 | 
			
		||||
        backgroundColor: theme.palette.neutral.light,
 | 
			
		||||
    },
 | 
			
		||||
}));
 | 
			
		||||
 | 
			
		||||
interface IFeatureOverviewSidePanelProps {
 | 
			
		||||
    hiddenEnvironments: Set<String>;
 | 
			
		||||
    setHiddenEnvironments: (environment: string) => void;
 | 
			
		||||
    environmentId: string;
 | 
			
		||||
    setEnvironmentId: React.Dispatch<React.SetStateAction<string>>;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const FeatureOverviewSidePanel = ({
 | 
			
		||||
    hiddenEnvironments,
 | 
			
		||||
    setHiddenEnvironments,
 | 
			
		||||
    environmentId,
 | 
			
		||||
    setEnvironmentId,
 | 
			
		||||
}: IFeatureOverviewSidePanelProps) => {
 | 
			
		||||
    const projectId = useRequiredPathParam('projectId');
 | 
			
		||||
    const featureId = useRequiredPathParam('featureId');
 | 
			
		||||
    const { feature } = useFeature(projectId, featureId);
 | 
			
		||||
    const isSticky = feature.environments?.length <= 3;
 | 
			
		||||
 | 
			
		||||
    const tabs: ITab[] = feature.environments.map(
 | 
			
		||||
        ({ name, enabled, strategies }) => ({
 | 
			
		||||
            id: name,
 | 
			
		||||
            label: name,
 | 
			
		||||
            description:
 | 
			
		||||
                strategies.length === 1
 | 
			
		||||
                    ? '1 strategy'
 | 
			
		||||
                    : `${strategies.length || 'No'} strategies`,
 | 
			
		||||
            startIcon: <EnvironmentIcon enabled={enabled} />,
 | 
			
		||||
        }),
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    useEffect(() => {
 | 
			
		||||
        if (!environmentId) {
 | 
			
		||||
            setEnvironmentId(tabs[0]?.id);
 | 
			
		||||
        }
 | 
			
		||||
    }, [tabs]);
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
        <StyledContainer as={isSticky ? Sticky : Box}>
 | 
			
		||||
            <FeatureOverviewSidePanelEnvironmentSwitches
 | 
			
		||||
                header={
 | 
			
		||||
                    <StyledHeader data-loading>
 | 
			
		||||
                        Enabled in environments (
 | 
			
		||||
                        {
 | 
			
		||||
                            feature.environments.filter(
 | 
			
		||||
                                ({ enabled }) => enabled,
 | 
			
		||||
                            ).length
 | 
			
		||||
                        }
 | 
			
		||||
                        )
 | 
			
		||||
                        <HelpIcon
 | 
			
		||||
                            tooltip='When a feature is switched off in an environment, it will always return false. When switched on, it will return true or false depending on its strategies.'
 | 
			
		||||
                            placement='top'
 | 
			
		||||
                        />
 | 
			
		||||
                    </StyledHeader>
 | 
			
		||||
                }
 | 
			
		||||
                feature={feature}
 | 
			
		||||
                hiddenEnvironments={hiddenEnvironments}
 | 
			
		||||
                setHiddenEnvironments={setHiddenEnvironments}
 | 
			
		||||
            <StyledHeader data-loading>
 | 
			
		||||
                Environments ({feature.environments.length})
 | 
			
		||||
            </StyledHeader>
 | 
			
		||||
            <StyledVerticalTabs
 | 
			
		||||
                tabs={tabs}
 | 
			
		||||
                value={environmentId}
 | 
			
		||||
                onChange={({ id }) => setEnvironmentId(id)}
 | 
			
		||||
            />
 | 
			
		||||
        </StyledContainer>
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
@ -0,0 +1,311 @@
 | 
			
		||||
import {
 | 
			
		||||
    type DragEventHandler,
 | 
			
		||||
    type RefObject,
 | 
			
		||||
    useEffect,
 | 
			
		||||
    useState,
 | 
			
		||||
} from 'react';
 | 
			
		||||
import { Alert, Pagination, styled } from '@mui/material';
 | 
			
		||||
import useFeatureStrategyApi from 'hooks/api/actions/useFeatureStrategyApi/useFeatureStrategyApi';
 | 
			
		||||
import { formatUnknownError } from 'utils/formatUnknownError';
 | 
			
		||||
import useToast from 'hooks/useToast';
 | 
			
		||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
 | 
			
		||||
import { StrategyDraggableItem } from '../FeatureOverviewEnvironments/FeatureOverviewEnvironment/EnvironmentAccordionBody/StrategyDraggableItem/StrategyDraggableItem';
 | 
			
		||||
import type { IFeatureEnvironment } from 'interfaces/featureToggle';
 | 
			
		||||
import { FeatureStrategyEmpty } from 'component/feature/FeatureStrategy/FeatureStrategyEmpty/FeatureStrategyEmpty';
 | 
			
		||||
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
 | 
			
		||||
import { useFeature } from 'hooks/api/getters/useFeature/useFeature';
 | 
			
		||||
import { useChangeRequestApi } from 'hooks/api/actions/useChangeRequestApi/useChangeRequestApi';
 | 
			
		||||
import { useChangeRequestsEnabled } from 'hooks/useChangeRequestsEnabled';
 | 
			
		||||
import { usePendingChangeRequests } from 'hooks/api/getters/usePendingChangeRequests/usePendingChangeRequests';
 | 
			
		||||
import usePagination from 'hooks/usePagination';
 | 
			
		||||
import type { IFeatureStrategy } from 'interfaces/strategy';
 | 
			
		||||
import { usePlausibleTracker } from 'hooks/usePlausibleTracker';
 | 
			
		||||
import { useUiFlag } from 'hooks/useUiFlag';
 | 
			
		||||
import isEqual from 'lodash/isEqual';
 | 
			
		||||
 | 
			
		||||
interface IEnvironmentAccordionBodyProps {
 | 
			
		||||
    isDisabled: boolean;
 | 
			
		||||
    featureEnvironment?: IFeatureEnvironment;
 | 
			
		||||
    otherEnvironments?: IFeatureEnvironment['name'][];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const StyledAccordionBody = styled('div')(({ theme }) => ({
 | 
			
		||||
    width: '100%',
 | 
			
		||||
    position: 'relative',
 | 
			
		||||
    paddingBottom: theme.spacing(2),
 | 
			
		||||
}));
 | 
			
		||||
 | 
			
		||||
const StyledAccordionBodyInnerContainer = styled('div')(({ theme }) => ({
 | 
			
		||||
    [theme.breakpoints.down(400)]: {
 | 
			
		||||
        padding: theme.spacing(1),
 | 
			
		||||
    },
 | 
			
		||||
}));
 | 
			
		||||
 | 
			
		||||
export const FeatureOverviewEnvironmentBody = ({
 | 
			
		||||
    featureEnvironment,
 | 
			
		||||
    isDisabled,
 | 
			
		||||
    otherEnvironments,
 | 
			
		||||
}: IEnvironmentAccordionBodyProps) => {
 | 
			
		||||
    const projectId = useRequiredPathParam('projectId');
 | 
			
		||||
    const featureId = useRequiredPathParam('featureId');
 | 
			
		||||
    const { setStrategiesSortOrder } = useFeatureStrategyApi();
 | 
			
		||||
    const { addChange } = useChangeRequestApi();
 | 
			
		||||
    const { isChangeRequestConfigured } = useChangeRequestsEnabled(projectId);
 | 
			
		||||
    const { refetch: refetchChangeRequests } =
 | 
			
		||||
        usePendingChangeRequests(projectId);
 | 
			
		||||
    const { setToastData, setToastApiError } = useToast();
 | 
			
		||||
    const { refetchFeature } = useFeature(projectId, featureId);
 | 
			
		||||
    const manyStrategiesPagination = useUiFlag('manyStrategiesPagination');
 | 
			
		||||
    const [strategies, setStrategies] = useState(
 | 
			
		||||
        featureEnvironment?.strategies || [],
 | 
			
		||||
    );
 | 
			
		||||
    const { trackEvent } = usePlausibleTracker();
 | 
			
		||||
 | 
			
		||||
    const [dragItem, setDragItem] = useState<{
 | 
			
		||||
        id: string;
 | 
			
		||||
        index: number;
 | 
			
		||||
        height: number;
 | 
			
		||||
    } | null>(null);
 | 
			
		||||
 | 
			
		||||
    const [isReordering, setIsReordering] = useState(false);
 | 
			
		||||
 | 
			
		||||
    useEffect(() => {
 | 
			
		||||
        if (isReordering) {
 | 
			
		||||
            if (isEqual(featureEnvironment?.strategies, strategies)) {
 | 
			
		||||
                setIsReordering(false);
 | 
			
		||||
            }
 | 
			
		||||
        } else {
 | 
			
		||||
            setStrategies(featureEnvironment?.strategies || []);
 | 
			
		||||
        }
 | 
			
		||||
    }, [featureEnvironment?.strategies]);
 | 
			
		||||
 | 
			
		||||
    useEffect(() => {
 | 
			
		||||
        if (strategies.length > 50) {
 | 
			
		||||
            trackEvent('many-strategies');
 | 
			
		||||
        }
 | 
			
		||||
    }, []);
 | 
			
		||||
 | 
			
		||||
    if (!featureEnvironment) {
 | 
			
		||||
        return null;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const pageSize = 20;
 | 
			
		||||
    const { page, pages, setPageIndex, pageIndex } =
 | 
			
		||||
        usePagination<IFeatureStrategy>(strategies, pageSize);
 | 
			
		||||
 | 
			
		||||
    const onReorder = async (payload: { id: string; sortOrder: number }[]) => {
 | 
			
		||||
        try {
 | 
			
		||||
            await setStrategiesSortOrder(
 | 
			
		||||
                projectId,
 | 
			
		||||
                featureId,
 | 
			
		||||
                featureEnvironment.name,
 | 
			
		||||
                payload,
 | 
			
		||||
            );
 | 
			
		||||
            refetchFeature();
 | 
			
		||||
            setToastData({
 | 
			
		||||
                title: 'Order of strategies updated',
 | 
			
		||||
                type: 'success',
 | 
			
		||||
            });
 | 
			
		||||
        } catch (error: unknown) {
 | 
			
		||||
            setToastApiError(formatUnknownError(error));
 | 
			
		||||
        }
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    const onChangeRequestReorder = async (
 | 
			
		||||
        payload: { id: string; sortOrder: number }[],
 | 
			
		||||
    ) => {
 | 
			
		||||
        await addChange(projectId, featureEnvironment.name, {
 | 
			
		||||
            action: 'reorderStrategy',
 | 
			
		||||
            feature: featureId,
 | 
			
		||||
            payload,
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        setToastData({
 | 
			
		||||
            title: 'Strategy execution order added to draft',
 | 
			
		||||
            type: 'success',
 | 
			
		||||
            confetti: true,
 | 
			
		||||
        });
 | 
			
		||||
        refetchChangeRequests();
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    const onStrategyReorder = async (
 | 
			
		||||
        payload: { id: string; sortOrder: number }[],
 | 
			
		||||
    ) => {
 | 
			
		||||
        try {
 | 
			
		||||
            if (isChangeRequestConfigured(featureEnvironment.name)) {
 | 
			
		||||
                await onChangeRequestReorder(payload);
 | 
			
		||||
            } else {
 | 
			
		||||
                await onReorder(payload);
 | 
			
		||||
            }
 | 
			
		||||
        } catch (error: unknown) {
 | 
			
		||||
            setToastApiError(formatUnknownError(error));
 | 
			
		||||
        }
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    const onDragStartRef =
 | 
			
		||||
        (
 | 
			
		||||
            ref: RefObject<HTMLDivElement>,
 | 
			
		||||
            index: number,
 | 
			
		||||
        ): DragEventHandler<HTMLButtonElement> =>
 | 
			
		||||
        (event) => {
 | 
			
		||||
            setIsReordering(true);
 | 
			
		||||
            setDragItem({
 | 
			
		||||
                id: strategies[index].id,
 | 
			
		||||
                index,
 | 
			
		||||
                height: ref.current?.offsetHeight || 0,
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            if (ref?.current) {
 | 
			
		||||
                event.dataTransfer.effectAllowed = 'move';
 | 
			
		||||
                event.dataTransfer.setData('text/html', ref.current.outerHTML);
 | 
			
		||||
                event.dataTransfer.setDragImage(ref.current, 20, 20);
 | 
			
		||||
            }
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
    const onDragOver =
 | 
			
		||||
        (targetId: string) =>
 | 
			
		||||
        (
 | 
			
		||||
            ref: RefObject<HTMLDivElement>,
 | 
			
		||||
            targetIndex: number,
 | 
			
		||||
        ): DragEventHandler<HTMLDivElement> =>
 | 
			
		||||
        (event) => {
 | 
			
		||||
            if (dragItem === null || ref.current === null) return;
 | 
			
		||||
            if (dragItem.index === targetIndex || targetId === dragItem.id)
 | 
			
		||||
                return;
 | 
			
		||||
 | 
			
		||||
            const { top, bottom } = ref.current.getBoundingClientRect();
 | 
			
		||||
            const overTargetTop = event.clientY - top < dragItem.height;
 | 
			
		||||
            const overTargetBottom = bottom - event.clientY < dragItem.height;
 | 
			
		||||
            const draggingUp = dragItem.index > targetIndex;
 | 
			
		||||
 | 
			
		||||
            // prevent oscillating by only reordering if there is sufficient space
 | 
			
		||||
            if (
 | 
			
		||||
                (overTargetTop && draggingUp) ||
 | 
			
		||||
                (overTargetBottom && !draggingUp)
 | 
			
		||||
            ) {
 | 
			
		||||
                const newStrategies = [...strategies];
 | 
			
		||||
                const movedStrategy = newStrategies.splice(
 | 
			
		||||
                    dragItem.index,
 | 
			
		||||
                    1,
 | 
			
		||||
                )[0];
 | 
			
		||||
                newStrategies.splice(targetIndex, 0, movedStrategy);
 | 
			
		||||
                setStrategies(newStrategies);
 | 
			
		||||
                setDragItem({
 | 
			
		||||
                    ...dragItem,
 | 
			
		||||
                    index: targetIndex,
 | 
			
		||||
                });
 | 
			
		||||
            }
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
    const onDragEnd = () => {
 | 
			
		||||
        setDragItem(null);
 | 
			
		||||
        onStrategyReorder(
 | 
			
		||||
            strategies.map((strategy, sortOrder) => ({
 | 
			
		||||
                id: strategy.id,
 | 
			
		||||
                sortOrder,
 | 
			
		||||
            })),
 | 
			
		||||
        );
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    const strategiesToDisplay = isReordering
 | 
			
		||||
        ? strategies
 | 
			
		||||
        : featureEnvironment.strategies;
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
        <StyledAccordionBody>
 | 
			
		||||
            <StyledAccordionBodyInnerContainer>
 | 
			
		||||
                <ConditionallyRender
 | 
			
		||||
                    condition={strategiesToDisplay.length > 0 && isDisabled}
 | 
			
		||||
                    show={() => (
 | 
			
		||||
                        <Alert severity='warning' sx={{ mb: 2 }}>
 | 
			
		||||
                            This environment is disabled, which means that none
 | 
			
		||||
                            of your strategies are executing.
 | 
			
		||||
                        </Alert>
 | 
			
		||||
                    )}
 | 
			
		||||
                />
 | 
			
		||||
                <ConditionallyRender
 | 
			
		||||
                    condition={strategiesToDisplay.length > 0}
 | 
			
		||||
                    show={
 | 
			
		||||
                        <ConditionallyRender
 | 
			
		||||
                            condition={
 | 
			
		||||
                                strategiesToDisplay.length < 50 ||
 | 
			
		||||
                                !manyStrategiesPagination
 | 
			
		||||
                            }
 | 
			
		||||
                            show={
 | 
			
		||||
                                <>
 | 
			
		||||
                                    {strategiesToDisplay.map(
 | 
			
		||||
                                        (strategy, index) => (
 | 
			
		||||
                                            <StrategyDraggableItem
 | 
			
		||||
                                                key={strategy.id}
 | 
			
		||||
                                                strategy={strategy}
 | 
			
		||||
                                                index={index}
 | 
			
		||||
                                                environmentName={
 | 
			
		||||
                                                    featureEnvironment.name
 | 
			
		||||
                                                }
 | 
			
		||||
                                                otherEnvironments={
 | 
			
		||||
                                                    otherEnvironments
 | 
			
		||||
                                                }
 | 
			
		||||
                                                isDragging={
 | 
			
		||||
                                                    dragItem?.id === strategy.id
 | 
			
		||||
                                                }
 | 
			
		||||
                                                onDragStartRef={onDragStartRef}
 | 
			
		||||
                                                onDragOver={onDragOver(
 | 
			
		||||
                                                    strategy.id,
 | 
			
		||||
                                                )}
 | 
			
		||||
                                                onDragEnd={onDragEnd}
 | 
			
		||||
                                            />
 | 
			
		||||
                                        ),
 | 
			
		||||
                                    )}
 | 
			
		||||
                                </>
 | 
			
		||||
                            }
 | 
			
		||||
                            elseShow={
 | 
			
		||||
                                <>
 | 
			
		||||
                                    <Alert severity='error'>
 | 
			
		||||
                                        We noticed you're using a high number of
 | 
			
		||||
                                        activation strategies. To ensure a more
 | 
			
		||||
                                        targeted approach, consider leveraging
 | 
			
		||||
                                        constraints or segments.
 | 
			
		||||
                                    </Alert>
 | 
			
		||||
                                    <br />
 | 
			
		||||
                                    {page.map((strategy, index) => (
 | 
			
		||||
                                        <StrategyDraggableItem
 | 
			
		||||
                                            key={strategy.id}
 | 
			
		||||
                                            strategy={strategy}
 | 
			
		||||
                                            index={index + pageIndex * pageSize}
 | 
			
		||||
                                            environmentName={
 | 
			
		||||
                                                featureEnvironment.name
 | 
			
		||||
                                            }
 | 
			
		||||
                                            otherEnvironments={
 | 
			
		||||
                                                otherEnvironments
 | 
			
		||||
                                            }
 | 
			
		||||
                                            isDragging={false}
 | 
			
		||||
                                            onDragStartRef={(() => {}) as any}
 | 
			
		||||
                                            onDragOver={(() => {}) as any}
 | 
			
		||||
                                            onDragEnd={(() => {}) as any}
 | 
			
		||||
                                        />
 | 
			
		||||
                                    ))}
 | 
			
		||||
                                    <br />
 | 
			
		||||
                                    <Pagination
 | 
			
		||||
                                        count={pages.length}
 | 
			
		||||
                                        shape='rounded'
 | 
			
		||||
                                        page={pageIndex + 1}
 | 
			
		||||
                                        onChange={(_, page) =>
 | 
			
		||||
                                            setPageIndex(page - 1)
 | 
			
		||||
                                        }
 | 
			
		||||
                                    />
 | 
			
		||||
                                </>
 | 
			
		||||
                            }
 | 
			
		||||
                        />
 | 
			
		||||
                    }
 | 
			
		||||
                    elseShow={
 | 
			
		||||
                        <FeatureStrategyEmpty
 | 
			
		||||
                            projectId={projectId}
 | 
			
		||||
                            featureId={featureId}
 | 
			
		||||
                            environmentId={featureEnvironment.name}
 | 
			
		||||
                        />
 | 
			
		||||
                    }
 | 
			
		||||
                />
 | 
			
		||||
            </StyledAccordionBodyInnerContainer>
 | 
			
		||||
        </StyledAccordionBody>
 | 
			
		||||
    );
 | 
			
		||||
};
 | 
			
		||||
@ -0,0 +1,51 @@
 | 
			
		||||
import { useFeatureToggleSwitch } from 'component/project/Project/ProjectFeatureToggles/FeatureToggleSwitch/useFeatureToggleSwitch';
 | 
			
		||||
import { FeatureToggleSwitch } from 'component/project/Project/ProjectFeatureToggles/FeatureToggleSwitch/FeatureToggleSwitch';
 | 
			
		||||
import { useChangeRequestsEnabled } from 'hooks/useChangeRequestsEnabled';
 | 
			
		||||
import { useFeature } from 'hooks/api/getters/useFeature/useFeature';
 | 
			
		||||
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
 | 
			
		||||
import type { IFeatureEnvironment } from 'interfaces/featureToggle';
 | 
			
		||||
 | 
			
		||||
interface IFeatureOverviewEnvironmentToggleProps {
 | 
			
		||||
    environment: IFeatureEnvironment;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const FeatureOverviewEnvironmentToggle = ({
 | 
			
		||||
    environment: { name, type, strategies, enabled },
 | 
			
		||||
}: IFeatureOverviewEnvironmentToggleProps) => {
 | 
			
		||||
    const projectId = useRequiredPathParam('projectId');
 | 
			
		||||
    const featureId = useRequiredPathParam('featureId');
 | 
			
		||||
    const { refetchFeature } = useFeature(projectId, featureId);
 | 
			
		||||
 | 
			
		||||
    const { isChangeRequestConfigured } = useChangeRequestsEnabled(projectId);
 | 
			
		||||
 | 
			
		||||
    const { onToggle: onFeatureToggle, modals: featureToggleModals } =
 | 
			
		||||
        useFeatureToggleSwitch(projectId);
 | 
			
		||||
 | 
			
		||||
    const onToggle = (newState: boolean, onRollback: () => void) =>
 | 
			
		||||
        onFeatureToggle(newState, {
 | 
			
		||||
            projectId,
 | 
			
		||||
            featureId,
 | 
			
		||||
            environmentName: name,
 | 
			
		||||
            environmentType: type,
 | 
			
		||||
            hasStrategies: strategies.length > 0,
 | 
			
		||||
            hasEnabledStrategies: strategies.some(
 | 
			
		||||
                (strategy) => !strategy.disabled,
 | 
			
		||||
            ),
 | 
			
		||||
            isChangeRequestEnabled: isChangeRequestConfigured(name),
 | 
			
		||||
            onRollback,
 | 
			
		||||
            onSuccess: refetchFeature,
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
        <>
 | 
			
		||||
            <FeatureToggleSwitch
 | 
			
		||||
                projectId={projectId}
 | 
			
		||||
                value={enabled}
 | 
			
		||||
                featureId={featureId}
 | 
			
		||||
                environmentName={name}
 | 
			
		||||
                onToggle={onToggle}
 | 
			
		||||
            />
 | 
			
		||||
            {featureToggleModals}
 | 
			
		||||
        </>
 | 
			
		||||
    );
 | 
			
		||||
};
 | 
			
		||||
@ -0,0 +1,131 @@
 | 
			
		||||
import { Box, styled } from '@mui/material';
 | 
			
		||||
import { useFeature } from 'hooks/api/getters/useFeature/useFeature';
 | 
			
		||||
import useFeatureMetrics from 'hooks/api/getters/useFeatureMetrics/useFeatureMetrics';
 | 
			
		||||
import { getFeatureMetrics } from 'utils/getFeatureMetrics';
 | 
			
		||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
 | 
			
		||||
import { FeatureOverviewEnvironmentBody } from './FeatureOverviewEnvironmentBody';
 | 
			
		||||
import FeatureOverviewEnvironmentMetrics from '../FeatureOverviewEnvironments/FeatureOverviewEnvironment/FeatureOverviewEnvironmentMetrics/FeatureOverviewEnvironmentMetrics';
 | 
			
		||||
import { FeatureStrategyMenu } from 'component/feature/FeatureStrategy/FeatureStrategyMenu/FeatureStrategyMenu';
 | 
			
		||||
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
 | 
			
		||||
import { FeatureOverviewEnvironmentToggle } from './FeatureOverviewEnvironmentToggle';
 | 
			
		||||
 | 
			
		||||
const StyledFeatureOverviewEnvironment = styled('div')(({ theme }) => ({
 | 
			
		||||
    padding: theme.spacing(1, 3),
 | 
			
		||||
    borderRadius: theme.shape.borderRadiusLarge,
 | 
			
		||||
    backgroundColor: theme.palette.background.paper,
 | 
			
		||||
}));
 | 
			
		||||
 | 
			
		||||
const StyledFeatureOverviewEnvironmentBody = styled(
 | 
			
		||||
    FeatureOverviewEnvironmentBody,
 | 
			
		||||
)(({ theme }) => ({
 | 
			
		||||
    width: '100%',
 | 
			
		||||
    position: 'relative',
 | 
			
		||||
    paddingBottom: theme.spacing(2),
 | 
			
		||||
}));
 | 
			
		||||
 | 
			
		||||
const StyledHeader = styled('div')(({ theme }) => ({
 | 
			
		||||
    display: 'flex',
 | 
			
		||||
    marginBottom: theme.spacing(2),
 | 
			
		||||
}));
 | 
			
		||||
 | 
			
		||||
const StyledHeaderToggleContainer = styled('div')(({ theme }) => ({
 | 
			
		||||
    display: 'flex',
 | 
			
		||||
    alignItems: 'center',
 | 
			
		||||
    gap: theme.spacing(1),
 | 
			
		||||
}));
 | 
			
		||||
 | 
			
		||||
const StyledHeaderTitleContainer = styled('div')(({ theme }) => ({
 | 
			
		||||
    display: 'flex',
 | 
			
		||||
    flexDirection: 'column',
 | 
			
		||||
    justifyContent: 'center',
 | 
			
		||||
}));
 | 
			
		||||
 | 
			
		||||
const StyledHeaderTitleLabel = styled('span')(({ theme }) => ({
 | 
			
		||||
    fontSize: theme.fontSizes.smallerBody,
 | 
			
		||||
    lineHeight: 0.5,
 | 
			
		||||
    color: theme.palette.text.secondary,
 | 
			
		||||
}));
 | 
			
		||||
 | 
			
		||||
const StyledHeaderTitle = styled('span')(({ theme }) => ({
 | 
			
		||||
    fontSize: theme.fontSizes.mainHeader,
 | 
			
		||||
    fontWeight: theme.typography.fontWeightBold,
 | 
			
		||||
}));
 | 
			
		||||
 | 
			
		||||
interface INewFeatureOverviewEnvironmentProps {
 | 
			
		||||
    environmentId: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const NewFeatureOverviewEnvironment = ({
 | 
			
		||||
    environmentId,
 | 
			
		||||
}: INewFeatureOverviewEnvironmentProps) => {
 | 
			
		||||
    const projectId = useRequiredPathParam('projectId');
 | 
			
		||||
    const featureId = useRequiredPathParam('featureId');
 | 
			
		||||
    const { metrics } = useFeatureMetrics(projectId, featureId);
 | 
			
		||||
    const { feature } = useFeature(projectId, featureId);
 | 
			
		||||
 | 
			
		||||
    const featureMetrics = getFeatureMetrics(feature?.environments, metrics);
 | 
			
		||||
    const environmentMetric = featureMetrics.find(
 | 
			
		||||
        ({ environment }) => environment === environmentId,
 | 
			
		||||
    );
 | 
			
		||||
    const featureEnvironment = feature?.environments.find(
 | 
			
		||||
        ({ name }) => name === environmentId,
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    if (!featureEnvironment)
 | 
			
		||||
        return (
 | 
			
		||||
            <StyledFeatureOverviewEnvironment className='skeleton'>
 | 
			
		||||
                <Box sx={{ height: '400px' }} />
 | 
			
		||||
            </StyledFeatureOverviewEnvironment>
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
        <StyledFeatureOverviewEnvironment>
 | 
			
		||||
            <StyledHeader data-loading>
 | 
			
		||||
                <StyledHeaderToggleContainer>
 | 
			
		||||
                    <FeatureOverviewEnvironmentToggle
 | 
			
		||||
                        environment={featureEnvironment}
 | 
			
		||||
                    />
 | 
			
		||||
                    <StyledHeaderTitleContainer>
 | 
			
		||||
                        <StyledHeaderTitleLabel>
 | 
			
		||||
                            Environment
 | 
			
		||||
                        </StyledHeaderTitleLabel>
 | 
			
		||||
                        <StyledHeaderTitle>{environmentId}</StyledHeaderTitle>
 | 
			
		||||
                    </StyledHeaderTitleContainer>
 | 
			
		||||
                </StyledHeaderToggleContainer>
 | 
			
		||||
                <FeatureOverviewEnvironmentMetrics
 | 
			
		||||
                    environmentMetric={environmentMetric}
 | 
			
		||||
                    disabled={!featureEnvironment.enabled}
 | 
			
		||||
                />
 | 
			
		||||
            </StyledHeader>
 | 
			
		||||
 | 
			
		||||
            <StyledFeatureOverviewEnvironmentBody
 | 
			
		||||
                featureEnvironment={featureEnvironment}
 | 
			
		||||
                isDisabled={!featureEnvironment.enabled}
 | 
			
		||||
                otherEnvironments={feature?.environments
 | 
			
		||||
                    .map(({ name }) => name)
 | 
			
		||||
                    .filter((name) => name !== environmentId)}
 | 
			
		||||
            />
 | 
			
		||||
            <ConditionallyRender
 | 
			
		||||
                condition={(featureEnvironment?.strategies?.length || 0) > 0}
 | 
			
		||||
                show={
 | 
			
		||||
                    <>
 | 
			
		||||
                        <Box
 | 
			
		||||
                            sx={{
 | 
			
		||||
                                display: 'flex',
 | 
			
		||||
                                justifyContent: 'center',
 | 
			
		||||
                                py: 1,
 | 
			
		||||
                            }}
 | 
			
		||||
                        >
 | 
			
		||||
                            <FeatureStrategyMenu
 | 
			
		||||
                                label='Add strategy'
 | 
			
		||||
                                projectId={projectId}
 | 
			
		||||
                                featureId={featureId}
 | 
			
		||||
                                environmentId={environmentId}
 | 
			
		||||
                            />
 | 
			
		||||
                        </Box>
 | 
			
		||||
                    </>
 | 
			
		||||
                }
 | 
			
		||||
            />
 | 
			
		||||
        </StyledFeatureOverviewEnvironment>
 | 
			
		||||
    );
 | 
			
		||||
};
 | 
			
		||||
@ -68,7 +68,7 @@ export const ProjectSettings = () => {
 | 
			
		||||
        ...paidTabs({
 | 
			
		||||
            id: 'change-requests',
 | 
			
		||||
            label: 'Change request configuration',
 | 
			
		||||
            icon: isPro() ? (
 | 
			
		||||
            endIcon: isPro() ? (
 | 
			
		||||
                <StyledBadgeContainer>
 | 
			
		||||
                    <EnterpriseBadge />
 | 
			
		||||
                </StyledBadgeContainer>
 | 
			
		||||
@ -80,7 +80,7 @@ export const ProjectSettings = () => {
 | 
			
		||||
        tabs.push({
 | 
			
		||||
            id: 'actions',
 | 
			
		||||
            label: 'Actions',
 | 
			
		||||
            icon: isPro() ? (
 | 
			
		||||
            endIcon: isPro() ? (
 | 
			
		||||
                <StyledBadgeContainer>
 | 
			
		||||
                    <EnterpriseBadge />
 | 
			
		||||
                </StyledBadgeContainer>
 | 
			
		||||
 | 
			
		||||
		Loading…
	
		Reference in New Issue
	
	Block a user