diff --git a/src/components/README.md b/src/components/README.md index 5cb01aa13f..d1348d27d4 100644 --- a/src/components/README.md +++ b/src/components/README.md @@ -1,5 +1,56 @@ -# About the component folder +## About the component folder Please refer to [instill.tech](https://github.com/instill-ai/instill.tech/tree/main/src/components) repo -This folder need rapidly refactoring. \ No newline at end of file +This folder is under rapidly refactoring. + +## About form components + +### General guideline + +In every component we separately function with sections, this sections have several rules. + +- Better to use useMemo state to control the form, reduce the complicate setState within lots of useEffect. +- The state will be arrange at the top of each sections to benefit other maintainer. +- Field on change callback will be put at the end of each section due to they usually need to use other state. +- Each section need to be separated by divider like this one. +- Submit the form using formik onSubmit handler and validator + +// ################################################################### +// # # +// # 1 - <_title_> # +// # # +// ################################################################### +// +// <_comment_> + +#### About the naming convention + +- Action verifirer: can\_\_\_\_ (canDeployModel, canSetupModel) +- Action: handle\_\_ (handleDeployModel, handleSetupModel) + +### Pipeline form + +Pipeline form is complicated, we can not simply leverage what Formik provided but need to write out own flow and centralize the data we gather using formik state, formik provide very handy form state management. + +When you read though pipeline form, it now separatelt to 5 step + +- Setup pipeline mode step +- Setup pipeline source step +- Setup pipeline model step + - use existing model flow + - create new model flow +- Setup pipeline destination step +- Setup pipeline details step + +### About the complicated form generation + +We are using airbyte potocol to control our destination connectors, they are using quite complicated yaml to generate their form. They generate all the whole formik component by digesting the JsonSchema ([ServiceForm](https://github.com/airbytehq/airbyte/blob/8076b56f3718d6fe054b660a838f2c1c6890ffc2/airbyte-webapp/src/views/Connector/ServiceForm/ServiceForm.tsx)). In my opinion this is not flexible and our form's structure is much complicated than airbyte due to we have the pipeline concept on top of their connection. + +The solution will be cell-design in normal formik's form's flow. Take `CreatePipelineForm` for example, We have a giant formik form that hold the full state of the flow, each step have its own logic and upon finish the step, they will fill in the value into formik state and call it the day. Here comes a problem, if we want to validate at the end of the flow, we have to write a giant validation schema to validate every possibility when create the model or the destination connector. This is appearantly not ideal. Below are the proposal for better implementation. + +- We should trust the validation of every step and not validate at the end of the flow. +- We will have a flag for every step's validation like `validModel`, at the end of the flow, it only needs to check the value of this kind of flag. +- About the complicated form like model definition and destination connection, we generate them from json-schema and compose a block(not a formik container), it's not a `
` HTML tag, but a pure functional component. It will digest the user's input, validate the input then submit. After the request is complete, it will fill in the return value(most of the cases, it will fill in the identifier of the resource, take connector for example, it may fill in `source-connectors/hi`) and move on. + +In this implementation we could have very flexible block that can install in near every form. and we will call each of this kind of component FormCell. diff --git a/src/components/destination/ConfigureDestinationForm.tsx b/src/components/destination/ConfigureDestinationForm.tsx new file mode 100644 index 0000000000..c824f4d09a --- /dev/null +++ b/src/components/destination/ConfigureDestinationForm.tsx @@ -0,0 +1,349 @@ +import { ChangeEvent, useCallback, useMemo, useState } from "react"; +import * as yup from "yup"; +import { + BasicProgressMessageBox, + BasicSingleSelect, + BasicTextArea, + ProgressMessageBoxState, + SolidButton, +} from "@instill-ai/design-system"; +import { AxiosError } from "axios"; +import Image from "next/image"; + +import { + AirbyteFieldErrors, + AirbyteFieldValues, + useAirbyteFieldValues, + useAirbyteFormTree, + useBuildAirbyteYup, + useAirbyteSelectedConditionMap, +} from "@/lib/airbytes"; +import { AirbyteDestinationFields } from "@/lib/airbytes/components"; +import dot from "@/lib/dot"; +import { + DestinationWithDefinition, + UpdateDestinationPayload, +} from "@/lib/instill"; +import { Nullable } from "@/types/general"; +import { FormBase } from "@/components/ui"; +import { useUpdateDestination } from "@/services/connector/destination/mutations"; +import { useAmplitudeCtx } from "@/contexts/AmplitudeContext"; +import { sendAmplitudeData } from "@/lib/amplitude"; + +export type ConfigureDestinationFormProps = { + destination: DestinationWithDefinition; +}; + +const ConfigureDestinationForm = ({ + destination, +}: ConfigureDestinationFormProps) => { + const { amplitudeIsInit } = useAmplitudeCtx(); + + // ########################################################################## + // # 1 - Get the destination definition and static state for fields # + // ########################################################################## + + const isSyncDestination = useMemo(() => { + if ( + destination.destination_connector_definition.connector_definition + .docker_repository === "instill-ai/destination-grpc" || + destination.destination_connector_definition.connector_definition + .docker_repository === "instill-ai/destination-http" + ) { + return true; + } + + return false; + }, [destination]); + + const destinationDefinitionOption = useMemo(() => { + return { + label: destination.destination_connector_definition.id, + value: destination.destination_connector_definition.id, + startIcon: ( + + ), + }; + }, [ + destination.destination_connector_definition.id, + destination.destination_connector_definition.connector_definition + .docker_repository, + destination.destination_connector_definition.connector_definition.icon, + ]); + + // ########################################################################## + // # 2 - Create interior state for managing the form # + // ########################################################################## + + const [formIsDirty, setFormIsDirty] = useState(false); + + const [fieldErrors, setFieldErrors] = + useState>(null); + + const destinationFormTree = useAirbyteFormTree( + destination.destination_connector_definition + ); + + const initialValues: AirbyteFieldValues = { + configuration: destination.connector.configuration, + ...dot.toDot(destination.connector.configuration), + description: destination.connector.description || undefined, + }; + + const [selectedConditionMap, setSelectedConditionMap] = + useAirbyteSelectedConditionMap(destinationFormTree, initialValues); + + const { fieldValues, setFieldValues } = useAirbyteFieldValues( + destinationFormTree, + initialValues + ); + + const [canEdit, setCanEdit] = useState(false); + const [messageBoxState, setMessageBoxState] = + useState({ + activate: false, + message: null, + description: null, + status: null, + }); + + const airbyteYup = useBuildAirbyteYup( + destination.destination_connector_definition.connector_definition.spec + .connection_specification ?? null, + selectedConditionMap, + null + ); + + const formYup = useMemo(() => { + if (!airbyteYup) return null; + + return yup.object({ + configuration: airbyteYup, + }); + }, [airbyteYup]); + + const updateDestination = useUpdateDestination(); + + const updateFieldValues = useCallback( + (field: string, value: string) => { + setFormIsDirty(true); + setFieldValues((prev) => { + return { + ...prev, + [field]: value, + }; + }); + }, + [setFieldValues, setFormIsDirty] + ); + + // ########################################################################## + // # 2 - Configure destination # + // ########################################################################## + + const handleSubmit = useCallback(async () => { + if ( + destination.destination_connector_definition.connector_definition + .docker_repository === "instill-ai/destination-grpc" || + destination.destination_connector_definition.connector_definition + .docker_repository === "instill-ai/destination-http" + ) { + return; + } + + if (!fieldValues || !formYup) { + return; + } + + let stripValues = {} as { configuration: AirbyteFieldValues }; + + if (!canEdit) { + setCanEdit(true); + return; + } else { + if (!formIsDirty) return; + try { + // We use yup to strip not necessary condition value. Please read + // /lib/airbyte/README.md for more information, especially the section + // How to remove old condition configuration when user select new one? + + stripValues = formYup.validateSync(fieldValues, { + abortEarly: false, + strict: false, + stripUnknown: true, + }); + } catch (error) { + if (error instanceof yup.ValidationError) { + const errors = {} as AirbyteFieldErrors; + for (const err of error.inner) { + if (err.path) { + const message = err.message.replace(err.path, "This field"); + const pathList = err.path.split("."); + + // Because we are using { configuration: airbyteYup } to construct the yup, yup will add "configuration" as prefix at the start + // of the path like configuration.tunnel_method + if (pathList[0] === "configuration") { + pathList.shift(); + } + + const removeConfigurationPrefixPath = pathList.join("."); + errors[removeConfigurationPrefixPath] = message; + } + } + setFieldErrors(errors); + } + + return; + } + setFieldErrors(null); + + const payload: UpdateDestinationPayload = { + name: destination.name, + connector: { + description: fieldValues.description as string | undefined, + ...stripValues, + }, + }; + + setMessageBoxState(() => ({ + activate: true, + status: "progressing", + description: null, + message: "Updating...", + })); + + updateDestination.mutate(payload, { + onSuccess: () => { + setCanEdit(false); + setFormIsDirty(false); + setMessageBoxState(() => ({ + activate: true, + status: "success", + description: null, + message: "Succeed.", + })); + + if (amplitudeIsInit) { + sendAmplitudeData("update_destination", { + type: "critical_action", + process: "destination", + }); + } + }, + onError: (error) => { + if (error instanceof AxiosError) { + setMessageBoxState(() => ({ + activate: true, + status: "error", + description: JSON.stringify( + error.response?.data.details, + null, + "\t" + ), + message: error.message, + })); + } else { + setMessageBoxState(() => ({ + activate: true, + status: "error", + description: null, + message: "Something went wrong when create the destination", + })); + } + }, + }); + + return; + } + }, [ + amplitudeIsInit, + formYup, + fieldValues, + canEdit, + setCanEdit, + formIsDirty, + setFormIsDirty, + destination.destination_connector_definition.connector_definition + .docker_repository, + destination.name, + updateDestination, + ]); + + return ( + +
+ Setup Guide`} + /> + {!isSyncDestination ? ( + ) => + updateFieldValues("description", event.target.value) + } + disabled={!canEdit} + /> + ) : null} + +
+
+ + handleSubmit()} + > + {canEdit ? "Save" : "Edit"} + +
+
+ ); +}; + +export default ConfigureDestinationForm; diff --git a/src/components/forms/connector/destination/CreateDestinationForm/CreateDestinationForm.tsx b/src/components/destination/CreateDestinationForm.tsx similarity index 83% rename from src/components/forms/connector/destination/CreateDestinationForm/CreateDestinationForm.tsx rename to src/components/destination/CreateDestinationForm.tsx index cb42714719..57d4ba8ace 100644 --- a/src/components/forms/connector/destination/CreateDestinationForm/CreateDestinationForm.tsx +++ b/src/components/destination/CreateDestinationForm.tsx @@ -1,12 +1,10 @@ import { - FC, useCallback, useMemo, useState, ChangeEvent, ReactElement, } from "react"; -import { useRouter } from "next/router"; import { BasicProgressMessageBox, BasicSingleSelect, @@ -14,11 +12,12 @@ import { BasicTextField, ProgressMessageBoxState, SingleSelectOption, + SolidButton, } from "@instill-ai/design-system"; import * as yup from "yup"; import Image from "next/image"; +import { AxiosError } from "axios"; -import { PrimaryButton } from "@/components/ui"; import { ConnectorDefinition, CreateDestinationPayload, @@ -30,7 +29,7 @@ import { useDestinationDefinitions, } from "@/services/connector"; import { useAmplitudeCtx } from "@/contexts/AmplitudeContext"; -import { FormBase } from "@/components/forms/commons"; +import { FormBase } from "@/components/ui"; import { AirbyteFieldValues, AirbyteFieldErrors, @@ -40,10 +39,7 @@ import { useAirbyteFieldValues, } from "@/lib/airbytes"; import { AirbyteDestinationFields } from "@/lib/airbytes/components"; -import { ValidationError } from "yup"; import { sendAmplitudeData } from "@/lib/amplitude"; -import { AxiosError } from "axios"; -import { ErrorDetails } from "@/lib/instill/types"; export type CreateDestinationFormProps = { setResult: Nullable<(destinationId: string) => void>; @@ -58,7 +54,7 @@ export type CreateDestinationFormProps = { pipelineMode: Nullable; }; -const CreateDestinationForm: FC = ({ +const CreateDestinationForm = ({ setResult, flex1, title, @@ -66,15 +62,12 @@ const CreateDestinationForm: FC = ({ marginBottom, onSuccessCb, pipelineMode, -}) => { - const router = useRouter(); +}: CreateDestinationFormProps) => { const { amplitudeIsInit } = useAmplitudeCtx(); - // ################################################################### - // # # - // # 1 - Initialize the destination definition # - // # # - // ################################################################### + // ########################################################################## + // # 1 - Get the destination definition and static state for fields # + // ########################################################################## const destinationDefinitions = useDestinationDefinitions(); @@ -125,7 +118,11 @@ const CreateDestinationForm: FC = ({ /> ), })); - }, [destinationDefinitions.isSuccess, destinationDefinitions.data]); + }, [ + destinationDefinitions.isSuccess, + destinationDefinitions.data, + pipelineMode, + ]); const [selectedDestinationDefinition, setSelectedDestinationDefinition] = useState>(null); @@ -133,16 +130,9 @@ const CreateDestinationForm: FC = ({ const [selectedDestinationOption, setSelectedDestinationOption] = useState>(null); - const destinationFormTree = useAirbyteFormTree(selectedDestinationDefinition); - - const { fieldValues, setFieldValues } = - useAirbyteFieldValues(destinationFormTree); - - const [fieldErrors, setFieldErrors] = - useState>(null); - - // Instill Ai provided connector HTTP and gRPC can only have default id destination-http and destination-grpc - // We need to make sure user have proper instruction on this issue. + // Instill Ai provided connector HTTP and gRPC can only have default id + // destination-http and destination-grpc. We need to make sure user have + // proper instruction on this issue. const canSetIdField = useMemo(() => { if (!selectedDestinationDefinition) return true; @@ -179,14 +169,39 @@ const CreateDestinationForm: FC = ({ return null; }, [selectedDestinationDefinition]); - // ################################################################### - // # # - // # 2 - handle state when create destination # - // # # - // ################################################################### + const getSetupGuide = useCallback(() => { + if (selectedDestinationDefinition) { + if (selectedDestinationDefinition.id === "destination-http") { + return "https://www.instill.tech/docs/destination-connectors/http"; + } else if (selectedDestinationDefinition.id === "destination-grpc") { + return "https://www.instill.tech/docs/destination-connectors/grpc"; + } + } + + return selectedDestinationDefinition + ? selectedDestinationDefinition.connector_definition.documentation_url + : "https://www.instill.tech/docs/destination-connectors/overview"; + }, [selectedDestinationDefinition]); + + // ########################################################################## + // # 2 - Create interior state for managing the form # + // ########################################################################## + + const destinationFormTree = useAirbyteFormTree(selectedDestinationDefinition); + + const { fieldValues, setFieldValues } = useAirbyteFieldValues( + destinationFormTree, + null + ); + + const [fieldErrors, setFieldErrors] = + useState>(null); + const [selectedConditionMap, setSelectedConditionMap] = useState>(null); + const [formIsDirty, setFormIsDirty] = useState(false); + const [messageBoxState, setMessageBoxState] = useState({ activate: false, @@ -205,13 +220,14 @@ const CreateDestinationForm: FC = ({ ); /** - * We store our data in two form, one is in dot.notation and the other is in object and - * the airbyteYup is planned to verify object part of the data + * We store our data in two form, one is in dot.notation and the other + * is in object and the airbyteYup is planned to verify object part of + * the data * * { * tunnel_method: "SSH", - * tunnel_method.tunnel_key: "hi", - * configuration: { + * tunnel_method.tunnel_key: "hi", <--- yup won't verify this + * configuration: { <--- yup will verify this object * tunnel_method: { * tunnel_method: "SSH", * tunnel_key: "hi" @@ -230,28 +246,43 @@ const CreateDestinationForm: FC = ({ : yup.string().nullable().notRequired(), configuration: airbyteYup, }); - }, [airbyteYup]); + }, [airbyteYup, canSetIdField]); + + // ########################################################################## + // # 3 - Create the destination # + // ########################################################################## const submitHandler = useCallback(async () => { if (!fieldValues || !formYup) { return; } + let stripValues = {} as { configuration: AirbyteFieldValues }; + try { - formYup.validateSync(fieldValues, { + // We use yup to strip not necessary condition values + // Please read /lib/airbyte/README.md for more information, especially + // the section: How to remove old condition configuration when user + // select new one? + + stripValues = formYup.validateSync(fieldValues, { abortEarly: false, - strict: true, + strict: false, + stripUnknown: true, }); } catch (error) { - if (error instanceof ValidationError) { + if (error instanceof yup.ValidationError) { const errors = {} as AirbyteFieldErrors; for (const err of error.inner) { if (err.path) { const message = err.message.replace(err.path, "This field"); const pathList = err.path.split("."); - // Because we are using { configuration: airbyteYup } to construct the yup, yup will add "configuration" as prefix at the start - // of the path like configuration.tunnel_method + // Because we are using { configuration: airbyteYup } to + // construct the yup, yup will add "configuration" as prefix at + // the start of the path like configuration.tunnel_method, we + // need to remove the prefix to make it clearner. + if (pathList[0] === "configuration") { pathList.shift(); } @@ -270,6 +301,9 @@ const CreateDestinationForm: FC = ({ let payload = {} as CreateDestinationPayload; + // destination-grpc and destination-http come from instill-ai and follow + // our own payload + if ( selectedDestinationDefinition?.connector_definition.docker_repository === "instill-ai/destination-grpc" @@ -281,8 +315,7 @@ const CreateDestinationForm: FC = ({ }`, connector: { description: fieldValues.description as string, - configuration: - (fieldValues.configuration as AirbyteFieldValues) ?? {}, + configuration: {}, }, }; } else if ( @@ -296,8 +329,7 @@ const CreateDestinationForm: FC = ({ }`, connector: { description: fieldValues.description as string, - configuration: - (fieldValues.configuration as AirbyteFieldValues) ?? {}, + configuration: {}, }, }; } else { @@ -308,8 +340,7 @@ const CreateDestinationForm: FC = ({ }`, connector: { description: fieldValues.description as string, - configuration: - (fieldValues.configuration as AirbyteFieldValues) ?? {}, + ...stripValues, }, }; } @@ -344,9 +375,11 @@ const CreateDestinationForm: FC = ({ setMessageBoxState(() => ({ activate: true, status: "error", - description: - (error.response?.data.details as ErrorDetails[])[0].description ?? + description: JSON.stringify( + error.response?.data.details, null, + "\t" + ), message: error.message, })); } else { @@ -361,7 +394,6 @@ const CreateDestinationForm: FC = ({ }); }, [ amplitudeIsInit, - router, createDestination, formYup, fieldValues, @@ -370,28 +402,17 @@ const CreateDestinationForm: FC = ({ onSuccessCb, ]); - const updateFieldValues = useCallback((field: string, value: string) => { - setFieldValues((prev) => { - return { - ...prev, - [field]: value, - }; - }); - }, []); - - const getSetupGuide = useCallback(() => { - if (selectedDestinationDefinition) { - if (selectedDestinationDefinition.id === "destination-http") { - return "https://www.instill.tech/docs/destination-connectors/http"; - } else if (selectedDestinationDefinition.id === "destination-grpc") { - return "https://www.instill.tech/docs/destination-connectors/grpc"; - } - } - - return selectedDestinationDefinition - ? selectedDestinationDefinition.connector_definition.documentation_url - : "https://www.instill.tech/docs/destination-connectors/overview"; - }, [selectedDestinationDefinition]); + const updateFieldValues = useCallback( + (field: string, value: string) => { + setFieldValues((prev) => { + return { + ...prev, + [field]: value, + }; + }); + }, + [setFieldValues] + ); return ( = ({ fieldErrors={fieldErrors} selectedConditionMap={selectedConditionMap} setSelectedConditionMap={setSelectedConditionMap} + disableAll={false} + formIsDirty={formIsDirty} + setFormIsDirty={setFormIsDirty} />
@@ -491,14 +515,15 @@ const CreateDestinationForm: FC = ({ width="w-[25vw]" closable={true} /> - submitHandler()} > Set up - +
); diff --git a/src/components/destination/index.ts b/src/components/destination/index.ts new file mode 100644 index 0000000000..b2aaa1e988 --- /dev/null +++ b/src/components/destination/index.ts @@ -0,0 +1,7 @@ +import ConfigureDestinationForm from "./ConfigureDestinationForm"; +import type { ConfigureDestinationFormProps } from "./ConfigureDestinationForm"; +import CreateDestinationForm from "./CreateDestinationForm"; +import type { CreateDestinationFormProps } from "./CreateDestinationForm"; + +export { ConfigureDestinationForm, CreateDestinationForm }; +export type { ConfigureDestinationFormProps, CreateDestinationFormProps }; diff --git a/src/components/forms/MockData.tsx b/src/components/forms/MockData.tsx deleted file mode 100644 index a01a8a4bcc..0000000000 --- a/src/components/forms/MockData.tsx +++ /dev/null @@ -1,103 +0,0 @@ -import { - ArtiVcIcon, - GitHubIcon, - GoogleSheetIcon, - GrpcIcon, - HttpIcon, - LocalUploadIcon, - ModelInstanceIcon, - MySqlIcon, -} from "@instill-ai/design-system"; - -export const syncDataConnectionOptions = [ - { - label: "http", - value: "http", - startIcon: ( - - ), - }, - { - label: "gRPC", - value: "grpc", - startIcon: ( - - ), - }, -]; - -export const mockAsyncDataConnectionOptions = [ - { - label: "Google Sheet", - value: "googleSheet", - startIcon: ( - - ), - }, - { - label: "MySQL", - value: "mySql", - startIcon: ( - - ), - }, -]; - -export const mockModelSourceOptions = [ - { - label: "GitHub", - value: "github", - startIcon: ( - - ), - }, - { - label: "Local", - value: "local", - startIcon: ( - - ), - }, - { - label: "ArtiVC", - value: "artivc", - startIcon: ( - - ), - }, -]; - -export const mockModelInstances = [ - { - label: "v1.0.0", - value: "v1.0,0", - startIcon: ( - - ), - }, -]; diff --git a/src/components/forms/README.md b/src/components/forms/README.md deleted file mode 100644 index 0e5a95a444..0000000000 --- a/src/components/forms/README.md +++ /dev/null @@ -1,50 +0,0 @@ -# About form components - -## General guideline - -In every component we separately function with sections, this sections have several rules. - -- Better to use useMemo state to control the form, reduce the complicate setState within lots of useEffect. -- The state will be arrange at the top of each sections to benefit other maintainer. -- Field on change callback will be put at the end of each section due to they usually need to use other state. -- Each section need to be separated by divider like this one. -- Submit the form using formik onSubmit handler and validator - -// ################################################################### -// # # -// # 1 - <_title_> # -// # # -// ################################################################### -// -// <_comment_> - -### About the naming convention - -- Action verifirer: can\_\_\_\_ (canDeployModel, canSetupModel) -- Action: handle\_\_ (handleDeployModel, handleSetupModel) - -## Pipeline form - -Pipeline form is complicated, we can not simply leverage what Formik provided but need to write out own flow and centralize the data we gather using formik state, formik provide very handy form state management. - -When you read though pipeline form, it now separatelt to 5 step - -- Setup pipeline mode step -- Setup pipeline source step -- Setup pipeline model step - - use existing model flow - - create new model flow -- Setup pipeline destination step -- Setup pipeline details step - -## About the complicated form generation - -We are using airbyte potocol to control our destination connectors, they are using quite complicated yaml to generate their form. They generate all the whole formik component by digesting the JsonSchema ([ServiceForm](https://github.com/airbytehq/airbyte/blob/8076b56f3718d6fe054b660a838f2c1c6890ffc2/airbyte-webapp/src/views/Connector/ServiceForm/ServiceForm.tsx)). In my opinion this is not flexible and our form's structure is much complicated than airbyte due to we have the pipeline concept on top of their connection. - -The solution will be cell-design in normal formik's form's flow. Take `CreatePipelineForm` for example, We have a giant formik form that hold the full state of the flow, each step have its own logic and upon finish the step, they will fill in the value into formik state and call it the day. Here comes a problem, if we want to validate at the end of the flow, we have to write a giant validation schema to validate every possibility when create the model or the destination connector. This is appearantly not ideal. Below are the proposal for better implementation. - -- We should trust the validation of every step and not validate at the end of the flow. -- We will have a flag for every step's validation like `validModel`, at the end of the flow, it only needs to check the value of this kind of flag. -- About the complicated form like model definition and destination connection, we generate them from json-schema and compose a block(not a formik container), it's not a `
` HTML tag, but a pure functional component. It will digest the user's input, validate the input then submit. After the request is complete, it will fill in the return value(most of the cases, it will fill in the identifier of the resource, take connector for example, it may fill in `source-connectors/hi`) and move on. - -In this implementation we could have very flexible block that can install in near every form. and we will call each of this kind of component FormCell. diff --git a/src/components/forms/commons/FormBase/index.ts b/src/components/forms/commons/FormBase/index.ts deleted file mode 100644 index f9e75ecdfb..0000000000 --- a/src/components/forms/commons/FormBase/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { default } from "./FormBase"; -export * from "./FormBase"; diff --git a/src/components/forms/commons/index.ts b/src/components/forms/commons/index.ts deleted file mode 100644 index abee55b67b..0000000000 --- a/src/components/forms/commons/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -import FormBase from "./FormBase"; -import type { FormBaseProps } from "./FormBase"; - -export { FormBase }; -export type { FormBaseProps }; diff --git a/src/components/forms/connector/destination/ConfigureDestinationForm/ConfigureDestinationForm.tsx b/src/components/forms/connector/destination/ConfigureDestinationForm/ConfigureDestinationForm.tsx deleted file mode 100644 index 669726db84..0000000000 --- a/src/components/forms/connector/destination/ConfigureDestinationForm/ConfigureDestinationForm.tsx +++ /dev/null @@ -1,261 +0,0 @@ -import { FC, useState, useEffect, useCallback } from "react"; -import { - BasicProgressMessageBox, - ProgressMessageBoxState, - SingleSelectOption, -} from "@instill-ai/design-system"; -import { Formik } from "formik"; -import { useRouter } from "next/router"; - -import { FormikFormBase, SingleSelect } from "@/components/formik"; -import { ConnectorIcon, PrimaryButton } from "@/components/ui"; -import { DestinationWithDefinition } from "@/lib/instill"; -import { Nullable } from "@/types/general"; -import { DeleteResourceModal } from "@/components/modals"; -import { useDeleteDestination } from "@/services/connector"; -import { useAmplitudeCtx } from "@/contexts/AmplitudeContext"; -import { sendAmplitudeData } from "@/lib/amplitude"; -import { AxiosError } from "axios"; -import { ErrorDetails, Violation } from "@/lib/instill/types"; -import OutlineButton from "@/components/ui/Buttons/OutlineButton"; -import useDeleteResourceGuard from "@/hooks/useDeleteResourceGuard"; - -export type ConfigureDestinationFormProps = { - destination: Nullable; -}; - -export type ConfigureDestinationFormValue = { - definition: Nullable; -}; - -const ConfigureDestinationForm: FC = ({ - destination, -}) => { - const router = useRouter(); - const { amplitudeIsInit } = useAmplitudeCtx(); - - // ################################################################### - // # # - // # Initialize the destination definition # - // # # - // ################################################################### - - const [ - syncDestinationDefinitionOptions, - setSyncDestinationDefinitionOptions, - ] = useState([]); - const [ - selectedDestinationDefinitionOption, - setSelectedDestinationDefinitionOption, - ] = useState>(null); - const [canEdit, setCanEdit] = useState(false); - - useEffect(() => { - if (!destination) return; - - const options = [ - { - label: "gRPC", - value: "destination-grpc", - startIcon: ( - - ), - }, - { - label: "HTTP", - value: "destination-http", - startIcon: ( - - ), - }, - ]; - - setSyncDestinationDefinitionOptions(options); - setSelectedDestinationDefinitionOption( - options.find((e) => e.value === destination.id) || null - ); - }, [destination]); - - const destinationDefinitionOnChangeCb = useCallback( - (option: SingleSelectOption) => { - setSelectedDestinationDefinitionOption( - syncDestinationDefinitionOptions.find( - (e) => e.value === option.value - ) || null - ); - }, - [syncDestinationDefinitionOptions] - ); - - // ################################################################### - // # # - // # Handle delete source # - // # # - // ################################################################### - - const { disableResourceDeletion } = useDeleteResourceGuard(); - - const [deleteDestinationModalIsOpen, setDeleteDestinationModalIsOpen] = - useState(false); - - const [messageBoxState, setMessageBoxState] = - useState({ - activate: false, - message: null, - description: null, - status: null, - }); - - const deleteDestination = useDeleteDestination(); - - const handleDeleteDestination = useCallback(() => { - if (!destination) return; - - setMessageBoxState(() => ({ - activate: true, - status: "progressing", - description: null, - message: "Deleting...", - })); - - deleteDestination.mutate(destination.name, { - onSuccess: () => { - setMessageBoxState(() => ({ - activate: true, - status: "success", - description: null, - message: "Succeed.", - })); - if (amplitudeIsInit) { - sendAmplitudeData("delete_destination", { - type: "critical_action", - process: "destination", - }); - } - router.push("/destinations"); - }, - onError: (error) => { - if (error instanceof AxiosError) { - setMessageBoxState(() => ({ - activate: true, - message: `${error.response?.status} - ${error.response?.data.message}`, - description: ( - (error.response?.data.details as ErrorDetails[])[0] - .violations as Violation[] - )[0].description, - status: "error", - })); - } else { - setMessageBoxState(() => ({ - activate: true, - status: "error", - description: null, - message: "Something went wrong when delete the destination", - })); - } - }, - }); - setDeleteDestinationModalIsOpen(false); - }, [destination, amplitudeIsInit, router, deleteDestination]); - - return ( - <> - { - if (!canEdit) { - setCanEdit(true); - return; - } - }} - > - {(formik) => { - return ( - -
- -
-
- setDeleteDestinationModalIsOpen(true)} - position="mr-auto my-auto" - type="button" - disabledBgColor="bg-instillGrey15" - bgColor="bg-white" - hoveredBgColor="hover:bg-instillRed" - disabledTextColor="text-instillGrey50" - textColor="text-instillRed" - hoveredTextColor="hover:text-instillGrey05" - width={null} - borderSize="border" - borderColor="border-instillRed" - hoveredBorderColor={null} - disabledBorderColor="border-instillGrey15" - > - Delete - - - {canEdit ? "Done" : "Edit"} - -
-
- -
-
- ); - }} -
- - - ); -}; - -export default ConfigureDestinationForm; diff --git a/src/components/forms/connector/destination/ConfigureDestinationForm/index.ts b/src/components/forms/connector/destination/ConfigureDestinationForm/index.ts deleted file mode 100644 index 528a697567..0000000000 --- a/src/components/forms/connector/destination/ConfigureDestinationForm/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { default } from "./ConfigureDestinationForm"; -export * from "./ConfigureDestinationForm"; diff --git a/src/components/forms/connector/destination/CreateDestinationForm/index.ts b/src/components/forms/connector/destination/CreateDestinationForm/index.ts deleted file mode 100644 index 77f4b27715..0000000000 --- a/src/components/forms/connector/destination/CreateDestinationForm/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { default } from "./CreateDestinationForm"; -export * from "./CreateDestinationForm"; diff --git a/src/components/forms/connector/destination/index.ts b/src/components/forms/connector/destination/index.ts deleted file mode 100644 index b0bf9d3277..0000000000 --- a/src/components/forms/connector/destination/index.ts +++ /dev/null @@ -1,14 +0,0 @@ -import CreateDestinationForm from "./CreateDestinationForm"; -import type { CreateDestinationFormProps } from "./CreateDestinationForm"; -import ConfigureDestinationForm from "./ConfigureDestinationForm"; -import type { - ConfigureDestinationFormProps, - ConfigureDestinationFormValue, -} from "./ConfigureDestinationForm"; - -export { CreateDestinationForm, ConfigureDestinationForm }; -export type { - CreateDestinationFormProps, - ConfigureDestinationFormProps, - ConfigureDestinationFormValue, -}; diff --git a/src/components/forms/connector/index.ts b/src/components/forms/connector/index.ts deleted file mode 100644 index 4b04dd223a..0000000000 --- a/src/components/forms/connector/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from "./source"; -export * from "./destination"; diff --git a/src/components/forms/connector/source/ConfigureSourceForm/index.ts b/src/components/forms/connector/source/ConfigureSourceForm/index.ts deleted file mode 100644 index 57a4266fcc..0000000000 --- a/src/components/forms/connector/source/ConfigureSourceForm/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { default } from "./ConfigureSourceForm"; -export * from "./ConfigureSourceForm"; diff --git a/src/components/forms/connector/source/CreateSourceForm/CreateSourceForm.tsx b/src/components/forms/connector/source/CreateSourceForm/CreateSourceForm.tsx deleted file mode 100644 index a9e58023a5..0000000000 --- a/src/components/forms/connector/source/CreateSourceForm/CreateSourceForm.tsx +++ /dev/null @@ -1,226 +0,0 @@ -import { FC, useEffect, useCallback, useState } from "react"; -import { Formik } from "formik"; -import { useRouter } from "next/router"; - -import { SingleSelect } from "../../../../formik/FormikField"; -import { FormikFormBase } from "@/components/formik"; -import { ConnectorIcon, PrimaryButton } from "@/components/ui"; -import { - BasicProgressMessageBox, - ProgressMessageBoxState, - SingleSelectOption, -} from "@instill-ai/design-system"; -import { useCreateSource, useSources } from "@/services/connector"; -import { CreateSourcePayload } from "@/lib/instill"; -import { Nullable } from "@/types/general"; -import { useAmplitudeCtx } from "@/contexts/AmplitudeContext"; -import { sendAmplitudeData } from "@/lib/amplitude"; - -export type CreateSourceFormValues = { - id: Nullable; - definition: Nullable; -}; - -const CreateSourceForm: FC = () => { - const router = useRouter(); - const { amplitudeIsInit } = useAmplitudeCtx(); - - // ################################################################### - // # # - // # 1 - Initialize the source definition # - // # # - // ################################################################### - // - // A user can only have a http source and a grpc source - - const [syncSourceDefinitionOptions, setSyncSourceDefinitionOptions] = - useState([]); - const [ - selectedSyncSourceDefinitionOption, - setSelectedSyncSourceDefinitionOption, - ] = useState>(null); - - const sources = useSources(); - const createSource = useCreateSource(); - - useEffect(() => { - if (!sources.isSuccess) return; - - setSyncSourceDefinitionOptions([ - { - label: "gRPC", - value: "source-grpc", - startIcon: ( - - ), - }, - { - label: "HTTP", - value: "source-http", - startIcon: ( - - ), - }, - ]); - }, [sources.isSuccess]); - - const sourceDefinitionOnChange = useCallback((option: SingleSelectOption) => { - setSelectedSyncSourceDefinitionOption(option); - }, []); - - // ################################################################### - // # # - // # 2 - handle create source # - // # # - // ################################################################### - - const [messageBoxState, setMessageBoxState] = - useState({ - activate: false, - message: null, - description: null, - status: null, - }); - - const validateForm = useCallback( - (values: CreateSourceFormValues) => { - const error: Partial = {}; - - if (!values.definition) { - error.definition = "Required"; - } - - if (sources.data?.find((e) => e.id === values.definition)) { - error.definition = - "You could only create one http and one grpc source. Check the setup guide for more information."; - } - - return error; - }, - [sources.data] - ); - - const handleSubmit = useCallback( - (values: CreateSourceFormValues) => { - if (!values.definition) return; - - const payload: CreateSourcePayload = { - id: values.definition, - source_connector_definition: `source-connector-definitions/${values.definition}`, - connector: { - configuration: {}, - }, - }; - - setMessageBoxState(() => ({ - activate: true, - status: "progressing", - description: null, - message: "Creating...", - })); - - createSource.mutate(payload, { - onSuccess: () => { - setMessageBoxState(() => ({ - activate: true, - status: "success", - description: null, - message: "Succeed.", - })); - if (amplitudeIsInit) { - sendAmplitudeData("create_source", { - type: "critical_action", - process: "source", - }); - } - router.push("/sources"); - }, - onError: (error) => { - if (error instanceof Error) { - setMessageBoxState(() => ({ - activate: true, - status: "error", - description: null, - message: error.message, - })); - } else { - setMessageBoxState(() => ({ - activate: true, - status: "error", - description: null, - message: "Something went wrong when create the source", - })); - } - }, - }); - }, - [amplitudeIsInit, createSource, router] - ); - - return ( - - {(formik) => { - return ( - - Setup Guide`} - /> -
- - - Set up - -
-
- ); - }} -
- ); -}; - -export default CreateSourceForm; diff --git a/src/components/forms/connector/source/CreateSourceForm/index.ts b/src/components/forms/connector/source/CreateSourceForm/index.ts deleted file mode 100644 index bc55f1bc66..0000000000 --- a/src/components/forms/connector/source/CreateSourceForm/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { default } from "./CreateSourceForm"; -export * from "./CreateSourceForm"; diff --git a/src/components/forms/index.ts b/src/components/forms/index.ts deleted file mode 100644 index 2db8cdd3dc..0000000000 --- a/src/components/forms/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -export * from "./connector"; -export * from "./model"; -export * from "./pipeline"; -export * from "./commons"; diff --git a/src/components/forms/model/ConfigureModelForm/ConfigureModelForm.tsx b/src/components/forms/model/ConfigureModelForm/ConfigureModelForm.tsx deleted file mode 100644 index 312e52ff66..0000000000 --- a/src/components/forms/model/ConfigureModelForm/ConfigureModelForm.tsx +++ /dev/null @@ -1,285 +0,0 @@ -import { FC, useCallback, useState } from "react"; -import { Formik } from "formik"; -import { - BasicProgressMessageBox, - ProgressMessageBoxState, -} from "@instill-ai/design-system"; - -import { FormikFormBase, TextArea } from "@/components/formik"; -import { PrimaryButton } from "@/components/ui"; -import { Model } from "@/lib/instill"; -import { useDeleteModel, useUpdateModel } from "@/services/model"; -import { Nullable } from "@/types/general"; -import { sendAmplitudeData } from "@/lib/amplitude"; -import { useAmplitudeCtx } from "@/contexts/AmplitudeContext"; -import useDeleteResourceModalState from "@/hooks/useDeleteResourceModalState"; -import { useRouter } from "next/router"; -import OutlineButton from "@/components/ui/Buttons/OutlineButton"; -import { DeleteResourceModal } from "@/components/modals"; -import { AxiosError } from "axios"; -import { ErrorDetails, Violation } from "@/lib/instill/types"; -import useDeleteResourceGuard from "@/hooks/useDeleteResourceGuard"; - -export type ConfigureModelFormProps = { - model: Nullable; - marginBottom: Nullable; -}; - -export type ConfigureModelFormValue = { - description: Nullable; -}; - -const ConfigureModelForm: FC = ({ - model, - marginBottom, -}) => { - const { amplitudeIsInit } = useAmplitudeCtx(); - const router = useRouter(); - - const [canEdit, setCanEdit] = useState(false); - - const handleEditButton = ( - values: ConfigureModelFormValue, - submitForm: () => Promise - ) => { - if (!canEdit) { - setCanEdit(true); - return; - } - - submitForm(); - }; - - // ################################################################### - // # # - // # 1 - handle update model # - // # # - // ################################################################### - - const [messageBoxState, setMessageBoxState] = - useState({ - activate: false, - message: null, - description: null, - status: null, - }); - - const updateModel = useUpdateModel(); - - const validateForm = useCallback((values: ConfigureModelFormValue) => { - const errors: Partial = {}; - - if (!values.description) { - errors.description = "Required"; - } - - return errors; - }, []); - - const handleSubmit = useCallback( - (values: ConfigureModelFormValue) => { - if (!model || !values.description) return; - - if (model.description === values.description) { - setCanEdit(false); - return; - } - - setMessageBoxState(() => ({ - activate: true, - status: "progressing", - description: null, - message: "Updating...", - })); - - updateModel.mutate( - { - name: model.name, - description: values.description, - }, - { - onSuccess: () => { - setCanEdit(false); - setMessageBoxState(() => ({ - activate: true, - status: "success", - description: null, - message: "Succeed.", - })); - - if (amplitudeIsInit) { - sendAmplitudeData("update_model", { - type: "critical_action", - process: "model", - }); - } - }, - onError: (error) => { - if (error instanceof Error) { - setMessageBoxState(() => ({ - activate: true, - status: "error", - description: null, - message: error.message, - })); - } else { - setMessageBoxState(() => ({ - activate: true, - status: "error", - description: null, - message: "Something went wrong when update the model", - })); - } - }, - } - ); - }, - [amplitudeIsInit, model, updateModel] - ); - - // ################################################################### - // # # - // # 2 - Handle delete model # - // # # - // ################################################################### - - const { disableResourceDeletion } = useDeleteResourceGuard(); - - const modalState = useDeleteResourceModalState(); - - const deleteModel = useDeleteModel(); - - const handleDeleteModel = useCallback(() => { - if (!model) return; - - setMessageBoxState({ - activate: true, - message: "Deleting...", - description: null, - status: "progressing", - }); - - deleteModel.mutate(model.name, { - onSuccess: () => { - setMessageBoxState({ - activate: true, - message: "Succeed.", - description: null, - status: "success", - }); - if (amplitudeIsInit) { - sendAmplitudeData("delete_model", { - type: "critical_action", - process: "model", - }); - } - router.push("/models"); - }, - onError: (error) => { - if (error instanceof AxiosError) { - setMessageBoxState({ - activate: true, - message: `${error.response?.status} - ${error.response?.data.message}`, - description: ( - (error.response?.data.details as ErrorDetails[])[0] - .violations as Violation[] - )[0].description, - status: "error", - }); - } else { - setMessageBoxState({ - activate: true, - message: "Something went wrong when delete the model", - description: null, - status: "error", - }); - } - modalState.setModalIsOpen(false); - }, - }); - }, [model, amplitudeIsInit, router, deleteModel, modalState]); - - return ( - <> - - {({ values, errors, submitForm }) => { - return ( - -
-