Skip to content

feat: redesign error alert #4403

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 14 commits into from
Oct 7, 2022
Prev Previous commit
Next Next commit
cleanup
  • Loading branch information
Kira-Pilot committed Oct 6, 2022
commit 44b2748fe3420f31fde1f1475dd11f37a05f794f
101 changes: 101 additions & 0 deletions site/src/components/AlertBanner/AlertBanner.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import { Story } from "@storybook/react"
import { AlertBanner, AlertBannerProps } from "./AlertBanner"
import Button from "@material-ui/core/Button"
import { makeMockApiError } from "testHelpers/entities"

export default {
title: "components/AlertBanner",
component: AlertBanner,
}

const ExampleAction = (
<Button onClick={() => null} size="small">
Button
</Button>
)

const mockError = makeMockApiError({
message: "Email or password was invalid",
detail: "Password is invalid",
})

const Template: Story<AlertBannerProps> = (args) => <AlertBanner {...args} />

export const Warning = Template.bind({})
Warning.args = {
text: "This is a warning",
severity: "warning",
}

export const ErrorWithDefaultMessage = Template.bind({})
ErrorWithDefaultMessage.args = {
text: "This is an error",
severity: "error",
}

export const ErrorWithErrorMessage = Template.bind({})
ErrorWithErrorMessage.args = {
error: mockError,
severity: "error",
}

export const WarningWithDismiss = Template.bind({})
WarningWithDismiss.args = {
text: "This is a warning",
dismissible: true,
severity: "warning",
}

export const ErrorWithDismiss = Template.bind({})
ErrorWithDismiss.args = {
error: mockError,
dismissible: true,
severity: "error",
}

export const WarningWithAction = Template.bind({})
WarningWithAction.args = {
text: "This is a warning",
actions: [ExampleAction],
severity: "warning",
}

export const ErrorWithAction = Template.bind({})
ErrorWithAction.args = {
error: mockError,
actions: [ExampleAction],
severity: "error",
}

export const WarningWithActionAndDismiss = Template.bind({})
WarningWithActionAndDismiss.args = {
text: "This is a warning",
actions: [ExampleAction],
dismissible: true,
severity: "warning",
}

export const ErrorWithActionAndDismiss = Template.bind({})
ErrorWithActionAndDismiss.args = {
error: mockError,
actions: [ExampleAction],
dismissible: true,
severity: "error",
}

export const ErrorWithRetry = Template.bind({})
ErrorWithRetry.args = {
error: mockError,
retry: () => null,
dismissible: true,
severity: "error",
}

export const ErrorWithActionRetryAndDismiss = Template.bind({})
ErrorWithActionRetryAndDismiss.args = {
error: mockError,
actions: [ExampleAction],
retry: () => null,
dismissible: true,
severity: "error",
}
Original file line number Diff line number Diff line change
@@ -1,59 +1,43 @@
import { useState, FC, ReactElement } from "react"
import { useState, FC } from "react"
import Collapse from "@material-ui/core/Collapse"
import { Stack } from "components/Stack/Stack"
import { makeStyles, Theme } from "@material-ui/core/styles"
import { colors } from "theme/colors"
import ReportProblemOutlinedIcon from "@material-ui/icons/ReportProblemOutlined"
import Button from "@material-ui/core/Button"
import { useTranslation } from "react-i18next"
import ErrorOutlineOutlinedIcon from "@material-ui/icons/ErrorOutlineOutlined"
import { ApiError, getErrorDetail, getErrorMessage } from "api/errors"
import { getErrorDetail, getErrorMessage } from "api/errors"
import { Expander } from "components/Expander/Expander"
import { Severity, AlertBannerProps } from "./alertTypes"
import { severityConstants } from "./severityConstants"
import { AlertBannerCtas } from "./AlertBannerCtas"

type Severity = "warning" | "error"

export interface WarningAlertProps {
text: string
severity: Severity
error?: ApiError | Error | unknown
dismissible?: boolean
actions?: ReactElement[]
}

const severityConstants: Record<Severity, { color: string; icon: ReactElement }> = {
warning: {
color: colors.orange[7],
icon: <ReportProblemOutlinedIcon fontSize="small" style={{ color: colors.orange[7] }} />,
},
error: {
color: colors.red[7],
icon: (
<ErrorOutlineOutlinedIcon
fontSize="small"
style={{ color: colors.red[7], marginTop: "8px" }}
/>
),
},
}

