improve launcher history and runtime config UX
ci/woodpecker/push/woodpecker Pipeline was successful

This commit is contained in:
2026-07-07 09:43:18 +09:00
parent ff23415818
commit a48055bf65
7 changed files with 182 additions and 81 deletions
+111 -22
View File
@@ -50,6 +50,14 @@ const commitRangeLabels: Record<CommitRange, string> = {
const commitPageSize = 5;
type DiffFile = {
id: string;
path: string;
lines: string[];
added: number;
removed: number;
};
function repoFullName(repo: GiteaRepo) {
return (repo as GiteaRepo & { fullName?: string }).fullName ?? repo.full_name ?? normalizeRepository(repo.name);
}
@@ -130,38 +138,118 @@ function commitRangeSince(range: CommitRange) {
return date.toISOString();
}
function normalizeDiffPath(value?: string) {
if (!value) return '';
const path = value.trim();
if (path === '/dev/null') return path;
return path.replace(/^[ab]\//, '');
}
function diffPathFromHeader(line: string) {
const match = line.match(/^diff --git a\/(.+?) b\/(.+)$/);
return normalizeDiffPath(match?.[2] ?? match?.[1]);
}
function parseDiffFiles(diff: string): DiffFile[] {
if (!diff.trim()) {
return [{ id: 'empty', path: '변경 내역', lines: ['표시할 변경 내역이 없습니다.'], added: 0, removed: 0 }];
}
const files: DiffFile[] = [];
let current: DiffFile | null = null;
const pushCurrent = () => {
if (current) files.push(current);
};
diff.split(/\r?\n/).forEach((line) => {
if (line.startsWith('diff --git ')) {
pushCurrent();
current = {
id: `file-${files.length}`,
path: diffPathFromHeader(line) || '변경 파일',
lines: [line],
added: 0,
removed: 0
};
return;
}
if (!current) {
current = {
id: 'file-0',
path: '변경 내역',
lines: [],
added: 0,
removed: 0
};
}
if (line.startsWith('+++ ')) {
const nextPath = normalizeDiffPath(line.slice(4));
if (nextPath && nextPath !== '/dev/null') current.path = nextPath;
}
if (line.startsWith('+') && !line.startsWith('+++')) current.added += 1;
if (line.startsWith('-') && !line.startsWith('---')) current.removed += 1;
current.lines.push(line);
});
pushCurrent();
return files;
}
function diffLineClass(line: string) {
if (line.startsWith('+++') || line.startsWith('---')) {
return 'bg-sky-500/10 text-sky-200';
return 'bg-[#111827] text-sky-200';
}
if (line.startsWith('@@')) {
return 'bg-violet-500/15 text-violet-200';
return 'bg-[#1e1b4b] text-violet-100';
}
if (line.startsWith('+')) {
return 'bg-emerald-500/15 text-emerald-100';
return 'bg-[#123524] text-emerald-50';
}
if (line.startsWith('-')) {
return 'bg-rose-500/15 text-rose-100';
return 'bg-[#3a1418] text-rose-50';
}
if (line.startsWith('diff --git') || line.startsWith('index ')) {
return 'bg-muted/40 text-foreground';
if (
line.startsWith('diff --git')
|| line.startsWith('index ')
|| line.startsWith('new file mode')
|| line.startsWith('deleted file mode')
) {
return 'bg-[#0f172a] text-slate-200';
}
return 'text-muted-foreground';
return 'text-slate-300';
}
function DiffViewer({ diff }: { diff: string }) {
const lines = diff ? diff.split(/\r?\n/) : ['표시할 변경 내역이 없습니다.'];
const files = parseDiffFiles(diff);
return (
<div className="max-h-[70vh] overflow-auto rounded-md border border-border bg-[#0b0f14] font-mono text-xs leading-5">
{lines.map((line, index) => (
<div key={`${index}-${line}`} className={`grid min-w-max grid-cols-[64px_minmax(0,1fr)] ${diffLineClass(line)}`}>
<span className="select-none border-r border-white/10 px-3 py-0.5 text-right text-[11px] text-muted-foreground">
{index + 1}
</span>
<pre className="whitespace-pre px-3 py-0.5">{line || ' '}</pre>
</div>
))}
<div className="h-[72vh] overflow-auto rounded-md border border-border bg-[#080b10] p-3 font-mono text-xs leading-5">
<div className="space-y-4">
{files.map((file) => (
<section key={file.id} className="min-w-max overflow-hidden rounded-md border border-slate-700/80 bg-[#0b0f14]">
<div className="sticky top-0 z-10 flex items-center justify-between gap-4 border-b border-slate-700/80 bg-[#111827] px-4 py-2">
<div className="min-w-0">
<p className="truncate text-[13px] font-semibold text-slate-100">{file.path}</p>
</div>
<div className="flex shrink-0 items-center gap-2 text-[11px]">
<span className="rounded bg-emerald-500/15 px-2 py-0.5 font-semibold text-emerald-100">+{file.added}</span>
<span className="rounded bg-rose-500/15 px-2 py-0.5 font-semibold text-rose-100">-{file.removed}</span>
</div>
</div>
{file.lines.map((line, index) => (
<div key={`${file.id}-${index}-${line}`} className={`grid grid-cols-[72px_minmax(0,1fr)] ${diffLineClass(line)}`}>
<span className="select-none border-r border-white/10 px-3 py-0.5 text-right text-[11px] text-slate-500">
{index + 1}
</span>
<pre className="whitespace-pre px-3 py-0.5">{line || ' '}</pre>
</div>
))}
</section>
))}
</div>
</div>
);
}
@@ -451,7 +539,7 @@ export function LauncherSourcePage() {
</div>
}
/>
<div className="overflow-x-auto">
<div className="min-h-[284px] overflow-x-auto">
<table className="min-w-[760px] w-full text-left text-sm">
<thead className="border-y border-border bg-muted/30 text-xs text-muted-foreground">
<tr>
@@ -465,17 +553,17 @@ export function LauncherSourcePage() {
<tbody>
{commitsLoading ? (
<tr>
<td colSpan={5} className="px-5 py-10 text-center text-sm text-muted-foreground">커밋을 불러오는 중입니다.</td>
<td colSpan={5} className="h-[240px] px-5 py-10 text-center text-sm text-muted-foreground">커밋을 불러오는 중입니다.</td>
</tr>
) : null}
{!commitsLoading && commitsError ? (
<tr>
<td colSpan={5} className="px-5 py-10 text-center text-sm text-muted-foreground">커밋 조회 실패: {commitsError}</td>
<td colSpan={5} className="h-[240px] px-5 py-10 text-center text-sm text-muted-foreground">커밋 조회 실패: {commitsError}</td>
</tr>
) : null}
{!commitsLoading && !commitsError && commits.length === 0 ? (
<tr>
<td colSpan={5} className="px-5 py-10 text-center text-sm text-muted-foreground">표시할 커밋이 없습니다.</td>
<td colSpan={5} className="h-[240px] px-5 py-10 text-center text-sm text-muted-foreground">표시할 커밋이 없습니다.</td>
</tr>
) : null}
{!commitsLoading && !commitsError ? commits.map((commit) => (
@@ -529,7 +617,8 @@ export function LauncherSourcePage() {
open={Boolean(diffTarget)}
title="커밋 내역 상세"
onClose={() => setDiffTarget(null)}
className="max-w-[min(96vw,1400px)]"
className="max-w-[min(98vw,1800px)]"
bodyClassName="max-h-[84vh] px-4 py-4"
footer={<Button variant="outline" onClick={() => setDiffTarget(null)}>닫기</Button>}
>
{diffTarget ? (