Skip to content

Commit

Permalink
πŸͺŸ πŸ› Allow Free Connector Program syncs in workspace with no credits (#…
Browse files Browse the repository at this point in the history
…22369)

* Add pollUntil utility for polling Promises

* poll backend for confirmed enrollment before showing success toast

* Put interval and maxTimeout inside options arg

* Improve comment and type signature

* add hook for detecting free connection

* fix enabled status in connection table

* remove unrelated files

* revert polling changes

* remove debugging code

* clean up diff

* add comment

* remove unrelated copy

* typo

* allow sync on status page

* remove useIsConnectionFree

* handle errors when enabling connection

* remove config change

* adjust copy

* remove appmonitoring service from test wrapper

* revert minor syntax change

* remove FeatureItem.AllowSync

---------

Co-authored-by: Alex Birdsall <ambirdsall@gmail.com>
Co-authored-by: Alex Birdsall <alexander@airbyte.io>
  • Loading branch information
3 people authored Feb 10, 2023
1 parent 8554b5b commit 67729e6
Show file tree
Hide file tree
Showing 18 changed files with 65 additions and 86 deletions.
5 changes: 2 additions & 3 deletions airbyte-webapp/src/components/EntityTable/ConnectionTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@ const ConnectionTable: React.FC<ConnectionTableProps> = ({ data, entity, onClick
const navigate = useNavigate();
const query = useQuery<{ sortBy?: string; order?: SortOrderEnum }>();
const allowAutoDetectSchema = useFeature(FeatureItem.AllowAutoDetectSchema);
const allowSync = useFeature(FeatureItem.AllowSync);

const sortBy = query.sortBy || "entityName";
const sortOrder = query.order || SortOrderEnum.ASC;
Expand Down Expand Up @@ -174,12 +173,12 @@ const ConnectionTable: React.FC<ConnectionTableProps> = ({ data, entity, onClick
cell: (props) => (
<StatusCell
schemaChange={props.row.original.schemaChange}
connection={props.row.original.connection}
enabled={props.cell.getValue()}
id={props.row.original.connectionId}
isSyncing={props.row.original.isSyncing}
isManual={props.row.original.scheduleType === ConnectionScheduleType.manual}
hasBreakingChange={allowAutoDetectSchema && props.row.original.schemaChange === SchemaChange.breaking}
allowSync={allowSync}
/>
),
}),
Expand All @@ -191,7 +190,7 @@ const ConnectionTable: React.FC<ConnectionTableProps> = ({ data, entity, onClick
cell: (props) => <ConnectionSettingsCell id={props.cell.getValue()} />,
}),
],
[columnHelper, sortBy, sortOrder, onSortClick, entity, allowAutoDetectSchema, allowSync]
[columnHelper, sortBy, sortOrder, onSortClick, entity, allowAutoDetectSchema]
);

return <NextTable columns={columns} data={sortingData} onClickRow={onClickRow} testId="connectionsTable" />;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { render, waitFor } from "@testing-library/react";
import { TestWrapper, TestSuspenseBoundary } from "test-utils";
import { TestWrapper, TestSuspenseBoundary, mockConnection } from "test-utils";

import { StatusCell } from "./StatusCell";

