feat(SPA): screen to manage API keys (PART 2/3)
This commit is contained in:
1 parent
844842fdae
commit
514a082c96
30 files changed
+3844
-83
No files matched your search
@@ -0,0 +1,190 @@
|
||||
<script setup lang="ts">
|
||||
import { t } from '@/i18n'
|
||||
|
||||
import { computed } from 'vue'
|
||||
import ApiKeyStatusBadge from '@/components/ApiKeyStatusBadge.vue'
|
||||
import { API_KEY_SKELETON_ROWS, type ApiKey } from '@/composables/useApiKeys'
|
||||
import {
|
||||
creatorLabel,
|
||||
displayPrefix,
|
||||
expiresLabel,
|
||||
formatKeyDate,
|
||||
lastUsedLabel,
|
||||
} from '@/lib/apiKeys'
|
||||
|
||||
const props = defineProps<{
|
||||
keys: ApiKey[]
|
||||
loading: boolean
|
||||
showCreatedBy: boolean
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{ revoke: [key: ApiKey] }>()
|
||||
|
||||
type Row = ApiKey | { skeleton: true; id: number }
|
||||
|
||||
const skeletonRows: Row[] = Array.from({ length: API_KEY_SKELETON_ROWS }, (_, index) => ({
|
||||
skeleton: true,
|
||||
id: index,
|
||||
}))
|
||||
|
||||
const data = computed<Row[]>(() => (props.loading ? skeletonRows : props.keys))
|
||||
|
||||
function fixed(width: string) {
|
||||
return { meta: { class: { th: width, td: width } } }
|
||||
}
|
||||
|
||||
const columns = computed(() => [
|
||||
{
|
||||
id: 'name',
|
||||
get header() {
|
||||
return t('Name')
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'prefix',
|
||||
get header() {
|
||||
return t('Key')
|
||||
},
|
||||
...fixed('w-30.5'),
|
||||
},
|
||||
{
|
||||
id: 'status',
|
||||
get header() {
|
||||
return t('Status')
|
||||
},
|
||||
...fixed('w-22'),
|
||||
},
|
||||
...(props.showCreatedBy
|
||||
? [
|
||||
{
|
||||
id: 'created_by',
|
||||
get header() {
|
||||
return t('Created by')
|
||||
},
|
||||
...fixed('w-54.75'),
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
id: 'created_at',
|
||||
get header() {
|
||||
return t('Created')
|
||||
},
|
||||
...fixed('w-28'),
|
||||
},
|
||||
{
|
||||
id: 'last_used_at',
|
||||
get header() {
|
||||
return t('Last used')
|
||||
},
|
||||
...fixed('w-28'),
|
||||
},
|
||||
{
|
||||
id: 'expires_at',
|
||||
get header() {
|
||||
return t('Expires')
|
||||
},
|
||||
...fixed('w-28'),
|
||||
},
|
||||
{ id: 'actions', header: '', ...fixed('w-24.5') },
|
||||
])
|
||||
|
||||
const meta = {
|
||||
class: {
|
||||
tr: (row: { original: Row }) => {
|
||||
const original = row.original
|
||||
return 'skeleton' in original || original.status !== 'revoked' ? '' : 'bg-muted'
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
function key(row: { original: Row }) {
|
||||
return row.original as ApiKey
|
||||
}
|
||||
|
||||
function isSkeleton(row: { original: Row }) {
|
||||
return 'skeleton' in row.original
|
||||
}
|
||||
|
||||
function isRevoked(row: { original: Row }) {
|
||||
return !isSkeleton(row) && key(row).status === 'revoked'
|
||||
}
|
||||
|
||||
function mutedClass(row: { original: Row }) {
|
||||
return isRevoked(row) ? 'text-muted' : 'text-toned'
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="border border-default bg-default" :aria-busy="loading ? 'true' : undefined">
|
||||
<span v-if="loading" role="status" class="sr-only">{{ t('Loading API keys') }}</span>
|
||||
<UTable
|
||||
:data="data"
|
||||
:columns="columns"
|
||||
:meta="meta"
|
||||
:ui="{
|
||||
base: 'table-fixed',
|
||||
thead: 'bg-elevated [&>tr]:border-t-0',
|
||||
separator: 'hidden',
|
||||
th: 'h-9 px-1.5 py-0 first:pl-3 last:pr-3',
|
||||
td: 'h-12 px-1.5 py-0 first:pl-3 last:pr-3',
|
||||
tbody: 'divide-y-0',
|
||||
tr: 'border-t border-default',
|
||||
}"
|
||||
>
|
||||
<template #name-cell="{ row }">
|
||||
<USkeleton v-if="isSkeleton(row)" class="h-3 w-[60%] rounded-none bg-accented" />
|
||||
<span
|
||||
v-else
|
||||
class="block truncate font-medium"
|
||||
:class="isRevoked(row) ? 'text-muted' : 'text-highlighted'"
|
||||
>{{ key(row).name }}</span
|
||||
>
|
||||
</template>
|
||||
<template #prefix-cell="{ row }">
|
||||
<USkeleton v-if="isSkeleton(row)" class="h-3 w-[70%] rounded-none bg-accented" />
|
||||
<span v-else class="font-mono text-xs" :class="mutedClass(row)">{{
|
||||
displayPrefix(key(row).prefix)
|
||||
}}</span>
|
||||
</template>
|
||||
<template #status-cell="{ row }">
|
||||
<USkeleton v-if="isSkeleton(row)" class="h-5 w-14 rounded-none bg-accented" />
|
||||
<ApiKeyStatusBadge v-else :status="key(row).status" />
|
||||
</template>
|
||||
<template #created_by-cell="{ row }">
|
||||
<USkeleton v-if="isSkeleton(row)" class="h-3 w-[60%] rounded-none bg-accented" />
|
||||
<span v-else class="block truncate" :class="mutedClass(row)">{{
|
||||
creatorLabel(key(row))
|
||||
}}</span>
|
||||
</template>
|
||||
<template #created_at-cell="{ row }">
|
||||
<span v-if="!isSkeleton(row)" :class="mutedClass(row)">{{
|
||||
formatKeyDate(key(row).created_at, '')
|
||||
}}</span>
|
||||
</template>
|
||||
<template #last_used_at-cell="{ row }">
|
||||
<span v-if="!isSkeleton(row)" :class="mutedClass(row)">{{
|
||||
lastUsedLabel(key(row))
|
||||
}}</span>
|
||||
</template>
|
||||
<template #expires_at-cell="{ row }">
|
||||
<span v-if="!isSkeleton(row)" :class="mutedClass(row)">{{
|
||||
expiresLabel(key(row))
|
||||
}}</span>
|
||||
</template>
|
||||
<template #actions-cell="{ row }">
|
||||
<span v-if="!isSkeleton(row) && !isRevoked(row)" class="flex justify-end">
|
||||
<UButton
|
||||
color="neutral"
|
||||
variant="outline"
|
||||
size="xs"
|
||||
class="h-7 border border-accented bg-default px-2.5 text-xs font-medium text-highlighted ring-0"
|
||||
@click="emit('revoke', key(row))"
|
||||
>
|
||||
{{ t('Revoke') }}
|
||||
</UButton>
|
||||
</span>
|
||||
</template>
|
||||
</UTable>
|
||||
</div>
|
||||
</template>
|
||||
Reference in new issue
Block a user