bafe799d00
CI / checks (pull_request) Failing after 12m24s
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.
217 lines
7.9 KiB
TypeScript
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;
|
|
}
|