feat: introduce folders, deprecate items owned by groups

This commit is contained in:
Ruben Fiszel
2022-12-19 18:59:31 +01:00
parent ef95d02bbc
commit 2ff914edd3
14 changed files with 803 additions and 97 deletions
+194 -4
View File
@@ -1821,7 +1821,8 @@ paths:
(default false)
show also the archived files.
when multiple archived hash share the same path, only the ones with the latest create_at
are displayed.
are
ed.
in: query
schema:
type: boolean
@@ -3620,6 +3621,172 @@ paths:
schema:
type: string
/w/{workspace}/folders/list:
get:
summary: list folders
operationId: listFolders
tags:
- folder
parameters:
- $ref: "#/components/parameters/WorkspaceId"
- $ref: "#/components/parameters/Page"
- $ref: "#/components/parameters/PerPage"
responses:
"200":
description: folder list
content:
application/json:
schema:
type: array
items:
$ref: "#/components/schemas/Folder"
/w/{workspace}/folders/listnames:
get:
summary: list folder names
operationId: listFolderNames
tags:
- folder
parameters:
- $ref: "#/components/parameters/WorkspaceId"
- name: only_member_of
in: query
description: only list the folders the user is member of (default false)
schema:
type: boolean
responses:
"200":
description: folder list
content:
application/json:
schema:
type: array
items:
type: string
/w/{workspace}/folders/create:
post:
summary: create folder
operationId: createFolder
tags:
- folder
parameters:
- $ref: "#/components/parameters/WorkspaceId"
requestBody:
description: create folder
required: true
content:
application/json:
schema:
type: object
properties:
name:
type: string
owners:
type: array
items:
type: string
extra_perms:
additionalProperties:
type: boolean
required:
- name
responses:
"200":
description: folder created
content:
text/plain:
schema:
type: string
/w/{workspace}/folders/delete/{name}:
delete:
summary: delete folder
operationId: deleteFolder
tags:
- folder
parameters:
- $ref: "#/components/parameters/WorkspaceId"
- $ref: "#/components/parameters/Name"
responses:
"200":
description: folder deleted
content:
text/plain:
schema:
type: string
/w/{workspace}/folders/get/{name}:
get:
summary: get folder
operationId: getFolder
tags:
- folder
parameters:
- $ref: "#/components/parameters/WorkspaceId"
- $ref: "#/components/parameters/Name"
responses:
"200":
description: folder
content:
application/json:
schema:
$ref: "#/components/schemas/Folder"
/w/{workspace}/folders/addowner/{name}:
post:
summary: add owner to folder
operationId: addOwnerToFolder
tags:
- folder
parameters:
- $ref: "#/components/parameters/WorkspaceId"
- $ref: "#/components/parameters/Name"
requestBody:
description: owner user to folder
required: true
content:
application/json:
schema:
type: object
properties:
owner:
type: string
responses:
"200":
description: owner added to folder
content:
text/plain:
schema:
type: string
/w/{workspace}/folders/removeowner/{name}:
post:
summary: remove owner to folder
operationId: removeOwnerToFolder
tags:
- folder
parameters:
- $ref: "#/components/parameters/WorkspaceId"
- $ref: "#/components/parameters/Name"
requestBody:
description: added owner to folder
required: true
content:
application/json:
schema:
type: object
properties:
owner:
type: string
responses:
"200":
description: owner removed from folder
content:
text/plain:
schema:
type: string
/workers/list:
get:
summary: list workers
@@ -3653,7 +3820,7 @@ paths:
required: true
schema:
type: string
enum: [script, group_, resource, schedule, variable, flow]
enum: [script, group_, resource, schedule, variable, flow, folder]
responses:
"200":
description: acls
@@ -3678,7 +3845,7 @@ paths:
required: true
schema:
type: string
enum: [script, group_, resource, schedule, variable, flow]
enum: [script, group_, resource, schedule, variable, flow, folder]
requestBody:
description: acl to add
required: true
@@ -3714,7 +3881,7 @@ paths:
required: true
schema:
type: string
enum: [script, group_, resource, schedule, variable, flow]
enum: [script, group_, resource, schedule, variable, flow, folder]
requestBody:
description: acl to add
required: true
@@ -4297,6 +4464,10 @@ components:
type: array
items:
type: string
folders:
type: array
items:
type: string
usage:
$ref: "#/components/schemas/Usage"
required:
@@ -4307,6 +4478,7 @@ components:
- created_at
- operator
- disabled
- folders
Usage:
type: object
@@ -4832,6 +5004,24 @@ components:
required:
- name
Folder:
type: object
properties:
name:
type: string
owners:
type: array
items:
type: string
extra_perms:
type: object
additionalProperties:
type: boolean
required:
- name
- owners
- extra_perms
WorkerPing:
type: object
properties:
+6 -6
View File
@@ -62,7 +62,7 @@ pub struct ListableApp {
#[derive(FromRow, Serialize, Deserialize)]
pub struct AppVersion {
pub id: i64,
pub flow_id: Uuid,
pub app_id: Uuid,
pub value: serde_json::Value,
pub created_by: String,
pub created_at: chrono::DateTime<chrono::Utc>,
@@ -210,7 +210,7 @@ async fn get_app_by_id(
"SELECT app.id, app.path, app.summary, app.versions, app.policy,
app.extra_perms, app_version.value,
app_version.created_at, app_version.created_by from app, app_version
WHERE app_version.id = $1 AND app.id = app_version.flow_id AND app.workspace_id = $2",
WHERE app_version.id = $1 AND app.id = app_version.app_id AND app.workspace_id = $2",
id,
&w_id
)
@@ -244,7 +244,7 @@ async fn create_app(
let v_id = sqlx::query_scalar!(
"INSERT INTO app_version
(flow_id, value, created_by)
(app_id, value, created_by)
VALUES ($1, $2, $3) RETURNING id",
id,
app.value,
@@ -358,7 +358,7 @@ async fn update_app(
"".to_string()
};
if let Some(nvalue) = &ns.value {
let flow_id = sqlx::query_scalar!(
let app_id = sqlx::query_scalar!(
"SELECT id FROM app WHERE path = $1 AND workspace_id = $2",
path,
w_id
@@ -368,9 +368,9 @@ async fn update_app(
let v_id = sqlx::query_scalar!(
"INSERT INTO app_version
(flow_id, value, created_by)
(app_id, value, created_by)
VALUES ($1, $2, $3) RETURNING id",
flow_id,
app_id,
nvalue,
authed.username,
)
+28
View File
@@ -72,6 +72,34 @@ impl UserDB {
)
.fetch_optional(&mut tx)
.await?;
let (folders_write, folders_read): &(Vec<_>, Vec<_>) =
&authed.folders.clone().into_iter().partition(|x| x.1);
let mut folders_read = folders_read.clone();
folders_read.extend(folders_write.clone());
sqlx::query!(
"SELECT set_config('session.folders_read', $1, true)",
folders_read
.iter()
.map(|x| x.0.clone())
.collect::<Vec<_>>()
.join(",")
)
.fetch_optional(&mut tx)
.await?;
sqlx::query!(
"SELECT set_config('session.folders_write', $1, true)",
folders_write
.iter()
.map(|x| x.0.clone())
.collect::<Vec<_>>()
.join(",")
)
.fetch_optional(&mut tx)
.await?;
Ok(tx)
}
}
+10 -2
View File
@@ -44,7 +44,11 @@ async fn add_granular_acl(
.ok_or_else(|| Error::BadRequest("Invalid path or kind".to_string()))?;
let mut tx = user_db.begin(&authed).await?;
let identifier = if kind == "group_" { "name" } else { "path" };
let identifier = if kind == "group_" || kind == "folder" {
"name"
} else {
"path"
};
let obj_o = sqlx::query_scalar::<_, serde_json::Value>(&format!(
"UPDATE {kind} SET extra_perms = jsonb_set(extra_perms, '{{\"{owner}\"}}', to_jsonb($1), \
true) WHERE {identifier} = $2 AND workspace_id = $3 RETURNING extra_perms"
@@ -73,7 +77,11 @@ async fn remove_granular_acl(
.ok_or_else(|| Error::BadRequest("Invalid path or kind".to_string()))?;
let mut tx = user_db.begin(&authed).await?;
let identifier = if kind == "group_" { "name" } else { "path" };
let identifier = if kind == "group_" || kind == "folder" {
"name"
} else {
"path"
};
let obj_o = sqlx::query_scalar::<_, serde_json::Value>(&format!(
"UPDATE {kind} SET extra_perms = extra_perms - $1 WHERE {identifier} = $2 AND \
workspace_id = $3 RETURNING extra_perms"
+3 -1
View File
@@ -29,6 +29,7 @@ mod capture;
mod db;
mod favorite;
mod flows;
mod folders;
mod granular_acls;
mod groups;
pub mod jobs;
@@ -126,7 +127,8 @@ pub async fn run_server(
.nest("/flows", flows::workspaced_service())
.nest("/capture", capture::workspaced_service())
.nest("/apps", apps::workspaced_service())
.nest("/favorites", favorite::workspaced_service()),
.nest("/favorites", favorite::workspaced_service())
.nest("/folders", folders::workspaced_service()),
)
.nest("/workspaces", workspaces::global_service())
.nest(
+3 -1
View File
@@ -1,6 +1,6 @@
import type { Script } from "./gen"
export type OwnerKind = 'group' | 'user'
export type OwnerKind = 'group' | 'user' | 'folder'
export type ActionKind = 'Create' | 'Update' | 'Delete' | 'Execute'
@@ -32,6 +32,8 @@ export function pathToMeta(path: string): Meta {
let ownerKind: OwnerKind
if (splitted[0] == 'g') {
ownerKind = 'group'
} else if (splitted[0] == 'f') {
ownerKind = 'folder'
} else if (splitted[0] == 'u') {
ownerKind = 'user'
} else {
@@ -0,0 +1,317 @@
<script lang="ts">
import { userStore, workspaceStore } from '$lib/stores'
import {
type Folder,
FolderService,
UserService,
GranularAclService,
GroupService
} from '$lib/gen'
import AutoComplete from 'simple-svelte-autocomplete'
import TableCustom from './TableCustom.svelte'
import { canWrite } from '$lib/utils'
import { Button, ToggleButton, ToggleButtonGroup } from './common'
import Skeleton from './common/skeleton/Skeleton.svelte'
import Tooltip from './Tooltip.svelte'
export let name: string
let can_write = false
type Role = 'viewer' | 'writer' | 'owner'
let folder: Folder | undefined
let perms: { owner_name: string; role: Role }[] | undefined = undefined
let managing_folders: string[] = []
let ownerItems: string[] | undefined = []
let ownerItem: string = ''
let folders: string[] = []
async function loadUsernames(): Promise<void> {
ownerItem = ''
ownerItems = await UserService.listUsernames({ workspace: $workspaceStore! })
}
async function loadGroups(): Promise<void> {
ownerItem = ''
ownerItems = await GroupService.listGroupNames({ workspace: $workspaceStore! })
}
$: {
if ($workspaceStore && $userStore) {
load()
}
}
$: {
if ($workspaceStore && $userStore) {
if (ownerKind == 'user') {
loadUsernames()
} else {
loadGroups()
}
}
}
async function load() {
loadFolders()
await loadFolder()
loadUsernames()
}
async function loadFolders(): Promise<void> {
folders = (await FolderService.listFolders({ workspace: $workspaceStore! })).map((x) => x.name)
}
async function addToFolder() {
await GranularAclService.addGranularAcls({
workspace: $workspaceStore ?? '',
path: name,
kind: 'folder',
requestBody: {
owner: (ownerKind == 'user' ? 'u/' : 'g/') + ownerItem
}
})
loadFolder()
}
async function loadFolder(): Promise<void> {
folder = await FolderService.getFolder({ workspace: $workspaceStore!, name })
can_write = canWrite(name!, folder.extra_perms ?? {}, $userStore)
perms = Array.from(
new Set(
Object.entries(folder?.extra_perms ?? {})
.map((x) => x[0])
.concat(folder?.owners ?? [])
)
).map((x) => {
return {
owner_name: x,
role: getRole(x)
}
})
managing_folders = Object.entries(folder?.extra_perms ?? {})
.filter(([k, v]) => k.startsWith('g/') && v)
.map(([k, v]) => k)
}
function getRole(x: string): Role {
const viewer = x in (folder?.extra_perms ?? {})
const writer = viewer && (folder?.extra_perms ?? {})[x]
const owner = folder?.owners?.includes(x)
if (owner) {
return 'owner'
} else if (writer) {
return 'writer'
} else {
return 'viewer'
}
}
let ownerKind: 'user' | 'group' = 'user'
</script>
<div class="flex flex-col gap-6">
<h1>{name}</h1>
<h2>Permissions</h2>
{#if can_write}
<div class="flex items-center gap-1">
<div>
<ToggleButtonGroup bind:selected={ownerKind}>
<ToggleButton position="left" value="user" size="xs">User</ToggleButton>
<ToggleButton position="right" value="group" size="xs">Group</ToggleButton>
</ToggleButtonGroup>
</div>
<AutoComplete items={ownerItems} bind:selectedItem={ownerItem} />
<Button
disabled={ownerItem == ''}
variant="contained"
color="blue"
size="sm"
on:click={addToFolder}
>
Grant permission to folder
</Button>
</div>
{/if}
{#if perms}
<TableCustom>
<tr slot="header-row">
<th>user</th>
<th />
<th />
</tr>
<tbody slot="body">
{#each perms as { owner_name, role }}<tr>
<td>{owner_name}</td>
<td>
{#if can_write}
<div>
<ToggleButtonGroup
selected={role}
on:selected={async (e) => {
const role = e.detail
// const wasInFolder = (folder?.owners ?? []).includes(folder)
// const inAcl = (
// folder?.extra_perms ? Object.keys(folder?.extra_perms) : []
// ).includes(folder)
if (role == 'owner') {
await FolderService.addOwnerToFolder({
workspace: $workspaceStore ?? '',
name,
requestBody: {
owner: owner_name
}
})
await GranularAclService.addGranularAcls({
workspace: $workspaceStore ?? '',
path: name,
kind: 'folder',
requestBody: {
owner: owner_name
}
})
} else if (role == 'writer') {
await FolderService.removeOwnerToFolder({
workspace: $workspaceStore ?? '',
name,
requestBody: {
owner: owner_name
}
})
await GranularAclService.addGranularAcls({
workspace: $workspaceStore ?? '',
path: name,
kind: 'folder',
requestBody: {
owner: owner_name,
write: true
}
})
} else if (role == 'viewer') {
await FolderService.removeOwnerToFolder({
workspace: $workspaceStore ?? '',
name,
requestBody: {
owner: owner_name
}
})
await GranularAclService.addGranularAcls({
workspace: $workspaceStore ?? '',
path: name,
kind: 'folder',
requestBody: {
owner: owner_name,
write: false
}
})
}
loadFolder()
}}
>
<ToggleButton position="left" value="viewer" size="xs"
>Viewer&nbsp;<Tooltip
>A viewer of a folder has read-only access to all the elements
(scripts/flows/apps/schedules/resources/variables) inside the folder</Tooltip
></ToggleButton
>
<ToggleButton position="center" value="writer" size="xs"
>Writer&nbsp;<Tooltip
>A viewer 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="owner" size="xs"
>Admin&nbsp;<Tooltip
>An admin of a folder has read AND write access to all the elements inside
the folders and can manage the permissions as well as add new admins</Tooltip
></ToggleButton
>
</ToggleButtonGroup>
</div>
{/if}</td
>
<td>
{#if can_write}
<button
class="ml-2 text-red-500"
on:click={async () => {
await FolderService.removeOwnerToFolder({
workspace: $workspaceStore ?? '',
name,
requestBody: { owner: owner_name }
})
await GranularAclService.removeGranularAcls({
workspace: $workspaceStore ?? '',
path: name,
kind: 'folder',
requestBody: {
owner: owner_name
}
})
loadFolder()
}}>remove</button
>
{/if}</td
>
</tr>{/each}
</tbody>
</TableCustom>
<!-- <h2 class="mt-10"
>Folders managing this folder <Tooltip
>Any owner of those folders can manage this folder</Tooltip
></h2
>
{#if can_write}
<div class="flex items-start">
<AutoComplete items={folders} bind:selectedItem={new_managing_folder} />
<Button
variant="contained"
color="blue"
size="sm"
btnClasses="!ml-4"
on:click={addToManagingFolder}
>
Add folder managing this folder
</Button>
</div>
{/if} -->
<!-- {#if managing_folders.length == 0}
<p class="text-gray-600 text-sm">No folder is managing this folder</p>
{:else}
<TableCustom>
<tr slot="header-row">
<th>folder</th>
<th />
</tr>
<tbody slot="body">
{#each managing_folders as managing_folder}<tr>
<td>{managing_folder.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: 'folder',
requestBody: {
owner: managing_folder
}
})
loadFolder()
}}>remove</button
>
{/if}</td
>
</tr>{/each}
</tbody>
</TableCustom>
{/if} -->
{:else}
<div class="flex flex-col">
{#each new Array(6) as _}
<Skeleton layout={[[2], 0.7]} />
{/each}
</div>
{/if}
</div>
+33 -23
View File
@@ -3,10 +3,12 @@
import { type Group, GroupService, UserService, GranularAclService } from '$lib/gen'
import AutoComplete from 'simple-svelte-autocomplete'
import TableCustom from './TableCustom.svelte'
import { canWrite } from '$lib/utils'
import { canWrite, sendUserToast } from '$lib/utils'
import { Button, ToggleButton, ToggleButtonGroup } from './common'
import Skeleton from './common/skeleton/Skeleton.svelte'
import Tooltip from './Tooltip.svelte'
import autosize from 'svelte-autosize'
import { createEventDispatcher } from 'svelte'
export let name: string
let can_write = false
@@ -19,6 +21,8 @@
let username: string = ''
let groups: string[] = []
const dispatch = createEventDispatcher()
async function loadUsernames(): Promise<void> {
usernames = await UserService.listUsernames({ workspace: $workspaceStore! })
}
@@ -48,19 +52,6 @@
loadGroup()
}
async function addToManagingGroup() {
await GranularAclService.addGranularAcls({
workspace: $workspaceStore ?? '',
path: name,
kind: 'group_',
requestBody: {
owner: 'g/' + new_managing_group,
write: true
}
})
loadGroup()
}
async function loadGroup(): Promise<void> {
group = await GroupService.getGroup({ workspace: $workspaceStore!, name })
can_write = canWrite(name!, group.extra_perms ?? {}, $userStore)
@@ -85,7 +76,7 @@
function getRole(x: string): Role {
const writer = 'u/' + x in (group?.extra_perms ?? {}) && (group?.extra_perms ?? {})['u/' + x]
const member = group?.members?.includes(x)
console.log(writer, member, x, group?.members, group?.extra_perms)
if (writer && member) {
return 'member_manager'
} else if (writer) {
@@ -94,14 +85,33 @@
return 'member'
}
}
let new_managing_group = ''
</script>
<div class="flex flex-col gap-6">
<h1>{name}</h1>
{#if group}
<p>{group?.summary ?? 'No summary'}</p>
<div class="flex flex-col gap-1">
<textarea
disabled={!can_write}
rows="2"
type="text"
use:autosize
bind:value={group.summary}
placeholder="Summary of the group"
/>
<Button
size="xs"
on:click={async () => {
await GroupService.updateGroup({
workspace: $workspaceStore ?? '',
name,
requestBody: { summary: group?.summary }
})
dispatch('update')
sendUserToast('New summary saved')
}}>Save Summary</Button
>
</div>
{:else}
<Skeleton layout={[[4]]} />
{/if}
@@ -195,14 +205,14 @@
everything the group can write, and generally act on behalf of the group</Tooltip
></ToggleButton
>
<ToggleButton position="center" value="manager" size="xs"
<!-- <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="member_manager" size="xs"
>Member & Manager</ToggleButton
>Admin</ToggleButton
>
</ToggleButtonGroup>
</div>
@@ -226,7 +236,7 @@
</tr>{/each}
</tbody>
</TableCustom>
<h2 class="mt-10"
<!-- <h2 class="mt-10"
>Groups managing this group <Tooltip>Any member of those groups can manage this group</Tooltip
></h2
>
@@ -276,7 +286,7 @@
</tr>{/each}
</tbody>
</TableCustom>
{/if}
{/if} -->
{:else}
<div class="flex flex-col">
{#each new Array(6) as _}
+46 -56
View File
@@ -3,22 +3,22 @@
import {
FlowService,
FolderService,
ResourceService,
ScheduleService,
ScriptService,
VariableService
} from '$lib/gen'
import { GroupService } from '$lib/gen'
import { superadmin, userStore, workspaceStore } from '$lib/stores'
import { createEventDispatcher } from 'svelte'
import Required from './Required.svelte'
import { Button, Drawer, DrawerContent } from './common'
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'
import Tooltip from './Tooltip.svelte'
import FolderEditor from './FolderEditor.svelte'
type PathKind = 'resource' | 'script' | 'variable' | 'flow' | 'schedule' | 'app'
let meta: Meta | undefined = undefined
@@ -35,7 +35,7 @@
const dispatch = createEventDispatcher()
let groups: string[] = []
let folders: string[] = []
$: meta && onMetaChange()
@@ -47,7 +47,7 @@
}
function metaToPath(meta: Meta): string {
return [meta.ownerKind === 'group' ? 'g' : 'u', meta.owner, meta.name].join('/')
return [meta.ownerKind === 'folder' ? 'f' : 'u', meta.owner, meta.name].join('/')
}
export function focus() {
@@ -80,15 +80,14 @@
}
}
async function loadGroups(): Promise<void> {
let initialGroups: string[] = []
if (initialPath?.split('/')?.[0] == 'g') {
initialGroups.push(initialPath?.split('/')?.[1])
async function loadFolders(): Promise<void> {
let initialFolders: string[] = []
if (initialPath?.split('/')?.[0] == 'f') {
initialFolders.push(initialPath?.split('/')?.[1])
}
groups = initialGroups.concat(
await GroupService.listGroupNames({
workspace: $workspaceStore!,
onlyMemberOf: !$userStore?.is_admin
folders = initialFolders.concat(
await FolderService.listFolderNames({
workspace: $workspaceStore!
})
)
}
@@ -149,6 +148,9 @@
} else if (!/^[\w-]+(\/[\w-]+)*$/.test(meta.name)) {
error = 'This name is not valid'
return false
} else if (meta.owner == '') {
error = 'Folder need to be chosen'
return false
} else {
return true
}
@@ -156,7 +158,7 @@
$: {
if ($workspaceStore && $userStore) {
loadGroups()
loadFolders()
initPath()
}
}
@@ -171,40 +173,43 @@
}
}
let newGroup: Drawer
let viewGroup: Drawer
let newGroupName: string
let groupCreated: string | undefined = undefined
let newFolder: Drawer
let viewFolder: Drawer
let newFolderName: string
let folderCreated: string | undefined = undefined
async function addGroup() {
await GroupService.createGroup({
async function addFolder() {
await FolderService.createFolder({
workspace: $workspaceStore ?? '',
requestBody: { name: newGroupName }
requestBody: { name: newFolderName }
})
groupCreated = newGroupName
loadGroups()
folderCreated = newFolderName
if (meta) {
meta.owner = newFolderName
}
loadFolders()
}
</script>
<Drawer bind:this={newGroup}>
<DrawerContent title="New Group" on:close={newGroup.closeDrawer}>
<Drawer bind:this={newFolder}>
<DrawerContent title="New Folder" on:close={newFolder.closeDrawer}>
<div class="flex flex-row">
<input class="mr-2" placeholder="New group name" bind:value={newGroupName} />
<Button size="md" startIcon={{ icon: faPlus }} disabled={!newGroupName} on:click={addGroup}>
New&nbsp;group
<input class="mr-2" placeholder="New folder name" bind:value={newFolderName} />
<Button size="md" startIcon={{ icon: faPlus }} disabled={!newFolderName} on:click={addFolder}>
New&nbsp;folder
</Button>
</div>
{#if groupCreated}
{#if folderCreated}
<div class="mt-8" />
<GroupEditor name={groupCreated} />
<FolderEditor name={folderCreated} />
{/if}
</DrawerContent>
</Drawer>
<Drawer bind:this={viewGroup}>
<DrawerContent title="Group {meta?.owner}" on:close={viewGroup.closeDrawer}>
<GroupEditor name={meta?.owner ?? ''} />
<Drawer bind:this={viewFolder}>
<DrawerContent title="Folder {meta?.owner}" on:close={viewFolder.closeDrawer}>
<FolderEditor name={meta?.owner ?? ''} />
</DrawerContent>
</Drawer>
@@ -220,10 +225,9 @@
bind:selected={meta.ownerKind}
on:selected={(e) => {
const kind = e.detail
console.log(kind)
if (meta) {
if (kind === 'group') {
meta.owner = 'all'
if (kind === 'folder') {
meta.owner = $userStore?.folders?.[0] ?? ''
} else {
meta.owner = $userStore?.username ?? ''
}
@@ -231,19 +235,12 @@
}}
>
<ToggleButton light size="xs" value="user" position="left">User</ToggleButton>
<ToggleButton light size="xs" value="group" position="right">Group</ToggleButton>
<ToggleButton light size="xs" value="folder" position="right">Folder</ToggleButton>
</ToggleButtonGroup>
</label>
{#if meta.ownerKind === 'user'}
<label class="block shrink min-w-0">
<span
><span class="text-gray-700 text-sm mr-1">Owner</span><Tooltip
>The prefix of a path defines the owner of an item. An owner has write permissions
and can modify the path. An item can still be made writable or readable using
granular perissioning.
<a href="https://docs.windmill.dev/docs/reference/namespaces">See docs</a></Tooltip
></span
>
<span class="text-gray-700 text-sm">User</span>
<input
class="!w-36"
type="text"
@@ -254,25 +251,18 @@
</label>
{:else}
<label class="block grow w-48">
<span
><span class="text-gray-700 text-sm mr-1">Owner</span><Tooltip
>The prefix of a path defines the owner of an item. An owner has write permissions
and can modify the path. An item can still be made writable or readable using
granular perissioning.
<a href="https://docs.windmill.dev/docs/reference/namespaces">docs</a></Tooltip
></span
>
<span class="text-gray-700 text-sm">Folder</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 folders as f}
<option>{f}</option>
{/each}
</select>
<Button variant="border" size="xs" on:click={viewGroup.openDrawer}>
<Button variant="border" size="xs" on:click={viewFolder.openDrawer}>
<Icon scale={0.8} data={faEye} /></Button
>
<Button variant="border" size="xs" on:click={newGroup.openDrawer}>
<Button variant="border" size="xs" on:click={newFolder.openDrawer}>
<Icon scale={0.8} data={faPlus} /></Button
></div
>
@@ -9,11 +9,12 @@
faRobot,
faUsersCog,
faCog,
faDollarSign
faDollarSign,
faFolderTree
} from '@fortawesome/free-solid-svg-icons'
import { faDiscord, faGithub } from '@fortawesome/free-brands-svg-icons'
import MenuLink from './MenuLink.svelte'
import { superadmin, userStore } from '$lib/stores'
import { userStore } from '$lib/stores'
const mainMenuLinks = [
{ label: 'Home', href: '/', icon: faHomeAlt },
@@ -24,6 +25,7 @@
$: secondaryMenuLinks = [
{ label: 'Schedules', href: '/schedules', icon: faCalendar, disabled: $userStore?.operator },
{ label: 'Folders', href: '/folders', icon: faFolderTree, disabled: $userStore?.operator },
{ label: 'Groups', href: '/groups', icon: faUsersCog, disabled: $userStore?.operator },
{ label: 'Audit Logs', href: '/audit_logs', icon: faEye, disabled: $userStore?.operator },
{
+1
View File
@@ -13,6 +13,7 @@ export interface UserExt {
created_at: string;
groups: string[];
pgroups: string[];
folders: string[]
}
let persistedWorkspace = browser && localStorage.getItem("workspace");
+6 -1
View File
@@ -199,6 +199,11 @@ export function canWrite(
) {
return true
}
if (
user.folders.findIndex((x) => path.startsWith('f/' + x)) != -1
) {
return true
}
return false
}
@@ -332,7 +337,7 @@ export function mapUserToUserExt(user: User): UserExt {
return {
...user,
groups: user.groups!,
pgroups: user.groups!.map((x) => `g/${x}`)
pgroups: user.groups!.map((x) => `g/${x}`),
}
}
+149
View File
@@ -0,0 +1,149 @@
<script context="module">
export function load() {
return {
stuff: { title: 'Folders' }
}
}
</script>
<script lang="ts">
import type { Folder } from '$lib/gen'
import { FolderService } from '$lib/gen'
import { canWrite } from '$lib/utils'
import CenteredPage from '$lib/components/CenteredPage.svelte'
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 } from '$lib/components/common'
type FolderW = Folder & { canWrite: boolean }
let newFolderName: string = ''
let folders: FolderW[] = []
let folderDrawer: Drawer
async function loadFolders(): Promise<void> {
folders = (await FolderService.listFolders({ workspace: $workspaceStore! })).map((x) => {
return { canWrite: canWrite(x.name, x.extra_perms ?? {}, $userStore), ...x }
})
}
function handleKeyUp(event: KeyboardEvent) {
const key = event.key
if (key === 'Enter') {
event.preventDefault()
addFolder()
}
}
async function addFolder() {
await FolderService.createFolder({
workspace: $workspaceStore ?? '',
requestBody: { name: newFolderName }
})
loadFolders()
editFolderName = newFolderName
folderDrawer.openDrawer()
}
$: {
if ($workspaceStore && $userStore) {
loadFolders()
}
}
let editFolderName: string = ''
</script>
<Drawer bind:this={folderDrawer}>
<DrawerContent title="Folder {editFolderName}" on:close={folderDrawer.closeDrawer}>
<FolderEditor on:update={loadFolders} name={editFolderName} />
</DrawerContent>
</Drawer>
<CenteredPage>
<PageHeader
title="Folders"
tooltip="Folder users together to grant roles and homegenous permissions. Same users can be in many folders at the same time."
>
<div class="flex flex-row">
<input
class="mr-2"
on:keyup={handleKeyUp}
placeholder="New folder name"
bind:value={newFolderName}
/>
<Button size="md" startIcon={{ icon: faPlus }} disabled={!newFolderName} on:click={addFolder}>
New&nbsp;folder
</Button>
</div>
</PageHeader>
<div class="relative mb-20">
<TableCustom>
<tr slot="header-row">
<th>Name</th>
<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}
{#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>
</CenteredPage>
<style>
</style>
+3 -1
View File
@@ -47,6 +47,8 @@
requestBody: { name: newGroupName }
})
loadGroups()
editGroupName = newGroupName
groupDrawer.openDrawer()
}
$: {
@@ -60,7 +62,7 @@
<Drawer bind:this={groupDrawer}>
<DrawerContent title="Group {editGroupName}" on:close={groupDrawer.closeDrawer}>
<GroupEditor name={editGroupName} />
<GroupEditor on:update={loadGroups} name={editGroupName} />
</DrawerContent>
</Drawer>