Add pluggable live collectors and simplify API connect form
This commit is contained in:
@@ -13,8 +13,6 @@ let sourceType = 'archive';
|
||||
let apiConnectPayload = null;
|
||||
let collectionJob = null;
|
||||
let collectionJobPollTimer = null;
|
||||
let collectionJobScenario = [];
|
||||
let collectionJobScenarioIndex = 0;
|
||||
let collectionJobLogCounter = 0;
|
||||
let apiPortTouchedByUser = false;
|
||||
let isAutoUpdatingApiPort = false;
|
||||
@@ -49,9 +47,8 @@ function initApiSource() {
|
||||
return;
|
||||
}
|
||||
|
||||
const authTypeField = document.getElementById('api-auth-type');
|
||||
const cancelJobButton = document.getElementById('cancel-job-btn');
|
||||
const fieldNames = ['host', 'protocol', 'port', 'username', 'auth_type', 'tls_mode', 'password', 'token'];
|
||||
const fieldNames = ['host', 'port', 'username', 'password'];
|
||||
|
||||
apiForm.addEventListener('submit', (event) => {
|
||||
event.preventDefault();
|
||||
@@ -83,12 +80,6 @@ function initApiSource() {
|
||||
|
||||
const eventName = field.tagName.toLowerCase() === 'select' ? 'change' : 'input';
|
||||
field.addEventListener(eventName, () => {
|
||||
if (fieldName === 'auth_type') {
|
||||
toggleApiAuthFields(authTypeField.value);
|
||||
}
|
||||
if (fieldName === 'protocol') {
|
||||
applyProtocolDefaultPort(field.value);
|
||||
}
|
||||
if (fieldName === 'port') {
|
||||
handleApiPortInput(field.value);
|
||||
}
|
||||
@@ -103,20 +94,15 @@ function initApiSource() {
|
||||
});
|
||||
});
|
||||
|
||||
applyProtocolDefaultPort(getApiValue('protocol'));
|
||||
toggleApiAuthFields(authTypeField.value);
|
||||
applyRedfishDefaultPort();
|
||||
renderCollectionJob();
|
||||
}
|
||||
|
||||
function validateCollectForm() {
|
||||
const host = getApiValue('host');
|
||||
const protocol = getApiValue('protocol');
|
||||
const portRaw = getApiValue('port');
|
||||
const username = getApiValue('username');
|
||||
const authType = getApiValue('auth_type');
|
||||
const tlsMode = getApiValue('tls_mode');
|
||||
const password = getApiValue('password');
|
||||
const token = getApiValue('token');
|
||||
|
||||
const errors = {};
|
||||
|
||||
@@ -124,10 +110,6 @@ function validateCollectForm() {
|
||||
errors.host = 'Укажите host.';
|
||||
}
|
||||
|
||||
if (!['redfish', 'ipmi'].includes(protocol)) {
|
||||
errors.protocol = 'Выберите протокол.';
|
||||
}
|
||||
|
||||
const port = Number(portRaw);
|
||||
const isPortInteger = Number.isInteger(port);
|
||||
if (!portRaw) {
|
||||
@@ -140,40 +122,25 @@ function validateCollectForm() {
|
||||
errors.username = 'Укажите username.';
|
||||
}
|
||||
|
||||
if (!['password', 'token'].includes(authType)) {
|
||||
errors.auth_type = 'Выберите тип авторизации.';
|
||||
}
|
||||
if (!['strict', 'insecure'].includes(tlsMode)) {
|
||||
errors.tls_mode = 'Выберите TLS режим.';
|
||||
}
|
||||
|
||||
if (authType === 'password' && !password) {
|
||||
if (!password) {
|
||||
errors.password = 'Введите пароль.';
|
||||
}
|
||||
|
||||
if (authType === 'token' && !token) {
|
||||
errors.token = 'Введите токен.';
|
||||
}
|
||||
|
||||
if (Object.keys(errors).length > 0) {
|
||||
return { isValid: false, errors, payload: null };
|
||||
}
|
||||
|
||||
// TODO: UI для выбора протокола вернем, когда откроем IPMI коннектор.
|
||||
const payload = {
|
||||
host,
|
||||
protocol,
|
||||
protocol: 'redfish',
|
||||
port,
|
||||
username,
|
||||
auth_type: authType,
|
||||
tls_mode: tlsMode
|
||||
auth_type: 'password',
|
||||
tls_mode: 'insecure',
|
||||
password
|
||||
};
|
||||
|
||||
if (authType === 'password') {
|
||||
payload.password = password;
|
||||
} else {
|
||||
payload.token = token;
|
||||
}
|
||||
|
||||
return { isValid: true, errors: {}, payload };
|
||||
}
|
||||
|
||||
@@ -184,7 +151,7 @@ function renderFormErrors(errors) {
|
||||
return;
|
||||
}
|
||||
|
||||
const errorFields = ['host', 'protocol', 'port', 'username', 'auth_type', 'tls_mode', 'password', 'token'];
|
||||
const errorFields = ['host', 'port', 'username', 'password'];
|
||||
errorFields.forEach((fieldName) => {
|
||||
const errorNode = apiForm.querySelector(`[data-error-for="${fieldName}"]`);
|
||||
if (!errorNode) {
|
||||
@@ -246,42 +213,43 @@ function clearApiConnectStatus() {
|
||||
|
||||
function startCollectionJob(payload) {
|
||||
resetCollectionJobState();
|
||||
|
||||
const totalSteps = 4;
|
||||
const finalStatus = Math.random() < 0.2 ? 'Failed' : 'Success';
|
||||
const jobId = `job-${Date.now()}-${Math.floor(Math.random() * 1000)}`;
|
||||
|
||||
collectionJob = {
|
||||
id: jobId,
|
||||
status: 'Queued',
|
||||
progress: 0,
|
||||
currentStep: 0,
|
||||
totalSteps,
|
||||
logs: [],
|
||||
payload
|
||||
};
|
||||
|
||||
collectionJobScenario = [
|
||||
{ status: 'Running', step: 1, message: 'Соединение с BMC установлено.' },
|
||||
{ status: 'Running', step: 2, message: 'Собираем базовую конфигурацию сервера.' },
|
||||
{ status: 'Running', step: 3, message: 'Собираем системные журналы и события.' },
|
||||
{
|
||||
status: finalStatus,
|
||||
step: 4,
|
||||
message: finalStatus === 'Success'
|
||||
? 'Сбор завершен. Данные готовы к следующему этапу.'
|
||||
: 'Сбор завершился с ошибкой: часть данных недоступна.'
|
||||
}
|
||||
];
|
||||
collectionJobScenarioIndex = 0;
|
||||
|
||||
appendJobLog('Задача создана и добавлена в очередь.');
|
||||
setApiFormBlocked(true);
|
||||
renderCollectionJob();
|
||||
|
||||
collectionJobPollTimer = window.setInterval(() => {
|
||||
pollCollectionJobStatus();
|
||||
}, 1200);
|
||||
fetch('/api/collect', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload)
|
||||
})
|
||||
.then(async (response) => {
|
||||
const body = await response.json().catch(() => ({}));
|
||||
if (!response.ok) {
|
||||
throw new Error(body.error || 'Не удалось запустить задачу');
|
||||
}
|
||||
|
||||
collectionJob = {
|
||||
id: body.job_id,
|
||||
status: normalizeJobStatus(body.status || 'queued'),
|
||||
progress: 0,
|
||||
logs: [],
|
||||
payload
|
||||
};
|
||||
appendJobLog(body.message || 'Задача поставлена в очередь');
|
||||
renderCollectionJob();
|
||||
|
||||
collectionJobPollTimer = window.setInterval(() => {
|
||||
pollCollectionJobStatus();
|
||||
}, 1200);
|
||||
})
|
||||
.catch((err) => {
|
||||
setApiFormBlocked(false);
|
||||
clearApiConnectStatus();
|
||||
renderApiConnectStatus(false, null);
|
||||
const status = document.getElementById('api-connect-status');
|
||||
if (status) {
|
||||
status.textContent = err.message || 'Ошибка запуска задачи';
|
||||
status.className = 'api-connect-status error';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function pollCollectionJobStatus() {
|
||||
@@ -290,33 +258,63 @@ function pollCollectionJobStatus() {
|
||||
return;
|
||||
}
|
||||
|
||||
const nextState = collectionJobScenario[collectionJobScenarioIndex];
|
||||
if (!nextState) {
|
||||
clearCollectionJobPolling();
|
||||
return;
|
||||
}
|
||||
fetch(`/api/collect/${encodeURIComponent(collectionJob.id)}`)
|
||||
.then(async (response) => {
|
||||
const body = await response.json().catch(() => ({}));
|
||||
if (!response.ok) {
|
||||
throw new Error(body.error || 'Не удалось получить статус задачи');
|
||||
}
|
||||
|
||||
collectionJobScenarioIndex += 1;
|
||||
collectionJob.status = nextState.status;
|
||||
collectionJob.currentStep = nextState.step;
|
||||
collectionJob.progress = Math.round((nextState.step / collectionJob.totalSteps) * 100);
|
||||
appendJobLog(nextState.message);
|
||||
renderCollectionJob();
|
||||
const prevStatus = collectionJob.status;
|
||||
collectionJob.status = normalizeJobStatus(body.status || collectionJob.status);
|
||||
collectionJob.progress = Number.isFinite(body.progress) ? body.progress : collectionJob.progress;
|
||||
collectionJob.error = body.error || '';
|
||||
syncServerLogs(body.logs);
|
||||
renderCollectionJob();
|
||||
|
||||
if (isCollectionJobTerminal(collectionJob.status)) {
|
||||
clearCollectionJobPolling();
|
||||
}
|
||||
if (isCollectionJobTerminal(collectionJob.status)) {
|
||||
clearCollectionJobPolling();
|
||||
if (collectionJob.status === 'success') {
|
||||
loadDataFromStatus();
|
||||
} else if (collectionJob.status === 'failed' && collectionJob.error) {
|
||||
appendJobLog(`Ошибка: ${collectionJob.error}`);
|
||||
renderCollectionJob();
|
||||
}
|
||||
} else if (prevStatus !== collectionJob.status && collectionJob.status === 'running') {
|
||||
appendJobLog('Сбор выполняется...');
|
||||
renderCollectionJob();
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
appendJobLog(`Ошибка статуса: ${err.message}`);
|
||||
renderCollectionJob();
|
||||
clearCollectionJobPolling();
|
||||
setApiFormBlocked(false);
|
||||
});
|
||||
}
|
||||
|
||||
function cancelCollectionJob() {
|
||||
if (!collectionJob || isCollectionJobTerminal(collectionJob.status)) {
|
||||
return;
|
||||
}
|
||||
|
||||
clearCollectionJobPolling();
|
||||
collectionJob.status = 'Canceled';
|
||||
appendJobLog('Задача отменена пользователем.');
|
||||
renderCollectionJob();
|
||||
fetch(`/api/collect/${encodeURIComponent(collectionJob.id)}/cancel`, {
|
||||
method: 'POST'
|
||||
})
|
||||
.then(async (response) => {
|
||||
const body = await response.json().catch(() => ({}));
|
||||
if (!response.ok) {
|
||||
throw new Error(body.error || 'Не удалось отменить задачу');
|
||||
}
|
||||
collectionJob.status = normalizeJobStatus(body.status || 'canceled');
|
||||
collectionJob.progress = Number.isFinite(body.progress) ? body.progress : collectionJob.progress;
|
||||
syncServerLogs(body.logs);
|
||||
clearCollectionJobPolling();
|
||||
renderCollectionJob();
|
||||
})
|
||||
.catch((err) => {
|
||||
appendJobLog(`Ошибка отмены: ${err.message}`);
|
||||
renderCollectionJob();
|
||||
});
|
||||
}
|
||||
|
||||
function appendJobLog(message) {
|
||||
@@ -355,13 +353,13 @@ function renderCollectionJob() {
|
||||
statusValue.className = `job-status-badge status-${collectionJob.status.toLowerCase()}`;
|
||||
const isTerminal = isCollectionJobTerminal(collectionJob.status);
|
||||
const terminalMessage = {
|
||||
Success: 'Сбор завершен',
|
||||
Failed: 'Сбор завершился ошибкой',
|
||||
Canceled: 'Сбор отменен'
|
||||
success: 'Сбор завершен',
|
||||
failed: 'Сбор завершился ошибкой',
|
||||
canceled: 'Сбор отменен'
|
||||
}[collectionJob.status];
|
||||
const progressLabel = isTerminal
|
||||
? terminalMessage
|
||||
: `Шаг ${collectionJob.currentStep} из ${collectionJob.totalSteps}`;
|
||||
: 'Сбор данных...';
|
||||
progressValue.textContent = `${collectionJob.progress}% · ${progressLabel}`;
|
||||
|
||||
logsList.innerHTML = collectionJob.logs.map((log) => (
|
||||
@@ -373,7 +371,7 @@ function renderCollectionJob() {
|
||||
}
|
||||
|
||||
function isCollectionJobTerminal(status) {
|
||||
return ['Success', 'Failed', 'Canceled'].includes(status);
|
||||
return ['success', 'failed', 'canceled'].includes(normalizeJobStatus(status));
|
||||
}
|
||||
|
||||
function setApiFormBlocked(shouldBlock) {
|
||||
@@ -400,34 +398,41 @@ function clearCollectionJobPolling() {
|
||||
function resetCollectionJobState() {
|
||||
clearCollectionJobPolling();
|
||||
collectionJob = null;
|
||||
collectionJobScenario = [];
|
||||
collectionJobScenarioIndex = 0;
|
||||
renderCollectionJob();
|
||||
}
|
||||
|
||||
function toggleApiAuthFields(authType) {
|
||||
const passwordField = document.getElementById('api-password-field');
|
||||
const tokenField = document.getElementById('api-token-field');
|
||||
|
||||
if (!passwordField || !tokenField) {
|
||||
function syncServerLogs(logs) {
|
||||
if (!collectionJob || !Array.isArray(logs)) {
|
||||
return;
|
||||
}
|
||||
if (logs.length <= collectionJob.logs.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const useToken = authType === 'token';
|
||||
passwordField.classList.toggle('hidden', useToken);
|
||||
tokenField.classList.toggle('hidden', !useToken);
|
||||
const from = collectionJob.logs.length;
|
||||
for (let i = from; i < logs.length; i += 1) {
|
||||
appendJobLog(logs[i]);
|
||||
}
|
||||
}
|
||||
|
||||
function applyProtocolDefaultPort(protocol) {
|
||||
const defaults = {
|
||||
redfish: '443',
|
||||
ipmi: '623'
|
||||
};
|
||||
const defaultPort = defaults[protocol];
|
||||
if (!defaultPort) {
|
||||
return;
|
||||
}
|
||||
function normalizeJobStatus(status) {
|
||||
return String(status || '').trim().toLowerCase();
|
||||
}
|
||||
|
||||
async function loadDataFromStatus() {
|
||||
try {
|
||||
const response = await fetch('/api/status');
|
||||
const payload = await response.json();
|
||||
if (!payload.loaded) {
|
||||
return;
|
||||
}
|
||||
await loadData(payload.vendor || '', payload.filename || '');
|
||||
} catch (err) {
|
||||
console.error('Failed to load data after collect:', err);
|
||||
}
|
||||
}
|
||||
|
||||
function applyRedfishDefaultPort() {
|
||||
const apiForm = document.getElementById('api-connect-form');
|
||||
if (!apiForm) {
|
||||
return;
|
||||
@@ -444,7 +449,7 @@ function applyProtocolDefaultPort(protocol) {
|
||||
}
|
||||
|
||||
isAutoUpdatingApiPort = true;
|
||||
portField.value = defaultPort;
|
||||
portField.value = '443';
|
||||
isAutoUpdatingApiPort = false;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user