Add OpenAPI import UI
This commit is contained in:
Vendored
+449
@@ -0,0 +1,449 @@
|
|||||||
|
.page-header-actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.openapi-import-modal[hidden] {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.openapi-import-modal {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
z-index: 1200;
|
||||||
|
}
|
||||||
|
|
||||||
|
.openapi-import-backdrop {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
background: rgba(15, 23, 42, 0.52);
|
||||||
|
backdrop-filter: blur(5px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.openapi-import-dialog {
|
||||||
|
position: relative;
|
||||||
|
width: min(1040px, calc(100vw - 32px));
|
||||||
|
max-height: min(860px, calc(100vh - 32px));
|
||||||
|
margin: 16px auto;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 22px;
|
||||||
|
background: var(--surface);
|
||||||
|
box-shadow: 0 24px 80px rgba(15, 23, 42, 0.28);
|
||||||
|
}
|
||||||
|
|
||||||
|
.openapi-import-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 18px;
|
||||||
|
padding: 22px 24px;
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.openapi-import-header h2 {
|
||||||
|
margin: 0 0 6px;
|
||||||
|
font-size: 22px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.openapi-import-header p {
|
||||||
|
margin: 0;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.openapi-import-body {
|
||||||
|
overflow: auto;
|
||||||
|
padding: 20px 24px 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.openapi-import-upload {
|
||||||
|
display: grid;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.openapi-file-label {
|
||||||
|
display: grid;
|
||||||
|
gap: 8px;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
#openapi-import-document {
|
||||||
|
min-height: 180px;
|
||||||
|
resize: vertical;
|
||||||
|
padding: 14px;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 14px;
|
||||||
|
background: var(--surface-muted);
|
||||||
|
color: var(--text-primary);
|
||||||
|
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.55;
|
||||||
|
}
|
||||||
|
|
||||||
|
.openapi-import-actions,
|
||||||
|
.openapi-import-footer {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.openapi-import-status {
|
||||||
|
margin-top: 14px;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.openapi-import-status.error {
|
||||||
|
color: var(--red);
|
||||||
|
}
|
||||||
|
|
||||||
|
.openapi-import-preview {
|
||||||
|
display: grid;
|
||||||
|
gap: 16px;
|
||||||
|
margin-top: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.openapi-import-source,
|
||||||
|
.openapi-import-group {
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 16px;
|
||||||
|
background: var(--surface-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.openapi-import-source {
|
||||||
|
padding: 14px 16px;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.openapi-import-server-row {
|
||||||
|
display: grid;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.openapi-import-server-row label {
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
#openapi-import-server,
|
||||||
|
#openapi-import-conflict-mode,
|
||||||
|
#openapi-import-search,
|
||||||
|
#openapi-import-method-filter,
|
||||||
|
.openapi-import-server-custom {
|
||||||
|
max-width: 520px;
|
||||||
|
padding: 10px 12px;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 12px;
|
||||||
|
background: var(--surface);
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.openapi-import-toolbar {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(220px, 1fr) minmax(160px, 220px) auto;
|
||||||
|
gap: 12px;
|
||||||
|
align-items: end;
|
||||||
|
padding: 14px 16px;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 16px;
|
||||||
|
background: var(--surface-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.openapi-import-filter {
|
||||||
|
display: grid;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.openapi-import-filter label {
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 800;
|
||||||
|
}
|
||||||
|
|
||||||
|
#openapi-import-search,
|
||||||
|
#openapi-import-method-filter {
|
||||||
|
width: 100%;
|
||||||
|
max-width: none;
|
||||||
|
background: var(--surface);
|
||||||
|
}
|
||||||
|
|
||||||
|
.openapi-import-bulk-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.openapi-import-groups {
|
||||||
|
display: grid;
|
||||||
|
gap: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.openapi-import-group-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 14px 16px;
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
font-weight: 800;
|
||||||
|
}
|
||||||
|
|
||||||
|
.openapi-import-group-count {
|
||||||
|
margin-left: auto;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 700;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.openapi-import-group-toggle {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.openapi-import-operation {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: auto 1fr auto;
|
||||||
|
gap: 12px;
|
||||||
|
align-items: start;
|
||||||
|
padding: 14px 16px;
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.openapi-import-operation:last-child {
|
||||||
|
border-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.openapi-import-operation-title {
|
||||||
|
font-weight: 800;
|
||||||
|
}
|
||||||
|
|
||||||
|
.openapi-import-operation-meta {
|
||||||
|
margin-top: 4px;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.openapi-import-method {
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: var(--accent-muted);
|
||||||
|
color: var(--accent);
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 800;
|
||||||
|
}
|
||||||
|
|
||||||
|
.openapi-import-findings {
|
||||||
|
grid-column: 2 / -1;
|
||||||
|
display: grid;
|
||||||
|
gap: 4px;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.openapi-import-finding {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: baseline;
|
||||||
|
gap: 6px;
|
||||||
|
color: var(--amber);
|
||||||
|
line-height: 1.45;
|
||||||
|
}
|
||||||
|
|
||||||
|
.openapi-import-finding-info {
|
||||||
|
color: var(--blue);
|
||||||
|
}
|
||||||
|
|
||||||
|
.openapi-import-finding-error {
|
||||||
|
color: var(--red);
|
||||||
|
}
|
||||||
|
|
||||||
|
.openapi-import-finding strong {
|
||||||
|
font-weight: 900;
|
||||||
|
}
|
||||||
|
|
||||||
|
.openapi-import-mapping-preview {
|
||||||
|
grid-column: 2 / -1;
|
||||||
|
display: grid;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 10px 12px;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 12px;
|
||||||
|
background: var(--surface);
|
||||||
|
}
|
||||||
|
|
||||||
|
.openapi-import-mapping-group {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.openapi-import-mapping-label {
|
||||||
|
min-width: 52px;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 800;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.openapi-import-mapping-chip {
|
||||||
|
padding: 3px 7px;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 999px;
|
||||||
|
background: var(--surface-muted);
|
||||||
|
color: var(--text-primary);
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.openapi-import-mapping-more {
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 800;
|
||||||
|
}
|
||||||
|
|
||||||
|
.openapi-import-document-findings,
|
||||||
|
.openapi-import-result {
|
||||||
|
display: grid;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 14px 16px;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 16px;
|
||||||
|
background: var(--surface-muted);
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.openapi-import-result strong {
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.openapi-import-primary-result {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-start;
|
||||||
|
margin: 4px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.openapi-import-primary-result .btn-primary {
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.openapi-import-result-table {
|
||||||
|
display: grid;
|
||||||
|
gap: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 14px;
|
||||||
|
background: var(--surface);
|
||||||
|
}
|
||||||
|
|
||||||
|
.openapi-import-result-row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(180px, 0.9fr) minmax(260px, 1.5fr) auto;
|
||||||
|
gap: 12px;
|
||||||
|
align-items: start;
|
||||||
|
padding: 12px 14px;
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.openapi-import-result-row:last-child {
|
||||||
|
border-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.openapi-import-result-head {
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 900;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
background: var(--surface-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.openapi-import-result-name {
|
||||||
|
color: var(--text-primary);
|
||||||
|
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 800;
|
||||||
|
}
|
||||||
|
|
||||||
|
.openapi-import-result-meta {
|
||||||
|
margin-top: 4px;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.openapi-import-result-findings {
|
||||||
|
display: grid;
|
||||||
|
gap: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.openapi-import-result-ok {
|
||||||
|
color: var(--green);
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 800;
|
||||||
|
}
|
||||||
|
|
||||||
|
.openapi-import-result-more {
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 800;
|
||||||
|
}
|
||||||
|
|
||||||
|
.openapi-import-result-action {
|
||||||
|
white-space: nowrap;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.openapi-import-result-skipped {
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.openapi-import-created-list {
|
||||||
|
display: grid;
|
||||||
|
gap: 6px;
|
||||||
|
margin: 4px 0 0;
|
||||||
|
padding: 0;
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.openapi-import-created-list a {
|
||||||
|
color: var(--accent);
|
||||||
|
font-weight: 700;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 720px) {
|
||||||
|
.openapi-import-toolbar {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.openapi-import-bulk-actions {
|
||||||
|
justify-content: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.openapi-import-operation {
|
||||||
|
grid-template-columns: auto 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.openapi-import-result-row {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.openapi-import-method {
|
||||||
|
justify-self: start;
|
||||||
|
}
|
||||||
|
}
|
||||||
+80
-4
@@ -9,6 +9,7 @@
|
|||||||
<link rel="stylesheet" href="css/layout.css">
|
<link rel="stylesheet" href="css/layout.css">
|
||||||
<link rel="stylesheet" href="css/catalog.css">
|
<link rel="stylesheet" href="css/catalog.css">
|
||||||
<link rel="stylesheet" href="css/pages.css">
|
<link rel="stylesheet" href="css/pages.css">
|
||||||
|
<link rel="stylesheet" href="css/openapi-import.css">
|
||||||
<script src="%CRANK_BUNDLE_PROTECTED_CORE%"></script>
|
<script src="%CRANK_BUNDLE_PROTECTED_CORE%"></script>
|
||||||
</head>
|
</head>
|
||||||
<body x-data="catalog()">
|
<body x-data="catalog()">
|
||||||
@@ -88,10 +89,16 @@
|
|||||||
<h1 class="page-heading" data-i18n="ops.title">Operations</h1>
|
<h1 class="page-heading" data-i18n="ops.title">Operations</h1>
|
||||||
<p class="page-subheading" data-i18n="ops.subtitle">Catalog of tools. List of created MCP tools for API endpoints.</p>
|
<p class="page-subheading" data-i18n="ops.subtitle">Catalog of tools. List of created MCP tools for API endpoints.</p>
|
||||||
</div>
|
</div>
|
||||||
<button class="btn-new" @click="handleNewOperation()">
|
<div class="page-header-actions">
|
||||||
<svg width="13" height="13"><use href="icons/general/plus.svg#icon"/></svg>
|
<button class="btn-secondary openapi-import-trigger" @click="handleOpenApiImport()">
|
||||||
<span data-i18n="ops.new">New operation</span>
|
<svg width="13" height="13"><use href="icons/wizard/upload.svg#icon"/></svg>
|
||||||
</button>
|
<span>Импорт OpenAPI</span>
|
||||||
|
</button>
|
||||||
|
<button class="btn-new" @click="handleNewOperation()">
|
||||||
|
<svg width="13" height="13"><use href="icons/general/plus.svg#icon"/></svg>
|
||||||
|
<span data-i18n="ops.new">New operation</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Stats -->
|
<!-- Stats -->
|
||||||
@@ -367,6 +374,75 @@
|
|||||||
|
|
||||||
</div><!-- /page -->
|
</div><!-- /page -->
|
||||||
|
|
||||||
|
<div class="openapi-import-modal" id="openapi-import-modal" hidden>
|
||||||
|
<div class="openapi-import-backdrop" data-openapi-close></div>
|
||||||
|
<section class="openapi-import-dialog" role="dialog" aria-modal="true" aria-labelledby="openapi-import-title">
|
||||||
|
<div class="openapi-import-header">
|
||||||
|
<div>
|
||||||
|
<h2 id="openapi-import-title">Импорт OpenAPI</h2>
|
||||||
|
<p>Загрузите OpenAPI/Swagger документ, выберите методы и создайте черновики MCP-инструментов.</p>
|
||||||
|
</div>
|
||||||
|
<button class="modal-close" type="button" data-openapi-close aria-label="Закрыть">
|
||||||
|
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round">
|
||||||
|
<line x1="1" y1="1" x2="11" y2="11"></line><line x1="11" y1="1" x2="1" y2="11"></line>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="openapi-import-body">
|
||||||
|
<div class="openapi-import-upload">
|
||||||
|
<label class="openapi-file-label">
|
||||||
|
<span>Файл OpenAPI/Swagger</span>
|
||||||
|
<input id="openapi-import-file" type="file" accept=".yaml,.yml,.json,application/json,text/yaml">
|
||||||
|
</label>
|
||||||
|
<textarea id="openapi-import-document" spellcheck="false" placeholder="Вставьте openapi.yaml или swagger.json"></textarea>
|
||||||
|
<div class="openapi-import-actions">
|
||||||
|
<button class="btn-primary" id="openapi-import-preview" type="button">Разобрать документ</button>
|
||||||
|
<button class="btn-secondary" id="openapi-import-reset" type="button">Сбросить</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="openapi-import-status" id="openapi-import-status"></div>
|
||||||
|
<div class="openapi-import-preview" id="openapi-import-preview-panel" hidden>
|
||||||
|
<div class="openapi-import-source" id="openapi-import-source"></div>
|
||||||
|
<div class="openapi-import-server-row">
|
||||||
|
<label for="openapi-import-server">Base URL</label>
|
||||||
|
<select id="openapi-import-server"></select>
|
||||||
|
<input id="openapi-import-server-custom" class="openapi-import-server-custom" type="url" placeholder="Или укажите свой base URL, например https://api.example.com">
|
||||||
|
</div>
|
||||||
|
<div class="openapi-import-server-row">
|
||||||
|
<label for="openapi-import-conflict-mode">Если операция уже существует</label>
|
||||||
|
<select id="openapi-import-conflict-mode">
|
||||||
|
<option value="rename" selected>Создать копию с новым именем</option>
|
||||||
|
<option value="skip">Пропустить</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="openapi-import-toolbar">
|
||||||
|
<div class="openapi-import-filter">
|
||||||
|
<label for="openapi-import-search">Поиск методов</label>
|
||||||
|
<input id="openapi-import-search" type="search" placeholder="Название, operationId или путь">
|
||||||
|
</div>
|
||||||
|
<div class="openapi-import-filter">
|
||||||
|
<label for="openapi-import-method-filter">HTTP-метод</label>
|
||||||
|
<select id="openapi-import-method-filter">
|
||||||
|
<option value="">Все методы</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="openapi-import-bulk-actions">
|
||||||
|
<button class="btn-secondary" id="openapi-import-select-visible" type="button">Выбрать видимые</button>
|
||||||
|
<button class="btn-secondary" id="openapi-import-clear-visible" type="button">Снять видимые</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="openapi-import-groups" id="openapi-import-groups"></div>
|
||||||
|
<div class="openapi-import-footer">
|
||||||
|
<span id="openapi-import-selection">Выбрано: 0</span>
|
||||||
|
<button class="btn-primary" id="openapi-import-create" type="button">Создать черновики</button>
|
||||||
|
</div>
|
||||||
|
<div class="openapi-import-result" id="openapi-import-result" hidden></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
|
||||||
<script src="%CRANK_BUNDLE_OPERATIONS%"></script>
|
<script src="%CRANK_BUNDLE_OPERATIONS%"></script>
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@@ -244,6 +244,14 @@
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
previewOpenApiImport: function(workspaceId, documentText) {
|
||||||
|
return post('/workspaces/' + encodeURIComponent(workspaceId) + '/imports/openapi/preview', {
|
||||||
|
document: documentText,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
createOpenApiImport: function(workspaceId, jobId, payload) {
|
||||||
|
return post('/workspaces/' + encodeURIComponent(workspaceId) + '/imports/openapi/' + encodeURIComponent(jobId) + '/create', payload);
|
||||||
|
},
|
||||||
listAgents: function(workspaceId) {
|
listAgents: function(workspaceId) {
|
||||||
return get('/workspaces/' + encodeURIComponent(workspaceId) + '/agents');
|
return get('/workspaces/' + encodeURIComponent(workspaceId) + '/agents');
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -506,6 +506,15 @@ document.addEventListener('alpine:init', function() {
|
|||||||
window.location.href = (window.CrankRoutes && window.CrankRoutes.wizard) || '/wizard/';
|
window.location.href = (window.CrankRoutes && window.CrankRoutes.wizard) || '/wizard/';
|
||||||
},
|
},
|
||||||
|
|
||||||
|
handleOpenApiImport() {
|
||||||
|
if (window.CrankOpenApiImport) {
|
||||||
|
window.CrankOpenApiImport.open({
|
||||||
|
workspaceId: this.workspaceId,
|
||||||
|
onImported: this.reload.bind(this),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
handleLogout() {
|
handleLogout() {
|
||||||
window.CrankAuth.logout();
|
window.CrankAuth.logout();
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -0,0 +1,554 @@
|
|||||||
|
(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, '&')
|
||||||
|
.replace(/</g, '<')
|
||||||
|
.replace(/>/g, '>')
|
||||||
|
.replace(/"/g, '"');
|
||||||
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
};
|
||||||
|
}());
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
const { test, expect } = require('@playwright/test');
|
const { test, expect } = require('@playwright/test');
|
||||||
const { login, localized } = require('./helpers');
|
const { login, localized, uniqueName } = require('./helpers');
|
||||||
|
|
||||||
test('operations page shows demo catalog and filter works', async ({ page }) => {
|
test('operations page shows demo catalog and filter works', async ({ page }) => {
|
||||||
await login(page);
|
await login(page);
|
||||||
@@ -11,3 +11,109 @@ test('operations page shows demo catalog and filter works', async ({ page }) =>
|
|||||||
await expect(page.locator('tbody tr')).toHaveCount(1);
|
await expect(page.locator('tbody tr')).toHaveCount(1);
|
||||||
await expect(page.locator('tbody tr').first()).toContainText(/frankfurter_latest_rate/i);
|
await expect(page.locator('tbody tr').first()).toContainText(/frankfurter_latest_rate/i);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('operations page imports OpenAPI methods as drafts', async ({ page }) => {
|
||||||
|
await login(page);
|
||||||
|
const operationId = uniqueName('get_rates');
|
||||||
|
const statusOperationId = `${operationId}_status`;
|
||||||
|
await page.getByRole('button', { name: /Импорт OpenAPI/i }).click();
|
||||||
|
await expect(page.locator('#openapi-import-modal')).toBeVisible();
|
||||||
|
await page.locator('#openapi-import-document').fill(`
|
||||||
|
openapi: 3.0.3
|
||||||
|
info:
|
||||||
|
title: Demo Import API
|
||||||
|
servers:
|
||||||
|
paths:
|
||||||
|
/v1/rates/{date}:
|
||||||
|
post:
|
||||||
|
operationId: ${operationId}
|
||||||
|
summary: Получить курсы валют
|
||||||
|
description: Курсы.
|
||||||
|
tags: [currency]
|
||||||
|
parameters:
|
||||||
|
- name: date
|
||||||
|
in: path
|
||||||
|
required: true
|
||||||
|
schema: { type: string }
|
||||||
|
- name: base
|
||||||
|
in: query
|
||||||
|
required: true
|
||||||
|
schema: { type: string }
|
||||||
|
- name: X-API-Version
|
||||||
|
in: header
|
||||||
|
required: false
|
||||||
|
schema: { type: string }
|
||||||
|
requestBody:
|
||||||
|
required: true
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
required: [symbols]
|
||||||
|
properties:
|
||||||
|
symbols: { type: string }
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: OK
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
base: { type: string }
|
||||||
|
/v1/status:
|
||||||
|
get:
|
||||||
|
operationId: ${statusOperationId}
|
||||||
|
summary: Проверить статус
|
||||||
|
tags: [service]
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: OK
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
status: { type: string }
|
||||||
|
`);
|
||||||
|
await page.locator('#openapi-import-preview').click();
|
||||||
|
await expect(page.locator('#openapi-import-preview-panel')).toBeVisible();
|
||||||
|
await expect(page.locator('.openapi-import-document-findings')).toContainText(/base URL/i);
|
||||||
|
await expect(page.locator('.openapi-import-group').filter({ hasText: 'currency' })).toBeVisible();
|
||||||
|
const importedOperation = page.locator('.openapi-import-operation').filter({ hasText: 'Получить курсы валют' });
|
||||||
|
await expect(importedOperation).toBeVisible();
|
||||||
|
await expect(page.locator('.openapi-import-operation').filter({ hasText: 'Проверить статус' })).toBeVisible();
|
||||||
|
await expect(importedOperation.locator('.openapi-import-mapping-preview')).toContainText('Path');
|
||||||
|
await expect(importedOperation.locator('.openapi-import-mapping-preview')).toContainText('date');
|
||||||
|
await expect(importedOperation.locator('.openapi-import-mapping-preview')).toContainText('Query');
|
||||||
|
await expect(importedOperation.locator('.openapi-import-mapping-preview')).toContainText('base');
|
||||||
|
await expect(importedOperation.locator('.openapi-import-mapping-preview')).toContainText('Header');
|
||||||
|
await expect(importedOperation.locator('.openapi-import-mapping-preview')).toContainText('X-API-Version');
|
||||||
|
await expect(importedOperation.locator('.openapi-import-mapping-preview')).toContainText('Body');
|
||||||
|
await expect(importedOperation.locator('.openapi-import-mapping-preview')).toContainText('symbols');
|
||||||
|
await expect(importedOperation.locator('.openapi-import-mapping-preview')).toContainText('Ответ');
|
||||||
|
await expect(page.locator('#openapi-import-selection')).toContainText('Выбрано: 2');
|
||||||
|
await page.locator('#openapi-import-search').fill('status');
|
||||||
|
await expect(page.locator('.openapi-import-operation:visible')).toHaveCount(1);
|
||||||
|
await expect(page.locator('.openapi-import-operation:visible').filter({ hasText: 'Проверить статус' })).toBeVisible();
|
||||||
|
await page.locator('#openapi-import-clear-visible').click();
|
||||||
|
await expect(page.locator('#openapi-import-selection')).toContainText('Выбрано: 1');
|
||||||
|
await page.locator('#openapi-import-search').fill('');
|
||||||
|
await page.locator('#openapi-import-method-filter').selectOption('POST');
|
||||||
|
await expect(page.locator('.openapi-import-operation:visible')).toHaveCount(1);
|
||||||
|
await expect(page.locator('.openapi-import-operation:visible').filter({ hasText: 'Получить курсы валют' })).toBeVisible();
|
||||||
|
await page.locator('#openapi-import-method-filter').selectOption('');
|
||||||
|
await page.locator('#openapi-import-server-custom').fill('https://api.example.test');
|
||||||
|
await page.locator('#openapi-import-create').click();
|
||||||
|
await expect(page.locator('#openapi-import-result')).toContainText('Результат импорта');
|
||||||
|
await expect(page.locator('.openapi-import-result-table')).toBeVisible();
|
||||||
|
await expect(page.locator('.openapi-import-result-row').filter({ hasText: operationId })).toContainText('POST /v1/rates/{date}');
|
||||||
|
await expect(page.locator('.openapi-import-result-row').filter({ hasText: operationId })).toContainText(/Описание инструмента слишком короткое/);
|
||||||
|
await expect(page.locator('.openapi-import-result-row').filter({ hasText: operationId })).toContainText('Исправить в мастере');
|
||||||
|
await expect(page.locator('.openapi-import-primary-result a')).toHaveAttribute(
|
||||||
|
'href',
|
||||||
|
/\/wizard\/\?mode=edit&operationId=op_/
|
||||||
|
);
|
||||||
|
await expect(page.locator('tbody')).toContainText(new RegExp(operationId, 'i'));
|
||||||
|
await expect(page.locator('tbody')).not.toContainText(new RegExp(statusOperationId, 'i'));
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user