106 lines
3.8 KiB
TypeScript
106 lines
3.8 KiB
TypeScript
/**
|
||
* 请求标签页栏。
|
||
*/
|
||
import { KIND_LABELS, findNode } from "@opsxapi/core";
|
||
import { useState } from "react";
|
||
import { useWorkspace } from "../workspaceStore";
|
||
import { ContextMenu, cx, type MenuItem } from "./common";
|
||
import { Icon, type IconName } from "./icons";
|
||
|
||
const KIND_TAB_ICON: Record<string, IconName> = {
|
||
graphql: "hexagon",
|
||
websocket: "zap",
|
||
sse: "radio",
|
||
socketio: "plug-zap",
|
||
mqtt: "mailbox",
|
||
};
|
||
|
||
export function TabBar() {
|
||
const collections = useWorkspace((s) => s.collections);
|
||
const openTabIds = useWorkspace((s) => s.openTabIds);
|
||
const activeTabId = useWorkspace((s) => s.activeTabId);
|
||
const setActiveTab = useWorkspace((s) => s.setActiveTab);
|
||
const closeTab = useWorkspace((s) => s.closeTab);
|
||
const closeOtherTabs = useWorkspace((s) => s.closeOtherTabs);
|
||
const [menu, setMenu] = useState<{ x: number; y: number; id: string } | null>(null);
|
||
|
||
const nodes = openTabIds
|
||
.map((id) => findNode(collections, id)?.node)
|
||
.filter((n): n is NonNullable<typeof n> => Boolean(n));
|
||
|
||
const menuItems = (id: string): MenuItem[] => [
|
||
{ label: "关闭", onClick: () => closeTab(id) },
|
||
{ label: "关闭其他", onClick: () => closeOtherTabs(id) },
|
||
{
|
||
label: "关闭全部",
|
||
onClick: () => {
|
||
for (const n of nodes) closeTab(n.id);
|
||
},
|
||
},
|
||
];
|
||
|
||
return (
|
||
<div
|
||
className="ops-panel-2 flex min-h-[38px] shrink-0 items-stretch overflow-x-auto"
|
||
style={{ borderBottom: "1px solid var(--border)" }}
|
||
onContextMenu={(e) => {
|
||
if (e.target === e.currentTarget) e.preventDefault();
|
||
}}
|
||
>
|
||
{nodes.length === 0 && (
|
||
<div className="ops-faint flex items-center px-4 text-xs">没有打开的请求</div>
|
||
)}
|
||
{nodes.map((node) => {
|
||
const active = node.id === activeTabId;
|
||
const runtime = useWorkspace.getState().runtime[node.id];
|
||
const busy = runtime?.loading || runtime?.status === "connecting" || runtime?.status === "open";
|
||
return (
|
||
<div
|
||
key={node.id}
|
||
className={cx(
|
||
"group flex cursor-pointer items-center gap-2 whitespace-nowrap px-3.5 text-xs transition",
|
||
active ? "" : "ops-faint hover:opacity-100",
|
||
)}
|
||
style={
|
||
active
|
||
? { background: "var(--panel)", borderTop: "2px solid var(--accent)", color: "var(--text)" }
|
||
: { borderTop: "2px solid transparent" }
|
||
}
|
||
onClick={() => setActiveTab(node.id)}
|
||
onAuxClick={(e) => e.button === 1 && closeTab(node.id)}
|
||
onContextMenu={(e) => {
|
||
e.preventDefault();
|
||
setMenu({ x: e.clientX, y: e.clientY, id: node.id });
|
||
}}
|
||
title={node.name}
|
||
>
|
||
{node.type === "request" && node.kind === "http" ? (
|
||
<span className={`ops-method-${node.data.method} font-bold`}>{node.data.method}</span>
|
||
) : node.type === "request" ? (
|
||
<Icon name={KIND_TAB_ICON[node.kind]} size={13} className="ops-dim shrink-0" title={KIND_LABELS[node.kind]} />
|
||
) : null}
|
||
<span className="max-w-48 truncate">{node.name}</span>
|
||
{busy && <span className="ops-dot ops-dot-connecting" />}
|
||
<button
|
||
className={cx(
|
||
"rounded px-1 text-[13px] leading-none opacity-40 transition group-hover:opacity-100",
|
||
)}
|
||
style={{ color: active ? "var(--text)" : "inherit" }}
|
||
onClick={(e) => {
|
||
e.stopPropagation();
|
||
closeTab(node.id);
|
||
}}
|
||
>
|
||
×
|
||
</button>
|
||
</div>
|
||
);
|
||
})}
|
||
<ContextMenu
|
||
menu={menu ? { x: menu.x, y: menu.y, items: menuItems(menu.id) } : null}
|
||
onClose={() => setMenu(null)}
|
||
/>
|
||
</div>
|
||
);
|
||
}
|