Skip to content

Commit 36cb15e

Browse files
committed
Surface templates page errors
1 parent c026464 commit 36cb15e

File tree

4 files changed

+159
-147
lines changed

4 files changed

+159
-147
lines changed

site/src/pages/TemplatesPage/TemplatesPage.tsx

+4-4
Original file line numberDiff line numberDiff line change
@@ -6,23 +6,23 @@ import { XServiceContext } from "../../xServices/StateContext"
66
import { templatesMachine } from "../../xServices/templates/templatesXService"
77
import { TemplatesPageView } from "./TemplatesPageView"
88

9-
const TemplatesPage: React.FC = () => {
9+
export const TemplatesPage: React.FC = () => {
1010
const xServices = useContext(XServiceContext)
1111
const [authState] = useActor(xServices.authXService)
1212
const [templatesState] = useMachine(templatesMachine)
13+
const { templates, getOrganizationsError, getTemplatesError } = templatesState.context
1314

1415
return (
1516
<>
1617
<Helmet>
1718
<title>{pageTitle("Templates")}</title>
1819
</Helmet>
1920
<TemplatesPageView
20-
templates={templatesState.context.templates}
21+
templates={templates}
2122
canCreateTemplate={authState.context.permissions?.createTemplates}
2223
loading={templatesState.hasTag("loading")}
24+
error={getOrganizationsError ?? getTemplatesError}
2325
/>
2426
</>
2527
)
2628
}
27-
28-
export default TemplatesPage

site/src/pages/TemplatesPage/TemplatesPageView.stories.tsx

+4-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { ComponentMeta, Story } from "@storybook/react"
2-
import { MockTemplate } from "../../testHelpers/entities"
2+
import { makeMockApiError, MockTemplate } from "../../testHelpers/entities"
33
import { TemplatesPageView, TemplatesPageViewProps } from "./TemplatesPageView"
44

55
export default {
@@ -48,3 +48,6 @@ EmptyCanCreate.args = {
4848

4949
export const EmptyCannotCreate = Template.bind({})
5050
EmptyCannotCreate.args = {}
51+
52+
export const Error = Template.bind({})
53+
Error.args = { error: makeMockApiError({ message: "Something went wrong fetching templates." }) }

site/src/pages/TemplatesPage/TemplatesPageView.tsx

+89-82
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import TableHead from "@material-ui/core/TableHead"
88
import TableRow from "@material-ui/core/TableRow"
99
import KeyboardArrowRight from "@material-ui/icons/KeyboardArrowRight"
1010
import useTheme from "@material-ui/styles/useTheme"
11+
import { ErrorSummary } from "components/ErrorSummary/ErrorSummary"
1112
import { FC } from "react"
1213
import { useNavigate } from "react-router-dom"
1314
import { createDayString } from "util/createDayString"
@@ -76,12 +77,14 @@ export interface TemplatesPageViewProps {
7677
loading?: boolean
7778
canCreateTemplate?: boolean
7879
templates?: TypesGen.Template[]
80+
error?: Error | unknown
7981
}
8082

8183
export const TemplatesPageView: FC<React.PropsWithChildren<TemplatesPageViewProps>> = (props) => {
8284
const styles = useStyles()
8385
const navigate = useNavigate()
8486
const theme: Theme = useTheme()
87+
const empty = !props.loading && !props.error && !props.templates?.length
8588

8689
return (
8790
<Margins>
@@ -113,94 +116,98 @@ export const TemplatesPageView: FC<React.PropsWithChildren<TemplatesPageViewProp
113116
)}
114117
</PageHeader>
115118

116-
<TableContainer>
117-
<Table>
118-
<TableHead>
119-
<TableRow>
120-
<TableCell width="50%">{Language.nameLabel}</TableCell>
121-
<TableCell width="16%">{Language.usedByLabel}</TableCell>
122-
<TableCell width="16%">{Language.lastUpdatedLabel}</TableCell>
123-
<TableCell width="16%">{Language.createdByLabel}</TableCell>
124-
<TableCell width="1%"></TableCell>
125-
</TableRow>
126-
</TableHead>
127-
<TableBody>
128-
{props.loading && <TableLoader />}
129-
{!props.loading && !props.templates?.length && (
119+
{props.error ? (
120+
<ErrorSummary error={props.error} />
121+
) : (
122+
<TableContainer>
123+
<Table>
124+
<TableHead>
130125
<TableRow>
131-
<TableCell colSpan={999}>
132-
<EmptyState
133-
message={Language.emptyMessage}
134-
description={
135-
props.canCreateTemplate
136-
? Language.emptyDescription
137-
: Language.emptyViewNoPerms
138-
}
139-
descriptionClassName={styles.emptyDescription}
140-
cta={<CodeExample code="coder templates init" />}
141-
/>
142-
</TableCell>
126+
<TableCell width="50%">{Language.nameLabel}</TableCell>
127+
<TableCell width="16%">{Language.usedByLabel}</TableCell>
128+
<TableCell width="16%">{Language.lastUpdatedLabel}</TableCell>
129+
<TableCell width="16%">{Language.createdByLabel}</TableCell>
130+
<TableCell width="1%"></TableCell>
143131
</TableRow>
144-
)}
145-
{props.templates?.map((template) => {
146-
const templatePageLink = `/templates/${template.name}`
147-
const hasIcon = template.icon && template.icon !== ""
148-
149-
return (
150-
<TableRow
151-
key={template.id}
152-
hover
153-
data-testid={`template-${template.id}`}
154-
tabIndex={0}
155-
onKeyDown={(event) => {
156-
if (event.key === "Enter") {
157-
navigate(templatePageLink)
158-
}
159-
}}
160-
className={styles.clickableTableRow}
161-
>
162-
<TableCellLink to={templatePageLink}>
163-
<AvatarData
164-
title={template.name}
165-
subtitle={template.description}
166-
highlightTitle
167-
avatar={
168-
hasIcon ? (
169-
<div className={styles.templateIconWrapper}>
170-
<img alt="" src={template.icon} />
171-
</div>
172-
) : undefined
132+
</TableHead>
133+
<TableBody>
134+
{props.loading && <TableLoader />}
135+
{empty && (
136+
<TableRow>
137+
<TableCell colSpan={999}>
138+
<EmptyState
139+
message={Language.emptyMessage}
140+
description={
141+
props.canCreateTemplate
142+
? Language.emptyDescription
143+
: Language.emptyViewNoPerms
173144
}
145+
descriptionClassName={styles.emptyDescription}
146+
cta={<CodeExample code="coder templates init" />}
174147
/>
175-
</TableCellLink>
148+
</TableCell>
149+
</TableRow>
150+
)}
151+
{props.templates?.map((template) => {
152+
const templatePageLink = `/templates/${template.name}`
153+
const hasIcon = template.icon && template.icon !== ""
176154

177-
<TableCellLink to={templatePageLink}>
178-
<span style={{ color: theme.palette.text.secondary }}>
179-
{Language.developerCount(template.workspace_owner_count)}
180-
</span>
181-
</TableCellLink>
155+
return (
156+
<TableRow
157+
key={template.id}
158+
hover
159+
data-testid={`template-${template.id}`}
160+
tabIndex={0}
161+
onKeyDown={(event) => {
162+
if (event.key === "Enter") {
163+
navigate(templatePageLink)
164+
}
165+
}}
166+
className={styles.clickableTableRow}
167+
>
168+
<TableCellLink to={templatePageLink}>
169+
<AvatarData
170+
title={template.name}
171+
subtitle={template.description}
172+
highlightTitle
173+
avatar={
174+
hasIcon ? (
175+
<div className={styles.templateIconWrapper}>
176+
<img alt="" src={template.icon} />
177+
</div>
178+
) : undefined
179+
}
180+
/>
181+
</TableCellLink>
182182

183-
<TableCellLink data-chromatic="ignore" to={templatePageLink}>
184-
<span style={{ color: theme.palette.text.secondary }}>
185-
{createDayString(template.updated_at)}
186-
</span>
187-
</TableCellLink>
188-
<TableCellLink to={templatePageLink}>
189-
<span style={{ color: theme.palette.text.secondary }}>
190-
{template.created_by_name}
191-
</span>
192-
</TableCellLink>
193-
<TableCellLink to={templatePageLink}>
194-
<div className={styles.arrowCell}>
195-
<KeyboardArrowRight className={styles.arrowRight} />
196-
</div>
197-
</TableCellLink>
198-
</TableRow>
199-
)
200-
})}
201-
</TableBody>
202-
</Table>
203-
</TableContainer>
183+
<TableCellLink to={templatePageLink}>
184+
<span style={{ color: theme.palette.text.secondary }}>
185+
{Language.developerCount(template.workspace_owner_count)}
186+
</span>
187+
</TableCellLink>
188+
189+
<TableCellLink data-chromatic="ignore" to={templatePageLink}>
190+
<span style={{ color: theme.palette.text.secondary }}>
191+
{createDayString(template.updated_at)}
192+
</span>
193+
</TableCellLink>
194+
<TableCellLink to={templatePageLink}>
195+
<span style={{ color: theme.palette.text.secondary }}>
196+
{template.created_by_name}
197+
</span>
198+
</TableCellLink>
199+
<TableCellLink to={templatePageLink}>
200+
<div className={styles.arrowCell}>
201+
<KeyboardArrowRight className={styles.arrowRight} />
202+
</div>
203+
</TableCellLink>
204+
</TableRow>
205+
)
206+
})}
207+
</TableBody>
208+
</Table>
209+
</TableContainer>
210+
)}
204211
</Margins>
205212
)
206213
}

site/src/xServices/templates/templatesXService.ts

+62-60
Original file line numberDiff line numberDiff line change
@@ -6,90 +6,92 @@ interface TemplatesContext {
66
organizations?: TypesGen.Organization[]
77
templates?: TypesGen.Template[]
88
canCreateTemplate?: boolean
9-
permissionsError?: Error | unknown
10-
organizationsError?: Error | unknown
11-
templatesError?: Error | unknown
9+
getOrganizationsError?: Error | unknown
10+
getTemplatesError?: Error | unknown
1211
}
1312

14-
export const templatesMachine = createMachine(
13+
export const templatesMachine =
14+
/** @xstate-layout N4IgpgJg5mDOIC5QBcwFsAOAbAhq2AysnmAHQzLICWAdlAPIBOUONVAXnlQPY2wDEEXmVoA3bgGsyFJizadqveEhAZusKopqJQAD0QAWAMwHSATgDsANgCsAJgt2rRuwAYAHBYsAaEAE9EAEZAu1IDMwiIm3dXG0DjdwBfRN9UTFx8IhJyMEpaBmZWDi4lfjBGRm5GUmw8ADMqtBzkWSKFHj4dVXVNDq79BGNTS1sHJxcPL18AhBMjUhtXJaWrOzM7O3cDG2TU9FrM4lRm6joAFX2MuEFhUjFJaVyL9JJlUDUNLX6gpeH1wJcIQs7giVmmiBB5kilgM7iMHmcOxSIDSBzgWWOFFOUGeaIE5Uq1QODUYTQouKub26nz6KgGgV+ULsAOZDhBZjB-kQbhspGWSxC0W2VncSORNG4EDgXVRlIxjzydFa8hKnRUH16vG+gzs4IQwTMYWhjgsRjm9l2KMur3lJ3yFNeXQ1XzpiCsVlcpFW4QMFnisJZeo5UMicQsNjMgRsSL2L0O2SENDATp6Lr0QTcFjCa2cfqsgRNeuC7j5-Ncq3WmwMgUtsptRzIBKqKZpWtd+sz2Y5RjzBYceo2WbNw9hrijZtr1vjqBbmu07cC7iLSWSiSAA */
15+
createMachine(
1516
{
16-
tsTypes: {} as import("./templatesXService.typegen").Typegen0,
17-
schema: {
18-
context: {} as TemplatesContext,
19-
services: {} as {
20-
getOrganizations: {
21-
data: TypesGen.Organization[]
22-
}
23-
getPermissions: {
24-
data: boolean
25-
}
26-
getTemplates: {
27-
data: TypesGen.Template[]
28-
}
29-
},
17+
tsTypes: {} as import("./templatesXService.typegen").Typegen0,
18+
schema: {
19+
context: {} as TemplatesContext,
20+
services: {} as {
21+
getOrganizations: {
22+
data: TypesGen.Organization[]
23+
}
24+
getTemplates: {
25+
data: TypesGen.Template[]
26+
}
3027
},
31-
id: "templatesState",
32-
initial: "gettingOrganizations",
33-
states: {
34-
gettingOrganizations: {
35-
entry: "clearOrganizationsError",
36-
invoke: {
37-
src: "getOrganizations",
38-
id: "getOrganizations",
39-
onDone: [
40-
{
41-
actions: ["assignOrganizations", "clearOrganizationsError"],
42-
target: "gettingTemplates",
43-
},
44-
],
45-
onError: [
46-
{
47-
actions: "assignOrganizationsError",
48-
target: "error",
49-
},
50-
],
51-
},
52-
tags: "loading",
28+
},
29+
id: "templatesState",
30+
initial: "gettingOrganizations",
31+
states: {
32+
gettingOrganizations: {
33+
entry: "clearGetOrganizationsError",
34+
invoke: {
35+
src: "getOrganizations",
36+
id: "getOrganizations",
37+
onDone: [
38+
{
39+
actions: ["assignOrganizations"],
40+
target: "gettingTemplates",
41+
},
42+
],
43+
onError: [
44+
{
45+
actions: "assignGetOrganizationsError",
46+
target: "error",
47+
},
48+
],
5349
},
54-
gettingTemplates: {
55-
entry: "clearTemplatesError",
56-
invoke: {
57-
src: "getTemplates",
58-
id: "getTemplates",
59-
onDone: {
50+
tags: "loading",
51+
},
52+
gettingTemplates: {
53+
entry: "clearGetTemplatesError",
54+
invoke: {
55+
src: "getTemplates",
56+
id: "getTemplates",
57+
onDone: [
58+
{
59+
actions: ["assignTemplates"],
6060
target: "done",
61-
actions: ["assignTemplates", "clearTemplatesError"],
6261
},
63-
onError: {
62+
],
63+
onError: [
64+
{
65+
actions: "assignGetTemplatesError",
6466
target: "error",
65-
actions: "assignTemplatesError",
6667
},
67-
},
68-
tags: "loading",
68+
],
6969
},
70-
done: {},
71-
error: {},
70+
tags: "loading",
7271
},
72+
done: {},
73+
error: {},
7374
},
75+
},
7476
{
7577
actions: {
7678
assignOrganizations: assign({
7779
organizations: (_, event) => event.data,
7880
}),
79-
assignOrganizationsError: assign({
80-
organizationsError: (_, event) => event.data,
81+
assignGetOrganizationsError: assign({
82+
getOrganizationsError: (_, event) => event.data,
8183
}),
82-
clearOrganizationsError: assign((context) => ({
84+
clearGetOrganizationsError: assign((context) => ({
8385
...context,
84-
organizationsError: undefined,
86+
getOrganizationsError: undefined,
8587
})),
8688
assignTemplates: assign({
8789
templates: (_, event) => event.data,
8890
}),
89-
assignTemplatesError: assign({
90-
templatesError: (_, event) => event.data,
91+
assignGetTemplatesError: assign({
92+
getTemplatesError: (_, event) => event.data,
9193
}),
92-
clearTemplatesError: (context) => assign({ ...context, getWorkspacesError: undefined }),
94+
clearGetTemplatesError: (context) => assign({ ...context, getTemplatesError: undefined }),
9395
},
9496
services: {
9597
getOrganizations: API.getOrganizations,

0 commit comments

Comments
 (0)