mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-09-11 00:06:06 +00:00
fix: improve performance of list users
This commit is contained in:
@@ -1734,6 +1734,31 @@ paths:
|
||||
items:
|
||||
$ref: "#/components/schemas/User"
|
||||
|
||||
/w/{workspace}/users/list_usage:
|
||||
get:
|
||||
summary: list users usage
|
||||
operationId: listUsersUsage
|
||||
tags:
|
||||
- user
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/WorkspaceId"
|
||||
responses:
|
||||
"200":
|
||||
description: user
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
email:
|
||||
type: string
|
||||
executions:
|
||||
type: number
|
||||
required:
|
||||
email
|
||||
|
||||
/w/{workspace}/users/list_usernames:
|
||||
get:
|
||||
summary: list usernames
|
||||
@@ -8399,8 +8424,6 @@ components:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
usage:
|
||||
$ref: "#/components/schemas/Usage"
|
||||
required:
|
||||
- email
|
||||
- username
|
||||
@@ -8412,12 +8435,6 @@ components:
|
||||
- folders
|
||||
- folders_owners
|
||||
|
||||
Usage:
|
||||
type: object
|
||||
properties:
|
||||
executions:
|
||||
type: number
|
||||
|
||||
Login:
|
||||
type: object
|
||||
properties:
|
||||
|
||||
@@ -57,6 +57,7 @@ const COOKIE_PATH: &str = "/";
|
||||
pub fn workspaced_service() -> Router {
|
||||
Router::new()
|
||||
.route("/list", get(list_users))
|
||||
.route("/list_usage", get(list_user_usage))
|
||||
.route("/list_usernames", get(list_usernames))
|
||||
.route("/exists", post(exists_username))
|
||||
.route("/update/:user", post(update_workspace_user))
|
||||
@@ -542,16 +543,11 @@ pub struct User {
|
||||
pub role: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(FromRow, Serialize)]
|
||||
pub struct Usage {
|
||||
pub executions: i64,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct UserWithUsage {
|
||||
#[serde(flatten)]
|
||||
pub user: User,
|
||||
pub usage: Usage,
|
||||
pub email: String,
|
||||
pub executions: Option<i64>,
|
||||
}
|
||||
|
||||
#[derive(FromRow, Serialize, Debug)]
|
||||
@@ -717,37 +713,55 @@ async fn list_users(
|
||||
authed: ApiAuthed,
|
||||
Extension(user_db): Extension<UserDB>,
|
||||
Path(w_id): Path<String>,
|
||||
) -> JsonResult<Vec<UserWithUsage>> {
|
||||
) -> JsonResult<Vec<User>> {
|
||||
if *CLOUD_HOSTED && w_id == "demo" {
|
||||
require_admin(authed.is_admin, &authed.username)?;
|
||||
}
|
||||
let mut tx = user_db.begin(&authed).await?;
|
||||
let rows = sqlx::query(
|
||||
let rows = sqlx::query_as!(User,
|
||||
"
|
||||
SELECT usr.*, usage.*
|
||||
SELECT *
|
||||
FROM usr
|
||||
, LATERAL (
|
||||
SELECT COALESCE(SUM(duration_ms + 1000)/1000 , 0)::BIGINT executions
|
||||
FROM completed_job
|
||||
WHERE workspace_id = $1
|
||||
AND job_kind NOT IN ('flow', 'flowpreview')
|
||||
AND email = usr.email
|
||||
AND now() - '1 week'::interval < created_at
|
||||
) usage
|
||||
WHERE workspace_id = $1
|
||||
",
|
||||
", w_id
|
||||
)
|
||||
.bind(&w_id)
|
||||
.try_map(|row| {
|
||||
// flatten not released yet https://github.com/launchbadge/sqlx/pull/1959
|
||||
Ok(UserWithUsage { user: FromRow::from_row(&row)?, usage: FromRow::from_row(&row)? })
|
||||
})
|
||||
.fetch_all(&mut *tx)
|
||||
.await?;
|
||||
tx.commit().await?;
|
||||
Ok(Json(rows))
|
||||
}
|
||||
|
||||
async fn list_user_usage(
|
||||
authed: ApiAuthed,
|
||||
Extension(user_db): Extension<UserDB>,
|
||||
Path(w_id): Path<String>) -> JsonResult<Vec<UserWithUsage>> {
|
||||
if *CLOUD_HOSTED && w_id == "demo" {
|
||||
require_admin(authed.is_admin, &authed.username)?;
|
||||
}
|
||||
let mut tx = user_db.begin(&authed).await?;
|
||||
let rows = sqlx::query_as!(
|
||||
UserWithUsage,
|
||||
"
|
||||
SELECT usr.email, usage.executions
|
||||
FROM usr
|
||||
, LATERAL (
|
||||
SELECT COALESCE(SUM(duration_ms + 1000)/1000 , 0)::BIGINT executions
|
||||
FROM completed_job
|
||||
WHERE workspace_id = $1
|
||||
AND job_kind NOT IN ('flow', 'flowpreview')
|
||||
AND email = usr.email
|
||||
AND now() - '1 week'::interval < created_at
|
||||
) usage
|
||||
WHERE workspace_id = $1
|
||||
",
|
||||
w_id
|
||||
)
|
||||
.fetch_all(&mut *tx)
|
||||
.await?;
|
||||
tx.commit().await?;
|
||||
Ok(Json(rows))
|
||||
}
|
||||
|
||||
async fn list_users_as_super_admin(
|
||||
authed: ApiAuthed,
|
||||
Extension(db): Extension<DB>,
|
||||
|
||||
@@ -11,16 +11,18 @@
|
||||
import Head from '$lib/components/table/Head.svelte'
|
||||
import Toggle from '$lib/components/Toggle.svelte'
|
||||
import Tooltip from '$lib/components/Tooltip.svelte'
|
||||
import type { User } from '$lib/gen'
|
||||
import type { CancelablePromise, User } from '$lib/gen'
|
||||
import { UserService, WorkspaceService, type WorkspaceInvite } from '$lib/gen'
|
||||
import { userStore, workspaceStore } from '$lib/stores'
|
||||
import { sendUserToast } from '$lib/toast'
|
||||
import { Mails, Search } from 'lucide-svelte'
|
||||
import { Loader2, Mails, Search } from 'lucide-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 { isCloudHosted } from '$lib/cloud'
|
||||
import { truncate } from '$lib/utils'
|
||||
import { onDestroy } from 'svelte'
|
||||
|
||||
let users: User[] | undefined = undefined
|
||||
let invites: WorkspaceInvite[] = []
|
||||
@@ -38,6 +40,22 @@
|
||||
autoAdd = settings.auto_add
|
||||
}
|
||||
|
||||
let getUsagePromise: CancelablePromise<Array<{
|
||||
email: string;
|
||||
executions?: number;
|
||||
}>> | undefined = undefined
|
||||
|
||||
let usage: Record<string, number> | undefined = undefined
|
||||
|
||||
async function getUsage() {
|
||||
getUsagePromise = UserService.listUsersUsage({ workspace: $workspaceStore! })
|
||||
const res = await getUsagePromise
|
||||
usage = res.reduce((acc, { email, executions }) => {
|
||||
acc[email] = executions ?? 0
|
||||
return acc
|
||||
}, {} as Record<string, number>)
|
||||
}
|
||||
|
||||
async function listUsers(): Promise<void> {
|
||||
users = await UserService.listUsers({ workspace: $workspaceStore! })
|
||||
}
|
||||
@@ -58,11 +76,16 @@
|
||||
if ($workspaceStore) {
|
||||
getDisallowedAutoDomain()
|
||||
listUsers()
|
||||
getUsage()
|
||||
listInvites()
|
||||
loadSettings()
|
||||
}
|
||||
}
|
||||
|
||||
onDestroy(() => {
|
||||
getUsagePromise?.cancel()
|
||||
})
|
||||
|
||||
let deleteConfirmedCallback: (() => void) | undefined = undefined
|
||||
|
||||
async function removeAllInvitesFromDomain() {
|
||||
@@ -145,11 +168,11 @@
|
||||
</Head>
|
||||
<tbody class="divide-y bg-surface">
|
||||
{#if filteredUsers}
|
||||
{#each filteredUsers.slice(0, nbDisplayed) as { email, username, is_admin, operator, usage, disabled } (email)}
|
||||
{#each filteredUsers.slice(0, nbDisplayed) as { email, username, is_admin, operator, disabled } (email)}
|
||||
<tr class="!hover:bg-surface-hover">
|
||||
<Cell first>{email}</Cell>
|
||||
<Cell>{username}</Cell>
|
||||
<Cell>{usage?.executions}</Cell>
|
||||
<Cell first>{truncate(email, 20)}</Cell>
|
||||
<Cell>{truncate(username, 30)}</Cell>
|
||||
<Cell>{#if usage?.[email] != undefined}{usage?.[email]}{:else}<Loader2 size={14} class="animate-spin" />{/if}</Cell>
|
||||
<Cell>
|
||||
<div class="flex gap-1">
|
||||
{#if disabled}
|
||||
|
||||
Reference in New Issue
Block a user