diff --git a/backend/windmill-api/openapi.yaml b/backend/windmill-api/openapi.yaml index f08a02f95d..af4188c2a7 100644 --- a/backend/windmill-api/openapi.yaml +++ b/backend/windmill-api/openapi.yaml @@ -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 diff --git a/backend/windmill-api/src/folders.rs b/backend/windmill-api/src/folders.rs index 2d164050a9..b0395a45fc 100644 --- a/backend/windmill-api/src/folders.rs +++ b/backend/windmill-api/src/folders.rs @@ -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, + Path((w_id, name)): Path<(String, String)>, +) -> JsonResult { + 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, diff --git a/frontend/src/lib/components/FolderInfo.svelte b/frontend/src/lib/components/FolderInfo.svelte new file mode 100644 index 0000000000..e00e68ce37 --- /dev/null +++ b/frontend/src/lib/components/FolderInfo.svelte @@ -0,0 +1,15 @@ + + +
({members.length}) +
{members?.join(', ')}
+ {members?.join(', ')}
diff --git a/frontend/src/lib/components/FolderUsageInfo.svelte b/frontend/src/lib/components/FolderUsageInfo.svelte new file mode 100644 index 0000000000..9145cd0aa9 --- /dev/null +++ b/frontend/src/lib/components/FolderUsageInfo.svelte @@ -0,0 +1,22 @@ + + +
+ {#each Object.entries(usage) as [k, v]} +
+ {k}: {v} +
+ {/each} +
diff --git a/frontend/src/lib/components/GroupInfo.svelte b/frontend/src/lib/components/GroupInfo.svelte new file mode 100644 index 0000000000..de79f23720 --- /dev/null +++ b/frontend/src/lib/components/GroupInfo.svelte @@ -0,0 +1,27 @@ + + +{#if members} +
({members.length}) +
{members?.join(', ')}
+ {members?.join(', ')}
+{/if} diff --git a/frontend/src/routes/folders.svelte b/frontend/src/routes/folders.svelte index f311297175..4ff57a9de6 100644 --- a/frontend/src/routes/folders.svelte +++ b/frontend/src/routes/folders.svelte @@ -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 { @@ -57,6 +59,14 @@ } let editFolderName: string = '' + + function computeMembers(owners: string[], extra_perms: Record) { + const members = new Set(owners) + for (const [user, _] of Object.entries(extra_perms)) { + members.add(user) + } + return Array.from(members) + } @@ -87,59 +97,72 @@ Name - + Usage + Participants - {#if folders.length === 0} - - No folders yet, create one! - + {#if folders === undefined} + {#each new Array(6) as _} + + {/each} + {:else} + {#if folders.length === 0} + + No folders yet, create one! + + {/if} + + {#each folders as { name, extra_perms, owners, canWrite }} + + + { + editFolderName = name + folderDrawer.openDrawer() + }} + >{name} + +
+ +
+ + + + + + { + editFolderName = name + folderDrawer.openDrawer() + } + }, + { + displayName: 'Delete', + + icon: faTrash, + type: 'delete', + disabled: !canWrite, + action: async () => { + await FolderService.deleteFolder({ + workspace: $workspaceStore ?? '', + name + }) + loadFolders() + } + } + ]} + /> + + + {/each} {/if} - - {#each folders as { name, extra_perms, canWrite }} - - - { - editFolderName = name - folderDrawer.openDrawer() - }} - >{name} - -
- -
- - - { - editFolderName = name - folderDrawer.openDrawer() - } - }, - { - displayName: 'Delete', - - icon: faTrash, - type: 'delete', - disabled: !canWrite, - action: async () => { - await FolderService.deleteFolder({ workspace: $workspaceStore ?? '', name }) - loadFolders() - } - } - ]} - /> - - - {/each}
diff --git a/frontend/src/routes/groups.svelte b/frontend/src/routes/groups.svelte index d525eb7e36..3180036e23 100644 --- a/frontend/src/routes/groups.svelte +++ b/frontend/src/routes/groups.svelte @@ -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 { @@ -89,54 +89,62 @@ Name Summary + Members - {#each groups as { name, summary, extra_perms, canWrite }} - - - { - editGroupName = name - groupDrawer.openDrawer() - }} - >{name} - -
- -
- - {summary ?? ''} - - { - editGroupName = name - groupDrawer.openDrawer() - } - }, - { - displayName: 'Delete', + {#if groups === undefined} + {#each new Array(6) as _} + + {/each} + {:else} + {#each groups as { name, summary, extra_perms, canWrite }} + + + { + editGroupName = name + groupDrawer.openDrawer() + }} + >{name} + +
+ +
+ + {summary ?? ''} + + + { + 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() + } } - } - ]} - /> - - - {/each} + ]} + /> + + + {/each} + {/if}