1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-07-12 13:48:35 +02:00

fix: archived feature layout (#2713)

This commit is contained in:
Tymoteusz Czech 2023-01-04 10:24:39 +01:00 committed by GitHub
parent 45652f6bf9
commit 111dddd746
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 148 additions and 188 deletions

View File

@ -1,19 +1,13 @@
import { import { FC } from 'react';
render, import { render, screen, within, fireEvent } from '@testing-library/react';
screen,
waitFor,
within,
getAllByRole,
fireEvent,
} from '@testing-library/react';
import { MemoryRouter, Routes, Route } from 'react-router-dom'; import { MemoryRouter, Routes, Route } from 'react-router-dom';
import { FeatureView } from '../feature/FeatureView/FeatureView';
import { ThemeProvider } from 'themes/ThemeProvider'; import { ThemeProvider } from 'themes/ThemeProvider';
import { MainLayout } from 'component/layout/MainLayout/MainLayout';
import { FeatureView } from '../feature/FeatureView/FeatureView';
import { AccessProvider } from '../providers/AccessProvider/AccessProvider'; import { AccessProvider } from '../providers/AccessProvider/AccessProvider';
import { AnnouncerProvider } from '../common/Announcer/AnnouncerProvider/AnnouncerProvider'; import { AnnouncerProvider } from '../common/Announcer/AnnouncerProvider/AnnouncerProvider';
import { testServerRoute, testServerSetup } from '../../utils/testServer'; import { testServerRoute, testServerSetup } from '../../utils/testServer';
import { UIProviderContainer } from '../providers/UIProvider/UIProviderContainer'; import { UIProviderContainer } from '../providers/UIProvider/UIProviderContainer';
import { FC } from 'react';
const server = testServerSetup(); const server = testServerSetup();
@ -227,7 +221,10 @@ const UnleashUiSetup: FC<{ path: string; pathTemplate: string }> = ({
<ThemeProvider> <ThemeProvider>
<AnnouncerProvider> <AnnouncerProvider>
<Routes> <Routes>
<Route path={pathTemplate} element={children} /> <Route
path={pathTemplate}
element={<MainLayout>{children}</MainLayout>}
/>
</Routes> </Routes>
</AnnouncerProvider> </AnnouncerProvider>
</ThemeProvider> </ThemeProvider>

View File

@ -31,7 +31,10 @@ export const FeatureNotFound = () => {
The feature{' '} The feature{' '}
<strong className={styles.featureId}>{featureId}</strong> has <strong className={styles.featureId}>{featureId}</strong> has
been archived. You can find it on the{' '} been archived. You can find it on the{' '}
<Link to={'/archive'}>archive page</Link>. <Link to={`/projects/${projectId}/archive`}>
project archive page
</Link>
.
</p> </p>
); );
} }

View File

