over / src /components /data-view.tsx
luguog's picture
fix(microsoft): use configurable Azure AD client ID for device-code flow
3f13033 verified
Raw
History Blame Contribute Delete
4.69 kB
"use client";
import { PageHeader, PageSection, Stat, EmptyState, Loading, ErrorState } from "./page-shell";
import { useApi } from "./use-api";
import type { LucideIcon } from "lucide-react";
function isObject(v: unknown): v is Record<string, unknown> {
return typeof v === "object" && v !== null && !Array.isArray(v);
}
function renderValue(v: unknown): React.ReactNode {
if (v === null || v === undefined) return <span className="text-muted-foreground"></span>;
if (typeof v === "boolean") return v ? "Yes" : "No";
if (typeof v === "string" || typeof v === "number") return String(v);
if (Array.isArray(v)) {
if (v.length === 0) return <span className="text-muted-foreground">None</span>;
return (
<ul className="space-y-1">
{v.map((item, i) => (
<li key={i} className="text-sm text-foreground/80">
{isObject(item) ? <ObjectTable obj={item} /> : String(item)}
</li>
))}
</ul>
);
}
if (isObject(v)) return <ObjectTable obj={v} />;
return String(v);
}
function ObjectTable({ obj }: { obj: Record<string, unknown> }) {
const entries = Object.entries(obj).filter(([, v]) => typeof v !== "function");
if (entries.length === 0) return <span className="text-muted-foreground">Empty</span>;
return (
<div className="overflow-x-auto rounded-lg border border-border bg-muted/10">
<table className="w-full text-sm">
<tbody>
{entries.map(([k, v]) => (
<tr key={k} className="border-b border-border last:border-0">
<td className="w-1/3 p-2.5 text-xs font-semibold uppercase tracking-wider text-muted-foreground">
{k.replace(/([A-Z])/g, " $1").replace(/_/g, " ")}
</td>
<td className="p-2.5 text-foreground/90">{renderValue(v)}</td>
</tr>
))}
</tbody>
</table>
</div>
);
}
export function RecordList({
items,
keyFn,
render,
empty,
}: {
items: any[];
keyFn: (item: any) => string;
render: (item: any) => React.ReactNode;
empty?: React.ReactNode;
}) {
if (items.length === 0) return empty || <p className="text-sm text-muted-foreground">No records found.</p>;
return <div className="space-y-3">{items.map((item) => <div key={keyFn(item)}>{render(item)}</div>)}</div>;
}
export function GenericDataPage({
icon: Icon,
title,
subtitle,
endpoint,
listKey,
renderItem,
listItemKey,
stats,
renderOverview,
hideOverview,
}: {
icon: LucideIcon;
title: string;
subtitle: string;
endpoint: string;
listKey?: string;
listItemKey?: (item: any) => string;
renderItem?: (item: any) => React.ReactNode;
stats?: { label: string; path: string }[];
renderOverview?: (data: any) => React.ReactNode;
hideOverview?: boolean;
}) {
const { data, loading, error, refetch } = useApi(endpoint);
if (loading) return <Loading message={`Loading ${title.toLowerCase()}…`} />;
if (error) return <ErrorState error={error} onRetry={refetch} />;
const list = listKey && data ? data[listKey] : data;
const values = stats
? stats.map((s: any) => {
const v = s.path.split(".").reduce((acc: any, k: string) => acc?.[k], data);
return { ...s, value: v };
})
: [];
return (
<div className="mx-auto max-w-7xl px-6 py-8 page-enter">
<PageHeader icon={Icon} title={title} subtitle={subtitle}>
<button onClick={refetch} className="btn btn-ghost text-xs">Refresh</button>
</PageHeader>
{values.length > 0 && (
<div className="mt-6 grid grid-cols-2 gap-3 sm:grid-cols-4 lg:grid-cols-6">
{values.map((s: any) => (
<Stat key={s.label} label={s.label} value={s.value ?? 0} />
))}
</div>
)}
{listKey && Array.isArray(list) ? (
<PageSection title={listKey.replace(/_/g, " ").replace(/\b\w/g, (l: string) => l.toUpperCase())} className="mt-6">
{list.length === 0 ? (
<EmptyState title="No records" description={`The ${title.toLowerCase()} ledger is empty.`} />
) : (
<div className="space-y-3">
{list.map((item: any) => (
<div key={(listItemKey ? listItemKey(item) : item.id) || Math.random()} className="card card-hover p-4">
{renderItem ? renderItem(item) : <ObjectTable obj={item} />}
</div>
))}
</div>
)}
</PageSection>
) : !hideOverview ? (
<PageSection title="Overview" className="mt-6">
{renderOverview ? renderOverview(data) : <ObjectTable obj={data || {}} />}
</PageSection>
) : null}
</div>
);
}