diff --git a/web-admin/src/features/edit-session/PublishPopover.svelte b/web-admin/src/features/edit-session/PublishPopover.svelte index fc7b670b4bb..6c16acbddb6 100644 --- a/web-admin/src/features/edit-session/PublishPopover.svelte +++ b/web-admin/src/features/edit-session/PublishPopover.svelte @@ -17,13 +17,16 @@ import { createRuntimeServiceGitMergeToBranchMutation, createRuntimeServiceGitPushMutation, - createRuntimeServiceGitStatus, getRuntimeServiceGitStatusQueryKey, } from "@rilldata/web-common/runtime-client"; import { useRuntimeClient } from "@rilldata/web-common/runtime-client/v2"; import { Rocket } from "lucide-svelte"; import { buildPostMergeUrl } from "./post-merge-url"; import { goto } from "$app/navigation"; + import { + fetchDeploymentGithubStatusChanges, + getDeploymentGithubStatus, + } from "@rilldata/web-admin/features/edit-session/selectors.ts"; export let organization: string; export let project: string; @@ -36,7 +39,7 @@ const client = useRuntimeClient(); const gitPushMutation = createRuntimeServiceGitPushMutation(client); const gitMergeMutation = createRuntimeServiceGitMergeToBranchMutation(client); - const gitStatusQuery = createRuntimeServiceGitStatus(client, {}); + const gitStatusQuery = getDeploymentGithubStatus(client, primaryBranch); // Query GetProject without a branch param so `data.deployment` reflects // the project's primary (prod) deployment — the same source of truth the // project layout uses. ListDeployments is too loose: it includes orphan @@ -44,21 +47,21 @@ const projectQuery = createAdminServiceGetProject(organization, project); const redeployProjectMutation = createAdminServiceRedeployProject(); - $: currentBranch = $gitStatusQuery.data?.branch ?? ""; - $: hasLocalChanges = $gitStatusQuery.data?.localChanges ?? false; + $: ({ + isPending, + data: { + hasLocalChanges, + hasChangesOnCurrent, + alreadyOnPrimary, + disabledPerGitStatus, + }, + } = $gitStatusQuery); + $: projectLoaded = $projectQuery.data !== undefined; $: prodDeployment = $projectQuery.data?.deployment; $: prodDeploymentActive = !!prodDeployment && isActiveDeployment(prodDeployment); - $: alreadyOnPrimary = - !!primaryBranch && !!currentBranch && currentBranch === primaryBranch; - // TODO: this should also check currentBranch vs primaryBranch once that API is available. - $: disabled = - !primaryBranch || - !currentBranch || - !projectLoaded || - alreadyOnPrimary || - isPublishing; + $: disabled = !projectLoaded || disabledPerGitStatus || isPublishing; // Prefetch prod's project parser commit SHA so the deploying page can // wait for prod to advance past it before redirecting to the dashboard, @@ -94,6 +97,22 @@ {}, ); + // Refetch local changes status, we predict this based on file watcher response. + // But we dont check if changes flipped to with changes to without changes. + hasLocalChanges = await fetchDeploymentGithubStatusChanges( + client, + queryClient, + primaryBranch, + ); + if (!hasLocalChanges && !hasChangesOnCurrent) { + eventBus.emit("notification", { + type: "default", + message: "No changes detected", + }); + isPublishing = false; + return; + } + try { if (hasLocalChanges) { await $gitPushMutation.mutateAsync({ @@ -193,7 +212,7 @@ {#if alreadyOnPrimary} Already on production - {:else if !primaryBranch || !currentBranch || !projectLoaded} + {:else if isPending || !projectLoaded} Loading project... {:else if !hasLocalChanges} No changes to publish diff --git a/web-admin/src/features/edit-session/selectors.ts b/web-admin/src/features/edit-session/selectors.ts new file mode 100644 index 00000000000..d811e3e4eb1 --- /dev/null +++ b/web-admin/src/features/edit-session/selectors.ts @@ -0,0 +1,101 @@ +import { RuntimeClient } from "@rilldata/web-common/runtime-client/v2"; +import { derived } from "svelte/store"; +import { + createRuntimeServiceGitStatus, + getRuntimeServiceGitStatusQueryKey, + runtimeServiceGitStatus, + type V1GitStatusResponse, +} from "@rilldata/web-common/runtime-client"; +import type { QueryClient } from "@tanstack/query-core"; + +export function getDeploymentGithubStatus( + runtimeClient: RuntimeClient, + primaryBranch: string | undefined, +) { + return derived( + [ + createRuntimeServiceGitStatus(runtimeClient, {}), + createRuntimeServiceGitStatus(runtimeClient, { + remoteBranch: primaryBranch, + }), + ], + ([currentBranchGitStatusResp, primaryBranchGitStatusResp]) => { + const isPending = + currentBranchGitStatusResp.isPending || + primaryBranchGitStatusResp.isPending; + const error = + currentBranchGitStatusResp.error || primaryBranchGitStatusResp.error; + if (isPending || error) { + return { + isPending, + error, + data: { + hasLocalChanges: false, + hasChangesOnCurrent: false, + alreadyOnPrimary: false, + disabledPerGitStatus: true, + }, + }; + } + + const currentBranch = currentBranchGitStatusResp.data?.branch ?? ""; + const hasChangesAgainstCurrent = Boolean( + currentBranchGitStatusResp.data?.localCommits || + currentBranchGitStatusResp.data?.localChanges, + ); + const hasChangesOnCurrent = Boolean( + primaryBranchGitStatusResp.data?.localCommits || + primaryBranchGitStatusResp.data?.localChanges, + ); + const hasLocalChanges = hasChangesAgainstCurrent || hasChangesOnCurrent; + + const alreadyOnPrimary = + !!primaryBranch && !!currentBranch && currentBranch === primaryBranch; + + const disabledPerGitStatus = + !primaryBranch || + !currentBranch || + alreadyOnPrimary || + !hasLocalChanges; + + return { + isPending: false, + error: undefined, + data: { + hasLocalChanges, + hasChangesOnCurrent, + alreadyOnPrimary, + disabledPerGitStatus, + }, + }; + }, + ); +} + +export async function fetchDeploymentGithubStatusChanges( + runtimeClient: RuntimeClient, + queryClient: QueryClient, + primaryBranch: string | undefined, +) { + const currentBranchGitStatusResp = await queryClient.fetchQuery({ + queryKey: getRuntimeServiceGitStatusQueryKey(runtimeClient.instanceId, {}), + queryFn: () => runtimeServiceGitStatus(runtimeClient, {}), + }); + const hasChangesAgainstCurrent = Boolean( + currentBranchGitStatusResp.localCommits || + currentBranchGitStatusResp.localChanges, + ); + + const primaryBranchGitStatusResp = + queryClient.getQueryData( + getRuntimeServiceGitStatusQueryKey(runtimeClient.instanceId, { + remoteBranch: primaryBranch, + }), + ); + const hasChangesAgainstPrimary = Boolean( + primaryBranchGitStatusResp?.localCommits || + primaryBranchGitStatusResp?.localChanges, + ); + + return hasChangesAgainstCurrent || hasChangesAgainstPrimary; +} diff --git a/web-common/src/runtime-client/invalidation/file-invalidators.spec.ts b/web-common/src/runtime-client/invalidation/file-invalidators.spec.ts index e6c2fb278a9..f238ab2f5f4 100644 --- a/web-common/src/runtime-client/invalidation/file-invalidators.spec.ts +++ b/web-common/src/runtime-client/invalidation/file-invalidators.spec.ts @@ -48,10 +48,12 @@ function fakeQueryClient() { invalidateQueries: vi.fn(), refetchQueries: vi.fn(), resetQueries: vi.fn(), + getQueryData: vi.fn(), } as unknown as QueryClient & { invalidateQueries: ReturnType; refetchQueries: ReturnType; resetQueries: ReturnType; + getQueryData: ReturnType; }; } @@ -229,10 +231,10 @@ describe("handleFileEvent", () => { ); const gitStatusKey = getRuntimeServiceGitStatusQueryKey(INSTANCE_ID, {}); - const gitHit = qc.invalidateQueries.mock.calls.some( - ([arg]) => - Array.isArray(arg.queryKey) && - JSON.stringify(arg.queryKey) === JSON.stringify(gitStatusKey), + const gitHit = qc.getQueryData.mock.calls.some( + ([queryKey]) => + Array.isArray(queryKey) && + JSON.stringify(queryKey) === JSON.stringify(gitStatusKey), ); expect(gitHit).toBe(true); }); diff --git a/web-common/src/runtime-client/invalidation/file-invalidators.ts b/web-common/src/runtime-client/invalidation/file-invalidators.ts index 436f66d4e72..9294ea875f8 100644 --- a/web-common/src/runtime-client/invalidation/file-invalidators.ts +++ b/web-common/src/runtime-client/invalidation/file-invalidators.ts @@ -9,6 +9,7 @@ import { getRuntimeServiceIssueDevJWTQueryKey, getRuntimeServiceListFilesQueryKey, V1FileEvent, + type V1GitStatusResponse, type V1WatchFilesResponse, } from "@rilldata/web-common/runtime-client"; import type { RuntimeClient } from "@rilldata/web-common/runtime-client/v2"; @@ -80,9 +81,7 @@ export async function handleFileEvent( } // Keep the cloud editor's commit button in sync with the working tree. - void queryClient.invalidateQueries({ - queryKey: getRuntimeServiceGitStatusQueryKey(instanceId, {}), - }); + resetGitStatusQuery(queryClient, instanceId); } // Throttle: when many files arrive at once (e.g. initial sync), one refetch @@ -95,3 +94,17 @@ export async function handleFileEvent( ); } } + +function resetGitStatusQuery(queryClient: QueryClient, instanceId: string) { + const queryKey = getRuntimeServiceGitStatusQueryKey(instanceId, {}); + + const existingQueryData = + queryClient.getQueryData(queryKey); + // Skip updating the cache if there is no query data. + if (!existingQueryData) return; + + queryClient.setQueryData(queryKey, { + ...existingQueryData, + localChanges: true, // Force localChanges=true + }); +}