mirror of
https://github.com/coder/coder.git
synced 2025-07-21 01:28:49 +00:00
refactor: clean up workspace and template settings (#9654)
This commit is contained in:
@ -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"];
|
||||
|
||||
|
20
site/src/api/queries/workspace.ts
Normal file
20
site/src/api/queries/workspace.ts
Normal file
@ -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<Workspace> => {
|
||||
return {
|
||||
queryKey: workspaceByOwnerAndNameKey(owner, name),
|
||||
queryFn: () => API.getWorkspaceByOwnerAndName(owner, name),
|
||||
};
|
||||
};
|
@ -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");
|
||||
},
|
||||
},
|
||||
|
@ -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<unknown>
|
||||
> = () => {
|
||||
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 },
|
||||
|
@ -138,6 +138,7 @@ export const TemplateScheduleForm: FC<TemplateScheduleForm> = ({
|
||||
}
|
||||
},
|
||||
initialTouched,
|
||||
enableReinitialize: true,
|
||||
});
|
||||
|
||||
const getFieldHelpers = getFormHelpers<TemplateScheduleFormValues>(
|
||||
|
@ -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
|
||||
|
@ -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<ReturnType<typeof fetchTemplateSettings>> | 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 <Loader />;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
@ -76,22 +44,22 @@ export const TemplateSettingsLayout: FC = () => {
|
||||
<title>{pageTitle([templateName, "Settings"])}</title>
|
||||
</Helmet>
|
||||
|
||||
{settings ? (
|
||||
<TemplateSettingsContext.Provider value={settings}>
|
||||
<Margins>
|
||||
<Stack className={styles.wrapper} direction="row" spacing={10}>
|
||||
<Sidebar template={settings.template} />
|
||||
<Margins>
|
||||
<Stack className={styles.wrapper} direction="row" spacing={10}>
|
||||
{isError ? (
|
||||
<ErrorAlert error={error} />
|
||||
) : (
|
||||
<TemplateSettings.Provider value={data}>
|
||||
<Sidebar template={data.template} />
|
||||
<Suspense fallback={<Loader />}>
|
||||
<main className={styles.content}>
|
||||
<Outlet />
|
||||
</main>
|
||||
</Suspense>
|
||||
</Stack>
|
||||
</Margins>
|
||||
</TemplateSettingsContext.Provider>
|
||||
) : (
|
||||
<Loader />
|
||||
)}
|
||||
</TemplateSettings.Provider>
|
||||
)}
|
||||
</Stack>
|
||||
</Margins>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@ -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: {
|
||||
|
@ -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),
|
||||
|
@ -201,6 +201,7 @@ export const WorkspaceScheduleForm: FC<
|
||||
onSubmit,
|
||||
validationSchema,
|
||||
initialTouched,
|
||||
enableReinitialize: true,
|
||||
});
|
||||
const formHelpers = getFormHelpers<WorkspaceScheduleFormValues>(
|
||||
form,
|
||||
|
@ -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),
|
||||
);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
@ -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<Workspace | undefined>(undefined);
|
||||
|
||||
return {
|
||||
workspace,
|
||||
};
|
||||
};
|
||||
|
||||
const useWorkspace = (owner: string, name: string) => {
|
||||
return useQuery({
|
||||
queryKey: ["workspace", name, "settings"],
|
||||
queryFn: () => fetchWorkspaceSettings(owner, name),
|
||||
});
|
||||
};
|
||||
|
||||
const WorkspaceSettingsContext = createContext<
|
||||
Awaited<ReturnType<typeof fetchWorkspaceSettings>> | 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 <Loader />;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
@ -57,22 +50,22 @@ export const WorkspaceSettingsLayout: FC = () => {
|
||||
<title>{pageTitle([workspaceName, "Settings"])}</title>
|
||||
</Helmet>
|
||||
|
||||
{settings ? (
|
||||
<WorkspaceSettingsContext.Provider value={settings}>
|
||||
<Margins>
|
||||
<Stack className={styles.wrapper} direction="row" spacing={10}>
|
||||
<Sidebar workspace={settings.workspace} username={username} />
|
||||
<Margins>
|
||||
<Stack className={styles.wrapper} direction="row" spacing={10}>
|
||||
{isError ? (
|
||||
<ErrorAlert error={error} />
|
||||
) : (
|
||||
<WorkspaceSettings.Provider value={workspace}>
|
||||
<Sidebar workspace={workspace} username={username} />
|
||||
<Suspense fallback={<Loader />}>
|
||||
<main className={styles.content}>
|
||||
<Outlet />
|
||||
</main>
|
||||
</Suspense>
|
||||
</Stack>
|
||||
</Margins>
|
||||
</WorkspaceSettingsContext.Provider>
|
||||
) : (
|
||||
<Loader />
|
||||
)}
|
||||
</WorkspaceSettings.Provider>
|
||||
)}
|
||||
</Stack>
|
||||
</Margins>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@ -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) =>
|
||||
|
Reference in New Issue
Block a user