feat: begin dashboard overhaul by splitting into cards and adding customization
This commit is contained in:
@@ -358,6 +358,15 @@ http {
|
|||||||
proxy_set_header X-Forwarded-Proto $scheme;
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
location ~ ^/dashboard/preferences(/.*)?$ {
|
||||||
|
proxy_pass http://127.0.0.1:30006;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
}
|
||||||
|
|
||||||
location ^~ /docker/console/ {
|
location ^~ /docker/console/ {
|
||||||
proxy_pass http://127.0.0.1:30008/;
|
proxy_pass http://127.0.0.1:30008/;
|
||||||
proxy_http_version 1.1;
|
proxy_http_version 1.1;
|
||||||
|
|||||||
@@ -347,6 +347,15 @@ http {
|
|||||||
proxy_set_header X-Forwarded-Proto $scheme;
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
location ~ ^/dashboard/preferences(/.*)?$ {
|
||||||
|
proxy_pass http://127.0.0.1:30006;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
}
|
||||||
|
|
||||||
location ^~ /docker/console/ {
|
location ^~ /docker/console/ {
|
||||||
proxy_pass http://127.0.0.1:30008/;
|
proxy_pass http://127.0.0.1:30008/;
|
||||||
proxy_http_version 1.1;
|
proxy_http_version 1.1;
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "termix",
|
"name": "termix",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "1.10.0",
|
"version": "1.10.1",
|
||||||
"description": "A web-based server management platform with SSH terminal, tunneling, and file editing capabilities",
|
"description": "A web-based server management platform with SSH terminal, tunneling, and file editing capabilities",
|
||||||
"author": "Karmaa",
|
"author": "Karmaa",
|
||||||
"main": "electron/main.cjs",
|
"main": "electron/main.cjs",
|
||||||
|
|||||||
@@ -1,9 +1,14 @@
|
|||||||
import express from "express";
|
import express from "express";
|
||||||
import cors from "cors";
|
import cors from "cors";
|
||||||
import cookieParser from "cookie-parser";
|
import cookieParser from "cookie-parser";
|
||||||
import { getDb } from "./database/db/index.js";
|
import { getDb, DatabaseSaveTrigger } from "./database/db/index.js";
|
||||||
import { recentActivity, sshData, hostAccess } from "./database/db/schema.js";
|
import {
|
||||||
import { eq, and, desc, or } from "drizzle-orm";
|
recentActivity,
|
||||||
|
sshData,
|
||||||
|
hostAccess,
|
||||||
|
dashboardPreferences,
|
||||||
|
} from "./database/db/schema.js";
|
||||||
|
import { eq, and, desc, or, sql } from "drizzle-orm";
|
||||||
import { dashboardLogger } from "./utils/logger.js";
|
import { dashboardLogger } from "./utils/logger.js";
|
||||||
import { SimpleDBOps } from "./utils/simple-db-ops.js";
|
import { SimpleDBOps } from "./utils/simple-db-ops.js";
|
||||||
import { AuthManager } from "./utils/auth-manager.js";
|
import { AuthManager } from "./utils/auth-manager.js";
|
||||||
@@ -350,6 +355,166 @@ app.delete("/activity/reset", async (req, res) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @openapi
|
||||||
|
* /dashboard/preferences:
|
||||||
|
* get:
|
||||||
|
* summary: Get dashboard layout preferences
|
||||||
|
* description: Returns the user's customized dashboard layout settings. If no preferences exist, returns default layout.
|
||||||
|
* tags:
|
||||||
|
* - Dashboard
|
||||||
|
* responses:
|
||||||
|
* 200:
|
||||||
|
* description: Dashboard preferences retrieved
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* type: object
|
||||||
|
* properties:
|
||||||
|
* cards:
|
||||||
|
* type: array
|
||||||
|
* items:
|
||||||
|
* type: object
|
||||||
|
* properties:
|
||||||
|
* id:
|
||||||
|
* type: string
|
||||||
|
* enabled:
|
||||||
|
* type: boolean
|
||||||
|
* order:
|
||||||
|
* type: integer
|
||||||
|
* gridColumns:
|
||||||
|
* type: integer
|
||||||
|
* 401:
|
||||||
|
* description: Session expired
|
||||||
|
* 500:
|
||||||
|
* description: Failed to get preferences
|
||||||
|
*/
|
||||||
|
app.get("/dashboard/preferences", async (req, res) => {
|
||||||
|
try {
|
||||||
|
const userId = (req as AuthenticatedRequest).userId;
|
||||||
|
|
||||||
|
if (!SimpleDBOps.isUserDataUnlocked(userId)) {
|
||||||
|
return res.status(401).json({
|
||||||
|
error: "Session expired - please log in again",
|
||||||
|
code: "SESSION_EXPIRED",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const preferences = await getDb()
|
||||||
|
.select()
|
||||||
|
.from(dashboardPreferences)
|
||||||
|
.where(eq(dashboardPreferences.userId, userId));
|
||||||
|
|
||||||
|
if (preferences.length === 0) {
|
||||||
|
const defaultLayout = {
|
||||||
|
cards: [
|
||||||
|
{ id: "server_overview", enabled: true, order: 1 },
|
||||||
|
{ id: "recent_activity", enabled: true, order: 2 },
|
||||||
|
{ id: "network_graph", enabled: false, order: 3 },
|
||||||
|
{ id: "quick_actions", enabled: true, order: 4 },
|
||||||
|
{ id: "server_stats", enabled: true, order: 5 },
|
||||||
|
],
|
||||||
|
gridColumns: 2,
|
||||||
|
};
|
||||||
|
return res.json(defaultLayout);
|
||||||
|
}
|
||||||
|
|
||||||
|
const layout = JSON.parse(preferences[0].layout as string);
|
||||||
|
res.json(layout);
|
||||||
|
} catch (err) {
|
||||||
|
dashboardLogger.error("Failed to get dashboard preferences", err);
|
||||||
|
res.status(500).json({ error: "Failed to get dashboard preferences" });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @openapi
|
||||||
|
* /dashboard/preferences:
|
||||||
|
* post:
|
||||||
|
* summary: Save dashboard layout preferences
|
||||||
|
* description: Saves or updates the user's customized dashboard layout settings.
|
||||||
|
* tags:
|
||||||
|
* - Dashboard
|
||||||
|
* requestBody:
|
||||||
|
* required: true
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* type: object
|
||||||
|
* properties:
|
||||||
|
* cards:
|
||||||
|
* type: array
|
||||||
|
* items:
|
||||||
|
* type: object
|
||||||
|
* properties:
|
||||||
|
* id:
|
||||||
|
* type: string
|
||||||
|
* enabled:
|
||||||
|
* type: boolean
|
||||||
|
* order:
|
||||||
|
* type: integer
|
||||||
|
* gridColumns:
|
||||||
|
* type: integer
|
||||||
|
* responses:
|
||||||
|
* 200:
|
||||||
|
* description: Preferences saved successfully
|
||||||
|
* 400:
|
||||||
|
* description: Invalid request body
|
||||||
|
* 401:
|
||||||
|
* description: Session expired
|
||||||
|
* 500:
|
||||||
|
* description: Failed to save preferences
|
||||||
|
*/
|
||||||
|
app.post("/dashboard/preferences", async (req, res) => {
|
||||||
|
try {
|
||||||
|
const userId = (req as AuthenticatedRequest).userId;
|
||||||
|
|
||||||
|
if (!SimpleDBOps.isUserDataUnlocked(userId)) {
|
||||||
|
return res.status(401).json({
|
||||||
|
error: "Session expired - please log in again",
|
||||||
|
code: "SESSION_EXPIRED",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const { cards, gridColumns } = req.body;
|
||||||
|
|
||||||
|
if (!cards || !Array.isArray(cards) || typeof gridColumns !== "number") {
|
||||||
|
return res.status(400).json({
|
||||||
|
error:
|
||||||
|
"Invalid request body. Expected { cards: Array, gridColumns: number }",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const layout = JSON.stringify({ cards, gridColumns });
|
||||||
|
|
||||||
|
const existing = await getDb()
|
||||||
|
.select()
|
||||||
|
.from(dashboardPreferences)
|
||||||
|
.where(eq(dashboardPreferences.userId, userId));
|
||||||
|
|
||||||
|
if (existing.length > 0) {
|
||||||
|
await getDb()
|
||||||
|
.update(dashboardPreferences)
|
||||||
|
.set({ layout, updatedAt: sql`CURRENT_TIMESTAMP` })
|
||||||
|
.where(eq(dashboardPreferences.userId, userId));
|
||||||
|
} else {
|
||||||
|
await getDb().insert(dashboardPreferences).values({ userId, layout });
|
||||||
|
}
|
||||||
|
|
||||||
|
await DatabaseSaveTrigger.triggerSave("dashboard_preferences_updated");
|
||||||
|
|
||||||
|
dashboardLogger.success("Dashboard preferences saved", {
|
||||||
|
operation: "save_dashboard_preferences",
|
||||||
|
userId,
|
||||||
|
});
|
||||||
|
|
||||||
|
res.json({ success: true, message: "Dashboard preferences saved" });
|
||||||
|
} catch (err) {
|
||||||
|
dashboardLogger.error("Failed to save dashboard preferences", err);
|
||||||
|
res.status(500).json({ error: "Failed to save dashboard preferences" });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
const PORT = 30006;
|
const PORT = 30006;
|
||||||
app.listen(PORT, async () => {
|
app.listen(PORT, async () => {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -703,6 +703,30 @@ const migrateSchema = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
sqlite
|
||||||
|
.prepare("SELECT id FROM dashboard_preferences LIMIT 1")
|
||||||
|
.get();
|
||||||
|
} catch {
|
||||||
|
try {
|
||||||
|
sqlite.exec(`
|
||||||
|
CREATE TABLE IF NOT EXISTS dashboard_preferences (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
user_id TEXT NOT NULL UNIQUE,
|
||||||
|
layout TEXT NOT NULL,
|
||||||
|
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
`);
|
||||||
|
} catch (createError) {
|
||||||
|
databaseLogger.warn("Failed to create dashboard_preferences table", {
|
||||||
|
operation: "schema_migration",
|
||||||
|
error: createError,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
sqlite.prepare("SELECT id FROM host_access LIMIT 1").get();
|
sqlite.prepare("SELECT id FROM host_access LIMIT 1").get();
|
||||||
} catch {
|
} catch {
|
||||||
|
|||||||
@@ -324,6 +324,21 @@ export const networkTopology = sqliteTable("network_topology", {
|
|||||||
.default(sql`CURRENT_TIMESTAMP`),
|
.default(sql`CURRENT_TIMESTAMP`),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const dashboardPreferences = sqliteTable("dashboard_preferences", {
|
||||||
|
id: integer("id").primaryKey({ autoIncrement: true }),
|
||||||
|
userId: text("user_id")
|
||||||
|
.notNull()
|
||||||
|
.unique()
|
||||||
|
.references(() => users.id, { onDelete: "cascade" }),
|
||||||
|
layout: text("layout").notNull(),
|
||||||
|
createdAt: text("created_at")
|
||||||
|
.notNull()
|
||||||
|
.default(sql`CURRENT_TIMESTAMP`),
|
||||||
|
updatedAt: text("updated_at")
|
||||||
|
.notNull()
|
||||||
|
.default(sql`CURRENT_TIMESTAMP`),
|
||||||
|
});
|
||||||
|
|
||||||
export const hostAccess = sqliteTable("host_access", {
|
export const hostAccess = sqliteTable("host_access", {
|
||||||
id: integer("id").primaryKey({ autoIncrement: true }),
|
id: integer("id").primaryKey({ autoIncrement: true }),
|
||||||
hostId: integer("host_id")
|
hostId: integer("host_id")
|
||||||
|
|||||||
@@ -2261,7 +2261,20 @@
|
|||||||
"noServerData": "No server data available",
|
"noServerData": "No server data available",
|
||||||
"cpu": "CPU",
|
"cpu": "CPU",
|
||||||
"ram": "RAM",
|
"ram": "RAM",
|
||||||
"notAvailable": "N/A"
|
"notAvailable": "N/A",
|
||||||
|
"customizeLayout": "Customize Dashboard",
|
||||||
|
"dashboardSettings": "Dashboard Settings",
|
||||||
|
"enableDisableCards": "Enable/Disable Cards",
|
||||||
|
"gridColumns": "Grid Columns",
|
||||||
|
"column": "Column",
|
||||||
|
"columns": "Columns",
|
||||||
|
"resetLayout": "Reset to Default",
|
||||||
|
"serverOverviewCard": "Server Overview",
|
||||||
|
"recentActivityCard": "Recent Activity",
|
||||||
|
"networkGraphCard": "Network Graph",
|
||||||
|
"quickActionsCard": "Quick Actions",
|
||||||
|
"serverStatsCard": "Server Stats",
|
||||||
|
"networkGraph": "Network Graph"
|
||||||
},
|
},
|
||||||
"rbac": {
|
"rbac": {
|
||||||
"shareHost": "Share Host",
|
"shareHost": "Share Host",
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import { TopNavbar } from "@/ui/desktop/navigation/TopNavbar.tsx";
|
|||||||
import { CommandHistoryProvider } from "@/ui/desktop/apps/features/terminal/command-history/CommandHistoryContext.tsx";
|
import { CommandHistoryProvider } from "@/ui/desktop/apps/features/terminal/command-history/CommandHistoryContext.tsx";
|
||||||
import { AdminSettings } from "@/ui/desktop/apps/admin/AdminSettings.tsx";
|
import { AdminSettings } from "@/ui/desktop/apps/admin/AdminSettings.tsx";
|
||||||
import { UserProfile } from "@/ui/desktop/user/UserProfile.tsx";
|
import { UserProfile } from "@/ui/desktop/user/UserProfile.tsx";
|
||||||
import { NetworkGraphView } from "@/ui/desktop/dashboard/network-graph";
|
import { NetworkGraphCard } from "@/ui/desktop/apps/dashboard/cards/NetworkGraphCard";
|
||||||
import { Toaster } from "@/components/ui/sonner.tsx";
|
import { Toaster } from "@/components/ui/sonner.tsx";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { CommandPalette } from "@/ui/desktop/apps/command-palette/CommandPalette.tsx";
|
import { CommandPalette } from "@/ui/desktop/apps/command-palette/CommandPalette.tsx";
|
||||||
@@ -414,11 +414,10 @@ function AppContent() {
|
|||||||
|
|
||||||
{showNetworkGraph && (
|
{showNetworkGraph && (
|
||||||
<div className="h-screen w-full visible pointer-events-auto static overflow-hidden">
|
<div className="h-screen w-full visible pointer-events-auto static overflow-hidden">
|
||||||
<NetworkGraphView
|
<NetworkGraphCard
|
||||||
isTopbarOpen={isTopbarOpen}
|
isTopbarOpen={isTopbarOpen}
|
||||||
rightSidebarOpen={rightSidebarOpen}
|
rightSidebarOpen={rightSidebarOpen}
|
||||||
rightSidebarWidth={rightSidebarWidth}
|
rightSidebarWidth={rightSidebarWidth}
|
||||||
isStandalone={true}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import NetworkGraphView from "@/ui/desktop/dashboard/network-graph/NetworkGraphView";
|
import { NetworkGraphCard } from "@/ui/desktop/apps/dashboard/cards/NetworkGraphCard";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
const NetworkGraphApp: React.FC = () => {
|
const NetworkGraphApp: React.FC = () => {
|
||||||
return (
|
return (
|
||||||
<div className="w-full h-screen">
|
<div className="w-full h-screen flex flex-col">
|
||||||
<NetworkGraphView />
|
<NetworkGraphCard />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import { NetworkGraphView } from "@/ui/desktop/dashboard/network-graph";
|
|
||||||
import { Auth } from "@/ui/desktop/authentication/Auth.tsx";
|
import { Auth } from "@/ui/desktop/authentication/Auth.tsx";
|
||||||
import { UpdateLog } from "@/ui/desktop/apps/dashboard/apps/UpdateLog.tsx";
|
|
||||||
import { AlertManager } from "@/ui/desktop/apps/dashboard/apps/alerts/AlertManager.tsx";
|
import { AlertManager } from "@/ui/desktop/apps/dashboard/apps/alerts/AlertManager.tsx";
|
||||||
import { Button } from "@/components/ui/button.tsx";
|
import { Button } from "@/components/ui/button.tsx";
|
||||||
import {
|
import {
|
||||||
@@ -11,7 +9,6 @@ import {
|
|||||||
getUptime,
|
getUptime,
|
||||||
getVersionInfo,
|
getVersionInfo,
|
||||||
getSSHHosts,
|
getSSHHosts,
|
||||||
getTunnelStatuses,
|
|
||||||
getCredentials,
|
getCredentials,
|
||||||
getRecentActivity,
|
getRecentActivity,
|
||||||
resetRecentActivity,
|
resetRecentActivity,
|
||||||
@@ -21,29 +18,16 @@ import {
|
|||||||
import { useSidebar } from "@/components/ui/sidebar.tsx";
|
import { useSidebar } from "@/components/ui/sidebar.tsx";
|
||||||
import { Separator } from "@/components/ui/separator.tsx";
|
import { Separator } from "@/components/ui/separator.tsx";
|
||||||
import { useTabs } from "@/ui/desktop/navigation/tabs/TabContext.tsx";
|
import { useTabs } from "@/ui/desktop/navigation/tabs/TabContext.tsx";
|
||||||
import { Kbd, KbdGroup } from "@/components/ui/kbd";
|
import { Kbd } from "@/components/ui/kbd";
|
||||||
import {
|
|
||||||
ChartLine,
|
|
||||||
Clock,
|
|
||||||
Database,
|
|
||||||
FastForward,
|
|
||||||
History,
|
|
||||||
Key,
|
|
||||||
Network,
|
|
||||||
Server,
|
|
||||||
UserPlus,
|
|
||||||
Settings,
|
|
||||||
User,
|
|
||||||
Loader2,
|
|
||||||
Terminal,
|
|
||||||
FolderOpen,
|
|
||||||
Activity,
|
|
||||||
Container,
|
|
||||||
ArrowDownUp,
|
|
||||||
} from "lucide-react";
|
|
||||||
import { Status } from "@/components/ui/shadcn-io/status";
|
|
||||||
import { BsLightning } from "react-icons/bs";
|
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { Settings as SettingsIcon } from "lucide-react";
|
||||||
|
import { ServerOverviewCard } from "@/ui/desktop/apps/dashboard/cards/ServerOverviewCard";
|
||||||
|
import { RecentActivityCard } from "@/ui/desktop/apps/dashboard/cards/RecentActivityCard";
|
||||||
|
import { QuickActionsCard } from "@/ui/desktop/apps/dashboard/cards/QuickActionsCard";
|
||||||
|
import { ServerStatsCard } from "@/ui/desktop/apps/dashboard/cards/ServerStatsCard";
|
||||||
|
import { NetworkGraphCard } from "@/ui/desktop/apps/dashboard/cards/NetworkGraphCard";
|
||||||
|
import { useDashboardPreferences } from "@/ui/desktop/apps/dashboard/hooks/useDashboardPreferences";
|
||||||
|
import { DashboardSettingsDialog } from "@/ui/desktop/apps/dashboard/components/DashboardSettingsDialog";
|
||||||
|
|
||||||
interface DashboardProps {
|
interface DashboardProps {
|
||||||
onSelectView: (view: string) => void;
|
onSelectView: (view: string) => void;
|
||||||
@@ -94,9 +78,15 @@ export function Dashboard({
|
|||||||
Array<{ id: number; name: string; cpu: number | null; ram: number | null }>
|
Array<{ id: number; name: string; cpu: number | null; ram: number | null }>
|
||||||
>([]);
|
>([]);
|
||||||
const [serverStatsLoading, setServerStatsLoading] = useState<boolean>(true);
|
const [serverStatsLoading, setServerStatsLoading] = useState<boolean>(true);
|
||||||
const [showNetworkGraph, setShowNetworkGraph] = useState<boolean>(false);
|
const [settingsDialogOpen, setSettingsDialogOpen] = useState(false);
|
||||||
|
|
||||||
const { addTab, setCurrentTab, tabs: tabList, updateTab } = useTabs();
|
const { addTab, setCurrentTab, tabs: tabList, updateTab } = useTabs();
|
||||||
|
const {
|
||||||
|
layout,
|
||||||
|
loading: preferencesLoading,
|
||||||
|
updateLayout,
|
||||||
|
resetLayout,
|
||||||
|
} = useDashboardPreferences();
|
||||||
|
|
||||||
let sidebarState: "expanded" | "collapsed" = "expanded";
|
let sidebarState: "expanded" | "collapsed" = "expanded";
|
||||||
try {
|
try {
|
||||||
@@ -436,8 +426,18 @@ export function Dashboard({
|
|||||||
>
|
>
|
||||||
<div className="flex flex-col relative z-10 w-full h-full min-w-0">
|
<div className="flex flex-col relative z-10 w-full h-full min-w-0">
|
||||||
<div className="flex flex-row items-center justify-between w-full px-3 mt-3 min-w-0 flex-wrap gap-2">
|
<div className="flex flex-row items-center justify-between w-full px-3 mt-3 min-w-0 flex-wrap gap-2">
|
||||||
<div className="text-2xl text-foreground font-semibold shrink-0">
|
<div className="flex flex-row items-center gap-3">
|
||||||
{t("dashboard.title")}
|
<div className="text-2xl text-foreground font-semibold shrink-0">
|
||||||
|
{t("dashboard.title")}
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
className="font-semibold shrink-0 !bg-canvas"
|
||||||
|
onClick={() => setSettingsDialogOpen(true)}
|
||||||
|
>
|
||||||
|
{t("dashboard.customizeLayout")}
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-row gap-3 flex-wrap min-w-0">
|
<div className="flex flex-row gap-3 flex-wrap min-w-0">
|
||||||
<div className="flex flex-col items-center gap-4 justify-center mr-5 min-w-0 shrink">
|
<div className="flex flex-col items-center gap-4 justify-center mr-5 min-w-0 shrink">
|
||||||
@@ -496,426 +496,91 @@ export function Dashboard({
|
|||||||
<Separator className="mt-3 p-0.25" />
|
<Separator className="mt-3 p-0.25" />
|
||||||
|
|
||||||
<div className="flex flex-col flex-1 my-5 mx-5 gap-4 min-h-0 min-w-0">
|
<div className="flex flex-col flex-1 my-5 mx-5 gap-4 min-h-0 min-w-0">
|
||||||
<div className="flex flex-row flex-1 gap-4 min-h-0 min-w-0">
|
{!preferencesLoading && layout && (
|
||||||
<div className="flex-1 min-w-0 border-2 border-edge rounded-md bg-elevated flex flex-col overflow-hidden transition-all duration-150 hover:border-primary/20">
|
<div
|
||||||
<div className="flex flex-col mx-3 my-2 overflow-y-auto overflow-x-hidden thin-scrollbar">
|
className="grid gap-4 flex-1 min-h-0 auto-rows-fr"
|
||||||
<p className="text-xl font-semibold mb-3 mt-1 flex flex-row items-center">
|
style={{
|
||||||
<Server className="mr-3" />
|
gridTemplateColumns: `repeat(${layout.gridColumns}, minmax(0, 1fr))`,
|
||||||
{t("dashboard.serverOverview")}
|
}}
|
||||||
</p>
|
>
|
||||||
<div className="bg-canvas w-full h-auto border-2 border-edge rounded-md px-3 py-3">
|
{layout.cards
|
||||||
<div className="flex flex-row items-center justify-between mb-3 min-w-0 gap-2">
|
.filter((card) => card.enabled)
|
||||||
<div className="flex flex-row items-center min-w-0">
|
.sort((a, b) => a.order - b.order)
|
||||||
<History size={20} className="shrink-0" />
|
.map((card) => {
|
||||||
<p className="ml-2 leading-none truncate">
|
if (card.id === "server_overview") {
|
||||||
{t("dashboard.version")}
|
return (
|
||||||
</p>
|
<ServerOverviewCard
|
||||||
</div>
|
key={card.id}
|
||||||
|
loggedIn={loggedIn}
|
||||||
<div className="flex flex-row items-center">
|
versionText={versionText}
|
||||||
<p className="leading-none text-muted-foreground">
|
versionStatus={versionStatus}
|
||||||
{versionText}
|
uptime={uptime}
|
||||||
</p>
|
dbHealth={dbHealth}
|
||||||
<Button
|
totalServers={totalServers}
|
||||||
variant="outline"
|
totalTunnels={totalTunnels}
|
||||||
size="sm"
|
totalCredentials={totalCredentials}
|
||||||
className={`ml-2 text-sm border-1 border-edge ${versionStatus === "up_to_date" ? "text-green-400" : "text-yellow-400"}`}
|
|
||||||
>
|
|
||||||
{versionStatus === "up_to_date"
|
|
||||||
? t("dashboard.upToDate")
|
|
||||||
: t("dashboard.updateAvailable")}
|
|
||||||
</Button>
|
|
||||||
<UpdateLog loggedIn={loggedIn} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex flex-row items-center justify-between mb-5 min-w-0 gap-2">
|
|
||||||
<div className="flex flex-row items-center min-w-0">
|
|
||||||
<Clock size={20} className="shrink-0" />
|
|
||||||
<p className="ml-2 leading-none truncate">
|
|
||||||
{t("dashboard.uptime")}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex flex-row items-center">
|
|
||||||
<p className="leading-none text-muted-foreground">
|
|
||||||
{uptime}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex flex-row items-center justify-between min-w-0 gap-2">
|
|
||||||
<div className="flex flex-row items-center min-w-0">
|
|
||||||
<Database size={20} className="shrink-0" />
|
|
||||||
<p className="ml-2 leading-none truncate">
|
|
||||||
{t("dashboard.database")}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex flex-row items-center">
|
|
||||||
<p
|
|
||||||
className={`leading-none ${dbHealth === "healthy" ? "text-green-400" : "text-red-400"}`}
|
|
||||||
>
|
|
||||||
{dbHealth === "healthy"
|
|
||||||
? t("dashboard.healthy")
|
|
||||||
: t("dashboard.error")}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col grid grid-cols-2 gap-2 mt-2">
|
|
||||||
<div className="flex flex-row items-center justify-between bg-canvas w-full h-auto mt-3 border-2 border-edge rounded-md px-3 py-3 min-w-0 gap-2">
|
|
||||||
<div className="flex flex-row items-center min-w-0">
|
|
||||||
<Server size={16} className="mr-3 shrink-0" />
|
|
||||||
<p className="m-0 leading-none truncate">
|
|
||||||
{t("dashboard.totalServers")}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<p className="m-0 leading-none text-muted-foreground font-semibold">
|
|
||||||
{totalServers}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-row items-center justify-between bg-canvas w-full h-auto mt-3 border-2 border-edge rounded-md px-3 py-3 min-w-0 gap-2">
|
|
||||||
<div className="flex flex-row items-center min-w-0">
|
|
||||||
<ArrowDownUp size={16} className="mr-3 shrink-0" />
|
|
||||||
<p className="m-0 leading-none truncate">
|
|
||||||
{t("dashboard.totalTunnels")}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<p className="m-0 leading-none text-muted-foreground font-semibold">
|
|
||||||
{totalTunnels}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col grid grid-cols-2 gap-2 mt-2">
|
|
||||||
<div className="flex flex-row items-center justify-between bg-canvas w-full h-auto mt-3 border-2 border-edge rounded-md px-3 py-3 min-w-0 gap-2">
|
|
||||||
<div className="flex flex-row items-center min-w-0">
|
|
||||||
<Key size={16} className="mr-3 shrink-0" />
|
|
||||||
<p className="m-0 leading-none truncate">
|
|
||||||
{t("dashboard.totalCredentials")}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<p className="m-0 leading-none text-muted-foreground font-semibold">
|
|
||||||
{totalCredentials}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex-1 min-w-0 border-2 border-edge rounded-md bg-elevated flex flex-col overflow-hidden transition-all duration-150 hover:border-primary/20">
|
|
||||||
<div className="flex flex-col mx-3 my-2 flex-1 overflow-hidden">
|
|
||||||
<div className="flex flex-row items-center justify-between mb-3 mt-1">
|
|
||||||
<p className="text-xl font-semibold flex flex-row items-center">
|
|
||||||
{showNetworkGraph ? (
|
|
||||||
<>
|
|
||||||
<Network className="mr-3" />
|
|
||||||
{t("dashboard.networkGraph", {
|
|
||||||
defaultValue: "Network Graph",
|
|
||||||
})}
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<Clock className="mr-3" />
|
|
||||||
{t("dashboard.recentActivity")}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</p>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
className="border-2 !border-dark-border h-7"
|
|
||||||
onClick={() => setShowNetworkGraph(!showNetworkGraph)}
|
|
||||||
>
|
|
||||||
{showNetworkGraph ? "Show Activity" : "Show Graph"}
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
className="border-2 !border-dark-border h-7"
|
|
||||||
onClick={handleResetActivity}
|
|
||||||
>
|
|
||||||
{t("dashboard.reset")}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
className="border-2 !border-edge h-7 !bg-canvas"
|
|
||||||
onClick={handleResetActivity}
|
|
||||||
>
|
|
||||||
{t("dashboard.reset")}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
className={`grid gap-4 grid-cols-3 auto-rows-min overflow-x-hidden thin-scrollbar ${recentActivityLoading ? "overflow-y-hidden" : "overflow-y-auto"}`}
|
|
||||||
>
|
|
||||||
{recentActivityLoading ? (
|
|
||||||
<div className="flex flex-row items-center text-muted-foreground text-sm animate-pulse">
|
|
||||||
<Loader2 className="animate-spin mr-2" size={16} />
|
|
||||||
<span>{t("dashboard.loadingRecentActivity")}</span>
|
|
||||||
</div>
|
|
||||||
) : recentActivity.length === 0 ? (
|
|
||||||
<p className="text-muted-foreground text-sm">
|
|
||||||
{t("dashboard.noRecentActivity")}
|
|
||||||
</p>
|
|
||||||
) : (
|
|
||||||
recentActivity
|
|
||||||
.filter((item, index, array) => {
|
|
||||||
if (index === 0) return true;
|
|
||||||
|
|
||||||
const prevItem = array[index - 1];
|
|
||||||
return !(
|
|
||||||
item.hostId === prevItem.hostId &&
|
|
||||||
item.type === prevItem.type
|
|
||||||
);
|
|
||||||
})
|
|
||||||
.map((item) => (
|
|
||||||
<Button
|
|
||||||
key={item.id}
|
|
||||||
variant="outline"
|
|
||||||
className="border-2 !border-edge !bg-canvas min-w-0"
|
|
||||||
onClick={() => handleActivityClick(item)}
|
|
||||||
>
|
|
||||||
{item.type === "terminal" ? (
|
|
||||||
<Terminal size={20} className="shrink-0" />
|
|
||||||
) : item.type === "file_manager" ? (
|
|
||||||
<FolderOpen size={20} className="shrink-0" />
|
|
||||||
) : item.type === "server_stats" ? (
|
|
||||||
<Server size={20} className="shrink-0" />
|
|
||||||
) : item.type === "tunnel" ? (
|
|
||||||
<ArrowDownUp size={20} className="shrink-0" />
|
|
||||||
) : item.type === "docker" ? (
|
|
||||||
<Container size={20} className="shrink-0" />
|
|
||||||
) : (
|
|
||||||
<Terminal size={20} className="shrink-0" />
|
|
||||||
)}
|
|
||||||
<p className="truncate ml-2 font-semibold">
|
|
||||||
{item.hostName}
|
|
||||||
</p>
|
|
||||||
</Button>
|
|
||||||
))
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
{showNetworkGraph ? (
|
|
||||||
<NetworkGraphView />
|
|
||||||
) : (
|
|
||||||
<div
|
|
||||||
className={`grid gap-4 grid-cols-3 auto-rows-min overflow-x-hidden ${recentActivityLoading ? "overflow-y-hidden" : "overflow-y-auto"}`}
|
|
||||||
>
|
|
||||||
{recentActivityLoading ? (
|
|
||||||
<div className="flex flex-row items-center text-muted-foreground text-sm animate-pulse">
|
|
||||||
<Loader2 className="animate-spin mr-2" size={16} />
|
|
||||||
<span>{t("dashboard.loadingRecentActivity")}</span>
|
|
||||||
</div>
|
|
||||||
) : recentActivity.length === 0 ? (
|
|
||||||
<p className="text-muted-foreground text-sm">
|
|
||||||
{t("dashboard.noRecentActivity")}
|
|
||||||
</p>
|
|
||||||
) : (
|
|
||||||
recentActivity.map((item) => (
|
|
||||||
<Button
|
|
||||||
key={item.id}
|
|
||||||
variant="outline"
|
|
||||||
className="border-2 !border-dark-border bg-dark-bg min-w-0"
|
|
||||||
onClick={() => handleActivityClick(item)}
|
|
||||||
>
|
|
||||||
{item.type === "terminal" ? (
|
|
||||||
<Terminal size={20} className="shrink-0" />
|
|
||||||
) : (
|
|
||||||
<FolderOpen size={20} className="shrink-0" />
|
|
||||||
)}
|
|
||||||
<p className="truncate ml-2 font-semibold">
|
|
||||||
{item.hostName}
|
|
||||||
</p>
|
|
||||||
</Button>
|
|
||||||
))
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-row flex-1 gap-4 min-h-0 min-w-0">
|
|
||||||
<div className="flex-1 min-w-0 border-2 border-edge rounded-md bg-elevated flex flex-col overflow-hidden transition-all duration-150 hover:border-primary/20">
|
|
||||||
<div className="flex flex-col mx-3 my-2 overflow-y-auto overflow-x-hidden thin-scrollbar">
|
|
||||||
<p className="text-xl font-semibold mb-3 mt-1 flex flex-row items-center">
|
|
||||||
<FastForward className="mr-3" />
|
|
||||||
{t("dashboard.quickActions")}
|
|
||||||
</p>
|
|
||||||
<div className="grid gap-4 grid-cols-3 auto-rows-min overflow-y-auto overflow-x-hidden thin-scrollbar">
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
className="border-2 !border-edge flex flex-col items-center justify-center h-auto p-3 !bg-canvas"
|
|
||||||
onClick={handleAddHost}
|
|
||||||
>
|
|
||||||
<div className="flex flex-col items-center w-full max-w-full">
|
|
||||||
<Server
|
|
||||||
className="shrink-0"
|
|
||||||
style={{ width: "40px", height: "40px" }}
|
|
||||||
/>
|
/>
|
||||||
<span
|
);
|
||||||
className="font-semibold text-sm mt-2 text-center block"
|
} else if (card.id === "recent_activity") {
|
||||||
style={{
|
return (
|
||||||
wordWrap: "break-word",
|
<RecentActivityCard
|
||||||
overflowWrap: "break-word",
|
key={card.id}
|
||||||
width: "100%",
|
activities={recentActivity}
|
||||||
maxWidth: "100%",
|
loading={recentActivityLoading}
|
||||||
hyphens: "auto",
|
onReset={handleResetActivity}
|
||||||
display: "block",
|
onActivityClick={handleActivityClick}
|
||||||
whiteSpace: "normal",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{t("dashboard.addHost")}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
className="border-2 !border-edge flex flex-col items-center justify-center h-auto p-3 !bg-canvas"
|
|
||||||
onClick={handleAddCredential}
|
|
||||||
>
|
|
||||||
<div className="flex flex-col items-center w-full max-w-full">
|
|
||||||
<Key
|
|
||||||
className="shrink-0"
|
|
||||||
style={{ width: "40px", height: "40px" }}
|
|
||||||
/>
|
/>
|
||||||
<span
|
);
|
||||||
className="font-semibold text-sm mt-2 text-center block"
|
} else if (card.id === "network_graph") {
|
||||||
style={{
|
return (
|
||||||
wordWrap: "break-word",
|
<NetworkGraphCard
|
||||||
overflowWrap: "break-word",
|
key={card.id}
|
||||||
width: "100%",
|
isTopbarOpen={isTopbarOpen}
|
||||||
maxWidth: "100%",
|
rightSidebarOpen={rightSidebarOpen}
|
||||||
hyphens: "auto",
|
rightSidebarWidth={rightSidebarWidth}
|
||||||
display: "block",
|
|
||||||
whiteSpace: "normal",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{t("dashboard.addCredential")}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</Button>
|
|
||||||
{isAdmin && (
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
className="border-2 !border-edge flex flex-col items-center justify-center h-auto p-3 !bg-canvas"
|
|
||||||
onClick={handleOpenAdminSettings}
|
|
||||||
>
|
|
||||||
<div className="flex flex-col items-center w-full max-w-full">
|
|
||||||
<Settings
|
|
||||||
className="shrink-0"
|
|
||||||
style={{ width: "40px", height: "40px" }}
|
|
||||||
/>
|
|
||||||
<span
|
|
||||||
className="font-semibold text-sm mt-2 text-center block"
|
|
||||||
style={{
|
|
||||||
wordWrap: "break-word",
|
|
||||||
overflowWrap: "break-word",
|
|
||||||
width: "100%",
|
|
||||||
maxWidth: "100%",
|
|
||||||
hyphens: "auto",
|
|
||||||
display: "block",
|
|
||||||
whiteSpace: "normal",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{t("dashboard.adminSettings")}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
className="border-2 !border-edge flex flex-col items-center justify-center h-auto p-3 !bg-canvas"
|
|
||||||
onClick={handleOpenUserProfile}
|
|
||||||
>
|
|
||||||
<div className="flex flex-col items-center w-full max-w-full">
|
|
||||||
<User
|
|
||||||
className="shrink-0"
|
|
||||||
style={{ width: "40px", height: "40px" }}
|
|
||||||
/>
|
/>
|
||||||
<span
|
);
|
||||||
className="font-semibold text-sm mt-2 text-center block"
|
} else if (card.id === "quick_actions") {
|
||||||
style={{
|
return (
|
||||||
wordWrap: "break-word",
|
<QuickActionsCard
|
||||||
overflowWrap: "break-word",
|
key={card.id}
|
||||||
width: "100%",
|
isAdmin={isAdmin}
|
||||||
maxWidth: "100%",
|
onAddHost={handleAddHost}
|
||||||
hyphens: "auto",
|
onAddCredential={handleAddCredential}
|
||||||
display: "block",
|
onOpenAdminSettings={handleOpenAdminSettings}
|
||||||
whiteSpace: "normal",
|
onOpenUserProfile={handleOpenUserProfile}
|
||||||
}}
|
/>
|
||||||
>
|
);
|
||||||
{t("dashboard.userProfile")}
|
} else if (card.id === "server_stats") {
|
||||||
</span>
|
return (
|
||||||
</div>
|
<ServerStatsCard
|
||||||
</Button>
|
key={card.id}
|
||||||
</div>
|
serverStats={serverStats}
|
||||||
</div>
|
loading={serverStatsLoading}
|
||||||
|
onServerClick={handleServerStatClick}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1 min-w-0 border-2 border-edge rounded-md bg-elevated flex flex-col overflow-hidden transition-all duration-150 hover:border-primary/20">
|
)}
|
||||||
<div className="flex flex-col mx-3 my-2 flex-1 overflow-hidden">
|
|
||||||
<p className="text-xl font-semibold mb-3 mt-1 flex flex-row items-center">
|
|
||||||
<ChartLine className="mr-3" />
|
|
||||||
{t("dashboard.serverStats")}
|
|
||||||
</p>
|
|
||||||
<div
|
|
||||||
className={`grid gap-4 grid-cols-3 auto-rows-min overflow-x-hidden thin-scrollbar ${serverStatsLoading ? "overflow-y-hidden" : "overflow-y-auto"}`}
|
|
||||||
>
|
|
||||||
{serverStatsLoading ? (
|
|
||||||
<div className="flex flex-row items-center text-muted-foreground text-sm animate-pulse">
|
|
||||||
<Loader2 className="animate-spin mr-2" size={16} />
|
|
||||||
<span>{t("dashboard.loadingServerStats")}</span>
|
|
||||||
</div>
|
|
||||||
) : serverStats.length === 0 ? (
|
|
||||||
<p className="text-muted-foreground text-sm">
|
|
||||||
{t("dashboard.noServerData")}
|
|
||||||
</p>
|
|
||||||
) : (
|
|
||||||
serverStats.map((server) => (
|
|
||||||
<Button
|
|
||||||
key={server.id}
|
|
||||||
variant="outline"
|
|
||||||
className="border-2 !border-edge bg-canvas h-auto p-3 min-w-0 !bg-canvas"
|
|
||||||
onClick={() =>
|
|
||||||
handleServerStatClick(server.id, server.name)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<div className="flex flex-col w-full">
|
|
||||||
<div className="flex flex-row items-center mb-2">
|
|
||||||
<Server size={20} className="shrink-0" />
|
|
||||||
<p className="truncate ml-2 font-semibold">
|
|
||||||
{server.name}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-row justify-start gap-4 text-xs text-muted-foreground">
|
|
||||||
<span>
|
|
||||||
{t("dashboard.cpu")}:{" "}
|
|
||||||
{server.cpu !== null
|
|
||||||
? `${server.cpu}%`
|
|
||||||
: t("dashboard.notAvailable")}
|
|
||||||
</span>
|
|
||||||
<span>
|
|
||||||
{t("dashboard.ram")}:{" "}
|
|
||||||
{server.ram !== null
|
|
||||||
? `${server.ram}%`
|
|
||||||
: t("dashboard.notAvailable")}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Button>
|
|
||||||
))
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<AlertManager userId={userId} loggedIn={loggedIn} />
|
<AlertManager userId={userId} loggedIn={loggedIn} />
|
||||||
|
|
||||||
|
{layout && (
|
||||||
|
<DashboardSettingsDialog
|
||||||
|
open={settingsDialogOpen}
|
||||||
|
onOpenChange={setSettingsDialogOpen}
|
||||||
|
currentLayout={layout}
|
||||||
|
onSave={updateLayout}
|
||||||
|
onReset={resetLayout}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
1261
src/ui/desktop/apps/dashboard/cards/NetworkGraphCard.tsx
Normal file
1261
src/ui/desktop/apps/dashboard/cards/NetworkGraphCard.tsx
Normal file
File diff suppressed because it is too large
Load Diff
141
src/ui/desktop/apps/dashboard/cards/QuickActionsCard.tsx
Normal file
141
src/ui/desktop/apps/dashboard/cards/QuickActionsCard.tsx
Normal file
@@ -0,0 +1,141 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { FastForward, Server, Key, Settings, User } from "lucide-react";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
|
||||||
|
interface QuickActionsCardProps {
|
||||||
|
isAdmin: boolean;
|
||||||
|
onAddHost: () => void;
|
||||||
|
onAddCredential: () => void;
|
||||||
|
onOpenAdminSettings: () => void;
|
||||||
|
onOpenUserProfile: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function QuickActionsCard({
|
||||||
|
isAdmin,
|
||||||
|
onAddHost,
|
||||||
|
onAddCredential,
|
||||||
|
onOpenAdminSettings,
|
||||||
|
onOpenUserProfile,
|
||||||
|
}: QuickActionsCardProps): React.ReactElement {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="border-2 border-edge rounded-md flex flex-col overflow-hidden transition-all duration-150 hover:border-primary/20">
|
||||||
|
<div className="flex flex-col mx-3 my-2 overflow-y-auto overflow-x-hidden thin-scrollbar">
|
||||||
|
<p className="text-xl font-semibold mb-3 mt-1 flex flex-row items-center">
|
||||||
|
<FastForward className="mr-3" />
|
||||||
|
{t("dashboard.quickActions")}
|
||||||
|
</p>
|
||||||
|
<div className="grid gap-4 grid-cols-3 auto-rows-min overflow-y-auto overflow-x-hidden thin-scrollbar">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
className="border-2 !border-edge flex flex-col items-center justify-center h-auto p-3"
|
||||||
|
onClick={onAddHost}
|
||||||
|
>
|
||||||
|
<div className="flex flex-col items-center w-full max-w-full">
|
||||||
|
<Server
|
||||||
|
className="shrink-0"
|
||||||
|
style={{ width: "40px", height: "40px" }}
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
className="font-semibold text-sm mt-2 text-center block"
|
||||||
|
style={{
|
||||||
|
wordWrap: "break-word",
|
||||||
|
overflowWrap: "break-word",
|
||||||
|
width: "100%",
|
||||||
|
maxWidth: "100%",
|
||||||
|
hyphens: "auto",
|
||||||
|
display: "block",
|
||||||
|
whiteSpace: "normal",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t("dashboard.addHost")}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
className="border-2 !border-edge flex flex-col items-center justify-center h-auto p-3"
|
||||||
|
onClick={onAddCredential}
|
||||||
|
>
|
||||||
|
<div className="flex flex-col items-center w-full max-w-full">
|
||||||
|
<Key
|
||||||
|
className="shrink-0"
|
||||||
|
style={{ width: "40px", height: "40px" }}
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
className="font-semibold text-sm mt-2 text-center block"
|
||||||
|
style={{
|
||||||
|
wordWrap: "break-word",
|
||||||
|
overflowWrap: "break-word",
|
||||||
|
width: "100%",
|
||||||
|
maxWidth: "100%",
|
||||||
|
hyphens: "auto",
|
||||||
|
display: "block",
|
||||||
|
whiteSpace: "normal",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t("dashboard.addCredential")}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</Button>
|
||||||
|
{isAdmin && (
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
className="border-2 !border-edge flex flex-col items-center justify-center h-auto p-3"
|
||||||
|
onClick={onOpenAdminSettings}
|
||||||
|
>
|
||||||
|
<div className="flex flex-col items-center w-full max-w-full">
|
||||||
|
<Settings
|
||||||
|
className="shrink-0"
|
||||||
|
style={{ width: "40px", height: "40px" }}
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
className="font-semibold text-sm mt-2 text-center block"
|
||||||
|
style={{
|
||||||
|
wordWrap: "break-word",
|
||||||
|
overflowWrap: "break-word",
|
||||||
|
width: "100%",
|
||||||
|
maxWidth: "100%",
|
||||||
|
hyphens: "auto",
|
||||||
|
display: "block",
|
||||||
|
whiteSpace: "normal",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t("dashboard.adminSettings")}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
className="border-2 !border-edge flex flex-col items-center justify-center h-auto p-3"
|
||||||
|
onClick={onOpenUserProfile}
|
||||||
|
>
|
||||||
|
<div className="flex flex-col items-center w-full max-w-full">
|
||||||
|
<User
|
||||||
|
className="shrink-0"
|
||||||
|
style={{ width: "40px", height: "40px" }}
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
className="font-semibold text-sm mt-2 text-center block"
|
||||||
|
style={{
|
||||||
|
wordWrap: "break-word",
|
||||||
|
overflowWrap: "break-word",
|
||||||
|
width: "100%",
|
||||||
|
maxWidth: "100%",
|
||||||
|
hyphens: "auto",
|
||||||
|
display: "block",
|
||||||
|
whiteSpace: "normal",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t("dashboard.userProfile")}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
97
src/ui/desktop/apps/dashboard/cards/RecentActivityCard.tsx
Normal file
97
src/ui/desktop/apps/dashboard/cards/RecentActivityCard.tsx
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import {
|
||||||
|
Clock,
|
||||||
|
Loader2,
|
||||||
|
Terminal,
|
||||||
|
FolderOpen,
|
||||||
|
Server,
|
||||||
|
ArrowDownUp,
|
||||||
|
Container,
|
||||||
|
} from "lucide-react";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { type RecentActivityItem } from "@/ui/main-axios";
|
||||||
|
|
||||||
|
interface RecentActivityCardProps {
|
||||||
|
activities: RecentActivityItem[];
|
||||||
|
loading: boolean;
|
||||||
|
onReset: () => void;
|
||||||
|
onActivityClick: (item: RecentActivityItem) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function RecentActivityCard({
|
||||||
|
activities,
|
||||||
|
loading,
|
||||||
|
onReset,
|
||||||
|
onActivityClick,
|
||||||
|
}: RecentActivityCardProps): React.ReactElement {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="border-2 border-edge rounded-md flex flex-col overflow-hidden transition-all duration-150 hover:border-primary/20">
|
||||||
|
<div className="flex flex-col mx-3 my-2 flex-1 overflow-hidden">
|
||||||
|
<div className="flex flex-row items-center justify-between mb-3 mt-1">
|
||||||
|
<p className="text-xl font-semibold flex flex-row items-center">
|
||||||
|
<Clock className="mr-3" />
|
||||||
|
{t("dashboard.recentActivity")}
|
||||||
|
</p>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
className="border-2 !border-edge h-7 !bg-canvas"
|
||||||
|
onClick={onReset}
|
||||||
|
>
|
||||||
|
{t("dashboard.reset")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className={`grid gap-4 grid-cols-3 auto-rows-min overflow-x-hidden thin-scrollbar ${loading ? "overflow-y-hidden" : "overflow-y-auto"}`}
|
||||||
|
>
|
||||||
|
{loading ? (
|
||||||
|
<div className="flex flex-row items-center text-muted-foreground text-sm animate-pulse">
|
||||||
|
<Loader2 className="animate-spin mr-2" size={16} />
|
||||||
|
<span>{t("dashboard.loadingRecentActivity")}</span>
|
||||||
|
</div>
|
||||||
|
) : activities.length === 0 ? (
|
||||||
|
<p className="text-muted-foreground text-sm">
|
||||||
|
{t("dashboard.noRecentActivity")}
|
||||||
|
</p>
|
||||||
|
) : (
|
||||||
|
activities
|
||||||
|
.filter((item, index, array) => {
|
||||||
|
if (index === 0) return true;
|
||||||
|
|
||||||
|
const prevItem = array[index - 1];
|
||||||
|
return !(
|
||||||
|
item.hostId === prevItem.hostId && item.type === prevItem.type
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.map((item) => (
|
||||||
|
<Button
|
||||||
|
key={item.id}
|
||||||
|
variant="outline"
|
||||||
|
className="border-2 !border-edge min-w-0"
|
||||||
|
onClick={() => onActivityClick(item)}
|
||||||
|
>
|
||||||
|
{item.type === "terminal" ? (
|
||||||
|
<Terminal size={20} className="shrink-0" />
|
||||||
|
) : item.type === "file_manager" ? (
|
||||||
|
<FolderOpen size={20} className="shrink-0" />
|
||||||
|
) : item.type === "server_stats" ? (
|
||||||
|
<Server size={20} className="shrink-0" />
|
||||||
|
) : item.type === "tunnel" ? (
|
||||||
|
<ArrowDownUp size={20} className="shrink-0" />
|
||||||
|
) : item.type === "docker" ? (
|
||||||
|
<Container size={20} className="shrink-0" />
|
||||||
|
) : (
|
||||||
|
<Terminal size={20} className="shrink-0" />
|
||||||
|
)}
|
||||||
|
<p className="truncate ml-2 font-semibold">{item.hostName}</p>
|
||||||
|
</Button>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
142
src/ui/desktop/apps/dashboard/cards/ServerOverviewCard.tsx
Normal file
142
src/ui/desktop/apps/dashboard/cards/ServerOverviewCard.tsx
Normal file
@@ -0,0 +1,142 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import {
|
||||||
|
Server,
|
||||||
|
History,
|
||||||
|
Clock,
|
||||||
|
Database,
|
||||||
|
Key,
|
||||||
|
ArrowDownUp,
|
||||||
|
} from "lucide-react";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { UpdateLog } from "@/ui/desktop/apps/dashboard/apps/UpdateLog";
|
||||||
|
|
||||||
|
interface ServerOverviewCardProps {
|
||||||
|
loggedIn: boolean;
|
||||||
|
versionText: string;
|
||||||
|
versionStatus: "up_to_date" | "requires_update";
|
||||||
|
uptime: string;
|
||||||
|
dbHealth: "healthy" | "error";
|
||||||
|
totalServers: number;
|
||||||
|
totalTunnels: number;
|
||||||
|
totalCredentials: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ServerOverviewCard({
|
||||||
|
loggedIn,
|
||||||
|
versionText,
|
||||||
|
versionStatus,
|
||||||
|
uptime,
|
||||||
|
dbHealth,
|
||||||
|
totalServers,
|
||||||
|
totalTunnels,
|
||||||
|
totalCredentials,
|
||||||
|
}: ServerOverviewCardProps): React.ReactElement {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="border-2 border-edge rounded-md flex flex-col overflow-hidden transition-all duration-150 hover:border-primary/20">
|
||||||
|
<div className="flex flex-col mx-3 my-2 overflow-y-auto overflow-x-hidden thin-scrollbar">
|
||||||
|
<p className="text-xl font-semibold mb-3 mt-1 flex flex-row items-center">
|
||||||
|
<Server className="mr-3" />
|
||||||
|
{t("dashboard.serverOverview")}
|
||||||
|
</p>
|
||||||
|
<div className="w-full h-auto border-2 border-edge rounded-md px-3 py-3">
|
||||||
|
<div className="flex flex-row items-center justify-between mb-3 min-w-0 gap-2">
|
||||||
|
<div className="flex flex-row items-center min-w-0">
|
||||||
|
<History size={20} className="shrink-0" />
|
||||||
|
<p className="ml-2 leading-none truncate">
|
||||||
|
{t("dashboard.version")}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-row items-center">
|
||||||
|
<p className="leading-none text-muted-foreground">
|
||||||
|
{versionText}
|
||||||
|
</p>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
className={`ml-2 text-sm border-1 border-edge ${versionStatus === "up_to_date" ? "text-green-400" : "text-yellow-400"}`}
|
||||||
|
>
|
||||||
|
{versionStatus === "up_to_date"
|
||||||
|
? t("dashboard.upToDate")
|
||||||
|
: t("dashboard.updateAvailable")}
|
||||||
|
</Button>
|
||||||
|
<UpdateLog loggedIn={loggedIn} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-row items-center justify-between mb-5 min-w-0 gap-2">
|
||||||
|
<div className="flex flex-row items-center min-w-0">
|
||||||
|
<Clock size={20} className="shrink-0" />
|
||||||
|
<p className="ml-2 leading-none truncate">
|
||||||
|
{t("dashboard.uptime")}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-row items-center">
|
||||||
|
<p className="leading-none text-muted-foreground">{uptime}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-row items-center justify-between min-w-0 gap-2">
|
||||||
|
<div className="flex flex-row items-center min-w-0">
|
||||||
|
<Database size={20} className="shrink-0" />
|
||||||
|
<p className="ml-2 leading-none truncate">
|
||||||
|
{t("dashboard.database")}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-row items-center">
|
||||||
|
<p
|
||||||
|
className={`leading-none ${dbHealth === "healthy" ? "text-green-400" : "text-red-400"}`}
|
||||||
|
>
|
||||||
|
{dbHealth === "healthy"
|
||||||
|
? t("dashboard.healthy")
|
||||||
|
: t("dashboard.error")}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col grid grid-cols-2 gap-2 mt-2">
|
||||||
|
<div className="flex flex-row items-center justify-between w-full h-auto mt-3 border-2 border-edge rounded-md px-3 py-3 min-w-0 gap-2">
|
||||||
|
<div className="flex flex-row items-center min-w-0">
|
||||||
|
<Server size={16} className="mr-3 shrink-0" />
|
||||||
|
<p className="m-0 leading-none truncate">
|
||||||
|
{t("dashboard.totalServers")}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<p className="m-0 leading-none text-muted-foreground font-semibold">
|
||||||
|
{totalServers}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-row items-center justify-between w-full h-auto mt-3 border-2 border-edge rounded-md px-3 py-3 min-w-0 gap-2">
|
||||||
|
<div className="flex flex-row items-center min-w-0">
|
||||||
|
<ArrowDownUp size={16} className="mr-3 shrink-0" />
|
||||||
|
<p className="m-0 leading-none truncate">
|
||||||
|
{t("dashboard.totalTunnels")}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<p className="m-0 leading-none text-muted-foreground font-semibold">
|
||||||
|
{totalTunnels}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col grid grid-cols-2 gap-2 mt-2">
|
||||||
|
<div className="flex flex-row items-center justify-between w-full h-auto mt-3 border-2 border-edge rounded-md px-3 py-3 min-w-0 gap-2">
|
||||||
|
<div className="flex flex-row items-center min-w-0">
|
||||||
|
<Key size={16} className="mr-3 shrink-0" />
|
||||||
|
<p className="m-0 leading-none truncate">
|
||||||
|
{t("dashboard.totalCredentials")}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<p className="m-0 leading-none text-muted-foreground font-semibold">
|
||||||
|
{totalCredentials}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
80
src/ui/desktop/apps/dashboard/cards/ServerStatsCard.tsx
Normal file
80
src/ui/desktop/apps/dashboard/cards/ServerStatsCard.tsx
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { ChartLine, Loader2, Server } from "lucide-react";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
|
||||||
|
interface ServerStat {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
cpu: number | null;
|
||||||
|
ram: number | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ServerStatsCardProps {
|
||||||
|
serverStats: ServerStat[];
|
||||||
|
loading: boolean;
|
||||||
|
onServerClick: (serverId: number, serverName: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ServerStatsCard({
|
||||||
|
serverStats,
|
||||||
|
loading,
|
||||||
|
onServerClick,
|
||||||
|
}: ServerStatsCardProps): React.ReactElement {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="border-2 border-edge rounded-md flex flex-col overflow-hidden transition-all duration-150 hover:border-primary/20">
|
||||||
|
<div className="flex flex-col mx-3 my-2 flex-1 overflow-hidden">
|
||||||
|
<p className="text-xl font-semibold mb-3 mt-1 flex flex-row items-center">
|
||||||
|
<ChartLine className="mr-3" />
|
||||||
|
{t("dashboard.serverStats")}
|
||||||
|
</p>
|
||||||
|
<div
|
||||||
|
className={`grid gap-4 grid-cols-3 auto-rows-min overflow-x-hidden thin-scrollbar ${loading ? "overflow-y-hidden" : "overflow-y-auto"}`}
|
||||||
|
>
|
||||||
|
{loading ? (
|
||||||
|
<div className="flex flex-row items-center text-muted-foreground text-sm animate-pulse">
|
||||||
|
<Loader2 className="animate-spin mr-2" size={16} />
|
||||||
|
<span>{t("dashboard.loadingServerStats")}</span>
|
||||||
|
</div>
|
||||||
|
) : serverStats.length === 0 ? (
|
||||||
|
<p className="text-muted-foreground text-sm">
|
||||||
|
{t("dashboard.noServerData")}
|
||||||
|
</p>
|
||||||
|
) : (
|
||||||
|
serverStats.map((server) => (
|
||||||
|
<Button
|
||||||
|
key={server.id}
|
||||||
|
variant="outline"
|
||||||
|
className="border-2 !border-edge h-auto p-3 min-w-0"
|
||||||
|
onClick={() => onServerClick(server.id, server.name)}
|
||||||
|
>
|
||||||
|
<div className="flex flex-col w-full">
|
||||||
|
<div className="flex flex-row items-center mb-2">
|
||||||
|
<Server size={20} className="shrink-0" />
|
||||||
|
<p className="truncate ml-2 font-semibold">{server.name}</p>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-row justify-start gap-4 text-xs text-muted-foreground">
|
||||||
|
<span>
|
||||||
|
{t("dashboard.cpu")}:{" "}
|
||||||
|
{server.cpu !== null
|
||||||
|
? `${server.cpu}%`
|
||||||
|
: t("dashboard.notAvailable")}
|
||||||
|
</span>
|
||||||
|
<span>
|
||||||
|
{t("dashboard.ram")}:{" "}
|
||||||
|
{server.ram !== null
|
||||||
|
? `${server.ram}%`
|
||||||
|
: t("dashboard.notAvailable")}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Button>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,164 @@
|
|||||||
|
import React, { useState, useEffect } from "react";
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
} from "@/components/ui/dialog";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Label } from "@/components/ui/label";
|
||||||
|
import { Checkbox } from "@/components/ui/checkbox";
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from "@/components/ui/select";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import type { DashboardLayout } from "@/ui/main-axios";
|
||||||
|
|
||||||
|
interface DashboardSettingsDialogProps {
|
||||||
|
open: boolean;
|
||||||
|
onOpenChange: (open: boolean) => void;
|
||||||
|
currentLayout: DashboardLayout;
|
||||||
|
onSave: (layout: DashboardLayout) => void;
|
||||||
|
onReset: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function DashboardSettingsDialog({
|
||||||
|
open,
|
||||||
|
onOpenChange,
|
||||||
|
currentLayout,
|
||||||
|
onSave,
|
||||||
|
onReset,
|
||||||
|
}: DashboardSettingsDialogProps): React.ReactElement {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const [layout, setLayout] = useState<DashboardLayout>(currentLayout);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setLayout(currentLayout);
|
||||||
|
}, [currentLayout, open]);
|
||||||
|
|
||||||
|
const handleCardToggle = (cardId: string, enabled: boolean) => {
|
||||||
|
setLayout((prev) => ({
|
||||||
|
...prev,
|
||||||
|
cards: prev.cards.map((card) =>
|
||||||
|
card.id === cardId ? { ...card, enabled } : card,
|
||||||
|
),
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleGridColumnsChange = (value: string) => {
|
||||||
|
setLayout((prev) => ({
|
||||||
|
...prev,
|
||||||
|
gridColumns: parseInt(value, 10),
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSave = () => {
|
||||||
|
onSave(layout);
|
||||||
|
onOpenChange(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleReset = () => {
|
||||||
|
onReset();
|
||||||
|
onOpenChange(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const cardLabels: Record<string, string> = {
|
||||||
|
server_overview: t("dashboard.serverOverviewCard"),
|
||||||
|
recent_activity: t("dashboard.recentActivityCard"),
|
||||||
|
network_graph: t("dashboard.networkGraphCard"),
|
||||||
|
quick_actions: t("dashboard.quickActionsCard"),
|
||||||
|
server_stats: t("dashboard.serverStatsCard"),
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||||
|
<DialogContent className="sm:max-w-[500px] bg-canvas border-2 border-edge">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>{t("dashboard.dashboardSettings")}</DialogTitle>
|
||||||
|
<DialogDescription className="text-muted-foreground">
|
||||||
|
{t("dashboard.customizeLayout")}
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<div className="space-y-6 py-4">
|
||||||
|
<div className="space-y-3">
|
||||||
|
<Label className="text-base font-semibold">
|
||||||
|
{t("dashboard.enableDisableCards")}
|
||||||
|
</Label>
|
||||||
|
<div className="space-y-3">
|
||||||
|
{layout.cards.map((card) => (
|
||||||
|
<div
|
||||||
|
key={card.id}
|
||||||
|
className="flex items-center space-x-3 border-2 border-edge rounded-md p-3"
|
||||||
|
>
|
||||||
|
<Checkbox
|
||||||
|
id={card.id}
|
||||||
|
checked={card.enabled}
|
||||||
|
onCheckedChange={(checked) =>
|
||||||
|
handleCardToggle(card.id, checked === true)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Label
|
||||||
|
htmlFor={card.id}
|
||||||
|
className="text-sm font-normal cursor-pointer flex-1"
|
||||||
|
>
|
||||||
|
{cardLabels[card.id] || card.id}
|
||||||
|
</Label>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-3">
|
||||||
|
<Label className="text-base font-semibold">
|
||||||
|
{t("dashboard.gridColumns")}
|
||||||
|
</Label>
|
||||||
|
<Select
|
||||||
|
value={layout.gridColumns.toString()}
|
||||||
|
onValueChange={handleGridColumnsChange}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="w-full border-2 border-edge">
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="1">
|
||||||
|
1 {t("dashboard.column", { count: 1 })}
|
||||||
|
</SelectItem>
|
||||||
|
<SelectItem value="2">
|
||||||
|
2 {t("dashboard.columns", { count: 2 })}
|
||||||
|
</SelectItem>
|
||||||
|
<SelectItem value="3">
|
||||||
|
3 {t("dashboard.columns", { count: 3 })}
|
||||||
|
</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DialogFooter className="flex-row gap-2">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={handleReset}
|
||||||
|
className="border-2 border-edge"
|
||||||
|
>
|
||||||
|
{t("dashboard.resetLayout")}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => onOpenChange(false)}
|
||||||
|
className="border-2 border-edge"
|
||||||
|
>
|
||||||
|
{t("common.cancel")}
|
||||||
|
</Button>
|
||||||
|
<Button onClick={handleSave}>{t("common.save")}</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
import { useState, useEffect, useCallback } from "react";
|
||||||
|
import {
|
||||||
|
getDashboardPreferences,
|
||||||
|
saveDashboardPreferences,
|
||||||
|
type DashboardLayout,
|
||||||
|
} from "@/ui/main-axios";
|
||||||
|
|
||||||
|
const DEFAULT_LAYOUT: DashboardLayout = {
|
||||||
|
cards: [
|
||||||
|
{ id: "server_overview", enabled: true, order: 1 },
|
||||||
|
{ id: "recent_activity", enabled: true, order: 2 },
|
||||||
|
{ id: "network_graph", enabled: false, order: 3 },
|
||||||
|
{ id: "quick_actions", enabled: true, order: 4 },
|
||||||
|
{ id: "server_stats", enabled: true, order: 5 },
|
||||||
|
],
|
||||||
|
gridColumns: 2,
|
||||||
|
};
|
||||||
|
|
||||||
|
export function useDashboardPreferences() {
|
||||||
|
const [layout, setLayout] = useState<DashboardLayout | null>(null);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [saveTimeout, setSaveTimeout] = useState<NodeJS.Timeout | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchPreferences = async () => {
|
||||||
|
try {
|
||||||
|
const preferences = await getDashboardPreferences();
|
||||||
|
setLayout(preferences);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to load dashboard preferences:", error);
|
||||||
|
setLayout(DEFAULT_LAYOUT);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fetchPreferences();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const updateLayout = useCallback(
|
||||||
|
(newLayout: DashboardLayout) => {
|
||||||
|
setLayout(newLayout);
|
||||||
|
|
||||||
|
if (saveTimeout) {
|
||||||
|
clearTimeout(saveTimeout);
|
||||||
|
}
|
||||||
|
|
||||||
|
const timeout = setTimeout(async () => {
|
||||||
|
try {
|
||||||
|
await saveDashboardPreferences(newLayout);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to save dashboard preferences:", error);
|
||||||
|
}
|
||||||
|
}, 1000);
|
||||||
|
|
||||||
|
setSaveTimeout(timeout);
|
||||||
|
},
|
||||||
|
[saveTimeout],
|
||||||
|
);
|
||||||
|
|
||||||
|
const resetLayout = useCallback(async () => {
|
||||||
|
setLayout(DEFAULT_LAYOUT);
|
||||||
|
try {
|
||||||
|
await saveDashboardPreferences(DEFAULT_LAYOUT);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to reset dashboard preferences:", error);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return {
|
||||||
|
layout,
|
||||||
|
loading,
|
||||||
|
updateLayout,
|
||||||
|
resetLayout,
|
||||||
|
};
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1 +0,0 @@
|
|||||||
export { default as NetworkGraphView } from './NetworkGraphView';
|
|
||||||
@@ -4,7 +4,7 @@ import { ServerStats as ServerView } from "@/ui/desktop/apps/features/server-sta
|
|||||||
import { FileManager } from "@/ui/desktop/apps/features/file-manager/FileManager.tsx";
|
import { FileManager } from "@/ui/desktop/apps/features/file-manager/FileManager.tsx";
|
||||||
import { TunnelManager } from "@/ui/desktop/apps/features/tunnel/TunnelManager.tsx";
|
import { TunnelManager } from "@/ui/desktop/apps/features/tunnel/TunnelManager.tsx";
|
||||||
import { DockerManager } from "@/ui/desktop/apps/features/docker/DockerManager.tsx";
|
import { DockerManager } from "@/ui/desktop/apps/features/docker/DockerManager.tsx";
|
||||||
import { NetworkGraphView } from "@/ui/desktop/dashboard/network-graph/NetworkGraphView.tsx";
|
import { NetworkGraphCard } from "@/ui/desktop/apps/dashboard/cards/NetworkGraphCard";
|
||||||
import { useTabs } from "@/ui/desktop/navigation/tabs/TabContext.tsx";
|
import { useTabs } from "@/ui/desktop/navigation/tabs/TabContext.tsx";
|
||||||
import {
|
import {
|
||||||
ResizablePanelGroup,
|
ResizablePanelGroup,
|
||||||
@@ -356,11 +356,10 @@ export function AppView({
|
|||||||
embedded
|
embedded
|
||||||
/>
|
/>
|
||||||
) : t.type === "network_graph" ? (
|
) : t.type === "network_graph" ? (
|
||||||
<NetworkGraphView
|
<NetworkGraphCard
|
||||||
isTopbarOpen={isTopbarOpen}
|
isTopbarOpen={isTopbarOpen}
|
||||||
rightSidebarOpen={rightSidebarOpen}
|
rightSidebarOpen={rightSidebarOpen}
|
||||||
rightSidebarWidth={rightSidebarWidth}
|
rightSidebarWidth={rightSidebarWidth}
|
||||||
isStandalone={true}
|
|
||||||
/>
|
/>
|
||||||
) : t.type === "tunnel" ? (
|
) : t.type === "tunnel" ? (
|
||||||
<TunnelManager
|
<TunnelManager
|
||||||
|
|||||||
@@ -3919,3 +3919,20 @@ export async function getContainerStats(
|
|||||||
throw handleApiError(error, "get container stats");
|
throw handleApiError(error, "get container stats");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface DashboardLayout {
|
||||||
|
cards: Array<{ id: string; enabled: boolean; order: number }>;
|
||||||
|
gridColumns: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getDashboardPreferences(): Promise<DashboardLayout> {
|
||||||
|
const response = await dashboardAxios.get("/dashboard/preferences");
|
||||||
|
return response.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function saveDashboardPreferences(
|
||||||
|
layout: DashboardLayout,
|
||||||
|
): Promise<{ success: boolean }> {
|
||||||
|
const response = await dashboardAxios.post("/dashboard/preferences", layout);
|
||||||
|
return response.data;
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user