Files
ss-tools/frontend/src/components/git/GitManager.svelte
2026-02-19 18:24:36 +03:00

303 lines
11 KiB
Svelte
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!-- [DEF:GitManager:Component] -->
<!--
@SEMANTICS: git, manager, dashboard, version_control, initialization
@PURPOSE: Центральный компонент для управления Git-операциями конкретного дашборда.
@LAYER: Component
@RELATION: USES -> BranchSelector
@RELATION: USES -> CommitModal
@RELATION: USES -> CommitHistory
@RELATION: USES -> DeploymentModal
@RELATION: USES -> ConflictResolver
@RELATION: CALLS -> gitService
-->
<script>
// [SECTION: IMPORTS]
import { onMount } from 'svelte';
import { gitService } from '../../services/gitService';
import { addToast as toast } from '../../lib/toasts.js';
import { t } from '../../lib/i18n';
import { Button, Card, PageHeader, Select, Input } from '../../lib/ui';
import BranchSelector from './BranchSelector.svelte';
import CommitModal from './CommitModal.svelte';
import CommitHistory from './CommitHistory.svelte';
import DeploymentModal from './DeploymentModal.svelte';
import ConflictResolver from './ConflictResolver.svelte';
// [/SECTION]
// [SECTION: PROPS]
let {
dashboardId,
dashboardTitle = "",
show = false,
} = $props();
// [/SECTION]
// [SECTION: STATE]
let currentBranch = 'main';
let showCommitModal = false;
let showDeployModal = false;
let showHistory = true;
let showConflicts = false;
let conflicts = [];
let loading = false;
let initialized = false;
let checkingStatus = true;
// Initialization form state
let configs = [];
let selectedConfigId = "";
let remoteUrl = "";
// [/SECTION]
// [DEF:checkStatus:Function]
/**
* @purpose Проверяет, инициализирован ли репозиторий для данного дашборда.
* @pre Component is mounted and has dashboardId.
* @post initialized state is set; configs loaded if not initialized.
*/
async function checkStatus() {
checkingStatus = true;
try {
// If we can get branches, it means repo exists
await gitService.getBranches(dashboardId);
initialized = true;
} catch (e) {
initialized = false;
// Load configs if not initialized
configs = await gitService.getConfigs();
if (configs.length > 0) selectedConfigId = configs[0].id;
} finally {
checkingStatus = false;
}
}
// [/DEF:checkStatus:Function]
// [DEF:handleInit:Function]
/**
* @purpose Инициализирует репозиторий для дашборда.
* @pre selectedConfigId and remoteUrl are provided.
* @post Repository is created on backend; initialized set to true.
*/
async function handleInit() {
if (!selectedConfigId || !remoteUrl) {
toast('Please select a Git server and provide remote URL', 'error');
return;
}
loading = true;
try {
await gitService.initRepository(dashboardId, selectedConfigId, remoteUrl);
toast('Repository initialized successfully', 'success');
initialized = true;
} catch (e) {
toast(e.message, 'error');
} finally {
loading = false;
}
}
// [/DEF:handleInit:Function]
// [DEF:handleSync:Function]
/**
* @purpose Синхронизирует состояние Superset с локальным Git-репозиторием.
* @pre Repository is initialized.
* @post Dashboard YAMLs are exported to Git and staged.
*/
async function handleSync() {
loading = true;
try {
// Try to get selected environment from localStorage (set by EnvSelector)
const sourceEnvId = localStorage.getItem('selected_env_id');
await gitService.sync(dashboardId, sourceEnvId);
toast('Dashboard state synced to Git', 'success');
} catch (e) {
toast(e.message, 'error');
} finally {
loading = false;
}
}
// [/DEF:handleSync:Function]
// [DEF:handlePush:Function]
/**
* @purpose Pushes local commits to the remote repository.
* @pre Repository is initialized and has commits.
* @post Changes are pushed to origin.
*/
async function handlePush() {
loading = true;
try {
await gitService.push(dashboardId);
toast('Changes pushed to remote', 'success');
} catch (e) {
toast(e.message, 'error');
} finally {
loading = false;
}
}
// [/DEF:handlePush:Function]
// [DEF:handlePull:Function]
/**
* @purpose Pulls changes from the remote repository.
* @pre Repository is initialized.
* @post Local branch is updated with remote changes.
*/
async function handlePull() {
loading = true;
try {
await gitService.pull(dashboardId);
toast('Changes pulled from remote', 'success');
} catch (e) {
toast(e.message, 'error');
} finally {
loading = false;
}
}
// [/DEF:handlePull:Function]
onMount(checkStatus);
</script>
<!-- [SECTION: TEMPLATE] -->
{#if show}
<div class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div class="bg-white p-6 rounded-lg shadow-2xl w-full max-w-4xl max-h-[90vh] overflow-y-auto">
<PageHeader title="{$t.git.management}: {dashboardTitle}">
<div slot="subtitle" class="text-sm text-gray-500">ID: {dashboardId}</div>
<div slot="actions">
<button on:click={() => show = false} class="text-gray-400 hover:text-gray-600 transition-colors">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" 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>
</PageHeader>
{#if checkingStatus}
<div class="flex justify-center py-12">
<div class="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600"></div>
</div>
{:else if !initialized}
<div class="max-w-md mx-auto py-8">
<Card>
<p class="text-sm text-gray-600 mb-6">
{$t.git.not_linked}
</p>
<div class="space-y-6">
<Select
label={$t.git.server}
bind:value={selectedConfigId}
options={configs.map(c => ({ value: c.id, label: `${c.name} (${c.provider})` }))}
/>
{#if configs.length === 0}
<p class="text-xs text-red-500 -mt-4">No Git servers configured. Go to Settings -> Git to add one.</p>
{/if}
<Input
label={$t.git.remote_url}
bind:value={remoteUrl}
placeholder="https://github.com/org/repo.git"
/>
<Button
on:click={handleInit}
disabled={loading || configs.length === 0}
isLoading={loading}
class="w-full"
>
{$t.git.init_repo}
</Button>
</div>
</Card>
</div>
{:else}
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
<!-- Left Column: Controls -->
<div class="md:col-span-1 space-y-6">
<section>
<h3 class="text-sm font-semibold text-gray-400 uppercase tracking-wider mb-3">{$t.git.branch}</h3>
<BranchSelector {dashboardId} bind:currentBranch />
</section>
<section class="space-y-3">
<h3 class="text-sm font-semibold text-gray-400 uppercase tracking-wider mb-3">{$t.git.actions}</h3>
<Button
variant="secondary"
on:click={handleSync}
disabled={loading}
class="w-full"
>
{$t.git.sync}
</Button>
<Button
on:click={() => showCommitModal = true}
disabled={loading}
class="w-full"
>
{$t.git.commit}
</Button>
<div class="grid grid-cols-2 gap-3">
<Button
variant="ghost"
on:click={handlePull}
disabled={loading}
class="border border-gray-200"
>
{$t.git.pull}
</Button>
<Button
variant="ghost"
on:click={handlePush}
disabled={loading}
class="border border-gray-200"
>
{$t.git.push}
</Button>
</div>
</section>
<section>
<h3 class="text-sm font-semibold text-gray-400 uppercase tracking-wider mb-3">{$t.git.deployment}</h3>
<Button
variant="primary"
on:click={() => showDeployModal = true}
disabled={loading}
class="w-full bg-green-600 hover:bg-green-700 focus-visible:ring-green-500"
>
{$t.git.deploy}
</Button>
</section>
</div>
<!-- Right Column: History -->
<div class="md:col-span-2 border-l pl-6">
<CommitHistory {dashboardId} />
</div>
</div>
{/if}
</div>
</div>
{/if}
<CommitModal
{dashboardId}
bind:show={showCommitModal}
on:commit={() => { /* Refresh history */ }}
/>
<DeploymentModal
{dashboardId}
bind:show={showDeployModal}
/>
<ConflictResolver
{conflicts}
bind:show={showConflicts}
on:resolve={() => { /* Handle resolution */ }}
/>
<!-- [/SECTION] -->
<!-- [/DEF:GitManager:Component] -->