mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-08 12:32:43 +00:00
fix(admin): truncate and copy the login-overview IP address
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.
This commit is contained in:
1 parent
45ca6ff24b
commit
2ed73a3a34
7 files changed
+75
-7
No files matched your search
+4
-1
@@ -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",
|
||||
|
||||
+4
-1
@@ -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",
|
||||
|
||||
+4
-1
@@ -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",
|
||||
|
||||
+4
-1
@@ -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",
|
||||
|
||||
+4
-1
@@ -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",
|
||||
|
||||
+4
-1
@@ -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",
|
||||
|
||||
@@ -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<string | null>(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() {
|
||||
</span>
|
||||
</td>
|
||||
<td className="text-muted-foreground px-3 py-2 font-mono text-xs">
|
||||
{entry.ipAddress ?? "—"}
|
||||
{entry.ipAddress ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() =>
|
||||
copyIp(entry.id, entry.ipAddress as string)
|
||||
}
|
||||
title={entry.ipAddress}
|
||||
aria-label={t("admin.copyIp")}
|
||||
data-slot="login-overview-ip"
|
||||
className="hover:text-foreground focus-visible:ring-ring inline-flex max-w-[10rem] items-center gap-1 rounded font-mono focus-visible:ring-2 focus-visible:outline-none"
|
||||
>
|
||||
<span className="truncate">
|
||||
{entry.ipAddress}
|
||||
</span>
|
||||
{copiedIp === entry.id ? (
|
||||
<Check
|
||||
className="text-dracula-green h-3 w-3 shrink-0"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
) : (
|
||||
<Copy
|
||||
className="h-3 w-3 shrink-0 opacity-60"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
)}
|
||||
</button>
|
||||
) : (
|
||||
"—"
|
||||
)}
|
||||
</td>
|
||||
<td className="text-muted-foreground px-3 py-2 text-xs">
|
||||
{entry.location ?? "—"}
|
||||
|
||||
Reference in new issue
Block a user