mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-07-16 05:13:36 +00:00
v2.3.0
This commit is contained in:
@@ -129,6 +129,31 @@ export function Dashboard({
|
||||
[mainWidthPct, layout, updateLayout],
|
||||
);
|
||||
|
||||
const handleCardHeightMouseDown = useCallback(
|
||||
(e: React.MouseEvent, cardId: string, currentHeight: number) => {
|
||||
e.preventDefault();
|
||||
const startY = e.clientY;
|
||||
const startH = currentHeight;
|
||||
const onMove = (ev: MouseEvent) => {
|
||||
const newH = Math.max(180, startH + (ev.clientY - startY));
|
||||
if (!layout) return;
|
||||
updateLayout({
|
||||
...layout,
|
||||
cards: layout.cards.map((c) =>
|
||||
c.id === cardId ? { ...c, height: Math.round(newH) } : c,
|
||||
),
|
||||
});
|
||||
};
|
||||
const onUp = () => {
|
||||
window.removeEventListener("mousemove", onMove);
|
||||
window.removeEventListener("mouseup", onUp);
|
||||
};
|
||||
window.addEventListener("mousemove", onMove);
|
||||
window.addEventListener("mouseup", onUp);
|
||||
},
|
||||
[layout, updateLayout],
|
||||
);
|
||||
|
||||
let sidebarState: "expanded" | "collapsed" = "expanded";
|
||||
try {
|
||||
const sidebar = useSidebar();
|
||||
@@ -385,7 +410,7 @@ export function Dashboard({
|
||||
name: string;
|
||||
cpu: number | null;
|
||||
ram: number | null;
|
||||
} => server !== null && server.cpu !== null && server.ram !== null,
|
||||
} => server !== null,
|
||||
);
|
||||
setServerStats(validServerStats);
|
||||
setServerStatsLoading(false);
|
||||
@@ -788,24 +813,40 @@ export function Dashboard({
|
||||
return null;
|
||||
};
|
||||
|
||||
const renderCardWithHandle = (
|
||||
card: (typeof enabledCards)[0],
|
||||
) => {
|
||||
const currentHeight = card.height ?? 280;
|
||||
return (
|
||||
<div
|
||||
key={card.id}
|
||||
className="flex flex-col"
|
||||
style={
|
||||
card.height
|
||||
? { height: card.height, flexShrink: 0 }
|
||||
: { flex: 1, minHeight: 280 }
|
||||
}
|
||||
>
|
||||
<div className="flex-1 min-h-0">{renderCard(card)}</div>
|
||||
<div
|
||||
className="flex-shrink-0 h-2 my-0.5 flex items-center justify-center cursor-row-resize group"
|
||||
onMouseDown={(e) =>
|
||||
handleCardHeightMouseDown(e, card.id, currentHeight)
|
||||
}
|
||||
>
|
||||
<div className="w-16 h-0.5 rounded-full bg-border group-hover:bg-primary/50 transition-colors" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className="flex flex-col gap-4 overflow-auto min-h-0"
|
||||
className="flex flex-col gap-2 overflow-auto min-h-0"
|
||||
style={{ width: `${mainWidthPct}%`, minWidth: 0 }}
|
||||
>
|
||||
{mainCards.map((card) => (
|
||||
<div
|
||||
key={card.id}
|
||||
style={
|
||||
card.height
|
||||
? { height: card.height, flexShrink: 0 }
|
||||
: { flex: 1, minHeight: 280 }
|
||||
}
|
||||
>
|
||||
{renderCard(card)}
|
||||
</div>
|
||||
))}
|
||||
{mainCards.map((card) => renderCardWithHandle(card))}
|
||||
</div>
|
||||
|
||||
<div
|
||||
@@ -816,21 +857,10 @@ export function Dashboard({
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="flex flex-col gap-4 overflow-auto min-h-0"
|
||||
className="flex flex-col gap-2 overflow-auto min-h-0"
|
||||
style={{ flex: 1, minWidth: 0 }}
|
||||
>
|
||||
{sideCards.map((card) => (
|
||||
<div
|
||||
key={card.id}
|
||||
style={
|
||||
card.height
|
||||
? { height: card.height, flexShrink: 0 }
|
||||
: { flex: 1, minHeight: 280 }
|
||||
}
|
||||
>
|
||||
{renderCard(card)}
|
||||
</div>
|
||||
))}
|
||||
{sideCards.map((card) => renderCardWithHandle(card))}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
+511
-375
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -22,6 +22,21 @@ interface RecentActivityCardProps {
|
||||
onActivityClick: (item: RecentActivityItem) => void;
|
||||
}
|
||||
|
||||
function formatRelativeTime(
|
||||
timestamp: string,
|
||||
t: (key: string) => string,
|
||||
): string {
|
||||
const diffMs = Date.now() - new Date(timestamp).getTime();
|
||||
if (diffMs < 0) return t("dashboard.justNow");
|
||||
const diffSec = Math.floor(diffMs / 1000);
|
||||
if (diffSec < 60) return t("dashboard.justNow");
|
||||
const diffMin = Math.floor(diffSec / 60);
|
||||
if (diffMin < 60) return `${diffMin}m`;
|
||||
const diffHour = Math.floor(diffMin / 60);
|
||||
if (diffHour < 24) return `${diffHour}h`;
|
||||
return `${Math.floor(diffHour / 24)}d`;
|
||||
}
|
||||
|
||||
export function RecentActivityCard({
|
||||
activities,
|
||||
loading,
|
||||
@@ -95,7 +110,14 @@ export function RecentActivityCard({
|
||||
) : (
|
||||
<Terminal size={20} className="shrink-0" />
|
||||
)}
|
||||
<p className="truncate ml-2 font-semibold">{item.hostName}</p>
|
||||
<div className="flex flex-col items-start min-w-0 ml-2">
|
||||
<p className="truncate font-semibold leading-none">
|
||||
{item.hostName}
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground mt-0.5 leading-none">
|
||||
{formatRelativeTime(item.timestamp, t)}
|
||||
</p>
|
||||
</div>
|
||||
</Button>
|
||||
))
|
||||
)}
|
||||
|
||||
@@ -56,22 +56,28 @@ export function ServerOverviewCard({
|
||||
<p className="leading-none text-muted-foreground">
|
||||
{versionText}
|
||||
</p>
|
||||
{!updateCheckDisabled && (
|
||||
{versionStatus === "beta" ? (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="ml-2 text-sm border-1 border-edge text-blue-400"
|
||||
>
|
||||
{t("dashboard.beta")}
|
||||
</Button>
|
||||
) : !updateCheckDisabled ? (
|
||||
<>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className={`ml-2 text-sm border-1 border-edge ${versionStatus === "up_to_date" ? "text-green-400" : versionStatus === "beta" ? "text-blue-400" : "text-yellow-400"}`}
|
||||
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")
|
||||
: versionStatus === "beta"
|
||||
? t("dashboard.beta")
|
||||
: t("dashboard.updateAvailable")}
|
||||
: t("dashboard.updateAvailable")}
|
||||
</Button>
|
||||
<UpdateLog loggedIn={loggedIn} />
|
||||
</>
|
||||
)}
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -23,6 +23,10 @@ export function ServerStatsCard({
|
||||
}: ServerStatsCardProps): React.ReactElement {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const visibleStats = serverStats.filter(
|
||||
(s) => s.cpu !== null || s.ram !== null,
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="border-2 border-edge rounded-md flex flex-col overflow-hidden transition-all duration-150 hover:border-primary/20 !bg-elevated">
|
||||
<div className="flex flex-col mx-3 my-2 flex-1 overflow-hidden">
|
||||
@@ -38,12 +42,12 @@ export function ServerStatsCard({
|
||||
<Loader2 className="animate-spin mr-2" size={16} />
|
||||
<span>{t("dashboard.loadingServerStats")}</span>
|
||||
</div>
|
||||
) : serverStats.length === 0 ? (
|
||||
) : visibleStats.length === 0 ? (
|
||||
<p className="text-muted-foreground text-sm">
|
||||
{t("dashboard.noServerData")}
|
||||
</p>
|
||||
) : (
|
||||
serverStats.map((server) => (
|
||||
visibleStats.map((server) => (
|
||||
<Button
|
||||
key={server.id}
|
||||
variant="outline"
|
||||
@@ -56,18 +60,16 @@ export function ServerStatsCard({
|
||||
<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>
|
||||
{server.cpu !== null && (
|
||||
<span>
|
||||
{t("dashboard.cpu")}: {server.cpu.toFixed(1)}%
|
||||
</span>
|
||||
)}
|
||||
{server.ram !== null && (
|
||||
<span>
|
||||
{t("dashboard.ram")}: {server.ram.toFixed(1)}%
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Button>
|
||||
|
||||
@@ -5,6 +5,8 @@ import {
|
||||
type DashboardLayout,
|
||||
} from "@/main-axios";
|
||||
|
||||
const LS_KEY = "dashboardLayout";
|
||||
|
||||
const DEFAULT_LAYOUT: DashboardLayout = {
|
||||
cards: [
|
||||
{ id: "server_overview", enabled: true, order: 1, panel: "main" },
|
||||
@@ -16,6 +18,42 @@ const DEFAULT_LAYOUT: DashboardLayout = {
|
||||
mainWidthPct: 68,
|
||||
};
|
||||
|
||||
function migrateLayout(preferences: DashboardLayout): DashboardLayout {
|
||||
const needsMigration = preferences.cards.some((c) => !c.panel);
|
||||
if (!needsMigration) return preferences;
|
||||
const defaultCardMap = new Map(DEFAULT_LAYOUT.cards.map((c) => [c.id, c]));
|
||||
return {
|
||||
...preferences,
|
||||
mainWidthPct: preferences.mainWidthPct ?? DEFAULT_LAYOUT.mainWidthPct,
|
||||
cards: preferences.cards.map((c) => ({
|
||||
...c,
|
||||
panel: c.panel ?? defaultCardMap.get(c.id)?.panel ?? "main",
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
function readFromLocalStorage(): DashboardLayout | null {
|
||||
try {
|
||||
const raw = localStorage.getItem(LS_KEY);
|
||||
if (!raw) return null;
|
||||
const parsed = JSON.parse(raw);
|
||||
if (parsed?.cards && Array.isArray(parsed.cards)) {
|
||||
return migrateLayout(parsed);
|
||||
}
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function writeToLocalStorage(layout: DashboardLayout) {
|
||||
try {
|
||||
localStorage.setItem(LS_KEY, JSON.stringify(layout));
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
export function useDashboardPreferences(enabled: boolean = true) {
|
||||
const [layout, setLayout] = useState<DashboardLayout | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
@@ -28,34 +66,29 @@ export function useDashboardPreferences(enabled: boolean = true) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Show cached layout immediately so the UI doesn't wait for the network
|
||||
const cached = readFromLocalStorage();
|
||||
if (cached) {
|
||||
setLayout(cached);
|
||||
setLoading(false);
|
||||
}
|
||||
|
||||
const fetchPreferences = async () => {
|
||||
try {
|
||||
const preferences = await getDashboardPreferences();
|
||||
if (preferences?.cards && Array.isArray(preferences.cards)) {
|
||||
// Migrate old layouts that don't have panel assignments
|
||||
const needsMigration = preferences.cards.some((c) => !c.panel);
|
||||
if (needsMigration) {
|
||||
const defaultCardMap = new Map(
|
||||
DEFAULT_LAYOUT.cards.map((c) => [c.id, c]),
|
||||
);
|
||||
const migrated: DashboardLayout = {
|
||||
...preferences,
|
||||
mainWidthPct:
|
||||
preferences.mainWidthPct ?? DEFAULT_LAYOUT.mainWidthPct,
|
||||
cards: preferences.cards.map((c) => ({
|
||||
...c,
|
||||
panel: c.panel ?? defaultCardMap.get(c.id)?.panel ?? "main",
|
||||
})),
|
||||
};
|
||||
setLayout(migrated);
|
||||
} else {
|
||||
setLayout(preferences);
|
||||
}
|
||||
const migrated = migrateLayout(preferences);
|
||||
setLayout(migrated);
|
||||
writeToLocalStorage(migrated);
|
||||
} else {
|
||||
setLayout(DEFAULT_LAYOUT);
|
||||
if (!cached) {
|
||||
setLayout(DEFAULT_LAYOUT);
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
setLayout(DEFAULT_LAYOUT);
|
||||
if (!cached) {
|
||||
setLayout(DEFAULT_LAYOUT);
|
||||
}
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
@@ -67,6 +100,7 @@ export function useDashboardPreferences(enabled: boolean = true) {
|
||||
const updateLayout = useCallback(
|
||||
(newLayout: DashboardLayout) => {
|
||||
setLayout(newLayout);
|
||||
writeToLocalStorage(newLayout);
|
||||
|
||||
if (saveTimeout) {
|
||||
clearTimeout(saveTimeout);
|
||||
@@ -87,6 +121,7 @@ export function useDashboardPreferences(enabled: boolean = true) {
|
||||
|
||||
const resetLayout = useCallback(async () => {
|
||||
setLayout(DEFAULT_LAYOUT);
|
||||
writeToLocalStorage(DEFAULT_LAYOUT);
|
||||
try {
|
||||
await saveDashboardPreferences(DEFAULT_LAYOUT);
|
||||
} catch (error) {
|
||||
|
||||
Reference in New Issue
Block a user