mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-09-11 00:06:06 +00:00
improve folder UX
This commit is contained in:
@@ -3733,6 +3733,43 @@ paths:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Folder"
|
||||
|
||||
/w/{workspace}/folders/getusage/{name}:
|
||||
get:
|
||||
summary: get folder usage
|
||||
operationId: getFolderUsage
|
||||
tags:
|
||||
- folder
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/WorkspaceId"
|
||||
- $ref: "#/components/parameters/Name"
|
||||
responses:
|
||||
"200":
|
||||
description: folder
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
scripts:
|
||||
type: number
|
||||
flows:
|
||||
type: number
|
||||
apps:
|
||||
type: number
|
||||
resources:
|
||||
type: number
|
||||
variables:
|
||||
type: number
|
||||
schedules:
|
||||
type: number
|
||||
required:
|
||||
- scripts
|
||||
- flows
|
||||
- apps
|
||||
- resources
|
||||
- variables
|
||||
- schedules
|
||||
|
||||
/w/{workspace}/folders/addowner/{name}:
|
||||
post:
|
||||
summary: add owner to folder
|
||||
|
||||
@@ -31,6 +31,7 @@ pub fn workspaced_service() -> Router {
|
||||
.route("/listnames", get(list_foldernames))
|
||||
.route("/create", post(create_folder))
|
||||
.route("/get/:name", get(get_folder))
|
||||
.route("/getusage/:name", get(get_folder_usage))
|
||||
.route("/delete/:name", delete(delete_folder))
|
||||
.route("/addowner/:name", post(add_owner))
|
||||
.route("/removeowner/:name", post(remove_owner))
|
||||
@@ -168,6 +169,87 @@ async fn get_folder(
|
||||
Ok(Json(folder))
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct FolderUsage {
|
||||
pub scripts: i64,
|
||||
pub schedules: i64,
|
||||
pub flows: i64,
|
||||
pub apps: i64,
|
||||
pub resources: i64,
|
||||
pub variables: i64,
|
||||
}
|
||||
async fn get_folder_usage(
|
||||
authed: Authed,
|
||||
Extension(user_db): Extension<UserDB>,
|
||||
Path((w_id, name)): Path<(String, String)>,
|
||||
) -> JsonResult<FolderUsage> {
|
||||
let mut tx = user_db.begin(&authed).await?;
|
||||
|
||||
let scripts = sqlx::query_scalar!(
|
||||
"SELECT count(path) FROM script WHERE path LIKE 'f/' || $1 || '%' AND archived IS false AND workspace_id = $2",
|
||||
name,
|
||||
w_id
|
||||
)
|
||||
.fetch_one(&mut tx)
|
||||
.await?
|
||||
.unwrap_or(0);
|
||||
|
||||
let schedules = sqlx::query_scalar!(
|
||||
"SELECT count(path) FROM schedule WHERE path LIKE 'f/' || $1 || '%' AND workspace_id = $2",
|
||||
name,
|
||||
w_id
|
||||
)
|
||||
.fetch_one(&mut tx)
|
||||
.await?
|
||||
.unwrap_or(0);
|
||||
|
||||
let flows = sqlx::query_scalar!(
|
||||
"SELECT count(path) FROM flow WHERE path LIKE 'f/' || $1 || '%' AND archived IS false AND workspace_id = $2",
|
||||
name,
|
||||
w_id
|
||||
)
|
||||
.fetch_one(&mut tx)
|
||||
.await?
|
||||
.unwrap_or(0);
|
||||
|
||||
let apps = sqlx::query_scalar!(
|
||||
"SELECT count(path) FROM app WHERE path LIKE 'f/' || $1 || '%' AND workspace_id = $2",
|
||||
name,
|
||||
w_id
|
||||
)
|
||||
.fetch_one(&mut tx)
|
||||
.await?
|
||||
.unwrap_or(0);
|
||||
|
||||
let resources = sqlx::query_scalar!(
|
||||
"SELECT count(path) FROM resource WHERE path LIKE 'f/' || $1 || '%' AND workspace_id = $2",
|
||||
name,
|
||||
w_id
|
||||
)
|
||||
.fetch_one(&mut tx)
|
||||
.await?
|
||||
.unwrap_or(0);
|
||||
|
||||
let variables = sqlx::query_scalar!(
|
||||
"SELECT count(path) FROM variable WHERE path LIKE 'f/' || $1 || '%' AND workspace_id = $2",
|
||||
name,
|
||||
w_id
|
||||
)
|
||||
.fetch_one(&mut tx)
|
||||
.await?
|
||||
.unwrap_or(0);
|
||||
|
||||
tx.commit().await?;
|
||||
Ok(Json(FolderUsage {
|
||||
scripts,
|
||||
flows,
|
||||
schedules,
|
||||
apps,
|
||||
resources,
|
||||
variables,
|
||||
}))
|
||||
}
|
||||
|
||||
async fn delete_folder(
|
||||
authed: Authed,
|
||||
Extension(user_db): Extension<UserDB>,
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
<script lang="ts">
|
||||
import Popover from './Popover.svelte'
|
||||
|
||||
export let members: string[]
|
||||
</script>
|
||||
|
||||
<Popover
|
||||
><div class="inline-flex gap-1 items-end"
|
||||
><span class="text-gray-500 text-xs mb-0.5">({members.length})</span>
|
||||
<div class="max-w-xs truncate"
|
||||
><span class="text-gray-600 text-xs ">{members?.join(', ')}</span></div
|
||||
></div
|
||||
>
|
||||
<span slot="text">{members?.join(', ')}</span></Popover
|
||||
>
|
||||
@@ -0,0 +1,22 @@
|
||||
<script lang="ts">
|
||||
import { FolderService, GroupService } from '$lib/gen'
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
|
||||
export let name: string
|
||||
|
||||
$: $workspaceStore && loadUsage()
|
||||
|
||||
let usage: Record<string, number> = {}
|
||||
|
||||
async function loadUsage() {
|
||||
usage = await FolderService.getFolderUsage({ workspace: $workspaceStore!, name })
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col text-xs text-gray-600">
|
||||
{#each Object.entries(usage) as [k, v]}
|
||||
<div>
|
||||
{k}: {v}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
@@ -0,0 +1,27 @@
|
||||
<script lang="ts">
|
||||
import { GroupService } from '$lib/gen'
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
import Popover from './Popover.svelte'
|
||||
|
||||
export let name: string
|
||||
|
||||
$: $workspaceStore && loadMembers()
|
||||
|
||||
let members: string[] | undefined = []
|
||||
|
||||
async function loadMembers() {
|
||||
members = (await GroupService.getGroup({ workspace: $workspaceStore!, name })).members
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if members}
|
||||
<Popover
|
||||
><div class="inline-flex gap-1 items-end"
|
||||
><span class="text-gray-500 text-xs mb-0.5">({members.length})</span>
|
||||
<div class="max-w-xs truncate"
|
||||
><span class="text-gray-600 text-xs ">{members?.join(', ')}</span></div
|
||||
></div
|
||||
>
|
||||
<span slot="text">{members?.join(', ')}</span></Popover
|
||||
>
|
||||
{/if}
|
||||
@@ -19,12 +19,14 @@
|
||||
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 } from '$lib/components/common'
|
||||
import { Button, Drawer, DrawerContent, Skeleton } from '$lib/components/common'
|
||||
import FolderInfo from '$lib/components/FolderInfo.svelte'
|
||||
import FolderUsageInfo from '$lib/components/FolderUsageInfo.svelte'
|
||||
|
||||
type FolderW = Folder & { canWrite: boolean }
|
||||
|
||||
let newFolderName: string = ''
|
||||
let folders: FolderW[] = []
|
||||
let folders: FolderW[] | undefined = undefined
|
||||
let folderDrawer: Drawer
|
||||
|
||||
async function loadFolders(): Promise<void> {
|
||||
@@ -57,6 +59,14 @@
|
||||
}
|
||||
|
||||
let editFolderName: string = ''
|
||||
|
||||
function computeMembers(owners: string[], extra_perms: Record<string, any>) {
|
||||
const members = new Set(owners)
|
||||
for (const [user, _] of Object.entries(extra_perms)) {
|
||||
members.add(user)
|
||||
}
|
||||
return Array.from(members)
|
||||
}
|
||||
</script>
|
||||
|
||||
<Drawer bind:this={folderDrawer}>
|
||||
@@ -87,59 +97,72 @@
|
||||
<TableCustom>
|
||||
<tr slot="header-row">
|
||||
<th>Name</th>
|
||||
<th />
|
||||
<th>Usage</th>
|
||||
<th>Participants</th>
|
||||
</tr>
|
||||
<tbody slot="body">
|
||||
{#if folders.length === 0}
|
||||
<tr>
|
||||
<td colspan="2" class="text-gray-600 mt-2"> No folders yet, create one! </td>
|
||||
</tr>
|
||||
{#if folders === undefined}
|
||||
{#each new Array(6) as _}
|
||||
<Skeleton layout={[0.25, [2], 0.25]} />
|
||||
{/each}
|
||||
{:else}
|
||||
{#if folders.length === 0}
|
||||
<tr>
|
||||
<td colspan="4" class="text-gray-600 mt-2"> No folders yet, create one! </td>
|
||||
</tr>
|
||||
{/if}
|
||||
|
||||
{#each folders as { name, extra_perms, owners, canWrite }}
|
||||
<tr>
|
||||
<td>
|
||||
<a
|
||||
href="#{name}"
|
||||
on:click={() => {
|
||||
editFolderName = name
|
||||
folderDrawer.openDrawer()
|
||||
}}
|
||||
>{name}
|
||||
</a>
|
||||
<div>
|
||||
<SharedBadge {canWrite} extraPerms={extra_perms} />
|
||||
</div>
|
||||
</td>
|
||||
<td><FolderUsageInfo {name} /></td>
|
||||
|
||||
<td><FolderInfo members={computeMembers(owners, extra_perms)} /></td>
|
||||
<td>
|
||||
<Dropdown
|
||||
placement="bottom-end"
|
||||
dropdownItems={[
|
||||
{
|
||||
displayName: 'Manage folder',
|
||||
icon: faEdit,
|
||||
disabled: !canWrite,
|
||||
action: () => {
|
||||
editFolderName = name
|
||||
folderDrawer.openDrawer()
|
||||
}
|
||||
},
|
||||
{
|
||||
displayName: 'Delete',
|
||||
|
||||
icon: faTrash,
|
||||
type: 'delete',
|
||||
disabled: !canWrite,
|
||||
action: async () => {
|
||||
await FolderService.deleteFolder({
|
||||
workspace: $workspaceStore ?? '',
|
||||
name
|
||||
})
|
||||
loadFolders()
|
||||
}
|
||||
}
|
||||
]}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
{/if}
|
||||
|
||||
{#each folders as { name, extra_perms, canWrite }}
|
||||
<tr>
|
||||
<td>
|
||||
<a
|
||||
href="#{name}"
|
||||
on:click={() => {
|
||||
editFolderName = name
|
||||
folderDrawer.openDrawer()
|
||||
}}
|
||||
>{name}
|
||||
</a>
|
||||
<div>
|
||||
<SharedBadge {canWrite} extraPerms={extra_perms} />
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<Dropdown
|
||||
placement="bottom-end"
|
||||
dropdownItems={[
|
||||
{
|
||||
displayName: 'Manage folder',
|
||||
icon: faEdit,
|
||||
disabled: !canWrite,
|
||||
action: () => {
|
||||
editFolderName = name
|
||||
folderDrawer.openDrawer()
|
||||
}
|
||||
},
|
||||
{
|
||||
displayName: 'Delete',
|
||||
|
||||
icon: faTrash,
|
||||
type: 'delete',
|
||||
disabled: !canWrite,
|
||||
action: async () => {
|
||||
await FolderService.deleteFolder({ workspace: $workspaceStore ?? '', name })
|
||||
loadFolders()
|
||||
}
|
||||
}
|
||||
]}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</TableCustom>
|
||||
</div>
|
||||
|
||||
@@ -16,16 +16,16 @@
|
||||
import GroupEditor from '$lib/components/GroupEditor.svelte'
|
||||
import PageHeader from '$lib/components/PageHeader.svelte'
|
||||
import SharedBadge from '$lib/components/SharedBadge.svelte'
|
||||
import ShareModal from '$lib/components/ShareModal.svelte'
|
||||
import TableCustom from '$lib/components/TableCustom.svelte'
|
||||
import { userStore, workspaceStore } from '$lib/stores'
|
||||
import { faEdit, faPlus, faShare, faTrash } from '@fortawesome/free-solid-svg-icons'
|
||||
import { Button, Drawer, DrawerContent } from '$lib/components/common'
|
||||
import { Button, Drawer, DrawerContent, Skeleton } from '$lib/components/common'
|
||||
import GroupInfo from '$lib/components/GroupInfo.svelte'
|
||||
|
||||
type GroupW = Group & { canWrite: boolean }
|
||||
|
||||
let newGroupName: string = ''
|
||||
let groups: GroupW[] = []
|
||||
let groups: GroupW[] | undefined = undefined
|
||||
let groupDrawer: Drawer
|
||||
|
||||
async function loadGroups(): Promise<void> {
|
||||
@@ -89,54 +89,62 @@
|
||||
<tr slot="header-row">
|
||||
<th>Name</th>
|
||||
<th>Summary</th>
|
||||
<th>Members</th>
|
||||
<th />
|
||||
</tr>
|
||||
<tbody slot="body">
|
||||
{#each groups as { name, summary, extra_perms, canWrite }}
|
||||
<tr>
|
||||
<td>
|
||||
<a
|
||||
href="#{name}"
|
||||
on:click={() => {
|
||||
editGroupName = name
|
||||
groupDrawer.openDrawer()
|
||||
}}
|
||||
>{name}
|
||||
</a>
|
||||
<div>
|
||||
<SharedBadge {canWrite} extraPerms={extra_perms} />
|
||||
</div>
|
||||
</td>
|
||||
<td>{summary ?? ''}</td>
|
||||
<td>
|
||||
<Dropdown
|
||||
placement="bottom-end"
|
||||
dropdownItems={[
|
||||
{
|
||||
displayName: 'Manage group',
|
||||
icon: faEdit,
|
||||
disabled: !canWrite,
|
||||
action: () => {
|
||||
editGroupName = name
|
||||
groupDrawer.openDrawer()
|
||||
}
|
||||
},
|
||||
{
|
||||
displayName: 'Delete',
|
||||
{#if groups === undefined}
|
||||
{#each new Array(6) as _}
|
||||
<Skeleton layout={[0.25, [2], 0.25]} />
|
||||
{/each}
|
||||
{:else}
|
||||
{#each groups as { name, summary, extra_perms, canWrite }}
|
||||
<tr>
|
||||
<td>
|
||||
<a
|
||||
href="#{name}"
|
||||
on:click={() => {
|
||||
editGroupName = name
|
||||
groupDrawer.openDrawer()
|
||||
}}
|
||||
>{name}
|
||||
</a>
|
||||
<div>
|
||||
<SharedBadge {canWrite} extraPerms={extra_perms} />
|
||||
</div>
|
||||
</td>
|
||||
<td>{summary ?? ''}</td>
|
||||
<td><GroupInfo {name} /></td>
|
||||
<td>
|
||||
<Dropdown
|
||||
placement="bottom-end"
|
||||
dropdownItems={[
|
||||
{
|
||||
displayName: 'Manage group',
|
||||
icon: faEdit,
|
||||
disabled: !canWrite,
|
||||
action: () => {
|
||||
editGroupName = name
|
||||
groupDrawer.openDrawer()
|
||||
}
|
||||
},
|
||||
{
|
||||
displayName: 'Delete',
|
||||
|
||||
icon: faTrash,
|
||||
type: 'delete',
|
||||
disabled: !canWrite,
|
||||
action: async () => {
|
||||
await GroupService.deleteGroup({ workspace: $workspaceStore ?? '', name })
|
||||
loadGroups()
|
||||
icon: faTrash,
|
||||
type: 'delete',
|
||||
disabled: !canWrite,
|
||||
action: async () => {
|
||||
await GroupService.deleteGroup({ workspace: $workspaceStore ?? '', name })
|
||||
loadGroups()
|
||||
}
|
||||
}
|
||||
}
|
||||
]}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
]}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
{/if}
|
||||
</tbody>
|
||||
</TableCustom>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user