mirror of
https://github.com/Unleash/unleash.git
synced 2025-07-17 13:46:47 +02:00
feat: new environment box (#9342)
Co-authored-by: Thomas Heartman <thomas@getunleash.io>
This commit is contained in:
parent
71ca0e413e
commit
42a05ef418
@ -230,6 +230,9 @@ export const deleteFeatureStrategy_UI = (
|
|||||||
},
|
},
|
||||||
).as('deleteUserStrategy');
|
).as('deleteUserStrategy');
|
||||||
cy.visit(`/projects/${project}/features/${featureToggleName}`);
|
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_REMOVE_MENU_BTN]').first().click();
|
||||||
cy.get('[data-testid=STRATEGY_FORM_REMOVE_ID]').first().click();
|
cy.get('[data-testid=STRATEGY_FORM_REMOVE_ID]').first().click();
|
||||||
if (!shouldWait) return cy.get('[data-testid=DIALOGUE_CONFIRM_ID]').click();
|
if (!shouldWait) return cy.get('[data-testid=DIALOGUE_CONFIRM_ID]').click();
|
||||||
|
@ -1,17 +1,17 @@
|
|||||||
import { useTheme } from '@mui/material';
|
import { useTheme } from '@mui/material';
|
||||||
import type { CSSProperties } from 'react';
|
import type { CSSProperties } from 'react';
|
||||||
|
|
||||||
interface IPercentageCircleProps {
|
type PercentageCircleProps = {
|
||||||
percentage: number;
|
percentage: number;
|
||||||
size?: `${number}rem`;
|
size?: `${number}rem`;
|
||||||
disabled?: boolean | null;
|
disabled?: boolean | null;
|
||||||
}
|
};
|
||||||
|
|
||||||
const PercentageCircle = ({
|
const PercentageCircle = ({
|
||||||
percentage,
|
percentage,
|
||||||
size = '4rem',
|
size = '4rem',
|
||||||
disabled = false,
|
disabled = false,
|
||||||
}: IPercentageCircleProps) => {
|
}: PercentageCircleProps) => {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
|
|
||||||
const style: CSSProperties = {
|
const style: CSSProperties = {
|
||||||
|
@ -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 (
|
||||||
|
<StyledContainer>
|
||||||
|
{/* biome-ignore lint/a11y/noSvgWithoutTitle: should be in a figure with figcaption */}
|
||||||
|
<svg viewBox={`0 0 ${d} ${d}`} style={style} aria-hidden>
|
||||||
|
<circle
|
||||||
|
r={r}
|
||||||
|
cx={r}
|
||||||
|
cy={r}
|
||||||
|
fill='none'
|
||||||
|
stroke={theme.palette.background.elevation2}
|
||||||
|
strokeWidth={strokeWidth}
|
||||||
|
/>
|
||||||
|
<circle
|
||||||
|
r={r}
|
||||||
|
cx={r}
|
||||||
|
cy={r}
|
||||||
|
fill='none'
|
||||||
|
stroke={color}
|
||||||
|
strokeWidth={strokeWidth}
|
||||||
|
strokeDasharray={`${percentage} 100`}
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
<StyledContent color={color}>{children}</StyledContent>
|
||||||
|
</StyledContainer>
|
||||||
|
);
|
||||||
|
};
|
@ -46,7 +46,7 @@ import { usePlausibleTracker } from 'hooks/usePlausibleTracker';
|
|||||||
import { BuiltInStrategies, formatStrategyName } from 'utils/strategyNames';
|
import { BuiltInStrategies, formatStrategyName } from 'utils/strategyNames';
|
||||||
import { Badge } from 'component/common/Badge/Badge';
|
import { Badge } from 'component/common/Badge/Badge';
|
||||||
import EnvironmentIcon from 'component/common/EnvironmentIcon/EnvironmentIcon';
|
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 {
|
interface IFeatureStrategyFormProps {
|
||||||
feature: IFeatureToggle;
|
feature: IFeatureToggle;
|
||||||
|
@ -12,7 +12,7 @@ import { FeatureStrategyCreate } from 'component/feature/FeatureStrategy/Feature
|
|||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
import { useLastViewedFlags } from 'hooks/useLastViewedFlags';
|
import { useLastViewedFlags } from 'hooks/useLastViewedFlags';
|
||||||
import { useUiFlag } from 'hooks/useUiFlag';
|
import { useUiFlag } from 'hooks/useUiFlag';
|
||||||
import { FeatureOverviewEnvironment } from './NewFeatureOverviewEnvironment/NewFeatureOverviewEnvironment';
|
import { FeatureOverviewEnvironments } from './FeatureOverviewEnvironments/FeatureOverviewEnvironments';
|
||||||
import { default as LegacyFleatureOverview } from './LegacyFeatureOverview';
|
import { default as LegacyFleatureOverview } from './LegacyFeatureOverview';
|
||||||
import { useEnvironmentVisibility } from './FeatureOverviewMetaData/EnvironmentVisibilityMenu/hooks/useEnvironmentVisibility';
|
import { useEnvironmentVisibility } from './FeatureOverviewMetaData/EnvironmentVisibilityMenu/hooks/useEnvironmentVisibility';
|
||||||
|
|
||||||
@ -62,7 +62,7 @@ export const FeatureOverview = () => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<StyledMainContent>
|
<StyledMainContent>
|
||||||
<FeatureOverviewEnvironment
|
<FeatureOverviewEnvironments
|
||||||
hiddenEnvironments={hiddenEnvironments}
|
hiddenEnvironments={hiddenEnvironments}
|
||||||
/>
|
/>
|
||||||
</StyledMainContent>
|
</StyledMainContent>
|
||||||
|
@ -12,6 +12,9 @@ interface IEnvironmentFooterProps {
|
|||||||
environmentMetric?: IFeatureEnvironmentMetrics;
|
environmentMetric?: IFeatureEnvironmentMetrics;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated remove with `featureOverviewRedesign` flag
|
||||||
|
*/
|
||||||
export const EnvironmentFooter = ({
|
export const EnvironmentFooter = ({
|
||||||
environmentMetric,
|
environmentMetric,
|
||||||
}: IEnvironmentFooterProps) => {
|
}: IEnvironmentFooterProps) => {
|
||||||
|
@ -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<EnvironmentHeaderProps> = ({
|
||||||
|
environmentId,
|
||||||
|
children,
|
||||||
|
expandable = true,
|
||||||
|
...props
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<StyledAccordionSummary
|
||||||
|
{...props}
|
||||||
|
expandIcon={
|
||||||
|
<ExpandMore
|
||||||
|
sx={{ visibility: expandable ? 'visible' : 'hidden' }}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
expandable={expandable}
|
||||||
|
>
|
||||||
|
<StyledHeader data-loading>
|
||||||
|
<StyledHeaderTitle>
|
||||||
|
<StyledHeaderTitleLabel>Environment</StyledHeaderTitleLabel>
|
||||||
|
<StyledTruncator component='h2'>
|
||||||
|
{environmentId}
|
||||||
|
</StyledTruncator>
|
||||||
|
</StyledHeaderTitle>
|
||||||
|
{children}
|
||||||
|
</StyledHeader>
|
||||||
|
</StyledAccordionSummary>
|
||||||
|
);
|
||||||
|
};
|
@ -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) => (
|
||||||
|
<Tooltip {...props} classes={{ popper: className }} />
|
||||||
|
))({
|
||||||
|
[`& .${tooltipClasses.tooltip}`]: {
|
||||||
|
maxWidth: 200,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
type FeatureOverviewEnvironmentMetrics = {
|
||||||
|
environmentMetric?: Pick<IFeatureEnvironmentMetrics, 'yes' | 'no'>;
|
||||||
|
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
|
||||||
|
<br />
|
||||||
|
received in the last hour
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<span>
|
||||||
|
The flag has been evaluated{' '}
|
||||||
|
<b>
|
||||||
|
<PrettifyLargeNumber value={total} /> times
|
||||||
|
</b>
|
||||||
|
</span>{' '}
|
||||||
|
<span>
|
||||||
|
and enabled{' '}
|
||||||
|
<b>
|
||||||
|
<PrettifyLargeNumber value={environmentMetric.yes} /> times
|
||||||
|
</b>{' '}
|
||||||
|
in the last hour
|
||||||
|
</span>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<StyledContainer>
|
||||||
|
{!collapsed ? <StyledInfo>{content}</StyledInfo> : null}
|
||||||
|
<StyledTooltip title={collapsed ? content : ''} arrow>
|
||||||
|
<StyledPercentageCircle data-loading>
|
||||||
|
<PercentageDonut percentage={percentage} size='3rem'>
|
||||||
|
{!isEmpty ? `${percentage}%` : null}
|
||||||
|
</PercentageDonut>
|
||||||
|
</StyledPercentageCircle>
|
||||||
|
</StyledTooltip>
|
||||||
|
</StyledContainer>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default FeatureOverviewEnvironmentMetrics;
|
@ -55,6 +55,9 @@ const StyledPercentageCircle = styled('div')(({ theme }) => ({
|
|||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated remove with `flagOverviewRedesign` flag
|
||||||
|
*/
|
||||||
const FeatureOverviewEnvironmentMetrics = ({
|
const FeatureOverviewEnvironmentMetrics = ({
|
||||||
environmentMetric,
|
environmentMetric,
|
||||||
disabled = false,
|
disabled = false,
|
@ -1,3 +1,4 @@
|
|||||||
|
import { styled } from '@mui/material';
|
||||||
import { useFeatureToggleSwitch } from 'component/project/Project/ProjectFeatureToggles/FeatureToggleSwitch/useFeatureToggleSwitch';
|
import { useFeatureToggleSwitch } from 'component/project/Project/ProjectFeatureToggles/FeatureToggleSwitch/useFeatureToggleSwitch';
|
||||||
import { FeatureToggleSwitch } from 'component/project/Project/ProjectFeatureToggles/FeatureToggleSwitch/FeatureToggleSwitch';
|
import { FeatureToggleSwitch } from 'component/project/Project/ProjectFeatureToggles/FeatureToggleSwitch/FeatureToggleSwitch';
|
||||||
import { useChangeRequestsEnabled } from 'hooks/useChangeRequestsEnabled';
|
import { useChangeRequestsEnabled } from 'hooks/useChangeRequestsEnabled';
|
||||||
@ -5,13 +6,21 @@ import { useFeature } from 'hooks/api/getters/useFeature/useFeature';
|
|||||||
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
|
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
|
||||||
import type { IFeatureEnvironment } from 'interfaces/featureToggle';
|
import type { IFeatureEnvironment } from 'interfaces/featureToggle';
|
||||||
|
|
||||||
interface IFeatureOverviewEnvironmentToggleProps {
|
const StyledContainer = styled('div')(({ theme }) => ({
|
||||||
environment: IFeatureEnvironment;
|
order: -1,
|
||||||
}
|
flex: 0,
|
||||||
|
}));
|
||||||
|
|
||||||
|
type FeatureOverviewEnvironmentToggleProps = {
|
||||||
|
environment: Pick<
|
||||||
|
IFeatureEnvironment,
|
||||||
|
'name' | 'type' | 'strategies' | 'enabled'
|
||||||
|
>;
|
||||||
|
};
|
||||||
|
|
||||||
export const FeatureOverviewEnvironmentToggle = ({
|
export const FeatureOverviewEnvironmentToggle = ({
|
||||||
environment: { name, type, strategies, enabled },
|
environment: { name, type, strategies, enabled },
|
||||||
}: IFeatureOverviewEnvironmentToggleProps) => {
|
}: FeatureOverviewEnvironmentToggleProps) => {
|
||||||
const projectId = useRequiredPathParam('projectId');
|
const projectId = useRequiredPathParam('projectId');
|
||||||
const featureId = useRequiredPathParam('featureId');
|
const featureId = useRequiredPathParam('featureId');
|
||||||
const { refetchFeature } = useFeature(projectId, featureId);
|
const { refetchFeature } = useFeature(projectId, featureId);
|
||||||
@ -37,7 +46,7 @@ export const FeatureOverviewEnvironmentToggle = ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<StyledContainer onClick={(event) => event.stopPropagation()}>
|
||||||
<FeatureToggleSwitch
|
<FeatureToggleSwitch
|
||||||
projectId={projectId}
|
projectId={projectId}
|
||||||
value={enabled}
|
value={enabled}
|
||||||
@ -46,6 +55,6 @@ export const FeatureOverviewEnvironmentToggle = ({
|
|||||||
onToggle={onToggle}
|
onToggle={onToggle}
|
||||||
/>
|
/>
|
||||||
{featureToggleModals}
|
{featureToggleModals}
|
||||||
</>
|
</StyledContainer>
|
||||||
);
|
);
|
||||||
};
|
};
|
@ -1,34 +1,66 @@
|
|||||||
|
import type { ReactNode } from 'react';
|
||||||
import { screen } from '@testing-library/react';
|
import { screen } from '@testing-library/react';
|
||||||
import { render } from 'utils/testRenderer';
|
import { render } from 'utils/testRenderer';
|
||||||
import FeatureOverviewEnvironment from './FeatureOverviewEnvironment';
|
import { FeatureOverviewEnvironment } from './FeatureOverviewEnvironment';
|
||||||
import { Route, Routes } from 'react-router-dom';
|
import { Route, Routes } from 'react-router-dom';
|
||||||
import { CREATE_FEATURE_STRATEGY } from 'component/providers/AccessProvider/permissions';
|
import { CREATE_FEATURE_STRATEGY } from 'component/providers/AccessProvider/permissions';
|
||||||
|
|
||||||
const environmentWithoutStrategies = {
|
const renderRoute = (element: ReactNode, permissions: any[] = []) =>
|
||||||
name: 'production',
|
|
||||||
enabled: true,
|
|
||||||
type: 'production',
|
|
||||||
strategies: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
test('should allow to add strategy', async () => {
|
|
||||||
render(
|
render(
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route
|
<Route
|
||||||
path='/projects/:projectId/features/:featureId/strategies/create'
|
path='/projects/:projectId/features/:featureId/strategies/create'
|
||||||
element={
|
element={element}
|
||||||
<FeatureOverviewEnvironment
|
|
||||||
env={environmentWithoutStrategies}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
</Routes>,
|
</Routes>,
|
||||||
{
|
{
|
||||||
route: '/projects/default/features/featureWithoutStrategies/strategies/create',
|
route: '/projects/default/features/featureId/strategies/create',
|
||||||
permissions: [{ permission: CREATE_FEATURE_STRATEGY }],
|
permissions,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
const button = await screen.findByText('Add strategy');
|
describe('FeatureOverviewEnvironment', () => {
|
||||||
expect(button).toBeEnabled();
|
test('should allow to add strategy', async () => {
|
||||||
|
renderRoute(
|
||||||
|
<FeatureOverviewEnvironment
|
||||||
|
environment={{
|
||||||
|
name: 'production',
|
||||||
|
enabled: false,
|
||||||
|
type: 'production',
|
||||||
|
strategies: [],
|
||||||
|
}}
|
||||||
|
/>,
|
||||||
|
[{ 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(
|
||||||
|
<Routes>
|
||||||
|
<Route
|
||||||
|
path='/projects/:projectId/features/:featureId/strategies/create'
|
||||||
|
element={
|
||||||
|
<FeatureOverviewEnvironment
|
||||||
|
environment={{
|
||||||
|
name: 'production',
|
||||||
|
enabled: false,
|
||||||
|
type: 'production',
|
||||||
|
strategies: [],
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Routes>,
|
||||||
|
{
|
||||||
|
route: '/projects/default/features/featureWithoutStrategies/strategies/create',
|
||||||
|
permissions: [],
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const button = await screen.findByText('Add strategy');
|
||||||
|
expect(button).toBeDisabled();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@ -1,249 +1,138 @@
|
|||||||
import {
|
import { Accordion, AccordionDetails, styled } from '@mui/material';
|
||||||
Accordion,
|
import type {
|
||||||
AccordionDetails,
|
IFeatureEnvironment,
|
||||||
AccordionSummary,
|
IFeatureEnvironmentMetrics,
|
||||||
Box,
|
} from 'interfaces/featureToggle';
|
||||||
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 EnvironmentAccordionBody from './EnvironmentAccordionBody/EnvironmentAccordionBody';
|
||||||
import { EnvironmentFooter } from './EnvironmentFooter/EnvironmentFooter';
|
|
||||||
import FeatureOverviewEnvironmentMetrics from './FeatureOverviewEnvironmentMetrics/FeatureOverviewEnvironmentMetrics';
|
|
||||||
import { FeatureStrategyMenu } from 'component/feature/FeatureStrategy/FeatureStrategyMenu/FeatureStrategyMenu';
|
import { FeatureStrategyMenu } from 'component/feature/FeatureStrategy/FeatureStrategyMenu/FeatureStrategyMenu';
|
||||||
import { FEATURE_ENVIRONMENT_ACCORDION } from 'utils/testIds';
|
import { FEATURE_ENVIRONMENT_ACCORDION } from 'utils/testIds';
|
||||||
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
|
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
|
||||||
import { FeatureStrategyIcons } from 'component/feature/FeatureStrategy/FeatureStrategyIcons/FeatureStrategyIcons';
|
import { UpgradeChangeRequests } from './UpgradeChangeRequests/UpgradeChangeRequests';
|
||||||
import { useGlobalLocalStorage } from 'hooks/useGlobalLocalStorage';
|
|
||||||
import { Badge } from 'component/common/Badge/Badge';
|
|
||||||
import { UpgradeChangeRequests } from './UpgradeChangeRequests';
|
|
||||||
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
|
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 {
|
const StyledFeatureOverviewEnvironment = styled('div')(({ theme }) => ({
|
||||||
env: IFeatureEnvironment;
|
|
||||||
}
|
|
||||||
|
|
||||||
const StyledFeatureOverviewEnvironment = styled('div', {
|
|
||||||
shouldForwardProp: (prop) => prop !== 'enabled',
|
|
||||||
})<{ enabled: boolean }>(({ theme, enabled }) => ({
|
|
||||||
borderRadius: theme.shape.borderRadiusLarge,
|
borderRadius: theme.shape.borderRadiusLarge,
|
||||||
marginBottom: theme.spacing(2),
|
backgroundColor: theme.palette.background.paper,
|
||||||
backgroundColor: enabled
|
border: `1px solid ${theme.palette.divider}`,
|
||||||
? theme.palette.background.paper
|
|
||||||
: theme.palette.envAccordion.disabled,
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const StyledAccordion = styled(Accordion)({
|
const StyledAccordion = styled(Accordion)(({ theme }) => ({
|
||||||
boxShadow: 'none',
|
boxShadow: 'none',
|
||||||
background: 'none',
|
background: 'none',
|
||||||
});
|
'&&& .MuiAccordionSummary-root': {
|
||||||
|
borderRadius: theme.shape.borderRadiusLarge,
|
||||||
const StyledAccordionSummary = styled(AccordionSummary)(({ theme }) => ({
|
pointerEvents: 'auto',
|
||||||
boxShadow: 'none',
|
opacity: 1,
|
||||||
padding: theme.spacing(2, 4),
|
backgroundColor: theme.palette.background.paper,
|
||||||
[theme.breakpoints.down(400)]: {
|
|
||||||
padding: theme.spacing(1, 2),
|
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const StyledAccordionDetails = styled(AccordionDetails, {
|
const StyledAccordionDetails = styled(AccordionDetails)(({ theme }) => ({
|
||||||
shouldForwardProp: (prop) => prop !== 'enabled',
|
padding: 0,
|
||||||
})<{ enabled: boolean }>(({ theme }) => ({
|
|
||||||
padding: theme.spacing(3),
|
|
||||||
background: theme.palette.envAccordion.expanded,
|
background: theme.palette.envAccordion.expanded,
|
||||||
borderBottomLeftRadius: theme.shape.borderRadiusLarge,
|
borderBottomLeftRadius: theme.shape.borderRadiusLarge,
|
||||||
borderBottomRightRadius: theme.shape.borderRadiusLarge,
|
borderBottomRightRadius: theme.shape.borderRadiusLarge,
|
||||||
boxShadow: theme.boxShadows.accordionFooter,
|
boxShadow: theme.boxShadows.accordionFooter,
|
||||||
|
|
||||||
[theme.breakpoints.down('md')]: {
|
[theme.breakpoints.down('md')]: {
|
||||||
padding: theme.spacing(2, 1),
|
padding: theme.spacing(2, 1),
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const StyledEnvironmentAccordionBody = styled(EnvironmentAccordionBody)(
|
const StyledAccordionFooter = styled('footer')(({ theme }) => ({
|
||||||
({ theme }) => ({
|
padding: theme.spacing(2, 3, 3),
|
||||||
width: '100%',
|
|
||||||
position: 'relative',
|
|
||||||
paddingBottom: theme.spacing(2),
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
const StyledHeader = styled('div', {
|
|
||||||
shouldForwardProp: (prop) => prop !== 'enabled',
|
|
||||||
})<{ enabled: boolean }>(({ theme, enabled }) => ({
|
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
justifyContent: 'center',
|
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
color: enabled ? theme.palette.text.primary : theme.palette.text.secondary,
|
alignItems: 'flex-end',
|
||||||
}));
|
|
||||||
|
|
||||||
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),
|
gap: theme.spacing(2),
|
||||||
flexWrap: 'wrap',
|
borderTop: `1px solid ${theme.palette.divider}`,
|
||||||
[theme.breakpoints.down(560)]: {
|
|
||||||
flexDirection: 'column',
|
|
||||||
},
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const FeatureOverviewEnvironment = ({
|
const StyledEnvironmentAccordionContainer = styled('div')(({ theme }) => ({
|
||||||
env,
|
width: '100%',
|
||||||
}: IFeatureOverviewEnvironmentProps) => {
|
position: 'relative',
|
||||||
const projectId = useRequiredPathParam('projectId');
|
padding: theme.spacing(3, 3, 1),
|
||||||
const featureId = useRequiredPathParam('featureId');
|
}));
|
||||||
const { metrics } = useFeatureMetrics(projectId, featureId);
|
|
||||||
const { feature } = useFeature(projectId, featureId);
|
|
||||||
const { value: globalStore } = useGlobalLocalStorage();
|
|
||||||
|
|
||||||
const featureMetrics = getFeatureMetrics(feature?.environments, metrics);
|
type FeatureOverviewEnvironmentProps = {
|
||||||
const environmentMetric = featureMetrics.find(
|
environment: IFeatureEnvironment & {
|
||||||
(featureMetric) => featureMetric.environment === env.name,
|
releasePlans?: IReleasePlan[];
|
||||||
);
|
};
|
||||||
const featureEnvironment = feature?.environments.find(
|
metrics?: Pick<IFeatureEnvironmentMetrics, 'yes' | 'no'>;
|
||||||
(featureEnvironment) => featureEnvironment.name === env.name,
|
otherEnvironments?: string[];
|
||||||
);
|
|
||||||
const { isOss } = useUiConfig();
|
|
||||||
const showChangeRequestUpgrade = env.type === 'production' && isOss();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ConditionallyRender
|
|
||||||
condition={!new Set(globalStore.hiddenEnvironments).has(env.name)}
|
|
||||||
show={
|
|
||||||
<StyledFeatureOverviewEnvironment enabled={env.enabled}>
|
|
||||||
<StyledAccordion
|
|
||||||
TransitionProps={{ mountOnEnter: true }}
|
|
||||||
data-testid={`${FEATURE_ENVIRONMENT_ACCORDION}_${env.name}`}
|
|
||||||
className={`environment-accordion ${
|
|
||||||
env.enabled ? '' : 'accordion-disabled'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<StyledAccordionSummary
|
|
||||||
expandIcon={<ExpandMore titleAccess='Toggle' />}
|
|
||||||
>
|
|
||||||
<StyledHeader data-loading enabled={env.enabled}>
|
|
||||||
<StyledHeaderTitle>
|
|
||||||
<StyledEnvironmentIcon
|
|
||||||
enabled={env.enabled}
|
|
||||||
/>
|
|
||||||
<div>
|
|
||||||
<StyledStringTruncator
|
|
||||||
text={env.name}
|
|
||||||
maxWidth='100'
|
|
||||||
maxLength={15}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<ConditionallyRender
|
|
||||||
condition={!env.enabled}
|
|
||||||
show={
|
|
||||||
<Badge
|
|
||||||
color='neutral'
|
|
||||||
sx={{ ml: 1 }}
|
|
||||||
>
|
|
||||||
Disabled
|
|
||||||
</Badge>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</StyledHeaderTitle>
|
|
||||||
<StyledButtonContainer>
|
|
||||||
<FeatureStrategyMenu
|
|
||||||
label='Add strategy'
|
|
||||||
projectId={projectId}
|
|
||||||
featureId={featureId}
|
|
||||||
environmentId={env.name}
|
|
||||||
variant='outlined'
|
|
||||||
size='small'
|
|
||||||
/>
|
|
||||||
<FeatureStrategyIcons
|
|
||||||
strategies={
|
|
||||||
featureEnvironment?.strategies
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</StyledButtonContainer>
|
|
||||||
</StyledHeader>
|
|
||||||
|
|
||||||
<FeatureOverviewEnvironmentMetrics
|
|
||||||
environmentMetric={environmentMetric}
|
|
||||||
disabled={!env.enabled}
|
|
||||||
/>
|
|
||||||
</StyledAccordionSummary>
|
|
||||||
|
|
||||||
<StyledAccordionDetails enabled={env.enabled}>
|
|
||||||
<StyledEnvironmentAccordionBody
|
|
||||||
featureEnvironment={featureEnvironment}
|
|
||||||
isDisabled={!env.enabled}
|
|
||||||
otherEnvironments={feature?.environments
|
|
||||||
.map(({ name }) => name)
|
|
||||||
.filter((name) => name !== env.name)}
|
|
||||||
/>
|
|
||||||
<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={env.name}
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
<EnvironmentFooter
|
|
||||||
environmentMetric={
|
|
||||||
environmentMetric
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
{showChangeRequestUpgrade ? (
|
|
||||||
<UpgradeChangeRequests />
|
|
||||||
) : null}
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</StyledAccordionDetails>
|
|
||||||
</StyledAccordion>
|
|
||||||
</StyledFeatureOverviewEnvironment>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
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 (
|
||||||
|
<StyledFeatureOverviewEnvironment>
|
||||||
|
<StyledAccordion
|
||||||
|
TransitionProps={{ mountOnEnter: true, unmountOnExit: true }}
|
||||||
|
data-testid={`${FEATURE_ENVIRONMENT_ACCORDION}_${environment.name}`}
|
||||||
|
expanded={isOpen && hasActivations}
|
||||||
|
disabled={!hasActivations}
|
||||||
|
onChange={() => setIsOopen(isOpen ? !isOpen : hasActivations)}
|
||||||
|
>
|
||||||
|
<EnvironmentHeader
|
||||||
|
environmentId={environment.name}
|
||||||
|
expandable={hasActivations}
|
||||||
|
>
|
||||||
|
<FeatureOverviewEnvironmentToggle
|
||||||
|
environment={environment}
|
||||||
|
/>
|
||||||
|
{!hasActivations ? (
|
||||||
|
<FeatureStrategyMenu
|
||||||
|
label='Add strategy'
|
||||||
|
projectId={projectId}
|
||||||
|
featureId={featureId}
|
||||||
|
environmentId={environment.name}
|
||||||
|
variant='outlined'
|
||||||
|
size='small'
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
<FeatureOverviewEnvironmentMetrics
|
||||||
|
environmentMetric={metrics}
|
||||||
|
collapsed={!hasActivations}
|
||||||
|
/>
|
||||||
|
</EnvironmentHeader>
|
||||||
|
<StyledAccordionDetails>
|
||||||
|
<StyledEnvironmentAccordionContainer>
|
||||||
|
<EnvironmentAccordionBody
|
||||||
|
featureEnvironment={environment}
|
||||||
|
isDisabled={!environment.enabled}
|
||||||
|
otherEnvironments={otherEnvironments}
|
||||||
|
/>
|
||||||
|
</StyledEnvironmentAccordionContainer>
|
||||||
|
<StyledAccordionFooter>
|
||||||
|
<FeatureStrategyMenu
|
||||||
|
label='Add strategy'
|
||||||
|
projectId={projectId}
|
||||||
|
featureId={featureId}
|
||||||
|
environmentId={environment.name}
|
||||||
|
/>
|
||||||
|
{isOss() && environment?.type === 'production' ? (
|
||||||
|
<UpgradeChangeRequests />
|
||||||
|
) : null}
|
||||||
|
</StyledAccordionFooter>
|
||||||
|
</StyledAccordionDetails>
|
||||||
|
</StyledAccordion>
|
||||||
|
</StyledFeatureOverviewEnvironment>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
@ -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 (
|
||||||
|
<ConditionallyRender
|
||||||
|
condition={!new Set(globalStore.hiddenEnvironments).has(env.name)}
|
||||||
|
show={
|
||||||
|
<StyledFeatureOverviewEnvironment enabled={env.enabled}>
|
||||||
|
<StyledAccordion
|
||||||
|
TransitionProps={{ mountOnEnter: true }}
|
||||||
|
data-testid={`${FEATURE_ENVIRONMENT_ACCORDION}_${env.name}`}
|
||||||
|
className={`environment-accordion ${
|
||||||
|
env.enabled ? '' : 'accordion-disabled'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<StyledAccordionSummary
|
||||||
|
expandIcon={<ExpandMore titleAccess='Toggle' />}
|
||||||
|
>
|
||||||
|
<StyledHeader data-loading enabled={env.enabled}>
|
||||||
|
<StyledHeaderTitle>
|
||||||
|
<StyledEnvironmentIcon
|
||||||
|
enabled={env.enabled}
|
||||||
|
/>
|
||||||
|
<div>
|
||||||
|
<StyledStringTruncator
|
||||||
|
text={env.name}
|
||||||
|
maxWidth='100'
|
||||||
|
maxLength={15}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<ConditionallyRender
|
||||||
|
condition={!env.enabled}
|
||||||
|
show={
|
||||||
|
<Badge
|
||||||
|
color='neutral'
|
||||||
|
sx={{ ml: 1 }}
|
||||||
|
>
|
||||||
|
Disabled
|
||||||
|
</Badge>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</StyledHeaderTitle>
|
||||||
|
<StyledButtonContainer>
|
||||||
|
<FeatureStrategyMenu
|
||||||
|
label='Add strategy'
|
||||||
|
projectId={projectId}
|
||||||
|
featureId={featureId}
|
||||||
|
environmentId={env.name}
|
||||||
|
variant='outlined'
|
||||||
|
size='small'
|
||||||
|
/>
|
||||||
|
<FeatureStrategyIcons
|
||||||
|
strategies={
|
||||||
|
featureEnvironment?.strategies
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</StyledButtonContainer>
|
||||||
|
</StyledHeader>
|
||||||
|
|
||||||
|
<FeatureOverviewEnvironmentMetrics
|
||||||
|
environmentMetric={environmentMetric}
|
||||||
|
disabled={!env.enabled}
|
||||||
|
/>
|
||||||
|
</StyledAccordionSummary>
|
||||||
|
|
||||||
|
<StyledAccordionDetails enabled={env.enabled}>
|
||||||
|
<StyledEnvironmentAccordionBody
|
||||||
|
featureEnvironment={featureEnvironment}
|
||||||
|
isDisabled={!env.enabled}
|
||||||
|
otherEnvironments={feature?.environments
|
||||||
|
.map(({ name }) => name)
|
||||||
|
.filter((name) => name !== env.name)}
|
||||||
|
/>
|
||||||
|
<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={env.name}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
<EnvironmentFooter
|
||||||
|
environmentMetric={
|
||||||
|
environmentMetric
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
{showChangeRequestUpgrade ? (
|
||||||
|
<UpgradeChangeRequests />
|
||||||
|
) : null}
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</StyledAccordionDetails>
|
||||||
|
</StyledAccordion>
|
||||||
|
</StyledFeatureOverviewEnvironment>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default FeatureOverviewEnvironment;
|
@ -1,23 +1,79 @@
|
|||||||
|
import type { ComponentProps, FC } from 'react';
|
||||||
|
import { useUiFlag } from 'hooks/useUiFlag';
|
||||||
import { useFeature } from 'hooks/api/getters/useFeature/useFeature';
|
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 { 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<typeof FeatureOverviewEnvironment>
|
||||||
|
> = ({ environment, ...props }) => {
|
||||||
const projectId = useRequiredPathParam('projectId');
|
const projectId = useRequiredPathParam('projectId');
|
||||||
const featureId = useRequiredPathParam('featureId');
|
const featureId = useRequiredPathParam('featureId');
|
||||||
const { feature } = useFeature(projectId, featureId);
|
const { releasePlans } = useReleasePlans(
|
||||||
|
projectId,
|
||||||
if (!feature) return null;
|
featureId,
|
||||||
|
environment?.name,
|
||||||
const { environments } = feature;
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<FeatureOverviewEnvironment
|
||||||
{environments?.map((env) => (
|
{...props}
|
||||||
<FeatureOverviewEnvironment env={env} key={env.name} />
|
environment={{ ...environment, releasePlans }}
|
||||||
))}
|
/>
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
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) => (
|
||||||
|
<LegacyFeatureOverviewEnvironment
|
||||||
|
env={env}
|
||||||
|
key={env.name}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return feature.environments
|
||||||
|
?.filter((env) => !hiddenEnvironments.includes(env.name))
|
||||||
|
.map((env) => (
|
||||||
|
<FeatureOverviewWithReleasePlans
|
||||||
|
environment={env}
|
||||||
|
key={env.name}
|
||||||
|
metrics={featureMetrics.find(
|
||||||
|
(featureMetric) => featureMetric.environment === env?.name,
|
||||||
|
)}
|
||||||
|
otherEnvironments={
|
||||||
|
feature.environments
|
||||||
|
?.map((e) => e.name)
|
||||||
|
.filter(
|
||||||
|
(name) =>
|
||||||
|
name !== env.name &&
|
||||||
|
!hiddenEnvironments.includes(name),
|
||||||
|
) || []
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
};
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import FeatureOverviewEnvironments from './FeatureOverviewEnvironments/FeatureOverviewEnvironments';
|
import { FeatureOverviewEnvironments } from './FeatureOverviewEnvironments/FeatureOverviewEnvironments';
|
||||||
import { Route, Routes, useNavigate } from 'react-router-dom';
|
import { Route, Routes, useNavigate } from 'react-router-dom';
|
||||||
import { SidebarModal } from 'component/common/SidebarModal/SidebarModal';
|
import { SidebarModal } from 'component/common/SidebarModal/SidebarModal';
|
||||||
import {
|
import {
|
||||||
|
@ -51,6 +51,9 @@ const StyledBadge = styled(Badge)(({ theme }) => ({
|
|||||||
color: theme.palette.common.white,
|
color: theme.palette.common.white,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated initial version, clean up after done with `flagOverviewRedesign`
|
||||||
|
*/
|
||||||
export const FeatureOverviewEnvironmentBody = ({
|
export const FeatureOverviewEnvironmentBody = ({
|
||||||
featureEnvironment,
|
featureEnvironment,
|
||||||
isDisabled,
|
isDisabled,
|
||||||
|
@ -3,10 +3,10 @@ import { useFeature } from 'hooks/api/getters/useFeature/useFeature';
|
|||||||
import useFeatureMetrics from 'hooks/api/getters/useFeatureMetrics/useFeatureMetrics';
|
import useFeatureMetrics from 'hooks/api/getters/useFeatureMetrics/useFeatureMetrics';
|
||||||
import { getFeatureMetrics } from 'utils/getFeatureMetrics';
|
import { getFeatureMetrics } from 'utils/getFeatureMetrics';
|
||||||
import { FeatureOverviewEnvironmentBody } from './FeatureOverviewEnvironmentBody';
|
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 { FeatureStrategyMenu } from 'component/feature/FeatureStrategy/FeatureStrategyMenu/FeatureStrategyMenu';
|
||||||
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
|
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
|
||||||
import { FeatureOverviewEnvironmentToggle } from './FeatureOverviewEnvironmentToggle';
|
import { FeatureOverviewEnvironmentToggle } from '../FeatureOverviewEnvironments/FeatureOverviewEnvironment/EnvironmentHeader/FeatureOverviewEnvironmentToggle/FeatureOverviewEnvironmentToggle';
|
||||||
|
|
||||||
const StyledFeatureOverviewEnvironment = styled('div')(({ theme }) => ({
|
const StyledFeatureOverviewEnvironment = styled('div')(({ theme }) => ({
|
||||||
padding: theme.spacing(1, 3),
|
padding: theme.spacing(1, 3),
|
||||||
@ -55,6 +55,9 @@ interface INewFeatureOverviewEnvironmentProps {
|
|||||||
hiddenEnvironments: string[];
|
hiddenEnvironments: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated initial version, clean up after done with `flagOverviewRedesign`
|
||||||
|
*/
|
||||||
export const FeatureOverviewEnvironment = ({
|
export const FeatureOverviewEnvironment = ({
|
||||||
hiddenEnvironments,
|
hiddenEnvironments,
|
||||||
}: INewFeatureOverviewEnvironmentProps) => {
|
}: INewFeatureOverviewEnvironmentProps) => {
|
||||||
|
@ -509,13 +509,13 @@ export const lightTheme = createTheme({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
// Environment accordion
|
|
||||||
MuiAccordion: {
|
MuiAccordion: {
|
||||||
styleOverrides: {
|
styleOverrides: {
|
||||||
root: ({ theme }) => ({
|
root: ({ theme }) => ({
|
||||||
'&:first-of-type, &:last-of-type': {
|
'&:first-of-type, &:last-of-type': {
|
||||||
borderRadius: theme.shape.borderRadiusLarge,
|
borderRadius: theme.shape.borderRadiusLarge,
|
||||||
},
|
},
|
||||||
|
// Environment accordion -- remove with `flagOverviewRedesign` flag
|
||||||
'&.environment-accordion.Mui-expanded': {
|
'&.environment-accordion.Mui-expanded': {
|
||||||
outline: `2px solid ${alpha(
|
outline: `2px solid ${alpha(
|
||||||
theme.palette.background.alternative,
|
theme.palette.background.alternative,
|
||||||
|
@ -13,11 +13,10 @@ const emptyMetric = (environment: string) => ({
|
|||||||
export const getFeatureMetrics = (
|
export const getFeatureMetrics = (
|
||||||
environments: IFeatureEnvironment[],
|
environments: IFeatureEnvironment[],
|
||||||
metrics: IFeatureMetrics,
|
metrics: IFeatureMetrics,
|
||||||
) => {
|
) =>
|
||||||
return environments.map((env) => {
|
environments.map((env) => {
|
||||||
const envMetric = metrics.lastHourUsage.find(
|
const envMetric = metrics.lastHourUsage.find(
|
||||||
(metric) => metric.environment === env.name,
|
(metric) => metric.environment === env.name,
|
||||||
);
|
);
|
||||||
return envMetric || emptyMetric(env.name);
|
return envMetric || emptyMetric(env.name);
|
||||||
});
|
});
|
||||||
};
|
|
||||||
|
Loading…
Reference in New Issue
Block a user