feat: introduce connections page in the admin panel

This commit is contained in:
Kasia
2023-11-23 11:13:58 +00:00
committed by Ali BARIN
parent 3801f9cfa0
commit 8f09681771
18 changed files with 851 additions and 9 deletions

View File

@@ -0,0 +1,168 @@
import type { IApp, IField, IJSONObject } from '@automatisch/types';
import LoadingButton from '@mui/lab/LoadingButton';
import Alert from '@mui/material/Alert';
import Dialog from '@mui/material/Dialog';
import DialogContent from '@mui/material/DialogContent';
import DialogContentText from '@mui/material/DialogContentText';
import DialogTitle from '@mui/material/DialogTitle';
import * as React from 'react';
import { FieldValues, SubmitHandler } from 'react-hook-form';
import { useNavigate, useSearchParams } from 'react-router-dom';
import AppAuthClientsDialog from 'components/AppAuthClientsDialog/index.ee';
import InputCreator from 'components/InputCreator';
import * as URLS from 'config/urls';
import useAuthenticateApp from 'hooks/useAuthenticateApp.ee';
import useFormatMessage from 'hooks/useFormatMessage';
import { generateExternalLink } from '../../helpers/translationValues';
import { Form } from './style';
type AdminApplicationConnectionCreateProps = {
onClose: (response: Record<string, unknown>) => void;
application: IApp;
connectionId?: string;
};
export default function AdminApplicationConnectionCreate(
props: AdminApplicationConnectionCreateProps
): React.ReactElement {
const { application, connectionId, onClose } = props;
const { name, authDocUrl, key, auth } = application;
const navigate = useNavigate();
const [searchParams] = useSearchParams();
const formatMessage = useFormatMessage();
const [error, setError] = React.useState<IJSONObject | null>(null);
const [inProgress, setInProgress] = React.useState(false);
const hasConnection = Boolean(connectionId);
const useShared = searchParams.get('shared') === 'true';
const appAuthClientId = searchParams.get('appAuthClientId') || undefined;
const { authenticate } = useAuthenticateApp({
appKey: key,
connectionId,
appAuthClientId,
useShared: !!appAuthClientId,
});
React.useEffect(function relayProviderData() {
if (window.opener) {
window.opener.postMessage({
source: 'automatisch',
payload: { search: window.location.search, hash: window.location.hash },
});
window.close();
}
}, []);
React.useEffect(
function initiateSharedAuthenticationForGivenAuthClient() {
if (!appAuthClientId) return;
if (!authenticate) return;
const asyncAuthenticate = async () => {
await authenticate();
navigate(URLS.ADMIN_APP_CONNECTIONS(key));
};
asyncAuthenticate();
},
[appAuthClientId, authenticate]
);
const handleClientClick = (appAuthClientId: string) =>
navigate(
URLS.ADMIN_APP_CONNECTIONS_CREATE_WITH_AUTH_CLIENT_ID(
key,
appAuthClientId
)
);
const handleAuthClientsDialogClose = () =>
navigate(URLS.ADMIN_APP_CONNECTIONS(key));
const submitHandler: SubmitHandler<FieldValues> = React.useCallback(
async (data) => {
if (!authenticate) return;
setInProgress(true);
try {
const response = await authenticate({
fields: data,
});
onClose(response as Record<string, unknown>);
} catch (err) {
const error = err as IJSONObject;
console.log(error);
setError((error.graphQLErrors as IJSONObject[])?.[0]);
} finally {
setInProgress(false);
}
},
[authenticate]
);
if (useShared)
return (
<AppAuthClientsDialog
appKey={key}
onClose={handleAuthClientsDialogClose}
onClientClick={handleClientClick}
/>
);
if (appAuthClientId) return <React.Fragment />;
return (
<Dialog open={true} onClose={onClose}>
<DialogTitle>
{hasConnection
? formatMessage('adminAppsConnections.reconnectConnection')
: formatMessage('adminAppsConnections.createConnection')}
</DialogTitle>
{authDocUrl && (
<Alert severity="info" sx={{ fontWeight: 300 }}>
{formatMessage('adminAppsConnections.callToDocs', {
appName: name,
docsLink: generateExternalLink(authDocUrl),
})}
</Alert>
)}
{error && (
<Alert
severity="error"
sx={{ mt: 1, fontWeight: 500, wordBreak: 'break-all' }}
>
{error.message}
{error.details && (
<pre style={{ whiteSpace: 'pre-wrap' }}>
{JSON.stringify(error.details, null, 2)}
</pre>
)}
</Alert>
)}
<DialogContent>
<DialogContentText tabIndex={-1} component="div">
<Form onSubmit={submitHandler}>
{auth?.fields?.map((field: IField) => (
<InputCreator key={field.key} schema={field} />
))}
<LoadingButton
type="submit"
variant="contained"
color="primary"
sx={{ boxShadow: 2 }}
loading={inProgress}
>
{formatMessage('adminAppsConnections.submit')}
</LoadingButton>
</Form>
</DialogContentText>
</DialogContent>
</Dialog>
);
}

