mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
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:
co-authored by
Parkreiner
Michael Smith
parent
5be4b12378
commit
301c045aad
@@ -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),
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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),
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -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),
|
||||
};
|
||||
};
|
||||
@@ -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;
|
||||
|
||||
@@ -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 />
|
||||
|
||||
+1
-1
@@ -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>
|
||||
>;
|
||||
Reference in New Issue
Block a user