From a9b549f49e4bbbdc4945413820e558d2bda53f0c Mon Sep 17 00:00:00 2001 From: AviPeltz Date: Sun, 2 Aug 2026 18:53:31 -0700 Subject: [PATCH 1/2] feat(desktop): add a top bar ports dropdown as an alternative to the sidebar chip MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Extend the ports-layout experiment from a single inline chip to a two-way display mode: "inline" (a chip under each workspace item, unchanged default) or "topbar" (a compact pill with the live port count in the persistent sidebar-header chrome — or the TopBar/tab-bar trailing slot when that chrome isn't visible). Its dropdown lists every detected port grouped by workspace with per-row open/copy/close, a per-workspace close-all on each group header, and a global close-all footer. A single DashboardSidebarPortsProvider in the dashboard layout backs both modes so port data survives the workspace-navigation remounts that the TopBar/tab-bar mounts sit inside — the pane store is now also seeded synchronously from its persisted layout instead of starting empty, so the tab bar (and the pill/background-terminals button inside it) no longer blinks on every workspace switch. The old three-mode design (a "panel" consolidated at the sidebar bottom) was prototyped and dropped before shipping; this history skips straight to the two-mode result, so useDashboardSidebarPortsData and useDashboardSidebarPortKill live directly under DashboardSidebar/hooks/ alongside usePortOpenActions rather than modeling their interim home under the never-shipped panel component. Claude-Session: https://claude.ai/code/session_018hKiDErNgDrZCGMwQcRjdn --- .../DashboardSidebar/DashboardSidebar.tsx | 247 +++++++++--------- .../DashboardSidebarHeader.tsx | 6 +- .../DashboardSidebarPortsList.tsx | 73 ------ .../DashboardSidebarPortBadge.tsx | 163 ------------ .../DashboardSidebarPortBadge/index.ts | 1 - .../DashboardSidebarPortGroup.tsx | 82 ------ .../DashboardSidebarPortGroup/index.ts | 1 - .../DashboardSidebarPortsList/index.ts | 1 - .../DashboardSidebarPortsChip.tsx | 4 +- .../DashboardSidebarPortHoverRow.tsx | 4 +- .../DashboardSidebarWorkspaceContextMenu.tsx | 2 +- .../useDashboardSidebarPortKill/index.ts | 0 .../useDashboardSidebarPortKill.ts | 0 .../useDashboardSidebarPortsData/index.ts | 0 .../useDashboardSidebarPortsData.test.ts | 0 .../useDashboardSidebarPortsData.ts | 0 .../useDashboardSidebarPortsData.utils.ts | 2 +- .../usePortOpenActions/usePortOpenActions.ts | 2 +- .../DashboardSidebarPortsProvider.tsx | 2 +- .../_dashboard/components/TopBar/TopBar.tsx | 4 + .../TopBarPortsDropdown.tsx | 90 +++++++ .../TopBarPortRow/TopBarPortRow.tsx | 107 ++++++++ .../components/TopBarPortRow/index.ts | 1 + .../TopBarPortsFooter/TopBarPortsFooter.tsx | 54 ++++ .../components/TopBarPortsFooter/index.ts | 1 + .../TopBarPortsGroup/TopBarPortsGroup.tsx | 90 +++++++ .../components/TopBarPortsGroup/index.ts | 1 + .../components/TopBarPortsDropdown/index.ts | 1 + .../_authenticated/_dashboard/layout.tsx | 121 +++++---- .../useV2WorkspacePaneLayout.ts | 30 ++- .../v2-workspace/$workspaceId/page.tsx | 18 +- .../ExperimentalSettings.tsx | 20 +- .../utils/settings-search/settings-search.ts | 7 +- .../renderer/stores/inline-workspace-ports.ts | 80 ++++-- 34 files changed, 660 insertions(+), 555 deletions(-) delete mode 100644 apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarPortsList/DashboardSidebarPortsList.tsx delete mode 100644 apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarPortsList/components/DashboardSidebarPortBadge/DashboardSidebarPortBadge.tsx delete mode 100644 apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarPortsList/components/DashboardSidebarPortBadge/index.ts delete mode 100644 apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarPortsList/components/DashboardSidebarPortGroup/DashboardSidebarPortGroup.tsx delete mode 100644 apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarPortsList/components/DashboardSidebarPortGroup/index.ts delete mode 100644 apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarPortsList/index.ts rename apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/{components/DashboardSidebarPortsList => }/hooks/useDashboardSidebarPortKill/index.ts (100%) rename apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/{components/DashboardSidebarPortsList => }/hooks/useDashboardSidebarPortKill/useDashboardSidebarPortKill.ts (100%) rename apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/{components/DashboardSidebarPortsList => }/hooks/useDashboardSidebarPortsData/index.ts (100%) rename apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/{components/DashboardSidebarPortsList => }/hooks/useDashboardSidebarPortsData/useDashboardSidebarPortsData.test.ts (100%) rename apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/{components/DashboardSidebarPortsList => }/hooks/useDashboardSidebarPortsData/useDashboardSidebarPortsData.ts (100%) rename apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/{components/DashboardSidebarPortsList => }/hooks/useDashboardSidebarPortsData/useDashboardSidebarPortsData.utils.ts (98%) create mode 100644 apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/TopBar/components/TopBarPortsDropdown/TopBarPortsDropdown.tsx create mode 100644 apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/TopBar/components/TopBarPortsDropdown/components/TopBarPortRow/TopBarPortRow.tsx create mode 100644 apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/TopBar/components/TopBarPortsDropdown/components/TopBarPortRow/index.ts create mode 100644 apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/TopBar/components/TopBarPortsDropdown/components/TopBarPortsFooter/TopBarPortsFooter.tsx create mode 100644 apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/TopBar/components/TopBarPortsDropdown/components/TopBarPortsFooter/index.ts create mode 100644 apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/TopBar/components/TopBarPortsDropdown/components/TopBarPortsGroup/TopBarPortsGroup.tsx create mode 100644 apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/TopBar/components/TopBarPortsDropdown/components/TopBarPortsGroup/index.ts create mode 100644 apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/TopBar/components/TopBarPortsDropdown/index.ts diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/DashboardSidebar.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/DashboardSidebar.tsx index a7f2be26a50..41b62b4e81d 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/DashboardSidebar.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/DashboardSidebar.tsx @@ -31,13 +31,11 @@ import { useHotkeyDisplay } from "renderer/hotkeys"; import { OrganizationDropdown } from "renderer/routes/_authenticated/_dashboard/components/TopBar/components/OrganizationDropdown"; import { useDashboardSidebarState } from "renderer/routes/_authenticated/hooks/useDashboardSidebarState"; import { useLocalHostService } from "renderer/routes/_authenticated/providers/LocalHostServiceProvider"; -import { useInlineWorkspacePortsEnabled } from "renderer/stores/inline-workspace-ports"; import { useSidebarWorkspacesCollapseStore } from "renderer/stores/sidebar-workspaces-collapse"; import { DashboardSidebarBulkActions } from "./components/DashboardSidebarBulkActions"; import { DashboardSidebarHeader } from "./components/DashboardSidebarHeader"; import { DashboardSidebarHoverCardOverlay } from "./components/DashboardSidebarHoverCardOverlay"; import { DashboardSidebarPinnedSection } from "./components/DashboardSidebarPinnedSection"; -import { DashboardSidebarPortsList } from "./components/DashboardSidebarPortsList"; import { DashboardSidebarProjectSection } from "./components/DashboardSidebarProjectSection"; import { DashboardSidebarSectionRenameProvider } from "./components/DashboardSidebarSectionRenameContext"; import { DashboardSidebarWorkspacesHeader } from "./components/DashboardSidebarWorkspacesHeader"; @@ -45,7 +43,6 @@ import { V2SetupScriptCard } from "./components/V2SetupScriptCard"; import { useDashboardSidebarData } from "./hooks/useDashboardSidebarData"; import { useDashboardSidebarShortcuts } from "./hooks/useDashboardSidebarShortcuts"; import { DashboardSidebarHoverProvider } from "./providers/DashboardSidebarHoverProvider"; -import { DashboardSidebarPortsProvider } from "./providers/DashboardSidebarPortsProvider"; import { DashboardSidebarSelectionProvider } from "./providers/DashboardSidebarSelectionProvider"; import type { DashboardSidebarProject } from "./types"; import { getProjectChildrenWorkspaces } from "./utils/projectChildren"; @@ -118,7 +115,6 @@ export function DashboardSidebar({ const settingsHotkey = useHotkeyDisplay("OPEN_SETTINGS").text; const isSettingsOpen = !!matchRoute({ to: "/settings", fuzzy: true }); const { activeHostUrl } = useLocalHostService(); - const inlineWorkspacePortsEnabled = useInlineWorkspacePortsEnabled(); const v2RouteMatch = matchRoute({ to: "/v2-workspace/$workspaceId" }); const activeV2WorkspaceId = v2RouteMatch ? v2RouteMatch.workspaceId : null; const workspacesListCollapsed = useSidebarWorkspacesCollapseStore( @@ -220,138 +216,133 @@ export function DashboardSidebar({ > - - -
- + {/* Port data comes from the single DashboardSidebarPortsProvider in + the dashboard layout, which wraps this sidebar. */} + +
+ - {!isCollapsed && ( - - - - )} + {!isCollapsed && ( + + + + )} - - {(isCollapsed || !workspacesListCollapsed) && ( - - )} - {(isCollapsed || !workspacesListCollapsed) && ( - { - const project = groups.find((p) => p.id === active.id); - setActiveProject(project ?? null); - }} - onDragEnd={handleDragEnd} - onDragCancel={() => setActiveProject(null)} + + {(isCollapsed || !workspacesListCollapsed) && ( + + )} + {(isCollapsed || !workspacesListCollapsed) && ( + { + const project = groups.find((p) => p.id === active.id); + setActiveProject(project ?? null); + }} + onDragEnd={handleDragEnd} + onDragCancel={() => setActiveProject(null)} + > + - - {orderedGroups.map((project) => ( - - ))} - + {orderedGroups.map((project) => ( + + ))} + - {createPortal( - - {activeProject && ( - // Transparent on purpose: the sidebar surface comes from - // window vibrancy, so any opaque bg renders as a solid - // slab. Sortable siblings make room, so the row floats - // over empty sidebar, not over other rows. -
- {}} - onToggleCollapse={() => {}} - /> -
- )} -
, - document.body, - )} -
- )} -
- {!isCollapsed && !inlineWorkspacePortsEnabled && ( - + {createPortal( + + {activeProject && ( + // Transparent on purpose: the sidebar surface comes from + // window vibrancy, so any opaque bg renders as a solid + // slab. Sortable siblings make room, so the row floats + // over empty sidebar, not over other rows. +
+ {}} + onToggleCollapse={() => {}} + /> +
+ )} +
, + document.body, + )} +
)} - {!isCollapsed && activeV2Project && activeHostUrl && ( - +
+ {!isCollapsed && activeV2Project && activeHostUrl && ( + + )} + +
+ {isCollapsed ? ( + + ) : ( +
+ +
)} - -
- {isCollapsed ? ( - - ) : ( -
- -
- )} - - - - - - - {settingsHotkey !== "Unassigned" - ? `Settings (${settingsHotkey})` - : "Settings"} - - -
+ + + + + + + {settingsHotkey !== "Unassigned" + ? `Settings (${settingsHotkey})` + : "Settings"} + +
- - +
+
diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarHeader/DashboardSidebarHeader.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarHeader/DashboardSidebarHeader.tsx index 9052ba8310f..d05ea6ed361 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarHeader/DashboardSidebarHeader.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarHeader/DashboardSidebarHeader.tsx @@ -27,6 +27,7 @@ import { electronTrpc } from "renderer/lib/electron-trpc"; import { useFolderFirstImport } from "renderer/routes/_authenticated/_dashboard/components/AddRepositoryModals/hooks/useFolderFirstImport"; import { NavigationControls } from "renderer/routes/_authenticated/_dashboard/components/NavigationControls"; import { SidebarToggle } from "renderer/routes/_authenticated/_dashboard/components/SidebarToggle"; +import { TopBarPortsDropdown } from "renderer/routes/_authenticated/_dashboard/components/TopBar/components/TopBarPortsDropdown"; import { useFailedAutomations } from "renderer/routes/_authenticated/_dashboard/hooks/useFailedAutomations"; import { tasksSearchFromFilters, @@ -328,9 +329,12 @@ export function DashboardSidebarHeader({ className="drag h-full shrink-0" style={{ width: isMac ? `${80 / zoomFactor}px` : "8px" }} /> - + + {/* Lives here (persistent chrome) rather than the workspace tab + bar, which remounts on every navigation. */} +
diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarPortsList/DashboardSidebarPortsList.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarPortsList/DashboardSidebarPortsList.tsx deleted file mode 100644 index 2fc623a1c0a..00000000000 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarPortsList/DashboardSidebarPortsList.tsx +++ /dev/null @@ -1,73 +0,0 @@ -import { COMPANY } from "@superset/shared/constants"; -import { Tooltip, TooltipContent, TooltipTrigger } from "@superset/ui/tooltip"; -import { LuChevronRight, LuCircleHelp, LuRadioTower } from "react-icons/lu"; -import { STROKE_WIDTH } from "renderer/screens/main/components/WorkspaceSidebar/constants"; -import { usePortsStore } from "renderer/stores"; -import { useDashboardSidebarAllPorts } from "../../providers/DashboardSidebarPortsProvider"; -import { DashboardSidebarPortGroup } from "./components/DashboardSidebarPortGroup"; - -const PORTS_DOCS_URL = `${COMPANY.DOCS_URL}/ports`; - -export function DashboardSidebarPortsList() { - const isCollapsed = usePortsStore((state) => state.isListCollapsed); - const toggleCollapsed = usePortsStore((state) => state.toggleListCollapsed); - const { totalPortCount, workspacePortGroups } = useDashboardSidebarAllPorts(); - - if (totalPortCount === 0) { - return null; - } - - const handleOpenPortsDocs = (e: React.MouseEvent) => { - e.stopPropagation(); - window.open(PORTS_DOCS_URL, "_blank"); - }; - - return ( -
-
- - - - - - - -

Learn about port labels

-
-
- {totalPortCount} -
- {!isCollapsed && ( -
- {workspacePortGroups.map((group) => ( - - ))} -
- )} -
- ); -} 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 deleted file mode 100644 index 324fee40a6a..00000000000 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarPortsList/components/DashboardSidebarPortBadge/DashboardSidebarPortBadge.tsx +++ /dev/null @@ -1,163 +0,0 @@ -import { - 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 { - LuAppWindow, - LuEllipsisVertical, - LuExternalLink, - LuLoaderCircle, - LuSquareTerminal, - LuX, -} from "react-icons/lu"; -import { STROKE_WIDTH } from "renderer/screens/main/components/WorkspaceSidebar/constants"; -import { usePortOpenActions } from "../../../../hooks/usePortOpenActions"; -import { useDashboardSidebarPortKill } from "../../hooks/useDashboardSidebarPortKill"; -import type { DashboardSidebarPort } from "../../hooks/useDashboardSidebarPortsData"; - -interface DashboardSidebarPortBadgeProps { - port: DashboardSidebarPort; -} - -export function DashboardSidebarPortBadge({ - port, -}: DashboardSidebarPortBadgeProps) { - const { isPending, killPort } = useDashboardSidebarPortKill(); - const { - canOpenInBrowser, - isOpenExternalPending, - portOpenAction, - openExternal, - openInApp, - openWorkspace, - openPrimary, - } = usePortOpenActions(port); - const hostLabel = - port.hostType === "local-device" ? "Local device" : "Remote host"; - - const handleClose = () => { - if (isPending) return; - void killPort(port); - }; - - return ( -
- - - - - -
- {port.label &&
{port.label}
} -
- localhost:{port.port} -
-
{hostLabel}
- {(port.processName || port.pid != null) && ( -
- {port.processName} - {port.pid != null && ` (pid ${port.pid})`} -
- )} -
- {canOpenInBrowser - ? "Click to open in browser" - : "Click to open workspace"} -
-
-
-
- - - - - event.preventDefault()} - > - {canOpenInBrowser && ( - <> - - - Open in External Browser - - - openInApp( - portOpenAction === "pane" ? "current-tab" : "new-tab", - ) - } - > - - Open in Superset Browser - - - )} - - - Go to Workspace - - - - - Close Port - - - -
- ); -} diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarPortsList/components/DashboardSidebarPortBadge/index.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarPortsList/components/DashboardSidebarPortBadge/index.ts deleted file mode 100644 index 098aca2af7d..00000000000 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarPortsList/components/DashboardSidebarPortBadge/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { DashboardSidebarPortBadge } from "./DashboardSidebarPortBadge"; 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 deleted file mode 100644 index 67616b84b93..00000000000 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarPortsList/components/DashboardSidebarPortGroup/DashboardSidebarPortGroup.tsx +++ /dev/null @@ -1,82 +0,0 @@ -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"; - -interface DashboardSidebarPortGroupProps { - group: DashboardSidebarPortGroupType; -} - -export function DashboardSidebarPortGroup({ - group, -}: DashboardSidebarPortGroupProps) { - const navigate = useNavigate(); - const { isPending, killPorts } = useDashboardSidebarPortKill(); - - const handleWorkspaceClick = () => { - void navigateToV2Workspace(group.workspaceId, navigate); - }; - - const handleCloseAll = () => { - if (isPending) return; - void killPorts(group.ports); - }; - - return ( -
-
- - - {group.hostType === "local-device" ? "Local" : "Remote"} - - - - - - -

Close all ports

-
-
-
- - {group.ports.map((port) => ( - - ))} - -
- ); -} diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarPortsList/components/DashboardSidebarPortGroup/index.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarPortsList/components/DashboardSidebarPortGroup/index.ts deleted file mode 100644 index 9ba80d10775..00000000000 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarPortsList/components/DashboardSidebarPortGroup/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { DashboardSidebarPortGroup } from "./DashboardSidebarPortGroup"; diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarPortsList/index.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarPortsList/index.ts deleted file mode 100644 index b231559d058..00000000000 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarPortsList/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { DashboardSidebarPortsList } from "./DashboardSidebarPortsList"; diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/components/DashboardSidebarExpandedWorkspaceRow/components/DashboardSidebarWorkspaceChips/components/DashboardSidebarPortsChip/DashboardSidebarPortsChip.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/components/DashboardSidebarExpandedWorkspaceRow/components/DashboardSidebarWorkspaceChips/components/DashboardSidebarPortsChip/DashboardSidebarPortsChip.tsx index a877289f414..b1cdc983b82 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/components/DashboardSidebarExpandedWorkspaceRow/components/DashboardSidebarWorkspaceChips/components/DashboardSidebarPortsChip/DashboardSidebarPortsChip.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/components/DashboardSidebarExpandedWorkspaceRow/components/DashboardSidebarWorkspaceChips/components/DashboardSidebarPortsChip/DashboardSidebarPortsChip.tsx @@ -8,8 +8,8 @@ import { Separator } from "@superset/ui/separator"; import { toast } from "@superset/ui/sonner"; import { cn } from "@superset/ui/utils"; import { LuLoaderCircle, LuRadioTower, LuX } from "react-icons/lu"; -import { useDashboardSidebarPortKill } from "renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarPortsList/hooks/useDashboardSidebarPortKill"; -import type { DashboardSidebarPort } from "renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarPortsList/hooks/useDashboardSidebarPortsData"; +import { useDashboardSidebarPortKill } from "renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/hooks/useDashboardSidebarPortKill"; +import type { DashboardSidebarPort } from "renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/hooks/useDashboardSidebarPortsData"; import { STROKE_WIDTH } from "renderer/screens/main/components/WorkspaceSidebar/constants"; import { useDashboardSidebarChipHoverSuppression } from "../../hooks/useDashboardSidebarChipHoverSuppression"; import { DashboardSidebarPortHoverRow } from "./components/DashboardSidebarPortHoverRow"; diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/components/DashboardSidebarExpandedWorkspaceRow/components/DashboardSidebarWorkspaceChips/components/DashboardSidebarPortsChip/components/DashboardSidebarPortHoverRow/DashboardSidebarPortHoverRow.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/components/DashboardSidebarExpandedWorkspaceRow/components/DashboardSidebarWorkspaceChips/components/DashboardSidebarPortsChip/components/DashboardSidebarPortHoverRow/DashboardSidebarPortHoverRow.tsx index 55b619810a2..62d8f77f22b 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/components/DashboardSidebarExpandedWorkspaceRow/components/DashboardSidebarWorkspaceChips/components/DashboardSidebarPortsChip/components/DashboardSidebarPortHoverRow/DashboardSidebarPortHoverRow.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/components/DashboardSidebarExpandedWorkspaceRow/components/DashboardSidebarWorkspaceChips/components/DashboardSidebarPortsChip/components/DashboardSidebarPortHoverRow/DashboardSidebarPortHoverRow.tsx @@ -1,6 +1,6 @@ import { LuX } from "react-icons/lu"; -import { useDashboardSidebarPortKill } from "renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarPortsList/hooks/useDashboardSidebarPortKill"; -import type { DashboardSidebarPort } from "renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarPortsList/hooks/useDashboardSidebarPortsData"; +import { useDashboardSidebarPortKill } from "renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/hooks/useDashboardSidebarPortKill"; +import type { DashboardSidebarPort } from "renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/hooks/useDashboardSidebarPortsData"; import { usePortOpenActions } from "renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/hooks/usePortOpenActions"; import { STROKE_WIDTH } from "renderer/screens/main/components/WorkspaceSidebar/constants"; diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/components/DashboardSidebarWorkspaceContextMenu/DashboardSidebarWorkspaceContextMenu.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/components/DashboardSidebarWorkspaceContextMenu/DashboardSidebarWorkspaceContextMenu.tsx index 3d191083634..71c4f9a7066 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/components/DashboardSidebarWorkspaceContextMenu/DashboardSidebarWorkspaceContextMenu.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/components/DashboardSidebarWorkspaceContextMenu/DashboardSidebarWorkspaceContextMenu.tsx @@ -29,10 +29,10 @@ import { LuX, } from "react-icons/lu"; import { useHotkeyDisplay } from "renderer/hotkeys"; +import { useDashboardSidebarPortKill } from "renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/hooks/useDashboardSidebarPortKill"; import { useCollections } from "renderer/routes/_authenticated/providers/CollectionsProvider"; import { useDashboardSidebarHover } from "../../../../providers/DashboardSidebarHoverProvider"; import { useDashboardSidebarWorkspacePorts } from "../../../../providers/DashboardSidebarPortsProvider"; -import { useDashboardSidebarPortKill } from "../../../DashboardSidebarPortsList/hooks/useDashboardSidebarPortKill"; interface DashboardSidebarWorkspaceContextMenuProps { workspaceId: string; diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarPortsList/hooks/useDashboardSidebarPortKill/index.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/hooks/useDashboardSidebarPortKill/index.ts similarity index 100% rename from apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarPortsList/hooks/useDashboardSidebarPortKill/index.ts rename to apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/hooks/useDashboardSidebarPortKill/index.ts diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarPortsList/hooks/useDashboardSidebarPortKill/useDashboardSidebarPortKill.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/hooks/useDashboardSidebarPortKill/useDashboardSidebarPortKill.ts similarity index 100% rename from apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarPortsList/hooks/useDashboardSidebarPortKill/useDashboardSidebarPortKill.ts rename to apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/hooks/useDashboardSidebarPortKill/useDashboardSidebarPortKill.ts diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarPortsList/hooks/useDashboardSidebarPortsData/index.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/hooks/useDashboardSidebarPortsData/index.ts similarity index 100% rename from apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarPortsList/hooks/useDashboardSidebarPortsData/index.ts rename to apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/hooks/useDashboardSidebarPortsData/index.ts diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarPortsList/hooks/useDashboardSidebarPortsData/useDashboardSidebarPortsData.test.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/hooks/useDashboardSidebarPortsData/useDashboardSidebarPortsData.test.ts similarity index 100% rename from apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarPortsList/hooks/useDashboardSidebarPortsData/useDashboardSidebarPortsData.test.ts rename to apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/hooks/useDashboardSidebarPortsData/useDashboardSidebarPortsData.test.ts diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarPortsList/hooks/useDashboardSidebarPortsData/useDashboardSidebarPortsData.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/hooks/useDashboardSidebarPortsData/useDashboardSidebarPortsData.ts similarity index 100% rename from apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarPortsList/hooks/useDashboardSidebarPortsData/useDashboardSidebarPortsData.ts rename to apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/hooks/useDashboardSidebarPortsData/useDashboardSidebarPortsData.ts diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarPortsList/hooks/useDashboardSidebarPortsData/useDashboardSidebarPortsData.utils.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/hooks/useDashboardSidebarPortsData/useDashboardSidebarPortsData.utils.ts similarity index 98% rename from apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarPortsList/hooks/useDashboardSidebarPortsData/useDashboardSidebarPortsData.utils.ts rename to apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/hooks/useDashboardSidebarPortsData/useDashboardSidebarPortsData.utils.ts index 52de50e3026..5b7c7634375 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarPortsList/hooks/useDashboardSidebarPortsData/useDashboardSidebarPortsData.utils.ts +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/hooks/useDashboardSidebarPortsData/useDashboardSidebarPortsData.utils.ts @@ -1,7 +1,7 @@ import { buildHostRoutingKey } from "@superset/shared/host-routing"; import type { PortChangedPayload } from "@superset/workspace-client"; import type { DetectedPort } from "shared/types"; -import type { DashboardSidebarWorkspaceHostType } from "../../../../types"; +import type { DashboardSidebarWorkspaceHostType } from "../../types"; export interface DashboardSidebarPort extends RemotePort { hostId: string; diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/hooks/usePortOpenActions/usePortOpenActions.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/hooks/usePortOpenActions/usePortOpenActions.ts index 0b83f6dde7c..151d57ae795 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/hooks/usePortOpenActions/usePortOpenActions.ts +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/hooks/usePortOpenActions/usePortOpenActions.ts @@ -1,9 +1,9 @@ import { useNavigate } from "@tanstack/react-router"; import { useV2UserPreferences } from "renderer/hooks/useV2UserPreferences"; import { electronTrpc } from "renderer/lib/electron-trpc"; +import type { DashboardSidebarPort } from "renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/hooks/useDashboardSidebarPortsData"; import { navigateToV2Workspace } from "renderer/routes/_authenticated/_dashboard/utils/workspace-navigation"; import type { LinkAction } from "renderer/routes/_authenticated/providers/CollectionsProvider/dashboardSidebarLocal/schema"; -import type { DashboardSidebarPort } from "../../components/DashboardSidebarPortsList/hooks/useDashboardSidebarPortsData"; interface UsePortOpenActionsResult { canOpenInBrowser: boolean; diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/providers/DashboardSidebarPortsProvider/DashboardSidebarPortsProvider.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/providers/DashboardSidebarPortsProvider/DashboardSidebarPortsProvider.tsx index 1761c9fb731..880f1faa7ae 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/providers/DashboardSidebarPortsProvider/DashboardSidebarPortsProvider.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/providers/DashboardSidebarPortsProvider/DashboardSidebarPortsProvider.tsx @@ -2,7 +2,7 @@ import { createContext, type ReactNode, useContext, useMemo } from "react"; import { type DashboardSidebarPortGroup, useDashboardSidebarPortsData, -} from "../../components/DashboardSidebarPortsList/hooks/useDashboardSidebarPortsData"; +} from "renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/hooks/useDashboardSidebarPortsData"; interface DashboardSidebarPortsContextValue { workspacePortGroups: DashboardSidebarPortGroup[]; diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/TopBar/TopBar.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/TopBar/TopBar.tsx index ae10028e1d4..dcf387a1d55 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/TopBar/TopBar.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/TopBar/TopBar.tsx @@ -12,6 +12,7 @@ import { OpenInMenuButton } from "./components/OpenInMenuButton"; import { OrganizationDropdown } from "./components/OrganizationDropdown"; import { ResourceConsumption } from "./components/ResourceConsumption"; import { RightSidebarToggle } from "./components/RightSidebarToggle"; +import { TopBarPortsDropdown } from "./components/TopBarPortsDropdown"; import { V2WorkspaceTitle } from "./components/V2WorkspaceTitle"; import { WindowControls } from "./components/WindowControls"; @@ -79,6 +80,9 @@ export function TopBar() {
+ {/* When the expanded sidebar hosts the chrome, its header also hosts + the ports pill — don't render a duplicate here. */} + {!sidebarHostsChrome && } {!isOnline && (
diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/TopBar/components/TopBarPortsDropdown/TopBarPortsDropdown.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/TopBar/components/TopBarPortsDropdown/TopBarPortsDropdown.tsx new file mode 100644 index 00000000000..ee4e4a3a4b0 --- /dev/null +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/TopBar/components/TopBarPortsDropdown/TopBarPortsDropdown.tsx @@ -0,0 +1,90 @@ +import { Popover, PopoverContent, PopoverTrigger } from "@superset/ui/popover"; +import { Tooltip, TooltipContent, TooltipTrigger } from "@superset/ui/tooltip"; +import { useState } from "react"; +import { LuRadioTower } from "react-icons/lu"; +import { useIsV2CloudEnabled } from "renderer/hooks/useIsV2CloudEnabled"; +import { useDashboardSidebarAllPorts } from "renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/providers/DashboardSidebarPortsProvider"; +import { STROKE_WIDTH } from "renderer/screens/main/components/WorkspaceSidebar/constants"; +import { usePortsDisplayMode } from "renderer/stores/inline-workspace-ports"; +import { TopBarPortsFooter } from "./components/TopBarPortsFooter"; +import { TopBarPortsGroup } from "./components/TopBarPortsGroup"; + +/** + * Top-bar entry point for the "topbar" ports layout: a compact pill showing + * the live port count, opening a dropdown that lists every detected port + * across all visible workspaces and hosts. + * + * Also mounted in the v2 workspace tab bar's trailing slot, which replaces + * the top bar on that route. Port data comes from the + * DashboardSidebarPortsProvider instance in the dashboard layout — this + * component remounts on workspace navigation, so it must not own the data + * (the first empty-data frames would blink the pill out on every remount). + */ +interface TopBarPortsDropdownProps { + align?: "start" | "end"; +} + +export function TopBarPortsDropdown({ + align = "end", +}: TopBarPortsDropdownProps) { + const isV2CloudEnabled = useIsV2CloudEnabled(); + const portsDisplayMode = usePortsDisplayMode(); + + if (!isV2CloudEnabled || portsDisplayMode !== "topbar") { + return null; + } + return ; +} + +function TopBarPortsDropdownInner({ align }: { align: "start" | "end" }) { + const [open, setOpen] = useState(false); + const { workspacePortGroups, totalPortCount } = useDashboardSidebarAllPorts(); + + if (totalPortCount === 0) { + return null; + } + + const workspaceCount = workspacePortGroups.length; + + return ( + + + + + + + + +

+ {totalPortCount === 1 ? "1 port" : `${totalPortCount} ports`} across{" "} + {workspaceCount === 1 + ? "1 workspace" + : `${workspaceCount} workspaces`} +

+
+
+ +
+ {workspacePortGroups.map((group) => ( + setOpen(false)} + /> + ))} +
+ +
+
+ ); +} diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/TopBar/components/TopBarPortsDropdown/components/TopBarPortRow/TopBarPortRow.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/TopBar/components/TopBarPortsDropdown/components/TopBarPortRow/TopBarPortRow.tsx new file mode 100644 index 00000000000..54d1a8d4506 --- /dev/null +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/TopBar/components/TopBarPortsDropdown/components/TopBarPortRow/TopBarPortRow.tsx @@ -0,0 +1,107 @@ +import { toast } from "@superset/ui/sonner"; +import { cn } from "@superset/ui/utils"; +import { LuCopy, LuExternalLink, LuLoaderCircle, LuX } from "react-icons/lu"; +import { useDashboardSidebarPortKill } from "renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/hooks/useDashboardSidebarPortKill"; +import type { DashboardSidebarPort } from "renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/hooks/useDashboardSidebarPortsData"; +import { usePortOpenActions } from "renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/hooks/usePortOpenActions"; +import { STROKE_WIDTH } from "renderer/screens/main/components/WorkspaceSidebar/constants"; + +interface TopBarPortRowProps { + port: DashboardSidebarPort; + // Close the dropdown after an action that moves focus elsewhere (opening + // the port or jumping to its workspace). + onNavigate: () => void; +} + +/** + * One port in the top-bar dropdown: a fixed mono port column, the label and + * process, and a hover-revealed action cluster (open / copy / close). + */ +export function TopBarPortRow({ port, onNavigate }: TopBarPortRowProps) { + const { isPending, killPort } = useDashboardSidebarPortKill(); + const { canOpenInBrowser, portUrl, openExternal, openPrimary } = + usePortOpenActions(port); + + const description = [port.label, port.processName] + .filter(Boolean) + .join(" · "); + + const handleOpen = () => { + openPrimary(); + onNavigate(); + }; + + const handleOpenExternal = () => { + openExternal(); + onNavigate(); + }; + + const handleCopy = () => { + void navigator.clipboard.writeText(portUrl); + toast.success(`Copied ${portUrl.replace(/^https?:\/\//, "")}`); + }; + + const handleClose = () => { + if (isPending) return; + void killPort(port); + }; + + return ( +
+ +
+ {canOpenInBrowser && ( + + )} + + +
+
+ ); +} diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/TopBar/components/TopBarPortsDropdown/components/TopBarPortRow/index.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/TopBar/components/TopBarPortsDropdown/components/TopBarPortRow/index.ts new file mode 100644 index 00000000000..62a34d75801 --- /dev/null +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/TopBar/components/TopBarPortsDropdown/components/TopBarPortRow/index.ts @@ -0,0 +1 @@ +export { TopBarPortRow } from "./TopBarPortRow"; diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/TopBar/components/TopBarPortsDropdown/components/TopBarPortsFooter/TopBarPortsFooter.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/TopBar/components/TopBarPortsDropdown/components/TopBarPortsFooter/TopBarPortsFooter.tsx new file mode 100644 index 00000000000..f397fa2212d --- /dev/null +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/TopBar/components/TopBarPortsDropdown/components/TopBarPortsFooter/TopBarPortsFooter.tsx @@ -0,0 +1,54 @@ +import { toast } from "@superset/ui/sonner"; +import { LuLoaderCircle, LuX } from "react-icons/lu"; +import { useDashboardSidebarPortKill } from "renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/hooks/useDashboardSidebarPortKill"; +import type { DashboardSidebarPortGroup } from "renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/hooks/useDashboardSidebarPortsData"; +import { STROKE_WIDTH } from "renderer/screens/main/components/WorkspaceSidebar/constants"; + +interface TopBarPortsFooterProps { + groups: DashboardSidebarPortGroup[]; + totalPortCount: number; +} + +/** Dropdown footer: live-port total plus close-all across every workspace. */ +export function TopBarPortsFooter({ + groups, + totalPortCount, +}: TopBarPortsFooterProps) { + const { isPending, killPorts } = useDashboardSidebarPortKill(); + + const handleCloseAll = async () => { + if (isPending) return; + const results = await killPorts(groups.flatMap((group) => group.ports)); + const closedCount = results.filter((result) => result.success).length; + if (closedCount > 0) { + toast.success( + closedCount === 1 ? "Closed 1 port" : `Closed ${closedCount} ports`, + ); + } + }; + + return ( +
+ + {totalPortCount === 1 ? "1 live port" : `${totalPortCount} live ports`} + + +
+ ); +} diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/TopBar/components/TopBarPortsDropdown/components/TopBarPortsFooter/index.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/TopBar/components/TopBarPortsDropdown/components/TopBarPortsFooter/index.ts new file mode 100644 index 00000000000..68b6567687f --- /dev/null +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/TopBar/components/TopBarPortsDropdown/components/TopBarPortsFooter/index.ts @@ -0,0 +1 @@ +export { TopBarPortsFooter } from "./TopBarPortsFooter"; diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/TopBar/components/TopBarPortsDropdown/components/TopBarPortsGroup/TopBarPortsGroup.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/TopBar/components/TopBarPortsDropdown/components/TopBarPortsGroup/TopBarPortsGroup.tsx new file mode 100644 index 00000000000..0eb501bd2ad --- /dev/null +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/TopBar/components/TopBarPortsDropdown/components/TopBarPortsGroup/TopBarPortsGroup.tsx @@ -0,0 +1,90 @@ +import { toast } from "@superset/ui/sonner"; +import { Tooltip, TooltipContent, TooltipTrigger } from "@superset/ui/tooltip"; +import { useNavigate } from "@tanstack/react-router"; +import { LuLoaderCircle, LuX } from "react-icons/lu"; +import { useDashboardSidebarPortKill } from "renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/hooks/useDashboardSidebarPortKill"; +import type { DashboardSidebarPortGroup } from "renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/hooks/useDashboardSidebarPortsData"; +import { navigateToV2Workspace } from "renderer/routes/_authenticated/_dashboard/utils/workspace-navigation"; +import { STROKE_WIDTH } from "renderer/screens/main/components/WorkspaceSidebar/constants"; +import { TopBarPortRow } from "../TopBarPortRow"; + +interface TopBarPortsGroupProps { + group: DashboardSidebarPortGroup; + onNavigate: () => void; +} + +/** + * One workspace's ports in the top-bar dropdown: a header that navigates to + * the workspace (with a hover-revealed close-all for the group), then a row + * per port. + */ +export function TopBarPortsGroup({ group, onNavigate }: TopBarPortsGroupProps) { + const navigate = useNavigate(); + const { isPending, killPorts } = useDashboardSidebarPortKill(); + + const handleWorkspaceClick = () => { + void navigateToV2Workspace(group.workspaceId, navigate); + onNavigate(); + }; + + const handleCloseAll = async () => { + if (isPending) return; + const results = await killPorts(group.ports); + const closedCount = results.filter((result) => result.success).length; + if (closedCount > 0) { + toast.success( + closedCount === 1 ? "Closed 1 port" : `Closed ${closedCount} ports`, + ); + } + }; + + return ( +
+
+ + {group.hostType !== "local-device" && ( + + remote + + )} + + + + + +

Close all ports in this workspace

+
+
+
+ {group.ports.map((port) => ( + + ))} +
+ ); +} diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/TopBar/components/TopBarPortsDropdown/components/TopBarPortsGroup/index.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/TopBar/components/TopBarPortsDropdown/components/TopBarPortsGroup/index.ts new file mode 100644 index 00000000000..95b242d5906 --- /dev/null +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/TopBar/components/TopBarPortsDropdown/components/TopBarPortsGroup/index.ts @@ -0,0 +1 @@ +export { TopBarPortsGroup } from "./TopBarPortsGroup"; diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/TopBar/components/TopBarPortsDropdown/index.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/TopBar/components/TopBarPortsDropdown/index.ts new file mode 100644 index 00000000000..a339b6b71c1 --- /dev/null +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/TopBar/components/TopBarPortsDropdown/index.ts @@ -0,0 +1 @@ +export { TopBarPortsDropdown } from "./TopBarPortsDropdown"; diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/layout.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/layout.tsx index 95270a01c75..d2b98999232 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/layout.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/layout.tsx @@ -12,12 +12,14 @@ import { useHotkey } from "renderer/hotkeys"; import { electronTrpc } from "renderer/lib/electron-trpc"; import { DashboardSidebar } from "renderer/routes/_authenticated/_dashboard/components/DashboardSidebar"; import { DashboardSidebarDeleteDialog } from "renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarDeleteDialog"; +import { DashboardSidebarPortsProvider } from "renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/providers/DashboardSidebarPortsProvider"; import { useDashboardSidebarState } from "renderer/routes/_authenticated/hooks/useDashboardSidebarState"; import { useDevSeedV2Sidebar } from "renderer/routes/_authenticated/hooks/useDevSeedV2Sidebar"; import { useHostWorkspaces } from "renderer/routes/_authenticated/providers/HostWorkspacesProvider"; 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 { usePortsDisplayMode } from "renderer/stores/inline-workspace-ports"; import { useOpenNewWorkspaceModal } from "renderer/stores/new-workspace-modal"; import { COLLAPSED_WORKSPACE_SIDEBAR_WIDTH, @@ -51,6 +53,7 @@ function DashboardLayout() { const navigate = useNavigate(); const openNewWorkspaceModal = useOpenNewWorkspaceModal(); const isV2CloudEnabled = useIsV2CloudEnabled(); + const portsDisplayMode = usePortsDisplayMode(); const { workspaces: hostWorkspaces } = useHostWorkspaces(); const { removeWorkspaceFromSidebar } = useDashboardSidebarState(); useDevSeedV2Sidebar(); @@ -219,59 +222,77 @@ function DashboardLayout() { ((onNewWorkspaceRoute || onDashboardViewRoute) && sidebarOutsideColumn); return ( -
- - {sidebarOutsideColumn && sidebarPanel} -
- {!hideTopBar && } -
- {!sidebarOutsideColumn && sidebarPanel} -
- {versionMismatch ? ( - // A v2 user on a stale v1 workspace route has nothing to go - // back to, so send them somewhere actionable instead of a - // dead-end "pick a workspace" screen. v1 users keep the - // static state — /new-workspace is a v2-only surface. - isV2CloudEnabled ? ( - + // The single ports-data provider for both layout modes. It lives up here + // (not in the sidebar) because in topbar mode the pill renders inside + // subtrees that remount on workspace navigation (TopBar / the workspace + // tab bar) — the data must survive those remounts or the pill blinks out + // for the first empty-data frames. The inline chip in the sidebar reads + // the same context; polling stays off when nothing renders ports (v1, or + // a collapsed/closed sidebar in inline mode). + +
+ + {sidebarOutsideColumn && sidebarPanel} +
+ {!hideTopBar && } +
+ {!sidebarOutsideColumn && sidebarPanel} +
+ {versionMismatch ? ( + // A v2 user on a stale v1 workspace route has nothing to go + // back to, so send them somewhere actionable instead of a + // dead-end "pick a workspace" screen. v1 users keep the + // static state — /new-workspace is a v2-only surface. + isV2CloudEnabled ? ( + + ) : ( + + ) ) : ( - - ) - ) : ( - - )} + + )} +
-
-
- - {deleteTarget?.version === "v1" && ( - { - if (!open) setDeleteTarget(null); - }} +
- )} - {deleteTarget?.version === "v2" && ( - { - setDeleteTarget((target) => - target?.version === "v2" ? { ...target, open } : target, - ); - }} - onDeleted={() => { - removeWorkspaceFromSidebar(deleteTarget.workspaceId); - setDeleteTarget(null); - }} - /> - )} -
+ + {deleteTarget?.version === "v1" && ( + { + if (!open) setDeleteTarget(null); + }} + /> + )} + {deleteTarget?.version === "v2" && ( + { + setDeleteTarget((target) => + target?.version === "v2" ? { ...target, open } : target, + ); + }} + onDeleted={() => { + removeWorkspaceFromSidebar(deleteTarget.workspaceId); + setDeleteTarget(null); + }} + /> + )} +
+
); } diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/useV2WorkspacePaneLayout/useV2WorkspacePaneLayout.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/useV2WorkspacePaneLayout/useV2WorkspacePaneLayout.ts index f4fd3b8b083..0e97884a551 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/useV2WorkspacePaneLayout/useV2WorkspacePaneLayout.ts +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/useV2WorkspacePaneLayout/useV2WorkspacePaneLayout.ts @@ -24,19 +24,29 @@ export function useV2WorkspacePaneLayout() { // workspace switches, live queries can briefly return stale rows; sharing // the same store across that boundary lets panes from one worktree render // and persist under another. - const workspaceRuntime = useMemo( - () => ({ + // + // Seed the store synchronously from the collection row (keyed by this + // workspace's id, so it can't serve a stale neighbor) instead of starting + // empty: an empty initial state blanks the whole tab bar for the first + // paint after every workspace switch, flashing its contents in one + // effect-cycle late. + const workspaceRuntime = useMemo(() => { + const seededLayout = + (collections.v2WorkspaceLocalState.get(workspaceId)?.paneLayout as + | WorkspaceState + | undefined) ?? EMPTY_STATE; + return { workspaceId, + seededSnapshot: getSnapshot(seededLayout), store: createWorkspaceStore({ - initialState: EMPTY_STATE, + initialState: seededLayout, }), - }), - [workspaceId], - ); + }; + }, [collections, workspaceId]); const { store } = workspaceRuntime; const syncStateRef = useRef({ workspaceId, - lastSyncedSnapshot: getSnapshot(EMPTY_STATE), + lastSyncedSnapshot: workspaceRuntime.seededSnapshot, }); const { data: localWorkspaceRows = [], isReady: isLayoutReady } = @@ -63,10 +73,10 @@ export function useV2WorkspacePaneLayout() { useEffect(() => { syncStateRef.current = { - workspaceId, - lastSyncedSnapshot: getSnapshot(EMPTY_STATE), + workspaceId: workspaceRuntime.workspaceId, + lastSyncedSnapshot: workspaceRuntime.seededSnapshot, }; - }, [workspaceId]); + }, [workspaceRuntime]); useEffect(() => { const nextSnapshot = getSnapshot(persistedPaneLayout); diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/page.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/page.tsx index 17864d27530..69447946dca 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/page.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/page.tsx @@ -12,6 +12,7 @@ import { electronTrpc } from "renderer/lib/electron-trpc"; import { NavigationControls } from "renderer/routes/_authenticated/_dashboard/components/NavigationControls"; import { SidebarToggle } from "renderer/routes/_authenticated/_dashboard/components/SidebarToggle"; import { RightSidebarToggle } from "renderer/routes/_authenticated/_dashboard/components/TopBar/components/RightSidebarToggle"; +import { TopBarPortsDropdown } from "renderer/routes/_authenticated/_dashboard/components/TopBar/components/TopBarPortsDropdown"; import { WindowControls } from "renderer/routes/_authenticated/_dashboard/components/TopBar/components/WindowControls"; import { CommandPalette } from "renderer/screens/main/components/CommandPalette"; import { ResizablePanel } from "renderer/screens/main/components/ResizablePanel"; @@ -365,10 +366,19 @@ function V2WorkspaceContent() { } renderTabBarTrailing={() => (
- + {/* The expanded sidebar's header owns the ports pill; the + tab bar only hosts it for the collapsed rail, where + neither the header cluster nor the TopBar is visible. */} + {tabBarHostsChrome && } + {/* Until the pane layout hydrates, tabs read as empty and + every running terminal miscounts as "background", so the + button would flash a bogus count on navigation. */} + {isLayoutReady && ( + + )} {workspaceRunButton} {!isMac && } diff --git a/apps/desktop/src/renderer/routes/_authenticated/settings/experimental/components/ExperimentalSettings/ExperimentalSettings.tsx b/apps/desktop/src/renderer/routes/_authenticated/settings/experimental/components/ExperimentalSettings/ExperimentalSettings.tsx index 8f77686fcfb..1171f5df591 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/settings/experimental/components/ExperimentalSettings/ExperimentalSettings.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/settings/experimental/components/ExperimentalSettings/ExperimentalSettings.tsx @@ -7,8 +7,8 @@ import { } from "renderer/hooks/useIsV2CloudEnabled"; import { track } from "renderer/lib/analytics"; import { - useInlineWorkspacePortsEnabled, useInlineWorkspacePortsStore, + usePortsDisplayMode, } from "renderer/stores/inline-workspace-ports"; import { useOpenV1ImportModal } from "renderer/stores/v1-import-modal"; import { useV2LocalOverrideStore } from "renderer/stores/v2-local-override"; @@ -54,9 +54,9 @@ export function ExperimentalSettings({ const isV2OnlyUser = useIsV2OnlyUser(); const setOptInV2 = useV2LocalOverrideStore((state) => state.setOptInV2); const openV1ImportModal = useOpenV1ImportModal(); - const inlineWorkspacePortsEnabled = useInlineWorkspacePortsEnabled(); - const setInlineWorkspacePortsEnabled = useInlineWorkspacePortsStore( - (state) => state.setEnabled, + const portsDisplayMode = usePortsDisplayMode(); + const setPortsDisplayMode = useInlineWorkspacePortsStore( + (state) => state.setMode, ); const workspaceAgentsEnabled = useWorkspaceAgentsRowEnabled(); const setWorkspaceAgentsEnabled = useWorkspaceAgentsRowStore( @@ -129,17 +129,19 @@ export function ExperimentalSettings({ htmlFor="inline-workspace-ports" className="text-sm font-medium" > - Inline workspace ports + Ports in top bar dropdown

- Show detected ports under each workspace in the sidebar instead - of a single panel at the bottom. + Show detected ports as a dropdown in the top bar instead of a + chip under each workspace in the sidebar.

+ setPortsDisplayMode(checked ? "topbar" : "inline") + } />
)} diff --git a/apps/desktop/src/renderer/routes/_authenticated/settings/utils/settings-search/settings-search.ts b/apps/desktop/src/renderer/routes/_authenticated/settings/utils/settings-search/settings-search.ts index c9c84eaefad..89b4792068a 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/settings/utils/settings-search/settings-search.ts +++ b/apps/desktop/src/renderer/routes/_authenticated/settings/utils/settings-search/settings-search.ts @@ -991,15 +991,18 @@ export const SETTINGS_ITEMS: SettingsItem[] = [ { id: SETTING_ITEM_ID.EXPERIMENTAL_INLINE_WORKSPACE_PORTS, section: "experimental", - title: "Inline workspace ports", + title: "Ports in top bar dropdown", description: - "Show detected ports under each workspace in the sidebar instead of a single panel at the bottom", + "Show detected ports as a dropdown in the top bar instead of a chip under each workspace", keywords: [ "experimental", "ports", "port", "inline", "sidebar", + "topbar", + "top bar", + "dropdown", "workspace", "workspaces", "dev server", diff --git a/apps/desktop/src/renderer/stores/inline-workspace-ports.ts b/apps/desktop/src/renderer/stores/inline-workspace-ports.ts index 0173c67dc90..209807687de 100644 --- a/apps/desktop/src/renderer/stores/inline-workspace-ports.ts +++ b/apps/desktop/src/renderer/stores/inline-workspace-ports.ts @@ -2,47 +2,83 @@ import { create } from "zustand"; import { devtools, persist } from "zustand/middleware"; /** - * EXPERIMENT: inline workspace ports vs. bottom ports panel. + * EXPERIMENT: where workspace ports render. * - * This flag exists only to A/B the two port layouts. It is the single source of - * truth for the experiment — read it everywhere via {@link useInlineWorkspacePortsEnabled}. + * This store is the single source of truth for the ports-layout experiment — + * read it everywhere via {@link usePortsDisplayMode} (or the narrower + * {@link useInlineWorkspacePortsEnabled}). + * + * - "inline": ports render as a chip under each workspace item. + * - "topbar": ports render as a dropdown from the top bar (and the workspace + * tab bar, which replaces the top bar on the v2 workspace route); the + * sidebar shows no ports at all. + * + * A third mode, "panel" (a consolidated list at the bottom of the sidebar), + * was tried and removed — `migrate` below folds any persisted "panel" value + * into "topbar", its closest surviving equivalent. * * To conclude the experiment, pick the winning layout and remove the other: - * 1. This store + `useInlineWorkspacePortsEnabled`. - * 2. The toggle UI in `ExperimentalSettings` and its `settings-search` entry - * (`EXPERIMENTAL_INLINE_WORKSPACE_PORTS`). - * 3. The flag branch in `DashboardSidebar` (bottom `DashboardSidebarPortsList`). - * 4. The flag branch in `DashboardSidebarWorkspaceChips` (the inline ports - * chip). - * 5. The components belonging to the losing layout: - * - bottom: `DashboardSidebarPortsList` (keep its `hooks/` + `DashboardSidebarPortBadge`). + * 1. This store + `usePortsDisplayMode` + `useInlineWorkspacePortsEnabled`. + * 2. The mode select in `ExperimentalSettings` and its `settings-search` + * entry (`EXPERIMENTAL_INLINE_WORKSPACE_PORTS`). + * 3. The mode branches in `DashboardSidebarWorkspaceChips` (the inline + * ports chip), `TopBar`, and the v2 workspace page's tab-bar trailing + * slot (`TopBarPortsDropdown`), plus the ports-provider gating in the + * dashboard layout. + * 4. The components belonging to the losing layout: * - inline: `DashboardSidebarPortsChip` (under * `DashboardSidebarWorkspaceChips`). + * - topbar: `TopBarPortsDropdown` (under `TopBar/components`). * - * Both layouts read port data from `DashboardSidebarPortsProvider`, which stays - * regardless of the outcome. + * Both layouts read port data from the single `DashboardSidebarPortsProvider` + * mounted in the dashboard layout. */ +export type PortsDisplayMode = "inline" | "topbar"; + interface InlineWorkspacePortsState { - // When true, ports render inline under each workspace item. When false, they - // render in the consolidated panel at the bottom of the sidebar. - enabled: boolean; - setEnabled: (enabled: boolean) => void; + mode: PortsDisplayMode; + setMode: (mode: PortsDisplayMode) => void; } export const useInlineWorkspacePortsStore = create()( devtools( persist( (set) => ({ - enabled: true, - setEnabled: (enabled) => set({ enabled }), + mode: "inline", + setMode: (mode) => set({ mode }), }), - { name: "inline-workspace-ports" }, + { + name: "inline-workspace-ports", + version: 2, + // v0 persisted `{ enabled: boolean }` for the original inline-vs-panel + // A/B; v1 added a `{ mode }` string including the now-removed "panel". + migrate: (persisted, version) => { + if (version === 0 && persisted && typeof persisted === "object") { + const { enabled } = persisted as { enabled?: boolean }; + return { mode: enabled === false ? "topbar" : "inline" }; + } + if (version === 1 && persisted && typeof persisted === "object") { + const { mode } = persisted as { mode?: string }; + if (mode === "inline" || mode === "topbar") { + return { mode }; + } + // "panel" or anything unrecognized. + return { mode: "topbar" }; + } + return persisted as InlineWorkspacePortsState; + }, + }, ), { name: "InlineWorkspacePortsStore" }, ), ); -/** Single read path for the inline-ports experiment flag. */ +/** Single read path for the ports-layout experiment. */ +export function usePortsDisplayMode(): PortsDisplayMode { + return useInlineWorkspacePortsStore((state) => state.mode); +} + +/** True when ports render inline under each workspace item. */ export function useInlineWorkspacePortsEnabled(): boolean { - return useInlineWorkspacePortsStore((state) => state.enabled); + return useInlineWorkspacePortsStore((state) => state.mode === "inline"); } From 45bf888bdb6630158bf349e7078bb948bca2d583 Mon Sep 17 00:00:00 2001 From: AviPeltz Date: Sun, 2 Aug 2026 18:54:03 -0700 Subject: [PATCH 2/2] style(desktop): raise expanded sidebar minimum width to 225px Claude-Session: https://claude.ai/code/session_018hKiDErNgDrZCGMwQcRjdn --- apps/desktop/src/renderer/stores/workspace-sidebar-state.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/desktop/src/renderer/stores/workspace-sidebar-state.ts b/apps/desktop/src/renderer/stores/workspace-sidebar-state.ts index 35fc07e242b..fa264e9cdda 100644 --- a/apps/desktop/src/renderer/stores/workspace-sidebar-state.ts +++ b/apps/desktop/src/renderer/stores/workspace-sidebar-state.ts @@ -3,7 +3,7 @@ import { devtools, persist } from "zustand/middleware"; export const DEFAULT_WORKSPACE_SIDEBAR_WIDTH = 280; export const COLLAPSED_WORKSPACE_SIDEBAR_WIDTH = 52; -const MIN_WORKSPACE_SIDEBAR_WIDTH = 220; +const MIN_WORKSPACE_SIDEBAR_WIDTH = 225; export const MAX_WORKSPACE_SIDEBAR_WIDTH = 400; // Threshold for snapping to collapsed state