feat: introduce admin apps page (#1296)

* feat: introduce admin apps page

* feat: add access restriction and fix incorrectly placed key prop
This commit is contained in:
kattoczko
2023-10-05 09:55:00 +01:00
committed by GitHub
parent 82c1aadfa9
commit 584b9323ec
8 changed files with 102 additions and 7 deletions

View File

@@ -11,6 +11,7 @@ import UserInterface from 'pages/UserInterface';
import * as URLS from 'config/urls'; import * as URLS from 'config/urls';
import Can from 'components/Can'; import Can from 'components/Can';
import AdminApplications from 'pages/AdminApplications';
// TODO: consider introducing redirections to `/` as fallback // TODO: consider introducing redirections to `/` as fallback
export default ( export default (
@@ -107,6 +108,17 @@ export default (
} }
/> />
<Route
path={URLS.ADMIN_APPS}
element={
<Can I="update" a="App">
<AdminSettingsLayout>
<AdminApplications />
</AdminSettingsLayout>
</Can>
}
/>
<Route <Route
path={URLS.ADMIN_SETTINGS} path={URLS.ADMIN_SETTINGS}
element={<Navigate to={URLS.USERS} replace />} element={<Navigate to={URLS.USERS} replace />}

View File

@@ -3,6 +3,8 @@ import GroupIcon from '@mui/icons-material/Group';
import GroupsIcon from '@mui/icons-material/Groups'; import GroupsIcon from '@mui/icons-material/Groups';
import LockIcon from '@mui/icons-material/LockPerson'; import LockIcon from '@mui/icons-material/LockPerson';
import BrushIcon from '@mui/icons-material/Brush'; import BrushIcon from '@mui/icons-material/Brush';
import AppsIcon from '@mui/icons-material/Apps';
import Box from '@mui/material/Box'; import Box from '@mui/material/Box';
import Toolbar from '@mui/material/Toolbar'; import Toolbar from '@mui/material/Toolbar';
import { useTheme } from '@mui/material/styles'; import { useTheme } from '@mui/material/styles';
@@ -30,11 +32,13 @@ function createDrawerLinks({
canReadUser, canReadUser,
canUpdateConfig, canUpdateConfig,
canManageSamlAuthProvider, canManageSamlAuthProvider,
canUpdateApp,
}: { }: {
canReadRole: boolean; canReadRole: boolean;
canReadUser: boolean; canReadUser: boolean;
canUpdateConfig: boolean; canUpdateConfig: boolean;
canManageSamlAuthProvider: boolean; canManageSamlAuthProvider: boolean;
canUpdateApp: boolean;
}) { }) {
const items = [ const items = [
canReadUser canReadUser
@@ -69,6 +73,14 @@ function createDrawerLinks({
dataTest: 'authentication-drawer-link', dataTest: 'authentication-drawer-link',
} }
: null, : null,
canUpdateApp
? {
Icon: AppsIcon,
primary: 'adminSettingsDrawer.apps',
to: URLS.ADMIN_APPS,
dataTest: 'apps-drawer-link',
}
: null,
].filter(Boolean) as DrawerLink[]; ].filter(Boolean) as DrawerLink[];
return items; return items;
@@ -101,6 +113,7 @@ export default function SettingsLayout({
currentUserAbility.can('read', 'SamlAuthProvider') && currentUserAbility.can('read', 'SamlAuthProvider') &&
currentUserAbility.can('update', 'SamlAuthProvider') && currentUserAbility.can('update', 'SamlAuthProvider') &&
currentUserAbility.can('create', 'SamlAuthProvider'), currentUserAbility.can('create', 'SamlAuthProvider'),
canUpdateApp: currentUserAbility.can('update', 'App'),
}); });
return ( return (

View File

@@ -7,13 +7,13 @@ import ArrowForwardIosIcon from '@mui/icons-material/ArrowForwardIos';
import useFormatMessage from 'hooks/useFormatMessage'; import useFormatMessage from 'hooks/useFormatMessage';
import AppIcon from 'components/AppIcon'; import AppIcon from 'components/AppIcon';
import * as URLS from 'config/urls';
import type { IApp } from '@automatisch/types'; import type { IApp } from '@automatisch/types';
import { CardContent, Typography } from './style'; import { CardContent, Typography } from './style';
type AppRowProps = { type AppRowProps = {
application: IApp; application: IApp;
url: string;
}; };
const countTranslation = (value: React.ReactNode) => ( const countTranslation = (value: React.ReactNode) => (
@@ -25,11 +25,11 @@ const countTranslation = (value: React.ReactNode) => (
function AppRow(props: AppRowProps): React.ReactElement { function AppRow(props: AppRowProps): React.ReactElement {
const formatMessage = useFormatMessage(); const formatMessage = useFormatMessage();
const { name, key, primaryColor, iconUrl, connectionCount, flowCount } = const { name, primaryColor, iconUrl, connectionCount, flowCount } =
props.application; props.application;
return ( return (
<Link to={URLS.APP(key)} data-test="app-row"> <Link to={props.url} data-test="app-row">
<Card sx={{ mb: 1 }}> <Card sx={{ mb: 1 }}>
<CardActionArea> <CardActionArea>
<CardContent> <CardContent>
@@ -48,7 +48,7 @@ function AppRow(props: AppRowProps): React.ReactElement {
sx={{ display: ['none', 'inline-block'] }} sx={{ display: ['none', 'inline-block'] }}
> >
{formatMessage('app.connectionCount', { {formatMessage('app.connectionCount', {
count: countTranslation(connectionCount), count: countTranslation(connectionCount || '-'),
})} })}
</Typography> </Typography>
</Box> </Box>
@@ -60,7 +60,7 @@ function AppRow(props: AppRowProps): React.ReactElement {
sx={{ display: ['none', 'inline-block'] }} sx={{ display: ['none', 'inline-block'] }}
> >
{formatMessage('app.flowCount', { {formatMessage('app.flowCount', {
count: countTranslation(flowCount), count: countTranslation(flowCount || '-'),
})} })}
</Typography> </Typography>
</Box> </Box>

View File

@@ -95,6 +95,8 @@ export const ROLE_PATTERN = `${ROLES}/:roleId`;
export const CREATE_ROLE = `${ROLES}/create`; export const CREATE_ROLE = `${ROLES}/create`;
export const USER_INTERFACE = `${ADMIN_SETTINGS}/user-interface`; export const USER_INTERFACE = `${ADMIN_SETTINGS}/user-interface`;
export const AUTHENTICATION = `${ADMIN_SETTINGS}/authentication`; export const AUTHENTICATION = `${ADMIN_SETTINGS}/authentication`;
export const ADMIN_APPS = `${ADMIN_SETTINGS}/apps`;
export const ADMIN_APP = (appKey: string) => `${ADMIN_SETTINGS}/apps/${appKey}`;
export const DASHBOARD = FLOWS; export const DASHBOARD = FLOWS;

View File

@@ -18,6 +18,7 @@ export const GET_APPS = gql`
authDocUrl authDocUrl
primaryColor primaryColor
connectionCount connectionCount
flowCount
supportsConnections supportsConnections
auth { auth {
fields { fields {

View File

@@ -18,6 +18,7 @@
"adminSettingsDrawer.authentication": "Authentication", "adminSettingsDrawer.authentication": "Authentication",
"adminSettingsDrawer.userInterface": "User Interface", "adminSettingsDrawer.userInterface": "User Interface",
"adminSettingsDrawer.goBack": "Go to the dashboard", "adminSettingsDrawer.goBack": "Go to the dashboard",
"adminSettingsDrawer.apps": "Applications",
"app.connectionCount": "{count} connections", "app.connectionCount": "{count} connections",
"app.flowCount": "{count} flows", "app.flowCount": "{count} flows",
"app.addConnection": "Add connection", "app.addConnection": "Add connection",
@@ -246,5 +247,6 @@
"roleMappingsForm.appendRoleMapping": "Append", "roleMappingsForm.appendRoleMapping": "Append",
"roleMappingsForm.save": "Save", "roleMappingsForm.save": "Save",
"roleMappingsForm.notFound": "No role mappings have found.", "roleMappingsForm.notFound": "No role mappings have found.",
"roleMappingsForm.successfullySaved": "Role mappings have been saved." "roleMappingsForm.successfullySaved": "Role mappings have been saved.",
"adminApps.title": "Apps"
} }

View File

@@ -0,0 +1,63 @@
import * as React from 'react';
import Grid from '@mui/material/Grid';
import CircularProgress from '@mui/material/CircularProgress';
import Divider from '@mui/material/Divider';
import { useQuery } from '@apollo/client';
import { IApp } from '@automatisch/types';
import PageTitle from 'components/PageTitle';
import Container from 'components/Container';
import SearchInput from 'components/SearchInput';
import AppRow from 'components/AppRow';
import * as URLS from 'config/urls';
import useFormatMessage from 'hooks/useFormatMessage';
import { GET_APPS } from 'graphql/queries/get-apps';
function AdminApplications() {
const formatMessage = useFormatMessage();
const [appName, setAppName] = React.useState(null);
const { data, loading: appsLoading } = useQuery(GET_APPS, {
variables: { name: appName },
});
const apps = data?.getApps;
const onSearchChange = React.useCallback((event) => {
setAppName(event.target.value);
}, []);
return (
<Container sx={{ py: 3, display: 'flex', justifyContent: 'center' }}>
<Grid container item xs={12} sm={10} md={9}>
<Grid container sx={{ mb: [0, 3] }} columnSpacing={1.5} rowSpacing={3}>
<Grid container item xs sm alignItems="center" order={{ xs: 0 }}>
<PageTitle>{formatMessage('adminApps.title')}</PageTitle>
</Grid>
<Grid item xs={12} sm="auto" order={{ xs: 2, sm: 1 }}>
<SearchInput onChange={onSearchChange} />
</Grid>
</Grid>
<Grid item xs={12}>
<Divider sx={{ mt: [2, 0], mb: 2 }} />
</Grid>
{appsLoading && (
<CircularProgress
data-test="apps-loader"
sx={{ display: 'block', margin: '20px auto' }}
/>
)}
{!appsLoading &&
apps?.map((app: IApp) => (
<Grid item xs={12} key={app.name}>
<AppRow application={app} url={URLS.ADMIN_APP(app.key)} />
</Grid>
))}
</Grid>
</Container>
);
}
export default AdminApplications;

View File

@@ -97,7 +97,9 @@ export default function Applications(): React.ReactElement {
)} )}
{!loading && {!loading &&
apps?.map((app: IApp) => <AppRow key={app.name} application={app} />)} apps?.map((app: IApp) => (
<AppRow key={app.name} application={app} url={URLS.APP(app.key)} />
))}
<Routes> <Routes>
<Route <Route