diff --git a/site/src/components/AlertBanner/AlertBanner.tsx b/site/src/components/AlertBanner/AlertBanner.tsx index 9df5a92ae161a..f95eb6ade879f 100644 --- a/site/src/components/AlertBanner/AlertBanner.tsx +++ b/site/src/components/AlertBanner/AlertBanner.tsx @@ -85,7 +85,8 @@ const useStyles = makeStyles((theme) => ({ padding: `${theme.spacing(1)}px ${theme.spacing(2)}px`, backgroundColor: `${colors.gray[16]}`, - "& svg": { + // targeting the alert icon rather than the expander icon + "& svg:nth-child(2)": { marginTop: props.hasDetail ? `${theme.spacing(1)}px` : "inherit", marginRight: `${theme.spacing(1)}px`, }, diff --git a/site/src/components/ErrorSummary/ErrorSummary.stories.tsx b/site/src/components/ErrorSummary/ErrorSummary.stories.tsx deleted file mode 100644 index 02da50d462219..0000000000000 --- a/site/src/components/ErrorSummary/ErrorSummary.stories.tsx +++ /dev/null @@ -1,61 +0,0 @@ -import { action } from "@storybook/addon-actions" -import { ComponentMeta, Story } from "@storybook/react" -import { ErrorSummary, ErrorSummaryProps } from "./ErrorSummary" - -export default { - title: "components/ErrorSummary", - component: ErrorSummary, -} as ComponentMeta - -const Template: Story = (args) => - -export const WithError = Template.bind({}) -WithError.args = { - error: new Error("Something went wrong!"), -} - -export const WithRetry = Template.bind({}) -WithRetry.args = { - error: new Error("Failed to fetch something!"), - retry: () => { - action("retry") - }, -} - -export const WithUndefined = Template.bind({}) - -export const WithDefaultMessage = Template.bind({}) -WithDefaultMessage.args = { - // Unknown error type - error: { - message: "Failed to fetch something!", - }, - defaultMessage: "This is a default error message", -} - -export const WithDismissible = Template.bind({}) -WithDismissible.args = { - error: { - response: { - data: { - message: "Failed to fetch something!", - }, - }, - isAxiosError: true, - }, - dismissible: true, -} - -export const WithDetails = Template.bind({}) -WithDetails.args = { - error: { - response: { - data: { - message: "Failed to fetch something!", - detail: "The resource you requested does not exist in the database.", - }, - }, - isAxiosError: true, - }, - dismissible: true, -} diff --git a/site/src/components/ErrorSummary/ErrorSummary.test.tsx b/site/src/components/ErrorSummary/ErrorSummary.test.tsx deleted file mode 100644 index 1fda20d683fd9..0000000000000 --- a/site/src/components/ErrorSummary/ErrorSummary.test.tsx +++ /dev/null @@ -1,81 +0,0 @@ -import { fireEvent, render, screen } from "@testing-library/react" -import { ErrorSummary } from "./ErrorSummary" -import { i18n } from "i18n" - -const { t } = i18n - -describe("ErrorSummary", () => { - it("renders", async () => { - // When - const error = new Error("test error message") - render() - - // Then - const element = await screen.findByText("test error message") - expect(element).toBeDefined() - }) - - it("shows details on More click", async () => { - // When - const error = { - response: { - data: { - message: "Failed to fetch something!", - detail: "The resource you requested does not exist in the database.", - }, - }, - isAxiosError: true, - } - render() - - // Then - const expandText = t("ctas.expand", { ns: "common" }) - fireEvent.click(screen.getByText(expandText)) - const element = await screen.findByText( - "The resource you requested does not exist in the database.", - { exact: false }, - ) - expect(element.closest(".MuiCollapse-entered")).toBeDefined() - }) - - it("hides details on Less click", async () => { - // When - const error = { - response: { - data: { - message: "Failed to fetch something!", - detail: "The resource you requested does not exist in the database.", - }, - }, - isAxiosError: true, - } - render() - - // Then - const expandText = t("ctas.expand", { ns: "common" }) - const collapseText = t("ctas.collapse", { ns: "common" }) - - fireEvent.click(screen.getByText(expandText)) - fireEvent.click(screen.getByText(collapseText)) - const element = await screen.findByText( - "The resource you requested does not exist in the database.", - { exact: false }, - ) - expect(element.closest(".MuiCollapse-hidden")).toBeDefined() - }) - - it("renders nothing on closing", async () => { - // When - const error = new Error("test error message") - render() - - // Then - const element = await screen.findByText("test error message") - expect(element).toBeDefined() - - const closeIcon = screen.getAllByRole("button")[0] - fireEvent.click(closeIcon) - const nullElement = screen.queryByText("test error message") - expect(nullElement).toBeNull() - }) -}) diff --git a/site/src/components/ErrorSummary/ErrorSummary.tsx b/site/src/components/ErrorSummary/ErrorSummary.tsx deleted file mode 100644 index 5ac024e8d9077..0000000000000 --- a/site/src/components/ErrorSummary/ErrorSummary.tsx +++ /dev/null @@ -1,129 +0,0 @@ -import Button from "@material-ui/core/Button" -import Collapse from "@material-ui/core/Collapse" -import IconButton from "@material-ui/core/IconButton" -import { darken, lighten, makeStyles, Theme } from "@material-ui/core/styles" -import CloseIcon from "@material-ui/icons/Close" -import RefreshIcon from "@material-ui/icons/Refresh" -import { ApiError, getErrorDetail, getErrorMessage } from "api/errors" -import { Expander } from "components/Expander/Expander" -import { Stack } from "components/Stack/Stack" -import { FC, useState } from "react" - -export const Language = { - retryMessage: "Retry", - unknownErrorMessage: "An unknown error has occurred", - moreDetails: "More", - lessDetails: "Less", -} - -export interface ErrorSummaryProps { - error: ApiError | Error | unknown - retry?: () => void - dismissible?: boolean - defaultMessage?: string -} - -export const ErrorSummary: FC> = ({ - error, - retry, - dismissible, - defaultMessage, -}) => { - const message = getErrorMessage(error, defaultMessage || Language.unknownErrorMessage) - const detail = getErrorDetail(error) - const [showDetails, setShowDetails] = useState(false) - const [isOpen, setOpen] = useState(true) - - const styles = useStyles({ showDetails }) - - const closeError = () => { - setOpen(false) - } - - if (!isOpen) { - return null - } - - return ( - - - - {message} - {Boolean(detail) && } - - {dismissible && ( - - - - )} - - -
{detail}
-
- {retry && ( -
- -
- )} -
- ) -} - -interface StyleProps { - showDetails?: boolean -} - -const useStyles = makeStyles((theme) => ({ - root: { - background: darken(theme.palette.error.main, 0.6), - padding: `${theme.spacing(2)}px`, - borderRadius: theme.shape.borderRadius, - gap: 0, - }, - flex: { - display: "flex", - }, - messageBox: { - justifyContent: "space-between", - }, - errorMessage: { - marginRight: `${theme.spacing(1)}px`, - }, - detailsLink: { - cursor: "pointer", - color: `${lighten(theme.palette.primary.light, 0.2)}`, - }, - details: { - marginTop: `${theme.spacing(2)}px`, - padding: `${theme.spacing(2)}px`, - background: darken(theme.palette.error.main, 0.7), - borderRadius: theme.shape.borderRadius, - }, - iconButton: { - padding: 0, - }, - closeIcon: { - width: 25, - height: 25, - color: theme.palette.primary.contrastText, - }, - retry: { - marginTop: `${theme.spacing(2)}px`, - }, - retryButton: { - color: theme.palette.error.contrastText, - borderColor: theme.palette.error.contrastText, - - "&:hover": { - backgroundColor: theme.palette.error.dark, - }, - }, -})) diff --git a/site/src/components/Resources/Resources.tsx b/site/src/components/Resources/Resources.tsx index 3ee0ce745a0f5..672cfb8533047 100644 --- a/site/src/components/Resources/Resources.tsx +++ b/site/src/components/Resources/Resources.tsx @@ -9,7 +9,6 @@ import TableRow from "@material-ui/core/TableRow" import { Skeleton } from "@material-ui/lab" import useTheme from "@material-ui/styles/useTheme" import { CloseDropdown, OpenDropdown } from "components/DropdownArrows/DropdownArrows" -import { ErrorSummary } from "components/ErrorSummary/ErrorSummary" import { PortForwardButton } from "components/PortForwardButton/PortForwardButton" import { TableCellDataPrimary } from "components/TableCellData/TableCellData" import { FC, useState } from "react" @@ -25,6 +24,7 @@ import { AgentOutdatedTooltip } from "../Tooltips/AgentOutdatedTooltip" import { ResourcesHelpTooltip } from "../Tooltips/ResourcesHelpTooltip" import { ResourceAgentLatency } from "./ResourceAgentLatency" import { ResourceAvatarData } from "./ResourceAvatarData" +import { AlertBanner } from "components/AlertBanner/AlertBanner" const Language = { resources: "Resources", @@ -68,7 +68,7 @@ export const Resources: FC> = ({
{getResourcesError ? ( - + ) : ( diff --git a/site/src/components/SettingsAccountForm/SettingsAccountForm.tsx b/site/src/components/SettingsAccountForm/SettingsAccountForm.tsx index 2873c3e6bfe86..4a5b0f90aebd9 100644 --- a/site/src/components/SettingsAccountForm/SettingsAccountForm.tsx +++ b/site/src/components/SettingsAccountForm/SettingsAccountForm.tsx @@ -1,11 +1,11 @@ import TextField from "@material-ui/core/TextField" -import { ErrorSummary } from "components/ErrorSummary/ErrorSummary" import { FormikContextType, FormikTouched, useFormik } from "formik" import { FC } from "react" import * as Yup from "yup" import { getFormHelpers, nameValidator, onChangeTrimmed } from "../../util/formUtils" import { LoadingButton } from "../LoadingButton/LoadingButton" import { Stack } from "../Stack/Stack" +import { AlertBanner } from "components/AlertBanner/AlertBanner" export interface AccountFormValues { username: string @@ -53,7 +53,9 @@ export const AccountForm: FC> = ({ <>
- {updateProfileError ? : <>} + {Boolean(updateProfileError) && ( + + )} = ({ <> - {updateSecurityError ? : <>} + {Boolean(updateSecurityError) && ( + + )} > = ({ - {Object.keys(loginErrors).map((errorKey: string) => - loginErrors[errorKey as LoginErrors] ? ( - - ) : null, + {Object.keys(loginErrors).map( + (errorKey: string) => + Boolean(loginErrors[errorKey as LoginErrors]) && ( + + ), )} > = ({ const hasTemplateIcon = workspace.template_icon && workspace.template_icon !== "" const buildError = Boolean(workspaceErrors[WorkspaceErrors.BUILD_ERROR]) && ( - + ) const cancellationError = Boolean(workspaceErrors[WorkspaceErrors.CANCELLATION_ERROR]) && ( - + ) const workspaceRefreshWarning = Boolean(workspaceErrors[WorkspaceErrors.GET_RESOURCES_ERROR]) && ( ) @@ -161,7 +168,10 @@ export const Workspace: FC> = ({ {workspaceErrors[WorkspaceErrors.GET_BUILDS_ERROR] ? ( - + ) : ( )} diff --git a/site/src/components/WorkspaceQuota/WorkspaceQuota.tsx b/site/src/components/WorkspaceQuota/WorkspaceQuota.tsx index e55ebf10c9f0d..14fa26bfcb994 100644 --- a/site/src/components/WorkspaceQuota/WorkspaceQuota.tsx +++ b/site/src/components/WorkspaceQuota/WorkspaceQuota.tsx @@ -2,7 +2,7 @@ import Box from "@material-ui/core/Box" import LinearProgress from "@material-ui/core/LinearProgress" import { makeStyles } from "@material-ui/core/styles" import Skeleton from "@material-ui/lab/Skeleton" -import { ErrorSummary } from "components/ErrorSummary/ErrorSummary" +import { AlertBanner } from "components/AlertBanner/AlertBanner" import { Stack } from "components/Stack/Stack" import { FC } from "react" import * as TypesGen from "../../api/typesGenerated" @@ -28,7 +28,7 @@ export const WorkspaceQuota: FC = ({ quota, error }) => { Workspace Quota - + ) diff --git a/site/src/components/WorkspaceScheduleForm/WorkspaceScheduleForm.tsx b/site/src/components/WorkspaceScheduleForm/WorkspaceScheduleForm.tsx index 05f225813d371..d445450bf47bf 100644 --- a/site/src/components/WorkspaceScheduleForm/WorkspaceScheduleForm.tsx +++ b/site/src/components/WorkspaceScheduleForm/WorkspaceScheduleForm.tsx @@ -8,7 +8,7 @@ import MenuItem from "@material-ui/core/MenuItem" import makeStyles from "@material-ui/core/styles/makeStyles" import Switch from "@material-ui/core/Switch" import TextField from "@material-ui/core/TextField" -import { ErrorSummary } from "components/ErrorSummary/ErrorSummary" +import { AlertBanner } from "components/AlertBanner/AlertBanner" import { Section } from "components/Section/Section" import dayjs from "dayjs" import advancedFormat from "dayjs/plugin/advancedFormat" @@ -227,7 +227,9 @@ export const WorkspaceScheduleForm: FC - {submitScheduleError ? : <>} + {Boolean(submitScheduleError) && ( + + )}
- {props.createWorkspaceErrors[CreateWorkspaceErrors.GET_TEMPLATES_ERROR] ? ( - - ) : null} - {props.createWorkspaceErrors[CreateWorkspaceErrors.GET_TEMPLATE_SCHEMA_ERROR] ? ( - - ) : null} + )} ) } @@ -122,7 +124,8 @@ export const CreateWorkspacePageView: FC {Boolean(props.createWorkspaceErrors[CreateWorkspaceErrors.CREATE_WORKSPACE_ERROR]) && ( - )} diff --git a/site/src/pages/TemplatePage/TemplatePage.tsx b/site/src/pages/TemplatePage/TemplatePage.tsx index b2e17ee66a50d..8f1044f7740b1 100644 --- a/site/src/pages/TemplatePage/TemplatePage.tsx +++ b/site/src/pages/TemplatePage/TemplatePage.tsx @@ -1,7 +1,7 @@ import { makeStyles } from "@material-ui/core/styles" import { useMachine, useSelector } from "@xstate/react" +import { AlertBanner } from "components/AlertBanner/AlertBanner" import { DeleteDialog } from "components/Dialogs/DeleteDialog/DeleteDialog" -import { ErrorSummary } from "components/ErrorSummary/ErrorSummary" import { Margins } from "components/Margins/Margins" import { FC, useContext } from "react" import { Helmet } from "react-helmet-async" @@ -57,7 +57,7 @@ export const TemplatePage: FC> = () => { return (
- +
) diff --git a/site/src/pages/TemplatePage/TemplatePageView.tsx b/site/src/pages/TemplatePage/TemplatePageView.tsx index 82414c32bba38..1a58f44c40d24 100644 --- a/site/src/pages/TemplatePage/TemplatePageView.tsx +++ b/site/src/pages/TemplatePage/TemplatePageView.tsx @@ -6,7 +6,7 @@ import AddCircleOutline from "@material-ui/icons/AddCircleOutline" import SettingsOutlined from "@material-ui/icons/SettingsOutlined" import { DeleteButton } from "components/DropdownButton/ActionCtas" import { DropdownButton } from "components/DropdownButton/DropdownButton" -import { ErrorSummary } from "components/ErrorSummary/ErrorSummary" +import { AlertBanner } from "components/AlertBanner/AlertBanner" import { Markdown } from "components/Markdown/Markdown" import frontMatter from "front-matter" import { FC } from "react" @@ -65,10 +65,8 @@ export const TemplatePageView: FC const readme = frontMatter(activeTemplateVersion.readme) const hasIcon = template.icon && template.icon !== "" - const deleteError = deleteTemplateError ? ( - - ) : ( - <> + const deleteError = Boolean(deleteTemplateError) && ( + ) const getStartedResources = (resources: WorkspaceResource[]) => { diff --git a/site/src/pages/TemplateSettingsPage/TemplateSettingsPageView.tsx b/site/src/pages/TemplateSettingsPage/TemplateSettingsPageView.tsx index a4c2515f2714a..f05fabbbec001 100644 --- a/site/src/pages/TemplateSettingsPage/TemplateSettingsPageView.tsx +++ b/site/src/pages/TemplateSettingsPage/TemplateSettingsPageView.tsx @@ -1,5 +1,5 @@ import { Template, UpdateTemplateMeta } from "api/typesGenerated" -import { ErrorSummary } from "components/ErrorSummary/ErrorSummary" +import { AlertBanner } from "components/AlertBanner/AlertBanner" import { FullPageForm } from "components/FullPageForm/FullPageForm" import { Loader } from "components/Loader/Loader" import { ComponentProps, FC } from "react" @@ -33,7 +33,9 @@ export const TemplateSettingsPageView: FC = ({ return ( - {Boolean(errors.getTemplateError) && } + {Boolean(errors.getTemplateError) && ( + + )} {isLoading && } {template && ( - - diff --git a/site/src/pages/UserSettingsPage/AccountPage/AccountPage.test.tsx b/site/src/pages/UserSettingsPage/AccountPage/AccountPage.test.tsx index 0d8624373ba33..7bc906d4d69cf 100644 --- a/site/src/pages/UserSettingsPage/AccountPage/AccountPage.test.tsx +++ b/site/src/pages/UserSettingsPage/AccountPage/AccountPage.test.tsx @@ -1,11 +1,13 @@ import { fireEvent, screen, waitFor } from "@testing-library/react" -import { Language as ErrorSummaryLanguage } from "components/ErrorSummary/ErrorSummary" import * as API from "../../../api/api" import { GlobalSnackbar } from "../../../components/GlobalSnackbar/GlobalSnackbar" import * as AccountForm from "../../../components/SettingsAccountForm/SettingsAccountForm" import { renderWithAuth } from "../../../testHelpers/renderHelpers" import * as AuthXService from "../../../xServices/auth/authXService" import { AccountPage } from "./AccountPage" +import i18next from "i18next" + +const { t } = i18next const renderPage = () => { return renderWithAuth( @@ -83,7 +85,8 @@ describe("AccountPage", () => { const { user } = renderPage() await fillAndSubmitForm() - const errorMessage = await screen.findByText(ErrorSummaryLanguage.unknownErrorMessage) + const errorText = t("warningsAndErrors.somethingWentWrong", { ns: "common" }) + const errorMessage = await screen.findByText(errorText) expect(errorMessage).toBeDefined() expect(API.updateProfile).toBeCalledTimes(1) expect(API.updateProfile).toBeCalledWith(user.id, newData) diff --git a/site/src/pages/UserSettingsPage/SSHKeysPage/SSHKeysPageView.tsx b/site/src/pages/UserSettingsPage/SSHKeysPage/SSHKeysPageView.tsx index 694919454f396..a4dae9aa0db4e 100644 --- a/site/src/pages/UserSettingsPage/SSHKeysPage/SSHKeysPageView.tsx +++ b/site/src/pages/UserSettingsPage/SSHKeysPage/SSHKeysPageView.tsx @@ -2,8 +2,8 @@ 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 { AlertBanner } from "components/AlertBanner/AlertBanner" import { CodeExample } from "components/CodeExample/CodeExample" -import { ErrorSummary } from "components/ErrorSummary/ErrorSummary" import { Stack } from "components/Stack/Stack" import { FC } from "react" @@ -42,15 +42,14 @@ export const SSHKeysPageView: FC> {/* Regenerating the key is not an option if getSSHKey fails. Only one of the error messages will exist at a single time */} - {getSSHKeyError ? : <>} - {regenerateSSHKeyError ? ( - } + {Boolean(regenerateSSHKeyError) && ( + - ) : ( - <> )} {hasLoaded && sshKey && ( <> diff --git a/site/src/pages/UserSettingsPage/SecurityPage/SecurityPage.test.tsx b/site/src/pages/UserSettingsPage/SecurityPage/SecurityPage.test.tsx index eded17490ccb4..2c9058fe06b2b 100644 --- a/site/src/pages/UserSettingsPage/SecurityPage/SecurityPage.test.tsx +++ b/site/src/pages/UserSettingsPage/SecurityPage/SecurityPage.test.tsx @@ -1,11 +1,13 @@ import { fireEvent, screen, waitFor } from "@testing-library/react" -import { Language as ErrorSummaryLanguage } from "components/ErrorSummary/ErrorSummary" import * as API from "../../../api/api" import { GlobalSnackbar } from "../../../components/GlobalSnackbar/GlobalSnackbar" import * as SecurityForm from "../../../components/SettingsSecurityForm/SettingsSecurityForm" import { renderWithAuth } from "../../../testHelpers/renderHelpers" import * as AuthXService from "../../../xServices/auth/authXService" import { SecurityPage } from "./SecurityPage" +import i18next from "i18next" + +const { t } = i18next const renderPage = () => { return renderWithAuth( @@ -105,7 +107,8 @@ describe("SecurityPage", () => { const { user } = renderPage() await fillAndSubmitForm() - const errorMessage = await screen.findByText(ErrorSummaryLanguage.unknownErrorMessage) + const errorText = t("warningsAndErrors.somethingWentWrong", { ns: "common" }) + const errorMessage = await screen.findByText(errorText) expect(errorMessage).toBeDefined() expect(API.updateUserPassword).toBeCalledTimes(1) expect(API.updateUserPassword).toBeCalledWith(user.id, newData) diff --git a/site/src/pages/WorkspacePage/WorkspacePage.tsx b/site/src/pages/WorkspacePage/WorkspacePage.tsx index 7daa934990507..8c8c09dd2026a 100644 --- a/site/src/pages/WorkspacePage/WorkspacePage.tsx +++ b/site/src/pages/WorkspacePage/WorkspacePage.tsx @@ -1,12 +1,12 @@ import { makeStyles } from "@material-ui/core/styles" import { useMachine } from "@xstate/react" +import { AlertBanner } from "components/AlertBanner/AlertBanner" import { ChooseOne, Cond } from "components/Conditionals/ChooseOne" import { FC, useEffect } from "react" import { useParams } from "react-router-dom" -import { ErrorSummary } from "../../components/ErrorSummary/ErrorSummary" -import { FullScreenLoader } from "../../components/Loader/FullScreenLoader" -import { firstOrItem } from "../../util/array" -import { workspaceMachine } from "../../xServices/workspace/workspaceXService" +import { FullScreenLoader } from "components/Loader/FullScreenLoader" +import { firstOrItem } from "util/array" +import { workspaceMachine } from "xServices/workspace/workspaceXService" import { WorkspaceReadyPage } from "./WorkspaceReadyPage" export const WorkspacePage: FC = () => { @@ -30,9 +30,13 @@ export const WorkspacePage: FC = () => {
- {Boolean(getWorkspaceError) && } - {Boolean(getTemplateWarning) && } - {Boolean(checkPermissionsError) && } + {Boolean(getWorkspaceError) && } + {Boolean(getTemplateWarning) && ( + + )} + {Boolean(checkPermissionsError) && ( + + )}
diff --git a/site/src/pages/WorkspaceSchedulePage/WorkspaceSchedulePage.tsx b/site/src/pages/WorkspaceSchedulePage/WorkspaceSchedulePage.tsx index 7548d22135b47..b88f12ac2f2d6 100644 --- a/site/src/pages/WorkspaceSchedulePage/WorkspaceSchedulePage.tsx +++ b/site/src/pages/WorkspaceSchedulePage/WorkspaceSchedulePage.tsx @@ -1,10 +1,10 @@ import { useMachine } from "@xstate/react" +import { AlertBanner } from "components/AlertBanner/AlertBanner" import { scheduleToAutoStart } from "pages/WorkspaceSchedulePage/schedule" import { ttlMsToAutoStop } from "pages/WorkspaceSchedulePage/ttl" import React, { useEffect, useState } from "react" import { Navigate, useNavigate, useParams } from "react-router-dom" import * as TypesGen from "../../api/typesGenerated" -import { ErrorSummary } from "../../components/ErrorSummary/ErrorSummary" import { FullScreenLoader } from "../../components/Loader/FullScreenLoader" import { WorkspaceScheduleForm } from "../../components/WorkspaceScheduleForm/WorkspaceScheduleForm" import { firstOrItem } from "../../util/array" @@ -59,18 +59,17 @@ export const WorkspaceSchedulePage: React.FC = () => { if (scheduleState.matches("error")) { return ( - scheduleSend({ type: "GET_WORKSPACE", username, workspaceName })} /> ) } if (!permissions?.updateWorkspace) { - return + return } if (scheduleState.matches("presentForm") || scheduleState.matches("submittingSchedule")) {