Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
16 changes: 16 additions & 0 deletions apps/deploy-web/next.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,22 @@ const nextConfig = {
},
redirects: async () => {
return [
{
source: "/sdl-builder",
has: [{ type: "query", key: "id", value: "(?<userTemplateId>.+)" }],
destination: "/new-deployment/configure?userTemplateId=:userTemplateId",
permanent: true
},
{
source: "/sdl-builder",
destination: "/new-deployment/configure",
permanent: true
},
{
source: "/deploy-linux",
destination: "/new-deployment/configure?vm=true",
permanent: true
},
{
source: "/deploy",
destination: "/cloud-deploy",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { describe, expect, it, vi } from "vitest";
import { mock } from "vitest-mock-extended";

import sdlStore from "@src/store/sdlStore";
import type { TemplateCreation } from "@src/types";
import type { ITemplate, TemplateCreation } from "@src/types";
import { helloWorldTemplate } from "@src/utils/templates";
import type { DeploymentFlow } from "../useDeploymentFlow/useDeploymentFlow";
import type { DEPENDENCIES } from "./ConfigureDeployment";
Expand Down Expand Up @@ -141,6 +141,62 @@ describe(ConfigureDeployment.name, () => {
expect(AutoDeployFlow).toHaveBeenCalledWith(expect.objectContaining({ sdl: helloWorldTemplate.content, flow: expect.anything() }), expect.anything());
});

it("hydrates the form from the fetched user template's SDL and title", () => {
const { ConfigureDeploymentForm, useUserTemplate } = setup({
userTemplateId: "user-1",
userTemplate: { isLoading: false, isSuccess: true, data: mock<ITemplate>({ sdl: "user: sdl", title: "My Saved Template" }) }
});

expect(useUserTemplate).toHaveBeenCalledWith("user-1");
expect(ConfigureDeploymentForm).toHaveBeenCalledWith(
expect.objectContaining({ initialSdl: "user: sdl", initialName: "My Saved Template" }),
expect.anything()
);
});

it("shows a loading state while the user template is being fetched", () => {
const { ConfigureDeploymentForm } = setup({ userTemplateId: "user-1", userTemplate: { isLoading: true } });

expect(screen.getByRole("status")).toBeInTheDocument();
expect(ConfigureDeploymentForm).not.toHaveBeenCalled();
});

it("surfaces an error and falls back to a default when the user template can't be loaded", () => {
const { ConfigureDeploymentForm, enqueueSnackbar } = setup({ userTemplateId: "user-1", userTemplate: { isLoading: false, isError: true } });

expect(enqueueSnackbar).toHaveBeenCalledWith(expect.anything(), expect.objectContaining({ variant: "error" }));
expect(ConfigureDeploymentForm).toHaveBeenCalledWith(expect.objectContaining({ initialSdl: undefined }), expect.anything());
});

it("surfaces an error when the user template isn't visible to the viewer", () => {
const { ConfigureDeploymentForm, enqueueSnackbar } = setup({
userTemplateId: "user-1",
userTemplate: { isLoading: false, isSuccess: true, data: null }
});

expect(enqueueSnackbar).toHaveBeenCalledWith(expect.anything(), expect.objectContaining({ variant: "error" }));
expect(ConfigureDeploymentForm).toHaveBeenCalledWith(expect.objectContaining({ initialSdl: undefined }), expect.anything());
});

it("restores the draft's persisted SDL and skips the user template fetch", () => {
const { ConfigureDeploymentForm, useUserTemplate } = setup({ userTemplateId: "user-1", persistedSdl: "restored: sdl" });

expect(useUserTemplate).toHaveBeenCalledWith(undefined);
expect(ConfigureDeploymentForm).toHaveBeenCalledWith(expect.objectContaining({ initialSdl: "restored: sdl" }), expect.anything());
});

it("forwards the user template id into the form's intent", () => {
const { ConfigureDeploymentForm } = setup({
userTemplateId: "user-1",
userTemplate: { isLoading: false, isSuccess: true, data: mock<ITemplate>({ sdl: "user: sdl" }) }
});

expect(ConfigureDeploymentForm).toHaveBeenCalledWith(
expect.objectContaining({ intent: expect.objectContaining({ userTemplateId: "user-1" }) }),
expect.anything()
);
});

function setup(input: {
templateId?: string | null;
sdlStrategy?: string;
Expand All @@ -149,6 +205,8 @@ describe(ConfigureDeployment.name, () => {
persistedSdl?: string;
persistedName?: string;
template?: { isLoading?: boolean; isError?: boolean; data?: TemplateOutput };
userTemplateId?: string;
userTemplate?: { isLoading?: boolean; isError?: boolean; isSuccess?: boolean; data?: ITemplate | null };
deploySdl?: TemplateCreation | null;
vm?: boolean;
}) {
Expand All @@ -157,6 +215,7 @@ describe(ConfigureDeployment.name, () => {
const DeploymentFlowProvider = vi.fn(({ children }) => <>{children({ flow: mock<DeploymentFlow>() })}</>);
const enqueueSnackbar = vi.fn();
const usePublicTemplate = vi.fn(() => mock<ReturnType<typeof DEPENDENCIES.usePublicTemplate>>(input.template as never));
const useUserTemplate = vi.fn(() => mock<ReturnType<typeof DEPENDENCIES.useUserTemplate>>(input.userTemplate as never));
const save = vi.fn();
const clear = vi.fn();
const useConfigureDraft = vi.fn(() =>
Expand All @@ -171,6 +230,7 @@ describe(ConfigureDeployment.name, () => {

const query: Record<string, string> = {};
if (input.templateId) query.templateId = input.templateId;
if (input.userTemplateId) query.userTemplateId = input.userTemplateId;
if (input.sdlStrategy) query["sdl-strategy"] = input.sdlStrategy;
if (input.bidStrategy) query["bid-strategy"] = input.bidStrategy;
if (input.vm) query.vm = "true";
Expand All @@ -184,6 +244,7 @@ describe(ConfigureDeployment.name, () => {
DeploymentFlowProvider: DeploymentFlowProvider as never,
ResumeDeploymentGuard: vi.fn(({ children }) => <>{children({ activeLeases: [] })}</>) as never,
usePublicTemplate: usePublicTemplate as never,
useUserTemplate: useUserTemplate as never,
useConfigureDraft: useConfigureDraft as never,
useSearchParams: () => params as unknown as ReadonlyURLSearchParams,
useParams: (() => ({})) as never,
Expand All @@ -200,6 +261,6 @@ describe(ConfigureDeployment.name, () => {
</JotaiStoreProvider>
);

return { ConfigureDeploymentForm, AutoDeployFlow, DeploymentFlowProvider, usePublicTemplate, useConfigureDraft, enqueueSnackbar };
return { ConfigureDeploymentForm, AutoDeployFlow, DeploymentFlowProvider, usePublicTemplate, useUserTemplate, useConfigureDraft, enqueueSnackbar };
}
});
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { NextSeo } from "next-seo";
import { useSnackbar } from "notistack";

import Layout from "@src/components/layout/Layout";
import { usePublicTemplate } from "@src/queries/useTemplateQuery";
import { usePublicTemplate, useTemplate } from "@src/queries/useTemplateQuery";
import sdlStore from "@src/store/sdlStore";
import type { TemplateCreation } from "@src/types";
import { hardcodedTemplates } from "@src/utils/templates";
Expand All @@ -28,6 +28,7 @@ export const DEPENDENCIES = {
DeploymentFlowProvider,
ResumeDeploymentGuard,
usePublicTemplate,
useUserTemplate: useTemplate,
useConfigureDraft,
useSearchParams,
useParams,
Expand All @@ -44,10 +45,12 @@ type Props = {
* `useConfigureDraft`) identifies a started session by a `draftId`: when present its persisted working SDL is restored
* and the template is ignored; when absent an id is minted and written into the URL so a reload resumes the same draft.
* Without a draft, a `templateId` is resolved the same way the legacy flow does: hardcoded templates (e.g. hello-world)
* carry their SDL inline and are matched by code, while everything else is fetched as a public gallery template; with
* neither, the carried-in `deploySdl` atom is used, except on a `vm=true` entry, which ignores the atom so a fresh
* Container-VM session always seeds deterministically. Keeping resolution here lets the form initialize synchronously
* from a single source — and lets a resume skip the template fetch.
* carry their SDL inline and are matched by code, while everything else is fetched as a public gallery template. A
* `userTemplateId` instead fetches the viewer's own saved template, which the API answers with an empty body when it
* isn't theirs to see — surfaced as the same "couldn't load" fallback as a failed request. With neither, the carried-in
* `deploySdl` atom is used, except on a `vm=true` entry, which ignores the atom so a fresh Container-VM session always
* seeds deterministically. Keeping resolution here lets the form initialize synchronously from a single source — and
* lets a resume skip the template fetch.
*/
export const ConfigureDeployment: FC<Props> = ({ dependencies: d = DEPENDENCIES }) => {
const searchParams = d.useSearchParams();
Expand All @@ -58,37 +61,49 @@ export const ConfigureDeployment: FC<Props> = ({ dependencies: d = DEPENDENCIES
const resolvedIntent = useMemo<DeploymentIntent>(
() => ({
templateId: intent.templateId,
userTemplateId: intent.userTemplateId,
sdlStrategy: intent.sdlStrategy,
bidStrategy: intent.bidStrategy,
dseq: intent.dseq,
draftId: draft.draftId,
vm: intent.vm
}),
[intent.templateId, intent.sdlStrategy, intent.bidStrategy, intent.dseq, draft.draftId, intent.vm]
[intent.templateId, intent.userTemplateId, intent.sdlStrategy, intent.bidStrategy, intent.dseq, draft.draftId, intent.vm]
);

const templateId = intent.templateId;
const deploySdl = useAtomValue(sdlStore.deploySdl);
const hardcodedTemplate: TemplateCreation | undefined = templateId ? hardcodedTemplates.find(template => template.code === templateId) : undefined;
const fetchedTemplateId = draft.persistedSdl === undefined && !hardcodedTemplate ? templateId : undefined;
const isDraftRestored = draft.persistedSdl !== undefined;
const fetchedTemplateId = isDraftRestored || hardcodedTemplate ? undefined : templateId;
const fetchedUserTemplateId = isDraftRestored ? undefined : intent.userTemplateId;
const templateQuery = d.usePublicTemplate(fetchedTemplateId);
const userTemplateQuery = d.useUserTemplate(fetchedUserTemplateId);
const { enqueueSnackbar } = d.useSnackbar();

const isFetchingTemplate = !!fetchedTemplateId || !!fetchedUserTemplateId;
const isTemplateLoading = (!!fetchedTemplateId && templateQuery.isLoading) || (!!fetchedUserTemplateId && userTemplateQuery.isLoading);
const hasTemplateFailed =
(!!fetchedTemplateId && templateQuery.isError) ||
(!!fetchedUserTemplateId && (userTemplateQuery.isError || (userTemplateQuery.isSuccess && !userTemplateQuery.data?.sdl)));

useEffect(
function notifyOnTemplateError() {
if (!fetchedTemplateId || !templateQuery.isError) {
if (!hasTemplateFailed) {
return;
}
enqueueSnackbar(<d.Snackbar title="Couldn't load the template" subTitle="Starting from a default deployment instead." iconVariant="error" />, {
variant: "error"
});
},
[fetchedTemplateId, templateQuery.isError, enqueueSnackbar, d]
[hasTemplateFailed, enqueueSnackbar, d]
);

const fetchedSdl = fetchedTemplateId ? templateQuery.data?.deploy : userTemplateQuery.data?.sdl;
const fetchedName = fetchedTemplateId ? templateQuery.data?.name : userTemplateQuery.data?.title;
const carriedInSdl = intent.vm ? undefined : deploySdl?.content;
const initialSdl = draft.persistedSdl ?? hardcodedTemplate?.content ?? (fetchedTemplateId ? templateQuery.data?.deploy : carriedInSdl);
const initialName = draft.persistedName ?? hardcodedTemplate?.name ?? (fetchedTemplateId ? templateQuery.data?.name : undefined);
const initialSdl = draft.persistedSdl ?? hardcodedTemplate?.content ?? (isFetchingTemplate ? fetchedSdl : carriedInSdl);
const initialName = draft.persistedName ?? hardcodedTemplate?.name ?? (isFetchingTemplate ? fetchedName : undefined);

const isAutoDeploy = resolvedIntent.sdlStrategy === "default" && resolvedIntent.bidStrategy === "auto";
const templateName = templateQuery.data?.name ?? hardcodedTemplate?.title ?? "your deployment";
Expand All @@ -101,7 +116,7 @@ export const ConfigureDeployment: FC<Props> = ({ dependencies: d = DEPENDENCIES
return (
<d.ResumeDeploymentGuard intent={resolvedIntent} canResume={isAutoDeploy && !!initialSdl}>
{resume => {
if (fetchedTemplateId && templateQuery.isLoading) {
if (isTemplateLoading) {
return (
<d.Layout background="white" disableContainer containerClassName="flex h-[calc(100vh-57px)] flex-col">
<d.NextSeo title="Configure your deployment" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,4 +72,28 @@ describe(parseDeploymentIntent.name, () => {
expect(intent.sdlStrategy).toBe("edit");
expect(intent.vm).toBe(true);
});

it("reads a user template id", () => {
const intent = parseDeploymentIntent({ dseqSegment: undefined, searchParams: new URLSearchParams("userTemplateId=user-1") });
expect(intent.userTemplateId).toBe("user-1");
expect(intent.templateId).toBeUndefined();
expect(intent.sdlStrategy).toBe("edit");
});

it("treats an empty user template id as missing", () => {
const intent = parseDeploymentIntent({ dseqSegment: undefined, searchParams: new URLSearchParams("userTemplateId=") });
expect(intent.userTemplateId).toBeUndefined();
});

it("drops the user template id on a vm entry", () => {
const intent = parseDeploymentIntent({ dseqSegment: undefined, searchParams: new URLSearchParams("vm=true&userTemplateId=user-1") });
expect(intent.userTemplateId).toBeUndefined();
expect(intent.vm).toBe(true);
});

it("prefers a gallery template id over a user template id", () => {
const intent = parseDeploymentIntent({ dseqSegment: undefined, searchParams: new URLSearchParams("templateId=abc&userTemplateId=user-1") });
expect(intent.templateId).toBe("abc");
expect(intent.userTemplateId).toBeUndefined();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ export type BidStrategy = "auto" | "select";

export interface DeploymentIntent {
templateId?: string;
/** A user-authored template (`?userTemplateId=`), resolved against the private user-template API rather than the public gallery. */
userTemplateId?: string;
sdlStrategy: SdlStrategy;
bidStrategy: BidStrategy;
dseq?: string;
Expand All @@ -23,15 +25,19 @@ interface ParseInput {
* safe manual defaults (`edit`/`select`) on anything unrecognized, and `sdl-strategy` is only
* honored alongside a `templateId` (it governs creating *from a template*). A `vm=true` entry
* is a Container-VM seed, not a template build: it drops any `templateId` (and with it the
* `default` strategy), so the manual form always opens.
* `default` strategy), so the manual form always opens. The two template sources are mutually
* exclusive and resolved here rather than downstream: a `vm` entry drops both, and a gallery
* `templateId` wins over a `userTemplateId`. A user template never carries a `sdl-strategy`, so
* it always opens the manual form rather than the auto-deploy flow.
*/
export function parseDeploymentIntent({ dseqSegment, searchParams }: ParseInput): DeploymentIntent {
const vm = searchParams.get("vm") === "true";
const templateId = vm ? undefined : searchParams.get("templateId") ?? undefined;
const userTemplateId = vm || templateId ? undefined : searchParams.get("userTemplateId") || undefined;
const sdlStrategy = templateId ? toSdlStrategy(searchParams.get("sdl-strategy")) : "edit";
const bidStrategy = toBidStrategy(searchParams.get("bid-strategy"));
const draftId = searchParams.get("draftId") || undefined;
return { templateId, sdlStrategy, bidStrategy, dseq: dseqSegment || undefined, draftId, vm };
return { templateId, userTemplateId, sdlStrategy, bidStrategy, dseq: dseqSegment || undefined, draftId, vm };
}

/** Narrows the raw `sdl-strategy` param to the union, defaulting to `edit`. */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -899,4 +899,18 @@ describe(buildConfigureUrl.name, () => {

expect(url).not.toContain("vm=");
});

it("preserves the user template id across rewrites", () => {
const url = buildConfigureUrl({ sdlStrategy: "edit", bidStrategy: "select", userTemplateId: "user-1", draftId: "draft-1", vm: false }, "999", "select");

expect(url).toContain("/new-deployment/configure/999");
expect(url).toContain("userTemplateId=user-1");
expect(url).toContain("draftId=draft-1");
});

it("omits the user template id when the intent carries none", () => {
const url = buildConfigureUrl({ sdlStrategy: "edit", bidStrategy: "select", vm: false }, undefined, "select");

expect(url).not.toContain("userTemplateId");
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -490,11 +490,12 @@ function deploymentResourcesFromSdl(sdl: string): { gpuAmount: number; cpuAmount
}
}

/** Builds the canonical configure URL preserving templateId/sdl-strategy/draftId/vm and the current dseq + bid-strategy. */
/** Builds the canonical configure URL preserving both template ids, sdl-strategy, draftId and vm alongside the current dseq + bid-strategy. */
export function buildConfigureUrl(intent: DeploymentIntent, dseq: string | undefined, bidStrategy: BidStrategy): string {
return UrlService.configureDeployment({
dseq,
templateId: intent.templateId,
userTemplateId: intent.userTemplateId,
sdlStrategy: intent.templateId ? intent.sdlStrategy : undefined,
bidStrategy,
draftId: intent.draftId,
Expand Down

This file was deleted.

This file was deleted.

Loading