Merge pull request #1696 from automatisch/AUT-685

refactor: implement rest API endpoint for get app and get apps
This commit is contained in:
Ali BARIN
2024-03-11 15:26:54 +01:00
committed by GitHub
25 changed files with 401 additions and 458 deletions

View File

@@ -1,17 +0,0 @@
import App from '../../models/app.js';
const getApps = async (_parent, params) => {
const apps = await App.findAll(params.name);
if (params.onlyWithTriggers) {
return apps.filter((app) => app.triggers?.length);
}
if (params.onlyWithActions) {
return apps.filter((app) => app.actions?.length);
}
return apps;
};
export default getApps;

View File

@@ -2,7 +2,6 @@ import getApp from './queries/get-app.js';
import getAppAuthClient from './queries/get-app-auth-client.ee.js';
import getAppAuthClients from './queries/get-app-auth-clients.ee.js';
import getAppConfig from './queries/get-app-config.ee.js';
import getApps from './queries/get-apps.js';
import getBillingAndUsage from './queries/get-billing-and-usage.ee.js';
import getConfig from './queries/get-config.ee.js';
import getConnectedApps from './queries/get-connected-apps.js';
@@ -37,7 +36,6 @@ const queryResolvers = {
getAppAuthClient,
getAppAuthClients,
getAppConfig,
getApps,
getBillingAndUsage,
getConfig,
getConnectedApps,

View File

@@ -1,9 +1,4 @@
type Query {
getApps(
name: String
onlyWithTriggers: Boolean
onlyWithActions: Boolean
): [App]
getApp(key: String!): App
getAppConfig(key: String!): AppConfig
getAppAuthClient(id: String!): AppAuthClient

View File

@@ -16,10 +16,12 @@ import useAuthenticateApp from 'hooks/useAuthenticateApp.ee';
import useFormatMessage from 'hooks/useFormatMessage';
import { generateExternalLink } from 'helpers/translationValues';
import { Form } from './style';
import useAppAuth from 'hooks/useAppAuth';
function AddAppConnection(props) {
const { application, connectionId, onClose } = props;
const { name, authDocUrl, key, auth } = application;
const { name, authDocUrl, key } = application;
const { data: auth } = useAppAuth(key);
const navigate = useNavigate();
const [searchParams] = useSearchParams();
const formatMessage = useFormatMessage();
@@ -34,31 +36,40 @@ function AddAppConnection(props) {
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.APP_CONNECTIONS(key));
};
asyncAuthenticate();
},
[appAuthClientId, authenticate],
);
const handleClientClick = (appAuthClientId) =>
navigate(URLS.APP_ADD_CONNECTION_WITH_AUTH_CLIENT_ID(key, appAuthClientId));
const handleAuthClientsDialogClose = () =>
navigate(URLS.APP_CONNECTIONS(key));
const submitHandler = React.useCallback(
async (data) => {
if (!authenticate) return;
@@ -78,6 +89,7 @@ function AddAppConnection(props) {
},
[authenticate],
);
if (useShared)
return (
<AppAuthClientsDialog
@@ -86,7 +98,9 @@ function AddAppConnection(props) {
onClientClick={handleClientClick}
/>
);
if (appAuthClientId) return <React.Fragment />;
return (
<Dialog open={true} onClose={onClose} data-test="add-app-connection-dialog">
<DialogTitle>
@@ -121,7 +135,7 @@ function AddAppConnection(props) {
<DialogContent>
<DialogContentText tabIndex={-1} component="div">
<Form onSubmit={submitHandler}>
{auth?.fields?.map((field) => (
{auth?.data?.fields?.map((field) => (
<InputCreator key={field.key} schema={field} />
))}

View File

@@ -1,6 +1,5 @@
import * as React from 'react';
import PropTypes from 'prop-types';
import { useLazyQuery } from '@apollo/client';
import { Link } from 'react-router-dom';
import debounce from 'lodash/debounce';
import { useTheme } from '@mui/material/styles';
@@ -20,15 +19,17 @@ import InputLabel from '@mui/material/InputLabel';
import OutlinedInput from '@mui/material/OutlinedInput';
import FormControl from '@mui/material/FormControl';
import Box from '@mui/material/Box';
import * as URLS from 'config/urls';
import AppIcon from 'components/AppIcon';
import { GET_APPS } from 'graphql/queries/get-apps';
import useFormatMessage from 'hooks/useFormatMessage';
import useLazyApps from 'hooks/useLazyApps';
function createConnectionOrFlow(appKey, supportsConnections = false) {
if (!supportsConnections) {
return URLS.CREATE_FLOW_WITH_APP(appKey);
}
return URLS.APP_ADD_CONNECTION(appKey);
}
function AddNewAppConnection(props) {
@@ -36,29 +37,28 @@ function AddNewAppConnection(props) {
const theme = useTheme();
const matchSmallScreens = useMediaQuery(theme.breakpoints.down('sm'));
const formatMessage = useFormatMessage();
const [appName, setAppName] = React.useState(null);
const [loading, setLoading] = React.useState(false);
const [getApps, { data }] = useLazyQuery(GET_APPS, {
onCompleted: () => {
setLoading(false);
const [appName, setAppName] = React.useState('');
const [isLoading, setIsLoading] = React.useState(false);
const { data: apps, mutate } = useLazyApps({
appName,
onSuccess: () => {
setIsLoading(false);
},
});
const fetchData = React.useMemo(
() => debounce((name) => getApps({ variables: { name } }), 300),
[getApps],
);
React.useEffect(
function fetchAppsOnAppNameChange() {
setLoading(true);
fetchData(appName);
},
[fetchData, appName],
);
React.useEffect(function cancelDebounceOnUnmount() {
const fetchData = React.useMemo(() => debounce(mutate, 300), [mutate]);
React.useEffect(() => {
setIsLoading(true);
fetchData(appName);
return () => {
fetchData.cancel();
};
}, []);
}, [fetchData, appName]);
return (
<Dialog
open={true}
@@ -102,15 +102,15 @@ function AddNewAppConnection(props) {
<DialogContent>
<List sx={{ pt: 2, width: '100%' }}>
{loading && (
{isLoading && (
<CircularProgress
data-test="search-for-app-loader"
sx={{ display: 'block', margin: '20px auto' }}
/>
)}
{!loading &&
data?.getApps?.map((app) => (
{!isLoading &&
apps?.data.map((app) => (
<ListItem disablePadding key={app.name} data-test="app-list-item">
<ListItemButton
component={Link}

View File

@@ -8,12 +8,14 @@ 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';
import useAppAuth from 'hooks/useAppAuth';
function AdminApplicationCreateAuthClient(props) {
const { appKey, application, onClose } = props;
const { auth } = application;
const { appKey, onClose } = props;
const { data: auth } = useAppAuth(appKey);
const formatMessage = useFormatMessage();
const { appConfig, loading: loadingAppConfig } = useAppConfig(appKey);
const [
createAppConfig,
{ loading: loadingCreateAppConfig, error: createAppConfigError },
@@ -21,6 +23,7 @@ function AdminApplicationCreateAuthClient(props) {
refetchQueries: ['GetAppConfig'],
context: { autoSnackbar: false },
});
const [
createAppAuthClient,
{ loading: loadingCreateAppAuthClient, error: createAppAuthClientError },
@@ -28,8 +31,10 @@ function AdminApplicationCreateAuthClient(props) {
refetchQueries: ['GetAppAuthClients'],
context: { autoSnackbar: false },
});
const submitHandler = async (values) => {
let appConfigId = appConfig?.id;
if (!appConfigId) {
const { data: appConfigData } = await createAppConfig({
variables: {
@@ -41,8 +46,10 @@ function AdminApplicationCreateAuthClient(props) {
},
},
});
appConfigId = appConfigData.createAppConfig.id;
}
const { name, active, ...formattedAuthDefaults } = values;
await createAppAuthClient({
variables: {
@@ -54,22 +61,28 @@ function AdminApplicationCreateAuthClient(props) {
},
},
});
onClose();
};
const getAuthFieldsDefaultValues = useCallback(() => {
if (!auth?.fields) {
if (!auth?.data?.fields) {
return {};
}
const defaultValues = {};
auth.fields.forEach((field) => {
auth.data.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]);
}, [auth?.data?.fields]);
const defaultValues = useMemo(
() => ({
name: '',
@@ -78,6 +91,7 @@ function AdminApplicationCreateAuthClient(props) {
}),
[getAuthFieldsDefaultValues],
);
return (
<AdminApplicationAuthClientDialog
onClose={onClose}
@@ -85,7 +99,7 @@ function AdminApplicationCreateAuthClient(props) {
title={formatMessage('createAuthClient.title')}
loading={loadingAppConfig}
submitHandler={submitHandler}
authFields={auth?.fields}
authFields={auth?.data?.fields}
submitting={loadingCreateAppConfig || loadingCreateAppAuthClient}
defaultValues={defaultValues}
/>

View File

@@ -8,23 +8,29 @@ import ListItem from '@mui/material/ListItem';
import TextField from '@mui/material/TextField';
import Autocomplete from '@mui/material/Autocomplete';
import Chip from '@mui/material/Chip';
import useFormatMessage from 'hooks/useFormatMessage';
import useApps from 'hooks/useApps';
import { EditorContext } from 'contexts/Editor';
import FlowSubstepTitle from 'components/FlowSubstepTitle';
import { StepPropType, SubstepPropType } from 'propTypes/propTypes';
import useTriggers from 'hooks/useTriggers';
import useActions from 'hooks/useActions';
const optionGenerator = (app) => ({
label: app.name,
value: app.key,
});
const eventOptionGenerator = (app) => ({
label: app.name,
value: app.key,
type: app?.type,
});
const getOption = (options, selectedOptionValue) =>
options.find((option) => option.value === selectedOptionValue);
function ChooseAppAndEventSubstep(props) {
const {
substep,
@@ -39,26 +45,48 @@ function ChooseAppAndEventSubstep(props) {
const editorContext = React.useContext(EditorContext);
const isTrigger = step.type === 'trigger';
const isAction = step.type === 'action';
const { apps } = useApps({
onlyWithTriggers: isTrigger,
onlyWithActions: isAction,
});
const app = apps?.find((currentApp) => currentApp.key === step.appKey);
const appOptions = React.useMemo(
() => apps?.map((app) => optionGenerator(app)) || [],
[apps],
const useAppsOptions = {};
if (isTrigger) {
useAppsOptions.onlyWithTriggers = true;
}
if (isAction) {
useAppsOptions.onlyWithActions = true;
}
const { data: apps } = useApps(useAppsOptions);
const app = apps?.data?.find(
(currentApp) => currentApp?.key === step?.appKey,
);
const actionsOrTriggers = (isTrigger ? app?.triggers : app?.actions) || [];
const { data: triggers } = useTriggers(app?.key);
const { data: actions } = useActions(app?.key);
const appOptions = React.useMemo(
() => apps?.data?.map((app) => optionGenerator(app)) || [],
[apps?.data],
);
const actionsOrTriggers = (isTrigger ? triggers?.data : actions?.data) || [];
const actionOrTriggerOptions = React.useMemo(
() => actionsOrTriggers.map((trigger) => eventOptionGenerator(trigger)),
[app?.key],
[actionsOrTriggers],
);
const selectedActionOrTrigger = actionsOrTriggers.find(
(actionOrTrigger) => actionOrTrigger.key === step?.key,
);
const isWebhook = isTrigger && selectedActionOrTrigger?.type === 'webhook';
const { name } = substep;
const valid = !!step.key && !!step.appKey;
// placeholders
const onEventChange = React.useCallback(
(event, selectedOption) => {
@@ -79,6 +107,7 @@ function ChooseAppAndEventSubstep(props) {
},
[step, onChange],
);
const onAppChange = React.useCallback(
(event, selectedOption) => {
if (typeof selectedOption === 'object') {
@@ -100,7 +129,9 @@ function ChooseAppAndEventSubstep(props) {
},
[step, onChange],
);
const onToggle = expanded ? onCollapse : onExpand;
return (
<React.Fragment>
<FlowSubstepTitle

View File

@@ -176,6 +176,7 @@ function ChooseConnectionSubstep(props) {
}
}, [step.connection?.id, retestConnection]);
const onToggle = expanded ? onCollapse : onExpand;
return (
<React.Fragment>
<FlowSubstepTitle

View File

@@ -9,6 +9,7 @@ import Tab from '@mui/material/Tab';
import Typography from '@mui/material/Typography';
import Tooltip from '@mui/material/Tooltip';
import Box from '@mui/material/Box';
import TabPanel from 'components/TabPanel';
import SearchableJSONViewer from 'components/SearchableJSONViewer';
import AppIcon from 'components/AppIcon';
@@ -26,11 +27,13 @@ import { ExecutionStepPropType, StepPropType } from 'propTypes/propTypes';
function ExecutionStepId(props) {
const formatMessage = useFormatMessage();
const id = (
<Typography variant="caption" component="span">
{props.id}
</Typography>
);
return (
<Box sx={{ display: 'flex' }} gridArea="id">
<Typography variant="caption" fontWeight="bold">
@@ -48,6 +51,7 @@ function ExecutionStepDate(props) {
const formatMessage = useFormatMessage();
const createdAt = DateTime.fromMillis(parseInt(props.createdAt, 10));
const relativeCreatedAt = createdAt.toRelative();
return (
<Tooltip
title={createdAt.toLocaleString(DateTime.DATETIME_FULL_WITH_SECONDS)}
@@ -75,15 +79,27 @@ function ExecutionStep(props) {
const isTrigger = step.type === 'trigger';
const isAction = step.type === 'action';
const formatMessage = useFormatMessage();
const { apps } = useApps({
onlyWithTriggers: isTrigger,
onlyWithActions: isAction,
});
const app = apps?.find((currentApp) => currentApp.key === step.appKey);
if (!apps) return null;
const useAppsOptions = {};
if (isTrigger) {
useAppsOptions.onlyWithTriggers = true;
}
if (isAction) {
useAppsOptions.onlyWithActions = true;
}
const { data: apps } = useApps(useAppsOptions);
const app = apps?.data?.find((currentApp) => currentApp.key === step.appKey);
if (!apps?.data) return null;
const validationStatusIcon =
executionStep.status === 'success' ? validIcon : errorIcon;
const hasError = !!executionStep.errorDetails;
return (
<Wrapper elevation={1} data-test="execution-step">
<Header>

View File

@@ -14,6 +14,7 @@ import CircularProgress from '@mui/material/CircularProgress';
import CheckCircleIcon from '@mui/icons-material/CheckCircle';
import { yupResolver } from '@hookform/resolvers/yup';
import * as yup from 'yup';
import { EditorContext } from 'contexts/Editor';
import { StepExecutionsProvider } from 'contexts/StepExecutions';
import TestSubstep from 'components/TestSubstep';
@@ -35,6 +36,10 @@ import {
} from './style';
import isEmpty from 'helpers/isEmpty';
import { StepPropType } from 'propTypes/propTypes';
import useTriggers from 'hooks/useTriggers';
import useActions from 'hooks/useActions';
import useTriggerSubsteps from 'hooks/useTriggerSubsteps';
import useActionSubsteps from 'hooks/useActionSubsteps';
const validIcon = <CheckCircleIcon color="success" />;
const errorIcon = <ErrorIcon color="error" />;
@@ -83,6 +88,7 @@ function generateValidationSchema(substeps) {
}
}
}
return {
...allValidations,
...substepArgumentValidations,
@@ -90,9 +96,11 @@ function generateValidationSchema(substeps) {
},
{},
);
const validationSchema = yup.object({
parameters: yup.object(fieldValidations),
});
return yupResolver(validationSchema);
}
@@ -106,16 +114,25 @@ function FlowStep(props) {
const isAction = step.type === 'action';
const formatMessage = useFormatMessage();
const [currentSubstep, setCurrentSubstep] = React.useState(0);
const { apps } = useApps({
onlyWithTriggers: isTrigger,
onlyWithActions: isAction,
});
const useAppsOptions = {};
if (isTrigger) {
useAppsOptions.onlyWithTriggers = true;
}
if (isAction) {
useAppsOptions.onlyWithActions = true;
}
const { data: apps } = useApps(useAppsOptions);
const [
getStepWithTestExecutions,
{ data: stepWithTestExecutionsData, called: stepWithTestExecutionsCalled },
] = useLazyQuery(GET_STEP_WITH_TEST_EXECUTIONS, {
fetchPolicy: 'network-only',
});
React.useEffect(() => {
if (!stepWithTestExecutionsCalled && !collapsed && !isTrigger) {
getStepWithTestExecutions({
@@ -131,26 +148,56 @@ function FlowStep(props) {
step.id,
isTrigger,
]);
const app = apps?.find((currentApp) => currentApp.key === step.appKey);
const actionsOrTriggers = (isTrigger ? app?.triggers : app?.actions) || [];
const app = apps?.data?.find((currentApp) => currentApp.key === step.appKey);
const { data: triggers } = useTriggers(app?.key);
const { data: actions } = useActions(app?.key);
const actionsOrTriggers = (isTrigger ? triggers?.data : actions?.data) || [];
const actionOrTrigger = actionsOrTriggers?.find(
({ key }) => key === step.key,
);
const substeps = actionOrTrigger?.substeps || [];
const { data: triggerSubsteps } = useTriggerSubsteps(
app?.key,
actionOrTrigger?.key,
);
const triggerSubstepsData = triggerSubsteps?.data || [];
const { data: actionSubsteps } = useActionSubsteps(
app?.key,
actionOrTrigger?.key,
);
const actionSubstepsData = actionSubsteps?.data || [];
const substeps =
triggerSubstepsData.length > 0
? triggerSubstepsData
: actionSubstepsData || [];
const handleChange = React.useCallback(({ step }) => {
onChange(step);
}, []);
const expandNextStep = React.useCallback(() => {
setCurrentSubstep((currentSubstep) => (currentSubstep ?? 0) + 1);
}, []);
const handleSubmit = (val) => {
handleChange({ step: val });
};
const stepValidationSchema = React.useMemo(
() => generateValidationSchema(substeps),
[substeps],
);
if (!apps) {
if (!apps?.data) {
return (
<CircularProgress
data-test="step-circular-loader"
@@ -158,22 +205,29 @@ function FlowStep(props) {
/>
);
}
const onContextMenuClose = (event) => {
event.stopPropagation();
setAnchorEl(null);
};
const onContextMenuClick = (event) => {
event.stopPropagation();
setAnchorEl(contextButtonRef.current);
};
const onOpen = () => collapsed && props.onOpen?.();
const onClose = () => props.onClose?.();
const toggleSubstep = (substepIndex) =>
setCurrentSubstep((value) =>
value !== substepIndex ? substepIndex : null,
);
const validationStatusIcon =
step.status === 'completed' ? validIcon : errorIcon;
return (
<Wrapper
elevation={collapsed ? 1 : 4}

View File

@@ -1,105 +0,0 @@
import { gql } from '@apollo/client';
export const GET_APP = gql`
query GetApp($key: String!) {
getApp(key: $key) {
name
key
iconUrl
docUrl
authDocUrl
primaryColor
supportsConnections
auth {
fields {
key
label
type
required
readOnly
value
description
docUrl
clickToCopy
options {
label
value
}
}
authenticationSteps {
type
name
arguments {
name
value
type
properties {
name
value
}
}
}
sharedAuthenticationSteps {
type
name
arguments {
name
value
type
properties {
name
value
}
}
}
reconnectionSteps {
type
name
arguments {
name
value
type
properties {
name
value
}
}
}
sharedReconnectionSteps {
type
name
arguments {
name
value
type
properties {
name
value
}
}
}
}
connections {
id
}
triggers {
name
key
type
showWebhookUrl
pollInterval
description
substeps {
name
}
}
actions {
name
key
description
substeps {
name
}
}
}
}
`;

View File

@@ -1,234 +0,0 @@
import { gql } from '@apollo/client';
export const GET_APPS = gql`
query GetApps(
$name: String
$onlyWithTriggers: Boolean
$onlyWithActions: Boolean
) {
getApps(
name: $name
onlyWithTriggers: $onlyWithTriggers
onlyWithActions: $onlyWithActions
) {
name
key
iconUrl
docUrl
authDocUrl
primaryColor
connectionCount
flowCount
supportsConnections
auth {
fields {
key
label
type
required
readOnly
value
placeholder
description
docUrl
clickToCopy
options {
label
value
}
}
authenticationSteps {
type
name
arguments {
name
value
type
properties {
name
value
}
}
}
sharedAuthenticationSteps {
type
name
arguments {
name
value
type
properties {
name
value
}
}
}
reconnectionSteps {
type
name
arguments {
name
value
type
properties {
name
value
}
}
}
sharedReconnectionSteps {
type
name
arguments {
name
value
type
properties {
name
value
}
}
}
}
triggers {
name
key
type
showWebhookUrl
pollInterval
description
substeps {
key
name
arguments {
label
key
type
required
description
variables
dependsOn
options {
label
value
}
source {
type
name
arguments {
name
value
}
}
additionalFields {
type
name
arguments {
name
value
}
}
fields {
label
key
type
required
description
variables
value
dependsOn
options {
label
value
}
source {
type
name
arguments {
name
value
}
}
additionalFields {
type
name
arguments {
name
value
}
}
}
}
}
}
actions {
name
key
description
substeps {
key
name
arguments {
label
key
type
required
description
variables
dependsOn
value
options {
label
value
}
source {
type
name
arguments {
name
value
}
}
additionalFields {
type
name
arguments {
name
value
}
}
fields {
label
key
type
required
description
variables
value
dependsOn
options {
label
value
}
source {
type
name
arguments {
name
value
}
}
additionalFields {
type
name
arguments {
name
value
}
}
}
}
}
}
}
}
`;

View File

@@ -0,0 +1,22 @@
import { useQuery } from '@tanstack/react-query';
import api from 'helpers/api';
export default function useActionSubsteps(appKey, actionKey) {
const query = useQuery({
queryKey: ['actionSubsteps', appKey, actionKey],
queryFn: async ({ payload, signal }) => {
const { data } = await api.get(
`/v1/apps/${appKey}/actions/${actionKey}/substeps`,
{
signal,
},
);
return data;
},
enabled: !!appKey,
});
return query;
}

View File

@@ -0,0 +1,19 @@
import { useQuery } from '@tanstack/react-query';
import api from 'helpers/api';
export default function useActions(appKey) {
const query = useQuery({
queryKey: ['actions', appKey],
queryFn: async ({ payload, signal }) => {
const { data } = await api.get(`/v1/apps/${appKey}/actions`, {
signal,
});
return data;
},
enabled: !!appKey,
});
return query;
}

View File

@@ -6,7 +6,9 @@ export default function useAdminAppAuthClient(id) {
const query = useQuery({
queryKey: ['adminAppAuthClient', id],
queryFn: async ({ payload, signal }) => {
const { data } = await api.get(`/v1/admin/app-auth-clients/${id}`);
const { data } = await api.get(`/v1/admin/app-auth-clients/${id}`, {
signal,
});
return data;
},

View File

@@ -1,12 +1,19 @@
import { useQuery } from '@apollo/client';
import { GET_APP } from 'graphql/queries/get-app';
export default function useApp(key) {
const { data, loading } = useQuery(GET_APP, {
variables: { key },
import { useQuery } from '@tanstack/react-query';
import api from 'helpers/api';
export default function useApp(appKey) {
const query = useQuery({
queryKey: ['app', appKey],
queryFn: async ({ payload, signal }) => {
const { data } = await api.get(`/v1/apps/${appKey}`, {
signal,
});
return data;
},
enabled: !!appKey,
});
const app = data?.getApp;
return {
app,
loading,
};
return query;
}

View File

@@ -0,0 +1,19 @@
import { useQuery } from '@tanstack/react-query';
import api from 'helpers/api';
export default function useAppAuth(appKey) {
const query = useQuery({
queryKey: ['appAuth', appKey],
queryFn: async ({ payload, signal }) => {
const { data } = await api.get(`/v1/apps/${appKey}/auth`, {
signal,
});
return data;
},
enabled: !!appKey,
});
return query;
}

View File

@@ -1,12 +1,19 @@
import { useQuery } from '@apollo/client';
import { GET_APPS } from 'graphql/queries/get-apps';
import { useQuery } from '@tanstack/react-query';
import api from 'helpers/api';
export default function useApps(variables) {
const { data, loading } = useQuery(GET_APPS, {
variables,
const query = useQuery({
queryKey: ['apps', variables],
queryFn: async ({ payload, signal }) => {
const { data } = await api.get('/v1/apps', {
params: variables,
signal,
});
return data;
},
});
const apps = data?.getApps;
return {
apps,
loading,
};
return query;
}

View File

@@ -1,7 +1,9 @@
import * as React from 'react';
import { processStep } from 'helpers/authenticationSteps';
import computeAuthStepVariables from 'helpers/computeAuthStepVariables';
import useApp from './useApp';
import useAppAuth from './useAppAuth';
function getSteps(auth, hasConnection, useShared) {
if (hasConnection) {
if (useShared) {
@@ -9,19 +11,24 @@ function getSteps(auth, hasConnection, useShared) {
}
return auth?.reconnectionSteps;
}
if (useShared) {
return auth?.sharedAuthenticationSteps;
}
return auth?.authenticationSteps;
}
export default function useAuthenticateApp(payload) {
const { appKey, appAuthClientId, connectionId, useShared = false } = payload;
const { app } = useApp(appKey);
const { data: auth } = useAppAuth(appKey);
const [authenticationInProgress, setAuthenticationInProgress] =
React.useState(false);
const steps = getSteps(app?.auth, !!connectionId, useShared);
const steps = getSteps(auth?.data, !!connectionId, useShared);
const authenticate = React.useMemo(() => {
if (!steps?.length) return;
return async function authenticate(payload = {}) {
const { fields } = payload;
setAuthenticationInProgress(true);
@@ -34,9 +41,11 @@ export default function useAuthenticateApp(payload) {
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;
@@ -46,6 +55,7 @@ export default function useAuthenticateApp(payload) {
throw err;
}
stepIndex++;
if (stepIndex === steps.length) {
return response;
}
@@ -53,6 +63,7 @@ export default function useAuthenticateApp(payload) {
}
};
}, [steps, appKey, appAuthClientId, connectionId]);
return {
authenticate,
inProgress: authenticationInProgress,

View File

@@ -0,0 +1,30 @@
import { useMutation } from '@tanstack/react-query';
import api from 'helpers/api';
import React from 'react';
export default function useLazyApps({ appName, onSuccess }) {
const abortControllerRef = React.useRef(new AbortController());
React.useEffect(() => {
abortControllerRef.current = new AbortController();
return () => {
abortControllerRef.current?.abort();
};
}, [appName]);
const query = useMutation({
mutationFn: async ({ payload }) => {
const { data } = await api.get('/v1/apps', {
params: { name: appName },
signal: abortControllerRef.current.signal,
});
return data;
},
onSuccess,
});
return query;
}

View File

@@ -0,0 +1,22 @@
import { useQuery } from '@tanstack/react-query';
import api from 'helpers/api';
export default function useTriggerSubsteps(appKey, triggerKey) {
const query = useQuery({
queryKey: ['triggerSubsteps', appKey, triggerKey],
queryFn: async ({ payload, signal }) => {
const { data } = await api.get(
`/v1/apps/${appKey}/triggers/${triggerKey}/substeps`,
{
signal,
},
);
return data;
},
enabled: !!appKey,
});
return query;
}

View File

@@ -0,0 +1,19 @@
import { useQuery } from '@tanstack/react-query';
import api from 'helpers/api';
export default function useTriggers(appKey) {
const query = useQuery({
queryKey: ['triggers', appKey],
queryFn: async ({ payload, signal }) => {
const { data } = await api.get(`/v1/apps/${appKey}/triggers`, {
signal,
});
return data;
},
enabled: !!appKey,
});
return query;
}

View File

@@ -1,5 +1,4 @@
import * as React from 'react';
import { useQuery } from '@apollo/client';
import {
Link,
Route,
@@ -15,8 +14,8 @@ import Box from '@mui/material/Box';
import Grid from '@mui/material/Grid';
import Tabs from '@mui/material/Tabs';
import Tab from '@mui/material/Tab';
import useFormatMessage from 'hooks/useFormatMessage';
import { GET_APP } from 'graphql/queries/get-app';
import * as URLS from 'config/urls';
import AppIcon from 'components/AppIcon';
import Container from 'components/Container';
@@ -25,6 +24,8 @@ import AdminApplicationSettings from 'components/AdminApplicationSettings';
import AdminApplicationAuthClients from 'components/AdminApplicationAuthClients';
import AdminApplicationCreateAuthClient from 'components/AdminApplicationCreateAuthClient';
import AdminApplicationUpdateAuthClient from 'components/AdminApplicationUpdateAuthClient';
import useApp from 'hooks/useApp';
export default function AdminApplication() {
const theme = useTheme();
const matchSmallScreens = useMediaQuery(theme.breakpoints.down('md'));
@@ -43,10 +44,15 @@ export default function AdminApplication() {
end: false,
});
const { appKey } = useParams();
const { data, loading } = useQuery(GET_APP, { variables: { key: appKey } });
const app = data?.getApp || {};
const { data, loading } = useApp(appKey);
const app = data?.data || {};
const goToAuthClientsPage = () => navigate('auth-clients');
if (loading) return null;
return (
<>
<Container sx={{ py: 3, display: 'flex', justifyContent: 'center' }}>

View File

@@ -2,24 +2,25 @@ import * as React from 'react';
import Grid from '@mui/material/Grid';
import CircularProgress from '@mui/material/CircularProgress';
import Divider from '@mui/material/Divider';
import { useQuery } from '@apollo/client';
import PageTitle from 'components/PageTitle';
import Container from 'components/Container';
import SearchInput from 'components/SearchInput';
import AppRow from 'components/AppRow';
import * as URLS from 'config/urls';
import useFormatMessage from 'hooks/useFormatMessage';
import { GET_APPS } from 'graphql/queries/get-apps';
import useApps from 'hooks/useApps';
function AdminApplications() {
const formatMessage = useFormatMessage();
const [appName, setAppName] = React.useState(null);
const { data, loading: appsLoading } = useQuery(GET_APPS, {
variables: { name: appName },
});
const apps = data?.getApps;
const { data: apps, isLoading: isAppsLoading } = useApps(appName);
const onSearchChange = React.useCallback((event) => {
setAppName(event.target.value);
}, []);
return (
<Container sx={{ py: 3, display: 'flex', justifyContent: 'center' }}>
<Grid container item xs={12} sm={10} md={9}>
@@ -36,15 +37,15 @@ function AdminApplications() {
<Divider sx={{ mt: [2, 0], mb: 2 }} />
</Grid>
{appsLoading && (
{isAppsLoading && (
<CircularProgress
data-test="apps-loader"
sx={{ display: 'block', margin: '20px auto' }}
/>
)}
{!appsLoading &&
apps?.map((app) => (
{!isAppsLoading &&
apps?.data?.map((app) => (
<Grid item xs={12} key={app.name}>
<AppRow application={app} url={URLS.ADMIN_APP(app.key)} />
</Grid>

View File

@@ -1,5 +1,4 @@
import * as React from 'react';
import { useQuery } from '@apollo/client';
import {
Link,
Route,
@@ -17,9 +16,9 @@ import Grid from '@mui/material/Grid';
import Tabs from '@mui/material/Tabs';
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';
@@ -29,9 +28,12 @@ import AddAppConnection from 'components/AddAppConnection';
import AppIcon from 'components/AppIcon';
import Container from 'components/Container';
import PageTitle from 'components/PageTitle';
import useApp from 'hooks/useApp';
const ReconnectConnection = (props) => {
const { application, onClose } = props;
const { connectionId } = useParams();
return (
<AddAppConnection
onClose={onClose}
@@ -40,6 +42,7 @@ const ReconnectConnection = (props) => {
/>
);
};
export default function Application() {
const theme = useTheme();
const matchSmallScreens = useMediaQuery(theme.breakpoints.down('md'));
@@ -52,11 +55,15 @@ export default function Application() {
const [searchParams] = useSearchParams();
const { appKey } = useParams();
const navigate = useNavigate();
const { data, loading } = useQuery(GET_APP, { variables: { key: appKey } });
const { data, loading } = useApp(appKey);
const app = data?.data || {};
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;
@@ -68,6 +75,7 @@ export default function Application() {
to: URLS.APP_ADD_CONNECTION(appKey, appConfig?.canConnect),
},
];
if (shouldHaveCustomConnection) {
options.push({
label: formatMessage('app.addCustomConnection'),
@@ -76,9 +84,12 @@ export default function Application() {
to: URLS.APP_ADD_CONNECTION(appKey),
});
}
return options;
}, [appKey, appConfig]);
if (loading) return null;
return (
<>
<Box sx={{ py: 3 }}>