Files
windmill/frontend/src/lib/components/AddUser.svelte
2026-02-17 08:59:01 +00:00

199 lines
5.8 KiB
Svelte

<script lang="ts">
import { createEventDispatcher } from 'svelte'
import { globalEmailInvite, superadmin, workspaceStore } from '$lib/stores'
import { SettingService, UserService, WorkspaceService } from '$lib/gen'
import { Button } from './common'
import Popover from './meltComponents/Popover.svelte'
import { sendUserToast } from '$lib/toast'
import { isCloudHosted } from '$lib/cloud'
import { goto } from '$lib/navigation'
import ToggleButtonGroup from './common/toggleButton-v2/ToggleButtonGroup.svelte'
import ToggleButton from './common/toggleButton-v2/ToggleButton.svelte'
import { UserPlus } from 'lucide-svelte'
import AutocompleteSelect from './select/AutocompleteSelect.svelte'
import InputError from './InputError.svelte'
import TextInput from './text_input/TextInput.svelte'
const dispatch = createEventDispatcher()
let { workspaceEmails = [] }: { workspaceEmails?: string[] } = $props()
let email: string | undefined = $state()
let username: string | undefined = $state()
function handleKeyUp(event: KeyboardEvent) {
const key = event.key
if (key === 'Enter') {
event.preventDefault()
addUser()
}
}
let automateUsernameCreation = $state(false)
async function getAutomateUsernameCreationSetting() {
automateUsernameCreation =
((await SettingService.getGlobal({ key: 'automate_username_creation' })) as any) ?? false
}
getAutomateUsernameCreationSetting()
let allInstanceEmails: string[] | undefined = $state(undefined)
let emailsLoading = $state(!isCloudHosted())
let instanceEmails = $derived.by(() => {
if (!allInstanceEmails) return []
const workspaceSet = new Set(workspaceEmails)
return allInstanceEmails
.filter((e) => !workspaceSet.has(e))
.map((e) => ({ label: e, value: e }))
})
async function loadInstanceEmails() {
if (isCloudHosted() || !$workspaceStore) return
try {
allInstanceEmails = await UserService.listInstanceEmails({ workspace: $workspaceStore })
} catch {
allInstanceEmails = undefined
} finally {
emailsLoading = false
}
}
loadInstanceEmails()
async function addUser() {
await WorkspaceService.addUser({
workspace: $workspaceStore!,
requestBody: {
email: email!,
username: automateUsernameCreation ? undefined : username,
is_admin: selected == 'admin',
operator: selected == 'operator'
}
})
sendUserToast(`Added ${email}`)
if (!(await UserService.existsEmail({ email: email! }))) {
let isSuperadmin = $superadmin
if (!isCloudHosted()) {
const emailCopy = email!
sendUserToast(
`User ${email} is not registered yet on the instance. ${
!isSuperadmin
? `If not using SSO, ask an administrator to add ${email} to the instance`
: ''
}`,
true,
isSuperadmin
? [
{
label: 'Add user to the instance',
callback: () => {
$globalEmailInvite = emailCopy
goto('#superadmin-settings')
}
}
]
: []
)
}
}
dispatch('new')
}
let emailTouched = $state(false)
let emailError = $derived.by(() => {
if (!email || !emailTouched) return undefined
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
return emailRegex.test(email) ? undefined : 'Please enter a valid email address'
})
let selected: 'operator' | 'developer' | 'admin' = $state('developer')
</script>
<Popover placement="bottom-end">
{#snippet trigger()}
<Button variant="accent" unifiedSize="md" nonCaptureEvent={true} startIcon={{ icon: UserPlus }}>
Add new user
</Button>
{/snippet}
{#snippet content()}
<div class="flex flex-col w-72 p-4">
<span class="text-sm mb-2 leading-6 font-semibold">Add a new user</span>
<span class="text-xs mb-1 leading-6">Email</span>
{#if !isCloudHosted()}
<AutocompleteSelect
items={instanceEmails}
bind:value={email}
placeholder={emailsLoading ? 'Loading...' : 'Select or type an email'}
loading={emailsLoading}
disablePortal={true}
error={!!emailError}
onBlur={() => (emailTouched = true)}
/>
{:else}
<TextInput
inputProps={{
type: 'email',
onkeyup: handleKeyUp,
placeholder: 'email',
onblur: () => (emailTouched = true)
}}
bind:value={email}
error={!!emailError}
/>
{/if}
{#if emailError}
<InputError error={emailError} />
{/if}
{#if !automateUsernameCreation}
<span class="text-xs mb-1 pt-2 leading-6">Username</span>
<TextInput
inputProps={{ type: 'text', onkeyup: handleKeyUp, placeholder: 'username' }}
bind:value={username}
/>
{/if}
<span class="text-xs mb-1 pt-6 leading-6">Role</span>
<ToggleButtonGroup bind:selected class="mb-4">
{#snippet children({ item })}
<ToggleButton
value="operator"
label="Operator"
tooltip="An operator can only execute and view scripts/flows/apps from your workspace, and only those that he has visibility on."
{item}
/>
<ToggleButton
value="developer"
label="Developer"
tooltip="A Developer can execute and view scripts/flows/apps, but they can also create new ones and edit those they are allowed to by their path (either u/ or Writer or Admin of their folder found at /f)."
{item}
/>
<ToggleButton
value="admin"
label="Admin"
tooltip="An admin has full control over a specific Windmill workspace, including the ability to manage users, edit entities, and control permissions within the workspace."
{item}
/>
{/snippet}
</ToggleButtonGroup>
<Button
variant="accent"
size="sm"
on:click={() => {
addUser().then(() => {
// @ts-ignore
email = undefined
// @ts-ignore
username = undefined
})
}}
disabled={email === undefined ||
!!emailError ||
(!automateUsernameCreation && username === undefined)}
>
Add
</Button>
</div>
{/snippet}
</Popover>