Skip to content

Fix OAuth login buttons #19135

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

Merged
merged 5 commits into from
Nov 8, 2022
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
4 changes: 2 additions & 2 deletions airbyte-webapp/src/views/Connector/ConnectorForm/FormRoot.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ export const FormRoot: React.FC<FormRootProps> = ({
selectedConnector,
}) => {
const { dirty, isSubmitting, isValid } = useFormikContext<ConnectorFormValues>();
const { resetConnectorForm, isLoadingSchema, selectedService, isEditMode, formType } = useConnectorForm();
const { resetConnectorForm, isLoadingSchema, selectedConnectorDefinition, isEditMode, formType } = useConnectorForm();

return (
<Form>
Expand All @@ -47,7 +47,7 @@ export const FormRoot: React.FC<FormRootProps> = ({
<div className={styles.loaderContainer}>
<Spinner />
<div className={styles.loadingMessage}>
<ShowLoadingMessage connector={selectedService?.name} />
<ShowLoadingMessage connector={selectedConnectorDefinition?.name} />
</div>
</div>
)}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { screen, render } from "@testing-library/react";
import { TestWrapper } from "test-utils/testutils";

import { ConnectorDefinition, ConnectorDefinitionSpecification } from "core/domain/connector";
import { useFormikOauthAdapter } from "views/Connector/ConnectorForm/components/Sections/auth/useOauthFlowAdapter";
import { useConnectorForm } from "views/Connector/ConnectorForm/connectorFormContext";
import { useAuthentication } from "views/Connector/ConnectorForm/useAuthentication";
Expand Down Expand Up @@ -36,10 +37,9 @@ const baseUseFormikOauthAdapterValues = {

jest.mock("views/Connector/ConnectorForm/connectorFormContext");
const mockUseConnectorForm = useConnectorForm as unknown as jest.Mock<Partial<typeof useConnectorForm>>;
const baseUseConnectorFormValues = {
selectedConnector: "abcde",
allowOAuthConnector: true,
selectedService: undefined,
const baseUseConnectorFormValues: Partial<ReturnType<typeof useConnectorForm>> = {
selectedConnectorDefinition: { sourceDefinitionId: "abcde", name: "Acme" } as unknown as ConnectorDefinition,
selectedConnectorDefinitionSpecification: {} as unknown as ConnectorDefinitionSpecification,
};

jest.mock("views/Connector/ConnectorForm/useAuthentication");
Expand All @@ -55,9 +55,9 @@ describe("auth button", () => {
it("initially renders with correct message and no status message", () => {
// no auth errors
mockUseConnectorForm.mockImplementationOnce(() => {
const { selectedConnector, selectedService } = baseUseConnectorFormValues;
const { selectedConnectorDefinitionSpecification, selectedConnectorDefinition } = baseUseConnectorFormValues;

return { selectedConnector, selectedService };
return { selectedConnectorDefinitionSpecification, selectedConnectorDefinition };
});

// not done
Expand All @@ -75,7 +75,7 @@ describe("auth button", () => {
);

// correct button text
const button = screen.getByRole("button", { name: "Authenticate your account" });
const button = screen.getByRole("button", { name: "Authenticate your Acme account" });
expect(button).toBeInTheDocument();

// no error message
Expand All @@ -90,9 +90,9 @@ describe("auth button", () => {
it("after successful authentication, it renders with correct message and success message", () => {
// no auth errors
mockUseConnectorForm.mockImplementationOnce(() => {
const { selectedConnector, selectedService } = baseUseConnectorFormValues;
const { selectedConnectorDefinitionSpecification, selectedConnectorDefinition } = baseUseConnectorFormValues;

return { selectedConnector, selectedService };
return { selectedConnectorDefinitionSpecification, selectedConnectorDefinition };
});

// done
Expand Down Expand Up @@ -123,9 +123,9 @@ describe("auth button", () => {
mockUseAuthentication.mockReturnValue({ hiddenAuthFieldErrors: { field: "form.empty.error" } });

mockUseConnectorForm.mockImplementationOnce(() => {
const { selectedConnector, selectedService } = baseUseConnectorFormValues;
const { selectedConnectorDefinitionSpecification, selectedConnectorDefinition } = baseUseConnectorFormValues;

return { selectedConnector, selectedService };
return { selectedConnectorDefinitionSpecification, selectedConnectorDefinition };
});

// not done
Expand All @@ -143,7 +143,7 @@ describe("auth button", () => {
);

// correct button
const button = screen.getByRole("button", { name: "Authenticate your account" });
const button = screen.getByRole("button", { name: "Authenticate your Acme account" });
expect(button).toBeInTheDocument();

// error message
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,19 +41,19 @@ function getAuthenticateMessageId(connectorDefinitionId: string): string {
}

export const AuthButton: React.FC = () => {
const { selectedService, selectedConnector } = useConnectorForm();
const { selectedConnectorDefinition, selectedConnectorDefinitionSpecification } = useConnectorForm();
const { hiddenAuthFieldErrors } = useAuthentication();
const authRequiredError = Object.values(hiddenAuthFieldErrors).includes("form.empty.error");

// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const { loading, done, run, hasRun } = useFormikOauthAdapter(selectedConnector!);
const { loading, done, run, hasRun } = useFormikOauthAdapter(selectedConnectorDefinitionSpecification!);

if (!selectedConnector) {
if (!selectedConnectorDefinitionSpecification) {
console.error("Entered non-auth flow while no connector is selected");
return null;
}

const definitionId = ConnectorSpecification.id(selectedConnector);
const definitionId = ConnectorSpecification.id(selectedConnectorDefinitionSpecification);
const Component = getButtonComponent(definitionId);

const messageStyle = classnames(styles.message, {
Expand All @@ -63,7 +63,10 @@ export const AuthButton: React.FC = () => {
const buttonLabel = done ? (
<FormattedMessage id="connectorForm.reauthenticate" />
) : (
<FormattedMessage id={getAuthenticateMessageId(definitionId)} values={{ connector: selectedService?.name }} />
<FormattedMessage
id={getAuthenticateMessageId(definitionId)}
values={{ connector: selectedConnectorDefinition?.name }}
/>
);
return (
<div className={styles.authSectionRow}>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ interface ConnectorFormContext {
addUnfinishedFlow: (key: string, info?: Record<string, unknown>) => void;
removeUnfinishedFlow: (key: string) => void;
resetConnectorForm: () => void;
selectedService?: ConnectorDefinition;
selectedConnector?: ConnectorDefinitionSpecification;
selectedConnectorDefinition?: ConnectorDefinition;
selectedConnectorDefinitionSpecification?: ConnectorDefinitionSpecification;
isLoadingSchema?: boolean;
isEditMode?: boolean;
validationSchema: AnySchema;
Expand Down Expand Up @@ -63,7 +63,7 @@ export const ConnectorFormContextProvider: React.FC<React.PropsWithChildren<Conn

const ctx = useMemo<ConnectorFormContext>(() => {
const unfinishedFlows = widgetsInfo["_common.unfinishedFlows"] ?? {};
return {
const context: ConnectorFormContext = {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ℹ️ Assigning this object to a variable that's explicitally types, will cause TypeScript to now properly check if the object has keys that it shouldn't have.

widgetsInfo,
getValues,
setUiWidgetsInfo,
Expand All @@ -89,6 +89,7 @@ export const ConnectorFormContextProvider: React.FC<React.PropsWithChildren<Conn
resetUiWidgetsInfo();
},
};
return context;
}, [
widgetsInfo,
getValues,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,8 @@ const mockContext = ({ connector, values, submitCount, fieldMeta = {} }: MockPar
});
mockConnectorForm.mockReturnValue({
// eslint-disable-next-line @typescript-eslint/no-explicit-any
selectedConnector: { ...connector, sourceDefinitionId: "12345", jobInfo: {} as any },
selectedConnectorDefinitionSpecification: { ...connector, sourceDefinitionId: "12345", jobInfo: {} as any },
getValues: (values) => values,
});
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -131,29 +131,32 @@ interface AuthenticationHook {

export const useAuthentication = (): AuthenticationHook => {
const { values, getFieldMeta, submitCount } = useFormikContext<ConnectorFormValues>();
const { selectedConnector } = useConnectorForm();
const { getValues, selectedConnectorDefinitionSpecification: connectorSpec } = useConnectorForm();

const allowOAuthConnector = useFeature(FeatureItem.AllowOAuthConnector);

const advancedAuth = selectedConnector?.advancedAuth;
const legacyOauthSpec = selectedConnector?.authSpecification?.oauth2Specification;
const advancedAuth = connectorSpec?.advancedAuth;
const legacyOauthSpec = connectorSpec?.authSpecification?.oauth2Specification;

const spec = selectedConnector?.connectionSpecification as JSONSchema7;
const spec = connectorSpec?.connectionSpecification as JSONSchema7;

const valuesWithDefaults = useMemo(() => getValues(values), [getValues, values]);

const isAuthButtonVisible = useMemo(() => {
const shouldShowAdvancedAuth =
advancedAuth && shouldShowButtonForAdvancedAuth(advancedAuth.predicateKey, advancedAuth.predicateValue, values);
advancedAuth &&
shouldShowButtonForAdvancedAuth(advancedAuth.predicateKey, advancedAuth.predicateValue, valuesWithDefaults);
const shouldShowLegacyAuth =
legacyOauthSpec && shouldShowButtonForLegacyAuth(spec, legacyOauthSpec.rootObject as Path, values);
legacyOauthSpec && shouldShowButtonForLegacyAuth(spec, legacyOauthSpec.rootObject as Path, valuesWithDefaults);
return Boolean(allowOAuthConnector && (shouldShowAdvancedAuth || shouldShowLegacyAuth));
}, [values, advancedAuth, legacyOauthSpec, spec, allowOAuthConnector]);
}, [advancedAuth, valuesWithDefaults, legacyOauthSpec, spec, allowOAuthConnector]);

// Fields that are filled by the OAuth flow and thus won't need to be shown in the UI if OAuth is available
const implicitAuthFieldPaths = useMemo(
() => [
// Fields from `advancedAuth` connectors
...(advancedAuth
? Object.values(serverProvidedOauthPaths(selectedConnector)).map((f) =>
? Object.values(serverProvidedOauthPaths(connectorSpec)).map((f) =>
makeConnectionConfigurationPath(f.path_in_connector_config)
)
: []),
Expand All @@ -165,7 +168,7 @@ export const useAuthentication = (): AuthenticationHook => {
]
: []),
],
[advancedAuth, legacyOauthSpec, selectedConnector]
[advancedAuth, legacyOauthSpec, connectorSpec]
);

const isHiddenAuthField = useCallback(
Expand Down Expand Up @@ -208,8 +211,8 @@ export const useAuthentication = (): AuthenticationHook => {
);

const hasAuthFieldValues: boolean = useMemo(() => {
return implicitAuthFieldPaths.some((path) => getIn(values, path) !== undefined);
}, [implicitAuthFieldPaths, values]);
return implicitAuthFieldPaths.some((path) => !!getIn(valuesWithDefaults, path));
}, [implicitAuthFieldPaths, valuesWithDefaults]);

return {
isHiddenAuthField,
Expand Down