Skip to content

Commit

Permalink
Highlight invalid cursor or primary key in new connection streams table
Browse files Browse the repository at this point in the history
- Added individual error handling for Cursor and Primary Key
- Added feature variable to encapsulate new changes with new streams table
  • Loading branch information
Mark Berger committed Jan 3, 2023
1 parent 353a64c commit b6e118d
Show file tree
Hide file tree
Showing 7 changed files with 139 additions and 28 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,6 @@ const CatalogSectionInner: React.FC<CatalogSectionInnerProps> = ({
onExpand={onExpand}
changedSelected={changedSelected}
hasError={hasError}
configErrors={configErrors}
disabled={disabled}
/>
{isRowExpanded &&
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,6 @@ export interface StreamHeaderProps {
onExpand: () => void;
changedSelected: boolean;
hasError: boolean;
configErrors?: Record<string, string>;
disabled?: boolean;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,6 @@ export const CatalogTreeTableRow: React.FC<StreamHeaderProps> = ({
fields,
onExpand,
disabled,
configErrors,
}) => {
const { primaryKey, cursorField, syncMode, destinationSyncMode } = stream.config ?? {};
const { defaultCursorField } = stream.stream ?? {};
Expand All @@ -53,7 +52,7 @@ export const CatalogTreeTableRow: React.FC<StreamHeaderProps> = ({
const fieldCount = fields?.length ?? 0;
const onRowClick = fieldCount > 0 ? () => onExpand() : undefined;

const { streamHeaderContentStyle, pillButtonVariant } = useCatalogTreeTableRowProps(stream);
const { streamHeaderContentStyle, pillButtonVariant, configErrors } = useCatalogTreeTableRowProps(stream);

const checkboxCellCustomStyle = classnames(styles.checkboxCell, styles.streamRowCheckboxCell);

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/* eslint-disable css-modules/no-unused-class */
import classNames from "classnames";
import { useField } from "formik";
import { getIn, useFormikContext } from "formik";
import isEqual from "lodash/isEqual";
import { useMemo } from "react";

Expand All @@ -11,16 +11,15 @@ import { useBulkEditSelect } from "hooks/services/BulkEdit/BulkEditService";
import { useConnectionFormService } from "hooks/services/ConnectionForm/ConnectionFormService";

import styles from "./CatalogTreeTableRow.module.scss";

type StatusToDisplay = "disabled" | "added" | "removed" | "changed" | "unchanged";

export const useCatalogTreeTableRowProps = (stream: SyncSchemaStream) => {
const { initialValues } = useConnectionFormService();
const [isSelected] = useBulkEditSelect(stream.id);
const { initialValues } = useConnectionFormService();
const { errors } = useFormikContext();

const [, { error }] = useField(`schema.streams[${stream.id}].config`);

// in case error is an empty string
const hasError = error !== undefined;
const configErrors = getIn(errors, `syncCatalog.streams[${stream.id}].config`);

const isStreamEnabled = stream.config?.selected;

Expand Down Expand Up @@ -67,13 +66,13 @@ export const useCatalogTreeTableRowProps = (stream: SyncSchemaStream) => {
[styles.removed]: statusToDisplay === "removed" && !isSelected,
[styles.changed]: statusToDisplay === "changed" || isSelected,
[styles.disabled]: statusToDisplay === "disabled",
[styles.error]: hasError,
});

return {
streamHeaderContentStyle,
isSelected,
statusToDisplay,
pillButtonVariant,
configErrors,
};
};
Original file line number Diff line number Diff line change
Expand Up @@ -85,12 +85,23 @@ const useConnectionForm = ({
const formId = useUniqueFormId();

const getErrorMessage = useCallback(
(formValid: boolean, connectionDirty: boolean) =>
submitError
(formValid: boolean, connectionDirty: boolean) => {
const isNewStreamsTableEnabled = process.env.REACT_APP_NEW_STREAMS_TABLE ?? false;

if (isNewStreamsTableEnabled) {
// There is a case when some fields could be dropped in the database. We need to validate the form without property dirty
return submitError
? generateMessageFromError(submitError)
: !formValid
? formatMessage({ id: "connectionForm.validation.error" })
: null;
}
return submitError
? generateMessageFromError(submitError)
: connectionDirty && !formValid
? formatMessage({ id: "connectionForm.validation.error" })
: null,
: null;
},
[formatMessage, submitError]
);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -203,18 +203,20 @@ export const ConnectionReplicationTab: React.FC = () => {
dirty={dirty || schemaHasBeenRefreshed}
/>
{status.editControlsVisible && (
<EditControls
isSubmitting={isSubmitting}
submitDisabled={!isValid}
dirty={dirty}
resetForm={async () => {
resetForm();
discardRefreshedSchema();
}}
successMessage={saved && !dirty && <FormattedMessage id="form.changesSaved" />}
errorMessage={getErrorMessage(isValid, dirty)}
enableControls={schemaHasBeenRefreshed || dirty}
/>
<div>
<EditControls
isSubmitting={isSubmitting}
submitDisabled={!isValid}
dirty={dirty}
resetForm={async () => {
resetForm();
discardRefreshedSchema();
}}
successMessage={saved && !dirty && <FormattedMessage id="form.changesSaved" />}
errorMessage={getErrorMessage(isValid, dirty)}
enableControls={schemaHasBeenRefreshed || dirty}
/>
</div>
)}
</Form>
</SchemaChangeBackdrop>
Expand Down
106 changes: 104 additions & 2 deletions airbyte-webapp/src/views/Connection/ConnectionForm/formConfig.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -90,8 +90,109 @@ export const createConnectionValidationSchema = ({
mode,
allowSubOneHourCronExpressions,
allowAutoDetectSchema,
}: CreateConnectionValidationSchemaArgs) =>
yup
}: CreateConnectionValidationSchemaArgs) => {
const isNewStreamsTableEnabled = process.env.REACT_APP_NEW_STREAMS_TABLE ?? false;

if (isNewStreamsTableEnabled) {
return yup
.object({
// The connection name during Editing is handled separately from the form
name: mode === "create" ? yup.string().required("form.empty.error") : yup.string().notRequired(),
geography: yup.mixed<Geography>().oneOf(Object.values(Geography)),
scheduleType: yup
.string()
.oneOf([ConnectionScheduleType.manual, ConnectionScheduleType.basic, ConnectionScheduleType.cron]),
scheduleData: yup.mixed().when("scheduleType", (scheduleType) => {
if (scheduleType === ConnectionScheduleType.basic) {
return yup.object({
basicSchedule: yup
.object({
units: yup.number().required("form.empty.error"),
timeUnit: yup.string().required("form.empty.error"),
})
.defined("form.empty.error"),
});
} else if (scheduleType === ConnectionScheduleType.manual) {
return yup.mixed().notRequired();
}
return yup.object({
cron: yup
.object({
cronExpression: yup
.string()
.trim()
.required("form.empty.error")
.test("validCron", "form.cronExpression.error", validateCronExpression)
.test(
"validCronFrequency",
"form.cronExpression.underOneHourNotAllowed",
(expression) => allowSubOneHourCronExpressions || validateCronFrequencyOneHourOrMore(expression)
),
cronTimeZone: yup.string().required("form.empty.error"),
})
.defined("form.empty.error"),
});
}),
nonBreakingChangesPreference: allowAutoDetectSchema
? yup.mixed().oneOf(Object.values(NonBreakingChangesPreference)).required("form.empty.error")
: yup.mixed().notRequired(),
namespaceDefinition: yup
.string()
.oneOf([
NamespaceDefinitionType.source,
NamespaceDefinitionType.destination,
NamespaceDefinitionType.customformat,
])
.required("form.empty.error"),
namespaceFormat: yup.string().when("namespaceDefinition", {
is: NamespaceDefinitionType.customformat,
then: yup.string().trim().required("form.empty.error"),
}),
prefix: yup.string(),
syncCatalog: yup.object({
streams: yup.array().of(
yup.object({
id: yup
.string()
// This is required to get rid of id fields we are using to detect stream for edition
.when("$isRequest", (isRequest: boolean, schema: yup.StringSchema) =>
isRequest ? schema.strip(true) : schema
),
stream: yup.object(),
config: yup.object({
selected: yup.boolean(),
syncMode: yup.string(),
destinationSyncMode: yup.string(),
primaryKey: yup
.array()
.of(yup.array().of(yup.string()))
.when(["syncMode", "destinationSyncMode", "selected"], {
is: (syncMode: SyncMode, destinationSyncMode: DestinationSyncMode, selected: boolean) =>
syncMode === SyncMode.incremental &&
destinationSyncMode === DestinationSyncMode.append_dedup &&
selected,
then: yup.array().of(yup.array().of(yup.string())).min(1, "form.empty.error"),
}),
cursorField: yup
.array()
.of(yup.string())
.when(["syncMode", "destinationSyncMode", "selected"], {
is: (syncMode: SyncMode, destinationSyncMode: DestinationSyncMode, selected: boolean) =>
(destinationSyncMode === DestinationSyncMode.append ||
destinationSyncMode === DestinationSyncMode.append_dedup) &&
syncMode === SyncMode.incremental &&
selected,
then: yup.array().of(yup.string()).min(1, "form.empty.error"),
}),
}),
})
),
}),
})
.noUnknown();
}

return yup
.object({
// The connection name during Editing is handled separately from the form
name: mode === "create" ? yup.string().required("form.empty.error") : yup.string().notRequired(),
Expand Down Expand Up @@ -206,6 +307,7 @@ export const createConnectionValidationSchema = ({
}),
})
.noUnknown();
};

/**
* Returns {@link Operation}[]
Expand Down

0 comments on commit b6e118d

Please sign in to comment.