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) + }, }, }, - }, -) + )