feat(ui): validate API form and improve error UX
This commit is contained in:
@@ -108,10 +108,92 @@ main {
|
|||||||
border: 1px solid #e0e0e0;
|
border: 1px solid #e0e0e0;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
padding: 2rem;
|
padding: 2rem;
|
||||||
text-align: center;
|
|
||||||
color: #555;
|
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 {
|
#upload-status {
|
||||||
margin-top: 1rem;
|
margin-top: 1rem;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
initSourceType();
|
initSourceType();
|
||||||
|
initApiSource();
|
||||||
initUpload();
|
initUpload();
|
||||||
initTabs();
|
initTabs();
|
||||||
initFilters();
|
initFilters();
|
||||||
@@ -9,6 +10,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
let sourceType = 'archive';
|
let sourceType = 'archive';
|
||||||
|
let apiConnectPayload = null;
|
||||||
|
|
||||||
function initSourceType() {
|
function initSourceType() {
|
||||||
const sourceButtons = document.querySelectorAll('.source-switch-btn');
|
const sourceButtons = document.querySelectorAll('.source-switch-btn');
|
||||||
@@ -29,9 +31,214 @@ function setSourceType(nextType) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const archiveContent = document.getElementById('archive-source-content');
|
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');
|
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 = `<strong>Исправьте ошибки в форме:</strong><ul>${messages.map(msg => `<li>${escapeHtml(msg)}</li>`).join('')}</ul>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
// Load and display available parsers
|
||||||
|
|||||||
@@ -30,8 +30,68 @@
|
|||||||
<div id="parsers-info" class="parsers-info"></div>
|
<div id="parsers-info" class="parsers-info"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="api-source-placeholder" class="api-placeholder hidden">
|
<div id="api-source-content" class="api-placeholder hidden">
|
||||||
<p>Подключение по API будет реализовано на следующих шагах.</p>
|
<form id="api-connect-form" novalidate>
|
||||||
|
<h3>Подключение к BMC API</h3>
|
||||||
|
<div id="api-form-errors" class="form-errors hidden"></div>
|
||||||
|
|
||||||
|
<div class="api-form-grid">
|
||||||
|
<label class="api-form-field" for="api-host">
|
||||||
|
<span>Host</span>
|
||||||
|
<input id="api-host" name="host" type="text" placeholder="10.0.0.10 или bmc.example.local">
|
||||||
|
<span class="field-error" data-error-for="host"></span>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label class="api-form-field" for="api-protocol">
|
||||||
|
<span>Протокол</span>
|
||||||
|
<select id="api-protocol" name="protocol">
|
||||||
|
<option value="">Выберите протокол</option>
|
||||||
|
<option value="redfish">Redfish</option>
|
||||||
|
<option value="ipmi">IPMI</option>
|
||||||
|
</select>
|
||||||
|
<span class="field-error" data-error-for="protocol"></span>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label class="api-form-field" for="api-port">
|
||||||
|
<span>Порт</span>
|
||||||
|
<input id="api-port" name="port" type="number" min="1" max="65535" placeholder="443">
|
||||||
|
<span class="field-error" data-error-for="port"></span>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label class="api-form-field" for="api-username">
|
||||||
|
<span>Username</span>
|
||||||
|
<input id="api-username" name="username" type="text" placeholder="admin">
|
||||||
|
<span class="field-error" data-error-for="username"></span>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label class="api-form-field" for="api-auth-type">
|
||||||
|
<span>Тип авторизации</span>
|
||||||
|
<select id="api-auth-type" name="authType">
|
||||||
|
<option value="">Выберите тип</option>
|
||||||
|
<option value="password">Пароль</option>
|
||||||
|
<option value="token">Токен</option>
|
||||||
|
</select>
|
||||||
|
<span class="field-error" data-error-for="authType"></span>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label class="api-form-field" id="api-password-field" for="api-password">
|
||||||
|
<span>Пароль</span>
|
||||||
|
<input id="api-password" name="password" type="password" autocomplete="current-password">
|
||||||
|
<span class="field-error" data-error-for="password"></span>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label class="api-form-field hidden" id="api-token-field" for="api-token">
|
||||||
|
<span>Токен</span>
|
||||||
|
<input id="api-token" name="token" type="text" autocomplete="off">
|
||||||
|
<span class="field-error" data-error-for="token"></span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="api-form-actions">
|
||||||
|
<button id="api-connect-btn" type="submit">Подключиться</button>
|
||||||
|
</div>
|
||||||
|
<div id="api-connect-status" class="api-connect-status"></div>
|
||||||
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user