diff --git a/site/src/AppRouter.tsx b/site/src/AppRouter.tsx
index 55c4a975371db..045f25aa3a9c3 100644
--- a/site/src/AppRouter.tsx
+++ b/site/src/AppRouter.tsx
@@ -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"
diff --git a/site/src/i18n/en/index.ts b/site/src/i18n/en/index.ts
index d6900f9463100..32fe288646865 100644
--- a/site/src/i18n/en/index.ts
+++ b/site/src/i18n/en/index.ts
@@ -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,
}
diff --git a/site/src/i18n/en/templatesPage.json b/site/src/i18n/en/templatesPage.json
new file mode 100644
index 0000000000000..34d56b2788f07
--- /dev/null
+++ b/site/src/i18n/en/templatesPage.json
@@ -0,0 +1,6 @@
+{
+ "errors": {
+ "getOrganizationError": "Something went wrong fetching organizations.",
+ "getTemplatesError": "Something went wrong fetching templates."
+ }
+}
diff --git a/site/src/pages/TemplatesPage/TemplatesPage.test.tsx b/site/src/pages/TemplatesPage/TemplatesPage.test.tsx
index 608f9d3a95790..d5b2ec4477c7d 100644
--- a/site/src/pages/TemplatesPage/TemplatesPage.test.tsx
+++ b/site/src/pages/TemplatesPage/TemplatesPage.test.tsx
@@ -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", () => {
diff --git a/site/src/pages/TemplatesPage/TemplatesPage.tsx b/site/src/pages/TemplatesPage/TemplatesPage.tsx
index 9d05f8aa95b46..bdab3edeae3b1 100644
--- a/site/src/pages/TemplatesPage/TemplatesPage.tsx
+++ b/site/src/pages/TemplatesPage/TemplatesPage.tsx
@@ -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 = () => {
{pageTitle("Templates")}
>
)
}
-
-export default TemplatesPage
diff --git a/site/src/pages/TemplatesPage/TemplatesPageView.stories.tsx b/site/src/pages/TemplatesPage/TemplatesPageView.stories.tsx
index f2a22f28d099d..d191a8098254c 100644
--- a/site/src/pages/TemplatesPage/TemplatesPageView.stories.tsx
+++ b/site/src/pages/TemplatesPage/TemplatesPageView.stories.tsx
@@ -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." }),
+}
diff --git a/site/src/pages/TemplatesPage/TemplatesPageView.tsx b/site/src/pages/TemplatesPage/TemplatesPageView.tsx
index b40dab7b21d2d..76f5a89d5001c 100644
--- a/site/src/pages/TemplatesPage/TemplatesPageView.tsx
+++ b/site/src/pages/TemplatesPage/TemplatesPageView.tsx
@@ -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> = (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 (
@@ -114,94 +124,110 @@ export const TemplatesPageView: FC
-
-
-
-
- {Language.nameLabel}
- {Language.usedByLabel}
- {Language.lastUpdatedLabel}
- {Language.createdByLabel}
-
-
-
-
- {props.loading && }
- {!props.loading && !props.templates?.length && (
+ {props.getOrganizationsError ? (
+
+ ) : props.getTemplatesError ? (
+
+ ) : (
+
+
+
-
- }
- />
-
+ {Language.nameLabel}
+ {Language.usedByLabel}
+ {Language.lastUpdatedLabel}
+ {Language.createdByLabel}
+
- )}
- {props.templates?.map((template) => {
- const templatePageLink = `/templates/${template.name}`
- const hasIcon = template.icon && template.icon !== ""
-
- return (
- {
- if (event.key === "Enter") {
- navigate(templatePageLink)
- }
- }}
- className={styles.clickableTableRow}
- >
-
-
-
-
- ) : undefined
+
+
+ {props.loading && }
+
+ {empty ? (
+
+
+ }
/>
-
-
-
-
- {Language.developerCount(template.active_user_count)}
-
-
-
-
-
- {createDayString(template.updated_at)}
-
-
-
-
- {template.created_by_name}
-
-
-
-
-
-
-
+
- )
- })}
-
-
-
+ ) : (
+ props.templates?.map((template) => {
+ const templatePageLink = `/templates/${template.name}`
+ const hasIcon = template.icon && template.icon !== ""
+
+ return (
+ {
+ if (event.key === "Enter") {
+ navigate(templatePageLink)
+ }
+ }}
+ className={styles.clickableTableRow}
+ >
+
+
+
+
+ )
+ }
+ />
+
+
+
+
+ {Language.developerCount(template.active_user_count)}
+
+
+
+
+
+ {createDayString(template.updated_at)}
+
+
+
+
+
+ {template.created_by_name}
+
+
+
+
+
+
+
+
+
+ )
+ })
+ )}
+
+
+
+ )}
)
}
diff --git a/site/src/xServices/templates/templatesXService.ts b/site/src/xServices/templates/templatesXService.ts
index 68e7a847e9fb7..139d0e8e454c5 100644
--- a/site/src/xServices/templates/templatesXService.ts
+++ b/site/src/xServices/templates/templatesXService.ts
@@ -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"],
+ id: "templatesState",
+ initial: "gettingOrganizations",
+ states: {
+ gettingOrganizations: {
+ entry: "clearGetOrganizationsError",
+ invoke: {
+ src: "getOrganizations",
+ id: "getOrganizations",
+ onDone: [
+ {
+ actions: ["assignOrganizations"],
+ target: "gettingTemplates",
+ },
+ ],
+ onError: [
+ {
+ actions: "assignGetOrganizationsError",
+ target: "error",
+ },
+ ],
},
- onError: {
- target: "error",
- actions: "assignTemplatesError",
+ tags: "loading",
+ },
+ gettingTemplates: {
+ entry: "clearGetTemplatesError",
+ invoke: {
+ src: "getTemplates",
+ id: "getTemplates",
+ onDone: [
+ {
+ actions: ["assignTemplates"],
+ target: "done",
+ },
+ ],
+ onError: [
+ {
+ actions: "assignGetTemplatesError",
+ target: "error",
+ },
+ ],
},
+ tags: "loading",
},
- tags: "loading",
+ done: {},
+ error: {},
},
- 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)
+ {
+ 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)
+ },
},
},
- },
-)
+ )