From 53a985ff1198efe49e71d4b00ecacb9bbd5795bf Mon Sep 17 00:00:00 2001 From: Kayla Washburn Date: Wed, 13 Sep 2023 11:55:27 -0600 Subject: [PATCH] refactor: clean up workspace and template settings (#9654) --- site/src/api/queries/templates.ts | 34 +++++++ site/src/api/queries/workspace.ts | 20 +++++ .../TemplateSettingsPage.tsx | 16 ++-- .../TemplatePermissionsPage.tsx | 4 +- .../TemplateScheduleForm.tsx | 1 + .../TemplateSchedulePage.tsx | 16 ++-- .../TemplateSettingsLayout.tsx | 90 ++++++------------- .../TemplateVariablesPage.tsx | 4 +- .../WorkspaceParametersPage.tsx | 4 +- .../WorkspaceScheduleForm.tsx | 1 + .../WorkspaceSchedulePage.tsx | 13 ++- .../WorkspaceSettingsLayout.tsx | 69 +++++++------- .../WorkspaceSettingsPage.tsx | 4 +- 13 files changed, 153 insertions(+), 123 deletions(-) create mode 100644 site/src/api/queries/workspace.ts diff --git a/site/src/api/queries/templates.ts b/site/src/api/queries/templates.ts index 155129b5aa..45d5347302 100644 --- a/site/src/api/queries/templates.ts +++ b/site/src/api/queries/templates.ts @@ -1,4 +1,38 @@ import * as API from "api/api"; +import { type Template, type AuthorizationResponse } from "api/typesGenerated"; +import { type QueryOptions } from "@tanstack/react-query"; + +export const templateByNameKey = (orgId: string, name: string) => [ + orgId, + "template", + name, + "settings", +]; + +export const templateByName = ( + orgId: string, + name: string, +): QueryOptions<{ template: Template; permissions: AuthorizationResponse }> => { + return { + queryKey: templateByNameKey(orgId, name), + queryFn: async () => { + const template = await API.getTemplateByName(orgId, name); + const permissions = await API.checkAuthorization({ + checks: { + canUpdateTemplate: { + object: { + resource_type: "template", + resource_id: template.id, + }, + action: "update", + }, + }, + }); + + return { template, permissions }; + }, + }; +}; const getTemplatesQueryKey = (orgId: string) => [orgId, "templates"]; diff --git a/site/src/api/queries/workspace.ts b/site/src/api/queries/workspace.ts new file mode 100644 index 0000000000..cb54407479 --- /dev/null +++ b/site/src/api/queries/workspace.ts @@ -0,0 +1,20 @@ +import * as API from "api/api"; +import { type Workspace } from "api/typesGenerated"; +import { type QueryOptions } from "@tanstack/react-query"; + +export const workspaceByOwnerAndNameKey = (owner: string, name: string) => [ + "workspace", + owner, + name, + "settings", +]; + +export const workspaceByOwnerAndName = ( + owner: string, + name: string, +): QueryOptions => { + return { + queryKey: workspaceByOwnerAndNameKey(owner, name), + queryFn: () => API.getWorkspaceByOwnerAndName(owner, name), + }; +}; diff --git a/site/src/pages/TemplateSettingsPage/TemplateGeneralSettingsPage/TemplateSettingsPage.tsx b/site/src/pages/TemplateSettingsPage/TemplateGeneralSettingsPage/TemplateSettingsPage.tsx index 818a82003a..aa3df19a6d 100644 --- a/site/src/pages/TemplateSettingsPage/TemplateGeneralSettingsPage/TemplateSettingsPage.tsx +++ b/site/src/pages/TemplateSettingsPage/TemplateGeneralSettingsPage/TemplateSettingsPage.tsx @@ -6,16 +6,16 @@ import { FC } from "react"; import { Helmet } from "react-helmet-async"; import { useNavigate, useParams } from "react-router-dom"; import { pageTitle } from "utils/page"; -import { - getTemplateQuery, - useTemplateSettingsContext, -} from "../TemplateSettingsLayout"; +import { useTemplateSettings } from "../TemplateSettingsLayout"; import { TemplateSettingsPageView } from "./TemplateSettingsPageView"; +import { templateByNameKey } from "api/queries/templates"; +import { useOrganizationId } from "hooks"; export const TemplateSettingsPage: FC = () => { const { template: templateName } = useParams() as { template: string }; const navigate = useNavigate(); - const { template } = useTemplateSettingsContext(); + const orgId = useOrganizationId(); + const { template } = useTemplateSettings(); const queryClient = useQueryClient(); const { mutate: updateTemplate, @@ -25,9 +25,9 @@ export const TemplateSettingsPage: FC = () => { (data: UpdateTemplateMeta) => updateTemplateMeta(template.id, data), { onSuccess: async () => { - await queryClient.invalidateQueries({ - queryKey: getTemplateQuery(templateName), - }); + await queryClient.invalidateQueries( + templateByNameKey(orgId, templateName), + ); displaySuccess("Template updated successfully"); }, }, diff --git a/site/src/pages/TemplateSettingsPage/TemplatePermissionsPage/TemplatePermissionsPage.tsx b/site/src/pages/TemplateSettingsPage/TemplatePermissionsPage/TemplatePermissionsPage.tsx index 8fd95ed13b..5c3ae473dd 100644 --- a/site/src/pages/TemplateSettingsPage/TemplatePermissionsPage/TemplatePermissionsPage.tsx +++ b/site/src/pages/TemplateSettingsPage/TemplatePermissionsPage/TemplatePermissionsPage.tsx @@ -11,7 +11,7 @@ import { FC } from "react"; import { Helmet } from "react-helmet-async"; import { pageTitle } from "utils/page"; import { templateACLMachine } from "xServices/template/templateACLXService"; -import { useTemplateSettingsContext } from "../TemplateSettingsLayout"; +import { useTemplateSettings } from "../TemplateSettingsLayout"; import { TemplatePermissionsPageView } from "./TemplatePermissionsPageView"; import { docs } from "utils/docs"; @@ -19,7 +19,7 @@ export const TemplatePermissionsPage: FC< React.PropsWithChildren > = () => { const organizationId = useOrganizationId(); - const { template, permissions } = useTemplateSettingsContext(); + const { template, permissions } = useTemplateSettings(); const { template_rbac: isTemplateRBACEnabled } = useFeatureVisibility(); const [state, send] = useMachine(templateACLMachine, { context: { templateId: template.id }, diff --git a/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateScheduleForm.tsx b/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateScheduleForm.tsx index c84c3e8920..f52ac32db3 100644 --- a/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateScheduleForm.tsx +++ b/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateScheduleForm.tsx @@ -138,6 +138,7 @@ export const TemplateScheduleForm: FC = ({ } }, initialTouched, + enableReinitialize: true, }); const getFieldHelpers = getFormHelpers( diff --git a/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateSchedulePage.tsx b/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateSchedulePage.tsx index 4e2b83c657..f8e8a58f72 100644 --- a/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateSchedulePage.tsx +++ b/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateSchedulePage.tsx @@ -1,4 +1,4 @@ -import { useMutation } from "@tanstack/react-query"; +import { useMutation, useQueryClient } from "@tanstack/react-query"; import { updateTemplateMeta } from "api/api"; import { UpdateTemplateMeta } from "api/typesGenerated"; import { useDashboard } from "components/Dashboard/DashboardProvider"; @@ -7,14 +7,17 @@ import { FC } from "react"; import { Helmet } from "react-helmet-async"; import { useNavigate, useParams } from "react-router-dom"; import { pageTitle } from "utils/page"; -import { useTemplateSettingsContext } from "../TemplateSettingsLayout"; +import { useTemplateSettings } from "../TemplateSettingsLayout"; import { TemplateSchedulePageView } from "./TemplateSchedulePageView"; -import { useLocalStorage } from "hooks"; +import { useLocalStorage, useOrganizationId } from "hooks"; +import { templateByNameKey } from "api/queries/templates"; const TemplateSchedulePage: FC = () => { const { template: templateName } = useParams() as { template: string }; const navigate = useNavigate(); - const { template } = useTemplateSettingsContext(); + const queryClient = useQueryClient(); + const orgId = useOrganizationId(); + const { template } = useTemplateSettings(); const { entitlements, experiments } = useDashboard(); const allowAdvancedScheduling = entitlements.features["advanced_template_scheduling"].enabled; @@ -33,7 +36,10 @@ const TemplateSchedulePage: FC = () => { } = useMutation( (data: UpdateTemplateMeta) => updateTemplateMeta(template.id, data), { - onSuccess: () => { + onSuccess: async () => { + await queryClient.invalidateQueries( + templateByNameKey(orgId, templateName), + ); displaySuccess("Template updated successfully"); // clear browser storage of workspaces impending deletion clearLocal("dismissedWorkspaceList"); // workspaces page diff --git a/site/src/pages/TemplateSettingsPage/TemplateSettingsLayout.tsx b/site/src/pages/TemplateSettingsPage/TemplateSettingsLayout.tsx index 4cec9e2c9e..5f4a825765 100644 --- a/site/src/pages/TemplateSettingsPage/TemplateSettingsLayout.tsx +++ b/site/src/pages/TemplateSettingsPage/TemplateSettingsLayout.tsx @@ -7,68 +7,36 @@ import { pageTitle } from "../../utils/page"; import { Loader } from "components/Loader/Loader"; import { Outlet, useParams } from "react-router-dom"; import { Margins } from "components/Margins/Margins"; -import { checkAuthorization, getTemplateByName } from "api/api"; import { useQuery } from "@tanstack/react-query"; import { useOrganizationId } from "hooks/useOrganizationId"; +import { templateByName } from "api/queries/templates"; +import { type AuthorizationResponse, type Template } from "api/typesGenerated"; +import { ErrorAlert } from "components/Alert/ErrorAlert"; -const templatePermissions = (templateId: string) => - ({ - canUpdateTemplate: { - object: { - resource_type: "template", - resource_id: templateId, - }, - action: "update", - }, - }) as const; - -const fetchTemplateSettings = async (orgId: string, name: string) => { - const template = await getTemplateByName(orgId, name); - const permissions = await checkAuthorization({ - checks: templatePermissions(template.id), - }); - - return { - template, - permissions, - }; -}; - -export const getTemplateQuery = (name: string) => [ - "template", - name, - "settings", -]; - -const useTemplate = (orgId: string, name: string) => { - return useQuery({ - queryKey: getTemplateQuery(name), - queryFn: () => fetchTemplateSettings(orgId, name), - keepPreviousData: true, - }); -}; - -const TemplateSettingsContext = createContext< - Awaited> | undefined +const TemplateSettings = createContext< + { template: Template; permissions: AuthorizationResponse } | undefined >(undefined); -export const useTemplateSettingsContext = () => { - const context = useContext(TemplateSettingsContext); - - if (!context) { - throw new Error( - "useTemplateSettingsContext must be used within a TemplateSettingsContext.Provider", - ); +export function useTemplateSettings() { + const value = useContext(TemplateSettings); + if (!value) { + throw new Error("This hook can only be used from a template settings page"); } - return context; -}; + return value; +} export const TemplateSettingsLayout: FC = () => { const styles = useStyles(); const orgId = useOrganizationId(); const { template: templateName } = useParams() as { template: string }; - const { data: settings } = useTemplate(orgId, templateName); + const { data, error, isLoading, isError } = useQuery( + templateByName(orgId, templateName), + ); + + if (isLoading) { + return ; + } return ( <> @@ -76,22 +44,22 @@ export const TemplateSettingsLayout: FC = () => { {pageTitle([templateName, "Settings"])} - {settings ? ( - - - - + + + {isError ? ( + + ) : ( + + }>
-
-
-
- ) : ( - - )} + + )} + + ); }; diff --git a/site/src/pages/TemplateSettingsPage/TemplateVariablesPage/TemplateVariablesPage.tsx b/site/src/pages/TemplateSettingsPage/TemplateVariablesPage/TemplateVariablesPage.tsx index 0fe74cb5f7..d7b61022d6 100644 --- a/site/src/pages/TemplateSettingsPage/TemplateVariablesPage/TemplateVariablesPage.tsx +++ b/site/src/pages/TemplateSettingsPage/TemplateVariablesPage/TemplateVariablesPage.tsx @@ -11,7 +11,7 @@ import { Helmet } from "react-helmet-async"; import { useNavigate, useParams } from "react-router-dom"; import { templateVariablesMachine } from "xServices/template/templateVariablesXService"; import { pageTitle } from "../../../utils/page"; -import { useTemplateSettingsContext } from "../TemplateSettingsLayout"; +import { useTemplateSettings } from "../TemplateSettingsLayout"; import { TemplateVariablesPageView } from "./TemplateVariablesPageView"; export const TemplateVariablesPage: FC = () => { @@ -20,7 +20,7 @@ export const TemplateVariablesPage: FC = () => { template: string; }; const organizationId = useOrganizationId(); - const { template } = useTemplateSettingsContext(); + const { template } = useTemplateSettings(); const navigate = useNavigate(); const [state, send] = useMachine(templateVariablesMachine, { context: { diff --git a/site/src/pages/WorkspaceSettingsPage/WorkspaceParametersPage/WorkspaceParametersPage.tsx b/site/src/pages/WorkspaceSettingsPage/WorkspaceParametersPage/WorkspaceParametersPage.tsx index 42f938e9e7..4faab99d7b 100644 --- a/site/src/pages/WorkspaceSettingsPage/WorkspaceParametersPage/WorkspaceParametersPage.tsx +++ b/site/src/pages/WorkspaceSettingsPage/WorkspaceParametersPage/WorkspaceParametersPage.tsx @@ -1,7 +1,7 @@ import { getWorkspaceParameters, postWorkspaceBuild } from "api/api"; import { Helmet } from "react-helmet-async"; import { pageTitle } from "utils/page"; -import { useWorkspaceSettingsContext } from "../WorkspaceSettingsLayout"; +import { useWorkspaceSettings } from "../WorkspaceSettingsLayout"; import { useMutation, useQuery } from "@tanstack/react-query"; import { Loader } from "components/Loader/Loader"; import { @@ -17,7 +17,7 @@ import { ErrorAlert } from "components/Alert/ErrorAlert"; import { WorkspaceBuildParameter } from "api/typesGenerated"; const WorkspaceParametersPage = () => { - const { workspace } = useWorkspaceSettingsContext(); + const workspace = useWorkspaceSettings(); const parameters = useQuery({ queryKey: ["workspace", workspace.id, "parameters"], queryFn: () => getWorkspaceParameters(workspace), diff --git a/site/src/pages/WorkspaceSettingsPage/WorkspaceSchedulePage/WorkspaceScheduleForm.tsx b/site/src/pages/WorkspaceSettingsPage/WorkspaceSchedulePage/WorkspaceScheduleForm.tsx index 1a8dbc1a4a..3587fec3e5 100644 --- a/site/src/pages/WorkspaceSettingsPage/WorkspaceSchedulePage/WorkspaceScheduleForm.tsx +++ b/site/src/pages/WorkspaceSettingsPage/WorkspaceSchedulePage/WorkspaceScheduleForm.tsx @@ -201,6 +201,7 @@ export const WorkspaceScheduleForm: FC< onSubmit, validationSchema, initialTouched, + enableReinitialize: true, }); const formHelpers = getFormHelpers( form, diff --git a/site/src/pages/WorkspaceSettingsPage/WorkspaceSchedulePage/WorkspaceSchedulePage.tsx b/site/src/pages/WorkspaceSettingsPage/WorkspaceSchedulePage/WorkspaceSchedulePage.tsx index 1b3ced6fe8..dc088aa7b8 100644 --- a/site/src/pages/WorkspaceSettingsPage/WorkspaceSchedulePage/WorkspaceSchedulePage.tsx +++ b/site/src/pages/WorkspaceSettingsPage/WorkspaceSchedulePage/WorkspaceSchedulePage.tsx @@ -10,12 +10,13 @@ import { scheduleChanged, } from "pages/WorkspaceSettingsPage/WorkspaceSchedulePage/schedule"; import { ttlMsToAutostop } from "pages/WorkspaceSettingsPage/WorkspaceSchedulePage/ttl"; -import { useWorkspaceSettingsContext } from "pages/WorkspaceSettingsPage/WorkspaceSettingsLayout"; +import { useWorkspaceSettings } from "pages/WorkspaceSettingsPage/WorkspaceSettingsLayout"; import { FC } from "react"; import { Helmet } from "react-helmet-async"; import { Navigate, useNavigate, useParams } from "react-router-dom"; import { pageTitle } from "utils/page"; import * as TypesGen from "api/typesGenerated"; +import { workspaceByOwnerAndNameKey } from "api/queries/workspace"; import { WorkspaceScheduleForm } from "./WorkspaceScheduleForm"; import { workspaceSchedule } from "xServices/workspaceSchedule/workspaceScheduleXService"; import { @@ -23,6 +24,7 @@ import { formValuesToTTLRequest, } from "./formToRequest"; import { ErrorAlert } from "components/Alert/ErrorAlert"; +import { useQueryClient } from "@tanstack/react-query"; const getAutostart = (workspace: TypesGen.Workspace) => scheduleToAutostart(workspace.autostart_schedule); @@ -44,7 +46,8 @@ export const WorkspaceSchedulePage: FC = () => { const navigate = useNavigate(); const username = params.username.replace("@", ""); const workspaceName = params.workspace; - const { workspace } = useWorkspaceSettingsContext(); + const queryClient = useQueryClient(); + const workspace = useWorkspaceSettings(); const [scheduleState, scheduleSend] = useMachine(workspaceSchedule, { context: { workspace }, }); @@ -97,7 +100,7 @@ export const WorkspaceSchedulePage: FC = () => { onCancel={() => { navigate(`/@${username}/${workspaceName}`); }} - onSubmit={(values) => { + onSubmit={async (values) => { scheduleSend({ type: "SUBMIT_SCHEDULE", autostart: formValuesToAutostartRequest(values), @@ -111,6 +114,10 @@ export const WorkspaceSchedulePage: FC = () => { values, ), }); + + await queryClient.invalidateQueries( + workspaceByOwnerAndNameKey(params.username, params.workspace), + ); }} /> )} diff --git a/site/src/pages/WorkspaceSettingsPage/WorkspaceSettingsLayout.tsx b/site/src/pages/WorkspaceSettingsPage/WorkspaceSettingsLayout.tsx index eb4603ae99..8902e9da81 100644 --- a/site/src/pages/WorkspaceSettingsPage/WorkspaceSettingsLayout.tsx +++ b/site/src/pages/WorkspaceSettingsPage/WorkspaceSettingsLayout.tsx @@ -7,39 +7,23 @@ import { pageTitle } from "../../utils/page"; import { Loader } from "components/Loader/Loader"; import { Outlet, useParams } from "react-router-dom"; import { Margins } from "components/Margins/Margins"; -import { getWorkspaceByOwnerAndName } from "api/api"; +import { workspaceByOwnerAndName } from "api/queries/workspace"; import { useQuery } from "@tanstack/react-query"; +import { ErrorAlert } from "components/Alert/ErrorAlert"; +import { type Workspace } from "api/typesGenerated"; -const fetchWorkspaceSettings = async (owner: string, name: string) => { - const workspace = await getWorkspaceByOwnerAndName(owner, name); +const WorkspaceSettings = createContext(undefined); - return { - workspace, - }; -}; - -const useWorkspace = (owner: string, name: string) => { - return useQuery({ - queryKey: ["workspace", name, "settings"], - queryFn: () => fetchWorkspaceSettings(owner, name), - }); -}; - -const WorkspaceSettingsContext = createContext< - Awaited> | undefined ->(undefined); - -export const useWorkspaceSettingsContext = () => { - const context = useContext(WorkspaceSettingsContext); - - if (!context) { +export function useWorkspaceSettings() { + const value = useContext(WorkspaceSettings); + if (!value) { throw new Error( - "useWorkspaceSettingsContext must be used within a WorkspaceSettingsContext.Provider", + "This hook can only be used from a workspace settings page", ); } - return context; -}; + return value; +} export const WorkspaceSettingsLayout: FC = () => { const styles = useStyles(); @@ -49,7 +33,16 @@ export const WorkspaceSettingsLayout: FC = () => { }; const workspaceName = params.workspace; const username = params.username.replace("@", ""); - const { data: settings } = useWorkspace(username, workspaceName); + const { + data: workspace, + error, + isLoading, + isError, + } = useQuery(workspaceByOwnerAndName(username, workspaceName)); + + if (isLoading) { + return ; + } return ( <> @@ -57,22 +50,22 @@ export const WorkspaceSettingsLayout: FC = () => { {pageTitle([workspaceName, "Settings"])} - {settings ? ( - - - - + + + {isError ? ( + + ) : ( + + }>
-
-
-
- ) : ( - - )} + + )} + + ); }; diff --git a/site/src/pages/WorkspaceSettingsPage/WorkspaceSettingsPage.tsx b/site/src/pages/WorkspaceSettingsPage/WorkspaceSettingsPage.tsx index af7d710b99..2ce2639d9e 100644 --- a/site/src/pages/WorkspaceSettingsPage/WorkspaceSettingsPage.tsx +++ b/site/src/pages/WorkspaceSettingsPage/WorkspaceSettingsPage.tsx @@ -1,7 +1,7 @@ import { Helmet } from "react-helmet-async"; import { useNavigate, useParams } from "react-router-dom"; import { pageTitle } from "utils/page"; -import { useWorkspaceSettingsContext } from "./WorkspaceSettingsLayout"; +import { useWorkspaceSettings } from "./WorkspaceSettingsLayout"; import { WorkspaceSettingsPageView } from "./WorkspaceSettingsPageView"; import { useMutation } from "@tanstack/react-query"; import { displaySuccess } from "components/GlobalSnackbar/utils"; @@ -15,7 +15,7 @@ const WorkspaceSettingsPage = () => { }; const workspaceName = params.workspace; const username = params.username.replace("@", ""); - const { workspace } = useWorkspaceSettingsContext(); + const workspace = useWorkspaceSettings(); const navigate = useNavigate(); const mutation = useMutation({ mutationFn: (formValues: WorkspaceSettingsFormValues) =>