Add device identity tooltips and MAC validation
Build and Deploy Gateway / build-and-push (push) Successful in 21s
Build and Deploy Gateway / deploy (push) Successful in 6s

This commit is contained in:
2026-08-11 11:45:11 +03:00
parent 7e4da4bdcf
commit 501c498edf
8 changed files with 43 additions and 25 deletions
+2 -2
View File
@@ -1,6 +1,6 @@
export const HARBOR_VERSIONS = Object.freeze({
macClient: '0.24.0',
gatewayClient: '0.25.0',
macClient: '0.24.1',
gatewayClient: '0.25.1',
gatewayBackend: '0.25.0',
});
+7 -2
View File
@@ -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')
+10 -4
View File
@@ -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);
}