diff --git a/frontend/src/component/feature/FeatureView/FeatureView.tsx b/frontend/src/component/feature/FeatureView/FeatureView.tsx index c1f14ffc0f6f..95b83e39f812 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 000000000000..2c502496cbe7 --- /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} + /> + + + ); +};