diff --git a/packages/types/index.d.ts b/packages/types/index.d.ts index 2e4e97d7..3adc8468 100644 --- a/packages/types/index.d.ts +++ b/packages/types/index.d.ts @@ -462,6 +462,7 @@ type AppAuthClient = { appConfigId: string; authDefaults: string; formattedAuthDefaults: IJSONObject; + active: boolean; }; type Notification = { diff --git a/packages/web/src/components/AdminApplicationAuthClientDialog/index.tsx b/packages/web/src/components/AdminApplicationAuthClientDialog/index.tsx new file mode 100644 index 00000000..6271bc5b --- /dev/null +++ b/packages/web/src/components/AdminApplicationAuthClientDialog/index.tsx @@ -0,0 +1,104 @@ +import React from 'react'; +import type { IField } 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 CircularProgress from '@mui/material/CircularProgress'; +import { FieldValues, SubmitHandler } from 'react-hook-form'; +import type { UseFormProps } from 'react-hook-form'; +import type { ApolloError } from '@apollo/client'; + +import useFormatMessage from 'hooks/useFormatMessage'; +import InputCreator from 'components/InputCreator'; +import Switch from 'components/Switch'; +import TextField from 'components/TextField'; + +import { Form } from './style'; + +type AdminApplicationAuthClientDialogProps = { + title: string; + authFields?: IField[]; + defaultValues: UseFormProps['defaultValues']; + loading: boolean; + submitting: boolean; + disabled?: boolean; + error?: ApolloError; + submitHandler: SubmitHandler; + onClose: () => void; +}; + +export default function AdminApplicationAuthClientDialog( + props: AdminApplicationAuthClientDialogProps +): React.ReactElement { + const { + error, + onClose, + title, + loading, + submitHandler, + authFields, + submitting, + defaultValues, + disabled = false, + } = props; + const formatMessage = useFormatMessage(); + + return ( + + {title} + {error && ( + + {error.message} + + )} + + {loading ? ( + + ) : ( + +
( + <> + + + {authFields?.map((field: IField) => ( + + ))} + + {formatMessage('authClient.buttonSubmit')} + + + )} + > +
+ )} +
+
+ ); +} diff --git a/packages/web/src/components/AdminApplicationAuthClientDialog/style.ts b/packages/web/src/components/AdminApplicationAuthClientDialog/style.ts new file mode 100644 index 00000000..411c8683 --- /dev/null +++ b/packages/web/src/components/AdminApplicationAuthClientDialog/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/AdminApplicationAuthClients/index.tsx b/packages/web/src/components/AdminApplicationAuthClients/index.tsx new file mode 100644 index 00000000..cfa5eadb --- /dev/null +++ b/packages/web/src/components/AdminApplicationAuthClients/index.tsx @@ -0,0 +1,89 @@ +import { Link } from 'react-router-dom'; +import CircularProgress from '@mui/material/CircularProgress'; +import Stack from '@mui/material/Stack'; +import Card from '@mui/material/Card'; +import CardActionArea from '@mui/material/CardActionArea'; +import CardContent from '@mui/material/CardContent'; +import Typography from '@mui/material/Typography'; +import Chip from '@mui/material/Chip'; +import Button from '@mui/material/Button'; + +import * as URLS from 'config/urls'; +import useFormatMessage from 'hooks/useFormatMessage'; +import useAppAuthClients from 'hooks/useAppAuthClients.ee'; + +import NoResultFound from 'components/NoResultFound'; + +type AdminApplicationAuthClientsProps = { + appKey: string; +}; + +function AdminApplicationAuthClients( + props: AdminApplicationAuthClientsProps +): React.ReactElement { + const { appKey } = props; + const formatMessage = useFormatMessage(); + const { appAuthClients, loading } = useAppAuthClients({ appKey }); + + if (loading) + return ; + + if (!appAuthClients?.length) { + return ( + + ); + } + + const sortedAuthClients = appAuthClients.slice().sort((a, b) => { + if (a.id < b.id) { + return -1; + } + if (a.id > b.id) { + return 1; + } + return 0; + }); + + return ( +
+ {sortedAuthClients.map((client) => ( + + + + + + {client.name} + + + + + + + ))} + + + + + +
+ ); +} + +export default AdminApplicationAuthClients; diff --git a/packages/web/src/components/AdminApplicationCreateAuthClient/index.tsx b/packages/web/src/components/AdminApplicationCreateAuthClient/index.tsx new file mode 100644 index 00000000..37ba56cf --- /dev/null +++ b/packages/web/src/components/AdminApplicationCreateAuthClient/index.tsx @@ -0,0 +1,112 @@ +import React, { useCallback, useMemo } from 'react'; +import type { IApp } from '@automatisch/types'; +import { FieldValues, SubmitHandler } from 'react-hook-form'; +import { useMutation } from '@apollo/client'; +import { CREATE_APP_CONFIG } from 'graphql/mutations/create-app-config'; +import { CREATE_APP_AUTH_CLIENT } from 'graphql/mutations/create-app-auth-client'; + +import useAppConfig from 'hooks/useAppConfig.ee'; +import useFormatMessage from 'hooks/useFormatMessage'; + +import AdminApplicationAuthClientDialog from 'components/AdminApplicationAuthClientDialog'; + +type AdminApplicationCreateAuthClientProps = { + appKey: string; + application: IApp; + onClose: () => void; +}; + +export default function AdminApplicationCreateAuthClient( + props: AdminApplicationCreateAuthClientProps +): React.ReactElement { + const { appKey, application, onClose } = props; + const { auth } = application; + const formatMessage = useFormatMessage(); + const { appConfig, loading: loadingAppConfig } = useAppConfig(appKey); + const [ + createAppConfig, + { loading: loadingCreateAppConfig, error: createAppConfigError }, + ] = useMutation(CREATE_APP_CONFIG, { + refetchQueries: ['GetAppConfig'], + context: { autoSnackbar: false }, + }); + const [ + createAppAuthClient, + { loading: loadingCreateAppAuthClient, error: createAppAuthClientError }, + ] = useMutation(CREATE_APP_AUTH_CLIENT, { + refetchQueries: ['GetAppAuthClients'], + context: { autoSnackbar: false }, + }); + + const submitHandler: SubmitHandler = async (values) => { + let appConfigId = appConfig?.id; + + if (!appConfigId) { + const { data: appConfigData } = await createAppConfig({ + variables: { + input: { + key: appKey, + allowCustomConnection: false, + shared: false, + disabled: false, + }, + }, + }); + appConfigId = appConfigData.createAppConfig.id; + } + + const { name, active, ...formattedAuthDefaults } = values; + + await createAppAuthClient({ + variables: { + input: { + appConfigId, + name, + active, + formattedAuthDefaults, + }, + }, + }); + + onClose(); + }; + + const getAuthFieldsDefaultValues = useCallback(() => { + if (!auth?.fields) { + return {}; + } + const defaultValues: { + [key: string]: any; + } = {}; + auth.fields.forEach((field) => { + if (field.value || field.type !== 'string') { + defaultValues[field.key] = field.value; + } else if (field.type === 'string') { + defaultValues[field.key] = ''; + } + }); + return defaultValues; + }, [auth?.fields]); + + const defaultValues = useMemo( + () => ({ + name: '', + active: false, + ...getAuthFieldsDefaultValues(), + }), + [getAuthFieldsDefaultValues] + ); + + return ( + + ); +} diff --git a/packages/web/src/components/AdminApplicationUpdateAuthClient/index.tsx b/packages/web/src/components/AdminApplicationUpdateAuthClient/index.tsx new file mode 100644 index 00000000..3897ea82 --- /dev/null +++ b/packages/web/src/components/AdminApplicationUpdateAuthClient/index.tsx @@ -0,0 +1,95 @@ +import React, { useCallback, useMemo } from 'react'; +import { useParams } from 'react-router-dom'; +import type { IApp } from '@automatisch/types'; +import { FieldValues, SubmitHandler } from 'react-hook-form'; +import { useMutation } from '@apollo/client'; +import { UPDATE_APP_AUTH_CLIENT } from 'graphql/mutations/update-app-auth-client'; + +import useAppAuthClient from 'hooks/useAppAuthClient.ee'; +import useFormatMessage from 'hooks/useFormatMessage'; +import AdminApplicationAuthClientDialog from 'components/AdminApplicationAuthClientDialog'; + +type AdminApplicationUpdateAuthClientProps = { + application: IApp; + onClose: () => void; +}; + +export default function AdminApplicationUpdateAuthClient( + props: AdminApplicationUpdateAuthClientProps +): React.ReactElement { + const { application, onClose } = props; + const { auth } = application; + const authFields = auth?.fields?.map((field) => ({ + ...field, + required: false, + })); + + const formatMessage = useFormatMessage(); + + const { clientId } = useParams(); + const { appAuthClient, loading: loadingAuthClient } = + useAppAuthClient(clientId); + const [updateAppAuthClient, { loading: loadingUpdateAppAuthClient, error }] = + useMutation(UPDATE_APP_AUTH_CLIENT, { + refetchQueries: ['GetAppAuthClients'], + context: { autoSnackbar: false }, + }); + + const submitHandler: SubmitHandler = async (values) => { + if (!appAuthClient) { + return; + } + const { name, active, ...formattedAuthDefaults } = values; + await updateAppAuthClient({ + variables: { + input: { + id: appAuthClient.id, + name, + active, + formattedAuthDefaults, + }, + }, + }); + onClose(); + }; + + const getAuthFieldsDefaultValues = useCallback(() => { + if (!authFields) { + return {}; + } + const defaultValues: { + [key: string]: any; + } = {}; + authFields.forEach((field) => { + if (field.value || field.type !== 'string') { + defaultValues[field.key] = field.value; + } else if (field.type === 'string') { + defaultValues[field.key] = ''; + } + }); + return defaultValues; + }, [auth?.fields]); + + const defaultValues = useMemo( + () => ({ + name: appAuthClient?.name || '', + active: appAuthClient?.active || false, + ...getAuthFieldsDefaultValues(), + }), + [appAuthClient, getAuthFieldsDefaultValues] + ); + + return ( + + ); +} diff --git a/packages/web/src/components/AppAuthClientsDialog/index.ee.tsx b/packages/web/src/components/AppAuthClientsDialog/index.ee.tsx index 511feea7..ff044f79 100644 --- a/packages/web/src/components/AppAuthClientsDialog/index.ee.tsx +++ b/packages/web/src/components/AppAuthClientsDialog/index.ee.tsx @@ -17,7 +17,7 @@ type AppAuthClientsDialogProps = { export default function AppAuthClientsDialog(props: AppAuthClientsDialogProps) { const { appKey, onClientClick, onClose } = props; - const { appAuthClients } = useAppAuthClients(appKey); + const { appAuthClients } = useAppAuthClients({ appKey, active: true }); const formatMessage = useFormatMessage(); React.useEffect( diff --git a/packages/web/src/config/urls.ts b/packages/web/src/config/urls.ts index bc9f4486..5d6affd5 100644 --- a/packages/web/src/config/urls.ts +++ b/packages/web/src/config/urls.ts @@ -107,6 +107,10 @@ export const ADMIN_APP_SETTINGS = (appKey: string) => `${ADMIN_SETTINGS}/apps/${appKey}/settings`; export const ADMIN_APP_AUTH_CLIENTS = (appKey: string) => `${ADMIN_SETTINGS}/apps/${appKey}/auth-clients`; +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 DASHBOARD = FLOWS; diff --git a/packages/web/src/graphql/mutations/create-app-auth-client.ts b/packages/web/src/graphql/mutations/create-app-auth-client.ts new file mode 100644 index 00000000..1703f075 --- /dev/null +++ b/packages/web/src/graphql/mutations/create-app-auth-client.ts @@ -0,0 +1,12 @@ +import { gql } from '@apollo/client'; + +export const CREATE_APP_AUTH_CLIENT = gql` + mutation CreateAppAuthClient($input: CreateAppAuthClientInput) { + createAppAuthClient(input: $input) { + id + appConfigId + name + active + } + } +`; diff --git a/packages/web/src/graphql/mutations/update-app-auth-client.ts b/packages/web/src/graphql/mutations/update-app-auth-client.ts new file mode 100644 index 00000000..6742bee3 --- /dev/null +++ b/packages/web/src/graphql/mutations/update-app-auth-client.ts @@ -0,0 +1,12 @@ +import { gql } from '@apollo/client'; + +export const UPDATE_APP_AUTH_CLIENT = gql` + mutation UpdateAppAuthClient($input: UpdateAppAuthClientInput) { + updateAppAuthClient(input: $input) { + id + appConfigId + name + active + } + } +`; diff --git a/packages/web/src/hooks/useAppAuthClient.ee.ts b/packages/web/src/hooks/useAppAuthClient.ee.ts index 32a45b05..8ff4d8b2 100644 --- a/packages/web/src/hooks/useAppAuthClient.ee.ts +++ b/packages/web/src/hooks/useAppAuthClient.ee.ts @@ -1,28 +1,26 @@ import { useLazyQuery } from '@apollo/client'; -import { AppConfig } from '@automatisch/types'; +import { AppAuthClient } from '@automatisch/types'; import * as React from 'react'; import { GET_APP_AUTH_CLIENT } from 'graphql/queries/get-app-auth-client.ee'; type QueryResponse = { - getAppAuthClient: AppConfig; -} + getAppAuthClient: AppAuthClient; +}; -export default function useAppAuthClient(id: string) { - const [ - getAppAuthClient, - { - data, - loading - } - ] = useLazyQuery(GET_APP_AUTH_CLIENT); +export default function useAppAuthClient(id?: string) { + const [getAppAuthClient, { data, loading }] = + useLazyQuery(GET_APP_AUTH_CLIENT); const appAuthClient = data?.getAppAuthClient; - React.useEffect(function fetchUponId() { - if (!id) return; + React.useEffect( + function fetchUponId() { + if (!id) return; - getAppAuthClient({ variables: { id } }); - }, [id]); + getAppAuthClient({ variables: { id } }); + }, + [id] + ); return { appAuthClient, diff --git a/packages/web/src/hooks/useAppAuthClients.ee.ts b/packages/web/src/hooks/useAppAuthClients.ee.ts index 411a2781..d2b9d178 100644 --- a/packages/web/src/hooks/useAppAuthClients.ee.ts +++ b/packages/web/src/hooks/useAppAuthClients.ee.ts @@ -6,25 +6,33 @@ import { GET_APP_AUTH_CLIENTS } from 'graphql/queries/get-app-auth-clients.ee'; type QueryResponse = { getAppAuthClients: AppAuthClient[]; -} +}; -export default function useAppAuthClient(appKey: string) { - const [ - getAppAuthClients, +export default function useAppAuthClient({ + appKey, + active, +}: { + appKey: string; + active?: boolean; +}) { + const [getAppAuthClients, { data, loading }] = useLazyQuery( + GET_APP_AUTH_CLIENTS, { - data, - loading + context: { autoSnackbar: false }, } - ] = useLazyQuery(GET_APP_AUTH_CLIENTS, { - context: { autoSnackbar: false }, - }); + ); const appAuthClients = data?.getAppAuthClients; - React.useEffect(function fetchUponAppKey() { - if (!appKey) return; + React.useEffect( + function fetchUponAppKey() { + if (!appKey) return; - getAppAuthClients({ variables: { appKey, active: true } }); - }, [appKey]); + getAppAuthClients({ + variables: { appKey, ...(typeof active === 'boolean' && { active }) }, + }); + }, + [appKey] + ); return { appAuthClients, diff --git a/packages/web/src/locales/en.json b/packages/web/src/locales/en.json index 4c082efb..2bd2c598 100644 --- a/packages/web/src/locales/en.json +++ b/packages/web/src/locales/en.json @@ -256,5 +256,14 @@ "adminAppsSettings.shared": "Shared", "adminAppsSettings.disabled": "Disabled", "adminAppsSettings.save": "Save", - "adminAppsSettings.successfullySaved": "Settings have been saved." + "adminAppsSettings.successfullySaved": "Settings have been saved.", + "adminAppsAuthClients.noAuthClients": "You don't have any auth clients yet.", + "adminAppsAuthClients.statusActive": "Active", + "adminAppsAuthClients.statusInactive": "Inactive", + "createAuthClient.button": "Create auth client", + "createAuthClient.title": "Create auth client", + "authClient.buttonSubmit": "Submit", + "authClient.inputName": "Name", + "authClient.inputActive": "Active", + "updateAuthClient.title": "Update auth client" } diff --git a/packages/web/src/pages/AdminApplication/index.tsx b/packages/web/src/pages/AdminApplication/index.tsx index 7b567ad9..19c6fa1a 100644 --- a/packages/web/src/pages/AdminApplication/index.tsx +++ b/packages/web/src/pages/AdminApplication/index.tsx @@ -7,6 +7,7 @@ import { Routes, useParams, useMatch, + useNavigate, } from 'react-router-dom'; import { useTheme } from '@mui/material/styles'; import useMediaQuery from '@mui/material/useMediaQuery'; @@ -23,6 +24,9 @@ import AppIcon from 'components/AppIcon'; import Container from 'components/Container'; import PageTitle from 'components/PageTitle'; import AdminApplicationSettings from 'components/AdminApplicationSettings'; +import AdminApplicationAuthClients from 'components/AdminApplicationAuthClients'; +import AdminApplicationCreateAuthClient from 'components/AdminApplicationCreateAuthClient'; +import AdminApplicationUpdateAuthClient from 'components/AdminApplicationUpdateAuthClient'; type AdminApplicationParams = { appKey: string; @@ -32,6 +36,7 @@ export default function AdminApplication(): React.ReactElement | null { const theme = useTheme(); const matchSmallScreens = useMediaQuery(theme.breakpoints.down('md')); const formatMessage = useFormatMessage(); + const navigate = useNavigate(); const connectionsPathMatch = useMatch({ path: URLS.ADMIN_APP_CONNECTIONS_PATTERN, @@ -51,79 +56,104 @@ export default function AdminApplication(): React.ReactElement | null { const app = data?.getApp || {}; + const goToAuthClientsPage = () => navigate('auth-clients'); + if (loading) return null; return ( - - - - - + <> + + + + + + + + {app.name} + - - {app.name} - - - - - - - - - - - + + + + + + + + + - - } - /> - Auth clients} - /> - App connections} - /> - - } - /> - + + } + /> + } + /> + App connections} + /> + + } + /> + + - - + + + + } + /> + + } + /> + + ); }