Skip to content

Commit d00c0a5

Browse files
committed
feat: add admin statistics grid and API key reveal dialog components
1 parent e32a5d0 commit d00c0a5

18 files changed

Lines changed: 2076 additions & 0 deletions
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
<script setup lang="ts">
2+
import Button from 'primevue/button'
3+
import Dialog from 'primevue/dialog'
4+
import Message from 'primevue/message'
5+
6+
defineProps<{
7+
apiKey: { token: string; tokenPrefix: string; userName: string } | null
8+
}>()
9+
10+
const visible = defineModel<boolean>('visible', { required: true })
11+
12+
defineEmits<{
13+
copy: []
14+
}>()
15+
</script>
16+
17+
<template>
18+
<Dialog v-model:visible="visible" modal header="API Key Created" :style="{ width: '38rem' }">
19+
<div v-if="apiKey" class="flex flex-col gap-4">
20+
<Message severity="success">
21+
This token is shown only once for {{ apiKey.userName }}.
22+
</Message>
23+
24+
<div
25+
class="rounded-xl border app-border bg-neutral-500/5 px-3 py-3 mono text-sm break-all"
26+
>
27+
{{ apiKey.token }}
28+
</div>
29+
30+
<div class="flex justify-end">
31+
<Button size="small" icon="ti ti-copy" label="Copy token" @click="$emit('copy')" />
32+
</div>
33+
</div>
34+
</Dialog>
35+
</template>
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
<script setup lang="ts">
2+
import { computed } from 'vue'
3+
import Button from 'primevue/button'
4+
import type { AdminApiKeyRecord, AdminUserRecord } from '@/types/admin'
5+
6+
const props = defineProps<{
7+
apiKeys: AdminApiKeyRecord[]
8+
users: AdminUserRecord[]
9+
canManageApiKeys: boolean
10+
}>()
11+
12+
defineEmits<{
13+
remove: [apiKey: AdminApiKeyRecord]
14+
}>()
15+
16+
const userEmailMap = computed(() => new Map(props.users.map((user) => [user.id, user.email])))
17+
</script>
18+
19+
<template>
20+
<section>
21+
<div class="flex items-center justify-between gap-3 mb-3">
22+
<div>
23+
<h2 class="text-lg font-semibold">API Keys</h2>
24+
<p class="text-sm opacity-70">
25+
Use these keys for automation or external provisioning calls.
26+
</p>
27+
</div>
28+
</div>
29+
30+
<div class="rounded-2xl border app-border overflow-hidden">
31+
<div
32+
v-for="apiKey of apiKeys"
33+
:key="apiKey.id"
34+
class="grid grid-cols-[1.4fr_1fr_1fr_auto] gap-4 px-4 py-3 border-b last:border-b-0 app-border items-center"
35+
>
36+
<div>
37+
<div class="font-medium">{{ apiKey.name }}</div>
38+
<div class="text-sm opacity-65">
39+
{{ userEmailMap.get(apiKey.userId) || apiKey.userId }}
40+
</div>
41+
</div>
42+
43+
<div class="mono text-sm opacity-70">{{ apiKey.tokenPrefix }}</div>
44+
45+
<div class="text-sm opacity-70">
46+
{{
47+
apiKey.expiresAt
48+
? `Expires ${new Date(apiKey.expiresAt).toLocaleString()}`
49+
: 'No expiry'
50+
}}
51+
</div>
52+
53+
<Button
54+
v-if="canManageApiKeys"
55+
size="small"
56+
icon="ti ti-trash"
57+
text
58+
rounded
59+
severity="secondary"
60+
@click="$emit('remove', apiKey)"
61+
/>
62+
</div>
63+
</div>
64+
</section>
65+
</template>
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
<script setup lang="ts">
2+
import Button from 'primevue/button'
3+
import Tag from 'primevue/tag'
4+
import type { AdminRoleRecord } from '@/types/admin'
5+
6+
defineProps<{
7+
roles: AdminRoleRecord[]
8+
canManageRoles: boolean
9+
}>()
10+
11+
defineEmits<{
12+
create: []
13+
edit: [role: AdminRoleRecord]
14+
remove: [role: AdminRoleRecord]
15+
}>()
16+
17+
function previewPermissions(permissions: string[]) {
18+
return permissions.slice(0, 4)
19+
}
20+
</script>
21+
22+
<template>
23+
<section class="mb-8">
24+
<div class="flex items-center justify-between gap-3 mb-3">
25+
<div>
26+
<h2 class="text-lg font-semibold">Roles</h2>
27+
<p class="text-sm opacity-70">
28+
Bundle permission patterns into reusable access profiles.
29+
</p>
30+
</div>
31+
32+
<Button
33+
v-if="canManageRoles"
34+
size="small"
35+
icon="ti ti-plus"
36+
label="Add role"
37+
@click="$emit('create')"
38+
/>
39+
</div>
40+
41+
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5 gap-4">
42+
<div
43+
v-for="role of roles"
44+
:key="role.id"
45+
class="rounded-2xl border app-border px-4 py-4 flex flex-col gap-3"
46+
>
47+
<div class="flex items-start justify-between gap-3">
48+
<div>
49+
<h3 class="font-semibold">{{ role.name }}</h3>
50+
<div class="mono text-xs opacity-55 mt-1">{{ role.slug }}</div>
51+
</div>
52+
53+
<div v-if="canManageRoles" class="flex items-center gap-1">
54+
<Button
55+
size="small"
56+
icon="ti ti-edit"
57+
text
58+
rounded
59+
severity="secondary"
60+
@click="$emit('edit', role)"
61+
/>
62+
<Button
63+
size="small"
64+
icon="ti ti-trash"
65+
text
66+
rounded
67+
severity="secondary"
68+
@click="$emit('remove', role)"
69+
/>
70+
</div>
71+
</div>
72+
73+
<p class="text-sm opacity-75 min-h-[2.5rem]">
74+
{{ role.description || 'No description yet.' }}
75+
</p>
76+
77+
<div class="flex flex-wrap gap-2">
78+
<Tag
79+
v-for="permission of previewPermissions(role.permissions)"
80+
:key="permission"
81+
:value="permission"
82+
severity="secondary"
83+
/>
84+
<Tag
85+
v-if="role.permissions.length > 4"
86+
:value="`+${role.permissions.length - 4} more`"
87+
severity="contrast"
88+
/>
89+
</div>
90+
</div>
91+
</div>
92+
</section>
93+
</template>
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
<script setup lang="ts">
2+
defineProps<{
3+
usersCount: number
4+
rolesCount: number
5+
apiKeysCount: number
6+
}>()
7+
8+
const cards = [
9+
{ label: 'Users', key: 'usersCount' },
10+
{ label: 'Roles', key: 'rolesCount' },
11+
{ label: 'API keys', key: 'apiKeysCount' },
12+
] as const
13+
</script>
14+
15+
<template>
16+
<div class="grid grid-cols-3 gap-4 mb-6">
17+
<div
18+
v-for="card in cards"
19+
:key="card.key"
20+
class="rounded-2xl border app-border px-4 py-3"
21+
>
22+
<div class="text-xs uppercase tracking-[0.16em] opacity-55 mono">{{ card.label }}</div>
23+
<div class="text-3xl font-semibold mt-2">{{ $props[card.key] }}</div>
24+
</div>
25+
</div>
26+
</template>
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
<script setup lang="ts">
2+
import { computed } from 'vue'
3+
import Button from 'primevue/button'
4+
import Tag from 'primevue/tag'
5+
import type { AdminRoleRecord, AdminUserRecord } from '@/types/admin'
6+
7+
const props = defineProps<{
8+
users: AdminUserRecord[]
9+
roles: AdminRoleRecord[]
10+
canManageUsers: boolean
11+
canManageApiKeys: boolean
12+
}>()
13+
14+
defineEmits<{
15+
create: []
16+
edit: [user: AdminUserRecord]
17+
remove: [user: AdminUserRecord]
18+
createApiKey: [user: AdminUserRecord]
19+
}>()
20+
21+
const roleNameMap = computed(() => new Map(props.roles.map((role) => [role.id, role.name])))
22+
23+
function previewPermissions(permissions: string[]) {
24+
return permissions.slice(0, 4)
25+
}
26+
</script>
27+
28+
<template>
29+
<section class="mb-8">
30+
<div class="flex items-center justify-between gap-3 mb-3">
31+
<div>
32+
<h2 class="text-lg font-semibold">Users</h2>
33+
<p class="text-sm opacity-70">
34+
Manage local users, direct permissions, and role assignments.
35+
</p>
36+
</div>
37+
38+
<Button
39+
v-if="canManageUsers"
40+
size="small"
41+
icon="ti ti-user-plus"
42+
label="Add user"
43+
@click="$emit('create')"
44+
/>
45+
</div>
46+
47+
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5 gap-4">
48+
<div
49+
v-for="user of users"
50+
:key="user.id"
51+
class="rounded-2xl border app-border px-4 py-4 flex flex-col gap-3"
52+
>
53+
<div class="flex items-start justify-between gap-3">
54+
<div>
55+
<h3 class="font-semibold">{{ user.name }}</h3>
56+
<div class="text-sm opacity-70">{{ user.email }}</div>
57+
</div>
58+
59+
<Tag
60+
:value="
61+
user.disabled ? 'Disabled' : user.authProvider === 'openid' ? 'OpenID' : 'Local'
62+
"
63+
:severity="user.disabled ? 'danger' : 'secondary'"
64+
/>
65+
</div>
66+
67+
<div class="flex flex-wrap gap-2">
68+
<Tag
69+
v-for="roleId of user.roleIds"
70+
:key="roleId"
71+
:value="roleNameMap.get(roleId) || roleId"
72+
severity="info"
73+
/>
74+
</div>
75+
76+
<div class="flex flex-wrap gap-2">
77+
<Tag
78+
v-for="permission of previewPermissions(user.permissions)"
79+
:key="permission"
80+
:value="permission"
81+
severity="secondary"
82+
/>
83+
<Tag
84+
v-if="user.permissions.length > 4"
85+
:value="`+${user.permissions.length - 4} more`"
86+
severity="contrast"
87+
/>
88+
</div>
89+
90+
<div class="flex items-center gap-2 pt-1">
91+
<Button
92+
v-if="canManageUsers"
93+
icon="ti ti-edit"
94+
label="Edit"
95+
size="small"
96+
severity="secondary"
97+
@click="$emit('edit', user)"
98+
/>
99+
<Button
100+
v-if="canManageApiKeys"
101+
icon="ti ti-key"
102+
label="API key"
103+
size="small"
104+
severity="secondary"
105+
@click="$emit('createApiKey', user)"
106+
/>
107+
<Button
108+
v-if="canManageUsers"
109+
icon="ti ti-trash"
110+
label="Delete"
111+
size="small"
112+
severity="danger"
113+
text
114+
@click="$emit('remove', user)"
115+
/>
116+
</div>
117+
</div>
118+
</div>
119+
</section>
120+
</template>

0 commit comments

Comments
 (0)