117 lines
4.5 KiB
TypeScript
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, '<')
|
|
.replace(/>/g, '>')
|
|
.replace(/"/g, '"');
|
|
}
|
|
|
|
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>`;
|
|
}
|