From 2ed73a3a348be95bfa2c37d3bf05924fa8b01412 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Tue, 30 Jun 2026 18:24:15 +0200 Subject: [PATCH] fix(admin): truncate and copy the login-overview IP address MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A long IPv6 in the login overview forced the table to scroll sideways and the full address was awkward to lift out. Cap the cell, ellipsize the address, and make it a button that copies the full value to the clipboard with a transient checkmark and a toast — keyboard-activatable, with an accessible label. The empty case stays plain text. --- messages/de.json | 5 +- messages/en.json | 5 +- messages/es.json | 5 +- messages/fr.json | 5 +- messages/it.json | 5 +- messages/pl.json | 5 +- .../admin/login-overview-section.tsx | 52 ++++++++++++++++++- 7 files changed, 75 insertions(+), 7 deletions(-) diff --git a/messages/de.json b/messages/de.json index 413ecb79f..4438719f7 100644 --- a/messages/de.json +++ b/messages/de.json @@ -5883,7 +5883,10 @@ "mfaEnforcedOn": "MFA an", "mfaEnforcedOff": "MFA aus", "mfaEnforcedOnHint": "Für diesen Nutzer ist ein zweiter Faktor erforderlich. Klicken, um die Anforderung aufzuheben.", - "mfaEnforcedOffHint": "Einen zweiten Faktor für diesen Nutzer verlangen." + "mfaEnforcedOffHint": "Einen zweiten Faktor für diesen Nutzer verlangen.", + "copyIp": "IP-Adresse kopieren", + "ipCopied": "IP-Adresse kopiert", + "copyFailed": "Kopieren fehlgeschlagen." }, "achievements": { "title": "Erfolge", diff --git a/messages/en.json b/messages/en.json index c2bf9e148..11fdc9ee9 100644 --- a/messages/en.json +++ b/messages/en.json @@ -5883,7 +5883,10 @@ "mfaEnforcedOn": "MFA on", "mfaEnforcedOff": "MFA off", "mfaEnforcedOnHint": "A second factor is required for this user. Click to stop requiring it.", - "mfaEnforcedOffHint": "Require a second factor for this user." + "mfaEnforcedOffHint": "Require a second factor for this user.", + "copyIp": "Copy IP address", + "ipCopied": "IP address copied", + "copyFailed": "Could not copy." }, "achievements": { "title": "Achievements", diff --git a/messages/es.json b/messages/es.json index 530ef26a6..c498e22e8 100644 --- a/messages/es.json +++ b/messages/es.json @@ -5883,7 +5883,10 @@ "mfaEnforcedOn": "MFA activado", "mfaEnforcedOff": "MFA desactivado", "mfaEnforcedOnHint": "Este usuario debe usar un segundo factor. Haz clic para dejar de exigirlo.", - "mfaEnforcedOffHint": "Exigir un segundo factor a este usuario." + "mfaEnforcedOffHint": "Exigir un segundo factor a este usuario.", + "copyIp": "Copiar dirección IP", + "ipCopied": "Dirección IP copiada", + "copyFailed": "No se pudo copiar." }, "achievements": { "title": "Logros", diff --git a/messages/fr.json b/messages/fr.json index 503f55f6e..8cba119f6 100644 --- a/messages/fr.json +++ b/messages/fr.json @@ -5883,7 +5883,10 @@ "mfaEnforcedOn": "MFA activé", "mfaEnforcedOff": "MFA désactivé", "mfaEnforcedOnHint": "Un second facteur est requis pour cet utilisateur. Cliquez pour ne plus l'exiger.", - "mfaEnforcedOffHint": "Exiger un second facteur pour cet utilisateur." + "mfaEnforcedOffHint": "Exiger un second facteur pour cet utilisateur.", + "copyIp": "Copier l’adresse IP", + "ipCopied": "Adresse IP copiée", + "copyFailed": "Impossible de copier." }, "achievements": { "title": "Réussites", diff --git a/messages/it.json b/messages/it.json index 850acef23..88a6e34fb 100644 --- a/messages/it.json +++ b/messages/it.json @@ -5883,7 +5883,10 @@ "mfaEnforcedOn": "MFA attivo", "mfaEnforcedOff": "MFA disattivato", "mfaEnforcedOnHint": "Per questo utente è richiesto un secondo fattore. Clicca per non richiederlo più.", - "mfaEnforcedOffHint": "Richiedi un secondo fattore per questo utente." + "mfaEnforcedOffHint": "Richiedi un secondo fattore per questo utente.", + "copyIp": "Copia indirizzo IP", + "ipCopied": "Indirizzo IP copiato", + "copyFailed": "Impossibile copiare." }, "achievements": { "title": "Obiettivi", diff --git a/messages/pl.json b/messages/pl.json index 71e383c3c..3b16a27c3 100644 --- a/messages/pl.json +++ b/messages/pl.json @@ -5883,7 +5883,10 @@ "mfaEnforcedOn": "MFA wł.", "mfaEnforcedOff": "MFA wył.", "mfaEnforcedOnHint": "Ten użytkownik musi używać drugiego składnika. Kliknij, aby przestać go wymagać.", - "mfaEnforcedOffHint": "Wymagaj drugiego składnika dla tego użytkownika." + "mfaEnforcedOffHint": "Wymagaj drugiego składnika dla tego użytkownika.", + "copyIp": "Kopiuj adres IP", + "ipCopied": "Skopiowano adres IP", + "copyFailed": "Nie udało się skopiować." }, "achievements": { "title": "Osiągnięcia", diff --git a/src/components/admin/login-overview-section.tsx b/src/components/admin/login-overview-section.tsx index 41d6e1ae4..54a6059fc 100644 --- a/src/components/admin/login-overview-section.tsx +++ b/src/components/admin/login-overview-section.tsx @@ -2,8 +2,11 @@ import { useMemo, useState } from "react"; import { useQuery } from "@tanstack/react-query"; +import { toast } from "sonner"; import { + Check, CheckCircle2, + Copy, Download, Loader2, MapPin, @@ -94,6 +97,25 @@ export function LoginOverviewSection() { const AUTH_ACTION_LABELS = useAuthActionLabels(); const AUTH_PROVIDER_LABELS = useAuthProviderLabels(); + // v1.25.11 (#151) — long IPv6 addresses are truncated in the table cell so + // they never force the wrapper into horizontal scroll; the full value is one + // click away. Track the most-recently-copied row so the icon can flip to a + // checkmark for transient feedback. + const [copiedIp, setCopiedIp] = useState(null); + + async function copyIp(id: string, ip: string) { + try { + await navigator.clipboard.writeText(ip); + setCopiedIp(id); + toast.success(t("admin.ipCopied")); + window.setTimeout(() => { + setCopiedIp((current) => (current === id ? null : current)); + }, 2_000); + } catch { + toast.error(t("admin.copyFailed")); + } + } + // Build the query string once so it's reused by the data-query key, the // export download, and the next-/prev- buttons. const queryParams = useMemo(() => { @@ -464,7 +486,35 @@ export function LoginOverviewSection() { - {entry.ipAddress ?? "—"} + {entry.ipAddress ? ( + + ) : ( + "—" + )} {entry.location ?? "—"}