Skip to content

Commit 4352686

Browse files
author
Joey Marshment-Howell
authored
🪟 🎉 Multi-cloud: edit connection & workspace data geographies in the UI (#18611)
* add geographies service and basic dropdown * add feature for changing data geography * add default data residency to workspace settings * add data residency card to connection creation * add data residency editing to connection settings tab
1 parent 8145be5 commit 4352686

File tree

36 files changed

+486
-42
lines changed

36 files changed

+486
-42
lines changed

‎airbyte-webapp/src/components/CreateConnection/CreateConnectionForm.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import {
2525

2626
import styles from "./CreateConnectionForm.module.scss";
2727
import { CreateConnectionNameField } from "./CreateConnectionNameField";
28+
import { DataResidency } from "./DataResidency";
2829
import { SchemaError } from "./SchemaError";
2930

3031
interface CreateConnectionProps {
@@ -39,7 +40,7 @@ interface CreateConnectionPropsInner extends Pick<CreateConnectionProps, "afterS
3940

4041
const CreateConnectionFormInner: React.FC<CreateConnectionPropsInner> = ({ schemaError, afterSubmitConnection }) => {
4142
const navigate = useNavigate();
42-
43+
const canEditDataGeographies = useFeature(FeatureItem.AllowChangeDataGeographies);
4344
const { mutateAsync: createConnection } = useCreateConnection();
4445

4546
const { clearFormChange } = useFormChangeTrackerService();
@@ -113,6 +114,7 @@ const CreateConnectionFormInner: React.FC<CreateConnectionPropsInner> = ({ schem
113114
{({ values, isSubmitting, isValid, dirty }) => (
114115
<Form>
115116
<CreateConnectionNameField />
117+
{canEditDataGeographies && <DataResidency />}
116118
<ConnectionFormFields values={values} isSubmitting={isSubmitting} dirty={dirty} />
117119
<OperationsSection
118120
onStartEditTransformation={() => setEditingTransformation(true)}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
@use "scss/variables";
2+
3+
.flexRow {
4+
display: flex;
5+
flex-direction: row;
6+
justify-content: flex-start;
7+
align-items: flex-start;
8+
gap: variables.$spacing-md;
9+
}
10+
11+
.leftFieldCol {
12+
flex: 1;
13+
max-width: 640px;
14+
padding-right: 30px;
15+
}
16+
17+
.rightFieldCol {
18+
flex: 1;
19+
max-width: 300px;
20+
}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import { Field, FieldProps, useFormikContext } from "formik";
2+
import { FormattedMessage, useIntl } from "react-intl";
3+
4+
import { ControlLabels } from "components/LabeledControl";
5+
import { DropDown } from "components/ui/DropDown";
6+
7+
import { useAvailableGeographies } from "packages/cloud/services/geographies/GeographiesService";
8+
import { links } from "utils/links";
9+
import { Section } from "views/Connection/ConnectionForm/components/Section";
10+
11+
import styles from "./DataResidency.module.scss";
12+
13+
interface DataResidencyProps {
14+
name?: string;
15+
}
16+
17+
export const DataResidency: React.FC<DataResidencyProps> = ({ name = "geography" }) => {
18+
const { formatMessage } = useIntl();
19+
const { setFieldValue } = useFormikContext();
20+
const { geographies } = useAvailableGeographies();
21+
22+
return (
23+
<Section title={formatMessage({ id: "connection.geographyTitle" })}>
24+
<Field name={name}>
25+
{({ field, form }: FieldProps<string>) => (
26+
<div className={styles.flexRow}>
27+
<div className={styles.leftFieldCol}>
28+
<ControlLabels
29+
nextLine
30+
label={<FormattedMessage id="connection.geographyTitle" />}
31+
message={
32+
<FormattedMessage
33+
id="connection.geographyDescription"
34+
values={{
35+
lnk: (node: React.ReactNode) => (
36+
<a href={links.cloudAllowlistIPsLink} target="_blank" rel="noreferrer">
37+
{node}
38+
</a>
39+
),
40+
}}
41+
/>
42+
}
43+
/>
44+
</div>
45+
<div className={styles.rightFieldCol}>
46+
<DropDown
47+
isDisabled={form.isSubmitting}
48+
options={geographies.map((geography) => ({
49+
label: formatMessage({
50+
id: `connection.geography.${geography}`,
51+
defaultMessage: geography.toUpperCase(),
52+
}),
53+
value: geography,
54+
}))}
55+
value={field.value}
56+
onChange={(geography) => setFieldValue(name, geography.value)}
57+
/>
58+
</div>
59+
</div>
60+
)}
61+
</Field>
62+
</Section>
63+
);
64+
};

‎airbyte-webapp/src/components/Label/Label.tsx

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,20 +6,18 @@ interface IProps {
66
nextLine?: boolean;
77
success?: boolean;
88
message?: string | React.ReactNode;
9-
additionLength?: number;
109
className?: string;
1110
onClick?: (data: unknown) => void;
1211
htmlFor?: string;
1312
}
1413

15-
const Content = styled.label<{ additionLength?: number | string }>`
14+
const Content = styled.label`
1615
display: block;
1716
font-weight: 500;
1817
font-size: 14px;
1918
line-height: 17px;
2019
color: ${({ theme }) => theme.textColor};
2120
padding-bottom: 5px;
22-
width: calc(100% + ${({ additionLength }) => (additionLength === 0 || additionLength ? additionLength : 30)}px);
2321
2422
& a {
2523
text-decoration: underline;
@@ -31,16 +29,18 @@ const MessageText = styled.span<Pick<IProps, "error" | "success">>`
3129
white-space: break-spaces;
3230
color: ${(props) =>
3331
props.error ? props.theme.dangerColor : props.success ? props.theme.successColor : props.theme.greyColor40};
34-
font-size: 13px;
32+
font-size: 12px;
33+
font-weight: 400;
34+
35+
a:link,
36+
a:hover,
37+
a:visited {
38+
color: ${(props) => props.theme.greyColor40};
39+
}
3540
`;
3641

3742
const Label: React.FC<React.PropsWithChildren<IProps>> = (props) => (
38-
<Content
39-
additionLength={props.additionLength}
40-
className={props.className}
41-
onClick={props.onClick}
42-
htmlFor={props.htmlFor}
43-
>
43+
<Content className={props.className} onClick={props.onClick} htmlFor={props.htmlFor}>
4444
{props.children}
4545
{props.message && (
4646
<span>

‎airbyte-webapp/src/components/LabeledControl/ControlLabels.tsx

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@ export interface ControlLabelsProps {
1414
success?: boolean;
1515
nextLine?: boolean;
1616
message?: React.ReactNode;
17-
labelAdditionLength?: number;
1817
label?: React.ReactNode;
1918
infoTooltipContent?: React.ReactNode;
2019
optional?: boolean;
@@ -27,7 +26,6 @@ const ControlLabels: React.FC<React.PropsWithChildren<ControlLabelsProps>> = (pr
2726
error={props.error}
2827
success={props.success}
2928
message={props.message}
30-
additionLength={props.labelAdditionLength}
3129
nextLine={props.nextLine}
3230
htmlFor={props.htmlFor}
3331
>

‎airbyte-webapp/src/components/LabeledInput/LabeledInput.tsx

Lines changed: 3 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -3,26 +3,11 @@ import React from "react";
33
import { ControlLabels, ControlLabelsProps } from "components/LabeledControl";
44
import { Input, InputProps } from "components/ui/Input";
55

6-
type LabeledInputProps = Pick<ControlLabelsProps, "success" | "message" | "label" | "labelAdditionLength"> &
6+
type LabeledInputProps = Pick<ControlLabelsProps, "success" | "message" | "label"> &
77
InputProps & { className?: string };
88

9-
const LabeledInput: React.FC<LabeledInputProps> = ({
10-
error,
11-
success,
12-
message,
13-
label,
14-
labelAdditionLength,
15-
className,
16-
...inputProps
17-
}) => (
18-
<ControlLabels
19-
error={error}
20-
success={success}
21-
message={message}
22-
label={label}
23-
className={className}
24-
labelAdditionLength={labelAdditionLength}
25-
>
9+
const LabeledInput: React.FC<LabeledInputProps> = ({ error, success, message, label, className, ...inputProps }) => (
10+
<ControlLabels error={error} success={success} message={message} label={label} className={className}>
2611
<Input {...inputProps} error={error} />
2712
</ControlLabels>
2813
);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
.wrapper {
2+
display: flex;
3+
align-items: center;
4+
justify-content: space-between;
5+
}
6+
7+
.dropdownWrapper {
8+
display: flex;
9+
flex: 1 0 310px;
10+
}
11+
12+
.spinner {
13+
width: 50px;
14+
display: flex;
15+
justify-content: center;
16+
align-items: center;
17+
}
18+
19+
.dropdown {
20+
flex-grow: 1;
21+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import React, { useState } from "react";
2+
import { FormattedMessage, useIntl } from "react-intl";
3+
4+
import { ControlLabels } from "components/LabeledControl";
5+
import { Card } from "components/ui/Card";
6+
import { DropDown } from "components/ui/DropDown";
7+
import { Spinner } from "components/ui/Spinner";
8+
9+
import { Geography } from "core/request/AirbyteClient";
10+
import { useConnectionEditService } from "hooks/services/ConnectionEdit/ConnectionEditService";
11+
import { useNotificationService } from "hooks/services/Notification";
12+
import { useAvailableGeographies } from "packages/cloud/services/geographies/GeographiesService";
13+
import { links } from "utils/links";
14+
15+
import styles from "./UpdateConnectionDataResidency.module.scss";
16+
17+
export const UpdateConnectionDataResidency: React.FC = () => {
18+
const { connection, updateConnection, connectionUpdating } = useConnectionEditService();
19+
const { registerNotification } = useNotificationService();
20+
const { formatMessage } = useIntl();
21+
const [selectedValue, setSelectedValue] = useState<Geography>();
22+
23+
const { geographies } = useAvailableGeographies();
24+
25+
const handleSubmit = async ({ value }: { value: Geography }) => {
26+
try {
27+
setSelectedValue(value);
28+
await updateConnection({
29+
connectionId: connection.connectionId,
30+
geography: value,
31+
});
32+
} catch (e) {
33+
registerNotification({
34+
id: "connection.geographyUpdateError",
35+
title: formatMessage({ id: "connection.geographyUpdateError" }),
36+
isError: true,
37+
});
38+
}
39+
setSelectedValue(undefined);
40+
};
41+
42+
return (
43+
<Card withPadding>
44+
<div className={styles.wrapper}>
45+
<div>
46+
<ControlLabels
47+
nextLine
48+
label={<FormattedMessage id="connection.geographyTitle" />}
49+
message={
50+
<FormattedMessage
51+
id="connection.geographyDescription"
52+
values={{
53+
lnk: (node: React.ReactNode) => (
54+
<a href={links.cloudAllowlistIPsLink} target="_blank" rel="noreferrer">
55+
{node}
56+
</a>
57+
),
58+
}}
59+
/>
60+
}
61+
/>
62+
</div>
63+
<div className={styles.dropdownWrapper}>
64+
<div className={styles.spinner}>{connectionUpdating && <Spinner small />}</div>
65+
<div className={styles.dropdown}>
66+
<DropDown
67+
isDisabled={connectionUpdating}
68+
options={geographies.map((geography) => ({
69+
label: formatMessage({
70+
id: `connection.geography.${geography}`,
71+
defaultMessage: geography.toUpperCase(),
72+
}),
73+
value: geography,
74+
}))}
75+
value={selectedValue || connection.geography}
76+
onChange={handleSubmit}
77+
/>
78+
</div>
79+
</div>
80+
</div>
81+
</Card>
82+
);
83+
};
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { UpdateConnectionDataResidency } from "./UpdateConnectionDataResidency";

‎airbyte-webapp/src/components/ui/Card/Card.module.scss

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
@use "../../../scss/colors";
2-
@use "../../../scss/variables" as vars;
1+
@use "scss/colors";
2+
@use "scss/variables";
33

44
.container {
55
width: auto;
@@ -17,9 +17,9 @@
1717
}
1818

1919
.title {
20-
padding: 25px 25px 22px;
20+
padding: variables.$spacing-xl;
2121
color: colors.$dark-blue;
22-
border-bottom: colors.$grey-100 vars.$border-thin solid;
22+
border-bottom: colors.$grey-100 variables.$border-thin solid;
2323
font-weight: 600;
2424
letter-spacing: 0.008em;
2525
border-top-left-radius: 10px;

‎airbyte-webapp/src/components/ui/SideMenu/SideMenu.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ interface SideMenuProps {
2727
}
2828

2929
const Content = styled.nav`
30-
min-width: 147px;
30+
min-width: 155px;
3131
`;
3232

3333
const Category = styled.div`

‎airbyte-webapp/src/hooks/services/Analytics/pageTrackingCodes.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ export enum PageTrackingCodes {
2727
SETTINGS_NOTIFICATION = "Settings.Notifications",
2828
SETTINGS_ACCESS_MANAGEMENT = "Settings.AccessManagement",
2929
SETTINGS_METRICS = "Settings.Metrics",
30+
SETTINGS_DATA_RESIDENCY = "Settings.DataResidency",
3031
CREDITS = "Credits",
3132
WORKSPACES = "Workspaces",
3233
PREFERENCES = "Preferences",

‎airbyte-webapp/src/hooks/services/ConnectionEdit/ConnectionEditService.test.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { act, renderHook } from "@testing-library/react-hooks";
22
import React from "react";
33
import mockConnection from "test-utils/mock-data/mockConnection.json";
44
import mockDest from "test-utils/mock-data/mockDestinationDefinition.json";
5+
import mockWorkspace from "test-utils/mock-data/mockWorkspace.json";
56
import { TestWrapper } from "test-utils/testutils";
67

78
import { WebBackendConnectionUpdate } from "core/request/AirbyteClient";
@@ -13,6 +14,10 @@ jest.mock("services/connector/DestinationDefinitionSpecificationService", () =>
1314
useGetDestinationDefinitionSpecification: () => mockDest,
1415
}));
1516

17+
jest.mock("services/workspaces/WorkspacesService", () => ({
18+
useCurrentWorkspace: () => mockWorkspace,
19+
}));
20+
1621
jest.mock("../useConnectionHook", () => ({
1722
useGetConnection: () => mockConnection,
1823
useWebConnectionService: () => ({

‎airbyte-webapp/src/hooks/services/ConnectionForm/ConnectionFormService.test.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { act, renderHook } from "@testing-library/react-hooks";
22
import React from "react";
33
import mockConnection from "test-utils/mock-data/mockConnection.json";
44
import mockDest from "test-utils/mock-data/mockDestinationDefinition.json";
5+
import mockWorkspace from "test-utils/mock-data/mockWorkspace.json";
56
import { TestWrapper } from "test-utils/testutils";
67

78
import { AirbyteCatalog, WebBackendConnectionRead } from "core/request/AirbyteClient";
@@ -17,6 +18,10 @@ jest.mock("services/connector/DestinationDefinitionSpecificationService", () =>
1718
useGetDestinationDefinitionSpecification: () => mockDest,
1819
}));
1920

21+
jest.mock("services/workspaces/WorkspacesService", () => ({
22+
useCurrentWorkspace: () => mockWorkspace,
23+
}));
24+
2025
describe("ConnectionFormService", () => {
2126
const Wrapper: React.FC<Parameters<typeof ConnectionFormServiceProvider>[0]> = ({ children, ...props }) => (
2227
<TestWrapper>

‎airbyte-webapp/src/hooks/services/Feature/types.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ export enum FeatureItem {
55
AllowUpdateConnectors = "ALLOW_UPDATE_CONNECTORS",
66
AllowOAuthConnector = "ALLOW_OAUTH_CONNECTOR",
77
AllowSync = "ALLOW_SYNC",
8+
AllowChangeDataGeographies = "ALLOW_CHANGE_DATA_GEOGRAPHIES",
89
AllowSyncSubOneHourCronExpressions = "ALLOW_SYNC_SUB_ONE_HOUR_CRON_EXPRESSIONS",
910
}
1011

0 commit comments

Comments
 (0)