Skip to content

feat: move serviceType selection out of serviceForm #14526

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { useState } from "react";
import { useToggle } from "react-use";

import { ServiceTypeDropdown } from "components/ServiceTypeDropdown/ServiceTypeDropdown";

import RequestConnectorModal from "views/Connector/RequestConnectorModal";

import { ServiceTypeControlProps } from "../ServiceTypeDropdown/ServiceTypeDropdown";

const SelectServiceType: React.FC<Omit<ServiceTypeControlProps, "onOpenRequestConnectorModal">> = ({
formType,
...restProps
}) => {
const [isOpenRequestModal, toggleOpenRequestModal] = useToggle(false);
const [initialRequestName, setInitialRequestName] = useState<string>();

return (
<>
<ServiceTypeDropdown
formType={formType}
onOpenRequestConnectorModal={(name) => {
setInitialRequestName(name);
toggleOpenRequestModal();
}}
{...restProps}
/>
{isOpenRequestModal && (
<RequestConnectorModal
connectorType="source"
initialName={initialRequestName}
onClose={toggleOpenRequestModal}
/>
)}
</>
);
};

export { SelectServiceType };
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
@import "../../scss/variables";

.buttonElement {
background: $grey-30;
padding: 6px 16px 8px;
width: 100%;
min-height: 34px;
border-top: 1px solid $grey-200;
}

.block {
cursor: pointer;
color: $dark-blue;

&:hover {
color: $blue;
}
}

.text {
display: flex;
flex-direction: row;
align-items: center;
}

.label {
margin-left: $spacing-l;
font-weight: 500;
font-size: 14px;
line-height: 17px;
}

.stage {
padding: 2px 6px;
height: 14px;
background: $grey-100;
border-radius: 25px;
text-transform: uppercase;
font-weight: 500;
font-size: 8px;
line-height: 10px;
color: $dark-blue;
}

.singleValueView {
display: flex;
flex-direction: row;
justify-content: left;
align-items: center;
}

.singleValueContent {
width: 100%;
padding-right: 38px;
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
}

