diff --git a/web-app/pontoon/src/app/destinations/new/page.js b/web-app/pontoon/src/app/destinations/new/page.js index bcfef50..2225b13 100644 --- a/web-app/pontoon/src/app/destinations/new/page.js +++ b/web-app/pontoon/src/app/destinations/new/page.js @@ -9,11 +9,13 @@ import { Checkbox, CircularProgress, FormHelperText, + Alert, } from "@mui/material"; import { useState } from "react"; import { Form, Formik } from "formik"; import * as Yup from "yup"; import useSWRMutation from "swr/mutation"; +import { mutate } from "swr"; import Link from "next/link"; import { ChevronLeft } from "@mui/icons-material"; import { useRouter } from "next/navigation"; @@ -51,126 +53,139 @@ import { getPostgresInitialValues, } from "@/app/components/forms/connection-details/PostgresConnectionDetails"; -const AddDestination = () => { - const Status = Object.freeze({ - NOT_STARTED: 0, - LOADING: 1, - SUCCESS: 2, - FAILED: 3, - }); - - // state of the connection test - const [testConnectionStatus, setTestConnectionStatus] = useState( - Status.NOT_STARTED - ); - - // state for the source being created - const [destinationCreated, setDestinationCreated] = useState(false); - const [destinationId, setDestinationId] = useState(""); - - const router = useRouter(); - - const createDestination = (params) => { - return postRequest("/destinations", { arg: params }); - }; - - const updateDestination = (destinationId, params) => { - return putRequest(`/destinations/${destinationId}`, { arg: params }); - }; - - const startDestinationCheck = (destinationId) => { - return postRequest(`/destinations/${destinationId}/check`, { arg: {} }); +const testConnection = async (key, { arg: values, destinationId }) => { + const params = { + destination_name: values.destination_name, + vendor_type: values.vendor_type, + recipient_id: values.recipient_id, + schedule: { + type: "INCREMENTAL", + frequency: values.schedule_frequency, + day: values.schedule_day, + hour: values.schedule_hour, + minute: 0, + }, + models: values.selectedModels, + connection_info: { + vendor_type: values.vendor_type, + ...values[values.vendor_type], + }, }; - const waitForDestinationCheck = (destinationId, taskId) => { - return pollTaskStatus(`/destinations/${destinationId}/check/${taskId}`); - }; + try { + if (!destinationId) { + console.log("Creating destination"); + const result = await postRequest("/destinations", { arg: params }); + const check = await postRequest( + `/destinations/${result.destination_id}/check`, + { + arg: {}, + } + ); + const status = await pollTaskStatus( + `/destinations/${result.destination_id}/check/${check.task_id}` + ); + + if ( + status.success === undefined || + status.success === null || + status.success === false + ) { + throw new Error(status.cause); + } - const updateCheckState = (result) => { - if (!result.success || result.success === false) { - setTestConnectionStatus(Status.FAILED); + return { + destination_id: result.destination_id, + success: status.success, + }; } else { - setTestConnectionStatus(Status.SUCCESS); + console.log("Updating destination"); + const result = await putRequest(`/destinations/${destinationId}`, { + arg: params, + }); + const check = await postRequest(`/destinations/${destinationId}/check`, { + arg: {}, + }); + const status = await pollTaskStatus( + `/destinations/${destinationId}/check/${check.task_id}` + ); + + if ( + status.success === undefined || + status.success === null || + status.success === false + ) { + throw new Error(status.cause); + } + + return { + destination_id: result.destination_id, + success: status.success, + }; } - }; + } catch (e) { + console.warn("Error testing connection: ", e); + throw e; + } +}; + +const enableDestination = async (key, { arg: destinationId }) => { + try { + const result = await putRequest(`/destinations/${destinationId}`, { + arg: { is_enabled: true, state: "CREATED" }, + }); + // Invalidate the destinations cache since there's a new destination + mutate("/destinations"); + return result; + } catch (e) { + console.warn("Error enabling destination: ", e); + throw e; + } +}; +const AddDestination = () => { const { - data: recipients, - error: recipientsError, - isLoading: recipientsLoading, - } = useSWR("/recipients", getRequest); + trigger: testConnectionTrigger, + data: testConnectionResult, + error: testConnectionError, + isMutating: isTestConnectionMutating, + } = useSWRMutation("/destinations/test_connection", testConnection); + const destinationId = testConnectionResult?.destination_id; const { - data: models, - error: modelsError, - isLoading: modelsLoading, - } = useSWR("/models", getRequest); - const modelIds = models?.map((m) => m.model_id); + trigger: enableDestinationTrigger, + data: enableDestinationResult, + error: enableDestinationError, + isMutating: isEnableDestinationMutating, + } = useSWRMutation( + (destinationId) => `/destinations/${destinationId}`, + enableDestination + ); + + const router = useRouter(); // form submission handler const handleCreateAndCheckDestination = async (values, validateForm) => { - setTestConnectionStatus(Status.LOADING); const errors = await validateForm(values); if (Object.keys(errors).length > 0) { console.log(errors); - setTestConnectionStatus(Status.FAILED); return; } - const params = { - destination_name: values.destination_name, - vendor_type: values.vendor_type, - recipient_id: values.recipient_id, - schedule: { - type: "INCREMENTAL", - frequency: values.schedule_frequency, - day: values.schedule_day, - hour: values.schedule_hour, - minute: 0, - }, - models: values.selectedModels, - connection_info: { - vendor_type: values.vendor_type, - ...values[values.vendor_type], - }, - }; try { - if (!destinationCreated) { - console.log("Creating destination"); - const result = await createDestination(params); - const check = await startDestinationCheck(result.destination_id); - const status = await waitForDestinationCheck( - result.destination_id, - check.task_id - ); - - setDestinationCreated(true); - setDestinationId(result.destination_id); - updateCheckState(status); - } else { - console.log("Updating destination"); - await updateDestination(destinationId, params); - const check = await startDestinationCheck(destinationId); - const status = await waitForDestinationCheck( - destinationId, - check.task_id - ); - updateCheckState(status); - } + const result = await testConnectionTrigger(values, destinationId); } catch (e) { - setTestConnectionStatus(Status.FAILED); - } finally { + console.warn("Error testing connection: ", e); + return; } }; // when the create button is clicked - const handleEnableDestination = async () => { - if (destinationCreated) { + const handleEnableDestination = async (values) => { + console.log("Submitting form"); + if (destinationId) { try { - await updateDestination(destinationId, { - is_enabled: true, - state: "CREATED", - }); + await enableDestinationTrigger(destinationId); router.push("/destinations"); } catch (e) { console.log("Enabling destination failed: ", e); @@ -178,6 +193,19 @@ const AddDestination = () => { } }; + const { + data: recipients, + error: recipientsError, + isLoading: recipientsLoading, + } = useSWR("/recipients", getRequest); + + const { + data: models, + error: modelsError, + isLoading: modelsLoading, + } = useSWR("/models", getRequest); + const modelIds = models?.map((m) => m.model_id); + if (recipientsError || modelsError) { return Error with API; } @@ -389,7 +417,8 @@ const AddDestination = () => { variant="contained" disabled={ isValidating || - testConnectionStatus == Status.LOADING || + isTestConnectionMutating || + isEnableDestinationMutating || !isValid || !dirty } @@ -399,31 +428,50 @@ const AddDestination = () => { > Test Connection - {testConnectionStatus == Status.LOADING ? ( - - ) : null} - {testConnectionStatus == Status.SUCCESS ? ( - - ) : null} - {testConnectionStatus == Status.FAILED ? ( - - ) : null} + {renderTestConnectionStatus( + testConnectionResult, + testConnectionError, + isTestConnectionMutating + )} Testing the connection may take a few minutes. + {testConnectionError ? ( + + {testConnectionError.message} + + ) : null} + {enableDestinationError ? ( + + + {enableDestinationError.message} + + + ) : null} - + + + {isEnableDestinationMutating ? ( + + ) : null} + ); @@ -449,6 +497,23 @@ const renderConnectionDetails = (vendor_type, setFieldValue, values) => { } }; +const renderTestConnectionStatus = ( + testConnectionResult, + testConnectionError, + isTestConnectionMutating +) => { + if (isTestConnectionMutating) { + return ; + } + if (testConnectionError) { + return ; + } + if (testConnectionResult?.success === true) { + return ; + } + return null; +}; + const renderScheduleDetails = (schedule_frequency) => { return ( <> diff --git a/web-app/pontoon/src/app/sources/new/AddSourceForm.js b/web-app/pontoon/src/app/sources/new/AddSourceForm.js index 85d6b0b..a40bbc3 100644 --- a/web-app/pontoon/src/app/sources/new/AddSourceForm.js +++ b/web-app/pontoon/src/app/sources/new/AddSourceForm.js @@ -7,11 +7,11 @@ import { CircularProgress, Divider, FormHelperText, + Alert, } from "@mui/material"; -import { useState } from "react"; import { Form, Formik } from "formik"; import * as Yup from "yup"; -import useSWR from "swr"; +import { mutate } from "swr"; import useSWRMutation from "swr/mutation"; import Link from "next/link"; import { ChevronLeft } from "@mui/icons-material"; @@ -40,116 +40,124 @@ import { getPostgresValidation, getPostgresInitialValues, } from "@/app/components/forms/connection-details/PostgresConnectionDetails"; -import { - getRequest, - postRequest, - putRequest, - pollTaskStatus, -} from "@/app/api/requests"; +import { postRequest, putRequest, pollTaskStatus } from "@/app/api/requests"; -const AddSourceForm = () => { - const Status = Object.freeze({ - NOT_STARTED: 0, - LOADING: 1, - SUCCESS: 2, - FAILED: 3, - }); +const testConnection = async (key, { arg: values, sourceId }) => { + const params = { + source_name: values.source_name, + vendor_type: values.vendor_type, + connection_info: { + vendor_type: values.vendor_type, + ...values[values.vendor_type], + }, + }; - // After refreshing the page, making the first API call resets the form. - // In order to avoid the refresh happening during form validation, this api call is made here, - // even though it is not used for anything - const { - data: sources, - error: isError, - isLoading, - } = useSWR("/sources", getRequest); + try { + if (!sourceId) { + console.log("Creating source"); + const result = await postRequest("/sources", { arg: params }); + const check = await postRequest(`/sources/${result.source_id}/check`, { + arg: {}, + }); + const status = await pollTaskStatus( + `/sources/${result.source_id}/check/${check.task_id}` + ); - // state of the connection test - const [testConnectionStatus, setTestConnectionStatus] = useState( - Status.NOT_STARTED - ); + if ( + status.success === undefined || + status.success === null || + status.success === false + ) { + throw new Error(status.message); + } - // state for the source being created - const [sourceCreated, setSourceCreated] = useState(false); - const [sourceId, setSourceId] = useState(""); + return { + source_id: result.source_id, + success: status.success, + }; + } else { + console.log("Updating source"); + const result = await putRequest(`/sources/${sourceId}`, { arg: params }); + const check = await postRequest(`/sources/${sourceId}/check`, { + arg: {}, + }); + const status = await pollTaskStatus( + `/sources/${sourceId}/check/${check.task_id}` + ); - const router = useRouter(); + if ( + status.success === undefined || + status.success === null || + status.success === false + ) { + throw new Error(status.message); + } - const createSource = (params) => { - return postRequest("/sources", { arg: params }); - }; + return { + source_id: result.source_id, + success: status.success, + }; + } + } catch (e) { + console.warn("Error testing connection: ", e); + throw e; + } +}; - const updateSource = (sourceId, params) => { - return putRequest(`/sources/${sourceId}`, { arg: params }); - }; +const enableSource = async (key, { arg: sourceId }) => { + try { + const result = await putRequest(`/sources/${sourceId}`, { + arg: { is_enabled: true, state: "CREATED" }, + }); + // Invalidate the sources cache since there's a new source + mutate("/sources"); + return result; + } catch (e) { + console.warn("Error enabling source: ", e); + throw e; + } +}; - const startSourceCheck = (sourceId) => { - return postRequest(`/sources/${sourceId}/check`, { arg: {} }); - }; +const AddSourceForm = () => { + const { + trigger: testConnectionTrigger, + data: testConnectionResult, + error: testConnectionError, + isMutating: isTestConnectionMutating, + } = useSWRMutation("/sources/test_connection", testConnection); + const sourceId = testConnectionResult?.source_id; - const waitForSourceCheck = (sourceId, taskId) => { - return pollTaskStatus(`/sources/${sourceId}/check/${taskId}`); - }; + const { + trigger: enableSourceTrigger, + data: enableSourceResult, + error: enableSourceError, + isMutating: isEnableSourceMutating, + } = useSWRMutation((sourceId) => `/sources/${sourceId}`, enableSource); - const updateCheckState = (result) => { - if (!result.success || result.success === false) { - setTestConnectionStatus(Status.FAILED); - } else { - setTestConnectionStatus(Status.SUCCESS); - } - }; + const router = useRouter(); // form submission handler const handleCreateAndCheckSource = async (values, validateForm) => { - setTestConnectionStatus(Status.LOADING); const errors = await validateForm(values); if (Object.keys(errors).length > 0) { console.log(errors); - setTestConnectionStatus(Status.FAILED); return; } - - const params = { - source_name: values.source_name, - vendor_type: values.vendor_type, - connection_info: { - vendor_type: values.vendor_type, - ...values[values.vendor_type], - }, - }; try { - if (!sourceCreated) { - console.log("Creating source"); - const result = await createSource(params); - const check = await startSourceCheck(result.source_id); - const status = await waitForSourceCheck( - result.source_id, - check.task_id - ); - - setSourceCreated(true); - setSourceId(result.source_id); - updateCheckState(status); - } else { - console.log("Updating source"); - await updateSource(sourceId, params); - const check = await startSourceCheck(sourceId); - const status = await waitForSourceCheck(sourceId, check.task_id); - updateCheckState(status); - } + const result = await testConnectionTrigger(values, sourceId); } catch (e) { - setTestConnectionStatus(Status.FAILED); - } finally { + console.warn("Error testing connection: ", e); + return; } }; // when the create button is clicked const handleEnableSource = async (values) => { console.log("Submitting form"); - if (sourceCreated) { + if (sourceId) { try { - await updateSource(sourceId, { is_enabled: true, state: "CREATED" }); + await enableSourceTrigger(sourceId); router.push("/sources"); } catch (e) { console.log("Enabling source failed: ", e); @@ -243,7 +251,8 @@ const AddSourceForm = () => { variant="contained" disabled={ isValidating || - testConnectionStatus == Status.LOADING || + isTestConnectionMutating || + isEnableSourceMutating || !isValid || !dirty } @@ -253,35 +262,47 @@ const AddSourceForm = () => { > Test Connection - {testConnectionStatus == Status.LOADING ? ( - - ) : null} - {testConnectionStatus == Status.SUCCESS ? ( - - ) : null} - {testConnectionStatus == Status.FAILED ? ( - - ) : null} + {renderTestConnectionStatus( + testConnectionResult, + testConnectionError, + isTestConnectionMutating + )} Testing the connection may take up to a minute. + {testConnectionError ? ( + + {testConnectionError.message} + + ) : null} + {enableSourceError ? ( + + {enableSourceError.message} + + ) : null} + + + + {isEnableSourceMutating ? : null} - )} @@ -290,6 +311,23 @@ const AddSourceForm = () => { ); }; +const renderTestConnectionStatus = ( + testConnectionResult, + testConnectionError, + isTestConnectionMutating +) => { + if (isTestConnectionMutating) { + return ; + } + if (testConnectionError) { + return ; + } + if (testConnectionResult?.success === true) { + return ; + } + return null; +}; + const renderConnectionDetails = (vendor_type) => { switch (vendor_type) { case "snowflake":