Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

🪟 🔧 [DRAFT] Refactor ServiceForm and ConnectorCard to move Formik and ServiceFormContext on top level #18229

Closed
wants to merge 12 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 5 additions & 25 deletions airbyte-webapp/src/components/DeleteBlock/DeleteBlock.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,13 @@
import React, { useCallback } from "react";
import React from "react";
import { FormattedMessage } from "react-intl";
import { useNavigate } from "react-router-dom";
import styled from "styled-components";

import { H5 } from "components/base/Titles";
import { Button } from "components/ui/Button";
import { Card } from "components/ui/Card";

import { useConfirmationModalService } from "hooks/services/ConfirmationModal";

interface IProps {
type: "source" | "destination" | "connection";
onDelete: () => Promise<unknown>;
}
import { DeleteBlockProps } from "./interfaces";
import { useDeleteModal } from "./useDeleteModal";

const DeleteBlockComponent = styled(Card)`
margin-top: 12px;
Expand All @@ -30,23 +25,8 @@ const Text = styled.div`
white-space: pre-line;
`;

const DeleteBlock: React.FC<IProps> = ({ type, onDelete }) => {
const { openConfirmationModal, closeConfirmationModal } = useConfirmationModalService();
const navigate = useNavigate();

const onDeleteButtonClick = useCallback(() => {
openConfirmationModal({
text: `tables.${type}DeleteModalText`,
title: `tables.${type}DeleteConfirm`,
submitButtonText: "form.delete",
onSubmit: async () => {
await onDelete();
closeConfirmationModal();
navigate("../..");
},
submitButtonDataId: "delete",
});
}, [closeConfirmationModal, onDelete, openConfirmationModal, navigate, type]);
const DeleteBlock: React.FC<DeleteBlockProps> = ({ type, onDelete }) => {
const { onDeleteButtonClick } = useDeleteModal({ type, onDelete });

return (
<DeleteBlockComponent>
Expand Down
4 changes: 4 additions & 0 deletions airbyte-webapp/src/components/DeleteBlock/interfaces.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export interface DeleteBlockProps {
type: "source" | "destination" | "connection";
onDelete: () => Promise<unknown>;
}
32 changes: 32 additions & 0 deletions airbyte-webapp/src/components/DeleteBlock/useDeleteModal.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { useCallback } from "react";
import { useNavigate } from "react-router-dom";

import { useConfirmationModalService } from "hooks/services/ConfirmationModal";

import { DeleteBlockProps } from "./interfaces";

export const useDeleteModal = ({ type, onDelete }: Partial<DeleteBlockProps>): { onDeleteButtonClick: () => void } => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like the introduction of this hook - its a nice way to keep this logic encapsulated while allowing it to be used in either a DeleteBlock or directly by a button

const { openConfirmationModal, closeConfirmationModal } = useConfirmationModalService();
const navigate = useNavigate();

const onDeleteButtonClick = useCallback(() => {
if (!onDelete) {
return;
}
openConfirmationModal({
text: `tables.${type}DeleteModalText`,
title: `tables.${type}DeleteConfirm`,
submitButtonText: "form.delete",
onSubmit: async () => {
await onDelete();
closeConfirmationModal();
navigate("../..");
},
submitButtonDataId: "delete",
});
}, [closeConfirmationModal, onDelete, openConfirmationModal, navigate, type]);

return {
onDeleteButtonClick,
};
};
2 changes: 1 addition & 1 deletion airbyte-webapp/src/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@
"form.change": "Change",
"form.add": "Add",
"form.saveChanges": "Save changes",
"form.saveChangesAndTest": "Save changes and test",
"form.saveChangesAndTest": "Save & Test",
"form.sourceRetest": "Retest source",
"form.destinationRetest": "Retest destination",
"form.discardChanges": "Discard changes",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
import React from "react";
import { FormattedMessage } from "react-intl";

import DeleteBlock from "components/DeleteBlock";

import { ConnectionConfiguration } from "core/domain/connection";
import { Connector } from "core/domain/connector";
import { DestinationRead, WebBackendConnectionListItem } from "core/request/AirbyteClient";
Expand Down Expand Up @@ -64,11 +62,11 @@ const DestinationsSettings: React.FC<DestinationsSettingsProps> = ({
...currentDestination,
serviceType: Connector.id(destinationDefinition),
}}
onDelete={onDelete}
connector={currentDestination}
selectedConnectorDefinitionSpecification={destinationSpecification}
title={<FormattedMessage id="destination.destinationSettings" />}
/>
<DeleteBlock type="destination" onDelete={onDelete} />
</div>
);
};
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
import React, { useEffect } from "react";
import { FormattedMessage } from "react-intl";

import DeleteBlock from "components/DeleteBlock";

import { ConnectionConfiguration } from "core/domain/connection";
import { SourceRead, WebBackendConnectionListItem } from "core/request/AirbyteClient";
import { useTrackPage, PageTrackingCodes } from "hooks/services/Analytics";
Expand Down Expand Up @@ -57,6 +55,7 @@ const SourceSettings: React.FC<SourceSettingsProps> = ({ currentSource, connecti
return (
<div className={styles.content}>
<ConnectorCard
onDelete={onDelete}
formId={formId}
title={<FormattedMessage id="sources.sourceSettings" />}
isEditMode
Expand All @@ -70,7 +69,6 @@ const SourceSettings: React.FC<SourceSettingsProps> = ({ currentSource, connecti
}}
selectedConnectorDefinitionSpecification={sourceDefinitionSpecification}
/>
<DeleteBlock type="source" onDelete={onDelete} />
</div>
);
};
Expand Down
200 changes: 95 additions & 105 deletions airbyte-webapp/src/views/Connector/ConnectorCard/ConnectorCard.tsx
Original file line number Diff line number Diff line change
@@ -1,128 +1,118 @@
import React, { useEffect, useState } from "react";
import { Formik } from "formik";
import React from "react";
import { FormattedMessage } from "react-intl";

import { JobItem } from "components/JobItem/JobItem";
import { Card } from "components/ui/Card";

import { Connector, ConnectorSpecification, ConnectorT } from "core/domain/connector";
import { SynchronousJobRead } from "core/request/AirbyteClient";
import { LogsRequestError } from "core/request/LogsRequestError";
import { useAdvancedModeSetting } from "hooks/services/useAdvancedModeSetting";
import { generateMessageFromError } from "utils/errorStatusMessage";
import { ServiceForm, ServiceFormProps, ServiceFormValues } from "views/Connector/ServiceForm";
import { ServiceForm } from "views/Connector/ServiceForm";

import { useAdvancedModeSetting } from "../../../hooks/services/useAdvancedModeSetting";
import { generateMessageFromError } from "../../../utils/errorStatusMessage";
import { ConnectorServiceTypeControl } from "../ServiceForm/components/Controls/ConnectorServiceTypeControl";
import { FormControls } from "../ServiceForm/FormControls";
import { ServiceFormContextProvider } from "../ServiceForm/serviceFormContext";
import styles from "./ConnectorCard.module.scss";
import { useAnalyticsTrackFunctions } from "./useAnalyticsTrackFunctions";
import { useConnectorCardService } from "./hooks/useConnectorCardService";
import { ConnectorCardProps } from "./interfaces";
import { useTestConnector } from "./useTestConnector";

type ConnectorCardProvidedProps = Omit<
ServiceFormProps,
"isKeyConnectionInProgress" | "isSuccess" | "onStopTesting" | "testConnector"
>;

interface ConnectorCardBaseProps extends ConnectorCardProvidedProps {
title?: React.ReactNode;
full?: boolean;
jobInfo?: SynchronousJobRead | null;
additionalSelectorComponent?: React.ReactNode;
}

interface ConnectorCardCreateProps extends ConnectorCardBaseProps {
isEditMode?: false;
}

interface ConnectorCardEditProps extends ConnectorCardBaseProps {
isEditMode: true;
connector: ConnectorT;
}

export const ConnectorCard: React.FC<ConnectorCardCreateProps | ConnectorCardEditProps> = ({
title,
full,
jobInfo,
onSubmit,
additionalSelectorComponent,
...props
}) => {
const [saved, setSaved] = useState(false);
const [errorStatusRequest, setErrorStatusRequest] = useState<Error | null>(null);
const [isFormSubmitting, setIsFormSubmitting] = useState(false);
export const ConnectorCard: React.FC<ConnectorCardProps> = (props) => {
const {
title,
full,
isLoading,
additionalSelectorComponent,
formType,
selectedConnectorDefinitionSpecification,
isEditMode,
availableServices,
onServiceSelect,
onDelete,
} = props;
const {
formFields,
getValues,
initialValues,
isFormSubmitting,
job,
jsonSchema,
onFormSubmit,
resetUiWidgetsInfo,
saved,
selectedConnectorDefinitionSpecificationId,
setUiWidgetsInfo,
uiWidgetsInfo,
uniqueFormId,
validationSchema,
} = useConnectorCardService(props);
const [advancedMode] = useAdvancedModeSetting();

const { testConnector, isTestConnectionInProgress, onStopTesting, error, reset } = useTestConnector(props);
const { trackTestConnectorFailure, trackTestConnectorSuccess, trackTestConnectorStarted } =
useAnalyticsTrackFunctions(props.formType);

useEffect(() => {
// Whenever the selected connector changed, reset the check connection call and other errors
reset();
setErrorStatusRequest(null);
}, [props.selectedConnectorDefinitionSpecification, reset]);

const onHandleSubmit = async (values: ServiceFormValues) => {
setErrorStatusRequest(null);
setIsFormSubmitting(true);

const connector = props.availableServices.find((item) => Connector.id(item) === values.serviceType);

const testConnectorWithTracking = async () => {
trackTestConnectorStarted(connector);
try {
await testConnector(values);
trackTestConnectorSuccess(connector);
} catch (e) {
trackTestConnectorFailure(connector);
throw e;
}
};

try {
await testConnectorWithTracking();
onSubmit(values);
setSaved(true);
} catch (e) {
setErrorStatusRequest(e);
setIsFormSubmitting(false);
}
};

const job = jobInfo || LogsRequestError.extractJobInfo(errorStatusRequest);

const { selectedConnectorDefinitionSpecification, onServiceSelect, availableServices, isEditMode } = props;
const selectedConnectorDefinitionSpecificationId =
selectedConnectorDefinitionSpecification && ConnectorSpecification.id(selectedConnectorDefinitionSpecification);
const { testConnector, isTestConnectionInProgress, onStopTesting, error } = useTestConnector(props);

return (
<Card title={title} fullWidth={full}>
<div className={styles.cardForm}>
<div className={styles.connectorSelectControl}>
<ConnectorServiceTypeControl
formType={props.formType}
onChangeServiceType={onServiceSelect}
availableServices={availableServices}
isEditMode={isEditMode}
selectedServiceId={selectedConnectorDefinitionSpecificationId}
disabled={isFormSubmitting}
/>
</div>
{additionalSelectorComponent}
<Formik
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Initially I thought it felt slightly weird to have the Formik be declared outside of ServiceForm, but given that the goal of this is to move the "controls" outside of the card itself, then this structure probably makes sense 👍

validateOnBlur
validateOnChange
initialValues={initialValues}
validationSchema={validationSchema}
onSubmit={onFormSubmit}
enableReinitialize
>
<ServiceFormContextProvider
widgetsInfo={uiWidgetsInfo}
getValues={getValues}
setUiWidgetsInfo={setUiWidgetsInfo}
resetUiWidgetsInfo={resetUiWidgetsInfo}
formType={formType}
selectedConnector={selectedConnectorDefinitionSpecification}
availableServices={availableServices}
isEditMode={isEditMode}
isLoadingSchema={isLoading}
validationSchema={validationSchema}
>
<div>
<ServiceForm
{...props}
<Card title={title} fullWidth={full}>
<div className={styles.cardForm}>
<div className={styles.connectorSelectControl}>
<ConnectorServiceTypeControl
formType={props.formType}
onChangeServiceType={onServiceSelect}
availableServices={availableServices}
isEditMode={isEditMode}
selectedServiceId={selectedConnectorDefinitionSpecificationId}
disabled={isFormSubmitting}
/>
</div>
{additionalSelectorComponent}
<div>
<ServiceForm
formId={uniqueFormId}
jsonSchema={jsonSchema}
isTestConnectionInProgress={isTestConnectionInProgress}
formFields={formFields}
initialValues={initialValues}
validationSchema={validationSchema}
availableServices={availableServices}
/>
{/* Show the job log only if advanced mode is turned on or the actual job failed (not the check inside the job) */}
{job && (advancedMode || !job.succeeded) && <JobItem job={job} />}
</div>
</div>
</Card>
<FormControls
onDelete={onDelete}
errorMessage={props.errorMessage || (error && generateMessageFromError(error))}
isTestConnectionInProgress={isTestConnectionInProgress}
onStopTesting={onStopTesting}
testConnector={testConnector}
onSubmit={onHandleSubmit}
onStopTestingConnector={onStopTesting ? () => onStopTesting() : undefined}
onRetest={testConnector ? async () => await testConnector() : undefined}
formFields={formFields}
selectedConnector={selectedConnectorDefinitionSpecification}
successMessage={
props.successMessage || (saved && props.isEditMode && <FormattedMessage id="form.changesSaved" />)
}
/>
{/* Show the job log only if advanced mode is turned on or the actual job failed (not the check inside the job) */}
{job && (advancedMode || !job.succeeded) && <JobItem job={job} />}
</div>
</div>
</Card>
</ServiceFormContextProvider>
</Formik>
);
};
Loading