@ -1,5 +1,5 @@
import { Tab, Tabs, useMediaQuery } from '@mui/material';
import { useState } from 'react'; import { useState } from 'react';
import { Tab, Tabs, useMediaQuery } from '@mui/material';
import { Archive, FileCopy, Label, WatchLater } from '@mui/icons-material'; import { Archive, FileCopy, Label, WatchLater } from '@mui/icons-material';
import { import {
Link, Link,
@ -30,11 +30,7 @@ import { FeatureStatusChip } from 'component/common/FeatureStatusChip/FeatureSta
import { FeatureNotFound } from 'component/feature/FeatureView/FeatureNotFound/FeatureNotFound'; import { FeatureNotFound } from 'component/feature/FeatureView/FeatureNotFound/FeatureNotFound';
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 { DraftBanner } from 'component/changeRequest/DraftBanner/DraftBanner';
import { MainLayout } from 'component/layout/MainLayout/MainLayout';
import { useChangeRequestsEnabled } from 'hooks/useChangeRequestsEnabled';
import { useFavoriteFeaturesApi } from 'hooks/api/actions/useFavoriteFeaturesApi/useFavoriteFeaturesApi'; import { useFavoriteFeaturesApi } from 'hooks/api/actions/useFavoriteFeaturesApi/useFavoriteFeaturesApi';
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
import { FavoriteIconButton } from 'component/common/FavoriteIconButton/FavoriteIconButton'; import { FavoriteIconButton } from 'component/common/FavoriteIconButton/FavoriteIconButton';
export const FeatureView = () => { export const FeatureView = () => {
@ -43,9 +39,6 @@ export const FeatureView = () => {
const { refetch: projectRefetch } = useProject(projectId); const { refetch: projectRefetch } = useProject(projectId);
const { favorite, unfavorite } = useFavoriteFeaturesApi(); const { favorite, unfavorite } = useFavoriteFeaturesApi();
const { refetchFeature } = useFeature(projectId, featureId); const { refetchFeature } = useFeature(projectId, featureId);
const { isChangeRequestConfiguredInAnyEnv } =
useChangeRequestsEnabled(projectId);
const { uiConfig } = useUiConfig();
const [openTagDialog, setOpenTagDialog] = useState(false); const [openTagDialog, setOpenTagDialog] = useState(false);
const [showDelDialog, setShowDelDialog] = useState(false); const [showDelDialog, setShowDelDialog] = useState(false);
@ -86,10 +79,6 @@ export const FeatureView = () => {
const activeTab = tabData.find(tab => tab.path === pathname) ?? tabData[0]; const activeTab = tabData.find(tab => tab.path === pathname) ?? tabData[0];
if (status === 404) {
return <FeatureNotFound />;
}
const onFavorite = async () => { const onFavorite = async () => {
if (feature?.favorite) { if (feature?.favorite) {
await unfavorite(projectId, feature.name); await unfavorite(projectId, feature.name);
@ -99,150 +88,125 @@ export const FeatureView = () => {
refetchFeature(); refetchFeature();
}; };
return ( if (status === 404) {
<MainLayout return <FeatureNotFound />;
ref={ref} }
subheader={
isChangeRequestConfiguredInAnyEnv() ? (
<DraftBanner project={projectId} />
) : null
}
>
<ConditionallyRender
condition={error === undefined}
show={
<div ref={ref}>
<div className={styles.header}>
<div className={styles.innerContainer}>
<div className={styles.toggleInfoContainer}>
<FavoriteIconButton
onClick={onFavorite}
isFavorite={feature?.favorite}
/>
<h1
className={styles.featureViewHeader}
data-loading
>
{feature.name}{' '}
</h1>
<ConditionallyRender
condition={!smallScreen}
show={
<FeatureStatusChip
stale={feature?.stale}
/>
}
/>
</div>
<div className={styles.toolbarContainer}> if (error !== undefined) {
<PermissionIconButton return <div ref={ref} />;
permission={CREATE_FEATURE} }
projectId={projectId}
data-loading return (
component={Link} <div ref={ref}>
to={`/projects/${projectId}/features/${featureId}/strategies/copy`} <div className={styles.header}>
tooltipProps={{ <div className={styles.innerContainer}>
title: 'Copy feature toggle', <div className={styles.toggleInfoContainer}>
}} <FavoriteIconButton
> onClick={onFavorite}
<FileCopy /> isFavorite={feature?.favorite}
</PermissionIconButton>
<PermissionIconButton
permission={DELETE_FEATURE}
projectId={projectId}
tooltipProps={{
title: 'Archive feature toggle',
}}
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>
</div>
</div>
<div className={styles.separator} />
<div className={styles.tabContainer}>
<Tabs
value={activeTab.path}
indicatorColor="primary"
textColor="primary"
>
{tabData.map(tab => (
<Tab
key={tab.title}
label={tab.title}
value={tab.path}
onClick={() => navigate(tab.path)}
className={styles.tabButton}
/>
))}
</Tabs>
</div>
</div>
<Routes>
<Route
path="metrics"
element={<FeatureMetrics />}
/>
<Route path="logs" element={<FeatureLog />} />
<Route
path="variants"
element={<FeatureVariants />}
/>
<Route
path="settings"
element={<FeatureSettings />}
/>
<Route path="*" element={<FeatureOverview />} />
</Routes>
<FeatureArchiveDialog
isOpen={showDelDialog}
onConfirm={() => {
projectRefetch();
navigate(`/projects/${projectId}`);
}}
onClose={() => setShowDelDialog(false)}
projectId={projectId}
featureId={featureId}
/> />
<FeatureStaleDialog <h1 className={styles.featureViewHeader} data-loading>
isStale={feature.stale} {feature.name}{' '}
isOpen={openStaleDialog} </h1>
onClose={() => { <ConditionallyRender
setOpenStaleDialog(false); condition={!smallScreen}
refetchFeature(); show={<FeatureStatusChip stale={feature?.stale} />}
}}
featureId={featureId}
projectId={projectId}
/>
<AddTagDialog
open={openTagDialog}
setOpen={setOpenTagDialog}
/> />
</div> </div>
}
<div className={styles.toolbarContainer}>
<PermissionIconButton
permission={CREATE_FEATURE}
projectId={projectId}
data-loading
component={Link}
to={`/projects/${projectId}/features/${featureId}/strategies/copy`}
tooltipProps={{
title: 'Copy feature toggle',
}}
>
<FileCopy />
</PermissionIconButton>
<PermissionIconButton
permission={DELETE_FEATURE}
projectId={projectId}
tooltipProps={{
title: 'Archive feature toggle',
}}
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>
</div>
</div>
<div className={styles.separator} />
<div className={styles.tabContainer}>
<Tabs
value={activeTab.path}
indicatorColor="primary"
textColor="primary"
>
{tabData.map(tab => (
<Tab
key={tab.title}
label={tab.title}
value={tab.path}
onClick={() => navigate(tab.path)}
className={styles.tabButton}
/>
))}
</Tabs>
</div>
</div>
<Routes>
<Route path="metrics" element={<FeatureMetrics />} />
<Route path="logs" element={<FeatureLog />} />
<Route path="variants" element={<FeatureVariants />} />
<Route path="settings" element={<FeatureSettings />} />
<Route path="*" element={<FeatureOverview />} />
</Routes>
<FeatureArchiveDialog
isOpen={showDelDialog}
onConfirm={() => {
projectRefetch();
navigate(`/projects/${projectId}`);
}}
onClose={() => setShowDelDialog(false)}
projectId={projectId}
featureId={featureId}
/> />
</MainLayout> <FeatureStaleDialog
isStale={feature.stale}
isOpen={openStaleDialog}
onClose={() => {
setOpenStaleDialog(false);
refetchFeature();
}}
featureId={featureId}
projectId={projectId}
/>
<AddTagDialog open={openTagDialog} setOpen={setOpenTagDialog} />
</div>
); );
}; };

View File

@ -2,10 +2,10 @@ import { FC, useState, VFC } from 'react';
import { Box, Button, styled, Typography } from '@mui/material'; import { Box, Button, styled, Typography } from '@mui/material';
import { useStyles as useAppStyles } from 'component/App.styles'; import { useStyles as useAppStyles } from 'component/App.styles';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { ChangeRequestSidebar } from '../ChangeRequestSidebar/ChangeRequestSidebar'; import { ChangeRequestSidebar } from 'component/changeRequest/ChangeRequestSidebar/ChangeRequestSidebar';
import { usePendingChangeRequests } from 'hooks/api/getters/usePendingChangeRequests/usePendingChangeRequests'; import { usePendingChangeRequests } from 'hooks/api/getters/usePendingChangeRequests/usePendingChangeRequests';
import { IChangeRequest } from '../changeRequest.types'; import { IChangeRequest } from 'component/changeRequest/changeRequest.types';
import { changesCount } from '../changesCount'; import { changesCount } from 'component/changeRequest/changesCount';
interface IDraftBannerProps { interface IDraftBannerProps {
project: string; project: string;

View File

@ -12,6 +12,10 @@ import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
import { SkipNavLink } from 'component/common/SkipNav/SkipNavLink'; import { SkipNavLink } from 'component/common/SkipNav/SkipNavLink';
import { SkipNavTarget } from 'component/common/SkipNav/SkipNavTarget'; import { SkipNavTarget } from 'component/common/SkipNav/SkipNavTarget';
import { formatAssetPath } from 'utils/formatPath'; import { formatAssetPath } from 'utils/formatPath';
import { useOptionalPathParam } from 'hooks/useOptionalPathParam';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { useChangeRequestsEnabled } from 'hooks/useChangeRequestsEnabled';
import { DraftBanner } from './DraftBanner/DraftBanner';
const useStyles = makeStyles()(theme => ({ const useStyles = makeStyles()(theme => ({
container: { container: {
@ -30,14 +34,17 @@ const useStyles = makeStyles()(theme => ({
interface IMainLayoutProps { interface IMainLayoutProps {
children: ReactNode; children: ReactNode;
subheader?: ReactNode;
} }
export const MainLayout = forwardRef<HTMLDivElement, IMainLayoutProps>( export const MainLayout = forwardRef<HTMLDivElement, IMainLayoutProps>(
({ children, subheader }, ref) => { ({ children }, ref) => {
const { classes } = useStyles(); const { classes } = useStyles();
const { classes: styles } = useAppStyles(); const { classes: styles } = useAppStyles();
const { uiConfig } = useUiConfig(); const { uiConfig } = useUiConfig();
const projectId = useOptionalPathParam('projectId');
const { isChangeRequestConfiguredInAnyEnv } = useChangeRequestsEnabled(
projectId || ''
);
return ( return (
<> <>
@ -46,7 +53,12 @@ export const MainLayout = forwardRef<HTMLDivElement, IMainLayoutProps>(
<SkipNavTarget /> <SkipNavTarget />
<Grid container className={classes.container}> <Grid container className={classes.container}>
<main className={classnames(styles.contentWrapper)}> <main className={classnames(styles.contentWrapper)}>
{subheader} <ConditionallyRender
condition={Boolean(
projectId && isChangeRequestConfiguredInAnyEnv()
)}
show={<DraftBanner project={projectId || ''} />}
/>
<Grid <Grid
item item
className={styles.content} className={styles.content}

View File

@ -54,7 +54,6 @@ exports[`returns all baseRoutes 1`] = `
}, },
{ {
"component": [Function], "component": [Function],
"isStandalone": true,
"menu": {}, "menu": {},
"parent": "/projects", "parent": "/projects",
"path": "/projects/:projectId/features/:featureId/*", "path": "/projects/:projectId/features/:featureId/*",
@ -80,7 +79,6 @@ exports[`returns all baseRoutes 1`] = `
{ {
"component": [Function], "component": [Function],
"flag": "P", "flag": "P",
"isStandalone": true,
"menu": {}, "menu": {},
"parent": "/projects", "parent": "/projects",
"path": "/projects/:projectId/*", "path": "/projects/:projectId/*",

View File

@ -123,7 +123,6 @@ export const routes: IRoute[] = [
title: 'FeatureView', title: 'FeatureView',
component: FeatureView, component: FeatureView,
type: 'protected', type: 'protected',
isStandalone: true,
menu: {}, menu: {},
}, },
{ {
@ -150,7 +149,6 @@ export const routes: IRoute[] = [
flag: P, flag: P,
type: 'protected', type: 'protected',
menu: {}, menu: {},
isStandalone: true,
}, },
{ {
path: '/projects', path: '/projects',

View File

@ -37,11 +37,8 @@ import { Navigate, Route, Routes, useLocation } from 'react-router-dom';
import { DeleteProjectDialogue } from './DeleteProject/DeleteProjectDialogue'; import { DeleteProjectDialogue } from './DeleteProject/DeleteProjectDialogue';
import { ProjectLog } from './ProjectLog/ProjectLog'; import { ProjectLog } from './ProjectLog/ProjectLog';
import { ChangeRequestOverview } from 'component/changeRequest/ChangeRequestOverview/ChangeRequestOverview'; import { ChangeRequestOverview } from 'component/changeRequest/ChangeRequestOverview/ChangeRequestOverview';
import { DraftBanner } from 'component/changeRequest/DraftBanner/DraftBanner';
import { MainLayout } from 'component/layout/MainLayout/MainLayout';
import { ProjectChangeRequests } from '../../changeRequest/ProjectChangeRequests/ProjectChangeRequests'; import { ProjectChangeRequests } from '../../changeRequest/ProjectChangeRequests/ProjectChangeRequests';
import { ProjectSettings } from './ProjectSettings/ProjectSettings'; import { ProjectSettings } from './ProjectSettings/ProjectSettings';
import { useChangeRequestsEnabled } from 'hooks/useChangeRequestsEnabled';
import { useFavoriteProjectsApi } from 'hooks/api/actions/useFavoriteProjectsApi/useFavoriteProjectsApi'; import { useFavoriteProjectsApi } from 'hooks/api/actions/useFavoriteProjectsApi/useFavoriteProjectsApi';
const Project = () => { const Project = () => {
@ -55,8 +52,6 @@ const Project = () => {
const { isOss } = useUiConfig(); const { isOss } = useUiConfig();
const basePath = `/projects/${projectId}`; const basePath = `/projects/${projectId}`;
const projectName = project?.name || projectId; const projectName = project?.name || projectId;
const { isChangeRequestConfiguredInAnyEnv } =
useChangeRequestsEnabled(projectId);
const { favorite, unfavorite } = useFavoriteProjectsApi(); const { favorite, unfavorite } = useFavoriteProjectsApi();
const [showDelDialog, setShowDelDialog] = useState(false); const [showDelDialog, setShowDelDialog] = useState(false);
@ -122,14 +117,7 @@ const Project = () => {
}; };
return ( return (
<MainLayout <div ref={ref}>
ref={ref}
subheader={
isChangeRequestConfiguredInAnyEnv() ? (
<DraftBanner project={projectId} />
) : null
}
>
<StyledHeader> <StyledHeader>
<StyledInnerContainer> <StyledInnerContainer>
<StyledTopRow> <StyledTopRow>
@ -259,7 +247,7 @@ const Project = () => {
<Route path="settings/*" element={<ProjectSettings />} /> <Route path="settings/*" element={<ProjectSettings />} />
<Route path="*" element={<ProjectOverview />} /> <Route path="*" element={<ProjectOverview />} />
</Routes> </Routes>
</MainLayout> </div>
); );
}; };