mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-10-19 00:03:19 +00:00
* feat: rework git sync settings into a flat repo list and setup modal Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * feat: add the connect step for gitlab and github in git sync setup Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * feat: open the github app install page from the git sync setup next step Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * feat: let git sync setup set the repository folder Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * feat: add the configure sync step to git sync setup Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * style: even out the git sync filter settings columns Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * style: pin the filter input to the bottom of the filter list Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * style: frame the git sync path filters in a box with an empty hint Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * refactor: use a multiselect for the git sync path filters Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * feat: grey out the skipped connect step in git sync setup Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * style: move the check job line to the bottom of the setup modal Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix: hide the check job line once the repository check resolved Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * style: center the repository check loader in the setup modal Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix: stop the setup modal shifting when the check job id arrives Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * style: move the existing resource hint to the setup modal footer Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * feat: always show the existing resource option when one exists Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * style: drop the card and collapse from the git sync filter settings Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * style: use a success alert for the pull from git notice Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * feat: make initializing the repository a step of the git sync setup Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * style: hide the push job lines once they succeed Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * style: reword the push step intro Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * style: copy button on the cli snippet and a taller changes list Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * style: match the push step loading to the check step Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix: stop the push step shifting when a job id arrives Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * style: let the changes list fill the push step Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * feat: end the git sync setup on a done step instead of a modal Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * feat: delete a git sync repository from a row menu with a confirmation Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * style: lift the git sync done message off the notices Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * feat: restore github app installation transfer between instances Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix: address regressions from the git sync setup rework Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix: keep the overlay z-index floor in step with the ai chat Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix: hold the push state in the setup dialog, not in the step it destroys Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix: block every dismissal while setup work is in flight and keep sync reachable Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix: regenerate the resource path when the repository changes Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix: configure the resource the connect step actually created Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix: keep a modal open when a drawer above it takes the click Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix: snapshot the topmost surface for keyboard clicks too Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * style: size the add repository buttons and explain git promotion Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * revert: drop the promotion-first action from the empty state Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * style: shrink the test connection result line Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * feat: offer the fork sync and fork pull request toggles during setup Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * style: group promotion repositories under their own label Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * feat: offer the deploy branch pull request toggle during setup Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix: wait for the repository credential before saving the setup Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix: ignore a superseded credential load and drop the bindable default Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix: key setup readiness to the repository it was loaded for Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix: tie setup readiness to the attempt that asked for it Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * feat: link to where the access token is created Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * style: say plainly how each provider connects Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * feat: sync resources, variables, schedules and triggers by default Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * style: use the accent colour for the token help link Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * style: move the CI/CD deploy hint next to the pull toggle Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix: keep setup gated when the connection could not be read Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix: only explain pull-from-git where the workspace can turn it on Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix: offer a save retry once the repository is initialized Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix: say when pulling fell back to polling on the last setup step Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix: clear the initialized flag when a new draft starts Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
601 lines
19 KiB
Svelte
601 lines
19 KiB
Svelte
<script lang="ts">
|
|
import type { Snippet } from 'svelte'
|
|
import { untrack } from 'svelte'
|
|
import { ArrowRight, Database } from 'lucide-svelte'
|
|
import Button from '../common/button/Button.svelte'
|
|
import Alert from '../common/alert/Alert.svelte'
|
|
import Modal2 from '../common/modal/Modal2.svelte'
|
|
import Stepper from '../common/stepper/Stepper.svelte'
|
|
import ResourcePicker from '../ResourcePicker.svelte'
|
|
import Path from '../Path.svelte'
|
|
import TextInput from '../text_input/TextInput.svelte'
|
|
import EEOnly from '../EEOnly.svelte'
|
|
import RadioCard from '../common/radioCard/RadioCard.svelte'
|
|
import GithubIcon from '../icons/GithubIcon.svelte'
|
|
import GitlabIcon from '../icons/GitlabIcon.svelte'
|
|
import GitlabProjectConnectForm from './setup/GitlabProjectConnectForm.svelte'
|
|
import GithubAppConnectForm from './setup/GithubAppConnectForm.svelte'
|
|
import TokenUrlConnectForm from './setup/TokenUrlConnectForm.svelte'
|
|
import { createRepositoryResource, repoSlug, type RepoConnection } from './setup/repoConnection'
|
|
import { GithubAppSetup } from './setup/githubAppSetup.svelte'
|
|
import { onDestroy } from 'svelte'
|
|
import { getGitSyncContext } from './GitSyncContext.svelte'
|
|
import ConfigureSyncStep from './setup/ConfigureSyncStep.svelte'
|
|
import GitPushPreview from './GitPushPreview.svelte'
|
|
import GitSyncSuccessContent from './GitSyncSuccessContent.svelte'
|
|
import { ResourceService } from '$lib/gen'
|
|
import { enterpriseLicense, workspaceStore } from '$lib/stores'
|
|
import { apiErrorMessage } from '$lib/utils'
|
|
|
|
type Provider = 'gitlab' | 'github_app' | 'github_pat' | 'existing'
|
|
|
|
type Props = {
|
|
opened: boolean
|
|
title: string
|
|
mode: 'sync' | 'promotion'
|
|
/** Resources already attached to a repository of this workspace, which cannot be picked twice. */
|
|
usedResourcePaths: string[]
|
|
}
|
|
|
|
let { opened = $bindable(), title, mode, usedResourcePaths }: Props = $props()
|
|
|
|
const ctx = getGitSyncContext()
|
|
|
|
const STEPS = [
|
|
'Choose a provider',
|
|
'Connect the repository',
|
|
'Configure sync',
|
|
'Initialize the repository',
|
|
'Done'
|
|
]
|
|
|
|
let step: 1 | 2 | 3 | 4 | 5 = $state(1)
|
|
/** What the last step reports, set by the save that got there. */
|
|
let done: { savedWithoutInit: boolean; autoPullOn: boolean; webhookError?: string } | undefined =
|
|
$state(undefined)
|
|
|
|
/** Resource path of the unsaved repository step 3 configures. By path rather than
|
|
* index, as the context's list can shift under it. */
|
|
let draftPath: string | undefined = $state(undefined)
|
|
const draftIdx = $derived(
|
|
draftPath === undefined
|
|
? -1
|
|
: ctx.repositories.findIndex((r) => r.git_repo_resource_path === draftPath)
|
|
)
|
|
const draft = $derived(draftIdx === -1 ? undefined : ctx.repositories[draftIdx])
|
|
let push: GitPushPreview | undefined = $state()
|
|
/** Each trip to step 3 is its own attempt, and readiness names the one it belongs to: a
|
|
* step destroyed mid-load still reports when its request lands, and that answer is about
|
|
* the attempt it was opened for — which may be for the same repository as the current one. */
|
|
let configureAttempt = $state(0)
|
|
let factsLoadedFor: number | undefined = $state(undefined)
|
|
const factsLoaded = $derived(factsLoadedFor === configureAttempt)
|
|
let saving = $state(false)
|
|
let saveError: string | undefined = $state(undefined)
|
|
let provider: Provider | undefined = $state(undefined)
|
|
let existingPath: string | undefined = $state(undefined)
|
|
/** The workspace has a git_repository resource no repository uses yet, so picking one is
|
|
* offered up front rather than behind the hint. */
|
|
let hasResource = $state(false)
|
|
|
|
let connection: RepoConnection | undefined = $state(undefined)
|
|
/** Path of the resource the run creates. `Path` keeps whatever non-empty path it is bound
|
|
* to, so it has to be cleared when the repository changes — otherwise the second
|
|
* repository is offered the first one's path, which is already taken. */
|
|
let newPath = $state('')
|
|
let newPathError = $state('')
|
|
let branch = $state('')
|
|
let folder = $state('')
|
|
let connecting = $state(false)
|
|
let connectError: string | undefined = $state(undefined)
|
|
let githubApp: GithubAppSetup | undefined = $state(undefined)
|
|
|
|
/** A push started from step 4. Owned here rather than read off `GitPushPreview`: closing
|
|
* the dialog destroys that component in the same flush as the dismissal, so its state is
|
|
* already gone by the time the dismissal is handled. */
|
|
let applying = $state(false)
|
|
/** Work in flight that a dismissal must not cut short. */
|
|
const busy = $derived(connecting || saving || applying)
|
|
/** The push of step 4 landed. What is left is saving the connection, so a failure there
|
|
* must not be offered as "save without initializing" — the repository already holds the
|
|
* workspace's content. */
|
|
let initialized = $state(false)
|
|
|
|
onDestroy(() => {
|
|
githubApp?.dispose()
|
|
discardDraft()
|
|
})
|
|
$effect(() => {
|
|
if (!opened)
|
|
untrack(() => {
|
|
githubApp?.dispose()
|
|
discardDraft()
|
|
})
|
|
})
|
|
|
|
/** Drops the unsaved repository, keeping the resource, which stays pickable.
|
|
*
|
|
* A push that is still running is left alone: it may yet initialize the repository, and
|
|
* the row it leaves behind is how the user finishes saving the connection. */
|
|
function discardDraft() {
|
|
if (busy) {
|
|
draftPath = undefined
|
|
return
|
|
}
|
|
if (draftIdx !== -1 && ctx.repositories[draftIdx]?.isUnsavedConnection) {
|
|
void ctx.removeRepository(draftIdx)
|
|
}
|
|
draftPath = undefined
|
|
}
|
|
|
|
/** Ignores a report from any attempt but the one on screen. */
|
|
function noteFacts(attempt: number, loaded: boolean) {
|
|
if (attempt !== configureAttempt) return
|
|
factsLoadedFor = loaded ? attempt : undefined
|
|
}
|
|
|
|
function enterConfigure(path: string) {
|
|
discardDraft()
|
|
configureAttempt += 1
|
|
factsLoadedFor = undefined
|
|
// Whatever was pushed belonged to the draft just discarded, not to this one.
|
|
initialized = false
|
|
const before = ctx.repositories.length
|
|
if (mode === 'promotion') ctx.addPromotionRepository()
|
|
else ctx.addSyncRepository()
|
|
// Refused (CE is limited to one repository): the context toasted why.
|
|
if (ctx.repositories.length === before) return
|
|
ctx.repositories[before].git_repo_resource_path = path
|
|
draftPath = path
|
|
saveError = undefined
|
|
step = 3
|
|
}
|
|
|
|
/** From step 3 the resource already exists, so going back offers it as the
|
|
* existing resource to use rather than reconnecting from scratch. */
|
|
function back() {
|
|
if (busy) return
|
|
if (step === 4) {
|
|
step = 3
|
|
return
|
|
}
|
|
if (step === 3 && draftPath) {
|
|
const path = draftPath
|
|
discardDraft()
|
|
provider = 'existing'
|
|
existingPath = path
|
|
}
|
|
step = 1
|
|
}
|
|
|
|
async function saveDraft(withoutInit = false) {
|
|
if (draftIdx === -1) return
|
|
saving = true
|
|
saveError = undefined
|
|
try {
|
|
// The last step says what the success modal would have, so it stays quiet.
|
|
await ctx.saveRepository(draftIdx, withoutInit, false)
|
|
done = {
|
|
savedWithoutInit: withoutInit,
|
|
autoPullOn: draft?.auto_pull?.enabled === true,
|
|
// Read back by the save: the webhook is registered after the settings commit and
|
|
// a failure only leaves polling on.
|
|
webhookError: draft?.auto_pull?.webhook_error
|
|
}
|
|
step = 5
|
|
} catch (e) {
|
|
saveError = apiErrorMessage(e)
|
|
} finally {
|
|
saving = false
|
|
}
|
|
}
|
|
|
|
async function reset() {
|
|
step = 1
|
|
done = undefined
|
|
applying = false
|
|
initialized = false
|
|
githubApp?.dispose()
|
|
githubApp = undefined
|
|
hasResource = false
|
|
provider = undefined
|
|
existingPath = undefined
|
|
connection = undefined
|
|
newPath = ''
|
|
newPathError = ''
|
|
namedFor = undefined
|
|
branch = ''
|
|
folder = ''
|
|
connectError = undefined
|
|
const workspace = $workspaceStore
|
|
if (!workspace) return
|
|
try {
|
|
const resources = await ResourceService.listResource({
|
|
workspace,
|
|
resourceType: 'git_repository'
|
|
})
|
|
const firstUnused = resources.find((r) => !usedResourcePaths.includes(r.path))
|
|
hasResource = !!firstUnused
|
|
if (provider === undefined && firstUnused) {
|
|
provider = 'existing'
|
|
existingPath = firstUnused.path
|
|
}
|
|
} catch (e) {
|
|
console.error('Failed to list git_repository resources', e)
|
|
}
|
|
}
|
|
|
|
$effect(() => {
|
|
if (opened) untrack(() => reset())
|
|
})
|
|
|
|
// Cast: the only writes to `connection` are the `bind:` on the provider forms, which the
|
|
// checker does not see as assignments — it narrows the variable to `undefined` without it.
|
|
const repoName = $derived.by(() => {
|
|
const url = (connection as RepoConnection | undefined)?.url
|
|
return url ? repoSlug(url) : undefined
|
|
})
|
|
let namedFor: string | undefined = $state(undefined)
|
|
$effect(() => {
|
|
const name = repoName
|
|
untrack(() => {
|
|
if (name === namedFor) return
|
|
namedFor = name
|
|
newPath = ''
|
|
newPathError = ''
|
|
})
|
|
})
|
|
|
|
// GitLab without EE has no server-side token store nor project listing, so it
|
|
// takes the same URL + token form as a GitHub PAT.
|
|
const gitlabHeld = $derived(provider === 'gitlab' && !!$enterpriseLicense)
|
|
|
|
const canContinue = $derived(
|
|
step === 1
|
|
? provider === 'existing'
|
|
? !!existingPath
|
|
: provider !== undefined
|
|
: step === 2
|
|
? !!connection && !!newPath && !newPathError && !connecting
|
|
: (draft?.detectionState === 'no-wmill' || draft?.detectionState === 'has-wmill') &&
|
|
factsLoaded &&
|
|
!saving
|
|
)
|
|
|
|
// Steps this run skips: connecting, when an existing resource is used, and initializing,
|
|
// when the repository already holds a Windmill configuration.
|
|
const disabledIndices = $derived([
|
|
...(provider === 'existing' ? [1] : []),
|
|
...(draft?.detectionState === 'has-wmill' ? [3] : [])
|
|
])
|
|
|
|
function selectProvider(p: Provider) {
|
|
if (p !== provider) connection = undefined
|
|
provider = p
|
|
if (p === 'github_app' && !githubApp && $workspaceStore) {
|
|
githubApp = new GithubAppSetup($workspaceStore)
|
|
void githubApp.reload()
|
|
}
|
|
}
|
|
|
|
async function next() {
|
|
if (step === 1) {
|
|
if (provider === 'existing') {
|
|
if (existingPath) enterConfigure(existingPath)
|
|
} else {
|
|
connectError = undefined
|
|
step = 2
|
|
// Nothing to pick from yet: go straight to installing. Only while this click
|
|
// is still being handled, or the browser blocks the tab.
|
|
if (provider === 'github_app' && githubApp?.loaded && githubApp.usable.length === 0) {
|
|
githubApp.openInstall()
|
|
}
|
|
}
|
|
return
|
|
}
|
|
const workspace = $workspaceStore
|
|
if (!connection || !workspace) return
|
|
connecting = true
|
|
connectError = undefined
|
|
// Read once: the fields stay editable while the request is in flight, and reading the
|
|
// path again afterwards would configure a repository this run never created.
|
|
const submitted = { path: newPath, connection, branch, folder }
|
|
try {
|
|
await createRepositoryResource(workspace, submitted.path, submitted.connection, {
|
|
branch: submitted.branch,
|
|
folder: submitted.folder
|
|
})
|
|
enterConfigure(submitted.path)
|
|
} catch (e) {
|
|
connectError = apiErrorMessage(e)
|
|
} finally {
|
|
connecting = false
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<Modal2
|
|
bind:isOpen={opened}
|
|
target="#content"
|
|
formStyling
|
|
preventDismiss={busy}
|
|
{title}
|
|
contentClasses="flex flex-col"
|
|
fixedWidth="md"
|
|
fixedHeight="lg"
|
|
>
|
|
<div class="flex h-full flex-col gap-4">
|
|
<Stepper
|
|
tabs={STEPS}
|
|
selectedIndex={step - 1}
|
|
maxReachedIndex={step - 1}
|
|
{disabledIndices}
|
|
small
|
|
on:click={(e) => {
|
|
if (e.detail.index === 0 && step > 1) back()
|
|
}}
|
|
/>
|
|
|
|
<div class="flex-1 flex flex-col min-h-0">
|
|
<div class="flex-1 overflow-y-auto flex flex-col gap-2">
|
|
{#if step === 1}
|
|
{#snippet githubIcon()}
|
|
<GithubIcon height={18} width={18} />
|
|
{/snippet}
|
|
{#snippet gitlabIcon()}
|
|
<GitlabIcon height={18} width={18} />
|
|
{/snippet}
|
|
{#snippet resourceIcon()}
|
|
<Database size={18} class="text-secondary" />
|
|
{/snippet}
|
|
<div
|
|
role="radiogroup"
|
|
aria-label="How to connect the repository"
|
|
class="flex flex-col gap-2"
|
|
>
|
|
{@render providerCard(
|
|
'github_app',
|
|
githubIcon,
|
|
'GitHub App (recommended)',
|
|
'Connect with the GitHub UI.',
|
|
!$enterpriseLicense
|
|
)}
|
|
{@render providerCard(
|
|
'github_pat',
|
|
githubIcon,
|
|
'GitHub personal access token',
|
|
'Generate a token from GitHub to allow Windmill to access your account.'
|
|
)}
|
|
{@render providerCard(
|
|
'gitlab',
|
|
gitlabIcon,
|
|
'GitLab',
|
|
'Generate a token from GitLab to allow Windmill to access your account.'
|
|
)}
|
|
{#if hasResource || provider === 'existing'}
|
|
{@render providerCard(
|
|
'existing',
|
|
resourceIcon,
|
|
'Use an existing git_repository resource',
|
|
'Pick a resource that already holds the repository URL and its credentials.'
|
|
)}
|
|
{/if}
|
|
</div>
|
|
{#if provider === 'existing'}
|
|
<ResourcePicker
|
|
bind:value={existingPath}
|
|
resourceType="git_repository"
|
|
excludedValues={usedResourcePaths}
|
|
/>
|
|
{/if}
|
|
{:else if step === 5}
|
|
{#if done}
|
|
<GitSyncSuccessContent
|
|
savedWithoutInit={done.savedWithoutInit}
|
|
autoPullOn={done.autoPullOn}
|
|
webhookError={done.webhookError}
|
|
/>
|
|
{/if}
|
|
{:else if step === 4}
|
|
{#if draft}
|
|
<GitPushPreview
|
|
bind:this={push}
|
|
gitRepoResourcePath={draft.git_repo_resource_path}
|
|
uiState={draft.settings}
|
|
onApplyStateChange={(v) => (applying = v)}
|
|
onSuccess={() => {
|
|
initialized = true
|
|
void saveDraft()
|
|
}}
|
|
/>
|
|
{/if}
|
|
{#if saveError}
|
|
<Alert type="error" size="xs" title="Could not save the repository">
|
|
{saveError}
|
|
</Alert>
|
|
{/if}
|
|
{:else if step === 3}
|
|
{#if draftIdx !== -1}
|
|
<!-- One step per attempt, so a repository configured again starts from scratch
|
|
rather than reusing the instance that is still loading. -->
|
|
{#key configureAttempt}
|
|
<ConfigureSyncStep
|
|
idx={draftIdx}
|
|
{mode}
|
|
attempt={configureAttempt}
|
|
onFactsChange={noteFacts}
|
|
/>
|
|
{/key}
|
|
{/if}
|
|
{#if saveError}
|
|
<Alert type="error" size="xs" title="Could not save the repository">
|
|
{saveError}
|
|
</Alert>
|
|
{/if}
|
|
{:else if $workspaceStore}
|
|
<div class="flex flex-col gap-6">
|
|
{#if provider === 'github_app' && githubApp}
|
|
<GithubAppConnectForm workspace={$workspaceStore} setup={githubApp} bind:connection />
|
|
{:else if gitlabHeld}
|
|
<GitlabProjectConnectForm workspace={$workspaceStore} bind:connection />
|
|
{:else if provider === 'gitlab'}
|
|
<TokenUrlConnectForm provider="gitlab" bind:connection />
|
|
{:else}
|
|
<TokenUrlConnectForm provider="github" bind:connection />
|
|
{/if}
|
|
|
|
{#if connection}
|
|
<div class="flex flex-col gap-4 border-t pt-4">
|
|
<label class="flex flex-col gap-1">
|
|
<span class="text-xs font-semibold text-emphasis">Branch</span>
|
|
<span class="text-xs text-secondary">
|
|
The branch deploys are committed to. Leave empty for the repository's default
|
|
branch.
|
|
</span>
|
|
<TextInput bind:value={branch} inputProps={{ placeholder: 'Default branch' }} />
|
|
</label>
|
|
<label class="flex flex-col gap-1">
|
|
<span class="text-xs font-semibold text-emphasis">Folder</span>
|
|
<span class="text-xs text-secondary">
|
|
Where in the repository the workspace is kept, relative to its root. Leave empty
|
|
for the root. The folder must already exist in the repository.
|
|
</span>
|
|
<TextInput bind:value={folder} inputProps={{ placeholder: 'Repository root' }} />
|
|
</label>
|
|
<div class="flex flex-col gap-1">
|
|
<span class="text-xs font-semibold text-emphasis">Save as resource</span>
|
|
<span class="text-xs text-secondary">
|
|
Windmill keeps the repository and its access as a <code>git_repository</code> resource
|
|
at this path.
|
|
</span>
|
|
{#key connection.url}
|
|
<Path
|
|
kind="resource"
|
|
initialPath=""
|
|
fullNamePlaceholder={repoSlug(connection.url)}
|
|
autofocus={false}
|
|
bind:path={newPath}
|
|
bind:error={newPathError}
|
|
/>
|
|
{/key}
|
|
</div>
|
|
</div>
|
|
{/if}
|
|
|
|
{#if connectError}
|
|
<Alert type="error" size="xs" title="Could not connect the repository">
|
|
{connectError}
|
|
</Alert>
|
|
{/if}
|
|
</div>
|
|
{/if}
|
|
</div>
|
|
|
|
<div class="flex justify-between items-center pt-3">
|
|
<div>
|
|
{#if step > 1 && step < 5}
|
|
<Button unifiedSize="sm" variant="default" disabled={busy} onClick={back}>Back</Button>
|
|
{:else if step === 1 && !hasResource && provider !== 'existing'}
|
|
<Button variant="subtle" unifiedSize="sm" onClick={() => selectProvider('existing')}>
|
|
Use an existing resource instead
|
|
</Button>
|
|
{/if}
|
|
</div>
|
|
{#if step === 5}
|
|
<Button unifiedSize="sm" variant="accent" onClick={() => (opened = false)}>Close</Button>
|
|
{:else if step === 4}
|
|
<div class="flex items-center gap-2">
|
|
{#if initialized}
|
|
<Button
|
|
unifiedSize="sm"
|
|
variant="accent"
|
|
disabled={saving}
|
|
loading={saving}
|
|
onClick={() => saveDraft()}
|
|
>
|
|
Retry saving the connection
|
|
</Button>
|
|
{:else}
|
|
<Button
|
|
unifiedSize="sm"
|
|
variant="default"
|
|
disabled={push?.status().applying || push?.status().previewing || saving}
|
|
loading={saving}
|
|
onClick={() => saveDraft(true)}
|
|
>
|
|
Save without initializing
|
|
</Button>
|
|
<Button
|
|
unifiedSize="sm"
|
|
variant="accent"
|
|
disabled={!push?.status().canApply || saving}
|
|
loading={push?.status().applying}
|
|
onClick={() => push?.apply()}
|
|
>
|
|
Initialize repository
|
|
</Button>
|
|
{/if}
|
|
</div>
|
|
{:else if step === 3}
|
|
{#if draft?.detectionState === 'no-wmill'}
|
|
<Button
|
|
unifiedSize="sm"
|
|
variant="accent"
|
|
disabled={!canContinue}
|
|
endIcon={{ icon: ArrowRight }}
|
|
onClick={() => (step = 4)}
|
|
>
|
|
Next
|
|
</Button>
|
|
{:else}
|
|
<Button
|
|
unifiedSize="sm"
|
|
variant="accent"
|
|
disabled={!canContinue}
|
|
loading={saving}
|
|
onClick={() => void saveDraft()}
|
|
>
|
|
Save and connect
|
|
</Button>
|
|
{/if}
|
|
{:else}
|
|
<Button
|
|
unifiedSize="sm"
|
|
variant="accent"
|
|
disabled={!canContinue}
|
|
loading={connecting}
|
|
endIcon={connecting ? undefined : { icon: ArrowRight }}
|
|
onClick={next}
|
|
>
|
|
{step === 2 ? 'Connect' : 'Next'}
|
|
</Button>
|
|
{/if}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</Modal2>
|
|
|
|
{#snippet providerCard(
|
|
key: Provider,
|
|
icon: Snippet,
|
|
title: string,
|
|
subtitle: string,
|
|
eeOnly: boolean = false
|
|
)}
|
|
{#snippet description()}
|
|
<span class="flex items-center gap-2">
|
|
<span>{subtitle}</span>
|
|
{#if eeOnly}<EEOnly />{/if}
|
|
</span>
|
|
{/snippet}
|
|
<RadioCard
|
|
label={title}
|
|
{description}
|
|
selected={provider === key}
|
|
disabled={eeOnly}
|
|
{icon}
|
|
showRadio={false}
|
|
onSelect={() => selectProvider(key)}
|
|
/>
|
|
{/snippet}
|