diff --git a/site/src/components/SignInForm/SignInForm.stories.tsx b/site/src/components/SignInForm/SignInForm.stories.tsx index 7e6b24f79ed2a..1937dfd066a5b 100644 --- a/site/src/components/SignInForm/SignInForm.stories.tsx +++ b/site/src/components/SignInForm/SignInForm.stories.tsx @@ -1,5 +1,6 @@ import { Story } from "@storybook/react" -import { SignInForm, SignInFormProps } from "./SignInForm" +import { makeMockApiError } from "testHelpers/entities" +import { LoginErrors, SignInForm, SignInFormProps } from "./SignInForm" export default { title: "components/SignInForm", @@ -15,7 +16,7 @@ const Template: Story = (args: SignInFormProps) => { return Promise.resolve() }, @@ -34,29 +35,39 @@ Loading.args = { export const WithLoginError = Template.bind({}) WithLoginError.args = { ...SignedOut.args, - authError: { - response: { - data: { - message: "Email or password was invalid", - validations: [ - { - field: "password", - detail: "Password is invalid.", - }, - ], - }, - }, - isAxiosError: true, + loginErrors: { + [LoginErrors.AUTH_ERROR]: makeMockApiError({ + message: "Email or password was invalid", + validations: [ + { + field: "password", + detail: "Password is invalid.", + }, + ], + }), }, initialTouched: { password: true, }, } +export const WithCheckPermissionsError = Template.bind({}) +WithCheckPermissionsError.args = { + ...SignedOut.args, + loginErrors: { + [LoginErrors.CHECK_PERMISSIONS_ERROR]: makeMockApiError({ + message: "Unable to fetch user permissions", + detail: "Resource not found or you do not have access to this resource.", + }), + }, +} + export const WithAuthMethodsError = Template.bind({}) WithAuthMethodsError.args = { ...SignedOut.args, - methodsError: new Error("Failed to fetch auth methods"), + loginErrors: { + [LoginErrors.GET_METHODS_ERROR]: new Error("Failed to fetch auth methods"), + }, } export const WithGithub = Template.bind({}) diff --git a/site/src/components/SignInForm/SignInForm.tsx b/site/src/components/SignInForm/SignInForm.tsx index bfa28ca8bd3cb..a4d75dad63dd7 100644 --- a/site/src/components/SignInForm/SignInForm.tsx +++ b/site/src/components/SignInForm/SignInForm.tsx @@ -23,13 +23,22 @@ interface BuiltInAuthFormValues { password: string } +export enum LoginErrors { + AUTH_ERROR = "authError", + CHECK_PERMISSIONS_ERROR = "checkPermissionsError", + GET_METHODS_ERROR = "getMethodsError", +} + export const Language = { emailLabel: "Email", passwordLabel: "Password", emailInvalid: "Please enter a valid email address.", emailRequired: "Please enter an email address.", - authErrorMessage: "Incorrect email or password.", - methodsErrorMessage: "Unable to fetch auth methods.", + errorMessages: { + [LoginErrors.AUTH_ERROR]: "Incorrect email or password.", + [LoginErrors.CHECK_PERMISSIONS_ERROR]: "Unable to fetch user permissions.", + [LoginErrors.GET_METHODS_ERROR]: "Unable to fetch auth methods.", + }, passwordSignIn: "Sign In", githubSignIn: "GitHub", } @@ -68,8 +77,7 @@ const useStyles = makeStyles((theme) => ({ export interface SignInFormProps { isLoading: boolean redirectTo: string - authError?: Error | unknown - methodsError?: Error | unknown + loginErrors: Partial> authMethods?: AuthMethods onSubmit: ({ email, password }: { email: string; password: string }) => Promise // initialTouched is only used for testing the error state of the form. @@ -80,8 +88,7 @@ export const SignInForm: FC = ({ authMethods, redirectTo, isLoading, - authError, - methodsError, + loginErrors, onSubmit, initialTouched, }) => { @@ -101,18 +108,24 @@ export const SignInForm: FC = ({ onSubmit, initialTouched, }) - const getFieldHelpers = getFormHelpersWithError(form, authError) + const getFieldHelpers = getFormHelpersWithError( + form, + loginErrors.authError, + ) return ( <>
- {authError && ( - - )} - {methodsError && ( - + {Object.keys(loginErrors).map((errorKey: string) => + loginErrors[errorKey as LoginErrors] ? ( + + ) : null, )} { server.use( // Make login fail rest.post("/api/v2/users/login", async (req, res, ctx) => { - return res(ctx.status(500), ctx.json({ message: Language.authErrorMessage })) + return res(ctx.status(500), ctx.json({ message: Language.errorMessages.authError })) }), ) @@ -45,7 +45,7 @@ describe("LoginPage", () => { act(() => signInButton.click()) // Then - const errorMessage = await screen.findByText(Language.authErrorMessage) + const errorMessage = await screen.findByText(Language.errorMessages.authError) expect(errorMessage).toBeDefined() expect(history.location.pathname).toEqual("/login") }) diff --git a/site/src/pages/LoginPage/LoginPage.tsx b/site/src/pages/LoginPage/LoginPage.tsx index 9f6e7a0a93247..305012425130d 100644 --- a/site/src/pages/LoginPage/LoginPage.tsx +++ b/site/src/pages/LoginPage/LoginPage.tsx @@ -40,6 +40,8 @@ export const LoginPage: React.FC = () => { authSend({ type: "SIGN_IN", email, password }) } + const { authError, checkPermissionsError, getMethodsError } = authState.context + if (authState.matches("signedIn")) { return } else { @@ -54,8 +56,11 @@ export const LoginPage: React.FC = () => { authMethods={authState.context.methods} redirectTo={redirectTo} isLoading={isLoading} - authError={authState.context.authError} - methodsError={authState.context.getMethodsError as Error} + loginErrors={{ + authError, + checkPermissionsError, + getMethodsError, + }} onSubmit={onSubmit} /> diff --git a/site/src/pages/UserSettingsPage/SSHKeysPage/SSHKeysPage.test.tsx b/site/src/pages/UserSettingsPage/SSHKeysPage/SSHKeysPage.test.tsx index 01c1ac82dada7..0fbde8225a9e0 100644 --- a/site/src/pages/UserSettingsPage/SSHKeysPage/SSHKeysPage.test.tsx +++ b/site/src/pages/UserSettingsPage/SSHKeysPage/SSHKeysPage.test.tsx @@ -4,6 +4,7 @@ import { GlobalSnackbar } from "../../../components/GlobalSnackbar/GlobalSnackba import { MockGitSSHKey, renderWithAuth } from "../../../testHelpers/renderHelpers" import { Language as authXServiceLanguage } from "../../../xServices/auth/authXService" import { Language as SSHKeysPageLanguage, SSHKeysPage } from "./SSHKeysPage" +import { Language as SSHKeysPageViewLanguage } from "./SSHKeysPageView" describe("SSH keys Page", () => { it("shows the SSH key", async () => { @@ -26,7 +27,7 @@ describe("SSH keys Page", () => { // Click on the "Regenerate" button to display the confirm dialog const regenerateButton = screen.getByRole("button", { - name: SSHKeysPageLanguage.regenerateLabel, + name: SSHKeysPageViewLanguage.regenerateLabel, }) fireEvent.click(regenerateButton) const confirmDialog = screen.getByRole("dialog") @@ -72,7 +73,7 @@ describe("SSH keys Page", () => { // Click on the "Regenerate" button to display the confirm dialog const regenerateButton = screen.getByRole("button", { - name: SSHKeysPageLanguage.regenerateLabel, + name: SSHKeysPageViewLanguage.regenerateLabel, }) fireEvent.click(regenerateButton) const confirmDialog = screen.getByRole("dialog") @@ -85,7 +86,7 @@ describe("SSH keys Page", () => { fireEvent.click(confirmButton) // Check if the error message is displayed - await screen.findByText(authXServiceLanguage.errorRegenerateSSHKey) + await screen.findByText(SSHKeysPageViewLanguage.errorRegenerateSSHKey) // Check if the API was called correctly expect(API.regenerateUserSSHKey).toBeCalledTimes(1) diff --git a/site/src/pages/UserSettingsPage/SSHKeysPage/SSHKeysPage.tsx b/site/src/pages/UserSettingsPage/SSHKeysPage/SSHKeysPage.tsx index 14beb82fe6379..fcb008a6c0655 100644 --- a/site/src/pages/UserSettingsPage/SSHKeysPage/SSHKeysPage.tsx +++ b/site/src/pages/UserSettingsPage/SSHKeysPage/SSHKeysPage.tsx @@ -1,19 +1,14 @@ -import Box from "@material-ui/core/Box" -import Button from "@material-ui/core/Button" -import CircularProgress from "@material-ui/core/CircularProgress" import { useActor } from "@xstate/react" import React, { useContext, useEffect } from "react" -import { CodeExample } from "../../../components/CodeExample/CodeExample" import { ConfirmDialog } from "../../../components/ConfirmDialog/ConfirmDialog" import { Section } from "../../../components/Section/Section" -import { Stack } from "../../../components/Stack/Stack" import { XServiceContext } from "../../../xServices/StateContext" +import { SSHKeysPageView } from "./SSHKeysPageView" export const Language = { title: "SSH keys", description: "Coder automatically inserts a private key into every workspace; you can add the corresponding public key to any services (such as Git) that you need access to from your workspace.", - regenerateLabel: "Regenerate", regenerateDialogTitle: "Regenerate SSH key?", regenerateDialogMessage: "You will need to replace the public SSH key on services you use it with, and you'll need to rebuild existing workspaces.", @@ -24,36 +19,30 @@ export const Language = { export const SSHKeysPage: React.FC = () => { const xServices = useContext(XServiceContext) const [authState, authSend] = useActor(xServices.authXService) - const { sshKey } = authState.context + const { sshKey, getSSHKeyError, regenerateSSHKeyError } = authState.context useEffect(() => { authSend({ type: "GET_SSH_KEY" }) }, [authSend]) + const isLoading = authState.matches("signedIn.ssh.gettingSSHKey") + const hasLoaded = authState.matches("signedIn.ssh.loaded") + + const onRegenerateClick = () => { + authSend({ type: "REGENERATE_SSH_KEY" }) + } + return ( <>
- {!sshKey && ( - - - - )} - - {sshKey && ( - - -
- -
-
- )} +
= (args: SSHKeysPageViewProps) => ( + +) + +export const Example = Template.bind({}) +Example.args = { + isLoading: false, + hasLoaded: true, + sshKey: { + user_id: "test-user-id", + created_at: "2022-07-28T07:45:50.795918897Z", + updated_at: "2022-07-28T07:45:50.795919142Z", + public_key: "SSH-Key", + }, + onRegenerateClick: () => { + return Promise.resolve() + }, +} + +export const Loading = Template.bind({}) +Loading.args = { + ...Example.args, + isLoading: true, +} + +export const WithGetSSHKeyError = Template.bind({}) +WithGetSSHKeyError.args = { + ...Example.args, + hasLoaded: false, + getSSHKeyError: makeMockApiError({ + message: "Failed to get SSH key", + }), +} + +export const WithRegenerateSSHKeyError = Template.bind({}) +WithRegenerateSSHKeyError.args = { + ...Example.args, + regenerateSSHKeyError: makeMockApiError({ + message: "Failed to regenerate SSH key", + }), +} diff --git a/site/src/pages/UserSettingsPage/SSHKeysPage/SSHKeysPageView.tsx b/site/src/pages/UserSettingsPage/SSHKeysPage/SSHKeysPageView.tsx new file mode 100644 index 0000000000000..9aa135bcf6956 --- /dev/null +++ b/site/src/pages/UserSettingsPage/SSHKeysPage/SSHKeysPageView.tsx @@ -0,0 +1,64 @@ +import Box from "@material-ui/core/Box" +import Button from "@material-ui/core/Button" +import CircularProgress from "@material-ui/core/CircularProgress" +import { GitSSHKey } from "api/typesGenerated" +import { CodeExample } from "components/CodeExample/CodeExample" +import { ErrorSummary } from "components/ErrorSummary/ErrorSummary" +import { Stack } from "components/Stack/Stack" +import { FC } from "react" + +export const Language = { + errorRegenerateSSHKey: "Error on regenerating the SSH Key", + regenerateLabel: "Regenerate", +} + +export interface SSHKeysPageViewProps { + isLoading: boolean + hasLoaded: boolean + getSSHKeyError?: Error | unknown + regenerateSSHKeyError?: Error | unknown + sshKey?: GitSSHKey + onRegenerateClick: () => void +} + +export const SSHKeysPageView: FC = ({ + isLoading, + hasLoaded, + getSSHKeyError, + regenerateSSHKeyError, + sshKey, + onRegenerateClick, +}) => { + if (isLoading) { + return ( + + + + ) + } + + return ( + + {/* Regenerating the key is not an option if getSSHKey fails. + Only one of the error messages will exist at a single time */} + {getSSHKeyError && } + {regenerateSSHKeyError && ( + + )} + {hasLoaded && sshKey && ( + <> + +
+ +
+ + )} +
+ ) +} diff --git a/site/src/xServices/auth/authXService.ts b/site/src/xServices/auth/authXService.ts index 11d2c9c68d7b3..cf0a9432ea33a 100644 --- a/site/src/xServices/auth/authXService.ts +++ b/site/src/xServices/auth/authXService.ts @@ -1,13 +1,12 @@ import { assign, createMachine } from "xstate" import * as API from "../../api/api" import * as TypesGen from "../../api/typesGenerated" -import { displayError, displaySuccess } from "../../components/GlobalSnackbar/utils" +import { displaySuccess } from "../../components/GlobalSnackbar/utils" export const Language = { successProfileUpdate: "Updated settings.", successSecurityUpdate: "Updated password.", successRegenerateSSHKey: "SSH Key regenerated successfully", - errorRegenerateSSHKey: "Error on regenerate the SSH Key", } export const checks = { @@ -130,7 +129,7 @@ const sshState = { ], onError: [ { - actions: ["assignRegenerateSSHKeyError", "notifySSHKeyRegenerationError"], + actions: ["assignRegenerateSSHKeyError"], target: "#authState.signedIn.ssh.loaded.idle", }, ], @@ -214,12 +213,13 @@ export const authMachine = tags: "loading", }, gettingUser: { + entry: "clearGetUserError", invoke: { src: "getMe", id: "getMe", onDone: [ { - actions: ["assignMe", "clearGetUserError"], + actions: ["assignMe"], target: "gettingPermissions", }, ], @@ -488,9 +488,6 @@ export const authMachine = notifySuccessSSHKeyRegenerated: () => { displaySuccess(Language.successRegenerateSSHKey) }, - notifySSHKeyRegenerationError: () => { - displayError(Language.errorRegenerateSSHKey) - }, }, }, ) pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy