fix(git): fix 17 missing async/await bugs + UX overhaul
Backend: - fix 17 missing 'await' in git route handlers causing silent no-ops (branches, diff, history, commit, push, pull, merge, promote, sync) - fix async coroutine passed to run_blocking in git_plugin.py Frontend: - add collapsible 'How it works' onboarding (GitHelpPanel) - add status legend with color-coded repository statuses - i18n: add 50+ missing keys, replace hardcoded strings - add Refresh button in modal header - add PROD deploy confirmation dialog (replaces browser prompt()) - add CommitHistory to workspace tab with timeline nodes - add post-commit success banner with next-step guidance - increase success toast duration to 8s - group local/remote branches in selector (optgroup) - format last_modified dates timezone-aware - change PROD badge from red to neutral indigo - extract shared resolveGitStatusToken to git-utils.ts - fix 'slug' label regression - remove dead init_repo_button key UI/UX audit fixes: - add descriptions to Create/Init buttons in init panel - add actionable CTA to server mismatch warning - improve checkbox text phrasing
This commit is contained in:
@@ -18,6 +18,8 @@
|
||||
import DashboardDataGrid from "./DashboardDataGrid.svelte";
|
||||
import GitManager from "$lib/components/git/GitManager.svelte";
|
||||
import { gitService } from "../../../services/gitService";
|
||||
import { resolveGitStatusToken } from "../../../services/git-utils";
|
||||
import { formatDateTime } from "$lib/utils/dateFormat";
|
||||
import { addToast } from "$lib/toasts.svelte.js";
|
||||
// [/SECTION]
|
||||
|
||||
@@ -48,7 +50,7 @@
|
||||
// ── Column definitions ─────────────────────────────────────────
|
||||
let columns: Column[] = $derived([
|
||||
{ key: "title", label: $t.dashboard?.title || "Title", sortable: true },
|
||||
{ key: "last_modified", label: $t.dashboard?.last_modified || "Last Modified", sortable: true },
|
||||
{ key: "last_modified", label: $t.dashboard?.last_modified || "Last Modified", sortable: true, render: (item: DashboardMetadata) => formatDateTime(item.last_modified) },
|
||||
{ key: "status", label: $t.dashboard?.status || "Status", sortable: true, raw: true, render: (item: DashboardMetadata) => renderStatusBadge(item) },
|
||||
]);
|
||||
|
||||
@@ -94,29 +96,9 @@
|
||||
// #endregion invalidateRepositoryStatuses:Function
|
||||
|
||||
// #region resolveRepositoryStatusToken:Function [TYPE Function]
|
||||
// @BRIEF Delegates to shared resolveGitStatusToken (git-utils) — single source of truth for grid + modal.
|
||||
function resolveRepositoryStatusToken(status: any): string {
|
||||
const syncState = String(status?.sync_state || "").toUpperCase();
|
||||
if (syncState === "DIVERGED") return "diverged";
|
||||
if (syncState === "BEHIND_REMOTE") return "behind_remote";
|
||||
if (syncState === "AHEAD_REMOTE") return "ahead_remote";
|
||||
if (syncState === "CHANGES") return "changes";
|
||||
if (syncState === "SYNCED") return "synced";
|
||||
const syncStatus = String(status?.sync_status || "").toUpperCase();
|
||||
if (syncStatus === "NO_REPO") return "no_repo";
|
||||
if (syncStatus === "ERROR") return "error";
|
||||
if (syncStatus === "DIFF") return "changes";
|
||||
if (syncStatus === "OK") return "synced";
|
||||
const aheadCount = Number(status?.ahead_count || 0);
|
||||
const behindCount = Number(status?.behind_count || 0);
|
||||
if (aheadCount > 0 && behindCount > 0) return "diverged";
|
||||
if (behindCount > 0) return "behind_remote";
|
||||
if (aheadCount > 0) return "ahead_remote";
|
||||
const hasChanges =
|
||||
Boolean(status?.is_dirty) ||
|
||||
(status?.untracked_files?.length || 0) > 0 ||
|
||||
(status?.modified_files?.length || 0) > 0 ||
|
||||
(status?.staged_files?.length || 0) > 0;
|
||||
return hasChanges ? "changes" : "synced";
|
||||
return resolveGitStatusToken(status);
|
||||
}
|
||||
// #endregion resolveRepositoryStatusToken:Function
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
import { onMount } from 'svelte';
|
||||
import { BranchModel } from '$lib/models/BranchModel.svelte.ts';
|
||||
import { t } from '$lib/i18n/index.svelte.js';
|
||||
import { Button, Select, Input } from '$lib/ui';
|
||||
import { Button, Input } from '$lib/ui';
|
||||
|
||||
let {
|
||||
dashboardId,
|
||||
@@ -29,6 +29,10 @@
|
||||
|
||||
const model = new BranchModel({ dashboardId, envId, currentBranch, onChange: onchange });
|
||||
|
||||
// Split branches into local (no origin/ prefix) and remote (origin/ prefix)
|
||||
let localBranches = $derived(model.branches.filter(b => !b.name.startsWith('origin/')));
|
||||
let remoteBranches = $derived(model.branches.filter(b => b.name.startsWith('origin/')));
|
||||
|
||||
// Sync prop changes to model
|
||||
$effect(() => {
|
||||
model.dashboardId = dashboardId;
|
||||
@@ -50,12 +54,27 @@
|
||||
<div class="space-y-3">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="flex-grow">
|
||||
<Select
|
||||
<select
|
||||
bind:value={model.currentBranch}
|
||||
onchange={(e) => model.handleSelect(e)}
|
||||
disabled={model.loading}
|
||||
options={model.branches.map(b => ({ value: b.name, label: b.name }))}
|
||||
/>
|
||||
class="flex h-10 w-full rounded-md border border-border-strong bg-surface-card px-3 py-2 text-sm text-text ring-offset-white focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
|
||||
>
|
||||
{#if localBranches.length > 0}
|
||||
<optgroup label={$t.git?.branch_group_local || 'Локальные'}>
|
||||
{#each localBranches as b}
|
||||
<option value={b.name}>{b.name}</option>
|
||||
{/each}
|
||||
</optgroup>
|
||||
{/if}
|
||||
{#if remoteBranches.length > 0}
|
||||
<optgroup label={$t.git?.branch_group_remote || 'Удалённые'}>
|
||||
{#each remoteBranches as b}
|
||||
<option value={b.name}>{b.name}</option>
|
||||
{/each}
|
||||
</optgroup>
|
||||
{/if}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
|
||||
@@ -87,14 +87,22 @@
|
||||
{:else if history.length === 0}
|
||||
<p class="text-text-muted italic text-center py-12">{$t.git.no_commits}</p>
|
||||
{:else}
|
||||
<div class="space-y-3 max-h-96 overflow-y-auto pr-2">
|
||||
{#each history as commit}
|
||||
<div class="border-l-2 border-primary-ring pl-4 py-1">
|
||||
<div class="flex justify-between items-start">
|
||||
<span class="font-medium text-sm">{commit.message}</span>
|
||||
<span class="text-xs text-text-subtle font-mono">{commit.hash.substring(0, 7)}</span>
|
||||
<div class="space-y-0 max-h-96 overflow-y-auto pr-2">
|
||||
{#each history as commit, i}
|
||||
<div class="relative pl-7 pb-4 last:pb-0">
|
||||
<!-- Timeline line -->
|
||||
{#if i < history.length - 1}
|
||||
<div class="absolute left-[7px] top-3 bottom-0 w-0.5 bg-primary-ring/30"></div>
|
||||
{/if}
|
||||
<!-- Node dot -->
|
||||
<div class="absolute left-0 top-1.5 flex h-3.5 w-3.5 items-center justify-center rounded-full border-2 border-primary-ring bg-surface-card">
|
||||
<div class="h-1.5 w-1.5 rounded-full bg-primary"></div>
|
||||
</div>
|
||||
<div class="text-xs text-text-muted mt-1">
|
||||
<div class="flex justify-between items-start">
|
||||
<span class="font-medium text-sm text-text">{commit.message}</span>
|
||||
<span class="text-xs text-text-subtle font-mono ml-2 shrink-0">{commit.hash.substring(0, 7)}</span>
|
||||
</div>
|
||||
<div class="text-xs text-text-muted mt-0.5">
|
||||
{commit.author} • {parseDateUTC(commit.timestamp).toLocaleString(undefined, { timeZone: appTimezone.current })}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
// [SECTION: IMPORTS]
|
||||
import { addToast as toast } from "$lib/toasts.svelte.js";
|
||||
import { log } from "$lib/cot-logger";
|
||||
import { t } from "$lib/i18n/index.svelte.js";
|
||||
// [/SECTION]
|
||||
|
||||
// [SECTION: PROPS]
|
||||
@@ -62,7 +63,7 @@
|
||||
if (unresolved.length > 0) {
|
||||
log("ConflictResolver", "EXPLORE", "Unresolved conflicts remain", { count: unresolved.length, files: unresolved.map(c => c.file_path) }, `${unresolved.length} unresolved conflicts`);
|
||||
toast(
|
||||
`Please resolve all conflicts first. (${unresolved.length} remaining)`,
|
||||
($t.git?.conflict?.unresolved_count || 'Please resolve all conflicts first. ({count} remaining)').replace('{count}', String(unresolved.length)),
|
||||
"error",
|
||||
);
|
||||
return;
|
||||
@@ -85,11 +86,10 @@
|
||||
class="bg-surface-card p-6 rounded-lg shadow-xl w-full max-w-5xl max-h-[90vh] flex flex-col"
|
||||
>
|
||||
<h2 class="text-xl font-bold mb-4 text-destructive">
|
||||
Merge Conflicts Detected
|
||||
{$t.git?.conflict?.title || 'Merge Conflicts Detected'}
|
||||
</h2>
|
||||
<p class="text-text-muted mb-4">
|
||||
The following files have conflicts. Please choose how to resolve
|
||||
them.
|
||||
{$t.git?.conflict?.description || 'The following files have conflicts. Please choose how to resolve them.'}
|
||||
</p>
|
||||
|
||||
<div class="flex-1 overflow-y-auto space-y-6 mb-4 pr-2">
|
||||
@@ -103,7 +103,7 @@
|
||||
<span
|
||||
class="text-xs bg-primary-light text-primary px-2 py-0.5 rounded-full uppercase font-bold"
|
||||
>
|
||||
Resolved: {resolutions[conflict.file_path]}
|
||||
{($t.git?.conflict?.resolved || 'Resolved: {strategy}').replace('{strategy}', resolutions[conflict.file_path])}
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -114,7 +114,7 @@
|
||||
<div
|
||||
class="bg-primary-light px-4 py-1 text-[10px] font-bold text-primary uppercase border-b"
|
||||
>
|
||||
Your Changes (Mine)
|
||||
{$t.git?.conflict?.your_changes || 'Your Changes (Mine)'}
|
||||
</div>
|
||||
<div class="p-4 bg-surface-card flex-1 overflow-auto">
|
||||
<pre
|
||||
@@ -129,14 +129,14 @@
|
||||
onclick={() =>
|
||||
resolve(conflict.file_path, "mine")}
|
||||
>
|
||||
Keep Mine
|
||||
{$t.git?.conflict?.keep_mine || 'Keep Mine'}
|
||||
</button>
|
||||
</div>
|
||||
<div class="p-0 flex flex-col">
|
||||
<div
|
||||
class="bg-success-light px-4 py-1 text-[10px] font-bold text-success uppercase border-b"
|
||||
>
|
||||
Remote Changes (Theirs)
|
||||
{$t.git?.conflict?.remote_changes || 'Remote Changes (Theirs)'}
|
||||
</div>
|
||||
<div class="p-4 bg-surface-card flex-1 overflow-auto">
|
||||
<pre
|
||||
@@ -151,7 +151,7 @@
|
||||
onclick={() =>
|
||||
resolve(conflict.file_path, "theirs")}
|
||||
>
|
||||
Keep Theirs
|
||||
{$t.git?.conflict?.keep_theirs || 'Keep Theirs'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -164,13 +164,13 @@
|
||||
onclick={() => (show = false)}
|
||||
class="px-4 py-2 text-text-muted hover:bg-surface-muted rounded transition-colors"
|
||||
>
|
||||
Cancel
|
||||
{$t.git?.conflict?.cancel || 'Cancel'}
|
||||
</button>
|
||||
<button
|
||||
onclick={handleSave}
|
||||
class="px-4 py-2 bg-primary text-white rounded hover:bg-primary-hover transition-colors shadow-sm"
|
||||
>
|
||||
Resolve & Continue
|
||||
{$t.git?.conflict?.resolve_continue || 'Resolve & Continue'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
66
frontend/src/lib/components/git/GitHelpPanel.svelte
Normal file
66
frontend/src/lib/components/git/GitHelpPanel.svelte
Normal file
@@ -0,0 +1,66 @@
|
||||
<!-- #region Git.HelpPanel [C:3] [TYPE Component] [SEMANTICS git,help,onboarding,instructions] -->
|
||||
<!-- @ingroup Git -->
|
||||
<!-- @BRIEF Collapsible "How it works" guide for the /git page — 5-step pipeline with icons, expand/collapse. -->
|
||||
<!-- @LAYER UI -->
|
||||
<!-- @UX_STATE Collapsed -> Only summary header visible, chevron points right. -->
|
||||
<!-- @UX_STATE Expanded -> All 5 steps visible with icons and descriptions, chevron points down. -->
|
||||
<!-- @UX_REACTIVITY LocalState -> $state(open). -->
|
||||
<!-- @RELATION USED_BY -> [frontend/src/routes/git/+page.svelte] -->
|
||||
<script lang="ts">
|
||||
import { t } from '$lib/i18n/index.svelte.js';
|
||||
|
||||
let open = $state(false);
|
||||
|
||||
const steps = $derived([
|
||||
{ icon: '⚙️', title: $t.git?.help_step1_title, desc: $t.git?.help_step1_desc },
|
||||
{ icon: '📋', title: $t.git?.help_step2_title, desc: $t.git?.help_step2_desc },
|
||||
{ icon: '🔧', title: $t.git?.help_step3_title, desc: $t.git?.help_step3_desc },
|
||||
{ icon: '📝', title: $t.git?.help_step4_title, desc: $t.git?.help_step4_desc },
|
||||
{ icon: '🚀', title: $t.git?.help_step5_title, desc: $t.git?.help_step5_desc },
|
||||
]);
|
||||
</script>
|
||||
|
||||
<div class="rounded-lg border border-border bg-surface-card mb-4">
|
||||
<button
|
||||
type="button"
|
||||
class="flex w-full items-center justify-between px-4 py-3 text-left transition-colors hover:bg-surface-muted"
|
||||
onclick={() => (open = !open)}
|
||||
aria-expanded={open}
|
||||
aria-controls="git-help-content"
|
||||
>
|
||||
<span class="flex items-center gap-2">
|
||||
<svg class="h-5 w-5 text-primary" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M8.228 9c.549-1.165 2.03-2 3.772-2 2.21 0 4 1.343 4 3 0 1.4-1.278 2.575-3.012 2.908M12 17v.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
<span class="text-sm font-semibold text-text">{$t.git?.help_title || 'How it works'}</span>
|
||||
<span class="text-xs text-text-muted hidden sm:inline">— {$t.git?.help_summary}</span>
|
||||
</span>
|
||||
<svg
|
||||
class="h-5 w-5 text-text-muted transition-transform {open ? 'rotate-180' : ''}"
|
||||
fill="none" stroke="currentColor" viewBox="0 0 24 24"
|
||||
>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
{#if open}
|
||||
<div id="git-help-content" class="border-t border-border px-4 py-4">
|
||||
<ol class="space-y-3">
|
||||
{#each steps as step, i}
|
||||
<li class="flex items-start gap-3">
|
||||
<span class="flex h-7 w-7 shrink-0 items-center justify-center rounded-full bg-primary-light text-primary text-sm font-semibold">
|
||||
{i + 1}
|
||||
</span>
|
||||
<span class="text-lg shrink-0">{step.icon}</span>
|
||||
<div class="min-w-0">
|
||||
<p class="text-sm font-medium text-text">{step.title}</p>
|
||||
<p class="text-sm text-text-muted">{step.desc}</p>
|
||||
</div>
|
||||
</li>
|
||||
{/each}
|
||||
</ol>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
<!-- #endregion Git.HelpPanel -->
|
||||
@@ -42,24 +42,30 @@
|
||||
bind:value={remoteUrl}
|
||||
placeholder={$t.git?.remote_url_placeholder}
|
||||
/>
|
||||
<Button
|
||||
variant="secondary"
|
||||
onclick={onCreateRemoteRepo}
|
||||
disabled={creatingRemoteRepo || configs.length === 0 || !selectedConfigId}
|
||||
isLoading={creatingRemoteRepo}
|
||||
class="w-full"
|
||||
>
|
||||
Create repo
|
||||
</Button>
|
||||
<div>
|
||||
<Button
|
||||
variant="secondary"
|
||||
onclick={onCreateRemoteRepo}
|
||||
disabled={creatingRemoteRepo || configs.length === 0 || !selectedConfigId}
|
||||
isLoading={creatingRemoteRepo}
|
||||
class="w-full"
|
||||
>
|
||||
{$t.git?.create_repo || 'Create repo'}
|
||||
</Button>
|
||||
<p class="mt-1.5 text-xs text-text-muted">{$t.git?.create_repo_desc}</p>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
onclick={onInit}
|
||||
disabled={loading || configs.length === 0 || creatingRemoteRepo}
|
||||
isLoading={loading}
|
||||
class="w-full"
|
||||
>
|
||||
{$t.git?.init_repo || 'Инициализировать Git-репозиторий'}
|
||||
</Button>
|
||||
<div>
|
||||
<Button
|
||||
onclick={onInit}
|
||||
disabled={loading || configs.length === 0 || creatingRemoteRepo}
|
||||
isLoading={loading}
|
||||
class="w-full"
|
||||
>
|
||||
{$t.git?.init_repo || 'Инициализировать Git-репозиторий'}
|
||||
</Button>
|
||||
<p class="mt-1.5 text-xs text-text-muted">{$t.git?.init_repo_desc}</p>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
@@ -39,6 +39,8 @@
|
||||
|
||||
let { dashboardId, envId = null, dashboardTitle = '', show = $bindable(false) } = $props();
|
||||
|
||||
let deployConfirmInput = $state('');
|
||||
|
||||
const model = new GitManagerModel({ dashboardId, envId, dashboardTitle });
|
||||
|
||||
$effect(() => {
|
||||
@@ -50,6 +52,10 @@
|
||||
});
|
||||
|
||||
function closeModal() { show = false; model.clearGitError(); }
|
||||
|
||||
$effect(() => {
|
||||
if (model.showDeployConfirm) deployConfirmInput = '';
|
||||
});
|
||||
function handleBackdropClick(e) { if (e.target === e.currentTarget) closeModal(); }
|
||||
|
||||
onMount(() => {
|
||||
@@ -71,9 +77,21 @@
|
||||
<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'}>
|
||||
<Icon name="close" size={20} strokeWidth={2} />
|
||||
</button>
|
||||
<div class="flex items-center gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onclick={() => model.refreshStatus()}
|
||||
disabled={model.checkingStatus || model.workspaceLoading}
|
||||
class="inline-flex items-center gap-1.5 rounded-lg px-3 py-1.5 text-xs font-medium text-text-muted transition-colors hover:bg-surface-muted hover:text-text disabled:opacity-50"
|
||||
aria-label={$t.common?.refresh || 'Refresh'}
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-3.5 w-3.5 {model.checkingStatus ? 'animate-spin' : ''}" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M16.023 9.348h4.992v-.001M2.985 19.644v-4.992m0 0h4.992m-4.993 0l3.181 3.183a8.25 8.25 0 0013.803-3.7M4.031 9.865a8.25 8.25 0 0113.803-3.7l3.181 3.182M16.023 9.348H20M14.985 19.644H20M2.985 14.652V20" /></svg>
|
||||
{$t.common?.refresh || 'Refresh'}
|
||||
</button>
|
||||
<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'}>
|
||||
<Icon name="close" size={20} strokeWidth={2} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Git Error Banner -->
|
||||
@@ -94,7 +112,7 @@
|
||||
<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>
|
||||
<summary class="cursor-pointer font-medium">{($t.git?.error_files_overwritten || 'Файлы, которые будут перезаписаны ({count})').replace('{count}', String(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>
|
||||
@@ -104,7 +122,7 @@
|
||||
{/if}
|
||||
{#if model.gitError.next_steps?.length}
|
||||
<div class="text-xs">
|
||||
<span class="font-medium">Рекомендации:</span>
|
||||
<span class="font-medium">{$t.git?.error_recommendations || 'Рекомендации:'}</span>
|
||||
<ol class="mt-1 list-decimal space-y-0.5 pl-5">
|
||||
{#each model.gitError.next_steps as step}
|
||||
<li>{step}</li>
|
||||
@@ -117,7 +135,7 @@
|
||||
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="Закрыть"
|
||||
aria-label={$t.common?.close || 'Закрыть'}
|
||||
>
|
||||
<Icon name="close" size={16} strokeWidth={2} />
|
||||
</button>
|
||||
@@ -134,7 +152,30 @@
|
||||
{: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>
|
||||
<div class="rounded-lg border border-warning bg-warning-light p-3 text-sm text-warning">
|
||||
<div class="flex items-start justify-between gap-2">
|
||||
<div>
|
||||
<div class="font-semibold">{$t.git?.git_server_mismatch_title || 'Git server mismatch detected'}</div>
|
||||
<div class="mt-1">{($t.git?.git_server_mismatch_desc || 'Configured: {configured}, origin: {origin}.').replace('{configured}', model.configHost).replace('{origin}', model.originHost)}</div>
|
||||
<div class="mt-1.5 text-xs text-warning/80">{$t.git?.git_server_mismatch_hint || 'Push/Pull будут отправлять на настроенный сервер, а не на origin. Обновите конфигурацию в Настройках → Git, если это не intended.'}</div>
|
||||
</div>
|
||||
<a href="/settings/git" class="shrink-0 rounded-md border border-warning bg-surface-card px-3 py-1.5 text-xs font-medium text-warning transition-colors hover:bg-warning-light">{$t.git?.git_server_mismatch_fix || 'Настроить'}</a>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
{#if model.commitCompleted}
|
||||
<div class="rounded-lg border border-success bg-success-light p-3 text-sm text-success">
|
||||
<div class="flex items-center gap-2">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 shrink-0" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>
|
||||
<div class="flex-1">
|
||||
<span class="font-medium">{$t.git?.commit_success_banner || '✅ Изменения закоммичены.'}</span>
|
||||
<span class="ml-1">{$t.git?.commit_next_step || 'Следующий шаг: вкладка «Релиз» для продвижения между ветками или «Серверные операции» для Pull/Push/Deploy.'}</span>
|
||||
</div>
|
||||
<button type="button" onclick={() => { model.commitCompleted = false; }} class="shrink-0 rounded p-0.5 text-success hover:bg-success-light" aria-label={$t.common?.close || 'Close'}>
|
||||
<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>
|
||||
</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">
|
||||
@@ -145,12 +186,12 @@
|
||||
<span class="hidden text-xs text-text-muted sm:inline">·</span>
|
||||
<span class="flex items-center gap-1.5 text-sm text-text-muted">
|
||||
<Icon name="code" size={16} class="text-text-subtle" strokeWidth={2} />
|
||||
<span class="hidden sm:inline">Ветка:</span> <strong class="font-mono text-text">{model.currentBranch}</strong>
|
||||
<span class="hidden sm:inline">{$t.git?.branch_label || 'Ветка:'}</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} изменений
|
||||
{($t.git?.changes_count || '{count} изменений').replace('{count}', String(model.changedFilesCount))}
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -159,22 +200,22 @@
|
||||
<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')}>
|
||||
<Icon name="edit" size={16} strokeWidth={2} />
|
||||
Фиксация изменений
|
||||
{$t.git?.tab_workspace || 'Фиксация изменений'}
|
||||
<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')}>
|
||||
<Icon name="lightning" size={16} strokeWidth={2} />
|
||||
Релиз
|
||||
{$t.git?.tab_release || 'Релиз'}
|
||||
<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')}>
|
||||
<Icon name="settings" size={16} strokeWidth={2} />
|
||||
Серверные операции
|
||||
{$t.git?.tab_operations || 'Серверные операции'}
|
||||
<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()} />
|
||||
<GitWorkspacePanel {dashboardId} envId={model.resolvedEnvId} commitHistoryKey={model.commitHistoryKey} 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}
|
||||
@@ -190,4 +231,25 @@
|
||||
{/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} />
|
||||
|
||||
{#if model.showDeployConfirm}
|
||||
<div class="fixed inset-0 z-50 flex items-center justify-center bg-black/50" onclick={() => { model.showDeployConfirm = false; }} onkeydown={(e) => { if (e.key === 'Escape') model.showDeployConfirm = false; }}>
|
||||
<div class="bg-surface-card rounded-xl shadow-xl p-6 max-w-md w-full mx-4 border border-border" role="alertdialog" aria-modal="true" aria-label={$t.git?.deploy || 'Deploy to Environment'} onclick={(e) => e.stopPropagation()}>
|
||||
<h3 class="text-lg font-semibold text-text mb-2">{$t.git?.deploy || 'Deploy to Environment'}</h3>
|
||||
<p class="text-sm text-text-muted mb-4">Подтвердите деплой в PROD. Введите slug дашборда: <strong>{model.deployConfirmSlug}</strong></p>
|
||||
<!-- svelte-ignore a11y-autofocus -->
|
||||
<input
|
||||
bind:value={deployConfirmInput}
|
||||
class="w-full rounded-lg border border-border-strong p-2.5 text-sm text-text outline-none focus:border-primary-ring focus:ring-2 focus:ring-primary-ring mb-4"
|
||||
placeholder={model.deployConfirmSlug}
|
||||
onkeydown={(e) => { if (e.key === 'Enter' && deployConfirmInput.trim()) model.confirmDeploy(deployConfirmInput); }}
|
||||
autofocus
|
||||
/>
|
||||
<div class="flex justify-end gap-3">
|
||||
<Button variant="secondary" onclick={() => { model.showDeployConfirm = false; deployConfirmInput = ''; }}>{$t.common?.cancel || 'Cancel'}</Button>
|
||||
<Button variant="destructive" onclick={() => model.confirmDeploy(deployConfirmInput)} disabled={!deployConfirmInput.trim()}>{$t.common?.confirm || 'Confirm'}</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
<!-- #endregion GitManager -->
|
||||
|
||||
@@ -104,7 +104,7 @@
|
||||
disabled={mergeRecoveryLoading || mergeResolveInProgress}
|
||||
isLoading={mergeResolveInProgress}
|
||||
>
|
||||
{$t.git?.unfinished_merge?.open_resolver || 'Open conflict resolver'}
|
||||
{$t.git?.unfinished_merge?.open_resolver}
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
@@ -113,14 +113,14 @@
|
||||
isLoading={mergeAbortInProgress}
|
||||
class="border border-destructive-ring text-destructive hover:bg-destructive-light"
|
||||
>
|
||||
{$t.git?.unfinished_merge?.abort_merge || 'Abort merge'}
|
||||
{$t.git?.unfinished_merge?.abort_merge}
|
||||
</Button>
|
||||
<Button
|
||||
onclick={onContinueMerge}
|
||||
disabled={mergeContinueInProgress}
|
||||
isLoading={mergeContinueInProgress}
|
||||
>
|
||||
{$t.git?.unfinished_merge?.continue_merge || 'Continue merge'}
|
||||
{$t.git?.unfinished_merge?.continue_merge}
|
||||
</Button>
|
||||
<Button onclick={onClose}>
|
||||
{$t.common?.close || 'Close'}
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
<!-- @LAYER UI -->
|
||||
<script lang="ts">
|
||||
import { Button } from "$lib/ui";
|
||||
import { t } from "$lib/i18n/index.svelte.js";
|
||||
|
||||
let {
|
||||
isPulling,
|
||||
@@ -24,7 +25,7 @@
|
||||
isLoading={isPulling}
|
||||
class="border border-border"
|
||||
>
|
||||
⬇ Pull
|
||||
⬇ {$t.git?.pull || 'Pull'}
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
@@ -33,14 +34,14 @@
|
||||
isLoading={isPushing}
|
||||
class="border border-border"
|
||||
>
|
||||
⬆ Push{#if workspaceStatus?.ahead_count > 0} ({workspaceStatus.ahead_count}){/if}
|
||||
⬆ {$t.git?.push || 'Push'}{#if workspaceStatus?.ahead_count > 0} ({workspaceStatus.ahead_count}){/if}
|
||||
</Button>
|
||||
<Button
|
||||
variant="primary"
|
||||
onclick={onDeploy}
|
||||
class={`w-full ${currentEnvStage === 'PROD' ? 'bg-destructive hover:bg-destructive-hover focus-visible:ring-destructive-ring' : 'bg-success hover:bg-success focus-visible:ring-success-ring'}`}
|
||||
>
|
||||
🚀 Deploy
|
||||
🚀 {$t.git?.deploy || 'Deploy'}
|
||||
</Button>
|
||||
</div>
|
||||
<!-- #endregion GitOperationsPanel -->
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
<script lang="ts">
|
||||
import { Button, Input, Select } from "$lib/ui";
|
||||
import { stageBadgeClass } from "../../../services/git-utils.js";
|
||||
import { t } from "$lib/i18n/index.svelte.js";
|
||||
|
||||
let {
|
||||
promoteFromBranch = $bindable(),
|
||||
@@ -24,7 +25,7 @@
|
||||
|
||||
<div class="space-y-4">
|
||||
<div class="rounded-lg border border-border bg-surface-page p-4">
|
||||
<div class="mb-3 text-sm font-semibold text-text">Текущий статус пайплайна</div>
|
||||
<div class="mb-3 text-sm font-semibold text-text">{$t.git?.release?.pipeline_status || 'Текущий статус пайплайна'}</div>
|
||||
<div class="flex flex-wrap items-center gap-2 text-sm">
|
||||
<span class={`rounded-full border px-3 py-1 font-semibold ${stageBadgeClass('DEV')}`}>DEV (dev)</span>
|
||||
<span class="text-text-subtle">➔</span>
|
||||
@@ -34,7 +35,7 @@
|
||||
</div>
|
||||
{#if preferredDeployTargetStage}
|
||||
<p class="mt-2 text-xs text-text-muted">
|
||||
Следующий шаг по GitFlow: <strong>{promoteFromBranch} ➔ {promoteToBranch}</strong>
|
||||
{$t.git?.release?.next_step || 'Следующий шаг по GitFlow:'} <strong>{promoteFromBranch} ➔ {promoteToBranch}</strong>
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -46,48 +47,48 @@
|
||||
class={`w-full ${promoteMode === 'direct' ? 'bg-destructive hover:bg-destructive-hover focus-visible:ring-destructive-ring' : ''}`}
|
||||
>
|
||||
{promoteMode === 'direct'
|
||||
? `Прямой перенос ${promoteFromBranch} ➔ ${promoteToBranch} (unsafe)`
|
||||
: `Создать Merge Request (${promoteFromBranch} ➔ ${promoteToBranch})`}
|
||||
? ($t.git?.release?.promote_direct || 'Прямой перенос {from} ➔ {to} (unsafe)').replace('{from}', promoteFromBranch).replace('{to}', promoteToBranch)
|
||||
: ($t.git?.release?.promote_mr || 'Создать Merge Request ({from} ➔ {to})').replace('{from}', promoteFromBranch).replace('{to}', promoteToBranch)}
|
||||
</Button>
|
||||
|
||||
<button
|
||||
class="text-sm text-text-muted hover:text-text"
|
||||
onclick={() => (showAdvancedPromote = !showAdvancedPromote)}
|
||||
>
|
||||
{showAdvancedPromote ? '▴ Скрыть advanced settings' : '▾ Advanced settings'}
|
||||
{showAdvancedPromote ? ($t.git?.release?.advanced_hide || '▴ Скрыть расширенные настройки') : ($t.git?.release?.advanced_show || '▾ Расширенные настройки')}
|
||||
</button>
|
||||
|
||||
{#if showAdvancedPromote}
|
||||
<div class="space-y-3 rounded-lg border border-border p-3">
|
||||
<div class="grid grid-cols-1 gap-2 md:grid-cols-2">
|
||||
<Input
|
||||
label="From branch"
|
||||
label={$t.git?.release?.from_branch || 'From branch'}
|
||||
bind:value={promoteFromBranch}
|
||||
placeholder="dev"
|
||||
/>
|
||||
<Input
|
||||
label="To branch"
|
||||
label={$t.git?.release?.to_branch || 'To branch'}
|
||||
bind:value={promoteToBranch}
|
||||
placeholder="preprod"
|
||||
/>
|
||||
</div>
|
||||
<Select
|
||||
label="Promotion mode"
|
||||
label={$t.git?.release?.promotion_mode || 'Режим переноса'}
|
||||
bind:value={promoteMode}
|
||||
options={[
|
||||
{ value: 'mr', label: 'Create MR/PR (Safe)' },
|
||||
{ value: 'direct', label: 'Direct merge without MR (Unsafe)' },
|
||||
{ value: 'mr', label: $t.git?.release?.mode_mr || 'Create MR/PR (Safe)' },
|
||||
{ value: 'direct', label: $t.git?.release?.mode_direct || 'Direct merge without MR (Unsafe)' },
|
||||
]}
|
||||
/>
|
||||
{#if promoteMode === 'direct'}
|
||||
<div class="rounded-lg border border-destructive-ring bg-destructive-light p-3 text-sm text-destructive">
|
||||
<div class="font-semibold">Внимание: прямой перенос без MR</div>
|
||||
<div class="mt-1">Это обходит процесс аппрува и записывается в audit лог.</div>
|
||||
<div class="font-semibold">{$t.git?.release?.direct_warning_title || 'Внимание: прямой перенос без MR'}</div>
|
||||
<div class="mt-1">{$t.git?.release?.direct_warning_desc || 'Это обходит процесс аппрува и записывается в audit лог.'}</div>
|
||||
</div>
|
||||
<Input
|
||||
label="Причина (обязательно)"
|
||||
label={$t.git?.release?.reason_label || 'Причина (обязательно)'}
|
||||
bind:value={promoteReason}
|
||||
placeholder="Почему bypass MR?"
|
||||
placeholder={$t.git?.release?.reason_placeholder || 'Почему bypass MR?'}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
33
frontend/src/lib/components/git/GitStatusLegend.svelte
Normal file
33
frontend/src/lib/components/git/GitStatusLegend.svelte
Normal file
@@ -0,0 +1,33 @@
|
||||
<!-- #region Git.StatusLegend [C:2] [TYPE Component] [SEMANTICS git,status,legend,help] -->
|
||||
<!-- @ingroup Git -->
|
||||
<!-- @BRIEF Status legend — chips with color + description for each repository status token. -->
|
||||
<!-- @LAYER UI -->
|
||||
<!-- @RELATION USED_BY -> [frontend/src/routes/git/+page.svelte] -->
|
||||
<script lang="ts">
|
||||
import { t } from '$lib/i18n/index.svelte.js';
|
||||
|
||||
const legendItems = [
|
||||
{ token: 'synced', class: 'bg-success-light text-success', label: $t.git?.repo_status?.synced, desc: $t.git?.legend_synced },
|
||||
{ token: 'changes', class: 'bg-warning-light text-warning', label: $t.git?.repo_status?.changes, desc: $t.git?.legend_changes },
|
||||
{ token: 'behind_remote', class: 'bg-primary-light text-primary', label: $t.git?.repo_status?.behind_remote, desc: $t.git?.legend_behind_remote },
|
||||
{ token: 'ahead_remote', class: 'bg-primary-light text-primary', label: $t.git?.repo_status?.ahead_remote, desc: $t.git?.legend_ahead_remote },
|
||||
{ token: 'diverged', class: 'bg-info-light text-info', label: $t.git?.repo_status?.diverged, desc: $t.git?.legend_diverged },
|
||||
{ token: 'no_repo', class: 'bg-surface-muted text-text-muted', label: $t.git?.repo_status?.no_repo, desc: $t.git?.legend_no_repo },
|
||||
{ token: 'error', class: 'bg-destructive-light text-destructive', label: $t.git?.repo_status?.error, desc: $t.git?.legend_error },
|
||||
];
|
||||
</script>
|
||||
|
||||
<div class="rounded-lg border border-border bg-surface-card p-4">
|
||||
<p class="text-sm font-semibold text-text mb-3">{$t.git?.legend_title || 'Repository statuses'}</p>
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-2">
|
||||
{#each legendItems as item}
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium shrink-0 {item.class}">
|
||||
{item.label}
|
||||
</span>
|
||||
<span class="text-xs text-text-muted">{item.desc}</span>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
<!-- #endregion Git.StatusLegend -->
|
||||
@@ -15,6 +15,7 @@
|
||||
import { Button, Icon } from "$lib/ui";
|
||||
import * as Diff2Html from 'diff2html';
|
||||
import 'diff2html/bundles/css/diff2html.min.css';
|
||||
import CommitHistory from './CommitHistory.svelte';
|
||||
|
||||
let {
|
||||
hasWorkspaceChanges,
|
||||
@@ -27,6 +28,9 @@
|
||||
autoPushAfterCommit = $bindable(),
|
||||
loading,
|
||||
pushProviderLabel,
|
||||
dashboardId = '',
|
||||
envId = null,
|
||||
commitHistoryKey = 0,
|
||||
onSync,
|
||||
onGenerateMessage,
|
||||
onCommit,
|
||||
@@ -111,7 +115,7 @@
|
||||
highlight: true,
|
||||
});
|
||||
} catch {
|
||||
return '<div class="p-4 text-sm text-destructive">Failed to render diff</div>';
|
||||
return `<div class="p-4 text-sm text-destructive">${$t.git?.diff_render_failed || 'Failed to render diff'}</div>`;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -132,14 +136,14 @@
|
||||
class="w-full"
|
||||
>
|
||||
<Icon name="refresh" size={16} class="-ml-1 mr-1.5" strokeWidth={2} />
|
||||
Синхронизировать из Superset
|
||||
{$t.git?.sync || 'Синхронизировать из Superset'}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<!-- Commit message -->
|
||||
<div class="rounded-lg border border-border bg-surface-card p-4 shadow-sm">
|
||||
<div class="mb-3 flex items-center justify-between">
|
||||
<h3 class="text-sm font-semibold text-text">Сообщение коммита</h3>
|
||||
<h3 class="text-sm font-semibold text-text">{$t.git?.commit_message || 'Сообщение коммита'}</h3>
|
||||
<button
|
||||
class="inline-flex items-center gap-1 rounded-md px-2 py-1 text-xs font-medium text-primary transition-colors hover:bg-primary-light disabled:opacity-50"
|
||||
onclick={onGenerateMessage}
|
||||
@@ -152,7 +156,7 @@
|
||||
<textarea
|
||||
bind:value={commitMessage}
|
||||
class={`h-32 w-full resize-none rounded-lg border border-border p-3 text-sm outline-none transition-colors focus:border-primary-ring focus:ring-2 focus:ring-primary-ring ${generatingMessage ? 'animate-pulse bg-surface-page' : 'bg-surface-card'}`}
|
||||
placeholder="Опишите изменения..."
|
||||
placeholder={$t.git?.describe_changes || 'Опишите изменения...'}
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
@@ -160,7 +164,7 @@
|
||||
<div class="rounded-lg border border-border bg-surface-card p-4 shadow-sm">
|
||||
<div class="mb-3 flex items-center gap-2 rounded-lg bg-surface-page px-3 py-2 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="M15 12H9m12 0a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>
|
||||
Файлов с изменениями: <strong class="text-text">{changedFilesCount}</strong>
|
||||
{$t.git?.files_with_changes || 'Файлов с изменениями:'} <strong class="text-text">{changedFilesCount}</strong>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
@@ -170,7 +174,7 @@
|
||||
class="w-full"
|
||||
>
|
||||
<Icon name="check" size={16} class="-ml-1 mr-1.5" strokeWidth={2} />
|
||||
Зафиксировать (Commit)
|
||||
{$t.git?.commit_button || 'Зафиксировать (Commit)'}
|
||||
</Button>
|
||||
|
||||
<label class="mt-3 flex items-center gap-2.5 rounded-md border border-border bg-surface-page px-3 py-2 text-xs text-text-muted transition-colors hover:bg-surface-muted">
|
||||
@@ -185,11 +189,11 @@
|
||||
<div class="flex items-center justify-between border-b border-border bg-surface-page px-4 py-3">
|
||||
<div class="flex items-center gap-2">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 text-text-muted" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M3.75 3.75v4.5m0-4.5h4.5m-4.5 0L9 9M3.75 20.25v-4.5m0 4.5h4.5m-4.5 0L9 15M20.25 3.75h-4.5m4.5 0v4.5m0-4.5L15 9m5.25 11.25h-4.5m4.5 0v-4.5m0 4.5L15 15"/></svg>
|
||||
<span class="text-sm font-semibold text-text">Diff (изменения)</span>
|
||||
<span class="text-sm font-semibold text-text">{$t.git?.diff_title || 'Diff (изменения)'}</span>
|
||||
</div>
|
||||
{#if hasWorkspaceChanges}
|
||||
<span class="inline-flex items-center gap-1 rounded-full bg-primary-light px-2.5 py-0.5 text-xs font-medium text-primary ring-1 ring-inset ring-primary-ring">
|
||||
{changedFilesCount} файлов
|
||||
{($t.git?.files_count || '{count} файлов').replace('{count}', String(changedFilesCount))}
|
||||
{#if totalChunks > 0}
|
||||
<span class="text-primary">· {renderedChunkCount}/{totalChunks}</span>
|
||||
{/if}
|
||||
@@ -241,19 +245,34 @@
|
||||
{:else if hasWorkspaceChanges}
|
||||
<div class="flex h-full flex-col items-center justify-center gap-3 text-sm text-text-subtle">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-12 w-12 text-text-subtle" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M3.75 3.75v4.5m0-4.5h4.5m-4.5 0L9 9M3.75 20.25v-4.5m0 4.5h4.5m-4.5 0L9 15M20.25 3.75h-4.5m4.5 0v4.5m0-4.5L15 9m5.25 11.25h-4.5m4.5 0v-4.5m0 4.5L15 15"/></svg>
|
||||
<span>Загрузка diff... (нажмите «Синхронизировать»)</span>
|
||||
<span>{$t.git?.diff_loading_hint || 'Загрузка diff... (нажмите «Синхронизировать»)'}</span>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="flex h-full flex-col items-center justify-center gap-3 text-sm text-text-subtle">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-16 w-16 text-text-subtle" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M9 12h3.75M9 15h3.75M9 18h3.75m3 .75H18a2.25 2.25 0 002.25-2.25V6.108c0-1.135-.845-2.098-1.976-2.192a48.424 48.424 0 00-1.123-.08m-5.801 0c-.065.21-.1.433-.1.664 0 .414.336.75.75.75h4.5a.75.75 0 00.75-.75 2.25 2.25 0 00-.1-.664m-5.8 0A2.251 2.251 0 0113.5 2.25H15c1.012 0 1.867.668 2.15 1.586m-5.8 0c-.376.023-.75.05-1.124.08C9.095 4.01 8.25 4.973 8.25 6.108V8.25m0 0H4.875c-.621 0-1.125.504-1.125 1.125v11.25c0 .621.504 1.125 1.125 1.125h9.75c.621 0 1.125-.504 1.125-1.125V9.375c0-.621-.504-1.125-1.125-1.125H8.25zM6.75 12h.008v.008H6.75V12zm0 3h.008v.008H6.75V15zm0 3h.008v.008H6.75V18z"/></svg>
|
||||
<span>Нет изменений для коммита</span>
|
||||
<span class="text-xs text-text-subtle">Синхронизируйте дашборд, чтобы увидеть изменения</span>
|
||||
<span>{$t.git?.no_changes_to_commit || 'Нет изменений для коммита'}</span>
|
||||
<span class="text-xs text-text-subtle">{$t.git?.sync_to_see_changes || 'Синхронизируйте дашборд, чтобы увидеть изменения'}</span>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Commit History collapsible section -->
|
||||
{#if dashboardId}
|
||||
<details class="mt-4 rounded-lg border border-border bg-surface-card overflow-hidden">
|
||||
<summary class="flex cursor-pointer items-center gap-2 px-4 py-3 text-sm font-medium text-text hover:bg-surface-muted transition-colors">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 text-text-muted" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M12 6v6h4.5m4.5 0a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>
|
||||
{$t.git?.history || 'Commit History'}
|
||||
</summary>
|
||||
<div class="px-4 pb-4">
|
||||
{#key commitHistoryKey}
|
||||
<CommitHistory {dashboardId} {envId} />
|
||||
{/key}
|
||||
</div>
|
||||
</details>
|
||||
{/if}
|
||||
|
||||
<!-- Custom styles for diff2html -->
|
||||
<style>
|
||||
:global(.diff-view .d2h-wrapper) {
|
||||
|
||||
@@ -1,6 +1,17 @@
|
||||
{
|
||||
"management": "Git Management",
|
||||
"branch": "Branch",
|
||||
"branch_label": "Branch:",
|
||||
"changes_count": "{count} changes",
|
||||
"tab_workspace": "Commit Changes",
|
||||
"tab_release": "Release",
|
||||
"tab_operations": "Server Operations",
|
||||
"error_files_overwritten": "Files that will be overwritten ({count})",
|
||||
"error_recommendations": "Recommendations:",
|
||||
"git_server_mismatch_title": "Git server mismatch detected",
|
||||
"git_server_mismatch_desc": "Configured: {configured}, origin: {origin}.",
|
||||
"git_server_mismatch_hint": "Push/Pull will target the configured server, not origin. Update the configuration in Settings → Git if this is not intended.",
|
||||
"git_server_mismatch_fix": "Configure",
|
||||
"actions": "Actions",
|
||||
"sync": "Sync from Superset",
|
||||
"commit": "Commit Changes",
|
||||
@@ -39,7 +50,17 @@
|
||||
"commit_success": "Changes committed successfully",
|
||||
"commit_and_push_success": "Changes committed and pushed to remote",
|
||||
"commit_message": "Commit Message",
|
||||
"auto_push_after_commit": "Push after commit to",
|
||||
"commit_button": "Commit",
|
||||
"files_with_changes": "Files with changes:",
|
||||
"diff_title": "Diff (changes)",
|
||||
"files_count": "{count} files",
|
||||
"diff_loading_hint": "Loading diff... (click “Sync”)",
|
||||
"no_changes_to_commit": "No changes to commit",
|
||||
"sync_to_see_changes": "Sync the dashboard to see changes",
|
||||
"diff_render_failed": "Failed to render diff",
|
||||
"commit_success_banner": "✅ Changes committed.",
|
||||
"commit_next_step": "Next step: go to the “Release” tab to promote between branches, or “Operations” for Pull/Push/Deploy.",
|
||||
"auto_push_after_commit": "Automatically push after commit to",
|
||||
"generate_with_ai": "Generate with AI",
|
||||
"describe_changes": "Describe your changes...",
|
||||
"changed_files": "Changed Files",
|
||||
@@ -64,6 +85,8 @@
|
||||
"switched_to": "Switched to {branch}",
|
||||
"created_branch": "Created branch {branch}",
|
||||
"branch_name_placeholder": "branch-name",
|
||||
"branch_group_local": "Local",
|
||||
"branch_group_remote": "Remote",
|
||||
"repo_status": {
|
||||
"loading": "Loading",
|
||||
"no_repo": "No Repo",
|
||||
@@ -81,11 +104,63 @@
|
||||
"files_label": "files: {count}",
|
||||
"sync_and_commit": "Sync & Commit",
|
||||
"show_diff": "Show diff",
|
||||
"init_repo_button": "Initialize Git Repository",
|
||||
"hint_workspace": "Sync dashboard with Git first, write a commit message, then click Commit. Enable auto-push to send to remote after commit.",
|
||||
"hint_release": "Promote changes between branches: choose from → to, provide a reason (in direct mode), then click Promote. Or create a Merge Request.",
|
||||
"hint_operations": "Pull — fetch changes from remote repository. Push — send local commits to remote. Deploy — publish dashboard to target environment.",
|
||||
"hint_branch_selector": "Switch between branches. If there are uncommitted changes, commit or stash them first.",
|
||||
"help_title": "How it works",
|
||||
"help_summary": "A quick guide to the Git dashboard management pipeline",
|
||||
"help_step1_title": "Configure a Git server",
|
||||
"help_step1_desc": "Go to Settings → Git and add a connection to Gitea/GitLab/GitHub.",
|
||||
"help_step2_title": "Select a dashboard",
|
||||
"help_step2_desc": "In the “Git Management” tab, check a dashboard and click “Manage Git”.",
|
||||
"help_step3_title": "Create or initialize a repository",
|
||||
"help_step3_desc": "“Create repo” creates a remote repository on the server. “Initialize” binds an existing remote URL to the dashboard.",
|
||||
"help_step4_title": "Sync and commit",
|
||||
"help_step4_desc": "In the “Commit” tab: Sync pulls the configuration from Superset, then write a message and click Commit.",
|
||||
"help_step5_title": "Promote and deploy",
|
||||
"help_step5_desc": "In the “Release” tab — promote between branches (MR or direct). In the “Operations” tab — Pull, Push, and Deploy to an environment.",
|
||||
"legend_title": "Repository statuses",
|
||||
"legend_synced": "Local branch matches remote — no changes.",
|
||||
"legend_changes": "There are uncommitted changes in the working area.",
|
||||
"legend_behind_remote": "Local branch is behind remote — run Pull.",
|
||||
"legend_ahead_remote": "Local branch is ahead of remote — run Push.",
|
||||
"legend_diverged": "Local and remote branches have diverged — merge or rebase required.",
|
||||
"legend_no_repo": "Dashboard is not linked to a Git repository.",
|
||||
"legend_error": "Failed to fetch status — check connection and settings.",
|
||||
"create_repo": "Create repo",
|
||||
"create_repo_desc": "Create a new remote repository on the Git server and link it to the dashboard.",
|
||||
"init_repo_desc": "Bind an existing remote repository by URL to the dashboard (local init).",
|
||||
"repo_already_exists": "Repository already exists. Enter its URL below and click “Initialize”.",
|
||||
"conflict": {
|
||||
"title": "Merge Conflicts Detected",
|
||||
"description": "The following files have conflicts. Please choose how to resolve them.",
|
||||
"your_changes": "Your Changes (Mine)",
|
||||
"remote_changes": "Remote Changes (Theirs)",
|
||||
"keep_mine": "Keep Mine",
|
||||
"keep_theirs": "Keep Theirs",
|
||||
"resolved": "Resolved: {strategy}",
|
||||
"cancel": "Cancel",
|
||||
"resolve_continue": "Resolve & Continue",
|
||||
"unresolved_count": "Please resolve all conflicts first. ({count} remaining)"
|
||||
},
|
||||
"release": {
|
||||
"pipeline_status": "Current pipeline status",
|
||||
"next_step": "Next GitFlow step:",
|
||||
"promote_direct": "Direct promote {from} ➔ {to} (unsafe)",
|
||||
"promote_mr": "Create Merge Request ({from} ➔ {to})",
|
||||
"advanced_show": "▾ Advanced settings",
|
||||
"advanced_hide": "▴ Hide advanced settings",
|
||||
"from_branch": "From branch",
|
||||
"to_branch": "To branch",
|
||||
"promotion_mode": "Promotion mode",
|
||||
"mode_mr": "Create MR/PR (Safe)",
|
||||
"mode_direct": "Direct merge without MR (Unsafe)",
|
||||
"direct_warning_title": "Warning: direct promote without MR",
|
||||
"direct_warning_desc": "This bypasses the approval process and is recorded in the audit log.",
|
||||
"reason_label": "Reason (required)",
|
||||
"reason_placeholder": "Why bypass MR?"
|
||||
},
|
||||
"diff_loading": "Loading changes...",
|
||||
"diff_show_more": "Show {count} more files",
|
||||
"diff_all_shown": "All changes shown ({count} files)",
|
||||
@@ -99,6 +174,14 @@
|
||||
"copy_commands": "Copy commands",
|
||||
"copy_success": "Commands copied to clipboard",
|
||||
"copy_failed": "Failed to copy commands",
|
||||
"copy_empty": "No commands to copy"
|
||||
"copy_empty": "No commands to copy",
|
||||
"open_resolver": "Open conflict resolver",
|
||||
"abort_merge": "Abort merge",
|
||||
"continue_merge": "Continue merge",
|
||||
"no_conflicts": "No unresolved conflicts were found",
|
||||
"resolve_empty": "No conflict resolutions selected",
|
||||
"resolve_success": "Conflicts were resolved and staged",
|
||||
"abort_success": "Merge was aborted",
|
||||
"continue_success": "Merge commit created successfully"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,17 @@
|
||||
{
|
||||
"management": "Управление Git",
|
||||
"branch": "Ветка",
|
||||
"branch_label": "Ветка:",
|
||||
"changes_count": "{count} изменений",
|
||||
"tab_workspace": "Фиксация изменений",
|
||||
"tab_release": "Релиз",
|
||||
"tab_operations": "Серверные операции",
|
||||
"error_files_overwritten": "Файлы, которые будут перезаписаны ({count})",
|
||||
"error_recommendations": "Рекомендации:",
|
||||
"git_server_mismatch_title": "Обнаружено несоответствие Git-сервера",
|
||||
"git_server_mismatch_desc": "Настроено: {configured}, origin: {origin}.",
|
||||
"git_server_mismatch_hint": "Push/Pull будут отправлять на настроенный сервер, а не на origin. Обновите конфигурацию в Настройках → Git, если это не intended.",
|
||||
"git_server_mismatch_fix": "Настроить",
|
||||
"actions": "Действия",
|
||||
"sync": "Синхронизировать из Superset",
|
||||
"commit": "Зафиксировать изменения",
|
||||
@@ -39,7 +50,17 @@
|
||||
"commit_success": "Изменения успешно закоммичены",
|
||||
"commit_and_push_success": "Изменения успешно закоммичены и отправлены в remote",
|
||||
"commit_message": "Сообщение коммита",
|
||||
"auto_push_after_commit": "Сделать push после commit в",
|
||||
"commit_button": "Зафиксировать (Commit)",
|
||||
"files_with_changes": "Файлов с изменениями:",
|
||||
"diff_title": "Diff (изменения)",
|
||||
"files_count": "{count} файлов",
|
||||
"diff_loading_hint": "Загрузка diff... (нажмите «Синхронизировать»)",
|
||||
"no_changes_to_commit": "Нет изменений для коммита",
|
||||
"sync_to_see_changes": "Синхронизируйте дашборд, чтобы увидеть изменения",
|
||||
"diff_render_failed": "Не удалось отобразить diff",
|
||||
"commit_success_banner": "✅ Изменения закоммичены.",
|
||||
"commit_next_step": "Следующий шаг: вкладка «Релиз» для продвижения между ветками или «Серверные операции» для Pull/Push/Deploy.",
|
||||
"auto_push_after_commit": "Автоматически отправить (Push) после фиксации в",
|
||||
"generate_with_ai": "Сгенерировать с AI",
|
||||
"describe_changes": "Опишите ваши изменения...",
|
||||
"changed_files": "Измененные файлы",
|
||||
@@ -64,6 +85,8 @@
|
||||
"switched_to": "Переключено на {branch}",
|
||||
"created_branch": "Создана ветка {branch}",
|
||||
"branch_name_placeholder": "имя-ветки",
|
||||
"branch_group_local": "Локальные",
|
||||
"branch_group_remote": "Удалённые",
|
||||
"repo_status": {
|
||||
"loading": "Загрузка",
|
||||
"no_repo": "Нет репозитория",
|
||||
@@ -81,11 +104,63 @@
|
||||
"files_label": "файлы: {count}",
|
||||
"sync_and_commit": "Синхронизировать и зафиксировать",
|
||||
"show_diff": "Показать diff",
|
||||
"init_repo_button": "Инициализировать Git-репозиторий",
|
||||
"hint_workspace": "Сначала синхронизируй дашборд с Git (Sync), затем напиши сообщение коммита и нажми Commit. Если нужно — включи автоотправку (Push) после коммита.",
|
||||
"hint_release": "Перенос изменений между ветками: выбери откуда → куда, укажи причину (при direct-режиме) и нажми Promote. Либо создай Merge Request.",
|
||||
"hint_operations": "Pull — забрать изменения из удалённого репозитория. Push — отправить локальные коммиты. Deploy — выкатить дашборд в целевое окружение.",
|
||||
"hint_branch_selector": "Переключение между ветками. Если есть незакоммиченные изменения — сначала зафиксируй их или отложи через stash.",
|
||||
"help_title": "Как это работает",
|
||||
"help_summary": "Краткое руководство по пайплайну Git-управления дашбордами",
|
||||
"help_step1_title": "Настройте Git-сервер",
|
||||
"help_step1_desc": "Перейдите в Настройки → Git и добавьте подключение к Gitea/GitLab/GitHub.",
|
||||
"help_step2_title": "Выберите дашборд",
|
||||
"help_step2_desc": "Во вкладке «Управление Git» отметьте дашборд и нажмите «Управление Git».",
|
||||
"help_step3_title": "Создайте или инициализируйте репозиторий",
|
||||
"help_step3_desc": "«Create repo» создаёт удалённый репозиторий на сервере. «Инициализировать» привязывает существующий remote-URL к дашборду.",
|
||||
"help_step4_title": "Синхронизируйте и зафиксируйте",
|
||||
"help_step4_desc": "Во вкладке «Фиксация изменений»: Sync забирает конфигурацию из Superset, затем напишите сообщение и нажмите Commit.",
|
||||
"help_step5_title": "Продвигайте и деплойте",
|
||||
"help_step5_desc": "Во вкладке «Релиз» — перенос между ветками (MR или прямой). Во вкладке «Серверные операции» — Pull, Push и Deploy в окружение.",
|
||||
"legend_title": "Статусы репозиториев",
|
||||
"legend_synced": "Локальная ветка совпадает с remote — изменений нет.",
|
||||
"legend_changes": "Есть незакоммиченные изменения в рабочей области.",
|
||||
"legend_behind_remote": "Локальная ветка отстаёт от remote — выполните Pull.",
|
||||
"legend_ahead_remote": "Локальная ветка опережает remote — выполните Push.",
|
||||
"legend_diverged": "Локальная и remote ветки разошлись — требуется merge или rebase.",
|
||||
"legend_no_repo": "Дашборд не привязан к Git-репозиторию.",
|
||||
"legend_error": "Ошибка получения статуса — проверьте подключение и настройки.",
|
||||
"create_repo": "Создать репозиторий",
|
||||
"create_repo_desc": "Создать новый удалённый репозиторий на Git-сервере и привязать его к дашборду.",
|
||||
"init_repo_desc": "Привязать существующий удалённый репозиторий по URL к дашборду (локальный init).",
|
||||
"repo_already_exists": "Репозиторий уже существует. Введите его URL ниже и нажмите «Инициализировать».",
|
||||
"conflict": {
|
||||
"title": "Обнаружены конфликты слияния",
|
||||
"description": "В следующих файлах есть конфликты. Выберите способ разрешения для каждого.",
|
||||
"your_changes": "Ваши изменения (Mine)",
|
||||
"remote_changes": "Удалённые изменения (Theirs)",
|
||||
"keep_mine": "Оставить мои",
|
||||
"keep_theirs": "Оставить их",
|
||||
"resolved": "Разрешён: {strategy}",
|
||||
"cancel": "Отмена",
|
||||
"resolve_continue": "Разрешить и продолжить",
|
||||
"unresolved_count": "Сначала разрешите все конфликты. (Осталось: {count})"
|
||||
},
|
||||
"release": {
|
||||
"pipeline_status": "Текущий статус пайплайна",
|
||||
"next_step": "Следующий шаг по GitFlow:",
|
||||
"promote_direct": "Прямой перенос {from} ➔ {to} (unsafe)",
|
||||
"promote_mr": "Создать Merge Request ({from} ➔ {to})",
|
||||
"advanced_show": "▾ Расширенные настройки",
|
||||
"advanced_hide": "▴ Скрыть расширенные настройки",
|
||||
"from_branch": "Из ветки",
|
||||
"to_branch": "В ветку",
|
||||
"promotion_mode": "Режим переноса",
|
||||
"mode_mr": "Создать MR/PR (безопасно)",
|
||||
"mode_direct": "Прямой merge без MR (небезопасно)",
|
||||
"direct_warning_title": "Внимание: прямой перенос без MR",
|
||||
"direct_warning_desc": "Это обходит процесс аппрува и записывается в audit лог.",
|
||||
"reason_label": "Причина (обязательно)",
|
||||
"reason_placeholder": "Почему bypass MR?"
|
||||
},
|
||||
"diff_loading": "Загрузка изменений...",
|
||||
"diff_show_more": "Показать ещё ({count} файлов)",
|
||||
"diff_all_shown": "Показаны все изменения ({count} файлов)",
|
||||
@@ -99,6 +174,14 @@
|
||||
"copy_commands": "Скопировать команды",
|
||||
"copy_success": "Команды скопированы в буфер обмена",
|
||||
"copy_failed": "Не удалось скопировать команды",
|
||||
"copy_empty": "Команды для копирования отсутствуют"
|
||||
"copy_empty": "Команды для копирования отсутствуют",
|
||||
"open_resolver": "Открыть разрешение конфликтов",
|
||||
"abort_merge": "Отменить слияние",
|
||||
"continue_merge": "Продолжить слияние",
|
||||
"no_conflicts": "Неразрешённых конфликтов не найдено",
|
||||
"resolve_empty": "Не выбрано разрешение конфликтов",
|
||||
"resolve_success": "Конфликты разрешены и добавлены в индекс",
|
||||
"abort_success": "Слияние отменено",
|
||||
"continue_success": "Merge-коммит успешно создан"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
// @STATE checking — Initial status check in progress.
|
||||
// @STATE error — Persistent error banner visible with message and dismiss action.
|
||||
// @ACTION checkStatus() — Checks if repository is initialized; loads workspace on success.
|
||||
// @ACTION refreshStatus() — Re-checks repository status and reloads workspace (manual refresh).
|
||||
// @ACTION loadWorkspace() — Loads workspace status and diff.
|
||||
// @ACTION handleSync() — Synchronizes dashboard state with Git.
|
||||
// @ACTION handleGenerateMessage() — Generates AI commit message from diff.
|
||||
@@ -218,6 +219,8 @@ export class GitManagerModel {
|
||||
|
||||
// ── Commit ──────────────────────────────────────────────────
|
||||
commitMessage: string = $state('');
|
||||
commitCompleted: boolean = $state(false);
|
||||
commitHistoryKey: number = $state(0);
|
||||
committing: boolean = $state(false);
|
||||
generatingMessage: boolean = $state(false);
|
||||
autoPushAfterCommit: boolean = $state(true);
|
||||
@@ -245,6 +248,8 @@ export class GitManagerModel {
|
||||
|
||||
// ── Deploy ──────────────────────────────────────────────────
|
||||
showDeployModal: boolean = $state(false);
|
||||
showDeployConfirm: boolean = $state(false);
|
||||
deployConfirmSlug: string = $state('');
|
||||
|
||||
// ── Merge Recovery ──────────────────────────────────────────
|
||||
showUnfinishedMergeDialog: boolean = $state(false);
|
||||
@@ -414,6 +419,18 @@ export class GitManagerModel {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Manual refresh: re-check repository status and reload workspace.
|
||||
* Called from the Refresh button in GitManager header.
|
||||
* @POST initialized re-evaluated, workspace reloaded if initialized.
|
||||
*/
|
||||
async refreshStatus(): Promise<void> {
|
||||
await this.checkStatus();
|
||||
if (this.initialized) {
|
||||
await this.loadWorkspace();
|
||||
}
|
||||
}
|
||||
|
||||
// ── Workspace ────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
@@ -503,11 +520,13 @@ export class GitManagerModel {
|
||||
await gitService.commit(this.dashboardId, this.commitMessage, [], this.resolvedEnvId);
|
||||
if (this.autoPushAfterCommit) {
|
||||
await gitService.push(this.dashboardId, this.resolvedEnvId);
|
||||
addToast((this._t?.git as Record<string, unknown>)?.commit_and_push_success as string || 'Коммит создан и отправлен в remote', 'success');
|
||||
addToast((this._t?.git as Record<string, unknown>)?.commit_and_push_success as string || 'Коммит создан и отправлен в remote', 'success', 8000);
|
||||
} else {
|
||||
addToast((this._t?.git as Record<string, unknown>)?.commit_success as string || 'Коммит успешно создан', 'success');
|
||||
addToast((this._t?.git as Record<string, unknown>)?.commit_success as string || 'Коммит успешно создан', 'success', 8000);
|
||||
}
|
||||
this.commitMessage = '';
|
||||
this.commitCompleted = true;
|
||||
this.commitHistoryKey++;
|
||||
await this.loadWorkspace();
|
||||
} catch (e: unknown) {
|
||||
this._setGitError(e);
|
||||
@@ -607,20 +626,33 @@ export class GitManagerModel {
|
||||
// ── Deploy Modal ─────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Open deployment target modal with PROD confirmation prompt.
|
||||
* Open deployment target modal — shows PROD confirmation dialog when stage is PROD.
|
||||
* Uses ConfirmDialog atom instead of browser prompt() for consistent UX.
|
||||
* @UX_FEEDBACK PROD confirmation dialog with slug verification.
|
||||
*/
|
||||
openDeployModal(): void {
|
||||
if (this.currentEnvStage === 'PROD') {
|
||||
const expected = String(this.dashboardId);
|
||||
const confirmation = prompt(`Подтвердите деплой в PROD. Введите slug дашборда: ${expected}`);
|
||||
if (String(confirmation || '').trim() !== expected) {
|
||||
addToast('Подтверждение PROD не пройдено. Деплой отменен.', 'error');
|
||||
return;
|
||||
}
|
||||
this.deployConfirmSlug = String(this.dashboardId);
|
||||
this.showDeployConfirm = true;
|
||||
return;
|
||||
}
|
||||
this.showDeployModal = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Confirm PROD deploy after user verified the slug in the confirmation dialog.
|
||||
* @POST If slug matches, opens deploy modal. Otherwise shows error toast.
|
||||
*/
|
||||
confirmDeploy(slug: string): void {
|
||||
if (slug.trim() !== this.deployConfirmSlug) {
|
||||
addToast('Подтверждение PROD не пройдено. Деплой отменен.', 'error');
|
||||
this.showDeployConfirm = false;
|
||||
return;
|
||||
}
|
||||
this.showDeployConfirm = false;
|
||||
this.showDeployModal = true;
|
||||
}
|
||||
|
||||
// ── Remote Repository ────────────────────────────────────────
|
||||
|
||||
/**
|
||||
|
||||
@@ -68,7 +68,7 @@
|
||||
class="inline-flex items-center justify-center rounded-lg bg-terminal-bg px-3 py-2 text-sm font-medium text-white transition-colors hover:bg-terminal-surface"
|
||||
onclick={() => (showGitManager = true)}
|
||||
>
|
||||
{$t.git?.init_repo_button || $t.git?.init_repo}
|
||||
{$t.git?.init_repo}
|
||||
</button>
|
||||
</div>
|
||||
{:else}
|
||||
|
||||
@@ -16,6 +16,8 @@
|
||||
import { SvelteSet } from "svelte/reactivity";
|
||||
import { onMount, untrack } from 'svelte';
|
||||
import RepositoryDashboardGrid from '$lib/components/dashboard/RepositoryDashboardGrid.svelte';
|
||||
import GitHelpPanel from '$lib/components/git/GitHelpPanel.svelte';
|
||||
import GitStatusLegend from '$lib/components/git/GitStatusLegend.svelte';
|
||||
import { addToast as toast } from '$lib/toasts.svelte.js';
|
||||
import { api } from '../../lib/api.js';
|
||||
import { gitService } from '../../services/gitService.js';
|
||||
@@ -232,6 +234,8 @@ import { SvelteSet } from "svelte/reactivity";
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
<GitHelpPanel />
|
||||
|
||||
<Card title={activeTab === 'repos' ? ($t.nav?.repositories || "Repositories") : ($t.git?.select_dashboard || "Select Dashboard to Manage")}>
|
||||
{#if fetchingDashboards || fetchingRepos}
|
||||
<p class="text-text-muted">{$t.common?.loading}</p>
|
||||
@@ -250,6 +254,12 @@ import { SvelteSet } from "svelte/reactivity";
|
||||
/>
|
||||
{/if}
|
||||
</Card>
|
||||
|
||||
{#if !fetchingDashboards && !fetchingRepos && dashboards.length > 0}
|
||||
<div class="mt-4">
|
||||
<GitStatusLegend />
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
<!-- #endregion GitDashboardPage -->
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// #region GitUtils [C:2] [TYPE Module] [SEMANTICS git, utils, helper, normalize, parse]
|
||||
// @BRIEF Shared utility functions extracted from GitManager.svelte.
|
||||
// #region GitUtils [C:3] [TYPE Module] [SEMANTICS git, utils, helper, normalize, parse, status]
|
||||
// @BRIEF Shared utility functions extracted from GitManager.svelte — status resolution, env/branch defaults, merge context parsing.
|
||||
|
||||
/**
|
||||
* Normalize environment stage with legacy fallback.
|
||||
@@ -16,7 +16,7 @@ export function normalizeEnvStage(env: Record<string, unknown> | null | undefine
|
||||
* Return visual class for environment stage badges.
|
||||
*/
|
||||
export function stageBadgeClass(stage: string): string {
|
||||
if (stage === 'PROD') return 'bg-red-100 text-red-800 border-red-200';
|
||||
if (stage === 'PROD') return 'bg-indigo-100 text-indigo-800 border-indigo-200';
|
||||
if (stage === 'PREPROD') return 'bg-amber-100 text-amber-800 border-amber-200';
|
||||
return 'bg-blue-100 text-blue-800 border-blue-200';
|
||||
}
|
||||
@@ -167,4 +167,41 @@ export function extractUnfinishedMergeContext(error: Record<string, unknown> | n
|
||||
commands,
|
||||
};
|
||||
}
|
||||
/**
|
||||
* Resolve a git status object into a UI status token.
|
||||
* Shared by RepositoryDashboardGrid (batch status) and GitManagerModel (single status)
|
||||
* to guarantee identical token mapping across grid and modal.
|
||||
*
|
||||
* Token set: loading | no_repo | synced | changes | behind_remote | ahead_remote | diverged | error
|
||||
*
|
||||
* @RATIONALE Previously two independent resolvers existed (grid vs model), causing
|
||||
* the grid to show "Синхронизирован" while the modal showed "не привязан" for the
|
||||
* same dashboard. Single source of truth eliminates the drift.
|
||||
*/
|
||||
export function resolveGitStatusToken(status: Record<string, unknown> | null | undefined): string {
|
||||
if (!status) return 'error';
|
||||
const syncState = String(status?.sync_state || '').toUpperCase();
|
||||
if (syncState === 'DIVERGED') return 'diverged';
|
||||
if (syncState === 'BEHIND_REMOTE') return 'behind_remote';
|
||||
if (syncState === 'AHEAD_REMOTE') return 'ahead_remote';
|
||||
if (syncState === 'CHANGES') return 'changes';
|
||||
if (syncState === 'SYNCED') return 'synced';
|
||||
const syncStatus = String(status?.sync_status || '').toUpperCase();
|
||||
if (syncStatus === 'NO_REPO') return 'no_repo';
|
||||
if (syncStatus === 'ERROR') return 'error';
|
||||
if (syncStatus === 'DIFF') return 'changes';
|
||||
if (syncStatus === 'OK') return 'synced';
|
||||
const aheadCount = Number(status?.ahead_count || 0);
|
||||
const behindCount = Number(status?.behind_count || 0);
|
||||
if (aheadCount > 0 && behindCount > 0) return 'diverged';
|
||||
if (behindCount > 0) return 'behind_remote';
|
||||
if (aheadCount > 0) return 'ahead_remote';
|
||||
const hasChanges =
|
||||
Boolean(status?.is_dirty) ||
|
||||
(Array.isArray(status?.untracked_files) && (status.untracked_files as unknown[]).length > 0) ||
|
||||
(Array.isArray(status?.modified_files) && (status.modified_files as unknown[]).length > 0) ||
|
||||
(Array.isArray(status?.staged_files) && (status.staged_files as unknown[]).length > 0);
|
||||
return hasChanges ? 'changes' : 'synced';
|
||||
}
|
||||
|
||||
// #endregion GitUtils
|
||||
|
||||
Reference in New Issue
Block a user