feat: add device selection and removal functionality in SecurityDevicesPage

This commit is contained in:
shuaiplus
2026-06-28 15:31:29 +08:00
parent 6a1a8357bf
commit a5ad16ac27
11 changed files with 163 additions and 4 deletions
+59 -3
View File
@@ -1,5 +1,5 @@
import { useState } from 'preact/hooks';
import { Clock3, Pencil, RefreshCw, ShieldCheck, ShieldOff, Trash2 } from 'lucide-preact';
import { CheckSquare, Clock3, Pencil, RefreshCw, ShieldCheck, ShieldOff, Trash2 } from 'lucide-preact';
import ConfirmDialog from '@/components/ConfirmDialog';
import LoadingState from '@/components/LoadingState';
import PendingAuthRequestsPanel from '@/components/PendingAuthRequestsPanel';
@@ -8,6 +8,7 @@ import { t } from '@/lib/i18n';
interface SecurityDevicesPageProps {
devices: AuthorizedDevice[];
currentDeviceIdentifier: string;
loading: boolean;
error: string;
pendingAuthRequests: AuthRequest[];
@@ -20,6 +21,7 @@ interface SecurityDevicesPageProps {
onRevokeTrust: (device: AuthorizedDevice) => void;
onTrustPermanently: (device: AuthorizedDevice) => void;
onRemoveDevice: (device: AuthorizedDevice) => void;
onRemoveSelectedDevices: (devices: AuthorizedDevice[]) => void;
onRevokeAll: () => void;
onRemoveAll: () => void;
}
@@ -62,6 +64,14 @@ export default function SecurityDevicesPage(props: SecurityDevicesPageProps) {
const [editingDevice, setEditingDevice] = useState<AuthorizedDevice | null>(null);
const [deviceNote, setDeviceNote] = useState('');
const [savingNote, setSavingNote] = useState(false);
const [selectedDeviceIds, setSelectedDeviceIds] = useState<string[]>([]);
const currentDeviceIdentifier = props.currentDeviceIdentifier;
const selectableDevices = props.devices.filter((device) => (
device.identifier !== currentDeviceIdentifier
));
const selectedDeviceIdSet = new Set(selectedDeviceIds);
const selectedDevices = selectableDevices.filter((device) => selectedDeviceIdSet.has(device.identifier));
const allSelectableSelected = selectableDevices.length > 0 && selectedDevices.length === selectableDevices.length;
async function handleSaveDeviceNote(): Promise<void> {
if (!editingDevice || savingNote) return;
@@ -75,6 +85,19 @@ export default function SecurityDevicesPage(props: SecurityDevicesPageProps) {
}
}
function toggleSelectAllDevices(): void {
setSelectedDeviceIds(allSelectableSelected ? [] : selectableDevices.map((device) => device.identifier));
}
function toggleSelectedDevice(device: AuthorizedDevice): void {
if (device.identifier === currentDeviceIdentifier) return;
setSelectedDeviceIds((current) => (
current.includes(device.identifier)
? current.filter((id) => id !== device.identifier)
: [...current, device.identifier]
));
}
return (
<>
<div className="stack">
@@ -101,6 +124,27 @@ export default function SecurityDevicesPage(props: SecurityDevicesPageProps) {
<RefreshCw size={14} className="btn-icon" />
{t('txt_refresh')}
</button>
<button
type="button"
className="btn btn-secondary small"
disabled={props.loading || selectableDevices.length === 0}
onClick={toggleSelectAllDevices}
>
<CheckSquare size={14} className="btn-icon" />
{allSelectableSelected ? t('txt_clear_selection') : t('txt_select_all')}
</button>
<button
type="button"
className="btn btn-danger small"
disabled={selectedDevices.length === 0}
onClick={() => {
props.onRemoveSelectedDevices(selectedDevices);
setSelectedDeviceIds([]);
}}
>
<Trash2 size={14} className="btn-icon" />
{t('txt_remove_selected_devices', { count: selectedDevices.length })}
</button>
<button type="button" className="btn btn-danger small" onClick={props.onRevokeAll}>
<ShieldOff size={14} className="btn-icon" />
{t('txt_revoke_all_trusted')}
@@ -122,6 +166,7 @@ export default function SecurityDevicesPage(props: SecurityDevicesPageProps) {
)}
<table className="table authorized-devices-table">
<colgroup>
<col className="authorized-devices-col-select" />
<col className="authorized-devices-col-device" />
<col className="authorized-devices-col-type" />
<col className="authorized-devices-col-status" />
@@ -132,6 +177,7 @@ export default function SecurityDevicesPage(props: SecurityDevicesPageProps) {
</colgroup>
<thead>
<tr>
<th>{t('txt_select')}</th>
<th>{t('txt_device')}</th>
<th>{t('txt_type')}</th>
<th>{t('txt_status')}</th>
@@ -144,6 +190,16 @@ export default function SecurityDevicesPage(props: SecurityDevicesPageProps) {
<tbody>
{props.devices.map((device) => (
<tr key={device.identifier}>
<td data-label={t('txt_select')}>
<input
type="checkbox"
className="authorized-device-checkbox"
checked={selectedDeviceIdSet.has(device.identifier)}
disabled={device.identifier === currentDeviceIdentifier}
aria-label={t('txt_select_device_name', { name: device.name || t('txt_unknown_device') })}
onChange={() => toggleSelectedDevice(device)}
/>
</td>
<td data-label={t('txt_device')}>
<div>{device.name || t('txt_unknown_device')}</div>
{!!device.deviceNote && !!device.systemName && device.systemName !== device.name && (
@@ -216,14 +272,14 @@ export default function SecurityDevicesPage(props: SecurityDevicesPageProps) {
))}
{props.loading && props.devices.length === 0 && (
<tr>
<td colSpan={7}>
<td colSpan={8}>
<LoadingState lines={5} compact />
</td>
</tr>
)}
{!props.loading && props.devices.length === 0 && (
<tr>
<td colSpan={7}>
<td colSpan={8}>
<div className="empty empty-comfortable">{t('txt_no_devices_found')}</div>
</td>
</tr>