Files
windmill/frontend/src/lib/components/ExploreAssetButton.svelte
T
8dbdfa1949 feat: workspace roles settings UI and capability-based gating (#11824)
* 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>
2026-10-21 16:31:04 +02:00

155 lines
5.1 KiB
Svelte

<script module lang="ts">
const OBJECT_STORAGE_RESOURCE_TYPES = [
's3',
'azure_blob',
's3_aws_oidc',
'azure_workload_identity',
'gcloud_storage'
]
export function isObjectStorageResourceType(resourceType: string | undefined): boolean {
return resourceType !== undefined && OBJECT_STORAGE_RESOURCE_TYPES.includes(resourceType)
}
export function assetCanBeExplored(
asset: Asset,
_resourceMetadata?: { resource_type?: string }
): boolean {
return (
asset.kind === 'ducklake' ||
asset.kind === 'datatable' ||
asset.kind === 's3object' ||
asset.kind === 'volume' ||
(asset.kind === 'resource' &&
(isDbType(_resourceMetadata?.resource_type) ||
isObjectStorageResourceType(_resourceMetadata?.resource_type)))
)
}
</script>
<script lang="ts">
import { isDbType } from '$lib/components/dbTypes'
import { formatAsset, type Asset } from '$lib/components/assets/lib'
import { Button, ButtonType } from '$lib/components/common'
import S3FilePicker from '$lib/components/S3FilePicker.svelte'
import { VolumeService } from '$lib/gen'
import { globalDbManagerDrawer, globalS3FilePickerExplorer } from '$lib/stores'
import { isS3Uri } from '$lib/utils'
import { Database, File, HardDriveIcon } from 'lucide-svelte'
import DucklakeIcon from './icons/DucklakeIcon.svelte'
import { useOperatingWorkspace } from '$lib/components/operatingWorkspace.svelte'
import { useRoleCapabilities } from '$lib/operatorWriteRights'
const operatingWorkspace = useOperatingWorkspace()
const roleCaps = useRoleCapabilities()
const {
asset,
_resourceMetadata,
s3FilePicker,
onClick,
class: className = '',
noText = false,
buttonVariant = 'default',
btnClasses = '',
disabled = false,
workspace = undefined
}: {
asset: Asset
_resourceMetadata?: { resource_type?: string }
s3FilePicker?: S3FilePicker
onClick?: () => void
class?: string
noText?: boolean
buttonVariant?: ButtonType.Variant
btnClasses?: string
disabled?: boolean
/** Workspace the explored asset lives in; defaults to the nav workspace. */
workspace?: string
} = $props()
let dbManagerDrawer = $derived(globalDbManagerDrawer.val)
let ws = $derived(workspace ?? $operatingWorkspace)
const assetUri = $derived(formatAsset(asset))
// Contexts with a select/upload flow pass their own picker; everything else
// (e.g. the resources list) falls back to the global read-only explorer.
let effectiveS3FilePicker = $derived(s3FilePicker ?? globalS3FilePickerExplorer.val)
let isStorageResource = $derived(
asset.kind === 'resource' && isObjectStorageResourceType(_resourceMetadata?.resource_type)
)
</script>
<Button
disabled={!$roleCaps.code || disabled}
unifiedSize={'md'}
variant={buttonVariant}
wrapperClasses={className}
iconOnly={noText}
{btnClasses}
on:click={async () => {
if (asset.kind === 'resource' && isDbType(_resourceMetadata?.resource_type)) {
let [resourcePath, specificTable] = asset.path.split('?table=')
dbManagerDrawer?.openDrawer(
{
type: 'database',
resourceType: _resourceMetadata.resource_type,
resourcePath,
specificTable
},
ws
)
} else if (isStorageResource) {
effectiveS3FilePicker?.open(undefined, { s3ResourcePath: asset.path, workspace: ws })
} else if (asset.kind === 's3object' && isS3Uri(assetUri)) {
effectiveS3FilePicker?.open(assetUri, { workspace: ws })
} else if (asset.kind === 'volume') {
const storage = (await VolumeService.getVolumeStorage({ workspace: ws! })) ?? undefined
effectiveS3FilePicker?.open(
{ s3: `volumes/${ws}/${asset.path}/`, storage },
{ workspace: ws }
)
} else if (asset.kind === 'ducklake') {
let ducklake = asset.path.split('/')[0]
let specificTableSplit = asset.path.split('/')[1]?.split('.') as string[] | undefined
let [specificSchema, specificTable] =
specificTableSplit?.length === 2
? [specificTableSplit[0], specificTableSplit[1]]
: [undefined, specificTableSplit?.[0]]
dbManagerDrawer?.openDrawer({ type: 'ducklake', ducklake, specificSchema, specificTable }, ws)
} else if (asset.kind === 'datatable') {
let datatable = asset.path.split('/')[0]
let specificTableSplit = asset.path.split('/')[1]?.split('.') as string[] | undefined
let [specificSchema, specificTable] =
specificTableSplit?.length === 2
? [specificTableSplit[0], specificTableSplit[1]]
: [undefined, specificTableSplit?.[0]]
dbManagerDrawer?.openDrawer(
{
type: 'database',
resourceType: 'postgresql',
resourcePath: `datatable://${datatable}`,
specificTable,
specificSchema
},
ws
)
}
onClick?.()
}}
endIcon={asset.kind === 's3object' || isStorageResource
? { icon: File }
: asset.kind === 'resource' || asset.kind === 'datatable'
? { icon: Database }
: asset.kind === 'ducklake'
? { icon: DucklakeIcon }
: asset.kind === 'volume'
? { icon: HardDriveIcon }
: undefined}
>
{#if asset.kind === 's3object' || asset.kind === 'volume' || isStorageResource}
<span class:hidden={noText}>Explore</span>
{:else if asset.kind === 'resource' || asset.kind === 'ducklake' || asset.kind === 'datatable'}
<span class:hidden={noText}>Manage</span>
{/if}
</Button>