mirror of
https://github.com/pocket-id/pocket-id.git
synced 2026-10-11 20:29:04 +02:00
feat: add various improvements to the table component (#961)
Co-authored-by: Kyle Mendell <kmendell@ofkm.us>
This commit is contained in:
co-authored by
Kyle Mendell
parent
24ca6a106d
commit
c20e93b55c
@@ -12,22 +12,16 @@
|
||||
import ApiKeyForm from './api-key-form.svelte';
|
||||
import ApiKeyList from './api-key-list.svelte';
|
||||
|
||||
let { data } = $props();
|
||||
let apiKeys = $state(data.apiKeys);
|
||||
let apiKeysRequestOptions = $state(data.apiKeysRequestOptions);
|
||||
|
||||
const apiKeyService = new ApiKeyService();
|
||||
let expandAddApiKey = $state(false);
|
||||
let apiKeyResponse = $state<ApiKeyResponse | null>(null);
|
||||
let listRef: ApiKeyList;
|
||||
|
||||
async function createApiKey(apiKeyData: ApiKeyCreate) {
|
||||
try {
|
||||
const response = await apiKeyService.create(apiKeyData);
|
||||
apiKeyResponse = response;
|
||||
|
||||
// After creation, reload the list of API keys
|
||||
apiKeys = await apiKeyService.list(apiKeysRequestOptions);
|
||||
|
||||
listRef.refresh();
|
||||
return true;
|
||||
} catch (e) {
|
||||
axiosErrorToast(e);
|
||||
@@ -40,52 +34,46 @@
|
||||
<title>{m.api_keys()}</title>
|
||||
</svelte:head>
|
||||
|
||||
<div>
|
||||
<Card.Root>
|
||||
<Card.Header>
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<Card.Title>
|
||||
<ShieldPlus class="text-primary/80 size-5" />
|
||||
{m.create_api_key()}
|
||||
</Card.Title>
|
||||
<Card.Description
|
||||
><FormattedMessage
|
||||
m={m.add_a_new_api_key_for_programmatic_access()}
|
||||
/></Card.Description
|
||||
>
|
||||
</div>
|
||||
{#if !expandAddApiKey}
|
||||
<Button onclick={() => (expandAddApiKey = true)}>{m.add_api_key()}</Button>
|
||||
{:else}
|
||||
<Button class="h-8 p-3" variant="ghost" onclick={() => (expandAddApiKey = false)}>
|
||||
<LucideMinus class="size-5" />
|
||||
</Button>
|
||||
{/if}
|
||||
<Card.Root>
|
||||
<Card.Header>
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<Card.Title>
|
||||
<ShieldPlus class="text-primary/80 size-5" />
|
||||
{m.create_api_key()}
|
||||
</Card.Title>
|
||||
<Card.Description
|
||||
><FormattedMessage m={m.add_a_new_api_key_for_programmatic_access()} /></Card.Description
|
||||
>
|
||||
</div>
|
||||
</Card.Header>
|
||||
{#if expandAddApiKey}
|
||||
<div transition:slide>
|
||||
<Card.Content>
|
||||
<ApiKeyForm callback={createApiKey} />
|
||||
</Card.Content>
|
||||
</div>
|
||||
{/if}
|
||||
</Card.Root>
|
||||
</div>
|
||||
{#if !expandAddApiKey}
|
||||
<Button onclick={() => (expandAddApiKey = true)}>{m.add_api_key()}</Button>
|
||||
{:else}
|
||||
<Button class="h-8 p-3" variant="ghost" onclick={() => (expandAddApiKey = false)}>
|
||||
<LucideMinus class="size-5" />
|
||||
</Button>
|
||||
{/if}
|
||||
</div>
|
||||
</Card.Header>
|
||||
{#if expandAddApiKey}
|
||||
<div transition:slide>
|
||||
<Card.Content>
|
||||
<ApiKeyForm callback={createApiKey} />
|
||||
</Card.Content>
|
||||
</div>
|
||||
{/if}
|
||||
</Card.Root>
|
||||
|
||||
<div>
|
||||
<Card.Root>
|
||||
<Card.Header>
|
||||
<Card.Title>
|
||||
<ShieldEllipsis class="text-primary/80 size-5" />
|
||||
{m.manage_api_keys()}
|
||||
</Card.Title>
|
||||
</Card.Header>
|
||||
<Card.Content>
|
||||
<ApiKeyList {apiKeys} requestOptions={apiKeysRequestOptions} />
|
||||
</Card.Content>
|
||||
</Card.Root>
|
||||
</div>
|
||||
<Card.Root class="gap-0">
|
||||
<Card.Header>
|
||||
<Card.Title>
|
||||
<ShieldEllipsis class="text-primary/80 size-5" />
|
||||
{m.manage_api_keys()}
|
||||
</Card.Title>
|
||||
</Card.Header>
|
||||
<Card.Content>
|
||||
<ApiKeyList bind:this={listRef} />
|
||||
</Card.Content>
|
||||
</Card.Root>
|
||||
|
||||
<ApiKeyDialog bind:apiKeyResponse />
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
import ApiKeyService from '$lib/services/api-key-service';
|
||||
import type { SearchPaginationSortRequest } from '$lib/types/pagination.type';
|
||||
import type { PageLoad } from './$types';
|
||||
|
||||
export const load: PageLoad = async () => {
|
||||
const apiKeyService = new ApiKeyService();
|
||||
|
||||
const apiKeysRequestOptions: SearchPaginationSortRequest = {
|
||||
sort: {
|
||||
column: 'lastUsedAt',
|
||||
direction: 'desc' as const
|
||||
}
|
||||
};
|
||||
|
||||
const apiKeys = await apiKeyService.list(apiKeysRequestOptions);
|
||||
|
||||
return { apiKeys, apiKeysRequestOptions };
|
||||
};
|
||||
@@ -1,31 +1,66 @@
|
||||
<script lang="ts">
|
||||
import AdvancedTable from '$lib/components/advanced-table.svelte';
|
||||
import { openConfirmDialog } from '$lib/components/confirm-dialog';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import * as Table from '$lib/components/ui/table';
|
||||
import AdvancedTable from '$lib/components/table/advanced-table.svelte';
|
||||
import { m } from '$lib/paraglide/messages';
|
||||
import ApiKeyService from '$lib/services/api-key-service';
|
||||
import type {
|
||||
AdvancedTableColumn,
|
||||
CreateAdvancedTableActions
|
||||
} from '$lib/types/advanced-table.type';
|
||||
import type { ApiKey } from '$lib/types/api-key.type';
|
||||
import type { Paginated, SearchPaginationSortRequest } from '$lib/types/pagination.type';
|
||||
import { axiosErrorToast } from '$lib/utils/error-util';
|
||||
import { LucideBan } from '@lucide/svelte';
|
||||
import { toast } from 'svelte-sonner';
|
||||
|
||||
let {
|
||||
apiKeys,
|
||||
requestOptions
|
||||
}: {
|
||||
apiKeys: Paginated<ApiKey>;
|
||||
requestOptions: SearchPaginationSortRequest;
|
||||
} = $props();
|
||||
|
||||
const apiKeyService = new ApiKeyService();
|
||||
|
||||
let tableRef: AdvancedTable<ApiKey>;
|
||||
|
||||
export function refresh() {
|
||||
return tableRef?.refresh();
|
||||
}
|
||||
|
||||
function formatDate(dateStr: string | undefined) {
|
||||
if (!dateStr) return m.never();
|
||||
return new Date(dateStr).toLocaleString();
|
||||
}
|
||||
|
||||
const columns: AdvancedTableColumn<ApiKey>[] = [
|
||||
{ label: m.name(), column: 'name', sortable: true },
|
||||
{
|
||||
label: m.description(),
|
||||
column: 'description'
|
||||
},
|
||||
{
|
||||
label: m.expires_at(),
|
||||
column: 'expiresAt',
|
||||
sortable: true,
|
||||
value: (item) => formatDate(item.expiresAt)
|
||||
},
|
||||
{
|
||||
label: m.last_used(),
|
||||
column: 'lastUsedAt',
|
||||
sortable: true,
|
||||
value: (item) => formatDate(item.lastUsedAt)
|
||||
},
|
||||
{
|
||||
label: m.created(),
|
||||
column: 'createdAt',
|
||||
sortable: true,
|
||||
hidden: true,
|
||||
value: (item) => formatDate(item.createdAt)
|
||||
}
|
||||
];
|
||||
|
||||
const actions: CreateAdvancedTableActions<ApiKey> = (apiKey) => [
|
||||
{
|
||||
label: m.revoke(),
|
||||
icon: LucideBan,
|
||||
variant: 'danger',
|
||||
onClick: (apiKey) => revokeApiKey(apiKey)
|
||||
}
|
||||
];
|
||||
|
||||
function revokeApiKey(apiKey: ApiKey) {
|
||||
openConfirmDialog({
|
||||
title: m.revoke_api_key(),
|
||||
@@ -38,7 +73,7 @@
|
||||
action: async () => {
|
||||
try {
|
||||
await apiKeyService.revoke(apiKey.id);
|
||||
apiKeys = await apiKeyService.list(requestOptions);
|
||||
await refresh();
|
||||
toast.success(m.api_key_revoked_successfully());
|
||||
} catch (e) {
|
||||
axiosErrorToast(e);
|
||||
@@ -50,27 +85,11 @@
|
||||
</script>
|
||||
|
||||
<AdvancedTable
|
||||
items={apiKeys}
|
||||
{requestOptions}
|
||||
onRefresh={async (o) => (apiKeys = await apiKeyService.list(o))}
|
||||
id="api-key-list"
|
||||
bind:this={tableRef}
|
||||
fetchCallback={apiKeyService.list}
|
||||
defaultSort={{ column: 'lastUsedAt', direction: 'desc' }}
|
||||
withoutSearch
|
||||
columns={[
|
||||
{ label: m.name(), sortColumn: 'name' },
|
||||
{ label: m.description() },
|
||||
{ label: m.expires_at(), sortColumn: 'expiresAt' },
|
||||
{ label: m.last_used(), sortColumn: 'lastUsedAt' },
|
||||
{ label: m.actions(), hidden: true }
|
||||
]}
|
||||
>
|
||||
{#snippet rows({ item })}
|
||||
<Table.Cell>{item.name}</Table.Cell>
|
||||
<Table.Cell class="text-muted-foreground">{item.description || '-'}</Table.Cell>
|
||||
<Table.Cell>{formatDate(item.expiresAt)}</Table.Cell>
|
||||
<Table.Cell>{formatDate(item.lastUsedAt)}</Table.Cell>
|
||||
<Table.Cell class="flex justify-end">
|
||||
<Button onclick={() => revokeApiKey(item)} size="sm" variant="outline" aria-label={m.revoke()}
|
||||
><LucideBan class="size-3 text-red-500" /></Button
|
||||
>
|
||||
</Table.Cell>
|
||||
{/snippet}
|
||||
</AdvancedTable>
|
||||
{columns}
|
||||
{actions}
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user