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 @@ -4,7 +4,7 @@ import { useOptimisticCollectionActions } from "renderer/routes/_authenticated/h
import { useDeletingWorkspaces } from "renderer/routes/_authenticated/providers/DeletingWorkspacesProvider";
import { RenameBranchDialog } from "renderer/screens/main/components/WorkspaceSidebar/WorkspaceListItem/components";
import { useV2WorkspaceNotificationStatus } from "renderer/stores/v2-notifications";
import { useWorkspaceCreateFailuresStore } from "renderer/stores/workspace-creates";
import { useWorkspaceCreatesStore } from "renderer/stores/workspace-creates";
import { useDashboardSidebarHover } from "../../providers/DashboardSidebarHoverProvider";
import type { DashboardSidebarWorkspace } from "../../types";
import { DashboardSidebarDeleteDialog } from "../DashboardSidebarDeleteDialog";
Expand Down Expand Up @@ -83,7 +83,7 @@ export function DashboardSidebarWorkspaceItem({
const isDeleting = useDeletingWorkspaces().isDeleting(id);

const handleDismissInFlight = useCallback(() => {
useWorkspaceCreateFailuresStore.getState().clear(id);
useWorkspaceCreatesStore.getState().remove(id);
}, [id]);

const {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { useDashboardSidebarState } from "renderer/routes/_authenticated/hooks/u
import { useCollections } from "renderer/routes/_authenticated/providers/CollectionsProvider";
import { getVisibleSidebarWorkspaces } from "renderer/routes/_authenticated/providers/CollectionsProvider/dashboardSidebarLocal";
import { useLocalHostService } from "renderer/routes/_authenticated/providers/LocalHostServiceProvider";
import { useWorkspaceCreateFailuresStore } from "renderer/stores/workspace-creates";
import { useWorkspaceCreatesStore } from "renderer/stores/workspace-creates";
import type {
DashboardSidebarProject,
DashboardSidebarProjectChild,
Expand Down Expand Up @@ -132,23 +132,26 @@ export function useDashboardSidebarData() {
const { toggleProjectCollapsed } = useDashboardSidebarState();
const queryClient = useQueryClient();

// Failed workspace.create operations — backing v2_workspaces row was rolled
// back, but we keep the snapshot in renderer memory so the user can retry
// from the detail page or dismiss from the sidebar.
const failuresMap = useWorkspaceCreateFailuresStore(
(store) => store.failures,
);
const failureSidebarRows = useMemo(
// In-flight workspace.create operations. These don't have a backing DB row
// — they're kept in renderer memory until the real v2Workspaces row arrives
// via Electric sync (or until error/dismiss).
const inFlightEntries = useWorkspaceCreatesStore((store) => store.entries);
const inFlightSidebarRows = useMemo(
() =>
Object.entries(failuresMap).map(([id, entry]) => ({
id,
projectId: entry.snapshot.projectId,
hostId: entry.hostId,
name: entry.snapshot.name ?? "New workspace",
branchName:
entry.snapshot.branch ?? entry.snapshot.name ?? "New workspace",
})),
[failuresMap],
inFlightEntries
.filter((entry) => entry.snapshot.id !== undefined)
.map((entry) => ({
id: entry.snapshot.id as string,
projectId: entry.snapshot.projectId,
name: entry.snapshot.name ?? "New workspace",
branchName:
entry.snapshot.branch ?? entry.snapshot.name ?? "New workspace",
status:
entry.state === "creating"
? ("creating" as const)
: ("failed" as const),
})),
[inFlightEntries],
);

const { data: hosts = [] } = useLiveQuery(
Expand Down Expand Up @@ -243,7 +246,6 @@ export function useDashboardSidebarData() {
branch: workspaces.branch,
createdAt: workspaces.createdAt,
updatedAt: workspaces.updatedAt,
synced: workspaces.$synced,
tabOrder: sidebarWorkspaces.sidebarState.tabOrder,
sectionId: sidebarWorkspaces.sidebarState.sectionId,
isHidden: sidebarWorkspaces.sidebarState.isHidden,
Expand All @@ -261,13 +263,14 @@ export function useDashboardSidebarData() {
[rawSidebarWorkspaces],
);

const { data: localWorkspaceCandidates = [] } = useLiveQuery(
const { data: localMainWorkspaces = [] } = useLiveQuery(
(q) =>
q
.from({ workspaces: collections.v2Workspaces })
.innerJoin({ hosts: collections.v2Hosts }, ({ workspaces, hosts }) =>
eq(workspaces.hostId, hosts.machineId),
)
.where(({ workspaces }) => eq(workspaces.type, "main"))
.select(({ workspaces, hosts }) => ({
id: workspaces.id,
projectId: workspaces.projectId,
Expand All @@ -278,7 +281,8 @@ export function useDashboardSidebarData() {
branch: workspaces.branch,
createdAt: workspaces.createdAt,
updatedAt: workspaces.updatedAt,
synced: workspaces.$synced,
tabOrder: MAIN_WORKSPACE_TAB_ORDER,
sectionId: null as string | null,
})),
[collections],
);
Expand All @@ -287,40 +291,17 @@ export function useDashboardSidebarData() {
const sidebarProjectIds = new Set(
sidebarProjects.map((project) => project.id),
);
const autoIncluded = localWorkspaceCandidates
.filter((workspace) => {
if (localStateWorkspaceIds.has(workspace.id)) return false;
if (workspace.hostId !== machineId) return false;
if (!sidebarProjectIds.has(workspace.projectId)) return false;
if (workspace.type === "main") return true;
return workspace.type === "worktree" && workspace.synced === false;
})
.map((workspace) => ({
...workspace,
tabOrder:
workspace.type === "main"
? MAIN_WORKSPACE_TAB_ORDER
: PENDING_WORKSPACE_TAB_ORDER,
sectionId: null as string | null,
creationStatus:
workspace.synced === false ? ("creating" as const) : undefined,
}));
// Pinned rows (those with v2WorkspaceLocalState) keep showing the
// creating spinner until Electric confirms `$synced`. The detail page
// reads `$synced` directly off the row, so without this the sidebar
// would clear its spinner the moment local state was inserted in
// `onInsert`, while the detail page would still show
// `WorkspaceCreatingState` until the shape stream caught up.
const sidebarWithSyncMeta = sidebarWorkspaces.map((workspace) => ({
...workspace,
creationStatus:
workspace.synced === false ? ("creating" as const) : undefined,
}));

return [...autoIncluded, ...sidebarWithSyncMeta];
const autoLocalMainWorkspaces = localMainWorkspaces.filter(
(workspace) =>
!localStateWorkspaceIds.has(workspace.id) &&
workspace.hostId === machineId &&
sidebarProjectIds.has(workspace.projectId),
);

return [...autoLocalMainWorkspaces, ...sidebarWorkspaces];
}, [
localMainWorkspaces,
localStateWorkspaceIds,
localWorkspaceCandidates,
machineId,
sidebarProjects,
sidebarWorkspaces,
Expand Down Expand Up @@ -461,7 +442,6 @@ export function useDashboardSidebarData() {
behindCount: null,
createdAt: workspace.createdAt,
updatedAt: workspace.updatedAt,
creationStatus: workspace.creationStatus,
};

if (workspace.sectionId) {
Expand All @@ -484,25 +464,23 @@ export function useDashboardSidebarData() {
});
}

// Inject failed workspace.create rows from the renderer failure store.
// The optimistic v2_workspaces row was rolled back, so the only signal
// left is the snapshot we kept in memory for retry/dismiss.
for (const failure of failureSidebarRows) {
if (localStateWorkspaceIds.has(failure.id)) continue;
const project = projectsById.get(failure.projectId);
// Inject in-flight workspaces (creating / failed) from the renderer-side
// in-flight store.
for (const pw of inFlightSidebarRows) {
if (localStateWorkspaceIds.has(pw.id)) continue;
const project = projectsById.get(pw.projectId);
if (!project) continue;

const failedItem: DashboardSidebarWorkspace = {
id: failure.id,
projectId: failure.projectId,
hostId: failure.hostId,
hostType:
failure.hostId === machineId ? "local-device" : "remote-device",
const pendingItem: DashboardSidebarWorkspace = {
id: pw.id,
projectId: pw.projectId,
hostId: "",
hostType: "local-device",
type: "worktree",
hostIsOnline: null,
accentColor: null,
name: failure.name,
branch: failure.branchName,
name: pw.name,
branch: pw.branchName,
pullRequest: null,
repoUrl:
project.githubOwner && project.githubRepoName
Expand All @@ -514,14 +492,14 @@ export function useDashboardSidebarData() {
behindCount: null,
createdAt: new Date(),
updatedAt: new Date(),
creationStatus: "failed",
creationStatus: pw.status,
};

project.childEntries.push({
tabOrder: PENDING_WORKSPACE_TAB_ORDER,
child: {
type: "workspace",
workspace: failedItem,
workspace: pendingItem,
},
});
}
Expand Down Expand Up @@ -564,7 +542,7 @@ export function useDashboardSidebarData() {
}, [
machineId,
pullRequestsByWorkspaceId,
failureSidebarRows,
inFlightSidebarRows,
localStateWorkspaceIds,
sidebarProjects,
sidebarSections,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ 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";
import { useOpenNewWorkspaceModal } from "renderer/stores/new-workspace-modal";
import { WorkspaceCreatesManager } from "renderer/stores/workspace-creates";
import {
COLLAPSED_WORKSPACE_SIDEBAR_WIDTH,
DEFAULT_WORKSPACE_SIDEBAR_WIDTH,
Expand Down Expand Up @@ -127,6 +128,7 @@ function DashboardLayout() {

return (
<div className="flex h-full w-full overflow-hidden">
<WorkspaceCreatesManager />
{sidebarOutsideColumn && sidebarPanel}
<div className="flex flex-1 flex-col min-w-0 min-h-0">
<TopBar />
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,7 @@
import { Button } from "@superset/ui/button";
import { useNavigate } from "@tanstack/react-router";
import { AlertCircle, GitBranch } from "lucide-react";
import { useNavigateAwayFromWorkspace } from "renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/hooks/useNavigateAwayFromWorkspace";
import {
useWorkspaceCreateFailuresStore,
useWorkspaceCreates,
} from "renderer/stores/workspace-creates";
import { useWorkspaceCreates } from "renderer/stores/workspace-creates";

interface WorkspaceCreateErrorStateProps {
workspaceId: string;
Expand All @@ -19,22 +16,12 @@ export function WorkspaceCreateErrorState({
branch,
error,
}: WorkspaceCreateErrorStateProps) {
const { submit } = useWorkspaceCreates();
const navigateAway = useNavigateAwayFromWorkspace();

const handleRetry = () => {
const failure =
useWorkspaceCreateFailuresStore.getState().failures[workspaceId];
if (!failure) return;
void submit({ hostId: failure.hostId, snapshot: failure.snapshot });
};
const navigate = useNavigate();
const { retry, dismiss } = useWorkspaceCreates();

const handleDismiss = () => {
useWorkspaceCreateFailuresStore.getState().clear(workspaceId);
// `navigateAway` jumps to the next sidebar workspace when we're viewing
// the one being dismissed — falls back to the top sidebar entry since
// the failed id was never in the sidebar list, then to "/" if empty.
navigateAway(workspaceId);
dismiss(workspaceId);
void navigate({ to: "/v2-workspaces" });
};

return (
Expand Down Expand Up @@ -79,7 +66,7 @@ export function WorkspaceCreateErrorState({
</div>

<div className="flex items-center gap-2">
<Button size="sm" onClick={handleRetry}>
<Button size="sm" onClick={() => void retry(workspaceId)}>
Try again
</Button>
<Button size="sm" variant="ghost" onClick={handleDismiss}>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { createFileRoute, Outlet, useMatchRoute } from "@tanstack/react-router";
import { useEffect, useRef } from "react";
import { useDashboardSidebarState } from "renderer/routes/_authenticated/hooks/useDashboardSidebarState";
import { useCollections } from "renderer/routes/_authenticated/providers/CollectionsProvider";
import { useWorkspaceCreateFailuresStore } from "renderer/stores/workspace-creates";
import { useWorkspaceCreatesStore } from "renderer/stores/workspace-creates";
import { WorkspaceCreateErrorState } from "./components/WorkspaceCreateErrorState";
import { WorkspaceCreatingState } from "./components/WorkspaceCreatingState";
import { WorkspaceNotFoundState } from "./components/WorkspaceNotFoundState";
Expand Down Expand Up @@ -34,47 +34,41 @@ function V2WorkspaceLayout() {
[collections, workspaceId],
);
const workspace = workspaces?.[0] ?? null;
// Read `$synced` straight off the row — useLiveQuery returns rows enriched
// with virtual props, and changes to optimistic state retrigger the query.
const isSynced = workspace?.$synced ?? false;
const failure = useWorkspaceCreateFailuresStore((store) =>
workspaceId ? store.failures[workspaceId] : undefined,
const inFlight = useWorkspaceCreatesStore((store) =>
workspaceId
? store.entries.find((entry) => entry.snapshot.id === workspaceId)
: undefined,
);

const lastEnsuredWorkspaceIdRef = useRef<string | null>(null);
useEffect(() => {
if (!workspace || lastEnsuredWorkspaceIdRef.current === workspace.id)
return;
// Only pin to the sidebar once the workspace is confirmed by the
// backend — pinning an optimistic row produces a sidebar state row
// that has to be cleaned up on rollback.
if (!isSynced) return;
lastEnsuredWorkspaceIdRef.current = workspace.id;
ensureWorkspaceInSidebar(workspace.id, workspace.projectId);
}, [ensureWorkspaceInSidebar, workspace, isSynced]);
}, [ensureWorkspaceInSidebar, workspace]);

if (!workspaceId || !isReady || !workspaces) {
return <div className="flex h-full w-full" />;
}

if (workspace && !isSynced) {
return (
<WorkspaceCreatingState
name={workspace.name}
branch={workspace.branch}
startedAt={workspace.createdAt.getTime()}
/>
);
}

if (!workspace) {
if (failure) {
if (inFlight?.state === "creating") {
return (
<WorkspaceCreatingState
name={inFlight.snapshot.name}
branch={inFlight.snapshot.branch}
startedAt={inFlight.startedAt}
/>
);
}
if (inFlight?.state === "error") {
return (
<WorkspaceCreateErrorState
workspaceId={workspaceId}
name={failure.snapshot.name}
branch={failure.snapshot.branch}
error={failure.error}
name={inFlight.snapshot.name}
branch={inFlight.snapshot.branch}
error={inFlight.error ?? "Unknown error"}
/>
);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,26 @@ import {
} from "renderer/routes/_authenticated/components/utils/paneLifecycleRows";
import { useCollections } from "renderer/routes/_authenticated/providers/CollectionsProvider";
import type { AppCollections } from "renderer/routes/_authenticated/providers/CollectionsProvider/collections";
import {
getNextTabOrder,
getPrependTabOrder,
isSidebarWorkspaceVisible,
} from "renderer/routes/_authenticated/providers/CollectionsProvider/dashboardSidebarLocal";
import { isSidebarWorkspaceVisible } from "renderer/routes/_authenticated/providers/CollectionsProvider/dashboardSidebarLocal";
import { PROJECT_CUSTOM_COLORS } from "shared/constants/project-colors";

function getNextTabOrder(items: Array<{ tabOrder: number }>): number {
const maxTabOrder = items.reduce(
(maxValue, item) => Math.max(maxValue, item.tabOrder),
0,
);
return maxTabOrder + 1;
}

function getPrependTabOrder(items: Array<{ tabOrder: number }>): number {
if (items.length === 0) return 1;
const minTabOrder = items.reduce(
(minValue, item) => Math.min(minValue, item.tabOrder),
Number.POSITIVE_INFINITY,
);
return minTabOrder - 1;
}

type ProjectTopLevelItem = {
type: "workspace" | "section";
id: string;
Expand Down
Loading
Loading