View File

@@ -0,0 +1,9 @@
import { styled } from '@mui/material/styles';
import BaseForm from 'components/Form';
export const Form = styled(BaseForm)(({ theme }) => ({
display: 'flex',
flexDirection: 'column',
gap: theme.spacing(2),
paddingTop: theme.spacing(1),
}));

View File

@@ -0,0 +1,70 @@
import { useFieldArray, useFormContext } from 'react-hook-form';
import FormControlLabel from '@mui/material/FormControlLabel';
import Divider from '@mui/material/Divider';
import Checkbox from '@mui/material/Checkbox';
import useFormatMessage from 'hooks/useFormatMessage';
import ControlledCheckbox from 'components/ControlledCheckbox';
import { Stack } from '@mui/material';
type Roles = { id: string; name: string; checked: boolean }[];
function RolesFieldArray() {
const formatMessage = useFormatMessage();
const { control, watch, setValue } = useFormContext();
const fieldArrayData = useFieldArray({
control,
name: 'roles',
});
const fields = fieldArrayData.fields as Roles;
const watchedFields = watch('roles') as Roles;
const allFieldsSelected = watchedFields.every((field) => field.checked);
const allFieldsDeselected = watchedFields.every((field) => !field.checked);
const handleSelectAllClick = () => {
setValue(
'roles',
watchedFields.map((field) => ({ ...field, checked: !allFieldsSelected })),
{ shouldDirty: true }
);
};
return (
<Stack direction="column" spacing={1}>
<FormControlLabel
control={
<Checkbox
color="primary"
indeterminate={!(allFieldsSelected || allFieldsDeselected)}
checked={allFieldsSelected}
onChange={handleSelectAllClick}
/>
}
label={
allFieldsSelected
? formatMessage('adminAppsConnections.deselectAll')
: formatMessage('adminAppsConnections.selectAll')
}
sx={{ margin: 0 }}
/>
<Divider />
{fields.map((role, index) => {
return (
<FormControlLabel
key={role.id}
control={
<ControlledCheckbox
name={`roles.${index}.checked`}
defaultValue={role.checked}
/>
}
label={role.name}
/>
);
})}
</Stack>
);
}
export default RolesFieldArray;

View File

