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 bcd04b07082..029f84e386e 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 @@ -17,13 +17,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 { useSidebarSectionsCollapseStore } from "renderer/stores/sidebar-sections-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 { DashboardSidebarSessionsSection } from "./components/DashboardSidebarSessionsSection"; @@ -34,7 +32,6 @@ import { useDashboardSidebarShortcuts } from "./hooks/useDashboardSidebarShortcu import { useDashboardSidebarDnd } from "./hooks/useSidebarDnd"; import { DashboardSidebarDndProvider } from "./providers/DashboardSidebarDndProvider"; import { DashboardSidebarHoverProvider } from "./providers/DashboardSidebarHoverProvider"; -import { DashboardSidebarPortsProvider } from "./providers/DashboardSidebarPortsProvider"; import { DashboardSidebarSelectionProvider } from "./providers/DashboardSidebarSelectionProvider"; import { DashboardSidebarWorkspaceStatusProvider, @@ -131,7 +128,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 = useSidebarSectionsCollapseStore( @@ -238,117 +234,112 @@ export function DashboardSidebar({ workspaces={statusWorkspaces} activeWorkspaceId={activeV2WorkspaceId} > - - - -
- + {/* Port data comes from the single DashboardSidebarPortsProvider in + the dashboard layout, which wraps this sidebar. */} + + +
+ - - {(isCollapsed || !workspacesListCollapsed) && ( - - )} - + {(isCollapsed || !workspacesListCollapsed) && ( + - {!isCollapsed && ( - - - - )} - {(isCollapsed || !workspacesListCollapsed) && ( - - {orderedGroups.map((project) => ( - - ))} - - )} - - {!isCollapsed && !inlineWorkspacePortsEnabled && ( - )} - {!isCollapsed && activeV2Project && activeHostUrl && ( - + + {!isCollapsed && ( + + + + )} + {(isCollapsed || !workspacesListCollapsed) && ( + + {orderedGroups.map((project) => ( + + ))} + + )} + + {!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 2f5e5e4863c..1388d5cf350 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 @@ -28,6 +28,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 { pullRequestsSearchFromFilters, @@ -393,9 +394,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 4e694e1b1a5..4967b87889d 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 @@ -30,10 +30,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 { useDashboardSidebarHoverActions } 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 d795e2f806e..7ec5c7cf505 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/layout.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/layout.tsx @@ -13,12 +13,14 @@ import { useIsV2CloudEnabled } from "renderer/hooks/useIsV2CloudEnabled"; import { useHotkey } from "renderer/hotkeys"; import { electronTrpc } from "renderer/lib/electron-trpc"; import { DashboardSidebar } from "renderer/routes/_authenticated/_dashboard/components/DashboardSidebar"; +import { DashboardSidebarPortsProvider } from "renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/providers/DashboardSidebarPortsProvider"; 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 { useDeleteWorkspaceIntent } from "renderer/stores/delete-workspace-intent"; +import { usePortsDisplayMode } from "renderer/stores/inline-workspace-ports"; import { useOpenNewWorkspaceModal } from "renderer/stores/new-workspace-modal"; import { COLLAPSED_WORKSPACE_SIDEBAR_WIDTH, @@ -48,6 +50,7 @@ function DashboardLayout() { const location = useLocation(); const openNewWorkspaceModal = useOpenNewWorkspaceModal(); const isV2CloudEnabled = useIsV2CloudEnabled(); + const portsDisplayMode = usePortsDisplayMode(); const { workspaces: hostWorkspaces } = useHostWorkspaces(); useDevSeedV2Sidebar(); // Get current workspace from route to pre-select project in new workspace modal @@ -215,54 +218,72 @@ 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 ? ( + + ) : ( + + ) ) : ( - - ) - ) : ( - // Contain content-route crashes to this pane: without a - // boundary they bubble to the root and unmount the whole - // app, which reads as Superset restarting itself - // (SUPER-1814). Resets on navigation. - location.href} - errorComponent={DashboardContentError} - > - - - )} + // Contain content-route crashes to this pane: without a + // boundary they bubble to the root and unmount the whole + // app, which reads as Superset restarting itself + // (SUPER-1814). Resets on navigation. + location.href} + errorComponent={DashboardContentError} + > + + + )} +
-
-
- - {deleteTarget && ( - { - if (!open) setDeleteTarget(null); - }} +
- )} -
+ + {deleteTarget && ( + { + if (!open) 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 7368b7abe58..1c13e93d0ca 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 @@ -14,6 +14,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"; @@ -364,10 +365,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 887721531aa..a31238ea503 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 @@ -8,8 +8,8 @@ import { import { track } from "renderer/lib/analytics"; import { HighlightText } from "renderer/routes/_authenticated/settings/components/HighlightText"; import { - useInlineWorkspacePortsEnabled, useInlineWorkspacePortsStore, + usePortsDisplayMode, } from "renderer/stores/inline-workspace-ports"; import { useSettingsSearchQuery } from "renderer/stores/settings-state"; import { useOpenV1ImportModal } from "renderer/stores/v1-import-modal"; @@ -57,9 +57,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( @@ -143,21 +143,23 @@ export function ExperimentalSettings({ className="text-sm font-medium" >

+ 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 6fc4ba57dbb..c5b36fcefc3 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 @@ -1047,15 +1047,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"); } 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