Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
b7f28bb
refactor(desktop): rewrite v1→v2 migration as pull-based importer (#4…
saddlepaddle May 6, 2026
4d1cdc8
refactor(desktop): drop v1_migration_state from importer UX (#4128)
saddlepaddle May 6, 2026
fb6094e
fix(desktop): place new v2 workspace at top of sidebar (#4139)
saddlepaddle May 6, 2026
82639e6
fix(desktop): prefer cloud-confirmed v2 project in v1→v2 workspace ad…
saddlepaddle May 6, 2026
22d5712
fix(desktop): unblock v2 workspace render when Electric is slow (#4141)
saddlepaddle May 6, 2026
b9e29a4
fix(desktop): remove v1→v2 import banner from dashboard (#4148)
saddlepaddle May 6, 2026
26ad0a7
feat(desktop): add v1 import intro page + fix preset import (#4151)
saddlepaddle May 6, 2026
a87df3d
feat(desktop): host-offline + version-mismatch screens for v2 workspa…
saddlepaddle May 7, 2026
f16bfc4
fix(desktop): resolve v2 default from local db, not stale localStorag…
saddlepaddle May 7, 2026
8b5b21e
fix(desktop): stop auto-opting users into v2 + onboarding (#4177)
saddlepaddle May 7, 2026
4132656
fix(desktop): make v1 import modal closable + responsive (#4179)
Kitenite May 7, 2026
62b8db4
fix(host-service): add v2Workspace.list + worktree-list dependencies
MocA-Love May 8, 2026
01db1e8
chore: drop unused upstream imports in V2PresetsSection
MocA-Love May 8, 2026
5933557
fix: drop V1ImportModal pages and narrow useRemoteHostStatus param
MocA-Love May 8, 2026
5d19666
ci: nudge workflow with content change
MocA-Love May 8, 2026
c3883b5
fix: ExperimentalSettings drop V1Import refs after V1ImportModal removal
MocA-Love May 8, 2026
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
80 changes: 6 additions & 74 deletions apps/desktop/src/lib/trpc/routers/migration/index.ts
Original file line number Diff line number Diff line change
@@ -1,30 +1,14 @@
import {
projects,
v1MigrationState,
workspaceSections,
workspaces,
worktrees,
} from "@superset/local-db";
import { eq, isNotNull, isNull } from "drizzle-orm";
import { projects, workspaces, worktrees } from "@superset/local-db";
import { isNotNull, isNull } from "drizzle-orm";
import { localDb } from "main/lib/local-db";
import { z } from "zod";
import { publicProcedure, router } from "../..";

const migrationStateRowSchema = z.object({
v1Id: z.string().min(1),
kind: z.enum(["project", "workspace"]),
v2Id: z.string().nullable(),
organizationId: z.string().min(1),
status: z.enum(["success", "linked", "error", "skipped"]),
reason: z.string().nullable().optional(),
});

export const createMigrationRouter = () => {
return router({
readV1Projects: publicProcedure.query(() => {
// Only migrate pinned projects. v1's `hideProject` nulls tab_order when
// the last workspace in a project is deleted, effectively abandoning the
// project — don't resurrect those in v2.
// Only surface pinned projects. v1's `hideProject` nulls tab_order
// when the last workspace in a project is deleted, effectively
// abandoning the project — don't resurrect those in v2.
return localDb
.select()
.from(projects)
Expand All @@ -43,58 +27,6 @@ export const createMigrationRouter = () => {
readV1Worktrees: publicProcedure.query(() => {
return localDb.select().from(worktrees).all();
}),

readV1WorkspaceSections: publicProcedure.query(() => {
return localDb.select().from(workspaceSections).all();
}),

listState: publicProcedure
.input(z.object({ organizationId: z.string().min(1) }))
.query(({ input }) => {
return localDb
.select()
.from(v1MigrationState)
.where(eq(v1MigrationState.organizationId, input.organizationId))
.all();
}),

upsertState: publicProcedure
.input(migrationStateRowSchema)
.mutation(({ input }) => {
localDb
.insert(v1MigrationState)
.values({
v1Id: input.v1Id,
kind: input.kind,
v2Id: input.v2Id,
organizationId: input.organizationId,
status: input.status,
reason: input.reason ?? null,
migratedAt: Date.now(),
})
.onConflictDoUpdate({
target: [
v1MigrationState.organizationId,
v1MigrationState.v1Id,
v1MigrationState.kind,
],
set: {
v2Id: input.v2Id,
status: input.status,
reason: input.reason ?? null,
migratedAt: Date.now(),
},
})
.run();
}),

clearState: publicProcedure
.input(z.object({ organizationId: z.string().min(1) }))
.mutation(({ input }) => {
localDb
.delete(v1MigrationState)
.where(eq(v1MigrationState.organizationId, input.organizationId))
.run();
}),
});
};
// CI rerun trigger
14 changes: 1 addition & 13 deletions apps/desktop/src/main/lib/host-service-coordinator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import * as fs from "node:fs";
import path from "node:path";
import { settings } from "@superset/local-db";
import { getHostId, getHostName } from "@superset/shared/host-info";
import { MIN_HOST_SERVICE_VERSION } from "@superset/shared/host-version";
import { app } from "electron";
import { env } from "main/env.main";
import semver from "semver";
Expand All @@ -30,19 +31,6 @@ import { localDb } from "./local-db";
import { killPersistentScope, spawnPersistent } from "./process-persistence";
import { HOOK_PROTOCOL_VERSION } from "./terminal/env";

/**
* Minimum host-service version this app can work with. Bumping this forces
* the coordinator to kill + respawn any adopted service older than this,
* which is how we prevent the renderer from talking to a stale host-service
* that's missing newly-added procedures/params.
*
* 0.3.0: host-service registers via cloud `host.ensure` (was
* `device.ensureV2Host`); v2_hosts/v2_users_hosts/v2_workspaces use
* machineId text instead of uuid surrogates.
* 0.2.0: `workspaceCreation.adopt` gained optional `worktreePath`.
*/
const MIN_HOST_SERVICE_VERSION = "0.3.0";

export type HostServiceStatus = "starting" | "running" | "stopped";

export interface Connection {
Expand Down
6 changes: 5 additions & 1 deletion apps/desktop/src/renderer/hooks/useIsV2CloudEnabled.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,15 @@ const IS_DEV = process.env.NODE_ENV === "development";
/**
* Returns effective v2 state: remote PostHog flag AND local opt-in.
* Also returns the raw remote flag so the toggle can be shown conditionally.
*
* FORK NOTE: keeps the upstream-style `optInV2 === true` strict check
* (since `optInV2` is now nullable per #4176) but preserves the fork's
* object-shape return value with the extra `isRemoteV2Enabled` flag.
*/
export function useIsV2CloudEnabled() {
const remoteV2Enabled =
useFeatureFlagEnabled(FEATURE_FLAGS.V2_CLOUD) ?? false;
const optInV2 = useV2LocalOverrideStore((s) => s.optInV2);
const optInV2 = useV2LocalOverrideStore((s) => s.optInV2 === true);

if (IS_DEV) {
return {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ import { useHotkey } from "renderer/hotkeys";
import { electronTrpc } from "renderer/lib/electron-trpc";
import { DashboardSidebar } from "renderer/routes/_authenticated/_dashboard/components/DashboardSidebar";
import { useDevSeedV2Sidebar } from "renderer/routes/_authenticated/hooks/useDevSeedV2Sidebar";
import { useMigrateV1DataToV2 } from "renderer/routes/_authenticated/hooks/useMigrateV1DataToV2";
import { ResizablePanel } from "renderer/screens/main/components/ResizablePanel";
import { WorkspaceSidebar } from "renderer/screens/main/components/WorkspaceSidebar";
import { DeleteWorkspaceDialog } from "renderer/screens/main/components/WorkspaceSidebar/WorkspaceListItem/components";
Expand All @@ -41,7 +40,6 @@ function DashboardLayout() {
const openNewWorkspaceModal = useOpenNewWorkspaceModal();
const { isV2CloudEnabled } = useIsV2CloudEnabled();
useDevSeedV2Sidebar();
useMigrateV1DataToV2();
// Get current workspace from route to pre-select project in new workspace modal
const matchRoute = useMatchRoute();
const currentWorkspaceMatch = matchRoute({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ import {
} from "renderer/assets/app-icons/preset-icons";
import { HotkeyMenuShortcut } from "renderer/components/HotkeyMenuShortcut";
import type { HotkeyId } from "renderer/hotkeys";
import { useMigrateV1PresetsToV2 } from "renderer/routes/_authenticated/hooks/useMigrateV1PresetsToV2";
import { useCollections } from "renderer/routes/_authenticated/providers/CollectionsProvider";
import type { V2TerminalPresetRow } from "renderer/routes/_authenticated/providers/CollectionsProvider/dashboardSidebarLocal";
import { V2PresetBarItem } from "./components/V2PresetBarItem";
Expand Down Expand Up @@ -68,7 +67,6 @@ export function V2PresetsBar({
const navigate = useNavigate();
const isDark = useIsDarkTheme();
const collections = useCollections();
useMigrateV1PresetsToV2();

const [localVisiblePresetIds, setLocalVisiblePresetIds] = useState<string[]>(
() => getVisiblePresetOrder(matchedPresets),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import { Button } from "@superset/ui/button";
import { Link } from "@tanstack/react-router";
import { ArrowRight, ArrowUpCircle, Monitor } from "lucide-react";

interface WorkspaceHostIncompatibleStateProps {
hostName: string;
hostVersion: string;
minVersion: string;
}

export function WorkspaceHostIncompatibleState({
hostName,
hostVersion,
minVersion,
}: WorkspaceHostIncompatibleStateProps) {
return (
<div className="flex h-full w-full items-center justify-center p-6">
<div className="flex w-full max-w-sm flex-col items-start gap-6">
<div className="relative">
<div className="grid size-10 place-items-center rounded-lg border border-border/60 bg-muted/30">
<Monitor
className="size-[18px] text-muted-foreground"
strokeWidth={1.5}
aria-hidden="true"
/>
</div>
<span
aria-hidden="true"
className="absolute -bottom-0.5 -right-0.5 grid size-3.5 place-items-center rounded-full bg-amber-500/90 text-background ring-2 ring-background"
>
<ArrowUpCircle className="size-2.5" strokeWidth={3} />
</span>
</div>

<div className="flex flex-col gap-1.5">
<h1 className="text-[15px] font-medium tracking-tight text-foreground">
Host needs an update
</h1>
<p className="select-text cursor-text text-[13px] leading-relaxed text-muted-foreground">
This workspace's host is on an older version of Superset than this
client supports. Update the Superset app on that device to
reconnect.
</p>
</div>

<div className="flex w-full flex-col gap-0 overflow-hidden rounded-md border border-border/60 bg-muted/30">
<div className="flex items-center gap-2.5 px-3 py-2">
<span
aria-hidden="true"
className="size-1.5 shrink-0 rounded-full bg-emerald-500"
/>
<span
className="select-text cursor-text min-w-0 truncate text-[13px] font-medium text-foreground"
title={hostName}
>
{hostName}
</span>
</div>
<div className="border-t border-border/60 px-3 py-2">
<div className="flex items-center justify-between gap-3">
<span className="text-[11px] uppercase tracking-wider text-muted-foreground/70">
Running
</span>
<code className="select-text cursor-text font-mono text-[12px] tabular-nums text-foreground">
{hostVersion}
</code>
</div>
<div className="mt-1 flex items-center justify-between gap-3">
<span className="text-[11px] uppercase tracking-wider text-muted-foreground/70">
Required
</span>
<code className="select-text cursor-text font-mono text-[12px] tabular-nums text-muted-foreground">
≥ {minVersion}
</code>
</div>
</div>
</div>

<Button
asChild
size="sm"
variant="ghost"
className="-ml-2 h-7 gap-1.5 px-2 text-[13px] font-medium text-foreground hover:bg-muted/60"
>
<Link to="/v2-workspaces">
Browse workspaces
<ArrowRight
className="size-3.5"
strokeWidth={2}
aria-hidden="true"
/>
</Link>
</Button>
</div>
</div>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { WorkspaceHostIncompatibleState } from "./WorkspaceHostIncompatibleState";
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { Button } from "@superset/ui/button";
import { Link } from "@tanstack/react-router";
import { ArrowRight, Monitor } from "lucide-react";

interface WorkspaceHostOfflineStateProps {
hostName: string;
}

export function WorkspaceHostOfflineState({
hostName,
}: WorkspaceHostOfflineStateProps) {
return (
<div className="flex h-full w-full items-center justify-center p-6">
<div className="flex w-full max-w-sm flex-col items-start gap-6">
<div className="relative">
<div className="grid size-10 place-items-center rounded-lg border border-border/60 bg-muted/30">
<Monitor
className="size-[18px] text-muted-foreground"
strokeWidth={1.5}
aria-hidden="true"
/>
</div>
<span
aria-hidden="true"
className="absolute -bottom-0.5 -right-0.5 size-2.5 rounded-full bg-muted-foreground/50 ring-2 ring-background"
/>
</div>

<div className="flex flex-col gap-1.5">
<h1 className="text-[15px] font-medium tracking-tight text-foreground">
Host is offline
</h1>
<p className="select-text cursor-text text-[13px] leading-relaxed text-muted-foreground">
This workspace lives on a device that isn't reachable right now.
Open Superset on that device to bring the workspace back online.
</p>
</div>

<div className="flex w-full items-center gap-2.5 rounded-md border border-border/60 bg-muted/30 px-3 py-2">
<span
aria-hidden="true"
className="size-1.5 shrink-0 rounded-full bg-muted-foreground/50"
/>
<span
className="select-text cursor-text min-w-0 truncate text-[13px] font-medium text-foreground"
title={hostName}
>
{hostName}
</span>
<span className="ml-auto shrink-0 text-[11px] uppercase tracking-wider text-muted-foreground/70">
Offline
</span>
</div>

<Button
asChild
size="sm"
variant="ghost"
className="-ml-2 h-7 gap-1.5 px-2 text-[13px] font-medium text-foreground hover:bg-muted/60"
>
<Link to="/v2-workspaces">
Browse workspaces
<ArrowRight
className="size-3.5"
strokeWidth={2}
aria-hidden="true"
/>
</Link>
</Button>
</div>
</div>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { WorkspaceHostOfflineState } from "./WorkspaceHostOfflineState";
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export {
type RemoteHostStatus,
useRemoteHostStatus,
} from "./useRemoteHostStatus";
Loading
Loading