@@ -0,0 +1,140 @@
import * as React from 'react';
import { useParams } from 'react-router-dom';
import Dialog from '@mui/material/Dialog';
import DialogTitle from '@mui/material/DialogTitle';
import DialogContent from '@mui/material/DialogContent';
import DialogContentText from '@mui/material/DialogContentText';
import LoadingButton from '@mui/lab/LoadingButton';
import Stack from '@mui/material/Stack';
import Alert from '@mui/material/Alert';
import { CircularProgress } from '@mui/material';
import { useMutation } from '@apollo/client';
import { IApp, IRole } from '@automatisch/types';
import { FieldValues, SubmitHandler } from 'react-hook-form';
import { SHARE_CONNECTION } from 'graphql/mutations/share-connection';
import useFormatMessage from 'hooks/useFormatMessage';
import useSharedConnectionRoleIds from 'hooks/useSharedConnectionRoleIds';
import useRoles from 'hooks/useRoles.ee';
import RolesFieldArray from './RolesFieldArray';
import { Form } from './style';
type AdminApplicationConnectionShareProps = {
onClose: (response: Record<string, unknown>) => void;
application: IApp;
};
type Params = {
connectionId: string;
};
function generateRolesData(roles: IRole[], roleIds: string[]) {
return roles.map(({ id, name }) => ({
id,
name,
checked: roleIds.includes(id),
}));
}
export default function AdminApplicationConnectionShare(
props: AdminApplicationConnectionShareProps
): React.ReactElement {
const { onClose } = props;
const { connectionId } = useParams() as Params;
const formatMessage = useFormatMessage();
const [
shareConnection,
{ loading: loadingShareConnection, error: shareConnectionError },
] = useMutation(SHARE_CONNECTION, {
context: { autoSnackbar: false },
});
const {
roleIds,
loading: roleIdsLoading,
error: roleIdsError,
} = useSharedConnectionRoleIds(connectionId, {
context: { autoSnackbar: false },
});
const { roles, loading: rolesLoading, error: rolesError } = useRoles();
const error = shareConnectionError || roleIdsError || rolesError;
const showDialogContent =
!roleIdsLoading && !rolesLoading && !roleIdsError && !rolesError;
const submitHandler: SubmitHandler<FieldValues> = React.useCallback(
async (data) => {
const roles = data.roles as {
id: string;
name: string;
checked: boolean;
}[];
const response = await shareConnection({
variables: {
input: {
id: connectionId,
roleIds: roles
.filter((role) => role.checked)
.map((role) => role.id),
},
},
});
onClose(response as Record<string, unknown>);
},
[]
);
const defaultValues = React.useMemo(
() => ({
roles: generateRolesData(roles, roleIds),
}),
[roles, roleIds]
);
return (
<Dialog open={true} onClose={onClose}>
<DialogTitle>
{formatMessage('adminAppsConnections.shareConnection')}
</DialogTitle>
{error && (
<Alert
severity="error"
sx={{ mt: 1, fontWeight: 500, wordBreak: 'break-all' }}
>
{error.message}
</Alert>
)}
{(roleIdsLoading || rolesLoading) && (
<CircularProgress sx={{ display: 'block', margin: '20px auto' }} />
)}
{showDialogContent && (
<DialogContent sx={{ pt: '0px !important' }}>
<DialogContentText tabIndex={-1} component="div">
<Form
defaultValues={defaultValues}
onSubmit={submitHandler}
render={({ formState: { isDirty } }) => {
return (
<Stack direction="column">
<RolesFieldArray />
<LoadingButton
type="submit"
variant="contained"
color="primary"
sx={{ boxShadow: 2, mt: 5 }}
disabled={!isDirty}
loading={loadingShareConnection}
>
{formatMessage('adminAppsConnections.submit')}
</LoadingButton>
</Stack>
);
}}
></Form>
</DialogContentText>
</DialogContent>
)}
</Dialog>
);
}

View File

@@ -0,0 +1,9 @@
import { styled } from '@mui/material/styles';
import BaseForm from 'components/Form';
export const Form = styled(BaseForm)(({ theme }) => ({
display: 'flex',
flexDirection: 'column',
gap: theme.spacing(2),
paddingTop: theme.spacing(1),
}));

View File

@@ -0,0 +1,85 @@
import * as React from 'react';
import { Link } from 'react-router-dom';
import Menu from '@mui/material/Menu';
import type { PopoverProps } from '@mui/material/Popover';
import MenuItem from '@mui/material/MenuItem';
import type { IConnection } from '@automatisch/types';
import * as URLS from 'config/urls';
import useFormatMessage from 'hooks/useFormatMessage';
type Action = {
type: 'test' | 'reconnect' | 'delete' | 'shareConnection';
};
type ContextMenuProps = {
appKey: string;
connection: IConnection;
onClose: () => void;
onMenuItemClick: (event: React.MouseEvent, action: Action) => void;
anchorEl: PopoverProps['anchorEl'];
disableReconnection: boolean;
};
export default function ContextMenu(
props: ContextMenuProps
): React.ReactElement {
const {
appKey,
connection,
onClose,
onMenuItemClick,
anchorEl,
disableReconnection,
} = props;
const formatMessage = useFormatMessage();
const createActionHandler = React.useCallback(
(action: Action) => {
return function clickHandler(event: React.MouseEvent) {
onMenuItemClick(event, action);
onClose();
};
},
[onMenuItemClick, onClose]
);
return (
<Menu
open={true}
onClose={onClose}
hideBackdrop={false}
anchorEl={anchorEl}
>
<MenuItem onClick={createActionHandler({ type: 'test' })}>
{formatMessage('adminAppsConnections.testConnection')}
</MenuItem>
<MenuItem
component={Link}
disabled={disableReconnection}
to={URLS.ADMIN_APP_RECONNECT_CONNECTION(
appKey,
connection.id,
connection.appAuthClientId
)}
onClick={createActionHandler({ type: 'reconnect' })}
>
{formatMessage('adminAppsConnections.reconnect')}
</MenuItem>
<MenuItem
component={Link}
to={URLS.ADMIN_APP_SHARE_CONNECTION(appKey, connection.id)}
onClick={createActionHandler({ type: 'shareConnection' })}
>
{formatMessage('adminAppsConnections.shareConnection')}
</MenuItem>
<MenuItem onClick={createActionHandler({ type: 'delete' })}>
{formatMessage('adminAppsConnections.delete')}
</MenuItem>
</Menu>
);
}

