Skip to content

Commit 6d96913

Browse files
committed
feat: use normalization CDK logic in Builder UI (#16708)
## What In airbytehq/airbyte-python-cdk#599, I made some changes to the manifest normalizer in the CDK to make it ready to be used by the new Builder implementation. See that PR for details on those changes if interested. This PR makes changes to both SchemaForm and the Builder to make it properly use the manifest normalizer. At a high level, this looks like: - When the Builder is first opened, `/resolve` is called with `should_normalize: true`, which causes the CDK to - Resolve all $refs - Find matching values for fields with `linkable: true` and point them to shared values placed in `definitions.shared` - Replace parent stream configs that just duplicate another stream with a $ref that points to that stream instead - Return the resulting manifest back to the UI - Builder then passes this result to SchemaForm which now: - Resolves all $refs pointing to `refTargetPath`, which is the `definitions.shared` field - RefsHandler keeps track of where those $refs were pointing and propagates changes back and forth between the shared components (most of this logic was already implemented - this PR just cleans up some parts of this logic) - Parent stream $refs are left alone since they aren't pointing to `refTargetPath` - this is desired since a follow-up PR adds an override to the parent stream field to show a dropdown for selecting the parent stream name, and $refs are used for this in the UI logic - When saving the manifest to the database, publishing, or exporting YAML, the Builder uses the new `exportValuesWithRefs` method from RefsHandler to re-insert the $refs for the linked fields so that the manifests stay concise and non-duplicative - If the user switches to YAML mode and makes changes, the updated manifest is again passed to `/resolve` with `should_normalize: true` to get the normalized manifest, and then the new `resetFormAndRefState` method in RefsHandler is used to update the ref mappings and form state to reflect the newly-normalized manifest, as if it was loaded for the first time like the above bullets described. ## Testing Note that the deploy preview is not enough to test this since this is based on other branches that modify backend services. To test this, you can deploy locally and see that parent stream $refs and linkable field $refs are properly returned by the `/resolve` call, and that the Builder UI handles this gracefully.
1 parent 7b74e6f commit 6d96913

File tree

11 files changed

+266
-135
lines changed

11 files changed

+266
-135
lines changed

airbyte-webapp/src/components/connectorBuilder/MenuBar/DownloadYamlButton.tsx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
import snakeCase from "lodash/snakeCase";
22
import { FormattedMessage } from "react-intl";
33

4+
import { useRefsHandler } from "components/forms/SchemaForm/RefsHandler";
45
import { Button } from "components/ui/Button";
56
import { FlexContainer } from "components/ui/Flex";
67
import { Icon } from "components/ui/Icon";
78
import { Tooltip } from "components/ui/Tooltip";
89

910
import { Action, Namespace, useAnalyticsService } from "core/services/analytics";
1011
import { downloadFile, FILE_TYPE_DOWNLOAD } from "core/utils/file";
12+
import { removeEmptyProperties } from "core/utils/form";
1113
import { useConnectorBuilderFormState } from "services/connectorBuilder/ConnectorBuilderStateService";
1214

1315
import styles from "./DownloadYamlButton.module.scss";
@@ -24,13 +26,14 @@ export const DownloadYamlButton: React.FC<DownloadYamlButtonProps> = ({ classNam
2426
const { validateAndTouch } = useBuilderErrors();
2527
const connectorNameField = useBuilderWatch("name");
2628
const { yamlIsValid } = useConnectorBuilderFormState();
27-
const manifest = useBuilderWatch("manifest");
29+
const { exportValuesWithRefs } = useRefsHandler();
2830
const yaml = useBuilderWatch("yaml");
2931
const mode = useBuilderWatch("mode");
3032
const { hasErrors } = useBuilderErrors();
3133

3234
const downloadYaml = () => {
33-
const yamlToDownload = mode === "ui" ? convertJsonToYaml(manifest) : yaml;
35+
const yamlToDownload =
36+
mode === "ui" ? convertJsonToYaml(removeEmptyProperties(exportValuesWithRefs().manifest, true)) : yaml;
3437
const file = new Blob([yamlToDownload], { type: FILE_TYPE_DOWNLOAD });
3538
downloadFile(file, connectorNameField ? `${snakeCase(connectorNameField)}.yaml` : "connector_builder.yaml");
3639
analyticsService.track(Namespace.CONNECTOR_BUILDER, Action.DOWNLOAD_YAML, {

airbyte-webapp/src/components/connectorBuilder/StreamTestingPanel/StreamTester.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -253,7 +253,9 @@ export const StreamTester: React.FC<{
253253
<Text bold>{resolveErrorMessage}</Text>
254254
{errorExceptionStack && (
255255
<Collapsible label={formatMessage({ id: "connectorBuilder.tracebackLabel" })} className={styles.traceback}>
256-
<Pre longLines>{errorExceptionStack}</Pre>
256+
<Pre longLines>
257+
{isString(errorExceptionStack) ? errorExceptionStack : JSON.stringify(errorExceptionStack, null, 2)}
258+
</Pre>
257259
</Collapsible>
258260
)}
259261
<Text>

airbyte-webapp/src/components/connectorBuilder/YamlEditor/YamlManifestEditor.tsx

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import React, { useMemo, useRef, useState } from "react";
55
import { useFormContext } from "react-hook-form";
66
import { FormattedMessage } from "react-intl";
77

8+
import { useRefsHandler } from "components/forms/SchemaForm/RefsHandler";
89
import { FlexItem } from "components/ui/Flex";
910
import { ExternalLink } from "components/ui/Link";
1011
import { ButtonTab, Tabs } from "components/ui/Tabs";
@@ -39,7 +40,8 @@ export const YamlManifestEditor: React.FC = () => {
3940
setYamlIsDirty,
4041
} = useConnectorBuilderFormState();
4142
const { resolveManifest } = useConnectorBuilderResolve();
42-
const { setValue } = useFormContext();
43+
const { setValue, getValues } = useFormContext();
44+
const { resetFormAndRefState } = useRefsHandler();
4345
const yamlManifestValue = useBuilderWatch("yaml");
4446

4547
// Add a simple counter reference to track the latest call
@@ -51,17 +53,25 @@ export const YamlManifestEditor: React.FC = () => {
5153
// Increment counter to track this call
5254
const thisCallId = ++lastCallIdRef.current;
5355

54-
resolveManifest(newManifest).then((resolvedManifest) => {
56+
resolveManifest(newManifest, true).then((resolvedManifest) => {
5557
// Only update state if this is still the latest call
5658
// This prevents yamlIsDirty from being set back to false when subsequent YAML changes are made
5759
// while previous resolve calls are still in progress.
5860
if (thisCallId === lastCallIdRef.current) {
59-
setValue("manifest", resolvedManifest.manifest);
61+
// shouldNormalize is set to true in the above resolveManifest call, which can result in $refs
62+
// pointing to the refTargetPath in the result.
63+
// To ensure that the linking behavior works as expected, we must update the refMappings to account
64+
// for these $refs, and then manually resolve them when updating the manifest form state.
65+
const newBuilderState = {
66+
...getValues(),
67+
manifest: resolvedManifest.manifest,
68+
};
69+
resetFormAndRefState(newBuilderState);
6070
setYamlIsDirty(false);
6171
}
6272
});
6373
}, 500),
64-
[resolveManifest, setValue, setYamlIsDirty]
74+
[getValues, resetFormAndRefState, resolveManifest, setYamlIsDirty]
6575
);
6676

6777
const areCustomComponentsEnabled = useCustomComponentsEnabled();

airbyte-webapp/src/components/forms/SchemaForm/Controls/SchemaFormControl.tsx

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { ReactMarkdown } from "react-markdown/lib/react-markdown";
55

66
import { LabelInfo } from "components/Label";
77
import { Badge } from "components/ui/Badge";
8+
import { FlexContainer } from "components/ui/Flex";
89
import { Tooltip } from "components/ui/Tooltip";
910

1011
import { ArrayOfObjectsControl } from "./ArrayOfObjectsControl";
@@ -122,10 +123,11 @@ export const SchemaFormControl = ({
122123
<LabelInfo description={targetSchema.description} examples={targetSchema.examples} />
123124
) : undefined,
124125
optional: isOptional,
125-
header: targetSchema.deprecated ? (
126-
<DeprecatedBadge message={targetSchema.deprecation_message} />
127-
) : (
128-
<LinkComponentsToggle path={path} fieldSchema={targetSchema} />
126+
header: (
127+
<FlexContainer alignItems="center">
128+
{targetSchema.deprecated && <DeprecatedBadge message={targetSchema.deprecation_message} />}
129+
<LinkComponentsToggle path={path} fieldSchema={targetSchema} />
130+
</FlexContainer>
129131
),
130132
containerControlClassName: className,
131133
onlyShowErrorIfTouched,

0 commit comments

Comments
 (0)