Skip to content

Commit 70ee071

Browse files
feat: move serviceType selection out of serviceForm
1 parent 19c33b0 commit 70ee071

File tree

22 files changed

+590
-179
lines changed

22 files changed

+590
-179
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { useState } from "react";
2+
import { useToggle } from "react-use";
3+
4+
import { ServiceTypeDropdown } from "components/ServiceTypeDropdown/ServiceTypeDropdown";
5+
6+
import RequestConnectorModal from "views/Connector/RequestConnectorModal";
7+
8+
import { ServiceTypeControlProps } from "../ServiceTypeDropdown/ServiceTypeDropdown";
9+
10+
const SelectServiceType: React.FC<Omit<ServiceTypeControlProps, "onOpenRequestConnectorModal">> = ({
11+
formType,
12+
...restProps
13+
}) => {
14+
const [isOpenRequestModal, toggleOpenRequestModal] = useToggle(false);
15+
const [initialRequestName, setInitialRequestName] = useState<string>();
16+
17+
return (
18+
<>
19+
<ServiceTypeDropdown
20+
formType={formType}
21+
onOpenRequestConnectorModal={(name) => {
22+
setInitialRequestName(name);
23+
toggleOpenRequestModal();
24+
}}
25+
{...restProps}
26+
/>
27+
{isOpenRequestModal && (
28+
<RequestConnectorModal
29+
connectorType="source"
30+
initialName={initialRequestName}
31+
onClose={toggleOpenRequestModal}
32+
/>
33+
)}
34+
</>
35+
);
36+
};
37+
38+
export { SelectServiceType };
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
@import "../../scss/variables";
2+
3+
.buttonElement {
4+
background: $grey-30;
5+
padding: 6px 16px 8px;
6+
width: 100%;
7+
min-height: 34px;
8+
border-top: 1px solid $grey-200;
9+
}
10+
11+
.block {
12+
cursor: pointer;
13+
color: $dark-blue;
14+
15+
&:hover {
16+
color: $blue;
17+
}
18+
}
19+
20+
.text {
21+
display: flex;
22+
flex-direction: row;
23+
align-items: center;
24+
}
25+
26+
.label {
27+
margin-left: $spacing-l;
28+
font-weight: 500;
29+
font-size: 14px;
30+
line-height: 17px;
31+
}
32+
33+
.stage {
34+
padding: 2px 6px;
35+
height: 14px;
36+
background: $grey-100;
37+
border-radius: 25px;
38+
text-transform: uppercase;
39+
font-weight: 500;
40+
font-size: 8px;
41+
line-height: 10px;
42+
color: $dark-blue;
43+
}
44+
45+
.singleValueView {
46+
display: flex;
47+
flex-direction: row;
48+
justify-content: left;
49+
align-items: center;
50+
}
51+
52+
.singleValueContent {
53+
width: 100%;
54+
padding-right: 38px;
55+
display: flex;
56+
flex-direction: row;
57+
justify-content: space-between;
58+
align-items: center;
59+
}
60+
61+
.icon {
62+
margin-right: 6px;
63+
display: inline-block;
64+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,214 @@
1+
import React, { useCallback, useEffect, useMemo } from "react";
2+
import { FormattedMessage, useIntl } from "react-intl";
3+
import { components } from "react-select";
4+
import { MenuListComponentProps } from "react-select/src/components/Menu";
5+
6+
import { ControlLabels, DropDown, DropDownRow } from "components";
7+
import { IDataItem, IProps as OptionProps, OptionView } from "components/base/DropDown/components/Option";
8+
import { IProps as SingleValueProps } from "components/base/DropDown/components/SingleValue";
9+
import { ConnectorIcon } from "components/ConnectorIcon";
10+
import { GAIcon } from "components/icons/GAIcon";
11+
12+
import { Connector, ConnectorDefinition } from "core/domain/connector";
13+
import { ReleaseStage } from "core/request/AirbyteClient";
14+
import { useAvailableConnectorDefinitions } from "hooks/domain/connector/useAvailableConnectorDefinitions";
15+
import { useAnalyticsService } from "hooks/services/Analytics";
16+
import { useExperiment } from "hooks/services/Experiment";
17+
import { useCurrentWorkspace } from "hooks/services/useWorkspace";
18+
import { naturalComparator } from "utils/objects";
19+
import { useDocumentationPanelContext } from "views/Connector/ConnectorDocumentationLayout/DocumentationPanelContext";
20+
21+
import styles from "./ServiceTypeDropdown.module.scss";
22+
23+
type MenuWithRequestButtonProps = MenuListComponentProps<IDataItem, false>;
24+
25+
/**
26+
* Returns the order for a specific release stage label. This will define
27+
* in what order the different release stages are shown inside the select.
28+
* They will be shown in an increasing order (i.e. 0 on top), unless not overwritten
29+
* by ORDER_OVERWRITE above.
30+
*/
31+
function getOrderForReleaseStage(stage?: ReleaseStage): number {
32+
switch (stage) {
33+
case ReleaseStage.beta:
34+
return 1;
35+
case ReleaseStage.alpha:
36+
return 2;
37+
default:
38+
return 0;
39+
}
40+
}
41+
42+
const ConnectorList: React.FC<MenuWithRequestButtonProps> = ({ children, ...props }) => (
43+
<>
44+
<components.MenuList {...props}>{children}</components.MenuList>
45+
<div className={styles.buttonElement}>
46+
<div
47+
className={styles.block}
48+
onClick={() => props.selectProps.selectProps.onOpenRequestConnectorModal(props.selectProps.inputValue)}
49+
>
50+
<FormattedMessage id="connector.requestConnectorBlock" />
51+
</div>
52+
</div>
53+
</>
54+
);
55+
56+
const StageLabel: React.FC<{ releaseStage?: ReleaseStage }> = ({ releaseStage }) => {
57+
if (!releaseStage) {
58+
return null;
59+
}
60+
61+
if (releaseStage === ReleaseStage.generally_available) {
62+
return <GAIcon />;
63+
}
64+
65+
return (
66+
<div className={styles.stage}>
67+
<FormattedMessage id={`connector.releaseStage.${releaseStage}`} defaultMessage={releaseStage} />
68+
</div>
69+
);
70+
};
71+
72+
const Option: React.FC<OptionProps> = (props) => {
73+
return (
74+
<components.Option {...props}>
75+
<OptionView data-testid={props.data.label} isSelected={props.isSelected} isDisabled={props.isDisabled}>
76+
<div className={styles.text}>
77+
{props.data.img || null}
78+
<div className={styles.label}>{props.label}</div>
79+
</div>
80+
<StageLabel releaseStage={props.data.releaseStage} />
81+
</OptionView>
82+
</components.Option>
83+
);
84+
};
85+
86+
const SingleValue: React.FC<SingleValueProps> = (props) => {
87+
return (
88+
<div className={styles.singleValueView}>
89+
{props.data.img && <div className={styles.icon}>{props.data.img}</div>}
90+
<div>
91+
<div {...props} className={styles.singleValueContent}>
92+
{props.data.label}
93+
<StageLabel releaseStage={props.data.releaseStage} />
94+
</div>
95+
</div>
96+
</div>
97+
);
98+
};
99+
100+
export interface ServiceTypeControlProps {
101+
formType: "source" | "destination";
102+
availableServices: ConnectorDefinition[];
103+
isEditMode?: boolean;
104+
documentationUrl?: string;
105+
value?: string | null;
106+
onChangeServiceType?: (id: string) => void;
107+
onOpenRequestConnectorModal: (initialName: string) => void;
108+
disabled?: boolean;
109+
}
110+
111+
const ServiceTypeDropdown: React.FC<ServiceTypeControlProps> = ({
112+
formType,
113+
isEditMode,
114+
onChangeServiceType,
115+
availableServices,
116+
documentationUrl,
117+
onOpenRequestConnectorModal,
118+
disabled,
119+
value,
120+
}) => {
121+
const { formatMessage } = useIntl();
122+
const orderOverwrite = useExperiment("connector.orderOverwrite", {});
123+
const analytics = useAnalyticsService();
124+
const workspace = useCurrentWorkspace();
125+
const availableConnectorDefinitions = useAvailableConnectorDefinitions(availableServices, workspace);
126+
const sortedDropDownData = useMemo(
127+
() =>
128+
availableConnectorDefinitions
129+
.map((item) => ({
130+
label: item.name,
131+
value: Connector.id(item),
132+
img: <ConnectorIcon icon={item.icon} />,
133+
releaseStage: item.releaseStage,
134+
}))
135+
.sort((a, b) => {
136+
const priorityA = orderOverwrite[a.value] ?? 0;
137+
const priorityB = orderOverwrite[b.value] ?? 0;
138+
// If they have different priority use the higher priority first, otherwise use the label
139+
if (priorityA !== priorityB) {
140+
return priorityB - priorityA;
141+
} else if (a.releaseStage !== b.releaseStage) {
142+
return getOrderForReleaseStage(a.releaseStage) - getOrderForReleaseStage(b.releaseStage);
143+
}
144+
return naturalComparator(a.label, b.label);
145+
}),
146+
// eslint-disable-next-line react-hooks/exhaustive-deps
147+
[availableServices, orderOverwrite]
148+
);
149+
150+
const { setDocumentationUrl } = useDocumentationPanelContext();
151+
152+
useEffect(() => setDocumentationUrl(documentationUrl ?? ""), [documentationUrl, setDocumentationUrl]);
153+
154+
const getNoOptionsMessage = useCallback(
155+
({ inputValue }: { inputValue: string }) => {
156+
analytics.track(
157+
formType === "source"
158+
? "Airbyte.UI.NewSource.NoMatchingConnector"
159+
: "Airbyte.UI.NewDestination.NoMatchingConnector",
160+
{
161+
query: inputValue,
162+
}
163+
);
164+
return formatMessage({ id: "form.noConnectorFound" });
165+
},
166+
[analytics, formType, formatMessage]
167+
);
168+
169+
const handleSelect = useCallback(
170+
(item: DropDownRow.IDataItem | null) => {
171+
if (item) {
172+
if (onChangeServiceType) {
173+
onChangeServiceType(item.value);
174+
}
175+
}
176+
},
177+
[onChangeServiceType]
178+
);
179+
180+
const onMenuOpen = () => {
181+
const eventName =
182+
formType === "source" ? "Airbyte.UI.NewSource.SelectionOpened" : "Airbyte.UI.NewDestination.SelectionOpened";
183+
analytics.track(eventName, {});
184+
};
185+
186+
return (
187+
<ControlLabels
188+
label={formatMessage({
189+
id: `form.${formType}Type`,
190+
})}
191+
>
192+
<DropDown
193+
value={value}
194+
components={{
195+
MenuList: ConnectorList,
196+
Option,
197+
SingleValue,
198+
}}
199+
selectProps={{ onOpenRequestConnectorModal }}
200+
isDisabled={isEditMode || disabled}
201+
isSearchable
202+
placeholder={formatMessage({
203+
id: "form.selectConnector",
204+
})}
205+
options={sortedDropDownData}
206+
onChange={handleSelect}
207+
onMenuOpen={onMenuOpen}
208+
noOptionsMessage={getNoOptionsMessage}
209+
/>
210+
</ControlLabels>
211+
);
212+
};
213+
214+
export { ServiceTypeDropdown };
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
@import "../../scss/variables";
2+
3+
.titleBlock {
4+
display: flex;
5+
align-items: center;
6+
justify-content: space-between;
7+
}
8+
9+
.titleContainer {
10+
font-size: 11px;
11+
line-height: 13px;
12+
color: $grey-400;
13+
white-space: pre-line;
14+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import React from "react";
2+
3+
import { H5 } from "components";
4+
5+
import styles from "./TitleBlock.module.scss";
6+
7+
interface IProps {
8+
title: React.ReactElement;
9+
actions?: React.ReactElement;
10+
}
11+
12+
const TitleBlock: React.FC<IProps> = ({ title, actions }) => {
13+
return (
14+
<div className={styles.titleBlock}>
15+
<div className={styles.titleContainer}>
16+
<H5 bold>{title}</H5>
17+
</div>
18+
{actions && <div>{actions}</div>}
19+
</div>
20+
);
21+
};
22+
23+
export default TitleBlock;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
@import "../../../../../scss/variables";
2+
3+
.contentCard {
4+
margin-bottom: $spacing-xl;
5+
padding: $spacing-xl $spacing-xl $spacing-xl;
6+
}
7+
8+
.serviceTypeContainer {
9+
margin-top: $spacing-l;
10+
}

0 commit comments

Comments
 (0)