Files
floci-ui/packages/frontend/src/components/ResourceTable.tsx
T
Zakaria bafe799d00
CI / checks (pull_request) Failing after 12m24s
feat(frontend): manage Cognito users and groups, and test sign-in
The Cognito panel gains a test sign-in that shows the decoded ID and
access token claims, a groups section, and a per-user panel to enable or
disable, confirm, sign out everywhere, reset or set a password and change
group membership.

Delete confirmations (row and bulk) now say what else goes, driven by
the schema's deleteImpact, and the inspector follows list refreshes
instead of the click-time snapshot.
2026-10-23 17:54:15 -04:00

217 lines
7.9 KiB
TypeScript

import {useState} from 'react';
import {Link} from 'react-router-dom';
import {Pencil, Trash2} from 'lucide-react';
import {describeDeleteImpact} from '@/lib/deleteImpact';
import type {CloudResource} from '@/types/resource';
import type {ServiceSchema} from '@/types/schema';
import {getPath} from '@/lib/resourcePath';
import {renderColumnValue} from '@/lib/columnFormat';
import {CopyValueButton} from '@/components/CopyValueButton';
interface ResourceTableProps {
schema: ServiceSchema;
resources: CloudResource[];
selectedId?: string;
onSelect: (resource: CloudResource) => void;
onEdit?: (resource: CloudResource) => void;
onDelete: (resource: CloudResource) => void;
deletingId?: string;
dataPath?: (resource: CloudResource) => string | undefined;
isDeleteMulti?: boolean;
selectedItems?: CloudResource[];
onToggleSelect?: (resource: CloudResource) => void;
onToggleSelectAll?: () => void;
setAllSelected?: (value: boolean) => void;
isAllSelected?: boolean;
}
export function ResourceTable({
schema,
resources,
selectedId,
onSelect,
onEdit,
onDelete,
deletingId,
dataPath,
isDeleteMulti,
selectedItems,
onToggleSelect,
onToggleSelectAll,
setAllSelected,
isAllSelected = false,
}: ResourceTableProps) {
const [confirmId, setConfirmId] = useState<string | null>(null);
const canDelete = schema.actions.includes('delete');
const canEdit = schema.actions.includes('update') && Boolean(schema.updateFields?.length) && Boolean(onEdit);
const hasActions = canDelete || canEdit || Boolean(dataPath && resources.some((resource) => dataPath(resource)));
if (resources.length === 0) {
const emptyTitle = `No ${schema.displayName} found.`;
const emptyDescription = `The connected runtime did not return any ${schema.displayName} resources.`;
return (
<div className="empty compact">
<h3>{emptyTitle}</h3>
<p>{emptyDescription}</p>
</div>
);
}
return (
<table className="table resource-table">
<thead>
<tr>
{isDeleteMulti && (
<th style={{ width: '1%' }}>
<input
type="checkbox"
aria-label="Select all resources"
checked={isAllSelected && resources.length > 0}
onChange={() => {
if (onToggleSelectAll) {
onToggleSelectAll();
} else {
setAllSelected?.(!isAllSelected);
}
}}
/>
</th>
)}
{schema.columns.map((column) => (
<th key={column.name} style={column.width ? { width: column.width } : undefined}>
{column.label}
</th>
))}
{hasActions && <th aria-label="Actions" />}
</tr>
</thead>
<tbody>
{resources.map((resource) => {
const explorePath = dataPath?.(resource);
return (
<tr key={resource.id} className={selectedId === resource.id ? 'selected' : ''}>
{isDeleteMulti && (
<td style={{ width: '1%' }} onClick={(e) => e.stopPropagation()}>
<input
type="checkbox"
aria-label={`Select ${resource.name || resource.id}`}
checked={selectedItems?.some((item) => item.id === resource.id) ?? false}
onChange={() => {
onToggleSelect?.(resource);
}}
/>
</td>
)}
{schema.columns.map((column) => (
<td key={column.name} onClick={() => onSelect(resource)}>
<CopyableColumnValue resource={resource} column={column} />
</td>
))}
{hasActions && (
<td className="table-actions">
<div className="table-actions-group">
{explorePath && (
<Link
className="button compact"
to={explorePath}
aria-label={`Explore data in ${resource.name}`}
onClick={(e) => e.stopPropagation()}
>
Explore data
</Link>
)}
{canEdit && (
<button
className={schema.cloud === 'aws' && schema.service === 'serverless' ? 'button compact lambda-edit-button' : 'icon-btn'}
type="button"
title={`Edit ${resource.name}`}
aria-label={`Edit ${resource.name}`}
onClick={(e) => {
e.stopPropagation();
onEdit?.(resource);
}}
>
<Pencil size={13} />
{schema.cloud === 'aws' && schema.service === 'serverless' && 'Edit Lambda'}
</button>
)}
{canDelete && (
confirmId === resource.id ? (
<div
className="table-confirm-actions"
onClick={(e) => e.stopPropagation()}
>
<DeleteImpact schema={schema} resource={resource} />
<button
className="button danger compact"
type="button"
disabled={deletingId === resource.id}
onClick={(e) => {
e.stopPropagation();
onDelete(resource);
}}
>
Confirm
</button>
<button
className="button success compact"
type="button"
disabled={deletingId === resource.id}
onClick={(e) => {
e.stopPropagation();
setConfirmId(null);
}}
>
Cancel
</button>
</div>
) : (
<button
className="icon-btn danger"
type="button"
title={`Delete ${resource.name}`}
aria-label={`Delete ${resource.name}`}
disabled={deletingId === resource.id}
onClick={(e) => {
e.stopPropagation();
setConfirmId(resource.id);
}}
>
<Trash2 size={13} />
</button>
)
)}
</div>
</td>
)}
</tr>
);
})}
</tbody>
</table>
);
}
function CopyableColumnValue({resource, column}: {resource: CloudResource; column: ServiceSchema['columns'][number]}) {
const value = getPath(resource, column.path ?? column.name);
const copyable = typeof value === 'string' && (
column.format === 'code' || /(^id$|id$|arn|ip|endpoint|url|uri|key)/i.test(column.name)
);
if (!copyable) {
return renderColumnValue(value, column);
}
return (
<span className="copy-value">
{renderColumnValue(value, column)}
<CopyValueButton value={value} label={column.label} />
</span>
);
}
function DeleteImpact({schema, resource}: {schema: ServiceSchema; resource: CloudResource}) {
const text = describeDeleteImpact(schema.deleteImpact, [resource]);
return text ? <span className="delete-impact" role="status">{text}</span> : null;
}