Show device identity details and resolve hostnames
This commit is contained in:
@@ -35,6 +35,8 @@ interface PinCollapse {
|
||||
result: 'pending' | 'saved' | 'failed';
|
||||
}
|
||||
|
||||
type DeviceCopyField = 'IP' | 'MAC' | 'Hostname';
|
||||
|
||||
function requestMessage(value: unknown) {
|
||||
if (!value || typeof value !== 'object' || Array.isArray(value)) return undefined;
|
||||
const message: unknown = Reflect.get(value, 'message');
|
||||
@@ -77,7 +79,7 @@ export function DevicesPanel({ feature }: { feature: DevicesFeature }) {
|
||||
const [alias, setAlias] = useState('');
|
||||
const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>('desc');
|
||||
const [trafficScale, setTrafficScale] = useState<'linear' | 'log'>('linear');
|
||||
const [copyFeedback, setCopyFeedback] = useState<Record<string, { failed: boolean }>>({});
|
||||
const [copyFeedback, setCopyFeedback] = useState<Record<string, { field: DeviceCopyField; failed: boolean }>>({});
|
||||
const [copyAnnouncement, setCopyAnnouncement] = useState<{ id: string; message: string } | null>(null);
|
||||
const [pencilAnimationId, setPencilAnimationId] = useState('');
|
||||
const [trafficDeltas, setTrafficDeltas] = useState<Record<string, TrafficDelta>>({});
|
||||
@@ -189,15 +191,14 @@ export function DevicesPanel({ feature }: { feature: DevicesFeature }) {
|
||||
setEditingId((current) => current === device.id ? '' : current);
|
||||
}
|
||||
|
||||
async function copyDeviceIp(device: Device) {
|
||||
if (!device.ip) return;
|
||||
async function copyDeviceValue(device: Device, field: DeviceCopyField, value: string) {
|
||||
const activeTimer = copyTimers.current.get(device.id);
|
||||
if (activeTimer) clearTimeout(activeTimer);
|
||||
const attempt = {};
|
||||
copyAttempts.current.set(device.id, attempt);
|
||||
let feedback: { failed: boolean };
|
||||
try {
|
||||
await copyText(device.ip);
|
||||
await copyText(value);
|
||||
feedback = { failed: false };
|
||||
} catch {
|
||||
feedback = { failed: true };
|
||||
@@ -205,11 +206,11 @@ export function DevicesPanel({ feature }: { feature: DevicesFeature }) {
|
||||
if (copyAttempts.current.get(device.id) !== attempt) return;
|
||||
const announcement = {
|
||||
id: device.id,
|
||||
message: feedback.failed ? `Не удалось скопировать IP ${device.ip}` : `IP ${device.ip} скопирован`,
|
||||
message: feedback.failed ? `Не удалось скопировать ${field} ${value}` : `${field} ${value} скопирован`,
|
||||
};
|
||||
const pendingTimer = copyTimers.current.get(device.id);
|
||||
if (pendingTimer) clearTimeout(pendingTimer);
|
||||
setCopyFeedback((current) => ({ ...current, [device.id]: feedback }));
|
||||
setCopyFeedback((current) => ({ ...current, [device.id]: { ...feedback, field } }));
|
||||
setCopyAnnouncement(announcement);
|
||||
copyTimers.current.set(device.id, setTimeout(() => {
|
||||
setCopyFeedback((current) => {
|
||||
@@ -387,7 +388,6 @@ export function DevicesPanel({ feature }: { feature: DevicesFeature }) {
|
||||
const groupStart = group !== previousGroup;
|
||||
const hasName = Boolean(device.alias || device.hostname);
|
||||
const title = device.alias || device.hostname || device.ip || 'Неизвестное устройство';
|
||||
const identityTooltipId = `device-identity-${device.id}`;
|
||||
const editing = editingId === device.id;
|
||||
const saving = savingId === device.id;
|
||||
const seen = formatLastSeen(device.lastSeenAt);
|
||||
@@ -400,7 +400,6 @@ export function DevicesPanel({ feature }: { feature: DevicesFeature }) {
|
||||
const hasProxyTraffic = proxyTotal > 0n;
|
||||
const trafficDelta = trafficDeltas[device.id] || {};
|
||||
const feedback = copyFeedback[device.id];
|
||||
const copied = Boolean(feedback);
|
||||
const policyBusy = device.policyStatus === 'applying';
|
||||
const policyFailed = device.policyStatus === 'failed';
|
||||
const policyPending = device.policyStatus === 'pending';
|
||||
@@ -453,7 +452,10 @@ export function DevicesPanel({ feature }: { feature: DevicesFeature }) {
|
||||
</span>}
|
||||
|
||||
<div className="client-device-main">
|
||||
<h4 className={`client-device-name-heading client-tooltip-anchor${hasName ? '' : ' is-address-only'}${editing ? ' is-editing' : ''}`}>
|
||||
<h4
|
||||
className={`client-device-name-heading${hasName ? '' : ' is-address-only'}${editing ? ' is-editing' : ''}`}
|
||||
tabIndex={!hasName && !editing ? 0 : undefined}
|
||||
>
|
||||
{editing ? (
|
||||
<input
|
||||
className="client-device-alias-input"
|
||||
@@ -462,7 +464,6 @@ export function DevicesPanel({ feature }: { feature: DevicesFeature }) {
|
||||
maxLength={64}
|
||||
autoFocus
|
||||
aria-label="Название устройства"
|
||||
aria-describedby={identityTooltipId}
|
||||
aria-busy={saving}
|
||||
disabled={saving}
|
||||
onChange={(event) => setAlias(event.target.value)}
|
||||
@@ -475,18 +476,29 @@ export function DevicesPanel({ feature }: { feature: DevicesFeature }) {
|
||||
className={`client-device-alias-trigger${device.alias ? ' is-custom-name' : ''}`}
|
||||
type="button"
|
||||
aria-label={`Изменить название ${title}`}
|
||||
aria-describedby={identityTooltipId}
|
||||
onClick={() => startEditing(device)}
|
||||
>{title}</button>}
|
||||
{(hasName || (editing && Boolean(alias))) && device.ip && <span className="client-device-name-separator" aria-hidden="true">·</span>}
|
||||
{device.ip ? <button
|
||||
className={`client-device-ip${copied ? feedback.failed ? ' is-copy-error' : ' is-copied' : ''}`}
|
||||
type="button"
|
||||
aria-label={`Скопировать IP ${device.ip} устройства ${title}`}
|
||||
aria-describedby={identityTooltipId}
|
||||
onClick={() => copyDeviceIp(device)}
|
||||
>{device.ip}</button> : !hasName && !editing && <span>Неизвестное устройство</span>}
|
||||
<Tooltip id={identityTooltipId}>Hostname: {device.hostname || '—'} · MAC: {device.mac}</Tooltip>
|
||||
{!hasName && !editing && <span className="client-device-fallback-name">{title}</span>}
|
||||
{!editing && <span className="client-device-identity-details" role="group" aria-label={`Технические данные устройства ${title}`}>
|
||||
{device.ip && <button
|
||||
className={`client-device-identity-copy${feedback?.field === 'IP' ? feedback.failed ? ' is-copy-error' : ' is-copied' : ''}`}
|
||||
type="button"
|
||||
aria-label={`Скопировать IP ${device.ip}`}
|
||||
onClick={() => copyDeviceValue(device, 'IP', device.ip!)}
|
||||
><b>IP</b><span>{device.ip}</span></button>}
|
||||
<button
|
||||
className={`client-device-identity-copy${feedback?.field === 'MAC' ? feedback.failed ? ' is-copy-error' : ' is-copied' : ''}`}
|
||||
type="button"
|
||||
aria-label={`Скопировать MAC ${device.mac}`}
|
||||
onClick={() => copyDeviceValue(device, 'MAC', device.mac)}
|
||||
><b>MAC</b><span>{device.mac}</span></button>
|
||||
{device.hostname && <button
|
||||
className={`client-device-identity-copy${feedback?.field === 'Hostname' ? feedback.failed ? ' is-copy-error' : ' is-copied' : ''}`}
|
||||
type="button"
|
||||
aria-label={`Скопировать Hostname ${device.hostname}`}
|
||||
onClick={() => copyDeviceValue(device, 'Hostname', device.hostname!)}
|
||||
><b>Host</b><span>{device.hostname}</span></button>}
|
||||
</span>}
|
||||
</h4>
|
||||
{!hasName && !editing && <span className="client-device-edit-wrap client-tooltip-anchor">
|
||||
<button
|
||||
|
||||
Reference in New Issue
Block a user