mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-09-07 00:01:49 +00:00
path improvements v1
This commit is contained in:
@@ -3460,6 +3460,11 @@ paths:
|
||||
- group
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/WorkspaceId"
|
||||
- name: only_member_of
|
||||
in: query
|
||||
description: only list the groups the user is member of (default false)
|
||||
schema:
|
||||
type: boolean
|
||||
responses:
|
||||
"200":
|
||||
description: group list
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
use crate::{
|
||||
db::{UserDB, DB},
|
||||
users::Authed,
|
||||
users::{get_groups_for_user, Authed},
|
||||
};
|
||||
use axum::{
|
||||
extract::{Extension, Path, Query},
|
||||
@@ -91,16 +91,26 @@ async fn list_groups(
|
||||
Ok(Json(rows))
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct QueryListGroup {
|
||||
pub only_member_of: Option<bool>,
|
||||
}
|
||||
async fn list_group_names(
|
||||
Authed { username, .. }: Authed,
|
||||
Extension(db): Extension<DB>,
|
||||
Query(QueryListGroup { only_member_of }): Query<QueryListGroup>,
|
||||
Path(w_id): Path<String>,
|
||||
) -> JsonResult<Vec<String>> {
|
||||
let rows = sqlx::query_scalar!(
|
||||
"SELECT name FROM group_ WHERE workspace_id = $1 ORDER BY name desc",
|
||||
w_id
|
||||
)
|
||||
.fetch_all(&db)
|
||||
.await?;
|
||||
let rows = if !only_member_of.unwrap_or(false) {
|
||||
sqlx::query_scalar!(
|
||||
"SELECT name FROM group_ WHERE workspace_id = $1 ORDER BY name desc",
|
||||
w_id
|
||||
)
|
||||
.fetch_all(&db)
|
||||
.await?
|
||||
} else {
|
||||
get_groups_for_user(&w_id, &username, &db).await?
|
||||
};
|
||||
|
||||
Ok(Json(rows))
|
||||
}
|
||||
|
||||
@@ -771,7 +771,7 @@ async fn get_user(w_id: &str, username: &str, db: &DB) -> Result<Option<UserInfo
|
||||
}))
|
||||
}
|
||||
|
||||
async fn get_groups_for_user(w_id: &str, username: &str, db: &DB) -> Result<Vec<String>> {
|
||||
pub async fn get_groups_for_user(w_id: &str, username: &str, db: &DB) -> Result<Vec<String>> {
|
||||
let groups = sqlx::query_scalar!(
|
||||
"SELECT group_ FROM usr_to_group where usr = $1 AND workspace_id = $2",
|
||||
username,
|
||||
|
||||
@@ -230,47 +230,53 @@
|
||||
>Groups managing this group <Tooltip>Any member of those groups can manage this group</Tooltip
|
||||
></h2
|
||||
>
|
||||
<div class="flex items-start">
|
||||
<AutoComplete items={groups} bind:selectedItem={new_managing_group} />
|
||||
<Button
|
||||
variant="contained"
|
||||
color="blue"
|
||||
size="sm"
|
||||
btnClasses="!ml-4"
|
||||
on:click={addToManagingGroup}
|
||||
>
|
||||
Add group managing this group
|
||||
</Button>
|
||||
</div>
|
||||
<TableCustom>
|
||||
<tr slot="header-row">
|
||||
<th>group</th>
|
||||
<th />
|
||||
</tr>
|
||||
<tbody slot="body">
|
||||
{#each managing_groups as managing_group}<tr>
|
||||
<td>{managing_group.split('/')[1]}</td>
|
||||
<td>
|
||||
{#if can_write}
|
||||
<button
|
||||
class="ml-2 text-red-500"
|
||||
on:click={async () => {
|
||||
await GranularAclService.removeGranularAcls({
|
||||
workspace: $workspaceStore ?? '',
|
||||
path: name,
|
||||
kind: 'group_',
|
||||
requestBody: {
|
||||
owner: managing_group
|
||||
}
|
||||
})
|
||||
loadGroup()
|
||||
}}>remove</button
|
||||
>
|
||||
{/if}</td
|
||||
>
|
||||
</tr>{/each}
|
||||
</tbody>
|
||||
</TableCustom>
|
||||
{#if can_write}
|
||||
<div class="flex items-start">
|
||||
<AutoComplete items={groups} bind:selectedItem={new_managing_group} />
|
||||
<Button
|
||||
variant="contained"
|
||||
color="blue"
|
||||
size="sm"
|
||||
btnClasses="!ml-4"
|
||||
on:click={addToManagingGroup}
|
||||
>
|
||||
Add group managing this group
|
||||
</Button>
|
||||
</div>
|
||||
{/if}
|
||||
{#if managing_groups.length == 0}
|
||||
<p class="text-gray-600 text-sm">No group is managing this group</p>
|
||||
{:else}
|
||||
<TableCustom>
|
||||
<tr slot="header-row">
|
||||
<th>group</th>
|
||||
<th />
|
||||
</tr>
|
||||
<tbody slot="body">
|
||||
{#each managing_groups as managing_group}<tr>
|
||||
<td>{managing_group.split('/')[1]}</td>
|
||||
<td>
|
||||
{#if can_write}
|
||||
<button
|
||||
class="ml-2 text-red-500"
|
||||
on:click={async () => {
|
||||
await GranularAclService.removeGranularAcls({
|
||||
workspace: $workspaceStore ?? '',
|
||||
path: name,
|
||||
kind: 'group_',
|
||||
requestBody: {
|
||||
owner: managing_group
|
||||
}
|
||||
})
|
||||
loadGroup()
|
||||
}}>remove</button
|
||||
>
|
||||
{/if}</td
|
||||
>
|
||||
</tr>{/each}
|
||||
</tbody>
|
||||
</TableCustom>
|
||||
{/if}
|
||||
{:else}
|
||||
<div class="flex flex-col">
|
||||
{#each new Array(6) as _}
|
||||
|
||||
@@ -16,8 +16,11 @@
|
||||
import Required from './Required.svelte'
|
||||
import Popover from './Popover.svelte'
|
||||
import { Button, Drawer, DrawerContent } from './common'
|
||||
import { faPlus } from '@fortawesome/free-solid-svg-icons'
|
||||
import { faEye, faPlus } from '@fortawesome/free-solid-svg-icons'
|
||||
import GroupEditor from './GroupEditor.svelte'
|
||||
import ToggleButtonGroup from './common/toggleButton/ToggleButtonGroup.svelte'
|
||||
import ToggleButton from './common/toggleButton/ToggleButton.svelte'
|
||||
import { Icon } from 'svelte-awesome'
|
||||
|
||||
type PathKind = 'resource' | 'script' | 'variable' | 'flow' | 'schedule' | 'app'
|
||||
let meta: Meta | undefined = undefined
|
||||
@@ -33,7 +36,7 @@
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
let groups: Group[] = []
|
||||
let groups: string[] = []
|
||||
|
||||
$: meta && onMetaChange()
|
||||
|
||||
@@ -82,7 +85,10 @@
|
||||
}
|
||||
|
||||
async function loadGroups(): Promise<void> {
|
||||
groups = await GroupService.listGroups({ workspace: $workspaceStore! })
|
||||
groups = await GroupService.listGroupNames({
|
||||
workspace: $workspaceStore!,
|
||||
onlyMemberOf: !($userStore?.is_admin || $superadmin)
|
||||
})
|
||||
}
|
||||
|
||||
async function validate(meta: Meta, path: string, kind: PathKind) {
|
||||
@@ -161,6 +167,7 @@
|
||||
}
|
||||
|
||||
let newGroup: Drawer
|
||||
let viewGroup: Drawer
|
||||
let newGroupName: string
|
||||
let groupCreated: string | undefined = undefined
|
||||
|
||||
@@ -190,63 +197,77 @@
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
|
||||
<div>
|
||||
<div class="flex flex-col sm:grid sm:grid-cols-4 sm:gap-4 pb-0 mb-1">
|
||||
{#if meta != undefined}
|
||||
<label class="block">
|
||||
<span class="text-gray-700 text-sm whitespace-nowrap">
|
||||
<Popover
|
||||
>Owner Kind
|
||||
<span slot="text"
|
||||
>Select the group <span class="font-mono">all</span>
|
||||
to share it with all workspace users, and <span class="font-mono">user</span> to keep
|
||||
it private.
|
||||
<a href="https://docs.windmill.dev/docs/reference/namespaces">docs</a>
|
||||
</span>
|
||||
</Popover>
|
||||
</span>
|
||||
<Drawer bind:this={viewGroup}>
|
||||
<DrawerContent title="Group {meta?.owner}" on:close={viewGroup.closeDrawer}>
|
||||
<GroupEditor name={meta?.owner ?? ''} />
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
|
||||
<select
|
||||
{disabled}
|
||||
bind:value={meta.ownerKind}
|
||||
on:change={() => {
|
||||
if (meta) {
|
||||
if (meta.ownerKind === 'group') {
|
||||
meta.owner = 'all'
|
||||
} else {
|
||||
meta.owner = $userStore?.username ?? ''
|
||||
<div>
|
||||
<div class="flex flex-col sm:grid sm:grid-cols-3 gap-2 sm:gap-4 pb-0 mb-1">
|
||||
{#if meta != undefined}
|
||||
<div class="flex gap-4 w-full">
|
||||
<label class="block">
|
||||
<span class="text-gray-700 text-sm whitespace-nowrap">
|
||||
<Popover
|
||||
>
|
||||
<span slot="text"
|
||||
>Select the group <span class="font-mono">all</span>
|
||||
to share it with all workspace users, and <span class="font-mono">user</span> to
|
||||
keep it private.
|
||||
<a href="https://docs.windmill.dev/docs/reference/namespaces">docs</a>
|
||||
</span>
|
||||
</Popover>
|
||||
</span>
|
||||
|
||||
<ToggleButtonGroup
|
||||
bind:selected={meta.ownerKind}
|
||||
on:selected={(e) => {
|
||||
const kind = e.detail
|
||||
console.log(kind)
|
||||
if (meta) {
|
||||
if (kind === 'group') {
|
||||
meta.owner = 'all'
|
||||
} else {
|
||||
meta.owner = $userStore?.username ?? ''
|
||||
}
|
||||
}
|
||||
}
|
||||
}}
|
||||
>
|
||||
<option>user</option>
|
||||
<option>group</option>
|
||||
</select>
|
||||
</label>
|
||||
{#if meta.ownerKind === 'user'}
|
||||
<label class="block">
|
||||
<span class="text-gray-700 text-sm">Owner</span>
|
||||
<input
|
||||
type="text"
|
||||
bind:value={meta.owner}
|
||||
placeholder={$userStore?.username ?? ''}
|
||||
disabled={!($superadmin || ($userStore?.is_admin ?? false))}
|
||||
/>
|
||||
</label>
|
||||
{:else}
|
||||
<label class="block">
|
||||
<span class="text-gray-700 text-sm inline-flex justify-between w-full"
|
||||
>Owner <button class=" text-xs text-blue-500" on:click={newGroup.openDrawer}
|
||||
>+group</button
|
||||
></span
|
||||
}}
|
||||
>
|
||||
<select {disabled} bind:value={meta.owner}>
|
||||
{#each groups as g}
|
||||
<option>{g.name}</option>
|
||||
{/each}
|
||||
</select>
|
||||
<ToggleButton light size="xs" value="user" position="left">User</ToggleButton>
|
||||
<ToggleButton light size="xs" value="group" position="right">Group</ToggleButton>
|
||||
</ToggleButtonGroup>
|
||||
</label>
|
||||
{/if}
|
||||
{#if meta.ownerKind === 'user'}
|
||||
<label class="block">
|
||||
<span class="text-gray-700 text-sm">Owner</span>
|
||||
<input
|
||||
type="text"
|
||||
bind:value={meta.owner}
|
||||
placeholder={$userStore?.username ?? ''}
|
||||
disabled={!($superadmin || ($userStore?.is_admin ?? false))}
|
||||
/>
|
||||
</label>
|
||||
{:else}
|
||||
<label class="block w-full">
|
||||
<span class="text-gray-700 text-sm inline-flex justify-between w-full">Owner</span>
|
||||
|
||||
<div class="flex flex-row gap-1 w-full">
|
||||
<select class="grow w-full" {disabled} bind:value={meta.owner}>
|
||||
{#each groups as g}
|
||||
<option>{g}</option>
|
||||
{/each}
|
||||
</select>
|
||||
<Button variant="border" size="xs" on:click={viewGroup.openDrawer}>
|
||||
<Icon scale={0.8} data={faEye} /></Button
|
||||
>
|
||||
<Button variant="border" size="xs" on:click={newGroup.openDrawer}>
|
||||
<Icon scale={0.8} data={faPlus} /></Button
|
||||
></div
|
||||
>
|
||||
</label>
|
||||
{/if}
|
||||
</div>
|
||||
<label class="block col-span-2">
|
||||
<span class="text-gray-700 text-sm">
|
||||
Name
|
||||
|
||||
@@ -173,129 +173,125 @@
|
||||
<!-- metadata -->
|
||||
{#if step === 1}
|
||||
<CenteredPage>
|
||||
<div class="space-y-6">
|
||||
<h2 class="border-b pb-1 mt-4">Path & Summary</h2>
|
||||
<Path
|
||||
bind:this={pathC}
|
||||
bind:error={pathError}
|
||||
bind:path={script.path}
|
||||
{initialPath}
|
||||
on:enter={() => changeStep(2)}
|
||||
namePlaceholder="my_script"
|
||||
kind="script"
|
||||
/>
|
||||
<label class="block ">
|
||||
<span class="text-gray-700 text-sm">Summary <Required required={false} /></span>
|
||||
<input
|
||||
type="text"
|
||||
bind:this={summaryC}
|
||||
bind:value={script.summary}
|
||||
placeholder="A very short summary of the script displayed when the script is listed"
|
||||
rows="1"
|
||||
/>
|
||||
</label>
|
||||
<h2 class="border-b pb-1 mt-4">Language</h2>
|
||||
<div class="flex flex-row gap-2 flex-wrap">
|
||||
{#each langs as [label, lang]}
|
||||
{@const isPicked = script.language == lang && template == 'script'}
|
||||
<Button
|
||||
size="sm"
|
||||
variant="border"
|
||||
color={isPicked ? 'blue' : 'dark'}
|
||||
btnClasses={isPicked ? '!border-2 !bg-blue-50/75' : 'm-[1px]'}
|
||||
on:click={() => {
|
||||
script.language = lang
|
||||
template = 'script'
|
||||
initContent(lang, script.kind, template)
|
||||
}}
|
||||
>
|
||||
<LanguageIcon {lang} /><span class="ml-2">{label}</span>
|
||||
</Button>
|
||||
{/each}
|
||||
<h2 class="border-b pb-1 mt-4 mb-2">Path</h2>
|
||||
<Path
|
||||
bind:this={pathC}
|
||||
bind:error={pathError}
|
||||
bind:path={script.path}
|
||||
{initialPath}
|
||||
on:enter={() => changeStep(2)}
|
||||
namePlaceholder="my_script"
|
||||
kind="script"
|
||||
/>
|
||||
<h2 class="border-b pb-1 mt-8 mb-4">Summary</h2>
|
||||
<input
|
||||
type="text"
|
||||
bind:this={summaryC}
|
||||
bind:value={script.summary}
|
||||
placeholder="A very short summary of the script displayed when the script is listed"
|
||||
rows="1"
|
||||
/>
|
||||
<h2 class="border-b pb-1 mt-8 mb-6">Language</h2>
|
||||
<div class="flex flex-row gap-2 flex-wrap">
|
||||
{#each langs as [label, lang]}
|
||||
{@const isPicked = script.language == lang && template == 'script'}
|
||||
<Button
|
||||
size="sm"
|
||||
variant="border"
|
||||
color={template == 'pgsql' ? 'blue' : 'dark'}
|
||||
btnClasses={template == 'pgsql' ? '!border-2 !bg-blue-50/75' : 'm-[1px]'}
|
||||
color={isPicked ? 'blue' : 'dark'}
|
||||
btnClasses={isPicked ? '!border-2 !bg-blue-50/75' : 'm-[1px]'}
|
||||
on:click={() => {
|
||||
script.language = Script.language.DENO
|
||||
template = 'pgsql'
|
||||
initContent(script.language, script.kind, template)
|
||||
script.language = lang
|
||||
template = 'script'
|
||||
initContent(lang, script.kind, template)
|
||||
}}
|
||||
>
|
||||
<LanguageIcon lang="pgsql" /><span class="ml-2">PostgreSQL</span>
|
||||
<LanguageIcon {lang} /><span class="ml-2">{label}</span>
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="border"
|
||||
color={template == 'mysql' ? 'blue' : 'dark'}
|
||||
btnClasses={template == 'mysql' ? '!border-2 !bg-blue-50/75' : 'm-[1px]'}
|
||||
on:click={() => {
|
||||
script.language = Script.language.DENO
|
||||
template = 'mysql'
|
||||
initContent(script.language, script.kind, template)
|
||||
}}
|
||||
>
|
||||
<LanguageIcon lang="mysql" /><span class="ml-2">MySQL</span>
|
||||
</Button>
|
||||
</div>
|
||||
<h2 class="border-b pb-1 mt-4">Advanced</h2>
|
||||
<div>
|
||||
<Button
|
||||
color="light"
|
||||
size="sm"
|
||||
endIcon={{ icon: viewScriptKind ? faChevronUp : faChevronDown }}
|
||||
on:click={() => (viewScriptKind = !viewScriptKind)}
|
||||
>
|
||||
Tag this script as having a specific purpose inside flows
|
||||
</Button>
|
||||
</div>
|
||||
{#if viewScriptKind}
|
||||
<div class="max-w-lg">
|
||||
<RadioButton
|
||||
label="Script Type"
|
||||
options={[
|
||||
['Action', Script.kind.SCRIPT],
|
||||
[
|
||||
{
|
||||
title: 'Trigger Script',
|
||||
desc: `First module of flows to trigger them based on watching changes external periodically using an internal state`
|
||||
},
|
||||
Script.kind.TRIGGER
|
||||
],
|
||||
[
|
||||
{
|
||||
title: 'Error Handler',
|
||||
desc: `Handle errors for flows after all retries attempts have been exhausted`
|
||||
},
|
||||
Script.kind.FAILURE
|
||||
],
|
||||
[
|
||||
{
|
||||
title: 'Approval Script',
|
||||
desc: `Send notification externally to ask for approval to continue a flow`
|
||||
},
|
||||
Script.kind.APPROVAL
|
||||
]
|
||||
{/each}
|
||||
<Button
|
||||
size="sm"
|
||||
variant="border"
|
||||
color={template == 'pgsql' ? 'blue' : 'dark'}
|
||||
btnClasses={template == 'pgsql' ? '!border-2 !bg-blue-50/75' : 'm-[1px]'}
|
||||
on:click={() => {
|
||||
script.language = Script.language.DENO
|
||||
template = 'pgsql'
|
||||
initContent(script.language, script.kind, template)
|
||||
}}
|
||||
>
|
||||
<LanguageIcon lang="pgsql" /><span class="ml-2">PostgreSQL</span>
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="border"
|
||||
color={template == 'mysql' ? 'blue' : 'dark'}
|
||||
btnClasses={template == 'mysql' ? '!border-2 !bg-blue-50/75' : 'm-[1px]'}
|
||||
on:click={() => {
|
||||
script.language = Script.language.DENO
|
||||
template = 'mysql'
|
||||
initContent(script.language, script.kind, template)
|
||||
}}
|
||||
>
|
||||
<LanguageIcon lang="mysql" /><span class="ml-2">MySQL</span>
|
||||
</Button>
|
||||
</div>
|
||||
<h2 class="border-b pb-1 mt-8 mb-4">Advanced</h2>
|
||||
<div class="mb-4">
|
||||
<Button
|
||||
color="light"
|
||||
size="sm"
|
||||
endIcon={{ icon: viewScriptKind ? faChevronUp : faChevronDown }}
|
||||
on:click={() => (viewScriptKind = !viewScriptKind)}
|
||||
>
|
||||
Tag this script as having a specific purpose inside flows
|
||||
</Button>
|
||||
</div>
|
||||
{#if viewScriptKind}
|
||||
<div class="max-w-lg">
|
||||
<RadioButton
|
||||
label="Script Type"
|
||||
options={[
|
||||
['Action', Script.kind.SCRIPT],
|
||||
[
|
||||
{
|
||||
title: 'Trigger',
|
||||
desc: `First module of flows to trigger them based on watching changes external periodically using an internal state`
|
||||
},
|
||||
Script.kind.TRIGGER
|
||||
],
|
||||
[
|
||||
{
|
||||
title: 'Approval',
|
||||
desc: `Send notification externally to ask for approval to continue a flow`
|
||||
},
|
||||
Script.kind.APPROVAL
|
||||
],
|
||||
[
|
||||
{
|
||||
title: 'Error Handler',
|
||||
desc: `Handle errors for flows after all retries attempts have been exhausted`
|
||||
},
|
||||
Script.kind.FAILURE
|
||||
]
|
||||
|
||||
// ['Command Handler', Script.kind.COMMAND]
|
||||
]}
|
||||
on:change={(e) => {
|
||||
if (isInitialCode(script.content)) {
|
||||
template = 'script'
|
||||
initContent(script.language, e.detail, template)
|
||||
}
|
||||
}}
|
||||
bind:value={script.kind}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="ml-3">
|
||||
<Toggle
|
||||
bind:checked={script.is_template}
|
||||
options={{ right: 'Save as a workspace template' }}
|
||||
// ['Command Handler', Script.kind.COMMAND]
|
||||
]}
|
||||
on:change={(e) => {
|
||||
if (isInitialCode(script.content)) {
|
||||
template = 'script'
|
||||
initContent(script.language, e.detail, template)
|
||||
}
|
||||
}}
|
||||
bind:value={script.kind}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="ml-3">
|
||||
<Toggle
|
||||
bind:checked={script.is_template}
|
||||
options={{ right: 'Save as a workspace template' }}
|
||||
/>
|
||||
</div>
|
||||
</CenteredPage>
|
||||
{:else if step === 2}
|
||||
|
||||
@@ -70,7 +70,7 @@
|
||||
|
||||
<svelte:window on:keydown={onKeyDown} />
|
||||
|
||||
<aside class="drawer" class:open class:close={!open && timeout} {style}>
|
||||
<aside class="drawer {$$props.class}" class:open class:close={!open && timeout} {style}>
|
||||
<div class="overlay" on:click={handleClickAway} />
|
||||
<div class="panel {placement}" class:size>
|
||||
{#if open || !timeout}
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
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 ? 'dark' : 'dark') : 'light'}
|
||||
color={$selected === value ? (light ? 'gray' : 'dark') : 'light'}
|
||||
variant="contained"
|
||||
>
|
||||
<slot />
|
||||
|
||||
Reference in New Issue
Block a user