import React, { useEffect, useState, useCallback, useMemo, useRef } from "react"; import { useNavigate } from "react-router-dom"; import { Box, Typography, Button, IconButton, Tooltip, Table, TableBody, TableCell, TableContainer, TableHead, TableRow, TablePagination, Paper, TableSortLabel, Dialog, DialogTitle, DialogContent, DialogActions, Grid, } from "@mui/material"; import AddIcon from "@mui/icons-material/Add"; import EditIcon from "@mui/icons-material/Edit"; import DeleteIcon from "@mui/icons-material/Delete"; import VisibilityIcon from "@mui/icons-material/Visibility"; import type { ResourceConfig, FieldConfig } from "../types"; import { useResource } from "../context/useResource"; import { useAppContext } from "../context/AppContext"; import { ListCellRenderer, DetailFieldRenderer, applyDisplayFormat } from "./fields"; import { FilterBar } from "./FilterBar"; import { readSseCache, appendSseCache, clearSseCache, nextSseSeq, setSseConnected } from "../context/useResource"; import { SseConnectionStatus } from "./SseConnectionStatus"; interface ResourceListProps { resource: ResourceConfig; basePath: string; } function matchRow(row: any, filters: Record, fields: FieldConfig[], allResources: ResourceConfig[]): boolean { for (const field of fields) { if (!field.filterable) continue; const isRange = field.type === "integer" || field.type === "number" || field.format === "date" || field.format === "date-time"; if (isRange) { const from = filters[field.name + "_from"]; const to = filters[field.name + "_to"]; if (from || to) { const cell = row[field.name]; if (cell == null) return false; if (field.type === "integer" || field.type === "number") { if (from && Number(cell) < Number(from)) return false; if (to && Number(cell) > Number(to)) return false; } else { if (from && String(cell) < String(from)) return false; if (to && String(cell) > String(to)) return false; } } continue; } const val = filters[field.name]; if (!val) continue; const cell = row[field.name]; if (cell == null) return false; let str: string; if (field.fk && typeof cell === "object" && cell !== null) { const targetRes = allResources.find((r) => r.name === field.fk!.resource); if (targetRes) { const items = Array.isArray(cell) ? cell : [cell]; str = items.map((item: any) => applyDisplayFormat(item, targetRes.displayFormat)).join(" "); } else { str = String(cell); } } else { str = String(cell); } const filterParts = val.split(",").filter(Boolean); if (!filterParts.some((part) => str.toLowerCase().includes(part.toLowerCase()))) return false; } return true; } export function ResourceList({ resource, basePath }: ResourceListProps) { const navigate = useNavigate(); const { components, ...crud } = useResource(resource.name); const { resources: allResources, config } = useAppContext(); const [data, setData] = useState([]); const [total, setTotal] = useState(0); const [page, setPage] = useState(0); const [rowsPerPage, setRowsPerPage] = useState(resource.pagination?.defaultLimit ?? 20); const [sortField, setSortField] = useState(null); const [sortDir, setSortDir] = useState<"asc" | "desc">("asc"); const [filters, setFilters] = useState>({}); const [detailRow, setDetailRow] = useState(null); const isStreaming = resource.streaming === true; const hasActions = resource.operations.get || resource.operations.update || resource.operations.delete; const filterMode = config.resourceConfig?.[resource.name]?.filterOptions?.mode ?? "client"; const isClientMode = filterMode === "client" && !isStreaming; const visibleColumns = resource.listColumns .map((colName) => resource.fields.find((f) => f.name === colName)) .filter((f): f is FieldConfig => !!f && !f.hidden?.list); useEffect(() => { setFilters({}); }, [resource.name]); useEffect(() => { if (!isStreaming || !crud.stream) return; setData(readSseCache(resource.name)); setSseConnected(resource.name, false); const sub = crud.stream({ onEvent: (evt) => { const enriched = { ...evt, _received_at: new Date().toISOString(), _seq: nextSseSeq() }; const updated = appendSseCache(resource.name, enriched); setData(updated); }, onOpen: () => setSseConnected(resource.name, true), onError: () => setSseConnected(resource.name, false), }); return () => { setSseConnected(resource.name, false); sub.close(); }; }, [isStreaming, crud.stream, resource.name]); const serverFetchData = useCallback(async () => { const params: Record = {}; if (resource.pagination) { params[resource.pagination.limitParam] = rowsPerPage; params[resource.pagination.offsetParam] = page * rowsPerPage; } if (sortField) { params.sort = sortDir === "desc" ? `-${sortField}` : sortField; } for (const [key, val] of Object.entries(filters)) { if (val) params[key] = val; } const result = await crud.list(params); setData(result.items ?? []); setTotal(result.total ?? result.items?.length ?? 0); }, [crud.list, resource.pagination, rowsPerPage, page, sortField, sortDir, filters]); const clientFetchAll = useCallback(async () => { const params: Record = {}; if (resource.pagination) { params[resource.pagination.limitParam] = 0; } const result = await crud.list(params); setData(result.items ?? []); setTotal(result.items?.length ?? 0); }, [crud.list, resource.pagination]); useEffect(() => { if (isStreaming) return; if (isClientMode) { clientFetchAll(); } else { serverFetchData(); } }, [isStreaming, isClientMode, clientFetchAll, serverFetchData]); useEffect(() => { if (isClientMode) { setPage(0); } }, [filters, isClientMode]); const filteredData = useMemo(() => { if (!isClientMode) return data; let items = data.filter((row) => matchRow(row, filters, resource.fields, allResources)); if (sortField) { items = [...items].sort((a, b) => { const aVal = a[sortField]; const bVal = b[sortField]; if (aVal == null) return 1; if (bVal == null) return -1; if (aVal < bVal) return sortDir === "asc" ? -1 : 1; if (aVal > bVal) return sortDir === "asc" ? 1 : -1; return 0; }); } const start = page * rowsPerPage; return items.slice(start, start + rowsPerPage); }, [data, isClientMode, filters, sortField, sortDir, page, rowsPerPage, resource.fields, allResources]); const clientTotal = useMemo(() => { if (!isClientMode) return total; return data.filter((row) => matchRow(row, filters, resource.fields, allResources)).length; }, [data, isClientMode, filters, resource.fields, allResources]); const displayData = isClientMode ? filteredData : data; const displayTotal = isClientMode ? clientTotal : total; const handleDelete = async (id: string | number) => { if (!window.confirm("Are you sure you want to delete this item?")) return; await crud.remove(id); if (isClientMode) { clientFetchAll(); } else { serverFetchData(); } }; const handleSort = (field: string) => { if (sortField === field) { setSortDir((d) => (d === "asc" ? "desc" : "asc")); } else { setSortField(field); setSortDir("asc"); } }; const handleFilterChange = (fieldName: string, value: string) => { setFilters((prev) => ({ ...prev, [fieldName]: value })); }; return ( {resource.displayName} {isStreaming && } {isStreaming && data.length > 0 && ( )} {resource.operations.create && !isStreaming && ( )} {!isStreaming && ( setFilters({})} data={data} /> )} {visibleColumns.map((col) => ( {col.sortable ? ( handleSort(col.name)} > {col.label} ) : ( col.label )} ))} {hasActions && Actions} {displayData.length === 0 ? ( {isStreaming ? "Waiting for events\u2026" : "No records found"} ) : ( displayData.map((row, idx) => { const rowId = isStreaming ? `evt-${row._seq ?? idx}` : row[resource.primaryKey]; return ( { if (isStreaming) { setDetailRow(row); } else { navigate(`${basePath}/${resource.name}/${rowId}`); } }} > {visibleColumns.map((col) => { let value = row[col.name]; let fmt = resource.displayFormat; if (col.fk) { const targetRes = allResources.find((r) => r.name === col.fk!.resource); fmt = targetRes!.displayFormat; } else if (col.refSchema && !col.fk && col.inlineDisplayFormat) { fmt = col.inlineDisplayFormat; } return ( ); })} {hasActions && ( e.stopPropagation()}> {resource.operations.get && !isStreaming && ( navigate(`${basePath}/${resource.name}/${rowId}`)}> )} {resource.operations.update && ( navigate(`${basePath}/${resource.name}/${rowId}/edit`)}> )} {resource.operations.delete && ( handleDelete(rowId)} color="error"> )} )} ); }) )}
{!isStreaming && (resource.pagination || isClientMode) && ( setPage(p)} rowsPerPage={rowsPerPage} onRowsPerPageChange={(e) => { setRowsPerPage(parseInt(e.target.value, 10)); setPage(0); }} rowsPerPageOptions={[10, 20, 50, 100]} /> )} {!isStreaming && displayData.length > 0 && ( {displayTotal} record{displayTotal !== 1 ? "s" : ""} )} setDetailRow(null)} maxWidth="sm" fullWidth> {resource.displayName} Event {detailRow && ( {visibleColumns.map((col) => ( ))} )}
); }