mirror of
https://github.com/Unleash/unleash.git
synced 2025-05-31 01:16:01 +02:00
refactor: move feature overview header into separate file (#9319)
This PR moves the flag page header into a separate file, so that the overview file is more clearly focused on the overview. Additionally, it moves the modals that are triggered from the header into the new file. This should give a nice little performance boost, as opening and closing these modals should no longer trigger a re-rendering of the full flag overview page, only the header.
This commit is contained in:
parent
2ede2a6578
commit
4701dc1552
@ -1,170 +1,15 @@
|
||||
import { type PropsWithChildren, useState, type FC } from 'react';
|
||||
import {
|
||||
IconButton,
|
||||
styled,
|
||||
Tab,
|
||||
Tabs,
|
||||
Tooltip,
|
||||
Typography,
|
||||
useMediaQuery,
|
||||
} from '@mui/material';
|
||||
import Archive from '@mui/icons-material/Archive';
|
||||
import ArchiveOutlined from '@mui/icons-material/ArchiveOutlined';
|
||||
import FileCopy from '@mui/icons-material/FileCopy';
|
||||
import FileCopyOutlined from '@mui/icons-material/FileCopyOutlined';
|
||||
import Label from '@mui/icons-material/Label';
|
||||
import WatchLater from '@mui/icons-material/WatchLater';
|
||||
import WatchLaterOutlined from '@mui/icons-material/WatchLaterOutlined';
|
||||
import LibraryAdd from '@mui/icons-material/LibraryAdd';
|
||||
import LibraryAddOutlined from '@mui/icons-material/LibraryAddOutlined';
|
||||
import Check from '@mui/icons-material/Check';
|
||||
import Star from '@mui/icons-material/Star';
|
||||
import {
|
||||
Link,
|
||||
Route,
|
||||
Routes,
|
||||
useLocation,
|
||||
useNavigate,
|
||||
} from 'react-router-dom';
|
||||
import { Link, Route, Routes } from 'react-router-dom';
|
||||
import { useFeature } from 'hooks/api/getters/useFeature/useFeature';
|
||||
import {
|
||||
CREATE_FEATURE,
|
||||
DELETE_FEATURE,
|
||||
UPDATE_FEATURE,
|
||||
} from 'component/providers/AccessProvider/permissions';
|
||||
import PermissionIconButton from 'component/common/PermissionIconButton/PermissionIconButton';
|
||||
import FeatureLog from './FeatureLog/FeatureLog';
|
||||
import FeatureOverview from './FeatureOverview/FeatureOverview';
|
||||
import { FeatureEnvironmentVariants } from './FeatureVariants/FeatureEnvironmentVariants/FeatureEnvironmentVariants';
|
||||
import { FeatureMetrics } from './FeatureMetrics/FeatureMetrics';
|
||||
import { FeatureSettings } from './FeatureSettings/FeatureSettings';
|
||||
import useLoading from 'hooks/useLoading';
|
||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||
import { FeatureStaleDialog } from 'component/common/FeatureStaleDialog/FeatureStaleDialog';
|
||||
import { ManageTagsDialog } from './FeatureOverview/ManageTagsDialog/ManageTagsDialog';
|
||||
import { FeatureStatusChip } from 'component/common/FeatureStatusChip/FeatureStatusChip';
|
||||
import { FeatureNotFound } from 'component/feature/FeatureView/FeatureNotFound/FeatureNotFound';
|
||||
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
|
||||
import { FeatureArchiveDialog } from 'component/common/FeatureArchiveDialog/FeatureArchiveDialog';
|
||||
import { FeatureArchiveNotAllowedDialog } from 'component/common/FeatureArchiveDialog/FeatureArchiveNotAllowedDialog';
|
||||
import { useFavoriteFeaturesApi } from 'hooks/api/actions/useFavoriteFeaturesApi/useFavoriteFeaturesApi';
|
||||
import { FavoriteIconButton } from 'component/common/FavoriteIconButton/FavoriteIconButton';
|
||||
import { ChildrenTooltip } from './FeatureOverview/FeatureOverviewMetaData/ChildrenTooltip';
|
||||
import copy from 'copy-to-clipboard';
|
||||
import useToast from 'hooks/useToast';
|
||||
import { useUiFlag } from 'hooks/useUiFlag';
|
||||
import type { IFeatureToggle } from 'interfaces/featureToggle';
|
||||
import { Collaborators } from './Collaborators';
|
||||
import StarBorder from '@mui/icons-material/StarBorder';
|
||||
import { TooltipResolver } from 'component/common/TooltipResolver/TooltipResolver';
|
||||
|
||||
const NewStyledHeader = styled('div')(({ theme }) => ({
|
||||
backgroundColor: 'none',
|
||||
marginBottom: theme.spacing(2),
|
||||
borderBottom: `1px solid ${theme.palette.divider}`,
|
||||
}));
|
||||
|
||||
const LowerHeaderRow = styled('div')(({ theme }) => ({
|
||||
display: 'flex',
|
||||
flexFlow: 'row nowrap',
|
||||
justifyContent: 'space-between',
|
||||
gap: theme.spacing(4),
|
||||
}));
|
||||
|
||||
const HeaderActions = styled('div')(({ theme }) => ({
|
||||
display: 'flex',
|
||||
flexFlow: 'row nowrap',
|
||||
alignItems: 'center',
|
||||
}));
|
||||
|
||||
const IconButtonWithTooltip: FC<
|
||||
PropsWithChildren<{
|
||||
onClick: () => void;
|
||||
label: string;
|
||||
}>
|
||||
> = ({ children, label, onClick }) => {
|
||||
return (
|
||||
<TooltipResolver
|
||||
title={label}
|
||||
arrow
|
||||
onClick={(e) => e.preventDefault()}
|
||||
>
|
||||
<IconButton aria-label={label} onClick={onClick}>
|
||||
{children}
|
||||
</IconButton>
|
||||
</TooltipResolver>
|
||||
);
|
||||
};
|
||||
|
||||
const StyledHeader = styled('div')(({ theme }) => ({
|
||||
backgroundColor: theme.palette.background.paper,
|
||||
borderRadius: theme.shape.borderRadiusLarge,
|
||||
marginBottom: theme.spacing(2),
|
||||
}));
|
||||
|
||||
const StyledInnerContainer = styled('div')(({ theme }) => ({
|
||||
padding: theme.spacing(2, 4, 2, 2),
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
[theme.breakpoints.down(500)]: {
|
||||
flexDirection: 'column',
|
||||
},
|
||||
}));
|
||||
|
||||
const StyledFlagInfoContainer = styled('div')({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
});
|
||||
|
||||
const StyledDependency = styled('div')(({ theme }) => ({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: theme.spacing(1),
|
||||
marginTop: theme.spacing(1),
|
||||
fontSize: theme.fontSizes.smallBody,
|
||||
padding: theme.spacing(0.75, 1.5),
|
||||
backgroundColor: theme.palette.background.elevation2,
|
||||
borderRadius: `${theme.shape.borderRadiusMedium}px`,
|
||||
width: 'max-content',
|
||||
}));
|
||||
|
||||
const StyledFeatureViewHeader = styled('h1')(({ theme }) => ({
|
||||
fontSize: theme.fontSizes.mainHeader,
|
||||
fontWeight: 'normal',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
wordBreak: 'break-all',
|
||||
}));
|
||||
|
||||
const StyledToolbarContainer = styled('div')({
|
||||
flexShrink: 0,
|
||||
display: 'flex',
|
||||
});
|
||||
|
||||
const StyledSeparator = styled('div')(({ theme }) => ({
|
||||
width: '100%',
|
||||
backgroundColor: theme.palette.divider,
|
||||
height: '1px',
|
||||
}));
|
||||
|
||||
const StyledTabRow = styled('div')(({ theme }) => ({
|
||||
display: 'flex',
|
||||
flexFlow: 'row nowrap',
|
||||
gap: theme.spacing(4),
|
||||
paddingInline: theme.spacing(4),
|
||||
justifyContent: 'space-between',
|
||||
}));
|
||||
|
||||
const StyledTabButton = styled(Tab)(({ theme }) => ({
|
||||
textTransform: 'none',
|
||||
width: 'auto',
|
||||
fontSize: theme.fontSizes.bodySize,
|
||||
padding: '0 !important',
|
||||
[theme.breakpoints.up('md')]: {
|
||||
minWidth: 160,
|
||||
},
|
||||
}));
|
||||
import { FeatureViewHeader } from './FeatureViewHeader';
|
||||
import { styled } from '@mui/material';
|
||||
|
||||
export const StyledLink = styled(Link)(() => ({
|
||||
maxWidth: '100%',
|
||||
@ -174,85 +19,17 @@ export const StyledLink = styled(Link)(() => ({
|
||||
},
|
||||
}));
|
||||
|
||||
const useLegacyVariants = (environments: IFeatureToggle['environments']) => {
|
||||
const enableLegacyVariants = useUiFlag('enableLegacyVariants');
|
||||
const existingLegacyVariantsExist = environments.some(
|
||||
(environment) => environment.variants?.length,
|
||||
);
|
||||
return enableLegacyVariants || existingLegacyVariantsExist;
|
||||
};
|
||||
|
||||
export const FeatureView = () => {
|
||||
const projectId = useRequiredPathParam('projectId');
|
||||
const featureId = useRequiredPathParam('featureId');
|
||||
const flagOverviewRedesign = useUiFlag('flagOverviewRedesign');
|
||||
const { favorite, unfavorite } = useFavoriteFeaturesApi();
|
||||
const { refetchFeature } = useFeature(projectId, featureId);
|
||||
const { setToastData, setToastApiError } = useToast();
|
||||
|
||||
const [openTagDialog, setOpenTagDialog] = useState(false);
|
||||
const [showDelDialog, setShowDelDialog] = useState(false);
|
||||
const [openStaleDialog, setOpenStaleDialog] = useState(false);
|
||||
const [isFeatureNameCopied, setIsFeatureNameCopied] = useState(false);
|
||||
const smallScreen = useMediaQuery(`(max-width:${500}px)`);
|
||||
|
||||
const { feature, loading, error, status } = useFeature(
|
||||
projectId,
|
||||
featureId,
|
||||
);
|
||||
|
||||
const navigate = useNavigate();
|
||||
const { pathname } = useLocation();
|
||||
const ref = useLoading(loading);
|
||||
|
||||
const basePath = `/projects/${projectId}/features/${featureId}`;
|
||||
|
||||
const showLegacyVariants = useLegacyVariants(feature.environments);
|
||||
|
||||
const tabData = [
|
||||
{
|
||||
title: 'Overview',
|
||||
path: `${basePath}`,
|
||||
name: 'overview',
|
||||
},
|
||||
{
|
||||
title: 'Metrics',
|
||||
path: `${basePath}/metrics`,
|
||||
name: 'Metrics',
|
||||
},
|
||||
...(showLegacyVariants
|
||||
? [
|
||||
{
|
||||
title: 'Variants',
|
||||
path: `${basePath}/variants`,
|
||||
name: 'Variants',
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{ title: 'Settings', path: `${basePath}/settings`, name: 'Settings' },
|
||||
{
|
||||
title: 'Event log',
|
||||
path: `${basePath}/logs`,
|
||||
name: 'Event log',
|
||||
},
|
||||
];
|
||||
|
||||
const activeTab =
|
||||
tabData.find((tab) => tab.path === pathname) ?? tabData[0];
|
||||
|
||||
const onFavorite = async () => {
|
||||
try {
|
||||
if (feature?.favorite) {
|
||||
await unfavorite(projectId, feature.name);
|
||||
} else {
|
||||
await favorite(projectId, feature.name);
|
||||
}
|
||||
refetchFeature();
|
||||
} catch (error) {
|
||||
setToastApiError('Something went wrong, could not update favorite');
|
||||
}
|
||||
};
|
||||
|
||||
if (status === 404) {
|
||||
return <FeatureNotFound />;
|
||||
}
|
||||
@ -261,245 +38,9 @@ export const FeatureView = () => {
|
||||
return <div ref={ref} />;
|
||||
}
|
||||
|
||||
const handleCopyToClipboard = () => {
|
||||
try {
|
||||
copy(feature.name);
|
||||
setIsFeatureNameCopied(true);
|
||||
setTimeout(() => {
|
||||
setIsFeatureNameCopied(false);
|
||||
}, 3000);
|
||||
} catch (error: unknown) {
|
||||
setToastData({
|
||||
type: 'error',
|
||||
text: 'Could not copy feature name',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div ref={ref}>
|
||||
{flagOverviewRedesign ? (
|
||||
<NewStyledHeader>
|
||||
<Typography variant='h1'>{feature.name}</Typography>
|
||||
<LowerHeaderRow>
|
||||
<Tabs
|
||||
value={activeTab.path}
|
||||
indicatorColor='primary'
|
||||
textColor='primary'
|
||||
>
|
||||
{tabData.map((tab) => (
|
||||
<StyledTabButton
|
||||
key={tab.title}
|
||||
label={tab.title}
|
||||
value={tab.path}
|
||||
onClick={() => navigate(tab.path)}
|
||||
data-testid={`TAB-${tab.title}`}
|
||||
/>
|
||||
))}
|
||||
</Tabs>
|
||||
<HeaderActions>
|
||||
<IconButtonWithTooltip
|
||||
label='Favorite this feature flag'
|
||||
onClick={onFavorite}
|
||||
data-loading
|
||||
>
|
||||
{feature?.favorite ? <Star /> : <StarBorder />}
|
||||
</IconButtonWithTooltip>
|
||||
|
||||
<IconButtonWithTooltip
|
||||
label='Copy flag name'
|
||||
onClick={handleCopyToClipboard}
|
||||
data-loading
|
||||
>
|
||||
{isFeatureNameCopied ? (
|
||||
<Check />
|
||||
) : (
|
||||
<FileCopyOutlined />
|
||||
)}
|
||||
</IconButtonWithTooltip>
|
||||
<PermissionIconButton
|
||||
permission={CREATE_FEATURE}
|
||||
projectId={projectId}
|
||||
data-loading
|
||||
component={Link}
|
||||
to={`/projects/${projectId}/features/${featureId}/copy`}
|
||||
tooltipProps={{
|
||||
title: 'Clone',
|
||||
}}
|
||||
>
|
||||
<LibraryAddOutlined />
|
||||
</PermissionIconButton>
|
||||
|
||||
<PermissionIconButton
|
||||
permission={DELETE_FEATURE}
|
||||
projectId={projectId}
|
||||
tooltipProps={{
|
||||
title: 'Archive feature flag',
|
||||
}}
|
||||
data-loading
|
||||
onClick={() => setShowDelDialog(true)}
|
||||
>
|
||||
<ArchiveOutlined />
|
||||
</PermissionIconButton>
|
||||
<PermissionIconButton
|
||||
onClick={() => setOpenStaleDialog(true)}
|
||||
permission={UPDATE_FEATURE}
|
||||
projectId={projectId}
|
||||
tooltipProps={{
|
||||
title: 'Toggle stale state',
|
||||
}}
|
||||
data-loading
|
||||
>
|
||||
<WatchLaterOutlined />
|
||||
</PermissionIconButton>
|
||||
</HeaderActions>
|
||||
</LowerHeaderRow>
|
||||
</NewStyledHeader>
|
||||
) : (
|
||||
<StyledHeader>
|
||||
<StyledInnerContainer>
|
||||
<StyledFlagInfoContainer>
|
||||
<FavoriteIconButton
|
||||
onClick={onFavorite}
|
||||
isFavorite={feature?.favorite}
|
||||
/>
|
||||
<div>
|
||||
<StyledFlagInfoContainer>
|
||||
<StyledFeatureViewHeader data-loading>
|
||||
{feature.name}{' '}
|
||||
</StyledFeatureViewHeader>
|
||||
<Tooltip
|
||||
title={
|
||||
isFeatureNameCopied
|
||||
? 'Copied!'
|
||||
: 'Copy name'
|
||||
}
|
||||
arrow
|
||||
>
|
||||
<IconButton
|
||||
onClick={handleCopyToClipboard}
|
||||
style={{ marginLeft: 8 }}
|
||||
>
|
||||
{isFeatureNameCopied ? (
|
||||
<Check
|
||||
style={{ fontSize: 16 }}
|
||||
/>
|
||||
) : (
|
||||
<FileCopy
|
||||
style={{ fontSize: 16 }}
|
||||
/>
|
||||
)}
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<ConditionallyRender
|
||||
condition={!smallScreen}
|
||||
show={
|
||||
<FeatureStatusChip
|
||||
stale={feature?.stale}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</StyledFlagInfoContainer>
|
||||
<ConditionallyRender
|
||||
condition={feature.dependencies.length > 0}
|
||||
show={
|
||||
<StyledDependency>
|
||||
<b>Has parent: </b>
|
||||
<StyledLink
|
||||
to={`/projects/${feature.project}/features/${feature?.dependencies[0]?.feature}`}
|
||||
>
|
||||
{
|
||||
feature?.dependencies[0]
|
||||
?.feature
|
||||
}
|
||||
</StyledLink>
|
||||
</StyledDependency>
|
||||
}
|
||||
/>
|
||||
<ConditionallyRender
|
||||
condition={feature.children.length > 0}
|
||||
show={
|
||||
<StyledDependency>
|
||||
<b>Has children:</b>
|
||||
<ChildrenTooltip
|
||||
childFeatures={feature.children}
|
||||
project={feature.project}
|
||||
/>
|
||||
</StyledDependency>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</StyledFlagInfoContainer>
|
||||
|
||||
<StyledToolbarContainer>
|
||||
<PermissionIconButton
|
||||
permission={CREATE_FEATURE}
|
||||
projectId={projectId}
|
||||
data-loading
|
||||
component={Link}
|
||||
to={`/projects/${projectId}/features/${featureId}/copy`}
|
||||
tooltipProps={{
|
||||
title: 'Clone',
|
||||
}}
|
||||
>
|
||||
<LibraryAdd />
|
||||
</PermissionIconButton>
|
||||
<PermissionIconButton
|
||||
permission={DELETE_FEATURE}
|
||||
projectId={projectId}
|
||||
tooltipProps={{
|
||||
title: 'Archive feature flag',
|
||||
}}
|
||||
data-loading
|
||||
onClick={() => setShowDelDialog(true)}
|
||||
>
|
||||
<Archive />
|
||||
</PermissionIconButton>
|
||||
<PermissionIconButton
|
||||
onClick={() => setOpenStaleDialog(true)}
|
||||
permission={UPDATE_FEATURE}
|
||||
projectId={projectId}
|
||||
tooltipProps={{
|
||||
title: 'Toggle stale state',
|
||||
}}
|
||||
data-loading
|
||||
>
|
||||
<WatchLater />
|
||||
</PermissionIconButton>
|
||||
<PermissionIconButton
|
||||
onClick={() => setOpenTagDialog(true)}
|
||||
permission={UPDATE_FEATURE}
|
||||
projectId={projectId}
|
||||
tooltipProps={{ title: 'Add tag' }}
|
||||
data-loading
|
||||
>
|
||||
<Label />
|
||||
</PermissionIconButton>
|
||||
</StyledToolbarContainer>
|
||||
</StyledInnerContainer>
|
||||
<StyledSeparator />
|
||||
<StyledTabRow>
|
||||
<Tabs
|
||||
value={activeTab.path}
|
||||
indicatorColor='primary'
|
||||
textColor='primary'
|
||||
>
|
||||
{tabData.map((tab) => (
|
||||
<StyledTabButton
|
||||
key={tab.title}
|
||||
label={tab.title}
|
||||
value={tab.path}
|
||||
onClick={() => navigate(tab.path)}
|
||||
data-testid={`TAB-${tab.title}`}
|
||||
/>
|
||||
))}
|
||||
</Tabs>
|
||||
<Collaborators
|
||||
collaborators={feature.collaborators?.users}
|
||||
/>
|
||||
</StyledTabRow>
|
||||
</StyledHeader>
|
||||
)}
|
||||
<FeatureViewHeader feature={feature} />
|
||||
<Routes>
|
||||
<Route path='metrics' element={<FeatureMetrics />} />
|
||||
<Route path='logs' element={<FeatureLog />} />
|
||||
@ -510,40 +51,6 @@ export const FeatureView = () => {
|
||||
<Route path='settings' element={<FeatureSettings />} />
|
||||
<Route path='*' element={<FeatureOverview />} />
|
||||
</Routes>
|
||||
<ConditionallyRender
|
||||
condition={feature.children.length > 0}
|
||||
show={
|
||||
<FeatureArchiveNotAllowedDialog
|
||||
features={feature.children}
|
||||
project={projectId}
|
||||
isOpen={showDelDialog}
|
||||
onClose={() => setShowDelDialog(false)}
|
||||
/>
|
||||
}
|
||||
elseShow={
|
||||
<FeatureArchiveDialog
|
||||
isOpen={showDelDialog}
|
||||
onConfirm={() => {
|
||||
navigate(`/projects/${projectId}`);
|
||||
}}
|
||||
onClose={() => setShowDelDialog(false)}
|
||||
projectId={projectId}
|
||||
featureIds={[featureId]}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
||||
<FeatureStaleDialog
|
||||
isStale={feature.stale}
|
||||
isOpen={openStaleDialog}
|
||||
onClose={() => {
|
||||
setOpenStaleDialog(false);
|
||||
refetchFeature();
|
||||
}}
|
||||
featureId={featureId}
|
||||
projectId={projectId}
|
||||
/>
|
||||
<ManageTagsDialog open={openTagDialog} setOpen={setOpenTagDialog} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
515
frontend/src/component/feature/FeatureView/FeatureViewHeader.tsx
Normal file
515
frontend/src/component/feature/FeatureView/FeatureViewHeader.tsx
Normal file
@ -0,0 +1,515 @@
|
||||
import { type PropsWithChildren, useState, type FC } from 'react';
|
||||
import {
|
||||
IconButton,
|
||||
styled,
|
||||
Tab,
|
||||
Tabs,
|
||||
Tooltip,
|
||||
Typography,
|
||||
useMediaQuery,
|
||||
} from '@mui/material';
|
||||
import Archive from '@mui/icons-material/Archive';
|
||||
import ArchiveOutlined from '@mui/icons-material/ArchiveOutlined';
|
||||
import FileCopy from '@mui/icons-material/FileCopy';
|
||||
import FileCopyOutlined from '@mui/icons-material/FileCopyOutlined';
|
||||
import Label from '@mui/icons-material/Label';
|
||||
import WatchLater from '@mui/icons-material/WatchLater';
|
||||
import WatchLaterOutlined from '@mui/icons-material/WatchLaterOutlined';
|
||||
import LibraryAdd from '@mui/icons-material/LibraryAdd';
|
||||
import LibraryAddOutlined from '@mui/icons-material/LibraryAddOutlined';
|
||||
import Check from '@mui/icons-material/Check';
|
||||
import Star from '@mui/icons-material/Star';
|
||||
import { Link, useLocation, useNavigate } from 'react-router-dom';
|
||||
import { useFeature } from 'hooks/api/getters/useFeature/useFeature';
|
||||
import {
|
||||
CREATE_FEATURE,
|
||||
DELETE_FEATURE,
|
||||
UPDATE_FEATURE,
|
||||
} from 'component/providers/AccessProvider/permissions';
|
||||
import PermissionIconButton from 'component/common/PermissionIconButton/PermissionIconButton';
|
||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||
import { FeatureStatusChip } from 'component/common/FeatureStatusChip/FeatureStatusChip';
|
||||
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
|
||||
import { useFavoriteFeaturesApi } from 'hooks/api/actions/useFavoriteFeaturesApi/useFavoriteFeaturesApi';
|
||||
import { FavoriteIconButton } from 'component/common/FavoriteIconButton/FavoriteIconButton';
|
||||
import { ChildrenTooltip } from './FeatureOverview/FeatureOverviewMetaData/ChildrenTooltip';
|
||||
import copy from 'copy-to-clipboard';
|
||||
import useToast from 'hooks/useToast';
|
||||
import { useUiFlag } from 'hooks/useUiFlag';
|
||||
import type { IFeatureToggle } from 'interfaces/featureToggle';
|
||||
import { Collaborators } from './Collaborators';
|
||||
import StarBorder from '@mui/icons-material/StarBorder';
|
||||
import { TooltipResolver } from 'component/common/TooltipResolver/TooltipResolver';
|
||||
import { ManageTagsDialog } from './FeatureOverview/ManageTagsDialog/ManageTagsDialog';
|
||||
import { FeatureStaleDialog } from 'component/common/FeatureStaleDialog/FeatureStaleDialog';
|
||||
import { FeatureArchiveDialog } from 'component/common/FeatureArchiveDialog/FeatureArchiveDialog';
|
||||
import { FeatureArchiveNotAllowedDialog } from 'component/common/FeatureArchiveDialog/FeatureArchiveNotAllowedDialog';
|
||||
|
||||
const NewStyledHeader = styled('div')(({ theme }) => ({
|
||||
backgroundColor: 'none',
|
||||
marginBottom: theme.spacing(2),
|
||||
borderBottom: `1px solid ${theme.palette.divider}`,
|
||||
}));
|
||||
|
||||
const LowerHeaderRow = styled('div')(({ theme }) => ({
|
||||
display: 'flex',
|
||||
flexFlow: 'row wrap',
|
||||
justifyContent: 'space-between',
|
||||
columnGap: theme.spacing(4),
|
||||
}));
|
||||
|
||||
const HeaderActions = styled('div')(({ theme }) => ({
|
||||
display: 'flex',
|
||||
flexFlow: 'row nowrap',
|
||||
alignItems: 'center',
|
||||
}));
|
||||
|
||||
const IconButtonWithTooltip: FC<
|
||||
PropsWithChildren<{
|
||||
onClick: () => void;
|
||||
label: string;
|
||||
}>
|
||||
> = ({ children, label, onClick }) => {
|
||||
return (
|
||||
<TooltipResolver
|
||||
title={label}
|
||||
arrow
|
||||
onClick={(e) => e.preventDefault()}
|
||||
>
|
||||
<IconButton aria-label={label} onClick={onClick}>
|
||||
{children}
|
||||
</IconButton>
|
||||
</TooltipResolver>
|
||||
);
|
||||
};
|
||||
|
||||
const StyledHeader = styled('div')(({ theme }) => ({
|
||||
backgroundColor: theme.palette.background.paper,
|
||||
borderRadius: theme.shape.borderRadiusLarge,
|
||||
marginBottom: theme.spacing(2),
|
||||
}));
|
||||
|
||||
const StyledInnerContainer = styled('div')(({ theme }) => ({
|
||||
padding: theme.spacing(2, 4, 2, 2),
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
[theme.breakpoints.down(500)]: {
|
||||
flexDirection: 'column',
|
||||
},
|
||||
}));
|
||||
|
||||
const StyledFlagInfoContainer = styled('div')({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
});
|
||||
|
||||
const StyledDependency = styled('div')(({ theme }) => ({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: theme.spacing(1),
|
||||
marginTop: theme.spacing(1),
|
||||
fontSize: theme.fontSizes.smallBody,
|
||||
padding: theme.spacing(0.75, 1.5),
|
||||
backgroundColor: theme.palette.background.elevation2,
|
||||
borderRadius: `${theme.shape.borderRadiusMedium}px`,
|
||||
width: 'max-content',
|
||||
}));
|
||||
|
||||
const StyledFeatureViewHeader = styled('h1')(({ theme }) => ({
|
||||
fontSize: theme.fontSizes.mainHeader,
|
||||
fontWeight: 'normal',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
wordBreak: 'break-all',
|
||||
}));
|
||||
|
||||
const StyledToolbarContainer = styled('div')({
|
||||
flexShrink: 0,
|
||||
display: 'flex',
|
||||
});
|
||||
|
||||
const StyledSeparator = styled('div')(({ theme }) => ({
|
||||
width: '100%',
|
||||
backgroundColor: theme.palette.divider,
|
||||
height: '1px',
|
||||
}));
|
||||
|
||||
const StyledTabRow = styled('div')(({ theme }) => ({
|
||||
display: 'flex',
|
||||
flexFlow: 'row nowrap',
|
||||
gap: theme.spacing(4),
|
||||
paddingInline: theme.spacing(4),
|
||||
justifyContent: 'space-between',
|
||||
}));
|
||||
|
||||
const StyledTabButton = styled(Tab)(({ theme }) => ({
|
||||
textTransform: 'none',
|
||||
width: 'auto',
|
||||
fontSize: theme.fontSizes.bodySize,
|
||||
padding: '0 !important',
|
||||
[theme.breakpoints.up('md')]: {
|
||||
minWidth: 160,
|
||||
},
|
||||
}));
|
||||
|
||||
export const StyledLink = styled(Link)(() => ({
|
||||
maxWidth: '100%',
|
||||
textDecoration: 'none',
|
||||
'&:hover, &:focus': {
|
||||
textDecoration: 'underline',
|
||||
},
|
||||
}));
|
||||
|
||||
const useLegacyVariants = (environments: IFeatureToggle['environments']) => {
|
||||
const enableLegacyVariants = useUiFlag('enableLegacyVariants');
|
||||
const existingLegacyVariantsExist = environments.some(
|
||||
(environment) => environment.variants?.length,
|
||||
);
|
||||
return enableLegacyVariants || existingLegacyVariantsExist;
|
||||
};
|
||||
|
||||
type Props = {
|
||||
feature: IFeatureToggle;
|
||||
};
|
||||
|
||||
export const FeatureViewHeader: FC<Props> = ({ feature }) => {
|
||||
const projectId = useRequiredPathParam('projectId');
|
||||
const featureId = useRequiredPathParam('featureId');
|
||||
const flagOverviewRedesign = useUiFlag('flagOverviewRedesign');
|
||||
const { favorite, unfavorite } = useFavoriteFeaturesApi();
|
||||
const { refetchFeature } = useFeature(projectId, featureId);
|
||||
const { setToastData, setToastApiError } = useToast();
|
||||
|
||||
const [openTagDialog, setOpenTagDialog] = useState(false);
|
||||
const [showDelDialog, setShowDelDialog] = useState(false);
|
||||
const [openStaleDialog, setOpenStaleDialog] = useState(false);
|
||||
|
||||
const [isFeatureNameCopied, setIsFeatureNameCopied] = useState(false);
|
||||
const smallScreen = useMediaQuery(`(max-width:${500}px)`);
|
||||
|
||||
const navigate = useNavigate();
|
||||
|
||||
const { pathname } = useLocation();
|
||||
|
||||
const basePath = `/projects/${projectId}/features/${featureId}`;
|
||||
|
||||
const showLegacyVariants = useLegacyVariants(feature.environments);
|
||||
|
||||
const tabData = [
|
||||
{
|
||||
title: 'Overview',
|
||||
path: `${basePath}`,
|
||||
name: 'overview',
|
||||
},
|
||||
{
|
||||
title: 'Metrics',
|
||||
path: `${basePath}/metrics`,
|
||||
name: 'Metrics',
|
||||
},
|
||||
...(showLegacyVariants
|
||||
? [
|
||||
{
|
||||
title: 'Variants',
|
||||
path: `${basePath}/variants`,
|
||||
name: 'Variants',
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{ title: 'Settings', path: `${basePath}/settings`, name: 'Settings' },
|
||||
{
|
||||
title: 'Event log',
|
||||
path: `${basePath}/logs`,
|
||||
name: 'Event log',
|
||||
},
|
||||
];
|
||||
|
||||
const activeTab =
|
||||
tabData.find((tab) => tab.path === pathname) ?? tabData[0];
|
||||
|
||||
const onFavorite = async () => {
|
||||
try {
|
||||
if (feature.favorite) {
|
||||
await unfavorite(projectId, feature.name);
|
||||
} else {
|
||||
await favorite(projectId, feature.name);
|
||||
}
|
||||
refetchFeature();
|
||||
} catch (error) {
|
||||
setToastApiError('Something went wrong, could not update favorite');
|
||||
}
|
||||
};
|
||||
|
||||
const handleCopyToClipboard = () => {
|
||||
try {
|
||||
copy(feature.name);
|
||||
setIsFeatureNameCopied(true);
|
||||
setTimeout(() => {
|
||||
setIsFeatureNameCopied(false);
|
||||
}, 3000);
|
||||
} catch (error: unknown) {
|
||||
setToastData({
|
||||
type: 'error',
|
||||
text: 'Could not copy feature name',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{flagOverviewRedesign ? (
|
||||
<NewStyledHeader>
|
||||
<Typography variant='h1'>{feature.name}</Typography>
|
||||
<LowerHeaderRow>
|
||||
<Tabs
|
||||
value={activeTab.path}
|
||||
indicatorColor='primary'
|
||||
textColor='primary'
|
||||
>
|
||||
{tabData.map((tab) => (
|
||||
<StyledTabButton
|
||||
key={tab.title}
|
||||
label={tab.title}
|
||||
value={tab.path}
|
||||
onClick={() => navigate(tab.path)}
|
||||
data-testid={`TAB-${tab.title}`}
|
||||
/>
|
||||
))}
|
||||
</Tabs>
|
||||
<HeaderActions>
|
||||
<IconButtonWithTooltip
|
||||
label='Favorite this feature flag'
|
||||
onClick={onFavorite}
|
||||
data-loading
|
||||
>
|
||||
{feature.favorite ? <Star /> : <StarBorder />}
|
||||
</IconButtonWithTooltip>
|
||||
|
||||
<IconButtonWithTooltip
|
||||
label='Copy flag name'
|
||||
onClick={handleCopyToClipboard}
|
||||
data-loading
|
||||
>
|
||||
{isFeatureNameCopied ? (
|
||||
<Check />
|
||||
) : (
|
||||
<FileCopyOutlined />
|
||||
)}
|
||||
</IconButtonWithTooltip>
|
||||
<PermissionIconButton
|
||||
permission={CREATE_FEATURE}
|
||||
projectId={projectId}
|
||||
data-loading
|
||||
component={Link}
|
||||
to={`/projects/${projectId}/features/${featureId}/copy`}
|
||||
tooltipProps={{
|
||||
title: 'Clone',
|
||||
}}
|
||||
>
|
||||
<LibraryAddOutlined />
|
||||
</PermissionIconButton>
|
||||
|
||||
<PermissionIconButton
|
||||
permission={DELETE_FEATURE}
|
||||
projectId={projectId}
|
||||
tooltipProps={{
|
||||
title: 'Archive feature flag',
|
||||
}}
|
||||
data-loading
|
||||
onClick={() => setShowDelDialog(true)}
|
||||
>
|
||||
<ArchiveOutlined />
|
||||
</PermissionIconButton>
|
||||
<PermissionIconButton
|
||||
onClick={() => setOpenStaleDialog(true)}
|
||||
permission={UPDATE_FEATURE}
|
||||
projectId={projectId}
|
||||
tooltipProps={{
|
||||
title: 'Toggle stale state',
|
||||
}}
|
||||
data-loading
|
||||
>
|
||||
<WatchLaterOutlined />
|
||||
</PermissionIconButton>
|
||||
</HeaderActions>
|
||||
</LowerHeaderRow>
|
||||
</NewStyledHeader>
|
||||
) : (
|
||||
<StyledHeader>
|
||||
<StyledInnerContainer>
|
||||
<StyledFlagInfoContainer>
|
||||
<FavoriteIconButton
|
||||
onClick={onFavorite}
|
||||
isFavorite={feature.favorite}
|
||||
/>
|
||||
<div>
|
||||
<StyledFlagInfoContainer>
|
||||
<StyledFeatureViewHeader data-loading>
|
||||
{feature.name}
|
||||
</StyledFeatureViewHeader>
|
||||
<Tooltip
|
||||
title={
|
||||
isFeatureNameCopied
|
||||
? 'Copied!'
|
||||
: 'Copy name'
|
||||
}
|
||||
arrow
|
||||
>
|
||||
<IconButton
|
||||
onClick={handleCopyToClipboard}
|
||||
style={{ marginLeft: 8 }}
|
||||
>
|
||||
{isFeatureNameCopied ? (
|
||||
<Check
|
||||
style={{ fontSize: 16 }}
|
||||
/>
|
||||
) : (
|
||||
<FileCopy
|
||||
style={{ fontSize: 16 }}
|
||||
/>
|
||||
)}
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<ConditionallyRender
|
||||
condition={!smallScreen}
|
||||
show={
|
||||
<FeatureStatusChip
|
||||
stale={feature.stale}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</StyledFlagInfoContainer>
|
||||
<ConditionallyRender
|
||||
condition={feature.dependencies.length > 0}
|
||||
show={
|
||||
<StyledDependency>
|
||||
<b>Has parent: </b>
|
||||
<StyledLink
|
||||
to={`/projects/${feature.project}/features/${feature.dependencies[0]?.feature}`}
|
||||
>
|
||||
{
|
||||
feature.dependencies[0]
|
||||
?.feature
|
||||
}
|
||||
</StyledLink>
|
||||
</StyledDependency>
|
||||
}
|
||||
/>
|
||||
<ConditionallyRender
|
||||
condition={feature.children.length > 0}
|
||||
show={
|
||||
<StyledDependency>
|
||||
<b>Has children:</b>
|
||||
<ChildrenTooltip
|
||||
childFeatures={feature.children}
|
||||
project={feature.project}
|
||||
/>
|
||||
</StyledDependency>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</StyledFlagInfoContainer>
|
||||
|
||||
<StyledToolbarContainer>
|
||||
<PermissionIconButton
|
||||
permission={CREATE_FEATURE}
|
||||
projectId={projectId}
|
||||
data-loading
|
||||
component={Link}
|
||||
to={`/projects/${projectId}/features/${featureId}/copy`}
|
||||
tooltipProps={{
|
||||
title: 'Clone',
|
||||
}}
|
||||
>
|
||||
<LibraryAdd />
|
||||
</PermissionIconButton>
|
||||
<PermissionIconButton
|
||||
permission={DELETE_FEATURE}
|
||||
projectId={projectId}
|
||||
tooltipProps={{
|
||||
title: 'Archive feature flag',
|
||||
}}
|
||||
data-loading
|
||||
onClick={() => setShowDelDialog(true)}
|
||||
>
|
||||
<Archive />
|
||||
</PermissionIconButton>
|
||||
<PermissionIconButton
|
||||
onClick={() => setOpenStaleDialog(true)}
|
||||
permission={UPDATE_FEATURE}
|
||||
projectId={projectId}
|
||||
tooltipProps={{
|
||||
title: 'Toggle stale state',
|
||||
}}
|
||||
data-loading
|
||||
>
|
||||
<WatchLater />
|
||||
</PermissionIconButton>
|
||||
<PermissionIconButton
|
||||
onClick={() => setOpenTagDialog(true)}
|
||||
permission={UPDATE_FEATURE}
|
||||
projectId={projectId}
|
||||
tooltipProps={{ title: 'Add tag' }}
|
||||
data-loading
|
||||
>
|
||||
<Label />
|
||||
</PermissionIconButton>
|
||||
</StyledToolbarContainer>
|
||||
</StyledInnerContainer>
|
||||
<StyledSeparator />
|
||||
<StyledTabRow>
|
||||
<Tabs
|
||||
value={activeTab.path}
|
||||
indicatorColor='primary'
|
||||
textColor='primary'
|
||||
>
|
||||
{tabData.map((tab) => (
|
||||
<StyledTabButton
|
||||
key={tab.title}
|
||||
label={tab.title}
|
||||
value={tab.path}
|
||||
onClick={() => navigate(tab.path)}
|
||||
data-testid={`TAB-${tab.title}`}
|
||||
/>
|
||||
))}
|
||||
</Tabs>
|
||||
<Collaborators
|
||||
collaborators={feature.collaborators?.users}
|
||||
/>
|
||||
</StyledTabRow>
|
||||
</StyledHeader>
|
||||
)}
|
||||
|
||||
{feature.children.length > 0 ? (
|
||||
<FeatureArchiveNotAllowedDialog
|
||||
features={feature.children}
|
||||
project={projectId}
|
||||
isOpen={showDelDialog}
|
||||
onClose={() => setShowDelDialog(false)}
|
||||
/>
|
||||
) : (
|
||||
<FeatureArchiveDialog
|
||||
isOpen={showDelDialog}
|
||||
onConfirm={() => {
|
||||
navigate(`/projects/${projectId}`);
|
||||
}}
|
||||
onClose={() => setShowDelDialog(false)}
|
||||
projectId={projectId}
|
||||
featureIds={[featureId]}
|
||||
/>
|
||||
)}
|
||||
|
||||
<FeatureStaleDialog
|
||||
isStale={feature.stale}
|
||||
isOpen={openStaleDialog}
|
||||
onClose={() => {
|
||||
setOpenStaleDialog(false);
|
||||
refetchFeature();
|
||||
}}
|
||||
featureId={featureId}
|
||||
projectId={projectId}
|
||||
/>
|
||||
<ManageTagsDialog open={openTagDialog} setOpen={setOpenTagDialog} />
|
||||
</>
|
||||
);
|
||||
};
|
Loading…
Reference in New Issue
Block a user