mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-09-08 00:03:07 +00:00
feat: include service accounts in instance settings users list (#9157)
* feat: include service accounts in instance settings users list Service accounts (workspace-scoped, no password row) now appear in the superadmin users list with a Bot icon, workspace badge, and a link to manage them in the workspace settings. Role is locked to Operator. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore: update sqlx offline cache Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix: use composite key for users each block Service accounts can share emails across workspaces, so key by email + workspace_id to avoid Svelte each_key_duplicate. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.7
parent
4f3a1e3109
commit
e5286f4607
+94
@@ -0,0 +1,94 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT email, login_type::TEXT, super_admin, devops, verified, name, company, username, NULL::bool as operator_only, first_time_user, role_source, disabled, NULL::text as workspace_id FROM password WHERE email = $1",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "email",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "login_type",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "super_admin",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "devops",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "verified",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 5,
|
||||
"name": "name",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 6,
|
||||
"name": "company",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 7,
|
||||
"name": "username",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 8,
|
||||
"name": "operator_only",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 9,
|
||||
"name": "first_time_user",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 10,
|
||||
"name": "role_source",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 11,
|
||||
"name": "disabled",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 12,
|
||||
"name": "workspace_id",
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
null,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
null,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "0142d9dc9c1b57487dd5709a0376794f18d33e5bd6340c0189be7818cda64328"
|
||||
}
|
||||
+95
@@ -0,0 +1,95 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT email as \"email!\", login_type::text, verified as \"verified!\", super_admin as \"super_admin!\", devops as \"devops!\", name, company, username, NULL::bool as operator_only, first_time_user as \"first_time_user!\", role_source as \"role_source!\", disabled as \"disabled!\", NULL::text as workspace_id FROM password\n UNION ALL\n SELECT email as \"email!\", 'service_account'::text as login_type, true as \"verified!\", false as \"super_admin!\", false as \"devops!\", NULL::text as name, NULL::text as company, username, true as operator_only, false as \"first_time_user!\", 'service_account'::text as \"role_source!\", disabled as \"disabled!\", workspace_id\n FROM usr\n WHERE is_service_account IS true\n ORDER BY \"super_admin!\" DESC, \"devops!\" DESC, \"email!\"\n LIMIT $1 OFFSET $2",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "email!",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "login_type",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "verified!",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "super_admin!",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "devops!",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 5,
|
||||
"name": "name",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 6,
|
||||
"name": "company",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 7,
|
||||
"name": "username",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 8,
|
||||
"name": "operator_only",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 9,
|
||||
"name": "first_time_user!",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 10,
|
||||
"name": "role_source!",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 11,
|
||||
"name": "disabled!",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 12,
|
||||
"name": "workspace_id",
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8",
|
||||
"Int8"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "16b4496c21d0619dab4521dca22e5fe144c59156a8f06d8592291684c49b2f37"
|
||||
}
|
||||
+95
@@ -0,0 +1,95 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "WITH active_users AS (SELECT distinct username as email FROM (SELECT username, timestamp, operation FROM audit_partitioned UNION ALL SELECT username, timestamp, operation FROM audit) AS a WHERE timestamp > NOW() - INTERVAL '1 month' AND (operation = 'users.login' OR operation = 'oauth.login' OR operation = 'users.token.refresh')),\n authors as (SELECT distinct email FROM usr WHERE usr.operator IS false)\n SELECT email as \"email!\", (email NOT IN (SELECT email FROM authors)) as operator_only, login_type::text, verified as \"verified!\", super_admin as \"super_admin!\", devops as \"devops!\", name, company, username, first_time_user as \"first_time_user!\", role_source as \"role_source!\", disabled as \"disabled!\", NULL::text as workspace_id\n FROM password\n WHERE email IN (SELECT email FROM active_users)\n UNION ALL\n SELECT email as \"email!\", true as operator_only, 'service_account'::text as login_type, true as \"verified!\", false as \"super_admin!\", false as \"devops!\", NULL::text as name, NULL::text as company, username, false as \"first_time_user!\", 'service_account'::text as \"role_source!\", disabled as \"disabled!\", workspace_id\n FROM usr\n WHERE is_service_account IS true\n ORDER BY \"super_admin!\" DESC, \"devops!\" DESC\n LIMIT $1 OFFSET $2",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "email!",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "operator_only",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "login_type",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "verified!",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "super_admin!",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 5,
|
||||
"name": "devops!",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 6,
|
||||
"name": "name",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 7,
|
||||
"name": "company",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 8,
|
||||
"name": "username",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 9,
|
||||
"name": "first_time_user!",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 10,
|
||||
"name": "role_source!",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 11,
|
||||
"name": "disabled!",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 12,
|
||||
"name": "workspace_id",
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8",
|
||||
"Int8"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "c17c39add3f70218dbae38595a909d02cfabe7e0864af577df6968428ac448ef"
|
||||
}
|
||||
@@ -210,6 +210,8 @@ pub struct GlobalUserInfo {
|
||||
first_time_user: bool,
|
||||
role_source: String,
|
||||
disabled: bool,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
workspace_id: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Debug)]
|
||||
@@ -451,13 +453,17 @@ async fn list_users_as_super_admin(
|
||||
let rows = if active_only.is_some_and(|x| x) {
|
||||
sqlx::query_as!(
|
||||
GlobalUserInfo,
|
||||
"WITH active_users AS (SELECT distinct username as email FROM (SELECT username, timestamp, operation FROM audit_partitioned UNION ALL SELECT username, timestamp, operation FROM audit) AS a WHERE timestamp > NOW() - INTERVAL '1 month' AND (operation = 'users.login' OR operation = 'oauth.login' OR operation = 'users.token.refresh')),
|
||||
r#"WITH active_users AS (SELECT distinct username as email FROM (SELECT username, timestamp, operation FROM audit_partitioned UNION ALL SELECT username, timestamp, operation FROM audit) AS a WHERE timestamp > NOW() - INTERVAL '1 month' AND (operation = 'users.login' OR operation = 'oauth.login' OR operation = 'users.token.refresh')),
|
||||
authors as (SELECT distinct email FROM usr WHERE usr.operator IS false)
|
||||
SELECT email, email NOT IN (SELECT email FROM authors) as operator_only, login_type::text, verified, super_admin, devops, name, company, username, first_time_user, role_source, disabled
|
||||
SELECT email as "email!", (email NOT IN (SELECT email FROM authors)) as operator_only, login_type::text, verified as "verified!", super_admin as "super_admin!", devops as "devops!", name, company, username, first_time_user as "first_time_user!", role_source as "role_source!", disabled as "disabled!", NULL::text as workspace_id
|
||||
FROM password
|
||||
WHERE email IN (SELECT email FROM active_users)
|
||||
ORDER BY super_admin DESC, devops DESC
|
||||
LIMIT $1 OFFSET $2",
|
||||
UNION ALL
|
||||
SELECT email as "email!", true as operator_only, 'service_account'::text as login_type, true as "verified!", false as "super_admin!", false as "devops!", NULL::text as name, NULL::text as company, username, false as "first_time_user!", 'service_account'::text as "role_source!", disabled as "disabled!", workspace_id
|
||||
FROM usr
|
||||
WHERE is_service_account IS true
|
||||
ORDER BY "super_admin!" DESC, "devops!" DESC
|
||||
LIMIT $1 OFFSET $2"#,
|
||||
per_page as i32,
|
||||
offset as i32
|
||||
)
|
||||
@@ -466,8 +472,13 @@ async fn list_users_as_super_admin(
|
||||
} else {
|
||||
sqlx::query_as!(
|
||||
GlobalUserInfo,
|
||||
"SELECT email, login_type::text, verified, super_admin, devops, name, company, username, NULL::bool as operator_only, first_time_user, role_source, disabled FROM password ORDER BY super_admin DESC, devops DESC, email LIMIT \
|
||||
$1 OFFSET $2",
|
||||
r#"SELECT email as "email!", login_type::text, verified as "verified!", super_admin as "super_admin!", devops as "devops!", name, company, username, NULL::bool as operator_only, first_time_user as "first_time_user!", role_source as "role_source!", disabled as "disabled!", NULL::text as workspace_id FROM password
|
||||
UNION ALL
|
||||
SELECT email as "email!", 'service_account'::text as login_type, true as "verified!", false as "super_admin!", false as "devops!", NULL::text as name, NULL::text as company, username, true as operator_only, false as "first_time_user!", 'service_account'::text as "role_source!", disabled as "disabled!", workspace_id
|
||||
FROM usr
|
||||
WHERE is_service_account IS true
|
||||
ORDER BY "super_admin!" DESC, "devops!" DESC, "email!"
|
||||
LIMIT $1 OFFSET $2"#,
|
||||
per_page as i32,
|
||||
offset as i32
|
||||
)
|
||||
@@ -716,7 +727,7 @@ async fn global_whoami(
|
||||
) -> JsonResult<GlobalUserInfo> {
|
||||
let user = sqlx::query_as!(
|
||||
GlobalUserInfo,
|
||||
"SELECT email, login_type::TEXT, super_admin, devops, verified, name, company, username, NULL::bool as operator_only, first_time_user, role_source, disabled FROM password WHERE \
|
||||
"SELECT email, login_type::TEXT, super_admin, devops, verified, name, company, username, NULL::bool as operator_only, first_time_user, role_source, disabled, NULL::text as workspace_id FROM password WHERE \
|
||||
email = $1",
|
||||
email
|
||||
)
|
||||
@@ -740,6 +751,7 @@ async fn global_whoami(
|
||||
first_time_user: false,
|
||||
role_source: "manual".to_string(),
|
||||
disabled: false,
|
||||
workspace_id: None,
|
||||
}))
|
||||
} else {
|
||||
// Service accounts don't have a password row
|
||||
@@ -756,6 +768,7 @@ async fn global_whoami(
|
||||
first_time_user: false,
|
||||
role_source: "service_account".to_string(),
|
||||
disabled: false,
|
||||
workspace_id: None,
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26363,7 +26363,7 @@ components:
|
||||
type: string
|
||||
login_type:
|
||||
type: string
|
||||
enum: ["password", "github"]
|
||||
enum: ["password", "github", "service_account"]
|
||||
super_admin:
|
||||
type: boolean
|
||||
devops:
|
||||
@@ -26382,9 +26382,11 @@ components:
|
||||
type: boolean
|
||||
role_source:
|
||||
type: string
|
||||
enum: ["manual", "instance_group"]
|
||||
enum: ["manual", "instance_group", "service_account"]
|
||||
disabled:
|
||||
type: boolean
|
||||
workspace_id:
|
||||
type: string
|
||||
|
||||
required:
|
||||
- email
|
||||
|
||||
@@ -21,12 +21,15 @@
|
||||
import {
|
||||
ArrowRightLeft,
|
||||
Ban,
|
||||
Bot,
|
||||
CheckCircle2,
|
||||
ExternalLink,
|
||||
Pencil,
|
||||
UserMinus,
|
||||
UserPlus
|
||||
} from 'lucide-svelte'
|
||||
import Badge from './common/badge/Badge.svelte'
|
||||
import Tooltip from './Tooltip.svelte'
|
||||
import DropdownV2 from './DropdownV2.svelte'
|
||||
import Popover from './meltComponents/Popover.svelte'
|
||||
import ConfirmationModal from './common/confirmationModal/ConfirmationModal.svelte'
|
||||
@@ -219,7 +222,16 @@
|
||||
{filter}
|
||||
items={users}
|
||||
bind:filteredItems={filteredUsers}
|
||||
f={(x) => x.email + ' ' + x.name + ' ' + x.company}
|
||||
f={(x) =>
|
||||
(x.email ?? '') +
|
||||
' ' +
|
||||
(x.name ?? '') +
|
||||
' ' +
|
||||
(x.company ?? '') +
|
||||
' ' +
|
||||
(x.username ?? '') +
|
||||
' ' +
|
||||
(x.workspace_id ?? '')}
|
||||
/>
|
||||
|
||||
<div class="flex flex-col h-full w-full">
|
||||
@@ -393,7 +405,8 @@
|
||||
</Head>
|
||||
<tbody>
|
||||
{#if filteredUsers && users}
|
||||
{#each filteredUsers.slice(0, nbDisplayed) as { email, super_admin, devops, login_type, name, username, operator_only, role_source, disabled }, i (email)}
|
||||
{#each filteredUsers.slice(0, nbDisplayed) as { email, super_admin, devops, login_type, name, username, operator_only, role_source, disabled, workspace_id }, i (email + '::' + (workspace_id ?? ''))}
|
||||
{@const isServiceAccount = login_type === 'service_account'}
|
||||
<tr
|
||||
class="{i % 2 === 0 ? 'bg-surface-tertiary' : 'bg-surface'} {disabled
|
||||
? 'opacity-60'
|
||||
@@ -401,9 +414,22 @@
|
||||
>
|
||||
<Cell first class="max-w-[250px]">
|
||||
<div class="flex items-center gap-1.5">
|
||||
<a href="mailto:{email}" title={email} class="truncate block"
|
||||
>{email}</a
|
||||
>
|
||||
{#if isServiceAccount}
|
||||
<Bot size={16} class="text-blue-500 shrink-0" />
|
||||
<span title={email} class="truncate block">{email}</span>
|
||||
{:else}
|
||||
<a href="mailto:{email}" title={email} class="truncate block"
|
||||
>{email}</a
|
||||
>
|
||||
{/if}
|
||||
{#if workspace_id}
|
||||
<a
|
||||
href="{base}/?workspace={workspace_id}"
|
||||
title="Workspace: {workspace_id}"
|
||||
>
|
||||
<Badge color="blue">{truncate(workspace_id, 20)}</Badge>
|
||||
</a>
|
||||
{/if}
|
||||
{#if disabled}
|
||||
<span
|
||||
class="text-2xs px-1.5 py-0.5 rounded bg-red-100 text-red-600 dark:bg-red-900 dark:text-red-300 whitespace-nowrap"
|
||||
@@ -449,184 +475,205 @@
|
||||
</Cell>
|
||||
{/if}
|
||||
<Cell>
|
||||
<div class="flex flex-col items-start">
|
||||
{#key `${super_admin}_${devops}_${role_source}`}
|
||||
<ToggleButtonGroup
|
||||
selected={super_admin
|
||||
? 'super_admin'
|
||||
: devops
|
||||
? 'devops'
|
||||
: 'user'}
|
||||
on:selected={async (e) => {
|
||||
if (email == $userStore?.email) {
|
||||
sendUserToast('You cannot demote yourself', true)
|
||||
{#if isServiceAccount}
|
||||
<div class="flex items-center gap-1">
|
||||
<span
|
||||
class="rounded-md text-xs px-2 py-1 bg-surface shadow-md font-bold"
|
||||
>
|
||||
Operator
|
||||
</span>
|
||||
<Tooltip>Service accounts are always operators.</Tooltip>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="flex flex-col items-start">
|
||||
{#key `${super_admin}_${devops}_${role_source}`}
|
||||
<ToggleButtonGroup
|
||||
selected={super_admin
|
||||
? 'super_admin'
|
||||
: devops
|
||||
? 'devops'
|
||||
: 'user'}
|
||||
on:selected={async (e) => {
|
||||
if (email == $userStore?.email) {
|
||||
sendUserToast('You cannot demote yourself', true)
|
||||
listUsers(activeOnly)
|
||||
return
|
||||
}
|
||||
|
||||
let role = e.detail
|
||||
|
||||
if (role === 'super_admin') {
|
||||
await UserService.globalUserUpdate({
|
||||
email,
|
||||
requestBody: {
|
||||
is_super_admin: true,
|
||||
is_devops: false
|
||||
}
|
||||
})
|
||||
}
|
||||
if (role === 'devops') {
|
||||
await UserService.globalUserUpdate({
|
||||
email,
|
||||
requestBody: {
|
||||
is_super_admin: false,
|
||||
is_devops: true
|
||||
}
|
||||
})
|
||||
}
|
||||
if (role === 'user') {
|
||||
await UserService.globalUserUpdate({
|
||||
email,
|
||||
requestBody: {
|
||||
is_super_admin: false,
|
||||
is_devops: false
|
||||
}
|
||||
})
|
||||
}
|
||||
sendUserToast('User updated')
|
||||
listUsers(activeOnly)
|
||||
return
|
||||
}
|
||||
|
||||
let role = e.detail
|
||||
|
||||
if (role === 'super_admin') {
|
||||
await UserService.globalUserUpdate({
|
||||
email,
|
||||
requestBody: {
|
||||
is_super_admin: true,
|
||||
is_devops: false
|
||||
}
|
||||
})
|
||||
}
|
||||
if (role === 'devops') {
|
||||
await UserService.globalUserUpdate({
|
||||
email,
|
||||
requestBody: {
|
||||
is_super_admin: false,
|
||||
is_devops: true
|
||||
}
|
||||
})
|
||||
}
|
||||
if (role === 'user') {
|
||||
await UserService.globalUserUpdate({
|
||||
email,
|
||||
requestBody: {
|
||||
is_super_admin: false,
|
||||
is_devops: false
|
||||
}
|
||||
})
|
||||
}
|
||||
sendUserToast('User updated')
|
||||
listUsers(activeOnly)
|
||||
}}
|
||||
>
|
||||
{#snippet children({ item })}
|
||||
<ToggleButton
|
||||
value={'user'}
|
||||
small
|
||||
label="User"
|
||||
disabled={role_source === 'instance_group' &&
|
||||
(super_admin || devops)}
|
||||
tooltip={role_source === 'instance_group' &&
|
||||
(super_admin || devops)
|
||||
? 'Role is set by an instance group. Remove the user from the group to demote to "User".'
|
||||
: undefined}
|
||||
showTooltipIcon={role_source === 'instance_group' &&
|
||||
(super_admin || devops)}
|
||||
{item}
|
||||
/>
|
||||
<ToggleButton
|
||||
value={'devops'}
|
||||
small
|
||||
label="Devops"
|
||||
tooltip="Devops is a role that grants visibilty similar to that of a super admin, but without giving all rights. For example devops users can see service logs and crtical alerts. You can think of it as a 'readonly' super admin"
|
||||
{item}
|
||||
/>
|
||||
<ToggleButton
|
||||
value={'super_admin'}
|
||||
small
|
||||
label="Superadmin"
|
||||
{item}
|
||||
/>
|
||||
{/snippet}
|
||||
</ToggleButtonGroup>
|
||||
{/key}
|
||||
{#if role_source === 'instance_group' && (super_admin || devops)}
|
||||
<a
|
||||
href="{base}/groups"
|
||||
class="text-2xs text-tertiary mt-0.5 ml-1 hover:underline"
|
||||
title="Role set by instance group. You can upgrade to a higher role manually, but demoting to "User" requires removing them from the group."
|
||||
onclick={() => closeDrawer?.()}
|
||||
>
|
||||
Set by instance group
|
||||
</a>
|
||||
{/if}
|
||||
</div>
|
||||
}}
|
||||
>
|
||||
{#snippet children({ item })}
|
||||
<ToggleButton
|
||||
value={'user'}
|
||||
small
|
||||
label="User"
|
||||
disabled={role_source === 'instance_group' &&
|
||||
(super_admin || devops)}
|
||||
tooltip={role_source === 'instance_group' &&
|
||||
(super_admin || devops)
|
||||
? 'Role is set by an instance group. Remove the user from the group to demote to "User".'
|
||||
: undefined}
|
||||
showTooltipIcon={role_source === 'instance_group' &&
|
||||
(super_admin || devops)}
|
||||
{item}
|
||||
/>
|
||||
<ToggleButton
|
||||
value={'devops'}
|
||||
small
|
||||
label="Devops"
|
||||
tooltip="Devops is a role that grants visibilty similar to that of a super admin, but without giving all rights. For example devops users can see service logs and crtical alerts. You can think of it as a 'readonly' super admin"
|
||||
{item}
|
||||
/>
|
||||
<ToggleButton
|
||||
value={'super_admin'}
|
||||
small
|
||||
label="Superadmin"
|
||||
{item}
|
||||
/>
|
||||
{/snippet}
|
||||
</ToggleButtonGroup>
|
||||
{/key}
|
||||
{#if role_source === 'instance_group' && (super_admin || devops)}
|
||||
<a
|
||||
href="{base}/groups"
|
||||
class="text-2xs text-tertiary mt-0.5 ml-1 hover:underline"
|
||||
title="Role set by instance group. You can upgrade to a higher role manually, but demoting to "User" requires removing them from the group."
|
||||
onclick={() => closeDrawer?.()}
|
||||
>
|
||||
Set by instance group
|
||||
</a>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</Cell>
|
||||
<Cell last>
|
||||
<div class="flex items-center justify-end">
|
||||
<div
|
||||
bind:this={editWrappers[email]}
|
||||
class="w-0 h-0 overflow-hidden"
|
||||
>
|
||||
<InstanceNameEditor
|
||||
{login_type}
|
||||
value={name}
|
||||
{username}
|
||||
{email}
|
||||
on:refresh={() => {
|
||||
listUsers(activeOnly)
|
||||
}}
|
||||
on:save={(e) => {
|
||||
updateName(e.detail, email)
|
||||
}}
|
||||
on:renamed={() => {
|
||||
listUsers(activeOnly)
|
||||
}}
|
||||
{automateUsernameCreation}
|
||||
/>
|
||||
</div>
|
||||
<DropdownV2
|
||||
items={[
|
||||
{
|
||||
displayName: 'Edit',
|
||||
icon: Pencil,
|
||||
action: () => {
|
||||
const btn = editWrappers[email]?.querySelector(
|
||||
'[aria-label="Popup button"]'
|
||||
)
|
||||
if (btn instanceof HTMLElement) btn.click()
|
||||
}
|
||||
},
|
||||
{
|
||||
displayName: disabled ? 'Enable' : 'Disable',
|
||||
icon: disabled ? CheckCircle2 : Ban,
|
||||
action: () => {
|
||||
if (!disabled) {
|
||||
disableUserEmail = email
|
||||
disableConfirmedCallback = async () => {
|
||||
try {
|
||||
await UserService.globalUserUpdate({
|
||||
email,
|
||||
requestBody: { disabled: true }
|
||||
})
|
||||
sendUserToast('User disabled')
|
||||
listUsers(activeOnly)
|
||||
} catch (e) {
|
||||
sendUserToast('Failed to disable user', true)
|
||||
{#if isServiceAccount}
|
||||
{#if workspace_id}
|
||||
<a
|
||||
href="{base}/workspace_settings?tab=users&workspace={workspace_id}"
|
||||
class="text-xs text-secondary hover:text-primary hover:underline"
|
||||
title="Manage in workspace settings">Manage in workspace</a
|
||||
>
|
||||
{/if}
|
||||
{:else}
|
||||
<div
|
||||
bind:this={editWrappers[email]}
|
||||
class="w-0 h-0 overflow-hidden"
|
||||
>
|
||||
<InstanceNameEditor
|
||||
{login_type}
|
||||
value={name}
|
||||
{username}
|
||||
{email}
|
||||
on:refresh={() => {
|
||||
listUsers(activeOnly)
|
||||
}}
|
||||
on:save={(e) => {
|
||||
updateName(e.detail, email)
|
||||
}}
|
||||
on:renamed={() => {
|
||||
listUsers(activeOnly)
|
||||
}}
|
||||
{automateUsernameCreation}
|
||||
/>
|
||||
</div>
|
||||
<DropdownV2
|
||||
items={[
|
||||
{
|
||||
displayName: 'Edit',
|
||||
icon: Pencil,
|
||||
action: () => {
|
||||
const btn = editWrappers[email]?.querySelector(
|
||||
'[aria-label="Popup button"]'
|
||||
)
|
||||
if (btn instanceof HTMLElement) btn.click()
|
||||
}
|
||||
},
|
||||
{
|
||||
displayName: disabled ? 'Enable' : 'Disable',
|
||||
icon: disabled ? CheckCircle2 : Ban,
|
||||
action: () => {
|
||||
if (!disabled) {
|
||||
disableUserEmail = email
|
||||
disableConfirmedCallback = async () => {
|
||||
try {
|
||||
await UserService.globalUserUpdate({
|
||||
email,
|
||||
requestBody: { disabled: true }
|
||||
})
|
||||
sendUserToast('User disabled')
|
||||
listUsers(activeOnly)
|
||||
} catch (e) {
|
||||
sendUserToast('Failed to disable user', true)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
UserService.globalUserUpdate({
|
||||
email,
|
||||
requestBody: { disabled: false }
|
||||
})
|
||||
.then(() => {
|
||||
sendUserToast('User enabled')
|
||||
listUsers(activeOnly)
|
||||
})
|
||||
.catch(() => {
|
||||
sendUserToast('Failed to enable user', true)
|
||||
})
|
||||
}
|
||||
} else {
|
||||
UserService.globalUserUpdate({
|
||||
email,
|
||||
requestBody: { disabled: false }
|
||||
})
|
||||
.then(() => {
|
||||
sendUserToast('User enabled')
|
||||
listUsers(activeOnly)
|
||||
})
|
||||
.catch(() => {
|
||||
sendUserToast('Failed to enable user', true)
|
||||
})
|
||||
}
|
||||
},
|
||||
{
|
||||
displayName: 'Reassign',
|
||||
icon: ArrowRightLeft,
|
||||
action: () => {
|
||||
offboardingEmail = email
|
||||
offboardingReassignOnly = true
|
||||
}
|
||||
},
|
||||
{
|
||||
displayName: 'Remove',
|
||||
icon: UserMinus,
|
||||
type: 'delete',
|
||||
action: () => {
|
||||
offboardingEmail = email
|
||||
offboardingReassignOnly = false
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
displayName: 'Reassign',
|
||||
icon: ArrowRightLeft,
|
||||
action: () => {
|
||||
offboardingEmail = email
|
||||
offboardingReassignOnly = true
|
||||
}
|
||||
},
|
||||
{
|
||||
displayName: 'Remove',
|
||||
icon: UserMinus,
|
||||
type: 'delete',
|
||||
action: () => {
|
||||
offboardingEmail = email
|
||||
offboardingReassignOnly = false
|
||||
}
|
||||
}
|
||||
]}
|
||||
/>
|
||||
]}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
</Cell>
|
||||
</tr>
|
||||
|
||||
Reference in New Issue
Block a user