mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-08-23 00:00:33 +00:00
groups
This commit is contained in:
@@ -0,0 +1,166 @@
|
||||
<script lang="ts">
|
||||
import { userStore, workspaceStore } from '$lib/stores'
|
||||
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 { 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' | 'member' | 'manager' | 'member_manager'
|
||||
let group: Group | undefined
|
||||
let members: { name: string; role: Role }[] | undefined = undefined
|
||||
let usernames: string[] | undefined = []
|
||||
let username: string = ''
|
||||
|
||||
async function loadUsernames(): Promise<void> {
|
||||
usernames = await UserService.listUsernames({ workspace: $workspaceStore! })
|
||||
}
|
||||
|
||||
$: {
|
||||
if ($workspaceStore && $userStore) {
|
||||
load()
|
||||
}
|
||||
}
|
||||
|
||||
async function load() {
|
||||
await loadGroup()
|
||||
await loadUsernames()
|
||||
}
|
||||
|
||||
async function addToGroup() {
|
||||
await GroupService.addUserToGroup({
|
||||
workspace: $workspaceStore ?? '',
|
||||
name,
|
||||
requestBody: { username }
|
||||
})
|
||||
loadGroup()
|
||||
}
|
||||
|
||||
async function loadGroup(): Promise<void> {
|
||||
group = await GroupService.getGroup({ workspace: $workspaceStore!, name })
|
||||
can_write = canWrite(group.name!, group.extra_perms ?? {}, $userStore)
|
||||
members = Object.keys(group?.extra_perms ?? {})
|
||||
.concat(group?.members ?? [])
|
||||
.map((x) => {
|
||||
return {
|
||||
name: x,
|
||||
role: getRole(x)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function getRole(x: string): Role {
|
||||
const writer = x in (group?.extra_perms ?? {}) && (group?.extra_perms ?? {})[name]
|
||||
const member = group?.members?.includes(x)
|
||||
if (writer && member) {
|
||||
return 'member_manager'
|
||||
} else if (writer) {
|
||||
return 'manager'
|
||||
} else if (member) {
|
||||
return 'member'
|
||||
}
|
||||
return 'viewer'
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col gap-6">
|
||||
<h2>Summary</h2>
|
||||
{#if group}
|
||||
<p>{group?.summary ?? 'No summary'}</p>
|
||||
{:else}
|
||||
<Skeleton layout={[[4]]} />
|
||||
{/if}
|
||||
<h2>Members</h2>
|
||||
{#if can_write}
|
||||
<div class="flex items-start">
|
||||
<AutoComplete items={usernames} bind:selectedItem={username} />
|
||||
<Button variant="contained" color="blue" size="sm" btnClasses="!ml-4" on:click={addToGroup}>
|
||||
Add member
|
||||
</Button>
|
||||
</div>
|
||||
{/if}
|
||||
{#if members}
|
||||
<TableCustom>
|
||||
<tr slot="header-row">
|
||||
<th>user</th>
|
||||
<th />
|
||||
<th />
|
||||
</tr>
|
||||
<tbody slot="body">
|
||||
{#each members as { name, role }}<tr>
|
||||
<td>{name}</td>
|
||||
<td>
|
||||
<ToggleButtonGroup
|
||||
selected={role}
|
||||
on:selected={async (e) => {
|
||||
const group = e.detail
|
||||
// const wasInGroup = (group?.members ?? []).includes(group)
|
||||
// const inAcl = (
|
||||
// group?.extra_perms ? Object.keys(group?.extra_perms) : []
|
||||
// ).includes(group)
|
||||
if (group == 'member') {
|
||||
GroupService.addUserToGroup(group)
|
||||
|
||||
GranularAclService.removeGranularAcls({
|
||||
workspace: $workspaceStore ?? '',
|
||||
path: name,
|
||||
kind: 'group_',
|
||||
requestBody: $userStore.
|
||||
})
|
||||
}
|
||||
loadGroup()
|
||||
}}
|
||||
>
|
||||
<ToggleButton position="left" value="member" size="xs"
|
||||
>Viewer <Tooltip
|
||||
>A viewer can see who are the members and managers of a group</Tooltip
|
||||
></ToggleButton
|
||||
>
|
||||
<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 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. He is not necessarily a member of the group.</Tooltip
|
||||
></ToggleButton
|
||||
>
|
||||
<ToggleButton position="right" value="member_manager" size="xs"
|
||||
>Member & Manager</ToggleButton
|
||||
>
|
||||
</ToggleButtonGroup></td
|
||||
>
|
||||
<td>
|
||||
{#if can_write}
|
||||
<button
|
||||
class="ml-2 text-red-500"
|
||||
on:click={async () => {
|
||||
await GroupService.removeUserToGroup({
|
||||
workspace: $workspaceStore ?? '',
|
||||
name: group?.name ?? '',
|
||||
requestBody: { username: name }
|
||||
})
|
||||
loadGroup()
|
||||
}}>remove</button
|
||||
>
|
||||
{/if}</td
|
||||
>
|
||||
</tr>{/each}
|
||||
</tbody>
|
||||
</TableCustom>
|
||||
{:else}
|
||||
<div class="flex flex-col">
|
||||
{#each new Array(6) as _}
|
||||
<Skeleton layout={[[2], 0.7]} />
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -1,105 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { userStore, workspaceStore } from '$lib/stores'
|
||||
import { type Group, GroupService, UserService } from '$lib/gen'
|
||||
import AutoComplete from 'simple-svelte-autocomplete'
|
||||
import TableCustom from './TableCustom.svelte'
|
||||
import { canWrite } from '$lib/utils'
|
||||
import { Button, Drawer } from './common'
|
||||
import DrawerContent from './common/drawer/DrawerContent.svelte'
|
||||
|
||||
let name = ''
|
||||
let drawer: Drawer
|
||||
let can_write = false
|
||||
|
||||
let group: Group | undefined
|
||||
let members: { name: string; isAdmin: boolean }[] = []
|
||||
let usernames: string[] = []
|
||||
let username: string = ''
|
||||
|
||||
async function loadUsernames(): Promise<void> {
|
||||
usernames = await UserService.listUsernames({ workspace: $workspaceStore! })
|
||||
}
|
||||
|
||||
$: {
|
||||
if (group && $workspaceStore && userStore) {
|
||||
members = (group.members ?? []).map((x) => {
|
||||
return {
|
||||
name: x,
|
||||
isAdmin: x in (group?.extra_perms ?? {}) && (group?.extra_perms ?? {})[name]
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
export function openDrawer(newName: string): void {
|
||||
name = newName
|
||||
loadGroup()
|
||||
loadUsernames()
|
||||
drawer.openDrawer()
|
||||
}
|
||||
|
||||
async function addToGroup() {
|
||||
await GroupService.addUserToGroup({
|
||||
workspace: $workspaceStore ?? '',
|
||||
name,
|
||||
requestBody: { username }
|
||||
})
|
||||
loadGroup()
|
||||
}
|
||||
|
||||
async function loadGroup(): Promise<void> {
|
||||
group = await GroupService.getGroup({ workspace: $workspaceStore!, name })
|
||||
can_write = canWrite(group.name!, group.extra_perms ?? {}, $userStore)
|
||||
}
|
||||
</script>
|
||||
|
||||
<Drawer bind:this={drawer}>
|
||||
<DrawerContent title="Group {name}" on:close={drawer.closeDrawer}>
|
||||
<div class="flex flex-col gap-6">
|
||||
<h2>Summary</h2>
|
||||
<p>{group?.summary ?? 'No summary'}</p>
|
||||
<h2>Members</h2>
|
||||
{#if can_write}
|
||||
<div class="flex items-start">
|
||||
<AutoComplete items={usernames} bind:selectedItem={username} />
|
||||
<Button
|
||||
variant="contained"
|
||||
color="blue"
|
||||
size="sm"
|
||||
btnClasses="!ml-4"
|
||||
on:click={addToGroup}
|
||||
>
|
||||
Add member
|
||||
</Button>
|
||||
</div>
|
||||
{/if}
|
||||
<TableCustom>
|
||||
<tr slot="header-row">
|
||||
<th>user</th>
|
||||
<th>admin of group</th>
|
||||
<th />
|
||||
</tr>
|
||||
<tbody slot="body">
|
||||
{#each members as { name, isAdmin }}<tr>
|
||||
<td>{name}</td>
|
||||
<td> {isAdmin ? 'admin' : ''} </td>
|
||||
<td>
|
||||
{#if can_write}
|
||||
<button
|
||||
class="ml-2 text-red-500"
|
||||
on:click={async () => {
|
||||
await GroupService.removeUserToGroup({
|
||||
workspace: $workspaceStore ?? '',
|
||||
name: group?.name ?? '',
|
||||
requestBody: { username: name }
|
||||
})
|
||||
loadGroup()
|
||||
}}>remove</button
|
||||
>
|
||||
{/if}</td
|
||||
>
|
||||
</tr>{/each}
|
||||
</tbody>
|
||||
</TableCustom>
|
||||
</div>
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
@@ -15,6 +15,9 @@
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
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 GroupEditor from './GroupEditor.svelte'
|
||||
|
||||
type PathKind = 'resource' | 'script' | 'variable' | 'flow' | 'schedule' | 'app'
|
||||
let meta: Meta | undefined = undefined
|
||||
@@ -155,8 +158,37 @@
|
||||
path = initialPath
|
||||
}
|
||||
}
|
||||
|
||||
let newGroup: Drawer
|
||||
let newGroupName: string
|
||||
let groupCreated: string | undefined = undefined
|
||||
|
||||
async function addGroup() {
|
||||
await GroupService.createGroup({
|
||||
workspace: $workspaceStore ?? '',
|
||||
requestBody: { name: newGroupName }
|
||||
})
|
||||
groupCreated = newGroupName
|
||||
loadGroups()
|
||||
}
|
||||
</script>
|
||||
|
||||
<Drawer bind:this={newGroup}>
|
||||
<DrawerContent title="New Group" on:close={newGroup.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 group
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{#if groupCreated}
|
||||
<div class="mt-8" />
|
||||
<GroupEditor name={groupCreated} />
|
||||
{/if}
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
|
||||
<div>
|
||||
<div class="flex flex-col sm:grid sm:grid-cols-4 sm:gap-4 pb-0 mb-1">
|
||||
{#if meta != undefined}
|
||||
@@ -202,7 +234,11 @@
|
||||
</label>
|
||||
{:else}
|
||||
<label class="block">
|
||||
<span class="text-gray-700 text-sm">Owner</span>
|
||||
<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>
|
||||
|
||||
@@ -132,8 +132,10 @@
|
||||
</div>
|
||||
{/each}
|
||||
{/if}
|
||||
|
||||
<InsertModuleButton on:new={(e) => insertNewModuleAtIndex(modules?.length ?? 0, e.detail)} />
|
||||
<InsertModuleButton
|
||||
trigger={modules?.length == 0}
|
||||
on:new={(e) => insertNewModuleAtIndex(modules?.length ?? 0, e.detail)}
|
||||
/>
|
||||
</ul>
|
||||
{#if root}
|
||||
<div class="sticky bottom-0 bg-gray-50 flex-none px-4 py-1 pb-2 border-t">
|
||||
|
||||
@@ -13,21 +13,21 @@
|
||||
|
||||
import CenteredPage from '$lib/components/CenteredPage.svelte'
|
||||
import Dropdown from '$lib/components/Dropdown.svelte'
|
||||
import GroupModal from '$lib/components/GroupModal.svelte'
|
||||
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 { Alert, Button } from '$lib/components/common'
|
||||
import { Button, Drawer, DrawerContent } from '$lib/components/common'
|
||||
|
||||
type GroupW = Group & { canWrite: boolean }
|
||||
|
||||
let newGroupName: string = ''
|
||||
let groups: GroupW[] = []
|
||||
let shareModal: ShareModal
|
||||
let groupModal: GroupModal
|
||||
let groupDrawer: Drawer
|
||||
|
||||
async function loadGroups(): Promise<void> {
|
||||
groups = (await GroupService.listGroups({ workspace: $workspaceStore! })).map((x) => {
|
||||
@@ -55,6 +55,8 @@
|
||||
loadGroups()
|
||||
}
|
||||
}
|
||||
|
||||
let editGroupName: string = ''
|
||||
</script>
|
||||
|
||||
<ShareModal
|
||||
@@ -65,7 +67,11 @@
|
||||
}}
|
||||
/>
|
||||
|
||||
<GroupModal bind:this={groupModal} />
|
||||
<Drawer bind:this={groupDrawer}>
|
||||
<DrawerContent title="Group {editGroupName}" on:close={groupDrawer.closeDrawer}>
|
||||
<GroupEditor name={editGroupName} />
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
|
||||
<CenteredPage>
|
||||
<PageHeader
|
||||
@@ -85,7 +91,6 @@
|
||||
</div>
|
||||
</PageHeader>
|
||||
|
||||
|
||||
<div class="relative mb-20">
|
||||
<TableCustom>
|
||||
<tr slot="header-row">
|
||||
@@ -100,7 +105,8 @@
|
||||
<a
|
||||
href="#{name}"
|
||||
on:click={() => {
|
||||
groupModal.openDrawer(name)
|
||||
editGroupName = name
|
||||
groupDrawer.openDrawer()
|
||||
}}
|
||||
>{name}
|
||||
</a>
|
||||
@@ -118,7 +124,8 @@
|
||||
icon: faEdit,
|
||||
disabled: !canWrite,
|
||||
action: () => {
|
||||
groupModal.openDrawer(name)
|
||||
editGroupName = name
|
||||
groupDrawer.openDrawer()
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user