update the adminpage ui
This commit is contained in:
299
src/features/admin/components/MonitoringTables.tsx
Normal file
299
src/features/admin/components/MonitoringTables.tsx
Normal file
@@ -0,0 +1,299 @@
|
||||
'use client';
|
||||
|
||||
import {proportional} from '@astryxdesign/core/Table';
|
||||
import type {TableColumn} from '@astryxdesign/core/Table';
|
||||
import {HStack} from '@astryxdesign/core/Layout';
|
||||
import {Text} from '@astryxdesign/core/Text';
|
||||
import {Link} from '@astryxdesign/core/Link';
|
||||
import {StatusDot} from '@astryxdesign/core/StatusDot';
|
||||
import {ResponsiveTable} from '@/shared/components/patterns/ResponsiveTable';
|
||||
import type {
|
||||
AdminAlert,
|
||||
AdminCamera,
|
||||
AdminCameraStatus,
|
||||
AdminEvent,
|
||||
AdminSeverity,
|
||||
AdminStore,
|
||||
} from '@/features/admin/types/monitoring';
|
||||
|
||||
/**
|
||||
* The loaded branch of every drill-down section.
|
||||
*
|
||||
* None of these render today — every capability behind them is off, so their
|
||||
* sections show PendingIntegration instead. They exist so that turning a level
|
||||
* on is a flag flip: the rows are already written against the shapes in
|
||||
* types/monitoring.ts, and a missing field prints a dash, never a guess.
|
||||
*/
|
||||
|
||||
const DASH = '—';
|
||||
|
||||
function when(iso?: string): string {
|
||||
return iso ? new Date(iso).toLocaleString() : DASH;
|
||||
}
|
||||
|
||||
export const adminHref = {
|
||||
merchants: '/admin/merchants',
|
||||
company: (companyId: string) =>
|
||||
`/admin/merchants/${encodeURIComponent(companyId)}`,
|
||||
store: (companyId: string, storeId: string) =>
|
||||
`${adminHref.company(companyId)}/shops/${encodeURIComponent(storeId)}`,
|
||||
camera: (companyId: string, storeId: string, cameraId: string) =>
|
||||
`${adminHref.store(companyId, storeId)}/cameras/${encodeURIComponent(cameraId)}`,
|
||||
};
|
||||
|
||||
/* ── Stores ──────────────────────────────────────────────────────────── */
|
||||
|
||||
type StoreRow = Record<string, unknown> & {
|
||||
id: string;
|
||||
name: string;
|
||||
slug: string;
|
||||
isOnline?: boolean;
|
||||
timezone: string;
|
||||
cameras: string;
|
||||
online: string;
|
||||
};
|
||||
|
||||
export function StoreTable({
|
||||
companyId,
|
||||
stores,
|
||||
}: {
|
||||
companyId: string;
|
||||
stores: AdminStore[];
|
||||
}) {
|
||||
const columns: TableColumn<StoreRow>[] = [
|
||||
{
|
||||
key: 'name',
|
||||
header: 'Shop',
|
||||
width: proportional(3),
|
||||
renderCell: (row) => (
|
||||
<Link href={adminHref.store(companyId, row.id)}>
|
||||
<Text size="sm" weight="medium">
|
||||
{row.name}
|
||||
</Text>
|
||||
</Link>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'isOnline',
|
||||
header: 'Status',
|
||||
width: proportional(1),
|
||||
renderCell: (row) =>
|
||||
row.isOnline === undefined ? (
|
||||
<Text size="sm">{DASH}</Text>
|
||||
) : (
|
||||
<CameraStatus status={row.isOnline ? 'online' : 'offline'} />
|
||||
),
|
||||
},
|
||||
{key: 'timezone', header: 'Timezone', width: proportional(2)},
|
||||
{key: 'cameras', header: 'Cameras', width: proportional(1), align: 'end'},
|
||||
{key: 'online', header: 'Online', width: proportional(1), align: 'end'},
|
||||
];
|
||||
const rows: StoreRow[] = stores.map((s) => ({
|
||||
id: s.id,
|
||||
name: s.name || s.slug,
|
||||
slug: s.slug,
|
||||
isOnline: s.isOnline,
|
||||
timezone: s.timezone ?? DASH,
|
||||
cameras: s.cameras === undefined ? DASH : String(s.cameras),
|
||||
online: s.camerasOnline === undefined ? DASH : String(s.camerasOnline),
|
||||
}));
|
||||
return (
|
||||
<ResponsiveTable
|
||||
data={rows}
|
||||
columns={columns}
|
||||
idKey="id"
|
||||
primaryKey="name"
|
||||
density="balanced"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
/* ── Cameras ─────────────────────────────────────────────────────────── */
|
||||
|
||||
const CAMERA_STATUS: Record<
|
||||
AdminCameraStatus,
|
||||
{label: string; dot: 'success' | 'error' | 'neutral'}
|
||||
> = {
|
||||
online: {label: 'Online', dot: 'success'},
|
||||
offline: {label: 'Offline', dot: 'error'},
|
||||
unknown: {label: 'Unknown', dot: 'neutral'},
|
||||
};
|
||||
|
||||
export function CameraStatus({status}: {status: AdminCameraStatus}) {
|
||||
const s = CAMERA_STATUS[status];
|
||||
return (
|
||||
<HStack gap={1.5} vAlign="center">
|
||||
<StatusDot variant={s.dot} label={s.label} />
|
||||
<Text size="sm" color="secondary">
|
||||
{s.label}
|
||||
</Text>
|
||||
</HStack>
|
||||
);
|
||||
}
|
||||
|
||||
type CameraRow = Record<string, unknown> & {
|
||||
id: string;
|
||||
name: string;
|
||||
zone: string;
|
||||
status: AdminCameraStatus;
|
||||
lastSeen: string;
|
||||
};
|
||||
|
||||
export function CameraTable({
|
||||
companyId,
|
||||
storeId,
|
||||
cameras,
|
||||
}: {
|
||||
companyId: string;
|
||||
storeId: string;
|
||||
cameras: AdminCamera[];
|
||||
}) {
|
||||
const columns: TableColumn<CameraRow>[] = [
|
||||
{
|
||||
key: 'name',
|
||||
header: 'Camera',
|
||||
width: proportional(3),
|
||||
renderCell: (row) => (
|
||||
<Link href={adminHref.camera(companyId, storeId, row.id)}>
|
||||
<Text size="sm" weight="medium">
|
||||
{row.name}
|
||||
</Text>
|
||||
</Link>
|
||||
),
|
||||
},
|
||||
{key: 'zone', header: 'Location', width: proportional(2)},
|
||||
{
|
||||
key: 'status',
|
||||
header: 'Status',
|
||||
width: proportional(1),
|
||||
renderCell: (row) => <CameraStatus status={row.status} />,
|
||||
},
|
||||
{key: 'lastSeen', header: 'Last seen', width: proportional(2), align: 'end'},
|
||||
];
|
||||
const rows: CameraRow[] = cameras.map((c) => ({
|
||||
id: c.id,
|
||||
name: c.name,
|
||||
zone: c.zone ?? DASH,
|
||||
status: c.status,
|
||||
lastSeen: when(c.lastSeenAt),
|
||||
}));
|
||||
return (
|
||||
<ResponsiveTable
|
||||
data={rows}
|
||||
columns={columns}
|
||||
idKey="id"
|
||||
primaryKey="name"
|
||||
summaryKeys={['status', 'lastSeen']}
|
||||
density="balanced"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
/* ── Events and alerts ───────────────────────────────────────────────── */
|
||||
|
||||
const SEVERITY: Record<
|
||||
AdminSeverity,
|
||||
{label: string; dot: 'error' | 'warning' | 'neutral'}
|
||||
> = {
|
||||
critical: {label: 'Critical', dot: 'error'},
|
||||
warning: {label: 'Warning', dot: 'warning'},
|
||||
info: {label: 'Info', dot: 'neutral'},
|
||||
};
|
||||
|
||||
function Severity({severity}: {severity?: AdminSeverity}) {
|
||||
if (!severity) return <Text size="sm">{DASH}</Text>;
|
||||
const s = SEVERITY[severity];
|
||||
return (
|
||||
<HStack gap={1.5} vAlign="center">
|
||||
<StatusDot variant={s.dot} label={s.label} />
|
||||
<Text size="sm" color="secondary">
|
||||
{s.label}
|
||||
</Text>
|
||||
</HStack>
|
||||
);
|
||||
}
|
||||
|
||||
type EventRow = Record<string, unknown> & {
|
||||
id: string;
|
||||
at: string;
|
||||
type: string;
|
||||
camera: string;
|
||||
severity?: AdminSeverity;
|
||||
};
|
||||
|
||||
export function EventTable({events}: {events: AdminEvent[]}) {
|
||||
const columns: TableColumn<EventRow>[] = [
|
||||
{key: 'at', header: 'Time', width: proportional(2)},
|
||||
{key: 'type', header: 'Event', width: proportional(2)},
|
||||
{key: 'camera', header: 'Camera', width: proportional(2)},
|
||||
{
|
||||
key: 'severity',
|
||||
header: 'Severity',
|
||||
width: proportional(1),
|
||||
renderCell: (row) => <Severity severity={row.severity} />,
|
||||
},
|
||||
];
|
||||
const rows: EventRow[] = events.map((e) => ({
|
||||
id: e.id,
|
||||
at: when(e.at),
|
||||
type: e.type,
|
||||
camera: e.cameraName ?? DASH,
|
||||
severity: e.severity,
|
||||
}));
|
||||
return (
|
||||
<ResponsiveTable
|
||||
data={rows}
|
||||
columns={columns}
|
||||
idKey="id"
|
||||
primaryKey="type"
|
||||
density="compact"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
type AlertRow = Record<string, unknown> & {
|
||||
id: string;
|
||||
at: string;
|
||||
title: string;
|
||||
camera: string;
|
||||
severity: AdminSeverity;
|
||||
status: string;
|
||||
};
|
||||
|
||||
const ALERT_STATUS: Record<AdminAlert['status'], string> = {
|
||||
open: 'Open',
|
||||
acknowledged: 'Acknowledged',
|
||||
resolved: 'Resolved',
|
||||
};
|
||||
|
||||
export function AlertTable({alerts}: {alerts: AdminAlert[]}) {
|
||||
const columns: TableColumn<AlertRow>[] = [
|
||||
{
|
||||
key: 'severity',
|
||||
header: 'Severity',
|
||||
width: proportional(1),
|
||||
renderCell: (row) => <Severity severity={row.severity} />,
|
||||
},
|
||||
{key: 'title', header: 'Alert', width: proportional(3)},
|
||||
{key: 'camera', header: 'Camera', width: proportional(2)},
|
||||
{key: 'at', header: 'Time', width: proportional(2)},
|
||||
{key: 'status', header: 'Status', width: proportional(1)},
|
||||
];
|
||||
const rows: AlertRow[] = alerts.map((a) => ({
|
||||
id: a.id,
|
||||
at: when(a.at),
|
||||
title: a.title,
|
||||
camera: a.cameraName ?? DASH,
|
||||
severity: a.severity,
|
||||
status: ALERT_STATUS[a.status],
|
||||
}));
|
||||
return (
|
||||
<ResponsiveTable
|
||||
data={rows}
|
||||
columns={columns}
|
||||
idKey="id"
|
||||
primaryKey="title"
|
||||
density="compact"
|
||||
/>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user