mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-10-27 16:02:43 +00:00
* feat: configurable workspace roles with per-workspace overrides Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix: close role review gaps in pickers, declarative sync and removal Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs: keep resolve_role_name doc on its function Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix: validate overrides on instance role changes and bill admins fully Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix: allow pipeline drafts and keep role checks on one connection Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs: list data pipelines in operator_can_draft Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test: expect a 403 role refusal when an operator resolves jobs Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat: require a valid license for custom workspace roles Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix: freeze workspace roles when the license lapses Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * chore: pin the ee companion for workspace roles Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * refactor: broadcast role override changes from the write path, not a trigger Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * chore: update ee-repo-ref to b5294d38db082bdd104d5358a47695390694c05d This commit updates the EE repository reference after PR #891 was merged in windmill-ee-private. Previous ee-repo-ref: 688e00c5982865b14098f530d54280e2311994c0 New ee-repo-ref: b5294d38db082bdd104d5358a47695390694c05d Automated by sync-ee-ref workflow. * feat: add workspace roles settings UI and role pickers Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix: show the server's reason when a settings save fails Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix: default role pickers to a defined role and confirm resets of unknown capabilities Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix: treat null role capabilities as unset and key role drafts by map Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat: gate the frontend on workspace role capabilities Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix: gate deploys, bulk actions and chat composition per role kind Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix: treat a superadmin as full in deploy permission checks Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix: leave page visibility out of isRestricted, as the server does Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix: let a member discard their own draft without the authoring right Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix: show custom roles as inactive without a valid license Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix: keep the user store import on the agent page Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix: show custom roles as frozen without a valid license Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix: reorder instance roles with a drag handle Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix: let an admin row that kept the operator flag hold full rights everywhere Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix: treat superadmin members as admins and return role names from get user Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix: move the instance role label into the expanded panel Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: windmill-internal-app[bot] <windmill-internal-app[bot]@users.noreply.github.com>
161 lines
5.6 KiB
Svelte
161 lines
5.6 KiB
Svelte
<script lang="ts">
|
|
/**
|
|
* Badge for a workspace locked against direct edits, with the rule names, the route into the dev
|
|
* workspace and the admin bypass in its popover. Renders nothing for operators, who have no edit
|
|
* affordance for it to explain.
|
|
*/
|
|
import { userWorkspaces, workspaceStore } from '$lib/stores'
|
|
import { resource } from 'runed'
|
|
import {
|
|
canUserBypassRuleKindInRulesets,
|
|
fetchProtectionRulesForWorkspace,
|
|
getActiveRulesetsForKindInRulesets,
|
|
isRuleActiveInRulesets,
|
|
protectionRulesState
|
|
} from '$lib/workspaceProtectionRules.svelte'
|
|
import { findCanonicalDevWorkspace } from '$lib/utils/workspaceHierarchy'
|
|
import { devLabelKey, devLabelNoun } from '$lib/utils/devWorkspaceLabel'
|
|
import { switchWorkspace } from '$lib/storeUtils'
|
|
import { Button, ButtonType } from './common'
|
|
import { twMerge } from 'tailwind-merge'
|
|
import Popover from './meltComponents/Popover.svelte'
|
|
import Toggle from './Toggle.svelte'
|
|
import { GitFork, Lock, ShieldOff } from 'lucide-svelte'
|
|
import {
|
|
useOperatingWorkspace,
|
|
useOperatingUser
|
|
} from '$lib/components/operatingWorkspace.svelte'
|
|
import { useRoleCapabilities } from '$lib/operatorWriteRights'
|
|
|
|
const operatingWorkspace = useOperatingWorkspace()
|
|
const operatingUser = useOperatingUser()
|
|
const roleCaps = useRoleCapabilities()
|
|
|
|
// The shared rules state holds the navigation workspace's rules, so a host acting on another
|
|
// workspace (an AI session's fork) has to ask for that one's.
|
|
const foreignRules = resource(
|
|
() => ($operatingWorkspace !== $workspaceStore ? $operatingWorkspace : undefined),
|
|
(workspace) => (workspace ? fetchProtectionRulesForWorkspace(workspace) : Promise.resolve([]))
|
|
)
|
|
let rulesets = $derived(
|
|
$operatingWorkspace === $workspaceStore
|
|
? (protectionRulesState.rulesets ?? [])
|
|
: (foreignRules.current ?? [])
|
|
)
|
|
|
|
let activeDeployRulesets = $derived(
|
|
getActiveRulesetsForKindInRulesets(rulesets, 'DisableDirectDeployment')
|
|
)
|
|
let canBypass = $derived(
|
|
canUserBypassRuleKindInRulesets(rulesets, 'DisableDirectDeployment', operatingUser.current)
|
|
)
|
|
let canonicalDev = $derived(findCanonicalDevWorkspace($operatingWorkspace, $userWorkspaces))
|
|
// Forking may itself be blocked by DisableWorkspaceForking, so only suggest it
|
|
// when the user can actually fork this workspace.
|
|
let canFork = $derived(
|
|
!isRuleActiveInRulesets(rulesets, 'DisableWorkspaceForking') ||
|
|
canUserBypassRuleKindInRulesets(rulesets, 'DisableWorkspaceForking', operatingUser.current)
|
|
)
|
|
let editAdvice = $derived(
|
|
canFork
|
|
? 'You will need to either fork the workspace, or make your changes locally and submit a PR to an authorized user.'
|
|
: 'You will need to make your changes locally and submit a PR to an authorized user.'
|
|
)
|
|
let overrideChecked = $state(false)
|
|
// The toggle is only offered to a user who can bypass, but the answer can change under a
|
|
// workspace switch, so the checked flag alone never grants the edit.
|
|
let bypassActive = $derived(canBypass && overrideChecked)
|
|
let canEdit = $derived(
|
|
!isRuleActiveInRulesets(rulesets, 'DisableDirectDeployment') || bypassActive
|
|
)
|
|
|
|
let {
|
|
onUpdateCanEditStatus = (value) => {}
|
|
}: {
|
|
onUpdateCanEditStatus?: (value: boolean) => void
|
|
} = $props()
|
|
|
|
$effect(() => {
|
|
onUpdateCanEditStatus(canEdit)
|
|
})
|
|
|
|
let badgeLabel = $derived(
|
|
bypassActive
|
|
? 'Protection bypassed'
|
|
: canonicalDev
|
|
? `Edits in ${devLabelKey(canonicalDev.dev_workspace_label)}`
|
|
: 'Edits restricted'
|
|
)
|
|
|
|
// The Popover renders its trigger as a <button>, so this wears the design system's subtle
|
|
// button rather than containing one — a <Button> here would nest a button inside a button.
|
|
const triggerClass = twMerge(
|
|
'inline-flex items-center gap-1 rounded-md whitespace-nowrap transition-all',
|
|
ButtonType.VariantStyles.subtle,
|
|
ButtonType.UnifiedSizingClasses.sm,
|
|
ButtonType.UnifiedHeightClasses.sm,
|
|
ButtonType.UnifiedFontSizes.sm,
|
|
'text-xs'
|
|
)
|
|
</script>
|
|
|
|
{#if ($roleCaps.code || $roleCaps.flows !== 'none' || $roleCaps.apps !== 'none') && activeDeployRulesets.length > 0}
|
|
<div class="my-2">
|
|
<Popover
|
|
placement="bottom-start"
|
|
class={triggerClass}
|
|
triggerAttrs={{ 'aria-label': badgeLabel }}
|
|
>
|
|
{#snippet trigger()}
|
|
{#if bypassActive}
|
|
<ShieldOff size={ButtonType.UnifiedIconSizes.sm} class="shrink-0" />
|
|
{:else if canonicalDev}
|
|
<GitFork size={ButtonType.UnifiedIconSizes.sm} class="shrink-0" />
|
|
{:else}
|
|
<Lock size={ButtonType.UnifiedIconSizes.sm} class="shrink-0" />
|
|
{/if}
|
|
{badgeLabel}
|
|
{/snippet}
|
|
{#snippet content()}
|
|
<div class="flex flex-col gap-3 p-4 text-xs max-w-sm">
|
|
{#if canonicalDev}
|
|
<p class="text-primary">
|
|
Edits to this workspace are made in its {devLabelNoun(
|
|
canonicalDev.dev_workspace_label
|
|
)}
|
|
<b>{canonicalDev.name}</b> ({canonicalDev.id}) and promoted here.
|
|
</p>
|
|
<div>
|
|
<Button
|
|
btnClasses="w-auto"
|
|
size="xs"
|
|
variant="accent"
|
|
startIcon={{ icon: GitFork }}
|
|
onclick={() => {
|
|
if (canonicalDev) switchWorkspace(canonicalDev.id)
|
|
}}
|
|
>
|
|
Go to {devLabelNoun(canonicalDev.dev_workspace_label)}
|
|
</Button>
|
|
</div>
|
|
{:else}
|
|
<p class="text-primary">
|
|
The rule{activeDeployRulesets.length > 1 ? 's' : ''}
|
|
<b>{activeDeployRulesets.map((r) => r.name).join(', ')}</b>
|
|
restrict{activeDeployRulesets.length > 1 ? '' : 's'} direct edits to this workspace.
|
|
{editAdvice}
|
|
</p>
|
|
{/if}
|
|
{#if canBypass}
|
|
<Toggle
|
|
size="xs"
|
|
bind:checked={overrideChecked}
|
|
options={{ right: 'Bypass restriction' }}
|
|
/>
|
|
{/if}
|
|
</div>
|
|
{/snippet}
|
|
</Popover>
|
|
</div>
|
|
{/if}
|