Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 22 additions & 41 deletions app/(dashboard)/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,16 @@ import { RefreshCcw } from "lucide-react";
import { SummaryCards } from "@/components/dashboard/summary-cards";
import { DeviceList } from "@/components/dashboard/device-list";
import { DeviceListMobile } from "@/components/dashboard/device-list-mobile";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Switch } from "@/components/ui/switch";
import { Skeleton } from "@/components/ui/skeleton";
import { useI18n } from "@/lib/i18n";
import { useSettings, useSummary, useSummaryTemp } from "@/lib/hooks";
import { toast } from "sonner";
import { DurationKey } from "@/lib/constants";
import { TempChartSection } from "@/components/dashboard/temp-chart-section";
import { RequestState } from "@/components/ui/request-state";
import { RequestSkeleton } from "@/components/ui/request-skeleton";
import { Skeleton } from "@/components/ui/skeleton";

export default function DashboardPage() {
const { t } = useI18n();
Expand Down Expand Up @@ -49,30 +50,21 @@ export default function DashboardPage() {

if (summary.error) {
return (
<Card className="glass-panel">
<CardHeader>
<CardTitle>{t("dashboard.title")}</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<p className="text-sm text-muted-foreground">{String(summary.error)}</p>
<Button onClick={handleRefresh}>{t("common.retry")}</Button>
</CardContent>
</Card>
<RequestState
title={t("dashboard.title")}
message={String(summary.error)}
actionLabel={t("common.retry")}
onAction={handleRefresh}
/>
);
}

if (!summary.data) {
return (
<div className="space-y-6">
<Skeleton className="h-8 w-1/2" />
<div className="grid gap-4 md:grid-cols-4">
<Skeleton className="h-24" />
<Skeleton className="h-24" />
<Skeleton className="h-24" />
<Skeleton className="h-24" />
</div>
<Skeleton className="h-64" />
</div>
<RequestSkeleton
titleClassName="h-8 w-1/2"
blocks={["h-24", "h-24", "h-24", "h-24", "h-64"]}
/>
);
}

Expand All @@ -84,26 +76,15 @@ export default function DashboardPage() {
<h1 className="text-2xl font-semibold md:text-3xl">{t("dashboard.title")}</h1>
<p className="text-sm text-muted-foreground">{t("dashboard.subtitle")}</p>
</div>
<Card className="glass-panel">
<CardHeader>
<CardTitle>{t("dashboard.empty.title")}</CardTitle>
</CardHeader>
<CardContent className="space-y-4 text-sm text-muted-foreground">
<p>{t("dashboard.empty.body")}</p>
<div className="rounded-md border bg-muted/30 p-3 font-mono text-xs text-foreground">
{t("dashboard.empty.command")}
</div>
<Button
variant="outline"
onClick={handleRefresh}
disabled={isRefreshing}
aria-label={t("common.retry")}
>
<RefreshCcw className={isRefreshing ? "h-4 w-4 animate-spin" : "h-4 w-4"} />
<span className="ml-2">{t("common.retry")}</span>
</Button>
</CardContent>
</Card>
<RequestState
title={t("dashboard.empty.title")}
message={t("dashboard.empty.body")}
actionLabel={t("common.retry")}
onAction={handleRefresh}
/>
<div className="rounded-md border bg-muted/30 p-3 font-mono text-xs text-foreground">
{t("dashboard.empty.command")}
</div>
</div>
);
}
Expand Down
79 changes: 32 additions & 47 deletions app/device/device-detail-client.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,20 +11,15 @@ import { AttributeTable } from "@/components/device/attribute-table";
import { Button } from "@/components/ui/button";
import { Card, CardContent } from "@/components/ui/card";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { Skeleton } from "@/components/ui/skeleton";
import { useI18n } from "@/lib/i18n";
import { useDeviceDetails, useSettings } from "@/lib/hooks";
import { deviceTitleWithFallback } from "@/lib/format";
import { archiveDevice, deleteDevice, unarchiveDevice } from "@/lib/api";
import { DurationKey, DURATION_KEYS } from "@/lib/constants";
import { ConfirmActionDialog } from "@/components/ui/confirm-action-dialog";
import { RequestState } from "@/components/ui/request-state";
import { RequestSkeleton } from "@/components/ui/request-skeleton";

const SmartChart = dynamic(() => import("@/components/device/smart-chart").then((mod) => mod.SmartChart), {
ssr: false,
Expand Down Expand Up @@ -96,22 +91,18 @@ export function DeviceDetailClient({ wwn }: DeviceDetailClientProps) {

if (details.error) {
return (
<Card className="glass-panel">
<CardContent className="space-y-4 p-6">
<p className="text-sm text-muted-foreground">{String(details.error)}</p>
<Button onClick={() => details.mutate()}>{t("common.retry")}</Button>
</CardContent>
</Card>
<RequestState
title={t("device.title")}
message={String(details.error)}
actionLabel={t("common.retry")}
onAction={() => details.mutate()}
/>
);
}

if (!details.data) {
return (
<div className="space-y-6">
<Skeleton className="h-24" />
<Skeleton className="h-72" />
<Skeleton className="h-64" />
</div>
<RequestSkeleton blocks={["h-24", "h-72", "h-64"]} titleClassName="h-0 w-0" />
);
}

Expand Down Expand Up @@ -196,34 +187,28 @@ export function DeviceDetailClient({ wwn }: DeviceDetailClientProps) {
settings={settings.data}
/>

<Dialog open={confirmState !== null} onOpenChange={(open) => !open && setConfirmState(null)}>
<DialogContent>
<DialogHeader>
<DialogTitle>
{confirmState === "delete"
? t("device.actions.delete")
: confirmState === "archive"
? t("device.actions.archive")
: t("device.actions.unarchive")}
</DialogTitle>
<DialogDescription>
{confirmState === "delete"
? t("device.dialog.delete")
: confirmState === "archive"
? t("device.dialog.archive")
: t("device.dialog.unarchive")}
</DialogDescription>
</DialogHeader>
<DialogFooter>
<Button variant="outline" onClick={() => setConfirmState(null)}>
{t("common.cancel")}
</Button>
<Button variant={confirmState === "delete" ? "destructive" : "default"} onClick={handleAction}>
{t("common.confirm")}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
<ConfirmActionDialog
open={confirmState !== null}
title={
confirmState === "delete"
? t("device.actions.delete")
: confirmState === "archive"
? t("device.actions.archive")
: t("device.actions.unarchive")
}
description={
confirmState === "delete"
? t("device.dialog.delete")
: confirmState === "archive"
? t("device.dialog.archive")
: t("device.dialog.unarchive")
}
confirmLabel={t("common.confirm")}
cancelLabel={t("common.cancel")}
confirmVariant={confirmState === "delete" ? "destructive" : "default"}
onConfirm={handleAction}
onCancel={() => setConfirmState(null)}
/>
</div>
);
}
21 changes: 10 additions & 11 deletions app/settings/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,10 @@ import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Switch } from "@/components/ui/switch";
import { Skeleton } from "@/components/ui/skeleton";
import { InfoTip } from "@/components/ui/info-tip";
import { RequestState } from "@/components/ui/request-state";
import { RequestSkeleton } from "@/components/ui/request-skeleton";
import { Skeleton } from "@/components/ui/skeleton";
import { useI18n } from "@/lib/i18n";
import { useHealth, useSettings } from "@/lib/hooks";
import { saveSettings, sendTestNotification } from "@/lib/api";
Expand Down Expand Up @@ -114,21 +116,18 @@ export default function SettingsPage() {

if (settings.error) {
return (
<Card className="glass-panel">
<CardContent className="space-y-4 p-6">
<p className="text-sm text-muted-foreground">{String(settings.error)}</p>
<Button onClick={() => settings.mutate()}>{t("common.retry")}</Button>
</CardContent>
</Card>
<RequestState
title={t("settings.title")}
message={String(settings.error)}
actionLabel={t("common.retry")}
onAction={() => settings.mutate()}
/>
);
}

if (!draft) {
return (
<div className="space-y-6">
<Skeleton className="h-8 w-1/3" />
<Skeleton className="h-64" />
</div>
<RequestSkeleton blocks={["h-64"]} />
);
}

Expand Down
9 changes: 7 additions & 2 deletions components/dashboard/device-card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
formatDateTime,
formatPowerOnHours,
formatTemperature,
statusBarClass,
summaryAgeClass,
} from "@/lib/format";
import { AppConfig, DeviceSummaryModel, MetricsStatusThreshold } from "@/lib/types";
Expand All @@ -42,17 +43,20 @@ export function DeviceCard({ deviceSummary, settings, threshold, t, variant, onA
ProtocolIcon,
protocol,
statusLabel,
status,
title,
} = getDeviceCardData(deviceSummary, settings, threshold, t);
const temperatureUnit = settings?.temperature_unit ?? "celsius";
const statusBar = statusBarClass(status);

if (variant === "mobile") {
const handleNavigate = () => onNavigate(deviceHref(deviceSummary.device.wwn));
return (
<div
className={`rounded-lg border bg-card p-4 cursor-pointer transition-colors hover:bg-muted/30 ${failedEmphasis}`}
className={`relative overflow-hidden rounded-lg border bg-card p-4 cursor-pointer transition-colors hover:bg-muted/30 ${failedEmphasis}`}
onClick={handleNavigate}
>
<div className={`pointer-events-none absolute inset-x-0 top-0 h-1 ${statusBar}`} />
<div className="flex items-start justify-between gap-3">
<div>
<div className="flex items-center gap-2">
Expand Down Expand Up @@ -136,7 +140,8 @@ export function DeviceCard({ deviceSummary, settings, threshold, t, variant, onA
}

return (
<Card className={`glass-panel ${failedEmphasis}`}>
<Card className={`relative overflow-hidden glass-panel ${failedEmphasis}`}>
<div className={`pointer-events-none absolute inset-x-0 top-0 h-1 ${statusBar}`} />
<CardContent className="flex h-full flex-col gap-4 pt-6">
<Link
href={deviceHref(deviceSummary.device.wwn)}
Expand Down
53 changes: 20 additions & 33 deletions components/dashboard/device-list-mobile.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,11 @@ import { toast } from "sonner";
import React from "react";
import { useRouter } from "next/navigation";

import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import { ConfirmActionDialog } from "@/components/ui/confirm-action-dialog";
import { useI18n } from "@/lib/i18n";
import { performDeviceAction } from "@/lib/device-actions";
import { AppConfig, DeviceSummaryModel, MetricsStatusThreshold } from "@/lib/types";
import { groupDevicesByHost } from "@/components/dashboard/device-list-shared";
import { groupDevicesByHost, sortDevicesForDashboard } from "@/components/dashboard/device-list-shared";
import { DeviceCard } from "@/components/dashboard/device-card";

interface DeviceListMobileProps {
Expand All @@ -41,7 +33,8 @@ export function DeviceListMobile({ summary, settings, showArchived, onAction }:
);

const unknownHostLabel = t("dashboard.devices.host_unknown");
const groupEntries = groupDevicesByHost(devices, unknownHostLabel);
const sortedDevices = sortDevicesForDashboard(devices, settings);
const groupEntries = groupDevicesByHost(sortedDevices, unknownHostLabel);

const handleConfirm = async () => {
if (!confirmState) return;
Expand Down Expand Up @@ -89,28 +82,22 @@ export function DeviceListMobile({ summary, settings, showArchived, onAction }:
</div>
))}

<Dialog open={!!confirmState} onOpenChange={() => setConfirmState(null)}>
<DialogContent>
<DialogHeader>
<DialogTitle>{confirmState?.label}</DialogTitle>
<DialogDescription>
{confirmState?.action === "delete"
? t("device.actions.delete_warning")
: confirmState?.action === "archive"
? t("device.actions.archive_confirm")
: t("device.actions.unarchive_confirm")}
</DialogDescription>
</DialogHeader>
<DialogFooter>
<Button variant="outline" onClick={() => setConfirmState(null)}>
{t("device.actions.cancel")}
</Button>
<Button variant="destructive" onClick={handleConfirm}>
{t("device.actions.confirm")}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
<ConfirmActionDialog
open={!!confirmState}
title={confirmState?.label ?? ""}
description={
confirmState?.action === "delete"
? t("device.actions.delete_warning")
: confirmState?.action === "archive"
? t("device.actions.archive_confirm")
: t("device.actions.unarchive_confirm")
}
confirmLabel={t("device.actions.confirm")}
cancelLabel={t("device.actions.cancel")}
confirmVariant="destructive"
onConfirm={handleConfirm}
onCancel={() => setConfirmState(null)}
/>
</div>
);
}
Loading