mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-08-24 16:01:42 +00:00
b4b0c6a93e
* feat: add dev workspaces paired with a lockable prod workspace Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix: gate dev-workspace prod-lock on admin and prevent attach cycles Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * feat: redirect locked-prod edits into the dev workspace Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix: make dev-workspace settings tab available on CE (was EE-gated) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * feat: lock prod against forking too and funnel edits to the dev workspace Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * feat: open dev item page on edit and tailor dev-workspace lock messages Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * feat: prevent nested dev workspaces and hide dev option when one exists Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * chore: drop the redundant already-has-dev hint on the fork form Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * feat: badge dev workspaces and sort them ahead of forks in the tree/switcher Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * feat: label dev workspaces as 'Dev workspace of X' instead of 'Fork of X' Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * feat: label edit as 'Edit in <dev>', cover editor headers, auto-expand dev in tree Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * feat: split prod lock into separate block-deploy and prevent-forking toggles Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * feat: make resources/variables workspace-specific from compare page Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * feat: steer AI-chat sessions to the dev workspace Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * feat: refine session fork options and lock guidance for dev/prod Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix: session picker reads prod's real rules, default to current ws Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * feat: copy members into forks and clarify dev-workspace root labeling Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * style: place the workspace id field under the fork name Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix: address dev-workspace review findings and harden fork detection Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * chore: regenerate sqlx offline cache Restores entries dropped during the origin/main merge and adds the dev-workspace queries (is_dev_workspace, ws_specific, has_parent). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix: address second-round dev-workspace review findings Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix: address Pi and Codex review findings on dev-workspace endpoints Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix: gate locked-dev git-branch fork on admin and validate ws_specific path Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix: clear prod dev-lock when deleting an attached dev workspace Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * refactor: consolidate dev-workspace migration and scope all-group join to attach Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix: restore dev-workspace CHECK into consolidated migration and scope all-group join Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * refactor: drop copy_members from the dev-workspace attach path Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix: dev-workspace lifecycle/auth fixes from Codex review round Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * feat: explicit create-in-other for workspace-specific items Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix: make create-in-other strictly create-only (never overwrite target) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix: return 403 (not 401) for dev-workspace permission denials Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * feat: allow attaching a same-family fork as a dev workspace Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * style: emphasize the go-to-dev action in the no-direct-deploy alert Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix: seed a resource's linked variables when creating it in the other workspace Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix: judge workspace deploy/fork locks against the user's identity in that workspace Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * style: clarify create-in help text in workspace-specific panel Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix: admin-gate dev-workspace creation and harden lock/seed edges Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix: preserve a staged fork's source on picker create-mode re-entry Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix: clear dev flag on archive and check dev existence server-side Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix: make create-in-other atomically create-only via direct create Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix: create-only resource insert, ws-specific list scopes, archive lock guard Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix: reserve the dev_workspace_lock protection-rule name from the public API Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * docs: reattach create_protection_rule doc comment to its function Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * refactor: make dev-archive pairing teardown atomic with the archive Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix: follow deploy_to on root rename; show dev pairing to non-member prod admins Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix: copy creator metadata on fork; invalidate fork routing cache on rename Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix: accept g/ paths in set_ws_specific; gate copy_members to dev workspaces Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
192 lines
6.3 KiB
Svelte
192 lines
6.3 KiB
Svelte
<script lang="ts">
|
|
import { workspaceStore, userWorkspaces, usersWorkspaceStore } from '$lib/stores'
|
|
import { WorkspaceService } from '$lib/gen'
|
|
import { Button } from '$lib/components/common'
|
|
import Select from '$lib/components/select/Select.svelte'
|
|
import Toggle from '$lib/components/Toggle.svelte'
|
|
import { sendUserToast } from '$lib/toast'
|
|
import { switchWorkspace } from '$lib/storeUtils'
|
|
import { goto } from '$app/navigation'
|
|
import { base } from '$lib/base'
|
|
import { findCanonicalDevWorkspace } from '$lib/utils/workspaceHierarchy'
|
|
import { loadProtectionRules } from '$lib/workspaceProtectionRules.svelte'
|
|
import { GitFork, ExternalLink } from 'lucide-svelte'
|
|
import { resource } from 'runed'
|
|
|
|
let currentWs = $derived($userWorkspaces.find((w) => w.id === $workspaceStore))
|
|
let isDev = $derived(currentWs?.is_dev_workspace ?? false)
|
|
let parentId = $derived(currentWs?.parent_workspace_id ?? undefined)
|
|
let canonicalDev = $derived(findCanonicalDevWorkspace($workspaceStore, $userWorkspaces))
|
|
|
|
// A prod admin who isn't a member of the dev can't see it in their workspace list, so ask the
|
|
// server (only when the client list doesn't already have it) — otherwise the tab would show the
|
|
// attach form instead of the existing pairing and detach control.
|
|
const devWorkspaceResource = resource(
|
|
() => (!isDev && !parentId && !canonicalDev ? $workspaceStore : undefined),
|
|
async (ws) => (ws ? await WorkspaceService.getDevWorkspace({ workspace: ws }) : undefined)
|
|
)
|
|
// The paired dev to display: the client entry when we're a member (so "Go to dev workspace" works),
|
|
// else the server result (pairing + detach still available to a prod admin).
|
|
let pairedDev = $derived(
|
|
canonicalDev
|
|
? { id: canonicalDev.id, name: canonicalDev.name, isMember: true }
|
|
: devWorkspaceResource.current
|
|
? {
|
|
id: devWorkspaceResource.current.id,
|
|
name: devWorkspaceResource.current.name,
|
|
isMember: false
|
|
}
|
|
: undefined
|
|
)
|
|
|
|
let selectedDevId = $state<string | undefined>(undefined)
|
|
let lockProdDeploy = $state(true)
|
|
let lockProdForking = $state(true)
|
|
let busy = $state(false)
|
|
|
|
// A standalone root workspace, or an existing fork of this prod (same family), can be attached.
|
|
// A fork parented to a different workspace can't (the backend rejects a parent that isn't this
|
|
// prod), so it's excluded here.
|
|
let attachCandidates = $derived(
|
|
$userWorkspaces
|
|
.filter(
|
|
(w) =>
|
|
w.id !== $workspaceStore &&
|
|
w.id !== 'admins' &&
|
|
(!w.parent_workspace_id || w.parent_workspace_id === $workspaceStore)
|
|
)
|
|
.map((w) => ({
|
|
label:
|
|
w.parent_workspace_id === $workspaceStore
|
|
? `${w.name} (${w.id}), fork of this workspace`
|
|
: `${w.name} (${w.id})`,
|
|
value: w.id
|
|
}))
|
|
)
|
|
|
|
async function refresh() {
|
|
usersWorkspaceStore.set(await WorkspaceService.listUserWorkspaces())
|
|
// Attach/detach changes this (root) workspace's protection rules; reload them so the
|
|
// direct-deploy / forking lock UI reflects the change without a workspace switch or reload.
|
|
if ($workspaceStore) {
|
|
await loadProtectionRules($workspaceStore)
|
|
}
|
|
}
|
|
|
|
async function attach() {
|
|
if (!selectedDevId || !$workspaceStore) return
|
|
busy = true
|
|
try {
|
|
await WorkspaceService.attachDevWorkspace({
|
|
workspace: $workspaceStore,
|
|
requestBody: {
|
|
dev_workspace_id: selectedDevId,
|
|
lock_prod_deploy: lockProdDeploy,
|
|
lock_prod_forking: lockProdForking
|
|
}
|
|
})
|
|
sendUserToast(`Attached ${selectedDevId} as dev workspace`)
|
|
selectedDevId = undefined
|
|
await refresh()
|
|
} catch (e: any) {
|
|
sendUserToast(`Failed to attach dev workspace: ${e?.body ?? e}`, true)
|
|
} finally {
|
|
busy = false
|
|
}
|
|
}
|
|
|
|
async function detach(devId: string) {
|
|
if (!$workspaceStore) return
|
|
busy = true
|
|
try {
|
|
await WorkspaceService.detachDevWorkspace({
|
|
workspace: $workspaceStore,
|
|
requestBody: { dev_workspace_id: devId }
|
|
})
|
|
sendUserToast(`Detached dev workspace ${devId}`)
|
|
await refresh()
|
|
} catch (e: any) {
|
|
sendUserToast(`Failed to detach dev workspace: ${e?.body ?? e}`, true)
|
|
} finally {
|
|
busy = false
|
|
}
|
|
}
|
|
</script>
|
|
|
|
{#if isDev && parentId}
|
|
<div class="flex flex-col gap-3 max-w-2xl">
|
|
<p class="text-sm">
|
|
This is a <b>dev workspace</b> paired with root workspace <b>{parentId}</b>. Promote changes
|
|
from the home page banner or the Compare & Deploy page.
|
|
</p>
|
|
<div>
|
|
<Button
|
|
variant="default"
|
|
startIcon={{ icon: ExternalLink }}
|
|
onclick={() => switchWorkspace(parentId)}
|
|
>
|
|
Go to root workspace
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
{:else if pairedDev}
|
|
<div class="flex flex-col gap-3 max-w-2xl">
|
|
<p class="text-sm">
|
|
This workspace's dev workspace is <b>{pairedDev.name}</b> ({pairedDev.id}). Edits to this
|
|
workspace are redirected there.
|
|
</p>
|
|
<div class="flex gap-2">
|
|
{#if pairedDev.isMember}
|
|
<Button
|
|
variant="default"
|
|
startIcon={{ icon: GitFork }}
|
|
onclick={() => switchWorkspace(pairedDev.id)}
|
|
>
|
|
Go to dev workspace
|
|
</Button>
|
|
{/if}
|
|
<Button color="red" disabled={busy} onclick={() => detach(pairedDev.id)}>Detach</Button>
|
|
</div>
|
|
</div>
|
|
{:else if parentId}
|
|
<p class="text-sm text-secondary max-w-2xl">
|
|
Dev workspace pairing is only available for root workspaces. This workspace is a fork of
|
|
<b>{parentId}</b>.
|
|
</p>
|
|
{:else}
|
|
<div class="flex flex-col gap-3 max-w-2xl">
|
|
<p class="text-sm text-secondary">
|
|
Pair this workspace with a dev workspace: the same code with a different environment (resource
|
|
and variable values). Edits are made in the dev workspace and promoted here.
|
|
</p>
|
|
<div class="flex flex-col gap-1">
|
|
<span class="text-xs font-semibold text-emphasis">Attach an existing workspace as dev</span>
|
|
<Select
|
|
items={attachCandidates}
|
|
bind:value={selectedDevId}
|
|
placeholder="Select a workspace"
|
|
clearable
|
|
/>
|
|
</div>
|
|
<Toggle
|
|
bind:checked={lockProdDeploy}
|
|
options={{
|
|
right: 'Block direct edits in this workspace (deploy via the dev workspace)'
|
|
}}
|
|
/>
|
|
<Toggle bind:checked={lockProdForking} options={{ right: 'Prevent forking this workspace' }} />
|
|
<div class="flex gap-2">
|
|
<Button variant="accent" disabled={busy || !selectedDevId} onclick={attach}>
|
|
Attach dev workspace
|
|
</Button>
|
|
<Button
|
|
variant="default"
|
|
startIcon={{ icon: GitFork }}
|
|
onclick={() => goto(`${base}/user/fork_workspace`)}
|
|
>
|
|
Create a new dev workspace
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
{/if}
|