Skip to content

Commit

Permalink
feat: show and hide environments (#9323)
Browse files Browse the repository at this point in the history
- 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 <[email protected]>
  • Loading branch information
Tymek and thomasheartman authored Feb 19, 2025
1 parent c16f720 commit 2a6487e
Show file tree
Hide file tree
Showing 12 changed files with 394 additions and 223 deletions.
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import NewFeatureOverviewMetaData from './FeatureOverviewMetaData/FeatureOverviewMetaData';
import FeatureOverviewEnvironments from './FeatureOverviewEnvironments/FeatureOverviewEnvironments';
import FeatureOverviewMetaData from './FeatureOverviewMetaData/FeatureOverviewMetaData';
import { Route, Routes, useNavigate } from 'react-router-dom';
import { SidebarModal } from 'component/common/SidebarModal/SidebarModal';
import {
Expand All @@ -8,79 +7,64 @@ import {
} from 'component/feature/FeatureStrategy/FeatureStrategyEdit/FeatureStrategyEdit';
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
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 { FeatureStrategyCreate } from 'component/feature/FeatureStrategy/FeatureStrategyCreate/FeatureStrategyCreate';
import { useEffect, useState } from 'react';
import { useEffect } 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 { NewFeatureOverviewEnvironment } from './NewFeatureOverviewEnvironment/NewFeatureOverviewEnvironment';
import { FeatureOverviewEnvironment } from './NewFeatureOverviewEnvironment/NewFeatureOverviewEnvironment';
import { default as LegacyFleatureOverview } from './LegacyFeatureOverview';
import { useEnvironmentVisibility } from './FeatureOverviewMetaData/EnvironmentVisibilityMenu/hooks/useEnvironmentVisibility';

const StyledContainer = styled('div')(({ theme }) => ({
display: 'flex',
width: '100%',
[theme.breakpoints.down(1000)]: {
gap: theme.spacing(2),
[theme.breakpoints.down('md')]: {
flexDirection: 'column',
},
}));

const StyledMainContent = styled('div')(({ theme }) => ({
display: 'flex',
flexDirection: 'column',
width: `calc(100% - (350px + 1rem))`,
[theme.breakpoints.down(1000)]: {
width: '100%',
},
flexGrow: 1,
gap: theme.spacing(2),
}));

const FeatureOverview = () => {
export const FeatureOverview = () => {
const navigate = useNavigate();
const projectId = useRequiredPathParam('projectId');
const featureId = useRequiredPathParam('featureId');
const featurePath = formatFeaturePath(projectId, featureId);
const { hiddenEnvironments, setHiddenEnvironments } =
useHiddenEnvironments();
const { hiddenEnvironments, onEnvironmentVisibilityChange } =
useEnvironmentVisibility();
const onSidebarClose = () => navigate(featurePath);
usePageTitle(featureId);
const { setLastViewed } = useLastViewedFlags();
useEffect(() => {
setLastViewed({ featureId, projectId });
}, [featureId]);
const [environmentId, setEnvironmentId] = useState('');
const flagOverviewRedesign = useUiFlag('flagOverviewRedesign');

if (!flagOverviewRedesign) {
return <LegacyFleatureOverview />;
}

return (
<StyledContainer>
<div>
{flagOverviewRedesign ? (
<>
<NewFeatureOverviewMetaData />
<NewFeatureOverviewSidePanel
environmentId={environmentId}
setEnvironmentId={setEnvironmentId}
/>
</>
) : (
<>
<OldFeatureOverviewMetaData />
<OldFeatureOverviewSidePanel
hiddenEnvironments={hiddenEnvironments}
setHiddenEnvironments={setHiddenEnvironments}
/>
</>
)}
<FeatureOverviewMetaData
hiddenEnvironments={hiddenEnvironments}
onEnvironmentVisibilityChange={
onEnvironmentVisibilityChange
}
/>
</div>
<StyledMainContent>
{flagOverviewRedesign ? (
<NewFeatureOverviewEnvironment
environmentId={environmentId}
/>
) : (
<FeatureOverviewEnvironments />
)}
<FeatureOverviewEnvironment
hiddenEnvironments={hiddenEnvironments}
/>
</StyledMainContent>
<Routes>
<Route
Expand Down Expand Up @@ -111,5 +95,3 @@ const FeatureOverview = () => {
</StyledContainer>
);
};

export default FeatureOverview;
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ export const AddTagButton: FC<AddTagButtonProps> = ({ project, onClick }) => (
variant='text'
onClick={onClick}
startIcon={<StyledAddIcon />}
data-loading
>
Add tag
</StyledAddTagButton>
Expand Down
Original file line number Diff line number Diff line change
@@ -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>
);
};
Original file line number Diff line number Diff line change
@@ -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,
};
};
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { type FC, useState } from 'react';
import { styled } from '@mui/material';
import { useNavigate } from 'react-router-dom';
import { useFeature } from 'hooks/api/getters/useFeature/useFeature';
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
import { FeatureArchiveDialog } from 'component/common/FeatureArchiveDialog/FeatureArchiveDialog';
import { useState } from 'react';
import { FeatureArchiveNotAllowedDialog } from 'component/common/FeatureArchiveDialog/FeatureArchiveNotAllowedDialog';
import { formatDateYMD } from 'utils/formatDate';
import { parseISO } from 'date-fns';
Expand All @@ -15,6 +15,7 @@ import { MarkCompletedDialogue } from '../FeatureLifecycle/MarkCompletedDialogue
import { TagRow } from './TagRow';
import { capitalizeFirst } from 'utils/capitalizeFirst';
import { Collaborators } from './Collaborators';
import { EnvironmentVisibilityMenu } from './EnvironmentVisibilityMenu/EnvironmentVisibilityMenu';

const StyledMetaDataContainer = styled('div')(({ theme }) => ({
padding: theme.spacing(3),
Expand All @@ -24,7 +25,8 @@ const StyledMetaDataContainer = styled('div')(({ theme }) => ({
flexDirection: 'column',
gap: theme.spacing(2),
width: '350px',
[theme.breakpoints.down(1000)]: {
border: `1px solid ${theme.palette.divider}`,
[theme.breakpoints.down('md')]: {
width: '100%',
},
}));
Expand Down Expand Up @@ -63,7 +65,15 @@ export const StyledMetaDataItemValue = styled('div')(({ theme }) => ({
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 featureId = useRequiredPathParam('featureId');
const { feature, refetchFeature } = useFeature(projectId, featureId);
Expand Down Expand Up @@ -156,6 +166,13 @@ const FeatureOverviewMetaData = () => {
<DependencyRow feature={feature} />
) : null}
<TagRow feature={feature} />
{onEnvironmentVisibilityChange ? (
<EnvironmentVisibilityMenu
environments={feature.environments || []}
hiddenEnvironments={hiddenEnvironments || []}
onChange={onEnvironmentVisibilityChange}
/>
) : null}
</StyledBody>
</StyledMetaDataContainer>
{feature.children.length > 0 ? (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ const StyledContainer = styled(Box)(({ theme }) => ({
flexDirection: 'column',
gap: theme.spacing(2),
width: '350px',
[theme.breakpoints.down(1000)]: {
[theme.breakpoints.down('md')]: {
width: '100%',
},
}));
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Loading

0 comments on commit 2a6487e

Please sign in to comment.