feat(frontend): Settings rework (#1983)

* feat(frontend): rework settings wup

* feat(frontend): rework settings wop

* feat(frontend): rework settings wip

* feat(frontend): rework workers + folders

* feat(frontend): modify the nord theme

* feat(frontend): folder columns size

* feat(frontend): Fix build
This commit is contained in:
Faton Ramadani
2023-08-03 12:24:38 +02:00
committed by GitHub
parent c7f881a057
commit b8e9338d72
23 changed files with 855 additions and 661 deletions
+64 -37
View File
@@ -2,11 +2,13 @@
import { createEventDispatcher } from 'svelte'
import { globalEmailInvite, superadmin, workspaceStore } from '$lib/stores'
import { UserService, WorkspaceService } from '$lib/gen'
import { Button, ToggleButton, ToggleButtonGroup } from './common'
import Tooltip from './Tooltip.svelte'
import { Button, Popup } from './common'
import { sendUserToast } from '$lib/toast'
import { isCloudHosted } from '$lib/cloud'
import { goto } from '$app/navigation'
import ToggleButtonGroup from './common/toggleButton-v2/ToggleButtonGroup.svelte'
import ToggleButton from './common/toggleButton-v2/ToggleButton.svelte'
import { UserPlus } from 'lucide-svelte'
const dispatch = createEventDispatcher()
@@ -62,39 +64,64 @@
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
<Popup
floatingConfig={{ strategy: 'absolute', placement: 'bottom-end' }}
containerClasses="border rounded-lg shadow-lg p-4 bg-surface"
>
<svelte:fragment slot="button">
<Button color="dark" size="xs" nonCaptureEvent={true}>
<div class="flex flex-row gap-1 items-center">
<UserPlus size={16} />
Add new user
</div>
</Button>
</svelte:fragment>
<div class="flex flex-col w-72 p-2">
<span class="text-sm mb-2 leading-6 font-semibold">Add a new user</span>
<span class="text-xs mb-1 leading-6">Email</span>
<input type="email mb-1" on:keyup={handleKeyUp} placeholder="email" bind:value={email} />
<span class="text-xs mb-1 pt-2 leading-6">Username</span>
<input type="text" on:keyup={handleKeyUp} placeholder="username" bind:value={username} />
<span class="text-xs mb-1 pt-2 leading-6">Role</span>
<ToggleButtonGroup bind:selected class="mb-4">
<ToggleButton
value="operator"
size="sm"
label="Operator"
tooltip="An operator can only execute and view scripts/flows/apps from your workspace, and only those that he has visibility on."
/>
<ToggleButton
position="center"
value="author"
size="sm"
label="Author"
tooltip="An Author can execute and view scripts/flows/apps, but he can also create new ones."
/>
<ToggleButton
position="right"
value="admin"
size="sm"
label="Admin"
tooltip="An admin has full control over a specific Windmill workspace, including the ability to manage users, edit entities, and control permissions within the workspace."
/>
</ToggleButtonGroup>
<Button
variant="contained"
color="blue"
size="sm"
on:click={() => {
addUser()
// @ts-ignore
email = undefined
// @ts-ignore
username = undefined
}}
disabled={email === undefined || username === undefined}
>
<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<Tooltip
>An admin has full control over a specific Windmill workspace, including the ability to manage users, edit entities, and control permissions within the workspace.</Tooltip
></ToggleButton>
</ToggleButtonGroup>
<Button
variant="contained"
color="blue"
size="sm"
btnClasses="!ml-8"
on:click={addUser}
disabled={email === undefined}
>
Add
</Button>
</div>
Add
</Button>
</div>
</Popup>
@@ -16,7 +16,7 @@
token: ''
},
{
foreground: '616e88',
foreground: '808b9f',
token: 'comment'
},
{
+27 -22
View File
@@ -9,12 +9,13 @@
} from '$lib/gen'
import AutoComplete from 'simple-svelte-autocomplete'
import TableCustom from './TableCustom.svelte'
import { Alert, Button, Drawer, DrawerContent, ToggleButton, ToggleButtonGroup } from './common'
import { Alert, Button, Drawer, DrawerContent } from './common'
import Skeleton from './common/skeleton/Skeleton.svelte'
import Tooltip from './Tooltip.svelte'
import { Icon } from 'svelte-awesome'
import { faEye, faPlus } from '@fortawesome/free-solid-svg-icons'
import GroupEditor from './GroupEditor.svelte'
import ToggleButtonGroup from './common/toggleButton-v2/ToggleButtonGroup.svelte'
import ToggleButton from './common/toggleButton-v2/ToggleButton.svelte'
export let name: string
let can_write = false
@@ -152,8 +153,8 @@
<div class="flex items-center gap-1">
<div>
<ToggleButtonGroup bind:selected={ownerKind} on:selected={() => (ownerItem = '')}>
<ToggleButton position="left" value="user" size="xs">User</ToggleButton>
<ToggleButton position="right" value="group" size="xs">Group</ToggleButton>
<ToggleButton value="user" size="xs" label="User" />
<ToggleButton value="group" size="xs" label="Group" />
</ToggleButtonGroup>
</div>
@@ -271,24 +272,28 @@
loadFolder()
}}
>
<ToggleButton position="left" value="viewer" size="xs"
>Viewer&nbsp;<Tooltip
>A viewer of a folder has read-only access to all the elements
(scripts/flows/apps/schedules/resources/variables) inside the folder</Tooltip
></ToggleButton
>
<ToggleButton position="center" value="writer" size="xs"
>Writer&nbsp;<Tooltip
>A writer of a folder has read AND write access to all the elements
(scripts/flows/apps/schedules/resources/variables) inside the folder</Tooltip
></ToggleButton
>
<ToggleButton position="right" value="admin" size="xs"
>Admin&nbsp;<Tooltip
>An admin of a folder has read AND write access to all the elements inside
the folders and can manage the permissions as well as add new admins</Tooltip
></ToggleButton
>
<ToggleButton
value="viewer"
size="xs"
label="Viewer"
tooltip="A viewer of a folder has read-only access to all the elements (scripts/flows/apps/schedules/resources/variables) inside the folder"
/>
<ToggleButton
position="center"
value="writer"
size="xs"
label="Writer"
tooltip="A writer of a folder has read AND write access to all the elements (scripts/flows/apps/schedules/resources/variables) inside the folder"
/>
<ToggleButton
position="right"
value="admin"
size="xs"
label="Admin"
tooltip="An admin of a folder has read AND write access to all the elements inside the folders and can manage the permissions as well as add new admins"
/>
</ToggleButtonGroup>
</div>
{:else}
@@ -4,12 +4,12 @@
export let members: string[]
</script>
<Popover
><div class="inline-flex gap-1 items-end"
><span class="text-tertiary text-xs mb-0.5">({members.length})</span>
<div class="max-w-xs truncate"
><span class="text-tertiary text-xs">{members?.join(', ')}</span></div
></div
>
<span slot="text">{members?.join(', ')}</span></Popover
>
<Popover>
<div class="inline-flex gap-1 items-center">
<span class="text-tertiary text-xs">({members.length}) </span>
<div class="max-w-xs truncate">
<span class="text-tertiary text-xs">{members?.join(', ')} </span>
</div>
</div>
<span slot="text">{members?.join(', ')} </span>
</Popover>
@@ -1,6 +1,7 @@
<script lang="ts">
import { FolderService } from '$lib/gen'
import { workspaceStore } from '$lib/stores'
import Cell from './table/Cell.svelte'
export let name: string
export let tabular = false
@@ -17,9 +18,9 @@
{#if tabular}
{#each order as key}
<td class="text-center">
<Cell class="w-20">
{usage[key] ?? ''}
</td>
</Cell>
{/each}
{:else}
<div class="flex flex-col text-xs text-tertiary">
+17 -14
View File
@@ -10,12 +10,13 @@
import AutoComplete from 'simple-svelte-autocomplete'
import { createEventDispatcher } from 'svelte'
import autosize from 'svelte-autosize'
import { Button, ToggleButton, ToggleButtonGroup } from './common'
import { Button } from './common'
import Skeleton from './common/skeleton/Skeleton.svelte'
import TableCustom from './TableCustom.svelte'
import Tooltip from './Tooltip.svelte'
import { sendUserToast } from '$lib/toast'
import { canWrite } from '$lib/utils'
import ToggleButtonGroup from './common/toggleButton-v2/ToggleButtonGroup.svelte'
import ToggleButton from './common/toggleButton-v2/ToggleButton.svelte'
export let name: string
let can_write = false
@@ -215,24 +216,26 @@
loadGroup()
}}
>
<ToggleButton position="left" value="member" size="xs"
>Member <Tooltip
>A Member of a group can see everything the group can see, write to
everything the group can write, and generally act on behalf of the group</Tooltip
></ToggleButton
>
<ToggleButton
value="member"
size="xs"
label="Member"
tooltip="A Member of a group can see everything the group can see, write to everything the group can write, and generally act on behalf of the group"
/>
<!-- <ToggleButton position="center" value="manager" size="xs"
>Manager <Tooltip
>A manager of a group can manage the group, adding and removing users and
change their roles. Being a manager does not make you a member.</Tooltip
></ToggleButton
> -->
<ToggleButton position="right" value="admin" size="xs"
>Admin <Tooltip
>An admin of a group is a member of a group that can also add and remove
members to the group, or make them admin.</Tooltip
></ToggleButton
>
<ToggleButton
position="right"
value="admin"
size="xs"
label="Admin"
tooltip="An admin of a group is a member of a group that can also add and remove members to the group, or make them admin."
/>
</ToggleButtonGroup>
</div>
{:else}
+58 -30
View File
@@ -3,10 +3,12 @@
import { createEventDispatcher } from 'svelte'
import { globalEmailInvite, superadmin, workspaceStore } from '$lib/stores'
import { UserService, WorkspaceService } from '$lib/gen'
import { Button, ToggleButton, ToggleButtonGroup } from './common'
import Tooltip from './Tooltip.svelte'
import { Button, Popup } from './common'
import { isCloudHosted } from '$lib/cloud'
import { goto } from '$app/navigation'
import ToggleButtonGroup from './common/toggleButton-v2/ToggleButtonGroup.svelte'
import ToggleButton from './common/toggleButton-v2/ToggleButton.svelte'
import { MailPlus } from 'lucide-svelte'
const dispatch = createEventDispatcher()
@@ -55,37 +57,63 @@
}
}
dispatch('new')
email = ''
}
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" />
<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
<Popup floatingConfig={{ strategy: 'absolute', placement: 'bottom-end' }}>
<svelte:fragment slot="button">
<Button color="dark" size="xs" nonCaptureEvent={true}>
<div class="flex flex-row gap-1 items-center">
<MailPlus size={16} />
Invite
</div>
</Button>
</svelte:fragment>
<div class="flex flex-col gap-2">
<input
type="email"
on:keyup={handleKeyUp}
placeholder="email"
bind:value={email}
class="mr-4"
/>
<ToggleButtonGroup bind:selected>
<ToggleButton
value="operator"
size="sm"
label="Operator"
tooltip="An operator can only execute and view scripts/flows/apps from your workspace, and only those that he has visibility on."
/>
<ToggleButton
position="center"
value="author"
size="sm"
label="Author"
tooltip="An Author can execute and view scripts/flows/apps, but he can also create new ones."
/>
<ToggleButton
position="right"
value="admin"
size="sm"
label="Admin"
tooltip="An admin has full control over a specific Windmill workspace, including the ability to manage users, edit entities, and control permissions within the workspace."
/>
</ToggleButtonGroup>
<Button
variant="contained"
color="blue"
size="sm"
on:click={inviteUser}
disabled={email === undefined}
>
<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<Tooltip
>An admin has full control over a specific Windmill workspace, including the ability to manage users, edit entities, and control permissions within the workspace.</Tooltip
></ToggleButton>
</ToggleButtonGroup>
<Button
variant="contained"
color="blue"
size="sm"
btnClasses="!ml-8"
on:click={inviteUser}
disabled={email === undefined}
>
Invite
</Button>
</div>
Invite
</Button>
</div>
</Popup>
+10 -6
View File
@@ -7,19 +7,23 @@
export let primary: boolean = true
</script>
<div class="flex flex-row flex-wrap justify-between mt-4 mb-2 min-h-[48px]">
<div class="flex flex-row flex-wrap justify-between pt-6 pb-2 my-4">
{#if primary}
<span class="flex items-center space-x-2 mb-2">
<h1>{title}</h1>
<span class="flex items-center space-x-2">
<h1 class="!text-2xl font-semibold leading-6 tracking-tight">{title}</h1>
{#if tooltip != '' || documentationLink}
<Tooltip {documentationLink} scale={0.9}>{tooltip}</Tooltip>
<Tooltip light {documentationLink} scale={0.9} wrapperClass="flex items-center">
{tooltip}
</Tooltip>
{/if}
</span>
{:else}
<span class="flex items-center space-x-2">
<h2>{title}</h2>
<h2 class="!text-sm font-semibold">{title}</h2>
{#if tooltip != '' || documentationLink}
<Tooltip>{tooltip}</Tooltip>
<Tooltip light {documentationLink} scale={0.9} wrapperClass="flex items-center">
{tooltip}
</Tooltip>
{/if}
</span>
{/if}
@@ -6,11 +6,13 @@
import { createEventDispatcher } from 'svelte'
import AutoComplete from 'simple-svelte-autocomplete'
import { userStore, workspaceStore } from '$lib/stores'
import { Alert, Button, Drawer, ToggleButton, ToggleButtonGroup } from './common'
import { Alert, Button, Drawer } from './common'
import DrawerContent from './common/drawer/DrawerContent.svelte'
import Tooltip from './Tooltip.svelte'
import { sendUserToast } from '$lib/toast'
import { isOwner } from '$lib/utils'
import ToggleButtonGroup from './common/toggleButton-v2/ToggleButtonGroup.svelte'
import ToggleButton from './common/toggleButton-v2/ToggleButton.svelte'
const dispatch = createEventDispatcher()
@@ -117,8 +119,8 @@
<div class="flex flex-row flex-wrap gap-2 items-center">
<div>
<ToggleButtonGroup bind:selected={ownerKind} on:selected={() => (owner = '')}>
<ToggleButton position="left" value="user" size="xs">User</ToggleButton>
<ToggleButton position="right" value="group" size="xs">Group</ToggleButton>
<ToggleButton value="user" size="xs" label="User" />
<ToggleButton value="group" size="xs" label="Group" />
</ToggleButtonGroup>
</div>
{#key ownerKind}
@@ -155,9 +157,8 @@
loadAcls()
}}
>
<ToggleButton position="left" value="viewer" size="xs">Viewer</ToggleButton>
<ToggleButton position="right" value="writer" size="xs">Writer</ToggleButton
>
<ToggleButton value="viewer" size="xs" label="Viewer" />
<ToggleButton value="writer" size="xs" label="Writer" />
</ToggleButtonGroup>
</div>
{:else}{write}{/if}</td
@@ -15,8 +15,6 @@ export { default as Skeleton } from './skeleton/Skeleton.svelte'
export { default as Tab } from './tabs/Tab.svelte'
export { default as TabContent } from './tabs/TabContent.svelte'
export { default as Tabs } from './tabs/Tabs.svelte'
export { default as ToggleButton } from './toggleButton/ToggleButton.svelte'
export { default as ToggleButtonGroup } from './toggleButton/ToggleButtonGroup.svelte'
export { default as Breadcrumb } from './breadcrumb/Breadcrumb.svelte'
export { default as FileInput } from './fileInput/FileInput.svelte'
@@ -26,7 +26,7 @@
<button
class={twMerge(
'border-b-2 py-1 px-4 cursor-pointer transition-all z-10 ease-linear font-medium',
'border-b-2 py-1 px-4 cursor-pointer transition-all z-10 ease-linear font-medium text-tertiary',
$selected?.startsWith(value)
? 'wm-tab-active'
: 'border-gray-300 dark:border-gray-600 border-opacity-0 hover:border-opacity-100 ',
@@ -1,28 +0,0 @@
<script lang="ts">
import { classNames } from '$lib/utils'
import { getContext } from 'svelte'
import Button from '../button/Button.svelte'
import type { ToggleButtonContext } from './ToggleButtonGroup.svelte'
export let value: any
export let position: 'left' | 'center' | 'right'
export let light = false
export let title: string | undefined = undefined
const { select, selected } = getContext<ToggleButtonContext>('ToggleButtonGroup')
</script>
<Button
{...$$props}
{title}
on:click={() => select(value)}
btnClasses={classNames(
'border-gray-200 focus:ring-0 w-full',
position === 'left' ? 'rounded-none rounded-l-lg border' : '',
position === 'center' ? 'rounded-none border-t border-b border-r' : '',
position === 'right' ? 'rounded-none rounded-r-md border-r border-y' : ''
)}
color={$selected === value ? (light ? 'gray' : 'dark') : 'light'}
variant="contained"
>
<slot />
</Button>
@@ -1,36 +0,0 @@
<script context="module" lang="ts">
export type ToggleButtonContext = {
selected: Writable<any>
select: (value: any) => void
}
</script>
<script lang="ts">
import { createEventDispatcher, setContext } from 'svelte'
import { writable, type Writable } from 'svelte/store'
export let selected: any
export let col = false
const dispatch = createEventDispatcher()
const selectedContent = writable(selected)
function setSelected(selected: any) {
selectedContent.set(selected)
}
$: setSelected(selected)
$: $selectedContent && dispatch('selected', $selectedContent)
setContext<ToggleButtonContext>('ToggleButtonGroup', {
selected: selectedContent,
select: (value: any) => {
selectedContent.set(value)
selected = value
}
})
</script>
<div class:flex-col={col} class="flex rounded-md {$$props.class}" role="group">
<slot />
</div>
@@ -0,0 +1,372 @@
<script lang="ts">
import AddUser from '$lib/components/AddUser.svelte'
import { Badge, Button, Skeleton } from '$lib/components/common'
import ToggleButton from '$lib/components/common/toggleButton-v2/ToggleButton.svelte'
import ToggleButtonGroup from '$lib/components/common/toggleButton-v2/ToggleButtonGroup.svelte'
import InviteUser from '$lib/components/InviteUser.svelte'
import PageHeader from '$lib/components/PageHeader.svelte'
import DataTable from '$lib/components/table/DataTable.svelte'
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 { UserService, WorkspaceService, type WorkspaceInvite } from '$lib/gen'
import { userStore, workspaceStore } from '$lib/stores'
import { sendUserToast } from '$lib/toast'
import { Search } from 'lucide-svelte'
import SearchItems from '../SearchItems.svelte'
import Cell from '../table/Cell.svelte'
import Row from '../table/Row.svelte'
let users: User[] | undefined = undefined
let invites: WorkspaceInvite[] = []
let filteredUsers: User[] | undefined = undefined
let userFilter = ''
let auto_invite_domain: string | undefined
let operatorOnly: boolean | undefined = undefined
let nbDisplayed = 30
async function loadSettings(): Promise<void> {
const settings = await WorkspaceService.getSettings({ workspace: $workspaceStore! })
auto_invite_domain = settings.auto_invite_domain
operatorOnly = settings.auto_invite_operator
}
async function listUsers(): Promise<void> {
users = await UserService.listUsers({ workspace: $workspaceStore! })
}
async function listInvites(): Promise<void> {
invites = await WorkspaceService.listPendingInvites({ workspace: $workspaceStore! })
}
let allowedAutoDomain = false
async function getDisallowedAutoDomain() {
allowedAutoDomain = await WorkspaceService.isDomainAllowed()
}
$: domain = $userStore?.email.split('@')[1]
$: {
if ($workspaceStore) {
getDisallowedAutoDomain()
listUsers()
listInvites()
loadSettings()
}
}
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
}
})
)
)
}
</script>
<SearchItems
filter={userFilter}
items={users}
bind:filteredItems={filteredUsers}
f={(x) => x.email + ' ' + x.name + ' ' + x.company}
/>
<div class="flex flex-row justify-between items-center">
<PageHeader
title="Members ({filteredUsers?.length ?? users?.length ?? ''})"
primary={false}
tooltip="Manage users manually or enable SSO authentication."
documentationLink="https://www.windmill.dev/docs/core_concepts/authentification"
/>
<div class="flex flex-row items-center gap-2 relative">
<input placeholder="Filter members" bind:value={userFilter} class="input !pl-8" />
<Search class="absolute left-2" size={14} />
<AddUser on:new={listUsers} />
</div>
</div>
<div class="">
<DataTable
shouldLoadMore={(filteredUsers?.length ?? 0) > 30}
loadMore={30}
on:loadMore={() => {
nbDisplayed += 30
}}
>
<Head>
<tr>
<Cell head first>Email</Cell>
<Cell head>Username</Cell>
<Cell head>
Executions (<abbr title="past 5 weeks">5w</abbr>)
<Tooltip light>
An execution is calculated as 1 for any runs of scripts + 1 for each seconds above the
first one
</Tooltip>
</Cell>
<Cell head>Status</Cell>
<Cell head>Role</Cell>
<Cell head last>
<span class="sr-only">Actions</span>
</Cell>
</tr>
</Head>
<tbody class="divide-y bg-surface">
{#if filteredUsers}
{#each filteredUsers.slice(0, nbDisplayed) as { email, username, is_admin, operator, usage, disabled } (email)}
<tr class="!hover:bg-surface-hover">
<Cell first>{email}</Cell>
<Cell>{username}</Cell>
<Cell>{usage?.executions}</Cell>
<Cell>
<div class="flex gap-1">
{#if disabled}
<Badge color="red">Disabled</Badge>
{:else}
<Badge color="green">Enabled</Badge>
{/if}
</div>
</Cell>
<Cell>
<div>
<ToggleButtonGroup
selected={is_admin ? 'admin' : operator ? 'operator' : 'author'}
on:selected={async (e) => {
if (is_admin && email == $userStore?.email && e.detail != 'admin') {
sendUserToast(
'Admins cannot be demoted by themselves, ask another admin to demote you',
true
)
e.preventDefault()
listUsers()
return
}
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
value="operator"
size="xs"
label="Operator"
tooltip="An operator can only execute and view scripts/flows/apps from your workspace, and only those that he has visibility on."
/>
<ToggleButton
value="author"
size="xs"
label="Author"
tooltip="An Author can execute and view scripts/flows/apps, but he can also create new ones."
/>
<ToggleButton
value="admin"
size="xs"
label="Admin"
tooltip="An admin has full control over a specific Windmill workspace, including the ability to manage users, edit entities, and control permissions within the workspace."
/>
</ToggleButtonGroup>
</div>
</Cell>
<Cell>
<div class="flex gap-1">
<Button
color="light"
variant="contained"
size="xs"
spacingSize="xs2"
on:click={async () => {
await UserService.updateUser({
workspace: $workspaceStore ?? '',
username,
requestBody: {
disabled: !disabled
}
})
listUsers()
}}
>
{disabled ? 'Enable' : 'Disable'}
</Button>
<Button
color="light"
variant="contained"
btnClasses="text-red-500"
size="xs"
spacingSize="xs2"
on:click={async () => {
await UserService.deleteUser({
workspace: $workspaceStore ?? '',
username
})
sendUserToast('User removed')
listUsers()
}}
>
Remove
</Button>
</div>
</Cell>
</tr>
{/each}
{:else}
{#each new Array(6) as _}
<tr class="border">
<td colspan={6}>
<Skeleton layout={[[4]]} />
</td>
</tr>
{/each}
{/if}
</tbody>
</DataTable>
</div>
<PageHeader
title="Invites ({invites.length ?? ''})"
primary={false}
tooltip="Manage invites on your workspace."
documentationLink="https://www.windmill.dev/docs/core_concepts/authentification#adding-users-to-a-workspace"
>
<InviteUser on:new={listInvites} />
</PageHeader>
<div class="max-h-screen">
<DataTable>
<Head>
<tr>
<Cell head first>Email</Cell>
<Cell head>Role</Cell>
<Cell head last><span class="sr-only">Actions</span></Cell>
</tr>
</Head>
<tbody class="divide-y bg-surface">
{#if invites?.length > 0}
{#each invites as { email, is_admin, operator }}
<Row>
<Cell first>{email}</Cell>
<Cell>
{#if operator}
<Badge>Operator</Badge>
{:else if is_admin}
<Badge>Admin</Badge>
{:else}
<Badge>Author</Badge>
{/if}
</Cell>
<Cell last>
<button
class="ml-2 text-red-500"
on:click={async () => {
await WorkspaceService.deleteInvite({
workspace: $workspaceStore ?? '',
requestBody: {
email,
is_admin,
operator
}
})
listInvites()
}}
>
Cancel
</button>
</Cell>
</Row>
{/each}
{:else}
<tr>
<td colspan="3" class="text-center py-8">
<div class="text-xs text-secondary"> No invites yet </div>
</td>
</tr>
{/if}
</tbody>
</DataTable>
</div>
<div class="mt-10" />
<PageHeader
title="Auto Invite"
tooltip="Auto invite to the workspace users from your domain."
documentationLink="https://www.windmill.dev/docs/core_concepts/authentification#auto-invite"
primary={false}
/>
<div class="flex gap-2">
{#if auto_invite_domain != domain}
<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}
<div class="flex flex-col gap-y-2">
<Toggle
bind:checked={operatorOnly}
options={{
right: `Auto-invited users to join as operators`
}}
on:change={async (e) => {
await removeAllInvitesFromDomain()
await WorkspaceService.editAutoInvite({
workspace: $workspaceStore ?? '',
requestBody: { operator: e.detail }
})
loadSettings()
listInvites()
}}
/>
<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}
<div class="text-red-400 text-xs mb-2">{domain} domain not allowed for auto-invite</div>
{/if}
@@ -0,0 +1,25 @@
<script lang="ts">
import { twMerge } from 'tailwind-merge'
export let first: boolean = false
export let last: boolean = false
export let numeric: boolean = false
export let head: boolean = false
let Tag = head ? 'th' : 'td'
</script>
<svelte:element
this={Tag}
{...$$restProps}
class={twMerge(
'px-2 py-3.5 text-left text-xs text-primary font-normal whitespace-nowrap',
first ? 'sm:pl-6' : '',
last ? 'sm:pr-6' : '',
numeric ? 'text-right' : '',
head ? 'font-semibold ' : '',
$$restProps.class
)}
>
<slot />
</svelte:element>
@@ -0,0 +1,53 @@
<script lang="ts">
import { createEventDispatcher } from 'svelte'
import Button from '../common/button/Button.svelte'
import { ArrowLeftIcon, ArrowRightIcon } from 'lucide-svelte'
export let paginated: boolean = false
export let currentPage: number = 1
export let showNext: boolean = true
export let loadMore: number = 0
export let shouldLoadMore: boolean = false
const dispatch = createEventDispatcher()
</script>
<div class="rounded-md border overflow-hidden">
<table class="min-w-full divide-y">
<slot />
</table>
{#if paginated}
<div class="bg-surface border-t flex flex-row justify-end p-1 items-center gap-2">
<span class="text-xs">Page: {currentPage}</span>
<Button
color="light"
size="xs2"
on:click={() => dispatch('previous')}
disabled={currentPage === 1}
>
<div class="flex flex-row gap-1 items-center">
<ArrowLeftIcon size={16} />
Previous
</div>
</Button>
{#if showNext}
<Button color="light" size="xs2" on:click={() => dispatch('next')}>
<div class="flex flex-row gap-1 items-center">
Next
<ArrowRightIcon size={16} />
</div>
</Button>
{/if}
</div>
{:else if shouldLoadMore}
<div class="bg-surface border-t flex flex-row justify-end p-1 items-center gap-2">
<Button color="light" size="xs2" on:click={() => dispatch('loadMore')}>
<div class="flex flex-row gap-1 items-center">
Load {loadMore} more
<ArrowRightIcon size={16} />
</div>
</Button>
</div>
{/if}
</div>
@@ -0,0 +1,3 @@
<thead class="bg-surface-secondary">
<slot />
</thead>
@@ -0,0 +1,21 @@
<script lang="ts">
import { createEventDispatcher } from 'svelte'
import { twMerge } from 'tailwind-merge'
export let hoverable: boolean = false
export let selected: boolean = false
const dispatch = createEventDispatcher()
</script>
<tr
class={twMerge(
hoverable ? 'hover:bg-surface-hover cursor-pointer' : '',
selected ? 'bg-surface-selected' : '',
'transition-all'
)}
on:click={() => {
dispatch('click')
}}
>
<slot />
</tr>
@@ -6,14 +6,16 @@
import Dropdown from '$lib/components/Dropdown.svelte'
import FolderEditor from '$lib/components/FolderEditor.svelte'
import PageHeader from '$lib/components/PageHeader.svelte'
import SharedBadge from '$lib/components/SharedBadge.svelte'
import TableCustom from '$lib/components/TableCustom.svelte'
import { userStore, workspaceStore } from '$lib/stores'
import { faEdit, faPlus, faTrash } from '@fortawesome/free-solid-svg-icons'
import { Button, Drawer, DrawerContent, Skeleton } from '$lib/components/common'
import FolderInfo from '$lib/components/FolderInfo.svelte'
import FolderUsageInfo from '$lib/components/FolderUsageInfo.svelte'
import { canWrite } from '$lib/utils'
import DataTable from '$lib/components/table/DataTable.svelte'
import Head from '$lib/components/table/Head.svelte'
import Cell from '$lib/components/table/Cell.svelte'
import Row from '$lib/components/table/Row.svelte'
type FolderW = Folder & { canWrite: boolean }
@@ -95,19 +97,21 @@
</PageHeader>
<div class="relative mb-20 pt-8">
<TableCustom>
<tr slot="header-row">
<th class="!px-0" />
<th>Name</th>
<th class="!text-center">Scripts</th>
<th class="!text-center">Flows</th>
<th class="!text-center">Apps</th>
<th class="!text-center">Schedules</th>
<th class="!text-center">Variables</th>
<th class="!text-center">Resources</th>
<th>Participants</th>
</tr>
<tbody slot="body">
<DataTable>
<Head>
<tr>
<Cell head first>Name</Cell>
<Cell head class="w-20">Scripts</Cell>
<Cell head class="w-20">Flows</Cell>
<Cell head class="w-20">Apps</Cell>
<Cell head class="w-20">Schedules</Cell>
<Cell head class="w-20">Variables</Cell>
<Cell head class="w-20">Resources</Cell>
<Cell head class="w-20">Participants</Cell>
<Cell head last />
</tr>
</Head>
<tbody class="divide-y">
{#if folders === undefined}
{#each new Array(4) as _}
<tr>
@@ -119,29 +123,25 @@
{:else}
{#if folders.length === 0}
<tr>
<td colspan="4" class="text-tertiary mt-2"> No folders yet, create one! </td>
<td colspan="4" class="text-tertiary mt-2">No folders yet, create one!</td>
</tr>
{/if}
{#each folders as { name, extra_perms, owners, canWrite }}
<tr>
<td class="!px-0 text-center">
<SharedBadge {canWrite} extraPerms={extra_perms} />
</td>
<td>
<a
href="#{name}"
on:click={() => {
editFolderName = name
folderDrawer.openDrawer()
}}
>{name}
</a>
</td>
<Row
hoverable
on:click={() => {
editFolderName = name
folderDrawer.openDrawer()
}}
>
<Cell first>
<span class="text-blue-500">{name}</span>
</Cell>
<FolderUsageInfo {name} tabular />
<td><FolderInfo members={computeMembers(owners, extra_perms)} /></td>
<td>
<Cell><FolderInfo members={computeMembers(owners, extra_perms)} /></Cell>
<Cell>
<Dropdown
placement="bottom-end"
dropdownItems={[
@@ -170,11 +170,11 @@
}
]}
/>
</td>
</tr>
</Cell>
</Row>
{/each}
{/if}
</tbody>
</TableCustom>
</DataTable>
</div>
</CenteredPage>
@@ -55,7 +55,7 @@
function computeJobKinds(jobKindsCat: string | undefined): string {
if (jobKindsCat == 'all') {
return `${CompletedJob.job_kind.SCRIPT},${CompletedJob.job_kind.FLOW},${CompletedJob.job_kind.DEPENDENCIES},${CompletedJob.job_kind.PREVIEW},${CompletedJob.job_kind.FLOWPREVIEW},${CompletedJob.job_kind.SCRIPT_HUB},${CompletedJob.job_kind.HTTP}`
return `${CompletedJob.job_kind.SCRIPT},${CompletedJob.job_kind.FLOW},${CompletedJob.job_kind.DEPENDENCIES},${CompletedJob.job_kind.PREVIEW},${CompletedJob.job_kind.FLOWPREVIEW},${CompletedJob.job_kind.SCRIPT_HUB}`
} else if (jobKindsCat == 'dependencies') {
return CompletedJob.job_kind.DEPENDENCIES
} else if (jobKindsCat == 'previews') {
@@ -3,7 +3,9 @@
import { Skeleton } from '$lib/components/common'
import Badge from '$lib/components/common/badge/Badge.svelte'
import PageHeader from '$lib/components/PageHeader.svelte'
import TableCustom from '$lib/components/TableCustom.svelte'
import Cell from '$lib/components/table/Cell.svelte'
import DataTable from '$lib/components/table/DataTable.svelte'
import Head from '$lib/components/table/Head.svelte'
import Tooltip from '$lib/components/Tooltip.svelte'
import { WorkerService, type WorkerPing } from '$lib/gen'
import { sendUserToast } from '$lib/toast'
@@ -64,46 +66,58 @@
{#if groupedWorkers.length == 0}
<p>No workers seems to be available</p>
{/if}
{#each groupedWorkers as [section, workers]}
<div class="mt-6">
Instance: {section} | IP:
<Badge large color="gray">{workers[0].ip}</Badge>
</div>
<TableCustom>
<tr slot="header-row">
<th>Worker</th>
<th
>Custom Tags <Tooltip
documentationLink="https://www.windmill.dev/docs/core_concepts/worker_groups#assign-custom-worker-groups"
>If defined, the workers only pull jobs with the same corresponding tag</Tooltip
></th
>
<th>Last ping</th>
<th>Worker start</th>
<th>Nb of jobs executed</th>
<th>Liveness</th>
<DataTable>
<Head>
<tr>
<Cell head first>Worker</Cell>
<Cell head>
<div class="flex flex-row items-center gap-1">
Custom Tags
<Tooltip
light
documentationLink="https://www.windmill.dev/docs/core_concepts/worker_groups#assign-custom-worker-groups"
>
If defined, the workers only pull jobs with the same corresponding tag
</Tooltip>
</div>
</Cell>
<Cell head>Last ping</Cell>
<Cell head>Worker start</Cell>
<Cell head>Nb of jobs executed</Cell>
<Cell head last>Liveness</Cell>
</tr>
<tbody slot="body">
</Head>
<tbody>
{#each groupedWorkers as [section, workers]}
<tr class="border-t">
<Cell first colspan="6" scope="colgroup" class="bg-surface-secondary/60 py-2 border-b">
Instance: <Badge color="gray">{section}</Badge>
IP: <Badge color="gray">{workers[0].ip}</Badge>
</Cell>
</tr>
{#if workers}
{#each workers as { worker, custom_tags, last_ping, started_at, jobs_executed }}
<tr>
<td class="py-1">{worker}</td>
<td class="py-1">{custom_tags?.join(', ') ?? ''}</td>
<td class="py-1"
>{last_ping != undefined ? last_ping + timeSinceLastPing : -1}s ago</td
>
<td class="py-1">{displayDate(started_at)}</td>
<td class="py-1">{jobs_executed}</td>
<td class="py-1"
>{last_ping != undefined ? (last_ping < 60 ? 'Alive' : 'Dead') : 'Unknown'}</td
>
<Cell first>{worker}</Cell>
<Cell>{custom_tags?.join(', ') ?? ''}</Cell>
<Cell>{last_ping != undefined ? last_ping + timeSinceLastPing : -1}s ago</Cell>
<Cell>{displayDate(started_at)}</Cell>
<Cell>{jobs_executed}</Cell>
<Cell last>
<Badge
color={last_ping != undefined ? (last_ping < 60 ? 'green' : 'red') : 'gray'}
>
{last_ping != undefined ? (last_ping < 60 ? 'Alive' : 'Dead') : 'Unknown'}
</Badge>
</Cell>
</tr>
{/each}
{/if}
</tbody>
</TableCustom>
{/each}
{/each}
</tbody>
</DataTable>
{:else}
<div class="flex flex-col">
{#each new Array(4) as _}
@@ -2,30 +2,20 @@
import { goto } from '$app/navigation'
import { page } from '$app/stores'
import { isCloudHosted } from '$lib/cloud'
import AddUser from '$lib/components/AddUser.svelte'
import CenteredPage from '$lib/components/CenteredPage.svelte'
import { Alert, Badge, Button, Skeleton, Tab, Tabs } from '$lib/components/common'
import ToggleButton from '$lib/components/common/toggleButton/ToggleButton.svelte'
import ToggleButtonGroup from '$lib/components/common/toggleButton/ToggleButtonGroup.svelte'
import { Alert, Badge, Button, Tab, Tabs } from '$lib/components/common'
import DeployToSetting from '$lib/components/DeployToSetting.svelte'
import InviteUser from '$lib/components/InviteUser.svelte'
import PageHeader from '$lib/components/PageHeader.svelte'
import ResourcePicker from '$lib/components/ResourcePicker.svelte'
import ScriptPicker from '$lib/components/ScriptPicker.svelte'
import SearchItems from '$lib/components/SearchItems.svelte'
import Slider from '$lib/components/Slider.svelte'
import TableCustom from '$lib/components/TableCustom.svelte'
import Toggle from '$lib/components/Toggle.svelte'
import Tooltip from '$lib/components/Tooltip.svelte'
import WorkspaceUserSettings from '$lib/components/settings/WorkspaceUserSettings.svelte'
import { WORKSPACE_SHOW_SLACK_CMD, WORKSPACE_SHOW_WEBHOOK_CLI_SYNC } from '$lib/consts'
import type { User } from '$lib/gen'
import {
OauthService,
Script,
UserService,
WorkspaceService,
type WorkspaceInvite
} from '$lib/gen'
import { OauthService, Script, WorkspaceService } from '$lib/gen'
import {
enterpriseLicense,
existsOpenaiResourcePath,
@@ -38,19 +28,14 @@
import { capitalize, setQueryWithoutLoad } from '$lib/utils'
import { faSlack } from '@fortawesome/free-brands-svg-icons'
import { faBarsStaggered, faExternalLink, faScroll } from '@fortawesome/free-solid-svg-icons'
import { Slack } from 'lucide-svelte'
let users: User[] | undefined = undefined
let invites: WorkspaceInvite[] = []
let filteredUsers: User[] | undefined = undefined
let userFilter = ''
let initialPath: string
let scriptPath: string
let team_name: string | undefined
let auto_invite_domain: string | undefined
let itemKind: 'flow' | 'script' = 'flow'
let operatorOnly: boolean | undefined = undefined
let premium_info: { premium: boolean; usage?: number } | undefined = undefined
let nbDisplayed = 30
let plan: string | undefined = undefined
let customer_id: string | undefined = undefined
let webhook: string | undefined = undefined
@@ -151,8 +136,7 @@
async function loadSettings(): Promise<void> {
const settings = await WorkspaceService.getSettings({ workspace: $workspaceStore! })
team_name = settings.slack_name
auto_invite_domain = settings.auto_invite_domain
operatorOnly = settings.auto_invite_operator
if (settings.slack_command_script) {
itemKind = settings.slack_command_script.split('/')[0] as 'flow' | 'script'
}
@@ -167,54 +151,19 @@
errorHandlerInitialPath = errorHandlerScriptPath
}
async function listUsers(): Promise<void> {
users = await UserService.listUsers({ workspace: $workspaceStore! })
}
async function listInvites(): Promise<void> {
invites = await WorkspaceService.listPendingInvites({ workspace: $workspaceStore! })
}
let allowedAutoDomain = false
async function getDisallowedAutoDomain() {
allowedAutoDomain = await WorkspaceService.isDomainAllowed()
}
async function loadPremiumInfo() {
if (isCloudHosted()) {
premium_info = await WorkspaceService.getPremiumInfo({ workspace: $workspaceStore! })
}
}
$: domain = $userStore?.email.split('@')[1]
$: {
if ($workspaceStore) {
getDisallowedAutoDomain()
listUsers()
listInvites()
loadSettings()
loadPremiumInfo()
}
}
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 editErrorHandler() {
errorHandlerInitialPath = errorHandlerScriptPath
if (errorHandlerScriptPath) {
@@ -254,16 +203,9 @@
}
</script>
<SearchItems
filter={userFilter}
items={users}
bind:filteredItems={filteredUsers}
f={(x) => x.email + ' ' + x.name + ' ' + x.company}
/>
<CenteredPage>
{#if $userStore?.is_admin || $superadmin}
<PageHeader title="Workspace Settings of {$workspaceStore}" />
<PageHeader title="Workspace settings: {$workspaceStore}" />
<div class="overflow-x-auto scrollbar-hidden">
<Tabs
@@ -272,287 +214,41 @@
setQueryWithoutLoad($page.url, [{ key: 'tab', value: tab }], 0)
}}
>
<Tab size="md" value="users">
<Tab size="xs" value="users">
<div class="flex gap-2 items-center my-1"> Users</div>
</Tab>
<Tab size="md" value="deploy_to">
<Tab size="xs" value="deploy_to">
<div class="flex gap-2 items-center my-1"> Dev/Staging/Prod</div>
</Tab>
{#if WORKSPACE_SHOW_SLACK_CMD}
<Tab size="md" value="slack">
<Tab size="xs" value="slack">
<div class="flex gap-2 items-center my-1"> Slack Command </div>
</Tab>
{/if}
{#if isCloudHosted()}
<Tab size="md" value="premium">
<Tab size="xs" value="premium">
<div class="flex gap-2 items-center my-1"> Premium Plans </div>
</Tab>
{/if}
<Tab size="md" value="export_delete">
<Tab size="xs" value="export_delete">
<div class="flex gap-2 items-center my-1"> Delete Workspace </div>
</Tab>
{#if WORKSPACE_SHOW_WEBHOOK_CLI_SYNC}
<Tab size="md" value="webhook">
<Tab size="xs" value="webhook">
<div class="flex gap-2 items-center my-1">Webhook</div>
</Tab>
{/if}
<Tab size="md" value="error_handler">
<Tab size="xs" value="error_handler">
<div class="flex gap-2 items-center my-1">Error Handler</div>
</Tab>
<Tab size="md" value="openai">
<Tab size="xs" value="openai">
<div class="flex gap-2 items-center my-1">Windmill AI</div>
</Tab>
</Tabs>
</div>
{#if tab == 'users'}
<PageHeader
title="Members ({users?.length ?? ''})"
primary={false}
tooltip="Manage users manually or enable SSO authentication."
documentationLink="https://www.windmill.dev/docs/core_concepts/authentification"
/>
<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 mb-20">
<TableCustom>
<tr slot="header-row">
<th>email</th>
<th>username</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.slice(0, nbDisplayed) as { email, username, is_admin, operator, usage, disabled } (email)}
<tr class="border">
<td>{email}</td>
<td>{username}</td>
<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) => {
if (is_admin && email == $userStore?.email && e.detail != 'admin') {
sendUserToast(
'Admins cannot be demoted by themselves, ask another admin to demote you',
true
)
e.preventDefault()
listUsers()
return
}
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<Tooltip
>An admin has full control over a specific Windmill workspace, including
the ability to manage users, edit entities, and control permissions
within the workspace.</Tooltip
></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}
{#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">
{#each new Array(4) as _}
<td>
<Skeleton layout={[[2]]} />
</td>
{/each}
</tr>
{/each}
{/if}
</tbody>
</TableCustom>
</div>
<PageHeader
title="Invites ({invites.length ?? ''})"
primary={false}
tooltip="Manage invites on your workspace."
documentationLink="https://www.windmill.dev/docs/core_concepts/authentification#adding-users-to-a-workspace"
>
<InviteUser on:new={listInvites} />
</PageHeader>
<div class="overflow-auto max-h-screen">
<TableCustom>
<tr slot="header-row">
<th>email</th>
<th>role</th>
<th />
</tr>
<tbody slot="body">
{#each invites as { email, is_admin, operator }}
<tr class="border">
<td>{email}</td>
<td
>{#if operator}<Badge>operator</Badge>{:else if is_admin}<Badge>admin</Badge>{/if}
</td>
<td>
<button
class="ml-2 text-red-500"
on:click={async () => {
await WorkspaceService.deleteInvite({
workspace: $workspaceStore ?? '',
requestBody: {
email,
is_admin,
operator
}
})
listInvites()
}}>cancel</button
></td
>
</tr>
{/each}
</tbody>
</TableCustom>
</div>
<div class="mt-10" />
<PageHeader
title="Auto Invite"
tooltip="Auto invite to the workspace users from your domain."
documentationLink="https://www.windmill.dev/docs/core_concepts/authentification#auto-invite"
primary={false}
/>
<div class="flex gap-2">
{#if auto_invite_domain != domain}
<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}
<div class="flex flex-col gap-y-2">
<Toggle
bind:checked={operatorOnly}
options={{
right: `Auto-invited users to join as operators`
}}
on:change={async (e) => {
await removeAllInvitesFromDomain()
await WorkspaceService.editAutoInvite({
workspace: $workspaceStore ?? '',
requestBody: { operator: e.detail }
})
loadSettings()
listInvites()
}}
/>
<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}
<div class="text-red-400 text-sm mb-2">{domain} domain not allowed for auto-invite</div>
{/if}
<WorkspaceUserSettings />
{:else if tab == 'deploy_to'}
<div class="my-2"
><Alert type="info" title="Link this workspace to another Staging/Prod workspace"
@@ -584,7 +280,7 @@
</div>
{/if}
<div class="text-sm mb-4 box p-2 max-w-3xl">
<div class="text-xs mb-4 box p-2 max-w-3xl">
{#if premium_info?.premium}
<div class="flex flex-col gap-0.5">
{#if plan}
@@ -678,7 +374,7 @@
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
{#each Object.entries(plans) as [planTitle, planDesc]}
<div class="box p-4 text-sm flex flex-col h-full overflow-hidden">
<div class="box p-4 text-xs flex flex-col h-full overflow-hidden">
<h2 class="mb-4">{planTitle}</h2>
<ul class="list-disc text-lg p-4">
{#each planDesc as item}
@@ -721,85 +417,94 @@
</div>
{/if}
{:else if tab == 'slack'}
<div class="mt-2"
><Alert type="info" title="Send commands from slack"
>Connect your windmill workspace to your slack workspace to trigger a script or a flow
with a '/windmill' command.</Alert
></div
>
<p class="text-xs text-secondary my-1 mt-2">
Status: {#if team_name}Connected to slack workspace <Badge>{team_name}</Badge>{:else}Not
connected{/if}
</p>
{#if team_name}
<div class="flex flex-col gap-2 max-w-sm">
<Button
size="sm"
endIcon={{ icon: faSlack }}
btnClasses="mt-2"
variant="border"
on:click={async () => {
await OauthService.disconnectSlack({
workspace: $workspaceStore ?? ''
})
loadSettings()
sendUserToast('Disconnected Slack')
}}
>
Disconnect Slack
</Button>
<Button
size="sm"
endIcon={{ icon: faScroll }}
href="/scripts/add?hub=hub%2F314%2Fslack%2Fexample_of_responding_to_a_slack_command_slack"
>
Create a script to handle slack commands
</Button>
<Button size="sm" endIcon={{ icon: faBarsStaggered }} href="/flows/add?hub=28">
Create a flow to handle slack commands
</Button>
<div class="flex flex-col gap-4 my-8">
<div class="flex flex-col gap-1">
<div class=" text-primary text-md font-semibold"> Send commands from slack </div>
<div class="text-tertiary text-xs">
Connect your windmill workspace to your slack workspace to trigger a script or a flow
with a '/windmill' command.
</div>
</div>
{:else}
<div class="flex">
<Button size="sm" endIcon={{ icon: faSlack }} href="/api/oauth/connect_slack">
Connect to Slack
</Button>
{#if team_name}
<div class="flex flex-col gap-2 max-w-sm">
<Button
size="sm"
endIcon={{ icon: faSlack }}
btnClasses="mt-2"
variant="border"
on:click={async () => {
await OauthService.disconnectSlack({
workspace: $workspaceStore ?? ''
})
loadSettings()
sendUserToast('Disconnected Slack')
}}
>
Disconnect Slack
</Button>
<Button
size="sm"
endIcon={{ icon: faScroll }}
href="/scripts/add?hub=hub%2F314%2Fslack%2Fexample_of_responding_to_a_slack_command_slack"
>
Create a script to handle slack commands
</Button>
<Button size="sm" endIcon={{ icon: faBarsStaggered }} href="/flows/add?hub=28">
Create a flow to handle slack commands
</Button>
</div>
{:else}
<div class="flex flex-row gap-2">
<Button size="xs" color="dark" href="/api/oauth/connect_slack">
<div class="flex flex-row gap-1 items-center">
<Slack size={14} />
Connect to Slack
</div>
</Button>
<Badge color="red">Not connnected</Badge>
</div>
{/if}
</div>
<div class="bg-surface-disabled p-4 rounded-md flex flex-col gap-1">
<div class="text-primary font-md font-semibold">
Script or flow to run on /windmill command
</div>
<div class="relative">
{#if !team_name}
<div class="absolute top-0 right-0 bottom-0 left-0 bg-surface-disabled/50 z-40" />
{/if}
<ScriptPicker
kind={Script.kind.SCRIPT}
allowFlow
bind:itemKind
bind:scriptPath
{initialPath}
on:select={editSlackCommand}
/>
</div>
{/if}
<h3 class="mt-5 text-secondary">
Script or flow to run on /windmill command
<Tooltip documentationLink="https://www.windmill.dev/docs/integrations/slack">
The script or flow to be triggered when the `/windmill` command is invoked.
</Tooltip>
</h3>
<ScriptPicker
kind={Script.kind.SCRIPT}
allowFlow
bind:itemKind
bind:scriptPath
{initialPath}
on:select={editSlackCommand}
/>
<br />
<div class="text-tertiary text-sm">
Pick a script or flow meant to be triggered when the `/windmill` command is invoked. Upon
connection, templates for a <a href="https://hub.windmill.dev/scripts/slack/1405/">script</a
>
and <a href="https://hub.windmill.dev/flows/28/">flow</a> are available.
<br /><br />
<div class="prose text-2xs text-tertiary">
Pick a script or flow meant to be triggered when the `/windmill` command is invoked. Upon
connection, templates for a <a href="https://hub.windmill.dev/scripts/slack/1405/"
>script</a
>
and <a href="https://hub.windmill.dev/flows/28/">flow</a> are available.
The script or flow chosen is passed the parameters `response_url: string` and `text: string`
respectively the url to reply directly to the trigger and the text of the command.
<br /><br />
<br /><br />
The script or flow chosen is passed the parameters `response_url: string` and `text:
string` respectively the url to reply directly to the trigger and the text of the command.
The script or flow is permissioned as group "slack" that will be automatically created after
connection to Slack.
<br /><br />
<br /><br />
The script or flow is permissioned as group "slack" that will be automatically created
after connection to Slack.
See more on <a href="https://www.windmill.dev/docs/integrations/slack">documentation</a>.
<br /><br />
See more on <a href="https://www.windmill.dev/docs/integrations/slack">documentation</a>.
</div>
</div>
{:else if tab == 'export_delete'}
<PageHeader title="Export workspace" primary={false} />
@@ -878,9 +583,7 @@
<div class="flex gap-2">
<input class="justify-start" type="text" bind:value={webhook} />
<Button color="blue" btnClasses="justify-end" size="md" on:click={editWebhook}
>Set Webhook</Button
>
<Button color="blue" btnClasses="justify-end" on:click={editWebhook}>Set Webhook</Button>
</div>
{:else if tab == 'error_handler'}
<PageHeader title="Script to run as error handler" primary={false} />
@@ -894,7 +597,7 @@
/>
<div class="flex flex-col gap-20 items-start mt-3">
<div class="w-2/3">
<div class="text-tertiary text-sm">
<div class="text-tertiary text-xs">
The following args will be passed to the error handler:
<ul class="mt-1 ml-2">
<li><b>path</b>: The path of the script or flow that errored.</li>