.icon {
margin-right: 6px;
display: inline-block;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,214 @@
import React, { useCallback, useEffect, useMemo } from "react";
import { FormattedMessage, useIntl } from "react-intl";
import { components } from "react-select";
import { MenuListComponentProps } from "react-select/src/components/Menu";

import { ControlLabels, DropDown, DropDownRow } from "components";
import { IDataItem, IProps as OptionProps, OptionView } from "components/base/DropDown/components/Option";
import { IProps as SingleValueProps } from "components/base/DropDown/components/SingleValue";
import { ConnectorIcon } from "components/ConnectorIcon";
import { GAIcon } from "components/icons/GAIcon";

import { Connector, ConnectorDefinition } from "core/domain/connector";
import { ReleaseStage } from "core/request/AirbyteClient";
import { useAvailableConnectorDefinitions } from "hooks/domain/connector/useAvailableConnectorDefinitions";
import { useAnalyticsService } from "hooks/services/Analytics";
import { useExperiment } from "hooks/services/Experiment";
import { useCurrentWorkspace } from "hooks/services/useWorkspace";
import { naturalComparator } from "utils/objects";
import { useDocumentationPanelContext } from "views/Connector/ConnectorDocumentationLayout/DocumentationPanelContext";

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

type MenuWithRequestButtonProps = MenuListComponentProps<IDataItem, false>;

/**
* Returns the order for a specific release stage label. This will define
* in what order the different release stages are shown inside the select.
* They will be shown in an increasing order (i.e. 0 on top), unless not overwritten
* by ORDER_OVERWRITE above.
*/
function getOrderForReleaseStage(stage?: ReleaseStage): number {
switch (stage) {
case ReleaseStage.beta:
return 1;
case ReleaseStage.alpha:
return 2;
default:
return 0;
}
}

const ConnectorList: React.FC<MenuWithRequestButtonProps> = ({ children, ...props }) => (
<>
<components.MenuList {...props}>{children}</components.MenuList>
<div className={styles.buttonElement}>
<div
className={styles.block}
onClick={() => props.selectProps.selectProps.onOpenRequestConnectorModal(props.selectProps.inputValue)}
>
<FormattedMessage id="connector.requestConnectorBlock" />
</div>
</div>
</>
);

const StageLabel: React.FC<{ releaseStage?: ReleaseStage }> = ({ releaseStage }) => {
if (!releaseStage) {
return null;
}

if (releaseStage === ReleaseStage.generally_available) {
return <GAIcon />;
}

return (
<div className={styles.stage}>
<FormattedMessage id={`connector.releaseStage.${releaseStage}`} defaultMessage={releaseStage} />
</div>
);
};

const Option: React.FC<OptionProps> = (props) => {
return (
<components.Option {...props}>
<OptionView data-testid={props.data.label} isSelected={props.isSelected} isDisabled={props.isDisabled}>
<div className={styles.text}>
{props.data.img || null}
<div className={styles.label}>{props.label}</div>
</div>
<StageLabel releaseStage={props.data.releaseStage} />
</OptionView>
</components.Option>
);
};

const SingleValue: React.FC<SingleValueProps> = (props) => {
return (
<div className={styles.singleValueView}>
{props.data.img && <div className={styles.icon}>{props.data.img}</div>}
<div>
<div {...props} className={styles.singleValueContent}>
{props.data.label}
<StageLabel releaseStage={props.data.releaseStage} />
</div>
</div>
</div>
);
};

export interface ServiceTypeControlProps {
formType: "source" | "destination";
availableServices: ConnectorDefinition[];
isEditMode?: boolean;
documentationUrl?: string;
value?: string | null;
onChangeServiceType?: (id: string) => void;
onOpenRequestConnectorModal: (initialName: string) => void;
disabled?: boolean;
}

const ServiceTypeDropdown: React.FC<ServiceTypeControlProps> = ({
formType,
isEditMode,
onChangeServiceType,
availableServices,
documentationUrl,
onOpenRequestConnectorModal,
disabled,
value,
}) => {
const { formatMessage } = useIntl();
const orderOverwrite = useExperiment("connector.orderOverwrite", {});
const analytics = useAnalyticsService();
const workspace = useCurrentWorkspace();
const availableConnectorDefinitions = useAvailableConnectorDefinitions(availableServices, workspace);
const sortedDropDownData = useMemo(
() =>
availableConnectorDefinitions
.map((item) => ({
label: item.name,
value: Connector.id(item),
img: <ConnectorIcon icon={item.icon} />,
releaseStage: item.releaseStage,
}))
.sort((a, b) => {
const priorityA = orderOverwrite[a.value] ?? 0;
const priorityB = orderOverwrite[b.value] ?? 0;
// If they have different priority use the higher priority first, otherwise use the label
if (priorityA !== priorityB) {
return priorityB - priorityA;
} else if (a.releaseStage !== b.releaseStage) {
return getOrderForReleaseStage(a.releaseStage) - getOrderForReleaseStage(b.releaseStage);
}
return naturalComparator(a.label, b.label);
}),
// eslint-disable-next-line react-hooks/exhaustive-deps
[availableServices, orderOverwrite]
);

const { setDocumentationUrl } = useDocumentationPanelContext();

useEffect(() => setDocumentationUrl(documentationUrl ?? ""), [documentationUrl, setDocumentationUrl]);

const getNoOptionsMessage = useCallback(
({ inputValue }: { inputValue: string }) => {
analytics.track(
formType === "source"
? "Airbyte.UI.NewSource.NoMatchingConnector"
: "Airbyte.UI.NewDestination.NoMatchingConnector",
{
query: inputValue,
}
);
return formatMessage({ id: "form.noConnectorFound" });
},
[analytics, formType, formatMessage]
);

const handleSelect = useCallback(
(item: DropDownRow.IDataItem | null) => {
if (item) {
if (onChangeServiceType) {
onChangeServiceType(item.value);
}
}
},
[onChangeServiceType]
);

const onMenuOpen = () => {
const eventName =
formType === "source" ? "Airbyte.UI.NewSource.SelectionOpened" : "Airbyte.UI.NewDestination.SelectionOpened";
analytics.track(eventName, {});
};

return (
<ControlLabels
label={formatMessage({
id: `form.${formType}Type`,
})}
>
<DropDown
value={value}
components={{
MenuList: ConnectorList,
Option,
SingleValue,
}}
selectProps={{ onOpenRequestConnectorModal }}
isDisabled={isEditMode || disabled}
isSearchable
placeholder={formatMessage({
id: "form.selectConnector",
})}
options={sortedDropDownData}
onChange={handleSelect}
onMenuOpen={onMenuOpen}
noOptionsMessage={getNoOptionsMessage}
/>
</ControlLabels>
);
};

export { ServiceTypeDropdown };
14 changes: 14 additions & 0 deletions airbyte-webapp/src/components/TitleBlock/TitleBlock.module.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
@import "../../scss/variables";

.titleBlock {
display: flex;
align-items: center;
justify-content: space-between;
}

.titleContainer {
font-size: 11px;
line-height: 13px;
color: $grey-400;
white-space: pre-line;
}
23 changes: 23 additions & 0 deletions airbyte-webapp/src/components/TitleBlock/TitleBlock.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import React from "react";

import { H5 } from "components";

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

interface IProps {
title: React.ReactElement;
actions?: React.ReactElement;
}

const TitleBlock: React.FC<IProps> = ({ title, actions }) => {
return (
<div className={styles.titleBlock}>
<div className={styles.titleContainer}>
<H5 bold>{title}</H5>
</div>
{actions && <div>{actions}</div>}
</div>
);
};

export default TitleBlock;
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
@import "../../../../../scss/variables";

.contentCard {
margin-bottom: $spacing-xl;
padding: $spacing-xl $spacing-xl $spacing-xl;
}

.serviceTypeContainer {
margin-top: $spacing-l;
}
Loading