Polish approval and import UI
CI / Rust Checks (push) Successful in 5m32s
CI / UI Checks (push) Successful in 4s
CI / Deployment Manifests (push) Successful in 3s
CI / Frontend E2E (push) Successful in 3m30s
CI / Deploy (push) Successful in 1m33s

This commit is contained in:
github-ops
2026-06-24 21:51:13 +00:00
parent 3b51cb89df
commit 209b3e1485
6 changed files with 142 additions and 98 deletions
+8 -4
View File
@@ -40,7 +40,7 @@
overflow: hidden;
border: 1px solid var(--border);
border-radius: 22px;
background: var(--bg-surface);
background: var(--bg-canvas);
box-shadow: 0 24px 80px rgba(0, 0, 0, 0.6);
}
@@ -50,7 +50,7 @@
gap: 18px;
padding: 22px 24px;
border-bottom: 1px solid var(--border);
background: var(--bg-surface);
background: var(--bg-canvas);
}
.openapi-import-header h2 {
@@ -67,12 +67,16 @@
.openapi-import-body {
overflow: auto;
padding: 20px 24px 24px;
background: var(--bg-surface);
background: var(--bg-canvas);
}
.openapi-import-upload {
display: grid;
gap: 12px;
padding: 16px;
border: 1px solid var(--border-subtle);
border-radius: 16px;
background: var(--bg-surface);
}
.openapi-file-label {
@@ -90,7 +94,7 @@
padding: 14px;
border: 1px solid var(--border);
border-radius: 14px;
background: var(--bg-overlay);
background: #0d1117;
color: var(--text-primary);
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
font-size: 12px;
+28
View File
@@ -222,8 +222,24 @@
.mapping-row-remove {
width: 32px;
height: 32px;
display: inline-flex;
align-items: center;
justify-content: center;
border: 1px solid var(--red-border);
border-radius: 8px;
background: var(--red-bg);
color: var(--red);
font-size: 18px;
line-height: 1;
font-weight: 800;
transition: background 0.15s, border-color 0.15s, color 0.15s, transform 0.15s;
}
.mapping-row-remove:hover {
border-color: rgba(248, 81, 73, 0.45);
background: rgba(248, 81, 73, 0.16);
color: #ff7b72;
transform: translateY(-1px);
}
.mapping-builder-actions {
@@ -1306,6 +1322,18 @@
box-shadow: none !important;
}
.form-group > .code-textarea {
border: 1px solid var(--border) !important;
border-radius: 8px !important;
background: #0d1117 !important;
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.015);
}
.form-group > .code-textarea:focus {
border-color: var(--accent) !important;
box-shadow: 0 0 0 3px var(--accent-ring), inset 0 0 0 1px rgba(255, 255, 255, 0.02) !important;
}
/* ── Section divider ── */
.section-divider {
display: flex;
+26 -14
View File
@@ -44,6 +44,19 @@
background: var(--accent);
color: #fff;
}
.key-kind-header-control {
display: flex;
align-items: center;
gap: 10px;
flex-wrap: wrap;
justify-content: flex-end;
}
.key-kind-header-hint {
flex-basis: 100%;
max-width: 520px;
margin: 0;
text-align: right;
}
.approval-warning-callout {
display: flex;
gap: 10px;
@@ -63,8 +76,10 @@
@media (max-width: 720px) {
#keys-table-wrap { display: none; }
.keys-card-list { display: grid; }
.key-kind-header-control { justify-content: stretch; width: 100%; }
.key-kind-tabs { width: 100%; }
.key-kind-tab { flex: 1; }
.key-kind-header-hint { text-align: left; }
}
</style>
<script src="%CRANK_BUNDLE_PROTECTED_CORE%"></script>
@@ -139,10 +154,17 @@
<p class="page-subtitle" data-i18n="apikeys.subtitle">These keys connect an MCP client to the MCP server and are issued for a specific agent.</p>
</div>
<div class="page-header-actions">
<button class="btn-primary" id="btn-create-key" type="button">
<svg width="13" height="13" viewBox="0 0 16 16" fill="currentColor"><path d="M7.75 2a.75.75 0 01.75.75V7h4.25a.75.75 0 010 1.5H8.5v4.25a.75.75 0 01-1.5 0V8.5H2.75a.75.75 0 010-1.5H7V2.75A.75.75 0 017.75 2z"/></svg>
<span id="btn-create-key-label" data-i18n="apikeys.new">Create key</span>
</button>
<div class="key-kind-header-control">
<div class="key-kind-tabs" role="tablist" aria-label="Key type">
<button class="key-kind-tab active" id="key-kind-mcp-client" type="button" data-key-kind="mcp_client" data-i18n="apikeys.kind.mcp">MCP clients</button>
<button class="key-kind-tab" id="key-kind-approval" type="button" data-key-kind="approval" data-i18n="apikeys.kind.approval">Approvals</button>
</div>
<button class="btn-primary" id="btn-create-key" type="button">
<svg width="13" height="13" viewBox="0 0 16 16" fill="currentColor"><path d="M7.75 2a.75.75 0 01.75.75V7h4.25a.75.75 0 010 1.5H8.5v4.25a.75.75 0 01-1.5 0V8.5H2.75a.75.75 0 010-1.5H7V2.75A.75.75 0 017.75 2z"/></svg>
<span id="btn-create-key-label" data-i18n="apikeys.new">Create key</span>
</button>
<div class="field-hint key-kind-header-hint" id="key-kind-hint"></div>
</div>
</div>
</div>
@@ -157,16 +179,6 @@
</div>
</div>
<div class="section-card">
<div class="section-card-body" style="display:flex;align-items:center;justify-content:space-between;gap:14px;flex-wrap:wrap;">
<div class="key-kind-tabs" role="tablist" aria-label="Key type">
<button class="key-kind-tab active" id="key-kind-mcp-client" type="button" data-key-kind="mcp_client" data-i18n="apikeys.kind.mcp">MCP clients</button>
<button class="key-kind-tab" id="key-kind-approval" type="button" data-key-kind="approval" data-i18n="apikeys.kind.approval">Approvals</button>
</div>
<div class="field-hint" id="key-kind-hint" style="max-width:560px;margin:0;"></div>
</div>
</div>
<div class="section-card">
<div class="section-card-header">
<div>
+79
View File
@@ -90,4 +90,83 @@
</div>
</div>
<div class="config-card approval-gate-card" style="margin-bottom: 20px;">
<div class="config-card-header">
<div class="config-card-header-icon">
<svg width="13" height="13" viewBox="0 0 16 16" fill="none" stroke="var(--text-secondary)" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round">
<path d="M8 2l5 2v4c0 3-2 5-5 6-3-1-5-3-5-6V4l5-2z"/>
<path d="M6 8l1.4 1.4L10.5 6"/>
</svg>
</div>
<div>
<div class="config-card-title" data-i18n="wizard.approval.title">Подтверждение человеком</div>
<div class="config-card-subtitle" data-i18n="wizard.approval.subtitle">Включайте для действий, которые нельзя выполнять без явного решения пользователя.</div>
</div>
</div>
<div class="config-card-body" style="gap: 16px;">
<label class="toggle-row approval-toggle-row" for="approval-required">
<span id="approval-required-toggle" class="toggle" aria-hidden="true"></span>
<span class="toggle-text">
<span class="toggle-label" data-i18n="wizard.approval.required_label">Требовать подтверждение перед выполнением</span>
<span class="toggle-desc" data-i18n="wizard.approval.required_desc">MCP клиент получит ожидающий запрос, а действие выполнится только после подтверждения через отдельный эндпоинт подтверждения.</span>
</span>
<input id="approval-required" type="checkbox" class="approval-toggle-input">
</label>
<div id="approval-config-fields" class="approval-config-fields" hidden>
<div class="form-row">
<div class="form-group">
<label class="form-label" for="approval-risk-level" data-i18n="wizard.approval.risk_level">Уровень риска</label>
<select id="approval-risk-level" class="form-select">
<option value="normal" data-i18n="wizard.approval.risk.normal">Обычное действие</option>
<option value="dangerous" data-i18n="wizard.approval.risk.dangerous">Опасное действие</option>
<option value="financial" data-i18n="wizard.approval.risk.financial">Финансовое действие</option>
<option value="irreversible" data-i18n="wizard.approval.risk.irreversible">Необратимое действие</option>
</select>
</div>
<div class="form-group">
<label class="form-label" for="approval-ttl-seconds" data-i18n="wizard.approval.ttl">Сколько ждать подтверждение</label>
<select id="approval-ttl-seconds" class="form-select">
<option value="60">1 минута</option>
<option value="180">3 минуты</option>
<option value="300" selected>5 минут</option>
</select>
</div>
</div>
<div class="form-group">
<label class="form-label" for="approval-title" data-i18n="wizard.approval.confirmation_title">Заголовок подтверждения</label>
<input id="approval-title" class="form-input" type="text" autocomplete="off" placeholder="Подтвердите выполнение операции">
<div class="form-hint" data-i18n="wizard.approval.confirmation_title_hint">Этот текст увидит внешний интерфейс подтверждения.</div>
</div>
<div class="form-group">
<label class="form-label" for="approval-body" data-i18n="wizard.approval.confirmation_body">Описание для пользователя</label>
<textarea id="approval-body" class="form-textarea" rows="4" placeholder="Проверьте параметры операции и подтвердите выполнение."></textarea>
<div class="form-hint" data-i18n="wizard.approval.confirmation_body_hint">Коротко объясните, что произойдет после подтверждения.</div>
</div>
<div class="form-row">
<label class="checkbox-pill approval-preview-pill">
<input id="approval-show-payload-preview" type="checkbox" checked>
<span data-i18n="wizard.approval.show_payload">Показывать параметры запроса</span>
</label>
<div class="form-group">
<label class="form-label" for="approval-payload-preview-mode" data-i18n="wizard.approval.payload_mode">Как показывать параметры</label>
<select id="approval-payload-preview-mode" class="form-select">
<option value="summary" data-i18n="wizard.approval.payload.summary">Краткое описание</option>
<option value="masked_json" data-i18n="wizard.approval.payload.masked_json">JSON с маскированием секретов</option>
</select>
</div>
</div>
<div class="approval-preview-card">
<div class="approval-preview-eyebrow" data-i18n="wizard.approval.preview_label">Предпросмотр для интерфейса подтверждения</div>
<div class="approval-preview-title" id="approval-preview-title">Подтвердите выполнение операции</div>
<div class="approval-preview-body" id="approval-preview-body">Проверьте параметры операции и подтвердите выполнение.</div>
</div>
</div>
</div>
</div>
</div><!-- /step-pane-3-rest -->
-79
View File
@@ -126,85 +126,6 @@ tls:
</div>
</div>
<div class="config-card approval-gate-card" style="margin-bottom: 20px;">
<div class="config-card-header">
<div class="config-card-header-icon">
<svg width="13" height="13" viewBox="0 0 16 16" fill="none" stroke="var(--text-secondary)" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round">
<path d="M8 2l5 2v4c0 3-2 5-5 6-3-1-5-3-5-6V4l5-2z"/>
<path d="M6 8l1.4 1.4L10.5 6"/>
</svg>
</div>
<div>
<div class="config-card-title" data-i18n="wizard.approval.title">Подтверждение человеком</div>
<div class="config-card-subtitle" data-i18n="wizard.approval.subtitle">Включайте для действий, которые нельзя выполнять без явного решения пользователя.</div>
</div>
</div>
<div class="config-card-body" style="gap: 16px;">
<label class="toggle-row approval-toggle-row" for="approval-required">
<span id="approval-required-toggle" class="toggle" aria-hidden="true"></span>
<span class="toggle-text">
<span class="toggle-label" data-i18n="wizard.approval.required_label">Требовать подтверждение перед выполнением</span>
<span class="toggle-desc" data-i18n="wizard.approval.required_desc">MCP клиент получит ожидающий запрос, а действие выполнится только после подтверждения через отдельный эндпоинт подтверждения.</span>
</span>
<input id="approval-required" type="checkbox" class="approval-toggle-input">
</label>
<div id="approval-config-fields" class="approval-config-fields" hidden>
<div class="form-row">
<div class="form-group">
<label class="form-label" for="approval-risk-level" data-i18n="wizard.approval.risk_level">Уровень риска</label>
<select id="approval-risk-level" class="form-select">
<option value="normal" data-i18n="wizard.approval.risk.normal">Обычное действие</option>
<option value="dangerous" data-i18n="wizard.approval.risk.dangerous">Опасное действие</option>
<option value="financial" data-i18n="wizard.approval.risk.financial">Финансовое действие</option>
<option value="irreversible" data-i18n="wizard.approval.risk.irreversible">Необратимое действие</option>
</select>
</div>
<div class="form-group">
<label class="form-label" for="approval-ttl-seconds" data-i18n="wizard.approval.ttl">Сколько ждать подтверждение</label>
<select id="approval-ttl-seconds" class="form-select">
<option value="60">1 минута</option>
<option value="180">3 минуты</option>
<option value="300" selected>5 минут</option>
</select>
</div>
</div>
<div class="form-group">
<label class="form-label" for="approval-title" data-i18n="wizard.approval.confirmation_title">Заголовок подтверждения</label>
<input id="approval-title" class="form-input" type="text" autocomplete="off" placeholder="Подтвердите выполнение операции">
<div class="form-hint" data-i18n="wizard.approval.confirmation_title_hint">Этот текст увидит внешний интерфейс подтверждения.</div>
</div>
<div class="form-group">
<label class="form-label" for="approval-body" data-i18n="wizard.approval.confirmation_body">Описание для пользователя</label>
<textarea id="approval-body" class="form-textarea" rows="4" placeholder="Проверьте параметры операции и подтвердите выполнение."></textarea>
<div class="form-hint" data-i18n="wizard.approval.confirmation_body_hint">Коротко объясните, что произойдет после подтверждения.</div>
</div>
<div class="form-row">
<label class="checkbox-pill approval-preview-pill">
<input id="approval-show-payload-preview" type="checkbox" checked>
<span data-i18n="wizard.approval.show_payload">Показывать параметры запроса</span>
</label>
<div class="form-group">
<label class="form-label" for="approval-payload-preview-mode" data-i18n="wizard.approval.payload_mode">Как показывать параметры</label>
<select id="approval-payload-preview-mode" class="form-select">
<option value="summary" data-i18n="wizard.approval.payload.summary">Краткое описание</option>
<option value="masked_json" data-i18n="wizard.approval.payload.masked_json">JSON с маскированием секретов</option>
</select>
</div>
</div>
<div class="approval-preview-card">
<div class="approval-preview-eyebrow" data-i18n="wizard.approval.preview_label">Предпросмотр для интерфейса подтверждения</div>
<div class="approval-preview-title" id="approval-preview-title">Подтвердите выполнение операции</div>
<div class="approval-preview-body" id="approval-preview-body">Проверьте параметры операции и подтвердите выполнение.</div>
</div>
</div>
</div>
</div>
<div class="section-divider" style="margin-bottom: 16px;">
<span class="section-divider-label" data-i18n="wizard.step5.live_title">Проверка и публикация</span>
<div class="section-divider-line"></div>
+1 -1
View File
@@ -828,8 +828,8 @@ test('wizard edit mode preserves explicit request mapping targets on save', asyn
await expect(page.locator('#tool-input-mapping')).toHaveValue(/query\.base/);
await expect(page.locator('#tool-input-mapping')).toHaveValue(/path\.date/);
await expect(page.locator('#tool-input-mapping')).toHaveValue(/transform: to_string/);
await page.evaluate(() => window.CrankWizardShell.doGoToStep(3));
await expect(page.locator('#approval-required')).toBeChecked();
await page.evaluate(() => window.CrankWizardShell.doGoToStep(5));
await expect(page.locator('#approval-config-fields')).toBeVisible();
await expect(page.locator('#approval-risk-level')).toHaveValue('financial');
await expect(page.locator('#approval-ttl-seconds')).toHaveValue('180');