feat: Improve dashboard API, improve tab system, various other fixes
This commit is contained in:
@@ -78,7 +78,7 @@ export function Dashboard({
|
||||
Array<{ id: number; name: string; cpu: number | null; ram: number | null }>
|
||||
>([]);
|
||||
|
||||
const { addTab } = useTabs();
|
||||
const { addTab, setCurrentTab, tabs: tabList } = useTabs();
|
||||
|
||||
let sidebarState: "expanded" | "collapsed" = "expanded";
|
||||
try {
|
||||
@@ -160,14 +160,27 @@ export function Dashboard({
|
||||
const hosts = await getSSHHosts();
|
||||
setTotalServers(hosts.length);
|
||||
|
||||
const tunnels = await getTunnelStatuses();
|
||||
setTotalTunnels(Object.keys(tunnels).length);
|
||||
// Count total tunnels across all hosts
|
||||
let totalTunnelsCount = 0;
|
||||
for (const host of hosts) {
|
||||
if (host.tunnelConnections) {
|
||||
try {
|
||||
const tunnelConnections = JSON.parse(host.tunnelConnections);
|
||||
if (Array.isArray(tunnelConnections)) {
|
||||
totalTunnelsCount += tunnelConnections.length;
|
||||
}
|
||||
} catch {
|
||||
// Ignore parse errors
|
||||
}
|
||||
}
|
||||
}
|
||||
setTotalTunnels(totalTunnelsCount);
|
||||
|
||||
const credentials = await getCredentials();
|
||||
setTotalCredentials(credentials.length);
|
||||
|
||||
// Fetch recent activity
|
||||
const activity = await getRecentActivity(10);
|
||||
// Fetch recent activity (35 items)
|
||||
const activity = await getRecentActivity(35);
|
||||
setRecentActivity(activity);
|
||||
|
||||
// Fetch server stats for first 5 servers
|
||||
@@ -237,6 +250,55 @@ export function Dashboard({
|
||||
});
|
||||
};
|
||||
|
||||
// Quick Actions handlers
|
||||
const handleAddHost = () => {
|
||||
const sshManagerTab = tabList.find((t) => t.type === "ssh_manager");
|
||||
if (sshManagerTab) {
|
||||
setCurrentTab(sshManagerTab.id);
|
||||
} else {
|
||||
const id = addTab({
|
||||
type: "ssh_manager",
|
||||
title: "Host Manager",
|
||||
initialTab: "add_host",
|
||||
});
|
||||
setCurrentTab(id);
|
||||
}
|
||||
};
|
||||
|
||||
const handleAddCredential = () => {
|
||||
const sshManagerTab = tabList.find((t) => t.type === "ssh_manager");
|
||||
if (sshManagerTab) {
|
||||
setCurrentTab(sshManagerTab.id);
|
||||
} else {
|
||||
const id = addTab({
|
||||
type: "ssh_manager",
|
||||
title: "Host Manager",
|
||||
initialTab: "add_credential",
|
||||
});
|
||||
setCurrentTab(id);
|
||||
}
|
||||
};
|
||||
|
||||
const handleOpenAdminSettings = () => {
|
||||
const adminTab = tabList.find((t) => t.type === "admin");
|
||||
if (adminTab) {
|
||||
setCurrentTab(adminTab.id);
|
||||
} else {
|
||||
const id = addTab({ type: "admin", title: "Admin Settings" });
|
||||
setCurrentTab(id);
|
||||
}
|
||||
};
|
||||
|
||||
const handleOpenUserProfile = () => {
|
||||
const userProfileTab = tabList.find((t) => t.type === "user_profile");
|
||||
if (userProfileTab) {
|
||||
setCurrentTab(userProfileTab.id);
|
||||
} else {
|
||||
const id = addTab({ type: "user_profile", title: "User Profile" });
|
||||
setCurrentTab(id);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{!loggedIn ? (
|
||||
@@ -486,7 +548,7 @@ export function Dashboard({
|
||||
<Button
|
||||
variant="outline"
|
||||
className="border-2 !border-dark-border flex flex-col items-center justify-center h-auto p-3"
|
||||
onClick={() => onSelectView("host-manager-add")}
|
||||
onClick={handleAddHost}
|
||||
>
|
||||
<Server
|
||||
className="shrink-0"
|
||||
@@ -499,7 +561,7 @@ export function Dashboard({
|
||||
<Button
|
||||
variant="outline"
|
||||
className="border-2 !border-dark-border flex flex-col items-center justify-center h-auto p-3"
|
||||
onClick={() => onSelectView("host-manager-credentials")}
|
||||
onClick={handleAddCredential}
|
||||
>
|
||||
<Key
|
||||
className="shrink-0"
|
||||
@@ -513,7 +575,7 @@ export function Dashboard({
|
||||
<Button
|
||||
variant="outline"
|
||||
className="border-2 !border-dark-border flex flex-col items-center justify-center h-auto p-3"
|
||||
onClick={() => onSelectView("admin-settings")}
|
||||
onClick={handleOpenAdminSettings}
|
||||
>
|
||||
<Settings
|
||||
className="shrink-0"
|
||||
@@ -527,7 +589,7 @@ export function Dashboard({
|
||||
<Button
|
||||
variant="outline"
|
||||
className="border-2 !border-dark-border flex flex-col items-center justify-center h-auto p-3"
|
||||
onClick={() => onSelectView("user-profile")}
|
||||
onClick={handleOpenUserProfile}
|
||||
>
|
||||
<User
|
||||
className="shrink-0"
|
||||
|
||||
@@ -47,6 +47,7 @@ import {
|
||||
removeRecentFile,
|
||||
addFolderShortcut,
|
||||
getPinnedFiles,
|
||||
logActivity,
|
||||
} from "@/ui/main-axios.ts";
|
||||
import type { SidebarItem } from "./FileManagerSidebar";
|
||||
|
||||
@@ -298,6 +299,15 @@ function FileManagerContent({ initialHost, onClose }: FileManagerProps) {
|
||||
|
||||
setSshSessionId(sessionId);
|
||||
|
||||
// Log activity for recent connections
|
||||
if (currentHost?.id) {
|
||||
const hostName =
|
||||
currentHost.name || `${currentHost.username}@${currentHost.ip}`;
|
||||
logActivity("file_manager", currentHost.id, hostName).catch((err) => {
|
||||
console.warn("Failed to log file manager activity:", err);
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await listSSHFiles(sessionId, currentPath);
|
||||
const files = Array.isArray(response)
|
||||
@@ -1247,6 +1257,15 @@ function FileManagerContent({ initialHost, onClose }: FileManagerProps) {
|
||||
setSshSessionId(totpSessionId);
|
||||
setTotpSessionId(null);
|
||||
|
||||
// Log activity for recent connections
|
||||
if (currentHost?.id) {
|
||||
const hostName =
|
||||
currentHost.name || `${currentHost.username}@${currentHost.ip}`;
|
||||
logActivity("file_manager", currentHost.id, hostName).catch((err) => {
|
||||
console.warn("Failed to log file manager activity:", err);
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await listSSHFiles(totpSessionId, currentPath);
|
||||
const files = Array.isArray(response)
|
||||
|
||||
@@ -16,9 +16,10 @@ import type { SSHHost, HostManagerProps } from "../../../types/index";
|
||||
|
||||
export function HostManager({
|
||||
isTopbarOpen,
|
||||
initialTab = "host_viewer",
|
||||
}: HostManagerProps): React.ReactElement {
|
||||
const { t } = useTranslation();
|
||||
const [activeTab, setActiveTab] = useState("host_viewer");
|
||||
const [activeTab, setActiveTab] = useState(initialTab);
|
||||
const [editingHost, setEditingHost] = useState<SSHHost | null>(null);
|
||||
|
||||
const [editingCredential, setEditingCredential] = useState<{
|
||||
|
||||
@@ -12,7 +12,7 @@ import { Unicode11Addon } from "@xterm/addon-unicode11";
|
||||
import { WebLinksAddon } from "@xterm/addon-web-links";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { toast } from "sonner";
|
||||
import { getCookie, isElectron } from "@/ui/main-axios.ts";
|
||||
import { getCookie, isElectron, logActivity } from "@/ui/main-axios.ts";
|
||||
import { TOTPDialog } from "@/ui/components/TOTPDialog";
|
||||
|
||||
interface HostConfig {
|
||||
@@ -469,6 +469,15 @@ export const Terminal = forwardRef<TerminalHandle, SSHTerminalProps>(
|
||||
}
|
||||
reconnectAttempts.current = 0;
|
||||
isReconnectingRef.current = false;
|
||||
|
||||
// Log activity for recent connections
|
||||
if (hostConfig.id) {
|
||||
const hostName =
|
||||
hostConfig.name || `${hostConfig.username}@${hostConfig.ip}`;
|
||||
logActivity("terminal", hostConfig.id, hostName).catch((err) => {
|
||||
console.warn("Failed to log terminal activity:", err);
|
||||
});
|
||||
}
|
||||
} else if (msg.type === "disconnected") {
|
||||
wasDisconnectedBySSH.current = true;
|
||||
setIsConnected(false);
|
||||
|
||||
@@ -164,6 +164,7 @@ function AppContent() {
|
||||
<HostManager
|
||||
onSelectView={handleSelectView}
|
||||
isTopbarOpen={isTopbarOpen}
|
||||
initialTab={currentTabData?.initialTab}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -24,11 +24,6 @@ interface TabProps {
|
||||
disableActivate?: boolean;
|
||||
disableSplit?: boolean;
|
||||
disableClose?: boolean;
|
||||
onDragStart?: () => void;
|
||||
onDragOver?: (e: React.DragEvent) => void;
|
||||
onDragLeave?: () => void;
|
||||
onDrop?: (e: React.DragEvent) => void;
|
||||
onDragEnd?: () => void;
|
||||
isDragging?: boolean;
|
||||
isDragOver?: boolean;
|
||||
}
|
||||
@@ -45,52 +40,37 @@ export function Tab({
|
||||
disableActivate = false,
|
||||
disableSplit = false,
|
||||
disableClose = false,
|
||||
onDragStart,
|
||||
onDragOver,
|
||||
onDragLeave,
|
||||
onDrop,
|
||||
onDragEnd,
|
||||
isDragging = false,
|
||||
isDragOver = false,
|
||||
}: TabProps): React.ReactElement {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const dragProps = {
|
||||
draggable: true,
|
||||
onDragStart,
|
||||
onDragOver,
|
||||
onDragLeave,
|
||||
onDrop,
|
||||
onDragEnd,
|
||||
};
|
||||
|
||||
// Firefox-style tab classes using cn utility
|
||||
const tabBaseClasses = cn(
|
||||
"relative flex items-center gap-1.5 px-3 py-2 min-w-fit max-w-[200px]",
|
||||
"relative flex items-center gap-1.5 px-3 min-w-fit max-w-[200px]",
|
||||
"rounded-t-lg border-t-2 border-l-2 border-r-2",
|
||||
"transition-all duration-150 select-none",
|
||||
"transition-all duration-150 h-[42px]",
|
||||
isDragOver &&
|
||||
"bg-background/40 text-muted-foreground border-border opacity-60 cursor-default",
|
||||
isDragging && "opacity-40 cursor-grabbing",
|
||||
"bg-background/40 text-muted-foreground border-border opacity-60",
|
||||
isDragging && "opacity-70",
|
||||
!isDragOver &&
|
||||
!isDragging &&
|
||||
isActive &&
|
||||
"bg-background text-foreground border-border z-10 cursor-pointer",
|
||||
"bg-background text-foreground border-border z-10",
|
||||
!isDragOver &&
|
||||
!isDragging &&
|
||||
!isActive &&
|
||||
"bg-background/80 text-muted-foreground border-border hover:bg-background/90 cursor-pointer",
|
||||
"bg-background/80 text-muted-foreground border-border hover:bg-background/90",
|
||||
);
|
||||
|
||||
if (tabType === "home") {
|
||||
return (
|
||||
<div
|
||||
className={tabBaseClasses}
|
||||
{...dragProps}
|
||||
onClick={!disableActivate ? onActivate : undefined}
|
||||
style={{
|
||||
marginBottom: "-2px",
|
||||
borderBottom: isActive ? "2px solid transparent" : "none",
|
||||
borderBottom: isActive ? "2px solid white" : "none",
|
||||
}}
|
||||
>
|
||||
<Home className="h-4 w-4" />
|
||||
@@ -121,10 +101,9 @@ export function Tab({
|
||||
return (
|
||||
<div
|
||||
className={tabBaseClasses}
|
||||
{...dragProps}
|
||||
style={{
|
||||
marginBottom: "-2px",
|
||||
borderBottom: isActive ? "2px solid transparent" : "none",
|
||||
borderBottom: isActive ? "2px solid white" : "none",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
@@ -183,10 +162,9 @@ export function Tab({
|
||||
return (
|
||||
<div
|
||||
className={tabBaseClasses}
|
||||
{...dragProps}
|
||||
style={{
|
||||
marginBottom: "-2px",
|
||||
borderBottom: isActive ? "2px solid transparent" : "none",
|
||||
borderBottom: isActive ? "2px solid white" : "none",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
@@ -220,10 +198,9 @@ export function Tab({
|
||||
return (
|
||||
<div
|
||||
className={tabBaseClasses}
|
||||
{...dragProps}
|
||||
style={{
|
||||
marginBottom: "-2px",
|
||||
borderBottom: isActive ? "2px solid transparent" : "none",
|
||||
borderBottom: isActive ? "2px solid white" : "none",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
|
||||
@@ -58,8 +58,19 @@ export function TopNavbar({
|
||||
const [isRecording, setIsRecording] = useState(false);
|
||||
const [selectedTabIds, setSelectedTabIds] = useState<number[]>([]);
|
||||
const [snippetsSidebarOpen, setSnippetsSidebarOpen] = useState(false);
|
||||
const [draggedTabIndex, setDraggedTabIndex] = useState<number | null>(null);
|
||||
const [dragOverTabIndex, setDragOverTabIndex] = useState<number | null>(null);
|
||||
const [dragState, setDragState] = useState<{
|
||||
draggedIndex: number | null;
|
||||
currentX: number;
|
||||
startX: number;
|
||||
targetIndex: number | null;
|
||||
}>({
|
||||
draggedIndex: null,
|
||||
currentX: 0,
|
||||
startX: 0,
|
||||
targetIndex: null,
|
||||
});
|
||||
const containerRef = React.useRef<HTMLDivElement | null>(null);
|
||||
const tabRefs = React.useRef<Map<number, HTMLDivElement>>(new Map());
|
||||
|
||||
const handleTabActivate = (tabId: number) => {
|
||||
setCurrentTab(tabId);
|
||||
@@ -238,33 +249,110 @@ export function TopNavbar({
|
||||
}
|
||||
};
|
||||
|
||||
const handleDragStart = (index: number) => {
|
||||
setDraggedTabIndex(index);
|
||||
const handleDragStart = (e: React.DragEvent, index: number) => {
|
||||
console.log("Drag start:", index, e.clientX);
|
||||
|
||||
// Create transparent drag image
|
||||
const img = new Image();
|
||||
img.src =
|
||||
"data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7";
|
||||
e.dataTransfer.setDragImage(img, 0, 0);
|
||||
|
||||
setDragState({
|
||||
draggedIndex: index,
|
||||
startX: e.clientX,
|
||||
currentX: e.clientX,
|
||||
targetIndex: index,
|
||||
});
|
||||
};
|
||||
|
||||
const handleDragOver = (e: React.DragEvent, index: number) => {
|
||||
e.preventDefault();
|
||||
if (draggedTabIndex !== null && draggedTabIndex !== index) {
|
||||
setDragOverTabIndex(index);
|
||||
const handleDrag = (e: React.DragEvent) => {
|
||||
if (e.clientX === 0) return; // Skip the final drag event
|
||||
if (dragState.draggedIndex === null) return;
|
||||
|
||||
console.log("Dragging:", e.clientX);
|
||||
|
||||
setDragState((prev) => ({
|
||||
...prev,
|
||||
currentX: e.clientX,
|
||||
}));
|
||||
|
||||
// Calculate target position based on mouse X
|
||||
if (!containerRef.current) return;
|
||||
|
||||
const containerRect = containerRef.current.getBoundingClientRect();
|
||||
const mouseX = e.clientX - containerRect.left;
|
||||
|
||||
let accumulatedX = 0;
|
||||
let newTargetIndex = dragState.draggedIndex;
|
||||
|
||||
tabs.forEach((tab, i) => {
|
||||
const tabEl = tabRefs.current.get(i);
|
||||
if (!tabEl) return;
|
||||
|
||||
const tabWidth = tabEl.getBoundingClientRect().width;
|
||||
const tabCenter = accumulatedX + tabWidth / 2;
|
||||
|
||||
if (mouseX < tabCenter && i === 0) {
|
||||
newTargetIndex = 0;
|
||||
} else if (mouseX >= tabCenter && mouseX < accumulatedX + tabWidth) {
|
||||
newTargetIndex = i;
|
||||
}
|
||||
|
||||
accumulatedX += tabWidth + 4; // 4px gap
|
||||
});
|
||||
|
||||
if (mouseX >= accumulatedX - 4) {
|
||||
newTargetIndex = tabs.length - 1;
|
||||
}
|
||||
|
||||
setDragState((prev) => ({
|
||||
...prev,
|
||||
targetIndex: newTargetIndex,
|
||||
}));
|
||||
};
|
||||
|
||||
const handleDragLeave = () => {
|
||||
setDragOverTabIndex(null);
|
||||
};
|
||||
|
||||
const handleDrop = (e: React.DragEvent, dropIndex: number) => {
|
||||
const handleDragOver = (e: React.DragEvent) => {
|
||||
e.preventDefault();
|
||||
if (draggedTabIndex !== null && draggedTabIndex !== dropIndex) {
|
||||
reorderTabs(draggedTabIndex, dropIndex);
|
||||
};
|
||||
|
||||
const handleDrop = (e: React.DragEvent) => {
|
||||
e.preventDefault();
|
||||
console.log("Drop:", dragState);
|
||||
|
||||
if (
|
||||
dragState.draggedIndex !== null &&
|
||||
dragState.targetIndex !== null &&
|
||||
dragState.draggedIndex !== dragState.targetIndex
|
||||
) {
|
||||
reorderTabs(dragState.draggedIndex, dragState.targetIndex);
|
||||
}
|
||||
setDraggedTabIndex(null);
|
||||
setDragOverTabIndex(null);
|
||||
|
||||
setDragState({
|
||||
draggedIndex: null,
|
||||
startX: 0,
|
||||
currentX: 0,
|
||||
targetIndex: null,
|
||||
});
|
||||
};
|
||||
|
||||
const handleDragEnd = () => {
|
||||
setDraggedTabIndex(null);
|
||||
setDragOverTabIndex(null);
|
||||
console.log("Drag end:", dragState);
|
||||
|
||||
if (
|
||||
dragState.draggedIndex !== null &&
|
||||
dragState.targetIndex !== null &&
|
||||
dragState.draggedIndex !== dragState.targetIndex
|
||||
) {
|
||||
reorderTabs(dragState.draggedIndex, dragState.targetIndex);
|
||||
}
|
||||
|
||||
setDragState({
|
||||
draggedIndex: null,
|
||||
startX: 0,
|
||||
currentX: 0,
|
||||
targetIndex: null,
|
||||
});
|
||||
};
|
||||
|
||||
const isSplitScreenActive =
|
||||
@@ -284,20 +372,19 @@ export function TopNavbar({
|
||||
return (
|
||||
<div>
|
||||
<div
|
||||
className="fixed z-10 h-[50px] bg-dark-bg border-2 border-dark-border rounded-lg transition-all duration-200 ease-linear flex flex-row transform-none m-0 p-0"
|
||||
className="fixed z-10 h-[50px] border-2 border-dark-border rounded-lg transition-all duration-200 ease-linear flex flex-row transform-none m-0 p-0"
|
||||
style={{
|
||||
top: isTopbarOpen ? "0.5rem" : "-3rem",
|
||||
left: leftPosition,
|
||||
right: "17px",
|
||||
backgroundColor: "#1e1e21",
|
||||
}}
|
||||
>
|
||||
<div className="h-full p-1 pr-2 border-r-2 border-dark-border w-[calc(100%-6rem)] flex items-center overflow-x-auto overflow-y-hidden gap-1 thin-scrollbar">
|
||||
<div
|
||||
ref={containerRef}
|
||||
className="h-full p-1 pr-2 border-r-2 border-dark-border w-[calc(100%-6rem)] flex items-center overflow-x-auto overflow-y-hidden gap-1 thin-scrollbar"
|
||||
>
|
||||
{tabs.map((tab: TabData, index: number) => {
|
||||
// Insert preview tab before this position if dragging over it
|
||||
const showPreviewBefore =
|
||||
draggedTabIndex !== null &&
|
||||
dragOverTabIndex === index &&
|
||||
draggedTabIndex > index;
|
||||
const isActive = tab.id === currentTab;
|
||||
const isSplit =
|
||||
Array.isArray(allSplitScreenTab) &&
|
||||
@@ -328,40 +415,77 @@ export function TopNavbar({
|
||||
tab.type === "user_profile") &&
|
||||
isSplitScreenActive);
|
||||
const disableClose = (isSplitScreenActive && isActive) || isSplit;
|
||||
const isDragging = draggedTabIndex === index;
|
||||
const isDragOver = dragOverTabIndex === index;
|
||||
|
||||
// Show preview after this position if dragging over and coming from before
|
||||
const showPreviewAfter =
|
||||
draggedTabIndex !== null &&
|
||||
dragOverTabIndex === index &&
|
||||
draggedTabIndex < index;
|
||||
const isDragging = dragState.draggedIndex === index;
|
||||
const dragOffset = isDragging
|
||||
? dragState.currentX - dragState.startX
|
||||
: 0;
|
||||
|
||||
const draggedTab =
|
||||
draggedTabIndex !== null ? tabs[draggedTabIndex] : null;
|
||||
// Calculate transform
|
||||
let transform = "";
|
||||
if (isDragging) {
|
||||
// Dragged tab follows cursor
|
||||
transform = `translateX(${dragOffset}px)`;
|
||||
} else if (
|
||||
dragState.draggedIndex !== null &&
|
||||
dragState.targetIndex !== null
|
||||
) {
|
||||
// Other tabs shift to make room
|
||||
const draggedIndex = dragState.draggedIndex;
|
||||
const targetIndex = dragState.targetIndex;
|
||||
|
||||
if (
|
||||
draggedIndex < targetIndex &&
|
||||
index > draggedIndex &&
|
||||
index <= targetIndex
|
||||
) {
|
||||
// Shifting left
|
||||
const draggedTabEl = tabRefs.current.get(draggedIndex);
|
||||
const draggedWidth =
|
||||
draggedTabEl?.getBoundingClientRect().width || 0;
|
||||
transform = `translateX(-${draggedWidth + 4}px)`;
|
||||
} else if (
|
||||
draggedIndex > targetIndex &&
|
||||
index >= targetIndex &&
|
||||
index < draggedIndex
|
||||
) {
|
||||
// Shifting right
|
||||
const draggedTabEl = tabRefs.current.get(draggedIndex);
|
||||
const draggedWidth =
|
||||
draggedTabEl?.getBoundingClientRect().width || 0;
|
||||
transform = `translateX(${draggedWidth + 4}px)`;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<React.Fragment key={tab.id}>
|
||||
{/* Preview tab before current position */}
|
||||
{showPreviewBefore && draggedTab && (
|
||||
<Tab
|
||||
tabType={draggedTab.type}
|
||||
title={draggedTab.title}
|
||||
isActive={false}
|
||||
canSplit={
|
||||
draggedTab.type === "terminal" ||
|
||||
draggedTab.type === "server" ||
|
||||
draggedTab.type === "file_manager"
|
||||
}
|
||||
canClose={true}
|
||||
disableActivate={true}
|
||||
disableSplit={true}
|
||||
disableClose={true}
|
||||
isDragging={false}
|
||||
isDragOver={true}
|
||||
/>
|
||||
)}
|
||||
|
||||
<div
|
||||
key={tab.id}
|
||||
ref={(el) => {
|
||||
if (el) {
|
||||
tabRefs.current.set(index, el);
|
||||
} else {
|
||||
tabRefs.current.delete(index);
|
||||
}
|
||||
}}
|
||||
draggable={true}
|
||||
onDragStart={(e) => {
|
||||
e.stopPropagation();
|
||||
handleDragStart(e, index);
|
||||
}}
|
||||
onDrag={handleDrag}
|
||||
onDragOver={handleDragOver}
|
||||
onDrop={handleDrop}
|
||||
onDragEnd={handleDragEnd}
|
||||
style={{
|
||||
transform,
|
||||
transition: isDragging ? "none" : "transform 200ms ease-out",
|
||||
zIndex: isDragging ? 1000 : 1,
|
||||
position: "relative",
|
||||
cursor: isDragging ? "grabbing" : "grab",
|
||||
userSelect: "none",
|
||||
WebkitUserSelect: "none",
|
||||
}}
|
||||
>
|
||||
<Tab
|
||||
tabType={tab.type}
|
||||
title={tab.title}
|
||||
@@ -392,35 +516,10 @@ export function TopNavbar({
|
||||
disableActivate={disableActivate}
|
||||
disableSplit={disableSplit}
|
||||
disableClose={disableClose}
|
||||
onDragStart={() => handleDragStart(index)}
|
||||
onDragOver={(e) => handleDragOver(e, index)}
|
||||
onDragLeave={handleDragLeave}
|
||||
onDrop={(e) => handleDrop(e, index)}
|
||||
onDragEnd={handleDragEnd}
|
||||
isDragging={isDragging}
|
||||
isDragOver={false}
|
||||
/>
|
||||
|
||||
{/* Preview tab after current position */}
|
||||
{showPreviewAfter && draggedTab && (
|
||||
<Tab
|
||||
tabType={draggedTab.type}
|
||||
title={draggedTab.title}
|
||||
isActive={false}
|
||||
canSplit={
|
||||
draggedTab.type === "terminal" ||
|
||||
draggedTab.type === "server" ||
|
||||
draggedTab.type === "file_manager"
|
||||
}
|
||||
canClose={true}
|
||||
disableActivate={true}
|
||||
disableSplit={true}
|
||||
disableClose={true}
|
||||
isDragging={false}
|
||||
isDragOver={true}
|
||||
/>
|
||||
)}
|
||||
</React.Fragment>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
@@ -460,7 +559,8 @@ export function TopNavbar({
|
||||
{!isTopbarOpen && (
|
||||
<div
|
||||
onClick={() => setIsTopbarOpen(true)}
|
||||
className="absolute top-0 left-0 w-full h-[10px] bg-dark-bg cursor-pointer z-20 flex items-center justify-center rounded-bl-md rounded-br-md"
|
||||
className="absolute top-0 left-0 w-full h-[10px] cursor-pointer z-20 flex items-center justify-center rounded-bl-md rounded-br-md"
|
||||
style={{ backgroundColor: "#1e1e21" }}
|
||||
>
|
||||
<ChevronDown size={10} />
|
||||
</div>
|
||||
|
||||
@@ -2042,7 +2042,7 @@ export async function getVersionInfo(): Promise<Record<string, unknown>> {
|
||||
|
||||
export async function getDatabaseHealth(): Promise<Record<string, unknown>> {
|
||||
try {
|
||||
const response = await authApi.get("/users/db-health");
|
||||
const response = await authApi.get("/health");
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
handleApiError(error, "check database health");
|
||||
|
||||
Reference in New Issue
Block a user