From 0bcd600c443eba84e5f91776c123bccc70536c45 Mon Sep 17 00:00:00 2001 From: Kiet Ho Date: Thu, 2 Jul 2026 17:13:44 -0700 Subject: [PATCH 1/4] refactor(desktop): compact inline agent/port sections in v2 sidebar - Restyle port and agent chips as neutral Linear/Notion-style tags (bg-muted, 11px text, mono tabular port numbers) - Split ports and agents into separate collapsible sections with per-section persisted collapse state (workspaceId:sectionKey) - Hide section chevron until hover; align chips with the summary label - New DashboardSidebarChipStrip: one row while chips fit, two-row grid on overflow, then horizontal scroll two rows deep - Move per-port actions (open in browser, close) from hover icons into a right-click context menu so pills never resize on hover --- .../DashboardSidebarChipStrip.tsx | 76 +++++++++ .../DashboardSidebarChipStrip/index.ts | 1 + .../DashboardSidebarPortBadge.tsx | 150 +++++++++--------- .../DashboardSidebarPortGroup.tsx | 9 +- .../DashboardSidebarWorkspaceAgentsRow.tsx | 11 +- .../DashboardSidebarWorkspaceAgentBadge.tsx | 10 +- .../DashboardSidebarWorkspaceDetails.tsx | 63 ++++---- ...DashboardSidebarWorkspaceDetailsToggle.tsx | 2 +- .../DashboardSidebarWorkspacePortsRow.tsx | 11 +- .../stores/workspace-details/store.ts | 32 ++-- 10 files changed, 218 insertions(+), 147 deletions(-) create mode 100644 apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarChipStrip/DashboardSidebarChipStrip.tsx create mode 100644 apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarChipStrip/index.ts diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarChipStrip/DashboardSidebarChipStrip.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarChipStrip/DashboardSidebarChipStrip.tsx new file mode 100644 index 00000000000..093c66f755b --- /dev/null +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarChipStrip/DashboardSidebarChipStrip.tsx @@ -0,0 +1,76 @@ +import { OverflowFadeContainer } from "@superset/ui/overflow-fade-container"; +import { cn } from "@superset/ui/utils"; +import { + type ReactNode, + useCallback, + useLayoutEffect, + useRef, + useState, +} from "react"; + +/** Must match the `gap-1.5` class on the container below. */ +const CHIP_GAP_PX = 6; + +interface DashboardSidebarChipStripProps { + className?: string; + children: ReactNode; +} + +/** + * Strip of chips (ports, agents) that stays a single row while everything + * fits, wraps into a two-row grid once it doesn't, and scrolls horizontally + * (two rows deep) beyond that. + * + * Layout is chosen by comparing the chips' natural widths against the + * container, re-measured on every render and on container resizes. Pure-CSS + * child growth (e.g. a chip revealing hover actions) deliberately doesn't + * trigger a re-measure, so hovering can't flip the layout. + */ +export function DashboardSidebarChipStrip({ + className, + children, +}: DashboardSidebarChipStripProps) { + const containerRef = useRef(null); + const [isTwoRows, setIsTwoRows] = useState(false); + + const measure = useCallback(() => { + const node = containerRef.current; + if (!node) return; + const styles = getComputedStyle(node); + const available = + node.clientWidth - + Number.parseFloat(styles.paddingLeft) - + Number.parseFloat(styles.paddingRight); + let needed = -CHIP_GAP_PX; + for (const child of Array.from(node.children)) { + needed += (child as HTMLElement).offsetWidth + CHIP_GAP_PX; + } + setIsTwoRows(needed > available); + }, []); + + // No dependency array: chips mounting/unmounting re-renders this component, + // and re-measuring is cheap (setState bails when the boolean is unchanged). + useLayoutEffect(measure); + + useLayoutEffect(() => { + const node = containerRef.current; + if (!node) return; + const observer = new ResizeObserver(measure); + observer.observe(node); + return () => observer.disconnect(); + }, [measure]); + + return ( + + {children} + + ); +} diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarChipStrip/index.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarChipStrip/index.ts new file mode 100644 index 00000000000..d488adaebfe --- /dev/null +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarChipStrip/index.ts @@ -0,0 +1 @@ +export { DashboardSidebarChipStrip } from "./DashboardSidebarChipStrip"; diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarPortsList/components/DashboardSidebarPortBadge/DashboardSidebarPortBadge.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarPortsList/components/DashboardSidebarPortBadge/DashboardSidebarPortBadge.tsx index fd149f88b22..3efb3fa7820 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarPortsList/components/DashboardSidebarPortBadge/DashboardSidebarPortBadge.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarPortsList/components/DashboardSidebarPortBadge/DashboardSidebarPortBadge.tsx @@ -1,3 +1,9 @@ +import { + ContextMenu, + ContextMenuContent, + ContextMenuItem, + ContextMenuTrigger, +} from "@superset/ui/context-menu"; import { Tooltip, TooltipContent, TooltipTrigger } from "@superset/ui/tooltip"; import { cn } from "@superset/ui/utils"; import { useNavigate } from "@tanstack/react-router"; @@ -60,88 +66,82 @@ export function DashboardSidebarPortBadge({ }; return ( - - -
- - {canOpenInBrowser && ( + + + + - )} - -
-
- -
- {port.label &&
{port.label}
} -
- localhost:{port.port} -
-
{hostLabel}
- {(port.processName || port.pid != null) && ( -
- {port.processName} - {port.pid != null && ` (pid ${port.pid})`} + + + +
+ {port.label &&
{port.label}
} +
+ localhost:{port.port}
- )} - {!canOpenInBrowser && ( +
{hostLabel}
+ {(port.processName || port.pid != null) && ( +
+ {port.processName} + {port.pid != null && ` (pid ${port.pid})`} +
+ )} + {!canOpenInBrowser && ( +
+ Browser open unavailable from this device +
+ )}
- Browser open unavailable from this device + Click to open workspace · Right-click for actions
- )} -
- Click to open workspace
-
-
- + + + + {canOpenInBrowser && ( + + + Open in Browser + + )} + + + Close Port + + + ); } diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarPortsList/components/DashboardSidebarPortGroup/DashboardSidebarPortGroup.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarPortsList/components/DashboardSidebarPortGroup/DashboardSidebarPortGroup.tsx index 1df0201bc4c..252d006351e 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarPortsList/components/DashboardSidebarPortGroup/DashboardSidebarPortGroup.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarPortsList/components/DashboardSidebarPortGroup/DashboardSidebarPortGroup.tsx @@ -1,10 +1,10 @@ -import { OverflowFadeContainer } from "@superset/ui/overflow-fade-container"; import { Tooltip, TooltipContent, TooltipTrigger } from "@superset/ui/tooltip"; import { cn } from "@superset/ui/utils"; import { useNavigate } from "@tanstack/react-router"; import { LuLoaderCircle, LuX } from "react-icons/lu"; import { navigateToV2Workspace } from "renderer/routes/_authenticated/_dashboard/utils/workspace-navigation"; import { STROKE_WIDTH } from "renderer/screens/main/components/WorkspaceSidebar/constants"; +import { DashboardSidebarChipStrip } from "../../../DashboardSidebarChipStrip"; import { useDashboardSidebarPortKill } from "../../hooks/useDashboardSidebarPortKill"; import type { DashboardSidebarPortGroup as DashboardSidebarPortGroupType } from "../../hooks/useDashboardSidebarPortsData"; import { DashboardSidebarPortBadge } from "../DashboardSidebarPortBadge"; @@ -69,17 +69,14 @@ export function DashboardSidebarPortGroup({
- + {group.ports.map((port) => ( ))} - +
); } diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/components/DashboardSidebarWorkspaceAgentsRow/DashboardSidebarWorkspaceAgentsRow.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/components/DashboardSidebarWorkspaceAgentsRow/DashboardSidebarWorkspaceAgentsRow.tsx index 26eecf31b28..62ae785f552 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/components/DashboardSidebarWorkspaceAgentsRow/DashboardSidebarWorkspaceAgentsRow.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/components/DashboardSidebarWorkspaceAgentsRow/DashboardSidebarWorkspaceAgentsRow.tsx @@ -1,4 +1,5 @@ import { cn } from "@superset/ui/utils"; +import { DashboardSidebarChipStrip } from "../../../../components/DashboardSidebarChipStrip"; import { DashboardSidebarWorkspaceAgentBadge } from "./components/DashboardSidebarWorkspaceAgentBadge"; import { useDashboardSidebarWorkspaceRunningAgents } from "./hooks/useDashboardSidebarWorkspaceRunningAgents"; @@ -18,11 +19,9 @@ export function DashboardSidebarWorkspaceAgentsRow({ } return ( -
{agents.map((agent) => ( ))} -
+ ); } diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/components/DashboardSidebarWorkspaceAgentsRow/components/DashboardSidebarWorkspaceAgentBadge/DashboardSidebarWorkspaceAgentBadge.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/components/DashboardSidebarWorkspaceAgentsRow/components/DashboardSidebarWorkspaceAgentBadge/DashboardSidebarWorkspaceAgentBadge.tsx index 8f3a1a039dc..dd8d58d8b15 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/components/DashboardSidebarWorkspaceAgentsRow/components/DashboardSidebarWorkspaceAgentBadge/DashboardSidebarWorkspaceAgentBadge.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/components/DashboardSidebarWorkspaceAgentsRow/components/DashboardSidebarWorkspaceAgentBadge/DashboardSidebarWorkspaceAgentBadge.tsx @@ -42,17 +42,17 @@ export function DashboardSidebarWorkspaceAgentBadge({ type="button" onClick={handleClick} className={cn( - "inline-flex max-w-40 items-center gap-1 rounded-md px-1.5 py-0.5", - "bg-primary/10 text-xs font-medium text-primary transition-colors hover:bg-primary/20", + "inline-flex max-w-40 shrink-0 items-center gap-1 rounded px-1.5 py-0.5", + "bg-muted/60 text-[11px] text-muted-foreground transition-colors hover:bg-muted hover:text-foreground", "focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring", )} > - + {agent.status === "idle" ? ( iconUrl ? ( - + ) : ( - + ) ) : ( diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/components/DashboardSidebarWorkspaceDetails/DashboardSidebarWorkspaceDetails.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/components/DashboardSidebarWorkspaceDetails/DashboardSidebarWorkspaceDetails.tsx index bca10c28bb5..0e60130f970 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/components/DashboardSidebarWorkspaceDetails/DashboardSidebarWorkspaceDetails.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/components/DashboardSidebarWorkspaceDetails/DashboardSidebarWorkspaceDetails.tsx @@ -1,4 +1,4 @@ -import { Fragment, type ReactNode } from "react"; +import type { ReactNode } from "react"; import { LuX } from "react-icons/lu"; import { STROKE_WIDTH } from "renderer/screens/main/components/WorkspaceSidebar/constants"; import { useWorkspaceDetailsStore } from "renderer/stores"; @@ -30,19 +30,21 @@ interface DashboardSidebarWorkspaceDetailsProps { /** * Collapsible area rendered beneath a workspace row. It hosts the per-workspace - * "detail" rows (ports today, running agents / other status rows in future). + * "detail" sections (ports today, running agents / other status rows in + * future). Each section renders its own toggle row and collapses + * independently, keyed by `${workspaceId}:${sectionKey}`. * - * To add a new detail row: call its data hook unconditionally in the section - * registry below, then push a {@link WorkspaceDetailSection} when it has - * something to show. Everything else — the toggle, summary, header actions, - * persisted collapse state — is handled here. + * To add a new detail section: call its data hook unconditionally in the + * section registry below, then push a {@link WorkspaceDetailSection} when it + * has something to show. Everything else — the toggle, summary, header + * actions, persisted collapse state — is handled here. */ export function DashboardSidebarWorkspaceDetails({ workspaceId, isInSection = false, }: DashboardSidebarWorkspaceDetailsProps) { - const isCollapsed = useWorkspaceDetailsStore( - (state) => !!state.collapsedWorkspaceIds[workspaceId], + const collapsedSectionKeys = useWorkspaceDetailsStore( + (state) => state.collapsedSectionKeys, ); const toggleExpanded = useWorkspaceDetailsStore( (state) => state.toggleExpanded, @@ -101,9 +103,6 @@ export function DashboardSidebarWorkspaceDetails({ return null; } - const isExpanded = !isCollapsed; - const headerActions = sections.filter((section) => section.headerAction); - return ( // Stop pointer/touch starts from bubbling to the sortable workspace item's // drag listeners, so scrolling overflowing ports or pressing a port control @@ -114,28 +113,28 @@ export function DashboardSidebarWorkspaceDetails({ onMouseDown={(event) => event.stopPropagation()} onTouchStart={(event) => event.stopPropagation()} > -
- section.summary).join(" · ")} - isInSection={isInSection} - onToggle={() => toggleExpanded(workspaceId)} - /> - {headerActions.length > 0 && ( -
- {headerActions.map((section) => ( - {section.headerAction} - ))} + {sections.map((section) => { + const sectionKey = `${workspaceId}:${section.key}`; + const isExpanded = !collapsedSectionKeys[sectionKey]; + return ( +
+
+ toggleExpanded(sectionKey)} + /> + {section.headerAction && ( +
+ {section.headerAction} +
+ )} +
+ {isExpanded &&
{section.content}
}
- )} -
- {isExpanded && ( -
- {sections.map((section) => ( -
{section.content}
- ))} -
- )} + ); + })}
); } diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/components/DashboardSidebarWorkspaceDetails/components/DashboardSidebarWorkspaceDetailsToggle/DashboardSidebarWorkspaceDetailsToggle.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/components/DashboardSidebarWorkspaceDetails/components/DashboardSidebarWorkspaceDetailsToggle/DashboardSidebarWorkspaceDetailsToggle.tsx index 2e17c63fbf2..482efc35fed 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/components/DashboardSidebarWorkspaceDetails/components/DashboardSidebarWorkspaceDetailsToggle/DashboardSidebarWorkspaceDetailsToggle.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/components/DashboardSidebarWorkspaceDetails/components/DashboardSidebarWorkspaceDetailsToggle/DashboardSidebarWorkspaceDetailsToggle.tsx @@ -30,7 +30,7 @@ export function DashboardSidebarWorkspaceDetailsToggle({ > {group.ports.map((port) => ( ))} - + ); } diff --git a/apps/desktop/src/renderer/stores/workspace-details/store.ts b/apps/desktop/src/renderer/stores/workspace-details/store.ts index 0c9896acfed..c428e6fd99d 100644 --- a/apps/desktop/src/renderer/stores/workspace-details/store.ts +++ b/apps/desktop/src/renderer/stores/workspace-details/store.ts @@ -2,41 +2,41 @@ import { create } from "zustand"; import { devtools, persist } from "zustand/middleware"; interface WorkspaceDetailsState { - // Workspaces are expanded by default; we only persist the ones the user has - // explicitly collapsed so the map stays small and new workspaces show their - // details automatically. - collapsedWorkspaceIds: Record; + // Sections are expanded by default; we only persist the ones the user has + // explicitly collapsed so the map stays small and new sections show their + // details automatically. Keys are `${workspaceId}:${sectionKey}`. + collapsedSectionKeys: Record; - setExpanded: (workspaceId: string, expanded: boolean) => void; - toggleExpanded: (workspaceId: string) => void; + setExpanded: (sectionKey: string, expanded: boolean) => void; + toggleExpanded: (sectionKey: string) => void; } export const useWorkspaceDetailsStore = create()( devtools( persist( (set, get) => ({ - collapsedWorkspaceIds: {}, + collapsedSectionKeys: {}, - setExpanded: (workspaceId, expanded) => + setExpanded: (sectionKey, expanded) => set((state) => { - const next = { ...state.collapsedWorkspaceIds }; + const next = { ...state.collapsedSectionKeys }; if (expanded) { - delete next[workspaceId]; + delete next[sectionKey]; } else { - next[workspaceId] = true; + next[sectionKey] = true; } - return { collapsedWorkspaceIds: next }; + return { collapsedSectionKeys: next }; }), - toggleExpanded: (workspaceId) => { - const isCollapsed = !!get().collapsedWorkspaceIds[workspaceId]; - get().setExpanded(workspaceId, isCollapsed); + toggleExpanded: (sectionKey) => { + const isCollapsed = !!get().collapsedSectionKeys[sectionKey]; + get().setExpanded(sectionKey, isCollapsed); }, }), { name: "workspace-details-store", partialize: (state) => ({ - collapsedWorkspaceIds: state.collapsedWorkspaceIds, + collapsedSectionKeys: state.collapsedSectionKeys, }), }, ), From 3e5fb8345d9f1f0b69ec9247639c3ee185b28297 Mon Sep 17 00:00:00 2001 From: Kiet Ho Date: Thu, 2 Jul 2026 17:20:52 -0700 Subject: [PATCH 2/4] =?UTF-8?q?feat(desktop):=20port=20pill=20opens=20in?= =?UTF-8?q?=20browser=20on=20click,=20actions=20behind=20hover=20=E2=8B=AE?= =?UTF-8?q?=20menu?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Primary click on a port pill now opens the port (per the Settings → Links → Ports preference); remote ports fall back to opening the workspace. A vertical ellipsis fades in on hover (opacity only, so the pill never resizes) with Open in Browser / Go to Workspace / Close Port. --- .../DashboardSidebarPortBadge.tsx | 159 +++++++++++------- 1 file changed, 98 insertions(+), 61 deletions(-) diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarPortsList/components/DashboardSidebarPortBadge/DashboardSidebarPortBadge.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarPortsList/components/DashboardSidebarPortBadge/DashboardSidebarPortBadge.tsx index 3efb3fa7820..39713b3ab82 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarPortsList/components/DashboardSidebarPortBadge/DashboardSidebarPortBadge.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarPortsList/components/DashboardSidebarPortBadge/DashboardSidebarPortBadge.tsx @@ -1,13 +1,20 @@ import { - ContextMenu, - ContextMenuContent, - ContextMenuItem, - ContextMenuTrigger, -} from "@superset/ui/context-menu"; + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "@superset/ui/dropdown-menu"; import { Tooltip, TooltipContent, TooltipTrigger } from "@superset/ui/tooltip"; import { cn } from "@superset/ui/utils"; import { useNavigate } from "@tanstack/react-router"; -import { LuExternalLink, LuLoaderCircle, LuX } from "react-icons/lu"; +import { + LuEllipsisVertical, + LuExternalLink, + LuLoaderCircle, + LuSquareTerminal, + LuX, +} from "react-icons/lu"; import { useV2UserPreferences } from "renderer/hooks/useV2UserPreferences"; import { electronTrpc } from "renderer/lib/electron-trpc"; import { navigateToV2Workspace } from "renderer/routes/_authenticated/_dashboard/utils/workspace-navigation"; @@ -60,47 +67,46 @@ export function DashboardSidebarPortBadge({ }); }; + // Opening the port is the primary action; remote ports can't open a local + // browser tab, so clicking those jumps to the workspace instead. + const handlePrimaryClick = canOpenInBrowser + ? handleOpenInBrowser + : handleWorkspaceClick; + const handleClose = () => { if (isPending) return; void killPort(port); }; return ( - +
- - - - - + + +
{port.label &&
{port.label}
} @@ -116,32 +122,63 @@ export function DashboardSidebarPortBadge({ {port.pid != null && ` (pid ${port.pid})`}
)} - {!canOpenInBrowser && ( -
- Browser open unavailable from this device -
- )}
- Click to open workspace · Right-click for actions + {canOpenInBrowser + ? "Click to open in browser" + : "Click to open workspace"}
- - {canOpenInBrowser && ( - + + + + event.preventDefault()} + > + {canOpenInBrowser && ( + + + Open in Browser + + )} + + + Go to Workspace + + + - - Open in Browser - - )} - - - Close Port - - - + + Close Port + + + + ); } From 60e72c7c2bb31d92250686895f234105ba184904 Mon Sep 17 00:00:00 2001 From: Kiet Ho Date: Thu, 2 Jul 2026 17:25:06 -0700 Subject: [PATCH 3/4] feat(desktop): split port menu open action into external and Superset browser --- .../DashboardSidebarPortBadge.tsx | 62 +++++++++++++------ 1 file changed, 43 insertions(+), 19 deletions(-) diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarPortsList/components/DashboardSidebarPortBadge/DashboardSidebarPortBadge.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarPortsList/components/DashboardSidebarPortBadge/DashboardSidebarPortBadge.tsx index 39713b3ab82..ebce2cef1ff 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarPortsList/components/DashboardSidebarPortBadge/DashboardSidebarPortBadge.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarPortsList/components/DashboardSidebarPortBadge/DashboardSidebarPortBadge.tsx @@ -9,6 +9,7 @@ import { Tooltip, TooltipContent, TooltipTrigger } from "@superset/ui/tooltip"; import { cn } from "@superset/ui/utils"; import { useNavigate } from "@tanstack/react-router"; import { + LuAppWindow, LuEllipsisVertical, LuExternalLink, LuLoaderCircle, @@ -46,27 +47,36 @@ export function DashboardSidebarPortBadge({ }); }; - const handleOpenInBrowser = () => { - if (!canOpenInBrowser) return; + const portUrl = `http://localhost:${port.port}`; - // Where the port opens is configurable under Settings → Links → Ports. - const url = `http://localhost:${port.port}`; - if (preferences.portOpenAction === "external") { - if (openUrl.isPending) return; - openUrl.mutate(url); - return; - } + const handleOpenExternal = () => { + if (!canOpenInBrowser || openUrl.isPending) return; + openUrl.mutate(portUrl); + }; + const handleOpenInApp = (target: "new-tab" | "current-tab") => { + if (!canOpenInBrowser) return; void navigateToV2Workspace(port.workspaceId, navigate, { search: { - openUrl: url, - openUrlTarget: - preferences.portOpenAction === "newTab" ? "new-tab" : "current-tab", + openUrl: portUrl, + openUrlTarget: target, openUrlRequestId: crypto.randomUUID(), }, }); }; + // Where a plain click opens the port is configurable under + // Settings → Links → Ports. + const handleOpenInBrowser = () => { + if (preferences.portOpenAction === "external") { + handleOpenExternal(); + return; + } + handleOpenInApp( + preferences.portOpenAction === "newTab" ? "new-tab" : "current-tab", + ); + }; + // Opening the port is the primary action; remote ports can't open a local // browser tab, so clicking those jumps to the workspace instead. const handlePrimaryClick = canOpenInBrowser @@ -156,13 +166,27 @@ export function DashboardSidebarPortBadge({ onCloseAutoFocus={(event) => event.preventDefault()} > {canOpenInBrowser && ( - - - Open in Browser - + <> + + + Open in External Browser + + + handleOpenInApp( + preferences.portOpenAction === "pane" + ? "current-tab" + : "new-tab", + ) + } + > + + Open in Superset Browser + + )} From 2fc582fce96b9dcb9f90ec50356ab44218450d33 Mon Sep 17 00:00:00 2001 From: Kiet Ho Date: Thu, 2 Jul 2026 17:34:52 -0700 Subject: [PATCH 4/4] fix(desktop): address PR review feedback on sidebar chip strip - justify-items-start in 2-row grid so stretched chips don't inflate measured widths and lock the strip in two-row mode (greptile P1) - narrow the collapsed-sections selector to this workspace's keys so toggling one workspace doesn't re-render all panels (greptile P2) - migrate persisted v0 collapsedWorkspaceIds into per-section keys instead of dropping users' collapse state (coderabbit) --- .../DashboardSidebarChipStrip.tsx | 6 +++++- .../DashboardSidebarWorkspaceDetails.tsx | 10 +++++++++- .../stores/workspace-details/store.ts | 19 +++++++++++++++++++ 3 files changed, 33 insertions(+), 2 deletions(-) diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarChipStrip/DashboardSidebarChipStrip.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarChipStrip/DashboardSidebarChipStrip.tsx index 093c66f755b..9c655f498e7 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarChipStrip/DashboardSidebarChipStrip.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarChipStrip/DashboardSidebarChipStrip.tsx @@ -65,7 +65,11 @@ export function DashboardSidebarChipStrip({ ref={containerRef} observeChildren className={cn( - isTwoRows ? "grid auto-cols-max grid-flow-col grid-rows-2" : "flex", + // justify-items-start keeps chips at natural width in grid mode; + // stretched chips would inflate the offsetWidths measure() sums. + isTwoRows + ? "grid auto-cols-max grid-flow-col grid-rows-2 justify-items-start" + : "flex", "items-center gap-1.5 overflow-x-auto hide-scrollbar", className, )} diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/components/DashboardSidebarWorkspaceDetails/DashboardSidebarWorkspaceDetails.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/components/DashboardSidebarWorkspaceDetails/DashboardSidebarWorkspaceDetails.tsx index 0e60130f970..fadd2c36b3b 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/components/DashboardSidebarWorkspaceDetails/DashboardSidebarWorkspaceDetails.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/components/DashboardSidebarWorkspaceDetails/DashboardSidebarWorkspaceDetails.tsx @@ -4,6 +4,7 @@ import { STROKE_WIDTH } from "renderer/screens/main/components/WorkspaceSidebar/ import { useWorkspaceDetailsStore } from "renderer/stores"; import { useInlineWorkspacePortsEnabled } from "renderer/stores/inline-workspace-ports"; import { useWorkspaceAgentsRowEnabled } from "renderer/stores/workspace-agents-row"; +import { useShallow } from "zustand/react/shallow"; import { useDashboardSidebarWorkspacePorts } from "../../../../providers/DashboardSidebarPortsProvider"; import { useDashboardSidebarPortKill } from "../../../DashboardSidebarPortsList/hooks/useDashboardSidebarPortKill"; import { DashboardSidebarWorkspaceAgentsRow } from "../DashboardSidebarWorkspaceAgentsRow"; @@ -43,8 +44,15 @@ export function DashboardSidebarWorkspaceDetails({ workspaceId, isInSection = false, }: DashboardSidebarWorkspaceDetailsProps) { + // Narrowed to this workspace's section keys (matching the registry below) + // so toggling one workspace's sections doesn't re-render every other panel. const collapsedSectionKeys = useWorkspaceDetailsStore( - (state) => state.collapsedSectionKeys, + useShallow((state) => ({ + [`${workspaceId}:ports`]: + state.collapsedSectionKeys[`${workspaceId}:ports`], + [`${workspaceId}:agents`]: + state.collapsedSectionKeys[`${workspaceId}:agents`], + })), ); const toggleExpanded = useWorkspaceDetailsStore( (state) => state.toggleExpanded, diff --git a/apps/desktop/src/renderer/stores/workspace-details/store.ts b/apps/desktop/src/renderer/stores/workspace-details/store.ts index c428e6fd99d..b7a1d242341 100644 --- a/apps/desktop/src/renderer/stores/workspace-details/store.ts +++ b/apps/desktop/src/renderer/stores/workspace-details/store.ts @@ -35,6 +35,25 @@ export const useWorkspaceDetailsStore = create()( }), { name: "workspace-details-store", + version: 1, + // v0 persisted one `collapsedWorkspaceIds[workspaceId]` flag for the + // whole details area; expand it to every per-section key. + migrate: (persisted, version) => { + if (version === 0) { + const old = persisted as { + collapsedWorkspaceIds?: Record; + }; + const collapsedSectionKeys: Record = {}; + for (const workspaceId of Object.keys( + old.collapsedWorkspaceIds ?? {}, + )) { + collapsedSectionKeys[`${workspaceId}:ports`] = true; + collapsedSectionKeys[`${workspaceId}:agents`] = true; + } + return { collapsedSectionKeys }; + } + return persisted as { collapsedSectionKeys: Record }; + }, partialize: (state) => ({ collapsedSectionKeys: state.collapsedSectionKeys, }),