Add device identity tooltips and MAC validation
This commit is contained in:
@@ -387,6 +387,7 @@ 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);
|
||||
@@ -452,7 +453,7 @@ export function DevicesPanel({ feature }: { feature: DevicesFeature }) {
|
||||
</span>}
|
||||
|
||||
<div className="client-device-main">
|
||||
<h4 className={`client-device-name-heading${hasName ? '' : ' is-address-only'}${editing ? ' is-editing' : ''}`}>
|
||||
<h4 className={`client-device-name-heading client-tooltip-anchor${hasName ? '' : ' is-address-only'}${editing ? ' is-editing' : ''}`}>
|
||||
{editing ? (
|
||||
<input
|
||||
className="client-device-alias-input"
|
||||
@@ -461,6 +462,7 @@ 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)}
|
||||
@@ -470,9 +472,10 @@ export function DevicesPanel({ feature }: { feature: DevicesFeature }) {
|
||||
}}
|
||||
/>
|
||||
) : hasName && <button
|
||||
className="client-device-alias-trigger"
|
||||
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>}
|
||||
@@ -480,8 +483,10 @@ export function DevicesPanel({ feature }: { feature: DevicesFeature }) {
|
||||
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>
|
||||
</h4>
|
||||
{!hasName && !editing && <span className="client-device-edit-wrap client-tooltip-anchor">
|
||||
<button
|
||||
|
||||
@@ -17,6 +17,7 @@ export interface Device extends Record<string, unknown> {
|
||||
id: string;
|
||||
alias: string | null;
|
||||
hostname: string | null;
|
||||
mac: string;
|
||||
ip: string | null;
|
||||
lastSeenAt: string | null;
|
||||
status: DeviceStatus;
|
||||
@@ -113,6 +114,8 @@ function validDevice(value: unknown): value is Device {
|
||||
&& /^dev_[a-f0-9]{16}$/.test(value.id)
|
||||
&& nullableString(value.alias)
|
||||
&& nullableString(value.hostname)
|
||||
&& typeof value.mac === 'string'
|
||||
&& /^[0-9a-f]{2}(?::[0-9a-f]{2}){5}$/.test(value.mac)
|
||||
&& nullableString(value.ip)
|
||||
&& nullableTimestamp(value.lastSeenAt)
|
||||
&& (value.status === 'online' || value.status === 'recent' || value.status === 'offline')
|
||||
|
||||
@@ -277,7 +277,7 @@
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
overflow: hidden;
|
||||
overflow: visible;
|
||||
margin: 0;
|
||||
gap: 5px;
|
||||
font: var(--type-item-title);
|
||||
@@ -314,6 +314,12 @@
|
||||
cursor: text;
|
||||
}
|
||||
|
||||
.client-device-alias-trigger.is-custom-name {
|
||||
font: var(--type-section-title);
|
||||
letter-spacing: var(--type-section-title-tracking);
|
||||
text-transform: var(--type-section-title-transform);
|
||||
}
|
||||
|
||||
.client-device-alias-input {
|
||||
width: var(--alias-width);
|
||||
min-width: 1px;
|
||||
@@ -324,9 +330,9 @@
|
||||
outline: 0;
|
||||
background: transparent;
|
||||
caret-color: var(--client-accent);
|
||||
font: var(--type-item-title);
|
||||
letter-spacing: var(--type-item-title-tracking);
|
||||
text-transform: var(--type-item-title-transform);
|
||||
font: var(--type-section-title);
|
||||
letter-spacing: var(--type-section-title-tracking);
|
||||
text-transform: var(--type-section-title-transform);
|
||||
animation: client-device-alias-edit-in 360ms cubic-bezier(0.16, 1, 0.3, 1);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user