export const WarningAlert: FC<WarningAlertProps> = ({
text,
/**
* severity: the level of alert severity (see ./severityTypes.ts)
* text: default text to be displayed to the user; useful for warnings or as a fallback error message
* error: should be passed in if the severity is 'Error'; warnings can use 'text' instead
* actions: an array of CTAs passed in by the consumer
* dismissible: determines whether or not the banner should have a `Dismiss` CTA
* retry: a handler to retry the action that spawned the error
*/
export const AlertBanner: FC<AlertBannerProps> = ({
severity,
text,
error,
dismissible = false,
actions = [],
retry,
dismissible = false,
}) => {
const { t } = useTranslation("common")
const classes = useStyles({ severity })

const [open, setOpen] = useState(true)

// if an error is passed in, display that error, otherwise
// display the text passed in, e.g. warning text
const alertMessage = getErrorMessage(error, text)
const alertMessage = getErrorMessage(error, text ?? t("warningsAndErrors.somethingWentWrong"))

// if we have an error, check if there's detail to display
const detail = error ? getErrorDetail(error) : undefined
const classes = useStyles({ severity, hasDetail: Boolean(detail) })

const [showDetails, setShowDetails] = useState(false)

return (
Expand All @@ -66,7 +50,7 @@ export const WarningAlert: FC<WarningAlertProps> = ({
justifyContent="space-between"
>
<Stack direction="row" spacing={1}>
{severityConstants[severity].icon}
<div className={classes.iconContainer}>{severityConstants[severity].icon}</div>
<Stack spacing={0}>
{alertMessage}
{detail && (
Expand All @@ -77,21 +61,20 @@ export const WarningAlert: FC<WarningAlertProps> = ({
</Stack>
</Stack>

<Stack direction="row">
{actions.length > 0 && actions.map((action) => <div key={String(action)}>{action}</div>)}
{dismissible && (
<Button size="small" onClick={() => setOpen(false)} variant="outlined">
{t("ctas.dismissCta")}
</Button>
)}
</Stack>
<AlertBannerCtas
actions={actions}
dismissible={dismissible}
retry={retry}
setOpen={setOpen}
/>
</Stack>
</Collapse>
)
}

interface StyleProps {
severity: Severity
hasDetail: boolean
}

const useStyles = makeStyles<Theme, StyleProps>((theme) => ({
Expand All @@ -102,4 +85,7 @@ const useStyles = makeStyles<Theme, StyleProps>((theme) => ({
padding: `${theme.spacing(1)}px ${theme.spacing(2)}px`,
backgroundColor: `${colors.gray[16]}`,
}),
iconContainer: (props) => ({
marginTop: props.hasDetail ? "8px" : "unset",
}),
}))
42 changes: 42 additions & 0 deletions site/src/components/AlertBanner/AlertBannerCtas.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { FC } from "react"
import { AlertBannerProps } from "./alertTypes"
import { Stack } from "components/Stack/Stack"
import Button from "@material-ui/core/Button"
import RefreshIcon from "@material-ui/icons/Refresh"
import { useTranslation } from "react-i18next"

type AlertBannerCtasProps = Pick<AlertBannerProps, "actions" | "dismissible" | "retry"> & {
setOpen: (arg0: boolean) => void
}

export const AlertBannerCtas: FC<AlertBannerCtasProps> = ({
actions = [],
dismissible,
retry,
setOpen,
}) => {
const { t } = useTranslation("common")

return (
<Stack direction="row">
{/* CTAs passed in by the consumer */}
{actions.length > 0 && actions.map((action) => <div key={String(action)}>{action}</div>)}

{/* retry CTA */}
{retry && (
<div>
<Button size="small" onClick={retry} startIcon={<RefreshIcon />} variant="outlined">
{t("ctas.retry")}
</Button>
</div>
)}

{/* close CTA */}
{dismissible && (
<Button size="small" onClick={() => setOpen(false)} variant="outlined">
{t("ctas.dismissCta")}
</Button>
)}
</Stack>
)
}
13 changes: 13 additions & 0 deletions site/src/components/AlertBanner/alertTypes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { ApiError } from "api/errors"
import { ReactElement } from 'react'

export type Severity = "warning" | "error"

export interface AlertBannerProps {
severity: Severity
text?: string
error?: ApiError | Error | unknown
actions?: ReactElement[]
dismissible?: boolean
retry?: () => void
}
16 changes: 16 additions & 0 deletions site/src/components/AlertBanner/severityConstants.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import ReportProblemOutlinedIcon from "@material-ui/icons/ReportProblemOutlined"
import ErrorOutlineOutlinedIcon from "@material-ui/icons/ErrorOutlineOutlined"
import { colors } from "theme/colors"
import { Severity } from "./alertTypes"
import { ReactElement } from "react"

export const severityConstants: Record<Severity, { color: string; icon: ReactElement }> = {
warning: {
color: colors.orange[7],
icon: <ReportProblemOutlinedIcon fontSize="small" style={{ color: colors.orange[7] }} />,
},
error: {
color: colors.red[7],
icon: <ErrorOutlineOutlinedIcon fontSize="small" style={{ color: colors.red[7] }} />,
},
}
38 changes: 0 additions & 38 deletions site/src/components/WarningAlert/WarningAlert.stories.tsx

This file was deleted.

4 changes: 2 additions & 2 deletions site/src/components/Workspace/Workspace.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import { WorkspaceScheduleBanner } from "../WorkspaceScheduleBanner/WorkspaceSch
import { WorkspaceScheduleButton } from "../WorkspaceScheduleButton/WorkspaceScheduleButton"
import { WorkspaceSection } from "../WorkspaceSection/WorkspaceSection"
import { WorkspaceStats } from "../WorkspaceStats/WorkspaceStats"
import { WarningAlert } from "../WarningAlert/WarningAlert"
import { AlertBanner } from "../AlertBanner/AlertBanner"
import { useTranslation } from "react-i18next"

export enum WorkspaceErrors {
Expand Down Expand Up @@ -87,7 +87,7 @@ export const Workspace: FC<React.PropsWithChildren<WorkspaceProps>> = ({
)

const workspaceRefreshWarning = Boolean(workspaceErrors[WorkspaceErrors.GET_RESOURCES_ERROR]) && (
<WarningAlert
<AlertBanner
text={t("warningsAndErrors.workspaceRefreshWarning")}
severity="warning"
dismissible
Expand Down
Loading