716 lines
20 KiB
Svelte
716 lines
20 KiB
Svelte
<script lang="ts">
|
|
import { run } from 'svelte/legacy'
|
|
|
|
import {
|
|
JobService,
|
|
ResourceService,
|
|
SettingService,
|
|
UserService,
|
|
VariableService,
|
|
WorkspaceService,
|
|
type AIProvider,
|
|
type CompletedJob
|
|
} from '$lib/gen'
|
|
import { validateUsername } from '$lib/utils'
|
|
import { logoutWithRedirect } from '$lib/logoutKit'
|
|
import { page } from '$app/state'
|
|
import { usersWorkspaceStore, workspaceStore } from '$lib/stores'
|
|
import { Button } from '$lib/components/common'
|
|
import Toggle from '$lib/components/Toggle.svelte'
|
|
import Tooltip from '$lib/components/Tooltip.svelte'
|
|
import { onMount } from 'svelte'
|
|
import { sendUserToast } from '$lib/toast'
|
|
import TestAIKey from '$lib/components/copilot/TestAIKey.svelte'
|
|
import { switchWorkspace } from '$lib/storeUtils'
|
|
import { isCloudHosted } from '$lib/cloud'
|
|
import ToggleButtonGroup from '$lib/components/common/toggleButton-v2/ToggleButtonGroup.svelte'
|
|
import ToggleButton from '$lib/components/common/toggleButton-v2/ToggleButton.svelte'
|
|
import { AI_PROVIDERS } from '$lib/components/copilot/lib'
|
|
import { LoaderCircle } from 'lucide-svelte'
|
|
import PrefixedInput from '../PrefixedInput.svelte'
|
|
import TextInput from '../text_input/TextInput.svelte'
|
|
import { jobManager } from '$lib/services/JobManager'
|
|
import Alert from '../common/alert/Alert.svelte'
|
|
import { base } from '$lib/base'
|
|
import Label from '../Label.svelte'
|
|
import ForkDatatableSection from './ForkDatatableSection.svelte'
|
|
|
|
interface Props {
|
|
isFork?: boolean
|
|
onFinish?: () => void
|
|
}
|
|
|
|
let { isFork = false, onFinish }: Props = $props()
|
|
|
|
let id = $state('')
|
|
let name = $state('')
|
|
let username = $state('')
|
|
|
|
let errorId = $state('')
|
|
let errorUser = $state('')
|
|
let aiKey = $state('')
|
|
let codeCompletionEnabled = $state(true)
|
|
let checking = $state(false)
|
|
|
|
let forkDatatableSection: ReturnType<typeof ForkDatatableSection> | undefined = $state(undefined)
|
|
|
|
let workspaceColor: string | undefined = $state(undefined)
|
|
let colorEnabled = $state(false)
|
|
|
|
function generateRandomColor() {
|
|
const randomColor =
|
|
'#' +
|
|
Math.floor(Math.random() * 16777215)
|
|
.toString(16)
|
|
.padStart(6, '0')
|
|
workspaceColor = randomColor
|
|
}
|
|
|
|
async function validateName(id: string): Promise<void> {
|
|
checking = true
|
|
let exists = await WorkspaceService.existsWorkspace({ requestBody: { id } })
|
|
if (exists) {
|
|
errorId = 'ID already exists'
|
|
} else if (id != '' && !/^\w+(-\w+)*$/.test(id)) {
|
|
errorId = 'ID can only contain letters, numbers and dashes and must not finish by a dash'
|
|
} else {
|
|
errorId = ''
|
|
}
|
|
checking = false
|
|
}
|
|
|
|
const WM_FORK_PREFIX = 'wm-fork-'
|
|
|
|
let forkCreationLoading = $state(false)
|
|
let forkCreationError = $state('')
|
|
let errorMsgs: string[] = $state([])
|
|
let failedSyncJobs: string[] = $state([])
|
|
|
|
async function fetchFailedSyncJobs(jobs: string[]): Promise<CompletedJob[]> {
|
|
let ret: CompletedJob[] = []
|
|
for (const job of jobs) {
|
|
let j = await JobService.getCompletedJob({
|
|
id: job,
|
|
workspace: $workspaceStore!
|
|
})
|
|
ret.push(j)
|
|
}
|
|
return ret
|
|
}
|
|
|
|
function isPathVersionLessThan(path: string | undefined, version: number): boolean {
|
|
if (!path || !path.startsWith('hub/')) {
|
|
return false
|
|
}
|
|
|
|
const parts = path.split('/')
|
|
|
|
if (parts.length < 2) {
|
|
return false
|
|
}
|
|
|
|
const embeddedVersion = parseInt(parts[1], 10)
|
|
|
|
if (isNaN(embeddedVersion)) {
|
|
return false
|
|
}
|
|
|
|
return embeddedVersion < version
|
|
}
|
|
|
|
async function createOrForkWorkspace() {
|
|
const prefixed_id = `${WM_FORK_PREFIX}${id}`
|
|
if (isFork) {
|
|
await forkWorkspace(prefixed_id)
|
|
} else {
|
|
await createWorkspace()
|
|
}
|
|
}
|
|
|
|
async function forkWorkspace(prefixed_id: string): Promise<void> {
|
|
if ($workspaceStore) {
|
|
forkCreationLoading = true
|
|
errorMsgs = []
|
|
failedSyncJobs = []
|
|
forkCreationError = ''
|
|
|
|
// Clone datatables BEFORE creating the workspace fork
|
|
if (forkDatatableSection) {
|
|
const queue = forkDatatableSection.buildCloneQueue(prefixed_id)
|
|
if (queue.length > 0) {
|
|
forkDatatableSection.startCloning(queue)
|
|
return
|
|
}
|
|
}
|
|
|
|
await completeFork(prefixed_id)
|
|
} else {
|
|
sendUserToast('No workspace selected, cannot fork non-existent workspace', true)
|
|
}
|
|
}
|
|
|
|
async function completeFork(prefixed_id: string): Promise<void> {
|
|
let gitSyncJobIds = await WorkspaceService.createWorkspaceForkGitBranch({
|
|
workspace: $workspaceStore!,
|
|
requestBody: {
|
|
id: prefixed_id,
|
|
name,
|
|
color: colorEnabled && workspaceColor ? workspaceColor : undefined
|
|
}
|
|
})
|
|
|
|
try {
|
|
await Promise.all(
|
|
gitSyncJobIds.map((jobId) =>
|
|
jobManager.runWithProgress(() => Promise.resolve(jobId), {
|
|
workspace: $workspaceStore!,
|
|
timeout: 60000,
|
|
timeoutMessage: `Deploy fork job timed out after 60s`,
|
|
onProgress: (status) => {
|
|
if (status.status === 'failure') {
|
|
errorMsgs.push(status.error ?? 'Deploy fork job failed')
|
|
failedSyncJobs.push(jobId)
|
|
}
|
|
}
|
|
})
|
|
)
|
|
)
|
|
} catch (error) {
|
|
forkCreationLoading = false
|
|
sendUserToast(
|
|
`Could not fork workspace ${$workspaceStore} because branch creation failed: ${errorMsgs} - ${error}`,
|
|
true
|
|
)
|
|
return
|
|
}
|
|
if (errorMsgs.length != 0) {
|
|
forkCreationError = 'Failed to create a branch for this fork on the git sync repo(s)'
|
|
forkCreationLoading = false
|
|
sendUserToast(
|
|
`Could not fork workspace ${$workspaceStore} because branch creation failed: ${errorMsgs}`,
|
|
true
|
|
)
|
|
return
|
|
}
|
|
|
|
try {
|
|
await WorkspaceService.createWorkspaceFork({
|
|
workspace: $workspaceStore!,
|
|
requestBody: {
|
|
id: prefixed_id,
|
|
name,
|
|
color: colorEnabled && workspaceColor ? workspaceColor : undefined
|
|
}
|
|
})
|
|
} catch (e) {
|
|
forkCreationError = `Failed to create fork '${prefixed_id}'`
|
|
errorMsgs.push(e?.body ?? e ?? 'Unknown error')
|
|
forkCreationLoading = false
|
|
sendUserToast(`Could not create fork '${prefixed_id}' ${e}`, true)
|
|
return
|
|
}
|
|
|
|
// Update forked workspace settings for cloned datatables
|
|
if (forkDatatableSection) {
|
|
const clonedJobs = forkDatatableSection.getCompletedCloneJobs()
|
|
if (clonedJobs.length > 0) {
|
|
try {
|
|
await applyPostForkDatatableUpdates(prefixed_id, clonedJobs)
|
|
} catch (e: any) {
|
|
sendUserToast(`Warning: datatable settings update failed: ${e?.body ?? e}`, true)
|
|
}
|
|
}
|
|
}
|
|
|
|
forkCreationLoading = false
|
|
sendUserToast(`Successfully forked workspace ${$workspaceStore} as: wm-fork-${id}`)
|
|
|
|
usersWorkspaceStore.set(await WorkspaceService.listUserWorkspaces())
|
|
switchWorkspace(prefixed_id)
|
|
|
|
onFinish?.()
|
|
}
|
|
|
|
async function applyPostForkDatatableUpdates(
|
|
forkWorkspaceId: string,
|
|
clonedJobs: import('./ForkDatatableSection.svelte').DatatableCloneJob[]
|
|
): Promise<void> {
|
|
// First, get the current datatable settings from the forked workspace
|
|
const settings = await WorkspaceService.getSettings({ workspace: forkWorkspaceId })
|
|
const datatableConfig = settings.datatable ?? { datatables: {} }
|
|
|
|
for (const job of clonedJobs) {
|
|
if (job._isInstance) {
|
|
// Instance: update resource_path and set forked_from
|
|
if (datatableConfig.datatables[job.name]) {
|
|
const originalPath = datatableConfig.datatables[job.name].database.resource_path
|
|
datatableConfig.datatables[job.name].database.resource_path = job._newDbName
|
|
;(datatableConfig.datatables[job.name] as any).forked_from = originalPath
|
|
}
|
|
} else {
|
|
// Resource: update the resource's dbname and set non_diffable
|
|
const resourcePath = job._resourcePath
|
|
try {
|
|
const res = await ResourceService.getResource({
|
|
workspace: forkWorkspaceId,
|
|
path: resourcePath
|
|
})
|
|
const value = (res.value as Record<string, any>) ?? {}
|
|
value.dbname = job._newDbName
|
|
await ResourceService.updateResource({
|
|
workspace: forkWorkspaceId,
|
|
path: resourcePath,
|
|
requestBody: {
|
|
value,
|
|
non_diffable: true
|
|
}
|
|
})
|
|
} catch (e: any) {
|
|
console.error(`Failed to update resource ${resourcePath}:`, e)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Save updated datatable settings for instance-type changes
|
|
const hasInstanceChanges = clonedJobs.some((j) => j._isInstance)
|
|
if (hasInstanceChanges) {
|
|
await WorkspaceService.editDataTableConfig({
|
|
workspace: forkWorkspaceId,
|
|
requestBody: { settings: datatableConfig }
|
|
})
|
|
}
|
|
}
|
|
|
|
async function createWorkspace(): Promise<void> {
|
|
await WorkspaceService.createWorkspace({
|
|
requestBody: {
|
|
id,
|
|
name,
|
|
color: colorEnabled && workspaceColor ? workspaceColor : undefined,
|
|
username: automateUsernameCreation ? undefined : username
|
|
}
|
|
})
|
|
if (auto_invite) {
|
|
await WorkspaceService.editAutoInvite({
|
|
workspace: id,
|
|
requestBody: { operator: operatorOnly, invite_all: !isCloudHosted(), auto_add: autoAdd }
|
|
})
|
|
}
|
|
if (aiKey != '') {
|
|
let actualUsername = username
|
|
if (automateUsernameCreation) {
|
|
const user = await UserService.whoami({
|
|
workspace: id
|
|
})
|
|
actualUsername = user.username
|
|
}
|
|
let path = `u/${actualUsername}/${selected}_windmill_codegen`
|
|
await VariableService.createVariable({
|
|
workspace: id,
|
|
requestBody: {
|
|
path,
|
|
value: aiKey,
|
|
is_secret: true,
|
|
description: 'Ai token'
|
|
}
|
|
})
|
|
await ResourceService.createResource({
|
|
workspace: id,
|
|
requestBody: {
|
|
path,
|
|
value: {
|
|
api_key: '$var:' + path
|
|
},
|
|
resource_type: selected
|
|
}
|
|
})
|
|
await WorkspaceService.editCopilotConfig({
|
|
workspace: id,
|
|
requestBody: aiKey
|
|
? {
|
|
providers: {
|
|
[selected]: {
|
|
resource_path: path,
|
|
models: [AI_PROVIDERS[selected].defaultModels[0]]
|
|
}
|
|
},
|
|
default_model: {
|
|
model: AI_PROVIDERS[selected].defaultModels[0],
|
|
provider: selected
|
|
},
|
|
code_completion_model: codeCompletionEnabled
|
|
? { model: AI_PROVIDERS[selected].defaultModels[0], provider: selected }
|
|
: undefined
|
|
}
|
|
: {}
|
|
})
|
|
|
|
usersWorkspaceStore.set(await WorkspaceService.listUserWorkspaces())
|
|
switchWorkspace(id)
|
|
}
|
|
|
|
sendUserToast(`Created workspace id: ${id}`)
|
|
|
|
usersWorkspaceStore.set(await WorkspaceService.listUserWorkspaces())
|
|
switchWorkspace(id)
|
|
onFinish?.()
|
|
}
|
|
|
|
function handleKeyUp(event: KeyboardEvent) {
|
|
const key = event.key
|
|
if (key === 'Enter') {
|
|
event.preventDefault()
|
|
createWorkspace()
|
|
}
|
|
}
|
|
|
|
async function loadWorkspaces() {
|
|
if (!$usersWorkspaceStore) {
|
|
try {
|
|
usersWorkspaceStore.set(await WorkspaceService.listUserWorkspaces())
|
|
} catch {}
|
|
}
|
|
if (!$usersWorkspaceStore) {
|
|
const url = page.url
|
|
console.log('logout 2')
|
|
await logoutWithRedirect(url.href.replace(url.origin, ''))
|
|
}
|
|
}
|
|
|
|
let automateUsernameCreation = $state(true)
|
|
async function getAutomateUsernameCreationSetting() {
|
|
automateUsernameCreation =
|
|
((await SettingService.getGlobal({ key: 'automate_username_creation' })) as any) ?? true
|
|
|
|
if (!automateUsernameCreation) {
|
|
UserService.globalWhoami().then((x) => {
|
|
let uname = ''
|
|
if (x.name) {
|
|
uname = x.name.split(' ')[0]
|
|
} else {
|
|
uname = x.email.split('@')[0]
|
|
}
|
|
uname = uname.replace(/\./gi, '')
|
|
username = uname.toLowerCase()
|
|
})
|
|
}
|
|
}
|
|
getAutomateUsernameCreationSetting()
|
|
|
|
onMount(() => {
|
|
loadWorkspaces()
|
|
|
|
WorkspaceService.isDomainAllowed().then((x) => {
|
|
isDomainAllowed = x
|
|
})
|
|
})
|
|
|
|
let isDomainAllowed: undefined | boolean = $state(undefined)
|
|
|
|
let auto_invite = $state(false)
|
|
let operatorOnly = $state(false)
|
|
let autoAdd = $state(true)
|
|
let selected: Exclude<AIProvider, 'customai'> = $state('openai')
|
|
run(() => {
|
|
id = name.toLowerCase().replace(/\s/gi, '-')
|
|
})
|
|
run(() => {
|
|
validateName(id)
|
|
})
|
|
run(() => {
|
|
errorUser = validateUsername(username)
|
|
})
|
|
run(() => {
|
|
colorEnabled && !workspaceColor && generateRandomColor()
|
|
})
|
|
let domain = $derived($usersWorkspaceStore?.email.split('@')[1])
|
|
</script>
|
|
|
|
<div class="flex flex-col flex-1">
|
|
<div class="flex-1 relative min-h-[32rem]">
|
|
<div class="flex flex-col gap-8 absolute inset-0 overflow-y-auto">
|
|
{#if errorMsgs.length != 0}
|
|
<Alert class="p-2" title={forkCreationError} type="error">
|
|
<ul class="pl-2 pr-4 break-words pb-5">
|
|
{#each errorMsgs as errorMsg}
|
|
<li><pre class="whitespace-pre-wrap">- {errorMsg}</pre></li>
|
|
{/each}
|
|
</ul>
|
|
{#if failedSyncJobs.length != 0}
|
|
More details on the jobs that failed:
|
|
{#await fetchFailedSyncJobs(failedSyncJobs)}
|
|
<LoaderCircle class="animate-spin" />
|
|
{:then failedJobs}
|
|
<ul class="pl-2 pr-4 break-words">
|
|
{#each failedJobs as job}
|
|
<li>
|
|
-
|
|
<a
|
|
target="_blank"
|
|
class="underline"
|
|
href={`/run/${job.id}?workspace=${$workspaceStore}`}
|
|
>
|
|
{job.id}
|
|
</a>
|
|
</li>
|
|
<!-- This 28073 is the version where git sync on fork was introduced -->
|
|
{#if isPathVersionLessThan(job.script_path, 28073)}
|
|
<div class="font-bold">
|
|
This job was not running the latest version of the git sync script available
|
|
on the hub. You might be able to solve this issue by going to `Workspace
|
|
Settings` -> `Git Sync` and updating the script.
|
|
</div>
|
|
{/if}
|
|
{/each}
|
|
</ul>
|
|
{:catch error}
|
|
Tried to fetch jobs to get more information, but failed: {error}. Here are the failed
|
|
job ids:
|
|
<ul class="pl-2 pr-4 break-words">
|
|
{#each failedSyncJobs as jobId}
|
|
<li>
|
|
-
|
|
<a
|
|
target="_blank"
|
|
class="underline"
|
|
href={`/run/${jobId}?workspace=${$workspaceStore}`}
|
|
>
|
|
{jobId}
|
|
</a>
|
|
</li>
|
|
{/each}
|
|
</ul>
|
|
{/await}
|
|
{/if}
|
|
</Alert>
|
|
{/if}
|
|
<label class="flex flex-col gap-1">
|
|
{#if isFork}
|
|
<span class="text-xs font-semibold text-emphasis">Fork name</span>
|
|
<span class="text-xs text-secondary">Displayable name of the forked workspace</span>
|
|
{:else}
|
|
<span class="text-xs font-semibold text-emphasis">Workspace name</span>
|
|
<span class="text-xs text-secondary">Displayable name</span>
|
|
{/if}
|
|
<!-- svelte-ignore a11y_autofocus -->
|
|
<TextInput inputProps={{ autofocus: true }} bind:value={name} />
|
|
</label>
|
|
<label class="flex flex-col gap-1">
|
|
<span class="text-xs font-semibold text-emphasis">Workspace ID</span>
|
|
{#if isFork}
|
|
<span class="text-xs text-secondary"
|
|
>Slug to uniquely identify your fork (this will also set the branch name)</span
|
|
>
|
|
{:else}
|
|
<span class="text-xs text-secondary">Slug to uniquely identify your workspace</span>
|
|
{/if}
|
|
|
|
{#if isFork}
|
|
<PrefixedInput
|
|
prefix={WM_FORK_PREFIX}
|
|
type="text"
|
|
bind:value={id}
|
|
placeholder="example.com"
|
|
class={errorId != '' ? 'input-error' : ''}
|
|
/>
|
|
{:else}
|
|
<TextInput bind:value={id} error={errorId} />
|
|
{/if}
|
|
{#if errorId}
|
|
<span class="text-red-500 text-2xs font-normal">{errorId}</span>
|
|
{/if}
|
|
</label>
|
|
<Label label="Workspace color">
|
|
<span class="text-xs text-secondary">
|
|
Color to identify the current workspace in the list of workspaces
|
|
</span>
|
|
<div class="flex items-center gap-4">
|
|
<Toggle bind:checked={colorEnabled} options={{ right: 'Enable' }} />
|
|
{#if colorEnabled}
|
|
<div class="flex items-center gap-1 grow">
|
|
<input
|
|
class="grow min-w-10"
|
|
type="color"
|
|
bind:value={workspaceColor}
|
|
disabled={!colorEnabled}
|
|
/>
|
|
|
|
<TextInput
|
|
class="w-24"
|
|
bind:value={workspaceColor}
|
|
inputProps={{ disabled: !colorEnabled }}
|
|
/>
|
|
<Button
|
|
on:click={generateRandomColor}
|
|
size="xs"
|
|
variant="default"
|
|
disabled={!colorEnabled}>Random</Button
|
|
>
|
|
</div>
|
|
{/if}
|
|
</div>
|
|
</Label>
|
|
{#if isFork}
|
|
<ForkDatatableSection
|
|
bind:this={forkDatatableSection}
|
|
onAllDone={() => {
|
|
completeFork(`${WM_FORK_PREFIX}${id}`)
|
|
}}
|
|
onCanceled={() => {
|
|
forkCreationLoading = false
|
|
}}
|
|
/>
|
|
{/if}
|
|
{#if !automateUsernameCreation}
|
|
<Label label="Your username in that workspace">
|
|
<TextInput
|
|
bind:value={username}
|
|
inputProps={{ onkeyup: handleKeyUp }}
|
|
error={errorUser}
|
|
/>
|
|
{#if errorUser}
|
|
<span class="text-red-500 text-2xs">{errorUser}</span>
|
|
{/if}
|
|
</Label>
|
|
{/if}
|
|
{#if !isFork}
|
|
<div class="block">
|
|
<div class="flex flex-col gap-1">
|
|
<label for="ai-key" class="flex flex-row gap-2">
|
|
<span class="text-xs font-semibold text-emphasis">
|
|
AI key for Windmill AI
|
|
<Tooltip>
|
|
Find out how it can help you <a
|
|
href="https://www.windmill.dev/docs/core_concepts/ai_generation"
|
|
target="_blank"
|
|
rel="noopener noreferrer">in the docs</a
|
|
>
|
|
</Tooltip>
|
|
</span>
|
|
<span class="text-2xs text-secondary">(optional but recommended)</span>
|
|
</label>
|
|
|
|
<ToggleButtonGroup bind:selected>
|
|
{#snippet children({ item })}
|
|
<ToggleButton value="openai" label="OpenAI" {item} />
|
|
<ToggleButton value="anthropic" label="Anthropic" {item} />
|
|
<ToggleButton value="mistral" label="Mistral" {item} />
|
|
<ToggleButton value="deepseek" label="DeepSeek" {item} />
|
|
{/snippet}
|
|
</ToggleButtonGroup>
|
|
<div class="flex flex-row gap-1">
|
|
<input
|
|
id="ai-key"
|
|
type="password"
|
|
autocomplete="new-password"
|
|
bind:value={aiKey}
|
|
onkeyup={handleKeyUp}
|
|
/>
|
|
<TestAIKey
|
|
apiKey={aiKey}
|
|
disabled={!aiKey}
|
|
aiProvider={selected}
|
|
model={AI_PROVIDERS[selected].defaultModels[0]}
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
{#if aiKey}
|
|
<div class="flex flex-col gap-2 mt-2">
|
|
<Toggle
|
|
disabled={!aiKey}
|
|
bind:checked={codeCompletionEnabled}
|
|
options={{ right: 'Enable code completion' }}
|
|
/>
|
|
</div>
|
|
{/if}
|
|
</div>
|
|
<div class="flex flex-col gap-1">
|
|
<label for="auto-invite" class="text-xs font-semibold text-emphasis"
|
|
>{isCloudHosted()
|
|
? `Auto-${autoAdd ? 'add' : 'invite'} anyone from ${domain}`
|
|
: `Auto-${autoAdd ? 'add' : 'invite'} anyone joining the instance`}</label
|
|
>
|
|
<Toggle
|
|
id="auto-invite"
|
|
disabled={isCloudHosted() && !isDomainAllowed}
|
|
bind:checked={auto_invite}
|
|
/>
|
|
{#if isCloudHosted() && isDomainAllowed == false}
|
|
<div class="text-secondary text-2xs">{domain} domain not allowed for auto-invite</div>
|
|
{/if}
|
|
|
|
{#if auto_invite}
|
|
<div class="bg-surface-tertiary p-4 rounded-md flex flex-col gap-8">
|
|
<!-- svelte-ignore a11y_label_has_associated_control -->
|
|
{#if isCloudHosted()}
|
|
<label class="flex flex-col gap-1">
|
|
<span class="text-xs font-semibold text-emphasis">Mode</span>
|
|
<span class="text-xs text-secondary font-normal"
|
|
>Whether to invite or add users directly to the workspace.</span
|
|
>
|
|
<ToggleButtonGroup
|
|
selected={autoAdd ? 'add' : 'invite'}
|
|
on:selected={async (e) => {
|
|
autoAdd = e.detail === 'add'
|
|
}}
|
|
>
|
|
{#snippet children({ item })}
|
|
<ToggleButton value="invite" label="Auto-invite" {item} />
|
|
<ToggleButton value="add" label="Auto-add" {item} />
|
|
{/snippet}
|
|
</ToggleButtonGroup>
|
|
</label>
|
|
{/if}
|
|
|
|
<label class="font-semibold flex flex-col gap-1">
|
|
<span class="text-xs font-semibold text-emphasis">Role</span>
|
|
<span class="text-xs text-secondary font-normal"
|
|
>Role of the auto-invited users</span
|
|
>
|
|
<ToggleButtonGroup
|
|
selected={operatorOnly ? 'operator' : 'developer'}
|
|
on:selected={(e) => {
|
|
operatorOnly = e.detail == 'operator'
|
|
}}
|
|
>
|
|
{#snippet children({ item })}
|
|
<ToggleButton value="operator" label="Operator" {item} />
|
|
<ToggleButton value="developer" label="Developer" {item} />
|
|
{/snippet}
|
|
</ToggleButtonGroup>
|
|
</label>
|
|
</div>
|
|
{/if}
|
|
</div>
|
|
{/if}
|
|
</div>
|
|
</div>
|
|
<div class="flex flex-wrap flex-row justify-between gap-4 pt-4">
|
|
<Button disabled={forkCreationLoading} variant="default" size="sm" href="{base}/user/workspaces"
|
|
>← Back to workspaces</Button
|
|
>
|
|
{#if !forkCreationLoading}
|
|
<Button
|
|
variant="accent"
|
|
disabled={checking ||
|
|
errorId != '' ||
|
|
!name ||
|
|
(!automateUsernameCreation && (errorUser != '' || !username)) ||
|
|
!id}
|
|
on:click={createOrForkWorkspace}
|
|
>
|
|
{#if isFork}
|
|
Fork workspace
|
|
{:else}
|
|
Create workspace
|
|
{/if}
|
|
</Button>
|
|
{:else}
|
|
<Button variant="accent" disabled={true}>
|
|
<LoaderCircle class="animate-spin" /> Creating branch
|
|
</Button>
|
|
{/if}
|
|
</div>
|
|
</div>
|