mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-09-21 00:02:30 +00:00
feat: add 'add user to workspace'
This commit is contained in:
@@ -670,6 +670,43 @@ paths:
|
||||
schema:
|
||||
type: string
|
||||
|
||||
/w/{workspace}/workspaces/add_user:
|
||||
post:
|
||||
summary: add user to workspace
|
||||
operationId: addUser
|
||||
tags:
|
||||
- workspace
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/WorkspaceId"
|
||||
requestBody:
|
||||
description: WorkspaceInvite
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
email:
|
||||
type: string
|
||||
is_admin:
|
||||
type: boolean
|
||||
username:
|
||||
type: string
|
||||
operator:
|
||||
type: boolean
|
||||
required:
|
||||
- email
|
||||
- is_admin
|
||||
- operator
|
||||
- username
|
||||
responses:
|
||||
"200":
|
||||
description: status
|
||||
content:
|
||||
text/plain:
|
||||
schema:
|
||||
type: string
|
||||
|
||||
/w/{workspace}/workspaces/delete_invite:
|
||||
post:
|
||||
summary: delete user invite
|
||||
|
||||
@@ -43,6 +43,7 @@ pub fn workspaced_service() -> Router {
|
||||
.route("/update", post(edit_workspace))
|
||||
.route("/archive", post(archive_workspace))
|
||||
.route("/invite_user", post(invite_user))
|
||||
.route("/add_user", post(add_user))
|
||||
.route("/delete_invite", post(delete_invite))
|
||||
.route("/get_settings", get(get_settings))
|
||||
.route("/edit_slack_command", post(edit_slack_command))
|
||||
@@ -152,6 +153,14 @@ pub struct NewWorkspaceInvite {
|
||||
pub operator: bool,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct NewWorkspaceUser {
|
||||
pub email: String,
|
||||
pub username: String,
|
||||
pub is_admin: bool,
|
||||
pub operator: bool,
|
||||
}
|
||||
|
||||
async fn list_pending_invites(
|
||||
authed: Authed,
|
||||
Extension(user_db): Extension<UserDB>,
|
||||
@@ -744,6 +753,37 @@ async fn invite_user(
|
||||
))
|
||||
}
|
||||
|
||||
async fn add_user(
|
||||
Authed { username, is_admin, .. }: Authed,
|
||||
Extension(db): Extension<DB>,
|
||||
Path(w_id): Path<String>,
|
||||
Json(nu): Json<NewWorkspaceUser>,
|
||||
) -> Result<(StatusCode, String)> {
|
||||
require_admin(is_admin, &username)?;
|
||||
|
||||
let mut tx = db.begin().await?;
|
||||
|
||||
sqlx::query!(
|
||||
"INSERT INTO usr
|
||||
(workspace_id, email, username, is_admin, operator)
|
||||
VALUES ($1, $2, $3, $4, $5)",
|
||||
&w_id,
|
||||
nu.email,
|
||||
nu.username,
|
||||
nu.is_admin,
|
||||
nu.operator
|
||||
)
|
||||
.execute(&mut tx)
|
||||
.await?;
|
||||
|
||||
tx.commit().await?;
|
||||
|
||||
Ok((
|
||||
StatusCode::CREATED,
|
||||
format!("user with email {} added", nu.email),
|
||||
))
|
||||
}
|
||||
|
||||
async fn delete_invite(
|
||||
Authed { username, is_admin, .. }: Authed,
|
||||
Extension(db): Extension<DB>,
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
<script lang="ts">
|
||||
import { sendUserToast } from '$lib/utils'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
import { WorkspaceService } from '$lib/gen'
|
||||
import { Button, ToggleButton, ToggleButtonGroup } from './common'
|
||||
import Tooltip from './Tooltip.svelte'
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
let email: string
|
||||
let username: string
|
||||
|
||||
function handleKeyUp(event: KeyboardEvent) {
|
||||
const key = event.key
|
||||
if (key === 'Enter') {
|
||||
event.preventDefault()
|
||||
addUser()
|
||||
}
|
||||
}
|
||||
|
||||
async function addUser() {
|
||||
await WorkspaceService.addUser({
|
||||
workspace: $workspaceStore!,
|
||||
requestBody: {
|
||||
email,
|
||||
username,
|
||||
is_admin: selected == 'admin',
|
||||
operator: selected == 'operator'
|
||||
}
|
||||
})
|
||||
sendUserToast(`Added ${email}`)
|
||||
dispatch('new')
|
||||
}
|
||||
|
||||
let selected: 'operator' | 'author' | 'admin' = 'author'
|
||||
</script>
|
||||
|
||||
<div class="flex flex-row">
|
||||
<input type="email" on:keyup={handleKeyUp} placeholder="email" bind:value={email} class="mr-4" />
|
||||
<input
|
||||
type="text"
|
||||
on:keyup={handleKeyUp}
|
||||
placeholder="username"
|
||||
bind:value={username}
|
||||
class="mr-4"
|
||||
/>
|
||||
<ToggleButtonGroup bind:selected>
|
||||
<ToggleButton position="left" value="operator" size="sm"
|
||||
>Operator <Tooltip
|
||||
>An operator can only execute and view scripts/flows/apps from your workspace, and only
|
||||
those that he has visibility on</Tooltip
|
||||
></ToggleButton
|
||||
>
|
||||
<ToggleButton position="center" value="author" size="sm"
|
||||
>Author <Tooltip
|
||||
>An Author can execute and view scripts/flows/apps, but he can also create new ones</Tooltip
|
||||
></ToggleButton
|
||||
>
|
||||
<ToggleButton position="right" value="admin" size="sm">Admin</ToggleButton>
|
||||
</ToggleButtonGroup>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="blue"
|
||||
size="sm"
|
||||
btnClasses="!ml-8"
|
||||
on:click={addUser}
|
||||
disabled={email === undefined}
|
||||
>
|
||||
Add
|
||||
</Button>
|
||||
</div>
|
||||
@@ -9,7 +9,6 @@
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
let email: string
|
||||
let is_admin = false
|
||||
|
||||
function handleKeyUp(event: KeyboardEvent) {
|
||||
const key = event.key
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
export const staticOutputs: string[] = ['result', 'loading']
|
||||
</script>
|
||||
|
||||
<RunnableWrapper bind:result bind:componentInput {id}>
|
||||
<RunnableWrapper flexWrap bind:result bind:componentInput {id}>
|
||||
<div class="w-full border-b px-2 text-xs p-1 font-semibold bg-gray-500 text-white rounded-t-sm">
|
||||
Results
|
||||
</div>
|
||||
|
||||
@@ -57,6 +57,7 @@
|
||||
<InputValue {id} input={configuration.size} bind:value={size} />
|
||||
|
||||
<RunnableWrapper
|
||||
flexWrap
|
||||
bind:runnableComponent
|
||||
bind:componentInput
|
||||
{id}
|
||||
|
||||
@@ -71,7 +71,7 @@
|
||||
<InputValue {id} input={configuration.theme} bind:value={theme} />
|
||||
<InputValue {id} input={configuration.line} bind:value={lineChart} />
|
||||
|
||||
<RunnableWrapper autoRefresh bind:componentInput {id} bind:result>
|
||||
<RunnableWrapper flexWrap autoRefresh bind:componentInput {id} bind:result>
|
||||
{#if result}
|
||||
{#if lineChart}
|
||||
<Line {data} {options} />
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
let result: string | undefined = undefined
|
||||
</script>
|
||||
|
||||
<RunnableWrapper bind:componentInput {id} bind:result>
|
||||
<RunnableWrapper flexWrap bind:componentInput {id} bind:result>
|
||||
<AlignWrapper {horizontalAlignment} {verticalAlignment}>
|
||||
<div
|
||||
on:pointerdown={(e) => {
|
||||
|
||||
@@ -65,7 +65,7 @@
|
||||
<InputValue {id} input={configuration.theme} bind:value={theme} />
|
||||
<InputValue {id} input={configuration.doughnutStyle} bind:value={doughnut} />
|
||||
|
||||
<RunnableWrapper autoRefresh bind:componentInput {id} bind:result>
|
||||
<RunnableWrapper flexWrap autoRefresh bind:componentInput {id} bind:result>
|
||||
{#if result}
|
||||
{#if doughnut}
|
||||
<Doughnut {data} {options} />
|
||||
|
||||
@@ -70,7 +70,7 @@
|
||||
<InputValue {id} input={configuration.zoomable} bind:value={zoomable} />
|
||||
<InputValue {id} input={configuration.pannable} bind:value={pannable} />
|
||||
|
||||
<RunnableWrapper autoRefresh bind:componentInput {id} bind:result>
|
||||
<RunnableWrapper flexWrap autoRefresh bind:componentInput {id} bind:result>
|
||||
{#if result}
|
||||
<Scatter {data} {options} />
|
||||
{/if}
|
||||
|
||||
@@ -54,7 +54,7 @@
|
||||
<InputValue {id} input={configuration.extraStyle} bind:value={extraStyle} />
|
||||
<InputValue {id} input={configuration.style} bind:value={style} />
|
||||
|
||||
<RunnableWrapper bind:componentInput {id} bind:result>
|
||||
<RunnableWrapper flexWrap bind:componentInput {id} bind:result>
|
||||
<AlignWrapper {horizontalAlignment} {verticalAlignment}>
|
||||
{#if !result || result === ''}
|
||||
<div class="text-gray-400 bg-gray-100 flex justify-center items-center h-full w-full">
|
||||
|
||||
@@ -85,7 +85,7 @@
|
||||
<InputValue {id} input={configuration.zoomable} bind:value={zoomable} />
|
||||
<InputValue {id} input={configuration.pannable} bind:value={pannable} />
|
||||
|
||||
<RunnableWrapper autoRefresh bind:componentInput {id} bind:result>
|
||||
<RunnableWrapper flexWrap autoRefresh bind:componentInput {id} bind:result>
|
||||
{#if result}
|
||||
<Scatter {data} {options} />
|
||||
{/if}
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
export let forceSchemaDisplay: boolean = false
|
||||
export let noMinH = false
|
||||
export let defaultUserInput = false
|
||||
export let flexWrap = false
|
||||
|
||||
const { worldStore, runnableComponents, workspace, appPath, isEditor, jobs, noBackend } =
|
||||
getContext<AppEditorContext>('AppEditorContext')
|
||||
@@ -279,8 +280,7 @@
|
||||
{#if schemaStripped && Object.keys(schemaStripped?.properties ?? {}).length > 0 && (autoRefresh || forceSchemaDisplay)}
|
||||
<div class="px-2 h-fit min-h-0">
|
||||
<SchemaForm
|
||||
compact
|
||||
flexWrap
|
||||
{flexWrap}
|
||||
schema={schemaStripped}
|
||||
bind:args
|
||||
{disabledArgs}
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
export let runnableComponent: RunnableComponent | undefined = undefined
|
||||
export let forceSchemaDisplay: boolean = false
|
||||
export let defaultUserInput = false
|
||||
export let flexWrap = false
|
||||
|
||||
const { staticExporter, noBackend } = getContext<AppEditorContext>('AppEditorContext')
|
||||
|
||||
@@ -35,6 +36,7 @@
|
||||
<slot />
|
||||
{:else if componentInput.type === 'runnable' && isRunnableDefined()}
|
||||
<RunnableComponent
|
||||
{flexWrap}
|
||||
{defaultUserInput}
|
||||
bind:this={runnableComponent}
|
||||
bind:fields={componentInput.fields}
|
||||
|
||||
@@ -118,7 +118,7 @@
|
||||
|
||||
<InputValue {id} input={configuration.search} bind:value={search} />
|
||||
|
||||
<RunnableWrapper bind:componentInput {id} bind:result>
|
||||
<RunnableWrapper flexWrap bind:componentInput {id} bind:result>
|
||||
{#if Array.isArray(result) && result.every(isObject)}
|
||||
<div class="border border-gray-300 shadow-sm divide-y divide-gray-300 flex flex-col h-full">
|
||||
{#if search !== 'Disabled'}
|
||||
|
||||
@@ -46,6 +46,8 @@
|
||||
|
||||
let loading = true
|
||||
|
||||
let nbDisplayed = 30
|
||||
|
||||
async function loadScripts(): Promise<void> {
|
||||
const loadedScripts = await ScriptService.listScripts({
|
||||
workspace: $workspaceStore!,
|
||||
@@ -294,9 +296,9 @@
|
||||
{:else if filteredItems.length === 0}
|
||||
<NoItemFound />
|
||||
{:else}
|
||||
<div class="border rounded-md divide-y divide-gray-200 mb-80">
|
||||
<div class="border rounded-md divide-y divide-gray-200">
|
||||
<!-- <VirtualList {items} let:item bind:start bind:end> -->
|
||||
{#each items ?? [] as item, i (item.type + '/' + item.path + (item.summary ?? ''))}
|
||||
{#each (items ?? []).slice(0, nbDisplayed) as item, i (item.type + '/' + item.path + (item.summary ?? ''))}
|
||||
{#if item.type == 'script'}
|
||||
<ScriptRow
|
||||
starred={item.starred ?? false}
|
||||
@@ -328,8 +330,14 @@
|
||||
{/each}
|
||||
<!-- </VirtualList> -->
|
||||
</div>
|
||||
<!-- <span class="text-xs">{pluralize(items?.length ?? 0, 'item')}</span>
|
||||
<span class="text-xs">{`(${start} - ${end})`}</span> -->
|
||||
{#if items && items?.length > 30}
|
||||
<span class="text-xs"
|
||||
>{nbDisplayed} items out of {items.length}
|
||||
<button class="ml-4" on:click={() => (nbDisplayed += 30)}>load 30 more</button></span
|
||||
>
|
||||
{/if}
|
||||
<div class="pb-80" />
|
||||
<!-- <span class="text-xs">{`(${start} - ${end})`}</span> --> -->
|
||||
{/if}
|
||||
</div>
|
||||
</CenteredPage>
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
import Toggle from '$lib/components/Toggle.svelte'
|
||||
import ToggleButtonGroup from '$lib/components/common/toggleButton/ToggleButtonGroup.svelte'
|
||||
import ToggleButton from '$lib/components/common/toggleButton/ToggleButton.svelte'
|
||||
import AddUser from '$lib/components/AddUser.svelte'
|
||||
|
||||
let users: User[] | undefined = undefined
|
||||
let invites: WorkspaceInvite[] = []
|
||||
@@ -35,6 +36,7 @@
|
||||
let itemKind: 'flow' | 'script' = 'flow'
|
||||
let operatorOnly: boolean | undefined = undefined
|
||||
let premium_info: { premium: boolean; usage?: number } | undefined = undefined
|
||||
let nbDisplayed = 30
|
||||
|
||||
// function getDropDownItems(username: string): DropdownItem[] {
|
||||
// return [
|
||||
@@ -138,12 +140,14 @@
|
||||
{#if $userStore?.is_admin}
|
||||
<PageHeader title="Workspace Settings of {$workspaceStore}" />
|
||||
|
||||
<PageHeader title="Members" primary={false} />
|
||||
<PageHeader title="Members ({users?.length ?? ''})" primary={false} />
|
||||
|
||||
<div class="pb-1">
|
||||
<AddUser on:new={listUsers} />
|
||||
|
||||
<div class="pt-2 pb-1">
|
||||
<input placeholder="Search users" bind:value={userFilter} class="input mt-1" />
|
||||
</div>
|
||||
<div class="overflow-auto max-h-screen">
|
||||
<div class="overflow-auto max-h-screen mb-20">
|
||||
<TableCustom>
|
||||
<tr slot="header-row">
|
||||
<th>email</th>
|
||||
@@ -160,7 +164,7 @@
|
||||
</tr>
|
||||
<tbody slot="body">
|
||||
{#if filteredUsers}
|
||||
{#each filteredUsers as { email, username, is_admin, operator, usage, disabled } (email)}
|
||||
{#each filteredUsers.slice(0, nbDisplayed) as { email, username, is_admin, operator, usage, disabled } (email)}
|
||||
<tr class="border">
|
||||
<td>{email}</td>
|
||||
<td>{username}</td>
|
||||
@@ -238,6 +242,13 @@
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
{#if filteredUsers?.length > 50}
|
||||
<span class="text-xs"
|
||||
>{nbDisplayed} items out of {filteredUsers.length}
|
||||
<button class="ml-4" on:click={() => (nbDisplayed += 30)}>load 30 more</button
|
||||
></span
|
||||
>
|
||||
{/if}
|
||||
{:else}
|
||||
{#each new Array(6) as _}
|
||||
<tr class="border">
|
||||
@@ -252,7 +263,7 @@
|
||||
</tbody>
|
||||
</TableCustom>
|
||||
</div>
|
||||
<PageHeader title="Pending invites" primary={false}>
|
||||
<PageHeader title="Pending Invites ({invites.length ?? ''})" primary={false}>
|
||||
<InviteUser on:new={listInvites} />
|
||||
</PageHeader>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user