diff --git a/frontend/src/app/data-providers/cloud-data-provider.tsx b/frontend/src/app/data-providers/cloud-data-provider.tsx index e3aecdc1b8..4ce52f0171 100644 --- a/frontend/src/app/data-providers/cloud-data-provider.tsx +++ b/frontend/src/app/data-providers/cloud-data-provider.tsx @@ -1010,6 +1010,27 @@ export const createProjectContext = ({ limit: opts?.limit, }); }, + // Polls the project image registry. Used by the onboarding frontend + // step to hold back the "Waiting for an Actor" caption until the user + // has pushed their first image to Rivet Compute. + currentProjectFirstImagePresentQueryOptions() { + return queryOptions({ + queryKey: [ + organization, + project, + "images", + "first-present", + ] as QueryKey, + queryFn: async () => { + const response = await client.docker.listImages(project, { + limit: 1, + org: organization, + }); + return response.images.length > 0; + }, + refetchInterval: 5000, + }); + }, upsertCurrentProjectManagedPoolMutationOptions() { return mutationOptions({ mutationKey: [organization, project, "managed-pool", "upsert"], diff --git a/frontend/src/app/getting-started.tsx b/frontend/src/app/getting-started.tsx index 3c3ed60104..f10ef90ee8 100644 --- a/frontend/src/app/getting-started.tsx +++ b/frontend/src/app/getting-started.tsx @@ -1487,6 +1487,24 @@ function useServerfullEndpoint() { return data?.url || engineEnv().VITE_APP_API_URL; } +// Cloud + Rivet compute: poll the project image registry. Until the user +// pushes their first image, no runner config exists and no actor can be +// scheduled, so the caller hides the "Waiting for an Actor" affordance and +// shows a deployment-pending caption instead. In non-cloud builds +// `useCloudNamespaceDataProvider` returns undefined and this is a no-op. +function useWaitingForFirstImage(provider: string | undefined): boolean { + const nsDataProvider = useCloudNamespaceDataProvider(); + const enabled = + features.compute && provider === "rivet" && nsDataProvider != null; + const { data: hasImage } = useQuery({ + ...(nsDataProvider + ? nsDataProvider.currentProjectFirstImagePresentQueryOptions() + : { queryKey: ["frontend-setup", "first-image-noop"] as const, queryFn: () => false }), + enabled, + }); + return enabled && hasImage !== true; +} + function FrontendSetup() { const dataProvider = useDataProvider(); @@ -1521,6 +1539,7 @@ function FrontendSetup() { const { data: nsData } = useQuery( nsDataProvider.currentProjectNamespaceQueryOptions({ namespace: namespaceParam }), ); + const waitingForFirstImage = useWaitingForFirstImage(provider); const deploymentUrl = useMemo(() => { if (provider === "rivet" && nsData?.access?.engineNamespaceName) { @@ -1571,7 +1590,9 @@ function FrontendSetup() {

- Waiting for an Actor to be created... + {waitingForFirstImage + ? "Waiting for your first deployment..." + : "Waiting for an Actor to be created..."}

diff --git a/frontend/src/lib/data.ts b/frontend/src/lib/data.ts index 99b93a7bcc..d0024db21b 100644 --- a/frontend/src/lib/data.ts +++ b/frontend/src/lib/data.ts @@ -52,10 +52,23 @@ export function deriveOnboardingState(opts: { .find((p) => p !== undefined); const hasRunnerNames = (runnerNames?.pages[0]?.names.length ?? 0) > 0; - const hasRunnerConfigs = - Object.keys(runnerConfigs?.pages[0]?.runnerConfigs ?? {}).length > 0; + // A runner-config datacenter counts as "user-onboarded" only when + // `metadata.provider` is set. Both onboarding signals write it: the + // serverless `upsertRunnerConfig` call here in the frontend, and the + // engine-ee compute-pool actor once GCP Cloud Run services come up. A + // bare runner config without provider metadata (e.g. compute pre-enabled + // out-of-band, or partial backend state) doesn't count, so the user + // keeps seeing the provider/backend onboarding step instead of being + // pushed straight to "Waiting for an Actor". + const hasConfiguredDatacenter = (runnerConfigs?.pages ?? []).some((page) => + Object.values(page.runnerConfigs).some((config) => + Object.values(config.datacenters).some( + (dc) => deriveProviderFromMetadata(dc.metadata) !== undefined, + ), + ), + ); const hasActors = actorCount > 0; - const hasBackendConfigured = hasRunnerNames || hasRunnerConfigs; + const hasBackendConfigured = hasRunnerNames || hasConfiguredDatacenter; return { displayOnboarding: !hasBackendConfigured && !hasActors,