Files
Infrastruktur-Monitoring-Sy…/frontend/src/pages/UsersPage.vue
Dennis Nemec 7e606cc884 Fix horizontal page overflow caused by wide tables
Wide tables stretched the whole page because the shell's main is a flex
item without min-width:0 and no table had a scroll container, so the
header and the summary tiles scrolled out of view. Every table now
scrolls inside its own overflow-x-auto container, the sidebar no longer
shrinks, and long CVE ids, versions, targets and image references are
kept on one line or truncated with a title tooltip.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-03 19:30:03 +02:00

181 lines
5.5 KiB
Vue

<script setup lang="ts">
import { onMounted, ref } from 'vue'
import { api, ApiError } from '../api/client'
import type { CreateUserPayload, UpdateUserPayload, User } from '../api/types'
import { useAuthStore } from '../stores/auth'
import { useToastStore } from '../stores/toast'
import UserForm from '../components/UserForm.vue'
import StatusBadge from '../components/StatusBadge.vue'
const auth = useAuthStore()
const toast = useToastStore()
const users = ref<User[]>([])
const loading = ref(true)
const busy = ref(false)
const dialog = ref<
{ mode: 'create' } | { mode: 'edit'; user: User } | { mode: 'password'; user: User } | null
>(null)
const newPassword = ref('')
async function load() {
loading.value = true
try {
users.value = await api.get<User[]>('/api/users')
} finally {
loading.value = false
}
}
onMounted(() => {
if (auth.isAdmin) load()
})
function fail(e: unknown) {
toast.error(e instanceof ApiError ? e.message : 'Request failed')
}
async function submit(payload: CreateUserPayload | UpdateUserPayload) {
if (!dialog.value || dialog.value.mode === 'password') return
busy.value = true
try {
if (dialog.value.mode === 'create') {
await api.post('/api/users', payload)
toast.success('User created')
} else {
await api.patch(`/api/users/${dialog.value.user.id}`, payload)
toast.success('User updated')
}
dialog.value = null
await load()
} catch (e) {
fail(e)
} finally {
busy.value = false
}
}
async function resetPassword() {
if (!dialog.value || dialog.value.mode !== 'password') return
busy.value = true
try {
await api.post(`/api/users/${dialog.value.user.id}/password`, { password: newPassword.value })
toast.success('Password reset')
dialog.value = null
newPassword.value = ''
} catch (e) {
fail(e)
} finally {
busy.value = false
}
}
</script>
<template>
<div v-if="!auth.isAdmin">
<h1 class="text-2xl font-semibold">Users</h1>
<p class="mt-2 text-gray-600">You do not have permission to manage users.</p>
</div>
<div v-else>
<div class="flex items-center justify-between">
<h1 class="text-2xl font-semibold">Users</h1>
<button
class="rounded-md bg-blue-600 px-4 py-2 text-sm text-white hover:bg-blue-700"
@click="dialog = { mode: 'create' }"
>
New user
</button>
</div>
<p v-if="loading" class="mt-6 text-gray-500">Loading…</p>
<div v-else data-testid="users-scroll" class="overflow-x-auto">
<table class="mt-6 w-full text-left text-sm">
<thead class="border-b border-gray-200 text-gray-500">
<tr>
<th class="py-2">Email</th>
<th>Name</th>
<th>Role</th>
<th>Status</th>
<th></th>
</tr>
</thead>
<tbody>
<tr v-for="u in users" :key="u.id" class="border-b border-gray-100">
<td class="py-2">{{ u.email }}</td>
<td>{{ u.display_name }}</td>
<td class="capitalize">{{ u.role }}</td>
<td>
<StatusBadge
:status="u.is_active ? 'ok' : 'error'"
:label="u.is_active ? 'active' : 'inactive'"
/>
</td>
<td class="space-x-3 text-right">
<button
class="text-blue-600 hover:underline"
@click="dialog = { mode: 'edit', user: u }"
>
Edit
</button>
<button
class="text-blue-600 hover:underline"
@click="dialog = { mode: 'password', user: u }"
>
Reset password
</button>
</td>
</tr>
</tbody>
</table>
</div>
<div v-if="dialog" class="fixed inset-0 flex items-center justify-center bg-black/30">
<div class="w-full max-w-md rounded-lg bg-white p-6 shadow-lg" role="dialog">
<template v-if="dialog.mode === 'password'">
<h2 class="mb-4 text-lg font-semibold">Reset password for {{ dialog.user.email }}</h2>
<form class="space-y-4" @submit.prevent="resetPassword">
<div>
<label for="np" class="block text-sm font-medium">New password</label>
<input
id="np"
v-model="newPassword"
type="password"
required
minlength="12"
class="mt-1 w-full rounded-md border border-gray-300 px-3 py-2"
/>
</div>
<div class="flex justify-end gap-2">
<button
type="button"
class="rounded-md border border-gray-300 px-4 py-2 text-sm"
@click="dialog = null"
>
Cancel
</button>
<button
type="submit"
:disabled="busy"
class="rounded-md bg-blue-600 px-4 py-2 text-sm text-white"
>
Reset
</button>
</div>
</form>
</template>
<template v-else>
<h2 class="mb-4 text-lg font-semibold">
{{ dialog.mode === 'create' ? 'New user' : 'Edit user' }}
</h2>
<UserForm
:key="dialog.mode === 'edit' ? dialog.user.id : 'new'"
:mode="dialog.mode"
:user="dialog.mode === 'edit' ? dialog.user : undefined"
:busy="busy"
@submit="submit"
@cancel="dialog = null"
/>
</template>
</div>
</div>
</div>
</template>