Files
windmill/frontend/src/lib/components/settings/TokensTable.svelte
T
dieribaandHugoCasa 5f364100f3 feat: granular token scopes (#6093)
* base

* add scopes in the UI

* remove legacy scope, unified create token into a components

* fix layout, convert old scope to new scope

* update scope

* update ui, and clear scope

* remove desc

* almsot there

* fix path

* fix delete and scope resource path matching

* update scope

* update scope and error message

* nits and fix logic

* nits and fix

* added multiple resource and jobs scope for script and flow

* add check_scope for endpoint

* clean front and improve fronetend code

* fix resource validation logic and backward compatibility with old scope

* fix frontend state and scope checks logic

* update scopes

* fix height

* nits: better_naming

* fix route

* fix add missing import

* fix import and move fn

* update repo ref

* fix import

* fix query and nits

* nits

* fix ,missing import

* fix

* revert add admin protection

* handle run action correctly

* fix

* add check scopes to some endpoint

* fix and nits

* nits

* remove unused import

* nits

* add chevron when domain is exapanded

* fix border bottom

* nits adds resource path button

* nits

* fix

* nits

* nits

* nits

* fix merge

* fix

* UI nits

* update repo ref

* add lazy static

* update scopes

---------

Co-authored-by: HugoCasa <hugo@casademont.ch>
2025-07-17 15:18:40 +00:00

139 lines
3.3 KiB
Svelte

<script lang="ts">
import TableCustom from '$lib/components/TableCustom.svelte'
import { displayDate } from '$lib/utils'
import { UserService, type TruncatedToken } from '$lib/gen'
import { createEventDispatcher } from 'svelte'
import { sendUserToast } from '$lib/toast'
import CreateToken from './CreateToken.svelte'
// --- Props ---
interface Props {
showMcpMode?: boolean
openWithMcpMode?: boolean
defaultNewTokenLabel?: string
defaultNewTokenWorkspace?: string
scopes?: string[]
}
let {
showMcpMode = false,
openWithMcpMode = false,
defaultNewTokenLabel,
defaultNewTokenWorkspace,
scopes
}: Props = $props()
// --- Local State ---
let tokens = $state<TruncatedToken[]>([])
let tokenPage = $state(1)
let newTokenLabel = $state<string | undefined>(defaultNewTokenLabel)
const dispatch = createEventDispatcher()
$effect(() => {
listTokens()
})
function handleTokenCreated(token: string) {
dispatch('tokenCreated', token)
listTokens()
}
async function handleDeleteClick(tokenPrefix: string) {
await UserService.deleteToken({ tokenPrefix })
sendUserToast('Successfully deleted token')
listTokens()
}
async function listTokens(): Promise<void> {
tokens = await UserService.listTokens({
excludeEphemeral: true,
page: tokenPage,
perPage: 100
})
}
function handleNextPage() {
tokenPage += 1
listTokens()
}
function handlePreviousPage() {
tokenPage -= 1
listTokens()
}
</script>
<div class="flex flex-col min-h-0">
<div class="text-2xs text-tertiary italic pb-2 mt-2">
Authenticate to the Windmill API with access tokens.
</div>
<CreateToken
{showMcpMode}
{openWithMcpMode}
bind:newTokenLabel
{defaultNewTokenWorkspace}
{scopes}
onTokenCreated={handleTokenCreated}
/>
<div class="overflow-auto flex-1 min-h-64">
<TableCustom>
<!-- @migration-task: migrate this slot by hand, `header-row` is an invalid identifier -->
<tr slot="header-row">
<th>prefix</th>
<th>label</th>
<th>expiration</th>
<th>scopes</th>
<th></th>
</tr>
{#snippet body()}
<tbody>
{#if tokens && tokens.length > 0}
{#each tokens as { token_prefix, expiration, label, scopes }}
<tr>
<td class="grow">{token_prefix}****</td>
<td class="grow">{label ?? ''}</td>
<td class="grow">{displayDate(expiration ?? '')}</td>
<td class="grow">{scopes?.join(', ') ?? ''}</td>
<td class="grow">
<button
class="text-red-500 text-xs underline"
onclick={() => handleDeleteClick(token_prefix)}
>
Delete
</button>
</td>
</tr>
{/each}
{:else if tokens && tokens.length === 0}
<tr class="px-6">
<td class="text-secondary italic text-xs"> There are no tokens yet</td>
</tr>
{:else}
<tr><td>Loading...</td></tr>
{/if}
</tbody>
{/snippet}
</TableCustom>
<div class="flex flex-row-reverse gap-2 w-full">
{#if tokens?.length == 100}
<button
class="p-1 underline text-sm whitespace-nowrap text-center"
onclick={handleNextPage}
>
Next
</button>
{/if}
{#if tokenPage > 1}
<button
class="p-1 underline text-sm whitespace-nowrap text-center"
onclick={handlePreviousPage}
>
Previous
</button>
{/if}
</div>
</div>
</div>