Skip to content

Commit c4a2339

Browse files
committed
Implement persistence with tanstack
1 parent dfe6579 commit c4a2339

9 files changed

Lines changed: 365 additions & 38 deletions

File tree

packages/web/package-lock.json

Lines changed: 52 additions & 7 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/web/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@
2525
"@radix-ui/react-slot": "^1.2.3",
2626
"@tailwindcss/vite": "^4.1.12",
2727
"@tanstack/query-devtools": "^5.84.0",
28+
"@tanstack/query-async-storage-persister": "^5.85.3",
29+
"@tanstack/react-query-persist-client": "^5.85.3",
2830
"@tanstack/react-query": "^5.85.3",
2931
"@tanstack/react-table": "^8.21.3",
3032
"axios": "^1.11.0",

packages/web/src/App.tsx

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,35 @@
11
import { BrowserRouter as Router, Routes, Route } from "react-router-dom";
2-
import { QueryClientProvider } from "@tanstack/react-query";
2+
import { PersistQueryClientProvider } from "@tanstack/react-query-persist-client";
3+
import type { Query } from "@tanstack/react-query";
34

45
import { ThemeProvider } from "@/components/theme-provider";
56
import { Layout } from "@/components/layout/layout";
67
import { Dashboard } from "@/pages/dashboard";
78
import { DeviceDetail } from "@/pages/device-detail";
89
import { Settings } from "@/pages/settings";
9-
import { queryClient } from "@/lib/query-client";
10+
import { queryClient, persister } from "@/lib/query-client";
1011

11-
// Initialize i18n
1212
import "@/i18n";
1313

