Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Remove form load from service
  • Loading branch information
BrunoQuaresma committed Oct 11, 2023
commit 816e596f2a51d54d2a9510363ed61f14e71760c8
6 changes: 4 additions & 2 deletions site/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import {
} from "@mui/material/styles";
import { ThemeProvider as EmotionThemeProvider } from "@emotion/react";

const queryClient = new QueryClient({
const defaultQueryClient = new QueryClient({
defaultOptions: {
queries: {
retry: false,
Expand All @@ -38,7 +38,9 @@ export const ThemeProviders: FC<PropsWithChildren> = ({ children }) => {
);
};

export const AppProviders: FC<PropsWithChildren> = ({ children }) => {
export const AppProviders: FC<
PropsWithChildren<{ queryClient?: QueryClient }>
> = ({ children, queryClient = defaultQueryClient }) => {
return (
<HelmetProvider>
<ThemeProviders>
Expand Down
7 changes: 7 additions & 0 deletions site/src/api/queries/templateVersions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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),
};
};
54 changes: 49 additions & 5 deletions site/src/pages/CreateWorkspacePage/CreateWorkspacePage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,7 @@ 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,
createWorkspaceMachine,
} from "xServices/createWorkspace/createWorkspaceXService";
import { createWorkspaceMachine } from "xServices/createWorkspace/createWorkspaceXService";
import { CreateWorkspacePageView } from "./CreateWorkspacePageView";
import { Loader } from "components/Loader/Loader";
import { ErrorAlert } from "components/Alert/ErrorAlert";
Expand All @@ -23,8 +20,15 @@ import {
NumberDictionary,
} from "unique-names-generator";
import { useMutation, useQuery, useQueryClient } from "react-query";
import { templateVersionExternalAuth } from "api/queries/templates";
import {
templateByName,
templateVersionExternalAuth,
} from "api/queries/templates";
import { autoCreateWorkspace } 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";

type CreateWorkspaceMode = "form" | "auto";

Expand Down Expand Up @@ -69,6 +73,21 @@ const CreateWorkspacePage: FC = () => {
autoCreateWorkspace(queryClient),
);

const templateQuery = useQuery(templateByName(organizationId, templateName));
const permissionsQuery = useQuery(
checkAuthorization({
checks: createWorkspaceChecks(organizationId),
}),
);
const realizedVersionId = versionId ?? templateQuery.data?.active_version_id;
const richParametersQuery = useQuery({
...richParameters(realizedVersionId ?? ""),
enabled: realizedVersionId !== undefined,
});
const realizedParameters = richParametersQuery.data
? richParametersQuery.data.filter(paramsUsedToCreateWorkspace)
: undefined;

const title = autoCreateWorkspaceMutation.isLoading
? "Creating workspace..."
: "Create workspace";
Expand Down Expand Up @@ -105,6 +124,31 @@ const CreateWorkspacePage: FC = () => {
templateName,
]);

useEffect(() => {
if (
templateQuery.data &&
permissionsQuery.data &&
realizedParameters &&
realizedVersionId
) {
send({
type: "LOAD_FORM_DATA",
data: {
template: templateQuery.data,
permissions: permissionsQuery.data as CreateWSPermissions,
parameters: realizedParameters,
versionId: realizedVersionId,
},
});
}
}, [
permissionsQuery.data,
realizedParameters,
send,
templateQuery.data,
realizedVersionId,
]);

return (
<>
<Helmet>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
16 changes: 16 additions & 0 deletions site/src/pages/CreateWorkspacePage/permissions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
export const createWorkspaceChecks = (organizationId: string) =>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm guessing that these types are coming from XState. Do we have any files that have some of these types defined for when we eventually remove the library? One of my worries is that once we remove XState, our codebase will still be partly influenced by how it sets things up, but we won't have the type definitions

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nops, there is no types coming from XState here 🤔

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, I just assumed that they were, because I've seen a few objects structured with these object and action properties around the codebase, and I know "action" is a state machine term

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No it is not, it is related to our internal RBAC system 😁

({
createWorkspaceForUser: {
object: {
resource_type: "workspace",
organization_id: organizationId,
owner_id: "*",
},
action: "create",
},
}) as const;

export type CreateWSPermissions = Record<
keyof ReturnType<typeof createWorkspaceChecks>,
boolean
>;
18 changes: 17 additions & 1 deletion site/src/testHelpers/renderHelpers.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,29 @@ import {
import { RequireAuth } from "../components/RequireAuth/RequireAuth";
import { MockUser } from "./entities";
import { ReactNode } from "react";
import { QueryClient } from "react-query";

export const renderWithRouter = (
router: ReturnType<typeof createMemoryRouter>,
) => {
return {
...tlRender(
<AppProviders>
<AppProviders
queryClient={
// Create one query client for each render isolate it avoid other
// tests to be affected
new QueryClient({
defaultOptions: {
queries: {
retry: false,
cacheTime: 0,
refetchOnWindowFocus: false,
networkMode: "offlineFirst",
},
},
})
}
>
<RouterProvider router={router} />
</AppProviders>,
),
Expand Down
81 changes: 15 additions & 66 deletions site/src/xServices/createWorkspace/createWorkspaceXService.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,4 @@
import {
checkAuthorization,
createWorkspace,
getTemplateByName,
getTemplateVersionRichParameters,
} from "api/api";
import { createWorkspace } from "api/api";
import {
CreateWorkspaceRequest,
Template,
Expand All @@ -12,8 +7,8 @@ import {
Workspace,
WorkspaceBuildParameter,
} from "api/typesGenerated";
import { CreateWSPermissions } from "pages/CreateWorkspacePage/permissions";
import { assign, createMachine } from "xstate";
import { paramsUsedToCreateWorkspace } from "utils/workspace";

type CreateWorkspaceContext = {
organizationId: string;
Expand Down Expand Up @@ -46,15 +41,18 @@ export const createWorkspaceMachine =
tsTypes: {} as import("./createWorkspaceXService.typegen").Typegen0,
schema: {
context: {} as CreateWorkspaceContext,
events: {} as CreateWorkspaceEvent,
events: {} as
| CreateWorkspaceEvent
| {
type: "LOAD_FORM_DATA";
data: {
template: Template;
permissions: CreateWSPermissions;
parameters: TemplateVersionParameter[];
versionId: string;
};
},
services: {} as {
loadFormData: {
data: {
template: Template;
permissions: CreateWSPermissions;
parameters: TemplateVersionParameter[];
};
};
createWorkspace: {
data: Workspace;
};
Expand All @@ -63,16 +61,11 @@ export const createWorkspaceMachine =
initial: "loadingFormData",
states: {
loadingFormData: {
invoke: {
src: "loadFormData",
onDone: {
on: {
LOAD_FORM_DATA: {
target: "idle",
actions: ["assignFormData"],
},
onError: {
target: "loadError",
actions: ["assignError"],
},
},
},
idle: {
Expand Down Expand Up @@ -120,26 +113,6 @@ export const createWorkspaceMachine =

return createWorkspace(organizationId, owner.id, request);
},
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) => {
Expand All @@ -157,27 +130,3 @@ export const createWorkspaceMachine =
},
},
);

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>
>;