feat: add various improvements to the table component (#961)

Co-authored-by: Kyle Mendell <kmendell@ofkm.us>
This commit is contained in:
Elias Schneider
2025-10-13 09:12:55 +00:00
committed by GitHub
co-authored by Kyle Mendell
parent 24ca6a106d
commit c20e93b55c
76 changed files with 1948 additions and 1434 deletions
@@ -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}
/>