fix: improve duration formatting (Xm Ys) and truncate exchange IDs

- formatDuration and formatDurationShort now show Xm Ys for durations >= 60s (e.g. "5m 21s" instead of "321s") and 1 decimal for 1-60s range ("6.7s" instead of "6.70s")
- Exchange ID column shows last 8 chars with ellipsis prefix; full ID on hover, copies to clipboard on click

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
hsiegeln
2026-04-09 18:34:04 +02:00
parent ba0a1850a9
commit eadcd160a3
14 changed files with 293 additions and 269 deletions

View File

@@ -35,6 +35,7 @@ import {
import type { UserDetail } from '../../api/queries/admin/rbac';
import { useAuthStore } from '../../auth/auth-store';
import styles from './UserManagement.module.css';
import sectionStyles from '../../styles/section-card.module.css';
export default function UsersTab({ highlightId, onHighlightConsumed }: { highlightId?: string | null; onHighlightConsumed?: () => void }) {
const { toast } = useToast();
@@ -366,24 +367,27 @@ export default function UsersTab({ highlightId, onHighlightConsumed }: { highlig
</Button>
</div>
<SectionHeader>Status</SectionHeader>
<div className={styles.sectionTags}>
<Tag label="Active" color="success" />
<div className={sectionStyles.section}>
<SectionHeader>Status</SectionHeader>
<div className={styles.sectionTags}>
<Tag label="Active" color="success" />
</div>
<div className={styles.metaGrid}>
<span className={styles.metaLabel}>ID</span>
<MonoText size="xs">{selected.userId}</MonoText>
<span className={styles.metaLabel}>Created</span>
<span className={styles.metaValue}>
{new Date(selected.createdAt).toLocaleDateString()}
</span>
<span className={styles.metaLabel}>Provider</span>
<span className={styles.metaValue}>{selected.provider}</span>
</div>
</div>
<div className={styles.metaGrid}>
<span className={styles.metaLabel}>ID</span>
<MonoText size="xs">{selected.userId}</MonoText>
<span className={styles.metaLabel}>Created</span>
<span className={styles.metaValue}>
{new Date(selected.createdAt).toLocaleDateString()}
</span>
<span className={styles.metaLabel}>Provider</span>
<span className={styles.metaValue}>{selected.provider}</span>
</div>
<SectionHeader>Security</SectionHeader>
<div className={styles.securitySection}>
<div className={sectionStyles.section}>
<SectionHeader>Security</SectionHeader>
<div className={styles.securitySection}>
{selected.provider === 'local' ? (
<>
<div className={styles.securityRow}>
@@ -445,133 +449,138 @@ export default function UsersTab({ highlightId, onHighlightConsumed }: { highlig
</InfoCallout>
</>
)}
</div>
</div>
<SectionHeader>Group membership (direct only)</SectionHeader>
<div className={styles.sectionTags}>
{selected.directGroups.map((g) => (
<Tag
key={g.id}
label={g.name}
color="success"
onRemove={() => {
removeFromGroup.mutate(
{ userId: selected.userId, groupId: g.id },
{
onSuccess: () =>
toast({ title: 'Group removed', variant: 'success' }),
onError: () =>
toast({
title: 'Failed to remove group',
variant: 'error',
duration: 86_400_000,
}),
},
);
}}
/>
))}
{selected.directGroups.length === 0 && (
<span className={styles.inheritedNote}>(no groups)</span>
)}
<MultiSelect
options={availableGroups}
value={[]}
onChange={(ids) => {
for (const groupId of ids) {
addToGroup.mutate(
{ userId: selected.userId, groupId },
{
onSuccess: () =>
toast({ title: 'Added to group', variant: 'success' }),
onError: () =>
toast({
title: 'Failed to add group',
variant: 'error',
duration: 86_400_000,
}),
},
);
}
}}
placeholder="+ Add"
/>
</div>
<SectionHeader>
Effective roles (direct + inherited)
</SectionHeader>
<div className={styles.sectionTags}>
{selected.directRoles.map((r) => (
<Tag
key={r.id}
label={r.name}
color="warning"
onRemove={() => {
removeRole.mutate(
{ userId: selected.userId, roleId: r.id },
{
onSuccess: () =>
toast({
title: 'Role removed',
description: r.name,
variant: 'success',
}),
onError: () =>
toast({
title: 'Failed to remove role',
variant: 'error',
duration: 86_400_000,
}),
},
);
}}
/>
))}
{inheritedRoles.map((r) => (
<Badge
key={r.id}
label={`${r.name} ↑ group`}
color="warning"
variant="dashed"
className={styles.inherited}
/>
))}
{selected.directRoles.length === 0 &&
inheritedRoles.length === 0 && (
<span className={styles.inheritedNote}>(no roles)</span>
<div className={sectionStyles.section}>
<SectionHeader>Group membership (direct only)</SectionHeader>
<div className={styles.sectionTags}>
{selected.directGroups.map((g) => (
<Tag
key={g.id}
label={g.name}
color="success"
onRemove={() => {
removeFromGroup.mutate(
{ userId: selected.userId, groupId: g.id },
{
onSuccess: () =>
toast({ title: 'Group removed', variant: 'success' }),
onError: () =>
toast({
title: 'Failed to remove group',
variant: 'error',
duration: 86_400_000,
}),
},
);
}}
/>
))}
{selected.directGroups.length === 0 && (
<span className={styles.inheritedNote}>(no groups)</span>
)}
<MultiSelect
options={availableRoles}
value={[]}
onChange={(roleIds) => {
for (const roleId of roleIds) {
assignRole.mutate(
{ userId: selected.userId, roleId },
{
onSuccess: () =>
toast({
title: 'Role assigned',
variant: 'success',
}),
onError: () =>
toast({
title: 'Failed to assign role',
variant: 'error',
duration: 86_400_000,
}),
},
);
}
}}
placeholder="+ Add"
/>
<MultiSelect
options={availableGroups}
value={[]}
onChange={(ids) => {
for (const groupId of ids) {
addToGroup.mutate(
{ userId: selected.userId, groupId },
{
onSuccess: () =>
toast({ title: 'Added to group', variant: 'success' }),
onError: () =>
toast({
title: 'Failed to add group',
variant: 'error',
duration: 86_400_000,
}),
},
);
}
}}
placeholder="+ Add"
/>
</div>
</div>
<div className={sectionStyles.section}>
<SectionHeader>
Effective roles (direct + inherited)
</SectionHeader>
<div className={styles.sectionTags}>
{selected.directRoles.map((r) => (
<Tag
key={r.id}
label={r.name}
color="warning"
onRemove={() => {
removeRole.mutate(
{ userId: selected.userId, roleId: r.id },
{
onSuccess: () =>
toast({
title: 'Role removed',
description: r.name,
variant: 'success',
}),
onError: () =>
toast({
title: 'Failed to remove role',
variant: 'error',
duration: 86_400_000,
}),
},
);
}}
/>
))}
{inheritedRoles.map((r) => (
<Badge
key={r.id}
label={`${r.name} ↑ group`}
color="warning"
variant="dashed"
className={styles.inherited}
/>
))}
{selected.directRoles.length === 0 &&
inheritedRoles.length === 0 && (
<span className={styles.inheritedNote}>(no roles)</span>
)}
<MultiSelect
options={availableRoles}
value={[]}
onChange={(roleIds) => {
for (const roleId of roleIds) {
assignRole.mutate(
{ userId: selected.userId, roleId },
{
onSuccess: () =>
toast({
title: 'Role assigned',
variant: 'success',
}),
onError: () =>
toast({
title: 'Failed to assign role',
variant: 'error',
duration: 86_400_000,
}),
},
);
}
}}
placeholder="+ Add"
/>
</div>
{inheritedRoles.length > 0 && (
<span className={styles.inheritedNote}>
Roles with are inherited through group membership
</span>
)}
</div>
{inheritedRoles.length > 0 && (
<span className={styles.inheritedNote}>
Roles with are inherited through group membership
</span>
)}
</>
) : null
}