Files
windmill/frontend/src/lib/components/git_sync/setup/GithubAppConnectForm.svelte
T
Diego ImbertandClaude Opus 5 504d01a563 feat: rework the git sync setup into a guided flow (#11308)
* 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>
2026-09-24 11:54:25 +02:00

233 lines
7.1 KiB
Svelte

<script lang="ts">
import { Download, ExternalLink, Loader2, Minus, RotateCw, Upload } from 'lucide-svelte'
import { Alert, Button } from '$lib/components/common'
import Select from '$lib/components/select/Select.svelte'
import DropdownV2 from '$lib/components/DropdownV2.svelte'
import TextInput from '$lib/components/text_input/TextInput.svelte'
import RepositorySelector from '$lib/components/RepositorySelector.svelte'
import {
addInstallationToWorkspace,
deleteInstallation,
exportInstallation,
importInstallation
} from '$lib/githubApp'
import type { RepoConnection } from './repoConnection'
import type { GithubAppSetup } from './githubAppSetup.svelte'
type Props = {
workspace: string
setup: GithubAppSetup
connection: RepoConnection | undefined
}
let { workspace, setup, connection = $bindable() }: Props = $props()
const gh = $derived(setup.gh)
const usable = $derived(setup.usable)
const broken = $derived(gh.workspaceGithubInstallations.filter((i) => !!i.error))
// Installations reachable from another workspace the user is in, deduplicated.
const elsewhere = $derived(
gh.githubInstallations.filter(
(i, idx, all) =>
!i.error &&
!gh.workspaceGithubInstallations.some((w) => w.installation_id === i.installation_id) &&
all.findIndex((o) => o.installation_id === i.installation_id) === idx
)
)
const selected = $derived(
usable.find((i) => i.installation_id === gh.selectedGHAppInstallationId)
)
const reload = () => setup.reload()
$effect(() => {
connection = gh.selectedGHAppRepository
? { url: gh.selectedGHAppRepository, isGithubApp: true }
: undefined
})
async function useFrom(installationId: number, sourceWorkspace: string) {
await addInstallationToWorkspace(workspace, installationId, sourceWorkspace, reload)
}
/** Pasting a JWT exported from another Windmill instance. */
let importing = $state(false)
// Export hands out a JWT the other instance imports; an installation reached through a
// GitHub Enterprise Server of its own is not transferable that way.
const installationActions = $derived([
...(selected && !selected.github_base_url
? [
{
displayName: 'Copy installation token (to another instance)',
icon: Download,
action: () => void exportInstallation(workspace, selected.installation_id)
}
]
: []),
...(gh.isGhesSelfManaged
? []
: [
{
displayName: 'Import an installation from another instance',
icon: Upload,
action: () => (importing = true)
}
]),
...(selected && !selected.provisioned_by_admin
? [
{
displayName: 'Remove installation from this workspace',
icon: Minus,
type: 'delete' as const,
action: () => void deleteInstallation(workspace, selected.installation_id, reload)
}
]
: [])
])
</script>
<div class="flex flex-col gap-4">
{#if (!setup.loaded || gh.loadingGithubInstallations) && gh.workspaceGithubInstallations.length === 0}
<div class="flex items-center gap-2 text-xs text-secondary">
<Loader2 size={14} class="animate-spin" /> Loading GitHub App installations...
</div>
{:else}
{#if usable.length === 0}
<Alert type="info" size="xs" bgClass="border-0" title="">
{#if gh.isCheckingInstallation}
Finish installing the Windmill GitHub App in the tab that opened. This dialog picks up the
installation on its own.
{:else}
Install the Windmill GitHub App on the GitHub account or organization that owns the
repository, then come back here.
{/if}
</Alert>
{:else}
<label class="flex flex-col gap-1">
<span class="text-xs font-semibold text-emphasis">GitHub account</span>
<Select
items={usable.map((i) => ({
label: i.account_id,
value: i.installation_id,
subtitle: `${i.total_count} repositories`
}))}
bind:value={
() => gh.selectedGHAppInstallationId,
(v) => {
gh.selectedGHAppInstallationId = v
gh.selectedGHAppRepository = undefined
}
}
clearable={false}
/>
</label>
{#if selected}
<label class="flex flex-col gap-1">
<span class="text-xs font-semibold text-emphasis">Repository</span>
<!-- RepositorySelector snapshots its repositories at mount -->
{#key selected.installation_id}
<RepositorySelector
bind:selectedRepository={gh.selectedGHAppRepository}
installationId={selected.installation_id}
initialRepositories={selected.repositories}
totalCount={selected.total_count}
perPage={selected.per_page}
/>
{/key}
</label>
{/if}
{/if}
{#if broken.length > 0}
<Alert type="warning" size="xs" title="Some installations cannot be used">
{broken.map((i) => `${i.account_id}: ${i.error}`).join('; ')}
</Alert>
{/if}
<div class="flex items-center gap-2">
<Button
variant="default"
unifiedSize="sm"
disabled={!gh.githubInstallationUrl}
startIcon={gh.isCheckingInstallation
? { icon: Loader2, classes: 'animate-spin' }
: undefined}
endIcon={{ icon: ExternalLink }}
title={gh.isCheckingInstallation ? 'Open the GitHub installation page again' : undefined}
onClick={() => setup.openInstall()}
>
{gh.isCheckingInstallation
? 'Waiting for the installation...'
: usable.length === 0
? 'Install the GitHub App'
: 'Install on another account'}
</Button>
<Button
variant="subtle"
unifiedSize="sm"
iconOnly
title="Refresh installations"
startIcon={{ icon: RotateCw }}
disabled={gh.loadingGithubInstallations}
onClick={reload}
/>
{#if installationActions.length > 0}
<DropdownV2 items={installationActions} />
{/if}
</div>
{#if importing}
<div class="flex flex-col gap-1">
<span class="text-xs font-semibold text-emphasis">Installation token</span>
<span class="text-xs text-secondary">
Paste the token copied from the instance that holds the installation.
</span>
<div class="flex items-center gap-2">
<TextInput
bind:value={gh.importJwt}
size="sm"
class="flex-1 min-w-0"
inputProps={{ placeholder: 'JWT token', autocomplete: 'off' }}
/>
<Button
variant="default"
unifiedSize="sm"
disabled={!gh.importJwt}
onClick={async () => {
await importInstallation(workspace, gh.importJwt, () => {
gh.importJwt = ''
importing = false
void reload()
})
}}
>
Import
</Button>
</div>
</div>
{/if}
{#if elsewhere.length > 0}
<div class="flex flex-col gap-1">
<span class="text-xs font-semibold text-emphasis">Installed in your other workspaces</span>
{#each elsewhere as inst (inst.installation_id)}
<div class="flex items-center justify-between gap-2 text-xs">
<span class="text-primary">
{inst.account_id}
<span class="text-secondary">from {inst.workspace_id}</span>
</span>
<Button
variant="default"
unifiedSize="xs"
onClick={() => inst.workspace_id && useFrom(inst.installation_id, inst.workspace_id)}
>
Use here
</Button>
</div>
{/each}
</div>
{/if}
{/if}
</div>