feat(git): add environment timeline visualization and branch commit APIs

Implement a new Git environment timeline component to visualize dashboard
versions across different deployment stages (Development, Pre-production,
and Production). This includes new backend endpoints and frontend
services to support historical data retrieval and version comparison.

- Add `get_branch_commits` and `get_commit_diff` endpoints to backend
  API and Git service.
- Implement `GitEnvironmentTimeline` Svelte component for visual
  representation of deployment history.
- Update `GitManagerModel` to manage timeline state, including
  environment histories and version selection for comparison.
- Add `getBranchCommits` and `getCommitDiff` methods to `gitService`.
- Improve UX by separating the branch selector from the version map.
- Add comprehensive i18n support for the new visualization features.
This commit is contained in:
2026-07-10 10:55:10 +03:00
parent db07bbb1d5
commit 3404967f76
12 changed files with 947 additions and 12 deletions

View File

@@ -27,10 +27,10 @@
import { onMount } from 'svelte';
import { t } from '$lib/i18n/index.svelte.js';
import { Button, HelpTooltip, Icon } from '$lib/ui';
import { addToast } from '$lib/toasts.svelte.js';
import { resolveDefaultConfig } from '../../../services/git-utils.js';
import { GitManagerModel } from '$lib/models/GitManagerModel.svelte.ts';
import { ROUTES } from '$lib/routes';
import BranchSelector from './BranchSelector.svelte';
import DeploymentModal from './DeploymentModal.svelte';
import ConflictResolver from './ConflictResolver.svelte';
import GitInitPanel from './GitInitPanel.svelte';
@@ -41,6 +41,8 @@
import GitLifecycleHeader from './GitLifecycleHeader.svelte';
import CreateBranchDialog from './CreateBranchDialog.svelte';
import MergeDialog from './MergeDialog.svelte';
import GitEnvironmentTimeline from './GitEnvironmentTimeline.svelte';
import BranchSelector from './BranchSelector.svelte';
let { dashboardId, envId = null, dashboardTitle = '', show = $bindable(false) } = $props();
@@ -249,14 +251,49 @@
preferredDeployTargetStage={model.preferredDeployTargetStage}
onRecommendedAction={handleRecommendedAction}
/>
<div class="flex flex-wrap items-center justify-between gap-3 rounded-lg border border-border bg-surface-page px-4 py-3">
<div class="text-sm font-medium text-text">{$t.git?.branch_label || 'Ветка:'}</div>
<div class="flex w-full items-center gap-1 sm:w-72"><BranchSelector {dashboardId} envId={model.resolvedEnvId} bind:currentBranch={model.currentBranch} onchange={(event) => model.handleBranchChanged(event)} onmerge={handleOpenMergeDialog} /><HelpTooltip text={$t.git?.hint_branch_selector || ''} ariaLabel={$t.git?.help_aria || 'Справка'} /></div>
<!-- Branch selector moved here per UX review: separate from version map to avoid confusion -->
<div class="flex items-center gap-3 mb-2">
<div class="text-xs font-medium text-text-muted uppercase tracking-wide">Рабочая ветка (текущие изменения):</div>
<div class="flex-1 max-w-[280px]">
<BranchSelector
{dashboardId}
envId={model.resolvedEnvId}
bind:currentBranch={model.currentBranch}
onchange={(event) => model.handleBranchChanged(event)}
onmerge={handleOpenMergeDialog}
/>
</div>
</div>
<!-- BI dashboard version timeline: focused on status map. Operations below in tabs. -->
<GitEnvironmentTimeline
branches={model.branches}
environmentHistories={model.environmentHistories}
historiesLoading={model.environmentHistoriesLoading}
{dashboardId}
envId={model.resolvedEnvId}
bind:currentBranch={model.currentBranch}
onBranchChange={(event) => model.handleBranchChanged(event)}
onMerge={handleOpenMergeDialog}
selectedA={model.selectedVersionA}
selectedB={model.selectedVersionB}
onSelectVersion={(hash, secondary) => model.selectVersion(hash, secondary)}
onClearSelection={() => model.clearVersionSelection()}
onRequestDiff={async () => {
const res = await model.getSelectedVersionsDiff();
if (res?.diff) {
model.workspaceDiff = res.diff;
model.activeTab = 'workspace';
addToast($t.git?.viz_diff_ready || 'Dashboard changes loaded for comparison', 'success');
}
}}
/>
<div class="flex flex-wrap items-center gap-1 border-b border-border pb-0" role="tablist" aria-label={$t.git?.management || 'Управление Git'}>
<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')} role="tab" aria-selected={model.activeTab === 'workspace'} aria-controls="git-tab-panel" id="git-tab-workspace" tabindex={model.activeTab === 'workspace' ? 0 : -1}>
<Icon name="edit" size={16} strokeWidth={2} />
{$t.git?.tab_workspace || 'Фиксация изменений'}
{$t.git?.tab_workspace || 'Изменения'}
<HelpTooltip text={$t.git?.hint_workspace || ''} ariaLabel={$t.git?.help_aria || 'Справка'} />
</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')} role="tab" aria-selected={model.activeTab === 'release'} aria-controls="git-tab-panel" id="git-tab-release" tabindex={model.activeTab === 'release' ? 0 : -1}>