diff --git a/packages/web/src/components/AdminApplicationConnectionCreate/index.tsx b/packages/web/src/components/AdminApplicationConnectionCreate/index.tsx new file mode 100644 index 00000000..43452686 --- /dev/null +++ b/packages/web/src/components/AdminApplicationConnectionCreate/index.tsx @@ -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) => 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(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 = React.useCallback( + async (data) => { + if (!authenticate) return; + + setInProgress(true); + + try { + const response = await authenticate({ + fields: data, + }); + onClose(response as Record); + } catch (err) { + const error = err as IJSONObject; + console.log(error); + setError((error.graphQLErrors as IJSONObject[])?.[0]); + } finally { + setInProgress(false); + } + }, + [authenticate] + ); + + if (useShared) + return ( + + ); + + if (appAuthClientId) return ; + + return ( + + + {hasConnection + ? formatMessage('adminAppsConnections.reconnectConnection') + : formatMessage('adminAppsConnections.createConnection')} + + + {authDocUrl && ( + + {formatMessage('adminAppsConnections.callToDocs', { + appName: name, + docsLink: generateExternalLink(authDocUrl), + })} + + )} + + {error && ( + + {error.message} + {error.details && ( +
+              {JSON.stringify(error.details, null, 2)}
+            
+ )} +
+ )} + + + +
+ {auth?.fields?.map((field: IField) => ( + + ))} + + + {formatMessage('adminAppsConnections.submit')} + + +
+
+
+ ); +} diff --git a/packages/web/src/components/AdminApplicationConnectionCreate/style.ts b/packages/web/src/components/AdminApplicationConnectionCreate/style.ts new file mode 100644 index 00000000..411c8683 --- /dev/null +++ b/packages/web/src/components/AdminApplicationConnectionCreate/style.ts @@ -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), +})); diff --git a/packages/web/src/components/AdminApplicationConnectionShare/RolesFieldArray/index.tsx b/packages/web/src/components/AdminApplicationConnectionShare/RolesFieldArray/index.tsx new file mode 100644 index 00000000..800d5521 --- /dev/null +++ b/packages/web/src/components/AdminApplicationConnectionShare/RolesFieldArray/index.tsx @@ -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 ( + + + } + label={ + allFieldsSelected + ? formatMessage('adminAppsConnections.deselectAll') + : formatMessage('adminAppsConnections.selectAll') + } + sx={{ margin: 0 }} + /> + + {fields.map((role, index) => { + return ( + + } + label={role.name} + /> + ); + })} + + ); +} + +export default RolesFieldArray; diff --git a/packages/web/src/components/AdminApplicationConnectionShare/index.tsx b/packages/web/src/components/AdminApplicationConnectionShare/index.tsx new file mode 100644 index 00000000..8635c65f --- /dev/null +++ b/packages/web/src/components/AdminApplicationConnectionShare/index.tsx @@ -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) => 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 = 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); + }, + [] + ); + + const defaultValues = React.useMemo( + () => ({ + roles: generateRolesData(roles, roleIds), + }), + [roles, roleIds] + ); + + return ( + + + {formatMessage('adminAppsConnections.shareConnection')} + + {error && ( + + {error.message} + + )} + {(roleIdsLoading || rolesLoading) && ( + + )} + {showDialogContent && ( + + +
{ + return ( + + + + {formatMessage('adminAppsConnections.submit')} + + + ); + }} + >
+
+
+ )} +
+ ); +} diff --git a/packages/web/src/components/AdminApplicationConnectionShare/style.ts b/packages/web/src/components/AdminApplicationConnectionShare/style.ts new file mode 100644 index 00000000..411c8683 --- /dev/null +++ b/packages/web/src/components/AdminApplicationConnectionShare/style.ts @@ -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), +})); diff --git a/packages/web/src/components/AdminApplicationConnections/AppConnectionContextMenu/index.tsx b/packages/web/src/components/AdminApplicationConnections/AppConnectionContextMenu/index.tsx new file mode 100644 index 00000000..666cbbfc --- /dev/null +++ b/packages/web/src/components/AdminApplicationConnections/AppConnectionContextMenu/index.tsx @@ -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 ( + + + {formatMessage('adminAppsConnections.testConnection')} + + + + {formatMessage('adminAppsConnections.reconnect')} + + + + {formatMessage('adminAppsConnections.shareConnection')} + + + + {formatMessage('adminAppsConnections.delete')} + + + ); +} diff --git a/packages/web/src/components/AdminApplicationConnections/AppConnectionRow/index.tsx b/packages/web/src/components/AdminApplicationConnections/AppConnectionRow/index.tsx new file mode 100644 index 00000000..913eca11 --- /dev/null +++ b/packages/web/src/components/AdminApplicationConnections/AppConnectionRow/index.tsx @@ -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(null); + const [anchorEl, setAnchorEl] = React.useState(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 ( + <> + + + + + + {formattedData?.screenName} {shared && 'shared'} + + + + {formatMessage('adminAppsConnections.addedAt', { + datetime: relativeCreatedAt, + })} + + + + + + {verificationVisible && testCalled && testLoading && ( + <> + + + {formatMessage('adminAppsConnections.testing')} + + + )} + {verificationVisible && testCalled && !testLoading && verified && ( + <> + + + {formatMessage('adminAppsConnections.testSuccessful')} + + + )} + {verificationVisible && + testCalled && + !testLoading && + !verified && ( + <> + + + {formatMessage('adminAppsConnections.testFailed')} + + + )} + + + + + + + + + + + {anchorEl && ( + + )} + + ); +} + +export default AppConnectionRow; diff --git a/packages/web/src/components/AdminApplicationConnections/AppConnectionRow/style.ts b/packages/web/src/components/AdminApplicationConnections/AppConnectionRow/style.ts new file mode 100644 index 00000000..9bbc429b --- /dev/null +++ b/packages/web/src/components/AdminApplicationConnections/AppConnectionRow/style.ts @@ -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', +})); diff --git a/packages/web/src/components/AdminApplicationConnections/index.tsx b/packages/web/src/components/AdminApplicationConnections/index.tsx new file mode 100644 index 00000000..e50cbc86 --- /dev/null +++ b/packages/web/src/components/AdminApplicationConnections/index.tsx @@ -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 ; + + if (appConnections.length === 0) { + return ( + + ); + } + + return ( +
+ {appConnections.map((appConnection) => ( + + ))} + + + + + +
+ ); +} + +export default AdminApplicationConnections; diff --git a/packages/web/src/config/urls.ts b/packages/web/src/config/urls.ts index 5d6affd5..4808d822 100644 --- a/packages/web/src/config/urls.ts +++ b/packages/web/src/config/urls.ts @@ -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; diff --git a/packages/web/src/graphql/mutations/share-connection.ts b/packages/web/src/graphql/mutations/share-connection.ts new file mode 100644 index 00000000..966b51b2 --- /dev/null +++ b/packages/web/src/graphql/mutations/share-connection.ts @@ -0,0 +1,9 @@ +import { gql } from '@apollo/client'; + +export const SHARE_CONNECTION = gql` + mutation ShareConnection($input: ShareConnectionInput) { + shareConnection(input: $input) { + id + } + } +`; diff --git a/packages/web/src/graphql/queries/get-shared-connection-role-ids.ts b/packages/web/src/graphql/queries/get-shared-connection-role-ids.ts new file mode 100644 index 00000000..968c7729 --- /dev/null +++ b/packages/web/src/graphql/queries/get-shared-connection-role-ids.ts @@ -0,0 +1,7 @@ +import { gql } from '@apollo/client'; + +export const GET_SHARED_CONNECTION_ROLE_IDS = gql` + query GetSharedConnectionRoleIds($id: String!) { + getSharedConnectionRoleIds(id: $id) + } +`; diff --git a/packages/web/src/hooks/useRoles.ee.ts b/packages/web/src/hooks/useRoles.ee.ts index 34549191..6307ed74 100644 --- a/packages/web/src/hooks/useRoles.ee.ts +++ b/packages/web/src/hooks/useRoles.ee.ts @@ -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(GET_ROLES, { context: { autoSnackbar: false } }); + const { data, loading, error } = useQuery(GET_ROLES, { + context: { autoSnackbar: false }, + }); return { roles: data?.getRoles || [], - loading + loading, + error, }; } diff --git a/packages/web/src/hooks/useSharedConnectionRoleIds.ts b/packages/web/src/hooks/useSharedConnectionRoleIds.ts new file mode 100644 index 00000000..c2ed737f --- /dev/null +++ b/packages/web/src/hooks/useSharedConnectionRoleIds.ts @@ -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(GET_SHARED_CONNECTION_ROLE_IDS, options); + + React.useEffect(() => { + if (connectionId) { + getSharedConnectionRoleIds({ + variables: { + id: connectionId, + }, + }); + } + }, [connectionId]); + + return { + roleIds: data?.getSharedConnectionRoleIds || [], + loading, + error, + }; +} diff --git a/packages/web/src/locales/en.json b/packages/web/src/locales/en.json index 2bd2c598..9f74419e 100644 --- a/packages/web/src/locales/en.json +++ b/packages/web/src/locales/en.json @@ -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 our documentation to see how to add connection for {appName}.", + "adminAppsConnections.submit": "Submit", + "adminAppsConnections.selectAll": "Select all roles", + "adminAppsConnections.deselectAll": "Deselect all roles" } diff --git a/packages/web/src/pages/AdminApplication/index.tsx b/packages/web/src/pages/AdminApplication/index.tsx index 19c6fa1a..2d7c7287 100644 --- a/packages/web/src/pages/AdminApplication/index.tsx +++ b/packages/web/src/pages/AdminApplication/index.tsx @@ -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 ( + + ); }; 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 { /> App connections} + element={} /> } /> + + } + /> + + } + /> + + } + /> ); diff --git a/packages/web/src/pages/Authentication/RoleMappings.tsx b/packages/web/src/pages/Authentication/RoleMappings.tsx index f93bccfd..5cb82c10 100644 --- a/packages/web/src/pages/Authentication/RoleMappings.tsx +++ b/packages/web/src/pages/Authentication/RoleMappings.tsx @@ -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) { diff --git a/packages/web/src/pages/Authentication/RoleMappingsFieldsArray.tsx b/packages/web/src/pages/Authentication/RoleMappingsFieldsArray.tsx index 60d0eab2..61ef9dee 100644 --- a/packages/web/src/pages/Authentication/RoleMappingsFieldsArray.tsx +++ b/packages/web/src/pages/Authentication/RoleMappingsFieldsArray.tsx @@ -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 }));