feat: add 'add user to workspace'

This commit is contained in:
Ruben Fiszel
2023-01-11 00:51:04 +01:00
parent 342ea5609b
commit a14623feaa
17 changed files with 190 additions and 20 deletions
+37
View File
@@ -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
+40
View File
@@ -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>