feat: simplify sync vs promotion mode ui in git sync settings (#6615)
* feat: simplify sync vs promotion mode ui in git sync settings * improvements
This commit is contained in:
@@ -6,13 +6,41 @@
|
||||
import Toggle from '$lib/components/Toggle.svelte'
|
||||
import { sendUserToast } from '$lib/toast'
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
import GitSyncModeDisplay from './GitSyncModeDisplay.svelte'
|
||||
|
||||
let { idx } = $props<{ idx: number }>()
|
||||
let { idx, mode } = $props<{ idx: number, mode?: 'sync' | 'promotion' }>()
|
||||
|
||||
const gitSyncContext = getGitSyncContext()
|
||||
const repo = $derived(gitSyncContext.getRepository(idx))
|
||||
let targetBranch = $state('main')
|
||||
|
||||
// Update target branch when repository changes
|
||||
$effect(() => {
|
||||
const abortController = new AbortController()
|
||||
|
||||
if (repo?.git_repo_resource_path) {
|
||||
gitSyncContext.getTargetBranch(repo).then(branch => {
|
||||
if (!abortController.signal.aborted) {
|
||||
targetBranch = branch
|
||||
}
|
||||
}).catch(error => {
|
||||
if (!abortController.signal.aborted) {
|
||||
console.warn('Failed to get target branch:', error)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return () => {
|
||||
abortController.abort()
|
||||
}
|
||||
})
|
||||
|
||||
async function handleDetect() {
|
||||
if (!repo) {
|
||||
sendUserToast('Repository not found', true)
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
await gitSyncContext.detectRepository(idx)
|
||||
} catch (error: any) {
|
||||
@@ -46,24 +74,13 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
{#if repo}
|
||||
<div class="space-y-4">
|
||||
{#if !repo.detectionState || repo.detectionState === 'idle'}
|
||||
<!-- Step 1: Show toggles first, then check button -->
|
||||
<div class="space-y-3">
|
||||
<Toggle
|
||||
disabled={!repo.git_repo_resource_path}
|
||||
bind:checked={repo.use_individual_branch}
|
||||
options={{
|
||||
left: 'Sync mode',
|
||||
leftTooltip: 'Changes will be committed directly to the branch',
|
||||
right: 'Promotion mode',
|
||||
rightTooltip:
|
||||
"Changes will be made to a new branch per deployed object (prefixed with 'wm_deploy/')"
|
||||
}}
|
||||
/>
|
||||
|
||||
{#if repo.use_individual_branch}
|
||||
<!-- Folder grouping option for promotion mode -->
|
||||
{#if mode === 'promotion'}
|
||||
<div class="space-y-3">
|
||||
<Toggle
|
||||
disabled={!repo.git_repo_resource_path}
|
||||
bind:checked={repo.group_by_folder}
|
||||
@@ -73,8 +90,8 @@
|
||||
'Instead of creating a branch per object, Windmill will create a branch per folder containing objects being deployed.'
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Check repo settings button -->
|
||||
<div class="flex justify-start">
|
||||
@@ -113,19 +130,7 @@
|
||||
useIndividualBranch={repo.use_individual_branch}
|
||||
/>
|
||||
|
||||
<!-- Display mode settings as prominent text -->
|
||||
<div class="text-base">
|
||||
{#if repo.use_individual_branch}
|
||||
<div
|
||||
><span class="font-bold">Promotion:</span> Creating branches whose promotion target is main</div
|
||||
>
|
||||
{#if repo.group_by_folder}
|
||||
<div class="text-sm text-tertiary mt-1">Grouped by folder</div>
|
||||
{/if}
|
||||
{:else}
|
||||
<div>Sync: <span class="font-bold">Syncing back to branch main</span></div>
|
||||
{/if}
|
||||
</div>
|
||||
<GitSyncModeDisplay {mode} {targetBranch} repository={repo} />
|
||||
|
||||
<!-- Initialize button -->
|
||||
<div class="flex justify-start">
|
||||
@@ -152,19 +157,7 @@
|
||||
useIndividualBranch={repo.use_individual_branch}
|
||||
/>
|
||||
|
||||
<!-- Display mode settings as prominent text -->
|
||||
<div class="text-base">
|
||||
{#if repo.use_individual_branch}
|
||||
<div
|
||||
><span class="font-bold">Promotion:</span> Creating branches whose promotion target is main</div
|
||||
>
|
||||
{#if repo.group_by_folder}
|
||||
<div class="text-sm text-tertiary mt-1">Grouped by folder</div>
|
||||
{/if}
|
||||
{:else}
|
||||
<div>Sync: <span class="font-bold">Syncing back to branch main</span></div>
|
||||
{/if}
|
||||
</div>
|
||||
<GitSyncModeDisplay {mode} {targetBranch} repository={repo} />
|
||||
|
||||
<!-- Save connection button -->
|
||||
<div class="flex justify-start">
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { getContext, setContext } from 'svelte'
|
||||
import { JobService, WorkspaceService } from '$lib/gen'
|
||||
import { JobService, WorkspaceService, ResourceService } from '$lib/gen'
|
||||
import type { GitRepositorySettings as BackendGitRepositorySettings, GitSyncObjectType } from '$lib/gen'
|
||||
import { jobManager } from '$lib/services/JobManager'
|
||||
import hubPaths from '$lib/hubPaths.json'
|
||||
@@ -20,6 +20,10 @@ export type GitSyncRepository = BackendGitRepositorySettings & {
|
||||
detectionJobStatus?: 'running' | 'success' | 'failure'
|
||||
// Internal tracking for resource path changes
|
||||
_trackedPath?: string
|
||||
// Cached target branch from git resource
|
||||
_targetBranch?: string
|
||||
// Detection timestamp to avoid race conditions
|
||||
_detectionTimestamp?: number
|
||||
}
|
||||
|
||||
export type GitSyncTestJob = {
|
||||
@@ -68,8 +72,15 @@ export function createGitSyncContext(workspace: string) {
|
||||
if (repo.isUnsavedConnection) {
|
||||
const currentPath = repo.git_repo_resource_path
|
||||
|
||||
if (repo._trackedPath && repo._trackedPath !== currentPath && repo.detectionState && repo.detectionState !== 'idle') {
|
||||
_resetRepoDetectionState(repo)
|
||||
if (repo._trackedPath && repo._trackedPath !== currentPath) {
|
||||
// Clear cached branch when resource path changes
|
||||
repo._targetBranch = undefined
|
||||
|
||||
// Reset detection state for any non-idle state (including errors)
|
||||
if (repo.detectionState && repo.detectionState !== 'idle') {
|
||||
// Cancel any running detection job by resetting immediately
|
||||
_resetRepoDetectionState(repo)
|
||||
}
|
||||
}
|
||||
|
||||
repo._trackedPath = currentPath
|
||||
@@ -273,6 +284,9 @@ export function createGitSyncContext(workspace: string) {
|
||||
repo.detectionJobId = undefined
|
||||
repo.detectionJobStatus = undefined
|
||||
|
||||
// Track the detection timestamp to avoid race conditions from old jobs
|
||||
const detectionTimestamp = Date.now()
|
||||
|
||||
try {
|
||||
const jobId = await JobService.runScriptByPath({
|
||||
workspace,
|
||||
@@ -291,15 +305,21 @@ export function createGitSyncContext(workspace: string) {
|
||||
|
||||
repo.detectionJobId = jobId
|
||||
repo.detectionJobStatus = 'running'
|
||||
repo._detectionTimestamp = detectionTimestamp
|
||||
|
||||
// Use JobManager for polling - result will be the actual job response
|
||||
await jobManager.runWithProgress(
|
||||
() => Promise.resolve(jobId),
|
||||
{
|
||||
workspace,
|
||||
timeout: 30000,
|
||||
timeoutMessage: 'Detection job timed out after 30s',
|
||||
timeout: 60000,
|
||||
timeoutMessage: 'Detection job timed out after 60s',
|
||||
onProgress: (status) => {
|
||||
// Only update state if this detection is still current
|
||||
if (repo._detectionTimestamp !== detectionTimestamp) {
|
||||
return
|
||||
}
|
||||
|
||||
repo.detectionJobStatus = status.status
|
||||
|
||||
// Process successful detection result
|
||||
@@ -313,7 +333,7 @@ export function createGitSyncContext(workspace: string) {
|
||||
if (response.local) {
|
||||
repo.extractedSettings = response.local
|
||||
// Auto-apply the extracted settings
|
||||
repo.settings = {
|
||||
repo.settings = {
|
||||
...response.local,
|
||||
exclude_path: response.local.exclude_path || [],
|
||||
extra_include_path: response.local.extra_include_path || []
|
||||
@@ -328,6 +348,11 @@ export function createGitSyncContext(workspace: string) {
|
||||
}
|
||||
)
|
||||
} catch (error: any) {
|
||||
// Only set error if this detection is still current
|
||||
if (repo._detectionTimestamp !== detectionTimestamp) {
|
||||
return
|
||||
}
|
||||
|
||||
repo.detectionState = 'error'
|
||||
repo.detectionError = error?.message || error?.toString() || 'Failed to detect repository'
|
||||
repo.detectionJobStatus = 'failure'
|
||||
@@ -533,6 +558,128 @@ export function createGitSyncContext(workspace: string) {
|
||||
}
|
||||
}
|
||||
|
||||
function getPrimarySyncRepository(): { repo: GitSyncRepository, idx: number } | null {
|
||||
const idx = repositories.findIndex(r => !r.use_individual_branch)
|
||||
return idx !== -1 ? { repo: repositories[idx], idx } : null
|
||||
}
|
||||
|
||||
function getPrimaryPromotionRepository(): { repo: GitSyncRepository, idx: number } | null {
|
||||
const idx = repositories.findIndex(r => r.use_individual_branch)
|
||||
return idx !== -1 ? { repo: repositories[idx], idx } : null
|
||||
}
|
||||
|
||||
function getSecondarySyncRepositories(): { repo: GitSyncRepository, idx: number }[] {
|
||||
const result: { repo: GitSyncRepository, idx: number }[] = []
|
||||
let foundFirst = false
|
||||
repositories.forEach((repo, idx) => {
|
||||
if (!repo.use_individual_branch) {
|
||||
if (foundFirst) {
|
||||
result.push({ repo, idx })
|
||||
} else {
|
||||
foundFirst = true
|
||||
}
|
||||
}
|
||||
})
|
||||
return result
|
||||
}
|
||||
|
||||
function getLegacyPromotionRepositories(): { repo: GitSyncRepository, idx: number }[] {
|
||||
const result: { repo: GitSyncRepository, idx: number }[] = []
|
||||
let foundFirst = false
|
||||
repositories.forEach((repo, idx) => {
|
||||
if (repo.use_individual_branch) {
|
||||
if (foundFirst) {
|
||||
result.push({ repo, idx })
|
||||
} else {
|
||||
foundFirst = true
|
||||
}
|
||||
}
|
||||
})
|
||||
return result
|
||||
}
|
||||
|
||||
async function removeRepositoryByPath(resourcePath: string) {
|
||||
const idx = repositories.findIndex(r => r.git_repo_resource_path === resourcePath)
|
||||
if (idx !== -1) {
|
||||
await removeRepository(idx)
|
||||
}
|
||||
}
|
||||
|
||||
function addSyncRepository() {
|
||||
repositories.push({
|
||||
git_repo_resource_path: '',
|
||||
script_path: hubPaths.gitSync,
|
||||
use_individual_branch: false,
|
||||
group_by_folder: false,
|
||||
settings: {
|
||||
include_path: ['f/**'],
|
||||
exclude_path: [],
|
||||
extra_include_path: [],
|
||||
include_type: ['script', 'flow', 'app', 'folder']
|
||||
},
|
||||
exclude_types_override: [],
|
||||
legacyImported: false,
|
||||
isUnsavedConnection: true,
|
||||
collapsed: false
|
||||
})
|
||||
gitSyncTestJobs.push({
|
||||
jobId: '',
|
||||
status: undefined
|
||||
})
|
||||
}
|
||||
|
||||
function addPromotionRepository() {
|
||||
repositories.push({
|
||||
git_repo_resource_path: '',
|
||||
script_path: hubPaths.gitSync,
|
||||
use_individual_branch: true,
|
||||
group_by_folder: false,
|
||||
settings: {
|
||||
include_path: ['f/**'],
|
||||
exclude_path: [],
|
||||
extra_include_path: [],
|
||||
include_type: ['script', 'flow', 'app', 'folder']
|
||||
},
|
||||
exclude_types_override: [],
|
||||
legacyImported: false,
|
||||
isUnsavedConnection: true,
|
||||
collapsed: false
|
||||
})
|
||||
gitSyncTestJobs.push({
|
||||
jobId: '',
|
||||
status: undefined
|
||||
})
|
||||
}
|
||||
|
||||
// Helper to get target branch from git resource
|
||||
async function getTargetBranch(repo: GitSyncRepository): Promise<string> {
|
||||
if (!repo.git_repo_resource_path) {
|
||||
return 'main'
|
||||
}
|
||||
|
||||
if (repo._targetBranch) {
|
||||
return repo._targetBranch
|
||||
}
|
||||
|
||||
try {
|
||||
const resource = await ResourceService.getResource({
|
||||
workspace,
|
||||
path: repo.git_repo_resource_path
|
||||
})
|
||||
|
||||
// Extract branch from git resource value
|
||||
const resourceValue = resource.value as any
|
||||
const targetBranch = resourceValue?.branch || 'main'
|
||||
|
||||
// Cache the result
|
||||
repo._targetBranch = targetBranch
|
||||
return targetBranch
|
||||
} catch (error) {
|
||||
console.warn('Failed to fetch git resource for branch info:', error)
|
||||
return 'main'
|
||||
}
|
||||
}
|
||||
|
||||
// Return context object
|
||||
return {
|
||||
// State (read-only access)
|
||||
@@ -553,7 +700,10 @@ export function createGitSyncContext(workspace: string) {
|
||||
|
||||
// Methods
|
||||
addRepository,
|
||||
addSyncRepository,
|
||||
addPromotionRepository,
|
||||
removeRepository,
|
||||
removeRepositoryByPath,
|
||||
getRepository,
|
||||
getValidation,
|
||||
revertRepository,
|
||||
@@ -569,6 +719,15 @@ export function createGitSyncContext(workspace: string) {
|
||||
closeSuccessModal,
|
||||
loadSettings,
|
||||
saveRepository,
|
||||
|
||||
// Repository categorization methods
|
||||
getPrimarySyncRepository,
|
||||
getPrimaryPromotionRepository,
|
||||
getSecondarySyncRepositories,
|
||||
getLegacyPromotionRepositories,
|
||||
|
||||
// Helper methods
|
||||
getTargetBranch,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
<script lang="ts">
|
||||
import type { GitSyncRepository } from './GitSyncContext.svelte'
|
||||
|
||||
let {
|
||||
mode,
|
||||
targetBranch,
|
||||
repository
|
||||
} = $props<{
|
||||
mode?: 'sync' | 'promotion' | null
|
||||
targetBranch: string
|
||||
repository?: GitSyncRepository | null
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<div class="text-base">
|
||||
{#if mode === 'promotion'}
|
||||
<div><span class="font-bold">Promotion:</span> Creating branches whose promotion target is {targetBranch}</div>
|
||||
{#if repository?.group_by_folder}
|
||||
<div class="text-sm text-tertiary mt-1">Grouped by folder</div>
|
||||
{/if}
|
||||
{:else}
|
||||
<div><span class="font-bold">Sync:</span> Syncing back to branch {targetBranch}</div>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { Save, Trash, XCircle, CheckCircle2, RotateCw, RotateCcw, Download, Upload } from 'lucide-svelte'
|
||||
import { Save, Trash, XCircle, CheckCircle2, RotateCw, RotateCcw, Download, Upload, Plus } from 'lucide-svelte'
|
||||
import { Button, Alert } from '$lib/components/common'
|
||||
import { getGitSyncContext } from './GitSyncContext.svelte'
|
||||
import ResourcePicker from '$lib/components/ResourcePicker.svelte'
|
||||
@@ -9,14 +9,58 @@
|
||||
import { fade } from 'svelte/transition'
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
import hubPaths from '$lib/hubPaths.json'
|
||||
import type { GitSyncRepository } from './GitSyncContext.svelte'
|
||||
import GitSyncModeDisplay from './GitSyncModeDisplay.svelte'
|
||||
|
||||
let { idx } = $props<{ idx: number }>()
|
||||
let {
|
||||
idx = null,
|
||||
isSecondary = false,
|
||||
isLegacy = false,
|
||||
variant = 'standard',
|
||||
mode = null,
|
||||
repository = null,
|
||||
onAdd = null,
|
||||
isCollapsible = true,
|
||||
showEmptyState = false
|
||||
} = $props<{
|
||||
idx?: number | null,
|
||||
isSecondary?: boolean,
|
||||
isLegacy?: boolean,
|
||||
variant?: 'primary-sync' | 'primary-promotion' | 'secondary' | 'legacy' | 'standard',
|
||||
mode?: 'sync' | 'promotion' | null,
|
||||
repository?: GitSyncRepository | null,
|
||||
onAdd?: (() => void) | null,
|
||||
isCollapsible?: boolean,
|
||||
showEmptyState?: boolean
|
||||
}>()
|
||||
|
||||
const gitSyncContext = getGitSyncContext()
|
||||
const repo = $derived(gitSyncContext.getRepository(idx))
|
||||
const validation = $derived(gitSyncContext.getValidation(idx))
|
||||
const gitSyncTestJob = $derived(gitSyncContext.gitSyncTestJobs?.[idx])
|
||||
const repo = $derived(repository || (idx !== null ? gitSyncContext.getRepository(idx) : null))
|
||||
const validation = $derived(idx !== null ? gitSyncContext.getValidation(idx) : null)
|
||||
const gitSyncTestJob = $derived(idx !== null ? gitSyncContext.gitSyncTestJobs?.[idx] : null)
|
||||
let confirmingDelete = $state(false)
|
||||
let targetBranch = $state('main') // Default to main, will be updated when resource is available
|
||||
|
||||
// Update target branch when repository changes
|
||||
$effect(() => {
|
||||
const abortController = new AbortController()
|
||||
|
||||
if (repo?.git_repo_resource_path) {
|
||||
gitSyncContext.getTargetBranch(repo).then(branch => {
|
||||
if (!abortController.signal.aborted) {
|
||||
targetBranch = branch
|
||||
}
|
||||
}).catch(error => {
|
||||
if (!abortController.signal.aborted) {
|
||||
console.warn('Failed to get target branch:', error)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return () => {
|
||||
abortController.abort()
|
||||
}
|
||||
})
|
||||
|
||||
// Compute already-used repository paths to exclude from picker
|
||||
const usedRepositoryPaths = $derived(
|
||||
@@ -25,8 +69,28 @@
|
||||
.filter((path): path is string => Boolean(path?.trim()))
|
||||
)
|
||||
|
||||
// Determine display title based on variant and legacy status
|
||||
const displayTitle = $derived(
|
||||
variant === 'primary-sync' ? (mode === 'sync' ? 'Sync mode' : 'Promotion mode') :
|
||||
variant === 'primary-promotion' ? 'Promotion mode' :
|
||||
isLegacy ? 'Legacy promotion repository' :
|
||||
isSecondary ? 'Secondary sync repository' :
|
||||
`Repository #${(idx ?? 0) + 1}`
|
||||
)
|
||||
|
||||
// Determine display description based on variant and mode
|
||||
const displayDescription = $derived(
|
||||
(variant === 'primary-sync' || variant === 'primary-promotion') ?
|
||||
(mode === 'sync' ? `Changes will be committed directly to the ${targetBranch} branch` :
|
||||
mode === 'promotion' ? `Changes will be made to new branches whose promotion target is ${targetBranch}` :
|
||||
null) :
|
||||
null
|
||||
)
|
||||
|
||||
const shouldShowEmptyState = $derived(showEmptyState || (!repo && (variant === 'primary-sync' || variant === 'primary-promotion')))
|
||||
|
||||
async function handleSave() {
|
||||
if (!repo) return
|
||||
if (!repo || idx === null) return
|
||||
|
||||
try {
|
||||
await gitSyncContext.saveRepository(idx)
|
||||
@@ -38,7 +102,7 @@
|
||||
}
|
||||
|
||||
function handleRevert() {
|
||||
if (!repo) return
|
||||
if (!repo || idx === null) return
|
||||
try {
|
||||
gitSyncContext.revertRepository?.(idx)
|
||||
sendUserToast('Reverted repository settings')
|
||||
@@ -53,6 +117,7 @@
|
||||
}
|
||||
|
||||
async function confirmDelete() {
|
||||
if (idx === null) return
|
||||
try {
|
||||
await gitSyncContext.removeRepository(idx)
|
||||
sendUserToast('Repository connection removed successfully')
|
||||
@@ -69,7 +134,7 @@
|
||||
}
|
||||
|
||||
function runGitSyncTestJob() {
|
||||
if (gitSyncContext.runTestJob) {
|
||||
if (idx !== null && gitSyncContext.runTestJob) {
|
||||
gitSyncContext.runTestJob(idx)
|
||||
}
|
||||
}
|
||||
@@ -79,271 +144,346 @@
|
||||
}
|
||||
|
||||
function handlePullSettings() {
|
||||
gitSyncContext.showPullModal(idx, true) // true for settingsOnly
|
||||
if (idx !== null) {
|
||||
gitSyncContext.showPullModal(idx, true)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
{#if repo}
|
||||
<div class="rounded-lg shadow-sm border p-0 w-full mb-4">
|
||||
<!-- Card Header -->
|
||||
<div class="flex items-center justify-between min-h-10 px-4 py-1 border-b">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="font-semibold">Repository #{idx + 1}</span>
|
||||
{#if repo.legacyImported}
|
||||
<span class="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-orange-100 text-orange-800 border border-orange-200">
|
||||
Legacy Configuration
|
||||
</span>
|
||||
{/if}
|
||||
<span class="text-xs text-tertiary pt-1 pl-8">
|
||||
{repo.git_repo_resource_path}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
{#if validation.hasChanges && validation.isValid && !repo.isUnsavedConnection}
|
||||
<Button
|
||||
size="xs"
|
||||
onclick={handleSave}
|
||||
startIcon={{ icon: Save }}
|
||||
>
|
||||
{repo.legacyImported ? 'Migrate and save' : 'Save changes'}
|
||||
</Button>
|
||||
{#if gitSyncContext.initialRepositories[idx] && !repo.legacyImported}
|
||||
<Button
|
||||
color="light"
|
||||
size="xs"
|
||||
onclick={handleRevert}
|
||||
startIcon={{ icon: RotateCcw }}
|
||||
>
|
||||
Revert
|
||||
</Button>
|
||||
{/if}
|
||||
{/if}
|
||||
<button
|
||||
class="text-secondary hover:text-primary focus:outline-none"
|
||||
onclick={() => (repo.collapsed = !repo.collapsed)}
|
||||
aria-label="Toggle collapse"
|
||||
<!-- Shared snippets for reusable content -->
|
||||
|
||||
{#snippet headerActions()}
|
||||
{#if !isLegacy}
|
||||
{#if validation?.hasChanges && validation?.isValid && !repo.isUnsavedConnection}
|
||||
<Button
|
||||
size="xs"
|
||||
onclick={handleSave}
|
||||
startIcon={{ icon: Save }}
|
||||
>
|
||||
{repo.legacyImported ? 'Migrate and save' : 'Save changes'}
|
||||
</Button>
|
||||
{#if idx !== null && gitSyncContext.initialRepositories[idx] && !repo.legacyImported}
|
||||
<Button
|
||||
color="light"
|
||||
size="xs"
|
||||
onclick={handleRevert}
|
||||
startIcon={{ icon: RotateCcw }}
|
||||
>
|
||||
{#if repo.collapsed}
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-5 w-5"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M19 9l-7 7-7-7"
|
||||
/>
|
||||
</svg>
|
||||
Revert
|
||||
</Button>
|
||||
{/if}
|
||||
{/if}
|
||||
{/if}
|
||||
{#if isCollapsible}
|
||||
<button
|
||||
class="text-secondary hover:text-primary focus:outline-none"
|
||||
onclick={() => (repo.collapsed = !repo.collapsed)}
|
||||
aria-label="Toggle collapse"
|
||||
>
|
||||
{#if repo.collapsed}
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-5 w-5"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M19 9l-7 7-7-7"
|
||||
/>
|
||||
</svg>
|
||||
{:else}
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-5 w-5"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M5 15l7-7 7 7"
|
||||
/>
|
||||
</svg>
|
||||
{/if}
|
||||
</button>
|
||||
{/if}
|
||||
{#if !confirmingDelete}
|
||||
<div transition:fade|local={{ duration: 100 }}>
|
||||
<Button
|
||||
size="xs"
|
||||
color="light"
|
||||
variant="border"
|
||||
onclick={initiateDelete}
|
||||
startIcon={{ icon: Trash }}
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="flex gap-1">
|
||||
<button
|
||||
transition:fade|local={{ duration: 100 }}
|
||||
class="px-3 py-1 text-xs bg-red-500 text-white rounded duration-200 hover:bg-red-600"
|
||||
onclick={confirmDelete}
|
||||
>
|
||||
Confirm delete
|
||||
</button>
|
||||
<button
|
||||
transition:fade|local={{ duration: 100 }}
|
||||
class="px-2 py-1 text-xs bg-surface-secondary rounded duration-200 hover:bg-surface-hover"
|
||||
onclick={cancelDelete}
|
||||
>
|
||||
<XCircle size={12} />
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
{/snippet}
|
||||
|
||||
{#snippet repositoryContent()}
|
||||
<div class="space-y-4">
|
||||
<!-- Resource Picker -->
|
||||
<div class="flex gap-2 items-center">
|
||||
<div class="font-medium">Resource:</div>
|
||||
<div class="flex-1">
|
||||
<ResourcePicker
|
||||
bind:value={repo.git_repo_resource_path}
|
||||
resourceType={'git_repository'}
|
||||
disabled={!repo.isUnsavedConnection}
|
||||
excludedValues={usedRepositoryPaths}
|
||||
/>
|
||||
</div>
|
||||
{#if !emptyString(repo.git_repo_resource_path)}
|
||||
<Button
|
||||
disabled={emptyString(repo.script_path)}
|
||||
color="dark"
|
||||
onclick={runGitSyncTestJob}
|
||||
size="xs"
|
||||
>
|
||||
Test connection
|
||||
</Button>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if !emptyString(repo.git_repo_resource_path)}
|
||||
<!-- Validation and Test Status -->
|
||||
{#if validation?.isDuplicate}
|
||||
<div class="text-red-600 text-sm">
|
||||
This resource is already used by another repository.
|
||||
</div>
|
||||
{/if}
|
||||
{#if gitSyncTestJob && gitSyncTestJob.status !== undefined}
|
||||
<div class="flex text-sm gap-1 items-center">
|
||||
{#if gitSyncTestJob.status === 'running'}
|
||||
<RotateCw size={14} class="animate-spin" />
|
||||
{:else if gitSyncTestJob.status === 'success'}
|
||||
<CheckCircle2 size={14} class="text-green-600" />
|
||||
{:else}
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-5 w-5"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M5 15l7-7 7 7"
|
||||
/>
|
||||
</svg>
|
||||
<XCircle size={14} class="text-red-700" />
|
||||
{/if}
|
||||
</button>
|
||||
{#if !confirmingDelete}
|
||||
<button
|
||||
transition:fade|local={{ duration: 100 }}
|
||||
class="rounded-full p-2 bg-surface-secondary duration-200 hover:bg-surface-hover"
|
||||
aria-label="Remove repository"
|
||||
onclick={initiateDelete}
|
||||
Git sync resource checked via Windmill job
|
||||
<a
|
||||
target="_blank"
|
||||
href={`/run/${gitSyncTestJob.jobId}?workspace=${$workspaceStore}`}
|
||||
class="text-blue-500 hover:underline"
|
||||
>
|
||||
<Trash size={14} />
|
||||
</button>
|
||||
{:else}
|
||||
<div class="flex gap-1">
|
||||
<button
|
||||
transition:fade|local={{ duration: 100 }}
|
||||
class="px-3 py-1 text-xs bg-red-500 text-white rounded duration-200 hover:bg-red-600"
|
||||
onclick={confirmDelete}
|
||||
{gitSyncTestJob.jobId}
|
||||
</a>
|
||||
<span class="text-secondary">WARNING: Only read permissions are verified.</span>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Warnings -->
|
||||
{#if repo.legacyImported}
|
||||
<Alert type="warning" title="Legacy git sync settings imported">
|
||||
This repository was initialized from workspace-level legacy Git-Sync settings. Review the filters and press <b>Save</b> to migrate.
|
||||
</Alert>
|
||||
{/if}
|
||||
|
||||
{#if repo.script_path != hubPaths.gitSync}
|
||||
<Alert type="warning" title="Script version mismatch">
|
||||
The git sync version for this repository is not latest. Current: <a
|
||||
target="_blank"
|
||||
href="https://hub.windmill.dev/scripts/windmill/6943/sync-script-to-git-repo-windmill/9014/versions"
|
||||
>{repo.script_path}</a>, latest:
|
||||
<a
|
||||
target="_blank"
|
||||
href="https://hub.windmill.dev/scripts/windmill/6943/sync-script-to-git-repo-windmill/9014/versions"
|
||||
>{hubPaths.gitSync}</a>
|
||||
<div class="flex mt-2">
|
||||
<Button
|
||||
size="xs"
|
||||
color="dark"
|
||||
onclick={() => {
|
||||
if (repo) {
|
||||
repo.script_path = hubPaths.gitSync
|
||||
}
|
||||
}}
|
||||
>
|
||||
Confirm delete
|
||||
</button>
|
||||
<button
|
||||
transition:fade|local={{ duration: 100 }}
|
||||
class="px-2 py-1 text-xs bg-surface-secondary rounded duration-200 hover:bg-surface-hover"
|
||||
onclick={cancelDelete}
|
||||
>
|
||||
<XCircle size={12} />
|
||||
</button>
|
||||
Update git sync script (require save git settings to be applied)
|
||||
</Button>
|
||||
</div>
|
||||
</Alert>
|
||||
{/if}
|
||||
|
||||
<!-- Configuration -->
|
||||
{#if repo.isUnsavedConnection && !emptyString(repo.git_repo_resource_path) && idx !== null}
|
||||
<DetectionFlow {idx} mode={variant === 'primary-promotion' || variant === 'legacy' ? 'promotion' : 'sync'} />
|
||||
{:else}
|
||||
<GitSyncFilterSettings
|
||||
bind:git_repo_resource_path={repo.git_repo_resource_path}
|
||||
bind:include_path={repo.settings.include_path}
|
||||
bind:include_type={repo.settings.include_type}
|
||||
bind:exclude_types_override={repo.exclude_types_override}
|
||||
isLegacyRepo={repo.legacyImported}
|
||||
bind:excludes={repo.settings.exclude_path}
|
||||
bind:extraIncludes={repo.settings.extra_include_path}
|
||||
isInitialSetup={false}
|
||||
requiresMigration={repo.legacyImported}
|
||||
useIndividualBranch={repo.use_individual_branch}
|
||||
>
|
||||
{#snippet actions()}
|
||||
<Button
|
||||
size="md"
|
||||
onclick={handlePullSettings}
|
||||
startIcon={{ icon: Download }}
|
||||
>
|
||||
Pull settings
|
||||
</Button>
|
||||
{/snippet}
|
||||
</GitSyncFilterSettings>
|
||||
|
||||
{#if !repo.isUnsavedConnection}
|
||||
<div class="flex justify-between items-start">
|
||||
<!-- Display mode settings as prominent text -->
|
||||
<div class="flex-1 mr-4">
|
||||
<GitSyncModeDisplay
|
||||
mode={variant === 'primary-promotion' || variant === 'legacy' ? 'promotion' : 'sync'}
|
||||
{targetBranch}
|
||||
repository={repo}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Manual sync section for existing repos -->
|
||||
{#if !emptyString(repo.git_repo_resource_path) && !repo.legacyImported}
|
||||
<div class="flex flex-col">
|
||||
<div class="text-sm text-secondary mb-2">Manual workspace content sync</div>
|
||||
<div class="flex gap-2">
|
||||
<Button
|
||||
size="xs"
|
||||
color="dark"
|
||||
variant="border"
|
||||
onclick={() => idx !== null && gitSyncContext.showPullModal(idx)}
|
||||
startIcon={{ icon: Download }}
|
||||
>
|
||||
Pull from repo
|
||||
</Button>
|
||||
<Button
|
||||
size="xs"
|
||||
color="dark"
|
||||
variant="border"
|
||||
onclick={() => idx !== null && gitSyncContext.showPushModal(idx)}
|
||||
startIcon={{ icon: Upload }}
|
||||
>
|
||||
Push to repo
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
{:else}
|
||||
<div class="text-xs text-tertiary">Please select a Git repository resource.</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/snippet}
|
||||
|
||||
<!-- Main component rendering -->
|
||||
|
||||
{#if shouldShowEmptyState}
|
||||
<!-- Empty State for Primary Variants -->
|
||||
<div class="rounded-lg border bg-surface p-4 mb-4">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<div class="flex flex-col">
|
||||
<h3 class="text-xl font-semibold">{displayTitle}</h3>
|
||||
{#if displayDescription}
|
||||
<p class="text-sm text-secondary">{displayDescription}</p>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{#if !repo.collapsed}
|
||||
<div class="px-4 py-2">
|
||||
<div class="flex mt-5 mb-1 gap-1">
|
||||
{#key repo}
|
||||
<div class="pt-1 font-semibold">Resource: </div>
|
||||
<ResourcePicker
|
||||
bind:value={repo.git_repo_resource_path}
|
||||
resourceType={'git_repository'}
|
||||
disabled={!repo.isUnsavedConnection}
|
||||
excludedValues={usedRepositoryPaths}
|
||||
/>
|
||||
{#if !emptyString(repo.git_repo_resource_path)}
|
||||
<Button
|
||||
disabled={emptyString(repo.script_path)}
|
||||
color="dark"
|
||||
onclick={runGitSyncTestJob}
|
||||
size="xs">Test connection</Button
|
||||
>
|
||||
{/if}
|
||||
{/key}
|
||||
</div>
|
||||
|
||||
{#if !emptyString(repo.git_repo_resource_path)}
|
||||
<div class="flex text-normal text-2xs gap-1">
|
||||
{#if validation.isDuplicate}
|
||||
<span class="text-red-600">This resource is already used by another repository.</span>
|
||||
{/if}
|
||||
{#if gitSyncTestJob && gitSyncTestJob.status !== undefined}
|
||||
{#if gitSyncTestJob.status === 'running'}
|
||||
<RotateCw size={14} class="animate-spin" />
|
||||
{:else if gitSyncTestJob.status === 'success'}
|
||||
<CheckCircle2 size={14} class="text-green-600" />
|
||||
{:else}
|
||||
<XCircle size={14} class="text-red-700" />
|
||||
{/if}
|
||||
Git sync resource checked via Windmill job
|
||||
<a
|
||||
target="_blank"
|
||||
href={`/run/${gitSyncTestJob.jobId}?workspace=${$workspaceStore}`}
|
||||
>
|
||||
{gitSyncTestJob.jobId}
|
||||
</a>WARNING: Only read permissions are verified.
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if repo.legacyImported}
|
||||
<Alert type="warning" title="Legacy git sync settings imported">
|
||||
This repository was initialized from workspace-level legacy Git-Sync settings. Review the filters and press <b>Save</b> to migrate.
|
||||
</Alert>
|
||||
<div class="flex flex-col items-center justify-center py-8 px-4 border-2 border-dashed rounded-md">
|
||||
<div class="text-center mb-4">
|
||||
<p class="text-secondary mb-2">
|
||||
{#if mode === 'sync'}
|
||||
No sync repository configured. Add one to enable direct synchronization.
|
||||
{:else if mode === 'promotion'}
|
||||
No promotion repository configured. Add one to enable branch-based workflows.
|
||||
{:else}
|
||||
No repository configured.
|
||||
{/if}
|
||||
<div class="flex flex-col mb-1 gap-4">
|
||||
{#if repo}
|
||||
{#if repo.script_path != hubPaths.gitSync}
|
||||
<Alert type="warning" title="Script version mismatch">
|
||||
The git sync version for this repository is not latest. Current: <a
|
||||
target="_blank"
|
||||
href="https://hub.windmill.dev/scripts/windmill/6943/sync-script-to-git-repo-windmill/9014/versions"
|
||||
>{repo.script_path}</a
|
||||
>, latest:
|
||||
<a
|
||||
target="_blank"
|
||||
href="https://hub.windmill.dev/scripts/windmill/6943/sync-script-to-git-repo-windmill/9014/versions"
|
||||
>{hubPaths.gitSync}</a
|
||||
>
|
||||
<div class="flex mt-2">
|
||||
<Button
|
||||
size="xs"
|
||||
color="dark"
|
||||
onclick={() => {
|
||||
if (repo) {
|
||||
repo.script_path = hubPaths.gitSync
|
||||
}
|
||||
}}
|
||||
>Update git sync script (require save git settings to be applied)</Button
|
||||
>
|
||||
</div>
|
||||
</Alert>
|
||||
{/if}
|
||||
{#if repo.isUnsavedConnection && !emptyString(repo.git_repo_resource_path)}
|
||||
<!-- Use DetectionFlow component -->
|
||||
<div class="mt-4">
|
||||
<DetectionFlow {idx} />
|
||||
</div>
|
||||
{:else}
|
||||
<!-- Existing saved connection flow -->
|
||||
<GitSyncFilterSettings
|
||||
bind:git_repo_resource_path={repo.git_repo_resource_path}
|
||||
bind:include_path={repo.settings.include_path}
|
||||
bind:include_type={repo.settings.include_type}
|
||||
bind:exclude_types_override={repo.exclude_types_override}
|
||||
isLegacyRepo={repo.legacyImported}
|
||||
bind:excludes={repo.settings.exclude_path}
|
||||
bind:extraIncludes={repo.settings.extra_include_path}
|
||||
isInitialSetup={false}
|
||||
requiresMigration={repo.legacyImported}
|
||||
useIndividualBranch={repo.use_individual_branch}
|
||||
>
|
||||
{#snippet actions()}
|
||||
<Button
|
||||
size="md"
|
||||
onclick={handlePullSettings}
|
||||
startIcon={{ icon: Download }}
|
||||
>
|
||||
Pull settings
|
||||
</Button>
|
||||
{/snippet}
|
||||
</GitSyncFilterSettings>
|
||||
{/if}
|
||||
{#if !repo.isUnsavedConnection}
|
||||
<div class="flex justify-between items-start">
|
||||
<!-- Display mode settings as prominent text -->
|
||||
<div class="text-base flex-1 mr-4">
|
||||
{#if repo.use_individual_branch}
|
||||
<div><span class="font-bold">Promotion:</span> Creating branches whose promotion target is main</div>
|
||||
{#if repo.group_by_folder}
|
||||
<div class="text-sm text-tertiary mt-1">Grouped by folder</div>
|
||||
{/if}
|
||||
{:else}
|
||||
<div>Sync: <span class="font-bold">Syncing back to branch main</span></div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Manual sync section for existing repos -->
|
||||
{#if !emptyString(repo.git_repo_resource_path) && !repo.legacyImported}
|
||||
<div class="flex flex-col">
|
||||
<div class="text-sm text-secondary mb-2">Manual workspace content sync</div>
|
||||
<div class="flex gap-2">
|
||||
<Button
|
||||
size="xs"
|
||||
color="dark"
|
||||
variant="border"
|
||||
onclick={() => gitSyncContext.showPullModal(idx)}
|
||||
startIcon={{ icon: Download }}
|
||||
>
|
||||
Pull from repo
|
||||
</Button>
|
||||
<Button
|
||||
size="xs"
|
||||
color="dark"
|
||||
variant="border"
|
||||
onclick={() => gitSyncContext.showPushModal(idx)}
|
||||
startIcon={{ icon: Upload }}
|
||||
>
|
||||
Push to repo
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{/if}
|
||||
|
||||
{/if}
|
||||
</div>
|
||||
{:else}
|
||||
<div class="text-xs text-tertiary pt-1 pl-8">Please select a Git repository resource.</div>
|
||||
{/if}
|
||||
</p>
|
||||
</div>
|
||||
{/if}
|
||||
{#if onAdd}
|
||||
<Button
|
||||
size="md"
|
||||
color="dark"
|
||||
variant="border"
|
||||
startIcon={{ icon: Plus }}
|
||||
onclick={onAdd}
|
||||
>
|
||||
Add {mode || 'repository'} repository
|
||||
</Button>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
{:else if repo}
|
||||
{#if variant === 'primary-sync' || variant === 'primary-promotion'}
|
||||
<!-- Primary Repository Layout -->
|
||||
<div class="rounded-lg border bg-surface p-4 mb-4">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<div class="flex flex-col">
|
||||
<h3 class="text-xl font-semibold">{displayTitle}</h3>
|
||||
{#if displayDescription}
|
||||
<p class="text-sm text-secondary">{displayDescription}</p>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
{@render headerActions()}
|
||||
</div>
|
||||
</div>
|
||||
{@render repositoryContent()}
|
||||
</div>
|
||||
{:else}
|
||||
<!-- Standard Repository Card Layout -->
|
||||
<div class="rounded-lg shadow-sm border p-0 w-full mb-4">
|
||||
<div class="flex items-center justify-between min-h-10 px-4 py-1 border-b">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-lg font-semibold">{displayTitle}</span>
|
||||
{#if repo.legacyImported}
|
||||
<span class="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-orange-100 text-orange-800 border border-orange-200">
|
||||
Legacy Configuration
|
||||
</span>
|
||||
{/if}
|
||||
<span class="text-xs text-tertiary pt-1 pl-8">
|
||||
{repo.git_repo_resource_path}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
{@render headerActions()}
|
||||
</div>
|
||||
</div>
|
||||
{#if !repo.collapsed}
|
||||
<div class="px-4 py-2">
|
||||
{@render repositoryContent()}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
@@ -1,9 +1,9 @@
|
||||
<script lang="ts">
|
||||
import { Plus, ExternalLink } from 'lucide-svelte'
|
||||
import { ExternalLink, ChevronDown, ChevronRight, Plus } from 'lucide-svelte'
|
||||
import { Button, Alert } from '$lib/components/common'
|
||||
import Description from '$lib/components/Description.svelte'
|
||||
import { setGitSyncContext } from './GitSyncContext.svelte'
|
||||
import GitSyncRepositoryList from './GitSyncRepositoryList.svelte'
|
||||
import GitSyncRepositoryCard from './GitSyncRepositoryCard.svelte'
|
||||
import GitSyncModalManager from './GitSyncModalManager.svelte'
|
||||
import { enterpriseLicense, workspaceStore } from '$lib/stores'
|
||||
import { sendUserToast } from '$lib/toast'
|
||||
@@ -27,6 +27,19 @@
|
||||
}
|
||||
})
|
||||
|
||||
// Derived state for repository categorization
|
||||
const primarySync = $derived(gitSyncContext?.getPrimarySyncRepository() || null)
|
||||
const primaryPromotion = $derived(gitSyncContext?.getPrimaryPromotionRepository() || null)
|
||||
const secondarySync = $derived(gitSyncContext?.getSecondarySyncRepositories() || [])
|
||||
const legacyPromotion = $derived(gitSyncContext?.getLegacyPromotionRepositories() || [])
|
||||
|
||||
// State for collapsible sections
|
||||
let secondarySyncExpanded = $state(false)
|
||||
let legacyPromotionExpanded = $state(false)
|
||||
|
||||
// Check if any secondary repositories are unsaved
|
||||
const hasUnsavedSecondary = $derived(secondarySync.some(s => s.repo.isUnsavedConnection))
|
||||
|
||||
</script>
|
||||
|
||||
{#if !gitSyncContext}
|
||||
@@ -72,19 +85,129 @@
|
||||
</div>
|
||||
<div class="pt-2"></div>
|
||||
|
||||
<!-- Repository list -->
|
||||
<GitSyncRepositoryList />
|
||||
<!-- Primary Sync Repository -->
|
||||
<div class="space-y-4">
|
||||
<GitSyncRepositoryCard
|
||||
variant="primary-sync"
|
||||
mode="sync"
|
||||
idx={primarySync?.idx ?? null}
|
||||
repository={primarySync?.repo ?? null}
|
||||
onAdd={() => gitSyncContext.addSyncRepository()}
|
||||
isCollapsible={false}
|
||||
showEmptyState={primarySync?.repo === null}
|
||||
/>
|
||||
|
||||
<!-- Add repository button -->
|
||||
<div class="flex mt-5 mb-5">
|
||||
<Button
|
||||
startIcon={{ icon: Plus }}
|
||||
color="dark"
|
||||
variant="border"
|
||||
onclick={() => gitSyncContext.addRepository()}
|
||||
>
|
||||
Add connection
|
||||
</Button>
|
||||
<!-- Secondary Sync Repositories -->
|
||||
{#if primarySync && !primarySync.repo?.isUnsavedConnection}
|
||||
{#if secondarySync.length > 0 || secondarySyncExpanded}
|
||||
<div class="mt-4">
|
||||
<button
|
||||
class="flex items-center gap-2 text-sm text-secondary hover:text-primary transition-colors"
|
||||
onclick={() => (secondarySyncExpanded = !secondarySyncExpanded)}
|
||||
>
|
||||
{#if secondarySyncExpanded}
|
||||
<ChevronDown size={16} />
|
||||
{:else}
|
||||
<ChevronRight size={16} />
|
||||
{/if}
|
||||
Secondary sync repositories ({secondarySync.length})
|
||||
</button>
|
||||
|
||||
{#if secondarySyncExpanded}
|
||||
<div class="mt-3 space-y-3">
|
||||
{#if secondarySync.length === 0}
|
||||
<div class="text-sm text-secondary italic">
|
||||
No secondary sync repositories configured
|
||||
</div>
|
||||
{:else}
|
||||
{#each secondarySync as { repo, idx } (repo.git_repo_resource_path)}
|
||||
<div class="pl-4">
|
||||
<GitSyncRepositoryCard
|
||||
variant="secondary"
|
||||
{idx}
|
||||
isSecondary={true}
|
||||
/>
|
||||
</div>
|
||||
{/each}
|
||||
{/if}
|
||||
|
||||
{#if !hasUnsavedSecondary}
|
||||
<div class="pl-4">
|
||||
<Button
|
||||
size="xs"
|
||||
color="light"
|
||||
variant="border"
|
||||
startIcon={{ icon: Plus }}
|
||||
onclick={() => gitSyncContext.addSyncRepository()}
|
||||
>
|
||||
Add secondary sync
|
||||
</Button>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{:else}
|
||||
<!-- Collapsed state when no secondary repos exist -->
|
||||
{#if !hasUnsavedSecondary}
|
||||
<div class="mt-2">
|
||||
<button
|
||||
class="text-xs text-tertiary hover:text-secondary transition-colors"
|
||||
onclick={() => {
|
||||
secondarySyncExpanded = true
|
||||
gitSyncContext.addSyncRepository()
|
||||
}}
|
||||
>
|
||||
+ Add secondary sync repository
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
<!-- Primary Promotion Repository -->
|
||||
<div class="mt-6">
|
||||
<GitSyncRepositoryCard
|
||||
variant="primary-promotion"
|
||||
mode="promotion"
|
||||
idx={primaryPromotion?.idx ?? null}
|
||||
repository={primaryPromotion?.repo ?? null}
|
||||
onAdd={() => gitSyncContext.addPromotionRepository()}
|
||||
isCollapsible={false}
|
||||
showEmptyState={primaryPromotion?.repo === null}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Legacy promotion repositories (backwards compatibility) -->
|
||||
{#if legacyPromotion.length > 0}
|
||||
<Alert type="warning" title="Multiple promotion repositories detected">
|
||||
Multiple promotion repositories are no longer supported. Please reduce to a single promotion repository.
|
||||
Only deletion is allowed for the additional repositories below.
|
||||
</Alert>
|
||||
<div class="mt-4">
|
||||
<button
|
||||
class="flex items-center gap-2 text-sm text-secondary hover:text-primary transition-colors"
|
||||
onclick={() => (legacyPromotionExpanded = !legacyPromotionExpanded)}
|
||||
>
|
||||
{#if legacyPromotionExpanded}
|
||||
<ChevronDown size={16} />
|
||||
{:else}
|
||||
<ChevronRight size={16} />
|
||||
{/if}
|
||||
Legacy promotion repositories ({legacyPromotion.length})
|
||||
</button>
|
||||
|
||||
{#if legacyPromotionExpanded}
|
||||
<div class="space-y-3 mt-3">
|
||||
{#each legacyPromotion as { repo, idx } (repo.git_repo_resource_path)}
|
||||
<div class="pl-4">
|
||||
<GitSyncRepositoryCard {idx} variant="legacy" isLegacy={true} />
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Modals -->
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
import { sendUserToast } from '$lib/toast'
|
||||
import hubPaths from '$lib/hubPaths.json'
|
||||
import { tryEvery } from '$lib/utils'
|
||||
import { jobManager } from '$lib/services/JobManager'
|
||||
import type { SyncResponse, SettingsResponse, SettingsObject } from '$lib/git-sync'
|
||||
|
||||
interface Props {
|
||||
@@ -163,50 +163,46 @@
|
||||
applyJobStatus = 'running'
|
||||
}
|
||||
|
||||
let jobSuccess = false
|
||||
let result: any = {}
|
||||
// Use JobManager instead of tryEvery
|
||||
const result = await jobManager.runWithProgress(
|
||||
() => Promise.resolve(jobId),
|
||||
{
|
||||
workspace,
|
||||
timeout: 60000,
|
||||
timeoutMessage: `${isPreview ? 'Preview' : 'Apply'} job timed out after 60s`,
|
||||
onProgress: (status) => {
|
||||
if (isPreview) {
|
||||
previewJobStatus = status.status
|
||||
} else {
|
||||
applyJobStatus = status.status
|
||||
}
|
||||
|
||||
await tryEvery({
|
||||
tryCode: async () => {
|
||||
const testResult = await JobService.getCompletedJob({ workspace, id: jobId })
|
||||
jobSuccess = !!testResult.success
|
||||
if (jobSuccess) {
|
||||
const jobResult = await JobService.getCompletedJobResult({ workspace, id: jobId })
|
||||
result = jobResult
|
||||
// Handle failure status
|
||||
if (status.status === 'failure') {
|
||||
if (isPreview) {
|
||||
previewError = status.error || 'Preview failed'
|
||||
} else {
|
||||
applyError = status.error || 'Pull failed'
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
timeoutCode: async () => {
|
||||
try {
|
||||
await JobService.cancelQueuedJob({
|
||||
workspace,
|
||||
id: jobId,
|
||||
requestBody: { reason: `${isPreview ? 'Preview' : 'Apply'} job timed out after 60s` }
|
||||
})
|
||||
} catch (err) {}
|
||||
},
|
||||
interval: 500,
|
||||
timeout: 60000
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
// Handle successful result
|
||||
if (isPreview) {
|
||||
previewJobStatus = jobSuccess ? 'success' : 'failure'
|
||||
if (jobSuccess) {
|
||||
previewResult = result
|
||||
} else {
|
||||
previewError = 'Preview failed'
|
||||
if (previewJobStatus === 'success') {
|
||||
previewResult = result as SyncResponse | SettingsResponse
|
||||
}
|
||||
} else {
|
||||
applyJobStatus = jobSuccess ? 'success' : 'failure'
|
||||
if (jobSuccess) {
|
||||
const settingsData = result?.local
|
||||
if (applyJobStatus === 'success') {
|
||||
const settingsData = (result as any)?.local
|
||||
const hasSettingsChanges = settingsData && onFilterUpdate
|
||||
if (hasSettingsChanges) {
|
||||
onFilterUpdate(settingsData)
|
||||
await saveUpdatedSettings()
|
||||
}
|
||||
onSuccess?.()
|
||||
} else {
|
||||
applyError = 'Pull failed'
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
import { JobService } from '$lib/gen'
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
import hubPaths from '$lib/hubPaths.json'
|
||||
import { tryEvery } from '$lib/utils'
|
||||
import { jobManager } from '$lib/services/JobManager'
|
||||
import type { SyncResponse, SettingsObject } from '$lib/git-sync'
|
||||
|
||||
interface Props {
|
||||
@@ -105,44 +105,40 @@
|
||||
applyJobStatus = 'running'
|
||||
}
|
||||
|
||||
let jobSuccess = false
|
||||
let result: any = {}
|
||||
// Use JobManager instead of tryEvery
|
||||
const result = await jobManager.runWithProgress(
|
||||
() => Promise.resolve(jobId),
|
||||
{
|
||||
workspace,
|
||||
timeout: 60000,
|
||||
timeoutMessage: `${isPreview ? 'Preview' : 'Apply'} job timed out after 60s`,
|
||||
onProgress: (status) => {
|
||||
if (isPreview) {
|
||||
previewJobStatus = status.status
|
||||
} else {
|
||||
applyJobStatus = status.status
|
||||
}
|
||||
|
||||
await tryEvery({
|
||||
tryCode: async () => {
|
||||
const testResult = await JobService.getCompletedJob({ workspace, id: jobId })
|
||||
jobSuccess = !!testResult.success
|
||||
if (jobSuccess) {
|
||||
const jobResult = await JobService.getCompletedJobResult({ workspace, id: jobId })
|
||||
result = jobResult
|
||||
// Handle failure status
|
||||
if (status.status === 'failure') {
|
||||
if (isPreview) {
|
||||
previewError = status.error || 'Preview failed'
|
||||
} else {
|
||||
applyError = status.error || 'Push failed'
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
timeoutCode: async () => {
|
||||
try {
|
||||
await JobService.cancelQueuedJob({
|
||||
workspace,
|
||||
id: jobId,
|
||||
requestBody: { reason: `${isPreview ? 'Preview' : 'Apply'} job timed out after 60s` }
|
||||
})
|
||||
} catch (err) {}
|
||||
},
|
||||
interval: 500,
|
||||
timeout: 60000
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
// Handle successful result
|
||||
if (isPreview) {
|
||||
previewJobStatus = jobSuccess ? 'success' : 'failure'
|
||||
if (jobSuccess) {
|
||||
previewResult = result
|
||||
} else {
|
||||
previewError = 'Preview failed'
|
||||
if (previewJobStatus === 'success') {
|
||||
previewResult = result as SyncResponse
|
||||
}
|
||||
} else {
|
||||
applyJobStatus = jobSuccess ? 'success' : 'failure'
|
||||
if (jobSuccess) {
|
||||
if (applyJobStatus === 'success') {
|
||||
onSuccess?.()
|
||||
} else {
|
||||
applyError = 'Push failed'
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
|
||||
@@ -84,10 +84,15 @@ export class JobManager {
|
||||
throw new Error('Job was cancelled')
|
||||
}
|
||||
|
||||
const jobResult = await JobService.getCompletedJob({
|
||||
workspace,
|
||||
id: jobId
|
||||
})
|
||||
let jobResult
|
||||
try {
|
||||
jobResult = await JobService.getCompletedJob({
|
||||
workspace,
|
||||
id: jobId
|
||||
})
|
||||
} catch (error) {
|
||||
throw error
|
||||
}
|
||||
|
||||
const success = !!jobResult.success
|
||||
const status: JobStatus = {
|
||||
@@ -98,10 +103,6 @@ export class JobManager {
|
||||
|
||||
onProgress?.(status)
|
||||
|
||||
if (!success) {
|
||||
throw new Error(status.error)
|
||||
}
|
||||
|
||||
return jobResult.result as T
|
||||
},
|
||||
timeoutCode: async () => {
|
||||
|
||||
Reference in New Issue
Block a user