mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-08-20 16:02:19 +00:00
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:
@@ -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'
|
||||
},
|
||||
{
|
||||
|
||||
@@ -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 <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 <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 <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">
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user