improve superadmin settings
This commit is contained in:
@@ -36,7 +36,7 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex flex-row">
|
||||
<div class="flex flex-row space-x-1">
|
||||
<input type="email" on:keyup={handleKeyUp} placeholder="email" bind:value={email} />
|
||||
|
||||
<Toggle class="mx-2" bind:checked={is_super_admin} options={{ right: 'admin' }} />
|
||||
|
||||
@@ -6,7 +6,8 @@
|
||||
import TableCustom from '$lib/components/TableCustom.svelte'
|
||||
import PageHeader from '$lib/components/PageHeader.svelte'
|
||||
import InviteGlobalUser from '$lib/components/InviteGlobalUser.svelte'
|
||||
import { Drawer, DrawerContent } from '$lib/components/common'
|
||||
import { Alert, Badge, Drawer, DrawerContent } from '$lib/components/common'
|
||||
import ConfirmationModal from '$lib/components/common/confirmationModal/ConfirmationModal.svelte'
|
||||
|
||||
let drawer: Drawer
|
||||
export function toggleDrawer() {
|
||||
@@ -17,6 +18,9 @@
|
||||
let filteredUsers: GlobalUserInfo[] | undefined
|
||||
let userFilter = ''
|
||||
|
||||
let deleteConfirmedCallback: (() => void) | undefined = undefined
|
||||
$: openConfirmation = Boolean(deleteConfirmedCallback)
|
||||
|
||||
const fuseOptions = {
|
||||
includeScore: false,
|
||||
keys: ['email', 'name', 'company']
|
||||
@@ -30,7 +34,7 @@
|
||||
}
|
||||
|
||||
async function listUsers(): Promise<void> {
|
||||
users = await UserService.listUsersAsSuperAdmin({})
|
||||
users = await UserService.listUsersAsSuperAdmin({ perPage: 100000 })
|
||||
fuse?.setCollection(users)
|
||||
}
|
||||
|
||||
@@ -38,57 +42,100 @@
|
||||
listUsers()
|
||||
</script>
|
||||
|
||||
<Drawer bind:this={drawer} size="800px">
|
||||
<DrawerContent title="Superadmin Settings" on:close={drawer.toggleDrawer}>
|
||||
<div class="text-xs pt-2 text-gray-500 ">
|
||||
Running windmill version (backend) {version}
|
||||
</div>
|
||||
<Drawer bind:this={drawer} on:open={listUsers} size="900px">
|
||||
<DrawerContent overflow_y={false} title="Superadmin Settings" on:close={drawer.toggleDrawer}>
|
||||
<ConfirmationModal
|
||||
open={openConfirmation}
|
||||
title="Remove user"
|
||||
confirmationText="Remove"
|
||||
on:canceled={() => {
|
||||
deleteConfirmedCallback = undefined
|
||||
}}
|
||||
on:confirmed={() => {
|
||||
if (deleteConfirmedCallback) {
|
||||
deleteConfirmedCallback()
|
||||
}
|
||||
deleteConfirmedCallback = undefined
|
||||
}}
|
||||
>
|
||||
<div class="flex flex-col w-full space-y-4">
|
||||
<span>Are you sure you want to remove this user?</span>
|
||||
<Alert type="info" title="Bypass confirmation">
|
||||
<div>
|
||||
You can press
|
||||
<Badge color="dark-gray">SHIFT</Badge>
|
||||
while removing a variable to bypass confirmation.
|
||||
</div>
|
||||
</Alert>
|
||||
</div>
|
||||
</ConfirmationModal>
|
||||
|
||||
<PageHeader title="All users" primary={false} />
|
||||
<div class="flex flex-col h-full">
|
||||
<div>
|
||||
<div class="text-xs pt-2 text-gray-500 ">
|
||||
Running windmill version (backend) {version}
|
||||
</div>
|
||||
|
||||
<div class="pb-1" />
|
||||
<InviteGlobalUser on:new={listUsers} />
|
||||
<div class="pb-1" />
|
||||
<PageHeader title="All users" primary={false} />
|
||||
|
||||
<input placeholder="Search users" bind:value={userFilter} class="input mt-1" />
|
||||
<div class="overflow-x-auto border mb-4">
|
||||
<TableCustom>
|
||||
<tr slot="header-row">
|
||||
<th>email</th>
|
||||
<th>superadmin</th>
|
||||
<th>auth</th>
|
||||
<th>name</th>
|
||||
<th>company</th>
|
||||
<th />
|
||||
</tr>
|
||||
<tbody slot="body">
|
||||
{#if filteredUsers && users}
|
||||
{#each userFilter === '' ? users : filteredUsers as { email, super_admin, login_type, name, company }}
|
||||
<tr class="border">
|
||||
<td>{email}</td>
|
||||
<td>{super_admin}</td>
|
||||
<td>{login_type}</td>
|
||||
<td>{name}</td>
|
||||
<td>{company}</td>
|
||||
<td>
|
||||
<button
|
||||
class="text-blue-500"
|
||||
on:click={async () => {
|
||||
await UserService.globalUserUpdate({
|
||||
email,
|
||||
requestBody: {
|
||||
is_super_admin: !super_admin
|
||||
}
|
||||
})
|
||||
listUsers()
|
||||
}}>{super_admin ? 'demote' : 'promote'}</button
|
||||
></td
|
||||
>
|
||||
</tr>
|
||||
{/each}
|
||||
{/if}
|
||||
</tbody>
|
||||
</TableCustom>
|
||||
<div class="pb-1" />
|
||||
<InviteGlobalUser on:new={listUsers} />
|
||||
<div class="pb-1" />
|
||||
|
||||
<input placeholder="Search users" bind:value={userFilter} class="input mt-1" />
|
||||
</div>
|
||||
<div class="border mb-4 overflow-auto">
|
||||
<TableCustom>
|
||||
<tr slot="header-row" class="sticky top-0 bg-white border-b">
|
||||
<th>email</th>
|
||||
<th>superadmin</th>
|
||||
<th>auth</th>
|
||||
<th>name</th>
|
||||
<th>company</th>
|
||||
<th />
|
||||
</tr>
|
||||
<tbody slot="body" class="overflow-y-auto h-full max-h-full">
|
||||
{#if filteredUsers && users}
|
||||
{#each userFilter === '' ? users : filteredUsers as { email, super_admin, login_type, name, company }}
|
||||
<tr class="border">
|
||||
<td>{email}</td>
|
||||
<td>{super_admin ? 'yes' : ''}</td>
|
||||
<td>{login_type}</td>
|
||||
<td><span class="break-all">{name ?? ''}</span></td>
|
||||
<td><span class="break-all">{company ?? ''}</span></td>
|
||||
<td>
|
||||
<div class="flex flex-row gap-x-1">
|
||||
<button
|
||||
class="text-blue-500"
|
||||
on:click={async () => {
|
||||
await UserService.globalUserUpdate({
|
||||
email,
|
||||
requestBody: {
|
||||
is_super_admin: !super_admin
|
||||
}
|
||||
})
|
||||
listUsers()
|
||||
}}>{super_admin ? 'demote' : 'promote'}</button
|
||||
>
|
||||
|
|
||||
<button
|
||||
class="text-red-500"
|
||||
on:click={async () => {
|
||||
deleteConfirmedCallback = async () => {
|
||||
await UserService.globalUserDelete({ email })
|
||||
listUsers()
|
||||
}
|
||||
openConfirmation = true
|
||||
}}>remove</button
|
||||
>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
{/if}
|
||||
</tbody>
|
||||
</TableCustom>
|
||||
</div>
|
||||
</div>
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
-->
|
||||
<div class="flex flex-col {$$props.class} min-w-full">
|
||||
<div class="inline-block min-w-full py-2 align-middle">
|
||||
<table class="table-custom min-w-full table-auto divide-y divide-gray-300 ">
|
||||
<table class="table-custom min-w-full table-auto divide-y divide-gray-300">
|
||||
<thead>
|
||||
<slot name="header-row" />
|
||||
</thead>
|
||||
|
||||
@@ -77,27 +77,34 @@
|
||||
|
||||
<Drawer bind:this={drawer} size="800px">
|
||||
<DrawerContent title="User Settings" on:close={drawer.toggleDrawer}>
|
||||
<div class="text-xs text-gray-500 italic py-4">
|
||||
Running windmill version (backend) {version}
|
||||
</div>
|
||||
<h2 class="border-b">User info</h2>
|
||||
<div class="">
|
||||
{#if passwordError}
|
||||
<div class="text-red-600 text-2xs grow">{passwordError}</div>
|
||||
{/if}
|
||||
<div class="flex flex-col gap-2 w-full ">
|
||||
<div class="mt-4">
|
||||
<label class="block w-60 mb-2 text-gray-500">
|
||||
<div class="text-gray-700">email</div>
|
||||
<input type="text" disabled value={$usersWorkspaceStore?.email} class="input mt-1" />
|
||||
</label>
|
||||
{#if login_type == 'password'}
|
||||
<label class="block w-120">
|
||||
<div class="text-gray-700">password</div>
|
||||
<input
|
||||
type="password"
|
||||
bind:value={newPassword}
|
||||
class="
|
||||
<div class="flex flex-col h-full">
|
||||
<div>
|
||||
<div class="text-xs text-gray-500 italic pb-4">
|
||||
Running windmill version (backend) {version}
|
||||
</div>
|
||||
<h2 class="border-b">User info</h2>
|
||||
<div class="">
|
||||
{#if passwordError}
|
||||
<div class="text-red-600 text-2xs grow">{passwordError}</div>
|
||||
{/if}
|
||||
<div class="flex flex-col gap-2 w-full ">
|
||||
<div class="mt-4">
|
||||
<label class="block w-60 mb-2 text-gray-500">
|
||||
<div class="text-gray-700">email</div>
|
||||
<input
|
||||
type="text"
|
||||
disabled
|
||||
value={$usersWorkspaceStore?.email}
|
||||
class="input mt-1"
|
||||
/>
|
||||
</label>
|
||||
{#if login_type == 'password'}
|
||||
<label class="block w-120">
|
||||
<div class="text-gray-700">password</div>
|
||||
<input
|
||||
type="password"
|
||||
bind:value={newPassword}
|
||||
class="
|
||||
w-full
|
||||
block
|
||||
py-1
|
||||
@@ -109,128 +116,133 @@
|
||||
focus:border-indigo-300 focus:ring focus:ring-indigo-200 focus:ring-opacity-50
|
||||
text-sm
|
||||
"
|
||||
/>
|
||||
<Button size="sm" btnClasses="mt-4" on:click={setPassword}>Set password</Button>
|
||||
</label>
|
||||
{:else if login_type == 'github'}
|
||||
<span>Authentified through Github OAuth2. Cannot set a password.</span>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 pt-24 pb-1">
|
||||
<h2 class="py-0 my-0 border-b">Tokens</h2>
|
||||
<div class="flex justify-end border-b pb-1">
|
||||
<Button
|
||||
size="sm"
|
||||
startIcon={{ icon: faPlus }}
|
||||
btnClasses={displayCreateToken ? 'hidden' : ''}
|
||||
on:click={() => {
|
||||
displayCreateToken = !displayCreateToken
|
||||
newToken = undefined
|
||||
newTokenExpiration = undefined
|
||||
newTokenLabel = undefined
|
||||
}}
|
||||
>
|
||||
Create token
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-2xs text-gray-500 italic pb-6">
|
||||
Authentify to the Windmill API with access tokens.
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div
|
||||
class="{newToken
|
||||
? ''
|
||||
: 'hidden'} border rounded-md mb-6 px-2 py-2 bg-green-50 flex flex-row flex-wrap "
|
||||
>
|
||||
<div>
|
||||
Added token: <button on:click={() => copyToClipboard(newToken ?? '')} class="inline"
|
||||
>{truncate(newToken ?? '', 10, '****')} <Icon data={faClipboard} />
|
||||
</button>
|
||||
</div>
|
||||
<div class="pt-1 text-xs text-right">
|
||||
Make sure to copy your personal access token now. You won’t be able to see it again!
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Token creation interface -->
|
||||
<div
|
||||
class="{displayCreateToken
|
||||
? ''
|
||||
: 'hidden'} py-3 px-3 border rounded-md mb-6 bg-gray-50 min-w-min"
|
||||
>
|
||||
<h3 class="pb-3 font-semibold">Add new token</h3>
|
||||
<div class="flex flex-row flex-wrap gap-x-2 w-full justify-between">
|
||||
<div class="flex flex-col">
|
||||
<label for="label">Label <span class="text-xs text-gray-500">(optional)</span></label>
|
||||
<input type="text" bind:value={newTokenLabel} />
|
||||
</div>
|
||||
<div class="flex flex-col ">
|
||||
<label for="expires"
|
||||
>Expires on <span class="text-xs text-gray-500">(optional)</span>
|
||||
</label>
|
||||
<input
|
||||
class="block md:w-1/2"
|
||||
type="date"
|
||||
id="expires"
|
||||
name="expiration-date"
|
||||
bind:value={newTokenExpiration}
|
||||
min={getToday().getFullYear() +
|
||||
'/' +
|
||||
getToday().getMonth() +
|
||||
'/' +
|
||||
getToday().getDate()}
|
||||
max={getToday().getFullYear() +
|
||||
1 +
|
||||
'/' +
|
||||
getToday().getMonth() +
|
||||
'/' +
|
||||
getToday().getDate()}
|
||||
/>
|
||||
</div>
|
||||
<div class="flex items-end">
|
||||
<Button btnClasses="!mt-2" on:click={createToken}>Submit</Button>
|
||||
/>
|
||||
<Button size="sm" btnClasses="mt-4" on:click={setPassword}>Set password</Button>
|
||||
</label>
|
||||
{:else if login_type == 'github'}
|
||||
<span>Authentified through Github OAuth2. Cannot set a password.</span>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<TableCustom>
|
||||
<tr slot="header-row">
|
||||
<th>prefix</th>
|
||||
<th>label</th>
|
||||
<th>expiration</th>
|
||||
<th />
|
||||
</tr>
|
||||
<tbody slot="body">
|
||||
{#if tokens && tokens.length > 0}
|
||||
{#each tokens as { token_prefix, expiration, label }}
|
||||
<tr>
|
||||
<td class="grow">{token_prefix}****</td>
|
||||
<td class="grow">{label ?? ''}</td>
|
||||
<td class="grow">{displayDate(expiration ?? '')}</td>
|
||||
<td class="grow"
|
||||
><button
|
||||
class="text-red-500 text-xs underline"
|
||||
on:click={() => {
|
||||
deleteToken(token_prefix)
|
||||
}}>Delete</button
|
||||
></td
|
||||
>
|
||||
</tr>
|
||||
{/each}
|
||||
{:else if tokens && tokens.length === 0}
|
||||
<tr class="px-6"
|
||||
><td class="text-gray-700 italic text-xs"> There are no tokens yet</td></tr
|
||||
<div class="grid grid-cols-2 pt-8 pb-1">
|
||||
<h2 class="py-0 my-0 border-b">Tokens</h2>
|
||||
<div class="flex justify-end border-b pb-1">
|
||||
<Button
|
||||
size="sm"
|
||||
startIcon={{ icon: faPlus }}
|
||||
btnClasses={displayCreateToken ? 'hidden' : ''}
|
||||
on:click={() => {
|
||||
displayCreateToken = !displayCreateToken
|
||||
newToken = undefined
|
||||
newTokenExpiration = undefined
|
||||
newTokenLabel = undefined
|
||||
}}
|
||||
>
|
||||
{:else}
|
||||
<tr> Loading...</tr>
|
||||
{/if}
|
||||
</tbody>
|
||||
</TableCustom>
|
||||
Create token
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-2xs text-gray-500 italic pb-6">
|
||||
Authentify to the Windmill API with access tokens.
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div
|
||||
class="{newToken
|
||||
? ''
|
||||
: 'hidden'} border rounded-md mb-6 px-2 py-2 bg-green-50 flex flex-row flex-wrap "
|
||||
>
|
||||
<div>
|
||||
Added token: <button on:click={() => copyToClipboard(newToken ?? '')} class="inline"
|
||||
>{truncate(newToken ?? '', 10, '****')} <Icon data={faClipboard} />
|
||||
</button>
|
||||
</div>
|
||||
<div class="pt-1 text-xs text-right">
|
||||
Make sure to copy your personal access token now. You won’t be able to see it again!
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Token creation interface -->
|
||||
<div
|
||||
class="{displayCreateToken
|
||||
? ''
|
||||
: 'hidden'} py-3 px-3 border rounded-md mb-6 bg-gray-50 min-w-min"
|
||||
>
|
||||
<h3 class="pb-3 font-semibold">Add new token</h3>
|
||||
<div class="flex flex-row flex-wrap gap-x-2 w-full justify-between">
|
||||
<div class="flex flex-col">
|
||||
<label for="label"
|
||||
>Label <span class="text-xs text-gray-500">(optional)</span></label
|
||||
>
|
||||
<input type="text" bind:value={newTokenLabel} />
|
||||
</div>
|
||||
<div class="flex flex-col ">
|
||||
<label for="expires"
|
||||
>Expires on <span class="text-xs text-gray-500">(optional)</span>
|
||||
</label>
|
||||
<input
|
||||
class="block md:w-1/2"
|
||||
type="date"
|
||||
id="expires"
|
||||
name="expiration-date"
|
||||
bind:value={newTokenExpiration}
|
||||
min={getToday().getFullYear() +
|
||||
'/' +
|
||||
getToday().getMonth() +
|
||||
'/' +
|
||||
getToday().getDate()}
|
||||
max={getToday().getFullYear() +
|
||||
1 +
|
||||
'/' +
|
||||
getToday().getMonth() +
|
||||
'/' +
|
||||
getToday().getDate()}
|
||||
/>
|
||||
</div>
|
||||
<div class="flex items-end">
|
||||
<Button btnClasses="!mt-2" on:click={createToken}>Submit</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="overflow-auto">
|
||||
<TableCustom>
|
||||
<tr slot="header-row">
|
||||
<th>prefix</th>
|
||||
<th>label</th>
|
||||
<th>expiration</th>
|
||||
<th />
|
||||
</tr>
|
||||
<tbody slot="body">
|
||||
{#if tokens && tokens.length > 0}
|
||||
{#each tokens as { token_prefix, expiration, label }}
|
||||
<tr>
|
||||
<td class="grow">{token_prefix}****</td>
|
||||
<td class="grow">{label ?? ''}</td>
|
||||
<td class="grow">{displayDate(expiration ?? '')}</td>
|
||||
<td class="grow"
|
||||
><button
|
||||
class="text-red-500 text-xs underline"
|
||||
on:click={() => {
|
||||
deleteToken(token_prefix)
|
||||
}}>Delete</button
|
||||
></td
|
||||
>
|
||||
</tr>
|
||||
{/each}
|
||||
{:else if tokens && tokens.length === 0}
|
||||
<tr class="px-6"
|
||||
><td class="text-gray-700 italic text-xs"> There are no tokens yet</td></tr
|
||||
>
|
||||
{:else}
|
||||
<tr> Loading...</tr>
|
||||
{/if}
|
||||
</tbody>
|
||||
</TableCustom>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
|
||||
@@ -116,7 +116,6 @@
|
||||
background: white;
|
||||
z-index: 3;
|
||||
transition: transform var(--duration) ease;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.panel.left {
|
||||
|
||||
@@ -4,10 +4,11 @@
|
||||
import Icon from 'svelte-awesome'
|
||||
|
||||
export let title: string | undefined = undefined
|
||||
export let overflow_y = true
|
||||
const dispatch = createEventDispatcher()
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col divide-y h-screen">
|
||||
<div class="flex flex-col divide-y h-screen max-h-screen">
|
||||
<div class="flex justify-between items-center py-2 px-4">
|
||||
<span class="text-sm font-bold">{title}</span>
|
||||
<button on:click={() => dispatch('close')}>
|
||||
@@ -15,12 +16,14 @@
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="p-2 h-full overflow-y-auto">
|
||||
<div class="p-2 grow h-full max-h-full" class:overflow-y-auto={overflow_y}>
|
||||
<slot />
|
||||
</div>
|
||||
<div class="flex flex-col bg-white border-gray-200 p-2">
|
||||
<div class="flex flex-row-reverse p-2 ">
|
||||
<slot name="submission" class="sticky" />
|
||||
{#if $$slots.submission}
|
||||
<div class="flex flex-col bg-white border-gray-200 p-2">
|
||||
<div class="flex flex-row-reverse p-2 ">
|
||||
<slot name="submission" class="sticky" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -58,7 +58,7 @@
|
||||
<div class="py-1" role="none">
|
||||
<a
|
||||
on:click={() => dispatch('superadmin-settings')}
|
||||
href="#user-settings"
|
||||
href="#superadmin-settings"
|
||||
class="text-gray-700 block px-4 py-2 text-sm hover:bg-gray-100 hover:text-gray-900"
|
||||
role="menuitem"
|
||||
tabindex="-1"
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
import { page } from '$app/stores'
|
||||
import { usersWorkspaceStore, workspaceStore } from '$lib/stores'
|
||||
import CenteredModal from '$lib/components/CenteredModal.svelte'
|
||||
import { Button } from '$lib/components/common'
|
||||
|
||||
let workspace_id = $page.url.searchParams.get('workspace') ?? ''
|
||||
let username = ''
|
||||
@@ -68,8 +69,9 @@
|
||||
/>
|
||||
</label>
|
||||
<div class="flex flex-row justify-between pt-4">
|
||||
<a href="/user/workspaces">← workspaces</a>
|
||||
|
||||
<Button variant="border" size="sm" href="/user/workspaces"
|
||||
>← Back to workspaces</Button
|
||||
>
|
||||
<button
|
||||
disabled={errorUsername != '' || !username}
|
||||
class="place-items-end bg-blue-500 hover:bg-blue-700 text-white font-bold py-1 px-2 border rounded"
|
||||
|
||||
Reference in New Issue
Block a user