feat: Connect dashboard to backend and update tab system to be similar to a browser (neither are fully finished)
This commit is contained in:
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user