View File

@@ -0,0 +1,155 @@
import type { IConnection } from '@automatisch/types';
import { useLazyQuery, useMutation } from '@apollo/client';
import CheckCircleIcon from '@mui/icons-material/CheckCircle';
import ErrorIcon from '@mui/icons-material/Error';
import MoreHorizIcon from '@mui/icons-material/MoreHoriz';
import Box from '@mui/material/Box';
import Card from '@mui/material/Card';
import CardActionArea from '@mui/material/CardActionArea';
import CircularProgress from '@mui/material/CircularProgress';
import Stack from '@mui/material/Stack';
import { DateTime } from 'luxon';
import * as React from 'react';
import { DELETE_CONNECTION } from 'graphql/mutations/delete-connection';
import { TEST_CONNECTION } from 'graphql/queries/test-connection';
import useEnqueueSnackbar from 'hooks/useEnqueueSnackbar';
import useFormatMessage from 'hooks/useFormatMessage';
import ConnectionContextMenu from '../AppConnectionContextMenu';
import { CardContent, Typography } from './style';
type AppConnectionRowProps = {
connection: IConnection;
};
function AppConnectionRow(props: AppConnectionRowProps): React.ReactElement {
const enqueueSnackbar = useEnqueueSnackbar();
const [verificationVisible, setVerificationVisible] = React.useState(false);
const [testConnection, { called: testCalled, loading: testLoading }] =
useLazyQuery(TEST_CONNECTION, {
fetchPolicy: 'network-only',
onCompleted: () => {
setTimeout(() => setVerificationVisible(false), 3000);
},
onError: () => {
setTimeout(() => setVerificationVisible(false), 3000);
},
});
const [deleteConnection] = useMutation(DELETE_CONNECTION);
const formatMessage = useFormatMessage();
const { id, key, formattedData, verified, createdAt, reconnectable, shared } =
props.connection;
const contextButtonRef = React.useRef<SVGSVGElement | null>(null);
const [anchorEl, setAnchorEl] = React.useState<SVGSVGElement | null>(null);
const handleClose = () => {
setAnchorEl(null);
};
const onContextMenuClick = () => setAnchorEl(contextButtonRef.current);
const onContextMenuAction = React.useCallback(
async (event, action: { [key: string]: string }) => {
if (action.type === 'delete') {
await deleteConnection({
variables: { input: { id } },
update: (cache) => {
const connectionCacheId = cache.identify({
__typename: 'Connection',
id,
});
cache.evict({
id: connectionCacheId,
});
},
});
enqueueSnackbar(formatMessage('adminAppsConnections.deletedMessage'), {
variant: 'success',
});
} else if (action.type === 'test') {
setVerificationVisible(true);
testConnection({ variables: { id } });
}
},
[deleteConnection, id, testConnection, formatMessage, enqueueSnackbar]
);
const relativeCreatedAt = DateTime.fromMillis(
parseInt(createdAt, 10)
).toRelative();
return (
<>
<Card sx={{ my: 2 }}>
<CardActionArea onClick={onContextMenuClick}>
<CardContent>
<Stack justifyContent="center" alignItems="flex-start" spacing={1}>
<Typography variant="h6" sx={{ textAlign: 'left' }}>
{formattedData?.screenName} {shared && 'shared'}
</Typography>
<Typography variant="caption">
{formatMessage('adminAppsConnections.addedAt', {
datetime: relativeCreatedAt,
})}
</Typography>
</Stack>
<Box>
<Stack direction="row" alignItems="center" spacing={1}>
{verificationVisible && testCalled && testLoading && (
<>
<CircularProgress size={16} />
<Typography variant="caption">
{formatMessage('adminAppsConnections.testing')}
</Typography>
</>
)}
{verificationVisible && testCalled && !testLoading && verified && (
<>
<CheckCircleIcon fontSize="small" color="success" />
<Typography variant="caption">
{formatMessage('adminAppsConnections.testSuccessful')}
</Typography>
</>
)}
{verificationVisible &&
testCalled &&
!testLoading &&
!verified && (
<>
<ErrorIcon fontSize="small" color="error" />
<Typography variant="caption">
{formatMessage('adminAppsConnections.testFailed')}
</Typography>
</>
)}
</Stack>
</Box>
<Box>
<MoreHorizIcon ref={contextButtonRef} />
</Box>
</CardContent>
</CardActionArea>
</Card>
{anchorEl && (
<ConnectionContextMenu
appKey={key}
connection={props.connection}
disableReconnection={!reconnectable}
onClose={handleClose}
onMenuItemClick={onContextMenuAction}
anchorEl={anchorEl}
/>
)}
</>
);
}
export default AppConnectionRow;

