Skip to content

Commit

Permalink
feat: add tabs (#6267)
Browse files Browse the repository at this point in the history
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">
  • Loading branch information
thomasheartman committed Feb 20, 2024
1 parent 7e6a3c7 commit d967d4a
Show file tree
Hide file tree
Showing 4 changed files with 242 additions and 13 deletions.
238 changes: 238 additions & 0 deletions frontend/src/component/application/Application.tsx
@@ -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>
</>
);
};
Expand Up @@ -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);
Expand Down Expand Up @@ -87,13 +85,6 @@ export const ApplicationEdit = () => {
},
];

if (showAdvancedApplicationMetrics) {
tabData.push({
label: 'Connected instances',
component: <ConnectedInstances />,
});
}

if (loading) {
return (
<div>
Expand Down
Expand Up @@ -144,7 +144,7 @@ exports[`returns all baseRoutes 1`] = `
"component": [Function],
"menu": {},
"parent": "/applications",
"path": "/applications/:name",
"path": "/applications/:name/*",
"title": ":name",
"type": "protected",
},
Expand Down
6 changes: 3 additions & 3 deletions frontend/src/component/menu/routes.ts
Expand Up @@ -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';
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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: {},
},
Expand Down

0 comments on commit d967d4a

Please sign in to comment.