Files
opsxapi/packages/ui/src/components/TabBar.tsx
T

106 lines
3.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 请求标签页栏。
*/
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>
);
}