feat: introduce app configs with shared auth clients (#1213)
This commit is contained in:
@@ -1,17 +1,19 @@
|
||||
import * as React from 'react';
|
||||
import type { IApp, IField, IJSONObject } from '@automatisch/types';
|
||||
import LoadingButton from '@mui/lab/LoadingButton';
|
||||
import Alert from '@mui/material/Alert';
|
||||
import DialogTitle from '@mui/material/DialogTitle';
|
||||
import Dialog from '@mui/material/Dialog';
|
||||
import DialogContent from '@mui/material/DialogContent';
|
||||
import DialogContentText from '@mui/material/DialogContentText';
|
||||
import Dialog from '@mui/material/Dialog';
|
||||
import LoadingButton from '@mui/lab/LoadingButton';
|
||||
import DialogTitle from '@mui/material/DialogTitle';
|
||||
import * as React from 'react';
|
||||
import { FieldValues, SubmitHandler } from 'react-hook-form';
|
||||
import type { IApp, IJSONObject, IField } from '@automatisch/types';
|
||||
import { useNavigate, useSearchParams } from 'react-router-dom';
|
||||
|
||||
import useFormatMessage from 'hooks/useFormatMessage';
|
||||
import computeAuthStepVariables from 'helpers/computeAuthStepVariables';
|
||||
import { processStep } from 'helpers/authenticationSteps';
|
||||
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';
|
||||
|
||||
@@ -21,24 +23,27 @@ type AddAppConnectionProps = {
|
||||
connectionId?: string;
|
||||
};
|
||||
|
||||
type Response = {
|
||||
[key: string]: any;
|
||||
};
|
||||
|
||||
export default function AddAppConnection(
|
||||
props: AddAppConnectionProps
|
||||
): 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 steps = hasConnection
|
||||
? auth?.reconnectionSteps
|
||||
: auth?.authenticationSteps;
|
||||
const useShared = searchParams.get('shared') === 'true';
|
||||
const appAuthClientId = searchParams.get('appAuthClientId') || undefined;
|
||||
const { authenticate } = useAuthenticateApp({
|
||||
appKey: key,
|
||||
connectionId,
|
||||
appAuthClientId,
|
||||
useShared: !!appAuthClientId,
|
||||
});
|
||||
|
||||
React.useEffect(() => {
|
||||
React.useEffect(function relayProviderData() {
|
||||
if (window.opener) {
|
||||
window.opener.postMessage({
|
||||
source: 'automatisch',
|
||||
@@ -48,51 +53,61 @@ export default function AddAppConnection(
|
||||
}
|
||||
}, []);
|
||||
|
||||
const submitHandler: SubmitHandler<FieldValues> = React.useCallback(
|
||||
async (data) => {
|
||||
if (!steps) return;
|
||||
React.useEffect(
|
||||
function initiateSharedAuthenticationForGivenAuthClient() {
|
||||
if (!appAuthClientId) return;
|
||||
if (!authenticate) return;
|
||||
|
||||
setInProgress(true);
|
||||
setError(null);
|
||||
const asyncAuthenticate = async () => {
|
||||
await authenticate();
|
||||
|
||||
const response: Response = {
|
||||
key,
|
||||
connection: {
|
||||
id: connectionId,
|
||||
},
|
||||
fields: data,
|
||||
navigate(URLS.APP_CONNECTIONS(key));
|
||||
};
|
||||
|
||||
let stepIndex = 0;
|
||||
while (stepIndex < steps.length) {
|
||||
const step = steps[stepIndex];
|
||||
const variables = computeAuthStepVariables(step.arguments, response);
|
||||
|
||||
try {
|
||||
const stepResponse = await processStep(step, variables);
|
||||
|
||||
response[step.name] = stepResponse;
|
||||
} catch (err) {
|
||||
const error = err as IJSONObject;
|
||||
console.log(error);
|
||||
setError((error.graphQLErrors as IJSONObject[])?.[0]);
|
||||
setInProgress(false);
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
stepIndex++;
|
||||
|
||||
if (stepIndex === steps.length) {
|
||||
onClose(response);
|
||||
}
|
||||
}
|
||||
|
||||
setInProgress(false);
|
||||
asyncAuthenticate();
|
||||
},
|
||||
[connectionId, key, steps, onClose]
|
||||
[appAuthClientId, authenticate]
|
||||
);
|
||||
|
||||
const handleClientClick = (appAuthClientId: string) =>
|
||||
navigate(URLS.APP_ADD_CONNECTION_WITH_AUTH_CLIENT_ID(key, appAuthClientId));
|
||||
|
||||
const handleAuthClientsDialogClose = () =>
|
||||
navigate(URLS.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} data-test="add-app-connection-dialog">
|
||||
<DialogTitle>
|
||||
|
@@ -0,0 +1,50 @@
|
||||
import Dialog from '@mui/material/Dialog';
|
||||
import DialogTitle from '@mui/material/DialogTitle';
|
||||
import List from '@mui/material/List';
|
||||
import ListItem from '@mui/material/ListItem';
|
||||
import ListItemButton from '@mui/material/ListItemButton';
|
||||
import ListItemText from '@mui/material/ListItemText';
|
||||
import * as React from 'react';
|
||||
|
||||
import useAppAuthClients from 'hooks/useAppAuthClients.ee';
|
||||
import useFormatMessage from 'hooks/useFormatMessage';
|
||||
|
||||
type AppAuthClientsDialogProps = {
|
||||
appKey: string;
|
||||
onClientClick: (appAuthClientId: string) => void;
|
||||
onClose: () => void;
|
||||
};
|
||||
|
||||
export default function AppAuthClientsDialog(props: AppAuthClientsDialogProps) {
|
||||
const { appKey, onClientClick, onClose } = props;
|
||||
const { appAuthClients } = useAppAuthClients(appKey);
|
||||
const formatMessage = useFormatMessage();
|
||||
|
||||
React.useEffect(
|
||||
function autoAuthenticateSingleClient() {
|
||||
if (appAuthClients?.length === 1) {
|
||||
onClientClick(appAuthClients[0].id);
|
||||
}
|
||||
},
|
||||
[appAuthClients]
|
||||
);
|
||||
|
||||
if (!appAuthClients?.length || appAuthClients?.length === 1)
|
||||
return <React.Fragment />;
|
||||
|
||||
return (
|
||||
<Dialog onClose={onClose} open={true}>
|
||||
<DialogTitle>{formatMessage('appAuthClientsDialog.title')}</DialogTitle>
|
||||
|
||||
<List sx={{ pt: 0 }}>
|
||||
{appAuthClients.map((appAuthClient) => (
|
||||
<ListItem disableGutters key={appAuthClient.id}>
|
||||
<ListItemButton onClick={() => onClientClick(appAuthClient.id)}>
|
||||
<ListItemText primary={appAuthClient.name} />
|
||||
</ListItemButton>
|
||||
</ListItem>
|
||||
))}
|
||||
</List>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
@@ -3,6 +3,7 @@ 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';
|
||||
@@ -13,16 +14,24 @@ type Action = {
|
||||
|
||||
type ContextMenuProps = {
|
||||
appKey: string;
|
||||
connectionId: 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, connectionId, onClose, onMenuItemClick, anchorEl } = props;
|
||||
const {
|
||||
appKey,
|
||||
connection,
|
||||
onClose,
|
||||
onMenuItemClick,
|
||||
anchorEl,
|
||||
disableReconnection,
|
||||
} = props;
|
||||
const formatMessage = useFormatMessage();
|
||||
|
||||
const createActionHandler = React.useCallback(
|
||||
@@ -45,7 +54,7 @@ export default function ContextMenu(
|
||||
>
|
||||
<MenuItem
|
||||
component={Link}
|
||||
to={URLS.APP_FLOWS_FOR_CONNECTION(appKey, connectionId)}
|
||||
to={URLS.APP_FLOWS_FOR_CONNECTION(appKey, connection.id)}
|
||||
onClick={createActionHandler({ type: 'viewFlows' })}
|
||||
>
|
||||
{formatMessage('connection.viewFlows')}
|
||||
@@ -57,7 +66,12 @@ export default function ContextMenu(
|
||||
|
||||
<MenuItem
|
||||
component={Link}
|
||||
to={URLS.APP_RECONNECT_CONNECTION(appKey, connectionId)}
|
||||
disabled={disableReconnection}
|
||||
to={URLS.APP_RECONNECT_CONNECTION(
|
||||
appKey,
|
||||
connection.id,
|
||||
connection.appAuthClientId
|
||||
)}
|
||||
onClick={createActionHandler({ type: 'reconnect' })}
|
||||
>
|
||||
{formatMessage('connection.reconnect')}
|
||||
|
@@ -45,8 +45,15 @@ function AppConnectionRow(props: AppConnectionRowProps): React.ReactElement {
|
||||
const [deleteConnection] = useMutation(DELETE_CONNECTION);
|
||||
|
||||
const formatMessage = useFormatMessage();
|
||||
const { id, key, formattedData, verified, createdAt, flowCount } =
|
||||
props.connection;
|
||||
const {
|
||||
id,
|
||||
key,
|
||||
formattedData,
|
||||
verified,
|
||||
createdAt,
|
||||
flowCount,
|
||||
reconnectable,
|
||||
} = props.connection;
|
||||
|
||||
const contextButtonRef = React.useRef<SVGSVGElement | null>(null);
|
||||
const [anchorEl, setAnchorEl] = React.useState<SVGSVGElement | null>(null);
|
||||
@@ -159,7 +166,8 @@ function AppConnectionRow(props: AppConnectionRowProps): React.ReactElement {
|
||||
{anchorEl && (
|
||||
<ConnectionContextMenu
|
||||
appKey={key}
|
||||
connectionId={id}
|
||||
connection={props.connection}
|
||||
disableReconnection={!reconnectable}
|
||||
onClose={handleClose}
|
||||
onMenuItemClick={onContextMenuAction}
|
||||
anchorEl={anchorEl}
|
||||
|
@@ -1,18 +1,21 @@
|
||||
import * as React from 'react';
|
||||
import { useQuery, useLazyQuery } from '@apollo/client';
|
||||
import TextField from '@mui/material/TextField';
|
||||
import { useLazyQuery, useQuery } from '@apollo/client';
|
||||
import Autocomplete from '@mui/material/Autocomplete';
|
||||
import Button from '@mui/material/Button';
|
||||
import Collapse from '@mui/material/Collapse';
|
||||
import ListItem from '@mui/material/ListItem';
|
||||
import Autocomplete from '@mui/material/Autocomplete';
|
||||
import TextField from '@mui/material/TextField';
|
||||
import * as React from 'react';
|
||||
|
||||
import type { IApp, IConnection, IStep, ISubstep } from '@automatisch/types';
|
||||
import useFormatMessage from 'hooks/useFormatMessage';
|
||||
import { EditorContext } from 'contexts/Editor';
|
||||
import FlowSubstepTitle from 'components/FlowSubstepTitle';
|
||||
import AddAppConnection from 'components/AddAppConnection';
|
||||
import AppAuthClientsDialog from 'components/AppAuthClientsDialog/index.ee';
|
||||
import FlowSubstepTitle from 'components/FlowSubstepTitle';
|
||||
import useAppConfig from 'hooks/useAppConfig.ee';
|
||||
import { EditorContext } from 'contexts/Editor';
|
||||
import { GET_APP_CONNECTIONS } from 'graphql/queries/get-app-connections';
|
||||
import { TEST_CONNECTION } from 'graphql/queries/test-connection';
|
||||
import useAuthenticateApp from 'hooks/useAuthenticateApp.ee';
|
||||
import useFormatMessage from 'hooks/useFormatMessage';
|
||||
|
||||
type ChooseConnectionSubstepProps = {
|
||||
application: IApp;
|
||||
@@ -26,6 +29,7 @@ type ChooseConnectionSubstepProps = {
|
||||
};
|
||||
|
||||
const ADD_CONNECTION_VALUE = 'ADD_CONNECTION';
|
||||
const ADD_SHARED_CONNECTION_VALUE = 'ADD_SHARED_CONNECTION';
|
||||
|
||||
const optionGenerator = (
|
||||
connection: IConnection
|
||||
@@ -53,11 +57,18 @@ function ChooseConnectionSubstep(
|
||||
const { connection, appKey } = step;
|
||||
const formatMessage = useFormatMessage();
|
||||
const editorContext = React.useContext(EditorContext);
|
||||
const { authenticate } = useAuthenticateApp({
|
||||
appKey: application.key,
|
||||
useShared: true,
|
||||
});
|
||||
const [showAddConnectionDialog, setShowAddConnectionDialog] =
|
||||
React.useState(false);
|
||||
const [showAddSharedConnectionDialog, setShowAddSharedConnectionDialog] =
|
||||
React.useState(false);
|
||||
const { data, loading, refetch } = useQuery(GET_APP_CONNECTIONS, {
|
||||
variables: { key: appKey },
|
||||
});
|
||||
const { appConfig } = useAppConfig(application.key);
|
||||
// TODO: show detailed error when connection test/verification fails
|
||||
const [
|
||||
testConnection,
|
||||
@@ -86,13 +97,49 @@ function ChooseConnectionSubstep(
|
||||
optionGenerator(connection)
|
||||
) || [];
|
||||
|
||||
options.push({
|
||||
label: formatMessage('chooseConnectionSubstep.addNewConnection'),
|
||||
value: ADD_CONNECTION_VALUE,
|
||||
});
|
||||
if (!appConfig || appConfig.canCustomConnect) {
|
||||
options.push({
|
||||
label: formatMessage('chooseConnectionSubstep.addNewConnection'),
|
||||
value: ADD_CONNECTION_VALUE,
|
||||
});
|
||||
}
|
||||
|
||||
if (appConfig?.canConnect) {
|
||||
options.push({
|
||||
label: formatMessage('chooseConnectionSubstep.addNewSharedConnection'),
|
||||
value: ADD_SHARED_CONNECTION_VALUE,
|
||||
});
|
||||
}
|
||||
|
||||
return options;
|
||||
}, [data, formatMessage]);
|
||||
}, [data, formatMessage, appConfig]);
|
||||
|
||||
const handleClientClick = async (appAuthClientId: string) => {
|
||||
try {
|
||||
const response = await authenticate?.({
|
||||
appAuthClientId,
|
||||
});
|
||||
|
||||
const connectionId = response?.createConnection.id;
|
||||
|
||||
if (connectionId) {
|
||||
await refetch();
|
||||
|
||||
onChange({
|
||||
step: {
|
||||
...step,
|
||||
connection: {
|
||||
id: connectionId,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
// void
|
||||
} finally {
|
||||
setShowAddSharedConnectionDialog(false);
|
||||
}
|
||||
};
|
||||
|
||||
const { name } = substep;
|
||||
|
||||
@@ -131,6 +178,11 @@ function ChooseConnectionSubstep(
|
||||
return;
|
||||
}
|
||||
|
||||
if (connectionId === ADD_SHARED_CONNECTION_VALUE) {
|
||||
setShowAddSharedConnectionDialog(true);
|
||||
return;
|
||||
}
|
||||
|
||||
if (connectionId !== step.connection?.id) {
|
||||
onChange({
|
||||
step: {
|
||||
@@ -216,6 +268,14 @@ function ChooseConnectionSubstep(
|
||||
application={application}
|
||||
/>
|
||||
)}
|
||||
|
||||
{application && showAddSharedConnectionDialog && (
|
||||
<AppAuthClientsDialog
|
||||
appKey={application.key}
|
||||
onClose={() => setShowAddSharedConnectionDialog(false)}
|
||||
onClientClick={handleClientClick}
|
||||
/>
|
||||
)}
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
116
packages/web/src/components/SplitButton/index.tsx
Normal file
116
packages/web/src/components/SplitButton/index.tsx
Normal file
@@ -0,0 +1,116 @@
|
||||
import ArrowDropDownIcon from '@mui/icons-material/ArrowDropDown';
|
||||
import Button from '@mui/material/Button';
|
||||
import ButtonGroup from '@mui/material/ButtonGroup';
|
||||
import ClickAwayListener from '@mui/material/ClickAwayListener';
|
||||
import Grow from '@mui/material/Grow';
|
||||
import MenuItem from '@mui/material/MenuItem';
|
||||
import MenuList from '@mui/material/MenuList';
|
||||
import Paper from '@mui/material/Paper';
|
||||
import Popper from '@mui/material/Popper';
|
||||
import * as React from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
type SplitButtonProps = {
|
||||
options: {
|
||||
key: string;
|
||||
'data-test'?: string;
|
||||
label: React.ReactNode;
|
||||
to: string;
|
||||
}[];
|
||||
disabled?: boolean;
|
||||
defaultActionIndex?: number;
|
||||
};
|
||||
|
||||
export default function SplitButton(props: SplitButtonProps) {
|
||||
const { options, disabled, defaultActionIndex = 0 } = props;
|
||||
const [open, setOpen] = React.useState(false);
|
||||
const anchorRef = React.useRef<HTMLDivElement>(null);
|
||||
|
||||
const multiOptions = options.length > 1;
|
||||
const selectedOption = options[defaultActionIndex];
|
||||
|
||||
const handleToggle = () => {
|
||||
setOpen((prevOpen) => !prevOpen);
|
||||
};
|
||||
|
||||
const handleClose = (event: Event) => {
|
||||
if (
|
||||
anchorRef.current &&
|
||||
anchorRef.current.contains(event.target as HTMLElement)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
setOpen(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<ButtonGroup
|
||||
variant="contained"
|
||||
ref={anchorRef}
|
||||
aria-label="split button"
|
||||
disabled={disabled}
|
||||
>
|
||||
<Button
|
||||
size="large"
|
||||
data-test={selectedOption['data-test']}
|
||||
component={Link}
|
||||
to={selectedOption.to}
|
||||
sx={{
|
||||
// Link component causes style loss in ButtonGroup
|
||||
borderRadius: 0,
|
||||
borderRight: '1px solid #bdbdbd',
|
||||
}}
|
||||
>
|
||||
{selectedOption.label}
|
||||
</Button>
|
||||
|
||||
{multiOptions && (
|
||||
<Button size="small" onClick={handleToggle} sx={{ borderRadius: 0 }}>
|
||||
<ArrowDropDownIcon />
|
||||
</Button>
|
||||
)}
|
||||
</ButtonGroup>
|
||||
|
||||
{multiOptions && (
|
||||
<Popper
|
||||
sx={{
|
||||
zIndex: 1,
|
||||
}}
|
||||
open={open}
|
||||
anchorEl={anchorRef.current}
|
||||
transition
|
||||
disablePortal
|
||||
>
|
||||
{({ TransitionProps, placement }) => (
|
||||
<Grow
|
||||
{...TransitionProps}
|
||||
style={{
|
||||
transformOrigin:
|
||||
placement === 'bottom' ? 'center top' : 'center bottom',
|
||||
}}
|
||||
>
|
||||
<Paper>
|
||||
<ClickAwayListener onClickAway={handleClose}>
|
||||
<MenuList autoFocusItem>
|
||||
{options.map((option, index) => (
|
||||
<MenuItem
|
||||
key={option.key}
|
||||
selected={index === defaultActionIndex}
|
||||
component={Link}
|
||||
to={option.to}
|
||||
>
|
||||
{option.label}
|
||||
</MenuItem>
|
||||
))}
|
||||
</MenuList>
|
||||
</ClickAwayListener>
|
||||
</Paper>
|
||||
</Grow>
|
||||
)}
|
||||
</Popper>
|
||||
)}
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
@@ -20,13 +20,24 @@ export const APP_PATTERN = '/app/:appKey';
|
||||
export const APP_CONNECTIONS = (appKey: string) =>
|
||||
`/app/${appKey}/connections`;
|
||||
export const APP_CONNECTIONS_PATTERN = '/app/:appKey/connections';
|
||||
export const APP_ADD_CONNECTION = (appKey: string) =>
|
||||
`/app/${appKey}/connections/add`;
|
||||
export const APP_ADD_CONNECTION = (appKey: string, shared = false) =>
|
||||
`/app/${appKey}/connections/add?shared=${shared}`;
|
||||
export const APP_ADD_CONNECTION_WITH_AUTH_CLIENT_ID = (appKey: string, appAuthClientId: string) =>
|
||||
`/app/${appKey}/connections/add?appAuthClientId=${appAuthClientId}`;
|
||||
export const APP_ADD_CONNECTION_PATTERN = '/app/:appKey/connections/add';
|
||||
export const APP_RECONNECT_CONNECTION = (
|
||||
appKey: string,
|
||||
connectionId: string
|
||||
) => `/app/${appKey}/connections/${connectionId}/reconnect`;
|
||||
connectionId: string,
|
||||
appAuthClientId?: string,
|
||||
) => {
|
||||
const path = `/app/${appKey}/connections/${connectionId}/reconnect`;
|
||||
|
||||
if (appAuthClientId) {
|
||||
return `${path}?appAuthClientId=${appAuthClientId}`;
|
||||
}
|
||||
|
||||
return path;
|
||||
};
|
||||
export const APP_RECONNECT_CONNECTION_PATTERN =
|
||||
'/app/:appKey/connections/:connectionId/reconnect';
|
||||
export const APP_FLOWS = (appKey: string) => `/app/${appKey}/flows`;
|
||||
|
13
packages/web/src/graphql/queries/get-app-auth-client.ee.ts
Normal file
13
packages/web/src/graphql/queries/get-app-auth-client.ee.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { gql } from '@apollo/client';
|
||||
|
||||
export const GET_APP_AUTH_CLIENT = gql`
|
||||
query GetAppAuthClient($id: String!) {
|
||||
getAppAuthClient(id: $id) {
|
||||
id
|
||||
appConfigId
|
||||
name
|
||||
active
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
13
packages/web/src/graphql/queries/get-app-auth-clients.ee.ts
Normal file
13
packages/web/src/graphql/queries/get-app-auth-clients.ee.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { gql } from '@apollo/client';
|
||||
|
||||
export const GET_APP_AUTH_CLIENTS = gql`
|
||||
query GetAppAuthClients($appKey: String!, $active: Boolean) {
|
||||
getAppAuthClients(appKey: $appKey, active: $active) {
|
||||
id
|
||||
appConfigId
|
||||
name
|
||||
active
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
16
packages/web/src/graphql/queries/get-app-config.ee.ts
Normal file
16
packages/web/src/graphql/queries/get-app-config.ee.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { gql } from '@apollo/client';
|
||||
|
||||
export const GET_APP_CONFIG = gql`
|
||||
query GetAppConfig($key: String!) {
|
||||
getAppConfig(key: $key) {
|
||||
id
|
||||
key
|
||||
allowCustomConnection
|
||||
canConnect
|
||||
canCustomConnect
|
||||
shared
|
||||
disabled
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
@@ -7,6 +7,8 @@ export const GET_APP_CONNECTIONS = gql`
|
||||
connections {
|
||||
id
|
||||
key
|
||||
reconnectable
|
||||
appAuthClientId
|
||||
verified
|
||||
flowCount
|
||||
formattedData {
|
||||
|
@@ -39,6 +39,19 @@ export const GET_APP = gql`
|
||||
}
|
||||
}
|
||||
}
|
||||
sharedAuthenticationSteps {
|
||||
type
|
||||
name
|
||||
arguments {
|
||||
name
|
||||
value
|
||||
type
|
||||
properties {
|
||||
name
|
||||
value
|
||||
}
|
||||
}
|
||||
}
|
||||
reconnectionSteps {
|
||||
type
|
||||
name
|
||||
@@ -52,6 +65,19 @@ export const GET_APP = gql`
|
||||
}
|
||||
}
|
||||
}
|
||||
sharedReconnectionSteps {
|
||||
type
|
||||
name
|
||||
arguments {
|
||||
name
|
||||
value
|
||||
type
|
||||
properties {
|
||||
name
|
||||
value
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
connections {
|
||||
id
|
||||
|
@@ -49,6 +49,19 @@ export const GET_APPS = gql`
|
||||
}
|
||||
}
|
||||
}
|
||||
sharedAuthenticationSteps {
|
||||
type
|
||||
name
|
||||
arguments {
|
||||
name
|
||||
value
|
||||
type
|
||||
properties {
|
||||
name
|
||||
value
|
||||
}
|
||||
}
|
||||
}
|
||||
reconnectionSteps {
|
||||
type
|
||||
name
|
||||
@@ -62,6 +75,19 @@ export const GET_APPS = gql`
|
||||
}
|
||||
}
|
||||
}
|
||||
sharedReconnectionSteps {
|
||||
type
|
||||
name
|
||||
arguments {
|
||||
name
|
||||
value
|
||||
type
|
||||
properties {
|
||||
name
|
||||
value
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
triggers {
|
||||
name
|
||||
|
@@ -57,7 +57,9 @@ const processOpenWithPopup = (
|
||||
popup?.focus();
|
||||
|
||||
const closeCheckIntervalId = setInterval(() => {
|
||||
if (popup.closed) {
|
||||
if (!popup) return;
|
||||
|
||||
if (popup?.closed) {
|
||||
clearInterval(closeCheckIntervalId);
|
||||
reject({ message: 'Error occured while verifying credentials!' });
|
||||
}
|
||||
|
26
packages/web/src/hooks/useApp.ts
Normal file
26
packages/web/src/hooks/useApp.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { useQuery } from '@apollo/client';
|
||||
import { IApp } from '@automatisch/types';
|
||||
|
||||
import { GET_APP } from 'graphql/queries/get-app';
|
||||
|
||||
type QueryResponse = {
|
||||
getApp: IApp;
|
||||
}
|
||||
|
||||
export default function useApp(key: string) {
|
||||
const {
|
||||
data,
|
||||
loading
|
||||
} = useQuery<QueryResponse>(
|
||||
GET_APP,
|
||||
{
|
||||
variables: { key }
|
||||
}
|
||||
);
|
||||
const app = data?.getApp;
|
||||
|
||||
return {
|
||||
app,
|
||||
loading,
|
||||
};
|
||||
}
|
31
packages/web/src/hooks/useAppAuthClient.ee.ts
Normal file
31
packages/web/src/hooks/useAppAuthClient.ee.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import { useLazyQuery } from '@apollo/client';
|
||||
import { AppConfig } 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;
|
||||
}
|
||||
|
||||
export default function useAppAuthClient(id: string) {
|
||||
const [
|
||||
getAppAuthClient,
|
||||
{
|
||||
data,
|
||||
loading
|
||||
}
|
||||
] = useLazyQuery<QueryResponse>(GET_APP_AUTH_CLIENT);
|
||||
const appAuthClient = data?.getAppAuthClient;
|
||||
|
||||
React.useEffect(function fetchUponId() {
|
||||
if (!id) return;
|
||||
|
||||
getAppAuthClient({ variables: { id } });
|
||||
}, [id]);
|
||||
|
||||
return {
|
||||
appAuthClient,
|
||||
loading,
|
||||
};
|
||||
}
|
33
packages/web/src/hooks/useAppAuthClients.ee.ts
Normal file
33
packages/web/src/hooks/useAppAuthClients.ee.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import { useLazyQuery } from '@apollo/client';
|
||||
import { AppAuthClient } from '@automatisch/types';
|
||||
import * as React from 'react';
|
||||
|
||||
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,
|
||||
{
|
||||
data,
|
||||
loading
|
||||
}
|
||||
] = useLazyQuery<QueryResponse>(GET_APP_AUTH_CLIENTS, {
|
||||
context: { autoSnackbar: false },
|
||||
});
|
||||
const appAuthClients = data?.getAppAuthClients;
|
||||
|
||||
React.useEffect(function fetchUponAppKey() {
|
||||
if (!appKey) return;
|
||||
|
||||
getAppAuthClients({ variables: { appKey, active: true } });
|
||||
}, [appKey]);
|
||||
|
||||
return {
|
||||
appAuthClients,
|
||||
loading,
|
||||
};
|
||||
}
|
27
packages/web/src/hooks/useAppConfig.ee.ts
Normal file
27
packages/web/src/hooks/useAppConfig.ee.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { useQuery } from '@apollo/client';
|
||||
import { AppConfig } from '@automatisch/types';
|
||||
|
||||
import { GET_APP_CONFIG } from 'graphql/queries/get-app-config.ee';
|
||||
|
||||
type QueryResponse = {
|
||||
getAppConfig: AppConfig;
|
||||
}
|
||||
|
||||
export default function useAppConfig(key: string) {
|
||||
const {
|
||||
data,
|
||||
loading
|
||||
} = useQuery<QueryResponse>(
|
||||
GET_APP_CONFIG,
|
||||
{
|
||||
variables: { key },
|
||||
context: { autoSnackbar: false }
|
||||
}
|
||||
);
|
||||
const appConfig = data?.getAppConfig;
|
||||
|
||||
return {
|
||||
appConfig,
|
||||
loading,
|
||||
};
|
||||
}
|
100
packages/web/src/hooks/useAuthenticateApp.ee.ts
Normal file
100
packages/web/src/hooks/useAuthenticateApp.ee.ts
Normal file
@@ -0,0 +1,100 @@
|
||||
import { IApp } from '@automatisch/types';
|
||||
import * as React from 'react';
|
||||
|
||||
import { processStep } from 'helpers/authenticationSteps';
|
||||
import computeAuthStepVariables from 'helpers/computeAuthStepVariables';
|
||||
import useApp from './useApp';
|
||||
|
||||
type UseAuthenticateAppParams = {
|
||||
appKey: string;
|
||||
appAuthClientId?: string;
|
||||
useShared?: boolean;
|
||||
connectionId?: string;
|
||||
}
|
||||
|
||||
type AuthenticatePayload = {
|
||||
fields?: Record<string, string>;
|
||||
appAuthClientId?: string;
|
||||
}
|
||||
|
||||
function getSteps(auth: IApp['auth'], hasConnection: boolean, useShared: boolean) {
|
||||
if (hasConnection) {
|
||||
if (useShared) {
|
||||
return auth?.sharedReconnectionSteps;
|
||||
}
|
||||
|
||||
return auth?.reconnectionSteps;
|
||||
}
|
||||
|
||||
if (useShared) {
|
||||
return auth?.sharedAuthenticationSteps;
|
||||
}
|
||||
|
||||
return auth?.authenticationSteps;
|
||||
}
|
||||
|
||||
export default function useAuthenticateApp(payload: UseAuthenticateAppParams) {
|
||||
const {
|
||||
appKey,
|
||||
appAuthClientId,
|
||||
connectionId,
|
||||
useShared = false,
|
||||
} = payload;
|
||||
const { app } = useApp(appKey);
|
||||
const [
|
||||
authenticationInProgress,
|
||||
setAuthenticationInProgress
|
||||
] = React.useState(false);
|
||||
const steps = getSteps(app?.auth, !!connectionId, useShared);
|
||||
|
||||
const authenticate = React.useMemo(() => {
|
||||
if (!steps?.length) return;
|
||||
|
||||
return async function authenticate(payload: AuthenticatePayload = {}) {
|
||||
const {
|
||||
fields,
|
||||
} = payload;
|
||||
setAuthenticationInProgress(true);
|
||||
|
||||
const response: Record<string, any> = {
|
||||
key: appKey,
|
||||
appAuthClientId: appAuthClientId || payload.appAuthClientId,
|
||||
connection: {
|
||||
id: connectionId,
|
||||
},
|
||||
fields
|
||||
};
|
||||
|
||||
let stepIndex = 0;
|
||||
while (stepIndex < steps?.length) {
|
||||
const step = steps[stepIndex];
|
||||
const variables = computeAuthStepVariables(step.arguments, response);
|
||||
|
||||
try {
|
||||
const stepResponse = await processStep(step, variables);
|
||||
|
||||
response[step.name] = stepResponse;
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
throw err;
|
||||
|
||||
setAuthenticationInProgress(false);
|
||||
break;
|
||||
}
|
||||
|
||||
stepIndex++;
|
||||
|
||||
if (stepIndex === steps.length) {
|
||||
return response;
|
||||
}
|
||||
|
||||
setAuthenticationInProgress(false);
|
||||
}
|
||||
}
|
||||
}, [steps, appKey, appAuthClientId, connectionId]);
|
||||
|
||||
return {
|
||||
authenticate,
|
||||
inProgress: authenticationInProgress,
|
||||
};
|
||||
}
|
@@ -19,6 +19,7 @@
|
||||
"app.connectionCount": "{count} connections",
|
||||
"app.flowCount": "{count} flows",
|
||||
"app.addConnection": "Add connection",
|
||||
"app.addCustomConnection": "Add custom connection",
|
||||
"app.reconnectConnection": "Reconnect connection",
|
||||
"app.createFlow": "Create flow",
|
||||
"app.settings": "Settings",
|
||||
@@ -69,6 +70,7 @@
|
||||
"filterConditions.orContinueIf": "OR continue if…",
|
||||
"chooseConnectionSubstep.continue": "Continue",
|
||||
"chooseConnectionSubstep.addNewConnection": "Add new connection",
|
||||
"chooseConnectionSubstep.addNewSharedConnection": "Add new shared connection",
|
||||
"chooseConnectionSubstep.chooseConnection": "Choose connection",
|
||||
"flow.createdAt": "created {datetime}",
|
||||
"flow.updatedAt": "updated {datetime}",
|
||||
@@ -209,5 +211,6 @@
|
||||
"roleList.description": "Description",
|
||||
"permissionSettings.cancel": "Cancel",
|
||||
"permissionSettings.apply": "Apply",
|
||||
"permissionSettings.title": "Conditions"
|
||||
"permissionSettings.title": "Conditions",
|
||||
"appAuthClientsDialog.title": "Choose your authentication client"
|
||||
}
|
||||
|
@@ -19,9 +19,11 @@ import Tab from '@mui/material/Tab';
|
||||
import AddIcon from '@mui/icons-material/Add';
|
||||
|
||||
import useFormatMessage from 'hooks/useFormatMessage';
|
||||
import useAppConfig from 'hooks/useAppConfig.ee';
|
||||
import { GET_APP } from 'graphql/queries/get-app';
|
||||
import * as URLS from 'config/urls';
|
||||
|
||||
import SplitButton from 'components/SplitButton';
|
||||
import ConditionalIconButton from 'components/ConditionalIconButton';
|
||||
import AppConnections from 'components/AppConnections';
|
||||
import AppFlows from 'components/AppFlows';
|
||||
@@ -35,6 +37,13 @@ type ApplicationParams = {
|
||||
connectionId?: string;
|
||||
};
|
||||
|
||||
type ConnectionOption = {
|
||||
key: string;
|
||||
label: string;
|
||||
'data-test': string;
|
||||
to: string;
|
||||
};
|
||||
|
||||
const ReconnectConnection = (props: any): React.ReactElement => {
|
||||
const { application, onClose } = props;
|
||||
const { connectionId } = useParams() as ApplicationParams;
|
||||
@@ -61,11 +70,36 @@ export default function Application(): React.ReactElement | null {
|
||||
const { appKey } = useParams() as ApplicationParams;
|
||||
const navigate = useNavigate();
|
||||
const { data, loading } = useQuery(GET_APP, { variables: { key: appKey } });
|
||||
const { appConfig } = useAppConfig(appKey);
|
||||
|
||||
const connectionId = searchParams.get('connectionId') || undefined;
|
||||
const goToApplicationPage = () => navigate('connections');
|
||||
const app = data?.getApp || {};
|
||||
|
||||
const connectionOptions = React.useMemo(() => {
|
||||
const shouldHaveCustomConnection =
|
||||
appConfig?.canConnect && appConfig?.canCustomConnect;
|
||||
const options: ConnectionOption[] = [
|
||||
{
|
||||
label: formatMessage('app.addConnection'),
|
||||
key: 'addConnection',
|
||||
'data-test': 'add-connection-button',
|
||||
to: URLS.APP_ADD_CONNECTION(appKey, appConfig?.canConnect),
|
||||
},
|
||||
];
|
||||
|
||||
if (shouldHaveCustomConnection) {
|
||||
options.push({
|
||||
label: formatMessage('app.addCustomConnection'),
|
||||
key: 'addCustomConnection',
|
||||
'data-test': 'add-custom-connection-button',
|
||||
to: URLS.APP_ADD_CONNECTION(appKey),
|
||||
});
|
||||
}
|
||||
|
||||
return options;
|
||||
}, [appKey, appConfig]);
|
||||
|
||||
if (loading) return null;
|
||||
|
||||
return (
|
||||
@@ -111,19 +145,14 @@ export default function Application(): React.ReactElement | null {
|
||||
<Route
|
||||
path={`${URLS.CONNECTIONS}/*`}
|
||||
element={
|
||||
<ConditionalIconButton
|
||||
type="submit"
|
||||
variant="contained"
|
||||
color="primary"
|
||||
size="large"
|
||||
component={Link}
|
||||
to={URLS.APP_ADD_CONNECTION(appKey)}
|
||||
fullWidth
|
||||
icon={<AddIcon />}
|
||||
data-test="add-connection-button"
|
||||
>
|
||||
{formatMessage('app.addConnection')}
|
||||
</ConditionalIconButton>
|
||||
<SplitButton
|
||||
disabled={
|
||||
appConfig &&
|
||||
!appConfig?.canConnect &&
|
||||
!appConfig?.canCustomConnect
|
||||
}
|
||||
options={connectionOptions}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</Routes>
|
||||
|
Reference in New Issue
Block a user