Files
crank/apps/ui/js/openapi-import.js
T
2026-06-23 21:03:14 +00:00

555 lines
22 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
(function() {
var state = {
workspaceId: null,
onImported: null,
jobId: null,
preview: null,
filterQuery: '',
filterMethod: '',
};
function qs(id) {
return document.getElementById(id);
}
function setStatus(message, isError) {
var node = qs('openapi-import-status');
if (!node) return;
node.textContent = message || '';
node.classList.toggle('error', !!isError);
}
function selectedKeys() {
return Array.from(document.querySelectorAll('[data-openapi-operation]:checked'))
.map(function(input) { return input.value; });
}
function operationRows() {
return Array.from(document.querySelectorAll('.openapi-import-operation'));
}
function visibleOperationRows() {
return operationRows().filter(function(row) { return !row.hidden; });
}
function updateSelection() {
var node = qs('openapi-import-selection');
var selectedCount = selectedKeys().length;
var totalCount = document.querySelectorAll('[data-openapi-operation]').length;
var visibleCount = visibleOperationRows().length;
if (node) node.textContent = 'Выбрано: ' + selectedCount + ' из ' + totalCount + ', показано: ' + visibleCount;
updateGroupSelectionState();
}
function updateGroupSelectionState() {
document.querySelectorAll('.openapi-import-group').forEach(function(groupNode) {
var rows = Array.from(groupNode.querySelectorAll('.openapi-import-operation'));
var visibleRows = rows.filter(function(row) { return !row.hidden; });
var visibleInputs = visibleRows
.map(function(row) { return row.querySelector('[data-openapi-operation]'); })
.filter(Boolean);
var selectedVisible = visibleInputs.filter(function(input) { return input.checked; }).length;
var selectedTotal = rows
.map(function(row) { return row.querySelector('[data-openapi-operation]'); })
.filter(function(input) { return input && input.checked; }).length;
var checkbox = groupNode.querySelector('[data-openapi-group]');
if (checkbox) {
checkbox.indeterminate = selectedVisible > 0 && selectedVisible < visibleInputs.length;
checkbox.checked = visibleInputs.length > 0 && selectedVisible === visibleInputs.length;
}
var counter = groupNode.querySelector('[data-openapi-group-count]');
if (counter) {
counter.textContent = 'выбрано ' + selectedTotal + ' из ' + rows.length + ', показано ' + visibleRows.length;
}
});
}
function applyFilters() {
var query = state.filterQuery.toLowerCase();
var method = state.filterMethod.toLowerCase();
document.querySelectorAll('.openapi-import-group').forEach(function(groupNode) {
var visibleInGroup = 0;
groupNode.querySelectorAll('.openapi-import-operation').forEach(function(row) {
var rowMethod = String(row.dataset.openapiMethod || '').toLowerCase();
var searchText = String(row.dataset.openapiSearch || '').toLowerCase();
var visible = (!method || rowMethod === method) && (!query || searchText.indexOf(query) >= 0);
row.hidden = !visible;
if (visible) visibleInGroup += 1;
});
groupNode.hidden = visibleInGroup === 0;
});
updateSelection();
}
function setVisibleSelection(checked) {
visibleOperationRows().forEach(function(row) {
var input = row.querySelector('[data-openapi-operation]');
if (input) input.checked = checked;
});
updateSelection();
}
function renderPreview(response) {
state.jobId = response.job_id;
state.preview = response.preview;
state.filterQuery = '';
state.filterMethod = '';
var preview = response.preview;
qs('openapi-import-preview-panel').hidden = false;
qs('openapi-import-result').hidden = true;
qs('openapi-import-result').innerHTML = '';
qs('openapi-import-source').textContent = [
preview.source.title || 'Imported API',
preview.source.format + (preview.source.version ? ' ' + preview.source.version : ''),
'методов: ' + preview.groups.reduce(function(sum, group) { return sum + group.operations.length; }, 0),
].join(' · ');
var serverSelect = qs('openapi-import-server');
serverSelect.innerHTML = '';
var servers = preview.source.servers && preview.source.servers.length ? preview.source.servers : [''];
servers.forEach(function(server) {
var option = document.createElement('option');
option.value = server;
option.textContent = server || 'Указать позже';
serverSelect.appendChild(option);
});
var searchInput = qs('openapi-import-search');
var methodFilter = qs('openapi-import-method-filter');
if (searchInput) searchInput.value = '';
if (methodFilter) {
methodFilter.innerHTML = '<option value="">Все методы</option>';
Array.from(new Set(preview.groups.flatMap(function(group) {
return group.operations.map(function(operation) { return operation.method; });
}))).sort().forEach(function(method) {
var option = document.createElement('option');
option.value = method;
option.textContent = method;
methodFilter.appendChild(option);
});
}
var groupsNode = qs('openapi-import-groups');
groupsNode.innerHTML = '';
if (preview.findings && preview.findings.length) {
var documentFindings = document.createElement('div');
documentFindings.className = 'openapi-import-document-findings';
documentFindings.innerHTML = preview.findings.map(function(finding) {
return renderFinding(finding);
}).join('');
groupsNode.appendChild(documentFindings);
}
preview.groups.forEach(function(group) {
var groupNode = document.createElement('section');
groupNode.className = 'openapi-import-group';
var header = document.createElement('div');
header.className = 'openapi-import-group-header';
header.innerHTML = '<span>' + escapeHtml(group.title) + ' · ' + group.operations.length + '</span>'
+ '<span class="openapi-import-group-count" data-openapi-group-count></span>'
+ '<label class="openapi-import-group-toggle"><input type="checkbox" data-openapi-group checked> Выбрать группу</label>';
groupNode.appendChild(header);
group.operations.forEach(function(operation) {
var row = document.createElement('label');
row.className = 'openapi-import-operation';
row.dataset.openapiMethod = operation.method;
row.dataset.openapiSearch = [
group.title,
operation.key,
operation.suggested_name,
operation.suggested_display_name,
operation.path,
operation.method,
].join(' ');
row.innerHTML = [
'<input type="checkbox" data-openapi-operation value="' + escapeHtml(operation.key) + '" checked>',
'<div><div class="openapi-import-operation-title">' + escapeHtml(operation.suggested_display_name) + '</div>',
'<div class="openapi-import-operation-meta"><code>' + escapeHtml(operation.suggested_name) + '</code> · ' + escapeHtml(operation.path) + ' · входов: ' + operation.input_fields + ' · выходов: ' + operation.output_fields + '</div></div>',
'<span class="openapi-import-method">' + escapeHtml(operation.method) + '</span>',
].join('');
row.appendChild(renderOperationMappingPreview(operation));
if (operation.findings && operation.findings.length) {
var findings = document.createElement('div');
findings.className = 'openapi-import-findings';
findings.innerHTML = operation.findings.map(function(finding) {
return renderFinding(finding);
}).join('');
row.appendChild(findings);
}
groupNode.appendChild(row);
});
var groupCheckbox = groupNode.querySelector('[data-openapi-group]');
groupCheckbox.addEventListener('change', function() {
groupNode.querySelectorAll('.openapi-import-operation').forEach(function(row) {
if (row.hidden) return;
var input = row.querySelector('[data-openapi-operation]');
if (input) input.checked = groupCheckbox.checked;
});
updateSelection();
});
groupsNode.appendChild(groupNode);
});
groupsNode.querySelectorAll('[data-openapi-operation]').forEach(function(input) {
input.addEventListener('change', updateSelection);
});
applyFilters();
}
function renderOperationMappingPreview(operation) {
var details = document.createElement('div');
details.className = 'openapi-import-mapping-preview';
var request = summarizeRequestMapping(operation);
[
['Path', request.path],
['Query', request.query],
['Header', request.headers],
['Body', request.body],
].forEach(function(entry) {
appendMappingGroup(details, entry[0], entry[1]);
});
appendMappingGroup(details, 'Ответ', summarizeResponseMapping(operation));
return details;
}
function summarizeRequestMapping(operation) {
var result = {
path: [],
query: [],
headers: [],
body: [],
};
var rules = operation
&& operation.draft
&& operation.draft.input_mapping
&& Array.isArray(operation.draft.input_mapping.rules)
? operation.draft.input_mapping.rules
: [];
rules.forEach(function(rule) {
var target = String(rule.target || '');
if (target.indexOf('$.request.path.') === 0) {
result.path.push(target.replace('$.request.path.', ''));
} else if (target.indexOf('$.request.query.') === 0) {
result.query.push(target.replace('$.request.query.', ''));
} else if (target.indexOf('$.request.headers.') === 0) {
result.headers.push(target.replace('$.request.headers.', ''));
} else if (target.indexOf('$.request.body.') === 0) {
result.body.push(target.replace('$.request.body.', ''));
} else if (target === '$.request.body') {
result.body.push('body');
}
});
return result;
}
function summarizeResponseMapping(operation) {
var rules = operation
&& operation.draft
&& operation.draft.output_mapping
&& Array.isArray(operation.draft.output_mapping.rules)
? operation.draft.output_mapping.rules
: [];
return rules.map(function(rule) {
var target = String(rule.target || '').replace('$.output.', '').replace('$.output', 'result');
var source = String(rule.source || '').replace('$.response.body.', '').replace('$.response.body', 'body');
return target + ' ← ' + source;
});
}
function appendMappingGroup(root, label, values) {
if (!values || !values.length) return;
var group = document.createElement('div');
group.className = 'openapi-import-mapping-group';
var title = document.createElement('span');
title.className = 'openapi-import-mapping-label';
title.textContent = label;
group.appendChild(title);
values.slice(0, 8).forEach(function(value) {
var chip = document.createElement('code');
chip.className = 'openapi-import-mapping-chip';
chip.textContent = value;
group.appendChild(chip);
});
if (values.length > 8) {
var more = document.createElement('span');
more.className = 'openapi-import-mapping-more';
more.textContent = '+' + (values.length - 8);
group.appendChild(more);
}
root.appendChild(group);
}
function escapeHtml(value) {
return String(value || '')
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;');
}
function renderFinding(finding) {
var severity = String(finding && finding.severity || 'warning').toLowerCase();
var label = severity === 'info' ? 'i' : severity === 'error' ? '×' : '!';
return '<span class="openapi-import-finding openapi-import-finding-' + escapeHtml(severity) + '">'
+ '<strong>' + label + '</strong> '
+ escapeHtml(finding.message)
+ '</span>';
}
function previewOperationByName(name) {
if (!state.preview || !state.preview.groups) return null;
for (var groupIndex = 0; groupIndex < state.preview.groups.length; groupIndex += 1) {
var operations = state.preview.groups[groupIndex].operations || [];
for (var operationIndex = 0; operationIndex < operations.length; operationIndex += 1) {
if (operations[operationIndex].suggested_name === name) {
return operations[operationIndex];
}
}
}
return null;
}
function wizardHref(operationId) {
return ((window.CrankRoutes && window.CrankRoutes.wizard) || '/wizard/')
+ '?mode=edit&operationId=' + encodeURIComponent(operationId);
}
function appendFindingNodes(root, findings) {
if (!findings || !findings.length) {
var empty = document.createElement('span');
empty.className = 'openapi-import-result-ok';
empty.textContent = 'Критичных замечаний нет';
root.appendChild(empty);
return;
}
findings.slice(0, 4).forEach(function(finding) {
var wrapper = document.createElement('div');
wrapper.innerHTML = renderFinding(finding);
root.appendChild(wrapper.firstElementChild);
});
if (findings.length > 4) {
var more = document.createElement('span');
more.className = 'openapi-import-result-more';
more.textContent = '+' + (findings.length - 4) + ' еще';
root.appendChild(more);
}
}
async function preview() {
if (!state.workspaceId) {
setStatus('Не выбран workspace.', true);
return;
}
var documentText = qs('openapi-import-document').value.trim();
if (!documentText) {
setStatus('Вставьте документ OpenAPI/Swagger или выберите файл.', true);
return;
}
setStatus('Разбираю документ...');
qs('openapi-import-preview').disabled = true;
try {
var response = await window.CrankApi.previewOpenApiImport(state.workspaceId, documentText);
renderPreview(response);
setStatus('Документ разобран. Выберите методы для импорта.');
} catch (error) {
setStatus(error.message || 'Не удалось разобрать документ.', true);
} finally {
qs('openapi-import-preview').disabled = false;
}
}
async function createDrafts() {
var keys = selectedKeys();
if (!keys.length) {
setStatus('Выберите хотя бы один метод.', true);
return;
}
if (!state.jobId) {
setStatus('Сначала разберите OpenAPI документ.', true);
return;
}
var serverUrl = (qs('openapi-import-server-custom').value || qs('openapi-import-server').value || '').trim();
if (!serverUrl) {
setStatus('Укажите base URL для создаваемых операций.', true);
return;
}
setStatus('Создаю черновики...');
qs('openapi-import-create').disabled = true;
try {
var response = await window.CrankApi.createOpenApiImport(state.workspaceId, state.jobId, {
selected_operation_keys: keys,
server_url: serverUrl,
conflict_mode: qs('openapi-import-conflict-mode').value || 'rename',
});
var message = 'Создано: ' + response.created.length;
if (response.skipped.length) message += ', пропущено: ' + response.skipped.length;
setStatus(message);
renderResult(response);
if (typeof state.onImported === 'function') {
await state.onImported();
}
} catch (error) {
setStatus(error.message || 'Не удалось создать черновики.', true);
} finally {
qs('openapi-import-create').disabled = false;
}
}
function renderResult(response) {
var node = qs('openapi-import-result');
if (!node) return;
node.replaceChildren();
var title = document.createElement('strong');
title.textContent = 'Результат импорта';
node.appendChild(title);
if (response.created && response.created.length) {
var firstHref = wizardHref(response.created[0].operation_id);
var primary = document.createElement('div');
primary.className = 'openapi-import-primary-result';
primary.innerHTML = '<a class="btn-primary" href="' + firstHref + '">Открыть первый черновик в мастере</a>';
node.appendChild(primary);
var table = document.createElement('div');
table.className = 'openapi-import-result-table';
table.setAttribute('role', 'table');
table.innerHTML = [
'<div class="openapi-import-result-row openapi-import-result-head" role="row">',
'<div role="columnheader">Черновик</div>',
'<div role="columnheader">Замечания</div>',
'<div role="columnheader">Действие</div>',
'</div>',
].join('');
response.created.forEach(function(operation) {
var previewOperation = previewOperationByName(operation.name);
var row = document.createElement('div');
row.className = 'openapi-import-result-row';
row.setAttribute('role', 'row');
var nameCell = document.createElement('div');
nameCell.setAttribute('role', 'cell');
var name = document.createElement('div');
name.className = 'openapi-import-result-name';
name.textContent = operation.name;
var meta = document.createElement('div');
meta.className = 'openapi-import-result-meta';
meta.textContent = previewOperation
? previewOperation.method + ' ' + previewOperation.path + ' · v' + operation.version
: 'v' + operation.version;
nameCell.appendChild(name);
nameCell.appendChild(meta);
var findingsCell = document.createElement('div');
findingsCell.className = 'openapi-import-result-findings';
findingsCell.setAttribute('role', 'cell');
appendFindingNodes(findingsCell, previewOperation ? previewOperation.findings : []);
var actionCell = document.createElement('div');
actionCell.setAttribute('role', 'cell');
var action = document.createElement('a');
action.className = 'btn-secondary openapi-import-result-action';
action.href = wizardHref(operation.operation_id);
action.textContent = previewOperation && previewOperation.findings && previewOperation.findings.length
? 'Исправить в мастере'
: 'Открыть в мастере';
actionCell.appendChild(action);
row.appendChild(nameCell);
row.appendChild(findingsCell);
row.appendChild(actionCell);
table.appendChild(row);
});
node.appendChild(table);
}
if (response.skipped && response.skipped.length) {
var skipped = document.createElement('div');
skipped.className = 'openapi-import-result-skipped';
skipped.textContent = 'Пропущено: ' + response.skipped.map(function(item) {
return (item.name || item.operation_key) + ' — ' + item.reason;
}).join('; ');
node.appendChild(skipped);
}
if (response.findings && response.findings.length) {
var responseFindings = document.createElement('div');
responseFindings.className = 'openapi-import-result-findings';
response.findings.forEach(function(finding) {
var wrapper = document.createElement('div');
wrapper.innerHTML = renderFinding(finding);
responseFindings.appendChild(wrapper.firstElementChild);
});
node.appendChild(responseFindings);
}
node.hidden = false;
}
function reset() {
state.jobId = null;
state.preview = null;
state.filterQuery = '';
state.filterMethod = '';
qs('openapi-import-document').value = '';
qs('openapi-import-file').value = '';
qs('openapi-import-server-custom').value = '';
qs('openapi-import-conflict-mode').value = 'rename';
if (qs('openapi-import-search')) qs('openapi-import-search').value = '';
if (qs('openapi-import-method-filter')) qs('openapi-import-method-filter').innerHTML = '<option value="">Все методы</option>';
qs('openapi-import-preview-panel').hidden = true;
qs('openapi-import-groups').innerHTML = '';
qs('openapi-import-result').hidden = true;
qs('openapi-import-result').innerHTML = '';
setStatus('');
updateSelection();
}
function open(options) {
state.workspaceId = options.workspaceId;
state.onImported = options.onImported;
qs('openapi-import-modal').hidden = false;
}
function close() {
qs('openapi-import-modal').hidden = true;
}
document.addEventListener('DOMContentLoaded', function() {
if (!qs('openapi-import-modal')) return;
qs('openapi-import-preview').addEventListener('click', preview);
qs('openapi-import-create').addEventListener('click', createDrafts);
qs('openapi-import-reset').addEventListener('click', reset);
qs('openapi-import-search').addEventListener('input', function(event) {
state.filterQuery = event.target.value.trim();
applyFilters();
});
qs('openapi-import-method-filter').addEventListener('change', function(event) {
state.filterMethod = event.target.value;
applyFilters();
});
qs('openapi-import-select-visible').addEventListener('click', function() {
setVisibleSelection(true);
});
qs('openapi-import-clear-visible').addEventListener('click', function() {
setVisibleSelection(false);
});
qs('openapi-import-file').addEventListener('change', async function(event) {
var file = event.target.files && event.target.files[0];
if (!file) return;
qs('openapi-import-document').value = await file.text();
});
document.querySelectorAll('[data-openapi-close]').forEach(function(node) {
node.addEventListener('click', close);
});
});
window.CrankOpenApiImport = {
open: open,
close: close,
reset: reset,
};
}());