chore(site): remove create workspace xservice (#10217)

* Move xstate transitions to provider

* Centrlize auth logic in the provider

* Remove actor

* Remove auth xservice

* Add loader while AuthProvider is loading

* Simplify and fix a few computed states

* Add a few replaces

* Fix logout

* Remove unused import

* Fix RequireAuth test

* Fix wait loader

* Fix tests

* Remove unecessary type

* Rename workspace queries module

* Remove auto create from workspace xservice

* Move external auth into its own hook

* Remove permissions fetching from templateByName query

* Remove form load from service

* Remove create workspace service entirely

* refactor: update workspace automation to use useEffectEvent

---------

Co-authored-by: Parkreiner <michaelsmith@coder.com>
Co-authored-by: Michael Smith <throwawayclover@gmail.com>
This commit is contained in:
Bruno Quaresma
2023-10-11 20:42:25 +00:00
committed by GitHub
co-authored by Parkreiner Michael Smith
parent 5be4b12378
commit 301c045aad
13 changed files with 277 additions and 363 deletions
+7
View File
@@ -6,3 +6,10 @@ export const templateVersionLogs = (versionId: string) => {
queryFn: () => API.getTemplateVersionLogs(versionId),
};
};
export const richParameters = (versionId: string) => {
return {
queryKey: ["templateVersion", versionId, "richParameters"],
queryFn: () => API.getTemplateVersionRichParameters(versionId),
};
};
+2 -18
View File
@@ -1,7 +1,6 @@
import * as API from "api/api";
import {
type Template,
type AuthorizationResponse,
type CreateTemplateVersionRequest,
type ProvisionerJobStatus,
type TemplateVersion,
@@ -21,25 +20,10 @@ export const templateByNameKey = (orgId: string, name: string) => [
export const templateByName = (
orgId: string,
name: string,
): QueryOptions<{ template: Template; permissions: AuthorizationResponse }> => {
): QueryOptions<Template> => {
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 };
},
queryFn: async () => API.getTemplateByName(orgId, name),
};
};
-20
View File
@@ -1,20 +0,0 @@
import * as API from "api/api";
import { type Workspace } from "api/typesGenerated";
import { type QueryOptions } from "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),
};
};
+80 -1
View File
@@ -1,10 +1,89 @@
import * as API from "api/api";
import { type QueryOptions } from "react-query";
import { QueryClient, type QueryOptions } from "react-query";
import {
type WorkspaceBuildParameter,
type Workspace,
type CreateWorkspaceRequest,
type WorkspacesResponse,
type WorkspacesRequest,
} from "api/typesGenerated";
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),
};
};
type AutoCreateWorkspaceOptions = {
templateName: string;
versionId?: string;
organizationId: string;
defaultBuildParameters?: WorkspaceBuildParameter[];
defaultName: string;
};
type CreateWorkspaceMutationVariables = CreateWorkspaceRequest & {
userId: string;
organizationId: string;
};
export const createWorkspace = (queryClient: QueryClient) => {
return {
mutationFn: async (variables: CreateWorkspaceMutationVariables) => {
const { userId, organizationId, ...req } = variables;
return API.createWorkspace(organizationId, userId, req);
},
onSuccess: async () => {
await queryClient.invalidateQueries(["workspaces"]);
},
};
};
export const autoCreateWorkspace = (queryClient: QueryClient) => {
return {
mutationFn: async ({
templateName,
versionId,
organizationId,
defaultBuildParameters,
defaultName,
}: AutoCreateWorkspaceOptions) => {
let templateVersionParameters;
if (versionId) {
templateVersionParameters = { template_version_id: versionId };
} else {
const template = await API.getTemplateByName(
organizationId,
templateName,
);
templateVersionParameters = { template_id: template.id };
}
return API.createWorkspace(organizationId, "me", {
...templateVersionParameters,
name: defaultName,
rich_parameter_values: defaultBuildParameters,
});
},
onSuccess: async () => {
await queryClient.invalidateQueries(["workspaces"]);
},
};
};
export function workspacesKey(config: WorkspacesRequest = {}) {
const { q, limit } = config;
return ["workspaces", { q, limit }] as const;
+4 -3
View File
@@ -1,4 +1,4 @@
import type { FC } from "react";
import { forwardRef } from "react";
import type { CSSObject } from "@emotion/react";
export type StackProps = {
@@ -10,7 +10,7 @@ export type StackProps = {
wrap?: CSSObject["flexWrap"];
} & React.HTMLProps<HTMLDivElement>;
export const Stack: FC<StackProps> = (props) => {
export const Stack = forwardRef<HTMLDivElement, StackProps>((props, ref) => {
const {
children,
direction = "column",
@@ -24,6 +24,7 @@ export const Stack: FC<StackProps> = (props) => {
return (
<div
{...divProps}
ref={ref}
css={(theme) => ({
display: "flex",
flexDirection: direction,
@@ -37,4 +38,4 @@ export const Stack: FC<StackProps> = (props) => {
{children}
</div>
);
};
});
@@ -22,8 +22,7 @@ export const DuplicateTemplateView = () => {
const templateByNameQuery = useQuery(
templateByName(organizationId, searchParams.get("fromTemplate")!),
);
const activeVersionId =
templateByNameQuery.data?.template.active_version_id ?? "";
const activeVersionId = templateByNameQuery.data?.active_version_id ?? "";
const templateVersionQuery = useQuery({
...templateVersion(activeVersionId),
enabled: templateByNameQuery.isSuccess,
@@ -63,7 +62,7 @@ export const DuplicateTemplateView = () => {
return (
<CreateTemplateForm
{...formPermissions}
copiedTemplate={templateByNameQuery.data!.template}
copiedTemplate={templateByNameQuery.data!}
error={createTemplateMutation.error}
isSubmitting={createTemplateMutation.isLoading}
variables={templateVersionVariablesQuery.data}
@@ -1,6 +1,6 @@
import { useMachine } from "@xstate/react";
import {
TemplateVersionParameter,
Workspace,
WorkspaceBuildParameter,
} from "api/typesGenerated";
import { useMe } from "hooks/useMe";
@@ -9,11 +9,6 @@ import { type FC, useCallback, useState, useEffect } from "react";
import { Helmet } from "react-helmet-async";
import { useNavigate, useParams, useSearchParams } from "react-router-dom";
import { pageTitle } from "utils/page";
import {
CreateWSPermissions,
CreateWorkspaceMode,
createWorkspaceMachine,
} from "xServices/createWorkspace/createWorkspaceXService";
import { CreateWorkspacePageView } from "./CreateWorkspacePageView";
import { Loader } from "components/Loader/Loader";
import { ErrorAlert } from "components/Alert/ErrorAlert";
@@ -23,8 +18,19 @@ import {
colors,
NumberDictionary,
} from "unique-names-generator";
import { useQuery } from "react-query";
import { templateVersionExternalAuth } from "api/queries/templates";
import { useMutation, useQuery, useQueryClient } from "react-query";
import {
templateByName,
templateVersionExternalAuth,
} from "api/queries/templates";
import { autoCreateWorkspace, createWorkspace } from "api/queries/workspaces";
import { checkAuthorization } from "api/queries/authCheck";
import { CreateWSPermissions, createWorkspaceChecks } from "./permissions";
import { richParameters } from "api/queries/templateVersions";
import { paramsUsedToCreateWorkspace } from "utils/workspace";
import { useEffectEvent } from "hooks/hookPolyfills";
type CreateWorkspaceMode = "form" | "auto";
export type ExternalAuthPollingState = "idle" | "polling" | "abandoned";
@@ -33,31 +39,127 @@ const CreateWorkspacePage: FC = () => {
const { template: templateName } = useParams() as { template: string };
const me = useMe();
const navigate = useNavigate();
const [searchParams] = useSearchParams();
const [searchParams, setSearchParams] = useSearchParams();
const defaultBuildParameters = getDefaultBuildParameters(searchParams);
const mode = (searchParams.get("mode") ?? "form") as CreateWorkspaceMode;
const [createWorkspaceState, send] = useMachine(createWorkspaceMachine, {
context: {
organizationId,
templateName,
mode,
defaultBuildParameters,
defaultName:
mode === "auto" ? generateUniqueName() : searchParams.get("name") ?? "",
versionId: searchParams.get("version") ?? undefined,
},
actions: {
onCreateWorkspace: (_, event) => {
navigate(`/@${event.data.owner_name}/${event.data.name}`);
},
},
const customVersionId = searchParams.get("version") ?? undefined;
const defaultName =
mode === "auto" ? generateUniqueName() : searchParams.get("name") ?? "";
const queryClient = useQueryClient();
const autoCreateWorkspaceMutation = useMutation(
autoCreateWorkspace(queryClient),
);
const createWorkspaceMutation = useMutation(createWorkspace(queryClient));
const templateQuery = useQuery(templateByName(organizationId, templateName));
const permissionsQuery = useQuery(
checkAuthorization({
checks: createWorkspaceChecks(organizationId),
}),
);
const realizedVersionId =
customVersionId ?? templateQuery.data?.active_version_id;
const richParametersQuery = useQuery({
...richParameters(realizedVersionId ?? ""),
enabled: realizedVersionId !== undefined,
});
const { template, parameters, permissions, defaultName, versionId } =
createWorkspaceState.context;
const title = createWorkspaceState.matches("autoCreating")
const realizedParameters = richParametersQuery.data
? richParametersQuery.data.filter(paramsUsedToCreateWorkspace)
: undefined;
const { externalAuth, externalAuthPollingState, startPollingExternalAuth } =
useExternalAuth(realizedVersionId);
const isLoadingFormData =
templateQuery.isLoading ||
permissionsQuery.isLoading ||
richParametersQuery.isLoading;
const loadFormDataError =
templateQuery.error ?? permissionsQuery.error ?? richParametersQuery.error;
const title = autoCreateWorkspaceMutation.isLoading
? "Creating workspace..."
: "Create workspace";
const onCreateWorkspace = useCallback(
(workspace: Workspace) => {
navigate(`/@${workspace.owner_name}/${workspace.name}`);
},
[navigate],
);
const automateWorkspaceCreation = useEffectEvent(async () => {
try {
const newWorkspace = await autoCreateWorkspaceMutation.mutateAsync({
templateName,
organizationId,
defaultBuildParameters,
defaultName,
versionId: realizedVersionId,
});
onCreateWorkspace(newWorkspace);
} catch (err) {
searchParams.delete("mode");
setSearchParams(searchParams);
}
});
useEffect(() => {
if (mode === "auto") {
void automateWorkspaceCreation();
}
}, [automateWorkspaceCreation, mode]);
return (
<>
<Helmet>
<title>{pageTitle(title)}</title>
</Helmet>
{loadFormDataError && <ErrorAlert error={loadFormDataError} />}
{isLoadingFormData || autoCreateWorkspaceMutation.isLoading ? (
<Loader />
) : (
<CreateWorkspacePageView
defaultName={defaultName}
defaultOwner={me}
defaultBuildParameters={defaultBuildParameters}
error={createWorkspaceMutation.error}
template={templateQuery.data!}
versionId={realizedVersionId}
externalAuth={externalAuth ?? []}
externalAuthPollingState={externalAuthPollingState}
startPollingExternalAuth={startPollingExternalAuth}
permissions={permissionsQuery.data as CreateWSPermissions}
parameters={realizedParameters as TemplateVersionParameter[]}
creatingWorkspace={createWorkspaceMutation.isLoading}
onCancel={() => {
navigate(-1);
}}
onSubmit={async (request, owner) => {
if (realizedVersionId) {
request = {
...request,
template_id: undefined,
template_version_id: realizedVersionId,
};
}
const workspace = await createWorkspaceMutation.mutateAsync({
...request,
userId: owner.id,
organizationId,
});
onCreateWorkspace(workspace);
}}
/>
)}
</>
);
};
const useExternalAuth = (versionId: string | undefined) => {
const [externalAuthPollingState, setExternalAuthPollingState] =
useState<ExternalAuthPollingState>("idle");
@@ -65,7 +167,7 @@ const CreateWorkspacePage: FC = () => {
setExternalAuthPollingState("polling");
}, []);
const { data: externalAuth, error } = useQuery(
const { data: externalAuth } = useQuery(
versionId
? {
...templateVersionExternalAuth(versionId),
@@ -97,50 +199,13 @@ const CreateWorkspacePage: FC = () => {
};
}, [externalAuthPollingState, allSignedIn]);
return (
<>
<Helmet>
<title>{pageTitle(title)}</title>
</Helmet>
{Boolean(
createWorkspaceState.matches("loadingFormData") ||
createWorkspaceState.matches("autoCreating"),
) && <Loader />}
{createWorkspaceState.matches("loadError") && (
<ErrorAlert error={error} />
)}
{createWorkspaceState.matches("idle") && (
<CreateWorkspacePageView
defaultName={defaultName}
defaultOwner={me}
defaultBuildParameters={defaultBuildParameters}
error={error}
template={template!}
versionId={versionId}
externalAuth={externalAuth ?? []}
externalAuthPollingState={externalAuthPollingState}
startPollingExternalAuth={startPollingExternalAuth}
permissions={permissions as CreateWSPermissions}
parameters={parameters!}
creatingWorkspace={createWorkspaceState.matches("creatingWorkspace")}
onCancel={() => {
navigate(-1);
}}
onSubmit={(request, owner) => {
send({
type: "CREATE_WORKSPACE",
request,
owner,
});
}}
/>
)}
</>
);
return {
startPollingExternalAuth,
externalAuth,
externalAuthPollingState,
};
};
export default CreateWorkspacePage;
const getDefaultBuildParameters = (
urlSearchParams: URLSearchParams,
): WorkspaceBuildParameter[] => {
@@ -178,3 +243,5 @@ const generateUniqueName = () => {
style: "lowerCase",
});
};
export default CreateWorkspacePage;
@@ -26,12 +26,12 @@ import {
ImmutableTemplateParametersSection,
MutableTemplateParametersSection,
} from "components/TemplateParameters/TemplateParameters";
import { CreateWSPermissions } from "xServices/createWorkspace/createWorkspaceXService";
import { ExternalAuth } from "./ExternalAuth";
import { ErrorAlert } from "components/Alert/ErrorAlert";
import { Stack } from "components/Stack/Stack";
import { type ExternalAuthPollingState } from "./CreateWorkspacePage";
import { useSearchParams } from "react-router-dom";
import { CreateWSPermissions } from "./permissions";
export interface CreateWorkspacePageViewProps {
error: unknown;
@@ -0,0 +1,16 @@
export const createWorkspaceChecks = (organizationId: string) =>
({
createWorkspaceForUser: {
object: {
resource_type: "workspace",
organization_id: organizationId,
owner_id: "*",
},
action: "create",
},
}) as const;
export type CreateWSPermissions = Record<
keyof ReturnType<typeof createWorkspaceChecks>,
boolean
>;
@@ -12,6 +12,7 @@ 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";
import { checkAuthorization } from "api/queries/authCheck";
const TemplateSettings = createContext<
{ template: Template; permissions: AuthorizationResponse } | undefined
@@ -30,11 +31,23 @@ export const TemplateSettingsLayout: FC = () => {
const styles = useStyles();
const orgId = useOrganizationId();
const { template: templateName } = useParams() as { template: string };
const { data, error, isLoading, isError } = useQuery(
templateByName(orgId, templateName),
);
const templateQuery = useQuery(templateByName(orgId, templateName));
const permissionsQuery = useQuery({
...checkAuthorization({
checks: {
canUpdateTemplate: {
object: {
resource_type: "template",
resource_id: templateQuery.data?.id ?? "",
},
action: "update",
},
},
}),
enabled: templateQuery.isSuccess,
});
if (isLoading) {
if (templateQuery.isLoading || permissionsQuery.isLoading) {
return <Loader />;
}
@@ -46,11 +59,16 @@ export const TemplateSettingsLayout: FC = () => {
<Margins>
<Stack className={styles.wrapper} direction="row" spacing={10}>
{isError ? (
<ErrorAlert error={error} />
{templateQuery.isError || permissionsQuery.isError ? (
<ErrorAlert error={templateQuery.error} />
) : (
<TemplateSettings.Provider value={data}>
<Sidebar template={data.template} />
<TemplateSettings.Provider
value={{
template: templateQuery.data,
permissions: permissionsQuery.data,
}}
>
<Sidebar template={templateQuery.data} />
<Suspense fallback={<Loader />}>
<main className={styles.content}>
<Outlet />
@@ -15,7 +15,7 @@ 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 { workspaceByOwnerAndNameKey } from "api/queries/workspaces";
import { WorkspaceScheduleForm } from "./WorkspaceScheduleForm";
import { workspaceSchedule } from "xServices/workspaceSchedule/workspaceScheduleXService";
import {
@@ -7,7 +7,7 @@ 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 { workspaceByOwnerAndName } from "api/queries/workspace";
import { workspaceByOwnerAndName } from "api/queries/workspaces";
import { useQuery } from "react-query";
import { ErrorAlert } from "components/Alert/ErrorAlert";
import { type Workspace } from "api/typesGenerated";
@@ -1,237 +0,0 @@
import {
checkAuthorization,
createWorkspace,
getTemplateByName,
getTemplateVersionRichParameters,
} from "api/api";
import {
CreateWorkspaceRequest,
Template,
TemplateVersionParameter,
User,
Workspace,
WorkspaceBuildParameter,
} from "api/typesGenerated";
import { assign, createMachine } from "xstate";
import { paramsUsedToCreateWorkspace } from "utils/workspace";
export type CreateWorkspaceMode = "form" | "auto";
type CreateWorkspaceContext = {
organizationId: string;
templateName: string;
mode: CreateWorkspaceMode;
defaultName: string;
// Not exposed in the form yet, but can be set as a search param to
// create a workspace with a specific version of a template
versionId?: string;
error?: unknown;
// Form
template?: Template;
parameters?: TemplateVersionParameter[];
permissions?: Record<string, boolean>;
// Used on auto-create
defaultBuildParameters?: WorkspaceBuildParameter[];
};
type CreateWorkspaceEvent = {
type: "CREATE_WORKSPACE";
request: CreateWorkspaceRequest;
owner: User;
};
type RefreshGitAuthEvent = {
type: "REFRESH_GITAUTH";
};
export const createWorkspaceMachine =
/** @xstate-layout N4IgpgJg5mDOIC5QGMBOYCGAXMB1A9qgNawAOGyYAyltmAHTIAWYyRAlgHZQCy+EYAMQBtAAwBdRKFL5Y7LO3ycpIAB6IATABYAnPR2iDWgOwBmAGynRo4wFZbAGhABPRDoAc9d+8OnTARlF-Uy0NUQ0AXwinNEwcAmIyCmpaHEYWNi5efiFhf0kkEBk5BSUVdQQNU1svEPcq9wsDU3ctJ1dKv3pRd3NzHVNjf1sjHX8omPQ6BJJySho6egwAVyx8AGEphW5BCCUGLgA3fCIGWOnCWeSFtJW1zbishCP8ZGxFTjFxL5Vi+Q-yoh7MZ9MZjLoQqItN5jDp2ogALT+PSWWwaDR9dzGDTeXTuCYgc7xS5JeapBh3DZbLKCMCoVCEeikAA22AAZoQALaMLZ4ElzFKLSkPd7cZ6cY5vUqfCQ-Qp-aWAhAtPQtWxDaE+OxQ4zwhD+dw1US2cw4-zmLQ9WyicwEol8xICm4MZn4DAQLIAMS5ABFsBhdvt6C9Tjy4g6rmTFq73V7ff7xZL3kovnLpLJ-mVChVzNb6P5tDoMei7P5-G0XIgQqZ6BjbFrWuZGrC7byZqTBWkYx7uN7UJy-bRafTGSz2VywxdHddyfRu3H+4OMInXsmZd8JL8M4rs4hejWCzYLOYDSfjOY9SENPpgmYy+bgjodLbooS2-yZ4t2BBmUJ1gAlABRABBAAVQCAH1cAAeX-ABpKgAAVgPWQC0yKbcAV3BBLHMWs61EQZjQ0ZF3D1ewa36S0QlhasQlbcN2ydWciSyJjkkDTgDglE4znfacozSVjuHYygVylD5U03eVMKzUAc1MPRGh0cEtBMJ9Wj1MxPFsKwmg0DwwmGBip0jTs+MeESP0oYcGVQJlWSwDl+0nYkBPM1y2OssBxLXKSCnTEosPkxBQn8eh7FaewBjRU1THIwIb2CLQ+nrXN1SiV9OByeBCntUTzK3IK5LUKstFrWE4uNGxwQxPUkRsfNqnRfxzybE1+hMtyzOddJWA4bg+AEIrM2UbD6gq58qmqsFQgvSsEGfehLEMExWp0LRSK6iMO164VqW4EadxC5Ui2W0wNDBDVekfLTrx8cIAnBKxgVsbaCt6+de3jWgjuC0qcIsZbfBtPE-F1BaGn0AzIWxGK-HxV98u83rv1-P6SpzSx6EUtT+kIsYT38PUtFscKDTUw1zGxMmy3elGWIOqACoxsaTtNPCLs2-oGlNVq9SJ2tQcCS0eZS+n3N6+0IFZpV3A2-MtBadx1uRcIyIWuxPDsforEM6x7AlnrZ27QCR1QWXxthHHLrBJ8nxtJsSd0ehsQsJWNHrYYi0yiIgA */
createMachine(
{
id: "createWorkspaceState",
predictableActionArguments: true,
tsTypes: {} as import("./createWorkspaceXService.typegen").Typegen0,
schema: {
context: {} as CreateWorkspaceContext,
events: {} as CreateWorkspaceEvent | RefreshGitAuthEvent,
services: {} as {
loadFormData: {
data: {
template: Template;
permissions: CreateWSPermissions;
parameters: TemplateVersionParameter[];
};
};
createWorkspace: {
data: Workspace;
};
autoCreateWorkspace: {
data: Workspace;
};
},
},
initial: "checkingMode",
states: {
checkingMode: {
always: [
{
target: "autoCreating",
cond: ({ mode }) => mode === "auto",
},
{ target: "loadingFormData" },
],
},
autoCreating: {
invoke: {
src: "autoCreateWorkspace",
onDone: {
actions: ["onCreateWorkspace"],
},
onError: {
actions: ["assignError"],
target: "loadingFormData",
},
},
},
loadingFormData: {
invoke: {
src: "loadFormData",
onDone: {
target: "idle",
actions: ["assignFormData"],
},
onError: {
target: "loadError",
actions: ["assignError"],
},
},
},
idle: {
on: {
CREATE_WORKSPACE: {
target: "creatingWorkspace",
},
},
},
creatingWorkspace: {
entry: "clearError",
invoke: {
src: "createWorkspace",
onDone: {
actions: ["onCreateWorkspace"],
target: "created",
},
onError: {
actions: ["assignError"],
target: "idle",
},
},
},
created: {
type: "final",
},
loadError: {
type: "final",
},
},
},
{
services: {
createWorkspace: (
{ organizationId, versionId },
{ request, owner },
) => {
if (versionId) {
request = {
...request,
template_id: undefined,
template_version_id: versionId,
};
}
return createWorkspace(organizationId, owner.id, request);
},
autoCreateWorkspace: async ({
templateName,
versionId,
organizationId,
defaultBuildParameters,
defaultName,
}) => {
let templateVersionParameters;
if (versionId) {
templateVersionParameters = { template_version_id: versionId };
} else {
const template = await getTemplateByName(
organizationId,
templateName,
);
templateVersionParameters = { template_id: template.id };
}
return createWorkspace(organizationId, "me", {
...templateVersionParameters,
name: defaultName,
rich_parameter_values: defaultBuildParameters,
});
},
loadFormData: async ({ templateName, organizationId, versionId }) => {
const [template, permissions] = await Promise.all([
getTemplateByName(organizationId, templateName),
checkCreateWSPermissions(organizationId),
]);
const realizedVersionId = versionId ?? template.active_version_id;
const [parameters] = await Promise.all([
getTemplateVersionRichParameters(realizedVersionId).then((p) =>
p.filter(paramsUsedToCreateWorkspace),
),
]);
return {
template,
permissions,
parameters,
versionId: realizedVersionId,
};
},
},
actions: {
assignFormData: assign((ctx, event) => {
return {
...ctx,
...event.data,
};
}),
assignError: assign({
error: (_, event) => event.data,
}),
clearError: assign({
error: (_) => undefined,
}),
},
},
);
const checkCreateWSPermissions = async (organizationId: string) => {
// HACK: below, we pass in * for the owner_id, which is a hacky way of checking if the
// current user can create a workspace on behalf of anyone within the org (only org owners should be able to do this).
// This pattern should not be replicated outside of this narrow use case.
const permissionsToCheck = {
createWorkspaceForUser: {
object: {
resource_type: "workspace",
organization_id: organizationId,
owner_id: "*",
},
action: "create",
},
} as const;
return checkAuthorization({
checks: permissionsToCheck,
}) as Promise<Record<keyof typeof permissionsToCheck, boolean>>;
};
export type CreateWSPermissions = Awaited<
ReturnType<typeof checkCreateWSPermissions>
>;