Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -140,16 +140,16 @@ describe(AutoDeployFlow.name, () => {
expect(tryAgain).not.toHaveBeenCalled();
});

it("stops the autopilot and switches to manual bid selection when the user chooses a provider", () => {
it("stops the autopilot and switches to manual bid selection with the recommended bids when the user chooses a provider", () => {
const stopAutopilot = vi.fn();
const setBidStrategy = vi.fn();
const flow = buildFlow({ actions: mock<DeploymentFlow["actions"]>({ setBidStrategy }) });
const { sceneProps } = setup({ flow, autopilot: { stopAutopilot } });
const { sceneProps } = setup({ flow, autopilot: { stopAutopilot, recommendedBidIds: ["akash1provider/12345/1/1"] } });

sceneProps().onChooseProvider?.();

expect(stopAutopilot).toHaveBeenCalledTimes(1);
expect(setBidStrategy).toHaveBeenCalledWith("select");
expect(setBidStrategy).toHaveBeenCalledWith("select", ["akash1provider/12345/1/1"]);
});

it("stops the autopilot and switches to manual bid selection when the user tries another provider after a failed lease", () => {
Expand All @@ -161,17 +161,26 @@ describe(AutoDeployFlow.name, () => {
sceneProps().onTryAnotherProvider?.();

expect(stopAutopilot).toHaveBeenCalledTimes(1);
expect(setBidStrategy).toHaveBeenCalledWith("select");
expect(setBidStrategy).toHaveBeenCalledWith("select", []);
expect(analyticsService.track).not.toHaveBeenCalledWith("onboarding_choose_provider_click", expect.anything());
});

it("tracks the choose-provider click with the template name when the user switches to manual selection", () => {
const flow = buildFlow({ actions: mock<DeploymentFlow["actions"]>({ setBidStrategy: vi.fn() }) });
it("tracks the choose-provider click with the template name, deployment and bid count when the user switches to manual selection", () => {
const flow = buildFlow({
dseq: "12345",
bids: [mock<DeploymentFlow["bids"][number]>(), mock<DeploymentFlow["bids"][number]>()],
actions: mock<DeploymentFlow["actions"]>({ setBidStrategy: vi.fn() })
});
const { sceneProps, analyticsService } = setup({ flow, templateName: "my-app" });

sceneProps().onChooseProvider?.();

expect(analyticsService.track).toHaveBeenCalledWith("onboarding_choose_provider_click", { category: "onboarding", templateName: "my-app" });
expect(analyticsService.track).toHaveBeenCalledWith("onboarding_choose_provider_click", {
category: "onboarding",
templateName: "my-app",
dseq: "12345",
numberOfBids: 2
});
});

it("opens the configured contact-support URL when the scene requests support", () => {
Expand Down Expand Up @@ -240,6 +249,7 @@ describe(AutoDeployFlow.name, () => {
{ id: "preparing", label: "Preparing", status: "pending" }
],
matchedProviderAddress: null,
recommendedBidIds: [],
tryAgain: vi.fn(),
stopAutopilot: vi.fn(),
...overrides
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ export const AutoDeployFlow: FC<Props> = ({ templateName, sdl, resume, flow, dep
const openAddCredits = d.useAddCredits();
const { activeOrganization, projectsEnabled } = d.useActiveOrganization();
const router = d.useRouter();
const { state, progressPercent, phases, matchedProviderAddress, tryAgain, stopAutopilot } = d.useAutoDeploymentFlow({
const { state, progressPercent, phases, matchedProviderAddress, recommendedBidIds, tryAgain, stopAutopilot } = d.useAutoDeploymentFlow({
sdl,
resumeLeases: resume.activeLeases,
flow
Expand All @@ -77,9 +77,9 @@ export const AutoDeployFlow: FC<Props> = ({ templateName, sdl, resume, flow, dep
d.useLandOnDeploymentList(flow.autoLeasedDseq);

/** Halts the autopilot first, since the bid-strategy change only unmounts this flow once the URL propagates. */
function handOffToManualPick() {
function handOffToManualPick(bidIdsToSelect: string[]) {
stopAutopilot();
flow.actions.setBidStrategy("select");
flow.actions.setBidStrategy("select", bidIdsToSelect);
}

if (isStartingInBackground(flow, state)) {
Expand All @@ -102,7 +102,7 @@ export const AutoDeployFlow: FC<Props> = ({ templateName, sdl, resume, flow, dep
phases={phases}
focusedProviderAddress={matchedProviderAddress}
onTryAgain={tryAgain}
onTryAnotherProvider={handOffToManualPick}
onTryAnotherProvider={() => handOffToManualPick([])}
onAddFunds={() => {
openAddCredits({
initialTab: "purchase",
Expand All @@ -117,8 +117,13 @@ export const AutoDeployFlow: FC<Props> = ({ templateName, sdl, resume, flow, dep
window.open(publicConfig.NEXT_PUBLIC_CONTACT_SUPPORT_URL, "_blank", "noopener,noreferrer");
}}
onChooseProvider={() => {
analyticsService.track("onboarding_choose_provider_click", { category: "onboarding", templateName });
handOffToManualPick();
analyticsService.track("onboarding_choose_provider_click", {
category: "onboarding",
templateName,
dseq: flow.dseq,
numberOfBids: flow.bids.length
});
handOffToManualPick(recommendedBidIds);
}}
/>
</d.Layout>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -797,6 +797,48 @@ describe(ConfigureDeploymentForm.name, () => {
expect(flow.actions.selectProvider).not.toHaveBeenCalled();
});

describe("when the onboarding autopilot hands its recommended bids over", () => {
const RECOMMENDED_BID_ID = "akash1provider/999/1/1";
const OTHER_PLACEMENT_BID_ID = "akash1other/999/2/1";

it("starts with the bid recommended for each placement selected", () => {
const { flow } = setup({
initialSdl: VALID_SDL,
phase: "quoting",
recommendedBidIds: [OTHER_PLACEMENT_BID_ID, RECOMMENDED_BID_ID],
Panes: ProviderSelectProbePanes
});

expect(flow.actions.selectProvider).toHaveBeenCalledTimes(1);
expect(flow.actions.selectProvider).toHaveBeenCalledWith(screen.getByTestId("focused-placement").textContent, RECOMMENDED_BID_ID);
});

it("leaves a placement no recommended bid is for without a selection", () => {
const { flow } = setup({ initialSdl: VALID_SDL, phase: "quoting", recommendedBidIds: [OTHER_PLACEMENT_BID_ID] });

expect(flow.actions.selectProvider).not.toHaveBeenCalled();
});

it("selects the recommendation once, leaving every later pick to the user", () => {
const { flow, rerenderWith } = setup({ initialSdl: VALID_SDL, phase: "quoting", recommendedBidIds: [RECOMMENDED_BID_ID] });

flow.recommendedBidIds = ["akash1another/999/1/1"];
rerenderWith({});

expect(flow.actions.selectProvider).toHaveBeenCalledTimes(1);
expect(flow.actions.selectProvider).toHaveBeenCalledWith(expect.any(String), RECOMMENDED_BID_ID);
});

it("selects a recommendation handed over after the form opened", () => {
const { flow, rerenderWith } = setup({ initialSdl: VALID_SDL, phase: "quoting" });

flow.recommendedBidIds = [RECOMMENDED_BID_ID];
rerenderWith({});

expect(flow.actions.selectProvider).toHaveBeenCalledWith(expect.any(String), RECOMMENDED_BID_ID);
});
});

it("tracks the review modal opening from the header deploy action", () => {
const { ConfigureDeploymentHeader, analyticsService } = setup({ initialSdl: VALID_SDL });
const onDeploy = (ConfigureDeploymentHeader as ReturnType<typeof vi.fn>).mock.calls[0][0].onDeploy as () => void;
Expand Down Expand Up @@ -1185,6 +1227,7 @@ describe(ConfigureDeploymentForm.name, () => {
vm?: boolean;
phase?: DeploymentFlow["phase"];
pendingClose?: DeploymentFlow["pendingClose"];
recommendedBidIds?: string[];
twoPanel?: boolean;
Workspace?: (props: WorkspaceProbeProps) => ReactNode;
persistedInheritSecretsFrom?: string;
Expand Down Expand Up @@ -1261,6 +1304,7 @@ describe(ConfigureDeploymentForm.name, () => {
isDeployTakingLong: input.isDeployTakingLong ?? false,
noBidsReceived: input.noBidsReceived ?? false,
pendingClose: input.pendingClose ?? null,
recommendedBidIds: input.recommendedBidIds ?? [],
actions: mock<DeploymentFlow["actions"]>({ requestQuotes })
}),
{ error: input.flowError }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import type { SdlBuilderFormValuesType, ServiceType } from "@src/types";
import { SdlBuilderFormValuesSchema } from "@src/types";
import { parseBidId } from "@src/utils/bids/bidId";
import { defaultServiceWithPlacement, vmServiceOverrides } from "@src/utils/sdl/data";
import { getPlacementGseq } from "@src/utils/sdl/placementGseq";
import { severalRegionPicksOf } from "@src/utils/sdl/placementRegions";
import { generateSdl } from "@src/utils/sdl/sdlGenerator";
import { resolveSdlSecrets, secretReferenceNamesIn, withSecretsMarkedLike } from "@src/utils/sdl/sdlSecrets";
Expand Down Expand Up @@ -199,6 +200,7 @@ export const ConfigureDeploymentForm: FC<Props> = ({ initialSdl, initialName, in
const lastToastedDeployError = useRef(flow.deployError);
const lastToastedFlowError = useRef<typeof flow.error>(undefined);
const hasAutoFocusedFirstBids = useRef(false);
const hasSelectedRecommendedBids = useRef(false);

useEffect(
function toastDeployFailure() {
Expand Down Expand Up @@ -322,6 +324,19 @@ export const ConfigureDeploymentForm: FC<Props> = ({ initialSdl, initialName, in
[flow.phase, flow.selections, placementsWithBids, selectedPlacement.id, placements, services]
);

useEffect(
function selectRecommendedBidsOnce() {
if (hasSelectedRecommendedBids.current || flow.recommendedBidIds.length === 0) return;
hasSelectedRecommendedBids.current = true;
for (const placement of placements) {
const gseq = getPlacementGseq(liveSdl, placement.name);
const bidId = flow.recommendedBidIds.find(recommendedBidId => parseBidId(recommendedBidId).gseq === gseq);
if (bidId) flow.actions.selectProvider(placement.id, bidId);
}
},
[flow.recommendedBidIds, flow.actions, placements, liveSdl]
);

/**
* Records the provider chosen for a placement, then advances: focuses the next placement still missing a
* selection or, once none is missing, opens the review modal so the user confirms without hunting for the
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,17 @@ describe(DeploymentFlowProvider.name, () => {
expect(screen.getByTestId("lease-in-flight")).toHaveTextContent(String(isLeaseInFlight));
});

it.each([
["nothing has been sent to lease it", {}, false],
["its lease is in flight", { phase: "deploying", isLeaseInFlight: true }, true],
["it got its lease", { phase: "deploying", deploySucceeded: true }, true],
["the api took the whole one-click deploy on", { phase: "creating", autoLeasedDseq: "1234" }, true]
] as const)("tells its descendants whether the deployment keeps going without the page once %s", (_, state, keepsDeployingWithoutPage) => {
setup({ flow: flowIn({ phase: "quoting", ...state }) });

expect(screen.getByTestId("keeps-deploying-without-page")).toHaveTextContent(String(keepsDeployingWithoutPage));
});

it("lets its descendants discard the deployment through the flow", () => {
const flow = flowIn({ phase: "quoting" });
setup({ flow });
Expand Down Expand Up @@ -160,7 +171,7 @@ describe(DeploymentFlowProvider.name, () => {
it("guards nothing outside the provider", () => {
const { result } = renderHook(() => useDeploymentLeaveGuard());

expect(result.current).toMatchObject({ isCreating: false, holdsUnleasedDeployment: false, isLeaseInFlight: false });
expect(result.current).toMatchObject({ isCreating: false, holdsUnleasedDeployment: false, isLeaseInFlight: false, keepsDeployingWithoutPage: false });
expect(() => result.current.discard()).not.toThrow();
});
});
Expand All @@ -179,7 +190,10 @@ describe(DeploymentFlowProvider.name, () => {

function flowIn(
state: Partial<
Pick<DeploymentFlow, "phase" | "deploySucceeded" | "isCreatingInBackground" | "isDeployedInBackground" | "dseq" | "pendingClose" | "isLeaseInFlight">
Pick<
DeploymentFlow,
"phase" | "deploySucceeded" | "isCreatingInBackground" | "isDeployedInBackground" | "dseq" | "pendingClose" | "isLeaseInFlight" | "autoLeasedDseq"
>
>
): DeploymentFlow {
return mock<DeploymentFlow>({
Expand All @@ -189,6 +203,7 @@ describe(DeploymentFlowProvider.name, () => {
deploySucceeded: false,
dseq: null,
pendingClose: null,
autoLeasedDseq: null,
actions: mock<DeploymentFlowActions>(),
...state
});
Expand Down Expand Up @@ -234,11 +249,12 @@ describe(DeploymentFlowProvider.name, () => {
}

function LeaveGuardProbe() {
const { holdsUnleasedDeployment, isLeaseInFlight, discard } = useDeploymentLeaveGuard();
const { holdsUnleasedDeployment, isLeaseInFlight, keepsDeployingWithoutPage, discard } = useDeploymentLeaveGuard();
return (
<>
<span data-testid="holds-unleased-deployment">{String(holdsUnleasedDeployment)}</span>
<span data-testid="lease-in-flight">{String(isLeaseInFlight)}</span>
<span data-testid="keeps-deploying-without-page">{String(keepsDeployingWithoutPage)}</span>
<button type="button" onClick={discard}>
Discard
</button>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ export type DeploymentLeaveGuard = {
holdsUnleasedDeployment: boolean;
/** A lease already sent may still land once the page is gone, so leaving keeps the deployment instead of discarding it. */
isLeaseInFlight: boolean;
/** The deployment keeps going once the page is gone: its lease was sent or created, or the api took the whole one-click deploy on. */
keepsDeployingWithoutPage: boolean;
discard: () => void;
};

Expand All @@ -33,7 +35,13 @@ interface Props {
dependencies?: typeof DEPENDENCIES;
}

const NOTHING_TO_GUARD: DeploymentLeaveGuard = { isCreating: false, holdsUnleasedDeployment: false, isLeaseInFlight: false, discard: () => {} };
const NOTHING_TO_GUARD: DeploymentLeaveGuard = {
isCreating: false,
holdsUnleasedDeployment: false,
isLeaseInFlight: false,
keepsDeployingWithoutPage: false,
discard: () => {}
};

const DeploymentLeaveGuardContext = createContext<DeploymentLeaveGuard>(NOTHING_TO_GUARD);

Expand All @@ -60,12 +68,13 @@ export const DeploymentFlowProvider: FC<Props> = ({ intent, children, dependenci
const isCreating = flow.phase === "creating" && !flow.isCreatingInBackground;
const { isLeaseInFlight } = flow;
const holdsUnleasedDeployment = isAwaitingLease(flow) && !flow.isCreatingInBackground && !isLeaseInFlight;
const keepsDeployingWithoutPage = isLeaseInFlight || flow.deploySucceeded || flow.autoLeasedDseq !== null;
const { discard } = flow.actions;
d.useWarnBeforeUnload(isCreating);
d.useUnlockAppOnFirstCreate(flow.dseq);
const leaveGuard = useMemo(
() => ({ isCreating, holdsUnleasedDeployment, isLeaseInFlight, discard }),
[isCreating, holdsUnleasedDeployment, isLeaseInFlight, discard]
() => ({ isCreating, holdsUnleasedDeployment, isLeaseInFlight, keepsDeployingWithoutPage, discard }),
[isCreating, holdsUnleasedDeployment, isLeaseInFlight, keepsDeployingWithoutPage, discard]
);
const { enqueueSnackbar } = d.useSnackbar();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -826,6 +826,25 @@ describe(useDeploymentFlow.name, () => {
expect(replace).toHaveBeenCalledWith(expect.stringContaining("bid-strategy=auto"), undefined, { shallow: true });
});

it("hands the recommended bids to the manual pick it switches to", () => {
const { result } = setup({ intent: { sdlStrategy: "default", bidStrategy: "auto" } });

act(() => result.current.actions.setBidStrategy("select", ["akash1provider/999/1/1"]));

expect(result.current.bidStrategy).toBe("select");
expect(result.current.recommendedBidIds).toEqual(["akash1provider/999/1/1"]);
});

it("recommends no bid until a hand-off does, and drops the recommendation on a switch that carries none", () => {
const { result } = setup({ intent: { sdlStrategy: "default", bidStrategy: "auto" } });
expect(result.current.recommendedBidIds).toEqual([]);

act(() => result.current.actions.setBidStrategy("select", ["akash1provider/999/1/1"]));
act(() => result.current.actions.setBidStrategy("auto"));

expect(result.current.recommendedBidIds).toEqual([]);
});

it("preserves the draft id in the URL when creating a deployment", async () => {
const replace = vi.fn();
const createMutate = vi.fn((_args, { onSuccess }) => onSuccess({ data: { dseq: "999", manifest: "m" } }));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,8 @@ export interface DeploymentFlowState {
isLeaseInFlight: boolean;
/** The one-click deploy the api took on to create, match and lease by itself, which this page holds no dseq of and so can never close. */
autoLeasedDseq: string | null;
/** The bids the onboarding autopilot would have leased, one per placement, handed to the manual pick to start with them selected. */
recommendedBidIds: string[];
}

export interface RequestQuotesOptions {
Expand Down Expand Up @@ -124,7 +126,7 @@ export interface DeploymentFlowActions {
retryClose: () => void;
/** Gives the attempt up for good before leaving the page: the phase stays put so nothing flashes while the page navigates away, and nothing leases it meanwhile. No-op while a lease is in flight, since that lease may still land. */
discard: () => void;
setBidStrategy: (strategy: BidStrategy) => void;
setBidStrategy: (strategy: BidStrategy, recommendedBidIds?: string[]) => void;
refreshQuotes: () => void;
retry: () => void;
selectProvider: (placementId: string, bidId: string) => void;
Expand Down Expand Up @@ -266,6 +268,7 @@ export function useDeploymentFlow({ intent }: UseDeploymentFlowInput, dependenci
const [pendingClose, setPendingClose] = useState<PendingClose | null>(null);
const [noBidsReceived, setNoBidsReceived] = useState(false);
const [autoLeasedDseq, setAutoLeasedDseq] = useState<string | null>(null);
const [recommendedBidIds, setRecommendedBidIds] = useState<string[]>([]);

/** A close the api took on in the background, followed through the activity feed until it settles; `token` is the close that started it. */
const [backgroundClose, setBackgroundClose] = useState<{ dseq: string; token: number; activityId: string } | null>(null);
Expand Down Expand Up @@ -892,8 +895,9 @@ export function useDeploymentFlow({ intent }: UseDeploymentFlowInput, dependenci
);

const setBidStrategy = useCallback(
function setBidStrategy(strategy: BidStrategy) {
function setBidStrategy(strategy: BidStrategy, nextRecommendedBidIds: string[] = []) {
setBidStrategyState(strategy);
setRecommendedBidIds(nextRecommendedBidIds);
router.replace(buildConfigureUrl(intentRef.current, dseq ?? undefined, strategy), undefined, { shallow: true });
},
[router, dseq]
Expand Down Expand Up @@ -1164,6 +1168,7 @@ export function useDeploymentFlow({ intent }: UseDeploymentFlowInput, dependenci
isCreatingInBackground: backgroundCreate?.dseq === dseq || autoLeasedDseq !== null,
isLeaseInFlight,
autoLeasedDseq,
recommendedBidIds,
actions: {
requestQuotes,
requestQuotesAgain,
Expand Down
Loading
Loading