Expand Down Expand Up @@ -28,7 +28,7 @@ describe("<StatusCell />", () => {
it("renders switch when connection has schedule", () => {
const { getByTestId } = render(
<TestSuspenseBoundary>
<StatusCell id={mockId} allowSync enabled />
<StatusCell id={mockId} connection={mockConnection} enabled />
</TestSuspenseBoundary>,
{
wrapper: TestWrapper,
Expand All @@ -44,7 +44,7 @@ describe("<StatusCell />", () => {
it("renders button when connection does not have schedule", async () => {
const { getByTestId } = render(
<TestSuspenseBoundary>
<StatusCell id={mockId} allowSync enabled isManual />
<StatusCell id={mockId} connection={mockConnection} enabled isManual />
</TestSuspenseBoundary>,
{
wrapper: TestWrapper,
Expand All @@ -57,7 +57,7 @@ describe("<StatusCell />", () => {
it("disables switch when hasBreakingChange is true", () => {
const { getByTestId } = render(
<TestSuspenseBoundary>
<StatusCell id={mockId} allowSync hasBreakingChange />
<StatusCell id={mockId} connection={mockConnection} hasBreakingChange />
</TestSuspenseBoundary>,
{
wrapper: TestWrapper,
Expand All @@ -70,7 +70,7 @@ describe("<StatusCell />", () => {
it("disables manual sync button when hasBreakingChange is true", () => {
const { getByTestId } = render(
<TestSuspenseBoundary>
<StatusCell id={mockId} allowSync hasBreakingChange enabled isManual />
<StatusCell id={mockId} connection={mockConnection} hasBreakingChange enabled isManual />
</TestSuspenseBoundary>,
{
wrapper: TestWrapper,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,30 +1,30 @@
import React from "react";

import { SchemaChange } from "core/request/AirbyteClient";
import { SchemaChange, WebBackendConnectionListItem } from "core/request/AirbyteClient";
import { FeatureItem, useFeature } from "hooks/services/Feature";

import { ChangesStatusIcon } from "./ChangesStatusIcon";
import styles from "./StatusCell.module.scss";
import { StatusCellControl } from "./StatusCellControl";

interface StatusCellProps {
allowSync?: boolean;
hasBreakingChange?: boolean;
enabled?: boolean;
isSyncing?: boolean;
isManual?: boolean;
id: string;
schemaChange?: SchemaChange;
connection: WebBackendConnectionListItem;
}

export const StatusCell: React.FC<StatusCellProps> = ({
enabled,
isManual,
id,
isSyncing,
allowSync,
schemaChange,
hasBreakingChange,
connection,
}) => {
const allowAutoDetectSchema = useFeature(FeatureItem.AllowAutoDetectSchema);

Expand All @@ -36,7 +36,7 @@ export const StatusCell: React.FC<StatusCellProps> = ({
isSyncing={isSyncing}
isManual={isManual}
hasBreakingChange={hasBreakingChange}
allowSync={allowSync}
connection={connection}
/>
{allowAutoDetectSchema && <ChangesStatusIcon schemaChange={schemaChange} />}
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,35 +4,34 @@ import { FormattedMessage } from "react-intl";
import { Button } from "components/ui/Button";
import { Switch } from "components/ui/Switch";

import { useConnectionList, useEnableConnection, useSyncConnection } from "hooks/services/useConnectionHook";
import { WebBackendConnectionListItem } from "core/request/AirbyteClient";
import { useEnableConnection, useSyncConnection } from "hooks/services/useConnectionHook";

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

interface StatusCellControlProps {
allowSync?: boolean;
hasBreakingChange?: boolean;
enabled?: boolean;
isSyncing?: boolean;
isManual?: boolean;
id: string;
connection: WebBackendConnectionListItem;
}

export const StatusCellControl: React.FC<StatusCellControlProps> = ({
enabled,
isManual,
id,
isSyncing,
allowSync,
hasBreakingChange,
connection,
}) => {
const { connections } = useConnectionList();
const { mutateAsync: enableConnection, isLoading } = useEnableConnection();
const { mutateAsync: syncConnection, isLoading: isSyncStarting } = useSyncConnection();

const onRunManualSync = (event: React.SyntheticEvent) => {
event.stopPropagation();

const connection = connections.find((c) => c.connectionId === id);
if (connection) {
syncConnection(connection);
}
Expand All @@ -57,7 +56,7 @@ export const StatusCellControl: React.FC<StatusCellControlProps> = ({
<Switch
checked={enabled}
onChange={onSwitchChange}
disabled={!allowSync || hasBreakingChange}
disabled={hasBreakingChange}
loading={isLoading}
data-testid="enable-connection-switch"
/>
Expand All @@ -77,7 +76,7 @@ export const StatusCellControl: React.FC<StatusCellControlProps> = ({
<Button
onClick={onRunManualSync}
isLoading={isSyncStarting}
disabled={!allowSync || !enabled || hasBreakingChange || isSyncStarting}
disabled={!enabled || hasBreakingChange || isSyncStarting}
data-testid="manual-sync-button"
>
<FormattedMessage id="connection.startSync" />
Expand Down
8 changes: 7 additions & 1 deletion airbyte-webapp/src/components/EntityTable/types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
import { ConnectionScheduleData, ConnectionScheduleType, SchemaChange } from "../../core/request/AirbyteClient";
import {
ConnectionScheduleData,
ConnectionScheduleType,
SchemaChange,
WebBackendConnectionListItem,
} from "../../core/request/AirbyteClient";

interface EntityTableDataItem {
entityId: string;
Expand Down Expand Up @@ -30,6 +35,7 @@ interface ConnectionTableDataItem {
lastSyncStatus: string | null;
connectorIcon?: string;
entityIcon?: string;
connection: WebBackendConnectionListItem;
}

const enum Status {
Expand Down
1 change: 1 addition & 0 deletions airbyte-webapp/src/components/EntityTable/utils.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ export const getConnectionTableData = (
lastSyncStatus: getConnectionSyncStatus(connection.status, connection.latestSyncJobStatus),
connectorIcon: type === "destination" ? connection.source.icon : connection.destination.icon,
entityIcon: type === "destination" ? connection.destination.icon : connection.source.icon,
connection,
}));
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import { ConnectionStatus } from "core/request/AirbyteClient";
import { useSchemaChanges } from "hooks/connection/useSchemaChanges";
import { useConnectionEditService } from "hooks/services/ConnectionEdit/ConnectionEditService";
import { useConnectionFormService } from "hooks/services/ConnectionForm/ConnectionFormService";
import { FeatureItem, useFeature } from "hooks/services/Feature";
import { RoutePaths } from "pages/routePaths";

import styles from "./ConnectionInfoCard.module.scss";
Expand All @@ -25,8 +24,6 @@ export const ConnectionInfoCard: React.FC = () => {
const { hasSchemaChanges, hasBreakingSchemaChange, hasNonBreakingSchemaChange } = useSchemaChanges(schemaChange);
const { sourceDefinition, destDefinition } = useConnectionFormService();

const hasAllowSyncFeature = useFeature(FeatureItem.AllowSync);

const sourceConnectionPath = `../../${RoutePaths.Source}/${source.sourceId}`;
const destinationConnectionPath = `../../${RoutePaths.Destination}/${destination.destinationId}`;

Expand Down Expand Up @@ -72,7 +69,7 @@ export const ConnectionInfoCard: React.FC = () => {
{!isConnectionReadOnly && (
<>
<div className={styles.enabledControlContainer}>
<EnabledControl disabled={!hasAllowSyncFeature || hasBreakingSchemaChange} />
<EnabledControl disabled={hasBreakingSchemaChange} />
</div>
{hasSchemaChanges && <SchemaChangesDetected />}
</>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { AirbyteJSONSchema } from "core/jsonSchema/types";

import { traverseSchemaToField } from "./traverseSchemaToField";

describe(`${traverseSchemaToField}`, () => {
describe(`${traverseSchemaToField.name}`, () => {
it("traverses a nested schema", () => {
const nestedSchema: AirbyteJSONSchema = {
$schema: "http://json-schema.org/draft-04/schema#",
Expand Down
44 changes: 14 additions & 30 deletions airbyte-webapp/src/hooks/services/Feature/FeatureService.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { FeatureService, IfFeatureEnabled, useFeature, useFeatureService } from
import { FeatureItem, FeatureSet } from "./types";

const wrapper: React.FC<React.PropsWithChildren<unknown>> = ({ children }) => (
<FeatureService features={[FeatureItem.AllowDBTCloudIntegration, FeatureItem.AllowSync]}>{children}</FeatureService>
<FeatureService features={[FeatureItem.AllowDBTCloudIntegration]}>{children}</FeatureService>
);

type FeatureOverwrite = FeatureItem[] | FeatureSet | undefined;
Expand Down Expand Up @@ -46,7 +46,6 @@ describe("Feature Service", () => {
const getFeature = (feature: FeatureItem) => renderHook(() => useFeature(feature), { wrapper }).result.current;
expect(getFeature(FeatureItem.AllowDBTCloudIntegration)).toBe(true);
expect(getFeature(FeatureItem.AllowCustomDBT)).toBe(false);
expect(getFeature(FeatureItem.AllowSync)).toBe(true);
expect(getFeature(FeatureItem.AllowUpdateConnectors)).toBe(false);
});

Expand All @@ -55,12 +54,7 @@ describe("Feature Service", () => {
getFeatures({
workspace: [FeatureItem.AllowCustomDBT, FeatureItem.AllowUploadCustomImage],
}).result.current.sort()
).toEqual([
FeatureItem.AllowCustomDBT,
FeatureItem.AllowDBTCloudIntegration,
FeatureItem.AllowSync,
FeatureItem.AllowUploadCustomImage,
]);
).toEqual([FeatureItem.AllowCustomDBT, FeatureItem.AllowDBTCloudIntegration, FeatureItem.AllowUploadCustomImage]);
});

it("workspace features can disable default features", () => {
Expand All @@ -71,7 +65,7 @@ describe("Feature Service", () => {
[FeatureItem.AllowDBTCloudIntegration]: false,
} as FeatureSet,
}).result.current.sort()
).toEqual([FeatureItem.AllowCustomDBT, FeatureItem.AllowSync]);
).toEqual([FeatureItem.AllowCustomDBT]);
});

it("user features should merge correctly with workspace and default features", () => {
Expand All @@ -84,7 +78,6 @@ describe("Feature Service", () => {
FeatureItem.AllowCustomDBT,
FeatureItem.AllowDBTCloudIntegration,
FeatureItem.AllowOAuthConnector,
FeatureItem.AllowSync,
FeatureItem.AllowUploadCustomImage,
]);
});
Expand All @@ -99,30 +92,24 @@ describe("Feature Service", () => {
[FeatureItem.AllowDBTCloudIntegration]: false,
} as FeatureSet,
}).result.current.sort()
).toEqual([FeatureItem.AllowCustomDBT, FeatureItem.AllowOAuthConnector, FeatureItem.AllowSync]);
).toEqual([FeatureItem.AllowCustomDBT, FeatureItem.AllowOAuthConnector]);
});

it("user features can re-enable feature that are disabled per workspace", () => {
expect(
getFeatures({
workspace: { [FeatureItem.AllowCustomDBT]: true, [FeatureItem.AllowSync]: false } as FeatureSet,
user: [FeatureItem.AllowOAuthConnector, FeatureItem.AllowSync],
workspace: { [FeatureItem.AllowCustomDBT]: true } as FeatureSet,
user: [FeatureItem.AllowOAuthConnector],
}).result.current.sort()
).toEqual([
FeatureItem.AllowCustomDBT,
FeatureItem.AllowDBTCloudIntegration,
FeatureItem.AllowOAuthConnector,
FeatureItem.AllowSync,
]);
).toEqual([FeatureItem.AllowCustomDBT, FeatureItem.AllowDBTCloudIntegration, FeatureItem.AllowOAuthConnector]);
});

it("overwrite features can overwrite workspace and user features", () => {
expect(
getFeatures({
workspace: { [FeatureItem.AllowCustomDBT]: true, [FeatureItem.AllowSync]: false } as FeatureSet,
workspace: { [FeatureItem.AllowCustomDBT]: true } as FeatureSet,
user: {
[FeatureItem.AllowOAuthConnector]: true,
[FeatureItem.AllowSync]: true,
[FeatureItem.AllowDBTCloudIntegration]: false,
} as FeatureSet,
overwrite: [FeatureItem.AllowUploadCustomImage, FeatureItem.AllowDBTCloudIntegration],
Expand All @@ -131,36 +118,35 @@ describe("Feature Service", () => {
FeatureItem.AllowCustomDBT,
FeatureItem.AllowDBTCloudIntegration,
FeatureItem.AllowOAuthConnector,
FeatureItem.AllowSync,
FeatureItem.AllowUploadCustomImage,
]);
});

it("workspace features can be cleared again", () => {
const { result, rerender } = getFeatures({
workspace: { [FeatureItem.AllowCustomDBT]: true, [FeatureItem.AllowSync]: false } as FeatureSet,
workspace: { [FeatureItem.AllowCustomDBT]: true } as FeatureSet,
});
expect(result.current.sort()).toEqual([FeatureItem.AllowCustomDBT, FeatureItem.AllowDBTCloudIntegration]);
rerender({ workspace: undefined });
expect(result.current.sort()).toEqual([FeatureItem.AllowDBTCloudIntegration, FeatureItem.AllowSync]);
expect(result.current.sort()).toEqual([FeatureItem.AllowDBTCloudIntegration]);
});

it("user features can be cleared again", () => {
const { result, rerender } = getFeatures({
user: { [FeatureItem.AllowCustomDBT]: true, [FeatureItem.AllowSync]: false } as FeatureSet,
user: { [FeatureItem.AllowCustomDBT]: true } as FeatureSet,
});
expect(result.current.sort()).toEqual([FeatureItem.AllowCustomDBT, FeatureItem.AllowDBTCloudIntegration]);
rerender({ user: undefined });
expect(result.current.sort()).toEqual([FeatureItem.AllowDBTCloudIntegration, FeatureItem.AllowSync]);
expect(result.current.sort()).toEqual([FeatureItem.AllowDBTCloudIntegration]);
});

it("overwritten features can be cleared again", () => {
const { result, rerender } = getFeatures({
overwrite: { [FeatureItem.AllowCustomDBT]: true, [FeatureItem.AllowSync]: false } as FeatureSet,
overwrite: { [FeatureItem.AllowCustomDBT]: true } as FeatureSet,
});
expect(result.current.sort()).toEqual([FeatureItem.AllowCustomDBT, FeatureItem.AllowDBTCloudIntegration]);
rerender({ overwrite: undefined });
expect(result.current.sort()).toEqual([FeatureItem.AllowDBTCloudIntegration, FeatureItem.AllowSync]);
expect(result.current.sort()).toEqual([FeatureItem.AllowDBTCloudIntegration]);
});

describe("env variable overwrites", () => {
Expand All @@ -178,14 +164,12 @@ describe("Feature Service", () => {
it("should allow overwriting it in dev", () => {
(process.env.NODE_ENV as string) = "development";
const getFeature = (feature: FeatureItem) => renderHook(() => useFeature(feature), { wrapper }).result.current;
expect(getFeature(FeatureItem.AllowSync)).toBe(false);
expect(getFeature(FeatureItem.AllowChangeDataGeographies)).toBe(true);
});

it("should not overwrite in a non dev environment", () => {
(process.env.NODE_ENV as string) = "production";
const getFeature = (feature: FeatureItem) => renderHook(() => useFeature(feature), { wrapper }).result.current;
expect(getFeature(FeatureItem.AllowSync)).toBe(true);
expect(getFeature(FeatureItem.AllowChangeDataGeographies)).toBe(false);
});
});
Expand Down
2 changes: 0 additions & 2 deletions airbyte-webapp/src/hooks/services/Feature/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import { FeatureItem } from "./types";
export const defaultOssFeatures = [
FeatureItem.AllowAutoDetectSchema,
FeatureItem.AllowCustomDBT,
FeatureItem.AllowSync,
FeatureItem.AllowUpdateConnectors,
FeatureItem.AllowUploadCustomImage,
FeatureItem.AllowSyncSubOneHourCronExpressions,
Expand All @@ -12,7 +11,6 @@ export const defaultOssFeatures = [
export const defaultCloudFeatures = [
FeatureItem.AllowAutoDetectSchema,
FeatureItem.AllowOAuthConnector,
FeatureItem.AllowSync,
FeatureItem.AllowChangeDataGeographies,
FeatureItem.AllowDBTCloudIntegration,
FeatureItem.FreeConnectorProgram,
Expand Down
Loading

0 comments on commit 67729e6

Please sign in to comment.