View File

@@ -0,0 +1,16 @@
import { styled } from '@mui/material/styles';
import MuiCardContent from '@mui/material/CardContent';
import MuiTypography from '@mui/material/Typography';
export const CardContent = styled(MuiCardContent)(({ theme }) => ({
display: 'grid',
gridTemplateRows: 'auto',
gridTemplateColumns: '1fr auto auto auto',
gridColumnGap: theme.spacing(2),
alignItems: 'center',
}));
export const Typography = styled(MuiTypography)(() => ({
textAlign: 'center',
display: 'inline-block',
}));

View File

@@ -0,0 +1,55 @@
import { Link } from 'react-router-dom';
import { useQuery } from '@apollo/client';
import CircularProgress from '@mui/material/CircularProgress';
import Stack from '@mui/material/Stack';
import Button from '@mui/material/Button';
import type { IConnection } from '@automatisch/types';
import { GET_APP_CONNECTIONS } from 'graphql/queries/get-app-connections';
import * as URLS from 'config/urls';
import useFormatMessage from 'hooks/useFormatMessage';
import NoResultFound from 'components/NoResultFound';
import AppConnectionRow from './AppConnectionRow';
type AdminApplicationConnectionsProps = { appKey: string };
function AdminApplicationConnections(
props: AdminApplicationConnectionsProps
): React.ReactElement {
const { appKey } = props;
const formatMessage = useFormatMessage();
const { data, loading } = useQuery(GET_APP_CONNECTIONS, {
variables: { key: appKey },
});
const appConnections: IConnection[] = data?.getApp?.connections || [];
if (loading)
return <CircularProgress sx={{ display: 'block', margin: '20px auto' }} />;
if (appConnections.length === 0) {
return (
<NoResultFound
to={URLS.ADMIN_APP_CONNECTIONS_CREATE(appKey)}
text={formatMessage('adminAppsConnections.noConnections')}
/>
);
}
return (
<div>
{appConnections.map((appConnection) => (
<AppConnectionRow key={appConnection.id} connection={appConnection} />
))}
<Stack justifyContent="flex-end" direction="row">
<Link to={URLS.ADMIN_APP_CONNECTIONS_CREATE(appKey)}>
<Button variant="contained" sx={{ mt: 2 }} component="div">
{formatMessage('adminAppsConnections.createConnection')}
</Button>
</Link>
</Stack>
</div>
);
}
export default AdminApplicationConnections;

View File

