diff --git a/packages/backend/src/graphql/queries/get-apps.js b/packages/backend/src/graphql/queries/get-apps.js deleted file mode 100644 index 2c3a78a0..00000000 --- a/packages/backend/src/graphql/queries/get-apps.js +++ /dev/null @@ -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; diff --git a/packages/backend/src/graphql/query-resolvers.js b/packages/backend/src/graphql/query-resolvers.js index f4535809..71e75e9d 100644 --- a/packages/backend/src/graphql/query-resolvers.js +++ b/packages/backend/src/graphql/query-resolvers.js @@ -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, diff --git a/packages/backend/src/graphql/schema.graphql b/packages/backend/src/graphql/schema.graphql index f244b2f0..ddbdfdf6 100644 --- a/packages/backend/src/graphql/schema.graphql +++ b/packages/backend/src/graphql/schema.graphql @@ -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 diff --git a/packages/web/src/components/AddAppConnection/index.jsx b/packages/web/src/components/AddAppConnection/index.jsx index 3b9737ea..41d72328 100644 --- a/packages/web/src/components/AddAppConnection/index.jsx +++ b/packages/web/src/components/AddAppConnection/index.jsx @@ -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 ( ); + if (appAuthClientId) return ; + return ( @@ -121,7 +135,7 @@ function AddAppConnection(props) {
- {auth?.fields?.map((field) => ( + {auth?.data?.fields?.map((field) => ( ))} diff --git a/packages/web/src/components/AddNewAppConnection/index.jsx b/packages/web/src/components/AddNewAppConnection/index.jsx index def8b3cc..0ba62b66 100644 --- a/packages/web/src/components/AddNewAppConnection/index.jsx +++ b/packages/web/src/components/AddNewAppConnection/index.jsx @@ -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 ( - {loading && ( + {isLoading && ( )} - {!loading && - data?.getApps?.map((app) => ( + {!isLoading && + apps?.data.map((app) => ( { 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 ( diff --git a/packages/web/src/components/ChooseAppAndEventSubstep/index.jsx b/packages/web/src/components/ChooseAppAndEventSubstep/index.jsx index aa5da519..de37fcde 100644 --- a/packages/web/src/components/ChooseAppAndEventSubstep/index.jsx +++ b/packages/web/src/components/ChooseAppAndEventSubstep/index.jsx @@ -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 ( {props.id} ); + return ( @@ -48,6 +51,7 @@ function ExecutionStepDate(props) { const formatMessage = useFormatMessage(); const createdAt = DateTime.fromMillis(parseInt(props.createdAt, 10)); const relativeCreatedAt = createdAt.toRelative(); + return ( 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 (
diff --git a/packages/web/src/components/FlowStep/index.jsx b/packages/web/src/components/FlowStep/index.jsx index 8d360498..b999ec4f 100644 --- a/packages/web/src/components/FlowStep/index.jsx +++ b/packages/web/src/components/FlowStep/index.jsx @@ -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 = ; const errorIcon = ; @@ -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 ( ); } + 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 ( { + const { data } = await api.get( + `/v1/apps/${appKey}/actions/${actionKey}/substeps`, + { + signal, + }, + ); + + return data; + }, + enabled: !!appKey, + }); + + return query; +} diff --git a/packages/web/src/hooks/useActions.js b/packages/web/src/hooks/useActions.js new file mode 100644 index 00000000..41295ee9 --- /dev/null +++ b/packages/web/src/hooks/useActions.js @@ -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; +} diff --git a/packages/web/src/hooks/useAdminAppAuthClient.ee.js b/packages/web/src/hooks/useAdminAppAuthClient.ee.js index 29b23e75..32b31f27 100644 --- a/packages/web/src/hooks/useAdminAppAuthClient.ee.js +++ b/packages/web/src/hooks/useAdminAppAuthClient.ee.js @@ -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; }, diff --git a/packages/web/src/hooks/useApp.js b/packages/web/src/hooks/useApp.js index 0fcbc51d..bc4edf2b 100644 --- a/packages/web/src/hooks/useApp.js +++ b/packages/web/src/hooks/useApp.js @@ -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; } diff --git a/packages/web/src/hooks/useAppAuth.js b/packages/web/src/hooks/useAppAuth.js new file mode 100644 index 00000000..d66f556a --- /dev/null +++ b/packages/web/src/hooks/useAppAuth.js @@ -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; +} diff --git a/packages/web/src/hooks/useApps.js b/packages/web/src/hooks/useApps.js index 85511ee3..568a33c8 100644 --- a/packages/web/src/hooks/useApps.js +++ b/packages/web/src/hooks/useApps.js @@ -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; } diff --git a/packages/web/src/hooks/useAuthenticateApp.ee.js b/packages/web/src/hooks/useAuthenticateApp.ee.js index 77eb8465..68c8af8a 100644 --- a/packages/web/src/hooks/useAuthenticateApp.ee.js +++ b/packages/web/src/hooks/useAuthenticateApp.ee.js @@ -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, diff --git a/packages/web/src/hooks/useLazyApps.js b/packages/web/src/hooks/useLazyApps.js new file mode 100644 index 00000000..291ffcc2 --- /dev/null +++ b/packages/web/src/hooks/useLazyApps.js @@ -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; +} diff --git a/packages/web/src/hooks/useTriggerSubsteps.js b/packages/web/src/hooks/useTriggerSubsteps.js new file mode 100644 index 00000000..d697ba5e --- /dev/null +++ b/packages/web/src/hooks/useTriggerSubsteps.js @@ -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; +} diff --git a/packages/web/src/hooks/useTriggers.js b/packages/web/src/hooks/useTriggers.js new file mode 100644 index 00000000..c0caa287 --- /dev/null +++ b/packages/web/src/hooks/useTriggers.js @@ -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; +} diff --git a/packages/web/src/pages/AdminApplication/index.jsx b/packages/web/src/pages/AdminApplication/index.jsx index b42d3d11..44e73085 100644 --- a/packages/web/src/pages/AdminApplication/index.jsx +++ b/packages/web/src/pages/AdminApplication/index.jsx @@ -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 ( <> diff --git a/packages/web/src/pages/AdminApplications/index.jsx b/packages/web/src/pages/AdminApplications/index.jsx index 54e5ce5e..a4c65316 100644 --- a/packages/web/src/pages/AdminApplications/index.jsx +++ b/packages/web/src/pages/AdminApplications/index.jsx @@ -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 ( @@ -36,15 +37,15 @@ function AdminApplications() { - {appsLoading && ( + {isAppsLoading && ( )} - {!appsLoading && - apps?.map((app) => ( + {!isAppsLoading && + apps?.data?.map((app) => ( diff --git a/packages/web/src/pages/Application/index.jsx b/packages/web/src/pages/Application/index.jsx index a7ff4966..f7588d1a 100644 --- a/packages/web/src/pages/Application/index.jsx +++ b/packages/web/src/pages/Application/index.jsx @@ -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 ( { /> ); }; + 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 ( <>