feat(site): allow creating a workspace without connecting optional external auth providers (#12251)

This commit is contained in:
Kayla Washburn-Love
2024-02-22 10:27:36 -07:00
committed by GitHub
parent b8a53230c7
commit 7e6cb66a50
5 changed files with 117 additions and 29 deletions

View File

@ -233,19 +233,6 @@ describe("CreateWorkspacePage", () => {
);
});
it("external auth errors if unauthenticated", async () => {
jest
.spyOn(API, "getTemplateVersionExternalAuth")
.mockResolvedValueOnce([MockTemplateVersionExternalAuthGithub]);
renderCreateWorkspacePage();
await waitForLoaderToBeRemoved();
await screen.findByText(
"To create a workspace using the selected template, please ensure you are authenticated with all the external providers listed below.",
);
});
it("auto create a workspace if uses mode=auto", async () => {
const param = "first_parameter";
const paramValue = "It works!";
@ -312,7 +299,7 @@ describe("CreateWorkspacePage", () => {
route: `/templates/${MockWorkspace.name}/workspace?${params.toString()}`,
});
const warningMessage = await screen.findByRole("alert");
const warningMessage = await screen.findByTestId("duplication-warning");
const nameInput = await screen.findByRole("textbox", {
name: "Workspace Name",
});

View File

@ -126,6 +126,80 @@ export const ExternalAuth: Story = {
authenticate_url: "",
display_icon: "/icon/gitlab.svg",
display_name: "GitLab",
optional: true,
},
],
},
};
export const ExternalAuthError: Story = {
args: {
error: true,
externalAuth: [
{
id: "github",
type: "github",
authenticated: false,
authenticate_url: "",
display_icon: "/icon/github.svg",
display_name: "GitHub",
},
{
id: "gitlab",
type: "gitlab",
authenticated: false,
authenticate_url: "",
display_icon: "/icon/gitlab.svg",
display_name: "GitLab",
optional: true,
},
],
},
};
export const ExternalAuthAllRequiredConnected: Story = {
args: {
externalAuth: [
{
id: "github",
type: "github",
authenticated: true,
authenticate_url: "",
display_icon: "/icon/github.svg",
display_name: "GitHub",
},
{
id: "gitlab",
type: "gitlab",
authenticated: false,
authenticate_url: "",
display_icon: "/icon/gitlab.svg",
display_name: "GitLab",
optional: true,
},
],
},
};
export const ExternalAuthAllConnected: Story = {
args: {
externalAuth: [
{
id: "github",
type: "github",
authenticated: true,
authenticate_url: "",
display_icon: "/icon/github.svg",
display_name: "GitHub",
},
{
id: "gitlab",
type: "gitlab",
authenticated: true,
authenticate_url: "",
display_icon: "/icon/gitlab.svg",
display_name: "GitLab",
optional: true,
},
],
},

View File

@ -147,6 +147,10 @@ export const CreateWorkspacePageView: FC<CreateWorkspacePageViewProps> = ({
);
}, [autofillParameters]);
const hasAllRequiredExternalAuth = externalAuth.every(
(auth) => auth.optional || auth.authenticated,
);
return (
<Margins size="medium">
<PageHeader actions={<Button onClick={onCancel}>Cancel</Button>}>
@ -179,7 +183,7 @@ export const CreateWorkspacePageView: FC<CreateWorkspacePageViewProps> = ({
{Boolean(error) && <ErrorAlert error={error} />}
{mode === "duplicate" && (
<Alert severity="info" dismissible>
<Alert severity="info" dismissible data-testid="duplication-warning">
{Language.duplicationWarning}
</Alert>
)}
@ -248,21 +252,19 @@ export const CreateWorkspacePageView: FC<CreateWorkspacePageViewProps> = ({
{externalAuth && externalAuth.length > 0 && (
<FormSection
title="External Authentication"
description="This template requires authentication to external services."
description="This template uses external services for authentication."
>
<FormFields>
{requiresExternalAuth && (
// This should really be a `notice` but `severity` is a MUI prop, and we'd need
// to basically make our own `Alert` component.
<Alert severity="info">
To create a workspace using the selected template, please
ensure you are authenticated with all the external providers
listed below.
{Boolean(error) && !hasAllRequiredExternalAuth && (
<Alert severity="error">
To create a workspace using this template, please connect to
all required external authentication providers listed below.
</Alert>
)}
{externalAuth.map((auth) => (
<ExternalAuthButton
key={auth.id}
error={error}
auth={auth}
isLoading={externalAuthPollingState === "polling"}
onStartPolling={startPollingExternalAuth}
@ -313,6 +315,7 @@ export const CreateWorkspacePageView: FC<CreateWorkspacePageViewProps> = ({
<FormFooter
onCancel={onCancel}
isLoading={creatingWorkspace}
submitDisabled={!hasAllRequiredExternalAuth}
submitLabel="Create Workspace"
/>
</HorizontalForm>

View File

@ -12,7 +12,7 @@ const MockExternalAuth: TemplateVersionExternalAuth = {
};
const meta: Meta<typeof ExternalAuthButton> = {
title: "pages/CreateWorkspacePage/ExternalAuth",
title: "pages/CreateWorkspacePage/ExternalAuthButton",
component: ExternalAuthButton,
};
@ -25,6 +25,15 @@ export const Github: Story = {
},
};
export const GithubOptional: Story = {
args: {
auth: {
...MockExternalAuth,
optional: true,
},
},
};
export const GithubWithRetry: Story = {
args: {
auth: MockExternalAuth,
@ -48,6 +57,7 @@ export const Gitlab: Story = {
display_icon: "/icon/gitlab.svg",
display_name: "GitLab",
authenticated: false,
optional: true,
},
},
};
@ -70,6 +80,7 @@ export const AzureDevOps: Story = {
display_icon: "/icon/azure-devops.svg",
display_name: "Azure DevOps",
authenticated: false,
optional: true,
},
},
};
@ -92,6 +103,7 @@ export const Bitbucket: Story = {
display_icon: "/icon/bitbucket.svg",
display_name: "Bitbucket",
authenticated: false,
optional: true,
},
},
};

View File

@ -1,17 +1,19 @@
import ReplayIcon from "@mui/icons-material/Replay";
import Button from "@mui/material/Button";
import Tooltip from "@mui/material/Tooltip";
import { type FC } from "react";
import LoadingButton from "@mui/lab/LoadingButton";
import { visuallyHidden } from "@mui/utils";
import { type FC } from "react";
import type { TemplateVersionExternalAuth } from "api/typesGenerated";
import { ExternalImage } from "components/ExternalImage/ExternalImage";
import { TemplateVersionExternalAuth } from "api/typesGenerated";
import { Pill } from "components/Pill/Pill";
export interface ExternalAuthButtonProps {
auth: TemplateVersionExternalAuth;
displayRetry: boolean;
isLoading: boolean;
onStartPolling: () => void;
error?: unknown;
}
export const ExternalAuthButton: FC<ExternalAuthButtonProps> = ({
@ -19,6 +21,7 @@ export const ExternalAuthButton: FC<ExternalAuthButtonProps> = ({
displayRetry,
isLoading,
onStartPolling,
error,
}) => {
return (
<>
@ -48,9 +51,18 @@ export const ExternalAuthButton: FC<ExternalAuthButtonProps> = ({
onStartPolling();
}}
>
{auth.authenticated
? `Authenticated with ${auth.display_name}`
: `Login with ${auth.display_name}`}
{auth.authenticated ? (
`Authenticated with ${auth.display_name}`
) : (
<>
Login with {auth.display_name}
{!auth.optional && (
<Pill type={error ? "error" : "info"} css={{ marginLeft: 12 }}>
Required
</Pill>
)}
</>
)}
</LoadingButton>
{displayRetry && (