1
0
mirror of https://github.com/Unleash/unleash.git synced 2024-12-22 19:07:54 +01:00

feat: add tabs (#6267)

This PR adds a new file "Application.tsx", which is analogous to the
existing Project.tsx file in that it contains the base layout for the
application page, as well as tabs pointing to sub pages. Currently, the
overview tab uses a paragraph with some fallback text, while the
connected instances table displays the instances table.

I have mostly copied the existing ApplicationEdit component and used
that as a base, assuming that we'll delete that component when we're
done with this.

<img width="1449" alt="image"
src="https://github.com/Unleash/unleash/assets/17786332/ac574a83-3cf4-4de5-a4de-188575074ecb">
This commit is contained in:
Thomas Heartman 2024-02-20 17:32:33 +08:00 committed by GitHub
parent 7e6a3c7e69
commit d967d4adb0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 242 additions and 13 deletions

View File

@ -0,0 +1,238 @@
/* eslint react/no-multi-comp:off */
import React, { useContext, useState } from 'react';
import {
Box,
Avatar,
Icon,
IconButton,
LinearProgress,
Link,
Tab,
Tabs,
Typography,
styled,
} from '@mui/material';
import { Link as LinkIcon } from '@mui/icons-material';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { UPDATE_APPLICATION } from 'component/providers/AccessProvider/permissions';
import { ConnectedInstances } from './ConnectedInstances/ConnectedInstances';
import { Dialogue } from 'component/common/Dialogue/Dialogue';
import { PageContent } from 'component/common/PageContent/PageContent';
import { PageHeader } from 'component/common/PageHeader/PageHeader';
import AccessContext from 'contexts/AccessContext';
import useApplicationsApi from 'hooks/api/actions/useApplicationsApi/useApplicationsApi';
import useApplication from 'hooks/api/getters/useApplication/useApplication';
import { Route, Routes, useLocation, useNavigate } from 'react-router-dom';
import { useLocationSettings } from 'hooks/useLocationSettings';
import useToast from 'hooks/useToast';
import PermissionButton from 'component/common/PermissionButton/PermissionButton';
import { formatDateYMD } from 'utils/formatDate';
import { formatUnknownError } from 'utils/formatUnknownError';
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
import { useUiFlag } from 'hooks/useUiFlag';
import { ApplicationEdit } from './ApplicationEdit/ApplicationEdit';
type Tab = {
title: string;
path: string;
name: string;
};
const StyledHeader = styled('div')(({ theme }) => ({
backgroundColor: theme.palette.background.paper,
borderRadius: theme.shape.borderRadiusLarge,
marginBottom: theme.spacing(3),
}));
const TabContainer = styled('div')(({ theme }) => ({
padding: theme.spacing(0, 4),
}));
const Separator = styled('div')(({ theme }) => ({
width: '100%',
backgroundColor: theme.palette.divider,
height: '1px',
}));
const StyledTab = styled(Tab)(({ theme }) => ({
textTransform: 'none',
fontSize: theme.fontSizes.bodySize,
flexGrow: 1,
flexBasis: 0,
[theme.breakpoints.down('md')]: {
paddingLeft: theme.spacing(1),
paddingRight: theme.spacing(1),
},
[theme.breakpoints.up('md')]: {
minWidth: 160,
},
}));
export const Application = () => {
const useOldApplicationScreen = !useUiFlag('sdkReporting');
const navigate = useNavigate();
const name = useRequiredPathParam('name');
const { application, loading } = useApplication(name);
const { appName, url, description, icon = 'apps', createdAt } = application;
const { hasAccess } = useContext(AccessContext);
const { deleteApplication } = useApplicationsApi();
const { locationSettings } = useLocationSettings();
const { setToastData, setToastApiError } = useToast();
const { pathname } = useLocation();
if (useOldApplicationScreen) {
return <ApplicationEdit />;
}
const basePath = `/applications/${name}`;
const [showDialog, setShowDialog] = useState(false);
const toggleModal = () => {
setShowDialog(!showDialog);
};
const formatDate = (v: string) => formatDateYMD(v, locationSettings.locale);
const onDeleteApplication = async (evt: React.SyntheticEvent) => {
evt.preventDefault();
try {
await deleteApplication(appName);
setToastData({
title: 'Deleted Successfully',
text: 'Application deleted successfully',
type: 'success',
});
navigate('/applications');
} catch (error: unknown) {
setToastApiError(formatUnknownError(error));
}
};
const renderModal = () => (
<Dialogue
open={showDialog}
onClose={toggleModal}
onClick={onDeleteApplication}
title='Are you sure you want to delete this application?'
/>
);
if (loading) {
return (
<div>
<p>Loading...</p>
<LinearProgress />
</div>
);
} else if (!application) {
return <p>Application ({appName}) not found</p>;
}
const tabs: Tab[] = [
{
title: 'Overview',
path: basePath,
name: 'overview',
},
{
title: 'Connected instances',
path: `${basePath}/instances`,
name: 'instances',
},
];
const newActiveTab = tabs.find((tab) => tab.path === pathname);
return (
<>
<StyledHeader>
<PageContent>
<PageHeader
titleElement={
<span
style={{
display: 'flex',
alignItems: 'center',
}}
>
<Avatar style={{ marginRight: '8px' }}>
<Icon>{icon || 'apps'}</Icon>
</Avatar>
{appName}
</span>
}
title={appName}
actions={
<>
<ConditionallyRender
condition={Boolean(url)}
show={
<IconButton
component={Link}
href={url}
size='large'
>
<LinkIcon titleAccess={url} />
</IconButton>
}
/>
<PermissionButton
tooltipProps={{
title: 'Delete application',
}}
onClick={toggleModal}
permission={UPDATE_APPLICATION}
>
Delete
</PermissionButton>
</>
}
/>
<Box sx={(theme) => ({ marginTop: theme.spacing(1) })}>
<Typography variant='body1'>
{description || ''}
</Typography>
<Typography variant='body2'>
Created: <strong>{formatDate(createdAt)}</strong>
</Typography>
</Box>
</PageContent>
<Separator />
<TabContainer>
<Tabs
value={newActiveTab?.path}
indicatorColor='primary'
textColor='primary'
variant='scrollable'
allowScrollButtonsMobile
>
{tabs.map((tab) => {
return (
<StyledTab
key={tab.title}
label={tab.title}
value={tab.path}
onClick={() => navigate(tab.path)}
data-testid={`TAB_${tab.title}`}
/>
);
})}
</Tabs>
</TabContainer>
</StyledHeader>
<PageContent>
<ConditionallyRender
condition={hasAccess(UPDATE_APPLICATION)}
show={<div>{renderModal()}</div>}
/>
<Routes>
<Route path='instances' element={<ConnectedInstances />} />
<Route path='*' element={<p>This is a placeholder</p>} />
</Routes>
</PageContent>
</>
);
};

View File

@ -31,10 +31,8 @@ import { formatDateYMD } from 'utils/formatDate';
import { formatUnknownError } from 'utils/formatUnknownError';
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
import { TabPanel } from 'component/common/TabNav/TabPanel/TabPanel';
import { useUiFlag } from 'hooks/useUiFlag';
export const ApplicationEdit = () => {
const showAdvancedApplicationMetrics = useUiFlag('sdkReporting');
const navigate = useNavigate();
const name = useRequiredPathParam('name');
const { application, loading } = useApplication(name);
@ -87,13 +85,6 @@ export const ApplicationEdit = () => {
},
];
if (showAdvancedApplicationMetrics) {
tabData.push({
label: 'Connected instances',
component: <ConnectedInstances />,
});
}
if (loading) {
return (
<div>

View File

@ -144,7 +144,7 @@ exports[`returns all baseRoutes 1`] = `
"component": [Function],
"menu": {},
"parent": "/applications",
"path": "/applications/:name",
"path": "/applications/:name/*",
"title": ":name",
"type": "protected",
},

View File

@ -17,7 +17,6 @@ import EditTagType from 'component/tags/EditTagType/EditTagType';
import CreateTagType from 'component/tags/CreateTagType/CreateTagType';
import CreateFeature from 'component/feature/CreateFeature/CreateFeature';
import EditFeature from 'component/feature/EditFeature/EditFeature';
import { ApplicationEdit } from 'component/application/ApplicationEdit/ApplicationEdit';
import ContextList from 'component/context/ContextList/ContextList/ContextList';
import RedirectFeatureView from 'component/feature/RedirectFeatureView/RedirectFeatureView';
import { CreateIntegration } from 'component/integrations/CreateIntegration/CreateIntegration';
@ -47,6 +46,7 @@ import { ApplicationList } from '../application/ApplicationList/ApplicationList'
import { AddonRedirect } from 'component/integrations/AddonRedirect/AddonRedirect';
import { ExecutiveDashboard } from 'component/executiveDashboard/ExecutiveDashboard';
import { FeedbackList } from '../feedbackNew/FeedbackList';
import { Application } from 'component/application/Application';
export const routes: IRoute[] = [
// Splash
@ -163,10 +163,10 @@ export const routes: IRoute[] = [
// Applications
{
path: '/applications/:name',
path: '/applications/:name/*',
title: ':name',
parent: '/applications',
component: ApplicationEdit,
component: Application,
type: 'protected',
menu: {},
},