From 4701dc15522935651b59304330a97b0a0f577add Mon Sep 17 00:00:00 2001 From: Thomas Heartman Date: Tue, 18 Feb 2025 11:35:40 +0100 Subject: [PATCH] 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. --- .../feature/FeatureView/FeatureView.tsx | 501 +---------------- .../feature/FeatureView/FeatureViewHeader.tsx | 515 ++++++++++++++++++ 2 files changed, 519 insertions(+), 497 deletions(-) create mode 100644 frontend/src/component/feature/FeatureView/FeatureViewHeader.tsx diff --git a/frontend/src/component/feature/FeatureView/FeatureView.tsx b/frontend/src/component/feature/FeatureView/FeatureView.tsx index c1f14ffc0f..95b83e39f8 100644 --- a/frontend/src/component/feature/FeatureView/FeatureView.tsx +++ b/frontend/src/component/feature/FeatureView/FeatureView.tsx @@ -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 ( - e.preventDefault()} - > - - {children} - - - ); -}; - -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 ; } @@ -261,245 +38,9 @@ export const FeatureView = () => { return
; } - 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 ? ( - - {feature.name} - - - {tabData.map((tab) => ( - navigate(tab.path)} - data-testid={`TAB-${tab.title}`} - /> - ))} - - - - {feature?.favorite ? : } - - - - {isFeatureNameCopied ? ( - - ) : ( - - )} - - - - - - setShowDelDialog(true)} - > - - - setOpenStaleDialog(true)} - permission={UPDATE_FEATURE} - projectId={projectId} - tooltipProps={{ - title: 'Toggle stale state', - }} - data-loading - > - - - - - - ) : ( - - - - -
- - - {feature.name}{' '} - - - - {isFeatureNameCopied ? ( - - ) : ( - - )} - - - - } - /> - - 0} - show={ - - Has parent: - - { - feature?.dependencies[0] - ?.feature - } - - - } - /> - 0} - show={ - - Has children: - - - } - /> -
-
- - - - - - setShowDelDialog(true)} - > - - - setOpenStaleDialog(true)} - permission={UPDATE_FEATURE} - projectId={projectId} - tooltipProps={{ - title: 'Toggle stale state', - }} - data-loading - > - - - setOpenTagDialog(true)} - permission={UPDATE_FEATURE} - projectId={projectId} - tooltipProps={{ title: 'Add tag' }} - data-loading - > - - -
- - - - {tabData.map((tab) => ( - navigate(tab.path)} - data-testid={`TAB-${tab.title}`} - /> - ))} - - - -
- )} + } /> } /> @@ -510,40 +51,6 @@ export const FeatureView = () => { } /> } /> - 0} - show={ - setShowDelDialog(false)} - /> - } - elseShow={ - { - navigate(`/projects/${projectId}`); - }} - onClose={() => setShowDelDialog(false)} - projectId={projectId} - featureIds={[featureId]} - /> - } - /> - - { - setOpenStaleDialog(false); - refetchFeature(); - }} - featureId={featureId} - projectId={projectId} - /> -
); }; diff --git a/frontend/src/component/feature/FeatureView/FeatureViewHeader.tsx b/frontend/src/component/feature/FeatureView/FeatureViewHeader.tsx new file mode 100644 index 0000000000..2c502496cb --- /dev/null +++ b/frontend/src/component/feature/FeatureView/FeatureViewHeader.tsx @@ -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 ( + e.preventDefault()} + > + + {children} + + + ); +}; + +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 = ({ 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 ? ( + + {feature.name} + + + {tabData.map((tab) => ( + navigate(tab.path)} + data-testid={`TAB-${tab.title}`} + /> + ))} + + + + {feature.favorite ? : } + + + + {isFeatureNameCopied ? ( + + ) : ( + + )} + + + + + + setShowDelDialog(true)} + > + + + setOpenStaleDialog(true)} + permission={UPDATE_FEATURE} + projectId={projectId} + tooltipProps={{ + title: 'Toggle stale state', + }} + data-loading + > + + + + + + ) : ( + + + + +
+ + + {feature.name} + + + + {isFeatureNameCopied ? ( + + ) : ( + + )} + + + + } + /> + + 0} + show={ + + Has parent: + + { + feature.dependencies[0] + ?.feature + } + + + } + /> + 0} + show={ + + Has children: + + + } + /> +
+
+ + + + + + setShowDelDialog(true)} + > + + + setOpenStaleDialog(true)} + permission={UPDATE_FEATURE} + projectId={projectId} + tooltipProps={{ + title: 'Toggle stale state', + }} + data-loading + > + + + setOpenTagDialog(true)} + permission={UPDATE_FEATURE} + projectId={projectId} + tooltipProps={{ title: 'Add tag' }} + data-loading + > + + +
+ + + + {tabData.map((tab) => ( + navigate(tab.path)} + data-testid={`TAB-${tab.title}`} + /> + ))} + + + +
+ )} + + {feature.children.length > 0 ? ( + setShowDelDialog(false)} + /> + ) : ( + { + navigate(`/projects/${projectId}`); + }} + onClose={() => setShowDelDialog(false)} + projectId={projectId} + featureIds={[featureId]} + /> + )} + + { + setOpenStaleDialog(false); + refetchFeature(); + }} + featureId={featureId} + projectId={projectId} + /> + + + ); +};