@@ -103,6 +103,13 @@ export const ADMIN_APP_AUTH_CLIENTS_PATTERN = `${ADMIN_SETTINGS}/apps/:appKey/au
export const ADMIN_APP_CONNECTIONS_PATTERN = `${ADMIN_SETTINGS}/apps/:appKey/connections`;
export const ADMIN_APP_CONNECTIONS = (appKey: string) =>
`${ADMIN_SETTINGS}/apps/${appKey}/connections`;
export const ADMIN_APP_CONNECTIONS_CREATE = (appKey: string, shared = false) =>
`${ADMIN_SETTINGS}/apps/${appKey}/connections/create?shared=${shared}`;
export const ADMIN_APP_CONNECTIONS_CREATE_WITH_AUTH_CLIENT_ID = (
appKey: string,
appAuthClientId: string
) =>
`${ADMIN_SETTINGS}/apps/${appKey}/connections/create?appAuthClientId=${appAuthClientId}`;
export const ADMIN_APP_SETTINGS = (appKey: string) =>
`${ADMIN_SETTINGS}/apps/${appKey}/settings`;
export const ADMIN_APP_AUTH_CLIENTS = (appKey: string) =>
@@ -111,6 +118,23 @@ export const ADMIN_APP_AUTH_CLIENT = (appKey: string, id: string) =>
`${ADMIN_SETTINGS}/apps/${appKey}/auth-clients/${id}`;
export const ADMIN_APP_AUTH_CLIENTS_CREATE = (appKey: string) =>
`${ADMIN_SETTINGS}/apps/${appKey}/auth-clients/create`;
export const ADMIN_APP_RECONNECT_CONNECTION = (
appKey: string,
connectionId: string,
appAuthClientId?: string
) => {
const path = `${ADMIN_SETTINGS}/apps/${appKey}/connections/${connectionId}/reconnect`;
if (appAuthClientId) {
return `${path}?appAuthClientId=${appAuthClientId}`;
}
return path;
};
export const ADMIN_APP_SHARE_CONNECTION = (
appKey: string,
connectionId: string
) => `${ADMIN_SETTINGS}/apps/${appKey}/connections/${connectionId}/share`;
export const DASHBOARD = FLOWS;

View File

@@ -0,0 +1,9 @@
import { gql } from '@apollo/client';
export const SHARE_CONNECTION = gql`
mutation ShareConnection($input: ShareConnectionInput) {
shareConnection(input: $input) {
id
}
}
`;

View File

@@ -0,0 +1,7 @@
import { gql } from '@apollo/client';
export const GET_SHARED_CONNECTION_ROLE_IDS = gql`
query GetSharedConnectionRoleIds($id: String!) {
getSharedConnectionRoleIds(id: $id)
}
`;

View File

@@ -5,13 +5,16 @@ import { GET_ROLES } from 'graphql/queries/get-roles.ee';
type QueryResponse = {
getRoles: IRole[];
}
};
export default function useRoles() {
const { data, loading } = useQuery<QueryResponse>(GET_ROLES, { context: { autoSnackbar: false } });
const { data, loading, error } = useQuery<QueryResponse>(GET_ROLES, {
context: { autoSnackbar: false },
});
return {
roles: data?.getRoles || [],
loading
loading,
error,
};
}

View File

@@ -0,0 +1,32 @@
import * as React from 'react';
import { LazyQueryHookOptions, useLazyQuery } from '@apollo/client';
import { GET_SHARED_CONNECTION_ROLE_IDS } from 'graphql/queries/get-shared-connection-role-ids';
type QueryResponse = {
getSharedConnectionRoleIds: string[];
};
export default function useSharedConnectionRoleIds(
connectionId: string,
options?: LazyQueryHookOptions
) {
const [getSharedConnectionRoleIds, { data, loading, error }] =
useLazyQuery<QueryResponse>(GET_SHARED_CONNECTION_ROLE_IDS, options);
React.useEffect(() => {
if (connectionId) {
getSharedConnectionRoleIds({
variables: {
id: connectionId,
},
});
}
}, [connectionId]);
return {
roleIds: data?.getSharedConnectionRoleIds || [],
loading,
error,
};
}

View File

@@ -265,5 +265,21 @@
"authClient.buttonSubmit": "Submit",
"authClient.inputName": "Name",
"authClient.inputActive": "Active",
"updateAuthClient.title": "Update auth client"
"updateAuthClient.title": "Update auth client",
"adminAppsConnections.noConnections": "You don't have any connections yet.",
"adminAppsConnections.createConnection": "Create connection",
"adminAppsConnections.deletedMessage": "The connection has been deleted.",
"adminAppsConnections.addedAt": "added {datetime}",
"adminAppsConnections.testing": "Testing...",
"adminAppsConnections.testSuccessful": "Test successful",
"adminAppsConnections.testFailed": "Test failed",
"adminAppsConnections.testConnection": "Test connection",
"adminAppsConnections.delete": "Delete",
"adminAppsConnections.reconnect": "Reconnect",
"adminAppsConnections.shareConnection": "Share connection",
"adminAppsConnections.reconnectConnection": "Reconnect connection",
"adminAppsConnections.callToDocs": "Visit <docsLink>our documentation</docsLink> to see how to add connection for {appName}.",
"adminAppsConnections.submit": "Submit",
"adminAppsConnections.selectAll": "Select all roles",
"adminAppsConnections.deselectAll": "Deselect all roles"
}

View File

@@ -27,9 +27,26 @@ import AdminApplicationSettings from 'components/AdminApplicationSettings';
import AdminApplicationAuthClients from 'components/AdminApplicationAuthClients';
import AdminApplicationCreateAuthClient from 'components/AdminApplicationCreateAuthClient';
import AdminApplicationUpdateAuthClient from 'components/AdminApplicationUpdateAuthClient';
import AdminApplicationConnections from 'components/AdminApplicationConnections';
import AdminApplicationConnectionCreate from 'components/AdminApplicationConnectionCreate';
import AdminApplicationConnectionShare from 'components/AdminApplicationConnectionShare';
type AdminApplicationParams = {
appKey: string;
connectionId?: string;
};
const ReconnectConnection = (props: any): React.ReactElement => {
const { application, onClose } = props;
const { connectionId } = useParams() as AdminApplicationParams;
return (
<AdminApplicationConnectionCreate
onClose={onClose}
application={application}
connectionId={connectionId}
/>
);
};
export default function AdminApplication(): React.ReactElement | null {
@@ -57,6 +74,7 @@ export default function AdminApplication(): React.ReactElement | null {
const app = data?.getApp || {};
const goToAuthClientsPage = () => navigate('auth-clients');
const goToConnectionsPage = () => navigate('connections');
if (loading) return null;
@@ -120,7 +138,7 @@ export default function AdminApplication(): React.ReactElement | null {
/>
<Route
path={`/connections/*`}
element={<div>App connections</div>}
element={<AdminApplicationConnections appKey={appKey} />}
/>
<Route
path="/"
@@ -153,6 +171,33 @@ export default function AdminApplication(): React.ReactElement | null {
/>
}
/>
<Route
path="/connections/create"
element={
<AdminApplicationConnectionCreate
onClose={goToConnectionsPage}
application={app}
/>
}
/>
<Route
path="/connections/:connectionId/reconnect"
element={
<ReconnectConnection
application={app}
onClose={goToConnectionsPage}
/>
}
/>
<Route
path="/connections/:connectionId/share"
element={
<AdminApplicationConnectionShare
onClose={goToConnectionsPage}
application={app}
/>
}
/>
</Routes>
</>
);

View File

@@ -65,8 +65,8 @@ function RoleMappings({ provider, providerLoading }: RoleMappingsProps) {
enqueueSnackbar(formatMessage('roleMappingsForm.successfullySaved'), {
variant: 'success',
SnackbarProps: {
'data-test': 'snackbar-update-role-mappings-success'
}
'data-test': 'snackbar-update-role-mappings-success',
},
});
}
} catch (error) {

View File

@@ -5,13 +5,12 @@ import Stack from '@mui/material/Stack';
import DeleteIcon from '@mui/icons-material/Delete';
import IconButton from '@mui/material/IconButton';
import Button from '@mui/material/Button';
import { Divider, Typography } from '@mui/material';
import useRoles from 'hooks/useRoles.ee';
import useFormatMessage from 'hooks/useFormatMessage';
import ControlledAutocomplete from 'components/ControlledAutocomplete';
import TextField from 'components/TextField';
import { Divider, Typography } from '@mui/material';
function generateRoleOptions(roles: IRole[]) {
return roles?.map(({ name: label, id: value }) => ({ label, value }));