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`}
- />
-
-
- );
- }}
-
- );
-};
-
-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 (
-
-
-
-
-
-
modalState.setModalIsOpen(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
-
-
handleEditButton(values, submitForm)}
- position="ml-auto my-auto"
- type="button"
- >
- {canEdit ? "Done" : "Edit"}
-
-
-
-
-
-
- );
- }}
-
-
- >
- );
-};
-
-export default ConfigureModelForm;
diff --git a/src/components/forms/model/ConfigureModelForm/index.ts b/src/components/forms/model/ConfigureModelForm/index.ts
deleted file mode 100644
index 0a68f04714..0000000000
--- a/src/components/forms/model/ConfigureModelForm/index.ts
+++ /dev/null
@@ -1,2 +0,0 @@
-export { default } from "./ConfigureModelForm";
-export * from "./ConfigureModelForm";
diff --git a/src/components/forms/model/ConfigureModelInstanceForm/ConfigureModelInstanceForm.tsx b/src/components/forms/model/ConfigureModelInstanceForm/ConfigureModelInstanceForm.tsx
deleted file mode 100644
index 2483e6705f..0000000000
--- a/src/components/forms/model/ConfigureModelInstanceForm/ConfigureModelInstanceForm.tsx
+++ /dev/null
@@ -1,113 +0,0 @@
-import { FC, useState } from "react";
-import cn from "clsx";
-
-import { ModelInstance } from "@/lib/instill";
-import { Nullable } from "@/types/general";
-import { FormBase } from "../../commons";
-import {
- BasicProgressMessageBox,
- BasicTextField,
- ProgressMessageBoxState,
-} from "@instill-ai/design-system";
-
-export type ConfigureModelInstanceFormProps = {
- modelInstance: ModelInstance;
- marginBottom: Nullable;
-};
-
-export type ConfigureModelInstanceFormValue = {
- repo: Nullable;
- tag: Nullable;
- repoUrl: Nullable;
-};
-
-const ConfigureModelInstanceForm: FC = ({
- modelInstance,
- marginBottom,
-}) => {
- const [canEdit, setCanEdit] = useState(false);
-
- const [messageBoxState, setMessageBoxState] =
- useState({
- activate: false,
- message: null,
- description: null,
- status: null,
- });
-
- const handleEditButton = () => {
- if (!canEdit) {
- setCanEdit(true);
- return;
- }
- };
-
- return (
-
-
-
- {modelInstance.model_definition === "model-definitions/local" ? (
- <>>
- ) : null}
- {modelInstance.model_definition === "model-definitions/github" ? (
- <>
-
- >
- ) : null}
- {modelInstance.model_definition === "model-definitions/artivc" ? (
- <>
-
-
- >
- ) : null}
-
-
- {/*
-
handleEditButton()}
- position="ml-auto my-auto"
- type="button"
- >
- {canEdit ? "Done" : "Edit"}
-
-
*/}
-
-
-
-
- );
-};
-
-export default ConfigureModelInstanceForm;
diff --git a/src/components/forms/model/ConfigureModelInstanceForm/index.ts b/src/components/forms/model/ConfigureModelInstanceForm/index.ts
deleted file mode 100644
index dfefe1889f..0000000000
--- a/src/components/forms/model/ConfigureModelInstanceForm/index.ts
+++ /dev/null
@@ -1,2 +0,0 @@
-export { default } from "./ConfigureModelInstanceForm";
-export * from "./ConfigureModelInstanceForm";
diff --git a/src/components/forms/model/CreateModelForm/index.ts b/src/components/forms/model/CreateModelForm/index.ts
deleted file mode 100644
index 2570c2132c..0000000000
--- a/src/components/forms/model/CreateModelForm/index.ts
+++ /dev/null
@@ -1,2 +0,0 @@
-export { default } from "./CreateModelForm";
-export * from "./CreateModelForm";
diff --git a/src/components/forms/model/index.ts b/src/components/forms/model/index.ts
deleted file mode 100644
index 5f7cfa4755..0000000000
--- a/src/components/forms/model/index.ts
+++ /dev/null
@@ -1,21 +0,0 @@
-import ConfigureModelInstanceForm from "./ConfigureModelInstanceForm";
-import type {
- ConfigureModelInstanceFormProps,
- ConfigureModelInstanceFormValue,
-} from "./ConfigureModelInstanceForm";
-import CreateModelForm from "./CreateModelForm";
-import type { CreateModelFormValue } from "./CreateModelForm";
-import ConfigureModelForm from "./ConfigureModelForm";
-import type {
- ConfigureModelFormProps,
- ConfigureModelFormValue,
-} from "./ConfigureModelForm";
-
-export { ConfigureModelInstanceForm, CreateModelForm, ConfigureModelForm };
-export type {
- ConfigureModelInstanceFormProps,
- ConfigureModelInstanceFormValue,
- CreateModelFormValue,
- ConfigureModelFormProps,
- ConfigureModelFormValue,
-};
diff --git a/src/components/forms/pipeline/ConfigurePipelineForm/index.ts b/src/components/forms/pipeline/ConfigurePipelineForm/index.ts
deleted file mode 100644
index abdf5c7328..0000000000
--- a/src/components/forms/pipeline/ConfigurePipelineForm/index.ts
+++ /dev/null
@@ -1,2 +0,0 @@
-export { default } from "./ConfigurePipelineForm";
-export * from "./ConfigurePipelineForm";
diff --git a/src/components/layouts/PageBase/index.ts b/src/components/layouts/PageBase/index.ts
deleted file mode 100644
index 24fbf9e122..0000000000
--- a/src/components/layouts/PageBase/index.ts
+++ /dev/null
@@ -1,2 +0,0 @@
-export { default } from "./PageBase";
-export * from "./PageBase";
diff --git a/src/components/layouts/PageContentContainer/PageContentContainer.tsx b/src/components/layouts/PageContentContainer/PageContentContainer.tsx
deleted file mode 100644
index 9b93537233..0000000000
--- a/src/components/layouts/PageContentContainer/PageContentContainer.tsx
+++ /dev/null
@@ -1,11 +0,0 @@
-import { FC } from "react";
-
-const PageContentContainer: FC = ({ children }) => {
- return (
-
- {children}
-
- );
-};
-
-export default PageContentContainer;
diff --git a/src/components/layouts/PageContentContainer/index.ts b/src/components/layouts/PageContentContainer/index.ts
deleted file mode 100644
index 47fcb32901..0000000000
--- a/src/components/layouts/PageContentContainer/index.ts
+++ /dev/null
@@ -1 +0,0 @@
-export { default } from "./PageContentContainer";
diff --git a/src/components/layouts/PageHead/index.ts b/src/components/layouts/PageHead/index.ts
deleted file mode 100644
index b116796b31..0000000000
--- a/src/components/layouts/PageHead/index.ts
+++ /dev/null
@@ -1,2 +0,0 @@
-export { default } from "./PageHead";
-export * from "./PageHead";
diff --git a/src/components/layouts/index.ts b/src/components/layouts/index.ts
deleted file mode 100644
index d0ebeeb913..0000000000
--- a/src/components/layouts/index.ts
+++ /dev/null
@@ -1,6 +0,0 @@
-import PageBase from "./PageBase";
-import type { PageBaseProps } from "./PageBase";
-import PageContentContainer from "./PageContentContainer";
-
-export { PageBase, PageContentContainer };
-export type { PageBaseProps };
diff --git a/src/components/model/ConfigureModelForm.tsx b/src/components/model/ConfigureModelForm.tsx
new file mode 100644
index 0000000000..2bd06ec228
--- /dev/null
+++ b/src/components/model/ConfigureModelForm.tsx
@@ -0,0 +1,251 @@
+import { FC, useCallback, useState } from "react";
+import { useRouter } from "next/router";
+import { AxiosError } from "axios";
+import {
+ BasicProgressMessageBox,
+ ProgressMessageBoxState,
+ OutlineButton,
+ SolidButton,
+ BasicTextArea,
+} from "@instill-ai/design-system";
+
+import { DeleteResourceModal, FormBase } 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 useDeleteResourceGuard from "@/hooks/useDeleteResourceGuard";
+
+export type ConfigureModelFormProps = {
+ model: Nullable;
+ marginBottom: Nullable;
+};
+
+export type ConfigureModelFormValues = {
+ description: Nullable;
+};
+
+export type ConfigureModelFormErrors = {
+ description: Nullable;
+};
+
+const ConfigureModelForm: FC = ({
+ model,
+ marginBottom,
+}) => {
+ const { amplitudeIsInit } = useAmplitudeCtx();
+ const router = useRouter();
+
+ const [canEdit, setCanEdit] = useState(false);
+ const [fieldValues, setFieldValues] = useState({
+ description: model ? model.description : null,
+ });
+ const [formIsDirty, setFormIsDirty] = useState(false);
+
+ // ###################################################################
+ // # 1 - Handle update model #
+ // ###################################################################
+
+ const [messageBoxState, setMessageBoxState] =
+ useState({
+ activate: false,
+ message: null,
+ description: null,
+ status: null,
+ });
+
+ const updateModel = useUpdateModel();
+
+ const handleSubmit = useCallback(() => {
+ if (!canEdit) {
+ setCanEdit(true);
+ return;
+ }
+
+ if (!model || !formIsDirty) {
+ setCanEdit(false);
+ return;
+ }
+
+ if (model.description === fieldValues.description) {
+ setCanEdit(false);
+ return;
+ }
+
+ setMessageBoxState(() => ({
+ activate: true,
+ status: "progressing",
+ description: null,
+ message: "Updating...",
+ }));
+
+ updateModel.mutate(
+ {
+ name: model.name,
+ description: fieldValues.description || undefined,
+ },
+ {
+ onSuccess: () => {
+ setCanEdit(false);
+ setFormIsDirty(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, fieldValues, canEdit, formIsDirty]);
+
+ // ###################################################################
+ // # 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: JSON.stringify(
+ error.response?.data.details,
+ null,
+ "\t"
+ ),
+ 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 (
+ <>
+
+
+ {
+ setFormIsDirty(true);
+ setFieldValues({
+ description: event.target.value,
+ });
+ }}
+ />
+
+
+ modalState.setModalIsOpen(true)}
+ position="mr-auto my-auto"
+ type="button"
+ color="danger"
+ >
+ Delete
+
+
+ {canEdit ? "Save" : "Edit"}
+
+
+
+
+
+
+
+ >
+ );
+};
+
+export default ConfigureModelForm;
diff --git a/src/components/model/ConfigureModelInstanceForm.tsx b/src/components/model/ConfigureModelInstanceForm.tsx
new file mode 100644
index 0000000000..9febafa706
--- /dev/null
+++ b/src/components/model/ConfigureModelInstanceForm.tsx
@@ -0,0 +1,85 @@
+import { useState } from "react";
+import {
+ BasicProgressMessageBox,
+ BasicTextField,
+ ProgressMessageBoxState,
+} from "@instill-ai/design-system";
+
+import { ModelInstance } from "@/lib/instill";
+import { Nullable } from "@/types/general";
+import { FormBase } from "@/components/ui";
+
+export type ConfigureModelInstanceFormProps = {
+ modelInstance: ModelInstance;
+ marginBottom: Nullable;
+};
+
+const ConfigureModelInstanceForm = ({
+ modelInstance,
+ marginBottom,
+}: ConfigureModelInstanceFormProps) => {
+ const [messageBoxState, setMessageBoxState] =
+ useState({
+ activate: false,
+ message: null,
+ description: null,
+ status: null,
+ });
+
+ return (
+
+
+ {modelInstance.model_definition === "model-definitions/local" ? (
+ <>>
+ ) : null}
+ {modelInstance.model_definition === "model-definitions/github" ? (
+ <>
+
+ >
+ ) : null}
+ {modelInstance.model_definition === "model-definitions/artivc" ? (
+ <>
+
+
+ >
+ ) : null}
+
+
+
+
+
+ );
+};
+
+export default ConfigureModelInstanceForm;
diff --git a/src/components/forms/model/CreateModelForm/CreateModelForm.tsx b/src/components/model/CreateModelForm.tsx
similarity index 98%
rename from src/components/forms/model/CreateModelForm/CreateModelForm.tsx
rename to src/components/model/CreateModelForm.tsx
index 2e6e386f20..9a47b0023a 100644
--- a/src/components/forms/model/CreateModelForm/CreateModelForm.tsx
+++ b/src/components/model/CreateModelForm.tsx
@@ -1,4 +1,4 @@
-import { FC, useCallback, useMemo, useState } from "react";
+import { useCallback, useMemo, useState } from "react";
import { useRouter } from "next/router";
import { Formik } from "formik";
import {
@@ -6,6 +6,7 @@ import {
ModelInstanceIcon,
ProgressMessageBoxState,
SingleSelectOption,
+ SolidButton,
} from "@instill-ai/design-system";
import {
@@ -14,7 +15,7 @@ import {
TextArea,
TextField,
UploadFileField,
-} from "../../../formik";
+} from "../formik";
import {
useCreateArtivcModel,
useCreateGithubModel,
@@ -24,7 +25,7 @@ import {
useModelDefinitions,
useModelInstances,
} from "@/services/model";
-import { ModelDefinitionIcon, PrimaryButton } from "@/components/ui";
+import { ModelDefinitionIcon } from "@/components/ui";
import {
CreateArtivcModelPayload,
CreateGithubModelPayload,
@@ -50,7 +51,7 @@ export type CreateModelFormValue = {
huggingFaceRepo: Nullable;
};
-const CreateNewModelFlow: FC = () => {
+const CreateModelForm = () => {
const { amplitudeIsInit } = useAmplitudeCtx();
// ###################################################################
@@ -629,14 +630,15 @@ const CreateNewModelFlow: FC = () => {
width="w-[25vw]"
closable={true}
/>
- handelCreateModel(values)}
position="ml-auto my-auto"
type="button"
+ color="primary"
>
Set up
-
+
{canDisplayDeployModelInstanceSection ? (
<>
@@ -663,14 +665,15 @@ const CreateNewModelFlow: FC = () => {
width="w-[25vw]"
closable={true}
/>
- handleDeployModelInstance(values)}
position="ml-auto my-auto"
type="button"
+ color="primary"
>
Deploy
-
+
>
) : null}
@@ -681,4 +684,4 @@ const CreateNewModelFlow: FC = () => {
);
};
-export default CreateNewModelFlow;
+export default CreateModelForm;
diff --git a/src/components/model/TestModelInstanceSection/TestModelInstanceSection.tsx b/src/components/model/TestModelInstanceSection/TestModelInstanceSection.tsx
index cbb5286261..a16c6f13b9 100644
--- a/src/components/model/TestModelInstanceSection/TestModelInstanceSection.tsx
+++ b/src/components/model/TestModelInstanceSection/TestModelInstanceSection.tsx
@@ -12,7 +12,7 @@ import { useAmplitudeCtx } from "@/contexts/AmplitudeContext";
import { sendAmplitudeData } from "@/lib/amplitude";
import { useTestModelInstance } from "@/services/model";
import { CodeBlock } from "@/components/ui";
-import Prism from "prismjs";
+import { highlightAll } from "prismjs";
export type TestModelInstanceSectionProps = {
modelInstance: Nullable;
@@ -83,7 +83,7 @@ const TestModelInstanceSection: FC = ({
code: JSON.stringify(result, null, "\t"),
});
- Prism.highlightAll();
+ highlightAll();
} catch (err) {
console.log(err);
setTestResult(null);
diff --git a/src/components/model/index.ts b/src/components/model/index.ts
index 5549738a90..27ff9921d8 100644
--- a/src/components/model/index.ts
+++ b/src/components/model/index.ts
@@ -1,5 +1,25 @@
import TestModelInstanceSection from "./TestModelInstanceSection";
import type { TestModelInstanceSectionProps } from "./TestModelInstanceSection";
+import ConfigureModelForm from "./ConfigureModelForm";
+import type {
+ ConfigureModelFormProps,
+ ConfigureModelFormValues,
+} from "./ConfigureModelForm";
+import ConfigureModelInstanceForm from "./ConfigureModelInstanceForm";
+import type { ConfigureModelInstanceFormProps } from "./ConfigureModelInstanceForm";
+import CreateModelForm from "./CreateModelForm";
+import type { CreateModelFormValue } from "./CreateModelForm";
-export { TestModelInstanceSection };
-export type { TestModelInstanceSectionProps };
+export {
+ TestModelInstanceSection,
+ ConfigureModelForm,
+ ConfigureModelInstanceForm,
+ CreateModelForm,
+};
+export type {
+ TestModelInstanceSectionProps,
+ ConfigureModelFormProps,
+ ConfigureModelFormValues,
+ ConfigureModelInstanceFormProps,
+ CreateModelFormValue,
+};
diff --git a/src/components/onboarding/OnboardingForm/OnboardingForm.tsx b/src/components/onboarding/OnboardingForm/OnboardingForm.tsx
index c843a57a65..ddb1a9a9b0 100644
--- a/src/components/onboarding/OnboardingForm/OnboardingForm.tsx
+++ b/src/components/onboarding/OnboardingForm/OnboardingForm.tsx
@@ -1,7 +1,8 @@
-import { ChangeEvent, FC, useCallback, useEffect, useState } from "react";
+import { ChangeEvent, useCallback, useEffect, useState } from "react";
import { useRouter } from "next/router";
import axios from "axios";
import { v4 as uuidv4 } from "uuid";
+
import {
BasicProgressMessageBox,
BasicTextField,
@@ -9,44 +10,43 @@ import {
SingleSelectOption,
BasicSingleSelect,
BasicToggleField,
+ SolidButton,
} from "@instill-ai/design-system";
-import { PrimaryButton } from "@/components/ui";
import { useUpdateUser } from "@/services/mgmt";
import { User, mockMgmtRoles } from "@/lib/instill/mgmt";
import { useAmplitudeCtx } from "@/contexts/AmplitudeContext";
import { sendAmplitudeData } from "@/lib/amplitude";
import { Nullable } from "@/types/general";
-import { FormBase } from "@/components/forms";
+import { FormBase } from "@/components/ui";
export type OnboardingFormProps = {
user: Nullable>;
};
-export type OnboardingFormValue = {
+export type OnboardingFormValues = {
email: Nullable;
companyName: Nullable;
role: Nullable;
newsletterSubscription: Nullable;
};
-type OnboardingFormError = {
- email?: string;
- companyName?: string;
- role?: string;
- newsletterSubscription?: string;
+type OnboardingFormErrors = {
+ email: Nullable;
+ companyName: Nullable;
+ role: Nullable;
};
-const OnboardingForm: FC = ({ user }) => {
+const OnboardingForm = ({ user }: OnboardingFormProps) => {
const router = useRouter();
const updateUser = useUpdateUser();
const { amplitudeIsInit } = useAmplitudeCtx();
- const [values, setValues] = useState({
+ const [fieldValues, setFieldValues] = useState({
email: null,
companyName: null,
role: null,
- newsletterSubscription: null,
+ newsletterSubscription: true,
});
const [selectedRoleOption, setSelectedRoleOption] =
@@ -65,9 +65,9 @@ const OnboardingForm: FC = ({ user }) => {
// Handle fields change
const handleFieldChange = useCallback(
- (key: keyof OnboardingFormValue, event: ChangeEvent) => {
+ (key: keyof OnboardingFormValues, event: ChangeEvent) => {
setFormIsDirty(true);
- setValues((prev) => ({
+ setFieldValues((prev) => ({
...prev,
[key]:
key === "newsletterSubscription"
@@ -83,7 +83,7 @@ const OnboardingForm: FC = ({ user }) => {
if (!option) return;
setSelectedRoleOption(option);
- setValues((prev) => ({
+ setFieldValues((prev) => ({
...prev,
role: option.value as string,
}));
@@ -93,46 +93,55 @@ const OnboardingForm: FC = ({ user }) => {
// Validate form and deal with error handling
- const [errors, setErrors] = useState({});
+ const [fieldErrors, setFieldErrors] = useState({
+ email: null,
+ companyName: null,
+ role: null,
+ });
const [formIsValid, setFormIsValid] = useState(false);
useEffect(() => {
- if (!values.email) {
- if (formIsDirty) {
- errors.email = "Email is required";
- setErrors({
- email: "Email is required",
- });
- }
+ if (!formIsDirty) return;
+ const errors = {} as OnboardingFormErrors;
+
+ if (!fieldValues.email) {
+ errors["email"] = "Email is required";
} else {
- if (!/^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}$/i.test(values.email)) {
- setErrors({
- email: "Invalid email address",
- });
- return;
- } else {
- setErrors({
- email: undefined,
- });
- setFormIsValid(true);
+ if (
+ !/^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}$/i.test(fieldValues.email)
+ ) {
+ errors["email"] = "Invalid email address";
}
}
- }, [values]);
- // Handle form submission
+ if (!fieldValues.companyName) {
+ errors["companyName"] = "Company name is required";
+ }
+
+ if (!fieldValues.role) {
+ errors["role"] = "Role is required";
+ }
+
+ setFieldErrors(errors);
+
+ if (Object.keys(errors).length === 0) {
+ setFormIsValid(true);
+ }
+ }, [fieldValues, formIsDirty]);
const handleSubmit = useCallback(() => {
- if (!values.email) return;
+ if (!fieldValues.email || !fieldValues.companyName || !fieldValues.role)
+ return;
const token = uuidv4();
const payload: Partial = {
id: "local-user",
- email: values.email,
- company_name: values.companyName ?? undefined,
- role: values.role as string,
- newsletter_subscription: values.newsletterSubscription
- ? values.newsletterSubscription
+ email: fieldValues.email,
+ company_name: fieldValues.companyName ?? undefined,
+ role: fieldValues.role as string,
+ newsletter_subscription: fieldValues.newsletterSubscription
+ ? fieldValues.newsletterSubscription
: false,
cookie_token: user ? user.cookie_token : token,
};
@@ -183,7 +192,7 @@ const OnboardingForm: FC = ({ user }) => {
}
},
});
- }, [user, values, amplitudeIsInit, router, updateUser]);
+ }, [user, fieldValues, amplitudeIsInit, router, updateUser]);
return (
= ({ user }) => {
label="Your email"
description="Fill your email address"
required={true}
- value={values.email}
+ value={fieldValues.email}
onChange={(event) => handleFieldChange("email", event)}
- error={errors.email}
+ error={fieldErrors.email}
/>
handleFieldChange("companyName", event)}
- error={errors.companyName}
+ error={fieldErrors.companyName}
/>
handleFieldChange("newsletterSubscription", event)
}
- error={errors.newsletterSubscription}
/>
@@ -239,14 +249,15 @@ const OnboardingForm: FC
= ({ user }) => {
width="w-[25vw]"
closable={true}
/>
-
Start
-
+
);
diff --git a/src/components/onboarding/index.ts b/src/components/onboarding/index.ts
index e5583543bb..4267225a01 100644
--- a/src/components/onboarding/index.ts
+++ b/src/components/onboarding/index.ts
@@ -1,8 +1,8 @@
import OnboardingForm from "./OnboardingForm";
import type {
OnboardingFormProps,
- OnboardingFormValue,
+ OnboardingFormValues,
} from "./OnboardingForm";
export { OnboardingForm };
-export type { OnboardingFormProps, OnboardingFormValue };
+export type { OnboardingFormProps, OnboardingFormValues };
diff --git a/src/components/forms/pipeline/ConfigurePipelineForm/ConfigurePipelineForm.tsx b/src/components/pipeline/ConfigurePipelineForm.tsx
similarity index 90%
rename from src/components/forms/pipeline/ConfigurePipelineForm/ConfigurePipelineForm.tsx
rename to src/components/pipeline/ConfigurePipelineForm.tsx
index 941e35e9fe..9f7dafe98d 100644
--- a/src/components/forms/pipeline/ConfigurePipelineForm/ConfigurePipelineForm.tsx
+++ b/src/components/pipeline/ConfigurePipelineForm.tsx
@@ -1,20 +1,20 @@
import { FC, useCallback, useState } from "react";
import { Formik } from "formik";
+import { useRouter } from "next/router";
import {
BasicProgressMessageBox,
ProgressMessageBoxState,
+ OutlineButton,
+ SolidButton,
} from "@instill-ai/design-system";
import { FormikFormBase, TextArea } from "@/components/formik";
-import { PrimaryButton } from "@/components/ui";
+import { DeleteResourceModal } from "@/components/ui";
import { Pipeline, PipelineState } from "@/lib/instill";
import { Nullable } from "@/types/general";
import { useDeletePipeline, useUpdatePipeline } from "@/services/pipeline";
import { useAmplitudeCtx } from "@/contexts/AmplitudeContext";
import { sendAmplitudeData } from "@/lib/amplitude";
-import { DeleteResourceModal } from "@/components/modals";
-import { useRouter } from "next/router";
-import OutlineButton from "@/components/ui/Buttons/OutlineButton";
import useDeleteResourceGuard from "@/hooks/useDeleteResourceGuard";
export type ConfigurePipelineFormProps = {
@@ -52,7 +52,7 @@ const ConfigurePipelineForm: FC = ({
const updatePipeline = useUpdatePipeline();
- const validateForm = useCallback((values: ConfigurePipelineFormValue) => {
+ const validateForm = useCallback(() => {
const errors: Partial = {};
return errors;
@@ -226,28 +226,19 @@ const ConfigurePipelineForm: FC = ({
onClickHandler={() => setDeletePipelineModalIsOpen(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"
+ color="danger"
>
Delete
- handleEditButton(values, submitForm)}
position="ml-auto my-auto"
type="button"
+ color="primary"
>
{canEdit ? "Done" : "Edit"}
-
+
= ({
})
);
}
- }, [destinationDefinitions.isSuccess, destinationDefinitions.data]);
+ }, [
+ destinationDefinitions.isSuccess,
+ destinationDefinitions.data,
+ values.pipeline.mode,
+ ]);
const selectedDestinationDefinitionOption = useMemo(() => {
if (
@@ -180,14 +184,15 @@ const CreateNewDestinationFlow: FC = ({
required={true}
description={"Setup Guide"}
/>
-
Set up
-
+
);
};
diff --git a/src/components/forms/pipeline/CreatePipelineForm/SetupDestinationStep/SetupDestinationStep.tsx b/src/components/pipeline/CreatePipelineForm/SetupDestinationStep/SetupDestinationStep.tsx
similarity index 95%
rename from src/components/forms/pipeline/CreatePipelineForm/SetupDestinationStep/SetupDestinationStep.tsx
rename to src/components/pipeline/CreatePipelineForm/SetupDestinationStep/SetupDestinationStep.tsx
index 6da3060e75..f8d7a240fa 100644
--- a/src/components/forms/pipeline/CreatePipelineForm/SetupDestinationStep/SetupDestinationStep.tsx
+++ b/src/components/pipeline/CreatePipelineForm/SetupDestinationStep/SetupDestinationStep.tsx
@@ -1,12 +1,8 @@
import { FC, useEffect, useState } from "react";
import { useFormikContext } from "formik";
-import { SingleSelectOption } from "@instill-ai/design-system";
+import { SingleSelectOption, SolidButton } from "@instill-ai/design-system";
-import {
- ConnectorIcon,
- FormVerticalDivider,
- PrimaryButton,
-} from "@/components/ui";
+import { ConnectorIcon, FormVerticalDivider } from "@/components/ui";
import { SingleSelect, FormikStep } from "@/components/formik";
import {
StepNumberState,
@@ -17,7 +13,7 @@ import { useCreateDestination, useDestinations } from "@/services/connector";
import { CreateDestinationPayload } from "@/lib/instill";
import { useAmplitudeCtx } from "@/contexts/AmplitudeContext";
import { sendAmplitudeData } from "@/lib/amplitude";
-import { CreateDestinationForm } from "@/components/forms/connector";
+import { CreateDestinationForm } from "@/components/destination";
export type SetupDestinationStepProps = StepNumberState;
@@ -175,14 +171,15 @@ const SetupDestinationStep: FC = (props) => {
disabled={true}
required={true}
/>
-
Next
-
+
) : (
diff --git a/src/components/forms/pipeline/CreatePipelineForm/SetupDestinationStep/UseExistingDestinationFlow.tsx b/src/components/pipeline/CreatePipelineForm/SetupDestinationStep/UseExistingDestinationFlow.tsx
similarity index 94%
rename from src/components/forms/pipeline/CreatePipelineForm/SetupDestinationStep/UseExistingDestinationFlow.tsx
rename to src/components/pipeline/CreatePipelineForm/SetupDestinationStep/UseExistingDestinationFlow.tsx
index 3dd0925e3d..ba2e59752c 100644
--- a/src/components/forms/pipeline/CreatePipelineForm/SetupDestinationStep/UseExistingDestinationFlow.tsx
+++ b/src/components/pipeline/CreatePipelineForm/SetupDestinationStep/UseExistingDestinationFlow.tsx
@@ -1,8 +1,7 @@
import { FC, useEffect, useMemo, useState } from "react";
import { useFormikContext } from "formik";
-import { SingleSelectOption } from "@instill-ai/design-system";
+import { SingleSelectOption, SolidButton } from "@instill-ai/design-system";
-import { PrimaryButton } from "@/components/ui";
import { SingleSelect } from "@/components/formik";
import {
StepNumberState,
@@ -60,7 +59,7 @@ const UseExistingDestinationFlow: FC
= ({
})
);
}
- }, [destinations.isSuccess, destinations.data]);
+ }, [destinations.isSuccess, destinations.data, values.pipeline.mode]);
const selectedDestinationOption = useMemo(() => {
if (!values.destination.existing.id || !destinationOptions) return null;
@@ -111,14 +110,15 @@ const UseExistingDestinationFlow: FC = ({
error={errors.destination?.existing?.id || null}
required={true}
/>
-
Select
-
+
);
};
diff --git a/src/components/forms/pipeline/CreatePipelineForm/SetupDestinationStep/index.ts b/src/components/pipeline/CreatePipelineForm/SetupDestinationStep/index.ts
similarity index 100%
rename from src/components/forms/pipeline/CreatePipelineForm/SetupDestinationStep/index.ts
rename to src/components/pipeline/CreatePipelineForm/SetupDestinationStep/index.ts
diff --git a/src/components/forms/pipeline/CreatePipelineForm/SetupModelStep/CreateNewModelInstanceFlow.tsx b/src/components/pipeline/CreatePipelineForm/SetupModelStep/CreateNewModelInstanceFlow.tsx
similarity index 98%
rename from src/components/forms/pipeline/CreatePipelineForm/SetupModelStep/CreateNewModelInstanceFlow.tsx
rename to src/components/pipeline/CreatePipelineForm/SetupModelStep/CreateNewModelInstanceFlow.tsx
index 28680a2762..30558dd94b 100644
--- a/src/components/forms/pipeline/CreatePipelineForm/SetupModelStep/CreateNewModelInstanceFlow.tsx
+++ b/src/components/pipeline/CreatePipelineForm/SetupModelStep/CreateNewModelInstanceFlow.tsx
@@ -13,6 +13,7 @@ import {
ModelInstanceIcon,
ProgressMessageBoxState,
SingleSelectOption,
+ SolidButton,
} from "@instill-ai/design-system";
import {
@@ -34,7 +35,7 @@ import {
useModelDefinitions,
useModelInstances,
} from "@/services/model";
-import { ModelDefinitionIcon, PrimaryButton } from "@/components/ui";
+import { ModelDefinitionIcon } from "@/components/ui";
import {
CreateArtivcModelPayload,
CreateGithubModelPayload,
@@ -605,14 +606,15 @@ const CreateNewModelInstanceFlow: FC = ({
width="w-[15vw]"
closable={true}
/>
-
Set up
-
+
{canDisplayDeployModelSection ? (
@@ -639,14 +641,15 @@ const CreateNewModelInstanceFlow: FC = ({
width="w-[15vw]"
closable={true}
/>
-
Deploy
-
+
>
) : null}
diff --git a/src/components/forms/pipeline/CreatePipelineForm/SetupModelStep/SetupModelStep.tsx b/src/components/pipeline/CreatePipelineForm/SetupModelStep/SetupModelStep.tsx
similarity index 100%
rename from src/components/forms/pipeline/CreatePipelineForm/SetupModelStep/SetupModelStep.tsx
rename to src/components/pipeline/CreatePipelineForm/SetupModelStep/SetupModelStep.tsx
diff --git a/src/components/forms/pipeline/CreatePipelineForm/SetupModelStep/UseExistingModeInstancelFlow.tsx b/src/components/pipeline/CreatePipelineForm/SetupModelStep/UseExistingModeInstancelFlow.tsx
similarity index 96%
rename from src/components/forms/pipeline/CreatePipelineForm/SetupModelStep/UseExistingModeInstancelFlow.tsx
rename to src/components/pipeline/CreatePipelineForm/SetupModelStep/UseExistingModeInstancelFlow.tsx
index 456ce54699..e0cf448901 100644
--- a/src/components/forms/pipeline/CreatePipelineForm/SetupModelStep/UseExistingModeInstancelFlow.tsx
+++ b/src/components/pipeline/CreatePipelineForm/SetupModelStep/UseExistingModeInstancelFlow.tsx
@@ -8,9 +8,8 @@ import {
useState,
} from "react";
import { useFormikContext } from "formik";
-import { SingleSelectOption } from "@instill-ai/design-system";
+import { SingleSelectOption, SolidButton } from "@instill-ai/design-system";
-import { PrimaryButton } from "@/components/ui";
import { SingleSelect } from "@/components/formik";
import {
StepNumberState,
@@ -144,14 +143,15 @@ const UseExistingModeInstancelFlow: FC = ({
disabled={modelCreated ? true : false}
required={true}
/>
-
Select
-
+
);
};
diff --git a/src/components/forms/pipeline/CreatePipelineForm/SetupModelStep/index.ts b/src/components/pipeline/CreatePipelineForm/SetupModelStep/index.ts
similarity index 100%
rename from src/components/forms/pipeline/CreatePipelineForm/SetupModelStep/index.ts
rename to src/components/pipeline/CreatePipelineForm/SetupModelStep/index.ts
diff --git a/src/components/forms/pipeline/CreatePipelineForm/SetupPipelineDetailsStep/SetupPipelineDetailsStep.tsx b/src/components/pipeline/CreatePipelineForm/SetupPipelineDetailsStep/SetupPipelineDetailsStep.tsx
similarity index 98%
rename from src/components/forms/pipeline/CreatePipelineForm/SetupPipelineDetailsStep/SetupPipelineDetailsStep.tsx
rename to src/components/pipeline/CreatePipelineForm/SetupPipelineDetailsStep/SetupPipelineDetailsStep.tsx
index aafd882fc7..cce49fda0e 100644
--- a/src/components/forms/pipeline/CreatePipelineForm/SetupPipelineDetailsStep/SetupPipelineDetailsStep.tsx
+++ b/src/components/pipeline/CreatePipelineForm/SetupPipelineDetailsStep/SetupPipelineDetailsStep.tsx
@@ -3,9 +3,9 @@ import { useFormikContext } from "formik";
import {
BasicProgressMessageBox,
ProgressMessageBoxState,
+ SolidButton,
} from "@instill-ai/design-system";
-import { PrimaryButton } from "@/components/ui";
import { CreatePipelineFormValues } from "../CreatePipelineForm";
import { TextArea, TextField, FormikStep } from "@/components/formik";
import { useCreatePipeline, useUpdatePipeline } from "@/services/pipeline";
@@ -275,14 +275,15 @@ const SetupPipelineDetailsStep: FC = () => {
width="w-[25vw]"
closable={true}
/>
-
Set up
-
+
diff --git a/src/components/forms/pipeline/CreatePipelineForm/SetupPipelineDetailsStep/index.ts b/src/components/pipeline/CreatePipelineForm/SetupPipelineDetailsStep/index.ts
similarity index 100%
rename from src/components/forms/pipeline/CreatePipelineForm/SetupPipelineDetailsStep/index.ts
rename to src/components/pipeline/CreatePipelineForm/SetupPipelineDetailsStep/index.ts
diff --git a/src/components/forms/pipeline/CreatePipelineForm/SetupPipelineModeStep/SetupPipelineModeStep.tsx b/src/components/pipeline/CreatePipelineForm/SetupPipelineModeStep/SetupPipelineModeStep.tsx
similarity index 98%
rename from src/components/forms/pipeline/CreatePipelineForm/SetupPipelineModeStep/SetupPipelineModeStep.tsx
rename to src/components/pipeline/CreatePipelineForm/SetupPipelineModeStep/SetupPipelineModeStep.tsx
index 855b9fb9cd..0a7530edf3 100644
--- a/src/components/forms/pipeline/CreatePipelineForm/SetupPipelineModeStep/SetupPipelineModeStep.tsx
+++ b/src/components/pipeline/CreatePipelineForm/SetupPipelineModeStep/SetupPipelineModeStep.tsx
@@ -4,6 +4,7 @@ import {
AsyncIcon,
SingleSelectOption,
SyncIcon,
+ SolidButton,
} from "@instill-ai/design-system";
import { SingleSelect, FormikStep } from "@/components/formik";
@@ -11,7 +12,6 @@ import {
StepNumberState,
CreatePipelineFormValues,
} from "../CreatePipelineForm";
-import { PrimaryButton } from "@/components/ui";
import { useCreateSource, useSources } from "@/services/connector";
import ConnectorIcon from "@/components/ui/ConnectorIcon";
import { CreateSourcePayload } from "@/lib/instill";
@@ -210,14 +210,15 @@ const SetupPipelineModeStep: FC = ({
menuPlacement="auto"
/>
-
Next
-
+
);
};
diff --git a/src/components/forms/pipeline/CreatePipelineForm/SetupPipelineModeStep/index.ts b/src/components/pipeline/CreatePipelineForm/SetupPipelineModeStep/index.ts
similarity index 100%
rename from src/components/forms/pipeline/CreatePipelineForm/SetupPipelineModeStep/index.ts
rename to src/components/pipeline/CreatePipelineForm/SetupPipelineModeStep/index.ts
diff --git a/src/components/forms/pipeline/CreatePipelineForm/SetupSourceStep/CreateNewSourceFlow.tsx b/src/components/pipeline/CreatePipelineForm/SetupSourceStep/CreateNewSourceFlow.tsx
similarity index 93%
rename from src/components/forms/pipeline/CreatePipelineForm/SetupSourceStep/CreateNewSourceFlow.tsx
rename to src/components/pipeline/CreatePipelineForm/SetupSourceStep/CreateNewSourceFlow.tsx
index 9e21f876bc..374aae9bf2 100644
--- a/src/components/forms/pipeline/CreatePipelineForm/SetupSourceStep/CreateNewSourceFlow.tsx
+++ b/src/components/pipeline/CreatePipelineForm/SetupSourceStep/CreateNewSourceFlow.tsx
@@ -1,10 +1,8 @@
import { FC, useEffect, useMemo, useRef, useState } from "react";
import { useFormikContext } from "formik";
-import { SingleSelectOption } from "@instill-ai/design-system";
+import { SingleSelectOption, SolidButton } from "@instill-ai/design-system";
-import { PrimaryButton } from "@/components/ui";
import { SingleSelect, TextField } from "@/components/formik";
-import { mockAsyncDataConnectionOptions } from "../../../MockData";
import {
StepNumberState,
CreatePipelineFormValues,
@@ -38,7 +36,7 @@ const CreateNewSourceFlow: FC = ({
const sources = useSources();
useEffect(() => {
- setSourceOptions(mockAsyncDataConnectionOptions);
+ setSourceOptions([]);
}, []);
const canCreateNewSource = useMemo(() => {
@@ -125,14 +123,15 @@ const CreateNewSourceFlow: FC = ({
error={errors.source?.new?.definition || null}
required={true}
/>
-
Set up source
-
+
);
};
diff --git a/src/components/forms/pipeline/CreatePipelineForm/SetupSourceStep/SetupSourceStep.tsx b/src/components/pipeline/CreatePipelineForm/SetupSourceStep/SetupSourceStep.tsx
similarity index 100%
rename from src/components/forms/pipeline/CreatePipelineForm/SetupSourceStep/SetupSourceStep.tsx
rename to src/components/pipeline/CreatePipelineForm/SetupSourceStep/SetupSourceStep.tsx
diff --git a/src/components/forms/pipeline/CreatePipelineForm/SetupSourceStep/UseExistingSourceFlow.tsx b/src/components/pipeline/CreatePipelineForm/SetupSourceStep/UseExistingSourceFlow.tsx
similarity index 95%
rename from src/components/forms/pipeline/CreatePipelineForm/SetupSourceStep/UseExistingSourceFlow.tsx
rename to src/components/pipeline/CreatePipelineForm/SetupSourceStep/UseExistingSourceFlow.tsx
index 0227bc01cc..e766a61fea 100644
--- a/src/components/forms/pipeline/CreatePipelineForm/SetupSourceStep/UseExistingSourceFlow.tsx
+++ b/src/components/pipeline/CreatePipelineForm/SetupSourceStep/UseExistingSourceFlow.tsx
@@ -1,6 +1,6 @@
import { FC, useState, useEffect, useMemo } from "react";
import { useFormikContext } from "formik";
-import { SingleSelectOption } from "@instill-ai/design-system";
+import { SingleSelectOption, SolidButton } from "@instill-ai/design-system";
import { SingleSelect } from "@/components/formik";
import {
@@ -8,7 +8,7 @@ import {
CreatePipelineFormValues,
} from "../CreatePipelineForm";
import { useSources } from "@/services/connector";
-import { ConnectorIcon, PrimaryButton } from "@/components/ui";
+import { ConnectorIcon } from "@/components/ui";
import { Nullable } from "@/types/general";
import { useAmplitudeCtx } from "@/contexts/AmplitudeContext";
import { sendAmplitudeData } from "@/lib/amplitude";
@@ -114,14 +114,15 @@ const UseExistingSourceFlow: FC = ({
error={errors.source?.existing?.id || null}
required={true}
/>
-
Use model
-
+
);
};
diff --git a/src/components/forms/pipeline/CreatePipelineForm/SetupSourceStep/index.ts b/src/components/pipeline/CreatePipelineForm/SetupSourceStep/index.ts
similarity index 100%
rename from src/components/forms/pipeline/CreatePipelineForm/SetupSourceStep/index.ts
rename to src/components/pipeline/CreatePipelineForm/SetupSourceStep/index.ts
diff --git a/src/components/forms/pipeline/CreatePipelineForm/index.ts b/src/components/pipeline/CreatePipelineForm/index.ts
similarity index 100%
rename from src/components/forms/pipeline/CreatePipelineForm/index.ts
rename to src/components/pipeline/CreatePipelineForm/index.ts
diff --git a/src/components/forms/pipeline/index.ts b/src/components/pipeline/index.ts
similarity index 100%
rename from src/components/forms/pipeline/index.ts
rename to src/components/pipeline/index.ts
diff --git a/src/components/forms/connector/source/ConfigureSourceForm/ConfigureSourceForm.tsx b/src/components/source/ConfigureSourceForm.tsx
similarity index 54%
rename from src/components/forms/connector/source/ConfigureSourceForm/ConfigureSourceForm.tsx
rename to src/components/source/ConfigureSourceForm.tsx
index 4d169b4d5c..40c9e9733c 100644
--- a/src/components/forms/connector/source/ConfigureSourceForm/ConfigureSourceForm.tsx
+++ b/src/components/source/ConfigureSourceForm.tsx
@@ -1,45 +1,46 @@
-import { FC, useState, useEffect, useCallback } from "react";
-import { Formik } from "formik";
+import { useState, useEffect, useCallback } from "react";
import {
BasicProgressMessageBox,
ProgressMessageBoxState,
SingleSelectOption,
+ OutlineButton,
+ SolidButton,
+ BasicSingleSelect,
} from "@instill-ai/design-system";
import { useRouter } from "next/router";
+import { AxiosError } from "axios";
-import { FormikFormBase, SingleSelect } from "@/components/formik";
-import { ConnectorIcon, PrimaryButton } from "@/components/ui";
+import { ConnectorIcon, DeleteResourceModal, FormBase } from "@/components/ui";
import { SourceWithPipelines } from "@/lib/instill";
import { Nullable } from "@/types/general";
-import { DeleteResourceModal } from "@/components/modals";
import { useDeleteSource } 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 ConfigureSourceFormProps = {
source: Nullable;
+ marginBottom: Nullable;
};
export type ConfigureSourceFormValue = {
sourceDefinition: Nullable;
};
-const ConfigureSourceForm: FC = ({ source }) => {
+const ConfigureSourceForm = ({
+ source,
+ marginBottom,
+}: ConfigureSourceFormProps) => {
const router = useRouter();
const { amplitudeIsInit } = useAmplitudeCtx();
// ###################################################################
- // # #
// # Initialize the source definition #
- // # #
// ###################################################################
- const [syncSourceDefinitionOptions, setSyncSourceDefinitionOptions] =
- useState([]);
+ const [sourceDefinitionOptions, setSourceDefinitionOptions] =
+ useState>(null);
const [selectedSourceDefinitionOption, setSelectedSourceDefinitionOption] =
useState>(null);
const [canEdit, setCanEdit] = useState(false);
@@ -76,26 +77,26 @@ const ConfigureSourceForm: FC = ({ source }) => {
},
];
- setSyncSourceDefinitionOptions(options);
+ setSourceDefinitionOptions(options);
setSelectedSourceDefinitionOption(
options.find((e) => e.value === source.id) || null
);
}, [source]);
- const sourceDefinitionOnChangeCb = useCallback(
- (option: SingleSelectOption) => {
- setSelectedSourceDefinitionOption(
- syncSourceDefinitionOptions.find((e) => e.value === option.value) ||
- null
- );
- },
- [syncSourceDefinitionOptions]
- );
+ // ###################################################################
+ // # Handle delete destination #
+ // ###################################################################
+
+ const handleSubmit = useCallback(() => {
+ if (canEdit) {
+ setCanEdit(false);
+ } else {
+ setCanEdit(true);
+ }
+ }, [canEdit]);
// ###################################################################
- // # #
// # Handle delete destination #
- // # #
// ###################################################################
const { disableResourceDeletion } = useDeleteResourceGuard();
@@ -165,81 +166,52 @@ const ConfigureSourceForm: FC = ({ source }) => {
return (
<>
- {
- if (!canEdit) {
- setCanEdit(true);
- return;
- }
- }}
+
- {() => {
- return (
-
-
-
-
-
-
setDeleteSourceModalIsOpen(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"}
-
-
-
-
-
-
- );
- }}
-
+
+
+
+
+ setDeleteSourceModalIsOpen(true)}
+ position="mr-auto my-auto"
+ type="button"
+ color="danger"
+ >
+ Delete
+
+
+ {canEdit ? "Done" : "Edit"}
+
+
+
+
+
+
;
+};
+
+export type CreateSourceFormErrors = {
+ definition: Nullable;
+};
+
+export type CreateSourceFormProps = {
+ marginBottom: Nullable;
+};
+
+const CreateSourceForm = ({ marginBottom }: CreateSourceFormProps) => {
+ const router = useRouter();
+ const { amplitudeIsInit } = useAmplitudeCtx();
+
+ // ###################################################################
+ // # 1 - Initialize the source definition #
+ // ###################################################################
+ //
+ // A user can only have a http source and a grpc source
+
+ const [sourceDefinitionOptions, setSourceDefinitionOptions] = useState<
+ SingleSelectOption[]
+ >([]);
+ const [selectedSourceDefinitionOption, setSelectedSourceDefinitionOption] =
+ useState>(null);
+
+ const sources = useSources();
+ const createSource = useCreateSource();
+
+ useEffect(() => {
+ if (!sources.isSuccess) return;
+
+ setSourceDefinitionOptions([
+ {
+ label: "gRPC",
+ value: "source-grpc",
+ startIcon: (
+
+ ),
+ },
+ {
+ label: "HTTP",
+ value: "source-http",
+ startIcon: (
+
+ ),
+ },
+ ]);
+ }, [sources.isSuccess]);
+
+ // ###################################################################
+ // # 2 - handle create source #
+ // ###################################################################
+
+ const [fieldValues, setFieldValues] = useState({
+ definition: null,
+ });
+
+ const [fieldErrors, setFieldErrors] = useState({
+ definition: null,
+ });
+
+ const handleDefinitionChange = useCallback(
+ (option: Nullable) => {
+ setFieldErrors({
+ definition: null,
+ });
+ setSelectedSourceDefinitionOption(option);
+ setFieldValues({
+ definition: (option?.value as string) || null,
+ });
+ },
+ []
+ );
+
+ const [messageBoxState, setMessageBoxState] =
+ useState({
+ activate: false,
+ message: null,
+ description: null,
+ status: null,
+ });
+
+ const handleSubmit = useCallback(() => {
+ if (!fieldValues.definition) return;
+
+ if (!fieldValues.definition) {
+ setFieldErrors({
+ definition: "Required",
+ });
+ return;
+ }
+
+ if (sources.data?.find((e) => e.id === fieldValues.definition)) {
+ setFieldErrors({
+ definition:
+ "You could only create one http and one grpc source. Check the setup guide for more information.",
+ });
+ return;
+ }
+
+ const payload: CreateSourcePayload = {
+ id: fieldValues.definition,
+ source_connector_definition: `source-connector-definitions/${fieldValues.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, fieldValues, sources.data]);
+
+ return (
+
+ Setup Guide`}
+ />
+
+
+
+ Set up
+
+
+
+ );
+};
+
+export default CreateSourceForm;
diff --git a/src/components/forms/connector/source/index.ts b/src/components/source/index.ts
similarity index 100%
rename from src/components/forms/connector/source/index.ts
rename to src/components/source/index.ts
diff --git a/src/components/ui/Buttons/ButtonBase/ButtonBase.tsx b/src/components/ui/Buttons/ButtonBase/ButtonBase.tsx
deleted file mode 100644
index 7ebb23586d..0000000000
--- a/src/components/ui/Buttons/ButtonBase/ButtonBase.tsx
+++ /dev/null
@@ -1,74 +0,0 @@
-import { FC } from "react";
-import cn from "clsx";
-import { Nullable } from "@/types/general";
-
-export type ButtonBaseProps = {
- type: "button" | "submit" | "reset";
- disabled: boolean;
- onClickHandler: Nullable<(values?: any) => any>;
- bgColor: Nullable;
- hoveredBgColor: Nullable;
- disabledBgColor: Nullable;
- textColor: Nullable;
- hoveredTextColor: Nullable;
- disabledTextColor: Nullable;
- borderSize: Nullable;
- borderColor: Nullable;
- hoveredBorderColor: Nullable;
- disabledBorderColor: Nullable;
- position: Nullable;
- dataFlag?: Nullable;
- padding: Nullable;
- width: Nullable;
-};
-
-const ButtonBase: FC = ({
- bgColor,
- hoveredBgColor,
- disabled,
- disabledBgColor,
- disabledTextColor,
- textColor,
- hoveredTextColor,
- onClickHandler,
- position,
- type,
- dataFlag,
- children,
- padding,
- width,
- borderSize,
- borderColor,
- hoveredBorderColor,
- disabledBorderColor,
-}) => {
- return (
-
- {children}
-
- );
-};
-
-export default ButtonBase;
diff --git a/src/components/ui/Buttons/ButtonBase/index.ts b/src/components/ui/Buttons/ButtonBase/index.ts
deleted file mode 100644
index 36cd93b744..0000000000
--- a/src/components/ui/Buttons/ButtonBase/index.ts
+++ /dev/null
@@ -1,2 +0,0 @@
-export { default } from "./ButtonBase";
-export * from "./ButtonBase";
diff --git a/src/components/ui/Buttons/CollapseSidebarButton/CollapseSidebarButton.tsx b/src/components/ui/Buttons/CollapseSidebarButton/CollapseSidebarButton.tsx
deleted file mode 100644
index 14a4fd3569..0000000000
--- a/src/components/ui/Buttons/CollapseSidebarButton/CollapseSidebarButton.tsx
+++ /dev/null
@@ -1,63 +0,0 @@
-import { FC } from "react";
-import ButtonBase, { ButtonBaseProps } from "../ButtonBase";
-import { CollapseRightIcon, CollapseLeftIcon } from "@instill-ai/design-system";
-
-export type CollapseSidebarButtonProps = Omit<
- ButtonBaseProps,
- | "bgColor"
- | "textColor"
- | "disabledBgColor"
- | "disabledTextColor"
- | "padding"
- | "width"
- | "borderSize"
- | "borderColor"
- | "disabledBorderColor"
- | "hoveredBgColor"
- | "hoveredTextColor"
- | "hoveredBorderColor"
-> & {
- isCollapse: boolean;
-};
-
-const CollapseSidebarButton: FC = (props) => {
- return (
-
- {props.isCollapse ? (
-
- ) : (
-
- )}
-
- );
-};
-
-export default CollapseSidebarButton;
diff --git a/src/components/ui/Buttons/CollapseSidebarButton/index.ts b/src/components/ui/Buttons/CollapseSidebarButton/index.ts
deleted file mode 100644
index ab5749936f..0000000000
--- a/src/components/ui/Buttons/CollapseSidebarButton/index.ts
+++ /dev/null
@@ -1,2 +0,0 @@
-export { default } from "./CollapseSidebarButton";
-export * from "./CollapseSidebarButton";
diff --git a/src/components/ui/Buttons/OutlineButton/OutlineButton.tsx b/src/components/ui/Buttons/OutlineButton/OutlineButton.tsx
deleted file mode 100644
index 6d4e97033b..0000000000
--- a/src/components/ui/Buttons/OutlineButton/OutlineButton.tsx
+++ /dev/null
@@ -1,32 +0,0 @@
-import { FC } from "react";
-import ButtonBase, { ButtonBaseProps } from "../ButtonBase";
-
-export type OutlineButtonProps = Omit;
-
-const OutlineButton: FC = (props) => {
- return (
-
- {props.children}
-
- );
-};
-
-export default OutlineButton;
diff --git a/src/components/ui/Buttons/OutlineButton/index.ts b/src/components/ui/Buttons/OutlineButton/index.ts
deleted file mode 100644
index 9dc5553942..0000000000
--- a/src/components/ui/Buttons/OutlineButton/index.ts
+++ /dev/null
@@ -1,2 +0,0 @@
-export { default } from "./OutlineButton";
-export * from "./OutlineButton";
diff --git a/src/components/ui/Buttons/PrimaryButton/PrimaryButton.tsx b/src/components/ui/Buttons/PrimaryButton/PrimaryButton.tsx
deleted file mode 100644
index d4b80a70d8..0000000000
--- a/src/components/ui/Buttons/PrimaryButton/PrimaryButton.tsx
+++ /dev/null
@@ -1,46 +0,0 @@
-import { FC } from "react";
-import ButtonBase, { ButtonBaseProps } from "../ButtonBase";
-
-export type PrimaryButtonProps = Omit<
- ButtonBaseProps,
- | "bgColor"
- | "textColor"
- | "disabledBgColor"
- | "disabledTextColor"
- | "padding"
- | "width"
- | "borderSize"
- | "borderColor"
- | "disabledBorderColor"
- | "hoveredBgColor"
- | "hoveredTextColor"
- | "hoveredBorderColor"
->;
-
-const PrimaryButton: FC = (props) => {
- return (
-
- {props.children}
-
- );
-};
-
-export default PrimaryButton;
diff --git a/src/components/ui/Buttons/PrimaryButton/index.ts b/src/components/ui/Buttons/PrimaryButton/index.ts
deleted file mode 100644
index 3c26184787..0000000000
--- a/src/components/ui/Buttons/PrimaryButton/index.ts
+++ /dev/null
@@ -1,2 +0,0 @@
-export { default } from "./PrimaryButton";
-export * from "./PrimaryButton";
diff --git a/src/components/ui/Buttons/index.ts b/src/components/ui/Buttons/index.ts
deleted file mode 100644
index 9a3309b11b..0000000000
--- a/src/components/ui/Buttons/index.ts
+++ /dev/null
@@ -1,8 +0,0 @@
-import type { PrimaryButtonProps } from "./PrimaryButton";
-import PrimaryButton from "./PrimaryButton";
-import type { CollapseSidebarButtonProps } from "./CollapseSidebarButton";
-import CollapseSidebarButton from "./CollapseSidebarButton";
-
-export type { PrimaryButtonProps, CollapseSidebarButtonProps };
-
-export { PrimaryButton, CollapseSidebarButton };
diff --git a/src/components/forms/commons/FormBase/FormBase.tsx b/src/components/ui/FormBase.tsx
similarity index 100%
rename from src/components/forms/commons/FormBase/FormBase.tsx
rename to src/components/ui/FormBase.tsx
diff --git a/src/components/modals/DeleteResourceModal/DeleteResourceModal.tsx b/src/components/ui/Modals/DeleteResourceModal/DeleteResourceModal.tsx
similarity index 77%
rename from src/components/modals/DeleteResourceModal/DeleteResourceModal.tsx
rename to src/components/ui/Modals/DeleteResourceModal/DeleteResourceModal.tsx
index e3a5a7ef99..447ba001fd 100644
--- a/src/components/modals/DeleteResourceModal/DeleteResourceModal.tsx
+++ b/src/components/ui/Modals/DeleteResourceModal/DeleteResourceModal.tsx
@@ -1,8 +1,7 @@
-import { BasicTextField } from "@instill-ai/design-system";
+import { BasicTextField, OutlineButton } from "@instill-ai/design-system";
import {
ChangeEvent,
Dispatch,
- FC,
memo,
SetStateAction,
useCallback,
@@ -19,7 +18,6 @@ import {
} from "@/lib/instill";
import ModalBase from "../ModalBase";
import { Nullable } from "@/types/general";
-import OutlineButton from "@/components/ui/Buttons/OutlineButton";
export type DeleteResourceModalProps = {
resource: Nullable<
@@ -34,12 +32,12 @@ export type DeleteResourceModalProps = {
handleDeleteResource: () => void;
};
-const DeleteResourceModal: FC = ({
+const DeleteResourceModal = ({
modalIsOpen,
setModalIsOpen,
resource,
handleDeleteResource,
-}) => {
+}: DeleteResourceModalProps) => {
// ###################################################################
// # #
// # Initialize the function of modal #
@@ -151,42 +149,18 @@ const DeleteResourceModal: FC = ({
setModalIsOpen(false)}
- position={null}
+ color="secondary"
>
- Cancel
+ Cancel
- Delete
+ Delete
diff --git a/src/components/modals/DeleteResourceModal/index.ts b/src/components/ui/Modals/DeleteResourceModal/index.ts
similarity index 100%
rename from src/components/modals/DeleteResourceModal/index.ts
rename to src/components/ui/Modals/DeleteResourceModal/index.ts
diff --git a/src/components/modals/ModalBase/ModalBase.tsx b/src/components/ui/Modals/ModalBase/ModalBase.tsx
similarity index 93%
rename from src/components/modals/ModalBase/ModalBase.tsx
rename to src/components/ui/Modals/ModalBase/ModalBase.tsx
index d2e173234d..8804a73879 100644
--- a/src/components/modals/ModalBase/ModalBase.tsx
+++ b/src/components/ui/Modals/ModalBase/ModalBase.tsx
@@ -1,5 +1,5 @@
import { Nullable } from "@/types/general";
-import { FC, useRef, useEffect } from "react";
+import { useRef, useEffect, ReactNode } from "react";
import { createPortal } from "react-dom";
import cn from "clsx";
@@ -7,14 +7,15 @@ export type ModalBaseProps = {
modalIsOpen: boolean;
modalBgColor: Nullable;
modalPadding: Nullable;
+ children?: ReactNode;
};
-const ModalBase: FC = ({
+const ModalBase = ({
children,
modalIsOpen,
modalBgColor,
modalPadding,
-}) => {
+}: ModalBaseProps) => {
const el = useRef();
useEffect(() => {
diff --git a/src/components/modals/ModalBase/index.ts b/src/components/ui/Modals/ModalBase/index.ts
similarity index 100%
rename from src/components/modals/ModalBase/index.ts
rename to src/components/ui/Modals/ModalBase/index.ts
diff --git a/src/components/modals/README.md b/src/components/ui/Modals/README.md
similarity index 100%
rename from src/components/modals/README.md
rename to src/components/ui/Modals/README.md
diff --git a/src/components/modals/index.ts b/src/components/ui/Modals/index.ts
similarity index 100%
rename from src/components/modals/index.ts
rename to src/components/ui/Modals/index.ts
diff --git a/src/components/layouts/PageBase/PageBase.tsx b/src/components/ui/PageBase.tsx
similarity index 66%
rename from src/components/layouts/PageBase/PageBase.tsx
rename to src/components/ui/PageBase.tsx
index 91f848bfc7..7e8a5cd0d0 100644
--- a/src/components/layouts/PageBase/PageBase.tsx
+++ b/src/components/ui/PageBase.tsx
@@ -1,11 +1,11 @@
-import { Sidebar } from "components/ui";
-import { FC, ReactNode } from "react";
+import { Sidebar } from "@/components/ui";
+import { ReactNode } from "react";
-export interface PageBaseProps {
+export type PageBaseProps = {
children: ReactNode;
-}
+};
-const PageBase: FC = ({ children }) => {
+const PageBase = ({ children }: PageBaseProps) => {
return (
diff --git a/src/components/ui/PageContentContainer.tsx b/src/components/ui/PageContentContainer.tsx
new file mode 100644
index 0000000000..b3ecdf2c4c
--- /dev/null
+++ b/src/components/ui/PageContentContainer.tsx
@@ -0,0 +1,15 @@
+import { ReactNode } from "react";
+
+export type PageContentContainerProps = {
+ children?: ReactNode;
+};
+
+const PageContentContainer = ({ children }: PageContentContainerProps) => {
+ return (
+
+ {children}
+
+ );
+};
+
+export default PageContentContainer;
diff --git a/src/components/layouts/PageHead/PageHead.tsx b/src/components/ui/PageHead.tsx
similarity index 95%
rename from src/components/layouts/PageHead/PageHead.tsx
rename to src/components/ui/PageHead.tsx
index ebc3789bfb..211e5e8857 100644
--- a/src/components/layouts/PageHead/PageHead.tsx
+++ b/src/components/ui/PageHead.tsx
@@ -1,13 +1,13 @@
-import { FC, useEffect, useState } from "react";
+import { useEffect, useState } from "react";
import { useRouter } from "next/router";
import Head from "next/head";
import { Nullable } from "@/types/general";
-export interface PageHeadProps {
+export type PageHeadProps = {
title: Nullable
;
-}
+};
-const PageHead: FC = ({ title }) => {
+const PageHead = ({ title }: PageHeadProps) => {
const router = useRouter();
const meta = {
type: "website",
diff --git a/src/components/ui/PageTitle.tsx b/src/components/ui/PageTitle.tsx
index 4a10967ed9..68bce95407 100644
--- a/src/components/ui/PageTitle.tsx
+++ b/src/components/ui/PageTitle.tsx
@@ -3,7 +3,7 @@ import { useRouter } from "next/router";
import cn from "clsx";
import Breadcrumb, { BreadcrumbProps } from "./Breadcrumb";
-import { PrimaryButton } from "./Buttons";
+import { SolidButton } from "@instill-ai/design-system";
export type PageTitleProps = {
title: string;
@@ -34,14 +34,15 @@ const PageTitle = ({
{title}
{enableButton ? (
-
{buttonName}
-
+
) : null}
diff --git a/src/components/ui/Sidebar/Sidebar.tsx b/src/components/ui/Sidebar/Sidebar.tsx
index 8b2be4a512..2a12bc0c6b 100644
--- a/src/components/ui/Sidebar/Sidebar.tsx
+++ b/src/components/ui/Sidebar/Sidebar.tsx
@@ -9,11 +9,11 @@ import {
ResourceIcon,
DiscordIcon,
GitHubIcon,
+ CollapseSidebarButton,
} from "@instill-ai/design-system";
import { useRouter } from "next/router";
import LinkTab from "./LinkTab";
-import { CollapseSidebarButton } from "../Buttons";
import ButtonTab from "./ButtonTab";
type Tab =
@@ -255,7 +255,6 @@ const Sidebar: FC = () => {
= ({
+const TablePlaceholderBase = ({
placeholderItems,
placeholderTitle,
createButtonTitle,
createButtonLink,
marginBottom,
enableCreateButton,
-}) => {
+}: TablePlaceholderBaseProps) => {
const router = useRouter();
const handleOnClick = () => {
router.push(createButtonLink);
@@ -46,14 +46,15 @@ const TablePlaceholderBase: FC = ({
{placeholderTitle}
{enableCreateButton ? (
-
{createButtonTitle}
-
+
) : null}
diff --git a/src/components/ui/index.ts b/src/components/ui/index.ts
index a221378ab3..bad2b725da 100644
--- a/src/components/ui/index.ts
+++ b/src/components/ui/index.ts
@@ -4,6 +4,8 @@ import type { BreadcrumbProps } from "./Breadcrumb";
import Sidebar from "./Sidebar";
import StateIcon from "./StateIcon";
import type { StateIconProps } from "./StateIcon";
+import FormBase from "./FormBase";
+import type { FormBaseProps } from "./FormBase";
import FormVerticalDivider from "./FormVerticalDivider";
import TableContainer from "./TableContainer";
import TableRow from "./TableRow";
@@ -27,6 +29,12 @@ import TableLoadingProgress from "./TableLoadingProgress";
import type { TableLoadingProgressProps } from "./TableLoadingProgress";
import ModelInstanceTaskLabel from "./ModelInstanceTaskLabel";
import type { ModelInstanceTaskLabelProps } from "./ModelInstanceTaskLabel";
+import PageBase from "./PageBase";
+import type { PageBaseProps } from "./PageBase";
+import PageContentContainer from "./PageContentContainer";
+import type { PageContentContainerProps } from "./PageContentContainer";
+import PageHead from "./PageHead";
+import type { PageHeadProps } from "./PageHead";
import PageTitle from "./PageTitle";
import type { PageTitleProps } from "./PageTitle";
import ModelInstanceReadmeCard from "./ModelInstanceReadmeCard";
@@ -38,6 +46,7 @@ import ErrorBoundary from "./ErrorBoundary";
export {
Sidebar,
StateIcon,
+ FormBase,
FormVerticalDivider,
TableContainer,
TableRow,
@@ -50,6 +59,9 @@ export {
HorizontalDivider,
TableLoadingProgress,
ModelInstanceTaskLabel,
+ PageBase,
+ PageContentContainer,
+ PageHead,
PageTitle,
ModelInstanceReadmeCard,
ChangeResourceStateButton,
@@ -61,12 +73,16 @@ export {
export type {
StateIconProps,
StateLabelProps,
+ FormBaseProps,
PipelineModeLabelProps,
ConnectorIconProps,
ModelDefinitionIconProps,
HorizontalDividerProps,
TableLoadingProgressProps,
ModelInstanceTaskLabelProps,
+ PageBaseProps,
+ PageContentContainerProps,
+ PageHeadProps,
PageTitleProps,
ModelInstanceReadmeCardProps,
ChangeResourceStateButtonProps,
@@ -76,11 +92,7 @@ export type {
};
export * from "./TableHeads";
-
export * from "./TableCells";
-
export * from "./TablePlaceholders";
-
export * from "./Tables";
-
-export * from "./Buttons";
+export * from "./Modals";
diff --git a/src/hooks/useMultiStageQueryLoadingState.tsx b/src/hooks/useMultiStageQueryLoadingState.tsx
index faa24a7b6d..6b4a716295 100644
--- a/src/hooks/useMultiStageQueryLoadingState.tsx
+++ b/src/hooks/useMultiStageQueryLoadingState.tsx
@@ -1,5 +1,6 @@
import { useEffect, useState } from "react";
+/* eslint-disable @typescript-eslint/no-explicit-any */
export type UseMultiStageQueryLoadingStatePayload = {
data: any;
isLoading: boolean;
diff --git a/src/hooks/useRefSize.ts b/src/hooks/useRefSize.ts
index fa8a111f24..e29cdab04b 100644
--- a/src/hooks/useRefSize.ts
+++ b/src/hooks/useRefSize.ts
@@ -27,7 +27,7 @@ const useRefSize = (ref: RefObject): Nullable => {
observerRef.current.disconnect();
}
};
- }, []);
+ }, [ref]);
return refSize;
};
diff --git a/src/hooks/useSendAmplitudeData.ts b/src/hooks/useSendAmplitudeData.ts
index 73cfeed3cd..d941d31e07 100644
--- a/src/hooks/useSendAmplitudeData.ts
+++ b/src/hooks/useSendAmplitudeData.ts
@@ -15,5 +15,5 @@ export const useSendAmplitudeData = (
if (!amplitudeIsReady || !routerIsReady) return;
sendAmplitudeData(event, properties);
- }, [routerIsReady, amplitudeIsReady]);
+ }, [routerIsReady, amplitudeIsReady, event, properties]);
};
diff --git a/src/hooks/useStateOverviewCounts.ts b/src/hooks/useStateOverviewCounts.ts
index 34ac03c1e8..86ebfd6861 100644
--- a/src/hooks/useStateOverviewCounts.ts
+++ b/src/hooks/useStateOverviewCounts.ts
@@ -124,53 +124,3 @@ export const useStateOverviewCounts = (items: Item[] | null) => {
return stateOverviewCount;
};
-
-const isPipeline = (item: Item): item is Pipeline => {
- const itemNameList = item.name.split("/");
-
- if (itemNameList[0] === "pipelines") {
- return true;
- } else {
- return false;
- }
-};
-
-const isSource = (item: Item): item is Source => {
- const itemNameList = item.name.split("/");
-
- if (itemNameList[0] === "source-connectors") {
- return true;
- } else {
- return false;
- }
-};
-
-const isDestination = (item: Item): item is Destination => {
- const itemNameList = item.name.split("/");
-
- if (itemNameList[0] === "destination-connectors") {
- return true;
- } else {
- return false;
- }
-};
-
-const isModelInstance = (item: Item): item is ModelInstance => {
- const itemNameList = item.name.split("/");
-
- if (itemNameList[2] === "instances") {
- return true;
- } else {
- return false;
- }
-};
-
-const isModel = (item: Item): item is ModelWithInstance => {
- const itemNameList = item.name.split("/");
-
- if (itemNameList[0] === "model" && itemNameList[2] !== "instances") {
- return true;
- } else {
- return false;
- }
-};
diff --git a/src/hooks/useWindowSize.ts b/src/hooks/useWindowSize.ts
index 71445cfe41..5eb781c9eb 100644
--- a/src/hooks/useWindowSize.ts
+++ b/src/hooks/useWindowSize.ts
@@ -6,26 +6,26 @@ export type WindowSize = {
height: number;
};
-const getWindowSize = (): { width: number; height: number } => {
- const width =
- window.innerWidth ||
- document.documentElement.clientWidth ||
- document.body.clientWidth;
-
- const height =
- window.innerHeight ||
- document.documentElement.clientHeight ||
- document.body.clientHeight;
-
- return {
- width,
- height,
- };
-};
-
const useWindowSize = (): Nullable => {
const [windowSize, setWindowSize] = useState>(null);
+ const getWindowSize = (): { width: number; height: number } => {
+ const width =
+ window.innerWidth ||
+ document.documentElement.clientWidth ||
+ document.body.clientWidth;
+
+ const height =
+ window.innerHeight ||
+ document.documentElement.clientHeight ||
+ document.body.clientHeight;
+
+ return {
+ width,
+ height,
+ };
+ };
+
useEffect(() => {
if (typeof window === "undefined") {
return;
@@ -38,7 +38,7 @@ const useWindowSize = (): Nullable => {
window.addEventListener("resize", handleResize);
return () => window.removeEventListener("resize", handleResize);
- }, [getWindowSize]);
+ }, []);
return windowSize;
};
diff --git a/src/lib/airbytes/README.md b/src/lib/airbytes/README.md
index 6c71172fa0..5ec92496c2 100644
--- a/src/lib/airbytes/README.md
+++ b/src/lib/airbytes/README.md
@@ -12,48 +12,189 @@ We are using Airbyte protocol for generating, maintain, create our connectors, f
## Implementation details
-### Store in the `value.configuration` and the value itself
+### About selectedConditionMap
-In our backend we have to input flatten data
+- Internally we all use condition.title as selectedItem value. not the const field's value
+
+### How to get the condition's path
+
+We rely on the field inside of condition with const key and use its path to set correct value. e.g. Snowflake's loading method
+
+```js
+{
+ title: "Data Staging Method",
+ path: "loading_method",
+ "conditions": [
+ {
+ "[Recommended] Internal Staging": {
+ fieldKey: "loading_method"
+ properties: [
+ {
+ "default": "Internal Staging",
+ "description": "",
+ "title": "",
+ "const": "Internal Staging",
+ "_type": "formItem",
+ "path": "loading_method.method", // <-- This is the correct path
+ "fieldKey": "method",
+ "isRequired": true,
+ "isSecret": false,
+ "multiline": false,
+ "type": "string"
+ }
+ ...
+ ]
+ ...
+ }
+ }
+ ]
+}
+}
+```
+
+But sometimes the const field is not there (due to some human error of Airbyte side), we
+need to find the workaround. Normally there are multiple conditions under the conditionForm, we could loop though all the field to find the right one.
+
+### How to remove old condition configuration when user select new one?
+
+Take this destination for example, what if user select another tunnel_method? How do we update the tunnel_method as whole.
```js
+{
+ id: "sample",
+ name: "destination-connectors/sample,
+ uid: "badd8615-d68e-4fc1-8bc1-817766e285ec",
{
- tunnel_method: "SSH",
- tunnel_key: "key"
+ "description": "",
+ "configuration": {
+ "database": "foo-1",
+ "host": "fooHost",
+ "password": "admin",
+ "port": 3306,
+ "tunnel_method": {
+ "ssh_key": "barKey",
+ "tunnel_host": "barHost",
+ "tunnel_method": "NO_TUNNEL",
+ "tunnel_port": 22,
+ "tunnel_user": "barUser"
+ },
+ "username": "yoyoyman"
+ },
+ "state": "STATE_UNSPECIFIED",
+ "tombstone": false,
+ "user": "users/local-user",
+ "create_time": "2022-09-01T05:55:44.498367Z",
+ "update_time": "2022-09-01T05:55:44.498367Z"
}
+}
```
-But the YUP we build validate the configuration in a object
+The short answer is we don't bother control the field values at the first place. Because we had built up Yup according to the user selected condition. We can use yup to help us strip un-used/not-wanted/old condition data.
```js
+// This is the original data
{
- tunnel_method: {
- tunnel_key: "key";
+ "configuration": {
+ "database": "we3",
+ "host": "yojhojo",
+ "password": "ewr",
+ "port": 3306,
+ "tunnel_method": {
+ "ssh_key": "ewr",
+ "tunnel_host": "wer",
+ "tunnel_method": "SSH_KEY_AUTH",
+ "tunnel_port": 22,
+ "tunnel_user": "wer",
+ "tunnel_user_password": "ewrewr343434"
+ },
+ "username": "234"
+ },
+ "database": "we3",
+ "host": "yojhojo",
+ "password": "ewr",
+ "port": 3306,
+ "tunnel_method.ssh_key": "ewr",
+ "tunnel_method.tunnel_host": "wer",
+ "tunnel_method.tunnel_method": "SSH_KEY_AUTH",
+ "tunnel_method.tunnel_port": 22,
+ "tunnel_method.tunnel_user": "wer",
+ "username": "234",
+ "tunnel_method": "SSH_KEY_AUTH",
+ "tunnel_method.tunnel_user_password": "ewrewr343434"
+}
+
+// This is the striped data, we stripe tunnel_method.tunnel_user_password and others not used Ui
+// data (data that is not in configuration)
+{
+ "configuration": {
+ "username": "234",
+ "tunnel_method": {
+ "tunnel_user": "wer",
+ "tunnel_port": 22,
+ "tunnel_method": "SSH_KEY_AUTH",
+ "tunnel_host": "wer",
+ "ssh_key": "ewr"
+ },
+ "port": 3306,
+ "password": "ewr",
+ "host": "yojhojo",
+ "database": "we3"
}
}
```
-So right now we actually have something like this
+### How to edit OneOfCondition section when we have initial values?
+
+When user created a new destination and they want to edit it at ConfigurationDestinationForm, how to let them edit OneOfCondition section?
+
+Here is the sample destination response.
```js
{
- tunnel_method.tunnel_key: "key"
- configuration: {
- tunnel_method: {
- tunnel_key: "key";
- }
+ id: "sample",
+ name: "destination-connectors/sample,
+ uid: "badd8615-d68e-4fc1-8bc1-817766e285ec",
+ {
+ "description": "",
+ "configuration": {
+ "database": "foo-1",
+ "host": "fooHost",
+ "password": "admin",
+ "port": 3306,
+ "tunnel_method": {
+ "ssh_key": "barKey",
+ "tunnel_host": "barHost",
+ "tunnel_method": "NO_TUNNEL",
+ "tunnel_port": 22,
+ "tunnel_user": "barUser"
+ },
+ "username": "yoyoyman"
+ },
+ "state": "STATE_UNSPECIFIED",
+ "tombstone": false,
+ "user": "users/local-user",
+ "create_time": "2022-09-01T05:55:44.498367Z",
+ "update_time": "2022-09-01T05:55:44.498367Z"
}
}
```
-Due to the time constraint, we haven't figured out how to properly combine this two situation, here are possible direction
+Now we have a OneOfCondition field tunnel_method, how can we let the initial form construction knows we should display tunnel_method with "NO_TUNNEL" condition?
+
+- We use selectedConditionMap to store the current selection of condition. When the selectedConditionMap is empty (initial state), we choose the first condition as default.
+- When deal with configuration, once we get the initial value we have to map AirbyteFormTree and the configuration values to form the proper initial selectedConditionMap.
+- At OneOfConditionSection, when form is not dirty we will initialize correct selected condition.
-1. Rebuild the airbyteSchemaToYup, make result yup flatten
-2. Flatten the data at the end, when we need to post the payload
## Issues
- How to validate all the form, including oneOf condition and the nested oneOf
+- SnowFlakes auth_type doesn't have const field
+
+## Caveats
+
+- Be careful of selectedConditionMap, it will affect the yup which will affect the strip value that will be sent as payload too.
+- When build the yup, airbyte use condition.title to find the target condition. Take `tunnel_method` for example, it will use tunnel_method's title like `[Recommended] Internal Staging` to find the target value. But we actually send it's value `Internal Staging` to the backend. So next time when we fetch the backend, the data will store `Internal Staging` not the `[Recommended] Internal Staging`
## Useful refernece
@@ -66,3 +207,4 @@ Due to the time constraint, we haven't figured out how to properly combine this
- [Airbyte - Control](https://github.com/airbytehq/airbyte/blob/master/airbyte-webapp/src/views/Connector/ServiceForm/components/Property/Control.tsx)
- How they generate field based on their type, array, boolean, string, integer, array
- [Airbyte - schemaToYup](https://github.com/airbytehq/airbyte/blob/59e20f20de73ced59ae2c782612fa7554fc1fced/airbyte-webapp/src/core/jsonSchema/schemaToYup.ts)
+
diff --git a/src/lib/airbytes/components/AirbyteDestinationFields/AirbyteDestinationFields.tsx b/src/lib/airbytes/components/AirbyteDestinationFields/AirbyteDestinationFields.tsx
index 02bf6445d4..95c773583d 100644
--- a/src/lib/airbytes/components/AirbyteDestinationFields/AirbyteDestinationFields.tsx
+++ b/src/lib/airbytes/components/AirbyteDestinationFields/AirbyteDestinationFields.tsx
@@ -1,6 +1,4 @@
-import { ConnectorDefinition } from "@/lib/instill";
-import { Dispatch, FC, Fragment, SetStateAction, useMemo } from "react";
-import { airbyteSchemaToAirbyteFormTree } from "../../airbyteSchemaToAirbyteFormTree";
+import { Dispatch, FC, Fragment, SetStateAction } from "react";
import {
AirbyteFieldErrors,
AirbyteFieldValues,
@@ -17,6 +15,9 @@ export type AirbyteDestinationFieldsProps = {
destinationFormTree: Nullable;
selectedConditionMap: Nullable;
setSelectedConditionMap: Dispatch>>;
+ disableAll: boolean;
+ formIsDirty: boolean;
+ setFormIsDirty: Dispatch>;
};
const AirbyteDestinationFields: FC = ({
@@ -26,15 +27,20 @@ const AirbyteDestinationFields: FC = ({
destinationFormTree,
selectedConditionMap,
setSelectedConditionMap,
+ disableAll,
+ formIsDirty,
+ setFormIsDirty,
}) => {
const fields = useBuildAirbyteFields(
destinationFormTree,
- false,
+ disableAll,
fieldValues,
setFieldValues,
fieldErrors,
selectedConditionMap,
- setSelectedConditionMap
+ setSelectedConditionMap,
+ formIsDirty,
+ setFormIsDirty
);
return {fields} ;
diff --git a/src/lib/airbytes/components/OneOfConditionSection/OneOfConditionSection.tsx b/src/lib/airbytes/components/OneOfConditionSection/OneOfConditionSection.tsx
index 3128bd3ded..ef48e9f57c 100644
--- a/src/lib/airbytes/components/OneOfConditionSection/OneOfConditionSection.tsx
+++ b/src/lib/airbytes/components/OneOfConditionSection/OneOfConditionSection.tsx
@@ -13,6 +13,7 @@ import {
useState,
useEffect,
} from "react";
+import getConditionFormPath from "../../getConditionFormPath";
import {
AirbyteFormConditionItemWithUiFields,
AirbyteFormItem,
@@ -27,6 +28,9 @@ export type OneOfConditionSectionProps = {
setValues: Dispatch>>;
selectedConditionMap: Nullable;
setSelectedConditionMap: Dispatch>>;
+ disableAll: boolean;
+ formIsDirty: boolean;
+ setFormIsDirty: Dispatch>;
};
const OneOfConditionSection: FC = ({
@@ -35,92 +39,114 @@ const OneOfConditionSection: FC = ({
setValues,
selectedConditionMap,
setSelectedConditionMap,
+ disableAll,
+ formIsDirty,
+ setFormIsDirty,
}) => {
- // Caveat:
- // It's tempting to use selectedCondition as state and use it to get uiField here
- //
- // const [selectedCondition, setSelectedCondition] =
- // useState>(null);
- //
- // return (<>{selectedCondition ? selectedCondition.uiFields : null}>)
- //
- // Beaware that the selectedCondition value will get wiped out everytime the component re-render, so the field's
- // value will disappear. We should use the parent's component's props to store this kind of data.
+ // ##########################################################################
+ // # 1 - Initialize state #
+ // ##########################################################################
+
+ // We store the uiFields in formTree and pass it to this component to preserve
+ // the state of UI fields
const [selectedConditionOption, setSelectedConditionOption] =
useState>(null);
- const [conditionPath, setConditionPath] = useState>(null);
const conditionOptions: SingleSelectOption[] = useMemo(() => {
+ // Sometimes the const field is missing, we need to find a workaround
+ // TODO: Known issue: snowflake
+
return Object.entries(formTree.conditions).map(([k, v]) => {
return {
label: k.toString(),
- value: (v.properties.find((e) => "const" in e) as AirbyteFormItem)
- ?.const as string,
+ value:
+ ((v.properties.find((e) => "const" in e) as AirbyteFormItem)
+ ?.const as string | undefined) ?? k.toString(),
};
});
}, [formTree.conditions]);
- // Upon the initialize of the form, user haven't chosen any condition, we have to choose default one
- // Be careful of the update, make sure it won't cause infinite loop.
+ // We rely on the field inside of condition with const key and use its path
+ // to set correct value. But sometimes Airbyte's conditionForm's condition
+ // don't have proper const field We need to find the workaround. You can find
+ // more information in the README.
+
+ const [conditionPath, setConditionPath] = useState>(null);
useEffect(() => {
- if (!selectedConditionMap || !selectedConditionMap[formTree.path]) {
- setSelectedConditionOption(conditionOptions[0]);
+ const conditionFormPath = getConditionFormPath(formTree);
+ setConditionPath(conditionFormPath);
+ }, [formTree]);
+ useEffect(() => {
+ // When create new destination, upon the initialize of the form, user haven't
+ // chosen any condition, we have to choose default one. Be careful of the update,
+ // make sure it won't cause infinite loop.
+ // if (!selectedConditionMap || !selectedConditionMap[formTree.path]) {
+ // setSelectedConditionOption(conditionOptions[0]);
+
+ // const selectedCondition =
+ // formTree.conditions[conditionOptions[0].label] ?? null;
+
+ // const targetConstField = selectedCondition.properties.find(
+ // (e) => "const" in e
+ // ) as AirbyteFormItem;
+
+ // setConditionPath(targetConstField?.path ?? null);
+
+ // setSelectedConditionMap((prev) => ({
+ // ...(prev || {}),
+ // [formTree.path]: {
+ // selectedItem: selectedCondition
+ // ? selectedCondition.title ?? null
+ // : null,
+ // },
+ // }));
+
+ // if (targetConstField) {
+ // setValues((prev) => {
+ // const configuration = prev?.configuration ?? {};
+ // dot.setter(
+ // configuration,
+ // targetConstField.path,
+ // targetConstField.const
+ // );
+ // return {
+ // ...prev,
+ // configuration,
+ // };
+ // });
+ // } else {
+ // // Sometimes Airbyte doesn't have proper const field. We need to find a work around
+ // if (conditionPath) {
+ // setValues((prev) => {
+ // const configuration = prev?.configuration ?? {};
+ // dot.setter(configuration, conditionPath, selectedCondition.title);
+ // return {
+ // ...prev,
+ // configuration,
+ // };
+ // });
+ // }
+ // }
+ // return;
+ // }
+
+ // When user want to configure destination, they already had the initial value of conditionForm,
+ // we need to display correct condition field.
+ if (
+ selectedConditionMap &&
+ selectedConditionMap[formTree.path] &&
+ !formIsDirty
+ ) {
const selectedCondition =
- formTree.conditions[conditionOptions[0].label] ?? null;
-
- const targetConstField = selectedCondition.properties.find(
- (e) => "const" in e
- ) as AirbyteFormItem;
- setConditionPath(targetConstField?.path ?? null);
-
- setSelectedConditionMap((prev) => ({
- ...(prev || {}),
- [formTree.path]: {
- selectedItem: selectedCondition
- ? selectedCondition.title ?? null
- : null,
- },
- }));
-
- if (targetConstField) {
- setValues((prev) => {
- const configuration = prev?.configuration ?? {};
- dot.setter(
- configuration,
- targetConstField.path,
- targetConstField.const
- );
- return {
- ...prev,
- configuration,
- };
- });
- } else {
- // Airbyte's doesn't have proper const field. We need to find a work around
- if (conditionPath) {
- setValues((prev) => {
- const configuration = prev?.configuration ?? {};
- dot.setter(configuration, conditionPath, selectedCondition.title);
- return {
- ...prev,
- configuration,
- };
- });
- }
- }
+ conditionOptions.find(
+ (e) => e.label === selectedConditionMap[formTree.path].selectedItem
+ ) || null;
+ setSelectedConditionOption(selectedCondition);
}
- }, [
- conditionOptions,
- selectedConditionMap,
- conditionPath,
- formTree.conditions,
- formTree.path,
- setSelectedConditionMap,
- setValues,
- ]);
+ }, [selectedConditionMap, formTree.path, conditionOptions, formIsDirty]);
const onConditionChange = useCallback(
(option: Nullable) => {
@@ -128,30 +154,6 @@ const OneOfConditionSection: FC = ({
const selectedCondition = formTree.conditions[option.label] ?? null;
setSelectedConditionOption(option);
- // We will rely on the field inside of condition with const key and use its path to set correct value
- // e.g. Snowflake's loading method
- //
- // "[Recommended] Internal Staging": {
- // fieldKey: "loading_method"
- // properties: [
- // {
- // "default": "Internal Staging",
- // "description": "",
- // "title": "",
- // "const": "Internal Staging",
- // "_type": "formItem",
- // "path": "loading_method.method",
- // "fieldKey": "method",
- // "isRequired": true,
- // "isSecret": false,
- // "multiline": false,
- // "type": "string"
- // }
- // ...
- // ]
- // ...
- // }
-
const targetConstField = selectedCondition.properties.find(
(e) => "const" in e
) as AirbyteFormItem;
@@ -194,9 +196,13 @@ const OneOfConditionSection: FC = ({
setSelectedConditionMap((prev) => ({
...(prev || {}),
[formTree.path]: {
- selectedItem: option ? option.label : null,
+ selectedItem: option ? (option.label as string) : null,
},
}));
+
+ if (setFormIsDirty) {
+ setFormIsDirty(true);
+ }
},
[
formTree.path,
@@ -204,6 +210,7 @@ const OneOfConditionSection: FC = ({
setValues,
conditionPath,
formTree.conditions,
+ setFormIsDirty,
]
);
@@ -215,6 +222,7 @@ const OneOfConditionSection: FC = ({
{
+ const form: AirbyteFormConditionItem = {
+ description:
+ "Whether to initiate an SSH tunnel before connecting to the database, and if so, which kind of authentication to use.",
+ title: "SSH Tunnel Method",
+ _type: "formCondition",
+ path: "tunnel_method",
+ fieldKey: "tunnel_method",
+ conditions: {
+ "No Tunnel": {
+ title: "No Tunnel",
+ _type: "formGroup",
+ jsonSchema: {
+ properties: {
+ tunnel_method: {
+ const: "NO_TUNNEL",
+ description: "No ssh tunnel needed to connect to database",
+ order: 0,
+ type: "string",
+ },
+ },
+ required: ["tunnel_method"],
+ title: "No Tunnel",
+ type: "object",
+ },
+ path: "tunnel_method",
+ fieldKey: "tunnel_method",
+ properties: [
+ {
+ const: "NO_TUNNEL",
+ description: "No ssh tunnel needed to connect to database",
+ order: 0,
+ _type: "formItem",
+ path: "tunnel_method.tunnel_method",
+ fieldKey: "tunnel_method",
+ isRequired: true,
+ isSecret: false,
+ multiline: false,
+ type: "string",
+ },
+ ],
+ isRequired: false,
+ },
+ },
+ isRequired: false,
+ };
+
+ const path = getConditionFormPath(form);
+ expect(path).toBe("tunnel_method.tunnel_method");
+});
+
+test("find path when some of the const field is missing", () => {
+ const form: AirbyteFormConditionItem = {
+ description:
+ "Whether to initiate an SSH tunnel before connecting to the database, and if so, which kind of authentication to use.",
+ title: "SSH Tunnel Method",
+ _type: "formCondition",
+ path: "tunnel_method",
+ fieldKey: "tunnel_method",
+ conditions: {
+ "No Tunnel": {
+ title: "No Tunnel",
+ _type: "formGroup",
+ jsonSchema: {
+ properties: {
+ tunnel_method: {
+ const: "NO_TUNNEL",
+ description: "No ssh tunnel needed to connect to database",
+ order: 0,
+ type: "string",
+ },
+ },
+ required: ["tunnel_method"],
+ title: "No Tunnel",
+ type: "object",
+ },
+ path: "tunnel_method",
+ fieldKey: "tunnel_method",
+ properties: [],
+ isRequired: false,
+ },
+ "SSH Key Authentication": {
+ title: "SSH Key Authentication",
+ _type: "formGroup",
+ jsonSchema: {
+ properties: {
+ ssh_key: {
+ airbyte_secret: true,
+ description:
+ "OS-level user account ssh key credentials in RSA PEM format ( created with ssh-keygen -t rsa -m PEM -f myuser_rsa )",
+ multiline: true,
+ order: 4,
+ title: "SSH Private Key",
+ type: "string",
+ },
+ tunnel_host: {
+ description:
+ "Hostname of the jump server host that allows inbound ssh tunnel.",
+ order: 1,
+ title: "SSH Tunnel Jump Server Host",
+ type: "string",
+ },
+ },
+ required: ["tunnel_host", "ssh_key"],
+ title: "SSH Key Authentication",
+ type: "object",
+ },
+ path: "tunnel_method",
+ fieldKey: "tunnel_method",
+ properties: [
+ {
+ const: "SSH_KEY_AUTH",
+ description:
+ "Connect through a jump server tunnel host using username and ssh key",
+ order: 0,
+ _type: "formItem",
+ path: "tunnel_method.tunnel_method",
+ fieldKey: "tunnel_method",
+ isRequired: true,
+ isSecret: false,
+ multiline: false,
+ type: "string",
+ },
+ {
+ description:
+ "Hostname of the jump server host that allows inbound ssh tunnel.",
+ order: 1,
+ title: "SSH Tunnel Jump Server Host",
+ _type: "formItem",
+ path: "tunnel_method.tunnel_host",
+ fieldKey: "tunnel_host",
+ isRequired: true,
+ isSecret: false,
+ multiline: false,
+ type: "string",
+ },
+ {
+ description:
+ "OS-level user account ssh key credentials in RSA PEM format ( created with ssh-keygen -t rsa -m PEM -f myuser_rsa )",
+ order: 4,
+ title: "SSH Private Key",
+ _type: "formItem",
+ path: "tunnel_method.ssh_key",
+ fieldKey: "ssh_key",
+ isRequired: true,
+ isSecret: true,
+ multiline: true,
+ type: "string",
+ },
+ ],
+ isRequired: false,
+ },
+ },
+ isRequired: false,
+ };
+
+ const path = getConditionFormPath(form);
+ expect(path).toBe("tunnel_method.tunnel_method");
+});
diff --git a/src/lib/airbytes/getConditionFormPath.ts b/src/lib/airbytes/getConditionFormPath.ts
new file mode 100644
index 0000000000..ed559febf3
--- /dev/null
+++ b/src/lib/airbytes/getConditionFormPath.ts
@@ -0,0 +1,31 @@
+import { Nullable } from "@/types/general";
+import { AirbyteFormConditionItem, AirbyteFormItem } from "./types";
+
+const getConditionFormPath = (
+ item: AirbyteFormConditionItem
+): Nullable => {
+ // Try to find a workaround of Typescript limitation
+ // https://github.com/microsoft/TypeScript/issues/9998
+ const path: string[] = [];
+
+ Object.entries(item.conditions).every(([, v]) => {
+ const targetConstField = v.properties.find((e) => "const" in e) as
+ | AirbyteFormItem
+ | undefined;
+
+ if (targetConstField && targetConstField.path) {
+ path.push(targetConstField.path);
+ return false;
+ } else {
+ return true;
+ }
+ });
+
+ if (path.length > 0) {
+ return path[0];
+ } else {
+ return null;
+ }
+};
+
+export default getConditionFormPath;
diff --git a/src/lib/airbytes/getFieldPaths.test.ts b/src/lib/airbytes/getFieldPaths.test.ts
new file mode 100644
index 0000000000..043fb37bc8
--- /dev/null
+++ b/src/lib/airbytes/getFieldPaths.test.ts
@@ -0,0 +1,378 @@
+import getFieldPaths from "./getFieldsPaths";
+import { AirbyteFormTree } from "./types";
+
+test("should get paths from single formItem", () => {
+ const formTree: AirbyteFormTree = {
+ const: "SSH_KEY_AUTH",
+ description:
+ "Connect through a jump server tunnel host using username and ssh key",
+ order: 0,
+ _type: "formItem",
+ path: "tunnel_method.tunnel_method",
+ fieldKey: "tunnel_method",
+ isRequired: true,
+ isSecret: false,
+ multiline: false,
+ type: "string",
+ };
+
+ const paths = getFieldPaths(formTree, false);
+ expect(paths).toEqual(["tunnel_method.tunnel_method"]);
+});
+
+test("should get paths from single formGroup", () => {
+ const formTree: AirbyteFormTree = {
+ _type: "formGroup",
+ fieldKey: "key",
+ path: "key",
+ isRequired: true,
+ jsonSchema: {
+ properties: {
+ host: {
+ description: "Hostname of the database.",
+ type: "string",
+ },
+ },
+ required: ["host"],
+ title: "Postgres Source Spec",
+ type: "object",
+ },
+ properties: [
+ {
+ _type: "formItem",
+ description: "Hostname of the database.",
+ fieldKey: "host",
+ path: "key.host",
+ isRequired: true,
+ isSecret: false,
+ multiline: false,
+ type: "string",
+ },
+ ],
+ title: "Postgres Source Spec",
+ };
+
+ const paths = getFieldPaths(formTree, false);
+ expect(paths).toEqual(["key.host"]);
+});
+
+test("should get paths from multiple formGroups", () => {
+ const formTrees: AirbyteFormTree[] = [
+ {
+ _type: "formGroup",
+ fieldKey: "foo",
+ path: "foo",
+ isRequired: true,
+ jsonSchema: {
+ properties: {
+ host: {
+ description: "Hostname of the database.",
+ type: "string",
+ },
+ },
+ required: ["host"],
+ title: "Postgres Source Spec",
+ type: "object",
+ },
+ properties: [
+ {
+ _type: "formItem",
+ description: "Hostname of the database.",
+ fieldKey: "host",
+ path: "foo.host",
+ isRequired: true,
+ isSecret: false,
+ multiline: false,
+ type: "string",
+ },
+ ],
+ title: "Postgres Source Spec",
+ },
+ {
+ _type: "formGroup",
+ fieldKey: "bar",
+ path: "bar",
+ isRequired: true,
+ jsonSchema: {
+ properties: {
+ port: {
+ description: "Hostname of the database.",
+ type: "string",
+ },
+ },
+ required: ["port"],
+ title: "Postgres Source Spec",
+ type: "object",
+ },
+ properties: [
+ {
+ _type: "formItem",
+ description: "Hostname of the database.",
+ fieldKey: "port",
+ path: "bar.port",
+ isRequired: true,
+ isSecret: false,
+ multiline: false,
+ type: "string",
+ },
+ ],
+ title: "Postgres Source Spec",
+ },
+ ];
+
+ const paths = getFieldPaths(formTrees, false);
+ expect(paths).toEqual(["foo.host", "bar.port"]);
+});
+
+test("should get path from conditionForm and force paths to be unique", () => {
+ const formTree: AirbyteFormTree = {
+ description:
+ "Whether to initiate an SSH tunnel before connecting to the database, and if so, which kind of authentication to use.",
+ title: "SSH Tunnel Method",
+ _type: "formCondition",
+ path: "tunnel_method",
+ fieldKey: "tunnel_method",
+ conditions: {
+ "No Tunnel": {
+ title: "No Tunnel",
+ _type: "formGroup",
+ jsonSchema: {
+ properties: {
+ tunnel_method: {
+ const: "NO_TUNNEL",
+ description: "No ssh tunnel needed to connect to database",
+ order: 0,
+ type: "string",
+ },
+ },
+ required: ["tunnel_method"],
+ title: "No Tunnel",
+ type: "object",
+ },
+ path: "tunnel_method",
+ fieldKey: "tunnel_method",
+ properties: [
+ {
+ const: "NO_TUNNEL",
+ description: "No ssh tunnel needed to connect to database",
+ order: 0,
+ _type: "formItem",
+ path: "tunnel_method.tunnel_method",
+ fieldKey: "tunnel_method",
+ isRequired: true,
+ isSecret: false,
+ multiline: false,
+ type: "string",
+ },
+ ],
+ isRequired: false,
+ },
+ "SSH Key Authentication": {
+ title: "SSH Key Authentication",
+ _type: "formGroup",
+ jsonSchema: {
+ properties: {
+ ssh_key: {
+ airbyte_secret: true,
+ description:
+ "OS-level user account ssh key credentials in RSA PEM format ( created with ssh-keygen -t rsa -m PEM -f myuser_rsa )",
+ multiline: true,
+ order: 4,
+ title: "SSH Private Key",
+ type: "string",
+ },
+ tunnel_host: {
+ description:
+ "Hostname of the jump server host that allows inbound ssh tunnel.",
+ order: 1,
+ title: "SSH Tunnel Jump Server Host",
+ type: "string",
+ },
+ },
+ required: ["tunnel_host", "ssh_key"],
+ title: "SSH Key Authentication",
+ type: "object",
+ },
+ path: "tunnel_method",
+ fieldKey: "tunnel_method",
+ properties: [
+ {
+ const: "SSH_KEY_AUTH",
+ description:
+ "Connect through a jump server tunnel host using username and ssh key",
+ order: 0,
+ _type: "formItem",
+ path: "tunnel_method.tunnel_method",
+ fieldKey: "tunnel_method",
+ isRequired: true,
+ isSecret: false,
+ multiline: false,
+ type: "string",
+ },
+ {
+ description:
+ "Hostname of the jump server host that allows inbound ssh tunnel.",
+ order: 1,
+ title: "SSH Tunnel Jump Server Host",
+ _type: "formItem",
+ path: "tunnel_method.tunnel_host",
+ fieldKey: "tunnel_host",
+ isRequired: true,
+ isSecret: false,
+ multiline: false,
+ type: "string",
+ },
+ {
+ description:
+ "OS-level user account ssh key credentials in RSA PEM format ( created with ssh-keygen -t rsa -m PEM -f myuser_rsa )",
+ order: 4,
+ title: "SSH Private Key",
+ _type: "formItem",
+ path: "tunnel_method.ssh_key",
+ fieldKey: "ssh_key",
+ isRequired: true,
+ isSecret: true,
+ multiline: true,
+ type: "string",
+ },
+ ],
+ isRequired: false,
+ },
+ },
+ isRequired: false,
+ };
+
+ const paths = getFieldPaths(formTree, true);
+ expect(paths).toEqual([
+ "tunnel_method.tunnel_method",
+ "tunnel_method.tunnel_host",
+ "tunnel_method.ssh_key",
+ ]);
+});
+
+test("should get path from conditionForm and not forcing paths to be unique", () => {
+ const formTree: AirbyteFormTree = {
+ description:
+ "Whether to initiate an SSH tunnel before connecting to the database, and if so, which kind of authentication to use.",
+ title: "SSH Tunnel Method",
+ _type: "formCondition",
+ path: "tunnel_method",
+ fieldKey: "tunnel_method",
+ conditions: {
+ "No Tunnel": {
+ title: "No Tunnel",
+ _type: "formGroup",
+ jsonSchema: {
+ properties: {
+ tunnel_method: {
+ const: "NO_TUNNEL",
+ description: "No ssh tunnel needed to connect to database",
+ order: 0,
+ type: "string",
+ },
+ },
+ required: ["tunnel_method"],
+ title: "No Tunnel",
+ type: "object",
+ },
+ path: "tunnel_method",
+ fieldKey: "tunnel_method",
+ properties: [
+ {
+ const: "NO_TUNNEL",
+ description: "No ssh tunnel needed to connect to database",
+ order: 0,
+ _type: "formItem",
+ path: "tunnel_method.tunnel_method",
+ fieldKey: "tunnel_method",
+ isRequired: true,
+ isSecret: false,
+ multiline: false,
+ type: "string",
+ },
+ ],
+ isRequired: false,
+ },
+ "SSH Key Authentication": {
+ title: "SSH Key Authentication",
+ _type: "formGroup",
+ jsonSchema: {
+ properties: {
+ ssh_key: {
+ airbyte_secret: true,
+ description:
+ "OS-level user account ssh key credentials in RSA PEM format ( created with ssh-keygen -t rsa -m PEM -f myuser_rsa )",
+ multiline: true,
+ order: 4,
+ title: "SSH Private Key",
+ type: "string",
+ },
+ tunnel_host: {
+ description:
+ "Hostname of the jump server host that allows inbound ssh tunnel.",
+ order: 1,
+ title: "SSH Tunnel Jump Server Host",
+ type: "string",
+ },
+ },
+ required: ["tunnel_host", "ssh_key"],
+ title: "SSH Key Authentication",
+ type: "object",
+ },
+ path: "tunnel_method",
+ fieldKey: "tunnel_method",
+ properties: [
+ {
+ const: "SSH_KEY_AUTH",
+ description:
+ "Connect through a jump server tunnel host using username and ssh key",
+ order: 0,
+ _type: "formItem",
+ path: "tunnel_method.tunnel_method",
+ fieldKey: "tunnel_method",
+ isRequired: true,
+ isSecret: false,
+ multiline: false,
+ type: "string",
+ },
+ {
+ description:
+ "Hostname of the jump server host that allows inbound ssh tunnel.",
+ order: 1,
+ title: "SSH Tunnel Jump Server Host",
+ _type: "formItem",
+ path: "tunnel_method.tunnel_host",
+ fieldKey: "tunnel_host",
+ isRequired: true,
+ isSecret: false,
+ multiline: false,
+ type: "string",
+ },
+ {
+ description:
+ "OS-level user account ssh key credentials in RSA PEM format ( created with ssh-keygen -t rsa -m PEM -f myuser_rsa )",
+ order: 4,
+ title: "SSH Private Key",
+ _type: "formItem",
+ path: "tunnel_method.ssh_key",
+ fieldKey: "ssh_key",
+ isRequired: true,
+ isSecret: true,
+ multiline: true,
+ type: "string",
+ },
+ ],
+ isRequired: false,
+ },
+ },
+ isRequired: false,
+ };
+
+ const paths = getFieldPaths(formTree, false);
+ expect(paths).toEqual([
+ "tunnel_method.tunnel_method",
+ "tunnel_method.tunnel_method",
+ "tunnel_method.tunnel_host",
+ "tunnel_method.ssh_key",
+ ]);
+});
diff --git a/src/lib/airbytes/getFieldsPaths.ts b/src/lib/airbytes/getFieldsPaths.ts
new file mode 100644
index 0000000000..a543dec043
--- /dev/null
+++ b/src/lib/airbytes/getFieldsPaths.ts
@@ -0,0 +1,61 @@
+import { AirbyteFormTree } from "./types";
+
+const getFieldPaths = (
+ formTree: AirbyteFormTree | AirbyteFormTree[],
+ forceUnique: boolean
+): string[] => {
+ const fieldPaths = [];
+
+ if (Array.isArray(formTree)) {
+ for (const branch of formTree) {
+ const paths = pickPath(branch);
+ fieldPaths.push(...paths);
+ }
+ } else {
+ const paths = pickPath(formTree);
+ fieldPaths.push(...paths);
+ }
+
+ if (forceUnique) {
+ return Array.from(new Set(fieldPaths));
+ } else {
+ return fieldPaths;
+ }
+};
+
+const pickPath = (
+ formTree: AirbyteFormTree,
+ paths: string[] = []
+): string[] => {
+ if (formTree._type === "formGroup") {
+ const newPaths: string[] = [];
+ formTree.properties.map((e) => {
+ const childPaths = pickPath(e, paths);
+ newPaths.push(...childPaths);
+ });
+ newPaths.push(...paths);
+ return newPaths;
+ }
+
+ if (formTree._type === "formCondition") {
+ const newPaths: string[] = [];
+ Object.entries(formTree.conditions).forEach(([, v]) => {
+ const childPaths = pickPath(v, paths);
+ newPaths.push(...childPaths);
+ });
+ newPaths.push(...paths);
+ return newPaths;
+ }
+
+ if (formTree._type === "objectArray") {
+ let newPaths: string[] = [];
+ const childPaths = pickPath(formTree.properties, paths);
+ newPaths = [...childPaths, ...paths];
+ return newPaths;
+ }
+
+ const newPaths = [formTree.path, ...paths];
+ return newPaths;
+};
+
+export default getFieldPaths;
diff --git a/src/lib/airbytes/hooks/index.ts b/src/lib/airbytes/hooks/index.ts
index 5cfc00ce4f..d476a5e780 100644
--- a/src/lib/airbytes/hooks/index.ts
+++ b/src/lib/airbytes/hooks/index.ts
@@ -2,10 +2,12 @@ import useBuildAirbyteFields from "./useBuildAirbyteFields";
import useBuildAirbyteYup from "./useBuildAirbyteYup";
import useAirbyteFormTree from "./useAirbyteFormTree";
import useAirbyteFieldValues from "./useAirbyteFieldValues";
+import useAirbyteSelectedConditionMap from "./useAirbyteSelectedConditionMap";
export {
useBuildAirbyteFields,
useBuildAirbyteYup,
useAirbyteFormTree,
useAirbyteFieldValues,
+ useAirbyteSelectedConditionMap,
};
diff --git a/src/lib/airbytes/hooks/useAirbyteFieldValues.ts b/src/lib/airbytes/hooks/useAirbyteFieldValues.ts
index f293f70936..3c84ff51e7 100644
--- a/src/lib/airbytes/hooks/useAirbyteFieldValues.ts
+++ b/src/lib/airbytes/hooks/useAirbyteFieldValues.ts
@@ -3,15 +3,19 @@ import { Nullable } from "@/types/general";
import { Dispatch, SetStateAction, useEffect, useState } from "react";
import { AirbyteFieldValues, AirbyteFormTree } from "../types";
-const useAirbyteFieldValues = (formTree: Nullable) => {
+const useAirbyteFieldValues = (
+ formTree: Nullable,
+ initialValue: Nullable
+) => {
const [fieldValues, setFieldValues] =
- useState>(null);
+ useState>(initialValue);
useEffect(() => {
if (!formTree) return;
+ if (initialValue) return;
if (fieldValues && fieldValues.configuration) return;
pickInitialValues(formTree, fieldValues, setFieldValues);
- }, [formTree, fieldValues, setFieldValues]);
+ }, [formTree, fieldValues, setFieldValues, initialValue]);
return { fieldValues, setFieldValues };
};
@@ -31,7 +35,7 @@ export const pickInitialValues = (
}
if (formTree._type === "formCondition") {
- Object.entries(formTree.conditions).map(([_, v]) => {
+ Object.entries(formTree.conditions).map(([, v]) => {
pickInitialValues(v, fieldValues, setFieldValues);
});
return;
diff --git a/src/lib/airbytes/hooks/useAirbyteSelectedConditionMap.ts b/src/lib/airbytes/hooks/useAirbyteSelectedConditionMap.ts
new file mode 100644
index 0000000000..8dbeb4b6ef
--- /dev/null
+++ b/src/lib/airbytes/hooks/useAirbyteSelectedConditionMap.ts
@@ -0,0 +1,28 @@
+import { useState } from "react";
+import { Nullable, UseCustomHookResult } from "@/types/general";
+import pickSelectedConditionMap from "../pickSelectedConditionMap";
+import { AirbyteFieldValues, AirbyteFormTree, SelectedItemMap } from "../types";
+
+export type UseAirbyteSelectedConditionMapResult = UseCustomHookResult<
+ Nullable
+>;
+
+const useAirbyteSelectedConditionMap = (
+ formTree: Nullable,
+ initialValue: Nullable
+): UseAirbyteSelectedConditionMapResult => {
+ let initialConditionMap: Nullable = {};
+
+ if (formTree && initialValue) {
+ initialConditionMap = pickSelectedConditionMap(formTree, initialValue);
+ } else {
+ initialConditionMap = null;
+ }
+
+ const [selectedConditionMap, setSelectedConditionMap] =
+ useState>(initialConditionMap);
+
+ return [selectedConditionMap, setSelectedConditionMap];
+};
+
+export default useAirbyteSelectedConditionMap;
diff --git a/src/lib/airbytes/hooks/useBuildAirbyteFields.tsx b/src/lib/airbytes/hooks/useBuildAirbyteFields.tsx
index 6f9b064744..f2ecb3f1ae 100644
--- a/src/lib/airbytes/hooks/useBuildAirbyteFields.tsx
+++ b/src/lib/airbytes/hooks/useBuildAirbyteFields.tsx
@@ -7,7 +7,7 @@ import {
BasicToggleField,
ProtectedBasicTextField,
} from "@instill-ai/design-system";
-import { useEffect, Dispatch, ReactNode, SetStateAction, useMemo } from "react";
+import { Dispatch, ReactNode, SetStateAction, useMemo, Fragment } from "react";
import OneOfConditionSection from "../components/OneOfConditionSection";
import {
AirbyteFormConditionItemWithUiFields,
@@ -26,7 +26,9 @@ const useBuildAirbyteFields = (
setValues: Dispatch>>,
errors: Nullable,
selectedConditionMap: Nullable,
- setSelectedConditionMap: Dispatch>>
+ setSelectedConditionMap: Dispatch>>,
+ formIsDirty: boolean,
+ setFormIsDirty: Dispatch>
) => {
const fields = useMemo(() => {
if (!formTree) return <>>;
@@ -37,7 +39,9 @@ const useBuildAirbyteFields = (
setValues,
errors,
selectedConditionMap,
- setSelectedConditionMap
+ setSelectedConditionMap,
+ formIsDirty,
+ setFormIsDirty
);
}, [
formTree,
@@ -47,6 +51,8 @@ const useBuildAirbyteFields = (
selectedConditionMap,
setSelectedConditionMap,
setValues,
+ formIsDirty,
+ setFormIsDirty,
]);
return fields;
@@ -61,11 +67,13 @@ export const pickComponent = (
setValues: Dispatch>>,
errors: Nullable,
selectedConditionMap: Nullable,
- setSelectedConditionMap: Dispatch>>
+ setSelectedConditionMap: Dispatch>>,
+ formIsDirty: boolean,
+ setFormIsDirty: Dispatch>
): ReactNode => {
if (formTree._type === "formGroup") {
return (
-
+
{formTree.properties.map((e) =>
pickComponent(
e,
@@ -74,10 +82,12 @@ export const pickComponent = (
setValues,
errors,
selectedConditionMap,
- setSelectedConditionMap
+ setSelectedConditionMap,
+ formIsDirty,
+ setFormIsDirty
)
)}
-
+
);
}
@@ -96,7 +106,9 @@ export const pickComponent = (
setValues,
errors,
selectedConditionMap,
- setSelectedConditionMap
+ setSelectedConditionMap,
+ formIsDirty,
+ setFormIsDirty
),
},
];
@@ -111,6 +123,9 @@ export const pickComponent = (
selectedConditionMap={selectedConditionMap}
setSelectedConditionMap={setSelectedConditionMap}
errors={errors}
+ disableAll={disabledAll}
+ formIsDirty={formIsDirty}
+ setFormIsDirty={setFormIsDirty}
/>
);
}
@@ -123,7 +138,9 @@ export const pickComponent = (
setValues,
errors,
selectedConditionMap,
- setSelectedConditionMap
+ setSelectedConditionMap,
+ formIsDirty,
+ setFormIsDirty
);
}
@@ -163,32 +180,25 @@ export const pickComponent = (
readOnly={false}
error={errors ? errors[formTree.path] ?? null : null}
value={values ? (values[formTree.path] as boolean) ?? false : false}
- onChange={(event) =>
+ onChange={(event) => {
+ if (setFormIsDirty) setFormIsDirty(true);
setValues((prev) => {
const value = event.target.checked;
const configuration = prev?.configuration ?? {};
dot.setter(configuration, formTree.path, value);
+
return {
...prev,
configuration: configuration,
[formTree.path]: value,
};
- })
- }
+ });
+ }}
/>
);
}
if (formTree.type === "string" && formTree.enum && formTree.enum.length) {
- let setDefault = false;
- if (!values) {
- setDefault = true;
- } else {
- if (!values[formTree.path]) {
- setDefault = true;
- }
- }
-
const options = formTree.enum.map((e) => {
return {
label: e?.toString() ?? "",
@@ -214,7 +224,8 @@ export const pickComponent = (
null
: null
}
- onChange={(option) =>
+ onChange={(option) => {
+ if (setFormIsDirty) setFormIsDirty(true);
setValues((prev) => {
const configuration = prev?.configuration || {};
dot.setter(configuration, formTree.path, option?.value ?? null);
@@ -223,8 +234,8 @@ export const pickComponent = (
configuration: configuration,
[formTree.path]: option?.value ?? null,
};
- })
- }
+ });
+ }}
readOnly={false}
menuPlacement="auto"
additionalMessageOnLabel={null}
@@ -245,7 +256,8 @@ export const pickComponent = (
placeholder={placeholder ?? ""}
error={errors ? errors[formTree.path] ?? null : null}
value={values ? (values[formTree.path] as string) ?? "" : ""}
- onChange={(event) =>
+ onChange={(event) => {
+ if (setFormIsDirty) setFormIsDirty(true);
setValues((prev) => {
const value = event.target.value;
const configuration = prev?.configuration || {};
@@ -255,8 +267,8 @@ export const pickComponent = (
configuration: configuration,
[formTree.path]: value,
};
- })
- }
+ });
+ }}
/>
);
}
@@ -274,7 +286,8 @@ export const pickComponent = (
placeholder={placeholder ?? ""}
error={errors ? errors[formTree.path] ?? null : null}
value={values ? (values[formTree.path] as string) ?? "" : ""}
- onChange={(event) =>
+ onChange={(event) => {
+ if (setFormIsDirty) setFormIsDirty(true);
setValues((prev) => {
const value = event.target.value;
const configuration = prev?.configuration || {};
@@ -284,8 +297,8 @@ export const pickComponent = (
configuration: configuration,
[formTree.path]: value,
};
- })
- }
+ });
+ }}
readOnly={false}
/>
);
@@ -309,7 +322,7 @@ export const pickComponent = (
// In HTML type=number input, the value is still string, we need to transfer it into number
// But in HTML number input, user can input e as exponential, parseInt will return NaN.
// In this case, we pass the value to the Yup, and let it guard for us.
-
+ if (setFormIsDirty) setFormIsDirty(true);
setValues((prev) => {
const value = event.target.value;
const configuration = prev?.configuration || {};
diff --git a/src/lib/airbytes/index.ts b/src/lib/airbytes/index.ts
index 40e91d20c1..9ffe955694 100644
--- a/src/lib/airbytes/index.ts
+++ b/src/lib/airbytes/index.ts
@@ -5,6 +5,7 @@ import {
useBuildAirbyteYup,
useAirbyteFormTree,
useAirbyteFieldValues,
+ useAirbyteSelectedConditionMap,
} from "./hooks";
export {
@@ -12,4 +13,5 @@ export {
useBuildAirbyteYup,
useAirbyteFormTree,
useAirbyteFieldValues,
+ useAirbyteSelectedConditionMap,
};
diff --git a/src/lib/airbytes/pickSelectedConditionMap.test.ts b/src/lib/airbytes/pickSelectedConditionMap.test.ts
new file mode 100644
index 0000000000..5243a84468
--- /dev/null
+++ b/src/lib/airbytes/pickSelectedConditionMap.test.ts
@@ -0,0 +1,2541 @@
+import pickSelectedConditionMap from "./pickSelectedConditionMap";
+import { AirbyteFormTree } from "./types";
+
+test("should find one selected condition map", () => {
+ const formTree: AirbyteFormTree = {
+ title: "Snowflake Destination Spec",
+ _type: "formGroup",
+ jsonSchema: {
+ $schema: "http://json-schema.org/draft-07/schema#",
+ additionalProperties: true,
+ properties: {
+ credentials: {
+ description: "",
+ oneOf: [
+ {
+ properties: {
+ access_token: {
+ description: "Enter you application's Access Token",
+ title: "Access Token",
+ type: "string",
+ },
+ auth_type: {
+ const: "OAuth2.0",
+ default: "OAuth2.0",
+ enum: ["OAuth2.0"],
+ type: "string",
+ },
+ client_id: {
+ description: "Enter your application's Client ID",
+ title: "Client ID",
+ type: "string",
+ },
+ client_secret: {
+ description: "Enter your application's Client secret",
+ title: "Client Secret",
+ type: "string",
+ },
+ refresh_token: {
+ description: "Enter your application's Refresh Token",
+ title: "Refresh Token",
+ type: "string",
+ },
+ },
+ required: ["access_token", "refresh_token"],
+ title: "OAuth2.0",
+ type: "object",
+ },
+ {
+ properties: {
+ password: {
+ description:
+ "Enter the password associated with the username.",
+ title: "Password",
+ type: "string",
+ },
+ },
+ required: ["password"],
+ title: "Username and Password",
+ type: "object",
+ },
+ ],
+ order: 6,
+ title: "Authorization Method",
+ type: "object",
+ },
+ loading_method: {
+ description: "Select a data staging method",
+ oneOf: [
+ {
+ description: "Select another option",
+ properties: {
+ method: {
+ default: "Standard",
+ description: "",
+ enum: ["Standard"],
+ title: "",
+ type: "string",
+ },
+ },
+ required: ["method"],
+ title: "Select another option",
+ },
+ {
+ description:
+ "Recommended for large production workloads for better speed and scalability.",
+ properties: {
+ method: {
+ default: "Internal Staging",
+ description: "",
+ enum: ["Internal Staging"],
+ title: "",
+ type: "string",
+ },
+ },
+ required: ["method"],
+ title: "[Recommended] Internal Staging",
+ },
+ {
+ description:
+ "Recommended for large production workloads for better speed and scalability.",
+ properties: {
+ access_key_id: {
+ description:
+ 'Enter your AWS access key ID . Airbyte requires Read and Write permissions on your S3 bucket ',
+ title: "AWS access key ID",
+ type: "string",
+ },
+ encryption: {
+ default: {
+ encryption_type: "none",
+ },
+ description:
+ "Choose a data encryption method for the staging data",
+ oneOf: [
+ {
+ description: "Staging data will be stored in plaintext.",
+ properties: {
+ encryption_type: {
+ const: "none",
+ default: "none",
+ enum: ["none"],
+ type: "string",
+ },
+ },
+ required: ["encryption_type"],
+ title: "No encryption",
+ type: "object",
+ },
+ {
+ description:
+ "Staging data will be encrypted using AES-CBC envelope encryption.",
+ properties: {
+ encryption_type: {
+ const: "aes_cbc_envelope",
+ default: "aes_cbc_envelope",
+ enum: ["aes_cbc_envelope"],
+ type: "string",
+ },
+ key_encrypting_key: {
+ description:
+ "The key, base64-encoded. Must be either 128, 192, or 256 bits. Leave blank to have Airbyte generate an ephemeral key for each sync.",
+ title: "Key",
+ type: "string",
+ },
+ },
+ required: ["encryption_type"],
+ title: "AES-CBC envelope encryption",
+ type: "object",
+ },
+ ],
+ title: "Encryption",
+ type: "object",
+ },
+ file_name_pattern: {
+ description:
+ "The pattern allows you to set the file-name format for the S3 staging file(s)",
+ examples: [
+ "{date}",
+ "{date:yyyy_MM}",
+ "{timestamp}",
+ "{part_number}",
+ "{sync_id}",
+ ],
+ title: "S3 Filename pattern (Optional)",
+ type: "string",
+ },
+ method: {
+ default: "S3 Staging",
+ description: "",
+ enum: ["S3 Staging"],
+ title: "",
+ type: "string",
+ },
+ purge_staging_data: {
+ default: true,
+ description:
+ "Toggle to delete staging files from the S3 bucket after a successful sync",
+ title: "Purge Staging Files and Tables",
+ type: "boolean",
+ },
+ s3_bucket_name: {
+ description: "Enter your S3 bucket name",
+ examples: ["airbyte.staging"],
+ title: "S3 Bucket Name",
+ type: "string",
+ },
+ s3_bucket_region: {
+ default: "",
+ description: "Enter the region where your S3 bucket resides",
+ enum: [
+ "",
+ "us-east-1",
+ "us-east-2",
+ "us-west-1",
+ "us-west-2",
+ "af-south-1",
+ "ap-east-1",
+ "ap-south-1",
+ "ap-northeast-1",
+ "ap-northeast-2",
+ "ap-northeast-3",
+ "ap-southeast-1",
+ "ap-southeast-2",
+ "ca-central-1",
+ "cn-north-1",
+ "cn-northwest-1",
+ "eu-central-1",
+ "eu-west-1",
+ "eu-west-2",
+ "eu-west-3",
+ "eu-south-1",
+ "eu-north-1",
+ "sa-east-1",
+ "me-south-1",
+ ],
+ title: "S3 Bucket Region",
+ type: "string",
+ },
+ secret_access_key: {
+ description:
+ 'Enter your AWS secret access key ',
+ title: "AWS secret access key",
+ type: "string",
+ },
+ },
+ required: [
+ "method",
+ "s3_bucket_name",
+ "access_key_id",
+ "secret_access_key",
+ ],
+ title: "AWS S3 Staging",
+ },
+ {
+ description:
+ "Recommended for large production workloads for better speed and scalability.",
+ properties: {
+ bucket_name: {
+ description:
+ 'Enter the Cloud Storage bucket name ',
+ examples: ["airbyte-staging"],
+ title: "Cloud Storage bucket name",
+ type: "string",
+ },
+ credentials_json: {
+ description:
+ 'Enter your Google Cloud service account key in the JSON format with read/write access to your Cloud Storage staging bucket',
+ title: "Google Application Credentials",
+ type: "string",
+ },
+ method: {
+ default: "GCS Staging",
+ description: "",
+ enum: ["GCS Staging"],
+ title: "",
+ type: "string",
+ },
+ project_id: {
+ description:
+ 'Enter the Google Cloud project ID ',
+ examples: ["my-project"],
+ title: "Google Cloud project ID",
+ type: "string",
+ },
+ },
+ required: [
+ "method",
+ "project_id",
+ "bucket_name",
+ "credentials_json",
+ ],
+ title: "Google Cloud Storage Staging",
+ },
+ {
+ description:
+ "Recommended for large production workloads for better speed and scalability.",
+ properties: {
+ azure_blob_storage_account_name: {
+ description: "Enter your Azure Blob Storage account name",
+ examples: ["airbyte5storage"],
+ title:
+ 'Azure Blob Storage account name ',
+ type: "string",
+ },
+ azure_blob_storage_container_name: {
+ description:
+ 'Enter your Azure Blob Storage container name ',
+ examples: ["airbytetestcontainername"],
+ title: "Azure Blob Storage Container Name",
+ type: "string",
+ },
+ azure_blob_storage_endpoint_domain_name: {
+ default: "blob.core.windows.net",
+ description:
+ 'Enter the Azure Blob Storage endpoint domain name ',
+ examples: ["blob.core.windows.net"],
+ title: "Azure Blob Storage Endpoint",
+ type: "string",
+ },
+ azure_blob_storage_sas_token: {
+ description:
+ 'Enter the Shared access signature (SAS) token to grant Snowflake limited access to objects in your Azure Blob Storage account',
+ examples: [
+ "?sv=2016-05-31&ss=b&srt=sco&sp=rwdl&se=2018-06-27T10:05:50Z&st=2017-06-27T02:05:50Z&spr=https,http&sig=bgqQwoXwxzuD2GJfagRg7VOS8hzNr3QLT7rhS8OFRLQ%3D",
+ ],
+ title: "SAS Token",
+ type: "string",
+ },
+ method: {
+ default: "Azure Blob Staging",
+ description: "",
+ enum: ["Azure Blob Staging"],
+ title: "",
+ type: "string",
+ },
+ },
+ required: [
+ "method",
+ "azure_blob_storage_account_name",
+ "azure_blob_storage_container_name",
+ "azure_blob_storage_sas_token",
+ ],
+ title: "Azure Blob Storage Staging",
+ },
+ ],
+ order: 8,
+ title: "Data Staging Method",
+ type: "object",
+ },
+ username: {
+ description:
+ "Enter the name of the user you want to use to access the database",
+ examples: ["AIRBYTE_USER"],
+ order: 5,
+ title: "Username",
+ type: "string",
+ },
+ },
+ required: ["username"],
+ title: "Snowflake Destination Spec",
+ type: "object",
+ },
+ properties: [
+ {
+ description:
+ "Enter the name of the user you want to use to access the database",
+ examples: ["AIRBYTE_USER"],
+ order: 5,
+ title: "Username",
+ _type: "formItem",
+ path: "username",
+ fieldKey: "username",
+ isRequired: true,
+ isSecret: false,
+ multiline: false,
+ type: "string",
+ },
+ {
+ description: "",
+ order: 6,
+ title: "Authorization Method",
+ _type: "formCondition",
+ path: "credentials",
+ fieldKey: "credentials",
+ conditions: {
+ "OAuth2.0": {
+ order: 0,
+ title: "OAuth2.0",
+ _type: "formGroup",
+ jsonSchema: {
+ order: 0,
+ properties: {
+ access_token: {
+ airbyte_secret: true,
+ description: "Enter you application's Access Token",
+ title: "Access Token",
+ type: "string",
+ },
+ auth_type: {
+ const: "OAuth2.0",
+ default: "OAuth2.0",
+ enum: ["OAuth2.0"],
+ order: 0,
+ type: "string",
+ },
+ client_id: {
+ airbyte_secret: true,
+ description: "Enter your application's Client ID",
+ title: "Client ID",
+ type: "string",
+ },
+ client_secret: {
+ airbyte_secret: true,
+ description: "Enter your application's Client secret",
+ title: "Client Secret",
+ type: "string",
+ },
+ refresh_token: {
+ airbyte_secret: true,
+ description: "Enter your application's Refresh Token",
+ title: "Refresh Token",
+ type: "string",
+ },
+ },
+ required: ["access_token", "refresh_token"],
+ title: "OAuth2.0",
+ type: "object",
+ },
+ path: "credentials",
+ fieldKey: "credentials",
+ properties: [
+ {
+ const: "OAuth2.0",
+ default: "OAuth2.0",
+ order: 0,
+ _type: "formItem",
+ path: "credentials.auth_type",
+ fieldKey: "auth_type",
+ isRequired: false,
+ isSecret: false,
+ multiline: false,
+ type: "string",
+ },
+ {
+ description: "Enter you application's Access Token",
+ title: "Access Token",
+ _type: "formItem",
+ path: "credentials.access_token",
+ fieldKey: "access_token",
+ isRequired: true,
+ isSecret: true,
+ multiline: false,
+ type: "string",
+ },
+ {
+ description: "Enter your application's Client ID",
+ title: "Client ID",
+ _type: "formItem",
+ path: "credentials.client_id",
+ fieldKey: "client_id",
+ isRequired: false,
+ isSecret: true,
+ multiline: false,
+ type: "string",
+ },
+ {
+ description: "Enter your application's Client secret",
+ title: "Client Secret",
+ _type: "formItem",
+ path: "credentials.client_secret",
+ fieldKey: "client_secret",
+ isRequired: false,
+ isSecret: true,
+ multiline: false,
+ type: "string",
+ },
+ {
+ description: "Enter your application's Refresh Token",
+ title: "Refresh Token",
+ _type: "formItem",
+ path: "credentials.refresh_token",
+ fieldKey: "refresh_token",
+ isRequired: true,
+ isSecret: true,
+ multiline: false,
+ type: "string",
+ },
+ ],
+ isRequired: false,
+ },
+ "Username and Password": {
+ order: 1,
+ title: "Username and Password",
+ _type: "formGroup",
+ jsonSchema: {
+ order: 1,
+ properties: {
+ password: {
+ airbyte_secret: true,
+ description:
+ "Enter the password associated with the username.",
+ order: 1,
+ title: "Password",
+ type: "string",
+ },
+ },
+ required: ["password"],
+ title: "Username and Password",
+ type: "object",
+ },
+ path: "credentials",
+ fieldKey: "credentials",
+ properties: [
+ {
+ description: "Enter the password associated with the username.",
+ order: 1,
+ title: "Password",
+ _type: "formItem",
+ path: "credentials.password",
+ fieldKey: "password",
+ isRequired: true,
+ isSecret: true,
+ multiline: false,
+ type: "string",
+ },
+ ],
+ isRequired: false,
+ },
+ },
+ isRequired: false,
+ },
+ {
+ description: "Select a data staging method",
+ order: 8,
+ title: "Data Staging Method",
+ _type: "formCondition",
+ path: "loading_method",
+ fieldKey: "loading_method",
+ conditions: {
+ "Select another option": {
+ description: "Select another option",
+ title: "Select another option",
+ _type: "formGroup",
+ jsonSchema: {
+ description: "Select another option",
+ properties: {
+ method: {
+ default: "Standard",
+ description: "",
+ enum: ["Standard"],
+ title: "",
+ type: "string",
+ },
+ },
+ required: ["method"],
+ title: "Select another option",
+ type: "object",
+ },
+ path: "loading_method",
+ fieldKey: "loading_method",
+ properties: [
+ {
+ default: "Standard",
+ description: "",
+ title: "",
+ const: "Standard",
+ _type: "formItem",
+ path: "loading_method.method",
+ fieldKey: "method",
+ isRequired: true,
+ isSecret: false,
+ multiline: false,
+ type: "string",
+ },
+ ],
+ isRequired: false,
+ },
+ "[Recommended] Internal Staging": {
+ description:
+ "Recommended for large production workloads for better speed and scalability.",
+ title: "[Recommended] Internal Staging",
+ _type: "formGroup",
+ jsonSchema: {
+ description:
+ "Recommended for large production workloads for better speed and scalability.",
+ properties: {
+ method: {
+ default: "Internal Staging",
+ description: "",
+ enum: ["Internal Staging"],
+ title: "",
+ type: "string",
+ },
+ },
+ required: ["method"],
+ title: "[Recommended] Internal Staging",
+ type: "object",
+ },
+ path: "loading_method",
+ fieldKey: "loading_method",
+ properties: [
+ {
+ default: "Internal Staging",
+ description: "",
+ title: "",
+ const: "Internal Staging",
+ _type: "formItem",
+ path: "loading_method.method",
+ fieldKey: "method",
+ isRequired: true,
+ isSecret: false,
+ multiline: false,
+ type: "string",
+ },
+ ],
+ isRequired: false,
+ },
+ "AWS S3 Staging": {
+ description:
+ "Recommended for large production workloads for better speed and scalability.",
+ title: "AWS S3 Staging",
+ _type: "formGroup",
+ jsonSchema: {
+ description:
+ "Recommended for large production workloads for better speed and scalability.",
+ properties: {
+ access_key_id: {
+ airbyte_secret: true,
+ description:
+ 'Enter your AWS access key ID . Airbyte requires Read and Write permissions on your S3 bucket ',
+ order: 3,
+ title: "AWS access key ID",
+ type: "string",
+ },
+ encryption: {
+ default: {
+ encryption_type: "none",
+ },
+ description:
+ "Choose a data encryption method for the staging data",
+ oneOf: [
+ {
+ description: "Staging data will be stored in plaintext.",
+ properties: {
+ encryption_type: {
+ const: "none",
+ default: "none",
+ enum: ["none"],
+ type: "string",
+ },
+ },
+ required: ["encryption_type"],
+ title: "No encryption",
+ type: "object",
+ },
+ {
+ description:
+ "Staging data will be encrypted using AES-CBC envelope encryption.",
+ properties: {
+ encryption_type: {
+ const: "aes_cbc_envelope",
+ default: "aes_cbc_envelope",
+ enum: ["aes_cbc_envelope"],
+ type: "string",
+ },
+ key_encrypting_key: {
+ description:
+ "The key, base64-encoded. Must be either 128, 192, or 256 bits. Leave blank to have Airbyte generate an ephemeral key for each sync.",
+ title: "Key",
+ type: "string",
+ },
+ },
+ required: ["encryption_type"],
+ title: "AES-CBC envelope encryption",
+ type: "object",
+ },
+ ],
+ order: 6,
+ title: "Encryption",
+ type: "object",
+ },
+ file_name_pattern: {
+ description:
+ "The pattern allows you to set the file-name format for the S3 staging file(s)",
+ examples: [
+ "{date}",
+ "{date:yyyy_MM}",
+ "{timestamp}",
+ "{part_number}",
+ "{sync_id}",
+ ],
+ order: 7,
+ title: "S3 Filename pattern (Optional)",
+ type: "string",
+ },
+ method: {
+ default: "S3 Staging",
+ description: "",
+ enum: ["S3 Staging"],
+ order: 0,
+ title: "",
+ type: "string",
+ },
+ purge_staging_data: {
+ default: true,
+ description:
+ "Toggle to delete staging files from the S3 bucket after a successful sync",
+ order: 5,
+ title: "Purge Staging Files and Tables",
+ type: "boolean",
+ },
+ s3_bucket_name: {
+ description: "Enter your S3 bucket name",
+ examples: ["airbyte.staging"],
+ order: 1,
+ title: "S3 Bucket Name",
+ type: "string",
+ },
+ s3_bucket_region: {
+ default: "",
+ description: "Enter the region where your S3 bucket resides",
+ enum: [
+ "",
+ "us-east-1",
+ "us-east-2",
+ "us-west-1",
+ "us-west-2",
+ "af-south-1",
+ "ap-east-1",
+ "ap-south-1",
+ "ap-northeast-1",
+ "ap-northeast-2",
+ "ap-northeast-3",
+ "ap-southeast-1",
+ "ap-southeast-2",
+ "ca-central-1",
+ "cn-north-1",
+ "cn-northwest-1",
+ "eu-central-1",
+ "eu-west-1",
+ "eu-west-2",
+ "eu-west-3",
+ "eu-south-1",
+ "eu-north-1",
+ "sa-east-1",
+ "me-south-1",
+ ],
+ order: 2,
+ title: "S3 Bucket Region",
+ type: "string",
+ },
+ secret_access_key: {
+ airbyte_secret: true,
+ description:
+ 'Enter your AWS secret access key ',
+ order: 4,
+ title: "AWS secret access key",
+ type: "string",
+ },
+ },
+ required: [
+ "method",
+ "s3_bucket_name",
+ "access_key_id",
+ "secret_access_key",
+ ],
+ title: "AWS S3 Staging",
+ type: "object",
+ },
+ path: "loading_method",
+ fieldKey: "loading_method",
+ properties: [
+ {
+ default: "S3 Staging",
+ description: "",
+ order: 0,
+ title: "",
+ const: "S3 Staging",
+ _type: "formItem",
+ path: "loading_method.method",
+ fieldKey: "method",
+ isRequired: true,
+ isSecret: false,
+ multiline: false,
+ type: "string",
+ },
+ {
+ description: "Enter your S3 bucket name",
+ examples: ["airbyte.staging"],
+ order: 1,
+ title: "S3 Bucket Name",
+ _type: "formItem",
+ path: "loading_method.s3_bucket_name",
+ fieldKey: "s3_bucket_name",
+ isRequired: true,
+ isSecret: false,
+ multiline: false,
+ type: "string",
+ },
+ {
+ default: "",
+ description: "Enter the region where your S3 bucket resides",
+ order: 2,
+ title: "S3 Bucket Region",
+ enum: [
+ "",
+ "us-east-1",
+ "us-east-2",
+ "us-west-1",
+ "us-west-2",
+ "af-south-1",
+ "ap-east-1",
+ "ap-south-1",
+ "ap-northeast-1",
+ "ap-northeast-2",
+ "ap-northeast-3",
+ "ap-southeast-1",
+ "ap-southeast-2",
+ "ca-central-1",
+ "cn-north-1",
+ "cn-northwest-1",
+ "eu-central-1",
+ "eu-west-1",
+ "eu-west-2",
+ "eu-west-3",
+ "eu-south-1",
+ "eu-north-1",
+ "sa-east-1",
+ "me-south-1",
+ ],
+ _type: "formItem",
+ path: "loading_method.s3_bucket_region",
+ fieldKey: "s3_bucket_region",
+ isRequired: false,
+ isSecret: false,
+ multiline: false,
+ type: "string",
+ },
+ {
+ description:
+ 'Enter your AWS access key ID . Airbyte requires Read and Write permissions on your S3 bucket ',
+ order: 3,
+ title: "AWS access key ID",
+ _type: "formItem",
+ path: "loading_method.access_key_id",
+ fieldKey: "access_key_id",
+ isRequired: true,
+ isSecret: true,
+ multiline: false,
+ type: "string",
+ },
+ {
+ description:
+ 'Enter your AWS secret access key ',
+ order: 4,
+ title: "AWS secret access key",
+ _type: "formItem",
+ path: "loading_method.secret_access_key",
+ fieldKey: "secret_access_key",
+ isRequired: true,
+ isSecret: true,
+ multiline: false,
+ type: "string",
+ },
+ {
+ default: true,
+ description:
+ "Toggle to delete staging files from the S3 bucket after a successful sync",
+ order: 5,
+ title: "Purge Staging Files and Tables",
+ _type: "formItem",
+ path: "loading_method.purge_staging_data",
+ fieldKey: "purge_staging_data",
+ isRequired: false,
+ isSecret: false,
+ multiline: false,
+ type: "boolean",
+ },
+ {
+ description:
+ "Choose a data encryption method for the staging data",
+ order: 6,
+ title: "Encryption",
+ _type: "formCondition",
+ path: "loading_method.encryption",
+ fieldKey: "encryption",
+ conditions: {
+ "No encryption": {
+ description: "Staging data will be stored in plaintext.",
+ title: "No encryption",
+ _type: "formGroup",
+ jsonSchema: {
+ description: "Staging data will be stored in plaintext.",
+ properties: {
+ encryption_type: {
+ const: "none",
+ default: "none",
+ enum: ["none"],
+ type: "string",
+ },
+ },
+ required: ["encryption_type"],
+ title: "No encryption",
+ type: "object",
+ },
+ path: "loading_method.encryption",
+ fieldKey: "encryption",
+ properties: [
+ {
+ const: "none",
+ default: "none",
+ _type: "formItem",
+ path: "loading_method.encryption.encryption_type",
+ fieldKey: "encryption_type",
+ isRequired: true,
+ isSecret: false,
+ multiline: false,
+ type: "string",
+ },
+ ],
+ isRequired: false,
+ },
+ "AES-CBC envelope encryption": {
+ description:
+ "Staging data will be encrypted using AES-CBC envelope encryption.",
+ title: "AES-CBC envelope encryption",
+ _type: "formGroup",
+ jsonSchema: {
+ description:
+ "Staging data will be encrypted using AES-CBC envelope encryption.",
+ properties: {
+ encryption_type: {
+ const: "aes_cbc_envelope",
+ default: "aes_cbc_envelope",
+ enum: ["aes_cbc_envelope"],
+ type: "string",
+ },
+ key_encrypting_key: {
+ airbyte_secret: true,
+ description:
+ "The key, base64-encoded. Must be either 128, 192, or 256 bits. Leave blank to have Airbyte generate an ephemeral key for each sync.",
+ title: "Key",
+ type: "string",
+ },
+ },
+ required: ["encryption_type"],
+ title: "AES-CBC envelope encryption",
+ type: "object",
+ },
+ path: "loading_method.encryption",
+ fieldKey: "encryption",
+ properties: [
+ {
+ const: "aes_cbc_envelope",
+ default: "aes_cbc_envelope",
+ _type: "formItem",
+ path: "loading_method.encryption.encryption_type",
+ fieldKey: "encryption_type",
+ isRequired: true,
+ isSecret: false,
+ multiline: false,
+ type: "string",
+ },
+ {
+ description:
+ "The key, base64-encoded. Must be either 128, 192, or 256 bits. Leave blank to have Airbyte generate an ephemeral key for each sync.",
+ title: "Key",
+ _type: "formItem",
+ path: "loading_method.encryption.key_encrypting_key",
+ fieldKey: "key_encrypting_key",
+ isRequired: false,
+ isSecret: true,
+ multiline: false,
+ type: "string",
+ },
+ ],
+ isRequired: false,
+ },
+ },
+ isRequired: false,
+ },
+ {
+ description:
+ "The pattern allows you to set the file-name format for the S3 staging file(s)",
+ examples: [
+ "{date}",
+ "{date:yyyy_MM}",
+ "{timestamp}",
+ "{part_number}",
+ "{sync_id}",
+ ],
+ order: 7,
+ title: "S3 Filename pattern (Optional)",
+ _type: "formItem",
+ path: "loading_method.file_name_pattern",
+ fieldKey: "file_name_pattern",
+ isRequired: false,
+ isSecret: false,
+ multiline: false,
+ type: "string",
+ },
+ ],
+ isRequired: false,
+ },
+ "Google Cloud Storage Staging": {
+ description:
+ "Recommended for large production workloads for better speed and scalability.",
+ title: "Google Cloud Storage Staging",
+ _type: "formGroup",
+ jsonSchema: {
+ description:
+ "Recommended for large production workloads for better speed and scalability.",
+ properties: {
+ bucket_name: {
+ description:
+ 'Enter the Cloud Storage bucket name ',
+ examples: ["airbyte-staging"],
+ order: 2,
+ title: "Cloud Storage bucket name",
+ type: "string",
+ },
+ credentials_json: {
+ airbyte_secret: true,
+ description:
+ 'Enter your Google Cloud service account key in the JSON format with read/write access to your Cloud Storage staging bucket',
+ multiline: true,
+ order: 3,
+ title: "Google Application Credentials",
+ type: "string",
+ },
+ method: {
+ default: "GCS Staging",
+ description: "",
+ enum: ["GCS Staging"],
+ order: 0,
+ title: "",
+ type: "string",
+ },
+ project_id: {
+ description:
+ 'Enter the Google Cloud project ID ',
+ examples: ["my-project"],
+ order: 1,
+ title: "Google Cloud project ID",
+ type: "string",
+ },
+ },
+ required: [
+ "method",
+ "project_id",
+ "bucket_name",
+ "credentials_json",
+ ],
+ title: "Google Cloud Storage Staging",
+ type: "object",
+ },
+ path: "loading_method",
+ fieldKey: "loading_method",
+ properties: [
+ {
+ default: "GCS Staging",
+ description: "",
+ order: 0,
+ title: "",
+ const: "GCS Staging",
+ _type: "formItem",
+ path: "loading_method.method",
+ fieldKey: "method",
+ isRequired: true,
+ isSecret: false,
+ multiline: false,
+ type: "string",
+ },
+ {
+ description:
+ 'Enter the Google Cloud project ID ',
+ examples: ["my-project"],
+ order: 1,
+ title: "Google Cloud project ID",
+ _type: "formItem",
+ path: "loading_method.project_id",
+ fieldKey: "project_id",
+ isRequired: true,
+ isSecret: false,
+ multiline: false,
+ type: "string",
+ },
+ {
+ description:
+ 'Enter the Cloud Storage bucket name ',
+ examples: ["airbyte-staging"],
+ order: 2,
+ title: "Cloud Storage bucket name",
+ _type: "formItem",
+ path: "loading_method.bucket_name",
+ fieldKey: "bucket_name",
+ isRequired: true,
+ isSecret: false,
+ multiline: false,
+ type: "string",
+ },
+ {
+ description:
+ 'Enter your Google Cloud service account key in the JSON format with read/write access to your Cloud Storage staging bucket',
+ order: 3,
+ title: "Google Application Credentials",
+ _type: "formItem",
+ path: "loading_method.credentials_json",
+ fieldKey: "credentials_json",
+ isRequired: true,
+ isSecret: true,
+ multiline: true,
+ type: "string",
+ },
+ ],
+ isRequired: false,
+ },
+ "Azure Blob Storage Staging": {
+ description:
+ "Recommended for large production workloads for better speed and scalability.",
+ title: "Azure Blob Storage Staging",
+ _type: "formGroup",
+ jsonSchema: {
+ description:
+ "Recommended for large production workloads for better speed and scalability.",
+ properties: {
+ azure_blob_storage_account_name: {
+ description: "Enter your Azure Blob Storage account name",
+ examples: ["airbyte5storage"],
+ order: 2,
+ title:
+ 'Azure Blob Storage account name ',
+ type: "string",
+ },
+ azure_blob_storage_container_name: {
+ description:
+ 'Enter your Azure Blob Storage container name ',
+ examples: ["airbytetestcontainername"],
+ order: 3,
+ title: "Azure Blob Storage Container Name",
+ type: "string",
+ },
+ azure_blob_storage_endpoint_domain_name: {
+ default: "blob.core.windows.net",
+ description:
+ 'Enter the Azure Blob Storage endpoint domain name ',
+ examples: ["blob.core.windows.net"],
+ order: 1,
+ title: "Azure Blob Storage Endpoint",
+ type: "string",
+ },
+ azure_blob_storage_sas_token: {
+ airbyte_secret: true,
+ description:
+ 'Enter the Shared access signature (SAS) token to grant Snowflake limited access to objects in your Azure Blob Storage account',
+ examples: [
+ "?sv=2016-05-31&ss=b&srt=sco&sp=rwdl&se=2018-06-27T10:05:50Z&st=2017-06-27T02:05:50Z&spr=https,http&sig=bgqQwoXwxzuD2GJfagRg7VOS8hzNr3QLT7rhS8OFRLQ%3D",
+ ],
+ order: 4,
+ title: "SAS Token",
+ type: "string",
+ },
+ method: {
+ default: "Azure Blob Staging",
+ description: "",
+ enum: ["Azure Blob Staging"],
+ order: 0,
+ title: "",
+ type: "string",
+ },
+ },
+ required: [
+ "method",
+ "azure_blob_storage_account_name",
+ "azure_blob_storage_container_name",
+ "azure_blob_storage_sas_token",
+ ],
+ title: "Azure Blob Storage Staging",
+ type: "object",
+ },
+ path: "loading_method",
+ fieldKey: "loading_method",
+ properties: [
+ {
+ default: "Azure Blob Staging",
+ description: "",
+ order: 0,
+ title: "",
+ const: "Azure Blob Staging",
+ _type: "formItem",
+ path: "loading_method.method",
+ fieldKey: "method",
+ isRequired: true,
+ isSecret: false,
+ multiline: false,
+ type: "string",
+ },
+ {
+ default: "blob.core.windows.net",
+ description:
+ 'Enter the Azure Blob Storage endpoint domain name ',
+ examples: ["blob.core.windows.net"],
+ order: 1,
+ title: "Azure Blob Storage Endpoint",
+ _type: "formItem",
+ path: "loading_method.azure_blob_storage_endpoint_domain_name",
+ fieldKey: "azure_blob_storage_endpoint_domain_name",
+ isRequired: false,
+ isSecret: false,
+ multiline: false,
+ type: "string",
+ },
+ {
+ description: "Enter your Azure Blob Storage account name",
+ examples: ["airbyte5storage"],
+ order: 2,
+ title:
+ 'Azure Blob Storage account name ',
+ _type: "formItem",
+ path: "loading_method.azure_blob_storage_account_name",
+ fieldKey: "azure_blob_storage_account_name",
+ isRequired: true,
+ isSecret: false,
+ multiline: false,
+ type: "string",
+ },
+ {
+ description:
+ 'Enter your Azure Blob Storage container name ',
+ examples: ["airbytetestcontainername"],
+ order: 3,
+ title: "Azure Blob Storage Container Name",
+ _type: "formItem",
+ path: "loading_method.azure_blob_storage_container_name",
+ fieldKey: "azure_blob_storage_container_name",
+ isRequired: true,
+ isSecret: false,
+ multiline: false,
+ type: "string",
+ },
+ {
+ description:
+ 'Enter the Shared access signature (SAS) token to grant Snowflake limited access to objects in your Azure Blob Storage account',
+ examples: [
+ "?sv=2016-05-31&ss=b&srt=sco&sp=rwdl&se=2018-06-27T10:05:50Z&st=2017-06-27T02:05:50Z&spr=https,http&sig=bgqQwoXwxzuD2GJfagRg7VOS8hzNr3QLT7rhS8OFRLQ%3D",
+ ],
+ order: 4,
+ title: "SAS Token",
+ _type: "formItem",
+ path: "loading_method.azure_blob_storage_sas_token",
+ fieldKey: "azure_blob_storage_sas_token",
+ isRequired: true,
+ isSecret: true,
+ multiline: false,
+ type: "string",
+ },
+ ],
+ isRequired: false,
+ },
+ },
+ isRequired: false,
+ },
+ ],
+ fieldKey: "",
+ isRequired: false,
+ path: "",
+ };
+
+ const initialValues = {
+ "loading_method.method": "Internal Staging",
+ configuration: {
+ loading_method: {
+ method: "Internal Staging",
+ },
+ },
+ };
+
+ const selectedConditionMap = pickSelectedConditionMap(
+ formTree,
+ initialValues
+ );
+
+ expect(selectedConditionMap).toEqual({
+ loading_method: {
+ selectedItem: "[Recommended] Internal Staging",
+ },
+ });
+});
+
+test("should find multiple selected condition map", () => {
+ const formTree: AirbyteFormTree = {
+ title: "Snowflake Destination Spec",
+ _type: "formGroup",
+ jsonSchema: {
+ $schema: "http://json-schema.org/draft-07/schema#",
+ additionalProperties: true,
+ properties: {
+ credentials: {
+ description: "",
+ oneOf: [
+ {
+ properties: {
+ access_token: {
+ description: "Enter you application's Access Token",
+ title: "Access Token",
+ type: "string",
+ },
+ auth_type: {
+ const: "OAuth2.0",
+ default: "OAuth2.0",
+ enum: ["OAuth2.0"],
+ type: "string",
+ },
+ client_id: {
+ description: "Enter your application's Client ID",
+ title: "Client ID",
+ type: "string",
+ },
+ client_secret: {
+ description: "Enter your application's Client secret",
+ title: "Client Secret",
+ type: "string",
+ },
+ refresh_token: {
+ description: "Enter your application's Refresh Token",
+ title: "Refresh Token",
+ type: "string",
+ },
+ },
+ required: ["access_token", "refresh_token"],
+ title: "OAuth2.0",
+ type: "object",
+ },
+ {
+ properties: {
+ password: {
+ description:
+ "Enter the password associated with the username.",
+ title: "Password",
+ type: "string",
+ },
+ },
+ required: ["password"],
+ title: "Username and Password",
+ type: "object",
+ },
+ ],
+ order: 6,
+ title: "Authorization Method",
+ type: "object",
+ },
+ loading_method: {
+ description: "Select a data staging method",
+ oneOf: [
+ {
+ description: "Select another option",
+ properties: {
+ method: {
+ default: "Standard",
+ description: "",
+ enum: ["Standard"],
+ title: "",
+ type: "string",
+ },
+ },
+ required: ["method"],
+ title: "Select another option",
+ },
+ {
+ description:
+ "Recommended for large production workloads for better speed and scalability.",
+ properties: {
+ method: {
+ default: "Internal Staging",
+ description: "",
+ enum: ["Internal Staging"],
+ title: "",
+ type: "string",
+ },
+ },
+ required: ["method"],
+ title: "[Recommended] Internal Staging",
+ },
+ {
+ description:
+ "Recommended for large production workloads for better speed and scalability.",
+ properties: {
+ access_key_id: {
+ description:
+ 'Enter your AWS access key ID . Airbyte requires Read and Write permissions on your S3 bucket ',
+ title: "AWS access key ID",
+ type: "string",
+ },
+ encryption: {
+ default: {
+ encryption_type: "none",
+ },
+ description:
+ "Choose a data encryption method for the staging data",
+ oneOf: [
+ {
+ description: "Staging data will be stored in plaintext.",
+ properties: {
+ encryption_type: {
+ const: "none",
+ default: "none",
+ enum: ["none"],
+ type: "string",
+ },
+ },
+ required: ["encryption_type"],
+ title: "No encryption",
+ type: "object",
+ },
+ {
+ description:
+ "Staging data will be encrypted using AES-CBC envelope encryption.",
+ properties: {
+ encryption_type: {
+ const: "aes_cbc_envelope",
+ default: "aes_cbc_envelope",
+ enum: ["aes_cbc_envelope"],
+ type: "string",
+ },
+ key_encrypting_key: {
+ description:
+ "The key, base64-encoded. Must be either 128, 192, or 256 bits. Leave blank to have Airbyte generate an ephemeral key for each sync.",
+ title: "Key",
+ type: "string",
+ },
+ },
+ required: ["encryption_type"],
+ title: "AES-CBC envelope encryption",
+ type: "object",
+ },
+ ],
+ title: "Encryption",
+ type: "object",
+ },
+ file_name_pattern: {
+ description:
+ "The pattern allows you to set the file-name format for the S3 staging file(s)",
+ examples: [
+ "{date}",
+ "{date:yyyy_MM}",
+ "{timestamp}",
+ "{part_number}",
+ "{sync_id}",
+ ],
+ title: "S3 Filename pattern (Optional)",
+ type: "string",
+ },
+ method: {
+ default: "S3 Staging",
+ description: "",
+ enum: ["S3 Staging"],
+ title: "",
+ type: "string",
+ },
+ purge_staging_data: {
+ default: true,
+ description:
+ "Toggle to delete staging files from the S3 bucket after a successful sync",
+ title: "Purge Staging Files and Tables",
+ type: "boolean",
+ },
+ s3_bucket_name: {
+ description: "Enter your S3 bucket name",
+ examples: ["airbyte.staging"],
+ title: "S3 Bucket Name",
+ type: "string",
+ },
+ s3_bucket_region: {
+ default: "",
+ description: "Enter the region where your S3 bucket resides",
+ enum: [
+ "",
+ "us-east-1",
+ "us-east-2",
+ "us-west-1",
+ "us-west-2",
+ "af-south-1",
+ "ap-east-1",
+ "ap-south-1",
+ "ap-northeast-1",
+ "ap-northeast-2",
+ "ap-northeast-3",
+ "ap-southeast-1",
+ "ap-southeast-2",
+ "ca-central-1",
+ "cn-north-1",
+ "cn-northwest-1",
+ "eu-central-1",
+ "eu-west-1",
+ "eu-west-2",
+ "eu-west-3",
+ "eu-south-1",
+ "eu-north-1",
+ "sa-east-1",
+ "me-south-1",
+ ],
+ title: "S3 Bucket Region",
+ type: "string",
+ },
+ secret_access_key: {
+ description:
+ 'Enter your AWS secret access key ',
+ title: "AWS secret access key",
+ type: "string",
+ },
+ },
+ required: [
+ "method",
+ "s3_bucket_name",
+ "access_key_id",
+ "secret_access_key",
+ ],
+ title: "AWS S3 Staging",
+ },
+ {
+ description:
+ "Recommended for large production workloads for better speed and scalability.",
+ properties: {
+ bucket_name: {
+ description:
+ 'Enter the Cloud Storage bucket name ',
+ examples: ["airbyte-staging"],
+ title: "Cloud Storage bucket name",
+ type: "string",
+ },
+ credentials_json: {
+ description:
+ 'Enter your Google Cloud service account key in the JSON format with read/write access to your Cloud Storage staging bucket',
+ title: "Google Application Credentials",
+ type: "string",
+ },
+ method: {
+ default: "GCS Staging",
+ description: "",
+ enum: ["GCS Staging"],
+ title: "",
+ type: "string",
+ },
+ project_id: {
+ description:
+ 'Enter the Google Cloud project ID ',
+ examples: ["my-project"],
+ title: "Google Cloud project ID",
+ type: "string",
+ },
+ },
+ required: [
+ "method",
+ "project_id",
+ "bucket_name",
+ "credentials_json",
+ ],
+ title: "Google Cloud Storage Staging",
+ },
+ {
+ description:
+ "Recommended for large production workloads for better speed and scalability.",
+ properties: {
+ azure_blob_storage_account_name: {
+ description: "Enter your Azure Blob Storage account name",
+ examples: ["airbyte5storage"],
+ title:
+ 'Azure Blob Storage account name ',
+ type: "string",
+ },
+ azure_blob_storage_container_name: {
+ description:
+ 'Enter your Azure Blob Storage container name ',
+ examples: ["airbytetestcontainername"],
+ title: "Azure Blob Storage Container Name",
+ type: "string",
+ },
+ azure_blob_storage_endpoint_domain_name: {
+ default: "blob.core.windows.net",
+ description:
+ 'Enter the Azure Blob Storage endpoint domain name ',
+ examples: ["blob.core.windows.net"],
+ title: "Azure Blob Storage Endpoint",
+ type: "string",
+ },
+ azure_blob_storage_sas_token: {
+ description:
+ 'Enter the Shared access signature (SAS) token to grant Snowflake limited access to objects in your Azure Blob Storage account',
+ examples: [
+ "?sv=2016-05-31&ss=b&srt=sco&sp=rwdl&se=2018-06-27T10:05:50Z&st=2017-06-27T02:05:50Z&spr=https,http&sig=bgqQwoXwxzuD2GJfagRg7VOS8hzNr3QLT7rhS8OFRLQ%3D",
+ ],
+ title: "SAS Token",
+ type: "string",
+ },
+ method: {
+ default: "Azure Blob Staging",
+ description: "",
+ enum: ["Azure Blob Staging"],
+ title: "",
+ type: "string",
+ },
+ },
+ required: [
+ "method",
+ "azure_blob_storage_account_name",
+ "azure_blob_storage_container_name",
+ "azure_blob_storage_sas_token",
+ ],
+ title: "Azure Blob Storage Staging",
+ },
+ ],
+ order: 8,
+ title: "Data Staging Method",
+ type: "object",
+ },
+ username: {
+ description:
+ "Enter the name of the user you want to use to access the database",
+ examples: ["AIRBYTE_USER"],
+ order: 5,
+ title: "Username",
+ type: "string",
+ },
+ },
+ required: ["username"],
+ title: "Snowflake Destination Spec",
+ type: "object",
+ },
+ properties: [
+ {
+ description:
+ "Enter the name of the user you want to use to access the database",
+ examples: ["AIRBYTE_USER"],
+ order: 5,
+ title: "Username",
+ _type: "formItem",
+ path: "username",
+ fieldKey: "username",
+ isRequired: true,
+ isSecret: false,
+ multiline: false,
+ type: "string",
+ },
+ {
+ description: "",
+ order: 6,
+ title: "Authorization Method",
+ _type: "formCondition",
+ path: "credentials",
+ fieldKey: "credentials",
+ conditions: {
+ "OAuth2.0": {
+ order: 0,
+ title: "OAuth2.0",
+ _type: "formGroup",
+ jsonSchema: {
+ order: 0,
+ properties: {
+ access_token: {
+ airbyte_secret: true,
+ description: "Enter you application's Access Token",
+ title: "Access Token",
+ type: "string",
+ },
+ auth_type: {
+ const: "OAuth2.0",
+ default: "OAuth2.0",
+ enum: ["OAuth2.0"],
+ order: 0,
+ type: "string",
+ },
+ client_id: {
+ airbyte_secret: true,
+ description: "Enter your application's Client ID",
+ title: "Client ID",
+ type: "string",
+ },
+ client_secret: {
+ airbyte_secret: true,
+ description: "Enter your application's Client secret",
+ title: "Client Secret",
+ type: "string",
+ },
+ refresh_token: {
+ airbyte_secret: true,
+ description: "Enter your application's Refresh Token",
+ title: "Refresh Token",
+ type: "string",
+ },
+ },
+ required: ["access_token", "refresh_token"],
+ title: "OAuth2.0",
+ type: "object",
+ },
+ path: "credentials",
+ fieldKey: "credentials",
+ properties: [
+ {
+ const: "OAuth2.0",
+ default: "OAuth2.0",
+ order: 0,
+ _type: "formItem",
+ path: "credentials.auth_type",
+ fieldKey: "auth_type",
+ isRequired: false,
+ isSecret: false,
+ multiline: false,
+ type: "string",
+ },
+ {
+ description: "Enter you application's Access Token",
+ title: "Access Token",
+ _type: "formItem",
+ path: "credentials.access_token",
+ fieldKey: "access_token",
+ isRequired: true,
+ isSecret: true,
+ multiline: false,
+ type: "string",
+ },
+ {
+ description: "Enter your application's Client ID",
+ title: "Client ID",
+ _type: "formItem",
+ path: "credentials.client_id",
+ fieldKey: "client_id",
+ isRequired: false,
+ isSecret: true,
+ multiline: false,
+ type: "string",
+ },
+ {
+ description: "Enter your application's Client secret",
+ title: "Client Secret",
+ _type: "formItem",
+ path: "credentials.client_secret",
+ fieldKey: "client_secret",
+ isRequired: false,
+ isSecret: true,
+ multiline: false,
+ type: "string",
+ },
+ {
+ description: "Enter your application's Refresh Token",
+ title: "Refresh Token",
+ _type: "formItem",
+ path: "credentials.refresh_token",
+ fieldKey: "refresh_token",
+ isRequired: true,
+ isSecret: true,
+ multiline: false,
+ type: "string",
+ },
+ ],
+ isRequired: false,
+ },
+ "Username and Password": {
+ order: 1,
+ title: "Username and Password",
+ _type: "formGroup",
+ jsonSchema: {
+ order: 1,
+ properties: {
+ password: {
+ airbyte_secret: true,
+ description:
+ "Enter the password associated with the username.",
+ order: 1,
+ title: "Password",
+ type: "string",
+ },
+ },
+ required: ["password"],
+ title: "Username and Password",
+ type: "object",
+ },
+ path: "credentials",
+ fieldKey: "credentials",
+ properties: [
+ {
+ description: "Enter the password associated with the username.",
+ order: 1,
+ title: "Password",
+ _type: "formItem",
+ path: "credentials.password",
+ fieldKey: "password",
+ isRequired: true,
+ isSecret: true,
+ multiline: false,
+ type: "string",
+ },
+ ],
+ isRequired: false,
+ },
+ },
+ isRequired: false,
+ },
+ {
+ description: "Select a data staging method",
+ order: 8,
+ title: "Data Staging Method",
+ _type: "formCondition",
+ path: "loading_method",
+ fieldKey: "loading_method",
+ conditions: {
+ "Select another option": {
+ description: "Select another option",
+ title: "Select another option",
+ _type: "formGroup",
+ jsonSchema: {
+ description: "Select another option",
+ properties: {
+ method: {
+ default: "Standard",
+ description: "",
+ enum: ["Standard"],
+ title: "",
+ type: "string",
+ },
+ },
+ required: ["method"],
+ title: "Select another option",
+ type: "object",
+ },
+ path: "loading_method",
+ fieldKey: "loading_method",
+ properties: [
+ {
+ default: "Standard",
+ description: "",
+ title: "",
+ const: "Standard",
+ _type: "formItem",
+ path: "loading_method.method",
+ fieldKey: "method",
+ isRequired: true,
+ isSecret: false,
+ multiline: false,
+ type: "string",
+ },
+ ],
+ isRequired: false,
+ },
+ "[Recommended] Internal Staging": {
+ description:
+ "Recommended for large production workloads for better speed and scalability.",
+ title: "[Recommended] Internal Staging",
+ _type: "formGroup",
+ jsonSchema: {
+ description:
+ "Recommended for large production workloads for better speed and scalability.",
+ properties: {
+ method: {
+ default: "Internal Staging",
+ description: "",
+ enum: ["Internal Staging"],
+ title: "",
+ type: "string",
+ },
+ },
+ required: ["method"],
+ title: "[Recommended] Internal Staging",
+ type: "object",
+ },
+ path: "loading_method",
+ fieldKey: "loading_method",
+ properties: [
+ {
+ default: "Internal Staging",
+ description: "",
+ title: "",
+ const: "Internal Staging",
+ _type: "formItem",
+ path: "loading_method.method",
+ fieldKey: "method",
+ isRequired: true,
+ isSecret: false,
+ multiline: false,
+ type: "string",
+ },
+ ],
+ isRequired: false,
+ },
+ "AWS S3 Staging": {
+ description:
+ "Recommended for large production workloads for better speed and scalability.",
+ title: "AWS S3 Staging",
+ _type: "formGroup",
+ jsonSchema: {
+ description:
+ "Recommended for large production workloads for better speed and scalability.",
+ properties: {
+ access_key_id: {
+ airbyte_secret: true,
+ description:
+ 'Enter your AWS access key ID . Airbyte requires Read and Write permissions on your S3 bucket ',
+ order: 3,
+ title: "AWS access key ID",
+ type: "string",
+ },
+ encryption: {
+ default: {
+ encryption_type: "none",
+ },
+ description:
+ "Choose a data encryption method for the staging data",
+ oneOf: [
+ {
+ description: "Staging data will be stored in plaintext.",
+ properties: {
+ encryption_type: {
+ const: "none",
+ default: "none",
+ enum: ["none"],
+ type: "string",
+ },
+ },
+ required: ["encryption_type"],
+ title: "No encryption",
+ type: "object",
+ },
+ {
+ description:
+ "Staging data will be encrypted using AES-CBC envelope encryption.",
+ properties: {
+ encryption_type: {
+ const: "aes_cbc_envelope",
+ default: "aes_cbc_envelope",
+ enum: ["aes_cbc_envelope"],
+ type: "string",
+ },
+ key_encrypting_key: {
+ description:
+ "The key, base64-encoded. Must be either 128, 192, or 256 bits. Leave blank to have Airbyte generate an ephemeral key for each sync.",
+ title: "Key",
+ type: "string",
+ },
+ },
+ required: ["encryption_type"],
+ title: "AES-CBC envelope encryption",
+ type: "object",
+ },
+ ],
+ order: 6,
+ title: "Encryption",
+ type: "object",
+ },
+ file_name_pattern: {
+ description:
+ "The pattern allows you to set the file-name format for the S3 staging file(s)",
+ examples: [
+ "{date}",
+ "{date:yyyy_MM}",
+ "{timestamp}",
+ "{part_number}",
+ "{sync_id}",
+ ],
+ order: 7,
+ title: "S3 Filename pattern (Optional)",
+ type: "string",
+ },
+ method: {
+ default: "S3 Staging",
+ description: "",
+ enum: ["S3 Staging"],
+ order: 0,
+ title: "",
+ type: "string",
+ },
+ purge_staging_data: {
+ default: true,
+ description:
+ "Toggle to delete staging files from the S3 bucket after a successful sync",
+ order: 5,
+ title: "Purge Staging Files and Tables",
+ type: "boolean",
+ },
+ s3_bucket_name: {
+ description: "Enter your S3 bucket name",
+ examples: ["airbyte.staging"],
+ order: 1,
+ title: "S3 Bucket Name",
+ type: "string",
+ },
+ s3_bucket_region: {
+ default: "",
+ description: "Enter the region where your S3 bucket resides",
+ enum: [
+ "",
+ "us-east-1",
+ "us-east-2",
+ "us-west-1",
+ "us-west-2",
+ "af-south-1",
+ "ap-east-1",
+ "ap-south-1",
+ "ap-northeast-1",
+ "ap-northeast-2",
+ "ap-northeast-3",
+ "ap-southeast-1",
+ "ap-southeast-2",
+ "ca-central-1",
+ "cn-north-1",
+ "cn-northwest-1",
+ "eu-central-1",
+ "eu-west-1",
+ "eu-west-2",
+ "eu-west-3",
+ "eu-south-1",
+ "eu-north-1",
+ "sa-east-1",
+ "me-south-1",
+ ],
+ order: 2,
+ title: "S3 Bucket Region",
+ type: "string",
+ },
+ secret_access_key: {
+ airbyte_secret: true,
+ description:
+ 'Enter your AWS secret access key ',
+ order: 4,
+ title: "AWS secret access key",
+ type: "string",
+ },
+ },
+ required: [
+ "method",
+ "s3_bucket_name",
+ "access_key_id",
+ "secret_access_key",
+ ],
+ title: "AWS S3 Staging",
+ type: "object",
+ },
+ path: "loading_method",
+ fieldKey: "loading_method",
+ properties: [
+ {
+ default: "S3 Staging",
+ description: "",
+ order: 0,
+ title: "",
+ const: "S3 Staging",
+ _type: "formItem",
+ path: "loading_method.method",
+ fieldKey: "method",
+ isRequired: true,
+ isSecret: false,
+ multiline: false,
+ type: "string",
+ },
+ {
+ description: "Enter your S3 bucket name",
+ examples: ["airbyte.staging"],
+ order: 1,
+ title: "S3 Bucket Name",
+ _type: "formItem",
+ path: "loading_method.s3_bucket_name",
+ fieldKey: "s3_bucket_name",
+ isRequired: true,
+ isSecret: false,
+ multiline: false,
+ type: "string",
+ },
+ {
+ default: "",
+ description: "Enter the region where your S3 bucket resides",
+ order: 2,
+ title: "S3 Bucket Region",
+ enum: [
+ "",
+ "us-east-1",
+ "us-east-2",
+ "us-west-1",
+ "us-west-2",
+ "af-south-1",
+ "ap-east-1",
+ "ap-south-1",
+ "ap-northeast-1",
+ "ap-northeast-2",
+ "ap-northeast-3",
+ "ap-southeast-1",
+ "ap-southeast-2",
+ "ca-central-1",
+ "cn-north-1",
+ "cn-northwest-1",
+ "eu-central-1",
+ "eu-west-1",
+ "eu-west-2",
+ "eu-west-3",
+ "eu-south-1",
+ "eu-north-1",
+ "sa-east-1",
+ "me-south-1",
+ ],
+ _type: "formItem",
+ path: "loading_method.s3_bucket_region",
+ fieldKey: "s3_bucket_region",
+ isRequired: false,
+ isSecret: false,
+ multiline: false,
+ type: "string",
+ },
+ {
+ description:
+ 'Enter your AWS access key ID . Airbyte requires Read and Write permissions on your S3 bucket ',
+ order: 3,
+ title: "AWS access key ID",
+ _type: "formItem",
+ path: "loading_method.access_key_id",
+ fieldKey: "access_key_id",
+ isRequired: true,
+ isSecret: true,
+ multiline: false,
+ type: "string",
+ },
+ {
+ description:
+ 'Enter your AWS secret access key ',
+ order: 4,
+ title: "AWS secret access key",
+ _type: "formItem",
+ path: "loading_method.secret_access_key",
+ fieldKey: "secret_access_key",
+ isRequired: true,
+ isSecret: true,
+ multiline: false,
+ type: "string",
+ },
+ {
+ default: true,
+ description:
+ "Toggle to delete staging files from the S3 bucket after a successful sync",
+ order: 5,
+ title: "Purge Staging Files and Tables",
+ _type: "formItem",
+ path: "loading_method.purge_staging_data",
+ fieldKey: "purge_staging_data",
+ isRequired: false,
+ isSecret: false,
+ multiline: false,
+ type: "boolean",
+ },
+ {
+ description:
+ "Choose a data encryption method for the staging data",
+ order: 6,
+ title: "Encryption",
+ _type: "formCondition",
+ path: "loading_method.encryption",
+ fieldKey: "encryption",
+ conditions: {
+ "No encryption": {
+ description: "Staging data will be stored in plaintext.",
+ title: "No encryption",
+ _type: "formGroup",
+ jsonSchema: {
+ description: "Staging data will be stored in plaintext.",
+ properties: {
+ encryption_type: {
+ const: "none",
+ default: "none",
+ enum: ["none"],
+ type: "string",
+ },
+ },
+ required: ["encryption_type"],
+ title: "No encryption",
+ type: "object",
+ },
+ path: "loading_method.encryption",
+ fieldKey: "encryption",
+ properties: [
+ {
+ const: "none",
+ default: "none",
+ _type: "formItem",
+ path: "loading_method.encryption.encryption_type",
+ fieldKey: "encryption_type",
+ isRequired: true,
+ isSecret: false,
+ multiline: false,
+ type: "string",
+ },
+ ],
+ isRequired: false,
+ },
+ "AES-CBC envelope encryption": {
+ description:
+ "Staging data will be encrypted using AES-CBC envelope encryption.",
+ title: "AES-CBC envelope encryption",
+ _type: "formGroup",
+ jsonSchema: {
+ description:
+ "Staging data will be encrypted using AES-CBC envelope encryption.",
+ properties: {
+ encryption_type: {
+ const: "aes_cbc_envelope",
+ default: "aes_cbc_envelope",
+ enum: ["aes_cbc_envelope"],
+ type: "string",
+ },
+ key_encrypting_key: {
+ airbyte_secret: true,
+ description:
+ "The key, base64-encoded. Must be either 128, 192, or 256 bits. Leave blank to have Airbyte generate an ephemeral key for each sync.",
+ title: "Key",
+ type: "string",
+ },
+ },
+ required: ["encryption_type"],
+ title: "AES-CBC envelope encryption",
+ type: "object",
+ },
+ path: "loading_method.encryption",
+ fieldKey: "encryption",
+ properties: [
+ {
+ const: "aes_cbc_envelope",
+ default: "aes_cbc_envelope",
+ _type: "formItem",
+ path: "loading_method.encryption.encryption_type",
+ fieldKey: "encryption_type",
+ isRequired: true,
+ isSecret: false,
+ multiline: false,
+ type: "string",
+ },
+ {
+ description:
+ "The key, base64-encoded. Must be either 128, 192, or 256 bits. Leave blank to have Airbyte generate an ephemeral key for each sync.",
+ title: "Key",
+ _type: "formItem",
+ path: "loading_method.encryption.key_encrypting_key",
+ fieldKey: "key_encrypting_key",
+ isRequired: false,
+ isSecret: true,
+ multiline: false,
+ type: "string",
+ },
+ ],
+ isRequired: false,
+ },
+ },
+ isRequired: false,
+ },
+ {
+ description:
+ "The pattern allows you to set the file-name format for the S3 staging file(s)",
+ examples: [
+ "{date}",
+ "{date:yyyy_MM}",
+ "{timestamp}",
+ "{part_number}",
+ "{sync_id}",
+ ],
+ order: 7,
+ title: "S3 Filename pattern (Optional)",
+ _type: "formItem",
+ path: "loading_method.file_name_pattern",
+ fieldKey: "file_name_pattern",
+ isRequired: false,
+ isSecret: false,
+ multiline: false,
+ type: "string",
+ },
+ ],
+ isRequired: false,
+ },
+ "Google Cloud Storage Staging": {
+ description:
+ "Recommended for large production workloads for better speed and scalability.",
+ title: "Google Cloud Storage Staging",
+ _type: "formGroup",
+ jsonSchema: {
+ description:
+ "Recommended for large production workloads for better speed and scalability.",
+ properties: {
+ bucket_name: {
+ description:
+ 'Enter the Cloud Storage bucket name ',
+ examples: ["airbyte-staging"],
+ order: 2,
+ title: "Cloud Storage bucket name",
+ type: "string",
+ },
+ credentials_json: {
+ airbyte_secret: true,
+ description:
+ 'Enter your Google Cloud service account key in the JSON format with read/write access to your Cloud Storage staging bucket',
+ multiline: true,
+ order: 3,
+ title: "Google Application Credentials",
+ type: "string",
+ },
+ method: {
+ default: "GCS Staging",
+ description: "",
+ enum: ["GCS Staging"],
+ order: 0,
+ title: "",
+ type: "string",
+ },
+ project_id: {
+ description:
+ 'Enter the Google Cloud project ID ',
+ examples: ["my-project"],
+ order: 1,
+ title: "Google Cloud project ID",
+ type: "string",
+ },
+ },
+ required: [
+ "method",
+ "project_id",
+ "bucket_name",
+ "credentials_json",
+ ],
+ title: "Google Cloud Storage Staging",
+ type: "object",
+ },
+ path: "loading_method",
+ fieldKey: "loading_method",
+ properties: [
+ {
+ default: "GCS Staging",
+ description: "",
+ order: 0,
+ title: "",
+ const: "GCS Staging",
+ _type: "formItem",
+ path: "loading_method.method",
+ fieldKey: "method",
+ isRequired: true,
+ isSecret: false,
+ multiline: false,
+ type: "string",
+ },
+ {
+ description:
+ 'Enter the Google Cloud project ID ',
+ examples: ["my-project"],
+ order: 1,
+ title: "Google Cloud project ID",
+ _type: "formItem",
+ path: "loading_method.project_id",
+ fieldKey: "project_id",
+ isRequired: true,
+ isSecret: false,
+ multiline: false,
+ type: "string",
+ },
+ {
+ description:
+ 'Enter the Cloud Storage bucket name ',
+ examples: ["airbyte-staging"],
+ order: 2,
+ title: "Cloud Storage bucket name",
+ _type: "formItem",
+ path: "loading_method.bucket_name",
+ fieldKey: "bucket_name",
+ isRequired: true,
+ isSecret: false,
+ multiline: false,
+ type: "string",
+ },
+ {
+ description:
+ 'Enter your Google Cloud service account key in the JSON format with read/write access to your Cloud Storage staging bucket',
+ order: 3,
+ title: "Google Application Credentials",
+ _type: "formItem",
+ path: "loading_method.credentials_json",
+ fieldKey: "credentials_json",
+ isRequired: true,
+ isSecret: true,
+ multiline: true,
+ type: "string",
+ },
+ ],
+ isRequired: false,
+ },
+ "Azure Blob Storage Staging": {
+ description:
+ "Recommended for large production workloads for better speed and scalability.",
+ title: "Azure Blob Storage Staging",
+ _type: "formGroup",
+ jsonSchema: {
+ description:
+ "Recommended for large production workloads for better speed and scalability.",
+ properties: {
+ azure_blob_storage_account_name: {
+ description: "Enter your Azure Blob Storage account name",
+ examples: ["airbyte5storage"],
+ order: 2,
+ title:
+ 'Azure Blob Storage account name ',
+ type: "string",
+ },
+ azure_blob_storage_container_name: {
+ description:
+ 'Enter your Azure Blob Storage container name ',
+ examples: ["airbytetestcontainername"],
+ order: 3,
+ title: "Azure Blob Storage Container Name",
+ type: "string",
+ },
+ azure_blob_storage_endpoint_domain_name: {
+ default: "blob.core.windows.net",
+ description:
+ 'Enter the Azure Blob Storage endpoint domain name ',
+ examples: ["blob.core.windows.net"],
+ order: 1,
+ title: "Azure Blob Storage Endpoint",
+ type: "string",
+ },
+ azure_blob_storage_sas_token: {
+ airbyte_secret: true,
+ description:
+ 'Enter the Shared access signature (SAS) token to grant Snowflake limited access to objects in your Azure Blob Storage account',
+ examples: [
+ "?sv=2016-05-31&ss=b&srt=sco&sp=rwdl&se=2018-06-27T10:05:50Z&st=2017-06-27T02:05:50Z&spr=https,http&sig=bgqQwoXwxzuD2GJfagRg7VOS8hzNr3QLT7rhS8OFRLQ%3D",
+ ],
+ order: 4,
+ title: "SAS Token",
+ type: "string",
+ },
+ method: {
+ default: "Azure Blob Staging",
+ description: "",
+ enum: ["Azure Blob Staging"],
+ order: 0,
+ title: "",
+ type: "string",
+ },
+ },
+ required: [
+ "method",
+ "azure_blob_storage_account_name",
+ "azure_blob_storage_container_name",
+ "azure_blob_storage_sas_token",
+ ],
+ title: "Azure Blob Storage Staging",
+ type: "object",
+ },
+ path: "loading_method",
+ fieldKey: "loading_method",
+ properties: [
+ {
+ default: "Azure Blob Staging",
+ description: "",
+ order: 0,
+ title: "",
+ const: "Azure Blob Staging",
+ _type: "formItem",
+ path: "loading_method.method",
+ fieldKey: "method",
+ isRequired: true,
+ isSecret: false,
+ multiline: false,
+ type: "string",
+ },
+ {
+ default: "blob.core.windows.net",
+ description:
+ 'Enter the Azure Blob Storage endpoint domain name ',
+ examples: ["blob.core.windows.net"],
+ order: 1,
+ title: "Azure Blob Storage Endpoint",
+ _type: "formItem",
+ path: "loading_method.azure_blob_storage_endpoint_domain_name",
+ fieldKey: "azure_blob_storage_endpoint_domain_name",
+ isRequired: false,
+ isSecret: false,
+ multiline: false,
+ type: "string",
+ },
+ {
+ description: "Enter your Azure Blob Storage account name",
+ examples: ["airbyte5storage"],
+ order: 2,
+ title:
+ 'Azure Blob Storage account name ',
+ _type: "formItem",
+ path: "loading_method.azure_blob_storage_account_name",
+ fieldKey: "azure_blob_storage_account_name",
+ isRequired: true,
+ isSecret: false,
+ multiline: false,
+ type: "string",
+ },
+ {
+ description:
+ 'Enter your Azure Blob Storage container name ',
+ examples: ["airbytetestcontainername"],
+ order: 3,
+ title: "Azure Blob Storage Container Name",
+ _type: "formItem",
+ path: "loading_method.azure_blob_storage_container_name",
+ fieldKey: "azure_blob_storage_container_name",
+ isRequired: true,
+ isSecret: false,
+ multiline: false,
+ type: "string",
+ },
+ {
+ description:
+ 'Enter the Shared access signature (SAS) token to grant Snowflake limited access to objects in your Azure Blob Storage account',
+ examples: [
+ "?sv=2016-05-31&ss=b&srt=sco&sp=rwdl&se=2018-06-27T10:05:50Z&st=2017-06-27T02:05:50Z&spr=https,http&sig=bgqQwoXwxzuD2GJfagRg7VOS8hzNr3QLT7rhS8OFRLQ%3D",
+ ],
+ order: 4,
+ title: "SAS Token",
+ _type: "formItem",
+ path: "loading_method.azure_blob_storage_sas_token",
+ fieldKey: "azure_blob_storage_sas_token",
+ isRequired: true,
+ isSecret: true,
+ multiline: false,
+ type: "string",
+ },
+ ],
+ isRequired: false,
+ },
+ },
+ isRequired: false,
+ },
+ ],
+ fieldKey: "",
+ isRequired: false,
+ path: "",
+ };
+
+ const initialValues = {
+ "loading_method.method": "Internal Staging",
+ "credentials.auth_type": "OAuth2.0",
+ configuration: {
+ loading_method: {
+ method: "Internal Staging",
+ },
+ credentials: {
+ auth_type: "OAuth2.0",
+ },
+ },
+ };
+
+ const selectedConditionMap = pickSelectedConditionMap(
+ formTree,
+ initialValues
+ );
+
+ expect(selectedConditionMap).toEqual({
+ loading_method: {
+ selectedItem: "[Recommended] Internal Staging",
+ },
+ credentials: {
+ selectedItem: "OAuth2.0",
+ },
+ });
+});
diff --git a/src/lib/airbytes/pickSelectedConditionMap.ts b/src/lib/airbytes/pickSelectedConditionMap.ts
new file mode 100644
index 0000000000..d3cab319f4
--- /dev/null
+++ b/src/lib/airbytes/pickSelectedConditionMap.ts
@@ -0,0 +1,83 @@
+import getConditionFormPath from "./getConditionFormPath";
+import {
+ AirbyteFieldValues,
+ AirbyteFormItem,
+ AirbyteFormTree,
+ SelectedItemMap,
+} from "./types";
+
+const pickSelectedConditionMap = (
+ formTree: AirbyteFormTree,
+ initialValue: AirbyteFieldValues,
+ parentMap: SelectedItemMap = {}
+): SelectedItemMap => {
+ if (formTree._type === "formGroup") {
+ let map = {} as SelectedItemMap;
+ formTree.properties.map((e) => {
+ const childMap = pickSelectedConditionMap(e, initialValue, parentMap);
+ map = {
+ ...map,
+ ...parentMap,
+ ...childMap,
+ };
+ });
+
+ return map;
+ }
+
+ if (formTree._type === "formCondition") {
+ const conditionFormPath = getConditionFormPath(formTree);
+
+ let map = {} as SelectedItemMap;
+
+ if (conditionFormPath && initialValue[conditionFormPath]) {
+ const conditionValue = initialValue[conditionFormPath];
+ const conditionTitle: string[] = [];
+
+ // Internally we all use condition.title as selectedItem value
+
+ Object.entries(formTree.conditions).forEach(([k, v]) => {
+ const constField = v.properties.find((e) => "const" in e) as
+ | AirbyteFormItem
+ | undefined;
+ if (constField && conditionValue === constField.const) {
+ conditionTitle.push(k);
+ }
+ });
+ if (conditionTitle.length > 0) {
+ map[formTree.path] = {
+ selectedItem: conditionTitle[0] as string,
+ };
+ }
+ }
+
+ Object.entries(formTree.conditions).map(([, v]) => {
+ const childMap = pickSelectedConditionMap(v, initialValue, parentMap);
+ map = {
+ ...map,
+ ...childMap,
+ };
+ });
+ return map;
+ }
+
+ if (formTree._type === "objectArray") {
+ let map = {} as SelectedItemMap;
+ const childMap = pickSelectedConditionMap(
+ formTree.properties,
+ initialValue,
+ parentMap
+ );
+
+ map = {
+ ...parentMap,
+ ...childMap,
+ };
+
+ return map;
+ }
+
+ return parentMap;
+};
+
+export default pickSelectedConditionMap;
diff --git a/src/lib/dot/index.test.ts b/src/lib/dot/index.test.ts
index 0b5a75d38b..81778d1172 100644
--- a/src/lib/dot/index.test.ts
+++ b/src/lib/dot/index.test.ts
@@ -105,6 +105,7 @@ describe("setter", () => {
expect(obj).toEqual({ x: "y", foo: { 0: "b" } });
});
+ // /* eslint-disable jest/no-commented-out-tests */
// We are currently don't support bracket path
// it("supports bracket path", () => {
@@ -125,3 +126,48 @@ describe("setter", () => {
expect(obj).toEqual({ x: [{ y: { z: true } }] });
});
});
+
+describe("toDot", () => {
+ it("can covert simple object to dot path object", () => {
+ const obj = {
+ foo: "hi",
+ bar: "yo",
+ fooBar: 123,
+ test: {
+ foo: "hi2",
+ bar: "yo2",
+ },
+ };
+
+ const dotObj = dot.toDot(obj);
+ expect(dotObj).toEqual({
+ foo: "hi",
+ bar: "yo",
+ fooBar: 123,
+ "test.foo": "hi2",
+ "test.bar": "yo2",
+ });
+ });
+
+ it("can conver nested object to dot path object", () => {
+ const obj = {
+ nested: {
+ foo: {
+ bar: 123,
+ nested: {
+ foo: "bar",
+ },
+ },
+ hello: "how are you",
+ good: null,
+ },
+ };
+ const dotObj = dot.toDot(obj);
+ expect(dotObj).toEqual({
+ "nested.foo.bar": 123,
+ "nested.foo.nested.foo": "bar",
+ "nested.hello": "how are you",
+ "nested.good": null,
+ });
+ });
+});
diff --git a/src/lib/dot/index.ts b/src/lib/dot/index.ts
index a07e16af6c..02fc5ed6c6 100644
--- a/src/lib/dot/index.ts
+++ b/src/lib/dot/index.ts
@@ -51,9 +51,34 @@ const setter = (obj: any, path: DotPath, value: any) => {
}
};
+/**
+ * Convert normal object to Dot
+ */
+
+const toDot = (obj: any, parentKey?: string, result: any = {}) => {
+ if (!isObject(obj)) {
+ throw new Error(
+ "Target value is not a object, toDot function only process object."
+ );
+ }
+
+ for (const key in obj) {
+ const value = obj[key];
+ const dotKey = parentKey ? parentKey + "." + key : key;
+ if (value && isObject(value)) {
+ toDot(value, dotKey, result);
+ } else {
+ result[dotKey] = value;
+ }
+ }
+
+ return result;
+};
+
export default {
getter,
setter,
+ toDot,
};
const toPath = (path: DotPath): string[] => {
diff --git a/src/lib/instill/connector/destination/index.ts b/src/lib/instill/connector/destination/index.ts
index 71c253adf7..c1f60c5c01 100644
--- a/src/lib/instill/connector/destination/index.ts
+++ b/src/lib/instill/connector/destination/index.ts
@@ -21,11 +21,14 @@ import {
import type {
CreateDestinationPayload,
CreateDestinationResponse,
+ UpdateDestinationResponse,
+ UpdateDestinationPayload,
} from "./mutations";
import {
createDestinationMutation,
deleteDestinationMutation,
+ updateDestinationMutation,
} from "./mutations";
export type {
@@ -38,6 +41,8 @@ export type {
GetDestinationResponse,
CreateDestinationPayload,
CreateDestinationResponse,
+ UpdateDestinationResponse,
+ UpdateDestinationPayload,
};
export {
@@ -47,4 +52,5 @@ export {
getDestinationQuery,
createDestinationMutation,
deleteDestinationMutation,
+ updateDestinationMutation,
};
diff --git a/src/lib/instill/connector/destination/mutations.ts b/src/lib/instill/connector/destination/mutations.ts
index d11c0ffe66..a8d1187b16 100644
--- a/src/lib/instill/connector/destination/mutations.ts
+++ b/src/lib/instill/connector/destination/mutations.ts
@@ -41,3 +41,33 @@ export const deleteDestinationMutation = async (destinationName: string) => {
return Promise.reject(err);
}
};
+
+export type UpdateDestinationResponse = {
+ destination_connector: Destination;
+};
+
+export type UpdateDestinationPayload = {
+ name: string;
+ connector: {
+ description?: string;
+ configuration:
+ | Record
+ | AirbyteFieldValues
+ | Record;
+ };
+};
+
+export const updateDestinationMutation = async (
+ payload: UpdateDestinationPayload
+) => {
+ try {
+ const { name, ...data } = payload;
+ const res = await axios.patch(
+ `${process.env.NEXT_PUBLIC_CONNECTOR_BACKEND_BASE_URL}/${process.env.NEXT_PUBLIC_API_VERSION}/${name}`,
+ data
+ );
+ return Promise.resolve(res.data.destination_connector);
+ } catch (err) {
+ return Promise.reject(err);
+ }
+};
diff --git a/src/lib/instill/connector/destination/queries.ts b/src/lib/instill/connector/destination/queries.ts
index 0cbaf4504a..cd9cfcc323 100644
--- a/src/lib/instill/connector/destination/queries.ts
+++ b/src/lib/instill/connector/destination/queries.ts
@@ -63,7 +63,7 @@ export const getDestinationDefinitionQuery = async (
) => {
try {
const { data } = await axios.get(
- `${process.env.NEXT_PUBLIC_CONNECTOR_BACKEND_BASE_URL}/${process.env.NEXT_PUBLIC_API_VERSION}/${destinationDefinitionName}`
+ `${process.env.NEXT_PUBLIC_CONNECTOR_BACKEND_BASE_URL}/${process.env.NEXT_PUBLIC_API_VERSION}/${destinationDefinitionName}?view=VIEW_FULL`
);
return Promise.resolve(data.destination_connector_definition);
diff --git a/src/lib/instill/connector/types.ts b/src/lib/instill/connector/types.ts
index f0f360dcbd..23747f62b1 100644
--- a/src/lib/instill/connector/types.ts
+++ b/src/lib/instill/connector/types.ts
@@ -1,6 +1,8 @@
import { Nullable } from "@/types/general";
import type { JSONSchema7 } from "json-schema";
+/* eslint-disable @typescript-eslint/no-explicit-any */
+
export type ConnectorState =
| "STATE_CONNECTED"
| "STATE_DISCONNECTED"
@@ -9,7 +11,7 @@ export type ConnectorState =
export type Connector = {
description: string;
- configuration: string;
+ configuration: Record;
tombstone: boolean;
user: string;
org: string;
diff --git a/src/lib/instill/pipeline/mocks.ts b/src/lib/instill/pipeline/mocks.ts
index 063888c3a6..1975f377b3 100644
--- a/src/lib/instill/pipeline/mocks.ts
+++ b/src/lib/instill/pipeline/mocks.ts
@@ -62,7 +62,7 @@ export const mockPipelines: Pipeline[] = [
},
connector: {
description: "",
- configuration: "null",
+ configuration: {},
tombstone: false,
user: "users/local-user",
create_time: "2022-05-25T02:12:39.644778Z",
@@ -120,7 +120,7 @@ export const mockPipelines: Pipeline[] = [
},
connector: {
description: "",
- configuration: "null",
+ configuration: {},
tombstone: false,
user: "users/local-user",
create_time: "2022-05-25T02:13:45.383123Z",
diff --git a/src/mocks/handlers/connector/source/getSource.ts b/src/mocks/handlers/connector/source/getSource.ts
index 1ef4a4b8a1..0f9996208a 100644
--- a/src/mocks/handlers/connector/source/getSource.ts
+++ b/src/mocks/handlers/connector/source/getSource.ts
@@ -14,7 +14,7 @@ export const getPredefindedSource = (sourceId: string): GetSourceResponse => {
source_connector_definition: `source-connector-definitions/${sourceId}`,
connector: {
description: "",
- configuration: "{}",
+ configuration: {},
state: "STATE_CONNECTED",
tombstone: false,
user: "users/local-user",
diff --git a/src/pages/destinations/[id].tsx b/src/pages/destinations/[id].tsx
index 85d42f4865..842a0fb98e 100644
--- a/src/pages/destinations/[id].tsx
+++ b/src/pages/destinations/[id].tsx
@@ -1,39 +1,23 @@
-import { FC, ReactElement } from "react";
+import { FC, ReactElement, useMemo } from "react";
import { useRouter } from "next/router";
-import { PageBase, PageContentContainer } from "@/components/layouts";
-import { StateLabel, PipelinesTable, PageTitle } from "@/components/ui";
-import { ConfigureDestinationForm } from "@/components/forms";
+import {
+ StateLabel,
+ PipelinesTable,
+ PageTitle,
+ PageBase,
+ PageContentContainer,
+ PageHead,
+} from "@/components/ui";
+import { ConfigureDestinationForm } from "@/components/destination";
import { useMultiStageQueryLoadingState } from "@/hooks/useMultiStageQueryLoadingState";
import { useDestinationWithPipelines } from "@/services/connector";
import { useAmplitudeCtx } from "@/contexts/AmplitudeContext";
import { useSendAmplitudeData } from "@/hooks/useSendAmplitudeData";
-import PageHead from "@/components/layouts/PageHead";
-// export const getServerSideProps: GetServerSideProps = async () => {
-// const data = await listRepoFileContent(
-// "instill-ai",
-// "connector-backend",
-// "configs/models/destination-definition.json"
-// );
-
-// const decodeSchema = Buffer.from(data.content, "base64").toString();
-// const jsonSchema = JSON.parse(decodeSchema);
-
-// //const fields = transformSchemaToFormFields(jsonSchema);
-
-// return {
-// props: {
-// schema: jsonSchema,
-// },
-// };
-// };
-
-interface GetLayOutProps {
+type GetLayOutProps = {
page: ReactElement;
-}
-
-// export type DestinationDetailsPageProps = {};
+};
const DestinationDetailsPage: FC & {
getLayout?: FC;
@@ -45,6 +29,12 @@ const DestinationDetailsPage: FC & {
id ? `destination-connectors/${id.toString()}` : null
);
+ const destination = useMemo(() => {
+ if (!destinationWithPipelines.isSuccess) return null;
+ const { pipelines, ...destination } = destinationWithPipelines.data;
+ return destination;
+ }, [destinationWithPipelines.isSuccess, destinationWithPipelines.data]);
+
const isLoading = useMultiStageQueryLoadingState({
data: destinationWithPipelines.data,
isError: destinationWithPipelines.isError,
@@ -53,9 +43,7 @@ const DestinationDetailsPage: FC & {
});
// ###################################################################
- // # #
// # Send page loaded data to Amplitude #
- // # #
// ###################################################################
const { amplitudeIsInit } = useAmplitudeCtx();
@@ -108,13 +96,9 @@ const DestinationDetailsPage: FC & {
/>
Setting
-
+ {destination ? (
+
+ ) : null}
>
diff --git a/src/pages/destinations/create.tsx b/src/pages/destinations/create.tsx
index 361251c7d3..b3255f1382 100644
--- a/src/pages/destinations/create.tsx
+++ b/src/pages/destinations/create.tsx
@@ -1,16 +1,19 @@
import { FC, ReactElement } from "react";
import { useRouter } from "next/router";
-import { CreateDestinationForm } from "@/components/forms";
-import { PageBase, PageContentContainer } from "@/components/layouts";
-import { PageTitle } from "@/components/ui";
+import { CreateDestinationForm } from "@/components/destination";
+import {
+ PageTitle,
+ PageBase,
+ PageContentContainer,
+ PageHead,
+} from "@/components/ui";
import { useAmplitudeCtx } from "@/contexts/AmplitudeContext";
import { useSendAmplitudeData } from "@/hooks/useSendAmplitudeData";
-import PageHead from "@/components/layouts/PageHead";
-interface GetLayOutProps {
+type GetLayOutProps = {
page: ReactElement;
-}
+};
const CreateDestinationPage: FC & {
getLayout?: FC;
diff --git a/src/pages/destinations/index.tsx b/src/pages/destinations/index.tsx
index ca2e27d62a..0668a81708 100644
--- a/src/pages/destinations/index.tsx
+++ b/src/pages/destinations/index.tsx
@@ -1,17 +1,21 @@
import { FC, ReactElement } from "react";
import { useRouter } from "next/router";
-import { PageBase, PageContentContainer } from "@/components/layouts";
-import { PageTitle, DestinationsTable } from "@/components/ui";
+import {
+ PageTitle,
+ DestinationsTable,
+ PageBase,
+ PageContentContainer,
+ PageHead,
+} from "@/components/ui";
import { useMultiStageQueryLoadingState } from "@/hooks/useMultiStageQueryLoadingState";
import { useDestinationsWithPipelines } from "@/services/connector";
import { useAmplitudeCtx } from "@/contexts/AmplitudeContext";
import { useSendAmplitudeData } from "@/hooks/useSendAmplitudeData";
-import PageHead from "@/components/layouts/PageHead";
-interface GetLayOutProps {
+type GetLayOutProps = {
page: ReactElement;
-}
+};
const DestinationPage: FC & {
getLayout?: FC;
diff --git a/src/pages/index.tsx b/src/pages/index.tsx
index 03c8bfc904..cb610a9663 100644
--- a/src/pages/index.tsx
+++ b/src/pages/index.tsx
@@ -1,4 +1,4 @@
-import { PageBase, PageContentContainer } from "@/components/layouts";
+import { PageBase, PageContentContainer } from "@/components/ui";
import { GetServerSideProps } from "next";
import { useRouter } from "next/router";
import { FC, ReactElement, useEffect } from "react";
diff --git a/src/pages/models/[id].tsx b/src/pages/models/[id].tsx
index 05557c8d6f..0a84d07d4b 100644
--- a/src/pages/models/[id].tsx
+++ b/src/pages/models/[id].tsx
@@ -12,7 +12,6 @@ import {
SingleSelectOption,
} from "@instill-ai/design-system";
-import { PageBase, PageContentContainer } from "@/components/layouts";
import {
useDeployModelInstance,
useModel,
@@ -29,22 +28,23 @@ import {
PageTitle,
ModelInstanceReadmeCard,
ChangeResourceStateButton,
+ PageBase,
+ PageContentContainer,
+ PageHead,
} from "@/components/ui";
import {
ConfigureModelForm,
ConfigureModelInstanceForm,
-} from "@/components/forms";
+ TestModelInstanceSection } from "@/components/model";
import { Nullable } from "@/types/general";
import { usePipelines } from "@/services/pipeline";
import { Pipeline } from "@/lib/instill";
import { useAmplitudeCtx } from "@/contexts/AmplitudeContext";
import { useSendAmplitudeData } from "@/hooks/useSendAmplitudeData";
-import PageHead from "@/components/layouts/PageHead";
-import { TestModelInstanceSection } from "@/components/model";
-interface GetLayOutProps {
+type GetLayOutProps = {
page: ReactElement;
-}
+};
const ModelDetailsPage: FC & {
getLayout?: FC;
diff --git a/src/pages/models/create.tsx b/src/pages/models/create.tsx
index 5df6222e51..0c4aa89bd7 100644
--- a/src/pages/models/create.tsx
+++ b/src/pages/models/create.tsx
@@ -1,26 +1,23 @@
import { FC, ReactElement } from "react";
import { useRouter } from "next/router";
-import { PageBase, PageContentContainer } from "@/components/layouts";
-import { PageTitle } from "@/components/ui";
-import { CreateModelForm } from "@/components/forms";
+import {
+ PageTitle,
+ PageHead,
+ PageBase,
+ PageContentContainer,
+} from "@/components/ui";
+import { CreateModelForm } from "@/components/model";
import { useAmplitudeCtx } from "@/contexts/AmplitudeContext";
import { useSendAmplitudeData } from "@/hooks/useSendAmplitudeData";
-import PageHead from "@/components/layouts/PageHead";
-interface GetLayOutProps {
+type GetLayOutProps = {
page: ReactElement;
-}
+};
const CreateModelPage: FC & {
getLayout?: FC;
} = () => {
- // ###################################################################
- // # #
- // # Send page loaded data to Amplitude #
- // # #
- // ###################################################################
-
const router = useRouter();
const { amplitudeIsInit } = useAmplitudeCtx();
diff --git a/src/pages/models/index.tsx b/src/pages/models/index.tsx
index 54a62fbc66..f37c04aae9 100644
--- a/src/pages/models/index.tsx
+++ b/src/pages/models/index.tsx
@@ -1,13 +1,17 @@
import { FC, ReactElement } from "react";
import { useRouter } from "next/router";
-import { PageBase, PageContentContainer } from "@/components/layouts";
-import { ModelsTable, PageTitle } from "@/components/ui/";
+import {
+ ModelsTable,
+ PageTitle,
+ PageBase,
+ PageContentContainer,
+ PageHead,
+} from "@/components/ui/";
import { useModelsWithInstances } from "@/services/model";
import { useMultiStageQueryLoadingState } from "@/hooks/useMultiStageQueryLoadingState";
import { useAmplitudeCtx } from "@/contexts/AmplitudeContext";
import { useSendAmplitudeData } from "@/hooks/useSendAmplitudeData";
-import PageHead from "@/components/layouts/PageHead";
interface GetLayOutProps {
page: ReactElement;
diff --git a/src/pages/onboarding.tsx b/src/pages/onboarding.tsx
index 6887ba25ed..bae4911ff9 100644
--- a/src/pages/onboarding.tsx
+++ b/src/pages/onboarding.tsx
@@ -3,8 +3,12 @@ import { GetServerSideProps } from "next";
import { parse } from "cookie";
import axios from "axios";
-import { PageTitle } from "@/components/ui";
-import { PageBase, PageContentContainer } from "@/components/layouts";
+import {
+ PageTitle,
+ PageBase,
+ PageContentContainer,
+ PageHead,
+} from "@/components/ui";
import { GetUserResponse, User } from "@/lib/instill/mgmt";
import { OnboardingForm } from "@/components/onboarding";
import { Nullable } from "@/types/general";
@@ -32,9 +36,9 @@ export type OnBoardingPageProps = {
cookies: Nullable>;
};
-interface GetLayOutProps {
+type GetLayOutProps = {
page: ReactElement;
-}
+};
const OnBoardingPage: FC & {
getLayout?: FC;
@@ -60,18 +64,21 @@ const OnBoardingPage: FC & {
} else {
setFetched(true);
}
- }, []);
+ }, [cookies]);
return (
-
-
- {fetched ? : null}
-
+ <>
+
+
+
+ {fetched ? : null}
+
+ >
);
};
diff --git a/src/pages/pipelines/[id].tsx b/src/pages/pipelines/[id].tsx
index 454c78f952..fff8266c76 100644
--- a/src/pages/pipelines/[id].tsx
+++ b/src/pages/pipelines/[id].tsx
@@ -1,8 +1,10 @@
import { FC, ReactElement, useEffect } from "react";
import { useRouter } from "next/router";
-import Prism from "prismjs";
+import { highlightAll } from "prismjs";
+import { GetServerSideProps } from "next";
+import { join } from "path";
+import fs from "fs";
-import { PageBase, PageContentContainer } from "@/components/layouts";
import {
useActivatePipeline,
useDeActivatePipeline,
@@ -15,15 +17,14 @@ import {
PageTitle,
ChangeResourceStateButton,
CodeBlock,
+ PageBase,
+ PageContentContainer,
+ PageHead,
} from "@/components/ui";
-import ConfigurePipelineForm from "@/components/forms/pipeline/ConfigurePipelineForm";
+import { ConfigurePipelineForm } from "@/components/pipeline";
import { useAmplitudeCtx } from "@/contexts/AmplitudeContext";
import { useSendAmplitudeData } from "@/hooks/useSendAmplitudeData";
-import PageHead from "@/components/layouts/PageHead";
import { Pipeline } from "@/lib/instill";
-import { GetServerSideProps } from "next";
-import { join } from "path";
-import fs from "fs";
export const getServerSideProps: GetServerSideProps = async (context) => {
const templatePath = join(
@@ -101,7 +102,7 @@ const PipelineDetailsPage: FC & {
// }, []);
useEffect(() => {
- Prism.highlightAll();
+ highlightAll();
}, []);
// ###################################################################
diff --git a/src/pages/pipelines/create.tsx b/src/pages/pipelines/create.tsx
index fe72c2e350..0c82e79732 100644
--- a/src/pages/pipelines/create.tsx
+++ b/src/pages/pipelines/create.tsx
@@ -1,16 +1,19 @@
import { FC, ReactElement, useMemo, useState } from "react";
import { useRouter } from "next/router";
-import { CreatePipelineForm } from "@/components/forms";
-import { PageBase, PageContentContainer } from "@/components/layouts";
-import { PageTitle } from "@/components/ui";
+import { CreatePipelineForm } from "@/components/pipeline";
+import {
+ PageTitle,
+ PageHead,
+ PageBase,
+ PageContentContainer,
+} from "@/components/ui";
import { useAmplitudeCtx } from "@/contexts/AmplitudeContext";
import { useSendAmplitudeData } from "@/hooks/useSendAmplitudeData";
-import PageHead from "@/components/layouts/PageHead";
-interface GetLayOutProps {
+type GetLayOutProps = {
page: ReactElement;
-}
+};
const CreatePipelinePage: FC & {
getLayout?: FC;
diff --git a/src/pages/pipelines/index.tsx b/src/pages/pipelines/index.tsx
index 1b6b978d93..600c75252f 100644
--- a/src/pages/pipelines/index.tsx
+++ b/src/pages/pipelines/index.tsx
@@ -1,18 +1,20 @@
import { FC, ReactElement } from "react";
import { useRouter } from "next/router";
-import { PageBase, PageContentContainer } from "@/components/layouts";
-import { PipelinesTable, PageTitle } from "@/components/ui";
+import {
+ PipelinesTable,
+ PageTitle,
+ PageBase,
+ PageContentContainer,
+ PageHead,
+} from "@/components/ui";
import { usePipelines } from "@/services/pipeline";
import { useAmplitudeCtx } from "@/contexts/AmplitudeContext";
import { useSendAmplitudeData } from "@/hooks/useSendAmplitudeData";
-import PageHead from "@/components/layouts/PageHead";
-interface GetLayOutProps {
+type GetLayOutProps = {
page: ReactElement;
-}
-
-// export type PipelinePageProps = {};
+};
const PipelinePage: FC & {
getLayout?: FC;
diff --git a/src/pages/sources/[id].tsx b/src/pages/sources/[id].tsx
index b0736d87a7..3b2b4e049a 100644
--- a/src/pages/sources/[id].tsx
+++ b/src/pages/sources/[id].tsx
@@ -1,39 +1,23 @@
import { FC, ReactElement } from "react";
import { useRouter } from "next/router";
-import { PageBase, PageContentContainer } from "@/components/layouts";
-import { StateLabel, PipelinesTable, PageTitle } from "@/components/ui";
+import {
+ StateLabel,
+ PipelinesTable,
+ PageTitle,
+ PageBase,
+ PageContentContainer,
+ PageHead,
+} from "@/components/ui";
import { useSourceWithPipelines } from "@/services/connector";
-import { ConfigureSourceForm } from "@/components/forms";
+import { ConfigureSourceForm } from "@/components/source";
import { useMultiStageQueryLoadingState } from "@/hooks/useMultiStageQueryLoadingState";
import { useAmplitudeCtx } from "@/contexts/AmplitudeContext";
import { useSendAmplitudeData } from "@/hooks/useSendAmplitudeData";
-import PageHead from "@/components/layouts/PageHead";
-// export const getServerSideProps: GetServerSideProps = async () => {
-// const data = await listRepoFileContent(
-// "instill-ai",
-// "connector-backend",
-// "config/models/source_connector_definition.json"
-// );
-
-// const decodeSchema = Buffer.from(data.content, "base64").toString();
-// const jsonSchema = JSON.parse(decodeSchema);
-
-// console.log(jsonSchema);
-
-// return {
-// props: {
-// schema: jsonSchema,
-// },
-// };
-// };
-
-interface GetLayOutProps {
+type GetLayOutProps = {
page: ReactElement;
-}
-
-// export type SourceDetailsPageProps = {};
+};
const SourceDetailsPage: FC & {
getLayout?: FC;
@@ -105,6 +89,7 @@ const SourceDetailsPage: FC & {
diff --git a/src/pages/sources/create.tsx b/src/pages/sources/create.tsx
index 9a27aa7570..204d0289a2 100644
--- a/src/pages/sources/create.tsx
+++ b/src/pages/sources/create.tsx
@@ -1,16 +1,19 @@
import { FC, ReactElement } from "react";
import { useRouter } from "next/router";
-import { CreateSourceForm } from "@/components/forms";
-import { PageBase, PageContentContainer } from "@/components/layouts";
-import { PageTitle } from "@/components/ui";
+import { CreateSourceForm } from "@/components/source";
+import {
+ PageTitle,
+ PageBase,
+ PageContentContainer,
+ PageHead,
+} from "@/components/ui";
import { useAmplitudeCtx } from "@/contexts/AmplitudeContext";
import { useSendAmplitudeData } from "@/hooks/useSendAmplitudeData";
-import PageHead from "@/components/layouts/PageHead";
-interface GetLayOutProps {
+type GetLayOutProps = {
page: ReactElement;
-}
+};
const CreateSourcePage: FC & {
getLayout?: FC;
@@ -41,7 +44,7 @@ const CreateSourcePage: FC & {
enableButton={false}
marginBottom="mb-10"
/>
-
+
>
);
diff --git a/src/pages/sources/index.tsx b/src/pages/sources/index.tsx
index dbec0896f3..4e746b8084 100644
--- a/src/pages/sources/index.tsx
+++ b/src/pages/sources/index.tsx
@@ -1,17 +1,21 @@
import { FC, ReactElement } from "react";
import { useRouter } from "next/router";
-import { PageBase, PageContentContainer } from "@/components/layouts";
-import { SourcesTable, PageTitle } from "@/components/ui";
+import {
+ SourcesTable,
+ PageTitle,
+ PageBase,
+ PageContentContainer,
+ PageHead,
+} from "@/components/ui";
import { useMultiStageQueryLoadingState } from "@/hooks/useMultiStageQueryLoadingState";
import { useSourcesWithPipelines } from "@/services/connector";
import { useAmplitudeCtx } from "@/contexts/AmplitudeContext";
import { useSendAmplitudeData } from "@/hooks/useSendAmplitudeData";
-import PageHead from "@/components/layouts/PageHead";
-interface GetLayOutProps {
+type GetLayOutProps = {
page: ReactElement;
-}
+};
const SourcePage: FC & {
getLayout?: FC;
diff --git a/src/services/connector/destination/mutations/index.ts b/src/services/connector/destination/mutations/index.ts
index 43fc34c745..a956c24de0 100644
--- a/src/services/connector/destination/mutations/index.ts
+++ b/src/services/connector/destination/mutations/index.ts
@@ -1,4 +1,5 @@
import useCreateDestination from "./useCreateDestination";
import useDeleteDestination from "./useDeleteDestination";
+import useUpdateDestination from "./useUpdateDestination";
-export { useCreateDestination, useDeleteDestination };
+export { useCreateDestination, useDeleteDestination, useUpdateDestination };
diff --git a/src/services/connector/destination/mutations/useUpdateDestination.ts b/src/services/connector/destination/mutations/useUpdateDestination.ts
new file mode 100644
index 0000000000..cddac3bd8f
--- /dev/null
+++ b/src/services/connector/destination/mutations/useUpdateDestination.ts
@@ -0,0 +1,47 @@
+import { useMutation, useQueryClient } from "react-query";
+import {
+ DestinationWithDefinition,
+ getDestinationDefinitionQuery,
+ updateDestinationMutation,
+ UpdateDestinationPayload,
+} from "@/lib/instill";
+
+const useUpdateDestination = () => {
+ const queryClient = useQueryClient();
+ return useMutation(
+ async (payload: UpdateDestinationPayload) => {
+ const res = await updateDestinationMutation(payload);
+ return Promise.resolve(res);
+ },
+ {
+ onSuccess: async (newDestination) => {
+ const destinationDefinition = await getDestinationDefinitionQuery(
+ newDestination.destination_connector_definition
+ );
+
+ const newDestinationWithDefinition: DestinationWithDefinition = {
+ ...newDestination,
+ destination_connector_definition: destinationDefinition,
+ };
+
+ queryClient.setQueryData(
+ ["destinations", newDestination.id],
+ newDestinationWithDefinition
+ );
+
+ queryClient.setQueryData(
+ ["destinations"],
+ (old) =>
+ old
+ ? [
+ ...old.filter((e) => e.id !== newDestination.id),
+ newDestinationWithDefinition,
+ ]
+ : [newDestinationWithDefinition]
+ );
+ },
+ }
+ );
+};
+
+export default useUpdateDestination;
diff --git a/src/services/transformation.ts b/src/services/transformation.ts
deleted file mode 100644
index 4a4e7f6beb..0000000000
--- a/src/services/transformation.ts
+++ /dev/null
@@ -1,124 +0,0 @@
-import { SingleSelectOption } from "@instill-ai/design-system";
-
-export type IndependentFormField = {
- kind: "independent" | "dependent";
- id: string;
- component: "text" | "textarea" | "select" | "toggle";
- type?: "email" | "password" | "text";
- title: string;
- description?: string;
- required: boolean;
- disabled: boolean;
- readonly: boolean;
- placeholder: string;
- pattern?: string;
- options?: SingleSelectOption[];
- enableCounter?: boolean;
- counterWordLimit?: number;
- default?: string | SingleSelectOption;
- order: number;
- minLength: number;
- maxLength: number;
-};
-
-export type DependentFormField = IndependentFormField & {
- dependOnId: string;
- renderCb: (dependOnFieldAnswer: any) => any;
-};
-
-export type FormField = DependentFormField | IndependentFormField;
-
-export const isDependentField = (
- field: FormField
-): field is DependentFormField => {
- return field && field.kind === "dependent";
-};
-type Schema = {
- title: string;
- type: string;
- $id: string;
- $schema: string;
- additionalProperties: boolean;
- description: string;
- required: string[];
- properties: Record;
- definitions: Record;
-};
-
-export const transformSchemaToFormFields = (
- schema: Schema
-): { fields: FormField[]; requiredFields: string[] } => {
- const formFields: FormField[] = [];
-
- const fullReferenceSchema = deReferenceJsonSchema(schema);
-
- for (const [fieldName, fieldConfig] of Object.entries(
- fullReferenceSchema.properties
- )) {
- const formField = {} as FormField;
-
- if (fieldConfig.ui_hidden) {
- continue;
- }
-
- formField.id = fieldName;
- formField.description = fieldConfig.description ?? null;
- formField.title = fieldConfig.title ?? null;
- formField.type = fieldConfig.type ?? null;
- formField.order = fieldConfig.ui_order ?? null;
-
- if (fieldConfig.enum) {
- formField.options = (fieldConfig.enum as string[]).map((e, i) => {
- return {
- label: fieldConfig.ui_enum[i] ?? null,
- value: e ?? null,
- };
- });
- }
-
- formField.required = schema.required.includes(fieldName) ? true : false;
- formField.pattern = fieldConfig.pattern ?? null;
- formField.component = fieldConfig.ui_component ?? null;
- formField.placeholder = fieldConfig.ui_placeholder ?? null;
- formField.disabled = fieldConfig.ui_disabled ?? false;
- formField.default = fieldConfig.default ?? null;
- formFields.push(formField);
- }
-
- return { fields: formFields, requiredFields: schema.required };
-};
-
-/**
- * Handle $ref in Json schema
- * - caveat: this function can't handle external sources
- * - it assumes the format of $ref is "#/definitions/"
- */
-
-export const deReferenceJsonSchema = (schema: Schema) => {
- // Dereference definitions first
-
- for (const [name, config] of Object.entries(schema.definitions)) {
- for (const [propertyName, propertyConfig] of Object.entries(
- config.properties
- )) {
- if (!propertyConfig.$ref) {
- continue;
- }
-
- const pathList = (propertyConfig.$ref as string).split("/");
- schema.definitions[name].properties[propertyName] =
- schema.definitions[pathList[2]];
- }
- }
-
- for (const [name, config] of Object.entries(schema.properties)) {
- if (!config.$ref) {
- continue;
- }
-
- const pathList = (config.$ref as string).split("/");
- schema.properties = schema.definitions[pathList[2]];
- }
-
- return schema;
-};
diff --git a/src/types/general.ts b/src/types/general.ts
index a34042a887..bff7682966 100644
--- a/src/types/general.ts
+++ b/src/types/general.ts
@@ -1,5 +1,6 @@
import { ModelState, PipelineState } from "@/lib/instill";
import { ConnectorState } from "@/lib/instill/connector";
+import { Dispatch, SetStateAction } from "react";
export type State = PipelineState | ConnectorState | ModelState;
@@ -10,3 +11,7 @@ export type Nullable = T | null;
export type InstillAiUserCookie = {
cookie_token: string;
};
+
+export type AtLeast = Partial & Pick;
+
+export type UseCustomHookResult = [T, Dispatch>];
diff --git a/src/utils/arrayUtils.ts b/src/utils/arrayUtils.ts
index 4db714ae98..61fd177d7f 100644
--- a/src/utils/arrayUtils.ts
+++ b/src/utils/arrayUtils.ts
@@ -2,6 +2,8 @@
* Credit: https://stackoverflow.com/a/62765924
*/
+/* eslint-disable @typescript-eslint/no-explicit-any */
+
export const groupBy = (
list: T[],
getKey: (item: T) => K