refactor(frontend): migrate legacy src/components/ → /components/, remove console.count from Sidebar
This commit is contained in:
193
frontend/src/lib/components/git/GitManager.svelte
Normal file
193
frontend/src/lib/components/git/GitManager.svelte
Normal file
@@ -0,0 +1,193 @@
|
||||
<!-- #region GitManager [C:4] [TYPE Component] [SEMANTICS git, manager, workflow, version-control, promote] -->
|
||||
<!-- @BRIEF Central Git management UI — thin shell delegating to sub-panels and composable handlers. -->
|
||||
<!-- @LAYER UI -->
|
||||
<!-- @RELATION USES -> [GitInitPanel] -->
|
||||
<!-- @RELATION USES -> [GitWorkspacePanel] -->
|
||||
<!-- @RELATION USES -> [GitReleasePanel] -->
|
||||
<!-- @RELATION USES -> [GitOperationsPanel] -->
|
||||
<!-- @RELATION USES -> [GitMergeDialog] -->
|
||||
<!-- @RELATION USES -> [EXT:frontend:BranchSelector] -->
|
||||
<!-- @RELATION USES -> [EXT:frontend:DeploymentModal] -->
|
||||
<!-- @RELATION USES -> [EXT:frontend:ConflictResolver] -->
|
||||
<!-- @RELATION BINDS_TO -> [GitManagerModel] -->
|
||||
<!-- @INVARIANT This component contains NO business logic — only renders model state and calls model.action(). -->
|
||||
<!-- @RATIONALE Decomposed from 1220→~370 lines by extracting sub-panels, utils, and composable handlers per INV_7. -->
|
||||
<!-- @REJECTED Keeping all logic inline was rejected because it exceeded 150-line contract limit. -->
|
||||
<!-- @UX_STATE CheckingStatus -> Spinner. -->
|
||||
<!-- @UX_STATE Uninitialized -> GitInitPanel. -->
|
||||
<!-- @UX_STATE Initialized -> Tabbed workspace/release/operations. -->
|
||||
<!-- @UX_STATE MergeDialog -> Recovery overlay. -->
|
||||
<!-- @UX_STATE Error -> Red banner with error details and dismiss action. -->
|
||||
<!-- @UX_FEEDBACK Error banner shown in modal body, persistent until dismissed or next operation. -->
|
||||
<!-- @UX_FEEDBACK Toast is still shown for quick notification alongside the persistent banner. -->
|
||||
<!-- @UX_RECOVERY Dismiss error, Retry operation after fixing the issue. -->
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { t } from '$lib/i18n/index.svelte.js';
|
||||
import { HelpTooltip } from '$lib/ui';
|
||||
import { stageBadgeClass, resolveDefaultConfig } from '../../../services/git-utils.js';
|
||||
import { GitManagerModel } from '$lib/models/GitManagerModel.svelte.ts';
|
||||
import BranchSelector from './BranchSelector.svelte';
|
||||
import DeploymentModal from './DeploymentModal.svelte';
|
||||
import ConflictResolver from './ConflictResolver.svelte';
|
||||
import GitInitPanel from './GitInitPanel.svelte';
|
||||
import GitWorkspacePanel from './GitWorkspacePanel.svelte';
|
||||
import GitReleasePanel from './GitReleasePanel.svelte';
|
||||
import GitOperationsPanel from './GitOperationsPanel.svelte';
|
||||
import GitMergeDialog from './GitMergeDialog.svelte';
|
||||
|
||||
let { dashboardId, envId = null, dashboardTitle = '', show = $bindable(false) } = $props();
|
||||
|
||||
const model = new GitManagerModel({ dashboardId, envId, dashboardTitle });
|
||||
|
||||
$effect(() => {
|
||||
model.envId = envId;
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
model.repositoryConfigUrl = resolveDefaultConfig(model.configs, model.selectedConfigId)?.url || '';
|
||||
});
|
||||
|
||||
function closeModal() { show = false; model.clearGitError(); }
|
||||
function handleBackdropClick(e) { if (e.target === e.currentTarget) closeModal(); }
|
||||
|
||||
onMount(() => {
|
||||
model.initialize();
|
||||
});
|
||||
</script>
|
||||
|
||||
{#if show}
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
<div class="fixed inset-0 z-50 flex items-start justify-center overflow-y-auto bg-black/60 pt-6 backdrop-blur-sm" onclick={handleBackdropClick} onkeydown={(e) => { if (e.key === 'Escape') closeModal(); }} role="dialog" tabindex="-1" aria-label={`${$t.git?.management || 'Управление Git'}: ${dashboardTitle}`}>
|
||||
<div class="relative w-[95vw] max-w-[1600px] overflow-hidden rounded-xl bg-surface-card shadow-2xl" role="document" aria-label={`${$t.git?.management || 'Управление Git'}: ${dashboardTitle}`}>
|
||||
<!-- Header -->
|
||||
<div class="flex items-center justify-between border-b border-border bg-gradient-to-r from-slate-50 to-white px-6 py-4">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="flex h-10 w-10 items-center justify-center rounded-lg bg-primary text-white shadow-sm">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M10 20l4-16m4 4l4 4-4 4M6 16l-4-4 4-4"/></svg>
|
||||
</div>
|
||||
<div>
|
||||
<h2 class="text-lg font-bold text-text">{$t.git?.management || 'Управление Git'}</h2>
|
||||
<p class="text-sm text-text-muted">{dashboardTitle} <span class="text-text-subtle">·</span> slug: {dashboardId}</p>
|
||||
</div>
|
||||
</div>
|
||||
<button type="button" onclick={closeModal} class="flex h-9 w-9 items-center justify-center rounded-lg text-text-subtle transition-colors hover:bg-surface-muted hover:text-text" aria-label={$t.common?.close || 'Close'}>
|
||||
<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="M6 18L18 6M6 6l12 12" /></svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Git Error Banner -->
|
||||
{#if model.gitError}
|
||||
<div
|
||||
class="mx-6 mt-4 flex items-start gap-3 rounded-lg border p-4 text-sm shadow-sm {model.gitErrorType === 'warning' ? 'border-warning bg-warning-light text-amber-900' : 'border-destructive-ring bg-destructive-light text-destructive'}"
|
||||
role="alert"
|
||||
aria-live="polite"
|
||||
>
|
||||
<div class="mt-0.5 flex-shrink-0">
|
||||
{#if model.gitErrorType === 'warning'}
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-warning" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L3.34 16.5c-.77.833.192 2.5 1.732 2.5z"/></svg>
|
||||
{:else}
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-destructive" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><path stroke-linecap="round" stroke-linejoin="round" d="M15 9l-6 6m0-6l6 6"/></svg>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="flex-1 space-y-2">
|
||||
<p class="font-medium">{model.gitError.message || model.gitError}</p>
|
||||
{#if model.gitError.files?.length}
|
||||
<details class="text-xs text-destructive/80">
|
||||
<summary class="cursor-pointer font-medium">Файлы, которые будут перезаписаны ({model.gitError.files.length})</summary>
|
||||
<ul class="mt-1 list-disc space-y-0.5 pl-5">
|
||||
{#each model.gitError.files as file}
|
||||
<li><code class="rounded bg-destructive-light/50 px-1">{file}</code></li>
|
||||
{/each}
|
||||
</ul>
|
||||
</details>
|
||||
{/if}
|
||||
{#if model.gitError.next_steps?.length}
|
||||
<div class="text-xs">
|
||||
<span class="font-medium">Рекомендации:</span>
|
||||
<ol class="mt-1 list-decimal space-y-0.5 pl-5">
|
||||
{#each model.gitError.next_steps as step}
|
||||
<li>{step}</li>
|
||||
{/each}
|
||||
</ol>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onclick={() => model.clearGitError()}
|
||||
class="flex-shrink-0 rounded p-1 transition-colors {model.gitErrorType === 'warning' ? 'hover:bg-warning-light' : 'hover:bg-destructive-light'}"
|
||||
aria-label="Закрыть"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Body: height fills space, scroll is inside GitWorkspacePanel diff panel -->
|
||||
<div class="flex flex-col p-6" style="height: calc(92vh - 80px);">
|
||||
|
||||
{#if model.checkingStatus}
|
||||
<div class="flex justify-center py-12"><div class="h-8 w-8 animate-spin rounded-full border-b-2 border-primary-ring"></div></div>
|
||||
{:else if !model.initialized}
|
||||
<GitInitPanel configs={model.configs} bind:selectedConfigId={model.selectedConfigId} bind:remoteUrl={model.remoteUrl} creatingRemoteRepo={model.creatingRemoteRepo} loading={model.loading} onCreateRemoteRepo={() => model.handleCreateRemoteRepo()} onInit={() => model.handleInit()} />
|
||||
{:else}
|
||||
<div class="flex min-h-0 flex-1 flex-col gap-4">
|
||||
{#if model.hasOriginConfigMismatch}
|
||||
<div class="rounded-lg border border-warning bg-warning-light p-3 text-sm text-warning"><div class="font-semibold">Git server mismatch detected</div><div class="mt-1">Configured: <code>{model.configHost}</code>, origin: <code>{model.originHost}</code>.</div></div>
|
||||
{/if}
|
||||
<div class="flex flex-wrap items-center justify-between gap-3 rounded-lg border border-border bg-surface-card px-4 py-3 shadow-sm">
|
||||
<div class="flex items-center gap-3">
|
||||
<span class={`inline-flex items-center gap-1.5 rounded-full border px-3 py-1 text-xs font-bold uppercase tracking-wider ${stageBadgeClass(model.currentEnvStage)}`}>
|
||||
<span class="h-1.5 w-1.5 rounded-full bg-current opacity-60"></span>
|
||||
{model.currentEnvStage || 'DEV'}
|
||||
</span>
|
||||
<span class="hidden text-xs text-text-muted sm:inline">·</span>
|
||||
<span class="flex items-center gap-1.5 text-sm text-text-muted">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 text-text-subtle" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M10 20l4-16m4 4l4 4-4 4M6 16l-4-4 4-4"/></svg>
|
||||
<span class="hidden sm:inline">Ветка:</span> <strong class="font-mono text-text">{model.currentBranch}</strong>
|
||||
</span>
|
||||
{#if model.changedFilesCount > 0}
|
||||
<span class="inline-flex items-center gap-1 rounded-full bg-warning-light px-2.5 py-0.5 text-xs font-medium text-warning ring-1 ring-inset ring-warning-ring">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-3.5 w-3.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M15 12H9m12 0a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>
|
||||
{model.changedFilesCount} изменений
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="flex w-full items-center gap-1 sm:w-64"><BranchSelector {dashboardId} envId={model.resolvedEnvId} bind:currentBranch={model.currentBranch} /><HelpTooltip text={$t.git?.hint_branch_selector || ''} /></div>
|
||||
</div>
|
||||
<div class="flex flex-wrap items-center gap-1 border-b border-border pb-0">
|
||||
<button class={`relative -mb-px inline-flex items-center gap-2 rounded-t-lg px-4 py-2.5 text-sm font-medium transition-colors ${model.activeTab === 'workspace' ? 'border border-b-white bg-surface-card text-primary shadow-sm' : 'border border-transparent text-text-muted hover:bg-surface-muted hover:text-text'}`} onclick={() => (model.activeTab = 'workspace')}>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"/></svg>
|
||||
Фиксация изменений
|
||||
<HelpTooltip text={$t.git?.hint_workspace || ''} />
|
||||
</button>
|
||||
<button class={`relative -mb-px inline-flex items-center gap-2 rounded-t-lg px-4 py-2.5 text-sm font-medium transition-colors ${model.activeTab === 'release' ? 'border border-b-white bg-surface-card text-primary shadow-sm' : 'border border-transparent text-text-muted hover:bg-surface-muted hover:text-text'}`} onclick={() => (model.activeTab = 'release')}>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M13 10V3L4 14h7v7l9-11h-7z"/></svg>
|
||||
Релиз
|
||||
<HelpTooltip text={$t.git?.hint_release || ''} />
|
||||
</button>
|
||||
<button class={`relative -mb-px inline-flex items-center gap-2 rounded-t-lg px-4 py-2.5 text-sm font-medium transition-colors ${model.activeTab === 'operations' ? 'border border-b-white bg-surface-card text-primary shadow-sm' : 'border border-transparent text-text-muted hover:bg-surface-muted hover:text-text'}`} onclick={() => (model.activeTab = 'operations')}>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.066 2.573c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.573 1.066c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.066-2.573c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"/><path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/></svg>
|
||||
Серверные операции
|
||||
<HelpTooltip text={$t.git?.hint_operations || ''} />
|
||||
</button>
|
||||
</div>
|
||||
{#if model.activeTab === 'workspace'}
|
||||
<GitWorkspacePanel hasWorkspaceChanges={model.hasWorkspaceChanges} changedFilesCount={model.changedFilesCount} workspaceLoading={model.workspaceLoading} workspaceDiff={model.workspaceDiff} committing={model.committing} generatingMessage={model.generatingMessage} bind:commitMessage={model.commitMessage} bind:autoPushAfterCommit={model.autoPushAfterCommit} loading={model.loading} pushProviderLabel={model.pushProviderLabel} onSync={() => model.handleSync()} onGenerateMessage={() => model.handleGenerateMessage()} onCommit={() => model.handleCommit()} />
|
||||
{:else if model.activeTab === 'release'}
|
||||
<GitReleasePanel currentEnvStage={model.currentEnvStage} bind:promoteFromBranch={model.promoteFromBranch} bind:promoteToBranch={model.promoteToBranch} bind:promoteMode={model.promoteMode} bind:promoteReason={model.promoteReason} preferredDeployTargetStage={model.preferredDeployTargetStage} bind:showAdvancedPromote={model.showAdvancedPromote} promoting={model.promoting} onPromote={() => model.handlePromote()} />
|
||||
{:else}
|
||||
<GitOperationsPanel isPulling={model.isPulling} isPushing={model.isPushing} workspaceStatus={model.workspaceStatus} currentEnvStage={model.currentEnvStage} onPull={() => model.handlePull()} onPush={() => model.handlePush()} onDeploy={() => model.openDeployModal()} />
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<GitMergeDialog bind:show={model.showUnfinishedMergeDialog} unfinishedMergeContext={model.unfinishedMergeContext} mergeRecoveryLoading={model.mergeRecoveryLoading} mergeResolveInProgress={model.mergeResolveInProgress} mergeAbortInProgress={model.mergeAbortInProgress} mergeContinueInProgress={model.mergeContinueInProgress} copyingUnfinishedMergeCommands={model.copyingUnfinishedMergeCommands} onRefresh={() => model.loadMergeRecoveryState()} onCopyCommands={() => model.handleCopyUnfinishedMergeCommands()} onOpenConflictResolver={() => model.handleOpenConflictResolver()} onAbortMerge={() => model.handleAbortUnfinishedMerge()} onContinueMerge={() => model.handleContinueUnfinishedMerge()} onClose={() => model.closeUnfinishedMergeDialog()} />
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
<ConflictResolver conflicts={model.mergeConflicts} bind:show={model.showConflictResolver} onresolve={(e) => model.handleResolveConflicts(e)} />
|
||||
<DeploymentModal {dashboardId} envId={model.resolvedEnvId} preferredTargetStage={model.preferredDeployTargetStage} bind:show={model.showDeployModal} />
|
||||
<!-- #endregion GitManager -->
|
||||
Reference in New Issue
Block a user