This commit is contained in:
LukeGus
2026-05-28 22:29:20 -05:00
parent 33dcde0827
commit 5777351145
238 changed files with 62303 additions and 98955 deletions
+57 -27
View File
@@ -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>
</>
);
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+23 -1
View File
@@ -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>
))
)}
+12 -6
View File
@@ -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>
+16 -14
View File
@@ -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) {