1414
function App() {
1515
return (
16-
<QueryClientProvider client={queryClient}>
16+
<PersistQueryClientProvider
17+
client={queryClient}
18+
persistOptions={{
19+
persister,
20+
maxAge: 1000 * 60 * 60 * 24 * 7, // 7 days (longer than our 2-day stale threshold)
21+
dehydrateOptions: {
22+
shouldDehydrateQuery: (query: Query) => {
23+
return (
24+
query.queryKey.length >= 2 &&
25+
query.queryKey[0] === "devices" &&
26+
query.queryKey[1] === "scan" &&
27+
query.state.data !== undefined
28+
);
29+
},
30+
},
31+
}}
32+
>
1733
<ThemeProvider defaultTheme="system" storageKey="shelly-manager-theme">
1834
<Router>
1935
<Routes>
@@ -25,7 +41,7 @@ function App() {
2541
</Routes>
2642
</Router>
2743
</ThemeProvider>
28-
</QueryClientProvider>
44+
</PersistQueryClientProvider>
2945
);
3046
}
3147

packages/web/src/components/dashboard/device-table.tsx

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ import {
4343
CardHeader,
4444
CardTitle,
4545
} from "@/components/ui/card";
46+
import { ScanTimestamp } from "@/components/dashboard/scan-timestamp";
4647
import type { Device } from "@/types/api";
4748

4849
interface DeviceTableProps {
@@ -255,10 +256,17 @@ export function DeviceTable({ devices, onBulkAction }: DeviceTableProps) {
255256
return (
256257
<Card>
257258
<CardHeader>
258-
<CardTitle>{t("dashboard.deviceTable.title")}</CardTitle>
259-
<CardDescription>
260-
{t("dashboard.deviceTable.description", { count: devices.length })}
261-
</CardDescription>
259+
<div className="flex items-start justify-between">
260+
<div>
261+
<CardTitle>{t("dashboard.deviceTable.title")}</CardTitle>
262+
<CardDescription>
263+
{t("dashboard.deviceTable.description", {
264+
count: devices.length,
265+
})}
266+
</CardDescription>
267+
</div>
268+
<ScanTimestamp />
269+
</div>
262270
</CardHeader>
263271
<CardContent>
264272
<div className="space-y-4">
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import { useTranslation } from "react-i18next";
2+
import { formatDistanceToNow } from "date-fns";
3+
import { Clock, AlertTriangle } from "lucide-react";
4+
import { getLastScanTimestamp, isDataStale } from "@/lib/storage";
5+
6+
interface ScanTimestampProps {
7+
className?: string;
8+
}
9+
10+
export function ScanTimestamp({ className }: ScanTimestampProps) {
11+
const { t } = useTranslation();
12+
13+
const lastScanTime = getLastScanTimestamp();
14+
const isStale = isDataStale();
15+
16+
if (!lastScanTime) {
17+
return null;
18+
}
19+
20+
const timeAgo = formatDistanceToNow(lastScanTime, { addSuffix: true });
21+
22+
return (
23+
<div
24+
className={`flex items-center space-x-2 text-sm text-muted-foreground ${className || ""}`}
25+
>
26+
<div className="flex items-center space-x-1">
27+
<Clock className="h-4 w-4" />
28+
<span>
29+
{t("dashboard.lastScanned", "Last scanned")}: {timeAgo}
30+
</span>
31+
</div>
32+
33+
{isStale && (
34+
<div className="relative group">
35+
<AlertTriangle className="h-4 w-4 text-amber-600 cursor-help" />
36+
<div className="absolute bottom-full left-1/2 transform -translate-x-1/2 mb-2 px-3 py-2 bg-gray-900 text-white text-xs rounded-lg opacity-0 group-hover:opacity-100 transition-opacity duration-200 pointer-events-none whitespace-nowrap z-10">
37+
{t(
38+
"dashboard.staleData",
39+
"Data older than 2 days, consider re-scanning.",
40+
)}
41+
<div className="absolute top-full left-1/2 transform -translate-x-1/2 border-4 border-transparent border-t-gray-900"></div>
42+
</div>
43+
</div>
44+
)}
45+
</div>
46+
);
47+
}

packages/web/src/components/layout/navbar.tsx

Lines changed: 8 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -43,25 +43,22 @@ export function Navbar() {
4343
<span>{t("navigation.dashboard")}</span>
4444
</Link>
4545
</Button>
46+
</div>
4647

48+
{/* Right side */}
49+
<div className="flex items-center space-x-2">
4750
<Button
48-
variant={isActive("/settings") ? "default" : "ghost"}
51+
variant="ghost"
4952
size="sm"
5053
asChild
51-
className={cn(
52-
"flex items-center space-x-2",
53-
isActive("/settings") && "bg-primary text-primary-foreground",
54-
)}
54+
title={t("navigation.settings")}
55+
className="h-8 w-8 px-0"
5556
>
5657
<Link to="/settings">
57-
<Settings className="h-4 w-4" />
58-
<span>{t("navigation.settings")}</span>
58+
<Settings className="h-[1.2rem] w-[1.2rem]" />
59+
<span className="sr-only">{t("navigation.settings")}</span>
5960
</Link>
6061
</Button>
61-
</div>
62-
63-
{/* Right side */}
64-
<div className="flex items-center space-x-2">
6562
<ThemeToggle />
6663
</div>
6764
</div>

packages/web/src/lib/query-client.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,26 @@
11
import { QueryClient } from "@tanstack/react-query";
2+
import { createAsyncStoragePersister } from "@tanstack/query-async-storage-persister";
3+
4+
export const persister = createAsyncStoragePersister({
5+
storage: window.localStorage,
6+
key: "shelly-query-cache",
7+
throttleTime: 1000,
8+
serialize: JSON.stringify,
9+
deserialize: (data: string) => {
10+
try {
11+
return JSON.parse(data);
12+
} catch {
13+
return null;
14+
}
15+
},
16+
});
217

318
export const queryClient = new QueryClient({
419
defaultOptions: {
520
queries: {
621
retry: 2,
722
staleTime: 5 * 60 * 1000,
8-
gcTime: 10 * 60 * 1000,
23+
gcTime: 1000 * 60 * 60 * 24, // 24 hours for persistence
924
enabled: false,
1025
},
1126
mutations: {

0 commit comments

Comments
 (0)