feat: Connect dashboard to backend and update tab system to be similar to a browser (neither are fully finished)

This commit is contained in:
LukeGus
2025-10-18 02:54:29 -05:00
parent 3901bc9899
commit a44e2be8a4
16 changed files with 1151 additions and 186 deletions

View File

@@ -1,7 +1,7 @@
import React from "react";
import { ButtonGroup } from "@/components/ui/button-group.tsx";
import { Button } from "@/components/ui/button.tsx";
import { useTranslation } from "react-i18next";
import { cn } from "@/lib/utils";
import {
Home,
SeparatorVertical,
@@ -24,6 +24,13 @@ 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;
}
export function Tab({
@@ -38,18 +45,56 @@ 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]",
"rounded-t-lg border-t-2 border-l-2 border-r-2",
"transition-all duration-150 select-none",
isDragOver &&
"bg-background/40 text-muted-foreground border-border opacity-60 cursor-default",
isDragging && "opacity-40 cursor-grabbing",
!isDragOver &&
!isDragging &&
isActive &&
"bg-background text-foreground border-border z-10 cursor-pointer",
!isDragOver &&
!isDragging &&
!isActive &&
"bg-background/80 text-muted-foreground border-border hover:bg-background/90 cursor-pointer",
);
if (tabType === "home") {
return (
<Button
variant="outline"
className={`!px-2 border-1 border-dark-border ${isActive ? "!bg-dark-bg-active !text-white !border-dark-border-active" : ""}`}
onClick={onActivate}
disabled={disableActivate}
<div
className={tabBaseClasses}
{...dragProps}
onClick={!disableActivate ? onActivate : undefined}
style={{
marginBottom: "-2px",
borderBottom: isActive ? "2px solid transparent" : "none",
}}
>
<Home />
</Button>
<Home className="h-4 w-4" />
</div>
);
}
@@ -62,102 +107,147 @@ export function Tab({
const isServer = tabType === "server";
const isFileManager = tabType === "file_manager";
const isUserProfile = tabType === "user_profile";
const displayTitle =
title ||
(isServer
? t("nav.serverStats")
: isFileManager
? t("nav.fileManager")
: isUserProfile
? t("nav.userProfile")
: t("nav.terminal"));
return (
<ButtonGroup>
<Button
variant="outline"
className={`!px-2 border-1 border-dark-border ${isActive ? "!bg-dark-bg-active !text-white !border-dark-border-active" : ""}`}
onClick={onActivate}
disabled={disableActivate}
<div
className={tabBaseClasses}
{...dragProps}
style={{
marginBottom: "-2px",
borderBottom: isActive ? "2px solid transparent" : "none",
}}
>
<div
className="flex items-center gap-1.5 flex-1 min-w-0"
onClick={!disableActivate ? onActivate : undefined}
>
{isServer ? (
<ServerIcon className="mr-1 h-4 w-4" />
<ServerIcon className="h-4 w-4 flex-shrink-0" />
) : isFileManager ? (
<FolderIcon className="mr-1 h-4 w-4" />
<FolderIcon className="h-4 w-4 flex-shrink-0" />
) : isUserProfile ? (
<UserIcon className="mr-1 h-4 w-4" />
<UserIcon className="h-4 w-4 flex-shrink-0" />
) : (
<TerminalIcon className="mr-1 h-4 w-4" />
<TerminalIcon className="h-4 w-4 flex-shrink-0" />
)}
{title ||
(isServer
? t("nav.serverStats")
: isFileManager
? t("nav.fileManager")
: isUserProfile
? t("nav.userProfile")
: t("nav.terminal"))}
</Button>
<span className="truncate text-sm">{displayTitle}</span>
</div>
{canSplit && (
<Button
variant="outline"
className="!px-2 border-1 border-dark-border"
onClick={onSplit}
variant="ghost"
size="icon"
className={cn("h-6 w-6", disableSplit && "opacity-50")}
onClick={(e) => {
e.stopPropagation();
if (!disableSplit && onSplit) onSplit();
}}
disabled={disableSplit}
title={
disableSplit ? t("nav.cannotSplitTab") : t("nav.splitScreen")
}
>
<SeparatorVertical className="w-[28px] h-[28px]" />
<SeparatorVertical className="h-4 w-4" />
</Button>
)}
{canClose && (
<Button
variant="outline"
className="!px-2 border-1 border-dark-border"
onClick={onClose}
variant="ghost"
size="icon"
className={cn("h-6 w-6", disableClose && "opacity-50")}
onClick={(e) => {
e.stopPropagation();
if (!disableClose && onClose) onClose();
}}
disabled={disableClose}
>
<X />
<X className="h-4 w-4" />
</Button>
)}
</ButtonGroup>
</div>
);
}
if (tabType === "ssh_manager") {
return (
<ButtonGroup>
<Button
variant="outline"
className={`!px-2 border-1 border-dark-border ${isActive ? "!bg-dark-bg-active !text-white !border-dark-border-active" : ""}`}
onClick={onActivate}
disabled={disableActivate}
<div
className={tabBaseClasses}
{...dragProps}
style={{
marginBottom: "-2px",
borderBottom: isActive ? "2px solid transparent" : "none",
}}
>
<div
className="flex items-center gap-1.5 flex-1 min-w-0"
onClick={!disableActivate ? onActivate : undefined}
>
{title || t("nav.sshManager")}
</Button>
<Button
variant="outline"
className="!px-2 border-1 border-dark-border"
onClick={onClose}
disabled={disableClose}
>
<X />
</Button>
</ButtonGroup>
<span className="truncate text-sm">
{title || t("nav.sshManager")}
</span>
</div>
{canClose && (
<Button
variant="ghost"
size="icon"
className={cn("h-6 w-6", disableClose && "opacity-50")}
onClick={(e) => {
e.stopPropagation();
if (!disableClose && onClose) onClose();
}}
disabled={disableClose}
>
<X className="h-4 w-4" />
</Button>
)}
</div>
);
}
if (tabType === "admin") {
return (
<ButtonGroup>
<Button
variant="outline"
className={`!px-2 border-1 border-dark-border ${isActive ? "!bg-dark-bg-active !text-white !border-dark-border-active" : ""}`}
onClick={onActivate}
disabled={disableActivate}
<div
className={tabBaseClasses}
{...dragProps}
style={{
marginBottom: "-2px",
borderBottom: isActive ? "2px solid transparent" : "none",
}}
>
<div
className="flex items-center gap-1.5 flex-1 min-w-0"
onClick={!disableActivate ? onActivate : undefined}
>
{title || t("nav.admin")}
</Button>
<Button
variant="outline"
className="!px-2 border-1 border-dark-border"
onClick={onClose}
disabled={disableClose}
>
<X />
</Button>
</ButtonGroup>
<span className="truncate text-sm">{title || t("nav.admin")}</span>
</div>
{canClose && (
<Button
variant="ghost"
size="icon"
className={cn("h-6 w-6", disableClose && "opacity-50")}
onClick={(e) => {
e.stopPropagation();
if (!disableClose && onClose) onClose();
}}
disabled={disableClose}
>
<X className="h-4 w-4" />
</Button>
)}
</div>
);
}

View File

@@ -20,6 +20,7 @@ interface TabContextType {
setCurrentTab: (tabId: number) => void;
setSplitScreenTab: (tabId: number) => void;
getTab: (tabId: number) => Tab | undefined;
reorderTabs: (fromIndex: number, toIndex: number) => void;
updateHostConfig: (
hostId: number,
newHostConfig: {
@@ -152,6 +153,15 @@ export function TabProvider({ children }: TabProviderProps) {
return tabs.find((tab) => tab.id === tabId);
};
const reorderTabs = (fromIndex: number, toIndex: number) => {
setTabs((prev) => {
const newTabs = [...prev];
const [movedTab] = newTabs.splice(fromIndex, 1);
newTabs.splice(toIndex, 0, movedTab);
return newTabs;
});
};
const updateHostConfig = (
hostId: number,
newHostConfig: {
@@ -187,6 +197,7 @@ export function TabProvider({ children }: TabProviderProps) {
setCurrentTab,
setSplitScreenTab,
getTab,
reorderTabs,
updateHostConfig,
};

View File

@@ -41,6 +41,7 @@ export function TopNavbar({
setSplitScreenTab,
removeTab,
allSplitScreenTab,
reorderTabs,
} = useTabs() as {
tabs: TabData[];
currentTab: number;
@@ -48,6 +49,7 @@ export function TopNavbar({
setSplitScreenTab: (id: number) => void;
removeTab: (id: number) => void;
allSplitScreenTab: number[];
reorderTabs: (fromIndex: number, toIndex: number) => void;
};
const leftPosition = state === "collapsed" ? "26px" : "264px";
const { t } = useTranslation();
@@ -56,6 +58,8 @@ 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 handleTabActivate = (tabId: number) => {
setCurrentTab(tabId);
@@ -234,6 +238,35 @@ export function TopNavbar({
}
};
const handleDragStart = (index: number) => {
setDraggedTabIndex(index);
};
const handleDragOver = (e: React.DragEvent, index: number) => {
e.preventDefault();
if (draggedTabIndex !== null && draggedTabIndex !== index) {
setDragOverTabIndex(index);
}
};
const handleDragLeave = () => {
setDragOverTabIndex(null);
};
const handleDrop = (e: React.DragEvent, dropIndex: number) => {
e.preventDefault();
if (draggedTabIndex !== null && draggedTabIndex !== dropIndex) {
reorderTabs(draggedTabIndex, dropIndex);
}
setDraggedTabIndex(null);
setDragOverTabIndex(null);
};
const handleDragEnd = () => {
setDraggedTabIndex(null);
setDragOverTabIndex(null);
};
const isSplitScreenActive =
Array.isArray(allSplitScreenTab) && allSplitScreenTab.length > 0;
const currentTabObj = tabs.find((t: TabData) => t.id === currentTab);
@@ -258,8 +291,13 @@ export function TopNavbar({
right: "17px",
}}
>
<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-2 thin-scrollbar">
{tabs.map((tab: TabData) => {
<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">
{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) &&
@@ -290,39 +328,99 @@ 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 draggedTab =
draggedTabIndex !== null ? tabs[draggedTabIndex] : null;
return (
<Tab
key={tab.id}
tabType={tab.type}
title={tab.title}
isActive={isActive}
onActivate={() => handleTabActivate(tab.id)}
onClose={
isTerminal ||
isServer ||
isFileManager ||
isSshManager ||
isAdmin ||
isUserProfile
? () => handleTabClose(tab.id)
: undefined
}
onSplit={
isSplittable ? () => handleTabSplit(tab.id) : undefined
}
canSplit={isSplittable}
canClose={
isTerminal ||
isServer ||
isFileManager ||
isSshManager ||
isAdmin ||
isUserProfile
}
disableActivate={disableActivate}
disableSplit={disableSplit}
disableClose={disableClose}
/>
<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}
/>
)}
<Tab
tabType={tab.type}
title={tab.title}
isActive={isActive}
onActivate={() => handleTabActivate(tab.id)}
onClose={
isTerminal ||
isServer ||
isFileManager ||
isSshManager ||
isAdmin ||
isUserProfile
? () => handleTabClose(tab.id)
: undefined
}
onSplit={
isSplittable ? () => handleTabSplit(tab.id) : undefined
}
canSplit={isSplittable}
canClose={
isTerminal ||
isServer ||
isFileManager ||
isSshManager ||
isAdmin ||
isUserProfile
}
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>