forked from baron/baron-sso
클라이언트 대시보드 통계 실지표 연동 및 백엔드 API 구현
This commit is contained in:
@@ -9,6 +9,7 @@ import {
|
||||
ShieldHalf,
|
||||
} from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { useAuth } from "react-oidc-context";
|
||||
import { Link, useNavigate } from "react-router-dom";
|
||||
import {
|
||||
Avatar,
|
||||
@@ -34,15 +35,29 @@ import {
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "../../components/ui/table";
|
||||
import { fetchClients } from "../../lib/devApi";
|
||||
import { fetchClients, fetchDevStats } from "../../lib/devApi";
|
||||
import { t } from "../../lib/i18n";
|
||||
import { cn } from "../../lib/utils";
|
||||
|
||||
function ClientsPage() {
|
||||
const navigate = useNavigate();
|
||||
const { data, isLoading, error } = useQuery({
|
||||
const auth = useAuth();
|
||||
const hasAccessToken = Boolean(auth.user?.access_token);
|
||||
|
||||
const {
|
||||
data,
|
||||
isLoading: isLoadingClients,
|
||||
error: clientError,
|
||||
} = useQuery({
|
||||
queryKey: ["clients"],
|
||||
queryFn: fetchClients,
|
||||
enabled: hasAccessToken,
|
||||
});
|
||||
|
||||
const { data: statsData, isLoading: isLoadingStats } = useQuery({
|
||||
queryKey: ["dev-stats"],
|
||||
queryFn: fetchDevStats,
|
||||
enabled: hasAccessToken,
|
||||
});
|
||||
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
@@ -63,11 +78,10 @@ function ClientsPage() {
|
||||
return matchesSearch && matchesType && matchesStatus;
|
||||
});
|
||||
|
||||
const totalClients = clients.length;
|
||||
const activeClients = clients.filter(
|
||||
(client) => client.status === "active",
|
||||
).length;
|
||||
// TODO: Replace with real session/auth-failure metrics when backend endpoints are available.
|
||||
const totalClients = statsData?.total_clients ?? clients.length;
|
||||
const activeSessions = statsData?.active_sessions ?? 0;
|
||||
const authFailures = statsData?.auth_failures_24h ?? 0;
|
||||
|
||||
type StatTone = "up" | "down" | "stable";
|
||||
type StatItem = {
|
||||
labelKey: string;
|
||||
@@ -90,7 +104,7 @@ function ClientsPage() {
|
||||
{
|
||||
labelKey: "ui.dev.clients.stats.active_sessions",
|
||||
labelFallback: "Active Sessions",
|
||||
value: activeClients.toString(),
|
||||
value: activeSessions.toString(),
|
||||
deltaKey: "ui.dev.clients.stats.realtime",
|
||||
deltaFallback: "Realtime",
|
||||
tone: "up" as const,
|
||||
@@ -98,14 +112,16 @@ function ClientsPage() {
|
||||
{
|
||||
labelKey: "ui.dev.clients.stats.auth_failures",
|
||||
labelFallback: "Auth Failures (24h)",
|
||||
value: "0",
|
||||
deltaKey: "ui.dev.clients.stats.stable",
|
||||
deltaFallback: "Stable",
|
||||
tone: "stable" as const,
|
||||
value: authFailures.toString(),
|
||||
deltaKey: authFailures > 0 ? "ui.dev.clients.stats.alert" : "ui.dev.clients.stats.stable",
|
||||
deltaFallback: authFailures > 0 ? "Check Logs" : "Stable",
|
||||
tone: authFailures > 0 ? ("down" as const) : ("stable" as const),
|
||||
},
|
||||
];
|
||||
|
||||
if (isLoading) {
|
||||
const isLoading = isLoadingClients || isLoadingStats;
|
||||
|
||||
if (auth.isLoading || !hasAccessToken || isLoading) {
|
||||
return (
|
||||
<div className="p-8 text-center">
|
||||
{t("msg.dev.clients.loading", "Loading clients...")}
|
||||
@@ -113,10 +129,10 @@ function ClientsPage() {
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
if (clientError) {
|
||||
const errMsg =
|
||||
(error as AxiosError<{ error?: string }>).response?.data?.error ??
|
||||
(error as Error).message;
|
||||
(clientError as AxiosError<{ error?: string }>).response?.data?.error ??
|
||||
(clientError as Error).message;
|
||||
return (
|
||||
<div className="p-8 text-center text-red-500">
|
||||
{t("msg.dev.clients.load_error", "Error loading clients: {{error}}", {
|
||||
@@ -268,7 +284,13 @@ function ClientsPage() {
|
||||
<div className="mt-1 flex items-baseline gap-2">
|
||||
<span className="text-3xl font-bold">{item.value}</span>
|
||||
<Badge
|
||||
variant={item.tone === "up" ? "success" : "muted"}
|
||||
variant={
|
||||
item.tone === "up"
|
||||
? "success"
|
||||
: item.tone === "down"
|
||||
? "destructive"
|
||||
: "muted"
|
||||
}
|
||||
className={cn(
|
||||
"px-2",
|
||||
item.tone === "stable" && "bg-muted/40 text-foreground",
|
||||
|
||||
@@ -20,6 +20,12 @@ export type ClientListResponse = {
|
||||
offset: number;
|
||||
};
|
||||
|
||||
export type DevStats = {
|
||||
total_clients: number;
|
||||
active_sessions: number;
|
||||
auth_failures_24h: number;
|
||||
};
|
||||
|
||||
export type DevAuditLog = {
|
||||
event_id: string;
|
||||
timestamp: string;
|
||||
@@ -121,6 +127,11 @@ export async function fetchClients() {
|
||||
return data;
|
||||
}
|
||||
|
||||
export async function fetchDevStats() {
|
||||
const { data } = await apiClient.get<DevStats>("/dev/stats");
|
||||
return data;
|
||||
}
|
||||
|
||||
export async function fetchClient(clientId: string) {
|
||||
const { data } = await apiClient.get<ClientDetailResponse>(
|
||||
`/dev/clients/${clientId}`,
|
||||
|
||||
Reference in New Issue
Block a user