added cache for dashboard and storage pages
This commit is contained in:
parent
eb69c8500c
commit
5c16371ae0
7 changed files with 81 additions and 15 deletions
File diff suppressed because one or more lines are too long
9
cmd/telesrv-admin/web/dist/assets/index-Cto7ZSSt.js
vendored
Normal file
9
cmd/telesrv-admin/web/dist/assets/index-Cto7ZSSt.js
vendored
Normal file
File diff suppressed because one or more lines are too long
2
cmd/telesrv-admin/web/dist/index.html
vendored
2
cmd/telesrv-admin/web/dist/index.html
vendored
|
|
@ -23,7 +23,7 @@
|
|||
})();
|
||||
</script>
|
||||
|
||||
<script type="module" crossorigin src="/assets/index-BnMOLFk0.js"></script>
|
||||
<script type="module" crossorigin src="/assets/index-Cto7ZSSt.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-pvHlBxfG.css">
|
||||
</head>
|
||||
<body>
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ import {
|
|||
} from "lucide-react";
|
||||
import { useEffect, useState, type ReactNode } from "react";
|
||||
import { api, errorMessage } from "../api";
|
||||
import { clearAdminCache } from "../lib/cache";
|
||||
import { permissionBotVerificationReview, permissionServerManage, permissionAdminsManage,
|
||||
permissionAccountsRead,
|
||||
permissionChannelsRead,
|
||||
|
|
@ -242,6 +243,11 @@ export function Shell({
|
|||
const thirdPartyVerificationHidden = useThirdPartyVerificationHidden();
|
||||
async function logout() {
|
||||
await api.logout().catch(() => undefined);
|
||||
// Drop the cached figures with the session. Without this the next operator
|
||||
// to sign in on this tab would open the dashboard on the previous one's
|
||||
// numbers -- briefly, but from an account that may not be allowed to see
|
||||
// them at all.
|
||||
clearAdminCache();
|
||||
onLogout();
|
||||
}
|
||||
|
||||
|
|
|
|||
35
cmd/telesrv-admin/web/src/lib/cache.ts
Normal file
35
cmd/telesrv-admin/web/src/lib/cache.ts
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
// Last-known values for screens that are visited repeatedly.
|
||||
//
|
||||
// Navigating back to the dashboard used to re-mount it with empty state, so an
|
||||
// operator watched the same numbers redraw from skeletons every time. The
|
||||
// screens already refresh themselves; what they lacked was something to show
|
||||
// while that happens. Reading from here on mount means the page opens on the
|
||||
// figures it had, and the refresh quietly replaces them.
|
||||
//
|
||||
// Kept in module memory rather than localStorage on purpose. This is admin data
|
||||
// -- account counts, storage figures -- and it has no business outliving the
|
||||
// tab or sitting on disk. It dies on reload, and clearAdminCache() drops it at
|
||||
// sign-out so the next operator in the same tab never sees the previous one's
|
||||
// figures.
|
||||
|
||||
const store = new Map<string, unknown>();
|
||||
|
||||
export function cacheGet<T>(key: string): T | undefined {
|
||||
return store.get(key) as T | undefined;
|
||||
}
|
||||
|
||||
export function cacheSet<T>(key: string, value: T): void {
|
||||
store.set(key, value);
|
||||
}
|
||||
|
||||
export function clearAdminCache(): void {
|
||||
store.clear();
|
||||
}
|
||||
|
||||
// Keys live here rather than as loose strings at each call site, so a typo
|
||||
// cannot quietly create a second cache that never hits.
|
||||
export const cacheKeys = {
|
||||
dashboard: "dashboard",
|
||||
storageStats: "storage.stats",
|
||||
storageAccounts: "storage.accounts"
|
||||
} as const;
|
||||
|
|
@ -17,13 +17,19 @@ import {
|
|||
} from "lucide-react";
|
||||
import { type ReactNode, useEffect, useState } from "react";
|
||||
import { api } from "../api";
|
||||
import { cacheGet, cacheKeys, cacheSet } from "../lib/cache";
|
||||
import { Alert } from "../components/ui";
|
||||
import type { Navigate } from "../routing";
|
||||
import { formatBytes, formatQuantity } from "../lib/format";
|
||||
import type { DashboardResponse } from "../types";
|
||||
|
||||
export function Dashboard({ navigate }: { navigate: Navigate }) {
|
||||
const [data, setData] = useState<DashboardResponse | null>(null);
|
||||
// Seeded from the last values this session saw, so coming back to the
|
||||
// dashboard opens on the numbers instead of on a grid of skeletons. The
|
||||
// 15s refresh below still runs and replaces them.
|
||||
const [data, setData] = useState<DashboardResponse | null>(
|
||||
() => cacheGet<DashboardResponse>(cacheKeys.dashboard) ?? null
|
||||
);
|
||||
const [error, setError] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
|
|
@ -31,6 +37,7 @@ export function Dashboard({ navigate }: { navigate: Navigate }) {
|
|||
async function load() {
|
||||
try {
|
||||
const res = await api.dashboard();
|
||||
cacheSet(cacheKeys.dashboard, res);
|
||||
if (!cancelled) setData(res);
|
||||
} catch (err) {
|
||||
if (!cancelled) setError(err instanceof Error ? err.message : "Failed to load dashboard");
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import { ArrowDown, ArrowUp, ArrowUpDown, ChevronDown, ChevronRight, Loader2, Re
|
|||
import { useEffect, useMemo, useState } from "react";
|
||||
import { createPortal } from "react-dom";
|
||||
import { api, errorMessage } from "../api";
|
||||
import { cacheGet, cacheKeys, cacheSet } from "../lib/cache";
|
||||
import { ActionButton } from "../components/ActionButton";
|
||||
import { Alert, EmptyRow, LoadingRow, Metric, PageFrame, QueryPanel, SectionHead } from "../components/ui";
|
||||
import { displayUsername, formatBytes, formatQuantity } from "../lib/format";
|
||||
|
|
@ -39,12 +40,18 @@ function SortableHeader({
|
|||
}
|
||||
|
||||
function StorageOverviewTab({ navigate }: { navigate: Navigate }) {
|
||||
const [stats, setStats] = useState<StorageStatsResponse | null>(null);
|
||||
// Seeded from this session's last figures, so returning to Storage opens on
|
||||
// numbers rather than on shimmering placeholders. loadStats still runs.
|
||||
const [stats, setStats] = useState<StorageStatsResponse | null>(
|
||||
() => cacheGet<StorageStatsResponse>(cacheKeys.storageStats) ?? null
|
||||
);
|
||||
// Tracked separately from `error`: loadStats deliberately swallows its
|
||||
// failure so it can't block the account list, which would otherwise leave
|
||||
// the metric skeletons shimmering forever on a stats-only outage.
|
||||
const [statsFailed, setStatsFailed] = useState(false);
|
||||
const [rows, setRows] = useState<AccountStorageRow[]>([]);
|
||||
const [rows, setRows] = useState<AccountStorageRow[]>(
|
||||
() => cacheGet<AccountStorageRow[]>(cacheKeys.storageAccounts) ?? []
|
||||
);
|
||||
const [hasMore, setHasMore] = useState(false);
|
||||
const [offset, setOffset] = useState(0);
|
||||
const [busy, setBusy] = useState(false);
|
||||
|
|
@ -55,7 +62,9 @@ function StorageOverviewTab({ navigate }: { navigate: Navigate }) {
|
|||
|
||||
async function loadStats() {
|
||||
try {
|
||||
setStats(await api.storageStats());
|
||||
const next = await api.storageStats();
|
||||
cacheSet(cacheKeys.storageStats, next);
|
||||
setStats(next);
|
||||
setStatsFailed(false);
|
||||
} catch {
|
||||
// Stats are a header nicety; a failure here shouldn't block the list.
|
||||
|
|
@ -77,7 +86,16 @@ function StorageOverviewTab({ navigate }: { navigate: Navigate }) {
|
|||
try {
|
||||
const result = await api.storageAccounts(params);
|
||||
const page = result.rows ?? [];
|
||||
setRows((current) => (next ? [...current, ...page] : page));
|
||||
setRows((current) => {
|
||||
const merged = next ? [...current, ...page] : page;
|
||||
// Only the first page is worth keeping: it is what the screen opens on,
|
||||
// and caching an appended list would restore a scroll position nobody
|
||||
// asked for.
|
||||
if (!next) {
|
||||
cacheSet(cacheKeys.storageAccounts, merged);
|
||||
}
|
||||
return merged;
|
||||
});
|
||||
setOffset(result.next_offset);
|
||||
setHasMore(Boolean(result.has_more));
|
||||
} catch (err) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue