From 5ea1d233c84e4881bee40a9ea392453584289eaf Mon Sep 17 00:00:00 2001 From: Bruno Quaresma Date: Wed, 22 Mar 2023 15:15:57 +0000 Subject: [PATCH 01/17] Add template settings --- site/src/AppRouter.tsx | 18 +- .../src/components/SettingsLayout/Sidebar.tsx | 1 + .../TemplateFilesPage/TemplateFilesPage.tsx | 0 .../pages/TemplateSettingsPage/Sidebar.tsx | 147 ++++++++++++++++ .../TemplateSettingsForm.tsx | 0 .../TemplateSettingsPage.test.tsx | 0 .../TemplateSettingsPage.tsx | 47 +++-- .../TemplateSettingsPageView.stories.tsx | 0 .../TemplateSettingsPageView.tsx | 53 ++++++ .../TemplatePermissionsPage.tsx | 0 .../TemplatePermissionsPageView.stories.tsx | 0 .../TemplatePermissionsPageView.tsx | 0 .../TemplateSettingsLayout.tsx | 81 +++++++++ .../TemplateSettingsPageView.tsx | 66 ------- site/src/theme/overrides.ts | 10 ++ .../templateSettingsXService.ts | 166 ------------------ 16 files changed, 325 insertions(+), 264 deletions(-) rename site/src/pages/{ => TemplatePage}/TemplateFilesPage/TemplateFilesPage.tsx (100%) create mode 100644 site/src/pages/TemplateSettingsPage/Sidebar.tsx rename site/src/pages/TemplateSettingsPage/{ => TemplateGeneralSettingsPage}/TemplateSettingsForm.tsx (100%) rename site/src/pages/TemplateSettingsPage/{ => TemplateGeneralSettingsPage}/TemplateSettingsPage.test.tsx (100%) rename site/src/pages/TemplateSettingsPage/{ => TemplateGeneralSettingsPage}/TemplateSettingsPage.tsx (52%) rename site/src/pages/TemplateSettingsPage/{ => TemplateGeneralSettingsPage}/TemplateSettingsPageView.stories.tsx (100%) create mode 100644 site/src/pages/TemplateSettingsPage/TemplateGeneralSettingsPage/TemplateSettingsPageView.tsx rename site/src/pages/{TemplatePage => TemplateSettingsPage}/TemplatePermissionsPage/TemplatePermissionsPage.tsx (100%) rename site/src/pages/{TemplatePage => TemplateSettingsPage}/TemplatePermissionsPage/TemplatePermissionsPageView.stories.tsx (100%) rename site/src/pages/{TemplatePage => TemplateSettingsPage}/TemplatePermissionsPage/TemplatePermissionsPageView.tsx (100%) create mode 100644 site/src/pages/TemplateSettingsPage/TemplateSettingsLayout.tsx delete mode 100644 site/src/pages/TemplateSettingsPage/TemplateSettingsPageView.tsx delete mode 100644 site/src/xServices/templateSettings/templateSettingsXService.ts diff --git a/site/src/AppRouter.tsx b/site/src/AppRouter.tsx index 6bce538c20373..9681faf7ca10b 100644 --- a/site/src/AppRouter.tsx +++ b/site/src/AppRouter.tsx @@ -6,7 +6,7 @@ import AuditPage from "pages/AuditPage/AuditPage" import GroupsPage from "pages/GroupsPage/GroupsPage" import LoginPage from "pages/LoginPage/LoginPage" import { SetupPage } from "pages/SetupPage/SetupPage" -import { TemplateSettingsPage } from "pages/TemplateSettingsPage/TemplateSettingsPage" +import { TemplateSettingsPage } from "pages/TemplateSettingsPage/TemplateGeneralSettingsPage/TemplateSettingsPage" import TemplatesPage from "pages/TemplatesPage/TemplatesPage" import UsersPage from "pages/UsersPage/UsersPage" import WorkspacesPage from "pages/WorkspacesPage/WorkspacesPage" @@ -16,6 +16,7 @@ import { DashboardLayout } from "./components/Dashboard/DashboardLayout" import { RequireAuth } from "./components/RequireAuth/RequireAuth" import { SettingsLayout } from "./components/SettingsLayout/SettingsLayout" import { DeploySettingsLayout } from "components/DeploySettingsLayout/DeploySettingsLayout" +import { TemplateSettingsLayout } from "pages/TemplateSettingsPage/TemplateSettingsLayout" // Lazy load pages // - Pages that are secondary, not in the main navigation or not usually accessed @@ -50,7 +51,7 @@ const TerminalPage = lazy(() => import("./pages/TerminalPage/TerminalPage")) const TemplatePermissionsPage = lazy( () => import( - "./pages/TemplatePage/TemplatePermissionsPage/TemplatePermissionsPage" + "./pages/TemplateSettingsPage/TemplatePermissionsPage/TemplatePermissionsPage" ), ) const TemplateSummaryPage = lazy( @@ -129,7 +130,7 @@ const CreateTokenPage = lazy( () => import("./pages/CreateTokenPage/CreateTokenPage"), ) const TemplateFilesPage = lazy( - () => import("./pages/TemplateFilesPage/TemplateFilesPage"), + () => import("./pages/TemplatePage/TemplateFilesPage/TemplateFilesPage"), ) export const AppRouter: FC = () => { @@ -160,15 +161,20 @@ export const AppRouter: FC = () => { }> } /> + + } /> + + + } /> + + }> + } /> } /> - } /> - } /> - } /> } /> diff --git a/site/src/components/SettingsLayout/Sidebar.tsx b/site/src/components/SettingsLayout/Sidebar.tsx index 99e9f471983ae..4113067ade30c 100644 --- a/site/src/components/SettingsLayout/Sidebar.tsx +++ b/site/src/components/SettingsLayout/Sidebar.tsx @@ -130,6 +130,7 @@ const useStyles = makeStyles((theme) => ({ fontWeight: 600, overflow: "hidden", textOverflow: "ellipsis", + whiteSpace: "nowrap", }, email: { color: theme.palette.text.secondary, diff --git a/site/src/pages/TemplateFilesPage/TemplateFilesPage.tsx b/site/src/pages/TemplatePage/TemplateFilesPage/TemplateFilesPage.tsx similarity index 100% rename from site/src/pages/TemplateFilesPage/TemplateFilesPage.tsx rename to site/src/pages/TemplatePage/TemplateFilesPage/TemplateFilesPage.tsx diff --git a/site/src/pages/TemplateSettingsPage/Sidebar.tsx b/site/src/pages/TemplateSettingsPage/Sidebar.tsx new file mode 100644 index 0000000000000..f276e0dc1d54d --- /dev/null +++ b/site/src/pages/TemplateSettingsPage/Sidebar.tsx @@ -0,0 +1,147 @@ +import { makeStyles } from "@material-ui/core/styles" +import ScheduleIcon from "@material-ui/icons/TimerOutlined" +import VariablesIcon from "@material-ui/icons/CodeOutlined" +import { Template } from "api/typesGenerated" +import { Stack } from "components/Stack/Stack" +import { FC, ElementType, PropsWithChildren, ReactNode } from "react" +import { NavLink } from "react-router-dom" +import { combineClasses } from "util/combineClasses" +import GeneralIcon from "@material-ui/icons/SettingsOutlined" +import SecurityIcon from "@material-ui/icons/LockOutlined" +import { Avatar } from "components/Avatar/Avatar" + +const SidebarNavItem: FC< + PropsWithChildren<{ href: string; icon: ReactNode }> +> = ({ children, href, icon }) => { + const styles = useStyles() + return ( + + combineClasses([ + styles.sidebarNavItem, + isActive ? styles.sidebarNavItemActive : undefined, + ]) + } + > + + {icon} + {children} + + + ) +} + +const SidebarNavItemIcon: React.FC<{ icon: ElementType }> = ({ + icon: Icon, +}) => { + const styles = useStyles() + return +} + +export const Sidebar: React.FC<{ template: Template }> = ({ template }) => { + const styles = useStyles() + + return ( + + ) +} + +const useStyles = makeStyles((theme) => ({ + sidebar: { + width: 245, + flexShrink: 0, + }, + sidebarNavItem: { + color: "inherit", + display: "block", + fontSize: 14, + textDecoration: "none", + padding: theme.spacing(1.5, 1.5, 1.5, 2), + borderRadius: theme.shape.borderRadius / 2, + transition: "background-color 0.15s ease-in-out", + marginBottom: 1, + position: "relative", + + "&:hover": { + backgroundColor: theme.palette.action.hover, + }, + }, + sidebarNavItemActive: { + backgroundColor: theme.palette.action.hover, + + "&:before": { + content: '""', + display: "block", + width: 3, + height: "100%", + position: "absolute", + left: 0, + top: 0, + backgroundColor: theme.palette.secondary.dark, + borderTopLeftRadius: theme.shape.borderRadius, + borderBottomLeftRadius: theme.shape.borderRadius, + }, + }, + sidebarNavItemIcon: { + width: theme.spacing(2), + height: theme.spacing(2), + }, + templateInfo: { + marginBottom: theme.spacing(2), + }, + templateData: { + overflow: "hidden", + }, + name: { + fontWeight: 600, + overflow: "hidden", + textOverflow: "ellipsis", + whiteSpace: "nowrap", + }, + secondary: { + color: theme.palette.text.secondary, + fontSize: 12, + overflow: "hidden", + textOverflow: "ellipsis", + }, +})) diff --git a/site/src/pages/TemplateSettingsPage/TemplateSettingsForm.tsx b/site/src/pages/TemplateSettingsPage/TemplateGeneralSettingsPage/TemplateSettingsForm.tsx similarity index 100% rename from site/src/pages/TemplateSettingsPage/TemplateSettingsForm.tsx rename to site/src/pages/TemplateSettingsPage/TemplateGeneralSettingsPage/TemplateSettingsForm.tsx diff --git a/site/src/pages/TemplateSettingsPage/TemplateSettingsPage.test.tsx b/site/src/pages/TemplateSettingsPage/TemplateGeneralSettingsPage/TemplateSettingsPage.test.tsx similarity index 100% rename from site/src/pages/TemplateSettingsPage/TemplateSettingsPage.test.tsx rename to site/src/pages/TemplateSettingsPage/TemplateGeneralSettingsPage/TemplateSettingsPage.test.tsx diff --git a/site/src/pages/TemplateSettingsPage/TemplateSettingsPage.tsx b/site/src/pages/TemplateSettingsPage/TemplateGeneralSettingsPage/TemplateSettingsPage.tsx similarity index 52% rename from site/src/pages/TemplateSettingsPage/TemplateSettingsPage.tsx rename to site/src/pages/TemplateSettingsPage/TemplateGeneralSettingsPage/TemplateSettingsPage.tsx index 9dca8255017e4..30ccc3f42dcc6 100644 --- a/site/src/pages/TemplateSettingsPage/TemplateSettingsPage.tsx +++ b/site/src/pages/TemplateSettingsPage/TemplateGeneralSettingsPage/TemplateSettingsPage.tsx @@ -1,38 +1,36 @@ -import { useMachine } from "@xstate/react" +import { useMutation } from "@tanstack/react-query" +import { updateTemplateMeta } from "api/api" +import { UpdateTemplateMeta } from "api/typesGenerated" import { useDashboard } from "components/Dashboard/DashboardProvider" -import { useOrganizationId } from "hooks/useOrganizationId" +import { displaySuccess } from "components/GlobalSnackbar/utils" import { FC } from "react" import { Helmet } from "react-helmet-async" import { useTranslation } from "react-i18next" import { useNavigate, useParams } from "react-router-dom" import { pageTitle } from "util/page" -import { templateSettingsMachine } from "xServices/templateSettings/templateSettingsXService" +import { useTemplateSettingsContext } from "../TemplateSettingsLayout" import { TemplateSettingsPageView } from "./TemplateSettingsPageView" export const TemplateSettingsPage: FC = () => { const { template: templateName } = useParams() as { template: string } const { t } = useTranslation("templateSettingsPage") const navigate = useNavigate() - const organizationId = useOrganizationId() - const [state, send] = useMachine(templateSettingsMachine, { - context: { templateName, organizationId }, - actions: { - onSave: (_, { data }) => { - // Use the data.name because the template name can be changed. Since the - // API can return 304 if the template name is not changed, we use the - // templateName from the URL as default. - navigate(`/templates/${data.name ?? templateName}`) - }, - }, - }) - const { - templateSettings: template, - saveTemplateSettingsError, - getTemplateError, - } = state.context + const template = useTemplateSettingsContext() const { entitlements } = useDashboard() const canSetMaxTTL = entitlements.features["advanced_template_scheduling"].enabled + const { + mutate: updateTemplate, + isLoading: isSubmitting, + error: submitError, + } = useMutation( + (data: UpdateTemplateMeta) => updateTemplateMeta(template.id, data), + { + onSuccess: () => { + displaySuccess("Template updated successfully") + }, + }, + ) return ( <> @@ -41,17 +39,14 @@ export const TemplateSettingsPage: FC = () => { { navigate(`/templates/${templateName}`) }} onSubmit={(templateSettings) => { - send({ type: "SAVE", templateSettings }) + updateTemplate(templateSettings) }} /> diff --git a/site/src/pages/TemplateSettingsPage/TemplateSettingsPageView.stories.tsx b/site/src/pages/TemplateSettingsPage/TemplateGeneralSettingsPage/TemplateSettingsPageView.stories.tsx similarity index 100% rename from site/src/pages/TemplateSettingsPage/TemplateSettingsPageView.stories.tsx rename to site/src/pages/TemplateSettingsPage/TemplateGeneralSettingsPage/TemplateSettingsPageView.stories.tsx diff --git a/site/src/pages/TemplateSettingsPage/TemplateGeneralSettingsPage/TemplateSettingsPageView.tsx b/site/src/pages/TemplateSettingsPage/TemplateGeneralSettingsPage/TemplateSettingsPageView.tsx new file mode 100644 index 0000000000000..4f1d36c8243a5 --- /dev/null +++ b/site/src/pages/TemplateSettingsPage/TemplateGeneralSettingsPage/TemplateSettingsPageView.tsx @@ -0,0 +1,53 @@ +import { Template, UpdateTemplateMeta } from "api/typesGenerated" +import { ComponentProps, FC } from "react" +import { TemplateSettingsForm } from "./TemplateSettingsForm" +import { useTranslation } from "react-i18next" +import { PageHeader, PageHeaderTitle } from "components/PageHeader/PageHeader" +import { makeStyles } from "@material-ui/core/styles" + +export interface TemplateSettingsPageViewProps { + template: Template + onSubmit: (data: UpdateTemplateMeta) => void + onCancel: () => void + isSubmitting: boolean + submitError?: unknown + initialTouched?: ComponentProps["initialTouched"] + canSetMaxTTL: boolean +} + +export const TemplateSettingsPageView: FC = ({ + template, + onCancel, + onSubmit, + isSubmitting, + canSetMaxTTL, + submitError, + initialTouched, +}) => { + const { t } = useTranslation("templateSettingsPage") + const styles = useStyles() + + return ( + <> + + {t("title")} + + + + + ) +} + +const useStyles = makeStyles(() => ({ + pageHeader: { + paddingTop: 0, + }, +})) diff --git a/site/src/pages/TemplatePage/TemplatePermissionsPage/TemplatePermissionsPage.tsx b/site/src/pages/TemplateSettingsPage/TemplatePermissionsPage/TemplatePermissionsPage.tsx similarity index 100% rename from site/src/pages/TemplatePage/TemplatePermissionsPage/TemplatePermissionsPage.tsx rename to site/src/pages/TemplateSettingsPage/TemplatePermissionsPage/TemplatePermissionsPage.tsx diff --git a/site/src/pages/TemplatePage/TemplatePermissionsPage/TemplatePermissionsPageView.stories.tsx b/site/src/pages/TemplateSettingsPage/TemplatePermissionsPage/TemplatePermissionsPageView.stories.tsx similarity index 100% rename from site/src/pages/TemplatePage/TemplatePermissionsPage/TemplatePermissionsPageView.stories.tsx rename to site/src/pages/TemplateSettingsPage/TemplatePermissionsPage/TemplatePermissionsPageView.stories.tsx diff --git a/site/src/pages/TemplatePage/TemplatePermissionsPage/TemplatePermissionsPageView.tsx b/site/src/pages/TemplateSettingsPage/TemplatePermissionsPage/TemplatePermissionsPageView.tsx similarity index 100% rename from site/src/pages/TemplatePage/TemplatePermissionsPage/TemplatePermissionsPageView.tsx rename to site/src/pages/TemplateSettingsPage/TemplatePermissionsPage/TemplatePermissionsPageView.tsx diff --git a/site/src/pages/TemplateSettingsPage/TemplateSettingsLayout.tsx b/site/src/pages/TemplateSettingsPage/TemplateSettingsLayout.tsx new file mode 100644 index 0000000000000..aa2d1dae971c8 --- /dev/null +++ b/site/src/pages/TemplateSettingsPage/TemplateSettingsLayout.tsx @@ -0,0 +1,81 @@ +import { makeStyles } from "@material-ui/core/styles" +import { Sidebar } from "./Sidebar" +import { Stack } from "components/Stack/Stack" +import { createContext, FC, Suspense, useContext } from "react" +import { Helmet } from "react-helmet-async" +import { pageTitle } from "../../util/page" +import { Loader } from "components/Loader/Loader" +import { Outlet, useParams } from "react-router-dom" +import { Margins } from "components/Margins/Margins" +import { getTemplateByName } from "api/api" +import { useQuery } from "@tanstack/react-query" +import { useOrganizationId } from "hooks/useOrganizationId" + +const fetchTemplate = (orgId: string, name: string) => { + return getTemplateByName(orgId, name) +} + +const useTemplate = (orgId: string, name: string) => { + return useQuery({ + queryKey: ["template", orgId, name], + queryFn: () => fetchTemplate(orgId, name), + }) +} + +const TemplateSettingsContext = createContext< + Awaited> | undefined +>(undefined) + +export const useTemplateSettingsContext = () => { + const context = useContext(TemplateSettingsContext) + + if (!context) { + throw new Error( + "useTemplateSettingsContext must be used within a TemplateSettingsContext.Provider", + ) + } + + return context +} + +export const TemplateSettingsLayout: FC = () => { + const styles = useStyles() + const orgId = useOrganizationId() + const { template: templateName } = useParams() as { template: string } + const { data: template } = useTemplate(orgId, templateName) + + return ( + <> + + Codestin Search App + + + {template ? ( + + + + + }> +
+ +
+
+
+
+
+ ) : ( + + )} + + ) +} + +const useStyles = makeStyles((theme) => ({ + wrapper: { + padding: theme.spacing(6, 0), + }, + + content: { + width: "100%", + }, +})) diff --git a/site/src/pages/TemplateSettingsPage/TemplateSettingsPageView.tsx b/site/src/pages/TemplateSettingsPage/TemplateSettingsPageView.tsx deleted file mode 100644 index 14eec2798bb95..0000000000000 --- a/site/src/pages/TemplateSettingsPage/TemplateSettingsPageView.tsx +++ /dev/null @@ -1,66 +0,0 @@ -import { Template, UpdateTemplateMeta } from "api/typesGenerated" -import { AlertBanner } from "components/AlertBanner/AlertBanner" -import { Loader } from "components/Loader/Loader" -import { ComponentProps, FC } from "react" -import { TemplateSettingsForm } from "./TemplateSettingsForm" -import { Stack } from "components/Stack/Stack" -import { makeStyles } from "@material-ui/core/styles" -import { useTranslation } from "react-i18next" -import { FullPageHorizontalForm } from "components/FullPageForm/FullPageHorizontalForm" - -export interface TemplateSettingsPageViewProps { - template?: Template - onSubmit: (data: UpdateTemplateMeta) => void - onCancel: () => void - isSubmitting: boolean - errors?: { - getTemplateError?: unknown - saveTemplateSettingsError?: unknown - } - initialTouched?: ComponentProps["initialTouched"] - canSetMaxTTL: boolean -} - -export const TemplateSettingsPageView: FC = ({ - template, - onCancel, - onSubmit, - isSubmitting, - canSetMaxTTL, - errors = {}, - initialTouched, -}) => { - const classes = useStyles() - const isLoading = !template && !errors.getTemplateError - const { t } = useTranslation("templateSettingsPage") - - return ( - - {Boolean(errors.getTemplateError) && ( - - - - )} - {isLoading && } - {template && ( - <> - - - )} - - ) -} - -const useStyles = makeStyles((theme) => ({ - errorContainer: { - marginBottom: theme.spacing(2), - }, -})) diff --git a/site/src/theme/overrides.ts b/site/src/theme/overrides.ts index e52681da016bb..e9ded0bf46218 100644 --- a/site/src/theme/overrides.ts +++ b/site/src/theme/overrides.ts @@ -242,5 +242,15 @@ export const getOverrides = ({ minWidth: 120, }, }, + MuiSnackbar: { + anchorOriginBottomRight: { + bottom: `${24 + 36}px !important`, // 36 is the bottom bar height + }, + }, + MuiSnackbarContent: { + root: { + borderRadius: "4px !important", + }, + }, } } diff --git a/site/src/xServices/templateSettings/templateSettingsXService.ts b/site/src/xServices/templateSettings/templateSettingsXService.ts deleted file mode 100644 index f8420a9625c71..0000000000000 --- a/site/src/xServices/templateSettings/templateSettingsXService.ts +++ /dev/null @@ -1,166 +0,0 @@ -import { getTemplateByName, updateTemplateMeta, deleteTemplate } from "api/api" -import { Template, UpdateTemplateMeta } from "api/typesGenerated" -import { createMachine } from "xstate" -import { assign } from "xstate/lib/actions" -import { displaySuccess } from "components/GlobalSnackbar/utils" -import { t } from "i18next" - -export const templateSettingsMachine = - /** @xstate-layout N4IgpgJg5mDOIC5QBcwFsAOAbAhqgymMsgJYB2UsAdFgPY4TlQDEEtZYV5AbrQNadUmXASKkK1OgyYIetAMZ4S7ANoAGALqJQGWrBKl22kAA9EANgCMVAKwB2AByWATJbWWAnABYv55w-MAGhAAT0RnAGZnKgibGw9nD3M1JJsHNQiAX0zgoWw8MEJiJmpIAyZmfABBADUAUWNdfUMyYzMESx9bSK8IhwdncwcbF2dgsIRejypzX2dXf09zCLUbbNz0fNFiiSpYHG4Ktg4uMl4BKjyRQrESvYOZOUUW9S0kECbyo3f25KoHOxeDwODx9EYeNTzcYWGxqKgeGw+SJ2cx+VZebI5EBkWgQODGK4FIriSg0eiMCiNPRfVo-RBeMahRAQqhqNnuWERFFeOyedYgQnbEmlRgkqnNZS00DtSwRcz-EY2PzeeYOLk2aEIWJ2WwOIEBNIeBG8-mCm47Un7Q6U96fFptenWRJDIZy+y9AFBJkIEbRbxeWUBRwIyx2U2ba7Eu5WyDimkOhAI2yxSx+foMpWWTV2RLJgIrPoA4bh4RE24SOP2ukdHXOgJq8zuvoozWWBys9nzSydNt2NQYzFAA */ - createMachine( - { - id: "templateSettings", - predictableActionArguments: true, - tsTypes: {} as import("./templateSettingsXService.typegen").Typegen0, - schema: {} as { - context: { - organizationId: string - templateName: string - templateSettings?: Template - getTemplateError?: unknown - saveTemplateSettingsError?: unknown - deleteTemplateError?: Error | unknown - } - services: { - getTemplateSettings: { - data: Template - } - saveTemplateSettings: { - data: Template - } - } - events: - | { type: "SAVE"; templateSettings: UpdateTemplateMeta } - | { type: "DELETE" } - | { type: "CONFIRM_DELETE" } - | { type: "CANCEL_DELETE" } - }, - initial: "loading", - states: { - loading: { - invoke: { - src: "getTemplateSettings", - onDone: [ - { - actions: "assignTemplateSettings", - target: "editing", - }, - ], - onError: { - target: "error", - actions: "assignGetTemplateError", - }, - }, - }, - editing: { - on: { - SAVE: { - target: "saving", - }, - DELETE: { - target: "confirmingDelete", - }, - }, - }, - confirmingDelete: { - on: { - CONFIRM_DELETE: { - target: "deleting", - }, - CANCEL_DELETE: { - target: "editing", - }, - }, - }, - deleting: { - entry: "clearDeleteTemplateError", - invoke: { - src: "deleteTemplate", - id: "deleteTemplate", - onDone: [ - { - target: "deleted", - actions: "displayDeleteSuccess", - }, - ], - onError: [ - { - actions: "assignDeleteTemplateError", - target: "editing", - }, - ], - }, - }, - deleted: { - type: "final", - }, - saving: { - invoke: { - src: "saveTemplateSettings", - onDone: [ - { - target: "saved", - }, - ], - onError: [ - { - target: "editing", - actions: ["assignSaveTemplateSettingsError"], - }, - ], - }, - tags: ["submitting"], - }, - saved: { - entry: "onSave", - type: "final", - tags: ["submitting"], - }, - error: { - type: "final", - }, - }, - }, - { - services: { - getTemplateSettings: async ({ organizationId, templateName }) => { - return getTemplateByName(organizationId, templateName) - }, - saveTemplateSettings: async ( - { templateSettings }, - { templateSettings: newTemplateSettings }, - ) => { - if (!templateSettings) { - throw new Error("templateSettings is not loaded yet.") - } - - return updateTemplateMeta(templateSettings.id, newTemplateSettings) - }, - deleteTemplate: (ctx) => { - if (!ctx.templateSettings) { - throw new Error("Template not loaded") - } - return deleteTemplate(ctx.templateSettings.id) - }, - }, - actions: { - assignTemplateSettings: assign({ - templateSettings: (_, { data }) => data, - }), - assignGetTemplateError: assign({ - getTemplateError: (_, { data }) => data, - }), - assignSaveTemplateSettingsError: assign({ - saveTemplateSettingsError: (_, { data }) => data, - }), - assignDeleteTemplateError: assign({ - deleteTemplateError: (_, event) => event.data, - }), - clearDeleteTemplateError: assign({ - deleteTemplateError: (_) => undefined, - }), - displayDeleteSuccess: () => - displaySuccess(t("deleteSuccess", { ns: "templatePage" })), - }, - }, - ) From b40c8e7570c2dc1af5188b8bf1f88c7341ce37c3 Mon Sep 17 00:00:00 2001 From: Bruno Quaresma Date: Wed, 22 Mar 2023 15:38:26 +0000 Subject: [PATCH 02/17] Move permissions page --- .../TemplateSettingsPage.tsx | 2 +- .../TemplatePermissionsPage.tsx | 4 +- .../TemplateSettingsLayout.tsx | 38 ++++++++++++++----- 3 files changed, 31 insertions(+), 13 deletions(-) diff --git a/site/src/pages/TemplateSettingsPage/TemplateGeneralSettingsPage/TemplateSettingsPage.tsx b/site/src/pages/TemplateSettingsPage/TemplateGeneralSettingsPage/TemplateSettingsPage.tsx index 30ccc3f42dcc6..162e06cede6cc 100644 --- a/site/src/pages/TemplateSettingsPage/TemplateGeneralSettingsPage/TemplateSettingsPage.tsx +++ b/site/src/pages/TemplateSettingsPage/TemplateGeneralSettingsPage/TemplateSettingsPage.tsx @@ -15,7 +15,7 @@ export const TemplateSettingsPage: FC = () => { const { template: templateName } = useParams() as { template: string } const { t } = useTranslation("templateSettingsPage") const navigate = useNavigate() - const template = useTemplateSettingsContext() + const { template } = useTemplateSettingsContext() const { entitlements } = useDashboard() const canSetMaxTTL = entitlements.features["advanced_template_scheduling"].enabled diff --git a/site/src/pages/TemplateSettingsPage/TemplatePermissionsPage/TemplatePermissionsPage.tsx b/site/src/pages/TemplateSettingsPage/TemplatePermissionsPage/TemplatePermissionsPage.tsx index 56b4e36793e75..21e0e0eee1bf4 100644 --- a/site/src/pages/TemplateSettingsPage/TemplatePermissionsPage/TemplatePermissionsPage.tsx +++ b/site/src/pages/TemplateSettingsPage/TemplatePermissionsPage/TemplatePermissionsPage.tsx @@ -5,20 +5,20 @@ import { useMachine } from "@xstate/react" import { ChooseOne, Cond } from "components/Conditionals/ChooseOne" import { Paywall } from "components/Paywall/Paywall" import { Stack } from "components/Stack/Stack" -import { useTemplateLayoutContext } from "components/TemplateLayout/TemplateLayout" import { useFeatureVisibility } from "hooks/useFeatureVisibility" import { useOrganizationId } from "hooks/useOrganizationId" import { FC } from "react" import { Helmet } from "react-helmet-async" import { pageTitle } from "util/page" import { templateACLMachine } from "xServices/template/templateACLXService" +import { useTemplateSettingsContext } from "../TemplateSettingsLayout" import { TemplatePermissionsPageView } from "./TemplatePermissionsPageView" export const TemplatePermissionsPage: FC< React.PropsWithChildren > = () => { const organizationId = useOrganizationId() - const { template, permissions } = useTemplateLayoutContext() + const { template, permissions } = useTemplateSettingsContext() const { template_rbac: isTemplateRBACEnabled } = useFeatureVisibility() const [state, send] = useMachine(templateACLMachine, { context: { templateId: template.id }, diff --git a/site/src/pages/TemplateSettingsPage/TemplateSettingsLayout.tsx b/site/src/pages/TemplateSettingsPage/TemplateSettingsLayout.tsx index aa2d1dae971c8..9255dd6c720aa 100644 --- a/site/src/pages/TemplateSettingsPage/TemplateSettingsLayout.tsx +++ b/site/src/pages/TemplateSettingsPage/TemplateSettingsLayout.tsx @@ -7,23 +7,41 @@ import { pageTitle } from "../../util/page" import { Loader } from "components/Loader/Loader" import { Outlet, useParams } from "react-router-dom" import { Margins } from "components/Margins/Margins" -import { getTemplateByName } from "api/api" +import { checkAuthorization, getTemplateByName } from "api/api" import { useQuery } from "@tanstack/react-query" import { useOrganizationId } from "hooks/useOrganizationId" -const fetchTemplate = (orgId: string, name: string) => { - return getTemplateByName(orgId, name) +const templatePermissions = (templateId: string) => ({ + canUpdateTemplate: { + object: { + resource_type: "template", + resource_id: templateId, + }, + action: "update", + }, +}) + +const fetchTemplateSettings = async (orgId: string, name: string) => { + const template = await getTemplateByName(orgId, name) + const permissions = await checkAuthorization({ + checks: templatePermissions(template.id), + }) + + return { + template, + permissions, + } } const useTemplate = (orgId: string, name: string) => { return useQuery({ - queryKey: ["template", orgId, name], - queryFn: () => fetchTemplate(orgId, name), + queryKey: ["template", name, "settings"], + queryFn: () => fetchTemplateSettings(orgId, name), }) } const TemplateSettingsContext = createContext< - Awaited> | undefined + Awaited> | undefined >(undefined) export const useTemplateSettingsContext = () => { @@ -42,7 +60,7 @@ export const TemplateSettingsLayout: FC = () => { const styles = useStyles() const orgId = useOrganizationId() const { template: templateName } = useParams() as { template: string } - const { data: template } = useTemplate(orgId, templateName) + const { data: settings } = useTemplate(orgId, templateName) return ( <> @@ -50,11 +68,11 @@ export const TemplateSettingsLayout: FC = () => { Codestin Search App - {template ? ( - + {settings ? ( + - + }>
From 13086b4f6c8f1b186a94b14ed73ed0a645224f36 Mon Sep 17 00:00:00 2001 From: Bruno Quaresma Date: Wed, 22 Mar 2023 15:46:46 +0000 Subject: [PATCH 03/17] Move template variables --- site/src/AppRouter.tsx | 10 ++- .../pages/TemplateSettingsPage/Sidebar.tsx | 2 +- .../TemplateVariablesForm.tsx | 0 .../TemplateVariablesPage.test.tsx | 0 .../TemplateVariablesPage.tsx | 9 ++- .../TemplateVariablesPageView.stories.tsx | 0 .../TemplateVariablesPageView.tsx | 80 ++++++++++--------- .../template/templateVariablesXService.ts | 41 +--------- 8 files changed, 58 insertions(+), 84 deletions(-) rename site/src/pages/{ => TemplateSettingsPage}/TemplateVariablesPage/TemplateVariablesForm.tsx (100%) rename site/src/pages/{ => TemplateSettingsPage}/TemplateVariablesPage/TemplateVariablesPage.test.tsx (100%) rename site/src/pages/{ => TemplateSettingsPage}/TemplateVariablesPage/TemplateVariablesPage.tsx (89%) rename site/src/pages/{ => TemplateSettingsPage}/TemplateVariablesPage/TemplateVariablesPageView.stories.tsx (100%) rename site/src/pages/{ => TemplateSettingsPage}/TemplateVariablesPage/TemplateVariablesPageView.tsx (51%) diff --git a/site/src/AppRouter.tsx b/site/src/AppRouter.tsx index 9681faf7ca10b..1d88be0af3328 100644 --- a/site/src/AppRouter.tsx +++ b/site/src/AppRouter.tsx @@ -121,7 +121,10 @@ const CreateTemplatePage = lazy( () => import("./pages/CreateTemplatePage/CreateTemplatePage"), ) const TemplateVariablesPage = lazy( - () => import("./pages/TemplateVariablesPage/TemplateVariablesPage"), + () => + import( + "./pages/TemplateSettingsPage/TemplateVariablesPage/TemplateVariablesPage" + ), ) const WorkspaceSettingsPage = lazy( () => import("./pages/WorkspaceSettingsPage/WorkspaceSettingsPage"), @@ -173,9 +176,12 @@ export const AppRouter: FC = () => { path="permissions" element={} /> + } + /> - } /> } /> diff --git a/site/src/pages/TemplateSettingsPage/Sidebar.tsx b/site/src/pages/TemplateSettingsPage/Sidebar.tsx index f276e0dc1d54d..6f937d724e258 100644 --- a/site/src/pages/TemplateSettingsPage/Sidebar.tsx +++ b/site/src/pages/TemplateSettingsPage/Sidebar.tsx @@ -71,7 +71,7 @@ export const Sidebar: React.FC<{ template: Template }> = ({ template }) => { Permissions } > Variables diff --git a/site/src/pages/TemplateVariablesPage/TemplateVariablesForm.tsx b/site/src/pages/TemplateSettingsPage/TemplateVariablesPage/TemplateVariablesForm.tsx similarity index 100% rename from site/src/pages/TemplateVariablesPage/TemplateVariablesForm.tsx rename to site/src/pages/TemplateSettingsPage/TemplateVariablesPage/TemplateVariablesForm.tsx diff --git a/site/src/pages/TemplateVariablesPage/TemplateVariablesPage.test.tsx b/site/src/pages/TemplateSettingsPage/TemplateVariablesPage/TemplateVariablesPage.test.tsx similarity index 100% rename from site/src/pages/TemplateVariablesPage/TemplateVariablesPage.test.tsx rename to site/src/pages/TemplateSettingsPage/TemplateVariablesPage/TemplateVariablesPage.test.tsx diff --git a/site/src/pages/TemplateVariablesPage/TemplateVariablesPage.tsx b/site/src/pages/TemplateSettingsPage/TemplateVariablesPage/TemplateVariablesPage.tsx similarity index 89% rename from site/src/pages/TemplateVariablesPage/TemplateVariablesPage.tsx rename to site/src/pages/TemplateSettingsPage/TemplateVariablesPage/TemplateVariablesPage.tsx index 93c4379571e60..673fc9cb12dac 100644 --- a/site/src/pages/TemplateVariablesPage/TemplateVariablesPage.tsx +++ b/site/src/pages/TemplateSettingsPage/TemplateVariablesPage/TemplateVariablesPage.tsx @@ -4,13 +4,15 @@ import { TemplateVersionVariable, VariableValue, } from "api/typesGenerated" +import { displaySuccess } from "components/GlobalSnackbar/utils" import { useOrganizationId } from "hooks/useOrganizationId" import { FC } from "react" import { Helmet } from "react-helmet-async" import { useTranslation } from "react-i18next" import { useNavigate, useParams } from "react-router-dom" import { templateVariablesMachine } from "xServices/template/templateVariablesXService" -import { pageTitle } from "../../util/page" +import { pageTitle } from "../../../util/page" +import { useTemplateSettingsContext } from "../TemplateSettingsLayout" import { TemplateVariablesPageView } from "./TemplateVariablesPageView" export const TemplateVariablesPage: FC = () => { @@ -19,15 +21,16 @@ export const TemplateVariablesPage: FC = () => { template: string } const organizationId = useOrganizationId() + const { template } = useTemplateSettingsContext() const navigate = useNavigate() const [state, send] = useMachine(templateVariablesMachine, { context: { organizationId, - templateName, + template, }, actions: { onUpdateTemplate: () => { - navigate(`/templates/${templateName}`) + displaySuccess("Template updated successfully") }, }, }) diff --git a/site/src/pages/TemplateVariablesPage/TemplateVariablesPageView.stories.tsx b/site/src/pages/TemplateSettingsPage/TemplateVariablesPage/TemplateVariablesPageView.stories.tsx similarity index 100% rename from site/src/pages/TemplateVariablesPage/TemplateVariablesPageView.stories.tsx rename to site/src/pages/TemplateSettingsPage/TemplateVariablesPage/TemplateVariablesPageView.stories.tsx diff --git a/site/src/pages/TemplateVariablesPage/TemplateVariablesPageView.tsx b/site/src/pages/TemplateSettingsPage/TemplateVariablesPage/TemplateVariablesPageView.tsx similarity index 51% rename from site/src/pages/TemplateVariablesPage/TemplateVariablesPageView.tsx rename to site/src/pages/TemplateSettingsPage/TemplateVariablesPage/TemplateVariablesPageView.tsx index d59675f5ac903..dbe21e5e5de78 100644 --- a/site/src/pages/TemplateVariablesPage/TemplateVariablesPageView.tsx +++ b/site/src/pages/TemplateSettingsPage/TemplateVariablesPage/TemplateVariablesPageView.tsx @@ -10,8 +10,8 @@ import { TemplateVariablesForm } from "./TemplateVariablesForm" import { Stack } from "components/Stack/Stack" import { makeStyles } from "@material-ui/core/styles" import { useTranslation } from "react-i18next" -import { FullPageHorizontalForm } from "components/FullPageForm/FullPageHorizontalForm" import { GoBackButton } from "components/GoBackButton/GoBackButton" +import { PageHeader, PageHeaderTitle } from "components/PageHeader/PageHeader" export interface TemplateVariablesPageViewProps { templateVersion?: TemplateVersion @@ -48,45 +48,44 @@ export const TemplateVariablesPageView: FC = ({ return ( <> - - {Boolean(errors.getTemplateDataError) && ( - - - - )} - {Boolean(errors.updateTemplateError) && ( - - - - )} - {Boolean(errors.jobError) && ( - - - - )} - {isLoading && } - {templateVersion && - templateVariables && - templateVariables.length > 0 && ( - - )} - {templateVariables && templateVariables.length === 0 && ( -
- -
- -
+ + {t("title")} + + {Boolean(errors.getTemplateDataError) && ( + + + + )} + {Boolean(errors.updateTemplateError) && ( + + + + )} + {Boolean(errors.jobError) && ( + + + + )} + {isLoading && } + {templateVersion && templateVariables && templateVariables.length > 0 && ( + + )} + {templateVariables && templateVariables.length === 0 && ( +
+ +
+
- )} - +
+ )} ) } @@ -100,4 +99,7 @@ const useStyles = makeStyles((theme) => ({ width: "100%", marginTop: 32, }, + pageHeader: { + paddingTop: 0, + }, })) diff --git a/site/src/xServices/template/templateVariablesXService.ts b/site/src/xServices/template/templateVariablesXService.ts index bdeda05c84f76..361ce23af0704 100644 --- a/site/src/xServices/template/templateVariablesXService.ts +++ b/site/src/xServices/template/templateVariablesXService.ts @@ -1,6 +1,5 @@ import { createTemplateVersion, - getTemplateByName, getTemplateVersion, getTemplateVersionVariables, updateActiveTemplateVersion, @@ -17,9 +16,8 @@ import { Message } from "api/types" type TemplateVariablesContext = { organizationId: string - templateName: string - template?: Template + template: Template activeTemplateVersion?: TemplateVersion templateVariables?: TemplateVersionVariable[] @@ -46,9 +44,6 @@ export const templateVariablesMachine = createMachine( context: {} as TemplateVariablesContext, events: {} as UpdateTemplateEvent, services: {} as { - getTemplate: { - data: Template - } getActiveTemplateVersion: { data: TemplateVersion } @@ -66,24 +61,8 @@ export const templateVariablesMachine = createMachine( } }, }, - initial: "gettingTemplate", + initial: "gettingActiveTemplateVersion", states: { - gettingTemplate: { - entry: "clearGetTemplateDataError", - invoke: { - src: "getTemplate", - onDone: [ - { - actions: ["assignTemplate"], - target: "gettingActiveTemplateVersion", - }, - ], - onError: { - actions: ["assignGetTemplateDataError"], - target: "error", - }, - }, - }, gettingActiveTemplateVersion: { entry: "clearGetTemplateDataError", invoke: { @@ -183,19 +162,10 @@ export const templateVariablesMachine = createMachine( }, { services: { - getTemplate: ({ organizationId, templateName }) => { - return getTemplateByName(organizationId, templateName) - }, getActiveTemplateVersion: ({ template }) => { - if (!template) { - throw new Error("No template selected") - } return getTemplateVersion(template.active_version_id) }, getTemplateVariables: ({ template }) => { - if (!template) { - throw new Error("No template selected") - } return getTemplateVersionVariables(template.active_version_id) }, createNewTemplateVersion: ({ @@ -224,10 +194,6 @@ export const templateVariablesMachine = createMachine( return newTemplateVersion }, updateTemplate: ({ template, newTemplateVersion }) => { - if (!template) { - throw new Error("No template selected") - } - if (!newTemplateVersion) { throw new Error("New template version is undefined") } @@ -238,9 +204,6 @@ export const templateVariablesMachine = createMachine( }, }, actions: { - assignTemplate: assign({ - template: (_, event) => event.data, - }), assignActiveTemplateVersion: assign({ activeTemplateVersion: (_, event) => event.data, }), From 674647c9777ed2cc320a65ba0c5531a413ab5a6a Mon Sep 17 00:00:00 2001 From: Bruno Quaresma Date: Wed, 22 Mar 2023 15:58:19 +0000 Subject: [PATCH 04/17] Add schedule --- site/src/AppRouter.tsx | 7 + .../pages/TemplateSettingsPage/Sidebar.tsx | 2 +- .../TemplateScheduleForm.tsx | 165 +++++++++++++++ .../TemplateSchedulePage.test.tsx | 188 ++++++++++++++++++ .../TemplateSchedulePage.tsx | 56 ++++++ .../TemplateSchedulePageView.stories.tsx | 30 +++ .../TemplateSchedulePageView.tsx | 53 +++++ 7 files changed, 500 insertions(+), 1 deletion(-) create mode 100644 site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateScheduleForm.tsx create mode 100644 site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateSchedulePage.test.tsx create mode 100644 site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateSchedulePage.tsx create mode 100644 site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateSchedulePageView.stories.tsx create mode 100644 site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateSchedulePageView.tsx diff --git a/site/src/AppRouter.tsx b/site/src/AppRouter.tsx index 1d88be0af3328..1910440f0ad08 100644 --- a/site/src/AppRouter.tsx +++ b/site/src/AppRouter.tsx @@ -135,6 +135,12 @@ const CreateTokenPage = lazy( const TemplateFilesPage = lazy( () => import("./pages/TemplatePage/TemplateFilesPage/TemplateFilesPage"), ) +const TemplateSchedulePage = lazy( + () => + import( + "./pages/TemplateSettingsPage/TemplateSchedulePage/TemplateSchedulePage" + ), +) export const AppRouter: FC = () => { return ( @@ -180,6 +186,7 @@ export const AppRouter: FC = () => { path="variables" element={} /> + } /> diff --git a/site/src/pages/TemplateSettingsPage/Sidebar.tsx b/site/src/pages/TemplateSettingsPage/Sidebar.tsx index 6f937d724e258..1c6f9ad7c1993 100644 --- a/site/src/pages/TemplateSettingsPage/Sidebar.tsx +++ b/site/src/pages/TemplateSettingsPage/Sidebar.tsx @@ -77,7 +77,7 @@ export const Sidebar: React.FC<{ template: Template }> = ({ template }) => { Variables } > Schedule diff --git a/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateScheduleForm.tsx b/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateScheduleForm.tsx new file mode 100644 index 0000000000000..a8a44f5aa91e8 --- /dev/null +++ b/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateScheduleForm.tsx @@ -0,0 +1,165 @@ +import TextField from "@material-ui/core/TextField" +import { Template, UpdateTemplateMeta } from "api/typesGenerated" +import { FormikContextType, FormikTouched, useFormik } from "formik" +import { FC } from "react" +import { getFormHelpers } from "util/formUtils" +import * as Yup from "yup" +import i18next from "i18next" +import { useTranslation } from "react-i18next" +import { Maybe } from "components/Conditionals/Maybe" +import { FormSection, HorizontalForm, FormFooter } from "components/Form/Form" +import { Stack } from "components/Stack/Stack" +import { makeStyles } from "@material-ui/core/styles" +import Link from "@material-ui/core/Link" + +const TTLHelperText = ({ + ttl, + translationName, +}: { + ttl?: number + translationName: string +}) => { + const { t } = useTranslation("templateSettingsPage") + const count = typeof ttl !== "number" ? 0 : ttl + return ( + // no helper text if ttl is negative - error will show once field is considered touched + = 0}> + {t(translationName, { count })} + + ) +} + +const MAX_TTL_DAYS = 7 +const MS_HOUR_CONVERSION = 3600000 + +export const getValidationSchema = (): Yup.AnyObjectSchema => + Yup.object({ + default_ttl_ms: Yup.number() + .integer() + .min(0, i18next.t("defaultTTLMinError", { ns: "templateSettingsPage" })) + .max( + 24 * MAX_TTL_DAYS /* 7 days in hours */, + i18next.t("defaultTTLMaxError", { ns: "templateSettingsPage" }), + ), + max_ttl_ms: Yup.number() + .integer() + .min(0, i18next.t("maxTTLMinError", { ns: "templateSettingsPage" })) + .max( + 24 * MAX_TTL_DAYS /* 7 days in hours */, + i18next.t("maxTTLMaxError", { ns: "templateSettingsPage" }), + ), + }) + +export interface TemplateScheduleForm { + template: Template + onSubmit: (data: UpdateTemplateMeta) => void + onCancel: () => void + isSubmitting: boolean + error?: unknown + canSetMaxTTL: boolean + // Helpful to show field errors on Storybook + initialTouched?: FormikTouched +} + +export const TemplateScheduleForm: FC = ({ + template, + onSubmit, + onCancel, + error, + canSetMaxTTL, + isSubmitting, + initialTouched, +}) => { + const { t: commonT } = useTranslation("common") + const validationSchema = getValidationSchema() + const form: FormikContextType = + useFormik({ + initialValues: { + // on display, convert from ms => hours + default_ttl_ms: template.default_ttl_ms / MS_HOUR_CONVERSION, + // the API ignores this value, but to avoid tripping up validation set + // it to zero if the user can't set the field. + max_ttl_ms: canSetMaxTTL ? template.max_ttl_ms / MS_HOUR_CONVERSION : 0, + }, + validationSchema, + onSubmit: (formData) => { + // on submit, convert from hours => ms + onSubmit({ + default_ttl_ms: formData.default_ttl_ms + ? formData.default_ttl_ms * MS_HOUR_CONVERSION + : undefined, + max_ttl_ms: formData.max_ttl_ms + ? formData.max_ttl_ms * MS_HOUR_CONVERSION + : undefined, + }) + }, + initialTouched, + }) + const getFieldHelpers = getFormHelpers(form, error) + const { t } = useTranslation("templateSettingsPage") + const styles = useStyles() + + return ( + + + + , + )} + disabled={isSubmitting} + fullWidth + inputProps={{ min: 0, step: 1 }} + label={t("defaultTtlLabel")} + variant="outlined" + type="number" + /> + + + ) : ( + <> + {commonT("licenseFieldTextHelper")}{" "} + + {commonT("learnMore")} + + . + + ), + )} + disabled={isSubmitting || !canSetMaxTTL} + fullWidth + inputProps={{ min: 0, step: 1 }} + label={t("maxTtlLabel")} + variant="outlined" + type="number" + /> + + + + + + ) +} + +const useStyles = makeStyles(() => ({ + ttlFields: { + width: "100%", + }, +})) diff --git a/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateSchedulePage.test.tsx b/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateSchedulePage.test.tsx new file mode 100644 index 0000000000000..9ebca80b3f6ab --- /dev/null +++ b/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateSchedulePage.test.tsx @@ -0,0 +1,188 @@ +import { screen, waitFor } from "@testing-library/react" +import userEvent from "@testing-library/user-event" +import * as API from "api/api" +import { UpdateTemplateMeta } from "api/typesGenerated" +import { Language as FooterFormLanguage } from "components/FormFooter/FormFooter" +import { MockTemplate } from "../../../testHelpers/entities" +import { renderWithAuth } from "../../../testHelpers/renderHelpers" +import { getValidationSchema } from "./TemplateScheduleForm" +import TemplateSchedulePage from "./TemplateSchedulePage" +import i18next from "i18next" + +const { t } = i18next + +const validFormValues = { + name: "Name", + display_name: "A display name", + description: "A description", + icon: "vscode.png", + // these are the form values which are actually hours + default_ttl_ms: 1, + max_ttl_ms: 2, + allow_user_cancel_workspace_jobs: false, +} + +const renderTemplateSchedulePage = async () => { + renderWithAuth(, { + route: `/templates/${MockTemplate.name}/settings`, + path: `/templates/:template/settings`, + extraRoutes: [{ path: "templates/:template", element: <> }], + }) + // Wait the form to be rendered + const label = t("nameLabel", { ns: "TemplateSchedulePage" }) + await screen.findAllByLabelText(label) +} + +const fillAndSubmitForm = async ({ + name, + display_name, + description, + default_ttl_ms, + max_ttl_ms, + icon, + allow_user_cancel_workspace_jobs, +}: Required) => { + const label = t("nameLabel", { ns: "TemplateSchedulePage" }) + const nameField = await screen.findByLabelText(label) + await userEvent.clear(nameField) + await userEvent.type(nameField, name) + + const displayNameLabel = t("displayNameLabel", { ns: "TemplateSchedulePage" }) + + const displayNameField = await screen.findByLabelText(displayNameLabel) + await userEvent.clear(displayNameField) + await userEvent.type(displayNameField, display_name) + + const descriptionLabel = t("descriptionLabel", { ns: "TemplateSchedulePage" }) + const descriptionField = await screen.findByLabelText(descriptionLabel) + await userEvent.clear(descriptionField) + await userEvent.type(descriptionField, description) + + const iconLabel = t("iconLabel", { ns: "TemplateSchedulePage" }) + const iconField = await screen.findByLabelText(iconLabel) + await userEvent.clear(iconField) + await userEvent.type(iconField, icon) + + const defaultTtlLabel = t("defaultTtlLabel", { ns: "TemplateSchedulePage" }) + const defaultTtlField = await screen.findByLabelText(defaultTtlLabel) + await userEvent.clear(defaultTtlField) + await userEvent.type(defaultTtlField, default_ttl_ms.toString()) + + const entitlements = await API.getEntitlements() + if (entitlements.features["advanced_template_scheduling"].enabled) { + const maxTtlLabel = t("maxTtlLabel", { ns: "TemplateSchedulePage" }) + const maxTtlField = await screen.findByLabelText(maxTtlLabel) + await userEvent.clear(maxTtlField) + await userEvent.type(maxTtlField, max_ttl_ms.toString()) + } + + const allowCancelJobsField = screen.getByRole("checkbox") + // checkbox is checked by default, so it must be clicked to get unchecked + if (!allow_user_cancel_workspace_jobs) { + await userEvent.click(allowCancelJobsField) + } + + const submitButton = await screen.findByText( + FooterFormLanguage.defaultSubmitLabel, + ) + await userEvent.click(submitButton) +} + +describe("TemplateSchedulePage", () => { + it("renders", async () => { + const { t } = i18next + const pageTitle = t("title", { + ns: "TemplateSchedulePage", + }) + await renderTemplateSchedulePage() + const element = await screen.findByText(pageTitle) + expect(element).toBeDefined() + }) + + it("succeeds", async () => { + await renderTemplateSchedulePage() + + jest.spyOn(API, "updateTemplateMeta").mockResolvedValueOnce({ + ...MockTemplate, + ...validFormValues, + }) + await fillAndSubmitForm(validFormValues) + + await waitFor(() => expect(API.updateTemplateMeta).toBeCalledTimes(1)) + }) + + test("ttl is converted to and from hours", async () => { + await renderTemplateSchedulePage() + + jest.spyOn(API, "updateTemplateMeta").mockResolvedValueOnce({ + ...MockTemplate, + ...validFormValues, + }) + + await fillAndSubmitForm(validFormValues) + await waitFor(() => expect(API.updateTemplateMeta).toBeCalledTimes(1)) + await waitFor(() => + expect(API.updateTemplateMeta).toBeCalledWith( + "test-template", + expect.objectContaining({ + ...validFormValues, + // convert from the display value (hours) to ms + default_ttl_ms: validFormValues.default_ttl_ms * 3600000, + // this value is undefined if not entitled + max_ttl_ms: undefined, + }), + ), + ) + }) + + it("allows a ttl of 7 days", () => { + const values: UpdateTemplateMeta = { + ...validFormValues, + default_ttl_ms: 24 * 7, + } + const validate = () => getValidationSchema().validateSync(values) + expect(validate).not.toThrowError() + }) + + it("allows ttl of 0", () => { + const values: UpdateTemplateMeta = { + ...validFormValues, + default_ttl_ms: 0, + } + const validate = () => getValidationSchema().validateSync(values) + expect(validate).not.toThrowError() + }) + + it("disallows a ttl of 7 days + 1 hour", () => { + const values: UpdateTemplateMeta = { + ...validFormValues, + default_ttl_ms: 24 * 7 + 1, + } + const validate = () => getValidationSchema().validateSync(values) + expect(validate).toThrowError( + t("defaultTTLMaxError", { ns: "TemplateSchedulePage" }), + ) + }) + + it("allows a description of 128 chars", () => { + const values: UpdateTemplateMeta = { + ...validFormValues, + description: + "Nam quis nulla. Integer malesuada. In in enim a arcu imperdiet malesuada. Sed vel lectus. Donec odio urna, tempus molestie, port", + } + const validate = () => getValidationSchema().validateSync(values) + expect(validate).not.toThrowError() + }) + + it("disallows a description of 128 + 1 chars", () => { + const values: UpdateTemplateMeta = { + ...validFormValues, + description: + "Nam quis nulla. Integer malesuada. In in enim a arcu imperdiet malesuada. Sed vel lectus. Donec odio urna, tempus molestie, port a", + } + const validate = () => getValidationSchema().validateSync(values) + expect(validate).toThrowError( + t("descriptionMaxError", { ns: "TemplateSchedulePage" }), + ) + }) +}) diff --git a/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateSchedulePage.tsx b/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateSchedulePage.tsx new file mode 100644 index 0000000000000..077e604bef67c --- /dev/null +++ b/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateSchedulePage.tsx @@ -0,0 +1,56 @@ +import { useMutation } from "@tanstack/react-query" +import { updateTemplateMeta } from "api/api" +import { UpdateTemplateMeta } from "api/typesGenerated" +import { useDashboard } from "components/Dashboard/DashboardProvider" +import { displaySuccess } from "components/GlobalSnackbar/utils" +import { FC } from "react" +import { Helmet } from "react-helmet-async" +import { useTranslation } from "react-i18next" +import { useNavigate, useParams } from "react-router-dom" +import { pageTitle } from "util/page" +import { useTemplateSettingsContext } from "../TemplateSettingsLayout" +import { TemplateSchedulePageView } from "./TemplateSchedulePageView" + +const TemplateSchedulePage: FC = () => { + const { template: templateName } = useParams() as { template: string } + const { t } = useTranslation("templateSettingsPage") + const navigate = useNavigate() + const { template } = useTemplateSettingsContext() + const { entitlements } = useDashboard() + const canSetMaxTTL = + entitlements.features["advanced_template_scheduling"].enabled + const { + mutate: updateTemplate, + isLoading: isSubmitting, + error: submitError, + } = useMutation( + (data: UpdateTemplateMeta) => updateTemplateMeta(template.id, data), + { + onSuccess: () => { + displaySuccess("Template updated successfully") + }, + }, + ) + + return ( + <> + + Codestin Search App + + { + navigate(`/templates/${templateName}`) + }} + onSubmit={(templateSettings) => { + updateTemplate(templateSettings) + }} + /> + + ) +} + +export default TemplateSchedulePage diff --git a/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateSchedulePageView.stories.tsx b/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateSchedulePageView.stories.tsx new file mode 100644 index 0000000000000..a9f5b94239bfc --- /dev/null +++ b/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateSchedulePageView.stories.tsx @@ -0,0 +1,30 @@ +import { action } from "@storybook/addon-actions" +import { Story } from "@storybook/react" +import * as Mocks from "../../../testHelpers/renderHelpers" +import { + TemplateSchedulePageView, + TemplateSchedulePageViewProps, +} from "./TemplateSchedulePageView" + +export default { + title: "pages/TemplateSchedulePageView", + component: TemplateSchedulePageView, + args: { + canSetMaxTTL: true, + template: Mocks.MockTemplate, + onSubmit: action("onSubmit"), + onCancel: action("cancel"), + }, +} + +const Template: Story = (args) => ( + +) + +export const Example = Template.bind({}) +Example.args = {} + +export const CantSetMaxTTL = Template.bind({}) +CantSetMaxTTL.args = { + canSetMaxTTL: false, +} diff --git a/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateSchedulePageView.tsx b/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateSchedulePageView.tsx new file mode 100644 index 0000000000000..393245009b0c4 --- /dev/null +++ b/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateSchedulePageView.tsx @@ -0,0 +1,53 @@ +import { Template, UpdateTemplateMeta } from "api/typesGenerated" +import { ComponentProps, FC } from "react" +import { TemplateScheduleForm } from "./TemplateScheduleForm" +import { useTranslation } from "react-i18next" +import { PageHeader, PageHeaderTitle } from "components/PageHeader/PageHeader" +import { makeStyles } from "@material-ui/core/styles" + +export interface TemplateSchedulePageViewProps { + template: Template + onSubmit: (data: UpdateTemplateMeta) => void + onCancel: () => void + isSubmitting: boolean + submitError?: unknown + initialTouched?: ComponentProps["initialTouched"] + canSetMaxTTL: boolean +} + +export const TemplateSchedulePageView: FC = ({ + template, + onCancel, + onSubmit, + isSubmitting, + canSetMaxTTL, + submitError, + initialTouched, +}) => { + const { t } = useTranslation("templateSettingsPage") + const styles = useStyles() + + return ( + <> + + {t("title")} + + + + + ) +} + +const useStyles = makeStyles(() => ({ + pageHeader: { + paddingTop: 0, + }, +})) From f5a96733eb00411dac9635aba892c8f53afb0a48 Mon Sep 17 00:00:00 2001 From: Bruno Quaresma Date: Wed, 22 Mar 2023 16:00:44 +0000 Subject: [PATCH 05/17] Remove schedule from general settings --- .../TemplateSettingsForm.tsx | 110 +----------------- 1 file changed, 2 insertions(+), 108 deletions(-) diff --git a/site/src/pages/TemplateSettingsPage/TemplateGeneralSettingsPage/TemplateSettingsForm.tsx b/site/src/pages/TemplateSettingsPage/TemplateGeneralSettingsPage/TemplateSettingsForm.tsx index 475a99d148344..f0172ec837ffd 100644 --- a/site/src/pages/TemplateSettingsPage/TemplateGeneralSettingsPage/TemplateSettingsForm.tsx +++ b/site/src/pages/TemplateSettingsPage/TemplateGeneralSettingsPage/TemplateSettingsForm.tsx @@ -11,7 +11,6 @@ import { import * as Yup from "yup" import i18next from "i18next" import { useTranslation } from "react-i18next" -import { Maybe } from "components/Conditionals/Maybe" import { LazyIconField } from "components/IconField/LazyIconField" import { FormFields, @@ -23,28 +22,8 @@ import { Stack } from "components/Stack/Stack" import Checkbox from "@material-ui/core/Checkbox" import { HelpTooltip, HelpTooltipText } from "components/Tooltips/HelpTooltip" import { makeStyles } from "@material-ui/core/styles" -import Link from "@material-ui/core/Link" - -const TTLHelperText = ({ - ttl, - translationName, -}: { - ttl?: number - translationName: string -}) => { - const { t } = useTranslation("templateSettingsPage") - const count = typeof ttl !== "number" ? 0 : ttl - return ( - // no helper text if ttl is negative - error will show once field is considered touched - = 0}> - {t(translationName, { count })} - - ) -} const MAX_DESCRIPTION_CHAR_LIMIT = 128 -const MAX_TTL_DAYS = 7 -const MS_HOUR_CONVERSION = 3600000 export const getValidationSchema = (): Yup.AnyObjectSchema => Yup.object({ @@ -58,20 +37,7 @@ export const getValidationSchema = (): Yup.AnyObjectSchema => MAX_DESCRIPTION_CHAR_LIMIT, i18next.t("descriptionMaxError", { ns: "templateSettingsPage" }), ), - default_ttl_ms: Yup.number() - .integer() - .min(0, i18next.t("defaultTTLMinError", { ns: "templateSettingsPage" })) - .max( - 24 * MAX_TTL_DAYS /* 7 days in hours */, - i18next.t("defaultTTLMaxError", { ns: "templateSettingsPage" }), - ), - max_ttl_ms: Yup.number() - .integer() - .min(0, i18next.t("maxTTLMinError", { ns: "templateSettingsPage" })) - .max( - 24 * MAX_TTL_DAYS /* 7 days in hours */, - i18next.t("maxTTLMaxError", { ns: "templateSettingsPage" }), - ), + allow_user_cancel_workspace_jobs: Yup.boolean(), }) @@ -81,7 +47,6 @@ export interface TemplateSettingsForm { onCancel: () => void isSubmitting: boolean error?: unknown - canSetMaxTTL: boolean // Helpful to show field errors on Storybook initialTouched?: FormikTouched } @@ -91,11 +56,9 @@ export const TemplateSettingsForm: FC = ({ onSubmit, onCancel, error, - canSetMaxTTL, isSubmitting, initialTouched, }) => { - const { t: commonT } = useTranslation("common") const validationSchema = getValidationSchema() const form: FormikContextType = useFormik({ @@ -103,28 +66,12 @@ export const TemplateSettingsForm: FC = ({ name: template.name, display_name: template.display_name, description: template.description, - // on display, convert from ms => hours - default_ttl_ms: template.default_ttl_ms / MS_HOUR_CONVERSION, - // the API ignores this value, but to avoid tripping up validation set - // it to zero if the user can't set the field. - max_ttl_ms: canSetMaxTTL ? template.max_ttl_ms / MS_HOUR_CONVERSION : 0, icon: template.icon, allow_user_cancel_workspace_jobs: template.allow_user_cancel_workspace_jobs, }, validationSchema, - onSubmit: (formData) => { - // on submit, convert from hours => ms - onSubmit({ - ...formData, - default_ttl_ms: formData.default_ttl_ms - ? formData.default_ttl_ms * MS_HOUR_CONVERSION - : undefined, - max_ttl_ms: formData.max_ttl_ms - ? formData.max_ttl_ms * MS_HOUR_CONVERSION - : undefined, - }) - }, + onSubmit, initialTouched, }) const getFieldHelpers = getFormHelpers(form, error) @@ -188,55 +135,6 @@ export const TemplateSettingsForm: FC = ({ - - - , - )} - disabled={isSubmitting} - fullWidth - inputProps={{ min: 0, step: 1 }} - label={t("defaultTtlLabel")} - variant="outlined" - type="number" - /> - - - ) : ( - <> - {commonT("licenseFieldTextHelper")}{" "} - - {commonT("learnMore")} - - . - - ), - )} - disabled={isSubmitting || !canSetMaxTTL} - fullWidth - inputProps={{ min: 0, step: 1 }} - label={t("maxTtlLabel")} - variant="outlined" - type="number" - /> - - - ({ fontSize: theme.spacing(1.5), color: theme.palette.text.secondary, }, - - ttlFields: { - width: "100%", - }, })) From ce33a5cd0404a27c71c95c7ae84a33ca59204ae2 Mon Sep 17 00:00:00 2001 From: Bruno Quaresma Date: Wed, 22 Mar 2023 16:11:04 +0000 Subject: [PATCH 06/17] Fix update --- .../TemplateGeneralSettingsPage/TemplateSettingsPage.tsx | 7 +++++-- .../TemplateSchedulePage/TemplateSchedulePage.tsx | 7 +++++-- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/site/src/pages/TemplateSettingsPage/TemplateGeneralSettingsPage/TemplateSettingsPage.tsx b/site/src/pages/TemplateSettingsPage/TemplateGeneralSettingsPage/TemplateSettingsPage.tsx index 162e06cede6cc..83111e8215f7c 100644 --- a/site/src/pages/TemplateSettingsPage/TemplateGeneralSettingsPage/TemplateSettingsPage.tsx +++ b/site/src/pages/TemplateSettingsPage/TemplateGeneralSettingsPage/TemplateSettingsPage.tsx @@ -26,7 +26,7 @@ export const TemplateSettingsPage: FC = () => { } = useMutation( (data: UpdateTemplateMeta) => updateTemplateMeta(template.id, data), { - onSuccess: () => { + onSuccess: async () => { displaySuccess("Template updated successfully") }, }, @@ -46,7 +46,10 @@ export const TemplateSettingsPage: FC = () => { navigate(`/templates/${templateName}`) }} onSubmit={(templateSettings) => { - updateTemplate(templateSettings) + updateTemplate({ + ...template, + ...templateSettings, + }) }} /> diff --git a/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateSchedulePage.tsx b/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateSchedulePage.tsx index 077e604bef67c..e74dd67e2169f 100644 --- a/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateSchedulePage.tsx +++ b/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateSchedulePage.tsx @@ -45,8 +45,11 @@ const TemplateSchedulePage: FC = () => { onCancel={() => { navigate(`/templates/${templateName}`) }} - onSubmit={(templateSettings) => { - updateTemplate(templateSettings) + onSubmit={(templateScheduleSettings) => { + updateTemplate({ + ...template, + ...templateScheduleSettings, + }) }} /> From 51ae1cd2a8a3d32a80093a4a3449e38c27d48d0a Mon Sep 17 00:00:00 2001 From: Bruno Quaresma Date: Wed, 22 Mar 2023 16:13:20 +0000 Subject: [PATCH 07/17] Remove permissiosn from tab --- .../TemplateLayout/TemplateLayout.tsx | 43 ++++++------------- 1 file changed, 14 insertions(+), 29 deletions(-) diff --git a/site/src/components/TemplateLayout/TemplateLayout.tsx b/site/src/components/TemplateLayout/TemplateLayout.tsx index 547c927b9395a..ed1ce35093e1f 100644 --- a/site/src/components/TemplateLayout/TemplateLayout.tsx +++ b/site/src/components/TemplateLayout/TemplateLayout.tsx @@ -42,13 +42,6 @@ const fetchTemplate = async (orgId: string, templateName: string) => { } } -const useTemplateData = (orgId: string, templateName: string) => { - return useQuery({ - queryKey: ["template", templateName], - queryFn: () => fetchTemplate(orgId, templateName), - }) -} - type TemplateLayoutContextValue = Awaited> const TemplateLayoutContext = createContext< @@ -71,28 +64,31 @@ export const TemplateLayout: FC<{ children?: JSX.Element }> = ({ const navigate = useNavigate() const styles = useStyles() const orgId = useOrganizationId() - const { template } = useParams() as { template: string } - const templateData = useTemplateData(orgId, template) + const { template: templateName } = useParams() as { template: string } + const { data, error, isLoading } = useQuery({ + queryKey: ["template", templateName], + queryFn: () => fetchTemplate(orgId, templateName), + }) const dashboard = useDashboard() - if (templateData.error) { + if (error) { return (
- +
) } - if (templateData.isLoading || !templateData.data) { + if (isLoading || !data) { return } return ( <> { navigate("/templates") @@ -104,7 +100,7 @@ export const TemplateLayout: FC<{ children?: JSX.Element }> = ({ combineClasses([ styles.tabItem, @@ -115,18 +111,7 @@ export const TemplateLayout: FC<{ children?: JSX.Element }> = ({ Summary - combineClasses([ - styles.tabItem, - isActive ? styles.tabItemActive : undefined, - ]) - } - > - Permissions - - combineClasses([ styles.tabItem, @@ -141,7 +126,7 @@ export const TemplateLayout: FC<{ children?: JSX.Element }> = ({
- + }>{children} From d657b702b20142f45ec884c3f10a183f2664e562 Mon Sep 17 00:00:00 2001 From: Bruno Quaresma Date: Wed, 22 Mar 2023 16:15:23 +0000 Subject: [PATCH 08/17] Remove go back button --- .../components/GoBackButton/GoBackButton.tsx | 19 ------------------- .../TemplateVariablesPage.test.tsx | 16 ---------------- .../TemplateVariablesPageView.tsx | 8 +------- 3 files changed, 1 insertion(+), 42 deletions(-) delete mode 100644 site/src/components/GoBackButton/GoBackButton.tsx diff --git a/site/src/components/GoBackButton/GoBackButton.tsx b/site/src/components/GoBackButton/GoBackButton.tsx deleted file mode 100644 index 36818eb6458ae..0000000000000 --- a/site/src/components/GoBackButton/GoBackButton.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import Button from "@material-ui/core/Button" - -interface GoBackButtonProps { - onClick: () => void -} - -export const Language = { - ariaLabel: "Go back", -} - -export const GoBackButton: React.FC< - React.PropsWithChildren -> = ({ onClick }) => { - return ( - - ) -} diff --git a/site/src/pages/TemplateSettingsPage/TemplateVariablesPage/TemplateVariablesPage.test.tsx b/site/src/pages/TemplateSettingsPage/TemplateVariablesPage/TemplateVariablesPage.test.tsx index 135f980fd1238..8427efd8fc604 100644 --- a/site/src/pages/TemplateSettingsPage/TemplateVariablesPage/TemplateVariablesPage.test.tsx +++ b/site/src/pages/TemplateSettingsPage/TemplateVariablesPage/TemplateVariablesPage.test.tsx @@ -170,20 +170,4 @@ describe("TemplateVariablesPage", () => { const validationError = await screen.findByText(validationRequiredField) expect(validationError).toBeDefined() }) - - it("no managed variables", async () => { - jest.spyOn(API, "getTemplateByName").mockResolvedValueOnce(MockTemplate) - jest - .spyOn(API, "getTemplateVersion") - .mockResolvedValueOnce(MockTemplateVersion) - jest.spyOn(API, "getTemplateVersionVariables").mockResolvedValueOnce([]) - - renderTemplateVariablesPage() - - const element = await screen.findByText(pageTitleText) - expect(element).toBeDefined() - - const goBackButton = await screen.findByText("Go back") - expect(goBackButton).toBeDefined() - }) }) diff --git a/site/src/pages/TemplateSettingsPage/TemplateVariablesPage/TemplateVariablesPageView.tsx b/site/src/pages/TemplateSettingsPage/TemplateVariablesPage/TemplateVariablesPageView.tsx index dbe21e5e5de78..04bc10b26e0a9 100644 --- a/site/src/pages/TemplateSettingsPage/TemplateVariablesPage/TemplateVariablesPageView.tsx +++ b/site/src/pages/TemplateSettingsPage/TemplateVariablesPage/TemplateVariablesPageView.tsx @@ -10,7 +10,6 @@ import { TemplateVariablesForm } from "./TemplateVariablesForm" import { Stack } from "components/Stack/Stack" import { makeStyles } from "@material-ui/core/styles" import { useTranslation } from "react-i18next" -import { GoBackButton } from "components/GoBackButton/GoBackButton" import { PageHeader, PageHeaderTitle } from "components/PageHeader/PageHeader" export interface TemplateVariablesPageViewProps { @@ -79,12 +78,7 @@ export const TemplateVariablesPageView: FC = ({ /> )} {templateVariables && templateVariables.length === 0 && ( -
- -
- -
-
+ )} ) From 54fd3c38e46c1498a7d854f7a13ca8a9c97f03ed Mon Sep 17 00:00:00 2001 From: Bruno Quaresma Date: Wed, 22 Mar 2023 16:18:04 +0000 Subject: [PATCH 09/17] Make template name on sidebar a link --- site/src/pages/TemplateSettingsPage/Sidebar.tsx | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/site/src/pages/TemplateSettingsPage/Sidebar.tsx b/site/src/pages/TemplateSettingsPage/Sidebar.tsx index 1c6f9ad7c1993..84e9069daba06 100644 --- a/site/src/pages/TemplateSettingsPage/Sidebar.tsx +++ b/site/src/pages/TemplateSettingsPage/Sidebar.tsx @@ -4,7 +4,7 @@ import VariablesIcon from "@material-ui/icons/CodeOutlined" import { Template } from "api/typesGenerated" import { Stack } from "components/Stack/Stack" import { FC, ElementType, PropsWithChildren, ReactNode } from "react" -import { NavLink } from "react-router-dom" +import { Link, NavLink } from "react-router-dom" import { combineClasses } from "util/combineClasses" import GeneralIcon from "@material-ui/icons/SettingsOutlined" import SecurityIcon from "@material-ui/icons/LockOutlined" @@ -52,11 +52,11 @@ export const Sidebar: React.FC<{ template: Template }> = ({ template }) => { > - + {template.display_name !== "" ? template.display_name : template.name} - + {template.name} @@ -137,6 +137,8 @@ const useStyles = makeStyles((theme) => ({ overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap", + color: theme.palette.text.primary, + textDecoration: "none", }, secondary: { color: theme.palette.text.secondary, From 27093d4edaf4843385f578a00439d27e83afbb84 Mon Sep 17 00:00:00 2001 From: Bruno Quaresma Date: Wed, 22 Mar 2023 16:19:40 +0000 Subject: [PATCH 10/17] Remove variables version --- site/src/components/TemplateLayout/TemplatePageHeader.tsx | 7 ------- 1 file changed, 7 deletions(-) diff --git a/site/src/components/TemplateLayout/TemplatePageHeader.tsx b/site/src/components/TemplateLayout/TemplatePageHeader.tsx index 503c09e944d51..11fc83223a980 100644 --- a/site/src/components/TemplateLayout/TemplatePageHeader.tsx +++ b/site/src/components/TemplateLayout/TemplatePageHeader.tsx @@ -67,13 +67,6 @@ const TemplateMenu: FC<{ > {Language.settingsButton} - - {Language.variablesButton} - {canEditFiles && ( Date: Wed, 22 Mar 2023 16:27:34 +0000 Subject: [PATCH 11/17] Add titles --- site/src/i18n/en/templateSettingsPage.json | 2 +- .../TemplateGeneralSettingsPage/TemplateSettingsPage.tsx | 2 +- .../TemplatePermissionsPage/TemplatePermissionsPage.tsx | 2 +- .../TemplateSchedulePage/TemplateSchedulePage.tsx | 4 +--- .../pages/TemplateSettingsPage/TemplateSettingsLayout.tsx | 2 +- .../TemplateVariablesPage/TemplateVariablesPage.tsx | 2 +- site/src/util/page.ts | 5 +++-- 7 files changed, 9 insertions(+), 10 deletions(-) diff --git a/site/src/i18n/en/templateSettingsPage.json b/site/src/i18n/en/templateSettingsPage.json index bd0f35f42711d..66e97d5f3e607 100644 --- a/site/src/i18n/en/templateSettingsPage.json +++ b/site/src/i18n/en/templateSettingsPage.json @@ -1,5 +1,5 @@ { - "title": "Template settings", + "title": "General Settings", "nameLabel": "Name", "displayNameLabel": "Display name", "descriptionLabel": "Description", diff --git a/site/src/pages/TemplateSettingsPage/TemplateGeneralSettingsPage/TemplateSettingsPage.tsx b/site/src/pages/TemplateSettingsPage/TemplateGeneralSettingsPage/TemplateSettingsPage.tsx index 83111e8215f7c..bc36a6df3643d 100644 --- a/site/src/pages/TemplateSettingsPage/TemplateGeneralSettingsPage/TemplateSettingsPage.tsx +++ b/site/src/pages/TemplateSettingsPage/TemplateGeneralSettingsPage/TemplateSettingsPage.tsx @@ -35,7 +35,7 @@ export const TemplateSettingsPage: FC = () => { return ( <> - Codestin Search App + Codestin Search App - Codestin Search App + Codestin Search App diff --git a/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateSchedulePage.tsx b/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateSchedulePage.tsx index e74dd67e2169f..c9e1e33c5b3b5 100644 --- a/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateSchedulePage.tsx +++ b/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateSchedulePage.tsx @@ -5,7 +5,6 @@ import { useDashboard } from "components/Dashboard/DashboardProvider" import { displaySuccess } from "components/GlobalSnackbar/utils" import { FC } from "react" import { Helmet } from "react-helmet-async" -import { useTranslation } from "react-i18next" import { useNavigate, useParams } from "react-router-dom" import { pageTitle } from "util/page" import { useTemplateSettingsContext } from "../TemplateSettingsLayout" @@ -13,7 +12,6 @@ import { TemplateSchedulePageView } from "./TemplateSchedulePageView" const TemplateSchedulePage: FC = () => { const { template: templateName } = useParams() as { template: string } - const { t } = useTranslation("templateSettingsPage") const navigate = useNavigate() const { template } = useTemplateSettingsContext() const { entitlements } = useDashboard() @@ -35,7 +33,7 @@ const TemplateSchedulePage: FC = () => { return ( <> - Codestin Search App + Codestin Search App { return ( <> - Codestin Search App + Codestin Search App {settings ? ( diff --git a/site/src/pages/TemplateSettingsPage/TemplateVariablesPage/TemplateVariablesPage.tsx b/site/src/pages/TemplateSettingsPage/TemplateVariablesPage/TemplateVariablesPage.tsx index 673fc9cb12dac..f35a9c800ce0f 100644 --- a/site/src/pages/TemplateSettingsPage/TemplateVariablesPage/TemplateVariablesPage.tsx +++ b/site/src/pages/TemplateSettingsPage/TemplateVariablesPage/TemplateVariablesPage.tsx @@ -46,7 +46,7 @@ export const TemplateVariablesPage: FC = () => { return ( <> - Codestin Search App + Codestin Search App { - return `${prefix} – Coder` +export const pageTitle = (prefix: string | string[]): string => { + const title = Array.isArray(prefix) ? prefix.join(" · ") : prefix + return `${title} - Coder` } From d3081e8a8062e89894f1785e05b9e6e0617e4e63 Mon Sep 17 00:00:00 2001 From: Bruno Quaresma Date: Wed, 22 Mar 2023 16:40:41 +0000 Subject: [PATCH 12/17] Fix title --- .../TemplateSchedulePage/TemplateSchedulePageView.tsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateSchedulePageView.tsx b/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateSchedulePageView.tsx index 393245009b0c4..cef33c37e275e 100644 --- a/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateSchedulePageView.tsx +++ b/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateSchedulePageView.tsx @@ -1,7 +1,6 @@ import { Template, UpdateTemplateMeta } from "api/typesGenerated" import { ComponentProps, FC } from "react" import { TemplateScheduleForm } from "./TemplateScheduleForm" -import { useTranslation } from "react-i18next" import { PageHeader, PageHeaderTitle } from "components/PageHeader/PageHeader" import { makeStyles } from "@material-ui/core/styles" @@ -24,13 +23,12 @@ export const TemplateSchedulePageView: FC = ({ submitError, initialTouched, }) => { - const { t } = useTranslation("templateSettingsPage") const styles = useStyles() return ( <> - {t("title")} + Template schedule Date: Wed, 22 Mar 2023 17:17:52 +0000 Subject: [PATCH 13/17] Fix test for schedule --- .../TemplateSchedulePage.test.tsx | 127 +++++------------- site/src/testHelpers/entities.ts | 17 ++- site/src/testHelpers/renderHelpers.tsx | 45 +++++++ .../entitlements/entitlementsXService.ts | 2 +- 4 files changed, 93 insertions(+), 98 deletions(-) diff --git a/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateSchedulePage.test.tsx b/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateSchedulePage.test.tsx index 9ebca80b3f6ab..b902002a159da 100644 --- a/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateSchedulePage.test.tsx +++ b/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateSchedulePage.test.tsx @@ -3,8 +3,14 @@ import userEvent from "@testing-library/user-event" import * as API from "api/api" import { UpdateTemplateMeta } from "api/typesGenerated" import { Language as FooterFormLanguage } from "components/FormFooter/FormFooter" -import { MockTemplate } from "../../../testHelpers/entities" -import { renderWithAuth } from "../../../testHelpers/renderHelpers" +import { + MockEntitlementsWithScheduling, + MockTemplate, +} from "../../../testHelpers/entities" +import { + renderWithTemplateSettingsLayout, + waitForLoaderToBeRemoved, +} from "../../../testHelpers/renderHelpers" import { getValidationSchema } from "./TemplateScheduleForm" import TemplateSchedulePage from "./TemplateSchedulePage" import i18next from "i18next" @@ -12,102 +18,56 @@ import i18next from "i18next" const { t } = i18next const validFormValues = { - name: "Name", - display_name: "A display name", - description: "A description", - icon: "vscode.png", - // these are the form values which are actually hours default_ttl_ms: 1, max_ttl_ms: 2, - allow_user_cancel_workspace_jobs: false, } const renderTemplateSchedulePage = async () => { - renderWithAuth(, { - route: `/templates/${MockTemplate.name}/settings`, - path: `/templates/:template/settings`, - extraRoutes: [{ path: "templates/:template", element: <> }], + renderWithTemplateSettingsLayout(, { + route: `/templates/${MockTemplate.name}/settings/schedule`, + path: `/templates/:template/settings/schedule`, }) - // Wait the form to be rendered - const label = t("nameLabel", { ns: "TemplateSchedulePage" }) - await screen.findAllByLabelText(label) + await waitForLoaderToBeRemoved() } const fillAndSubmitForm = async ({ - name, - display_name, - description, default_ttl_ms, max_ttl_ms, - icon, - allow_user_cancel_workspace_jobs, -}: Required) => { - const label = t("nameLabel", { ns: "TemplateSchedulePage" }) - const nameField = await screen.findByLabelText(label) - await userEvent.clear(nameField) - await userEvent.type(nameField, name) - - const displayNameLabel = t("displayNameLabel", { ns: "TemplateSchedulePage" }) - - const displayNameField = await screen.findByLabelText(displayNameLabel) - await userEvent.clear(displayNameField) - await userEvent.type(displayNameField, display_name) - - const descriptionLabel = t("descriptionLabel", { ns: "TemplateSchedulePage" }) - const descriptionField = await screen.findByLabelText(descriptionLabel) - await userEvent.clear(descriptionField) - await userEvent.type(descriptionField, description) - - const iconLabel = t("iconLabel", { ns: "TemplateSchedulePage" }) - const iconField = await screen.findByLabelText(iconLabel) - await userEvent.clear(iconField) - await userEvent.type(iconField, icon) - - const defaultTtlLabel = t("defaultTtlLabel", { ns: "TemplateSchedulePage" }) +}: { + default_ttl_ms: number + max_ttl_ms: number +}) => { + const user = userEvent.setup() + const defaultTtlLabel = t("defaultTtlLabel", { ns: "templateSettingsPage" }) const defaultTtlField = await screen.findByLabelText(defaultTtlLabel) - await userEvent.clear(defaultTtlField) - await userEvent.type(defaultTtlField, default_ttl_ms.toString()) - - const entitlements = await API.getEntitlements() - if (entitlements.features["advanced_template_scheduling"].enabled) { - const maxTtlLabel = t("maxTtlLabel", { ns: "TemplateSchedulePage" }) - const maxTtlField = await screen.findByLabelText(maxTtlLabel) - await userEvent.clear(maxTtlField) - await userEvent.type(maxTtlField, max_ttl_ms.toString()) - } + await user.clear(defaultTtlField) + await user.type(defaultTtlField, default_ttl_ms.toString()) - const allowCancelJobsField = screen.getByRole("checkbox") - // checkbox is checked by default, so it must be clicked to get unchecked - if (!allow_user_cancel_workspace_jobs) { - await userEvent.click(allowCancelJobsField) - } + const maxTtlLabel = t("maxTtlLabel", { ns: "templateSettingsPage" }) + const maxTtlField = await screen.findByLabelText(maxTtlLabel) + await user.clear(maxTtlField) + await user.type(maxTtlField, max_ttl_ms.toString()) const submitButton = await screen.findByText( FooterFormLanguage.defaultSubmitLabel, ) - await userEvent.click(submitButton) + await user.click(submitButton) } describe("TemplateSchedulePage", () => { - it("renders", async () => { - const { t } = i18next - const pageTitle = t("title", { - ns: "TemplateSchedulePage", - }) - await renderTemplateSchedulePage() - const element = await screen.findByText(pageTitle) - expect(element).toBeDefined() + beforeEach(() => { + jest + .spyOn(API, "getEntitlements") + .mockResolvedValue(MockEntitlementsWithScheduling) }) it("succeeds", async () => { await renderTemplateSchedulePage() - jest.spyOn(API, "updateTemplateMeta").mockResolvedValueOnce({ ...MockTemplate, ...validFormValues, }) await fillAndSubmitForm(validFormValues) - await waitFor(() => expect(API.updateTemplateMeta).toBeCalledTimes(1)) }) @@ -125,11 +85,8 @@ describe("TemplateSchedulePage", () => { expect(API.updateTemplateMeta).toBeCalledWith( "test-template", expect.objectContaining({ - ...validFormValues, - // convert from the display value (hours) to ms default_ttl_ms: validFormValues.default_ttl_ms * 3600000, - // this value is undefined if not entitled - max_ttl_ms: undefined, + max_ttl_ms: validFormValues.max_ttl_ms * 3600000, }), ), ) @@ -160,29 +117,7 @@ describe("TemplateSchedulePage", () => { } const validate = () => getValidationSchema().validateSync(values) expect(validate).toThrowError( - t("defaultTTLMaxError", { ns: "TemplateSchedulePage" }), - ) - }) - - it("allows a description of 128 chars", () => { - const values: UpdateTemplateMeta = { - ...validFormValues, - description: - "Nam quis nulla. Integer malesuada. In in enim a arcu imperdiet malesuada. Sed vel lectus. Donec odio urna, tempus molestie, port", - } - const validate = () => getValidationSchema().validateSync(values) - expect(validate).not.toThrowError() - }) - - it("disallows a description of 128 + 1 chars", () => { - const values: UpdateTemplateMeta = { - ...validFormValues, - description: - "Nam quis nulla. Integer malesuada. In in enim a arcu imperdiet malesuada. Sed vel lectus. Donec odio urna, tempus molestie, port a", - } - const validate = () => getValidationSchema().validateSync(values) - expect(validate).toThrowError( - t("descriptionMaxError", { ns: "TemplateSchedulePage" }), + t("defaultTTLMaxError", { ns: "templateSettingsPage" }), ) }) }) diff --git a/site/src/testHelpers/entities.ts b/site/src/testHelpers/entities.ts index 478e841ddd29b..d462a33bd102d 100644 --- a/site/src/testHelpers/entities.ts +++ b/site/src/testHelpers/entities.ts @@ -657,9 +657,10 @@ export const MockWorkspace: TypesGen.Workspace = { owner_id: MockUser.id, owner_name: MockUser.username, autostart_schedule: MockWorkspaceAutostartEnabled.schedule, - ttl_ms: 2 * 60 * 60 * 1000, // 2 hours as milliseconds + ttl_ms: 2 * 60 * 60 * 1000, latest_build: MockWorkspaceBuild, last_used_at: "", + organization_id: MockOrganization.id, } export const MockStoppedWorkspace: TypesGen.Workspace = { @@ -1270,6 +1271,20 @@ export const MockEntitlementsWithAuditLog: TypesGen.Entitlements = { }), } +export const MockEntitlementsWithScheduling: TypesGen.Entitlements = { + errors: [], + warnings: [], + has_license: true, + require_telemetry: false, + trial: false, + features: withDefaultFeatures({ + advanced_template_scheduling: { + enabled: true, + entitlement: "entitled", + }, + }), +} + export const MockExperiments: TypesGen.Experiment[] = [] export const MockAuditLog: TypesGen.AuditLog = { diff --git a/site/src/testHelpers/renderHelpers.tsx b/site/src/testHelpers/renderHelpers.tsx index 033fe0f7bc7ff..52995e3f3cbd9 100644 --- a/site/src/testHelpers/renderHelpers.tsx +++ b/site/src/testHelpers/renderHelpers.tsx @@ -8,6 +8,7 @@ import { AppProviders } from "app" import { DashboardLayout } from "components/Dashboard/DashboardLayout" import { createMemoryHistory } from "history" import { i18n } from "i18n" +import { TemplateSettingsLayout } from "pages/TemplateSettingsPage/TemplateSettingsLayout" import { FC, ReactElement } from "react" import { I18nextProvider } from "react-i18next" import { @@ -86,6 +87,50 @@ export function renderWithAuth( } } +export function renderWithTemplateSettingsLayout( + element: JSX.Element, + { + path = "/", + route = "/", + extraRoutes = [], + nonAuthenticatedRoutes = [], + }: RenderWithAuthOptions = {}, +) { + const routes: RouteObject[] = [ + { + element: , + children: [ + { + element: , + children: [ + { + element: , + children: [{ path, element }, ...extraRoutes], + }, + ], + }, + ], + }, + ...nonAuthenticatedRoutes, + ] + + const router = createMemoryRouter(routes, { initialEntries: [route] }) + + const renderResult = wrappedRender( + + + + + , + ) + + return { + user: MockUser, + router, + ...renderResult, + } +} + export const waitForLoaderToBeRemoved = (): Promise => waitForElementToBeRemoved(() => screen.getByTestId("loader")) diff --git a/site/src/xServices/entitlements/entitlementsXService.ts b/site/src/xServices/entitlements/entitlementsXService.ts index 783bacc6c8358..11fb7f2a3008b 100644 --- a/site/src/xServices/entitlements/entitlementsXService.ts +++ b/site/src/xServices/entitlements/entitlementsXService.ts @@ -58,7 +58,7 @@ export const entitlementsMachine = createMachine( }), }, services: { - getEntitlements: API.getEntitlements, + getEntitlements: () => API.getEntitlements(), }, }, ) From 020d79d88df08bd32c52eacbab3898f7d0a75a5f Mon Sep 17 00:00:00 2001 From: Bruno Quaresma Date: Wed, 22 Mar 2023 17:30:15 +0000 Subject: [PATCH 14/17] Fix tests --- .../components/IconField/LazyIconField.tsx | 2 +- .../TemplateSettingsPage.test.tsx | 99 ++----------------- 2 files changed, 9 insertions(+), 92 deletions(-) diff --git a/site/src/components/IconField/LazyIconField.tsx b/site/src/components/IconField/LazyIconField.tsx index a196ac6cf22b0..b9eafa3b42b4d 100644 --- a/site/src/components/IconField/LazyIconField.tsx +++ b/site/src/components/IconField/LazyIconField.tsx @@ -5,7 +5,7 @@ const IconField = lazy(() => import("./IconField")) export const LazyIconField: FC = (props) => { return ( - + }> ) diff --git a/site/src/pages/TemplateSettingsPage/TemplateGeneralSettingsPage/TemplateSettingsPage.test.tsx b/site/src/pages/TemplateSettingsPage/TemplateGeneralSettingsPage/TemplateSettingsPage.test.tsx index 52b601a7c725e..e9b36e8e6b9cc 100644 --- a/site/src/pages/TemplateSettingsPage/TemplateGeneralSettingsPage/TemplateSettingsPage.test.tsx +++ b/site/src/pages/TemplateSettingsPage/TemplateGeneralSettingsPage/TemplateSettingsPage.test.tsx @@ -3,8 +3,11 @@ import userEvent from "@testing-library/user-event" import * as API from "api/api" import { UpdateTemplateMeta } from "api/typesGenerated" import { Language as FooterFormLanguage } from "components/FormFooter/FormFooter" -import { MockTemplate } from "../../testHelpers/entities" -import { renderWithAuth } from "../../testHelpers/renderHelpers" +import { MockTemplate } from "../../../testHelpers/entities" +import { + renderWithTemplateSettingsLayout, + waitForLoaderToBeRemoved, +} from "../../../testHelpers/renderHelpers" import { getValidationSchema } from "./TemplateSettingsForm" import { TemplateSettingsPage } from "./TemplateSettingsPage" import i18next from "i18next" @@ -16,32 +19,24 @@ const validFormValues = { display_name: "A display name", description: "A description", icon: "vscode.png", - // these are the form values which are actually hours - default_ttl_ms: 1, - max_ttl_ms: 2, allow_user_cancel_workspace_jobs: false, } const renderTemplateSettingsPage = async () => { - renderWithAuth(, { + renderWithTemplateSettingsLayout(, { route: `/templates/${MockTemplate.name}/settings`, path: `/templates/:template/settings`, - extraRoutes: [{ path: "templates/:template", element: <> }], }) - // Wait the form to be rendered - const label = t("nameLabel", { ns: "templateSettingsPage" }) - await screen.findAllByLabelText(label) + await waitForLoaderToBeRemoved() } const fillAndSubmitForm = async ({ name, display_name, description, - default_ttl_ms, - max_ttl_ms, icon, allow_user_cancel_workspace_jobs, -}: Required) => { +}: Required>) => { const label = t("nameLabel", { ns: "templateSettingsPage" }) const nameField = await screen.findByLabelText(label) await userEvent.clear(nameField) @@ -63,19 +58,6 @@ const fillAndSubmitForm = async ({ await userEvent.clear(iconField) await userEvent.type(iconField, icon) - const defaultTtlLabel = t("defaultTtlLabel", { ns: "templateSettingsPage" }) - const defaultTtlField = await screen.findByLabelText(defaultTtlLabel) - await userEvent.clear(defaultTtlField) - await userEvent.type(defaultTtlField, default_ttl_ms.toString()) - - const entitlements = await API.getEntitlements() - if (entitlements.features["advanced_template_scheduling"].enabled) { - const maxTtlLabel = t("maxTtlLabel", { ns: "templateSettingsPage" }) - const maxTtlField = await screen.findByLabelText(maxTtlLabel) - await userEvent.clear(maxTtlField) - await userEvent.type(maxTtlField, max_ttl_ms.toString()) - } - const allowCancelJobsField = screen.getByRole("checkbox") // checkbox is checked by default, so it must be clicked to get unchecked if (!allow_user_cancel_workspace_jobs) { @@ -89,79 +71,14 @@ const fillAndSubmitForm = async ({ } describe("TemplateSettingsPage", () => { - it("renders", async () => { - const { t } = i18next - const pageTitle = t("title", { - ns: "templateSettingsPage", - }) - await renderTemplateSettingsPage() - const element = await screen.findByText(pageTitle) - expect(element).toBeDefined() - }) - it("succeeds", async () => { await renderTemplateSettingsPage() - - jest.spyOn(API, "updateTemplateMeta").mockResolvedValueOnce({ - ...MockTemplate, - ...validFormValues, - }) - await fillAndSubmitForm(validFormValues) - - await waitFor(() => expect(API.updateTemplateMeta).toBeCalledTimes(1)) - }) - - test("ttl is converted to and from hours", async () => { - await renderTemplateSettingsPage() - jest.spyOn(API, "updateTemplateMeta").mockResolvedValueOnce({ ...MockTemplate, ...validFormValues, }) - await fillAndSubmitForm(validFormValues) await waitFor(() => expect(API.updateTemplateMeta).toBeCalledTimes(1)) - await waitFor(() => - expect(API.updateTemplateMeta).toBeCalledWith( - "test-template", - expect.objectContaining({ - ...validFormValues, - // convert from the display value (hours) to ms - default_ttl_ms: validFormValues.default_ttl_ms * 3600000, - // this value is undefined if not entitled - max_ttl_ms: undefined, - }), - ), - ) - }) - - it("allows a ttl of 7 days", () => { - const values: UpdateTemplateMeta = { - ...validFormValues, - default_ttl_ms: 24 * 7, - } - const validate = () => getValidationSchema().validateSync(values) - expect(validate).not.toThrowError() - }) - - it("allows ttl of 0", () => { - const values: UpdateTemplateMeta = { - ...validFormValues, - default_ttl_ms: 0, - } - const validate = () => getValidationSchema().validateSync(values) - expect(validate).not.toThrowError() - }) - - it("disallows a ttl of 7 days + 1 hour", () => { - const values: UpdateTemplateMeta = { - ...validFormValues, - default_ttl_ms: 24 * 7 + 1, - } - const validate = () => getValidationSchema().validateSync(values) - expect(validate).toThrowError( - t("defaultTTLMaxError", { ns: "templateSettingsPage" }), - ) }) it("allows a description of 128 chars", () => { From 9fb5b07614a57bfb2ec6ce1296745f156128e41d Mon Sep 17 00:00:00 2001 From: Bruno Quaresma Date: Wed, 22 Mar 2023 17:47:27 +0000 Subject: [PATCH 15/17] Fix tests --- .../TemplateVariablesPage.test.tsx | 27 ++++++++----------- 1 file changed, 11 insertions(+), 16 deletions(-) diff --git a/site/src/pages/TemplateSettingsPage/TemplateVariablesPage/TemplateVariablesPage.test.tsx b/site/src/pages/TemplateSettingsPage/TemplateVariablesPage/TemplateVariablesPage.test.tsx index 8427efd8fc604..1bc3f1b32998c 100644 --- a/site/src/pages/TemplateSettingsPage/TemplateVariablesPage/TemplateVariablesPage.test.tsx +++ b/site/src/pages/TemplateSettingsPage/TemplateVariablesPage/TemplateVariablesPage.test.tsx @@ -1,4 +1,4 @@ -import { screen, waitFor } from "@testing-library/react" +import { screen } from "@testing-library/react" import userEvent from "@testing-library/user-event" import { MockTemplate, @@ -6,16 +6,14 @@ import { MockTemplateVersion, MockTemplateVersionVariable1, MockTemplateVersionVariable2, - renderWithAuth, MockTemplateVersionVariable5, + renderWithTemplateSettingsLayout, + waitForLoaderToBeRemoved, } from "testHelpers/renderHelpers" import * as API from "api/api" import i18next from "i18next" import TemplateVariablesPage from "./TemplateVariablesPage" import { Language as FooterFormLanguage } from "components/FormFooter/FormFooter" -import * as router from "react-router" - -const navigate = jest.fn() const { t } = i18next @@ -30,12 +28,13 @@ const validationRequiredField = t("validationRequiredVariable", { ns: "templateVariablesPage", }) -const renderTemplateVariablesPage = () => { - return renderWithAuth(, { +const renderTemplateVariablesPage = async () => { + renderWithTemplateSettingsLayout(, { route: `/templates/${MockTemplate.name}/variables`, path: `/templates/:template/variables`, extraRoutes: [{ path: `/templates/${MockTemplate.name}`, element: <> }], }) + await waitForLoaderToBeRemoved() } describe("TemplateVariablesPage", () => { @@ -51,7 +50,7 @@ describe("TemplateVariablesPage", () => { MockTemplateVersionVariable2, ]) - renderTemplateVariablesPage() + await renderTemplateVariablesPage() const element = await screen.findByText(pageTitleText) expect(element).toBeDefined() @@ -84,9 +83,8 @@ describe("TemplateVariablesPage", () => { jest.spyOn(API, "updateActiveTemplateVersion").mockResolvedValueOnce({ message: "done", }) - jest.spyOn(router, "useNavigate").mockImplementation(() => navigate) - renderTemplateVariablesPage() + await renderTemplateVariablesPage() const element = await screen.findByText(pageTitleText) expect(element).toBeDefined() @@ -120,10 +118,8 @@ describe("TemplateVariablesPage", () => { ) await userEvent.click(submitButton) - // Wait for redirect - await waitFor(() => - expect(navigate).toHaveBeenCalledWith(`/templates/${MockTemplate.name}`), - ) + // Wait for the success message + await screen.findByText("Template updated successfully") }) it("user forgets to fill the required field", async () => { @@ -143,9 +139,8 @@ describe("TemplateVariablesPage", () => { jest.spyOn(API, "updateActiveTemplateVersion").mockResolvedValueOnce({ message: "done", }) - jest.spyOn(router, "useNavigate").mockImplementation(() => navigate) - renderTemplateVariablesPage() + await renderTemplateVariablesPage() const element = await screen.findByText(pageTitleText) expect(element).toBeDefined() From 2f44ba0a2c12e3602459ab0c4d0794a01e56377d Mon Sep 17 00:00:00 2001 From: Bruno Quaresma Date: Thu, 23 Mar 2023 10:46:34 +0000 Subject: [PATCH 16/17] Fix stories --- .../TemplateSettingsPageView.stories.tsx | 41 +++++-------------- .../TemplateSettingsPageView.tsx | 3 -- 2 files changed, 11 insertions(+), 33 deletions(-) diff --git a/site/src/pages/TemplateSettingsPage/TemplateGeneralSettingsPage/TemplateSettingsPageView.stories.tsx b/site/src/pages/TemplateSettingsPage/TemplateGeneralSettingsPage/TemplateSettingsPageView.stories.tsx index fb9a315f50846..2d16618c03dfe 100644 --- a/site/src/pages/TemplateSettingsPage/TemplateGeneralSettingsPage/TemplateSettingsPageView.stories.tsx +++ b/site/src/pages/TemplateSettingsPage/TemplateGeneralSettingsPage/TemplateSettingsPageView.stories.tsx @@ -1,7 +1,7 @@ import { action } from "@storybook/addon-actions" import { Story } from "@storybook/react" -import * as Mocks from "../../testHelpers/renderHelpers" -import { makeMockApiError } from "../../testHelpers/renderHelpers" +import * as Mocks from "../../../testHelpers/renderHelpers" +import { makeMockApiError } from "../../../testHelpers/renderHelpers" import { TemplateSettingsPageView, TemplateSettingsPageViewProps, @@ -11,7 +11,6 @@ export default { title: "pages/TemplateSettingsPageView", component: TemplateSettingsPageView, args: { - canSetMaxTTL: true, template: Mocks.MockTemplate, onSubmit: action("onSubmit"), onCancel: action("cancel"), @@ -25,35 +24,17 @@ const Template: Story = (args) => ( export const Example = Template.bind({}) Example.args = {} -export const CantSetMaxTTL = Template.bind({}) -CantSetMaxTTL.args = { - canSetMaxTTL: false, -} - -export const GetTemplateError = Template.bind({}) -GetTemplateError.args = { - template: undefined, - errors: { - getTemplateError: makeMockApiError({ - message: "Failed to fetch the template.", - detail: "You do not have permission to access this resource.", - }), - }, -} - export const SaveTemplateSettingsError = Template.bind({}) SaveTemplateSettingsError.args = { - errors: { - saveTemplateSettingsError: makeMockApiError({ - message: 'Template "test" already exists.', - validations: [ - { - field: "name", - detail: "This value is already in use and should be unique.", - }, - ], - }), - }, + submitError: makeMockApiError({ + message: 'Template "test" already exists.', + validations: [ + { + field: "name", + detail: "This value is already in use and should be unique.", + }, + ], + }), initialTouched: { allow_user_cancel_workspace_jobs: true, }, diff --git a/site/src/pages/TemplateSettingsPage/TemplateGeneralSettingsPage/TemplateSettingsPageView.tsx b/site/src/pages/TemplateSettingsPage/TemplateGeneralSettingsPage/TemplateSettingsPageView.tsx index 4f1d36c8243a5..e35cb6aa568b8 100644 --- a/site/src/pages/TemplateSettingsPage/TemplateGeneralSettingsPage/TemplateSettingsPageView.tsx +++ b/site/src/pages/TemplateSettingsPage/TemplateGeneralSettingsPage/TemplateSettingsPageView.tsx @@ -12,7 +12,6 @@ export interface TemplateSettingsPageViewProps { isSubmitting: boolean submitError?: unknown initialTouched?: ComponentProps["initialTouched"] - canSetMaxTTL: boolean } export const TemplateSettingsPageView: FC = ({ @@ -20,7 +19,6 @@ export const TemplateSettingsPageView: FC = ({ onCancel, onSubmit, isSubmitting, - canSetMaxTTL, submitError, initialTouched, }) => { @@ -34,7 +32,6 @@ export const TemplateSettingsPageView: FC = ({ Date: Thu, 23 Mar 2023 16:31:52 +0000 Subject: [PATCH 17/17] Fix e2e char --- site/e2e/tests/listTemplates.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/site/e2e/tests/listTemplates.spec.ts b/site/e2e/tests/listTemplates.spec.ts index 429ee7ba07db1..4d3569bfb47f3 100644 --- a/site/e2e/tests/listTemplates.spec.ts +++ b/site/e2e/tests/listTemplates.spec.ts @@ -5,5 +5,5 @@ test.use({ storageState: getStatePath("authState") }) test("list templates", async ({ page, baseURL }) => { await page.goto(`${baseURL}/templates`, { waitUntil: "networkidle" }) - await expect(page).toHaveTitle("Templates – Coder") + await expect(page).toHaveTitle("Templates - Coder") })