Загрузка данных
return { statusClass, statusStyle };
}
/**
* Рендерит ссылку на отчёт запуска.
*/
renderReportLink(id, innerHtml, { linkClass = "text-blue-600 hover:underline", ariaLabel = "" } = {}) {
const safeId = this.escapeHtml(id ?? "-");
const label = ariaLabel
? this.escapeHtml(ariaLabel)
: `Открыть отчёт запуска #${safeId}`;
return (
`<a href="/reports/${safeId}" class="${linkClass}" target="_blank" rel="noopener noreferrer" ` +
`aria-label="${label}">` +
innerHtml +
`</a>`
);
}
/**
* Рендерит текстовый статус для legacy-записей без status_stats.
*/
renderLegacyStatusText(item) {
const statusValue = String(item.status || "").toLowerCase();
const status = this.escapeHtml(item.status || "-");
const { statusClass, statusStyle } = this.getLegacyStatusPresentation(statusValue);
return (
`<span class="font-semibold ${statusClass}" style="${statusStyle}">${status}</span>`
);
}
/**
* Рендерит цветную шкалу статусов тестов.
*/
renderStatusBar(stats, id) {
const segments = [
{ key: TEST_STATUS.PASSED, count: stats.passed || 0, label: TEST_STATUS.PASSED },
{ key: TEST_STATUS.FAILED, count: stats.failed || 0, label: TEST_STATUS.FAILED },
{ key: TEST_STATUS.BROKEN, count: stats.broken || 0, label: TEST_STATUS.BROKEN },
{ key: TEST_STATUS.SKIPPED, count: stats.skipped || 0, label: TEST_STATUS.SKIPPED },
].filter((segment) => segment.count > 0);
const totalCount = stats.total || segments.reduce((sum, segment) => sum + segment.count, 0);
const labelClass = this.getStatusBarLabelClass(totalCount);
const ariaLabel = segments
.map((segment) => `${segment.label}: ${segment.count}`)
.join(", ");
const segmentHtml = segments
.map((segment) => {
const share = totalCount > 0 ? segment.count / totalCount : 0;
const showLabel = share >= 0.08;
const title = `${segment.label}: ${segment.count}`;
const backgroundColor = STATUS_BAR_COLORS[segment.key] || STATUS_BAR_COLORS[TEST_STATUS.FAILED];
const labelHtml = showLabel
? `<span class="${labelClass} leading-none">${this.escapeHtml(String(segment.count))}</span>`
: "";
return (
`<div class="flex items-center justify-center min-w-0 text-white font-semibold" ` +
`style="flex:${segment.count};background-color:${backgroundColor}" ` +
`title="${this.escapeHtml(title)}">` +
labelHtml +
`</div>`
);
})
.join("");
const barHtml =
`<div class="flex h-6 w-full rounded overflow-hidden" role="img" ` +
`aria-label="${this.escapeHtml(ariaLabel)}">` +
segmentHtml +
`</div>`;
return this.renderReportLink(id, barHtml, {
linkClass: "block no-underline hover:opacity-90",
ariaLabel: `Открыть отчёт запуска #${id}`,
});
}
/**
* Рендерит ячейку статуса: no_data, legacy-текст или шкала.
*/
renderStatusCell(item) {
const statusValue = String(item.status || "").toLowerCase();
if (statusValue === TEST_STATUS.NO_DATA) {
return `<span class="text-sm font-semibold text-gray-400">${TEST_STATUS.NO_DATA}</span>`;
}
if (!item.status_stats) {
return this.renderLegacyStatusText(item);
}
if (item.status_stats.total === 0) {
return `<span class="text-sm font-semibold text-gray-400">${TEST_STATUS.NO_DATA}</span>`;
}
return this.renderStatusBar(item.status_stats, item.id);
}
/**
* Перерисовывает тело таблицы.
* При пустом списке — показывает сообщение.
*
* @param {Array<Object>} items - Массив элементов отчётов.
* @returns {void}
*/
renderRows(items = []) {
if (!this.tableBody) {
return;
}
if (!items.length) {
const emptyText = this.hasActiveFilters()
? "Ничего не найдено по выбранным фильтрам."
: "Отчётов пока нет.";
this.tableBody.innerHTML = `
<tr>
<td colspan="5" class="px-4 py-6 text-center text-sm text-gray-500">
${emptyText}
</td>
</tr>`;
this.showMessage(emptyText, false);
this.adjustTableHeight();
return;
}
this.showMessage("", false);
const rows = items
.map((item) => {
const id = this.escapeHtml(item.id ?? "-");
const startDate = this.escapeHtml(this.formatLocalDate(item.start_date));
const endDate = this.escapeHtml(this.formatLocalDate(item.end_date));
const stand = this.escapeHtml(item.stand || "-");
const statusCell = this.renderStatusCell(item);
return (
'<tr class="hover:bg-gray-50">' +
`<td class="px-4 py-2 text-sm">` +
`<a href="/reports/${id}" class="text-blue-600 hover:underline" target="_blank" rel="noopener noreferrer">` +
`Запуск #${id}` +
`</a>` +
`</td>` +
`<td class="px-4 py-2 text-sm text-gray-600">${startDate}</td>` +
`<td class="px-4 py-2 text-sm text-gray-600">${endDate}</td>` +
`<td class="px-4 py-2 text-sm text-gray-600">${stand}</td>` +
`<td class="px-4 py-2 text-sm w-full text-center">${statusCell}</td>` +
"</tr>"
);
})
.join("");
this.tableBody.innerHTML = rows;
this.adjustTableHeight();
}