141 lines
4.8 KiB
JavaScript
141 lines
4.8 KiB
JavaScript
function escapeHtml(value) {
|
|
return String(value)
|
|
.replace(/&/g, '&')
|
|
.replace(/</g, '<')
|
|
.replace(/>/g, '>')
|
|
.replace(/"/g, '"');
|
|
}
|
|
|
|
function renderNode(node) {
|
|
if (node.type === 'workflow') {
|
|
const badgeClass = node.active ? 'bg-success' : 'bg-secondary';
|
|
const badgeText = node.active ? '有効' : '無効';
|
|
return `<li class="list-group-item d-flex justify-content-between align-items-center" data-workflow-id="${escapeHtml(node.id)}" role="button">
|
|
<span>${escapeHtml(node.name)}</span>
|
|
<span class="badge ${badgeClass}">${badgeText}</span>
|
|
</li>`;
|
|
}
|
|
return `<li class="list-group-item">
|
|
<div class="fw-bold mb-1">📁 ${escapeHtml(node.name)}</div>
|
|
<ul class="list-group ms-3">${renderFolderTree(node.children)}</ul>
|
|
</li>`;
|
|
}
|
|
|
|
function renderFolderTree(tree) {
|
|
if (!tree || tree.length === 0) return '';
|
|
return tree.map(renderNode).join('\n');
|
|
}
|
|
|
|
const EXECUTION_BADGE = {
|
|
success: 'bg-success',
|
|
error: 'bg-danger',
|
|
crashed: 'bg-danger',
|
|
running: 'bg-primary',
|
|
waiting: 'bg-warning text-dark',
|
|
canceled: 'bg-secondary',
|
|
new: 'bg-secondary',
|
|
unknown: 'bg-secondary',
|
|
};
|
|
|
|
const EXECUTION_STATUS_LABEL = {
|
|
success: '成功',
|
|
error: 'エラー',
|
|
crashed: 'クラッシュ',
|
|
running: '実行中',
|
|
waiting: '待機中',
|
|
canceled: 'キャンセル',
|
|
new: '新規',
|
|
unknown: '不明',
|
|
};
|
|
|
|
function formatJst(isoString) {
|
|
if (!isoString) return '';
|
|
const utc = new Date(isoString);
|
|
const jst = new Date(utc.getTime() + 9 * 60 * 60 * 1000);
|
|
const pad = (n) => String(n).padStart(2, '0');
|
|
return `${jst.getUTCFullYear()}-${pad(jst.getUTCMonth() + 1)}-${pad(jst.getUTCDate())} ${pad(jst.getUTCHours())}:${pad(jst.getUTCMinutes())}:${pad(jst.getUTCSeconds())}`;
|
|
}
|
|
|
|
function renderExecutionList(executions) {
|
|
if (!executions || executions.length === 0) {
|
|
return '<p class="text-muted">実行履歴なし</p>';
|
|
}
|
|
const rows = executions
|
|
.map((e) => {
|
|
const badgeClass = EXECUTION_BADGE[e.status] || 'bg-secondary';
|
|
const statusLabel = EXECUTION_STATUS_LABEL[e.status] || e.status;
|
|
return `<tr data-execution-id="${escapeHtml(e.id)}" role="button">
|
|
<td>${escapeHtml(formatJst(e.startedAt))}</td>
|
|
<td><span class="badge ${badgeClass}">${escapeHtml(statusLabel)}</span></td>
|
|
</tr>`;
|
|
})
|
|
.join('\n');
|
|
return `<table class="table table-sm table-hover">
|
|
<thead><tr><th>開始日時(JST)</th><th>状態</th></tr></thead>
|
|
<tbody>${rows}</tbody>
|
|
</table>`;
|
|
}
|
|
|
|
function renderExecutionDetail(execution) {
|
|
const runData = (execution.data && execution.data.resultData && execution.data.resultData.runData) || {};
|
|
const rows = Object.entries(runData)
|
|
.map(([nodeName, runs]) => {
|
|
const lastRun = runs[runs.length - 1] || {};
|
|
const hasError = Boolean(lastRun.error);
|
|
const badgeClass = hasError ? 'bg-danger' : 'bg-success';
|
|
const badgeText = hasError ? 'エラー' : '成功';
|
|
const errorMessage = hasError ? escapeHtml(lastRun.error.message || '') : '';
|
|
return `<tr>
|
|
<td>${escapeHtml(nodeName)}</td>
|
|
<td><span class="badge ${badgeClass}">${badgeText}</span></td>
|
|
<td>${errorMessage}</td>
|
|
</tr>`;
|
|
})
|
|
.join('\n');
|
|
return `<table class="table table-sm">
|
|
<thead><tr><th>ノード</th><th>結果</th><th>エラー内容</th></tr></thead>
|
|
<tbody>${rows}</tbody>
|
|
</table>`;
|
|
}
|
|
|
|
function renderN8nPage(tree) {
|
|
const treeHtml = renderFolderTree(tree);
|
|
return `<!DOCTYPE html>
|
|
<html lang="ja">
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
<title>n8nワークフロー</title>
|
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
|
|
</head>
|
|
<body class="bg-body-tertiary">
|
|
<nav class="navbar navbar-dark bg-dark mb-4">
|
|
<div class="container-fluid">
|
|
<span class="navbar-brand mb-0 h1">n8nワークフロー</span>
|
|
<a class="btn btn-outline-light btn-sm" href="/">ダッシュボード</a>
|
|
</div>
|
|
</nav>
|
|
<main class="container-fluid pb-5">
|
|
<div class="row">
|
|
<div class="col-12 col-md-4">
|
|
<h2 class="h5">ワークフロー一覧</h2>
|
|
<ul class="list-group" id="folderTree">${treeHtml}</ul>
|
|
</div>
|
|
<div class="col-12 col-md-4">
|
|
<h2 class="h5">実行履歴</h2>
|
|
<div id="executionList"><p class="text-muted">ワークフローを選択してください</p></div>
|
|
</div>
|
|
<div class="col-12 col-md-4">
|
|
<h2 class="h5">実行詳細</h2>
|
|
<div id="executionDetail"><p class="text-muted">実行を選択してください</p></div>
|
|
</div>
|
|
</div>
|
|
</main>
|
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
|
|
<script src="/n8n.js"></script>
|
|
</body>
|
|
</html>`;
|
|
}
|
|
|
|
module.exports = { renderFolderTree, renderExecutionList, renderExecutionDetail, renderN8nPage };
|