diff --git a/web/static/css/style.css b/web/static/css/style.css index a65bf9e..d4181a8 100644 --- a/web/static/css/style.css +++ b/web/static/css/style.css @@ -108,10 +108,92 @@ main { border: 1px solid #e0e0e0; border-radius: 8px; padding: 2rem; - text-align: center; color: #555; } +#api-connect-form h3 { + margin-bottom: 1rem; + color: #2c3e50; +} + +.api-form-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); + gap: 0.75rem 1rem; +} + +.api-form-field { + display: flex; + flex-direction: column; + gap: 0.35rem; + font-size: 0.875rem; + color: #2c3e50; +} + +.api-form-field input, +.api-form-field select { + border: 1px solid #d0d7de; + border-radius: 4px; + padding: 0.5rem 0.6rem; + font-size: 0.9rem; +} + +.api-form-field.has-error input, +.api-form-field.has-error select { + border-color: #dc3545; +} + +.field-error { + min-height: 1rem; + color: #dc3545; + font-size: 0.75rem; +} + +.form-errors { + margin-bottom: 1rem; + border: 1px solid #f0b9bf; + background: #fff4f5; + color: #8e1f2b; + border-radius: 6px; + padding: 0.75rem 0.9rem; + font-size: 0.85rem; +} + +.form-errors ul { + margin: 0.4rem 0 0; + padding-left: 1.1rem; +} + +.api-form-actions { + margin-top: 0.9rem; +} + +#api-connect-btn { + background: #3498db; + color: white; + border: none; + padding: 0.6rem 1.2rem; + border-radius: 4px; + cursor: pointer; +} + +#api-connect-btn:hover { + background: #2980b9; +} + +.api-connect-status { + margin-top: 0.75rem; + font-size: 0.85rem; +} + +.api-connect-status.success { + color: #1f8f4c; +} + +.api-connect-status.error { + color: #dc3545; +} + #upload-status { margin-top: 1rem; text-align: center; diff --git a/web/static/js/app.js b/web/static/js/app.js index 23ebeec..53da0b1 100644 --- a/web/static/js/app.js +++ b/web/static/js/app.js @@ -2,6 +2,7 @@ document.addEventListener('DOMContentLoaded', () => { initSourceType(); + initApiSource(); initUpload(); initTabs(); initFilters(); @@ -9,6 +10,7 @@ document.addEventListener('DOMContentLoaded', () => { }); let sourceType = 'archive'; +let apiConnectPayload = null; function initSourceType() { const sourceButtons = document.querySelectorAll('.source-switch-btn'); @@ -29,9 +31,214 @@ function setSourceType(nextType) { }); const archiveContent = document.getElementById('archive-source-content'); - const apiPlaceholder = document.getElementById('api-source-placeholder'); + const apiSourceContent = document.getElementById('api-source-content'); archiveContent.classList.toggle('hidden', sourceType !== 'archive'); - apiPlaceholder.classList.toggle('hidden', sourceType !== 'api'); + apiSourceContent.classList.toggle('hidden', sourceType !== 'api'); +} + +function initApiSource() { + const apiForm = document.getElementById('api-connect-form'); + if (!apiForm) { + return; + } + + const authTypeField = document.getElementById('api-auth-type'); + const fieldNames = ['host', 'protocol', 'port', 'username', 'authType', 'password', 'token']; + + apiForm.addEventListener('submit', (event) => { + event.preventDefault(); + const { isValid, payload, errors } = validateCollectForm(); + renderFormErrors(errors); + renderApiConnectStatus(isValid, payload); + + if (!isValid) { + apiConnectPayload = null; + return; + } + + apiConnectPayload = payload; + console.log('API payload prepared:', apiConnectPayload); + }); + + fieldNames.forEach((fieldName) => { + const field = apiForm.elements.namedItem(fieldName); + if (!field) { + return; + } + + const eventName = field.tagName.toLowerCase() === 'select' ? 'change' : 'input'; + field.addEventListener(eventName, () => { + if (fieldName === 'authType') { + toggleApiAuthFields(authTypeField.value); + } + + const { errors } = validateCollectForm(); + renderFormErrors(errors); + clearApiConnectStatus(); + }); + }); + + toggleApiAuthFields(authTypeField.value); +} + +function validateCollectForm() { + const host = getApiValue('host'); + const protocol = getApiValue('protocol'); + const portRaw = getApiValue('port'); + const username = getApiValue('username'); + const authType = getApiValue('authType'); + const password = getApiValue('password'); + const token = getApiValue('token'); + + const errors = {}; + + if (!host) { + errors.host = 'Укажите host.'; + } + + if (!['redfish', 'ipmi'].includes(protocol)) { + errors.protocol = 'Выберите протокол.'; + } + + const port = Number(portRaw); + const isPortInteger = Number.isInteger(port); + if (!portRaw) { + errors.port = 'Укажите порт.'; + } else if (!isPortInteger || port < 1 || port > 65535) { + errors.port = 'Порт должен быть от 1 до 65535.'; + } + + if (!username) { + errors.username = 'Укажите username.'; + } + + if (!['password', 'token'].includes(authType)) { + errors.authType = 'Выберите тип авторизации.'; + } + + if (authType === 'password' && !password) { + errors.password = 'Введите пароль.'; + } + + if (authType === 'token' && !token) { + errors.token = 'Введите токен.'; + } + + if (Object.keys(errors).length > 0) { + return { isValid: false, errors, payload: null }; + } + + const payload = { + sourceType: 'api', + connection: { + host, + protocol, + port, + username, + authType + } + }; + + if (authType === 'password') { + payload.connection.password = password; + } else { + payload.connection.token = token; + } + + return { isValid: true, errors: {}, payload }; +} + +function renderFormErrors(errors) { + const apiForm = document.getElementById('api-connect-form'); + const summary = document.getElementById('api-form-errors'); + if (!apiForm || !summary) { + return; + } + + const errorFields = ['host', 'protocol', 'port', 'username', 'authType', 'password', 'token']; + errorFields.forEach((fieldName) => { + const errorNode = apiForm.querySelector(`[data-error-for="${fieldName}"]`); + if (!errorNode) { + return; + } + + const fieldWrapper = errorNode.closest('.api-form-field'); + const message = errors[fieldName] || ''; + errorNode.textContent = message; + if (fieldWrapper) { + fieldWrapper.classList.toggle('has-error', Boolean(message)); + } + }); + + const messages = Object.values(errors); + if (messages.length === 0) { + summary.innerHTML = ''; + summary.classList.add('hidden'); + return; + } + + summary.classList.remove('hidden'); + summary.innerHTML = `Исправьте ошибки в форме:`; +} + +function renderApiConnectStatus(isValid, payload) { + const status = document.getElementById('api-connect-status'); + if (!status) { + return; + } + + if (!isValid) { + status.textContent = 'Форма не отправлена: есть ошибки.'; + status.className = 'api-connect-status error'; + return; + } + + const payloadPreview = { ...payload, connection: { ...payload.connection } }; + if (payloadPreview.connection.password) { + payloadPreview.connection.password = '***'; + } + if (payloadPreview.connection.token) { + payloadPreview.connection.token = '***'; + } + + status.textContent = `Payload сформирован: ${JSON.stringify(payloadPreview)}`; + status.className = 'api-connect-status success'; +} + +function clearApiConnectStatus() { + const status = document.getElementById('api-connect-status'); + if (!status) { + return; + } + + status.textContent = ''; + status.className = 'api-connect-status'; +} + +function toggleApiAuthFields(authType) { + const passwordField = document.getElementById('api-password-field'); + const tokenField = document.getElementById('api-token-field'); + + if (!passwordField || !tokenField) { + return; + } + + const useToken = authType === 'token'; + passwordField.classList.toggle('hidden', useToken); + tokenField.classList.toggle('hidden', !useToken); +} + +function getApiValue(fieldName) { + const apiForm = document.getElementById('api-connect-form'); + if (!apiForm) { + return ''; + } + + const field = apiForm.elements.namedItem(fieldName); + if (!field || typeof field.value !== 'string') { + return ''; + } + return field.value.trim(); } // Load and display available parsers diff --git a/web/templates/index.html b/web/templates/index.html index 85b439d..9ee54d4 100644 --- a/web/templates/index.html +++ b/web/templates/index.html @@ -30,8 +30,68 @@
-