posimai-root/posimai-guard-ext/src/ui/resultsPanel.ts

117 lines
4.5 KiB
TypeScript

import * as vscode from 'vscode';
import { ScanIssue } from '../scanner/prompt';
export class ResultsPanel {
private static _current: ResultsPanel | undefined;
private readonly _panel: vscode.WebviewPanel;
private _disposables: vscode.Disposable[] = [];
static show(context: vscode.ExtensionContext, issues: ScanIssue[], source: string): void {
if (ResultsPanel._current) {
ResultsPanel._current._panel.reveal(vscode.ViewColumn.Beside);
ResultsPanel._current._update(issues, source);
return;
}
ResultsPanel._current = new ResultsPanel(context, issues, source);
}
private constructor(
context: vscode.ExtensionContext,
issues: ScanIssue[],
source: string,
) {
this._panel = vscode.window.createWebviewPanel(
'guardResults',
'Guard スキャン結果',
vscode.ViewColumn.Beside,
{ enableScripts: false, retainContextWhenHidden: true },
);
this._panel.onDidDispose(() => {
ResultsPanel._current = undefined;
this._disposables.forEach(d => d.dispose());
}, null, this._disposables);
this._update(issues, source);
}
private _update(issues: ScanIssue[], source: string): void {
this._panel.title = `Guard: ${source}`;
this._panel.webview.html = buildHtml(issues, source);
}
}
function esc(s: string): string {
return s
.replace(/&/g, '&')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;');
}
function buildHtml(issues: ScanIssue[], source: string): string {
const danger = issues.filter(i => i.severity === 'danger');
const warning = issues.filter(i => i.severity === 'warning');
const info = issues.filter(i => i.severity === 'info');
const badge = (label: string, count: number, color: string) =>
count > 0
? `<span style="display:inline-block;padding:2px 8px;border-radius:4px;font-size:11px;font-weight:600;background:${color};color:#fff;margin-right:6px">${label} ${count}</span>`
: '';
const issueHtml = (issue: ScanIssue) => {
const color =
issue.severity === 'danger' ? '#e53e3e' :
issue.severity === 'warning' ? '#d69e2e' : '#3182ce';
const bg =
issue.severity === 'danger' ? '#fff5f5' :
issue.severity === 'warning' ? '#fffff0' : '#ebf8ff';
const fixSection = issue.fix
? `<pre style="margin:8px 0 0;padding:10px;background:#1a1a2e;color:#e2e8f0;border-radius:4px;font-size:11px;overflow-x:auto;white-space:pre-wrap;word-break:break-all">${esc(issue.fix)}</pre>`
: '';
const location = issue.line != null ? `${esc(issue.file)}:${issue.line}` : esc(issue.file);
return `
<div style="margin-bottom:10px;border:1px solid ${color}33;border-left:3px solid ${color};border-radius:4px;padding:12px 14px;background:${bg}">
<div style="display:flex;align-items:flex-start;gap:8px;margin-bottom:6px">
<span style="color:${color};font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.05em;flex-shrink:0;padding-top:1px">${esc(issue.severity)}</span>
<span style="font-weight:600;font-size:13px;color:#1a202c">${esc(issue.title)}</span>
</div>
<div style="font-size:12px;color:#4a5568;margin-bottom:6px;line-height:1.5">${esc(issue.description)}</div>
<div style="font-size:11px;color:#718096;font-family:monospace">${location}</div>
${fixSection}
</div>`;
};
const allHtml = [...danger, ...warning, ...info].map(issueHtml).join('');
return `<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src 'unsafe-inline';">
<meta name="viewport" content="width=device-width,initial-scale=1">
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: #f7fafc; color: #2d3748; padding: 20px; }
h1 { font-size: 15px; font-weight: 700; margin-bottom: 4px; color: #1a202c; }
.meta { font-size: 11px; color: #718096; margin-bottom: 16px; }
.summary { margin-bottom: 20px; }
.empty { text-align: center; padding: 40px; color: #718096; font-size: 14px; }
</style>
</head>
<body>
<h1>Guard スキャン結果</h1>
<div class="meta">${esc(source)}${issues.length} 件の指摘</div>
<div class="summary">
${badge('危険', danger.length, '#e53e3e')}
${badge('警告', warning.length, '#d69e2e')}
${badge('情報', info.length, '#3182ce')}
</div>
${issues.length === 0
? '<div class="empty">問題は検出されませんでした</div>'
: allHtml
}
</body>
</html>`;
}