diff --git a/airbyte-webapp/src/components/connectorBuilder/Builder/BuilderField.tsx b/airbyte-webapp/src/components/connectorBuilder/Builder/BuilderField.tsx index 30171879af3f..4b2443affc7b 100644 --- a/airbyte-webapp/src/components/connectorBuilder/Builder/BuilderField.tsx +++ b/airbyte-webapp/src/components/connectorBuilder/Builder/BuilderField.tsx @@ -32,6 +32,7 @@ interface BaseFieldProps { tooltip?: string; readOnly?: boolean; optional?: boolean; + pattern?: RegExp; } type BuilderFieldProps = BaseFieldProps & @@ -66,6 +67,7 @@ export const BuilderField: React.FC = ({ tooltip, optional = false, readOnly, + pattern, ...props }) => { const [field, meta, helpers] = useField(path); @@ -116,7 +118,10 @@ export const BuilderField: React.FC = ({ )} {hasError && ( - + )} diff --git a/airbyte-webapp/src/components/connectorBuilder/Builder/StreamConfigView.tsx b/airbyte-webapp/src/components/connectorBuilder/Builder/StreamConfigView.tsx index 09225da07c86..1171aac969e4 100644 --- a/airbyte-webapp/src/components/connectorBuilder/Builder/StreamConfigView.tsx +++ b/airbyte-webapp/src/components/connectorBuilder/Builder/StreamConfigView.tsx @@ -16,6 +16,7 @@ import { BuilderTitle } from "./BuilderTitle"; import { KeyValueListField } from "./KeyValueListField"; import { PaginationSection } from "./PaginationSection"; import styles from "./StreamConfigView.module.scss"; +import { StreamSlicerSection } from "./StreamSlicerSection"; interface StreamConfigViewProps { streamNum: number; @@ -97,6 +98,7 @@ export const StreamConfigView: React.FC = ({ streamNum }) /> + string; +} + +export const StreamSlicerSection: React.FC = ({ streamFieldPath }) => { + const [field, , helpers] = useField(streamFieldPath("streamSlicer")); + + const handleToggle = (newToggleValue: boolean) => { + if (newToggleValue) { + helpers.setValue({ + type: "ListStreamSlicer", + slice_values: [], + cursor_field: "", + }); + } else { + helpers.setValue(undefined); + } + }; + const toggledOn = field.value !== undefined; + + return ( + + ), + toggledOn, + onToggle: handleToggle, + }} + > + + + + + + + + ), + }, + { + label: "Datetime", + typeValue: "DatetimeStreamSlicer", + children: ( + <> + + + + + + + + + + + + + + + + + + ), + }, + ]} + /> + + ); +}; diff --git a/airbyte-webapp/src/components/connectorBuilder/types.ts b/airbyte-webapp/src/components/connectorBuilder/types.ts index 57d0c1f0dd01..f56dc225e156 100644 --- a/airbyte-webapp/src/components/connectorBuilder/types.ts +++ b/airbyte-webapp/src/components/connectorBuilder/types.ts @@ -15,6 +15,7 @@ import { SessionTokenAuthenticator, DefaultPaginatorAllOfPaginationStrategy, RequestOption, + SimpleRetrieverAllOfStreamSlicer, } from "core/request/ConnectorManifest"; export interface BuilderFormInput { @@ -61,6 +62,7 @@ export interface BuilderStream { pageTokenOption: RequestOption; pageSizeOption?: RequestOption; }; + streamSlicer?: SimpleRetrieverAllOfStreamSlicer; } export const DEFAULT_BUILDER_FORM_VALUES: BuilderFormValues = { @@ -213,6 +215,17 @@ export function getInferredInputs(values: BuilderFormValues): BuilderFormInput[] } export const injectIntoValues = ["request_parameter", "header", "path", "body_data", "body_json"]; +const nonPathRequestOptionSchema = yup + .object() + .shape({ + inject_into: yup.mixed().oneOf(injectIntoValues.filter((val) => val !== "path")), + field_name: yup.string().required("form.empty.error"), + }) + .notRequired() + .default(undefined); + +// eslint-disable-next-line no-useless-escape +export const timeDeltaRegex = /^(([\.\d]+?)y)?(([\.\d]+?)m)?(([\.\d]+?)w)?(([\.\d]+?)d)?$/; export const builderFormValidationSchema = yup.object().shape({ global: yup.object().shape({ @@ -261,14 +274,7 @@ export const builderFormValidationSchema = yup.object().shape({ paginator: yup .object() .shape({ - pageSizeOption: yup - .object() - .shape({ - inject_into: yup.mixed().oneOf(injectIntoValues.filter((val) => val !== "path")), - field_name: yup.string().required("form.empty.error"), - }) - .notRequired() - .default(undefined), + pageSizeOption: nonPathRequestOptionSchema, pageTokenOption: yup.object().shape({ inject_into: yup.mixed().oneOf(injectIntoValues), field_name: yup.mixed().when("inject_into", { @@ -305,6 +311,64 @@ export const builderFormValidationSchema = yup.object().shape({ }) .notRequired() .default(undefined), + streamSlicer: yup + .object() + .shape({ + cursor_field: yup.string().required("form.empty.error"), + slice_values: yup.mixed().when("type", { + is: "ListStreamSlicer", + then: yup.array().of(yup.string()), + otherwise: (schema) => schema.strip(), + }), + request_option: nonPathRequestOptionSchema, + start_datetime: yup.mixed().when("type", { + is: "DatetimeStreamSlicer", + then: yup.string().required("form.empty.error"), + otherwise: (schema) => schema.strip(), + }), + end_datetime: yup.mixed().when("type", { + is: "DatetimeStreamSlicer", + then: yup.string().required("form.empty.error"), + otherwise: (schema) => schema.strip(), + }), + step: yup.mixed().when("type", { + is: "DatetimeStreamSlicer", + then: yup.string().matches(timeDeltaRegex, "form.pattern.error").required("form.empty.error"), + otherwise: (schema) => schema.strip(), + }), + datetime_format: yup.mixed().when("type", { + is: "DatetimeStreamSlicer", + then: yup.string().required("form.empty.error"), + otherwise: (schema) => schema.strip(), + }), + start_time_option: yup.mixed().when("type", { + is: "DatetimeStreamSlicer", + then: nonPathRequestOptionSchema, + otherwise: (schema) => schema.strip(), + }), + end_time_option: yup.mixed().when("type", { + is: "DatetimeStreamSlicer", + then: nonPathRequestOptionSchema, + otherwise: (schema) => schema.strip(), + }), + stream_state_field_start: yup.mixed().when("type", { + is: "DatetimeStreamSlicer", + then: yup.string(), + otherwise: (schema) => schema.strip(), + }), + stream_state_field_end: yup.mixed().when("type", { + is: "DatetimeStreamSlicer", + then: yup.string(), + otherwise: (schema) => schema.strip(), + }), + lookback_window: yup.mixed().when("type", { + is: "DatetimeStreamSlicer", + then: yup.string(), + otherwise: (schema) => schema.strip(), + }), + }) + .notRequired() + .default(undefined), }) ), }); @@ -369,6 +433,7 @@ export const convertToManifest = (values: BuilderFormValues): PatchedConnectorMa url_base: values.global?.urlBase, } : { type: "NoPagination" }, + stream_slicer: stream.streamSlicer, config: {}, }, config: {},