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:
@@ -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 />}
|
||||||
|
@@ -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 (
|
||||||
|
@@ -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>
|
||||||
|
@@ -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;
|
||||||
|
|
||||||
|
@@ -18,6 +18,7 @@ export const GET_APPS = gql`
|
|||||||
authDocUrl
|
authDocUrl
|
||||||
primaryColor
|
primaryColor
|
||||||
connectionCount
|
connectionCount
|
||||||
|
flowCount
|
||||||
supportsConnections
|
supportsConnections
|
||||||
auth {
|
auth {
|
||||||
fields {
|
fields {
|
||||||
|
@@ -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"
|
||||||
}
|
}
|
||||||
|
63
packages/web/src/pages/AdminApplications/index.tsx
Normal file
63
packages/web/src/pages/AdminApplications/index.tsx
Normal 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;
|
@@ -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
|
||||||
|
Reference in New Issue
Block a user