Skip to content

Commit dc6c598

Browse files
committed
refactor(dashboard): merge stats and events into users sub-tabs
1 parent 8e20280 commit dc6c598

2 files changed

Lines changed: 60 additions & 20 deletions

File tree

web/src/Dashboard.tsx

Lines changed: 10 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,11 @@ import { AdminsSettings } from "./AdminsSettings";
33
import { getMe, logout } from "./api";
44
import { Credentials } from "./Credentials";
55
import { BrandLogo } from "./Logo";
6-
import { EventsPanel } from "./EventsPanel";
76
import { OverviewPanel } from "./OverviewPanel";
87
import { PaymentsPage } from "./PaymentsPage";
98
import { useIsAdmin, useIsOwner } from "./role";
109
import { navigate, useRoute } from "./router";
1110
import { SettingsPanel } from "./SettingsPanel";
12-
import { StatsPanel } from "./StatsPanel";
1311
import {
1412
cn,
1513
Drawer,
@@ -21,19 +19,14 @@ import {
2119
IconChevron,
2220
IconGithub,
2321
} from "./ui";
24-
import { UsersPanel } from "./UsersPanel";
22+
import { UsersPage } from "./UsersPage";
2523

2624
// "admins" is a page without a nav tab: the roster is reached from the account menu
2725
// (it's about who runs the panel, not about how the VPN is configured), so it never
2826
// appears in NAV — only in the route.
29-
type Tab =
30-
| "overview"
31-
| "users"
32-
| "stats"
33-
| "payments"
34-
| "events"
35-
| "settings"
36-
| "admins";
27+
// Statistics and the journal aren't tabs either: they're sub-tabs of "users"
28+
// (see UsersPage), because both only ever describe end users.
29+
type Tab = "overview" | "users" | "payments" | "settings" | "admins";
3730

3831
export function Dashboard({
3932
username,
@@ -74,16 +67,15 @@ export function Dashboard({
7467
return () => window.removeEventListener("rospanel:billing-changed", h);
7568
}, []);
7669

77-
// An operator gets the tabs whose routes they can actually call: end users, stats
78-
// and the journal. Settings and the payments desk are admin-and-up, so they're not
79-
// rendered — and if an operator navigates to /settings by hand, `tab` falls back
80-
// to the dashboard rather than showing a page whose every request would 403.
70+
// An operator gets the tabs whose routes they can actually call: the dashboard and
71+
// the users section (list, stats, journal). Settings and the payments desk are
72+
// admin-and-up, so they're not rendered — and if an operator navigates to /settings
73+
// by hand, `tab` falls back to the dashboard rather than showing a page whose every
74+
// request would 403.
8175
const NAV: { value: Tab; label: string }[] = [
8276
{ value: "overview", label: "Дашборд" },
8377
{ value: "users", label: "Пользователи" },
84-
{ value: "stats", label: "Статистика" },
8578
...(billing && isAdmin ? [{ value: "payments" as Tab, label: "Оплата" }] : []),
86-
{ value: "events", label: "Журнал" },
8779
...(isAdmin ? [{ value: "settings" as Tab, label: "Настройки" }] : []),
8880
];
8981
// The roster isn't in NAV, so resolve it separately — and only for the owner, so
@@ -232,10 +224,8 @@ export function Dashboard({
232224
<main className="mx-auto w-full max-w-6xl flex-1 px-3 py-6 sm:px-4">
233225
<div key={tab} className="animate-fade-in">
234226
{tab === "overview" && <OverviewPanel />}
235-
{tab === "users" && <UsersPanel />}
236-
{tab === "stats" && <StatsPanel />}
227+
{tab === "users" && <UsersPage />}
237228
{tab === "payments" && <PaymentsPage />}
238-
{tab === "events" && <EventsPanel />}
239229
{tab === "settings" && <SettingsPanel />}
240230
{tab === "admins" && <AdminsSettings />}
241231
</div>

web/src/UsersPage.tsx

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import { EventsPanel } from "./EventsPanel";
2+
import { navigate, useRoute } from "./router";
3+
import { StatsPanel } from "./StatsPanel";
4+
import { cn } from "./ui";
5+
import { UsersPanel } from "./UsersPanel";
6+
7+
// Statistics and the journal are both *about* end users — who spent how much
8+
// traffic, and what was done to whom — so they live as sub-tabs of this section
9+
// instead of eating two slots in the top nav.
10+
const SUBTABS = [
11+
{ value: "list", label: "Список" },
12+
{ value: "stats", label: "Статистика" },
13+
{ value: "events", label: "Журнал" },
14+
] as const;
15+
16+
type SubTab = (typeof SUBTABS)[number]["value"];
17+
18+
export function UsersPage() {
19+
const seg = useRoute();
20+
const tab = (SUBTABS.find((t) => t.value === seg[1])?.value ??
21+
"list") as SubTab;
22+
return (
23+
<div className="flex flex-col gap-4">
24+
<div className="no-scrollbar flex gap-1 overflow-x-auto border-b border-gray-200">
25+
{SUBTABS.map((t) => (
26+
<button
27+
key={t.value}
28+
onClick={() =>
29+
navigate(t.value === "list" ? "users" : `users/${t.value}`)
30+
}
31+
className={cn(
32+
"whitespace-nowrap border-b-2 px-3 py-2 text-sm font-semibold transition",
33+
tab === t.value
34+
? "border-brand-600 text-brand-800"
35+
: "border-transparent text-ink-muted hover:text-ink",
36+
)}
37+
>
38+
{t.label}
39+
</button>
40+
))}
41+
</div>
42+
43+
<div key={tab} className="animate-fade-in">
44+
{tab === "list" && <UsersPanel />}
45+
{tab === "stats" && <StatsPanel />}
46+
{tab === "events" && <EventsPanel />}
47+
</div>
48+
</div>
49+
);
50+
}

0 commit comments

Comments
 (0)