feat: display specific errors if templates page fails (#4023)

* Surface templates page errors

* Format

* Separate error messages

* Fix story

* Format

* Format

* Fix imports

* Remove unnecessary check

* Format
This commit is contained in:
Presley Pizzo
2022-09-13 10:26:58 -04:00
committed by GitHub
parent 21e8fb243b
commit 83c35bb916
8 changed files with 219 additions and 177 deletions
+1 -1
View File
@@ -16,7 +16,7 @@ import { NotFoundPage } from "./pages/404Page/404Page"
import { CliAuthenticationPage } from "./pages/CliAuthPage/CliAuthPage"
import { HealthzPage } from "./pages/HealthzPage/HealthzPage"
import { LoginPage } from "./pages/LoginPage/LoginPage"
import TemplatesPage from "./pages/TemplatesPage/TemplatesPage"
import { TemplatesPage } from "./pages/TemplatesPage/TemplatesPage"
import { AccountPage } from "./pages/UserSettingsPage/AccountPage/AccountPage"
import { SecurityPage } from "./pages/UserSettingsPage/SecurityPage/SecurityPage"
import { SSHKeysPage } from "./pages/UserSettingsPage/SSHKeysPage/SSHKeysPage"
+2
View File
@@ -1,6 +1,7 @@
import auditLog from "./auditLog.json"
import common from "./common.json"
import templatePage from "./templatePage.json"
import templatesPage from "./templatesPage.json"
import workspacePage from "./workspacePage.json"
export const en = {
@@ -8,4 +9,5 @@ export const en = {
workspacePage,
auditLog,
templatePage,
templatesPage,
}
+6
View File
@@ -0,0 +1,6 @@
{
"errors": {
"getOrganizationError": "Something went wrong fetching organizations.",
"getTemplatesError": "Something went wrong fetching templates."
}
}
@@ -4,7 +4,7 @@ import * as CreateDayString from "util/createDayString"
import { MockTemplate } from "../../testHelpers/entities"
import { history, render } from "../../testHelpers/renderHelpers"
import { server } from "../../testHelpers/server"
import TemplatesPage from "./TemplatesPage"
import { TemplatesPage } from "./TemplatesPage"
import { Language } from "./TemplatesPageView"
describe("TemplatesPage", () => {
@@ -6,10 +6,11 @@ import { XServiceContext } from "../../xServices/StateContext"
import { templatesMachine } from "../../xServices/templates/templatesXService"
import { TemplatesPageView } from "./TemplatesPageView"
const TemplatesPage: React.FC = () => {
export const TemplatesPage: React.FC = () => {
const xServices = useContext(XServiceContext)
const [authState] = useActor(xServices.authXService)
const [templatesState] = useMachine(templatesMachine)
const { templates, getOrganizationsError, getTemplatesError } = templatesState.context
return (
<>
@@ -17,12 +18,12 @@ const TemplatesPage: React.FC = () => {
<title>{pageTitle("Templates")}</title>
</Helmet>
<TemplatesPageView
templates={templatesState.context.templates}
templates={templates}
canCreateTemplate={authState.context.permissions?.createTemplates}
loading={templatesState.hasTag("loading")}
getOrganizationsError={getOrganizationsError}
getTemplatesError={getTemplatesError}
/>
</>
)
}
export default TemplatesPage
@@ -1,5 +1,5 @@
import { ComponentMeta, Story } from "@storybook/react"
import { MockTemplate } from "../../testHelpers/entities"
import { makeMockApiError, MockTemplate } from "../../testHelpers/entities"
import { TemplatesPageView, TemplatesPageViewProps } from "./TemplatesPageView"
export default {
@@ -49,3 +49,8 @@ EmptyCanCreate.args = {
export const EmptyCannotCreate = Template.bind({})
EmptyCannotCreate.args = {}
export const Error = Template.bind({})
Error.args = {
getTemplatesError: makeMockApiError({ message: "Something went wrong fetching templates." }),
}
@@ -8,7 +8,9 @@ import TableHead from "@material-ui/core/TableHead"
import TableRow from "@material-ui/core/TableRow"
import KeyboardArrowRight from "@material-ui/icons/KeyboardArrowRight"
import useTheme from "@material-ui/styles/useTheme"
import { ErrorSummary } from "components/ErrorSummary/ErrorSummary"
import { FC } from "react"
import { useTranslation } from "react-i18next"
import { useNavigate } from "react-router-dom"
import { createDayString } from "util/createDayString"
import { formatTemplateActiveDevelopers } from "util/templates"
@@ -77,12 +79,20 @@ export interface TemplatesPageViewProps {
loading?: boolean
canCreateTemplate?: boolean
templates?: TypesGen.Template[]
getOrganizationsError?: Error | unknown
getTemplatesError?: Error | unknown
}
export const TemplatesPageView: FC<React.PropsWithChildren<TemplatesPageViewProps>> = (props) => {
const styles = useStyles()
const navigate = useNavigate()
const { t } = useTranslation("templatesPage")
const theme: Theme = useTheme()
const empty =
!props.loading &&
!props.getOrganizationsError &&
!props.getTemplatesError &&
!props.templates?.length
return (
<Margins>
@@ -114,94 +124,110 @@ export const TemplatesPageView: FC<React.PropsWithChildren<TemplatesPageViewProp
)}
</PageHeader>
<TableContainer>
<Table>
<TableHead>
<TableRow>
<TableCell width="50%">{Language.nameLabel}</TableCell>
<TableCell width="16%">{Language.usedByLabel}</TableCell>
<TableCell width="16%">{Language.lastUpdatedLabel}</TableCell>
<TableCell width="16%">{Language.createdByLabel}</TableCell>
<TableCell width="1%"></TableCell>
</TableRow>
</TableHead>
<TableBody>
{props.loading && <TableLoader />}
{!props.loading && !props.templates?.length && (
{props.getOrganizationsError ? (
<ErrorSummary
error={props.getOrganizationsError}
defaultMessage={t("errors.getOrganizationsError")}
/>
) : props.getTemplatesError ? (
<ErrorSummary
error={props.getTemplatesError}
defaultMessage={t("errors.getTemplatesError")}
/>
) : (
<TableContainer>
<Table>
<TableHead>
<TableRow>
<TableCell colSpan={999}>
<EmptyState
message={Language.emptyMessage}
description={
props.canCreateTemplate
? Language.emptyDescription
: Language.emptyViewNoPerms
}
descriptionClassName={styles.emptyDescription}
cta={<CodeExample code="coder templates init" />}
/>
</TableCell>
<TableCell width="50%">{Language.nameLabel}</TableCell>
<TableCell width="16%">{Language.usedByLabel}</TableCell>
<TableCell width="16%">{Language.lastUpdatedLabel}</TableCell>
<TableCell width="16%">{Language.createdByLabel}</TableCell>
<TableCell width="1%"></TableCell>
</TableRow>
)}
{props.templates?.map((template) => {
const templatePageLink = `/templates/${template.name}`
const hasIcon = template.icon && template.icon !== ""
</TableHead>
<TableBody>
{props.loading && <TableLoader />}
return (
<TableRow
key={template.id}
hover
data-testid={`template-${template.id}`}
tabIndex={0}
onKeyDown={(event) => {
if (event.key === "Enter") {
navigate(templatePageLink)
}
}}
className={styles.clickableTableRow}
>
<TableCellLink to={templatePageLink}>
<AvatarData
title={template.name}
subtitle={template.description}
highlightTitle
avatar={
hasIcon ? (
<div className={styles.templateIconWrapper}>
<img alt="" src={template.icon} />
</div>
) : undefined
{empty ? (
<TableRow>
<TableCell colSpan={999}>
<EmptyState
message={Language.emptyMessage}
description={
props.canCreateTemplate
? Language.emptyDescription
: Language.emptyViewNoPerms
}
descriptionClassName={styles.emptyDescription}
cta={<CodeExample code="coder templates init" />}
/>
</TableCellLink>
<TableCellLink to={templatePageLink}>
<span style={{ color: theme.palette.text.secondary }}>
{Language.developerCount(template.active_user_count)}
</span>
</TableCellLink>
<TableCellLink data-chromatic="ignore" to={templatePageLink}>
<span style={{ color: theme.palette.text.secondary }}>
{createDayString(template.updated_at)}
</span>
</TableCellLink>
<TableCellLink to={templatePageLink}>
<span style={{ color: theme.palette.text.secondary }}>
{template.created_by_name}
</span>
</TableCellLink>
<TableCellLink to={templatePageLink}>
<div className={styles.arrowCell}>
<KeyboardArrowRight className={styles.arrowRight} />
</div>
</TableCellLink>
</TableCell>
</TableRow>
)
})}
</TableBody>
</Table>
</TableContainer>
) : (
props.templates?.map((template) => {
const templatePageLink = `/templates/${template.name}`
const hasIcon = template.icon && template.icon !== ""
return (
<TableRow
key={template.id}
hover
data-testid={`template-${template.id}`}
tabIndex={0}
onKeyDown={(event) => {
if (event.key === "Enter") {
navigate(templatePageLink)
}
}}
className={styles.clickableTableRow}
>
<TableCellLink to={templatePageLink}>
<AvatarData
title={template.name}
subtitle={template.description}
highlightTitle
avatar={
hasIcon && (
<div className={styles.templateIconWrapper}>
<img alt="" src={template.icon} />
</div>
)
}
/>
</TableCellLink>
<TableCellLink to={templatePageLink}>
<span style={{ color: theme.palette.text.secondary }}>
{Language.developerCount(template.active_user_count)}
</span>
</TableCellLink>
<TableCellLink data-chromatic="ignore" to={templatePageLink}>
<span style={{ color: theme.palette.text.secondary }}>
{createDayString(template.updated_at)}
</span>
</TableCellLink>
<TableCellLink to={templatePageLink}>
<span style={{ color: theme.palette.text.secondary }}>
{template.created_by_name}
</span>
</TableCellLink>
<TableCellLink to={templatePageLink}>
<div className={styles.arrowCell}>
<KeyboardArrowRight className={styles.arrowRight} />
</div>
</TableCellLink>
</TableRow>
)
})
)}
</TableBody>
</Table>
</TableContainer>
)}
</Margins>
)
}
@@ -6,99 +6,101 @@ interface TemplatesContext {
organizations?: TypesGen.Organization[]
templates?: TypesGen.Template[]
canCreateTemplate?: boolean
permissionsError?: Error | unknown
organizationsError?: Error | unknown
templatesError?: Error | unknown
getOrganizationsError?: Error | unknown
getTemplatesError?: Error | unknown
}
export const templatesMachine = createMachine(
{
tsTypes: {} as import("./templatesXService.typegen").Typegen0,
schema: {
context: {} as TemplatesContext,
services: {} as {
getOrganizations: {
data: TypesGen.Organization[]
}
getPermissions: {
data: boolean
}
getTemplates: {
data: TypesGen.Template[]
}
},
},
id: "templatesState",
initial: "gettingOrganizations",
states: {
gettingOrganizations: {
entry: "clearOrganizationsError",
invoke: {
src: "getOrganizations",
id: "getOrganizations",
onDone: [
{
actions: ["assignOrganizations", "clearOrganizationsError"],
target: "gettingTemplates",
},
],
onError: [
{
actions: "assignOrganizationsError",
target: "error",
},
],
export const templatesMachine =
/** @xstate-layout N4IgpgJg5mDOIC5QBcwFsAOAbAhq2AysnmAHQzLICWAdlAPIBOUONVAXnlQPY2wDEEXmVoA3bgGsyFJizadqveEhAZusKopqJQAD0QAWAMwHSATgDsANgCsAJgt2rRuwAYAHBYsAaEAE9EAEZAu1IDMwiIm3dXG0DjdwBfRN9UTFx8IhJyMEpaBmZWDi4lfjBGRm5GUmw8ADMqtBzkWSKFHj4dVXVNDq79BGNTS1sHJxcPL18AhBMjUhtXJaWrOzM7O3cDG2TU9FrM4lRm6joAFX2MuEFhUjFJaVyL9JJlUDUNLX6gpeH1wJcIQs7giVmmiBB5kilgM7iMHmcOxSIDSBzgWWOFFOUGeaIE5Uq1QODUYTQouKub26nz6KgGgV+ULsAOZDhBZjB-kQbhspGWSxC0W2VncSORNG4EDgXVRlIxjzydFa8hKnRUH16vG+gzs4IQwTMYWhjgsRjm9l2KMur3lJ3yFNeXQ1XzpiCsVlcpFW4QMFnisJZeo5UMicQsNjMgRsSL2L0O2SENDATp6Lr0QTcFjCa2cfqsgRNeuC7j5-Ncq3WmwMgUtsptRzIBKqKZpWtd+sz2Y5RjzBYceo2WbNw9hrijZtr1vjqBbmu07cC7iLSWSiSAA */
createMachine(
{
tsTypes: {} as import("./templatesXService.typegen").Typegen0,
schema: {
context: {} as TemplatesContext,
services: {} as {
getOrganizations: {
data: TypesGen.Organization[]
}
getTemplates: {
data: TypesGen.Template[]
}
},
tags: "loading",
},
gettingTemplates: {
entry: "clearTemplatesError",
invoke: {
src: "getTemplates",
id: "getTemplates",
onDone: {
target: "done",
actions: ["assignTemplates", "clearTemplatesError"],
},
onError: {
target: "error",
actions: "assignTemplatesError",
id: "templatesState",
initial: "gettingOrganizations",
states: {
gettingOrganizations: {
entry: "clearGetOrganizationsError",
invoke: {
src: "getOrganizations",
id: "getOrganizations",
onDone: [
{
actions: ["assignOrganizations"],
target: "gettingTemplates",
},
],
onError: [
{
actions: "assignGetOrganizationsError",
target: "error",
},
],
},
tags: "loading",
},
tags: "loading",
},
done: {},
error: {},
},
},
{
actions: {
assignOrganizations: assign({
organizations: (_, event) => event.data,
}),
assignOrganizationsError: assign({
organizationsError: (_, event) => event.data,
}),
clearOrganizationsError: assign((context) => ({
...context,
organizationsError: undefined,
})),
assignTemplates: assign({
templates: (_, event) => event.data,
}),
assignTemplatesError: assign({
templatesError: (_, event) => event.data,
}),
clearTemplatesError: (context) => assign({ ...context, getWorkspacesError: undefined }),
},
services: {
getOrganizations: API.getOrganizations,
getTemplates: async (context) => {
if (!context.organizations || context.organizations.length === 0) {
throw new Error("no organizations")
}
return API.getTemplates(context.organizations[0].id)
gettingTemplates: {
entry: "clearGetTemplatesError",
invoke: {
src: "getTemplates",
id: "getTemplates",
onDone: [
{
actions: ["assignTemplates"],
target: "done",
},
],
onError: [
{
actions: "assignGetTemplatesError",
target: "error",
},
],
},
tags: "loading",
},
done: {},
error: {},
},
},
},
)
{
actions: {
assignOrganizations: assign({
organizations: (_, event) => event.data,
}),
assignGetOrganizationsError: assign({
getOrganizationsError: (_, event) => event.data,
}),
clearGetOrganizationsError: assign((context) => ({
...context,
getOrganizationsError: undefined,
})),
assignTemplates: assign({
templates: (_, event) => event.data,
}),
assignGetTemplatesError: assign({
getTemplatesError: (_, event) => event.data,
}),
clearGetTemplatesError: (context) => assign({ ...context, getTemplatesError: undefined }),
},
services: {
getOrganizations: API.getOrganizations,
getTemplates: async (context) => {
if (!context.organizations || context.organizations.length === 0) {
throw new Error("no organizations")
}
return API.getTemplates(context.organizations[0].id)
},
},
},
)