mirror of
				https://github.com/Unleash/unleash.git
				synced 2025-10-27 11:02:16 +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:
		
							parent
							
								
									7e6a3c7e69
								
							
						
					
					
						commit
						d967d4adb0
					
				
							
								
								
									
										238
									
								
								frontend/src/component/application/Application.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										238
									
								
								frontend/src/component/application/Application.tsx
									
									
									
									
									
										Normal 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> | ||||||
|  |         </> | ||||||
|  |     ); | ||||||
|  | }; | ||||||
| @ -31,10 +31,8 @@ import { formatDateYMD } from 'utils/formatDate'; | |||||||
| import { formatUnknownError } from 'utils/formatUnknownError'; | import { formatUnknownError } from 'utils/formatUnknownError'; | ||||||
| import { useRequiredPathParam } from 'hooks/useRequiredPathParam'; | import { useRequiredPathParam } from 'hooks/useRequiredPathParam'; | ||||||
| import { TabPanel } from 'component/common/TabNav/TabPanel/TabPanel'; | import { TabPanel } from 'component/common/TabNav/TabPanel/TabPanel'; | ||||||
| import { useUiFlag } from 'hooks/useUiFlag'; |  | ||||||
| 
 | 
 | ||||||
| export const ApplicationEdit = () => { | export const ApplicationEdit = () => { | ||||||
|     const showAdvancedApplicationMetrics = useUiFlag('sdkReporting'); |  | ||||||
|     const navigate = useNavigate(); |     const navigate = useNavigate(); | ||||||
|     const name = useRequiredPathParam('name'); |     const name = useRequiredPathParam('name'); | ||||||
|     const { application, loading } = useApplication(name); |     const { application, loading } = useApplication(name); | ||||||
| @ -87,13 +85,6 @@ export const ApplicationEdit = () => { | |||||||
|         }, |         }, | ||||||
|     ]; |     ]; | ||||||
| 
 | 
 | ||||||
|     if (showAdvancedApplicationMetrics) { |  | ||||||
|         tabData.push({ |  | ||||||
|             label: 'Connected instances', |  | ||||||
|             component: <ConnectedInstances />, |  | ||||||
|         }); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     if (loading) { |     if (loading) { | ||||||
|         return ( |         return ( | ||||||
|             <div> |             <div> | ||||||
|  | |||||||
| @ -144,7 +144,7 @@ exports[`returns all baseRoutes 1`] = ` | |||||||
|     "component": [Function], |     "component": [Function], | ||||||
|     "menu": {}, |     "menu": {}, | ||||||
|     "parent": "/applications", |     "parent": "/applications", | ||||||
|     "path": "/applications/:name", |     "path": "/applications/:name/*", | ||||||
|     "title": ":name", |     "title": ":name", | ||||||
|     "type": "protected", |     "type": "protected", | ||||||
|   }, |   }, | ||||||
|  | |||||||
| @ -17,7 +17,6 @@ import EditTagType from 'component/tags/EditTagType/EditTagType'; | |||||||
| import CreateTagType from 'component/tags/CreateTagType/CreateTagType'; | import CreateTagType from 'component/tags/CreateTagType/CreateTagType'; | ||||||
| import CreateFeature from 'component/feature/CreateFeature/CreateFeature'; | import CreateFeature from 'component/feature/CreateFeature/CreateFeature'; | ||||||
| import EditFeature from 'component/feature/EditFeature/EditFeature'; | import EditFeature from 'component/feature/EditFeature/EditFeature'; | ||||||
| import { ApplicationEdit } from 'component/application/ApplicationEdit/ApplicationEdit'; |  | ||||||
| import ContextList from 'component/context/ContextList/ContextList/ContextList'; | import ContextList from 'component/context/ContextList/ContextList/ContextList'; | ||||||
| import RedirectFeatureView from 'component/feature/RedirectFeatureView/RedirectFeatureView'; | import RedirectFeatureView from 'component/feature/RedirectFeatureView/RedirectFeatureView'; | ||||||
| import { CreateIntegration } from 'component/integrations/CreateIntegration/CreateIntegration'; | 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 { AddonRedirect } from 'component/integrations/AddonRedirect/AddonRedirect'; | ||||||
| import { ExecutiveDashboard } from 'component/executiveDashboard/ExecutiveDashboard'; | import { ExecutiveDashboard } from 'component/executiveDashboard/ExecutiveDashboard'; | ||||||
| import { FeedbackList } from '../feedbackNew/FeedbackList'; | import { FeedbackList } from '../feedbackNew/FeedbackList'; | ||||||
|  | import { Application } from 'component/application/Application'; | ||||||
| 
 | 
 | ||||||
| export const routes: IRoute[] = [ | export const routes: IRoute[] = [ | ||||||
|     // Splash
 |     // Splash
 | ||||||
| @ -163,10 +163,10 @@ export const routes: IRoute[] = [ | |||||||
| 
 | 
 | ||||||
|     // Applications
 |     // Applications
 | ||||||
|     { |     { | ||||||
|         path: '/applications/:name', |         path: '/applications/:name/*', | ||||||
|         title: ':name', |         title: ':name', | ||||||
|         parent: '/applications', |         parent: '/applications', | ||||||
|         component: ApplicationEdit, |         component: Application, | ||||||
|         type: 'protected', |         type: 'protected', | ||||||
|         menu: {}, |         menu: {}, | ||||||
|     }, |     }, | ||||||
|  | |||||||
		Loading…
	
		Reference in New Issue
	
	Block a user