git
This commit is contained in:
@@ -1,13 +1,14 @@
|
||||
<!-- #region GitWorkspacePanel [C:3] [TYPE Component] [SEMANTICS git, workspace, commit, diff, sync] -->
|
||||
<!-- @BRIEF Git workspace panel: sync, commit message input, diff viewer, commit action. -->
|
||||
<!-- #region GitWorkspacePanel [C:4] [TYPE Component] [SEMANTICS git, workspace, commit, diff, sync] -->
|
||||
<!-- @BRIEF Git workspace panel: sync, commit, lazy-chunked diff viewer (IntersectionObserver). -->
|
||||
<!-- @LAYER UI -->
|
||||
<!-- @RELATION DEPENDS_ON -> [GitUtils] -->
|
||||
<!-- @RELATION CALLS -> [EXT:frontend:gitService] -->
|
||||
<!-- @PRE hasWorkspaceChanges and workspaceDiff are propagated from GitManager. -->
|
||||
<!-- @UX_STATE Idle -> "Нет изменений" placeholder when !hasWorkspaceChanges. -->
|
||||
<!-- @UX_STATE Changes -> workspaceDiff rendered when hasWorkspaceChanges && workspaceDiff. -->
|
||||
<!-- @UX_STATE Loading -> GeneratingMessage spinner, workspace loading. -->
|
||||
<!-- @UX_STATE Changes -> workspaceDiff rendered in chunks as user scrolls. -->
|
||||
<!-- @UX_STATE Loading -> GeneratingMessage spinner, diff-chunks loading indicator. -->
|
||||
<!-- @UX_STATE Error -> Toast on failure. -->
|
||||
<!-- @UX_RECOVERY Scroll down to load more diff chunks; sentinel triggers lazy render. -->
|
||||
<script>
|
||||
import { t } from "$lib/i18n";
|
||||
import { Button } from "$lib/ui";
|
||||
@@ -30,86 +31,265 @@
|
||||
onCommit,
|
||||
} = $props();
|
||||
|
||||
// ── Render diff as HTML via diff2html ──
|
||||
let diffHtml = $derived(workspaceDiff
|
||||
? Diff2Html.html(workspaceDiff, {
|
||||
// ── Lazy chunked diff rendering ──
|
||||
// Split diff into per-file sections on "diff --git " header
|
||||
let diffChunks = $derived.by(() => {
|
||||
if (!workspaceDiff) return [];
|
||||
const header = 'diff --git ';
|
||||
const parts = [];
|
||||
let start = 0;
|
||||
while (true) {
|
||||
const idx = workspaceDiff.indexOf(header, start);
|
||||
if (idx < 0) break;
|
||||
// Include the "diff --git " prefix in the chunk
|
||||
const chunkStart = idx;
|
||||
const nextIdx = workspaceDiff.indexOf(header, idx + 1);
|
||||
const chunkEnd = nextIdx >= 0 ? nextIdx : workspaceDiff.length;
|
||||
parts.push(workspaceDiff.slice(chunkStart, chunkEnd));
|
||||
start = nextIdx >= 0 ? nextIdx : workspaceDiff.length;
|
||||
if (nextIdx < 0) break;
|
||||
}
|
||||
// Fallback: if no "diff --git" headers, treat entire diff as one chunk
|
||||
if (parts.length === 0 && workspaceDiff.trim()) {
|
||||
parts.push(workspaceDiff);
|
||||
}
|
||||
return parts;
|
||||
});
|
||||
|
||||
const CHUNKS_PER_PAGE = 3; // render 3 file diffs at a time
|
||||
let renderedChunkCount = $state(0);
|
||||
let sentinelEl = $state(null);
|
||||
let chunkLoading = $state(false);
|
||||
|
||||
// When diff changes, reset to first chunk page
|
||||
$effect(() => {
|
||||
if (workspaceDiff) {
|
||||
renderedChunkCount = Math.min(CHUNKS_PER_PAGE, diffChunks.length);
|
||||
}
|
||||
});
|
||||
|
||||
// IntersectionObserver on sentinel to load more chunks
|
||||
$effect(() => {
|
||||
const el = sentinelEl;
|
||||
if (!el) return;
|
||||
if (renderedChunkCount >= diffChunks.length) return;
|
||||
const observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
for (const entry of entries) {
|
||||
if (entry.isIntersecting && renderedChunkCount < diffChunks.length) {
|
||||
chunkLoading = true;
|
||||
// Yield to let browser paint the loading indicator
|
||||
requestAnimationFrame(() => {
|
||||
requestAnimationFrame(() => {
|
||||
renderedChunkCount = Math.min(
|
||||
renderedChunkCount + CHUNKS_PER_PAGE,
|
||||
diffChunks.length
|
||||
);
|
||||
chunkLoading = false;
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
{ rootMargin: '200px 0px' } // trigger 200px before sentinel enters viewport
|
||||
);
|
||||
observer.observe(el);
|
||||
return () => observer.disconnect();
|
||||
});
|
||||
|
||||
// Render only visible chunks via Diff2Html individually
|
||||
let renderedHtml = $derived.by(() => {
|
||||
const count = renderedChunkCount;
|
||||
if (count === 0 || diffChunks.length === 0) return '';
|
||||
const visible = diffChunks.slice(0, count).join('\n');
|
||||
try {
|
||||
return Diff2Html.html(visible, {
|
||||
outputFormat: 'side-by-side',
|
||||
drawFileList: true,
|
||||
matching: 'lines',
|
||||
highlight: true,
|
||||
})
|
||||
: '');
|
||||
});
|
||||
} catch (_e) {
|
||||
return '<div class="p-4 text-sm text-red-500">Failed to render diff</div>';
|
||||
}
|
||||
});
|
||||
|
||||
const totalChunks = $derived(diffChunks.length);
|
||||
const hasMoreChunks = $derived(renderedChunkCount < totalChunks);
|
||||
</script>
|
||||
|
||||
<div class="grid grid-cols-1 gap-4 lg:grid-cols-2">
|
||||
<div class="space-y-4">
|
||||
<Button
|
||||
variant="secondary"
|
||||
onclick={onSync}
|
||||
disabled={loading}
|
||||
isLoading={loading}
|
||||
class="w-full"
|
||||
>
|
||||
🔄 Синхронизировать из Superset
|
||||
</Button>
|
||||
<div class="flex min-h-0 flex-1 flex-col gap-4 lg:flex-row">
|
||||
<!-- Left sidebar: commit controls (sticky, no scroll) -->
|
||||
<div class="w-full shrink-0 space-y-4 lg:w-80 xl:w-96">
|
||||
<!-- Sync button -->
|
||||
<div class="rounded-lg border border-slate-200 bg-white p-4 shadow-sm">
|
||||
<Button
|
||||
variant="secondary"
|
||||
onclick={onSync}
|
||||
disabled={loading}
|
||||
isLoading={loading}
|
||||
class="w-full"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="-ml-1 mr-1.5 h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"/></svg>
|
||||
Синхронизировать из Superset
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div class="rounded-lg border border-slate-200 p-3">
|
||||
<div class="mb-2 flex items-center justify-between">
|
||||
<!-- Commit message -->
|
||||
<div class="rounded-lg border border-slate-200 bg-white p-4 shadow-sm">
|
||||
<div class="mb-3 flex items-center justify-between">
|
||||
<h3 class="text-sm font-semibold text-slate-700">Сообщение коммита</h3>
|
||||
<button
|
||||
class="inline-flex items-center text-xs text-primary hover:text-blue-800 disabled:opacity-50"
|
||||
class="inline-flex items-center gap-1 rounded-md px-2 py-1 text-xs font-medium text-blue-600 transition-colors hover:bg-blue-50 disabled:opacity-50"
|
||||
onclick={onGenerateMessage}
|
||||
disabled={generatingMessage || workspaceLoading}
|
||||
>
|
||||
✨ Сгенерировать LLM
|
||||
<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="M9.813 15.904L9 18.75l-.813-2.846a4.5 4.5 0 00-3.09-3.09L2.25 12l2.846-.813a4.5 4.5 0 003.09-3.09L9 5.25l.813 2.846a4.5 4.5 0 003.09 3.09L15.75 12l-2.846.813a4.5 4.5 0 00-3.09 3.09zM18.259 8.715L18 9.75l-.259-1.035a3.375 3.375 0 00-2.455-2.456L14.25 6l1.036-.259a3.375 3.375 0 002.455-2.456L18 2.25l.259 1.035a3.375 3.375 0 002.455 2.456L21.75 6l-1.036.259a3.375 3.375 0 00-2.455 2.456z"/></svg>
|
||||
LLM
|
||||
</button>
|
||||
</div>
|
||||
<textarea
|
||||
bind:value={commitMessage}
|
||||
class={`h-36 w-full resize-none rounded border p-2 outline-none focus-visible:ring-2 focus-visible:ring-primary-ring ${generatingMessage ? 'animate-pulse bg-slate-100' : ''}`}
|
||||
class={`h-32 w-full resize-none rounded-lg border border-slate-200 p-3 text-sm outline-none transition-colors focus:border-blue-400 focus:ring-2 focus:ring-blue-100 ${generatingMessage ? 'animate-pulse bg-slate-50' : 'bg-white'}`}
|
||||
placeholder="Опишите изменения..."
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
<div class="rounded-lg border border-slate-200 bg-slate-50 px-3 py-2 text-xs text-slate-600">
|
||||
Файлов с изменениями: {changedFilesCount}
|
||||
</div>
|
||||
<!-- Commit action -->
|
||||
<div class="rounded-lg border border-slate-200 bg-white p-4 shadow-sm">
|
||||
<div class="mb-3 flex items-center gap-2 rounded-lg bg-slate-50 px-3 py-2 text-sm text-slate-600">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 text-slate-400" 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-slate-800">{changedFilesCount}</strong>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
onclick={onCommit}
|
||||
disabled={committing || workspaceLoading || !commitMessage || !hasWorkspaceChanges}
|
||||
isLoading={committing}
|
||||
class="w-full"
|
||||
>
|
||||
Зафиксировать (Commit)
|
||||
</Button>
|
||||
<label class="flex items-center gap-2 text-xs text-slate-600">
|
||||
<input type="checkbox" bind:checked={autoPushAfterCommit} />
|
||||
{$t.git?.auto_push_after_commit || 'Сделать push после commit в'} {pushProviderLabel}
|
||||
</label>
|
||||
<Button
|
||||
onclick={onCommit}
|
||||
disabled={committing || workspaceLoading || !commitMessage || !hasWorkspaceChanges}
|
||||
isLoading={committing}
|
||||
class="w-full"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="-ml-1 mr-1.5 h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5"/></svg>
|
||||
Зафиксировать (Commit)
|
||||
</Button>
|
||||
|
||||
<label class="mt-3 flex items-center gap-2.5 rounded-md border border-slate-100 bg-slate-50 px-3 py-2 text-xs text-slate-600 transition-colors hover:bg-slate-100">
|
||||
<input type="checkbox" bind:checked={autoPushAfterCommit} class="h-4 w-4 rounded border-slate-300 text-blue-600 focus:ring-blue-500" />
|
||||
<span>{$t.git?.auto_push_after_commit || 'Сделать push после commit в'} <strong class="font-medium text-slate-700">{pushProviderLabel}</strong></span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex min-h-[420px] flex-col overflow-hidden rounded-lg border border-slate-200 bg-slate-50">
|
||||
<div class="border-b bg-slate-100 px-3 py-2 text-xs font-semibold uppercase tracking-wide text-slate-600">
|
||||
Diff (изменения)
|
||||
<!-- Right area: diff preview — lazy chunked rendering -->
|
||||
<div class="flex min-h-0 flex-1 flex-col overflow-hidden rounded-lg border border-slate-200 bg-white shadow-sm">
|
||||
<div class="flex items-center justify-between border-b border-slate-200 bg-slate-50 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-slate-500" 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-slate-700">Diff (изменения)</span>
|
||||
</div>
|
||||
{#if hasWorkspaceChanges}
|
||||
<span class="inline-flex items-center gap-1 rounded-full bg-blue-50 px-2.5 py-0.5 text-xs font-medium text-blue-700 ring-1 ring-inset ring-blue-200">
|
||||
{changedFilesCount} файлов
|
||||
{#if totalChunks > 0}
|
||||
<span class="text-blue-400">· {renderedChunkCount}/{totalChunks}</span>
|
||||
{/if}
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="flex-1 overflow-auto p-3">
|
||||
<div class="flex-1 overflow-auto bg-white p-4" id="diff-scroll-container">
|
||||
{#if workspaceLoading}
|
||||
<div class="space-y-2">
|
||||
{#each Array(6) as _}
|
||||
<div class="h-4 animate-pulse rounded bg-slate-200"></div>
|
||||
<div class="space-y-3">
|
||||
{#each Array(8) as _}
|
||||
<div class="h-4 animate-pulse rounded bg-slate-100"></div>
|
||||
{/each}
|
||||
<div class="mt-6 grid grid-cols-2 gap-3">
|
||||
{#each Array(6) as _}
|
||||
<div class="h-6 animate-pulse rounded bg-slate-50"></div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{:else if hasWorkspaceChanges && diffHtml}
|
||||
<div class="diff-view">{@html diffHtml}</div>
|
||||
{:else if hasWorkspaceChanges && renderedHtml}
|
||||
<div class="diff-view">{@html renderedHtml}</div>
|
||||
<!-- Sentinel for IntersectionObserver — triggers next chunk load -->
|
||||
{#if hasMoreChunks}
|
||||
<div
|
||||
bind:this={sentinelEl}
|
||||
class="flex items-center justify-center py-6 text-sm text-slate-400"
|
||||
role="status"
|
||||
aria-live="polite"
|
||||
>
|
||||
{#if chunkLoading}
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="h-4 w-4 animate-spin rounded-full border-b-2 border-blue-500"></div>
|
||||
<span>{$t.git?.diff_loading || 'Загрузка изменений...'}</span>
|
||||
</div>
|
||||
{:else}
|
||||
<span class="cursor-pointer text-blue-500 hover:text-blue-700" onclick={() => {
|
||||
renderedChunkCount = Math.min(renderedChunkCount + CHUNKS_PER_PAGE, totalChunks);
|
||||
}}>
|
||||
{$t.git?.diff_show_more?.replace('{count}', totalChunks - renderedChunkCount) || `Показать ещё (${totalChunks - renderedChunkCount} файлов)`}
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
{:else if totalChunks > 0}
|
||||
<div class="flex items-center justify-center py-4 text-xs text-slate-400">
|
||||
{$t.git?.diff_all_shown?.replace('{count}', totalChunks) || `Показаны все изменения (${totalChunks} файлов)`}
|
||||
</div>
|
||||
{/if}
|
||||
{:else if hasWorkspaceChanges}
|
||||
<pre class="overflow-x-auto whitespace-pre-wrap font-mono text-xs leading-relaxed text-slate-500">Загрузка diff... (повторите синхронизацию)</pre>
|
||||
<div class="flex h-full flex-col items-center justify-center gap-3 text-sm text-slate-400">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-12 w-12 text-slate-200" 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>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="flex h-full items-center justify-center text-sm text-slate-500">
|
||||
Нет изменений для коммита
|
||||
<div class="flex h-full flex-col items-center justify-center gap-3 text-sm text-slate-400">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-16 w-16 text-slate-200" 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-slate-300">Синхронизируйте дашборд, чтобы увидеть изменения</span>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Custom styles for diff2html -->
|
||||
<style>
|
||||
:global(.diff-view .d2h-wrapper) {
|
||||
font-size: 12px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
:global(.diff-view .d2h-file-header) {
|
||||
background: #f8fafc;
|
||||
border-color: #e2e8f0;
|
||||
padding: 8px 12px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
}
|
||||
:global(.diff-view .d2h-code-line) {
|
||||
padding: 0 12px;
|
||||
}
|
||||
:global(.diff-view .d2h-ins) {
|
||||
background-color: #f0fdf4;
|
||||
}
|
||||
:global(.diff-view .d2h-del) {
|
||||
background-color: #fef2f2;
|
||||
}
|
||||
:global(.diff-view .d2h-code-side-linenumber) {
|
||||
width: 48px;
|
||||
min-width: 48px;
|
||||
}
|
||||
:global(.diff-view table.d2h-diff-table) {
|
||||
width: 100%;
|
||||
}
|
||||
:global(.diff-view .d2h-side-sides) {
|
||||
min-width: 100%;
|
||||
}
|
||||
|
||||
/* Fix: absolute-positioned line numbers must scroll with their parent container */
|
||||
:global(.diff-view .d2h-file-side-diff) {
|
||||
position: relative;
|
||||
}
|
||||
:global(.diff-view .d2h-code-wrapper) {
|
||||
position: relative;
|
||||
}
|
||||
</style>
|
||||
<!-- #endregion GitWorkspacePanel -->
|
||||
|
||||
Reference in New Issue
Block a user