mirror of
https://github.com/shuaiplus/nodewarden.git
synced 2026-06-20 21:00:41 +00:00
feat: add device note and last seen tracking to devices, enhance device management features
This commit is contained in:
@@ -1,4 +1,6 @@
|
||||
import { Clock3, RefreshCw, ShieldOff, Trash2 } from 'lucide-preact';
|
||||
import { useState } from 'preact/hooks';
|
||||
import { Clock3, Pencil, RefreshCw, ShieldOff, Trash2 } from 'lucide-preact';
|
||||
import ConfirmDialog from '@/components/ConfirmDialog';
|
||||
import type { AuthorizedDevice } from '@/lib/types';
|
||||
import { t } from '@/lib/i18n';
|
||||
|
||||
@@ -6,6 +8,7 @@ interface SecurityDevicesPageProps {
|
||||
devices: AuthorizedDevice[];
|
||||
loading: boolean;
|
||||
onRefresh: () => void;
|
||||
onRenameDevice: (device: AuthorizedDevice, name: string) => Promise<void>;
|
||||
onRevokeTrust: (device: AuthorizedDevice) => void;
|
||||
onRemoveDevice: (device: AuthorizedDevice) => void;
|
||||
onRevokeAll: () => void;
|
||||
@@ -41,9 +44,26 @@ function mapDeviceTypeName(type: number): string {
|
||||
}
|
||||
|
||||
export default function SecurityDevicesPage(props: SecurityDevicesPageProps) {
|
||||
const [editingDevice, setEditingDevice] = useState<AuthorizedDevice | null>(null);
|
||||
const [deviceNote, setDeviceNote] = useState('');
|
||||
const [savingNote, setSavingNote] = useState(false);
|
||||
|
||||
async function handleSaveDeviceNote(): Promise<void> {
|
||||
if (!editingDevice || savingNote) return;
|
||||
setSavingNote(true);
|
||||
try {
|
||||
await props.onRenameDevice(editingDevice, deviceNote);
|
||||
setEditingDevice(null);
|
||||
setDeviceNote('');
|
||||
} finally {
|
||||
setSavingNote(false);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="stack">
|
||||
<section className="card">
|
||||
<>
|
||||
<div className="stack">
|
||||
<section className="card">
|
||||
<div className="section-head">
|
||||
<div>
|
||||
<h3 style={{ margin: 0 }}>{t('txt_device_management')}</h3>
|
||||
@@ -66,9 +86,9 @@ export default function SecurityDevicesPage(props: SecurityDevicesPageProps) {
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</section>
|
||||
|
||||
<section className="card">
|
||||
<section className="card">
|
||||
<h3 style={{ marginTop: 0 }}>{t('txt_authorized_devices')}</h3>
|
||||
<table className="table">
|
||||
<thead>
|
||||
@@ -87,6 +107,9 @@ export default function SecurityDevicesPage(props: SecurityDevicesPageProps) {
|
||||
<tr key={device.identifier}>
|
||||
<td data-label={t('txt_device')}>
|
||||
<div>{device.name || t('txt_unknown_device')}</div>
|
||||
{!!device.deviceNote && !!device.systemName && device.systemName !== device.name && (
|
||||
<div className="muted-inline">{device.systemName}</div>
|
||||
)}
|
||||
<div className="muted-inline">{device.identifier}</div>
|
||||
</td>
|
||||
<td data-label={t('txt_type')}>{mapDeviceTypeName(device.type)}</td>
|
||||
@@ -96,7 +119,7 @@ export default function SecurityDevicesPage(props: SecurityDevicesPageProps) {
|
||||
</span>
|
||||
</td>
|
||||
<td data-label={t('txt_added')}>{formatDateTime(device.creationDate)}</td>
|
||||
<td data-label={t('txt_last_seen')}>{formatDateTime(device.revisionDate)}</td>
|
||||
<td data-label={t('txt_last_seen')}>{formatDateTime(device.lastSeenAt || device.revisionDate)}</td>
|
||||
<td data-label={t('txt_trusted_until')}>
|
||||
{device.trusted ? (
|
||||
<div className="trusted-cell">
|
||||
@@ -116,11 +139,28 @@ export default function SecurityDevicesPage(props: SecurityDevicesPageProps) {
|
||||
onClick={() => props.onRevokeTrust(device)}
|
||||
>
|
||||
<ShieldOff size={14} className="btn-icon" />
|
||||
{t('txt_revoke_trust')}
|
||||
{t('txt_untrust')}
|
||||
</button>
|
||||
<button type="button" className="btn btn-danger small" onClick={() => props.onRemoveDevice(device)}>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-secondary small"
|
||||
disabled={device.hasStoredDevice === false}
|
||||
onClick={() => {
|
||||
setEditingDevice(device);
|
||||
setDeviceNote(device.deviceNote || device.name || '');
|
||||
}}
|
||||
>
|
||||
<Pencil size={14} className="btn-icon" />
|
||||
{t('txt_device_note')}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-danger small"
|
||||
disabled={device.hasStoredDevice === false}
|
||||
onClick={() => props.onRemoveDevice(device)}
|
||||
>
|
||||
<Trash2 size={14} className="btn-icon" />
|
||||
{t('txt_remove_device_2')}
|
||||
{t('txt_delete')}
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
@@ -135,7 +175,35 @@ export default function SecurityDevicesPage(props: SecurityDevicesPageProps) {
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<ConfirmDialog
|
||||
open={!!editingDevice}
|
||||
title={t('txt_device_note')}
|
||||
message={t('txt_replace_device_name_with_note')}
|
||||
confirmText={t('txt_save')}
|
||||
cancelText={t('txt_cancel')}
|
||||
showIcon={false}
|
||||
confirmDisabled={savingNote}
|
||||
cancelDisabled={savingNote}
|
||||
onConfirm={() => void handleSaveDeviceNote()}
|
||||
onCancel={() => {
|
||||
if (savingNote) return;
|
||||
setEditingDevice(null);
|
||||
setDeviceNote('');
|
||||
}}
|
||||
>
|
||||
<label className="field">
|
||||
<span>{t('txt_device_note')}</span>
|
||||
<input
|
||||
className="input"
|
||||
maxLength={128}
|
||||
value={deviceNote}
|
||||
onInput={(e) => setDeviceNote((e.currentTarget as HTMLInputElement).value)}
|
||||
/>
|
||||
</label>
|
||||
</ConfirmDialog>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user