improve superadmin settings

This commit is contained in:
Ruben Fiszel
2022-11-05 12:30:11 +01:00
parent 39351d3fb8
commit cf33af7b76
8 changed files with 264 additions and 201 deletions
@@ -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>
+151 -139
View File
@@ -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 wont 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 wont 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">&leftarrow; workspaces</a>
<Button variant="border" size="sm" href="/user/workspaces"
>&leftarrow; 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"