improve launcher history and runtime config UX
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/push/woodpecker Pipeline was successful
This commit is contained in:
@@ -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 ? (
|
||||
|
||||
Reference in New Issue
Block a user