mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-09-08 16:03:27 +00:00
* feat: restrict folder creation, sharing and group creation in demo workspace Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix: guard update_folder sharing bypass and gate folder editor/share ACL controls in demo Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix: guard remove_owner grant path and gate alternate folder-create controls; keep pure-revoke controls enabled Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
713 lines
21 KiB
Svelte
713 lines
21 KiB
Svelte
<script lang="ts">
|
|
import { userStore, workspaceStore } from '$lib/stores'
|
|
import {
|
|
type Folder,
|
|
type FolderDefaultPermissionedAs,
|
|
FolderService,
|
|
UserService,
|
|
GranularAclService,
|
|
GroupService
|
|
} from '$lib/gen'
|
|
import TableCustom from './TableCustom.svelte'
|
|
import { DEMO_RESTRICTION_HINT, isDemoWorkspaceRestricted } from '$lib/cloud'
|
|
import { Alert, Button, Drawer, DrawerContent } from './common'
|
|
import Skeleton from './common/skeleton/Skeleton.svelte'
|
|
import GroupEditor from './GroupEditor.svelte'
|
|
import ToggleButtonGroup from './common/toggleButton-v2/ToggleButtonGroup.svelte'
|
|
import ToggleButton from './common/toggleButton-v2/ToggleButton.svelte'
|
|
import { ArrowDown, ArrowUp, Eye, Plus, Trash } from 'lucide-svelte'
|
|
import Label from './Label.svelte'
|
|
import { sendUserToast } from '$lib/toast'
|
|
import { createEventDispatcher, untrack } from 'svelte'
|
|
import Select from './select/Select.svelte'
|
|
import { safeSelectItems } from './select/utils.svelte'
|
|
import TextInput from './text_input/TextInput.svelte'
|
|
import PermissionHistory from './PermissionHistory.svelte'
|
|
import { Minimatch } from 'minimatch'
|
|
import Tooltip from './Tooltip.svelte'
|
|
import CollapseLink from './CollapseLink.svelte'
|
|
import LabelsInput from './LabelsInput.svelte'
|
|
import Badge from './common/badge/Badge.svelte'
|
|
|
|
interface Props {
|
|
name: string
|
|
}
|
|
|
|
let { name }: Props = $props()
|
|
let can_write = $state(false)
|
|
|
|
type Role = 'viewer' | 'writer' | 'admin'
|
|
let folder: Folder | undefined
|
|
let perms: { owner_name: string; role: Role }[] | undefined = $state(undefined)
|
|
let usernames: string[] = $state([])
|
|
let groups: string[] = $state([])
|
|
let ownerItem: string = $state('')
|
|
|
|
let newGroup: Drawer | undefined = $state(undefined)
|
|
let viewGroup: Drawer | undefined = $state(undefined)
|
|
|
|
async function loadUsernames(): Promise<void> {
|
|
usernames = await UserService.listUsernames({ workspace: $workspaceStore! })
|
|
}
|
|
|
|
async function loadGroups(): Promise<void> {
|
|
groups = await GroupService.listGroupNames({ workspace: $workspaceStore! })
|
|
}
|
|
|
|
async function load() {
|
|
loadUsernames()
|
|
loadGroups()
|
|
await loadFolder()
|
|
}
|
|
|
|
async function addToFolder() {
|
|
await GranularAclService.addGranularAcls({
|
|
workspace: $workspaceStore ?? '',
|
|
path: name,
|
|
kind: 'folder',
|
|
requestBody: {
|
|
owner: (ownerKind == 'user' ? 'u/' : 'g/') + ownerItem
|
|
}
|
|
})
|
|
ownerItem = ''
|
|
loadFolder()
|
|
}
|
|
|
|
let folderNotFound: boolean | undefined = $state(undefined)
|
|
|
|
async function loadFolder(): Promise<void> {
|
|
try {
|
|
folder = await FolderService.getFolder({ workspace: $workspaceStore!, name })
|
|
summary = folder.summary ?? ''
|
|
labels = [...(folder.labels ?? [])]
|
|
defaultPermissionedAs = (folder.default_permissioned_as ?? []).map((r) => ({ ...r }))
|
|
can_write =
|
|
$userStore != undefined &&
|
|
(folder?.owners.includes('u/' + $userStore.username) ||
|
|
($userStore.is_admin ?? false) ||
|
|
($userStore.is_super_admin ?? false) ||
|
|
$userStore.pgroups.findIndex((x) => folder?.owners.includes(x)) != -1)
|
|
|
|
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)
|
|
}
|
|
})
|
|
reloadHistory++
|
|
} catch (e) {
|
|
folderNotFound = true
|
|
}
|
|
}
|
|
|
|
// --- default_permissioned_as rules editor ---
|
|
let defaultPermissionedAs: FolderDefaultPermissionedAs = $state([])
|
|
|
|
const restricted = $derived(
|
|
isDemoWorkspaceRestricted($workspaceStore, $userStore?.is_admin, $userStore?.is_super_admin)
|
|
)
|
|
|
|
const canEditDefaults = $derived(
|
|
can_write &&
|
|
!restricted &&
|
|
($userStore?.is_admin ||
|
|
$userStore?.is_super_admin ||
|
|
($userStore?.groups ?? []).includes('wm_deployers'))
|
|
)
|
|
|
|
function isValidGlob(glob: string): boolean {
|
|
if (!glob) return false
|
|
try {
|
|
new Minimatch(glob)
|
|
return true
|
|
} catch {
|
|
return false
|
|
}
|
|
}
|
|
|
|
function isValidPermissionedAs(value: string): boolean {
|
|
return /^[ug]\/.+/.test(value) || value.includes('@')
|
|
}
|
|
|
|
// Split a permissioned_as value like "u/alice" or "g/prod" into its kind and name.
|
|
function ruleKind(value: string): 'user' | 'group' {
|
|
return value.startsWith('g/') ? 'group' : 'user'
|
|
}
|
|
function ruleName(value: string): string {
|
|
if (value.startsWith('u/') || value.startsWith('g/')) return value.slice(2)
|
|
return value
|
|
}
|
|
function setRulePermissionedAs(idx: number, kind: 'user' | 'group', name: string) {
|
|
const prefix = kind === 'user' ? 'u/' : 'g/'
|
|
defaultPermissionedAs[idx].permissioned_as = prefix + name
|
|
}
|
|
|
|
const defaultRulesInvalid = $derived(
|
|
defaultPermissionedAs.some(
|
|
(r) => !isValidGlob(r.path_glob) || !isValidPermissionedAs(r.permissioned_as)
|
|
)
|
|
)
|
|
|
|
function addDefaultRule() {
|
|
defaultPermissionedAs = [...defaultPermissionedAs, { path_glob: '**', permissioned_as: '' }]
|
|
}
|
|
|
|
function removeDefaultRule(idx: number) {
|
|
defaultPermissionedAs = defaultPermissionedAs.filter((_, i) => i !== idx)
|
|
}
|
|
|
|
function moveDefaultRule(idx: number, delta: -1 | 1) {
|
|
const next = [...defaultPermissionedAs]
|
|
const target = idx + delta
|
|
if (target < 0 || target >= next.length) return
|
|
;[next[idx], next[target]] = [next[target], next[idx]]
|
|
defaultPermissionedAs = next
|
|
}
|
|
|
|
async function saveDefaultRules() {
|
|
if (defaultRulesInvalid) {
|
|
sendUserToast('Some rules have invalid globs or permissioned_as values', true)
|
|
return
|
|
}
|
|
try {
|
|
await FolderService.updateFolder({
|
|
workspace: $workspaceStore ?? '',
|
|
name,
|
|
requestBody: { default_permissioned_as: defaultPermissionedAs }
|
|
})
|
|
sendUserToast('Default permissioned_as rules updated')
|
|
dispatch('update')
|
|
loadFolder()
|
|
} catch (e) {
|
|
sendUserToast(e.body ?? String(e), true)
|
|
}
|
|
}
|
|
|
|
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 'admin'
|
|
} else if (writer) {
|
|
return 'writer'
|
|
} else {
|
|
return 'viewer'
|
|
}
|
|
}
|
|
|
|
let ownerKind: 'user' | 'group' = $state('user')
|
|
let groupCreated: string | undefined = $state(undefined)
|
|
let newGroupName: string = $state('')
|
|
let summary: string = $state('')
|
|
let labels: string[] | undefined = $state(undefined)
|
|
|
|
async function saveLabels() {
|
|
try {
|
|
await FolderService.updateFolder({
|
|
workspace: $workspaceStore ?? '',
|
|
name,
|
|
requestBody: { labels: labels ?? [] }
|
|
})
|
|
sendUserToast('Folder labels updated')
|
|
dispatch('update')
|
|
} catch (e) {
|
|
sendUserToast(e.body ?? String(e), true)
|
|
loadFolder()
|
|
}
|
|
}
|
|
|
|
async function addGroup() {
|
|
await GroupService.createGroup({
|
|
workspace: $workspaceStore ?? '',
|
|
requestBody: { name: newGroupName }
|
|
})
|
|
groupCreated = newGroupName
|
|
$userStore?.folders?.push(newGroupName)
|
|
loadGroups()
|
|
ownerItem = newGroupName
|
|
}
|
|
|
|
const dispatch = createEventDispatcher()
|
|
|
|
async function updateFolder() {
|
|
await FolderService.updateFolder({
|
|
workspace: $workspaceStore ?? '',
|
|
name,
|
|
requestBody: { summary }
|
|
})
|
|
sendUserToast('Folder summary updated')
|
|
dispatch('update')
|
|
loadFolder()
|
|
}
|
|
$effect.pre(() => {
|
|
if ($workspaceStore && $userStore) {
|
|
untrack(() => {
|
|
load()
|
|
})
|
|
}
|
|
})
|
|
|
|
let reloadHistory = $state(0)
|
|
</script>
|
|
|
|
<Drawer bind:this={newGroup}>
|
|
<DrawerContent
|
|
title="New Group"
|
|
on:close={() => {
|
|
newGroup?.closeDrawer()
|
|
groupCreated = undefined
|
|
}}
|
|
>
|
|
{#if !groupCreated}
|
|
<div class="flex flex-row">
|
|
<input class="mr-2" placeholder="New group name" bind:value={newGroupName} />
|
|
<Button size="md" startIcon={{ icon: Plus }} disabled={!newGroupName} on:click={addGroup}>
|
|
New group
|
|
</Button>
|
|
</div>
|
|
{:else}
|
|
<GroupEditor name={groupCreated} />
|
|
{/if}
|
|
</DrawerContent>
|
|
</Drawer>
|
|
|
|
<Drawer bind:this={viewGroup}>
|
|
<DrawerContent title="Group {ownerItem}" on:close={viewGroup.closeDrawer}>
|
|
<GroupEditor name={ownerItem} />
|
|
</DrawerContent>
|
|
</Drawer>
|
|
|
|
<div class="flex flex-col gap-6">
|
|
<Label label="Summary">
|
|
<div class="flex flex-row gap-2">
|
|
<TextInput
|
|
inputProps={{ placeholder: 'Short summary to be displayed when listed' }}
|
|
bind:value={summary}
|
|
size="md"
|
|
/>
|
|
<Button variant="accent" unifiedSize="md" on:click={updateFolder} disabled={!can_write}
|
|
>Save</Button
|
|
>
|
|
</div>
|
|
</Label>
|
|
|
|
<Label label="Labels">
|
|
<div class="flex flex-col gap-1">
|
|
<div class="text-xs text-tertiary">
|
|
Scripts and flows inside this folder inherit these labels, and runs of items in this folder
|
|
are labeled with them.
|
|
</div>
|
|
{#if can_write}
|
|
<LabelsInput bind:labels onchange={saveLabels} />
|
|
{:else}
|
|
<div class="inline-flex items-center gap-1 h-5">
|
|
{#each labels ?? [] as label (label)}
|
|
<Badge color="blue" small>{label}</Badge>
|
|
{:else}
|
|
<span class="text-xs text-tertiary">No labels</span>
|
|
{/each}
|
|
</div>
|
|
{/if}
|
|
</div>
|
|
</Label>
|
|
|
|
<Label label={`Permissions (${perms?.length ?? 0})`}>
|
|
<div class="flex flex-col gap-2">
|
|
{#if can_write && restricted}
|
|
<Alert type="info" title="Sharing disabled">{DEMO_RESTRICTION_HINT}</Alert>
|
|
{:else if can_write}
|
|
<Alert type="info" title="New permissions may take up to 60s to apply">
|
|
Due to permissions cache invalidation
|
|
</Alert>
|
|
<div class="flex items-center gap-1">
|
|
<div>
|
|
<ToggleButtonGroup bind:selected={ownerKind} on:selected={() => (ownerItem = '')}>
|
|
{#snippet children({ item })}
|
|
<ToggleButton value="user" label="User" {item} />
|
|
<ToggleButton value="group" label="Group" {item} />
|
|
{/snippet}
|
|
</ToggleButtonGroup>
|
|
</div>
|
|
|
|
{#key ownerKind}
|
|
{@const items =
|
|
ownerKind === 'user'
|
|
? usernames.filter((x) => !perms?.map((y) => y.owner_name).includes('u/' + x))
|
|
: groups.filter((x) => !perms?.map((y) => y.owner_name).includes('g/' + x))}
|
|
<Select items={safeSelectItems(items)} bind:value={ownerItem} class="grow min-w-24" />
|
|
{#if ownerKind == 'group'}
|
|
<Button
|
|
title="View Group"
|
|
variant="default"
|
|
unifiedSize="md"
|
|
disabled={!ownerItem || ownerItem == ''}
|
|
on:click={viewGroup.openDrawer}
|
|
startIcon={{ icon: Eye }}
|
|
iconOnly
|
|
/>
|
|
<Button
|
|
title="New Group"
|
|
variant="default"
|
|
on:click={newGroup.openDrawer}
|
|
unifiedSize="md"
|
|
startIcon={{ icon: Plus }}
|
|
iconOnly
|
|
/>
|
|
{/if}
|
|
{/key}
|
|
<Button
|
|
disabled={ownerItem == ''}
|
|
variant="accent"
|
|
unifiedSize="md"
|
|
on:click={addToFolder}
|
|
>
|
|
Grant
|
|
</Button>
|
|
</div>
|
|
{/if}
|
|
|
|
{#if folderNotFound}
|
|
<Alert type="warning" title="Folder not found" size="xs">
|
|
The folder "{name}" does not exist in the workspace. You can create it by clicking the
|
|
button below. An item can seemingly be in a folder given its path without the folder
|
|
existing. A windmill folder has settable permissions that its children inherit. If an item
|
|
is within a non-existing folders, only admins will see it.
|
|
</Alert>
|
|
{#if !restricted}
|
|
<Button
|
|
variant="default"
|
|
wrapperClasses="w-min"
|
|
startIcon={{ icon: Plus }}
|
|
size="xs"
|
|
on:click={() => {
|
|
FolderService.createFolder({
|
|
workspace: $workspaceStore ?? '',
|
|
requestBody: { name }
|
|
}).then(() => {
|
|
loadFolder()
|
|
})
|
|
}}
|
|
>
|
|
Create folder "{name}"
|
|
</Button>
|
|
{/if}
|
|
{/if}
|
|
{#if perms}
|
|
<TableCustom>
|
|
{#snippet headerRow()}
|
|
<tr>
|
|
<th>user/group</th>
|
|
<th></th>
|
|
<th></th>
|
|
</tr>
|
|
{/snippet}
|
|
{#snippet body()}
|
|
<tbody>
|
|
{#each perms ?? [] as { owner_name, role }}<tr>
|
|
<td>{owner_name}</td>
|
|
<td>
|
|
{#if can_write && !restricted}
|
|
<div>
|
|
<ToggleButtonGroup
|
|
disabled={owner_name == 'u/' + $userStore?.username &&
|
|
!($userStore?.is_admin || $userStore?.is_super_admin)}
|
|
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 == 'admin') {
|
|
await FolderService.addOwnerToFolder({
|
|
workspace: $workspaceStore ?? '',
|
|
name,
|
|
requestBody: {
|
|
owner: owner_name
|
|
}
|
|
})
|
|
} else if (role == 'writer') {
|
|
await FolderService.removeOwnerToFolder({
|
|
workspace: $workspaceStore ?? '',
|
|
name,
|
|
requestBody: {
|
|
owner: owner_name,
|
|
write: true
|
|
}
|
|
})
|
|
} else if (role == 'viewer') {
|
|
await FolderService.removeOwnerToFolder({
|
|
workspace: $workspaceStore ?? '',
|
|
name,
|
|
requestBody: {
|
|
owner: owner_name,
|
|
write: false
|
|
}
|
|
})
|
|
}
|
|
loadFolder()
|
|
}}
|
|
>
|
|
{#snippet children({ item })}
|
|
<ToggleButton
|
|
value="viewer"
|
|
label="Viewer"
|
|
tooltip="A viewer of a folder has read-only access to all the elements (scripts/flows/apps/schedules/resources/variables) inside the folder"
|
|
{item}
|
|
size="sm"
|
|
/>
|
|
|
|
<ToggleButton
|
|
value="writer"
|
|
label="Writer"
|
|
tooltip="A writer of a folder has read AND write access to all the elements (scripts/flows/apps/schedules/resources/variables) inside the folder"
|
|
{item}
|
|
size="sm"
|
|
/>
|
|
|
|
<ToggleButton
|
|
value="admin"
|
|
label="Admin"
|
|
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"
|
|
{item}
|
|
size="sm"
|
|
/>
|
|
{/snippet}
|
|
</ToggleButtonGroup>
|
|
</div>
|
|
{:else}
|
|
{role}
|
|
{/if}</td
|
|
>
|
|
<td class="flex items-center justify-end">
|
|
{#if (can_write && owner_name != 'u/' + $userStore?.username) || $userStore?.is_admin}
|
|
<Button
|
|
variant="subtle"
|
|
destructive
|
|
unifiedSize="md"
|
|
startIcon={{ icon: Trash }}
|
|
iconOnly
|
|
onclick={async () => {
|
|
await Promise.all([
|
|
FolderService.removeOwnerToFolder({
|
|
workspace: $workspaceStore ?? '',
|
|
name,
|
|
requestBody: { owner: owner_name }
|
|
}),
|
|
GranularAclService.removeGranularAcls({
|
|
workspace: $workspaceStore ?? '',
|
|
path: name,
|
|
kind: 'folder',
|
|
requestBody: {
|
|
owner: owner_name
|
|
}
|
|
})
|
|
])
|
|
|
|
loadFolder()
|
|
}}
|
|
/>
|
|
{:else if can_write && owner_name == 'u/' + $userStore?.username}
|
|
<span class="text-primary text-xs">cannot remove yourself</span>
|
|
{/if}</td
|
|
>
|
|
</tr>{/each}
|
|
</tbody>
|
|
{/snippet}
|
|
</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="accent" size="sm" btnClasses="!ml-4" on:click={addToManagingFolder}>
|
|
Add folder managing this folder
|
|
</Button>
|
|
</div>
|
|
{/if} -->
|
|
<!-- {#if managing_folders.length == 0}
|
|
<p class="text-primary text-sm">No folder is managing this folder</p>
|
|
{:else}
|
|
<TableCustom>
|
|
<tr slot="headerRow">
|
|
<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 if folderNotFound === undefined}
|
|
<div class="flex flex-col">
|
|
{#each new Array(6) as _}
|
|
<Skeleton layout={[[2], 0.7]} />
|
|
{/each}
|
|
</div>
|
|
{/if}
|
|
</div>
|
|
</Label>
|
|
|
|
{#if canEditDefaults}
|
|
<CollapseLink text="Default permissioned as (advanced, prod only)">
|
|
<div class="flex flex-col gap-2">
|
|
<Alert type="info" title="Advanced — for prod workspaces (least privilege)" size="xs">
|
|
This setting is mostly relevant on <strong>production workspaces</strong> where you want
|
|
new items under this folder to run under a least-privilege service account rather than the
|
|
deploying admin's identity. When an admin or <code>wm_deployers</code> member creates a
|
|
trigger, schedule, app, script, or flow under this folder, the first matching rule
|
|
determines the default <code>permissioned_as</code>. Globs are relative to the folder root
|
|
(e.g. <code>jobs/**</code> matches <code>f/{name}/jobs/run_a</code>). Existing items are
|
|
never rewritten.
|
|
</Alert>
|
|
|
|
{#if defaultPermissionedAs.length > 0}
|
|
<TableCustom>
|
|
{#snippet headerRow()}
|
|
<tr>
|
|
<th>path_glob <Tooltip>Glob relative to <code>f/{name}/</code></Tooltip></th>
|
|
<th>permissioned as</th>
|
|
<th class="w-24"></th>
|
|
</tr>
|
|
{/snippet}
|
|
{#snippet body()}
|
|
<tbody>
|
|
{#each defaultPermissionedAs as rule, idx (idx)}
|
|
{@const kind = ruleKind(rule.permissioned_as)}
|
|
{@const itemsForKind = kind === 'user' ? usernames : groups}
|
|
<tr>
|
|
<td>
|
|
<TextInput
|
|
bind:value={rule.path_glob}
|
|
inputProps={{ placeholder: '**' }}
|
|
error={!isValidGlob(rule.path_glob)}
|
|
/>
|
|
</td>
|
|
<td>
|
|
<div class="flex items-center gap-1">
|
|
<ToggleButtonGroup
|
|
selected={kind}
|
|
on:selected={(e) => setRulePermissionedAs(idx, e.detail, '')}
|
|
>
|
|
{#snippet children({ item })}
|
|
<ToggleButton value="user" label="User" {item} size="sm" />
|
|
<ToggleButton value="group" label="Group" {item} size="sm" />
|
|
{/snippet}
|
|
</ToggleButtonGroup>
|
|
<Select
|
|
items={safeSelectItems(itemsForKind)}
|
|
bind:value={
|
|
() => ruleName(rule.permissioned_as),
|
|
(v) => setRulePermissionedAs(idx, kind, v ?? '')
|
|
}
|
|
class="grow min-w-32"
|
|
/>
|
|
</div>
|
|
</td>
|
|
<td>
|
|
<div class="flex items-center gap-1 justify-end">
|
|
<Button
|
|
variant="subtle"
|
|
unifiedSize="sm"
|
|
startIcon={{ icon: ArrowUp }}
|
|
iconOnly
|
|
disabled={idx === 0}
|
|
on:click={() => moveDefaultRule(idx, -1)}
|
|
/>
|
|
<Button
|
|
variant="subtle"
|
|
unifiedSize="sm"
|
|
startIcon={{ icon: ArrowDown }}
|
|
iconOnly
|
|
disabled={idx === defaultPermissionedAs.length - 1}
|
|
on:click={() => moveDefaultRule(idx, 1)}
|
|
/>
|
|
<Button
|
|
variant="subtle"
|
|
destructive
|
|
unifiedSize="sm"
|
|
startIcon={{ icon: Trash }}
|
|
iconOnly
|
|
on:click={() => removeDefaultRule(idx)}
|
|
/>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
{/each}
|
|
</tbody>
|
|
{/snippet}
|
|
</TableCustom>
|
|
{:else}
|
|
<div class="text-xs text-tertiary">No rules defined.</div>
|
|
{/if}
|
|
|
|
<div class="flex items-center gap-2">
|
|
<Button
|
|
variant="default"
|
|
unifiedSize="sm"
|
|
startIcon={{ icon: Plus }}
|
|
on:click={addDefaultRule}
|
|
>
|
|
Add rule
|
|
</Button>
|
|
<Button
|
|
variant="accent"
|
|
unifiedSize="sm"
|
|
disabled={defaultRulesInvalid}
|
|
on:click={saveDefaultRules}
|
|
>
|
|
Save rules
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</CollapseLink>
|
|
{/if}
|
|
|
|
{#if reloadHistory > 0}
|
|
{#key reloadHistory}
|
|
<PermissionHistory
|
|
{name}
|
|
fetchHistory={async (workspace, folderName, page, perPage) => {
|
|
return await FolderService.getFolderPermissionHistory({
|
|
workspace,
|
|
name: folderName,
|
|
page,
|
|
perPage
|
|
})
|
|
}}
|
|
/>
|
|
{/key}
|
|
{/if}
|
|
</div>
|