Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
import { COMPANY } from "@superset/shared/constants";
import { Tooltip, TooltipContent, TooltipTrigger } from "@superset/ui/tooltip";
import { LuChevronRight, LuCircleHelp, LuRadioTower } from "react-icons/lu";
import { useCallback, useRef } from "react";
import {
LuChevronRight,
LuCircleHelp,
LuFilter,
LuRadioTower,
} from "react-icons/lu";
import { usePortsStore } from "renderer/stores";
import { MIN_LIST_HEIGHT, MAX_LIST_HEIGHT } from "renderer/stores/ports/store";
import { STROKE_WIDTH } from "../constants";
import { WorkspacePortGroup } from "./components/WorkspacePortGroup";
import { usePortsData } from "./hooks/usePortsData";
Expand All @@ -11,10 +18,43 @@ const PORTS_DOCS_URL = `${COMPANY.DOCS_URL}/ports`;
export function PortsList() {
const isCollapsed = usePortsStore((s) => s.isListCollapsed);
const toggleCollapsed = usePortsStore((s) => s.toggleListCollapsed);
const listHeight = usePortsStore((s) => s.listHeight);
const setListHeight = usePortsStore((s) => s.setListHeight);
const showConfiguredOnly = usePortsStore((s) => s.showConfiguredOnly);
const setShowConfiguredOnly = usePortsStore((s) => s.setShowConfiguredOnly);

const { workspacePortGroups, totalPortCount } = usePortsData();

if (totalPortCount === 0) {
// --- Drag-to-resize handle ---
const isDragging = useRef(false);
const startY = useRef(0);
const startHeight = useRef(0);

const handleMouseDown = useCallback(
(e: React.MouseEvent) => {
e.preventDefault();
isDragging.current = true;
startY.current = e.clientY;
startHeight.current = listHeight;

const handleMouseMove = (ev: MouseEvent) => {
if (!isDragging.current) return;
// Dragging UP increases height (handle is at top of list)
const delta = startY.current - ev.clientY;
setListHeight(startHeight.current + delta);
};
const handleMouseUp = () => {
isDragging.current = false;
document.removeEventListener("mousemove", handleMouseMove);
document.removeEventListener("mouseup", handleMouseUp);
};
document.addEventListener("mousemove", handleMouseMove);
document.addEventListener("mouseup", handleMouseUp);
},
[listHeight, setListHeight],
);

if (totalPortCount === 0 && !showConfiguredOnly) {
return null;
}

Expand All @@ -24,7 +64,16 @@ export function PortsList() {
};

return (
<div className="pt-3 border-t border-border">
<div className="shrink-0 border-t border-border">
{/* Resize handle */}
{!isCollapsed && (
<div
className="h-1.5 cursor-ns-resize hover:bg-primary/20 active:bg-primary/40 transition-colors flex items-center justify-center"
onMouseDown={handleMouseDown}
>
<div className="w-8 h-px bg-muted-foreground/30" />
</div>
)}
<div className="group text-[11px] uppercase tracking-wider text-muted-foreground/70 px-3 pb-2 font-medium flex items-center gap-1.5 w-full hover:text-muted-foreground transition-colors">
<button
type="button"
Expand All @@ -40,27 +89,71 @@ export function PortsList() {
Ports
</button>

<Tooltip delayDuration={300}>
<TooltipTrigger asChild>
<button
type="button"
onClick={() => setShowConfiguredOnly(!showConfiguredOnly)}
className={`p-0.5 rounded transition-colors ${
showConfiguredOnly
? "text-primary bg-primary/10"
: "hover:bg-muted/50 opacity-0 group-hover:opacity-100 transition-opacity"
}`}
>
<LuFilter
className="size-3"
strokeWidth={STROKE_WIDTH}
/>
</button>
</TooltipTrigger>
<TooltipContent side="top" sideOffset={4}>
<p className="text-xs">
{showConfiguredOnly
? "Show all ports"
: "Show only ports.json ports"}
</p>
</TooltipContent>
</Tooltip>

<Tooltip delayDuration={300}>
<TooltipTrigger asChild>
<button
type="button"
onClick={handleOpenPortsDocs}
className="ml-auto p-0.5 rounded hover:bg-muted/50 opacity-0 group-hover:opacity-100 transition-opacity"
className="p-0.5 rounded hover:bg-muted/50 opacity-0 group-hover:opacity-100 transition-opacity"
>
<LuCircleHelp className="size-3" strokeWidth={STROKE_WIDTH} />
<LuCircleHelp
className="size-3"
strokeWidth={STROKE_WIDTH}
/>
</button>
</TooltipTrigger>
<TooltipContent side="top" sideOffset={4}>
<p className="text-xs">Learn about static port configuration</p>
<p className="text-xs">
Learn about static port configuration
</p>
</TooltipContent>
</Tooltip>
<span className="text-[10px] font-normal">{totalPortCount}</span>
<span className="ml-auto text-[10px] font-normal">
{totalPortCount}
</span>
</div>
{!isCollapsed && (
<div className="space-y-2 max-h-72 overflow-y-auto pb-1 hide-scrollbar">
{workspacePortGroups.map((group) => (
<WorkspacePortGroup key={group.workspaceId} group={group} />
))}
<div
className="space-y-2 overflow-y-auto pb-1 hide-scrollbar"
style={{
height: listHeight,
}}
>
{workspacePortGroups.length > 0 ? (
workspacePortGroups.map((group) => (
<WorkspacePortGroup key={group.workspaceId} group={group} />
))
) : showConfiguredOnly ? (
<p className="px-3 py-2 text-[10px] text-muted-foreground/50">
No ports defined in ports.json
</p>
) : null}
</div>
)}
</div>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { useMemo } from "react";
import { electronTrpc } from "renderer/lib/electron-trpc";
import { usePortsStore } from "renderer/stores";
import type { EnrichedPort } from "shared/types";

const PORTS_FALLBACK_REFETCH_INTERVAL_MS = 10_000;
Expand Down Expand Up @@ -29,7 +30,13 @@ export function usePortsData() {
},
});

const ports = detectedPorts ?? [];
const showConfiguredOnly = usePortsStore((s) => s.showConfiguredOnly);

const ports = useMemo(() => {
const all = detectedPorts ?? [];
if (!showConfiguredOnly) return all;
return all.filter((p) => p.label != null);
}, [detectedPorts, showConfiguredOnly]);

const workspaceNames = useMemo(() => {
if (!allWorkspaces) return {};
Expand Down
27 changes: 27 additions & 0 deletions apps/desktop/src/renderer/stores/ports/store.ts
Original file line number Diff line number Diff line change
@@ -1,28 +1,55 @@
import { create } from "zustand";
import { devtools, persist } from "zustand/middleware";

const DEFAULT_LIST_HEIGHT = 288; // 18rem (max-h-72)
const MIN_LIST_HEIGHT = 80;
const MAX_LIST_HEIGHT = 600;

interface PortsState {
isListCollapsed: boolean;
/** Height of the ports list in pixels (user-resizable). */
listHeight: number;
/** When true, only ports with a label from ports.json are shown. */
showConfiguredOnly: boolean;

setListCollapsed: (collapsed: boolean) => void;
toggleListCollapsed: () => void;
setListHeight: (height: number) => void;
setShowConfiguredOnly: (value: boolean) => void;
}

export { MIN_LIST_HEIGHT, MAX_LIST_HEIGHT };

export const usePortsStore = create<PortsState>()(
devtools(
persist(
(set, get) => ({
isListCollapsed: false,
listHeight: DEFAULT_LIST_HEIGHT,
showConfiguredOnly: false,

setListCollapsed: (collapsed) => set({ isListCollapsed: collapsed }),

toggleListCollapsed: () =>
set({ isListCollapsed: !get().isListCollapsed }),

setListHeight: (height) =>
set({
listHeight: Math.max(
MIN_LIST_HEIGHT,
Math.min(MAX_LIST_HEIGHT, height),
),
}),

setShowConfiguredOnly: (value) =>
set({ showConfiguredOnly: value }),
}),
{
name: "ports-store",
partialize: (state) => ({
isListCollapsed: state.isListCollapsed,
listHeight: state.listHeight,
showConfiguredOnly: state.showConfiguredOnly,
}),
},
),
Expand Down