From 0312c16f5d7475a61abd0b725ead10641c3fb2e2 Mon Sep 17 00:00:00 2001 From: Bruno Quaresma Date: Thu, 16 Feb 2023 16:22:36 +0000 Subject: [PATCH 1/9] Move danger zone --- .../TemplateLayout/TemplateLayout.tsx | 2 +- .../TemplateLayout/TemplatePageHeader.tsx | 109 ++++++++++++++++++ site/src/components/TemplateLayout/delete.ts | 38 ++++++ .../TemplateSettingsPage.tsx | 10 -- .../TemplateSettingsPageView.tsx | 85 +------------- 5 files changed, 151 insertions(+), 93 deletions(-) create mode 100644 site/src/components/TemplateLayout/TemplatePageHeader.tsx create mode 100644 site/src/components/TemplateLayout/delete.ts diff --git a/site/src/components/TemplateLayout/TemplateLayout.tsx b/site/src/components/TemplateLayout/TemplateLayout.tsx index fa02caff789ac..215b2916b7a75 100644 --- a/site/src/components/TemplateLayout/TemplateLayout.tsx +++ b/site/src/components/TemplateLayout/TemplateLayout.tsx @@ -110,7 +110,7 @@ export const TemplateLayout: FC<{ children?: JSX.Element }> = ({ const permissions = usePermissions() const hasIcon = template && template.icon && template.icon !== "" - if (!template) { + if (!template || !templatePermissions) { return } diff --git a/site/src/components/TemplateLayout/TemplatePageHeader.tsx b/site/src/components/TemplateLayout/TemplatePageHeader.tsx new file mode 100644 index 0000000000000..159631ec192ad --- /dev/null +++ b/site/src/components/TemplateLayout/TemplatePageHeader.tsx @@ -0,0 +1,109 @@ +import Button from "@material-ui/core/Button/Button" +import Link from "@material-ui/core/Link/Link" +import RemoveOutlined from "@material-ui/icons/RemoveOutlined" +import AddCircleOutline from "@material-ui/icons/AddCircleOutline" +import SettingsOutlined from "@material-ui/icons/SettingsOutlined" +import { AuthorizationResponse, Template } from "api/typesGenerated" +import { Avatar } from "components/Avatar/Avatar" +import { Maybe } from "components/Conditionals/Maybe" +import { DeleteDialog } from "components/Dialogs/DeleteDialog/DeleteDialog" +import { + PageHeader, + PageHeaderTitle, + PageHeaderSubtitle, +} from "components/PageHeader/PageHeader" +import { Stack } from "components/Stack/Stack" +import { FC } from "react" +import { Link as RouterLink } from "react-router-dom" +import { useDeleteTemplate } from "./delete" + +const TemplateSettingsButton: FC<{ templateName: string }> = ({ + templateName, +}) => ( + + + +) + +const CreateWorkspaceButton: FC<{ + templateName: string + className?: string +}> = ({ templateName, className }) => ( + + + +) + +const DeleteTemplateButton: FC<{ onClick: () => void }> = ({ onClick }) => ( + +) + +export const TemplatePageHeader: FC<{ + template: Template + permissions: AuthorizationResponse +}> = ({ template, permissions }) => { + const hasIcon = template.icon && template.icon !== "" + const deleteTemplate = useDeleteTemplate(template) + + return ( + <> + + + + + + + + } + > + + {hasIcon ? ( + + ) : ( + {template.name} + )} + +
+ + {template.display_name.length > 0 + ? template.display_name + : template.name} + + + {template.description === "" + ? "No description" + : template.description} + +
+
+
+ + + + ) +} diff --git a/site/src/components/TemplateLayout/delete.ts b/site/src/components/TemplateLayout/delete.ts new file mode 100644 index 0000000000000..df81b5655bc8d --- /dev/null +++ b/site/src/components/TemplateLayout/delete.ts @@ -0,0 +1,38 @@ +import { deleteTemplate } from "api/api" +import { Template } from "api/typesGenerated" +import { useState } from "react" +import { useNavigate } from "react-router-dom" + +type DeleteTemplateState = + | { status: "idle" } + | { status: "confirming" } + | { status: "deleting" } + +export const useDeleteTemplate = (template: Template) => { + const navigate = useNavigate() + const [state, setState] = useState({ status: "idle" }) + const isDeleteDialogOpen = + state.status === "confirming" || state.status === "deleting" + + const openDeleteConfirmation = () => { + setState({ status: "confirming" }) + } + + const cancelDeleteConfirmation = () => { + setState({ status: "idle" }) + } + + const confirmDelete = async () => { + setState({ status: "deleting" }) + await deleteTemplate(template.id) + navigate("/templates") + } + + return { + state, + isDeleteDialogOpen, + openDeleteConfirmation, + cancelDeleteConfirmation, + confirmDelete, + } +} diff --git a/site/src/pages/TemplateSettingsPage/TemplateSettingsPage.tsx b/site/src/pages/TemplateSettingsPage/TemplateSettingsPage.tsx index 21816f6a8780b..b00a56ac569d4 100644 --- a/site/src/pages/TemplateSettingsPage/TemplateSettingsPage.tsx +++ b/site/src/pages/TemplateSettingsPage/TemplateSettingsPage.tsx @@ -26,7 +26,6 @@ export const TemplateSettingsPage: FC = () => { templateSettings: template, saveTemplateSettingsError, getTemplateError, - deleteTemplateError, } = state.context return ( @@ -40,7 +39,6 @@ export const TemplateSettingsPage: FC = () => { errors={{ getTemplateError, saveTemplateSettingsError, - deleteTemplateError, }} onCancel={() => { navigate(`/templates/${templateName}`) @@ -48,14 +46,6 @@ export const TemplateSettingsPage: FC = () => { onSubmit={(templateSettings) => { send({ type: "SAVE", templateSettings }) }} - onDelete={() => { - send("DELETE") - }} - onConfirmDelete={() => send("CONFIRM_DELETE")} - onCancelDelete={() => send("CANCEL_DELETE")} - isConfirmingDelete={state.matches("confirmingDelete")} - isDeleting={state.matches("deleting")} - isDeleted={state.matches("deleted")} /> ) diff --git a/site/src/pages/TemplateSettingsPage/TemplateSettingsPageView.tsx b/site/src/pages/TemplateSettingsPage/TemplateSettingsPageView.tsx index eb4baffa5c7af..6c514e1470b9a 100644 --- a/site/src/pages/TemplateSettingsPage/TemplateSettingsPageView.tsx +++ b/site/src/pages/TemplateSettingsPage/TemplateSettingsPageView.tsx @@ -1,32 +1,21 @@ import { Template, UpdateTemplateMeta } from "api/typesGenerated" import { AlertBanner } from "components/AlertBanner/AlertBanner" -import { FullPageForm } from "components/FullPageForm/FullPageForm" import { Loader } from "components/Loader/Loader" import { ComponentProps, FC } from "react" import { TemplateSettingsForm } from "./TemplateSettingsForm" import { Stack } from "components/Stack/Stack" -import { DeleteDialog } from "components/Dialogs/DeleteDialog/DeleteDialog" import { makeStyles } from "@material-ui/core/styles" -import { colors } from "theme/colors" -import Button from "@material-ui/core/Button" import { useTranslation } from "react-i18next" -import { Navigate } from "react-router-dom" +import { FullPageHorizontalForm } from "components/FullPageForm/FullPageHorizontalForm" export interface TemplateSettingsPageViewProps { template?: Template onSubmit: (data: UpdateTemplateMeta) => void onCancel: () => void - onDelete: () => void - onConfirmDelete: () => void - onCancelDelete: () => void - isConfirmingDelete: boolean - isDeleting: boolean - isDeleted: boolean isSubmitting: boolean errors?: { getTemplateError?: unknown saveTemplateSettingsError?: unknown - deleteTemplateError?: unknown } initialTouched?: ComponentProps["initialTouched"] } @@ -35,12 +24,6 @@ export const TemplateSettingsPageView: FC = ({ template, onCancel, onSubmit, - onDelete, - onConfirmDelete, - onCancelDelete, - isConfirmingDelete, - isDeleting, - isDeleted, isSubmitting, errors = {}, initialTouched, @@ -49,22 +32,13 @@ export const TemplateSettingsPageView: FC = ({ const isLoading = !template && !errors.getTemplateError const { t } = useTranslation("templateSettingsPage") - if (isDeleted) { - return - } - return ( - + {Boolean(errors.getTemplateError) && ( )} - {Boolean(errors.deleteTemplateError) && ( - - - - )} {isLoading && } {template && ( <> @@ -76,39 +50,9 @@ export const TemplateSettingsPageView: FC = ({ onCancel={onCancel} error={errors.saveTemplateSettingsError} /> - -
- {t("dangerZone.dangerZoneHeader")} -
- - - -

- {t("dangerZone.deleteTemplateHeader")} -

- {t("dangerZone.deleteTemplateCaption")} -
- -
-
- - )} -
+ ) } @@ -116,27 +60,4 @@ const useStyles = makeStyles((theme) => ({ errorContainer: { marginBottom: theme.spacing(2), }, - dangerContainer: { - marginTop: theme.spacing(4), - }, - dangerHeader: { - fontSize: theme.typography.h5.fontSize, - color: theme.palette.text.secondary, - }, - dangerBorder: { - border: `1px solid ${colors.red[13]}`, - borderRadius: theme.shape.borderRadius, - padding: theme.spacing(2), - - "& p": { - marginTop: "0px", - }, - }, - deleteTemplateHeader: { - fontSize: theme.typography.h6.fontSize, - fontWeight: "bold", - }, - deleteButton: { - color: colors.red[8], - }, })) From bc56fb1632e6d8d33e3c11e5f6225d161192383b Mon Sep 17 00:00:00 2001 From: Bruno Quaresma Date: Thu, 16 Feb 2023 16:31:56 +0000 Subject: [PATCH 2/9] Fix links and buttons --- .../TemplateLayout/TemplateLayout.tsx | 114 +----------------- .../TemplateLayout/TemplatePageHeader.tsx | 53 ++++---- 2 files changed, 34 insertions(+), 133 deletions(-) diff --git a/site/src/components/TemplateLayout/TemplateLayout.tsx b/site/src/components/TemplateLayout/TemplateLayout.tsx index 215b2916b7a75..e04db9ed1dd17 100644 --- a/site/src/components/TemplateLayout/TemplateLayout.tsx +++ b/site/src/components/TemplateLayout/TemplateLayout.tsx @@ -1,22 +1,8 @@ -import Button from "@material-ui/core/Button" -import Link from "@material-ui/core/Link" import { makeStyles } from "@material-ui/core/styles" -import AddCircleOutline from "@material-ui/icons/AddCircleOutline" -import SettingsOutlined from "@material-ui/icons/SettingsOutlined" import { useMachine } from "@xstate/react" -import { - PageHeader, - PageHeaderSubtitle, - PageHeaderTitle, -} from "components/PageHeader/PageHeader" import { useOrganizationId } from "hooks/useOrganizationId" import { createContext, FC, Suspense, useContext } from "react" -import { - Link as RouterLink, - NavLink, - Outlet, - useParams, -} from "react-router-dom" +import { NavLink, Outlet, useParams } from "react-router-dom" import { combineClasses } from "util/combineClasses" import { TemplateContext, @@ -27,14 +13,7 @@ import { Stack } from "components/Stack/Stack" import { Permissions } from "xServices/auth/authXService" import { Loader } from "components/Loader/Loader" import { usePermissions } from "hooks/usePermissions" -import { Avatar } from "components/Avatar/Avatar" - -const Language = { - settingsButton: "Settings", - editButton: "Edit", - createButton: "Create workspace", - noDescription: "", -} +import { TemplatePageHeader } from "./TemplatePageHeader" const useTemplateName = () => { const { template } = useParams() @@ -65,35 +44,6 @@ export const useTemplateLayoutContext = (): TemplateLayoutContextValue => { return context } -const TemplateSettingsButton: FC<{ templateName: string }> = ({ - templateName, -}) => ( - - - -) - -const CreateWorkspaceButton: FC<{ - templateName: string - className?: string -}> = ({ templateName, className }) => ( - - - -) - export const TemplateLayout: FC<{ children?: JSX.Element }> = ({ children = , }) => { @@ -108,58 +58,17 @@ export const TemplateLayout: FC<{ children?: JSX.Element }> = ({ }) const { template, permissions: templatePermissions } = templateState.context const permissions = usePermissions() - const hasIcon = template && template.icon && template.icon !== "" if (!template || !templatePermissions) { return } - const generatePageHeaderActions = (): JSX.Element[] => { - const pageActions: JSX.Element[] = [] - - if (templatePermissions?.canUpdateTemplate) { - pageActions.push() - } - - pageActions.push() - - return pageActions - } - return ( <> - - - {generatePageHeaderActions().map((action, i) => ( -
{action}
- ))} - - } - > - - {hasIcon ? ( - - ) : ( - {template.name} - )} - -
- - {template.display_name.length > 0 - ? template.display_name - : template.name} - - - {template.description === "" - ? Language.noDescription - : template.description} - -
-
-
-
+
@@ -204,17 +113,6 @@ export const TemplateLayout: FC<{ children?: JSX.Element }> = ({ export const useStyles = makeStyles((theme) => { return { - pageTitle: { - alignItems: "center", - }, - iconWrapper: { - width: theme.spacing(6), - height: theme.spacing(6), - "& img": { - width: "100%", - }, - }, - tabs: { borderBottom: `1px solid ${theme.palette.divider}`, marginBottom: theme.spacing(5), diff --git a/site/src/components/TemplateLayout/TemplatePageHeader.tsx b/site/src/components/TemplateLayout/TemplatePageHeader.tsx index 159631ec192ad..6d85c0c19bf9d 100644 --- a/site/src/components/TemplateLayout/TemplatePageHeader.tsx +++ b/site/src/components/TemplateLayout/TemplatePageHeader.tsx @@ -1,6 +1,5 @@ -import Button from "@material-ui/core/Button/Button" -import Link from "@material-ui/core/Link/Link" -import RemoveOutlined from "@material-ui/icons/RemoveOutlined" +import Button from "@material-ui/core/Button" +import DeleteOutlined from "@material-ui/icons/DeleteOutlined" import AddCircleOutline from "@material-ui/icons/AddCircleOutline" import SettingsOutlined from "@material-ui/icons/SettingsOutlined" import { AuthorizationResponse, Template } from "api/typesGenerated" @@ -16,39 +15,43 @@ import { Stack } from "components/Stack/Stack" import { FC } from "react" import { Link as RouterLink } from "react-router-dom" import { useDeleteTemplate } from "./delete" +import { Margins } from "components/Margins/Margins" + +const Language = { + editButton: "Edit", + settingsButton: "Settings", + createButton: "Create workspace", + deleteButton: "Delete", +} const TemplateSettingsButton: FC<{ templateName: string }> = ({ templateName, }) => ( - } > - - + {Language.settingsButton} + ) const CreateWorkspaceButton: FC<{ templateName: string className?: string -}> = ({ templateName, className }) => ( - = ({ templateName }) => ( + - + {Language.createButton} + ) const DeleteTemplateButton: FC<{ onClick: () => void }> = ({ onClick }) => ( - ) @@ -60,7 +63,7 @@ export const TemplatePageHeader: FC<{ const deleteTemplate = useDeleteTemplate(template) return ( - <> + @@ -87,11 +90,11 @@ export const TemplatePageHeader: FC<{ ? template.display_name : template.name} - - {template.description === "" - ? "No description" - : template.description} - + {template.description !== "" && ( + + {template.description} + + )}
@@ -104,6 +107,6 @@ export const TemplatePageHeader: FC<{ entity="template" name={template.name} /> - + ) } From a92282da106138770498e11c4fec1d56ee7d8c2b Mon Sep 17 00:00:00 2001 From: Bruno Quaresma Date: Thu, 16 Feb 2023 16:56:50 +0000 Subject: [PATCH 3/9] refactor(site): Refactor template settings --- .../HorizontalForm/HorizontalForm.tsx | 129 ++++++++++++++++++ site/src/i18n/en/templateForm.json | 41 ++++++ site/src/i18n/en/templateSettingsPage.json | 16 ++- .../TemplateSettingsForm.tsx | 128 ++++++++--------- 4 files changed, 245 insertions(+), 69 deletions(-) create mode 100644 site/src/components/HorizontalForm/HorizontalForm.tsx create mode 100644 site/src/i18n/en/templateForm.json diff --git a/site/src/components/HorizontalForm/HorizontalForm.tsx b/site/src/components/HorizontalForm/HorizontalForm.tsx new file mode 100644 index 0000000000000..42dd9e00fa369 --- /dev/null +++ b/site/src/components/HorizontalForm/HorizontalForm.tsx @@ -0,0 +1,129 @@ +import { makeStyles } from "@material-ui/core/styles" +import { + FormFooterProps as BaseFormFooterProps, + FormFooter as BaseFormFooter, +} from "components/FormFooter/FormFooter" +import { Stack } from "components/Stack/Stack" +import { FC, HTMLProps, PropsWithChildren } from "react" + +export const HorizontalForm: FC< + PropsWithChildren & HTMLProps +> = ({ children, ...formProps }) => { + const styles = useStyles() + + return ( +
+ + {children} + +
+ ) +} + +export const FormSection: FC< + PropsWithChildren & { title: string; description: string | JSX.Element } +> = ({ children, title, description }) => { + const styles = useStyles() + + return ( +
+
+

{title}

+
{description}
+
+ + {children} +
+ ) +} + +export const FormFields: FC = ({ children }) => { + const styles = useStyles() + return ( + + {children} + + ) +} + +export const FormFooter: FC = (props) => { + const formFooterStyles = useFormFooterStyles() + return ( + + ) +} + +const useStyles = makeStyles((theme) => ({ + formSections: { + [theme.breakpoints.down("sm")]: { + gap: theme.spacing(8), + }, + }, + + formSection: { + display: "flex", + alignItems: "flex-start", + gap: theme.spacing(15), + + [theme.breakpoints.down("sm")]: { + flexDirection: "column", + gap: theme.spacing(2), + }, + }, + + formSectionInfo: { + width: 312, + flexShrink: 0, + position: "sticky", + top: theme.spacing(3), + + [theme.breakpoints.down("sm")]: { + width: "100%", + position: "initial", + }, + }, + + formSectionInfoTitle: { + fontSize: 20, + color: theme.palette.text.primary, + fontWeight: 400, + margin: 0, + marginBottom: theme.spacing(1), + }, + + formSectionInfoDescription: { + fontSize: 14, + color: theme.palette.text.secondary, + lineHeight: "160%", + margin: 0, + }, + + formSectionFields: { + width: "100%", + }, +})) + +const useFormFooterStyles = makeStyles((theme) => ({ + button: { + minWidth: theme.spacing(23), + + [theme.breakpoints.down("sm")]: { + width: "100%", + }, + }, + footer: { + display: "flex", + alignItems: "center", + justifyContent: "flex-start", + flexDirection: "row-reverse", + gap: theme.spacing(2), + + [theme.breakpoints.down("sm")]: { + flexDirection: "column", + gap: theme.spacing(1), + }, + }, +})) diff --git a/site/src/i18n/en/templateForm.json b/site/src/i18n/en/templateForm.json new file mode 100644 index 0000000000000..4787b89133a94 --- /dev/null +++ b/site/src/i18n/en/templateForm.json @@ -0,0 +1,41 @@ +{ + "generalInfo": { + "title": "General info", + "description": "The name is used to identify the template in URLs and the API. It must be unique within your organization." + }, + "displayInfo": { + "title": "Display info", + "description": "Give your template a friendly name, description, and icon." + }, + "schedule": { + "title": "Schedule", + "description": "Define when workspaces created from this template automatically stop." + }, + "operations": { + "title": "Operations", + "description": "Regulate actions allowed on workspaces created from this template." + }, + "parameters": { + "title": "Template params", + "description": "These params are provided by your template's Terraform configuration." + }, + "fields": { + "name": "Name", + "displayName": "Display name", + "description": "Description", + "icon": "Icon", + "autoStop": "Auto-stop default", + "allowUsersToCancel": "Allow users to cancel in-progress workspace jobs" + }, + "helperText": { + "autoStop": "Time in hours", + "allowUsersToCancel": "If checked, users may be able to corrupt their workspace." + }, + "upload": { + "removeTitle": "Remove file", + "title": "Upload template" + }, + "tooltip": { + "allowUsersToCancel": "Depending on your template, canceling builds may leave workspaces in an unhealthy state. This option isn't recommended for most use cases." + } +} diff --git a/site/src/i18n/en/templateSettingsPage.json b/site/src/i18n/en/templateSettingsPage.json index 5d3c61b3b0356..9ab10fbe9f372 100644 --- a/site/src/i18n/en/templateSettingsPage.json +++ b/site/src/i18n/en/templateSettingsPage.json @@ -15,10 +15,16 @@ "ttlHelperText_other": "Workspaces created from this template will default to stopping after {{count}} hours.", "allowUserCancelWorkspaceJobsLabel": "Allow users to cancel in-progress workspace jobs.", "allowUserCancelWorkspaceJobsNotice": "Depending on your template, canceling builds may leave workspaces in an unhealthy state. This option isn't recommended for most use cases.", - "dangerZone": { - "dangerZoneHeader": "Danger Zone", - "deleteTemplateHeader": "Delete this template", - "deleteTemplateCaption": "Do you want to permanently delete this template?", - "deleteCta": "Delete Template" + "generalInfo": { + "title": "General info", + "description": "The name is used to identify the template in URLs and the API. It must be unique within your organization." + }, + "displayInfo": { + "title": "Display info", + "description": "Give your template a friendly name, description, and icon." + }, + "schedule": { + "title": "Schedule", + "description": "Define when workspaces created from this template automatically stop." } } diff --git a/site/src/pages/TemplateSettingsPage/TemplateSettingsForm.tsx b/site/src/pages/TemplateSettingsPage/TemplateSettingsForm.tsx index d77aefedc56b1..109290d822b23 100644 --- a/site/src/pages/TemplateSettingsPage/TemplateSettingsForm.tsx +++ b/site/src/pages/TemplateSettingsPage/TemplateSettingsForm.tsx @@ -1,10 +1,5 @@ -import Box from "@material-ui/core/Box" -import Checkbox from "@material-ui/core/Checkbox" -import Typography from "@material-ui/core/Typography" import TextField from "@material-ui/core/TextField" import { Template, UpdateTemplateMeta } from "api/typesGenerated" -import { FormFooter } from "components/FormFooter/FormFooter" -import { Stack } from "components/Stack/Stack" import { FormikContextType, FormikTouched, useFormik } from "formik" import { FC } from "react" import { @@ -18,6 +13,12 @@ import i18next from "i18next" import { useTranslation } from "react-i18next" import { Maybe } from "components/Conditionals/Maybe" import { LazyIconField } from "components/IconField/LazyIconField" +import { + FormFields, + FormSection, + HorizontalForm, + FormFooter, +} from "components/HorizontalForm/HorizontalForm" const TTLHelperText = ({ ttl }: { ttl?: number }) => { const { t } = useTranslation("templateSettingsPage") @@ -103,46 +104,66 @@ export const TemplateSettingsForm: FC = ({ const { t } = useTranslation("templateSettingsPage") return ( -
- - + + + + + + - + + + - + - form.setFieldValue("icon", value)} - /> + form.setFieldValue("icon", value)} + /> + + + = ({ variant="outlined" type="number" /> - - -
- {/*"getFieldHelpers" can't be used as it requires "helperText" property to be present.*/} - -
- - - {t("allowUserCancelWorkspaceJobsLabel")} - - - {t("allowUserCancelWorkspaceJobsNotice")} - - -
-
+ - + ) } From 17598fce11549ad6ee015751356bc70002896236 Mon Sep 17 00:00:00 2001 From: Bruno Quaresma Date: Thu, 16 Feb 2023 14:24:25 -0300 Subject: [PATCH 4/9] Update site/src/i18n/en/templateForm.json Co-authored-by: Ben Potter --- site/src/i18n/en/templateForm.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/site/src/i18n/en/templateForm.json b/site/src/i18n/en/templateForm.json index 4787b89133a94..6f25facb1893c 100644 --- a/site/src/i18n/en/templateForm.json +++ b/site/src/i18n/en/templateForm.json @@ -16,8 +16,8 @@ "description": "Regulate actions allowed on workspaces created from this template." }, "parameters": { - "title": "Template params", - "description": "These params are provided by your template's Terraform configuration." + "title": "Template variables", + "description": "These variables are provided by your template's Terraform configuration." }, "fields": { "name": "Name", From 9caa4c0b7d07b4cafd1dc4087c8d763ad739693b Mon Sep 17 00:00:00 2001 From: Bruno Quaresma Date: Thu, 16 Feb 2023 14:24:30 -0300 Subject: [PATCH 5/9] Update site/src/i18n/en/templateForm.json Co-authored-by: Ben Potter --- site/src/i18n/en/templateForm.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/site/src/i18n/en/templateForm.json b/site/src/i18n/en/templateForm.json index 6f25facb1893c..2ee747df36409 100644 --- a/site/src/i18n/en/templateForm.json +++ b/site/src/i18n/en/templateForm.json @@ -1,7 +1,7 @@ { "generalInfo": { "title": "General info", - "description": "The name is used to identify the template in URLs and the API. It must be unique within your organization." + "description": "The name is used to identify the template in URLs and the API. It must be unique." }, "displayInfo": { "title": "Display info", From 48bf1314098479d4371e835894f7e1a43382f1e3 Mon Sep 17 00:00:00 2001 From: Bruno Quaresma Date: Thu, 16 Feb 2023 18:35:07 +0000 Subject: [PATCH 6/9] Add back allow cancel jobs --- site/src/i18n/en/templateSettingsPage.json | 5 ++ .../TemplateSettingsForm.tsx | 75 ++++++++++++++++++- 2 files changed, 79 insertions(+), 1 deletion(-) diff --git a/site/src/i18n/en/templateSettingsPage.json b/site/src/i18n/en/templateSettingsPage.json index 9ab10fbe9f372..f738567b50756 100644 --- a/site/src/i18n/en/templateSettingsPage.json +++ b/site/src/i18n/en/templateSettingsPage.json @@ -15,6 +15,7 @@ "ttlHelperText_other": "Workspaces created from this template will default to stopping after {{count}} hours.", "allowUserCancelWorkspaceJobsLabel": "Allow users to cancel in-progress workspace jobs.", "allowUserCancelWorkspaceJobsNotice": "Depending on your template, canceling builds may leave workspaces in an unhealthy state. This option isn't recommended for most use cases.", + "allowUsersCancelHelperText": "If checked, users may be able to corrupt their workspace.", "generalInfo": { "title": "General info", "description": "The name is used to identify the template in URLs and the API. It must be unique within your organization." @@ -26,5 +27,9 @@ "schedule": { "title": "Schedule", "description": "Define when workspaces created from this template automatically stop." + }, + "operations": { + "title": "Operations", + "description": "Regulate actions allowed on workspaces created from this template." } } diff --git a/site/src/pages/TemplateSettingsPage/TemplateSettingsForm.tsx b/site/src/pages/TemplateSettingsPage/TemplateSettingsForm.tsx index 109290d822b23..e63b8a085dabc 100644 --- a/site/src/pages/TemplateSettingsPage/TemplateSettingsForm.tsx +++ b/site/src/pages/TemplateSettingsPage/TemplateSettingsForm.tsx @@ -19,6 +19,10 @@ import { HorizontalForm, FormFooter, } from "components/HorizontalForm/HorizontalForm" +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" const TTLHelperText = ({ ttl }: { ttl?: number }) => { const { t } = useTranslation("templateSettingsPage") @@ -102,6 +106,7 @@ export const TemplateSettingsForm: FC = ({ }) const getFieldHelpers = getFormHelpers(form, error) const { t } = useTranslation("templateSettingsPage") + const styles = useStyles() return ( = ({ disabled={isSubmitting} onChange={onChangeTrimmed(form)} fullWidth - label={t("form.fields.icon")} + label={t("iconLabel")} variant="outlined" onPickEmoji={(value) => form.setFieldValue("icon", value)} /> @@ -178,7 +183,75 @@ export const TemplateSettingsForm: FC = ({ /> + + , + )} + disabled={isSubmitting} + fullWidth + inputProps={{ min: 0, step: 1 }} + label={t("defaultTtlLabel")} + variant="outlined" + type="number" + /> + + + + + + ) } + +const useStyles = makeStyles((theme) => ({ + optionText: { + fontSize: theme.spacing(2), + color: theme.palette.text.primary, + }, + + optionHelperText: { + fontSize: theme.spacing(1.5), + color: theme.palette.text.secondary, + }, +})) From 8b6bfcfa31e2f2d3bcf0df77cab13e62817dac2f Mon Sep 17 00:00:00 2001 From: Bruno Quaresma Date: Fri, 17 Feb 2023 12:35:07 +0000 Subject: [PATCH 7/9] Add a few more tests --- .../TemplateLayout/TemplateLayout.tsx | 6 ++- .../TemplatePageHeader.stories.tsx | 31 ++++++++++++ .../TemplateLayout/TemplatePageHeader.tsx | 15 ++++-- .../TemplateLayout/deleteTemplate.test.ts | 48 +++++++++++++++++++ .../{delete.ts => deleteTemplate.ts} | 6 +-- .../TemplateSettingsPage.test.tsx | 10 ---- 6 files changed, 97 insertions(+), 19 deletions(-) create mode 100644 site/src/components/TemplateLayout/TemplatePageHeader.stories.tsx create mode 100644 site/src/components/TemplateLayout/deleteTemplate.test.ts rename site/src/components/TemplateLayout/{delete.ts => deleteTemplate.ts} (83%) diff --git a/site/src/components/TemplateLayout/TemplateLayout.tsx b/site/src/components/TemplateLayout/TemplateLayout.tsx index e04db9ed1dd17..4716698baa392 100644 --- a/site/src/components/TemplateLayout/TemplateLayout.tsx +++ b/site/src/components/TemplateLayout/TemplateLayout.tsx @@ -2,7 +2,7 @@ import { makeStyles } from "@material-ui/core/styles" import { useMachine } from "@xstate/react" import { useOrganizationId } from "hooks/useOrganizationId" import { createContext, FC, Suspense, useContext } from "react" -import { NavLink, Outlet, useParams } from "react-router-dom" +import { NavLink, Outlet, useNavigate, useParams } from "react-router-dom" import { combineClasses } from "util/combineClasses" import { TemplateContext, @@ -47,6 +47,7 @@ export const useTemplateLayoutContext = (): TemplateLayoutContextValue => { export const TemplateLayout: FC<{ children?: JSX.Element }> = ({ children = , }) => { + const navigate = useNavigate() const styles = useStyles() const organizationId = useOrganizationId() const templateName = useTemplateName() @@ -68,6 +69,9 @@ export const TemplateLayout: FC<{ children?: JSX.Element }> = ({ { + navigate("/templates") + }} />
diff --git a/site/src/components/TemplateLayout/TemplatePageHeader.stories.tsx b/site/src/components/TemplateLayout/TemplatePageHeader.stories.tsx new file mode 100644 index 0000000000000..e8f0032d8973c --- /dev/null +++ b/site/src/components/TemplateLayout/TemplatePageHeader.stories.tsx @@ -0,0 +1,31 @@ +import { ComponentMeta, Story } from "@storybook/react" +import { MockTemplate } from "testHelpers/entities" +import { + TemplatePageHeader, + TemplatePageHeaderProps, +} from "./TemplatePageHeader" + +export default { + title: "Components/TemplatePageHeader", + component: TemplatePageHeader, + argTypes: { + template: { + defaultValue: MockTemplate, + }, + permissions: { + defaultValue: { + canUpdateTemplate: true, + }, + }, + }, +} as ComponentMeta + +const Template: Story = (args) => ( + +) + +export const CanUpdate = Template.bind({}) +CanUpdate.args = {} + +export const CanNotUpdate = Template.bind({}) +CanNotUpdate.args = {} diff --git a/site/src/components/TemplateLayout/TemplatePageHeader.tsx b/site/src/components/TemplateLayout/TemplatePageHeader.tsx index 6d85c0c19bf9d..3ddafafb211a7 100644 --- a/site/src/components/TemplateLayout/TemplatePageHeader.tsx +++ b/site/src/components/TemplateLayout/TemplatePageHeader.tsx @@ -14,7 +14,7 @@ import { import { Stack } from "components/Stack/Stack" import { FC } from "react" import { Link as RouterLink } from "react-router-dom" -import { useDeleteTemplate } from "./delete" +import { useDeleteTemplate } from "./deleteTemplate" import { Margins } from "components/Margins/Margins" const Language = { @@ -55,12 +55,19 @@ const DeleteTemplateButton: FC<{ onClick: () => void }> = ({ onClick }) => ( ) -export const TemplatePageHeader: FC<{ +export type TemplatePageHeaderProps = { template: Template permissions: AuthorizationResponse -}> = ({ template, permissions }) => { + onDeleteTemplate: () => void +} + +export const TemplatePageHeader: FC = ({ + template, + permissions, + onDeleteTemplate, +}) => { const hasIcon = template.icon && template.icon !== "" - const deleteTemplate = useDeleteTemplate(template) + const deleteTemplate = useDeleteTemplate(template, onDeleteTemplate) return ( diff --git a/site/src/components/TemplateLayout/deleteTemplate.test.ts b/site/src/components/TemplateLayout/deleteTemplate.test.ts new file mode 100644 index 0000000000000..c573db4951818 --- /dev/null +++ b/site/src/components/TemplateLayout/deleteTemplate.test.ts @@ -0,0 +1,48 @@ +import { act, renderHook, waitFor } from "@testing-library/react" +import { MockTemplate } from "testHelpers/entities" +import { useDeleteTemplate } from "./deleteTemplate" +import * as API from "api/api" + +test("delete dialog starts closed", () => { + const { result } = renderHook(() => + useDeleteTemplate(MockTemplate, jest.fn()), + ) + expect(result.current.isDeleteDialogOpen).toBeFalsy() +}) + +test("confirm template deletion", async () => { + const onDeleteTemplate = jest.fn() + const { result } = renderHook(() => + useDeleteTemplate(MockTemplate, onDeleteTemplate), + ) + + //Open delete confirmation + act(() => { + result.current.openDeleteConfirmation() + }) + expect(result.current.isDeleteDialogOpen).toBeTruthy() + + // Confirm delete + jest.spyOn(API, "deleteTemplate") + await act(async () => result.current.confirmDelete()) + await waitFor(() => expect(API.deleteTemplate).toBeCalledTimes(1)) + expect(onDeleteTemplate).toBeCalledTimes(1) +}) + +test("cancel template deletion", () => { + const { result } = renderHook(() => + useDeleteTemplate(MockTemplate, jest.fn()), + ) + + //Open delete confirmation + act(() => { + result.current.openDeleteConfirmation() + }) + expect(result.current.isDeleteDialogOpen).toBeTruthy() + + // Cancel deletion + act(() => { + result.current.cancelDeleteConfirmation() + }) + expect(result.current.isDeleteDialogOpen).toBeFalsy() +}) diff --git a/site/src/components/TemplateLayout/delete.ts b/site/src/components/TemplateLayout/deleteTemplate.ts similarity index 83% rename from site/src/components/TemplateLayout/delete.ts rename to site/src/components/TemplateLayout/deleteTemplate.ts index df81b5655bc8d..b3ef46bedb737 100644 --- a/site/src/components/TemplateLayout/delete.ts +++ b/site/src/components/TemplateLayout/deleteTemplate.ts @@ -1,15 +1,13 @@ import { deleteTemplate } from "api/api" import { Template } from "api/typesGenerated" import { useState } from "react" -import { useNavigate } from "react-router-dom" type DeleteTemplateState = | { status: "idle" } | { status: "confirming" } | { status: "deleting" } -export const useDeleteTemplate = (template: Template) => { - const navigate = useNavigate() +export const useDeleteTemplate = (template: Template, onDelete: () => void) => { const [state, setState] = useState({ status: "idle" }) const isDeleteDialogOpen = state.status === "confirming" || state.status === "deleting" @@ -25,7 +23,7 @@ export const useDeleteTemplate = (template: Template) => { const confirmDelete = async () => { setState({ status: "deleting" }) await deleteTemplate(template.id) - navigate("/templates") + onDelete() } return { diff --git a/site/src/pages/TemplateSettingsPage/TemplateSettingsPage.test.tsx b/site/src/pages/TemplateSettingsPage/TemplateSettingsPage.test.tsx index bab264530a146..94d227e67d4b9 100644 --- a/site/src/pages/TemplateSettingsPage/TemplateSettingsPage.test.tsx +++ b/site/src/pages/TemplateSettingsPage/TemplateSettingsPage.test.tsx @@ -88,16 +88,6 @@ describe("TemplateSettingsPage", () => { expect(element).toBeDefined() }) - it("allows an admin to delete a template", async () => { - const { t } = i18next - await renderTemplateSettingsPage() - const deleteCta = t("dangerZone.deleteCta", { - ns: "templateSettingsPage", - }) - const deleteButton = await screen.findByText(deleteCta) - expect(deleteButton).toBeDefined() - }) - it("succeeds", async () => { await renderTemplateSettingsPage() From 54f9d95b88a1ccfde7daae7627e8857cf9ac4c5c Mon Sep 17 00:00:00 2001 From: Bruno Quaresma Date: Fri, 17 Feb 2023 12:46:54 +0000 Subject: [PATCH 8/9] Remove duplicated ttl field --- .../TemplateSettingsForm.tsx | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/site/src/pages/TemplateSettingsPage/TemplateSettingsForm.tsx b/site/src/pages/TemplateSettingsPage/TemplateSettingsForm.tsx index e63b8a085dabc..84bdbe314b0fd 100644 --- a/site/src/pages/TemplateSettingsPage/TemplateSettingsForm.tsx +++ b/site/src/pages/TemplateSettingsPage/TemplateSettingsForm.tsx @@ -183,24 +183,6 @@ export const TemplateSettingsForm: FC = ({ /> - - , - )} - disabled={isSubmitting} - fullWidth - inputProps={{ min: 0, step: 1 }} - label={t("defaultTtlLabel")} - variant="outlined" - type="number" - /> - - Date: Fri, 17 Feb 2023 13:06:42 +0000 Subject: [PATCH 9/9] Fix stories --- .../TemplateLayout/TemplatePageHeader.stories.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/site/src/components/TemplateLayout/TemplatePageHeader.stories.tsx b/site/src/components/TemplateLayout/TemplatePageHeader.stories.tsx index e8f0032d8973c..07d5334961ea6 100644 --- a/site/src/components/TemplateLayout/TemplatePageHeader.stories.tsx +++ b/site/src/components/TemplateLayout/TemplatePageHeader.stories.tsx @@ -28,4 +28,8 @@ export const CanUpdate = Template.bind({}) CanUpdate.args = {} export const CanNotUpdate = Template.bind({}) -CanNotUpdate.args = {} +CanNotUpdate.args = { + permissions: { + canUpdateTemplate: false, + }, +}