mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-09-09 00:04:10 +00:00
* [ee] feat(service-accounts): allow choosing role at creation time Previously, service accounts were hardcoded to operator and could not be used as the CLI sync user since they had no write access. They also only counted as 0.5 seat each. This change: - Extends `NewServiceAccount` to accept optional `is_admin` / `operator` (defaults to `operator=true` for backward compatibility). - Exposes a role picker in `AddUser.svelte` when creating a service account (Operator / Developer / Admin). - Lets admins update a service account's role from the user list (it used to be locked to "Operator" with a tooltip). - Updates the OpenAPI spec + regenerates the frontend client. A developer/admin service account counts as 1 seat under the existing seat-cap logic (operators stay at 0.5). Companion PR on windmill-ee-private updates the `INSERT INTO usr` to honour the chosen role. Fixes WIN-1985 * [ee] feat(service-accounts): wm_deployers opt-in for Dev role When creating a service account with role=Developer, surface a toggle "Add to wm_deployers" (recommended). Members of wm_deployers can deploy on behalf of other users — the typical setup when the service account is used as the CLI sync / CI deploy identity. - `NewServiceAccount` gains an optional `add_to_deployers` flag. - Frontend defaults the toggle to on but only shows it under Developer (admins have it implicitly; operators can't deploy). - Tooltip links to docs.windmill.dev "Run on behalf of". Companion EE PR updates the handler to INSERT into usr_to_group for wm_deployers when the flag is set. Refs WIN-1985 * chore: update ee-repo-ref to 974ed42067d9f63acb42332b671b8c01ffd4b625 This commit updates the EE repository reference after PR #589 was merged in windmill-ee-private. Previous ee-repo-ref: f7dbc3cc2ba21c396f4828881e3b9d9ab6f50c69 New ee-repo-ref: 974ed42067d9f63acb42332b671b8c01ffd4b625 Automated by sync-ee-ref workflow. * [ee] fix(service-accounts): unhardcode role in superadmin user list Two review issues from the merged #9307 / #589: 1. P1 — The global Users tab in #superadmin-settings still pinned every service account to "Operator". Now it shows the actual role (Admin / Operator / Developer), derived from the SA's usr row. - `list_users_as_super_admin`: replaced `true as operator_only` with the real `operator` value, and added `is_workspace_admin` from the row (NULL for password users since their admin status is per-workspace). - `global_whoami`: when the email belongs to a service account, look up its real `operator` / `is_admin` instead of pinning to operator. - `SuperadminSettingsInner.svelte`: drop the hardcoded "Operator" badge; render Admin / Operator / Developer using the new fields, matching the workspace-level view. 2. P2 — Regenerate the bundled `openapi-deref.{yaml,json}` so the `createServiceAccount` body (now exposing `is_admin`, `operator`, `add_to_deployers`) and the new `GlobalUserInfo.is_workspace_admin` field show up at runtime in `/api/openapi.{yaml,json}`. Bumps `ee-repo-ref.txt` to the EE follow-up that adds the offline seat-cap check on `create_service_account`. Refs WIN-1985 * chore: update ee-repo-ref to b7a6068c1f3dc845e012959268b2426f0de4d697 This commit updates the EE repository reference after PR #590 was merged in windmill-ee-private. Previous ee-repo-ref: 0b1307c21d1bfd6fb43a03c2ba39d2a8bf8e6470 New ee-repo-ref: b7a6068c1f3dc845e012959268b2426f0de4d697 Automated by sync-ee-ref workflow. --------- Co-authored-by: windmill-internal-app[bot] <windmill-internal-app[bot]@users.noreply.github.com>
1284 lines
41 KiB
Svelte
1284 lines
41 KiB
Svelte
<script lang="ts">
|
|
import AddUser from '$lib/components/AddUser.svelte'
|
|
import { Alert, Badge, Button, Section, Skeleton } from '$lib/components/common'
|
|
import Popover from '$lib/components/meltComponents/Popover.svelte'
|
|
import ToggleButton from '$lib/components/common/toggleButton-v2/ToggleButton.svelte'
|
|
import ToggleButtonGroup from '$lib/components/common/toggleButton-v2/ToggleButtonGroup.svelte'
|
|
import WorkspaceOperatorSettings from '$lib/components/settings/WorkspaceOperatorSettings.svelte'
|
|
import InviteUser from '$lib/components/InviteUser.svelte'
|
|
import SettingsPageHeader from '$lib/components/settings/SettingsPageHeader.svelte'
|
|
|
|
import DataTable from '$lib/components/table/DataTable.svelte'
|
|
import Head from '$lib/components/table/Head.svelte'
|
|
import Toggle from '$lib/components/Toggle.svelte'
|
|
import Tooltip from '$lib/components/Tooltip.svelte'
|
|
import type { CancelablePromise, User, UserUsage } from '$lib/gen'
|
|
import { UserService, WorkspaceService, GroupService, type WorkspaceInvite } from '$lib/gen'
|
|
import {
|
|
userStore,
|
|
workspaceStore,
|
|
superadmin,
|
|
globalEmailInvite,
|
|
enterpriseLicense
|
|
} from '$lib/stores'
|
|
import { sendUserToast } from '$lib/toast'
|
|
import { Loader2, Mails, Search, Plus, UserMinus, X, Bot, LogIn } from 'lucide-svelte'
|
|
import Select from '$lib/components/select/Select.svelte'
|
|
import SearchItems from '../SearchItems.svelte'
|
|
import Cell from '../table/Cell.svelte'
|
|
import Row from '../table/Row.svelte'
|
|
import ConfirmationModal from '../common/confirmationModal/ConfirmationModal.svelte'
|
|
import UserOffboardingModal from '$lib/components/UserOffboardingModal.svelte'
|
|
import { isCloudHosted } from '$lib/cloud'
|
|
import { truncate } from '$lib/utils'
|
|
import { onDestroy, untrack } from 'svelte'
|
|
import { goto } from '$lib/navigation'
|
|
import { ArrowRightLeft } from 'lucide-svelte'
|
|
|
|
let offboardingUser: string | undefined = $state(undefined)
|
|
let offboardingReassignOnly = $state(false)
|
|
|
|
let users: User[] | undefined = $state(undefined)
|
|
let invites: WorkspaceInvite[] = $state([])
|
|
let filteredUsers: User[] | undefined = $state(undefined)
|
|
let userFilter = $state('')
|
|
let auto_invite_domain: string | undefined = $state()
|
|
let operatorOnly: boolean | undefined = $state(undefined)
|
|
let autoAdd: boolean | undefined = $state(false)
|
|
let nbDisplayed = $state(30)
|
|
|
|
// Instance group auto-add settings
|
|
let instanceGroups: Array<{ name: string; summary?: string; emails?: string[] }> = $state([])
|
|
let autoAddInstanceGroups: string[] = $state([])
|
|
let autoAddInstanceGroupsRoles: Record<string, string> = $state({})
|
|
|
|
// Add new instance group form state
|
|
let selectedNewInstanceGroup: string | undefined = $state(undefined)
|
|
let selectedNewRole: string | undefined = $state('developer')
|
|
|
|
// Service account creation
|
|
|
|
// Available groups for dropdowns - filter out already configured groups
|
|
let availableGroupItems = $derived(
|
|
instanceGroups
|
|
.filter((group) => !autoAddInstanceGroups.includes(group.name))
|
|
.map((group) => ({
|
|
value: group.name,
|
|
label: group.name + (group.summary ? ` - ${group.summary}` : '')
|
|
}))
|
|
)
|
|
|
|
// Sort users so manual users come first, then instance group users
|
|
let sortedUsers = $derived(() => {
|
|
const userList = (filteredUsers || users || []).slice()
|
|
return userList.sort((a: User, b: User) => {
|
|
const aIsInstanceGroup = a.added_via?.source === 'instance_group' ? 1 : 0
|
|
const bIsInstanceGroup = b.added_via?.source === 'instance_group' ? 1 : 0
|
|
return aIsInstanceGroup - bIsInstanceGroup
|
|
})
|
|
})
|
|
|
|
let hasNonManualUsers = $derived(
|
|
(filteredUsers || users || []).some(
|
|
(user: User) =>
|
|
user.added_via?.source === 'instance_group' || user.added_via?.source === 'domain'
|
|
)
|
|
)
|
|
|
|
// Function to check if a manual user can be converted to a group user
|
|
function canConvertToGroup(user: User): boolean {
|
|
// User must be manually added (not via instance group or domain)
|
|
if (user.added_via?.source === 'instance_group' || user.added_via?.source === 'domain') {
|
|
return false
|
|
}
|
|
|
|
// Check if user's email is in any configured instance group
|
|
const userEmail = user.email
|
|
for (const groupName of autoAddInstanceGroups) {
|
|
const group = instanceGroups.find((g) => g.name === groupName)
|
|
if (group && group.emails && group.emails.includes(userEmail)) {
|
|
return true
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
async function loadSettings(): Promise<void> {
|
|
const settings = await WorkspaceService.getSettings({ workspace: $workspaceStore! })
|
|
const autoInvite = settings.auto_invite as
|
|
| {
|
|
enabled?: boolean
|
|
domain?: string
|
|
operator?: boolean
|
|
mode?: string
|
|
instance_groups?: string[]
|
|
instance_groups_roles?: Record<string, string>
|
|
}
|
|
| undefined
|
|
auto_invite_domain = autoInvite?.enabled ? (autoInvite?.domain ?? '*') : undefined
|
|
operatorOnly = autoInvite?.operator ?? false
|
|
autoAdd = autoInvite?.mode === 'add'
|
|
autoAddInstanceGroups = autoInvite?.instance_groups || []
|
|
autoAddInstanceGroupsRoles = autoInvite?.instance_groups_roles || {}
|
|
}
|
|
|
|
let getUsagePromise: CancelablePromise<UserUsage[]> | undefined = undefined
|
|
|
|
let usage: Record<string, number> | undefined = $state(undefined)
|
|
|
|
async function getUsage() {
|
|
try {
|
|
getUsagePromise = UserService.listUsersUsage({ workspace: $workspaceStore! })
|
|
const res = await getUsagePromise
|
|
usage = res.reduce(
|
|
(acc, { email, executions }) => {
|
|
if (email) {
|
|
acc[email] = executions ?? 0
|
|
}
|
|
return acc
|
|
},
|
|
{} as Record<string, number>
|
|
)
|
|
} catch (e) {
|
|
console.warn(e)
|
|
}
|
|
}
|
|
|
|
async function listUsers(): Promise<void> {
|
|
users = await UserService.listUsers({ workspace: $workspaceStore! })
|
|
}
|
|
|
|
async function listInvites(): Promise<void> {
|
|
invites = await WorkspaceService.listPendingInvites({ workspace: $workspaceStore! })
|
|
}
|
|
|
|
let allowedAutoDomain = $state(false)
|
|
|
|
async function getDisallowedAutoDomain() {
|
|
allowedAutoDomain = await WorkspaceService.isDomainAllowed()
|
|
}
|
|
|
|
async function loadInstanceGroups(): Promise<void> {
|
|
try {
|
|
instanceGroups = await GroupService.listInstanceGroups()
|
|
} catch (e) {
|
|
console.warn('Failed to load instance groups:', e)
|
|
instanceGroups = []
|
|
}
|
|
}
|
|
|
|
async function saveInstanceGroupSettings(): Promise<void> {
|
|
try {
|
|
await WorkspaceService.editInstanceGroups({
|
|
workspace: $workspaceStore ?? '',
|
|
requestBody: {
|
|
groups: autoAddInstanceGroups,
|
|
roles: autoAddInstanceGroupsRoles
|
|
}
|
|
})
|
|
sendUserToast('Instance group settings saved')
|
|
// Refresh user list to show newly auto-added users
|
|
listUsers()
|
|
} catch (e) {
|
|
console.error('Failed to save instance group settings:', e)
|
|
sendUserToast('Failed to save settings', true)
|
|
}
|
|
}
|
|
|
|
async function addInstanceGroup(): Promise<void> {
|
|
if (!selectedNewInstanceGroup || !selectedNewRole) return
|
|
|
|
const groupToAdd = selectedNewInstanceGroup
|
|
const roleToAdd = selectedNewRole
|
|
|
|
try {
|
|
autoAddInstanceGroups = [...autoAddInstanceGroups, groupToAdd]
|
|
autoAddInstanceGroupsRoles[groupToAdd] = roleToAdd
|
|
|
|
// Reset form
|
|
selectedNewInstanceGroup = undefined
|
|
selectedNewRole = 'developer'
|
|
|
|
await saveInstanceGroupSettings()
|
|
} catch (e) {
|
|
// Rollback on error
|
|
autoAddInstanceGroups = autoAddInstanceGroups.filter((g) => g !== groupToAdd)
|
|
delete autoAddInstanceGroupsRoles[groupToAdd]
|
|
sendUserToast('Failed to add instance group', true)
|
|
}
|
|
}
|
|
|
|
async function removeInstanceGroup(groupName: string): Promise<void> {
|
|
const previousGroups = [...autoAddInstanceGroups]
|
|
const previousRole = autoAddInstanceGroupsRoles[groupName]
|
|
|
|
try {
|
|
autoAddInstanceGroups = autoAddInstanceGroups.filter((g) => g !== groupName)
|
|
delete autoAddInstanceGroupsRoles[groupName]
|
|
await saveInstanceGroupSettings()
|
|
} catch (e) {
|
|
// Rollback on error
|
|
autoAddInstanceGroups = previousGroups
|
|
if (previousRole) {
|
|
autoAddInstanceGroupsRoles[groupName] = previousRole
|
|
}
|
|
sendUserToast('Failed to remove instance group', true)
|
|
}
|
|
}
|
|
|
|
async function updateGroupRole(groupName: string, role: string): Promise<void> {
|
|
const previousRole = autoAddInstanceGroupsRoles[groupName]
|
|
|
|
try {
|
|
autoAddInstanceGroupsRoles[groupName] = role
|
|
await saveInstanceGroupSettings()
|
|
} catch (e) {
|
|
// Rollback on error
|
|
autoAddInstanceGroupsRoles[groupName] = previousRole
|
|
sendUserToast('Failed to update role', true)
|
|
}
|
|
}
|
|
|
|
async function convertUserToGroup(username: string): Promise<void> {
|
|
try {
|
|
await UserService.convertUserToGroup({
|
|
workspace: $workspaceStore ?? '',
|
|
username
|
|
})
|
|
sendUserToast('User converted to group user')
|
|
listUsers()
|
|
} catch (e) {
|
|
console.error('Failed to convert user:', e)
|
|
sendUserToast('Failed to convert user', true)
|
|
}
|
|
}
|
|
|
|
let domain = $derived($userStore?.email.split('@')[1])
|
|
|
|
$effect(() => {
|
|
if ($workspaceStore) {
|
|
untrack(() => {
|
|
getDisallowedAutoDomain()
|
|
listUsers()
|
|
getUsage()
|
|
listInvites()
|
|
loadSettings()
|
|
loadInstanceGroups()
|
|
})
|
|
}
|
|
})
|
|
|
|
onDestroy(() => {
|
|
try {
|
|
getUsagePromise?.cancel()
|
|
} catch (e) {
|
|
console.warn(e)
|
|
}
|
|
})
|
|
|
|
let removeInstanceGroupConfirmedCallback: (() => void) | undefined = $state(undefined)
|
|
let convertConfirmedCallback: (() => void) | undefined = $state(undefined)
|
|
|
|
// Auto-add/invite confirmation modal states
|
|
let autoAddConfirmCallback: (() => void) | undefined = $state(undefined)
|
|
let autoInviteDisableConfirmCallback: (() => void) | undefined = $state(undefined)
|
|
let switchToAutoAddConfirmCallback: (() => void) | undefined = $state(undefined)
|
|
|
|
async function removeAllInvitesFromDomain() {
|
|
await Promise.all(
|
|
invites
|
|
.filter((x) =>
|
|
isCloudHosted() ? x.email.endsWith('@' + (auto_invite_domain ?? '')) : true
|
|
)
|
|
.map(({ email, is_admin, operator }) =>
|
|
WorkspaceService.deleteInvite({
|
|
workspace: $workspaceStore ?? '',
|
|
requestBody: {
|
|
email,
|
|
is_admin,
|
|
operator
|
|
}
|
|
})
|
|
)
|
|
)
|
|
}
|
|
|
|
let nbInviteDisplayed = $state(50)
|
|
|
|
async function inviteUser(email: string, selected: 'operator' | 'developer' | 'admin') {
|
|
try {
|
|
await WorkspaceService.inviteUser({
|
|
workspace: $workspaceStore!,
|
|
requestBody: {
|
|
email,
|
|
is_admin: selected == 'admin',
|
|
operator: selected == 'operator'
|
|
}
|
|
})
|
|
sendUserToast(`Invited ${email}`)
|
|
} catch (e) {
|
|
console.error('Failed to invite user:', e)
|
|
sendUserToast('Failed to invite user', true)
|
|
}
|
|
|
|
if (!(await UserService.existsEmail({ email }))) {
|
|
let isSuperadmin = $superadmin
|
|
if (!isCloudHosted()) {
|
|
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 = email
|
|
goto('#superadmin-settings')
|
|
}
|
|
}
|
|
]
|
|
: []
|
|
)
|
|
}
|
|
}
|
|
|
|
listInvites()
|
|
}
|
|
|
|
async function updateAutoInvite(enable: boolean) {
|
|
// Cleanup invites if auto add is enabled
|
|
if (enable && autoAdd) {
|
|
await removeAllInvitesFromDomain()
|
|
}
|
|
const updateType = enable ? (autoInviteOrAddEnabled ? 'update' : 'enable') : 'disable'
|
|
try {
|
|
// await removeAllInvitesFromDomain()
|
|
await WorkspaceService.editAutoInvite({
|
|
workspace: $workspaceStore ?? '',
|
|
requestBody: enable
|
|
? {
|
|
operator: operatorOnly ?? false,
|
|
invite_all: !isCloudHosted(),
|
|
auto_add: autoAdd
|
|
}
|
|
: {
|
|
operator: undefined,
|
|
auto_add: undefined
|
|
}
|
|
})
|
|
const message =
|
|
updateType === 'update'
|
|
? `Auto-${autoAdd ? 'add' : 'invite'} updated`
|
|
: updateType === 'enable'
|
|
? `Auto-${autoAdd ? 'add' : 'invite'} enabled`
|
|
: `Auto-${autoAdd ? 'add' : 'invite'} disabled`
|
|
sendUserToast(message)
|
|
} catch (e) {
|
|
console.error('Failed to update auto invite:', e)
|
|
sendUserToast('Failed to update auto invite', true)
|
|
}
|
|
loadSettings()
|
|
listInvites()
|
|
listUsers()
|
|
}
|
|
|
|
const autoInviteOrAddEnabled = $derived(auto_invite_domain != undefined)
|
|
|
|
// Legacy auto-invite: user already has auto-invite enabled (not auto-add) on a non-cloud instance
|
|
// This preserves their existing setup even though auto-invite is deprecated for new setups
|
|
const isLegacyAutoInvite = $derived(autoInviteOrAddEnabled && !autoAdd && !isCloudHosted())
|
|
|
|
// Show auto-invite toggle only for:
|
|
// - Cloud hosted users (always available)
|
|
// - Legacy users who already have auto-invite enabled (preserve existing setup)
|
|
const showAutoInviteToggle = $derived(isCloudHosted() || isLegacyAutoInvite)
|
|
|
|
// Display mode for labels: for non-cloud, non-legacy users, always show "add"
|
|
// For cloud or legacy users, show based on actual autoAdd setting
|
|
const displayMode = $derived.by(() => {
|
|
if (!isCloudHosted() && !isLegacyAutoInvite) {
|
|
return 'add'
|
|
}
|
|
return autoAdd ? 'add' : 'invite'
|
|
})
|
|
|
|
const isAdminsWorkspaceWithoutEE = $derived($workspaceStore === 'admins' && !$enterpriseLicense)
|
|
</script>
|
|
|
|
<SearchItems
|
|
filter={userFilter}
|
|
items={users}
|
|
bind:filteredItems={filteredUsers}
|
|
f={(x) => x.email + ' ' + x.name + ' ' + x.company}
|
|
/>
|
|
|
|
<SettingsPageHeader
|
|
title="Members {(filteredUsers?.length ?? users?.length) != undefined
|
|
? `(${filteredUsers?.length ?? users?.length})`
|
|
: ''}"
|
|
description="Add members to your workspace and manage their roles. You can also auto-add users to join your workspace."
|
|
link="https://www.windmill.dev/docs/core_concepts/roles_and_permissions"
|
|
/>
|
|
|
|
{#if isAdminsWorkspaceWithoutEE}
|
|
<Alert type="info" title="Admins workspace">
|
|
The admins workspace is reserved for superadmins. Only users with superadmin privileges can
|
|
access it. Members cannot be manually added or invited to this workspace.
|
|
</Alert>
|
|
{/if}
|
|
|
|
<Section>
|
|
{#snippet action()}
|
|
<div class="flex flex-row items-center gap-2 relative whitespace-nowrap w-full">
|
|
<input placeholder="Filter members" bind:value={userFilter} class="input !pl-8 !w-56" />
|
|
<Search class="absolute left-2" size={14} />
|
|
|
|
{#if !isAdminsWorkspaceWithoutEE}
|
|
<Popover
|
|
floatingConfig={{ strategy: 'absolute', placement: 'bottom-end' }}
|
|
usePointerDownOutside
|
|
>
|
|
{#snippet trigger()}
|
|
<Button
|
|
variant="default"
|
|
unifiedSize="md"
|
|
nonCaptureEvent={true}
|
|
startIcon={{ icon: Mails }}
|
|
>Auto-{displayMode}: {autoInviteOrAddEnabled ? 'ON' : 'OFF'}
|
|
</Button>
|
|
{/snippet}
|
|
{#snippet content()}
|
|
<div class="flex flex-col items-start p-4 min-w-[320px] max-w-sm">
|
|
{#if showAutoInviteToggle}
|
|
<div class="text-xs mb-1 text-primary"
|
|
>Mode <Tooltip>Whether to invite or add users directly to the workspace.</Tooltip>
|
|
</div>
|
|
<ToggleButtonGroup
|
|
selected={displayMode}
|
|
on:selected={async (e) => {
|
|
const switchingToAdd = e.detail === 'add' && !autoAdd
|
|
|
|
// If switching from invite to add on non-cloud, show confirmation with warning
|
|
if (switchingToAdd && isLegacyAutoInvite) {
|
|
switchToAutoAddConfirmCallback = async () => {
|
|
autoAdd = true
|
|
if (autoInviteOrAddEnabled) {
|
|
await updateAutoInvite(true)
|
|
}
|
|
}
|
|
} else {
|
|
autoAdd = e.detail === 'add'
|
|
if (autoInviteOrAddEnabled) {
|
|
await updateAutoInvite(true)
|
|
}
|
|
}
|
|
}}
|
|
>
|
|
{#snippet children({ item })}
|
|
<ToggleButton value="invite" small label="Auto-invite" {item} />
|
|
<ToggleButton value="add" small label="Auto-add" {item} />
|
|
{/snippet}
|
|
</ToggleButtonGroup>
|
|
|
|
{#if isLegacyAutoInvite && !autoAdd}
|
|
<div class="mt-3 w-full">
|
|
<Alert type="warning" size="xs" title="Legacy mode">
|
|
Auto-invite is deprecated. Switching to auto-add will permanently disable
|
|
auto-invite for this workspace.
|
|
</Alert>
|
|
</div>
|
|
{/if}
|
|
|
|
<div class="mt-6"></div>
|
|
{/if}
|
|
|
|
<span class="text-xs mb-1">Role <Tooltip>Role of the auto-added users</Tooltip></span>
|
|
<ToggleButtonGroup
|
|
selected={operatorOnly ? 'operator' : 'developer'}
|
|
on:selected={async (e) => {
|
|
operatorOnly = e.detail === 'operator'
|
|
if (auto_invite_domain != undefined) {
|
|
await updateAutoInvite(true)
|
|
}
|
|
}}
|
|
>
|
|
{#snippet children({ item })}
|
|
<ToggleButton
|
|
value="operator"
|
|
small
|
|
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"
|
|
small
|
|
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}
|
|
/>
|
|
{/snippet}
|
|
</ToggleButtonGroup>
|
|
|
|
<div class="mt-6">
|
|
<Toggle
|
|
checked={autoInviteOrAddEnabled}
|
|
on:change={async (e) => {
|
|
const enabling = e.detail
|
|
|
|
if (enabling) {
|
|
// Non-cloud users without legacy auto-invite: force auto-add mode
|
|
if (!isCloudHosted() && !isLegacyAutoInvite) {
|
|
autoAdd = true
|
|
}
|
|
|
|
// Show confirmation when enabling auto-add
|
|
if (autoAdd || (!isCloudHosted() && !showAutoInviteToggle)) {
|
|
autoAddConfirmCallback = async () => {
|
|
await updateAutoInvite(true)
|
|
}
|
|
} else {
|
|
await updateAutoInvite(true)
|
|
}
|
|
} else {
|
|
// Disabling: show confirmation if currently using auto-invite (legacy)
|
|
if (isLegacyAutoInvite) {
|
|
autoInviteDisableConfirmCallback = async () => {
|
|
await updateAutoInvite(false)
|
|
}
|
|
} else {
|
|
await updateAutoInvite(false)
|
|
}
|
|
}
|
|
}}
|
|
disabled={isCloudHosted() && !allowedAutoDomain}
|
|
options={{
|
|
right: isCloudHosted()
|
|
? `Auto-${displayMode} anyone from ${
|
|
autoInviteOrAddEnabled ? auto_invite_domain : domain
|
|
}`
|
|
: `Auto-${displayMode} anyone joining the instance`
|
|
}}
|
|
/>
|
|
</div>
|
|
{#if isCloudHosted() && !allowedAutoDomain}
|
|
<div class="text-red-400 text-xs">{domain} domain not allowed for auto-add</div>
|
|
{/if}
|
|
</div>
|
|
{/snippet}
|
|
</Popover>
|
|
|
|
{#if instanceGroups.length > 0}
|
|
<Popover
|
|
floatingConfig={{ strategy: 'absolute', placement: 'bottom-end' }}
|
|
usePointerDownOutside
|
|
>
|
|
{#snippet trigger()}
|
|
<Button
|
|
color={autoAddInstanceGroups.length > 0 ? 'green' : 'gray'}
|
|
variant="border"
|
|
size="xs"
|
|
nonCaptureEvent={true}
|
|
startIcon={{ icon: Mails }}
|
|
>Instance groups: {autoAddInstanceGroups.length}
|
|
</Button>
|
|
{/snippet}
|
|
{#snippet content()}
|
|
<div class="flex flex-col p-4 min-w-[500px]">
|
|
<div class="flex flex-col gap-4">
|
|
<span class="text-sm leading-6 font-semibold"> Auto-add instance groups </span>
|
|
|
|
<!-- Add new instance group form -->
|
|
{#if availableGroupItems.length > 0}
|
|
<div class="flex w-full mt-1 gap-2 items-end justify-between">
|
|
<div class="flex gap-2 items-end">
|
|
<div class="flex flex-col gap-1">
|
|
<span class="text-xs text-primary">Instance group</span>
|
|
<Select
|
|
items={availableGroupItems}
|
|
placeholder="Select group"
|
|
bind:value={selectedNewInstanceGroup}
|
|
class="max-w-[160px]"
|
|
disablePortal={true}
|
|
/>
|
|
</div>
|
|
|
|
<div class="flex flex-col gap-1">
|
|
<span class="text-xs text-primary">Role</span>
|
|
<ToggleButtonGroup
|
|
selected={selectedNewRole}
|
|
on:selected={(e) => {
|
|
selectedNewRole = e.detail
|
|
}}
|
|
>
|
|
{#snippet children({ item })}
|
|
<ToggleButton
|
|
value="operator"
|
|
small
|
|
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"
|
|
small
|
|
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"
|
|
small
|
|
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>
|
|
</div>
|
|
</div>
|
|
|
|
<Button
|
|
color="blue"
|
|
size="xs"
|
|
startIcon={{ icon: Plus }}
|
|
disabled={!selectedNewInstanceGroup || !selectedNewRole}
|
|
onclick={addInstanceGroup}
|
|
>
|
|
Add
|
|
</Button>
|
|
</div>
|
|
{/if}
|
|
|
|
<!-- Configured groups table -->
|
|
{#if autoAddInstanceGroups.length > 0}
|
|
<div class="flex flex-col gap-2">
|
|
<p class="text-sm font-medium text-secondary">Configured groups:</p>
|
|
<div class="flex flex-col gap-1">
|
|
<table class="w-full text-sm">
|
|
<thead>
|
|
<tr class="text-left text-xs text-primary">
|
|
<th class="pb-2 w-1/2">Group</th>
|
|
<th class="pb-2 w-1/4">Role</th>
|
|
<th class="pb-2 w-1/4"></th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{#each autoAddInstanceGroups as groupName (groupName)}
|
|
{@const group = instanceGroups.find((g) => g.name === groupName)}
|
|
<tr class="border-t border-gray-200 dark:border-gray-700">
|
|
<td class="py-2">
|
|
<div class="font-medium">{groupName}</div>
|
|
{#if group?.summary}
|
|
<div class="text-xs text-primary">{group.summary}</div>
|
|
{/if}
|
|
</td>
|
|
<td class="py-2">
|
|
<div>
|
|
<ToggleButtonGroup
|
|
selected={autoAddInstanceGroupsRoles[groupName] ||
|
|
'developer'}
|
|
on:selected={async (e) => {
|
|
autoAddInstanceGroupsRoles[groupName] = e.detail
|
|
await updateGroupRole(groupName, e.detail)
|
|
}}
|
|
>
|
|
{#snippet children({ item })}
|
|
<ToggleButton
|
|
value="operator"
|
|
small
|
|
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"
|
|
small
|
|
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"
|
|
small
|
|
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>
|
|
</div>
|
|
</td>
|
|
<td class="py-2">
|
|
<div class="flex justify-end">
|
|
<Button
|
|
color="light"
|
|
variant="contained"
|
|
btnClasses="text-red-500"
|
|
size="xs"
|
|
spacingSize="xs2"
|
|
onclick={() => {
|
|
removeInstanceGroupConfirmedCallback = async () => {
|
|
await removeInstanceGroup(groupName)
|
|
}
|
|
}}
|
|
>
|
|
Remove
|
|
</Button>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
{/each}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
{:else}
|
|
<div class="text-center text-primary text-sm py-4">
|
|
No instance groups configured for auto-add
|
|
</div>
|
|
{/if}
|
|
</div>
|
|
</div>
|
|
{/snippet}
|
|
</Popover>
|
|
{/if}
|
|
|
|
{#if showAutoInviteToggle}
|
|
<InviteUser {inviteUser} />
|
|
{/if}
|
|
|
|
<AddUser
|
|
on:new={() => {
|
|
listUsers()
|
|
listInvites()
|
|
}}
|
|
/>
|
|
{/if}
|
|
</div>
|
|
{/snippet}
|
|
|
|
<DataTable
|
|
shouldLoadMore={(filteredUsers?.length ?? 0) > 30}
|
|
loadMore={30}
|
|
on:loadMore={() => {
|
|
nbDisplayed += 30
|
|
}}
|
|
>
|
|
<Head>
|
|
<tr>
|
|
<Cell head first>Email</Cell>
|
|
<Cell head>Username</Cell>
|
|
{#if hasNonManualUsers}
|
|
<Cell head>
|
|
Added via
|
|
<Tooltip>
|
|
Shows how the user was added to the workspace: manually, via domain auto-invite, or
|
|
through an instance group.
|
|
</Tooltip>
|
|
</Cell>
|
|
{/if}
|
|
|
|
<Cell head>
|
|
Executions (<abbr title="past 1w">1w</abbr>)
|
|
<Tooltip>
|
|
An execution is calculated as 1 for any runs of scripts + 1 for each seconds above the
|
|
first one
|
|
</Tooltip>
|
|
</Cell>
|
|
<Cell head>Role</Cell>
|
|
<Cell head>Enabled</Cell>
|
|
<Cell head last>
|
|
<span class="sr-only">Actions</span>
|
|
</Cell>
|
|
</tr>
|
|
</Head>
|
|
<tbody>
|
|
{#if filteredUsers}
|
|
{#each sortedUsers().slice(0, nbDisplayed) as user, index (user.email)}
|
|
{@const { email, username, is_admin, operator, disabled, added_via } = user}
|
|
<!-- Add separator between manual users and instance group users -->
|
|
{#if hasNonManualUsers && index > 0 && sortedUsers()[index - 1]?.added_via?.source !== 'instance_group' && added_via?.source === 'instance_group'}
|
|
<tr class="bg-surface-secondary">
|
|
<td colspan={hasNonManualUsers ? 8 : 7} class="px-4 py-2">
|
|
<div class="text-xs text-emphasis font-semibold"> Instance group users </div>
|
|
</td>
|
|
</tr>
|
|
{/if}
|
|
<tr class={index % 2 === 0 ? 'bg-surface-tertiary' : 'bg-surface'}>
|
|
<Cell first>
|
|
{#if user.is_service_account}
|
|
<span class="flex items-center gap-1.5 max-w-[150px]" title={email}>
|
|
<Bot size={16} class="text-blue-500 shrink-0" />
|
|
<span class="truncate">{email}</span>
|
|
</span>
|
|
{:else}
|
|
<a href="mailto:{email}" class="block truncate max-w-[150px]" title={email}
|
|
>{email}</a
|
|
>
|
|
{/if}
|
|
</Cell>
|
|
<Cell
|
|
><span class="block truncate max-w-[120px]" title={username}>{username}</span></Cell
|
|
>
|
|
{#if hasNonManualUsers}
|
|
<Cell>
|
|
<div class="flex items-center gap-2">
|
|
{#if added_via?.source === 'instance_group'}
|
|
<Badge color="blue">Group</Badge>
|
|
<span>{truncate(added_via.group || 'Unknown', 20)}</span>
|
|
{:else if added_via?.source === 'domain'}
|
|
<Badge color="blue">Auto-add</Badge>
|
|
{:else}
|
|
<Badge color="blue">Manual</Badge>
|
|
{/if}
|
|
</div>
|
|
</Cell>
|
|
{/if}
|
|
<Cell
|
|
>{#if usage != undefined}{usage[email] ?? 0}{:else}<Loader2
|
|
size={14}
|
|
class="animate-spin"
|
|
/>{/if}</Cell
|
|
>
|
|
<Cell>
|
|
<div>
|
|
{#if added_via?.source === 'instance_group'}
|
|
<div class="flex items-center gap-1">
|
|
<span class="rounded-md text-xs px-2 py-1 bg-surface shadow-md font-bold">
|
|
{is_admin ? 'Admin' : operator ? 'Operator' : 'Developer'}
|
|
</span>
|
|
<Tooltip>Role is managed through instance group configuration above.</Tooltip>
|
|
</div>
|
|
{:else}
|
|
<ToggleButtonGroup
|
|
selected={is_admin ? 'admin' : operator ? 'operator' : 'developer'}
|
|
on:selected={async (e) => {
|
|
if (is_admin && email == $userStore?.email && e.detail != 'admin') {
|
|
sendUserToast(
|
|
'Admins cannot be demoted by themselves, ask another admin to demote you',
|
|
true
|
|
)
|
|
e.preventDefault()
|
|
listUsers()
|
|
return
|
|
}
|
|
const body =
|
|
e.detail == 'admin'
|
|
? { is_admin: true, operator: false }
|
|
: e.detail == 'operator'
|
|
? { is_admin: false, operator: true }
|
|
: { is_admin: false, operator: false }
|
|
await UserService.updateUser({
|
|
workspace: $workspaceStore ?? '',
|
|
username,
|
|
requestBody: body
|
|
})
|
|
listUsers()
|
|
}}
|
|
>
|
|
{#snippet children({ item })}
|
|
<ToggleButton
|
|
value="operator"
|
|
small
|
|
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"
|
|
small
|
|
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"
|
|
small
|
|
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>
|
|
{/if}
|
|
</div>
|
|
</Cell>
|
|
<Cell>
|
|
<Toggle
|
|
checked={!disabled}
|
|
on:change={async (e) => {
|
|
try {
|
|
await UserService.updateUser({
|
|
workspace: $workspaceStore ?? '',
|
|
username,
|
|
requestBody: {
|
|
disabled: !disabled
|
|
}
|
|
})
|
|
sendUserToast(`User ${username} ${disabled ? 'enabled' : 'disabled'}`)
|
|
listUsers()
|
|
} catch (e) {
|
|
console.error('Failed to update user status:', e)
|
|
sendUserToast('Failed to update user status', true)
|
|
}
|
|
}}
|
|
size="xs"
|
|
/>
|
|
</Cell>
|
|
<Cell>
|
|
<div class="flex gap-1">
|
|
{#if user.is_service_account && $userStore?.is_admin}
|
|
<Button
|
|
unifiedSize="sm"
|
|
variant="default"
|
|
startIcon={{ icon: LogIn }}
|
|
disabled={!$enterpriseLicense}
|
|
title={!$enterpriseLicense ? 'Requires Enterprise Edition' : undefined}
|
|
onClick={async () => {
|
|
try {
|
|
// Backend sets the impersonation cookie and returns the old token
|
|
const oldToken = await UserService.impersonateServiceAccount({
|
|
workspace: $workspaceStore ?? '',
|
|
requestBody: { username }
|
|
})
|
|
if (oldToken) {
|
|
sessionStorage.setItem('pre_impersonation_token', oldToken)
|
|
sessionStorage.setItem('pre_impersonation_email', $userStore?.email ?? '')
|
|
}
|
|
window.location.href = '/'
|
|
} catch (e) {
|
|
sendUserToast('Failed to impersonate service account', true)
|
|
}
|
|
}}
|
|
>
|
|
Impersonate
|
|
</Button>
|
|
{/if}
|
|
{#snippet removeUserButton(disabled: boolean)}
|
|
<Button
|
|
unifiedSize="sm"
|
|
variant="subtle"
|
|
destructive
|
|
{disabled}
|
|
onClick={() => {
|
|
offboardingUser = username
|
|
offboardingReassignOnly = false
|
|
}}
|
|
startIcon={{ icon: UserMinus }}
|
|
>
|
|
Remove
|
|
</Button>
|
|
{/snippet}
|
|
|
|
{#if added_via?.source === 'instance_group'}
|
|
<div class="flex items-center gap-1">
|
|
{@render removeUserButton(true)}
|
|
<Tooltip
|
|
>Cannot remove users synced from instance groups. Either disable the user or
|
|
remove them from the SCIM group.</Tooltip
|
|
>
|
|
</div>
|
|
{:else if canConvertToGroup(user)}
|
|
<Button
|
|
variant="accent"
|
|
unifiedSize="sm"
|
|
on:click={() => {
|
|
convertConfirmedCallback = async () => {
|
|
await convertUserToGroup(username)
|
|
}
|
|
}}
|
|
>
|
|
Convert
|
|
</Button>
|
|
{:else}
|
|
<div class="flex items-center gap-1">
|
|
<Button
|
|
unifiedSize="sm"
|
|
variant="subtle"
|
|
onClick={() => {
|
|
offboardingUser = username
|
|
offboardingReassignOnly = true
|
|
}}
|
|
startIcon={{ icon: ArrowRightLeft }}
|
|
>
|
|
Reassign
|
|
</Button>
|
|
{@render removeUserButton(false)}
|
|
</div>
|
|
{/if}
|
|
</div>
|
|
</Cell>
|
|
</tr>
|
|
{/each}
|
|
{:else}
|
|
{#each new Array(6) as _}
|
|
<tr class="border">
|
|
<td colspan={6}>
|
|
<Skeleton layout={[[4]]} />
|
|
</td>
|
|
</tr>
|
|
{/each}
|
|
{/if}
|
|
</tbody>
|
|
</DataTable>
|
|
</Section>
|
|
|
|
<div class="pt-12"></div>
|
|
|
|
<WorkspaceOperatorSettings />
|
|
|
|
<div class="pt-12"></div>
|
|
|
|
{#if invites?.length > 0}
|
|
<Section
|
|
label="Invites ({invites.length ?? ''})"
|
|
tooltip="Manage invites on your workspace."
|
|
documentationLink="https://www.windmill.dev/docs/core_concepts/authentification#adding-users-to-a-workspace"
|
|
>
|
|
{#snippet action()}
|
|
{#if showAutoInviteToggle && !isAdminsWorkspaceWithoutEE}
|
|
<div class="flex gap-2 items-center">
|
|
<InviteUser {inviteUser} />
|
|
</div>
|
|
{/if}
|
|
{/snippet}
|
|
<DataTable>
|
|
<Head>
|
|
<tr>
|
|
<Cell head first>Email</Cell>
|
|
<Cell head>Role</Cell>
|
|
<Cell head last><span class="sr-only">Actions</span></Cell>
|
|
</tr>
|
|
</Head>
|
|
<tbody class="divide-y bg-surface">
|
|
{#if invites?.length > 0}
|
|
{#each invites.slice(0, nbInviteDisplayed) as { email, is_admin, operator }}
|
|
<Row>
|
|
<Cell first>{email}</Cell>
|
|
<Cell>
|
|
<div>
|
|
<ToggleButtonGroup
|
|
selected={is_admin ? 'admin' : operator ? 'operator' : 'developer'}
|
|
on:selected={async (e) => {
|
|
const body =
|
|
e.detail == 'admin'
|
|
? { is_admin: true, operator: false }
|
|
: e.detail == 'operator'
|
|
? { is_admin: false, operator: true }
|
|
: { is_admin: false, operator: false }
|
|
await WorkspaceService.inviteUser({
|
|
workspace: $workspaceStore ?? '',
|
|
requestBody: {
|
|
email,
|
|
...body
|
|
}
|
|
})
|
|
listUsers()
|
|
}}
|
|
>
|
|
{#snippet children({ item })}
|
|
<ToggleButton
|
|
value="operator"
|
|
small
|
|
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"
|
|
small
|
|
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"
|
|
small
|
|
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>
|
|
</div>
|
|
</Cell>
|
|
<Cell last>
|
|
<Button
|
|
variant="default"
|
|
destructive
|
|
unifiedSize="sm"
|
|
startIcon={{ icon: X }}
|
|
btnClasses="w-fit"
|
|
onClick={async () => {
|
|
await WorkspaceService.deleteInvite({
|
|
workspace: $workspaceStore ?? '',
|
|
requestBody: {
|
|
email,
|
|
is_admin,
|
|
operator
|
|
}
|
|
})
|
|
listInvites()
|
|
}}
|
|
>
|
|
Cancel invite
|
|
</Button>
|
|
</Cell>
|
|
</Row>
|
|
{/each}
|
|
{:else}
|
|
<tr>
|
|
<td colspan="3" class="text-center py-8">
|
|
<div class="text-xs text-secondary"> No invites yet </div>
|
|
</td>
|
|
</tr>
|
|
{/if}
|
|
</tbody>
|
|
</DataTable>
|
|
{#if invites && invites?.length > 50 && nbInviteDisplayed < invites.length}
|
|
<span class="text-xs"
|
|
>{nbInviteDisplayed} invites out of {invites.length}
|
|
<button class="ml-4" onclick={() => (nbInviteDisplayed += 50)}>load 50 more</button></span
|
|
>
|
|
{/if}
|
|
</Section>
|
|
{/if}
|
|
{#if offboardingUser}
|
|
<UserOffboardingModal
|
|
open={offboardingUser != null}
|
|
username={offboardingUser}
|
|
reassignOnly={offboardingReassignOnly}
|
|
onClose={() => {
|
|
offboardingUser = undefined
|
|
}}
|
|
onComplete={() => {
|
|
offboardingUser = undefined
|
|
listUsers()
|
|
}}
|
|
/>
|
|
{/if}
|
|
|
|
<div class="[&>div]:!z-[5002]">
|
|
<ConfirmationModal
|
|
open={Boolean(removeInstanceGroupConfirmedCallback)}
|
|
title="Remove instance group"
|
|
confirmationText="Remove"
|
|
on:canceled={() => {
|
|
removeInstanceGroupConfirmedCallback = undefined
|
|
}}
|
|
on:confirmed={() => {
|
|
if (removeInstanceGroupConfirmedCallback) {
|
|
removeInstanceGroupConfirmedCallback()
|
|
}
|
|
removeInstanceGroupConfirmedCallback = undefined
|
|
}}
|
|
>
|
|
<div class="flex flex-col w-full space-y-4">
|
|
<span
|
|
>Are you sure you want to remove this instance group from auto-add? This will not remove
|
|
users already added from this group.</span
|
|
>
|
|
</div>
|
|
</ConfirmationModal>
|
|
</div>
|
|
|
|
<ConfirmationModal
|
|
open={Boolean(convertConfirmedCallback)}
|
|
title="Convert to Group User"
|
|
confirmationText="Convert"
|
|
on:canceled={() => {
|
|
convertConfirmedCallback = undefined
|
|
}}
|
|
on:confirmed={() => {
|
|
if (convertConfirmedCallback) {
|
|
convertConfirmedCallback()
|
|
}
|
|
convertConfirmedCallback = undefined
|
|
}}
|
|
>
|
|
<div class="flex flex-col w-full space-y-4">
|
|
<span>Are you sure you want to convert this user to a group user?</span>
|
|
<span class="text-sm text-secondary">This will:</span>
|
|
<ul class="text-sm text-secondary list-disc ml-4 space-y-1">
|
|
<li>Change the user's role based on their instance group configuration</li>
|
|
<li>Make their role managed through the instance group settings</li>
|
|
<li>Prevent manual role changes for this user</li>
|
|
</ul>
|
|
</div>
|
|
</ConfirmationModal>
|
|
|
|
<!-- Auto-add/invite confirmation modals - z-index to appear above popover -->
|
|
<div class="[&>div]:!z-[5002]">
|
|
<ConfirmationModal
|
|
open={Boolean(autoAddConfirmCallback)}
|
|
title="Enable Auto-add"
|
|
confirmationText="Enable"
|
|
on:canceled={() => {
|
|
autoAddConfirmCallback = undefined
|
|
}}
|
|
on:confirmed={() => {
|
|
if (autoAddConfirmCallback) {
|
|
autoAddConfirmCallback()
|
|
}
|
|
autoAddConfirmCallback = undefined
|
|
}}
|
|
>
|
|
Are you sure you want to enable auto-add?<br />
|
|
Anyone added to the instance will automatically join this workspace.
|
|
</ConfirmationModal>
|
|
</div>
|
|
|
|
<div class="[&>div]:!z-[5002]">
|
|
<ConfirmationModal
|
|
open={Boolean(autoInviteDisableConfirmCallback)}
|
|
title="Disable Auto-invite"
|
|
confirmationText="Disable"
|
|
on:canceled={() => {
|
|
autoInviteDisableConfirmCallback = undefined
|
|
}}
|
|
on:confirmed={() => {
|
|
if (autoInviteDisableConfirmCallback) {
|
|
autoInviteDisableConfirmCallback()
|
|
}
|
|
autoInviteDisableConfirmCallback = undefined
|
|
}}
|
|
>
|
|
Are you sure you want to disable auto-invite? Auto-invite is a legacy feature. After disabling,
|
|
it will no longer be available for this workspace. You will only be able to use auto-add. <br />
|
|
Anyone added to the instance will automatically join this workspace.
|
|
</ConfirmationModal>
|
|
</div>
|
|
|
|
<div class="[&>div]:!z-[5002]">
|
|
<ConfirmationModal
|
|
open={Boolean(switchToAutoAddConfirmCallback)}
|
|
title="Switch to Auto-add"
|
|
confirmationText="Switch"
|
|
on:canceled={() => {
|
|
switchToAutoAddConfirmCallback = undefined
|
|
}}
|
|
on:confirmed={() => {
|
|
if (switchToAutoAddConfirmCallback) {
|
|
switchToAutoAddConfirmCallback()
|
|
}
|
|
switchToAutoAddConfirmCallback = undefined
|
|
}}
|
|
>
|
|
Are you sure you want to switch from auto-invite to auto-add?<br />
|
|
Auto-invite is a legacy feature. After switching to auto-add, auto-invite will no longer be available
|
|
for this workspace. <br />
|
|
With auto-add, anyone added to the instance will automatically join this workspace without needing
|
|
to accept an invitation.
|
|
</ConfirmationModal>
|
|
</div>
|