feat: add operator mode

This commit is contained in:
Ruben Fiszel
2022-12-13 20:16:57 +01:00
parent d813c007c0
commit 3485b07b25
33 changed files with 1144 additions and 841 deletions
+74 -70
View File
@@ -2,7 +2,7 @@
import { goto } from '$app/navigation'
import { page } from '$app/stores'
import { FlowService, ScheduleService, type Flow } from '$lib/gen'
import { workspaceStore } from '$lib/stores'
import { userStore, workspaceStore } from '$lib/stores'
import {
encodeState,
formatCron,
@@ -186,80 +186,84 @@
let flowViewer: Drawer
</script>
<UnsavedConfirmationModal />
{#if !$userStore?.operator}
<UnsavedConfirmationModal />
<Drawer bind:this={flowViewer} size="75%">
<DrawerContent title="View Graph" on:close={flowViewer.closeDrawer} noPadding>
<div class="overflow-hidden h-full w-full">
<FlowGraphViewer flow={$flowStore} />
</div>
</DrawerContent>
</Drawer>
<Drawer bind:this={flowViewer} size="75%">
<DrawerContent title="View Graph" on:close={flowViewer.closeDrawer} noPadding>
<div class="overflow-hidden h-full w-full">
<FlowGraphViewer flow={$flowStore} />
</div>
</DrawerContent>
</Drawer>
<div class="flex flex-col flex-1 h-screen">
<!-- Nav between steps-->
<div
class="justify-between flex flex-row w-full items-center pl-2.5 pr-6 space-x-4 overflow-x-auto scrollbar-hidden max-h-12 h-full"
>
<div class="flex flex-row">
<FlowImportExportMenu />
<Button
btnClasses="inline-flex"
startIcon={{ icon: faEye }}
variant="border"
color="light"
size="sm"
on:click={flowViewer.openDrawer}
>
View Graph
</Button>
</div>
<div class="gap-1 flex-row hidden md:flex shrink overflow-hidden">
<Button
btnClasses="hidden lg:inline-flex"
startIcon={{ icon: faPen }}
variant="contained"
color="light"
size="xs"
on:click={async () => {
select('settings')
document.getElementById('path')?.focus()
}}
>
{$flowStore.path && $flowStore.path != '' ? $flowStore.path : 'Choose a path'}
</Button>
<Button
startIcon={{ icon: faPen }}
variant="contained"
color="light"
size="xs"
on:click={async () => {
select('settings')
document.getElementById('flow-summary')?.focus()
}}
>
<div class="max-w-[10em] !truncate">
{$flowStore.summary == '' || !$flowStore.summary ? 'No summary' : $flowStore.summary}
</div>
</Button>
</div>
<div class="flex flex-row space-x-2">
<FlowPreviewButtons />
<div class="center-center">
<div class="flex flex-col flex-1 h-screen">
<!-- Nav between steps-->
<div
class="justify-between flex flex-row w-full items-center pl-2.5 pr-6 space-x-4 overflow-x-auto scrollbar-hidden max-h-12 h-full"
>
<div class="flex flex-row">
<FlowImportExportMenu />
<Button
disabled={pathError != ''}
startIcon={{ icon: faSave }}
btnClasses="inline-flex"
startIcon={{ icon: faEye }}
variant="border"
color="light"
size="sm"
on:click={saveFlow}>Save</Button
on:click={flowViewer.openDrawer}
>
View Graph
</Button>
</div>
<div class="gap-1 flex-row hidden md:flex shrink overflow-hidden">
<Button
btnClasses="hidden lg:inline-flex"
startIcon={{ icon: faPen }}
variant="contained"
color="light"
size="xs"
on:click={async () => {
select('settings')
document.getElementById('path')?.focus()
}}
>
{$flowStore.path && $flowStore.path != '' ? $flowStore.path : 'Choose a path'}
</Button>
<Button
startIcon={{ icon: faPen }}
variant="contained"
color="light"
size="xs"
on:click={async () => {
select('settings')
document.getElementById('flow-summary')?.focus()
}}
>
<div class="max-w-[10em] !truncate">
{$flowStore.summary == '' || !$flowStore.summary ? 'No summary' : $flowStore.summary}
</div>
</Button>
</div>
<div class="flex flex-row space-x-2">
<FlowPreviewButtons />
<div class="center-center">
<Button
disabled={pathError != ''}
startIcon={{ icon: faSave }}
size="sm"
on:click={saveFlow}>Save</Button
>
</div>
</div>
</div>
</div>
<!-- metadata -->
{#if $flowStateStore}
<FlowEditor {initialPath} {loading} />
{:else}
<CenteredPage>Loading...</CenteredPage>
{/if}
</div>
<!-- metadata -->
{#if $flowStateStore}
<FlowEditor {initialPath} {loading} />
{:else}
<CenteredPage>Loading...</CenteredPage>
{/if}
</div>
{:else}
Flow Builder not available to operators
{/if}
@@ -31,7 +31,7 @@
company
}
})
sendUserToast(`Successfully added ${email}. Welcome to them!`)
sendUserToast(`Added ${email}`)
dispatch('new')
}
</script>
@@ -39,7 +39,7 @@
<div class="flex flex-row space-x-1">
<input type="email" on:keyup={handleKeyUp} placeholder="email" bind:value={email} />
<Toggle class="mx-2" bind:checked={is_super_admin} options={{ right: 'admin' }} />
<Toggle class="mx-2" bind:checked={is_super_admin} options={{ right: 'superadmin' }} />
<input on:keyup={handleKeyUp} type="password" placeholder="password" bind:value={password} />
<input type="text" on:keyup={handleKeyUp} placeholder="name" bind:value={name} />
<input type="text" on:keyup={handleKeyUp} placeholder="company" bind:value={company} />
+21 -6
View File
@@ -3,8 +3,8 @@
import { createEventDispatcher } from 'svelte'
import { workspaceStore } from '$lib/stores'
import { WorkspaceService } from '$lib/gen'
import { Button } from './common'
import Toggle from '$lib/components/Toggle.svelte'
import { Button, ToggleButton, ToggleButtonGroup } from './common'
import Tooltip from './Tooltip.svelte'
const dispatch = createEventDispatcher()
@@ -24,18 +24,33 @@
workspace: $workspaceStore!,
requestBody: {
email,
is_admin
is_admin: selected == 'admin',
operator: selected == 'operator'
}
})
sendUserToast(`Successfully invited ${email}. Welcome to them!`)
sendUserToast(`Invited ${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" />
<Toggle bind:checked={is_admin} options={{ right: 'admin' }} />
<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"
@@ -58,7 +58,7 @@
path: resourceToEdit.path,
requestBody: { path, value: args, description }
})
sendUserToast(`Successfully updated resource at ${path}`)
sendUserToast(`Updated resource at ${path}`)
dispatch('refresh')
drawer.closeDrawer?.()
} else {
+212 -208
View File
@@ -5,7 +5,7 @@
import { page } from '$app/stores'
import { inferArgs } from '$lib/infer'
import { initialCode, isInitialCode } from '$lib/script_helpers'
import { workspaceStore } from '$lib/stores'
import { userStore, workspaceStore } from '$lib/stores'
import { encodeState, sendUserToast, setQueryWithoutLoad } from '$lib/utils'
import Path from './Path.svelte'
import RadioButton from './RadioButton.svelte'
@@ -24,7 +24,7 @@
export let script: Script
export let initialPath: string = ''
export let template: 'pgsql'| 'mysql' | 'script' = 'script'
export let template: 'pgsql' | 'mysql' | 'script' = 'script'
export let initialArgs: Record<string, any> = {}
const langs: [string, SupportedLanguage][] = [
@@ -91,228 +91,232 @@
$: kind = script.kind as 'script' | 'trigger' | 'approval' | undefined
</script>
<UnsavedConfirmationModal />
<div class="flex flex-col h-screen">
<!-- Nav between steps-->
<div class="flex flex-col w-full px-2 py-1 border-b shadow-sm">
<div
class="justify-between flex flex-row w-full items-center overflow-x-auto scrollbar-hidden px-2"
>
<div class="flex flex-row py-1">
<Breadcrumb
items={['Metadata', 'Code', 'Advanced']}
selectedIndex={step}
on:select={(e) => changeStep(e.detail.index + 1)}
disabled={pathError != ''}
>
<svelte:fragment slot="separator">/</svelte:fragment>
</Breadcrumb>
</div>
{#if !$userStore?.operator}
<UnsavedConfirmationModal />
<div class="flex flex-col h-screen">
<!-- Nav between steps-->
<div class="flex flex-col w-full px-2 py-1 border-b shadow-sm">
<div
class="justify-between flex flex-row w-full items-center overflow-x-auto scrollbar-hidden px-2"
>
<div class="flex flex-row py-1">
<Breadcrumb
items={['Metadata', 'Code', 'Advanced']}
selectedIndex={step}
on:select={(e) => changeStep(e.detail.index + 1)}
disabled={pathError != ''}
>
<svelte:fragment slot="separator">/</svelte:fragment>
</Breadcrumb>
</div>
<div class="gap-1 flex-row hidden md:flex shrink overflow-hidden">
<Button
btnClasses="hidden lg:inline-flex"
startIcon={{ icon: faPen }}
variant="contained"
color="light"
size="xs"
on:click={async () => {
await changeStep(1)
setTimeout(() => pathC?.focus(), 100)
}}
>
{script.path}
</Button>
<div class="gap-1 flex-row hidden md:flex shrink overflow-hidden">
<Button
btnClasses="hidden lg:inline-flex"
startIcon={{ icon: faPen }}
variant="contained"
color="light"
size="xs"
on:click={async () => {
await changeStep(1)
setTimeout(() => pathC?.focus(), 100)
}}
>
{script.path}
</Button>
<Button
startIcon={{ icon: faPen }}
variant="contained"
color="light"
size="xs"
on:click={async () => {
await changeStep(1)
setTimeout(() => summaryC?.focus(), 100)
}}
>
<div class="max-w-[10em] !truncate">
{script.summary == '' || !script.summary ? 'No summary' : script.summary}
</div>
</Button>
</div>
<div class="flex flex-row gap-x-2">
<Button
variant="border"
size="sm"
btnClasses={step == 1 ? 'hidden sm:invisible' : ''}
on:click={() => changeStep(step - 1)}
>
Back
</Button>
<Button
size="sm"
variant={step == 1 ? 'contained' : 'border'}
btnClasses={step == 3 ? 'invisible' : ''}
disabled={step === 1 && pathError !== ''}
on:click={() => changeStep(step + 1)}
>
Next
</Button>
<Button
size="sm"
variant={step == 1 ? 'border' : 'contained'}
disabled={step === 1 && pathError !== ''}
btnClasses={step == 1 && initialPath == '' ? 'invisible' : ''}
startIcon={{ icon: faSave }}
on:click={editScript}>Save</Button
>
<Button
startIcon={{ icon: faPen }}
variant="contained"
color="light"
size="xs"
on:click={async () => {
await changeStep(1)
setTimeout(() => summaryC?.focus(), 100)
}}
>
<div class="max-w-[10em] !truncate">
{script.summary == '' || !script.summary ? 'No summary' : script.summary}
</div>
</Button>
</div>
<div class="flex flex-row gap-x-2">
<Button
variant="border"
size="sm"
btnClasses={step == 1 ? 'hidden sm:invisible' : ''}
on:click={() => changeStep(step - 1)}
>
Back
</Button>
<Button
size="sm"
variant={step == 1 ? 'contained' : 'border'}
btnClasses={step == 3 ? 'invisible' : ''}
disabled={step === 1 && pathError !== ''}
on:click={() => changeStep(step + 1)}
>
Next
</Button>
<Button
size="sm"
variant={step == 1 ? 'border' : 'contained'}
disabled={step === 1 && pathError !== ''}
btnClasses={step == 1 && initialPath == '' ? 'invisible' : ''}
startIcon={{ icon: faSave }}
on:click={editScript}>Save</Button
>
</div>
</div>
</div>
</div>
<!-- metadata -->
{#if step === 1}
<CenteredPage>
<div class="space-y-6">
<h2 class="border-b pb-1 mt-4">Path & Summary</h2>
<Path
bind:this={pathC}
bind:error={pathError}
bind:path={script.path}
{initialPath}
on:enter={() => changeStep(2)}
namePlaceholder="my_script"
kind="script"
/>
<label class="block ">
<span class="text-gray-700 text-sm">Summary <Required required={false} /></span>
<input
type="text"
bind:this={summaryC}
bind:value={script.summary}
placeholder="A very short summary of the script displayed when the script is listed"
rows="1"
<!-- metadata -->
{#if step === 1}
<CenteredPage>
<div class="space-y-6">
<h2 class="border-b pb-1 mt-4">Path & Summary</h2>
<Path
bind:this={pathC}
bind:error={pathError}
bind:path={script.path}
{initialPath}
on:enter={() => changeStep(2)}
namePlaceholder="my_script"
kind="script"
/>
</label>
<h2 class="border-b pb-1 mt-4">Language</h2>
<div class="flex flex-row gap-2 flex-wrap">
{#each langs as [label, lang]}
{@const isPicked = script.language == lang && template == 'script'}
<label class="block ">
<span class="text-gray-700 text-sm">Summary <Required required={false} /></span>
<input
type="text"
bind:this={summaryC}
bind:value={script.summary}
placeholder="A very short summary of the script displayed when the script is listed"
rows="1"
/>
</label>
<h2 class="border-b pb-1 mt-4">Language</h2>
<div class="flex flex-row gap-2 flex-wrap">
{#each langs as [label, lang]}
{@const isPicked = script.language == lang && template == 'script'}
<Button
size="sm"
variant="border"
color={isPicked ? 'blue' : 'dark'}
btnClasses={isPicked ? '!border-2 !bg-blue-50/75' : 'm-[1px]'}
on:click={() => {
script.language = lang
template = 'script'
initContent(lang, script.kind, template)
}}
>
<LanguageIcon {lang} /><span class="ml-2">{label}</span>
</Button>
{/each}
<Button
size="sm"
variant="border"
color={isPicked ? 'blue' : 'dark'}
btnClasses={isPicked ? '!border-2 !bg-blue-50/75' : 'm-[1px]'}
color={template == 'pgsql' ? 'blue' : 'dark'}
btnClasses={template == 'pgsql' ? '!border-2 !bg-blue-50/75' : 'm-[1px]'}
on:click={() => {
script.language = lang
template = 'script'
initContent(lang, script.kind, template)
script.language = Script.language.DENO
template = 'pgsql'
initContent(script.language, script.kind, template)
}}
>
<LanguageIcon {lang} /><span class="ml-2">{label}</span>
<LanguageIcon lang="pgsql" /><span class="ml-2">PostgreSQL</span>
</Button>
{/each}
<Button
size="sm"
variant="border"
color={template == 'pgsql' ? 'blue' : 'dark'}
btnClasses={template == 'pgsql' ? '!border-2 !bg-blue-50/75' : 'm-[1px]'}
on:click={() => {
script.language = Script.language.DENO
template = 'pgsql'
initContent(script.language, script.kind, template)
}}
>
<LanguageIcon lang="pgsql" /><span class="ml-2">PostgreSQL</span>
</Button>
<Button
size="sm"
variant="border"
color={template == 'mysql' ? 'blue' : 'dark'}
btnClasses={template == 'mysql' ? '!border-2 !bg-blue-50/75' : 'm-[1px]'}
on:click={() => {
script.language = Script.language.DENO
template = 'mysql'
initContent(script.language, script.kind, template)
}}
>
<LanguageIcon lang="mysql" /><span class="ml-2">MySQL</span>
</Button>
</div>
<h2 class="border-b pb-1 mt-4">Advanced</h2>
<div>
<Button
color="light"
size="sm"
endIcon={{ icon: viewScriptKind ? faChevronUp : faChevronDown }}
on:click={() => (viewScriptKind = !viewScriptKind)}
>
Tag this script as having a specific purpose inside flows
</Button>
</div>
{#if viewScriptKind}
<div class="max-w-lg">
<RadioButton
label="Script Type"
options={[
['Common Script', Script.kind.SCRIPT],
[
{
title: 'Trigger Script',
desc: `First module of flows to trigger them based on watching changes external periodically using an internal state`
},
Script.kind.TRIGGER
],
[
{
title: 'Error Handler',
desc: `Handle errors for flows after all retries attempts have been exhausted`
},
Script.kind.FAILURE
],
[
{
title: 'Approval Script',
desc: `Send notification externally to ask for approval to continue a flow`
},
Script.kind.APPROVAL
]
// ['Command Handler', Script.kind.COMMAND]
]}
on:change={(e) => {
if (isInitialCode(script.content)) {
template = 'script'
initContent(script.language, e.detail, template)
}
<Button
size="sm"
variant="border"
color={template == 'mysql' ? 'blue' : 'dark'}
btnClasses={template == 'mysql' ? '!border-2 !bg-blue-50/75' : 'm-[1px]'}
on:click={() => {
script.language = Script.language.DENO
template = 'mysql'
initContent(script.language, script.kind, template)
}}
bind:value={script.kind}
>
<LanguageIcon lang="mysql" /><span class="ml-2">MySQL</span>
</Button>
</div>
<h2 class="border-b pb-1 mt-4">Advanced</h2>
<div>
<Button
color="light"
size="sm"
endIcon={{ icon: viewScriptKind ? faChevronUp : faChevronDown }}
on:click={() => (viewScriptKind = !viewScriptKind)}
>
Tag this script as having a specific purpose inside flows
</Button>
</div>
{#if viewScriptKind}
<div class="max-w-lg">
<RadioButton
label="Script Type"
options={[
['Common Script', Script.kind.SCRIPT],
[
{
title: 'Trigger Script',
desc: `First module of flows to trigger them based on watching changes external periodically using an internal state`
},
Script.kind.TRIGGER
],
[
{
title: 'Error Handler',
desc: `Handle errors for flows after all retries attempts have been exhausted`
},
Script.kind.FAILURE
],
[
{
title: 'Approval Script',
desc: `Send notification externally to ask for approval to continue a flow`
},
Script.kind.APPROVAL
]
// ['Command Handler', Script.kind.COMMAND]
]}
on:change={(e) => {
if (isInitialCode(script.content)) {
template = 'script'
initContent(script.language, e.detail, template)
}
}}
bind:value={script.kind}
/>
</div>
{/if}
<div class="ml-3">
<Toggle
bind:checked={script.is_template}
options={{ right: 'Save as a workspace template' }}
/>
</div>
{/if}
<div class="ml-3">
<Toggle
bind:checked={script.is_template}
options={{ right: 'Save as a workspace template' }}
/>
</div>
</div>
</CenteredPage>
{:else if step === 2}
<ScriptEditor
bind:schema={script.schema}
path={script.path}
bind:code={script.content}
lang={script.language}
{initialArgs}
{kind}
/>
{:else if step === 3}
<CenteredPage>
<ScriptSchema
bind:summary={script.summary}
bind:description={script.description}
</CenteredPage>
{:else if step === 2}
<ScriptEditor
bind:schema={script.schema}
path={script.path}
bind:code={script.content}
lang={script.language}
{initialArgs}
{kind}
/>
</CenteredPage>
{/if}
</div>
{:else if step === 3}
<CenteredPage>
<ScriptSchema
bind:summary={script.summary}
bind:description={script.description}
bind:schema={script.schema}
/>
</CenteredPage>
{/if}
</div>
{:else}
Script Builder not available to operators
{/if}
@@ -26,9 +26,6 @@
let users: GlobalUserInfo[] = []
let filteredUsers: GlobalUserInfo[] = []
let deleteConfirmedCallback: (() => void) | undefined = undefined
$: openConfirmation = Boolean(deleteConfirmedCallback)
async function loadVersion(): Promise<void> {
version = await SettingsService.backendVersion()
}
@@ -47,39 +44,13 @@
<Drawer bind:this={drawer} on:open={listUsers} size="900px">
<DrawerContent overflow_y={false} title="Superadmin Settings" on:close={drawer.closeDrawer}>
<ConfirmationModal
open={openConfirmation}
title="Remove user"
confirmationText="Remove"
on:canceled={() => {
deleteConfirmedCallback = undefined
}}
on:confirmed={() => {
if (deleteConfirmedCallback) {
deleteConfirmedCallback()
}
deleteConfirmedCallback = undefined
}}
>
<div class="flex flex-col w-full space-y-4">
<span>Are you sure you want to remove this user?</span>
<Alert type="info" title="Bypass confirmation">
<div>
You can press
<Badge color="dark-gray">SHIFT</Badge>
while removing a variable to bypass confirmation.
</div>
</Alert>
</div>
</ConfirmationModal>
<div class="flex flex-col h-full">
<div>
<div class="text-xs pt-1 text-gray-500 ">
Windmill {version}
</div>
<PageHeader title="All users" primary={false} />
<PageHeader title="All global users" primary={false} />
<div class="pb-1" />
<InviteGlobalUser on:new={listUsers} />
@@ -91,21 +62,23 @@
<TableCustom>
<tr slot="header-row" class="sticky top-0 bg-white border-b">
<th>email</th>
<th>superadmin</th>
<th>auth</th>
<th>name</th>
<th>company</th>
<th />
<th />
</tr>
<tbody slot="body" class="overflow-y-auto h-full max-h-full">
{#if filteredUsers && users}
{#each filteredUsers as { email, super_admin, login_type, name, company } (email)}
<tr class="border">
<td>{email}</td>
<td>{super_admin ? 'yes' : ''}</td>
<td>{login_type}</td>
<td><span class="break-words">{name ?? ''}</span></td>
<td><span class="break-words">{company ?? ''}</span></td>
<td
>{#if super_admin}<Badge>Superadmin</Badge>{/if}</td
>
<td>
<div class="flex flex-row gap-x-1">
<button
@@ -119,18 +92,15 @@
})
sendUserToast('User updated')
listUsers()
}}>{super_admin ? 'demote' : 'promote'}</button
}}>{super_admin ? 'non-superadmin' : 'superadmin'}</button
>
|
<button
class="text-red-500"
on:click={async () => {
deleteConfirmedCallback = async () => {
await UserService.globalUserDelete({ email })
sendUserToast(`User ${email} removed`)
await listUsers()
}
openConfirmation = false
await UserService.globalUserDelete({ email })
sendUserToast(`User ${email} removed`)
listUsers()
}}>remove</button
>
</div>
@@ -92,7 +92,7 @@
description: variable.description
}
})
sendUserToast(`Successfully created variable ${path}`)
sendUserToast(`Created variable ${path}`)
dispatch('create')
drawer.closeDrawer()
}
@@ -115,7 +115,7 @@
description: getV.description != variable.description ? variable.description : undefined
}
})
sendUserToast(`Successfully updated variable at ${initialPath}`)
sendUserToast(`Updated variable ${initialPath}`)
dispatch('create')
drawer.closeDrawer()
} catch (err) {
@@ -25,6 +25,7 @@
import ContextPanel from './contextPanel/ContextPanel.svelte'
import { classNames } from '$lib/utils'
import AppPreview from './AppPreview.svelte'
import { userStore } from '$lib/stores'
export let app: App
export let path: string
@@ -67,63 +68,67 @@
$: width = $breakpoint === 'sm' ? 'w-[640px]' : 'w-full '
</script>
{#if initialMode !== 'preview'}
<AppEditorHeader bind:title={$appStore.title} bind:mode={$mode} bind:breakpoint={$breakpoint} />
{/if}
{#if !$userStore?.operator}
{#if initialMode !== 'preview'}
<AppEditorHeader bind:title={$appStore.title} bind:mode={$mode} bind:breakpoint={$breakpoint} />
{/if}
{#if previewing}
<AppPreview app={$appStore} />
{#if previewing}
<AppPreview app={$appStore} />
{:else}
<SplitPanesWrapper class="max-w-full overflow-hidden">
<Pane size={20} minSize={15} maxSize={40}>
<ContextPanel appPath={path} />
</Pane>
<Pane size={60}>
<div class="p-4 bg-gray-100 min-h-full w-full relative">
{#if $appStore.grid}
<div class={classNames('mx-auto h-full', width)}>
<GridEditor />
</div>
{/if}
{#if $connectingInput.opened}
<div
class="absolute top-0 left-0 w-full h-full bg-black border-2 bg-opacity-25 z-1 flex justify-center items-center"
/>
{/if}
</div>
</Pane>
<Pane size={30} minSize={25} maxSize={40}>
<Tabs bind:selected={selectedTab}>
<Tab value="insert" size="xs">
<div class="m-1 flex flex-row gap-2">
<Icon data={faPlus} />
<span>Insert</span>
</div>
</Tab>
<Tab value="settings" size="xs">
<div class="m-1 flex flex-row gap-2">
<Icon data={faSliders} />
<span>Settings</span>
</div>
</Tab>
<svelte:fragment slot="content">
<TabContent value="settings">
{#if $selectedComponent !== undefined}
{#each $appStore.grid as gridItem (gridItem.id)}
{#if gridItem.data.id === $selectedComponent}
<ComponentPanel bind:component={gridItem.data} />
{/if}
{/each}
{/if}
{#if $selectedComponent === undefined}
<div class="p-4 text-sm">No component selected.</div>
{/if}
</TabContent>
<TabContent value="insert">
<ComponentList />
</TabContent>
</svelte:fragment>
</Tabs>
</Pane>
</SplitPanesWrapper>
{/if}
{:else}
<SplitPanesWrapper class="max-w-full overflow-hidden">
<Pane size={20} minSize={15} maxSize={40}>
<ContextPanel appPath={path} />
</Pane>
<Pane size={60}>
<div class="p-4 bg-gray-100 min-h-full w-full relative">
{#if $appStore.grid}
<div class={classNames('mx-auto h-full', width)}>
<GridEditor />
</div>
{/if}
{#if $connectingInput.opened}
<div
class="absolute top-0 left-0 w-full h-full bg-black border-2 bg-opacity-25 z-1 flex justify-center items-center"
/>
{/if}
</div>
</Pane>
<Pane size={30} minSize={25} maxSize={40}>
<Tabs bind:selected={selectedTab}>
<Tab value="insert" size="xs">
<div class="m-1 flex flex-row gap-2">
<Icon data={faPlus} />
<span>Insert</span>
</div>
</Tab>
<Tab value="settings" size="xs">
<div class="m-1 flex flex-row gap-2">
<Icon data={faSliders} />
<span>Settings</span>
</div>
</Tab>
<svelte:fragment slot="content">
<TabContent value="settings">
{#if $selectedComponent !== undefined}
{#each $appStore.grid as gridItem (gridItem.id)}
{#if gridItem.data.id === $selectedComponent}
<ComponentPanel bind:component={gridItem.data} />
{/if}
{/each}
{/if}
{#if $selectedComponent === undefined}
<div class="p-4 text-sm">No component selected.</div>
{/if}
</TabContent>
<TabContent value="insert">
<ComponentList />
</TabContent>
</svelte:fragment>
</Tabs>
</Pane>
</SplitPanesWrapper>
App editor not available to operators
{/if}
@@ -41,10 +41,10 @@
}
})
.then(() => {
sendUserToast('Saved successfully.')
sendUserToast('Saved')
})
.catch(() => {
sendUserToast('Error during saving. Please try again later.', true)
sendUserToast('Error during saving. Please try again later', true)
})
.finally(() => {
loading.save = false
@@ -2,7 +2,7 @@
import Dropdown from '$lib/components/Dropdown.svelte'
import SharedBadge from '$lib/components/SharedBadge.svelte'
import { AppService, type ListableApp } from '$lib/gen'
import { workspaceStore } from '$lib/stores'
import { userStore, workspaceStore } from '$lib/stores'
import { faEdit, faEye, faTrashAlt } from '@fortawesome/free-solid-svg-icons'
import { createEventDispatcher } from 'svelte'
import Button from '../button/Button.svelte'
@@ -32,18 +32,20 @@
</svelte:fragment>
<svelte:fragment slot="actions">
<span class="hidden md:inline-flex gap-x-1">
{#if canWrite}
<div>
<Button
color="light"
size="xs"
variant="border"
startIcon={{ icon: faEdit }}
href="/apps/edit/{path}"
>
Edit
</Button>
</div>
{#if !$userStore?.operator}
{#if canWrite}
<div>
<Button
color="light"
size="xs"
variant="border"
startIcon={{ icon: faEdit }}
href="/apps/edit/{path}"
>
Edit
</Button>
</div>
{/if}
{/if}
<Button
href="/apps/get/{path}"
@@ -4,7 +4,7 @@
import type ShareModal from '$lib/components/ShareModal.svelte'
import { FlowService, type Flow } from '$lib/gen'
import { workspaceStore } from '$lib/stores'
import { userStore, workspaceStore } from '$lib/stores'
import { sendUserToast } from '$lib/utils'
import {
faArchive,
@@ -34,7 +34,7 @@
try {
await FlowService.archiveFlowByPath({ workspace: $workspaceStore!, path })
dispatch('change')
sendUserToast(`Successfully archived flow ${path}`)
sendUserToast(`Archived flow ${path}`)
} catch (err) {
sendUserToast(`Could not archive this flow ${err.body}`, true)
}
@@ -56,30 +56,32 @@
</svelte:fragment>
<svelte:fragment slot="actions">
<span class="hidden md:inline-flex gap-x-1">
{#if canWrite}
<div>
<Button
color="light"
size="xs"
variant="border"
startIcon={{ icon: faEdit }}
href="/flows/edit/{path}"
>
Edit
</Button>
</div>
{:else}
<div>
<Button
color="light"
size="xs"
variant="border"
startIcon={{ icon: faCodeFork }}
href="/flows/add?template={path}"
>
Fork
</Button>
</div>
{#if !$userStore?.operator}
{#if canWrite}
<div>
<Button
color="light"
size="xs"
variant="border"
startIcon={{ icon: faEdit }}
href="/flows/edit/{path}"
>
Edit
</Button>
</div>
{:else}
<div>
<Button
color="light"
size="xs"
variant="border"
startIcon={{ icon: faCodeFork }}
href="/flows/add?template={path}"
>
Fork
</Button>
</div>
{/if}
{/if}
<Button
@@ -4,7 +4,7 @@
import type ShareModal from '$lib/components/ShareModal.svelte'
import { ScriptService, type Script } from '$lib/gen'
import { workspaceStore } from '$lib/stores'
import { userStore, workspaceStore } from '$lib/stores'
import { capitalize, sendUserToast } from '$lib/utils'
import {
faArchive,
@@ -44,7 +44,7 @@
async function archiveScript(path: string): Promise<void> {
await ScriptService.archiveScriptByPath({ workspace: $workspaceStore!, path })
dispatch('change')
sendUserToast(`Successfully archived script ${path}`)
sendUserToast(`Archived script ${path}`)
}
</script>
@@ -74,30 +74,32 @@
<svelte:fragment slot="actions">
<span class="hidden md:inline-flex gap-x-1">
{#if canWrite}
<div>
<Button
color="light"
size="xs"
variant="border"
startIcon={{ icon: faEdit }}
href="/scripts/edit/{hash}?step=2"
>
Edit
</Button>
</div>
{:else}
<div>
<Button
color="light"
size="xs"
variant="border"
startIcon={{ icon: faCodeFork }}
href="/scripts/add?template={path}"
>
Fork
</Button>
</div>
{#if !$userStore?.operator}
{#if canWrite}
<div>
<Button
color="light"
size="xs"
variant="border"
startIcon={{ icon: faEdit }}
href="/scripts/edit/{hash}?step=2"
>
Edit
</Button>
</div>
{:else}
<div>
<Button
color="light"
size="xs"
variant="border"
startIcon={{ icon: faCodeFork }}
href="/scripts/add?template={path}"
>
Fork
</Button>
</div>
{/if}
{/if}
<Button
@@ -128,6 +130,7 @@
icon: faEye,
href: `/scripts/get/${hash}`
},
{
displayName: 'Edit',
icon: faEdit,
@@ -55,7 +55,8 @@
return {
canWrite:
canWrite(script.path, script.extra_perms, $userStore) &&
script.workspace_id == $workspaceStore,
script.workspace_id == $workspaceStore &&
!$userStore?.operator,
...script
}
})
@@ -66,7 +67,10 @@
async function loadFlows(): Promise<void> {
flows = (await FlowService.listFlows({ workspace: $workspaceStore! })).map((x: Flow) => {
return {
canWrite: canWrite(x.path, x.extra_perms, $userStore) && x.workspace_id == $workspaceStore,
canWrite:
canWrite(x.path, x.extra_perms, $userStore) &&
x.workspace_id == $workspaceStore &&
!$userStore?.operator,
...x
}
})
@@ -77,7 +81,9 @@
apps = (await AppService.listApps({ workspace: $workspaceStore! })).map((app: ListableApp) => {
return {
canWrite:
canWrite(app.path!, app.extra_perms!, $userStore) && app.workspace_id == $workspaceStore,
canWrite(app.path!, app.extra_perms!, $userStore) &&
app.workspace_id == $workspaceStore &&
!$userStore?.operator,
...app
}
})
@@ -18,21 +18,21 @@
const mainMenuLinks = [
{ label: 'Home', href: '/', icon: faHomeAlt },
{ label: 'Runs', href: '/runs', icon: faPlay },
{ label: 'Variables', href: '/variables', icon: faDollarSign },
{ label: 'Resources', href: '/resources', icon: faCubes }
{ label: 'Variables', href: '/variables', icon: faDollarSign, disabled: $userStore?.operator },
{ label: 'Resources', href: '/resources', icon: faCubes, disabled: $userStore?.operator }
]
$: secondaryMenuLinks = [
{ label: 'Schedules', href: '/schedules', icon: faCalendar },
{ label: 'Groups', href: '/groups', icon: faUsersCog },
{ label: 'Audit Logs', href: '/audit_logs', icon: faEye },
{ label: 'Schedules', href: '/schedules', icon: faCalendar, disabled: $userStore?.operator },
{ label: 'Groups', href: '/groups', icon: faUsersCog, disabled: $userStore?.operator },
{ label: 'Audit Logs', href: '/audit_logs', icon: faEye, disabled: $userStore?.operator },
{
label: 'Workspace',
href: '/workspace_settings',
icon: faCog,
disabled: !($userStore?.is_admin || $superadmin)
},
{ label: 'Workers', href: '/workers', icon: faRobot }
{ label: 'Workers', href: '/workers', icon: faRobot, disabled: $userStore?.operator }
]
const thirdMenuLinks = [
@@ -3,7 +3,7 @@
import { userStore, usersWorkspaceStore, superadmin, usageStore } from '$lib/stores'
import { classNames, isCloudHosted } from '$lib/utils'
import { faCrown, faUser } from '@fortawesome/free-solid-svg-icons'
import { faCrown, faPlay, faUser } from '@fortawesome/free-solid-svg-icons'
import { createEventDispatcher } from 'svelte'
import Icon from 'svelte-awesome'
@@ -31,6 +31,8 @@
{$userStore?.username ?? ($superadmin ? $superadmin : '___')}
{#if $userStore?.is_admin || $superadmin}
<Icon data={faCrown} scale={0.6} />
{:else if $userStore?.operator}
<Icon class="ml-2" data={faPlay} scale={0.8} />
{/if}
</span>
{/if}
@@ -42,7 +44,15 @@
<p class="text-sm font-medium text-gray-900 truncate" role="none">
{$usersWorkspaceStore?.email}
</p>
<span class="text-xs text-gray-600">
{#if $userStore?.is_admin}
Admin of this workspace <Icon data={faCrown} scale={0.6} />
{:else if $userStore?.operator}
Operator in this workspace <Icon class="ml-2" data={faPlay} scale={0.8} />
{/if}
</span>
</div>
<div class="py-1" role="none">
<a
on:click={() => dispatch('user-settings')}
+1
View File
@@ -9,6 +9,7 @@ export interface UserExt {
username: string;
is_admin: boolean;
is_super_admin: boolean;
operator: boolean;
created_at: string;
groups: string[];
pgroups: string[];
+21 -19
View File
@@ -157,25 +157,27 @@
>
Run
</Button>
<Button
href="/flows/edit/{path}"
variant="contained"
color="blue"
size="xs"
startIcon={{ icon: faEdit }}
disabled={!can_write}
>
Edit
</Button>
<Button
href="/flows/add?template={flow.path}"
variant="contained"
color="blue"
size="xs"
startIcon={{ icon: faCodeFork }}
>
Use as template/Fork
</Button>
{#if !$userStore?.operator}
<Button
href="/flows/edit/{path}"
variant="contained"
color="blue"
size="xs"
startIcon={{ icon: faEdit }}
disabled={!can_write}
>
Edit
</Button>
<Button
href="/flows/add?template={flow.path}"
variant="contained"
color="blue"
size="xs"
startIcon={{ icon: faCodeFork }}
>
Use as template/Fork
</Button>
{/if}
<Button href="/runs/{flow.path}" color="blue" size="xs" startIcon={{ icon: faList }}>
View runs
</Button>
+28 -24
View File
@@ -1,6 +1,6 @@
<script lang="ts">
import { FlowService, type OpenFlow } from '$lib/gen'
import { workspaceStore } from '$lib/stores'
import { userStore, workspaceStore } from '$lib/stores'
import { Alert, Button, Drawer, DrawerContent, Tab, Tabs } from '$lib/components/common'
import PageHeader from '$lib/components/PageHeader.svelte'
import CreateActionsFlow from '$lib/components/flows/CreateActionsFlow.svelte'
@@ -130,32 +130,36 @@
{/if}
<PageHeader title="Home">
<div class="flex flex-row gap-2 flex-wrap">
<CreateApp />
<CreateActionsScript />
<CreateActionsFlow />
{#if !$userStore?.operator}
<CreateApp />
<CreateActionsScript />
<CreateActionsFlow />
{/if}
</div>
</PageHeader>
<Tabs bind:selected={tab}>
<Tab size="md" value="workspace">
<div class="flex gap-2 items-center my-1">
<Building size={18} />
Workspace
</div>
</Tab>
<Tab size="md" value="hubscripts">
<div class="flex gap-2 items-center my-1">
<Globe2 size={18} />
Hub Scripts
</div>
</Tab>
<Tab size="md" value="hubflows">
<div class="flex gap-2 items-center my-1">
<Globe2 size={18} />
Hub Flows
</div>
</Tab>
</Tabs>
{#if !$userStore?.operator}
<Tabs bind:selected={tab}>
<Tab size="md" value="workspace">
<div class="flex gap-2 items-center my-1">
<Building size={18} />
Workspace
</div>
</Tab>
<Tab size="md" value="hubscripts">
<div class="flex gap-2 items-center my-1">
<Globe2 size={18} />
Hub Scripts
</div>
</Tab>
<Tab size="md" value="hubflows">
<div class="flex gap-2 items-center my-1">
<Globe2 size={18} />
Hub Flows
</div>
</Tab>
</Tabs>
{/if}
<div class="my-2" />
<div class="flex flex-col gap-y-16">
<div class="flex flex-col">
+10 -8
View File
@@ -193,14 +193,16 @@
size="xs"
startIcon={{ icon: faRefresh }}>Run again</Button
>
{#if canWrite(job?.script_path ?? '', {}, $userStore)}
<Button
disabled={not_same_workspace}
href={editHref}
color="blue"
size="xs"
startIcon={{ icon: faEdit }}>Edit</Button
>
{#if !$userStore?.operator}
{#if canWrite(job?.script_path ?? '', {}, $userStore)}
<Button
disabled={not_same_workspace}
href={editHref}
color="blue"
size="xs"
startIcon={{ icon: faEdit }}>Edit</Button
>
{/if}
{/if}
<Button
disabled={not_same_workspace}
@@ -157,24 +157,26 @@
>
Run
</Button>
<Button
href={`/scripts/edit/${script.hash}?step=2`}
color="blue"
size="md"
startIcon={{ icon: faEdit }}
disabled={!can_write}
>
Edit
</Button>
{#if !topHash}
{#if !$userStore?.operator}
<Button
href={`/scripts/add?template=${script.path}`}
variant="border"
href={`/scripts/edit/${script.hash}?step=2`}
color="blue"
size="md"
startIcon={{ icon: faCodeFork }}
startIcon={{ icon: faEdit }}
disabled={!can_write}
>
Fork
Edit
</Button>
{#if !topHash}
<Button
href={`/scripts/add?template=${script.path}`}
variant="border"
size="md"
startIcon={{ icon: faCodeFork }}
>
Fork
</Button>
{/if}
{/if}
</div>
</div>
@@ -69,8 +69,7 @@
requestBody: args,
scheduledFor
})
sendUserToast(`Job <a href='/run/${run}?workspace=${$workspaceStore}'>${run}</a> started`)
goto('/run/' + run + '?workspace=' + $workspaceStore)
await goto('/run/' + run + '?workspace=' + $workspaceStore)
} catch (err) {
sendUserToast(`Could not create job: ${err.body}`, true)
}
@@ -8,6 +8,7 @@
import CenteredModal from '$lib/components/CenteredModal.svelte'
import { Button } from '$lib/components/common'
import Toggle from '$lib/components/Toggle.svelte'
import Tooltip from '$lib/components/Tooltip.svelte'
const rd = $page.url.searchParams.get('rd')
@@ -45,10 +46,10 @@
if (auto_invite) {
await WorkspaceService.editAutoInvite({
workspace: id,
requestBody: { set: true }
requestBody: { operator: operatorOnly }
})
}
sendUserToast(`Successfully created workspace id: ${id}`)
sendUserToast(`Created workspace id: ${id}`)
usersWorkspaceStore.set(await WorkspaceService.listUserWorkspaces())
workspaceStore.set(id)
@@ -94,6 +95,7 @@
$: domain = $usersWorkspaceStore?.email.split('@')[1]
let auto_invite = false
let operatorOnly = false
</script>
<CenteredModal title="New Workspace">
@@ -120,6 +122,16 @@
bind:checked={auto_invite}
options={{ right: `Auto invite users with the same domain (${domain})` }}
/>
{#if auto_invite}
<Toggle
bind:checked={operatorOnly}
options={{ right: `Auto invited users to join as operators` }}
/>
<Tooltip
>An operator can only execute and view scripts/flows/apps from your workspace, and only those
that he has visibility on</Tooltip
>
{/if}
{#if !isDomainAllowed}
<div class="text-gray-600 text-sm mb-4 mt-2">{domain} domain not allowed for auto-invite</div>
{/if}
@@ -131,6 +131,8 @@
<span class="font-mono font-semibold">{invite.workspace_id}</span>
{#if invite.is_admin}
<span class="text-sm">as an admin</span>
{:else if invite.operator}
<span class="text-sm">as an operator</span>
{/if}
</div>
<div class="flex justify-end items-center flex-col sm:flex-row gap-1">
+155 -59
View File
@@ -28,6 +28,9 @@
import Tooltip from '$lib/components/Tooltip.svelte'
import { faScroll, faBarsStaggered } from '@fortawesome/free-solid-svg-icons'
import SearchItems from '$lib/components/SearchItems.svelte'
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'
let users: User[] | undefined = undefined
let invites: WorkspaceInvite[] = []
@@ -38,6 +41,7 @@
let team_name: string | undefined
let auto_invite_domain: string | undefined
let itemKind: 'flow' | 'script' = 'flow'
let operatorOnly: boolean | undefined = undefined
// function getDropDownItems(username: string): DropdownItem[] {
// return [
@@ -76,6 +80,7 @@
const settings = await WorkspaceService.getSettings({ workspace: $workspaceStore! })
team_name = settings.slack_name
auto_invite_domain = settings.auto_invite_domain
operatorOnly = settings.auto_invite_operator
scriptPath = (settings.slack_command_script ?? '').split('/').slice(1).join('/')
initialPath = scriptPath
}
@@ -104,6 +109,34 @@
loadSettings()
}
}
$: operatorOnly != undefined && auto_invite_domain && setOperatorOnly()
async function removeAllInvitesFromDomain() {
await Promise.all(
invites
.filter((x) => x.email.endsWith('@' + auto_invite_domain ?? ''))
.map(({ email, is_admin, operator }) =>
WorkspaceService.deleteInvite({
workspace: $workspaceStore ?? '',
requestBody: {
email,
is_admin,
operator
}
})
)
)
}
async function setOperatorOnly() {
await removeAllInvitesFromDomain()
await WorkspaceService.editAutoInvite({
workspace: $workspaceStore ?? '',
requestBody: { operator: operatorOnly }
})
loadSettings()
listInvites()
}
</script>
<SearchItems
@@ -127,47 +160,94 @@
<tr slot="header-row">
<th>email</th>
<th>username</th>
<th>role</th>
<th colspan="3">jobs &amp; flows (<abbr title="past two weeks">2w</abbr>)</th>
<th
>executions (<abbr title="past 5 weeks">5w</abbr>) <Tooltip
>An execution is calculated as 1 for any runs of scripts + 1 for each seconds above
the first one</Tooltip
>
</th>
<th />
<th />
<th />
</tr>
<tbody slot="body">
{#if filteredUsers}
{#each filteredUsers as { email, username, is_admin, usage } (email)}
{#each filteredUsers as { email, username, is_admin, operator, usage, disabled } (email)}
<tr class="border">
<td>{email}</td>
<td>{username}</td>
<td>{is_admin ? 'admin' : 'user'}</td>
<td>{usage?.jobs}</td>
<td>{usage?.flows}</td>
<td>{msToSec(usage?.duration_ms)}s</td>
<td class="whitespace-nowrap"
><button
class="ml-2 text-red-500"
on:click={async () => {
await UserService.deleteUser({
workspace: $workspaceStore ?? '',
username
})
sendUserToast('User removed')
listUsers()
}}>remove</button
>
-
<button
class="text-blue-500"
on:click={async () => {
await UserService.updateUser({
workspace: $workspaceStore ?? '',
username,
requestBody: {
is_admin: !is_admin
}
})
sendUserToast('User updated')
listUsers()
}}>{is_admin ? 'demote' : 'promote'}</button
<td>{usage?.executions}</td>
<td
><div class="flex gap-1"
>{#if disabled}
<Badge color="red">disabled</Badge>
{/if}</div
></td
>
<td>
<div>
<ToggleButtonGroup
selected={is_admin ? 'admin' : operator ? 'operator' : 'author'}
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 UserService.updateUser({
workspace: $workspaceStore ?? '',
username,
requestBody: body
})
listUsers()
}}
>
<ToggleButton position="left" value="operator" size="xs"
>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="xs"
>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="xs">Admin</ToggleButton>
</ToggleButtonGroup>
</div>
</td>
<td>
<div class="flex gap-1">
<button
class="text-blue-500"
on:click={async () => {
await UserService.updateUser({
workspace: $workspaceStore ?? '',
username,
requestBody: {
disabled: !disabled
}
})
listUsers()
}}>{disabled ? 'enable' : 'disable'}</button
>
|
<button
class="text-red-500"
on:click={async () => {
await UserService.deleteUser({
workspace: $workspaceStore ?? '',
username
})
sendUserToast('User removed')
listUsers()
}}>remove</button
>
</div>
</td>
</tr>
{/each}
{:else}
@@ -196,10 +276,12 @@
<th />
</tr>
<tbody slot="body">
{#each invites as { email, is_admin }}
{#each invites as { email, is_admin, operator }}
<tr class="border">
<td>{email}</td>
<td>{is_admin ? 'admin' : 'user'}</td>
<td
>{#if operator}<Badge>operator</Badge>{:else if is_admin}<Badge>admin</Badge>{/if}
</td>
<td>
<button
class="ml-2 text-red-500"
@@ -208,11 +290,12 @@
workspace: $workspaceStore ?? '',
requestBody: {
email,
is_admin
is_admin,
operator
}
})
listInvites()
}}>remove</button
}}>cancel</button
></td
>
</tr>
@@ -224,29 +307,42 @@
<PageHeader title="Auto Invite" primary={false} />
<div class="flex gap-2">
{#if auto_invite_domain != domain}
<Button
disabled={!allowedAutoDomain}
on:click={async () => {
await WorkspaceService.editAutoInvite({
workspace: $workspaceStore ?? '',
requestBody: { set: true }
})
loadSettings()
listInvites()
}}>Set auto-invite to {domain}</Button
>
<div>
<Button
disabled={!allowedAutoDomain}
on:click={async () => {
await WorkspaceService.editAutoInvite({
workspace: $workspaceStore ?? '',
requestBody: { operator: false }
})
loadSettings()
listInvites()
}}>Set auto-invite to {domain}</Button
>
</div>
{/if}
{#if auto_invite_domain}
<Button
on:click={async () => {
await WorkspaceService.editAutoInvite({
workspace: $workspaceStore ?? '',
requestBody: { set: false }
})
loadSettings()
listInvites()
}}>Unset auto-invite from {auto_invite_domain}</Button
>
<div class="flex flex-col gap-y-2">
<Toggle
bind:checked={operatorOnly}
options={{
right: `Auto-invited users to join as operators`
}}
/>
<div>
<Button
on:click={async () => {
await removeAllInvitesFromDomain()
await WorkspaceService.editAutoInvite({
workspace: $workspaceStore ?? '',
requestBody: { operator: undefined }
})
loadSettings()
listInvites()
}}>Unset auto-invite from {auto_invite_domain} domain</Button
>
</div>
</div>
{/if}
</div>
{#if !allowedAutoDomain}
@@ -334,7 +430,7 @@
btnClasses="mt-2"
on:click={async () => {
await WorkspaceService.deleteWorkspace({ workspace: $workspaceStore ?? '' })
sendUserToast(`Successfully deleted workspace ${$workspaceStore}`)
sendUserToast(`Deleted workspace ${$workspaceStore}`)
workspaceStore.set(undefined)
usersWorkspaceStore.set(undefined)
goto('/user/workspaces')