mirror of
https://github.com/Unleash/unleash.git
synced 2025-04-10 01:16:39 +02:00
feat: show and hide environments (#9323)
- Button to show and hide environments - Refactored hook storing state of hidden environments - Changed the way flag is triggered for feature overview - Visual updates for new page look --------- Co-authored-by: Thomas Heartman <thomas@getunleash.io>
This commit is contained in:
parent
c16f7208a1
commit
2a6487e7e9
@ -1,5 +1,4 @@
|
|||||||
import NewFeatureOverviewMetaData from './FeatureOverviewMetaData/FeatureOverviewMetaData';
|
import FeatureOverviewMetaData from './FeatureOverviewMetaData/FeatureOverviewMetaData';
|
||||||
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 {
|
||||||
@ -8,21 +7,20 @@ import {
|
|||||||
} from 'component/feature/FeatureStrategy/FeatureStrategyEdit/FeatureStrategyEdit';
|
} from 'component/feature/FeatureStrategy/FeatureStrategyEdit/FeatureStrategyEdit';
|
||||||
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
|
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
|
||||||
import { usePageTitle } from 'hooks/usePageTitle';
|
import { usePageTitle } from 'hooks/usePageTitle';
|
||||||
import { FeatureOverviewSidePanel as NewFeatureOverviewSidePanel } from 'component/feature/FeatureView/FeatureOverview/FeatureOverviewSidePanel/FeatureOverviewSidePanel';
|
|
||||||
import { useHiddenEnvironments } from 'hooks/useHiddenEnvironments';
|
|
||||||
import { styled } from '@mui/material';
|
import { styled } from '@mui/material';
|
||||||
import { FeatureStrategyCreate } from 'component/feature/FeatureStrategy/FeatureStrategyCreate/FeatureStrategyCreate';
|
import { FeatureStrategyCreate } from 'component/feature/FeatureStrategy/FeatureStrategyCreate/FeatureStrategyCreate';
|
||||||
import { useEffect, useState } 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 OldFeatureOverviewMetaData from './FeatureOverviewMetaData/OldFeatureOverviewMetaData';
|
import { FeatureOverviewEnvironment } from './NewFeatureOverviewEnvironment/NewFeatureOverviewEnvironment';
|
||||||
import { OldFeatureOverviewSidePanel } from 'component/feature/FeatureView/FeatureOverview/FeatureOverviewSidePanel/OldFeatureOverviewSidePanel';
|
import { default as LegacyFleatureOverview } from './LegacyFeatureOverview';
|
||||||
import { NewFeatureOverviewEnvironment } from './NewFeatureOverviewEnvironment/NewFeatureOverviewEnvironment';
|
import { useEnvironmentVisibility } from './FeatureOverviewMetaData/EnvironmentVisibilityMenu/hooks/useEnvironmentVisibility';
|
||||||
|
|
||||||
const StyledContainer = styled('div')(({ theme }) => ({
|
const StyledContainer = styled('div')(({ theme }) => ({
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
width: '100%',
|
width: '100%',
|
||||||
[theme.breakpoints.down(1000)]: {
|
gap: theme.spacing(2),
|
||||||
|
[theme.breakpoints.down('md')]: {
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
@ -30,57 +28,43 @@ const StyledContainer = styled('div')(({ theme }) => ({
|
|||||||
const StyledMainContent = styled('div')(({ theme }) => ({
|
const StyledMainContent = styled('div')(({ theme }) => ({
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
width: `calc(100% - (350px + 1rem))`,
|
flexGrow: 1,
|
||||||
[theme.breakpoints.down(1000)]: {
|
gap: theme.spacing(2),
|
||||||
width: '100%',
|
|
||||||
},
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const FeatureOverview = () => {
|
export const FeatureOverview = () => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const projectId = useRequiredPathParam('projectId');
|
const projectId = useRequiredPathParam('projectId');
|
||||||
const featureId = useRequiredPathParam('featureId');
|
const featureId = useRequiredPathParam('featureId');
|
||||||
const featurePath = formatFeaturePath(projectId, featureId);
|
const featurePath = formatFeaturePath(projectId, featureId);
|
||||||
const { hiddenEnvironments, setHiddenEnvironments } =
|
const { hiddenEnvironments, onEnvironmentVisibilityChange } =
|
||||||
useHiddenEnvironments();
|
useEnvironmentVisibility();
|
||||||
const onSidebarClose = () => navigate(featurePath);
|
const onSidebarClose = () => navigate(featurePath);
|
||||||
usePageTitle(featureId);
|
usePageTitle(featureId);
|
||||||
const { setLastViewed } = useLastViewedFlags();
|
const { setLastViewed } = useLastViewedFlags();
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setLastViewed({ featureId, projectId });
|
setLastViewed({ featureId, projectId });
|
||||||
}, [featureId]);
|
}, [featureId]);
|
||||||
const [environmentId, setEnvironmentId] = useState('');
|
|
||||||
const flagOverviewRedesign = useUiFlag('flagOverviewRedesign');
|
const flagOverviewRedesign = useUiFlag('flagOverviewRedesign');
|
||||||
|
|
||||||
|
if (!flagOverviewRedesign) {
|
||||||
|
return <LegacyFleatureOverview />;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledContainer>
|
<StyledContainer>
|
||||||
<div>
|
<div>
|
||||||
{flagOverviewRedesign ? (
|
<FeatureOverviewMetaData
|
||||||
<>
|
hiddenEnvironments={hiddenEnvironments}
|
||||||
<NewFeatureOverviewMetaData />
|
onEnvironmentVisibilityChange={
|
||||||
<NewFeatureOverviewSidePanel
|
onEnvironmentVisibilityChange
|
||||||
environmentId={environmentId}
|
}
|
||||||
setEnvironmentId={setEnvironmentId}
|
/>
|
||||||
/>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<OldFeatureOverviewMetaData />
|
|
||||||
<OldFeatureOverviewSidePanel
|
|
||||||
hiddenEnvironments={hiddenEnvironments}
|
|
||||||
setHiddenEnvironments={setHiddenEnvironments}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
<StyledMainContent>
|
<StyledMainContent>
|
||||||
{flagOverviewRedesign ? (
|
<FeatureOverviewEnvironment
|
||||||
<NewFeatureOverviewEnvironment
|
hiddenEnvironments={hiddenEnvironments}
|
||||||
environmentId={environmentId}
|
/>
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<FeatureOverviewEnvironments />
|
|
||||||
)}
|
|
||||||
</StyledMainContent>
|
</StyledMainContent>
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route
|
<Route
|
||||||
@ -111,5 +95,3 @@ const FeatureOverview = () => {
|
|||||||
</StyledContainer>
|
</StyledContainer>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default FeatureOverview;
|
|
||||||
|
@ -29,6 +29,7 @@ export const AddTagButton: FC<AddTagButtonProps> = ({ project, onClick }) => (
|
|||||||
variant='text'
|
variant='text'
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
startIcon={<StyledAddIcon />}
|
startIcon={<StyledAddIcon />}
|
||||||
|
data-loading
|
||||||
>
|
>
|
||||||
Add tag
|
Add tag
|
||||||
</StyledAddTagButton>
|
</StyledAddTagButton>
|
||||||
|
@ -0,0 +1,69 @@
|
|||||||
|
import { Button, Checkbox, Menu, MenuItem, styled } from '@mui/material';
|
||||||
|
import { useState, type FC } from 'react';
|
||||||
|
|
||||||
|
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
|
||||||
|
import ExpandLessIcon from '@mui/icons-material/ExpandLess';
|
||||||
|
|
||||||
|
type EnvironmentVisibilityMenuProps = {
|
||||||
|
environments: Array<{ name: string }>;
|
||||||
|
hiddenEnvironments: string[];
|
||||||
|
onChange: (name: string) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const buttonId = 'environment-visibility-button';
|
||||||
|
const menuId = 'environment-visibility-menu';
|
||||||
|
|
||||||
|
const StyledContainer = styled('div')(({ theme }) => ({
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'center',
|
||||||
|
paddingTop: theme.spacing(4),
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const EnvironmentVisibilityMenu: FC<EnvironmentVisibilityMenuProps> = ({
|
||||||
|
environments,
|
||||||
|
hiddenEnvironments,
|
||||||
|
onChange,
|
||||||
|
}) => {
|
||||||
|
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
|
||||||
|
const isOpen = Boolean(anchorEl);
|
||||||
|
const handleOpen = (event: React.MouseEvent<HTMLButtonElement>) => {
|
||||||
|
setAnchorEl(event.currentTarget);
|
||||||
|
};
|
||||||
|
const handleClose = () => {
|
||||||
|
setAnchorEl(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<StyledContainer>
|
||||||
|
<Button
|
||||||
|
onClick={handleOpen}
|
||||||
|
endIcon={isOpen ? <ExpandLessIcon /> : <ExpandMoreIcon />}
|
||||||
|
variant='outlined'
|
||||||
|
id={buttonId}
|
||||||
|
aria-controls={isOpen ? menuId : undefined}
|
||||||
|
aria-haspopup='true'
|
||||||
|
aria-expanded={isOpen ? 'true' : undefined}
|
||||||
|
data-loading
|
||||||
|
>
|
||||||
|
Hide/show environments
|
||||||
|
</Button>
|
||||||
|
<Menu
|
||||||
|
id={menuId}
|
||||||
|
anchorEl={anchorEl}
|
||||||
|
open={isOpen}
|
||||||
|
onClose={handleClose}
|
||||||
|
MenuListProps={{ 'aria-labelledby': buttonId }}
|
||||||
|
>
|
||||||
|
{environments.map(({ name }) => (
|
||||||
|
<MenuItem key={name} onClick={() => onChange(name)}>
|
||||||
|
<Checkbox
|
||||||
|
onChange={() => onChange(name)}
|
||||||
|
checked={!hiddenEnvironments?.includes(name)}
|
||||||
|
/>
|
||||||
|
{name}
|
||||||
|
</MenuItem>
|
||||||
|
))}
|
||||||
|
</Menu>
|
||||||
|
</StyledContainer>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,39 @@
|
|||||||
|
import { useLocalStorageState } from 'hooks/useLocalStorageState';
|
||||||
|
import { usePlausibleTracker } from 'hooks/usePlausibleTracker';
|
||||||
|
import { createLocalStorage } from 'utils/createLocalStorage';
|
||||||
|
|
||||||
|
// Reading legacy value will be safely refactored out in a next version - related to `flagOverviewRedesign` flag
|
||||||
|
const { value: legacyStoreValue } = createLocalStorage<{
|
||||||
|
hiddenEnvironments?: Array<string>;
|
||||||
|
}>('global:v1', {});
|
||||||
|
|
||||||
|
export const useEnvironmentVisibility = () => {
|
||||||
|
const [value, setValue] = useLocalStorageState<Array<string>>(
|
||||||
|
'environment-visibiilty',
|
||||||
|
legacyStoreValue?.hiddenEnvironments || [],
|
||||||
|
);
|
||||||
|
const { trackEvent } = usePlausibleTracker();
|
||||||
|
|
||||||
|
const onEnvironmentVisibilityChange = (environment: string) => {
|
||||||
|
if (value.includes(environment)) {
|
||||||
|
setValue(value.filter((env) => env !== environment));
|
||||||
|
trackEvent('hidden_environment', {
|
||||||
|
props: {
|
||||||
|
eventType: `environment unhidden`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
setValue([...value, environment]);
|
||||||
|
trackEvent('hidden_environment', {
|
||||||
|
props: {
|
||||||
|
eventType: `environment hidden`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
hiddenEnvironments: value,
|
||||||
|
onEnvironmentVisibilityChange,
|
||||||
|
};
|
||||||
|
};
|
@ -1,9 +1,9 @@
|
|||||||
|
import { type FC, useState } from 'react';
|
||||||
import { styled } from '@mui/material';
|
import { styled } from '@mui/material';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { useFeature } from 'hooks/api/getters/useFeature/useFeature';
|
import { useFeature } from 'hooks/api/getters/useFeature/useFeature';
|
||||||
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
|
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
|
||||||
import { FeatureArchiveDialog } from 'component/common/FeatureArchiveDialog/FeatureArchiveDialog';
|
import { FeatureArchiveDialog } from 'component/common/FeatureArchiveDialog/FeatureArchiveDialog';
|
||||||
import { useState } from 'react';
|
|
||||||
import { FeatureArchiveNotAllowedDialog } from 'component/common/FeatureArchiveDialog/FeatureArchiveNotAllowedDialog';
|
import { FeatureArchiveNotAllowedDialog } from 'component/common/FeatureArchiveDialog/FeatureArchiveNotAllowedDialog';
|
||||||
import { formatDateYMD } from 'utils/formatDate';
|
import { formatDateYMD } from 'utils/formatDate';
|
||||||
import { parseISO } from 'date-fns';
|
import { parseISO } from 'date-fns';
|
||||||
@ -15,6 +15,7 @@ import { MarkCompletedDialogue } from '../FeatureLifecycle/MarkCompletedDialogue
|
|||||||
import { TagRow } from './TagRow';
|
import { TagRow } from './TagRow';
|
||||||
import { capitalizeFirst } from 'utils/capitalizeFirst';
|
import { capitalizeFirst } from 'utils/capitalizeFirst';
|
||||||
import { Collaborators } from './Collaborators';
|
import { Collaborators } from './Collaborators';
|
||||||
|
import { EnvironmentVisibilityMenu } from './EnvironmentVisibilityMenu/EnvironmentVisibilityMenu';
|
||||||
|
|
||||||
const StyledMetaDataContainer = styled('div')(({ theme }) => ({
|
const StyledMetaDataContainer = styled('div')(({ theme }) => ({
|
||||||
padding: theme.spacing(3),
|
padding: theme.spacing(3),
|
||||||
@ -24,7 +25,8 @@ const StyledMetaDataContainer = styled('div')(({ theme }) => ({
|
|||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
gap: theme.spacing(2),
|
gap: theme.spacing(2),
|
||||||
width: '350px',
|
width: '350px',
|
||||||
[theme.breakpoints.down(1000)]: {
|
border: `1px solid ${theme.palette.divider}`,
|
||||||
|
[theme.breakpoints.down('md')]: {
|
||||||
width: '100%',
|
width: '100%',
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
@ -63,7 +65,15 @@ export const StyledMetaDataItemValue = styled('div')(({ theme }) => ({
|
|||||||
gap: theme.spacing(1),
|
gap: theme.spacing(1),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const FeatureOverviewMetaData = () => {
|
type FeatureOverviewMetaDataProps = {
|
||||||
|
hiddenEnvironments?: string[];
|
||||||
|
onEnvironmentVisibilityChange?: (environment: string) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const FeatureOverviewMetaData: FC<FeatureOverviewMetaDataProps> = ({
|
||||||
|
hiddenEnvironments,
|
||||||
|
onEnvironmentVisibilityChange,
|
||||||
|
}) => {
|
||||||
const projectId = useRequiredPathParam('projectId');
|
const projectId = useRequiredPathParam('projectId');
|
||||||
const featureId = useRequiredPathParam('featureId');
|
const featureId = useRequiredPathParam('featureId');
|
||||||
const { feature, refetchFeature } = useFeature(projectId, featureId);
|
const { feature, refetchFeature } = useFeature(projectId, featureId);
|
||||||
@ -156,6 +166,13 @@ const FeatureOverviewMetaData = () => {
|
|||||||
<DependencyRow feature={feature} />
|
<DependencyRow feature={feature} />
|
||||||
) : null}
|
) : null}
|
||||||
<TagRow feature={feature} />
|
<TagRow feature={feature} />
|
||||||
|
{onEnvironmentVisibilityChange ? (
|
||||||
|
<EnvironmentVisibilityMenu
|
||||||
|
environments={feature.environments || []}
|
||||||
|
hiddenEnvironments={hiddenEnvironments || []}
|
||||||
|
onChange={onEnvironmentVisibilityChange}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
</StyledBody>
|
</StyledBody>
|
||||||
</StyledMetaDataContainer>
|
</StyledMetaDataContainer>
|
||||||
{feature.children.length > 0 ? (
|
{feature.children.length > 0 ? (
|
||||||
|
@ -19,7 +19,7 @@ const StyledContainer = styled(Box)(({ theme }) => ({
|
|||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
gap: theme.spacing(2),
|
gap: theme.spacing(2),
|
||||||
width: '350px',
|
width: '350px',
|
||||||
[theme.breakpoints.down(1000)]: {
|
[theme.breakpoints.down('md')]: {
|
||||||
width: '100%',
|
width: '100%',
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
@ -0,0 +1,91 @@
|
|||||||
|
import FeatureOverviewEnvironments from './FeatureOverviewEnvironments/FeatureOverviewEnvironments';
|
||||||
|
import { Route, Routes, useNavigate } from 'react-router-dom';
|
||||||
|
import { SidebarModal } from 'component/common/SidebarModal/SidebarModal';
|
||||||
|
import {
|
||||||
|
FeatureStrategyEdit,
|
||||||
|
formatFeaturePath,
|
||||||
|
} from 'component/feature/FeatureStrategy/FeatureStrategyEdit/FeatureStrategyEdit';
|
||||||
|
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
|
||||||
|
import { usePageTitle } from 'hooks/usePageTitle';
|
||||||
|
import { useHiddenEnvironments } from 'hooks/useHiddenEnvironments';
|
||||||
|
import { styled } from '@mui/material';
|
||||||
|
import { FeatureStrategyCreate } from 'component/feature/FeatureStrategy/FeatureStrategyCreate/FeatureStrategyCreate';
|
||||||
|
import { useEffect } from 'react';
|
||||||
|
import { useLastViewedFlags } from 'hooks/useLastViewedFlags';
|
||||||
|
import OldFeatureOverviewMetaData from './FeatureOverviewMetaData/OldFeatureOverviewMetaData';
|
||||||
|
import { OldFeatureOverviewSidePanel } from 'component/feature/FeatureView/FeatureOverview/FeatureOverviewSidePanel/OldFeatureOverviewSidePanel';
|
||||||
|
|
||||||
|
const StyledContainer = styled('div')(({ theme }) => ({
|
||||||
|
display: 'flex',
|
||||||
|
width: '100%',
|
||||||
|
[theme.breakpoints.down(1000)]: {
|
||||||
|
flexDirection: 'column',
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
const StyledMainContent = styled('div')(({ theme }) => ({
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
width: `calc(100% - (350px + 1rem))`,
|
||||||
|
[theme.breakpoints.down(1000)]: {
|
||||||
|
width: '100%',
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
const FeatureOverview = () => {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const projectId = useRequiredPathParam('projectId');
|
||||||
|
const featureId = useRequiredPathParam('featureId');
|
||||||
|
const featurePath = formatFeaturePath(projectId, featureId);
|
||||||
|
const { hiddenEnvironments, setHiddenEnvironments } =
|
||||||
|
useHiddenEnvironments();
|
||||||
|
const onSidebarClose = () => navigate(featurePath);
|
||||||
|
usePageTitle(featureId);
|
||||||
|
const { setLastViewed } = useLastViewedFlags();
|
||||||
|
useEffect(() => {
|
||||||
|
setLastViewed({ featureId, projectId });
|
||||||
|
}, [featureId]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<StyledContainer>
|
||||||
|
<div>
|
||||||
|
<OldFeatureOverviewMetaData />
|
||||||
|
<OldFeatureOverviewSidePanel
|
||||||
|
hiddenEnvironments={hiddenEnvironments}
|
||||||
|
setHiddenEnvironments={setHiddenEnvironments}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<StyledMainContent>
|
||||||
|
<FeatureOverviewEnvironments />
|
||||||
|
</StyledMainContent>
|
||||||
|
<Routes>
|
||||||
|
<Route
|
||||||
|
path='strategies/create'
|
||||||
|
element={
|
||||||
|
<SidebarModal
|
||||||
|
label='Create feature strategy'
|
||||||
|
onClose={onSidebarClose}
|
||||||
|
open
|
||||||
|
>
|
||||||
|
<FeatureStrategyCreate />
|
||||||
|
</SidebarModal>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path='strategies/edit'
|
||||||
|
element={
|
||||||
|
<SidebarModal
|
||||||
|
label='Edit feature strategy'
|
||||||
|
onClose={onSidebarClose}
|
||||||
|
open
|
||||||
|
>
|
||||||
|
<FeatureStrategyEdit />
|
||||||
|
</SidebarModal>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Routes>
|
||||||
|
</StyledContainer>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default FeatureOverview;
|
@ -8,7 +8,6 @@ import { Alert, Pagination, styled } from '@mui/material';
|
|||||||
import useFeatureStrategyApi from 'hooks/api/actions/useFeatureStrategyApi/useFeatureStrategyApi';
|
import useFeatureStrategyApi from 'hooks/api/actions/useFeatureStrategyApi/useFeatureStrategyApi';
|
||||||
import { formatUnknownError } from 'utils/formatUnknownError';
|
import { formatUnknownError } from 'utils/formatUnknownError';
|
||||||
import useToast from 'hooks/useToast';
|
import useToast from 'hooks/useToast';
|
||||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
|
||||||
import { StrategyDraggableItem } from '../FeatureOverviewEnvironments/FeatureOverviewEnvironment/EnvironmentAccordionBody/StrategyDraggableItem/StrategyDraggableItem';
|
import { StrategyDraggableItem } from '../FeatureOverviewEnvironments/FeatureOverviewEnvironment/EnvironmentAccordionBody/StrategyDraggableItem/StrategyDraggableItem';
|
||||||
import type { IFeatureEnvironment } from 'interfaces/featureToggle';
|
import type { IFeatureEnvironment } from 'interfaces/featureToggle';
|
||||||
import { FeatureStrategyEmpty } from 'component/feature/FeatureStrategy/FeatureStrategyEmpty/FeatureStrategyEmpty';
|
import { FeatureStrategyEmpty } from 'component/feature/FeatureStrategy/FeatureStrategyEmpty/FeatureStrategyEmpty';
|
||||||
@ -230,132 +229,91 @@ export const FeatureOverviewEnvironmentBody = ({
|
|||||||
return (
|
return (
|
||||||
<StyledAccordionBody>
|
<StyledAccordionBody>
|
||||||
<StyledAccordionBodyInnerContainer>
|
<StyledAccordionBodyInnerContainer>
|
||||||
<ConditionallyRender
|
{(releasePlans.length > 0 || strategiesToDisplay.length > 0) &&
|
||||||
condition={
|
isDisabled ? (
|
||||||
(releasePlans.length > 0 ||
|
<Alert severity='warning' sx={{ mb: 2 }}>
|
||||||
strategiesToDisplay.length > 0) &&
|
This environment is disabled, which means that none of
|
||||||
isDisabled
|
your strategies are executing.
|
||||||
}
|
</Alert>
|
||||||
show={() => (
|
) : null}
|
||||||
<Alert severity='warning' sx={{ mb: 2 }}>
|
{releasePlans.length > 0 || strategiesToDisplay.length > 0 ? (
|
||||||
This environment is disabled, which means that none
|
<>
|
||||||
of your strategies are executing.
|
{releasePlans.map((plan) => (
|
||||||
</Alert>
|
<ReleasePlan
|
||||||
)}
|
key={plan.id}
|
||||||
/>
|
plan={plan}
|
||||||
<ConditionallyRender
|
environmentIsDisabled={isDisabled}
|
||||||
condition={
|
/>
|
||||||
releasePlans.length > 0 ||
|
))}
|
||||||
strategiesToDisplay.length > 0
|
{releasePlans.length > 0 && strategies.length > 0 ? (
|
||||||
}
|
<SectionSeparator>
|
||||||
show={
|
<StyledBadge>OR</StyledBadge>
|
||||||
<>
|
</SectionSeparator>
|
||||||
{releasePlans.map((plan) => (
|
) : null}
|
||||||
<ReleasePlan
|
{strategiesToDisplay.length < 50 ||
|
||||||
key={plan.id}
|
!manyStrategiesPagination ? (
|
||||||
plan={plan}
|
<>
|
||||||
environmentIsDisabled={isDisabled}
|
{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}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<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)
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
))}
|
</>
|
||||||
<ConditionallyRender
|
)}
|
||||||
condition={
|
</>
|
||||||
releasePlans.length > 0 &&
|
) : (
|
||||||
strategies.length > 0
|
<FeatureStrategyEmpty
|
||||||
}
|
projectId={projectId}
|
||||||
show={
|
featureId={featureId}
|
||||||
<SectionSeparator>
|
environmentId={featureEnvironment.name}
|
||||||
<StyledBadge>OR</StyledBadge>
|
/>
|
||||||
</SectionSeparator>
|
)}
|
||||||
}
|
|
||||||
/>
|
|
||||||
<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>
|
</StyledAccordionBodyInnerContainer>
|
||||||
</StyledAccordionBody>
|
</StyledAccordionBody>
|
||||||
);
|
);
|
||||||
|
@ -2,7 +2,6 @@ import { Box, styled } from '@mui/material';
|
|||||||
import { useFeature } from 'hooks/api/getters/useFeature/useFeature';
|
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 { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
|
||||||
import { FeatureOverviewEnvironmentBody } from './FeatureOverviewEnvironmentBody';
|
import { FeatureOverviewEnvironmentBody } from './FeatureOverviewEnvironmentBody';
|
||||||
import FeatureOverviewEnvironmentMetrics from '../FeatureOverviewEnvironments/FeatureOverviewEnvironment/FeatureOverviewEnvironmentMetrics/FeatureOverviewEnvironmentMetrics';
|
import FeatureOverviewEnvironmentMetrics from '../FeatureOverviewEnvironments/FeatureOverviewEnvironment/FeatureOverviewEnvironmentMetrics/FeatureOverviewEnvironmentMetrics';
|
||||||
import { FeatureStrategyMenu } from 'component/feature/FeatureStrategy/FeatureStrategyMenu/FeatureStrategyMenu';
|
import { FeatureStrategyMenu } from 'component/feature/FeatureStrategy/FeatureStrategyMenu/FeatureStrategyMenu';
|
||||||
@ -13,6 +12,7 @@ const StyledFeatureOverviewEnvironment = styled('div')(({ theme }) => ({
|
|||||||
padding: theme.spacing(1, 3),
|
padding: theme.spacing(1, 3),
|
||||||
borderRadius: theme.shape.borderRadiusLarge,
|
borderRadius: theme.shape.borderRadiusLarge,
|
||||||
backgroundColor: theme.palette.background.paper,
|
backgroundColor: theme.palette.background.paper,
|
||||||
|
border: `1px solid ${theme.palette.divider}`,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const StyledFeatureOverviewEnvironmentBody = styled(
|
const StyledFeatureOverviewEnvironmentBody = styled(
|
||||||
@ -52,62 +52,75 @@ const StyledHeaderTitle = styled('span')(({ theme }) => ({
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
interface INewFeatureOverviewEnvironmentProps {
|
interface INewFeatureOverviewEnvironmentProps {
|
||||||
environmentId: string;
|
hiddenEnvironments: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export const NewFeatureOverviewEnvironment = ({
|
export const FeatureOverviewEnvironment = ({
|
||||||
environmentId,
|
hiddenEnvironments,
|
||||||
}: INewFeatureOverviewEnvironmentProps) => {
|
}: INewFeatureOverviewEnvironmentProps) => {
|
||||||
const projectId = useRequiredPathParam('projectId');
|
const projectId = useRequiredPathParam('projectId');
|
||||||
const featureId = useRequiredPathParam('featureId');
|
const featureId = useRequiredPathParam('featureId');
|
||||||
const { metrics } = useFeatureMetrics(projectId, featureId);
|
const { metrics } = useFeatureMetrics(projectId, featureId);
|
||||||
const { feature } = useFeature(projectId, featureId);
|
const { feature } = useFeature(projectId, featureId);
|
||||||
|
|
||||||
const featureMetrics = getFeatureMetrics(feature?.environments, metrics);
|
const environments =
|
||||||
const environmentMetric = featureMetrics.find(
|
feature?.environments.filter(
|
||||||
({ environment }) => environment === environmentId,
|
({ name }) => !hiddenEnvironments.includes(name),
|
||||||
);
|
) || [];
|
||||||
const featureEnvironment = feature?.environments.find(
|
|
||||||
({ name }) => name === environmentId,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!featureEnvironment)
|
if (!environments || environments.length === 0) {
|
||||||
return (
|
return (
|
||||||
<StyledFeatureOverviewEnvironment className='skeleton'>
|
<StyledFeatureOverviewEnvironment className='skeleton'>
|
||||||
<Box sx={{ height: '400px' }} />
|
<Box sx={{ height: '400px' }} />
|
||||||
</StyledFeatureOverviewEnvironment>
|
</StyledFeatureOverviewEnvironment>
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return environments.map(({ name: environmentId }) => {
|
||||||
<StyledFeatureOverviewEnvironment>
|
const featureMetrics = getFeatureMetrics(
|
||||||
<StyledHeader data-loading>
|
feature?.environments,
|
||||||
<StyledHeaderToggleContainer>
|
metrics,
|
||||||
<FeatureOverviewEnvironmentToggle
|
);
|
||||||
environment={featureEnvironment}
|
const environmentMetric = featureMetrics.find(
|
||||||
|
({ environment }) => environment === environmentId,
|
||||||
|
);
|
||||||
|
const featureEnvironment = feature?.environments.find(
|
||||||
|
({ name }) => name === environmentId,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!featureEnvironment) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<StyledFeatureOverviewEnvironment key={environmentId}>
|
||||||
|
<StyledHeader data-loading>
|
||||||
|
<StyledHeaderToggleContainer>
|
||||||
|
<FeatureOverviewEnvironmentToggle
|
||||||
|
environment={featureEnvironment}
|
||||||
|
/>
|
||||||
|
<StyledHeaderTitleContainer>
|
||||||
|
<StyledHeaderTitleLabel>
|
||||||
|
Environment
|
||||||
|
</StyledHeaderTitleLabel>
|
||||||
|
<StyledHeaderTitle>
|
||||||
|
{environmentId}
|
||||||
|
</StyledHeaderTitle>
|
||||||
|
</StyledHeaderTitleContainer>
|
||||||
|
</StyledHeaderToggleContainer>
|
||||||
|
<FeatureOverviewEnvironmentMetrics
|
||||||
|
environmentMetric={environmentMetric}
|
||||||
|
disabled={!featureEnvironment.enabled}
|
||||||
/>
|
/>
|
||||||
<StyledHeaderTitleContainer>
|
</StyledHeader>
|
||||||
<StyledHeaderTitleLabel>
|
<StyledFeatureOverviewEnvironmentBody
|
||||||
Environment
|
featureEnvironment={featureEnvironment}
|
||||||
</StyledHeaderTitleLabel>
|
isDisabled={!featureEnvironment.enabled}
|
||||||
<StyledHeaderTitle>{environmentId}</StyledHeaderTitle>
|
otherEnvironments={feature?.environments
|
||||||
</StyledHeaderTitleContainer>
|
.map(({ name }) => name)
|
||||||
</StyledHeaderToggleContainer>
|
.filter((name) => name !== environmentId)}
|
||||||
<FeatureOverviewEnvironmentMetrics
|
|
||||||
environmentMetric={environmentMetric}
|
|
||||||
disabled={!featureEnvironment.enabled}
|
|
||||||
/>
|
/>
|
||||||
</StyledHeader>
|
{featureEnvironment?.strategies?.length > 0 ? (
|
||||||
|
|
||||||
<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
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
@ -124,8 +137,8 @@ export const NewFeatureOverviewEnvironment = ({
|
|||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
</>
|
</>
|
||||||
}
|
) : null}
|
||||||
/>
|
</StyledFeatureOverviewEnvironment>
|
||||||
</StyledFeatureOverviewEnvironment>
|
);
|
||||||
);
|
});
|
||||||
};
|
};
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { Link, Route, Routes } from 'react-router-dom';
|
import { Link, Route, Routes } from 'react-router-dom';
|
||||||
import { useFeature } from 'hooks/api/getters/useFeature/useFeature';
|
import { useFeature } from 'hooks/api/getters/useFeature/useFeature';
|
||||||
import FeatureLog from './FeatureLog/FeatureLog';
|
import FeatureLog from './FeatureLog/FeatureLog';
|
||||||
import FeatureOverview from './FeatureOverview/FeatureOverview';
|
import { FeatureOverview } from './FeatureOverview/FeatureOverview';
|
||||||
import { FeatureEnvironmentVariants } from './FeatureVariants/FeatureEnvironmentVariants/FeatureEnvironmentVariants';
|
import { FeatureEnvironmentVariants } from './FeatureVariants/FeatureEnvironmentVariants/FeatureEnvironmentVariants';
|
||||||
import { FeatureMetrics } from './FeatureMetrics/FeatureMetrics';
|
import { FeatureMetrics } from './FeatureMetrics/FeatureMetrics';
|
||||||
import { FeatureSettings } from './FeatureSettings/FeatureSettings';
|
import { FeatureSettings } from './FeatureSettings/FeatureSettings';
|
||||||
|
@ -5,6 +5,9 @@ interface IGlobalStore {
|
|||||||
hiddenEnvironments?: Array<string>;
|
hiddenEnvironments?: Array<string>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated use tested `useLocalStorageState` hook instead
|
||||||
|
*/
|
||||||
export const useGlobalLocalStorage = () => {
|
export const useGlobalLocalStorage = () => {
|
||||||
const { value, setValue } = createLocalStorage<IGlobalStore>(
|
const { value, setValue } = createLocalStorage<IGlobalStore>(
|
||||||
'global:v1',
|
'global:v1',
|
||||||
|
@ -2,6 +2,9 @@ import { useGlobalLocalStorage } from './useGlobalLocalStorage';
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { usePlausibleTracker } from 'hooks/usePlausibleTracker';
|
import { usePlausibleTracker } from 'hooks/usePlausibleTracker';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated remove with `flagOverviewRedesign`
|
||||||
|
*/
|
||||||
export const useHiddenEnvironments = () => {
|
export const useHiddenEnvironments = () => {
|
||||||
const { trackEvent } = usePlausibleTracker();
|
const { trackEvent } = usePlausibleTracker();
|
||||||
|
|
||||||
@ -25,11 +28,6 @@ export const useHiddenEnvironments = () => {
|
|||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
hiddenEnvironments.add(environment);
|
hiddenEnvironments.add(environment);
|
||||||
trackEvent('hidden_environment', {
|
|
||||||
props: {
|
|
||||||
eventType: `environment hidden`,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
setStoredHiddenEnvironments(hiddenEnvironments);
|
setStoredHiddenEnvironments(hiddenEnvironments);
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user