Skip to content

Commit

Permalink
feat: allow forking from manifest-only connectors in Builder Fork pag…
Browse files Browse the repository at this point in the history
…e (#13671)
  • Loading branch information
lmossman committed Sep 12, 2024
1 parent 7b59c7b commit b172a40
Show file tree
Hide file tree
Showing 9 changed files with 293 additions and 113 deletions.
95 changes: 49 additions & 46 deletions airbyte-webapp/src/components/ui/ComboBox/ComboBox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export interface Option {
description?: string;
}

interface OptionSection {
export interface OptionSection {
sectionTitle?: string;
innerOptions: Option[];
}
Expand Down Expand Up @@ -53,6 +53,7 @@ export interface ComboBoxProps extends BaseProps {
disabled?: boolean;
allowCustomValue?: boolean;
optionsConfig?: OptionsConfig;
"data-testid"?: string;
}

export interface MultiComboBoxProps extends BaseProps {
Expand Down Expand Up @@ -85,23 +86,6 @@ const ComboBoxOption = ({ option }: { option: Option }) => (
</ComboboxOption>
);

const getOptionsList = ({ optionSections }: { optionSections: OptionSection[] }) => {
return optionSections.map(({ sectionTitle, innerOptions }, index) => (
<FlexContainer direction="column" key={`${sectionTitle}_${index}`} gap="none">
{sectionTitle && (
<Box p="md">
<Text size="sm" color="grey">
{sectionTitle}
</Text>
</Box>
)}
{innerOptions.map((option) => (
<ComboBoxOption key={getLabel(option)} option={option} />
))}
</FlexContainer>
));
};

const OptionsLoading = ({ message }: { message: ReactNode }) => {
return (
<FlexContainer
Expand All @@ -123,31 +107,38 @@ const OptionsInstruction = ({ message }: { message: ReactNode }) => {
);
};

const Options = React.forwardRef<HTMLDivElement, OptionsProps>(
({ optionSections, loadingMessage, instructionMessage, loading = false }, ref) => {
const { formatMessage } = useIntl();
const defaultLoadingMessage = formatMessage({ id: "ui.loading" });
const optionsList = getOptionsList({ optionSections });
if (optionSections.length === 0 && !loading) {
return null;
}
const Options: React.FC<OptionsProps> = ({ optionSections, loadingMessage, instructionMessage, loading = false }) => {
const { formatMessage } = useIntl();
const defaultLoadingMessage = formatMessage({ id: "ui.loading" });
const optionsList = optionSections.map(({ sectionTitle, innerOptions }, index) => (
<FlexContainer direction="column" key={`${sectionTitle}_${index}`} gap="none">
{sectionTitle && (
<Box p="md">
<Text size="sm" color="grey">
{sectionTitle}
</Text>
</Box>
)}
{innerOptions.map((option) => (
<ComboBoxOption key={option.value} option={option} />
))}
</FlexContainer>
));

if (loading) {
optionsList.unshift(<OptionsLoading message={loadingMessage || defaultLoadingMessage} />);
}
if (optionSections.length === 0 && !loading) {
return null;
}

if (instructionMessage) {
optionsList.unshift(<OptionsInstruction message={instructionMessage} />);
}
if (loading) {
optionsList.unshift(<OptionsLoading message={loadingMessage || defaultLoadingMessage} />);
}

return (
<ComboboxOptions ref={ref} as="ul" className={styles.optionsMenu} modal={false}>
{optionsList}
</ComboboxOptions>
);
if (instructionMessage) {
optionsList.unshift(<OptionsInstruction message={instructionMessage} />);
}
);
Options.displayName = "Options";

return <>{optionsList}</>;
};

const normalizeOptionsAsSections = (options: Option[] | OptionSection[]): OptionSection[] => {
if (options.length === 0) {
Expand Down Expand Up @@ -210,6 +201,7 @@ export const ComboBox = ({
optionsConfig,
filterOptions = true,
allowCustomValue,
"data-testid": testId,
}: ComboBoxProps) => {
// Stores the value that the user types in to filter the options
const [query, setQuery] = useState("");
Expand All @@ -230,12 +222,14 @@ export const ComboBox = ({
return value;
}

return undefined;
return "";
}, [allowCustomValue, inputOptionSections, query, value]);

const displayOptionSections = useMemo(() => {
const nonEmptyOptionSections = inputOptionSections.filter((section) => section.innerOptions.length > 0);

const filteredOptionSections =
filterOptions && query ? filterOptionSectionsByQuery(inputOptionSections, query) : inputOptionSections;
filterOptions && query ? filterOptionSectionsByQuery(nonEmptyOptionSections, query) : nonEmptyOptionSections;

const shouldAddCustomValue =
allowCustomValue && currentInputValue && isCustomValue(currentInputValue, filteredOptionSections);
Expand All @@ -246,23 +240,25 @@ export const ComboBox = ({

return (
<Combobox
value={value}
value={value ?? ""}
onChange={(newValue) => onChange(newValue ?? "")}
onClose={() => {
setQuery("");
}}
immediate
as="div"
data-testid={testId}
>
<Float adaptiveWidth placement="bottom-start">
<Float adaptiveWidth placement="bottom-start" as={React.Fragment}>
<ComboboxInput as={React.Fragment}>
<Input
{...fieldInputProps}
spellCheck={false}
value={currentInputValue}
error={error}
adornment={
adornment ?? (
<ComboboxButton className={styles.caretButton}>
<ComboboxButton className={styles.caretButton} data-testid={testId ? `${testId}--button` : undefined}>
<Icon type="caretDown" />
</ComboboxButton>
)
Expand All @@ -277,13 +273,18 @@ export const ComboBox = ({
onChange(selectedOption?.value ?? newQuery);
} else if (selectedOption) {
onChange(selectedOption.value);
} else {
onChange("");
}
}}
onBlur={onBlur ? (e) => onBlur?.(e) : fieldInputProps?.onBlur}
disabled={disabled}
data-testid={testId ? `${testId}--input` : undefined}
/>
</ComboboxInput>
<Options optionSections={displayOptionSections} {...optionsConfig} />
<ComboboxOptions as="ul" className={styles.optionsMenu} modal={false}>
<Options optionSections={displayOptionSections} {...optionsConfig} />
</ComboboxOptions>
</Float>
</Combobox>
);
Expand All @@ -309,6 +310,8 @@ export const MultiComboBox = ({
disabled={disabled}
/>
</ComboboxInput>
<Options optionSections={normalizeOptionsAsSections(options)} />
<ComboboxOptions as="ul" className={styles.optionsMenu} modal={false}>
<Options optionSections={normalizeOptionsAsSections(options)} />
</ComboboxOptions>
</Combobox>
);
23 changes: 23 additions & 0 deletions airbyte-webapp/src/core/api/hooks/connectorBuilderProject.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
updateConnectorBuilderProjectTestingValues,
updateDeclarativeManifestVersion,
getDeclarativeManifestBaseImage,
createForkedConnectorBuilderProject,
} from "../generated/AirbyteClient";
import { SCOPE_WORKSPACE } from "../scopes";
import {
Expand All @@ -34,6 +35,7 @@ import {
ConnectorBuilderProjectStreamReadSlicesItemStateItem,
DeclarativeManifestRequestBody,
DeclarativeManifestBaseImageRead,
SourceDefinitionId,
} from "../types/AirbyteClient";
import { DeclarativeComponentSchema, DeclarativeStream, NoPaginationType } from "../types/ConnectorManifest";
import { useRequestOptions } from "../useRequestOptions";
Expand Down Expand Up @@ -149,6 +151,27 @@ export const useCreateBuilderProject = () => {
);
};

export const useCreateSourceDefForkedBuilderProject = () => {
const requestOptions = useRequestOptions();
const queryClient = useQueryClient();
const workspaceId = useCurrentWorkspaceId();

return useMutation<ConnectorBuilderProjectIdWithWorkspaceId, Error, SourceDefinitionId>(
async (sourceDefinitionId) => {
return createForkedConnectorBuilderProject(
{ workspaceId, baseActorDefinitionId: sourceDefinitionId },
requestOptions
);
},
{
onSuccess: () => {
// invalidate cached projects list
queryClient.invalidateQueries(connectorBuilderProjectsKeys.list(workspaceId));
},
}
);
};

export const useDeleteBuilderProject = () => {
const queryClient = useQueryClient();
const requestOptions = useRequestOptions();
Expand Down
11 changes: 7 additions & 4 deletions airbyte-webapp/src/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -1541,7 +1541,10 @@
"connectorBuilder.generatePage.firstStreamLabel": "First Data Stream",
"connectorBuilder.generatePage.firstStreamPlaceholder": "e.g. users",
"connectorBuilder.generatePage.firstStreamTooltip": "The name of the first object from the API that you want to load into your destination.",
"connectorBuilder.forkPage.prompt": "Load an existing connector",
"connectorBuilder.forkPage.prompt": "Fork an existing connector",
"connectorBuilder.forkPage.description": "Create a new builder connector based off of an existing connector",
"connectorBuilder.forkPage.connectorSelectionText.builderProject": "Copy <b>{version}</b> of <b>{name}</b> into a new custom connector.",
"connectorBuilder.forkPage.connectorSelectionText.catalogConnector": "Copy the latest <b>{name}</b> version into a new custom connector, with the option to contribute your changes back to Airbyte's catalog.",
"connectorBuilder.loadingStreamList": "Loading",
"connectorBuilder.noStreamSelected": "No stream selected",
"connectorBuilder.streamTestLimitReached": "Stream testing limit reached. During testing a maximum of {recordLimit} records, or {sliceLimit} stream partitions with {pageLimit} pages each will be returned.",
Expand All @@ -1568,9 +1571,9 @@
"connectorBuilder.deleteProjectModal.submitButton": "Delete",
"connectorBuilder.deleteProject.error": "Failed to delete custom connector: {reason}",
"connectorBuilder.deleteProject.publishedTooltip": "It's not possible to delete a builder connector after being published",
"connectorBuilder.createPage.loadExistingConnector.title": "Load an existing connector",
"connectorBuilder.createPage.loadExistingConnector.description": "Select an existing builder connector to use as a starting point",
"connectorBuilder.createPage.loadExistingConnector.button": "Load a connector",
"connectorBuilder.createPage.forkExistingConnector.title": "Fork an existing connector",
"connectorBuilder.createPage.forkExistingConnector.description": "Select an existing connector to use as a starting point",
"connectorBuilder.createPage.forkExistingConnector.button": "Fork a connector",
"connectorBuilder.forkPage.copyName": "{oldName} - copy",
"connectorBuilder.forkPage.createLabel": "Create",
"connectorBuilder.backButtonLabel": "Back",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,13 +32,16 @@ import StartFromScratchImage from "./start-from-scratch.svg?react";
import { AirbyteTitle } from "../components/AirbyteTitle";
import { BackButton } from "../components/BackButton";
import { useCreateAndNavigate } from "../components/useCreateAndNavigate";
import { useManifestOnlySourceDefinitions } from "../components/useManifestOnlySourceDefinitions";
import { ConnectorBuilderRoutePaths } from "../ConnectorBuilderRoutes";

const YAML_UPLOAD_ERROR_ID = "connectorBuilder.yamlUpload.error";

const ConnectorBuilderCreatePageInner: React.FC = () => {
const analyticsService = useAnalyticsService();
const existingProjects = useListBuilderProjects();
const { manifestOnlySourceDefinitions } = useManifestOnlySourceDefinitions();

const [activeTile, setActiveTile] = useState<"yaml" | "empty" | undefined>();
const navigate = useNavigate();

Expand Down Expand Up @@ -155,12 +158,12 @@ const ConnectorBuilderCreatePageInner: React.FC = () => {
}}
dataTestId="import-yaml"
/>
{existingProjects.length > 0 && (
{(existingProjects.length > 0 || manifestOnlySourceDefinitions.length > 0) && (
<Tile
image={<LoadExistingConnectorImage />}
title="connectorBuilder.createPage.loadExistingConnector.title"
description="connectorBuilder.createPage.loadExistingConnector.description"
buttonText="connectorBuilder.createPage.loadExistingConnector.button"
title="connectorBuilder.createPage.forkExistingConnector.title"
description="connectorBuilder.createPage.forkExistingConnector.description"
buttonText="connectorBuilder.createPage.forkExistingConnector.button"
buttonProps={{ disabled: buttonsDisabledState }}
onClick={() => {
navigate(`../${ConnectorBuilderRoutePaths.Fork}`);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,10 @@
.form {
padding: variables.$spacing-xl;
}

$iconWidth: 20px;

.connectorIcon {
width: $iconWidth;
height: $iconWidth;
}
Loading

0 comments on commit b172a40

Please sign in to comment.