Clarify request mapping UI
This commit is contained in:
+40
-1
@@ -202,6 +202,7 @@
|
|||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns:
|
grid-template-columns:
|
||||||
minmax(130px, 1fr)
|
minmax(130px, 1fr)
|
||||||
|
24px
|
||||||
minmax(105px, 0.65fr)
|
minmax(105px, 0.65fr)
|
||||||
minmax(130px, 1fr)
|
minmax(130px, 1fr)
|
||||||
minmax(120px, 0.8fr)
|
minmax(120px, 0.8fr)
|
||||||
@@ -216,7 +217,39 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.response-mapping-row {
|
.response-mapping-row {
|
||||||
grid-template-columns: minmax(160px, 1fr) minmax(140px, 1fr) 34px;
|
grid-template-columns: minmax(160px, 1fr) 24px minmax(140px, 1fr) 34px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mapping-field {
|
||||||
|
display: grid;
|
||||||
|
gap: 5px;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mapping-field-label {
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-size: 10.5px;
|
||||||
|
font-weight: 800;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
line-height: 1;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mapping-arrow {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
align-self: end;
|
||||||
|
width: 24px;
|
||||||
|
height: 34px;
|
||||||
|
color: var(--accent);
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 900;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mapping-default-value {
|
||||||
|
border-style: dashed;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mapping-row-remove {
|
.mapping-row-remove {
|
||||||
@@ -331,6 +364,12 @@
|
|||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mapping-arrow {
|
||||||
|
width: 100%;
|
||||||
|
height: 18px;
|
||||||
|
transform: rotate(90deg);
|
||||||
|
}
|
||||||
|
|
||||||
.mapping-row-remove {
|
.mapping-row-remove {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -114,16 +114,6 @@
|
|||||||
</label>
|
</label>
|
||||||
|
|
||||||
<div id="approval-config-fields" class="approval-config-fields" hidden>
|
<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">
|
<div class="form-group">
|
||||||
<label class="form-label" for="approval-ttl-seconds" data-i18n="wizard.approval.ttl">Сколько ждать подтверждение</label>
|
<label class="form-label" for="approval-ttl-seconds" data-i18n="wizard.approval.ttl">Сколько ждать подтверждение</label>
|
||||||
<select id="approval-ttl-seconds" class="form-select">
|
<select id="approval-ttl-seconds" class="form-select">
|
||||||
@@ -132,7 +122,6 @@
|
|||||||
<option value="300" selected>5 минут</option>
|
<option value="300" selected>5 минут</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="form-label" for="approval-title" data-i18n="wizard.approval.confirmation_title">Заголовок подтверждения</label>
|
<label class="form-label" for="approval-title" data-i18n="wizard.approval.confirmation_title">Заголовок подтверждения</label>
|
||||||
|
|||||||
@@ -598,11 +598,6 @@ var TRANSLATIONS = {
|
|||||||
'wizard.approval.subtitle': 'Enable this for actions that must not run without an explicit user decision.',
|
'wizard.approval.subtitle': 'Enable this for actions that must not run without an explicit user decision.',
|
||||||
'wizard.approval.required_label': 'Require confirmation before execution',
|
'wizard.approval.required_label': 'Require confirmation before execution',
|
||||||
'wizard.approval.required_desc': 'The MCP client receives a pending request, and the action runs only after confirmation through a separate approval endpoint.',
|
'wizard.approval.required_desc': 'The MCP client receives a pending request, and the action runs only after confirmation through a separate approval endpoint.',
|
||||||
'wizard.approval.risk_level': 'Risk level',
|
|
||||||
'wizard.approval.risk.normal': 'Normal action',
|
|
||||||
'wizard.approval.risk.dangerous': 'Dangerous action',
|
|
||||||
'wizard.approval.risk.financial': 'Financial action',
|
|
||||||
'wizard.approval.risk.irreversible': 'Irreversible action',
|
|
||||||
'wizard.approval.ttl': 'How long to wait for confirmation',
|
'wizard.approval.ttl': 'How long to wait for confirmation',
|
||||||
'wizard.approval.confirmation_title': 'Confirmation title',
|
'wizard.approval.confirmation_title': 'Confirmation title',
|
||||||
'wizard.approval.confirmation_title_hint': 'This text will be shown by the external confirmation interface.',
|
'wizard.approval.confirmation_title_hint': 'This text will be shown by the external confirmation interface.',
|
||||||
@@ -1465,11 +1460,6 @@ var TRANSLATIONS = {
|
|||||||
'wizard.approval.subtitle': 'Включайте для действий, которые нельзя выполнять без явного решения пользователя.',
|
'wizard.approval.subtitle': 'Включайте для действий, которые нельзя выполнять без явного решения пользователя.',
|
||||||
'wizard.approval.required_label': 'Требовать подтверждение перед выполнением',
|
'wizard.approval.required_label': 'Требовать подтверждение перед выполнением',
|
||||||
'wizard.approval.required_desc': 'MCP клиент получит ожидающий запрос, а действие выполнится только после подтверждения через отдельный эндпоинт подтверждения.',
|
'wizard.approval.required_desc': 'MCP клиент получит ожидающий запрос, а действие выполнится только после подтверждения через отдельный эндпоинт подтверждения.',
|
||||||
'wizard.approval.risk_level': 'Уровень риска',
|
|
||||||
'wizard.approval.risk.normal': 'Обычное действие',
|
|
||||||
'wizard.approval.risk.dangerous': 'Опасное действие',
|
|
||||||
'wizard.approval.risk.financial': 'Финансовое действие',
|
|
||||||
'wizard.approval.risk.irreversible': 'Необратимое действие',
|
|
||||||
'wizard.approval.ttl': 'Сколько ждать подтверждение',
|
'wizard.approval.ttl': 'Сколько ждать подтверждение',
|
||||||
'wizard.approval.confirmation_title': 'Заголовок подтверждения',
|
'wizard.approval.confirmation_title': 'Заголовок подтверждения',
|
||||||
'wizard.approval.confirmation_title_hint': 'Этот текст увидит внешний интерфейс подтверждения.',
|
'wizard.approval.confirmation_title_hint': 'Этот текст увидит внешний интерфейс подтверждения.',
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ function buildApprovalPolicy() {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
required: true,
|
required: true,
|
||||||
risk_level: textValue('approval-risk-level') || 'normal',
|
risk_level: 'normal',
|
||||||
confirmation_title: title,
|
confirmation_title: title,
|
||||||
confirmation_body_template: body,
|
confirmation_body_template: body,
|
||||||
ttl_seconds: normalizeApprovalTtlSeconds(textValue('approval-ttl-seconds')),
|
ttl_seconds: normalizeApprovalTtlSeconds(textValue('approval-ttl-seconds')),
|
||||||
@@ -191,7 +191,6 @@ function setApprovalPolicyEditor(policy) {
|
|||||||
var enabled = !!(policy && policy.required);
|
var enabled = !!(policy && policy.required);
|
||||||
var required = document.getElementById('approval-required');
|
var required = document.getElementById('approval-required');
|
||||||
if (required) required.checked = enabled;
|
if (required) required.checked = enabled;
|
||||||
setValue('approval-risk-level', policy && policy.risk_level ? policy.risk_level : 'normal');
|
|
||||||
setValue('approval-ttl-seconds', policy && policy.ttl_seconds ? String(policy.ttl_seconds) : '300');
|
setValue('approval-ttl-seconds', policy && policy.ttl_seconds ? String(policy.ttl_seconds) : '300');
|
||||||
setValue('approval-title', policy && policy.confirmation_title ? policy.confirmation_title : tKey('wizard.approval.default_title'));
|
setValue('approval-title', policy && policy.confirmation_title ? policy.confirmation_title : tKey('wizard.approval.default_title'));
|
||||||
setValue('approval-body', policy && policy.confirmation_body_template ? policy.confirmation_body_template : tKey('wizard.approval.default_body'));
|
setValue('approval-body', policy && policy.confirmation_body_template ? policy.confirmation_body_template : tKey('wizard.approval.default_body'));
|
||||||
@@ -219,7 +218,6 @@ function updateApprovalPolicyUi() {
|
|||||||
function bindApprovalPolicyControls() {
|
function bindApprovalPolicyControls() {
|
||||||
[
|
[
|
||||||
'approval-required',
|
'approval-required',
|
||||||
'approval-risk-level',
|
|
||||||
'approval-ttl-seconds',
|
'approval-ttl-seconds',
|
||||||
'approval-title',
|
'approval-title',
|
||||||
'approval-body',
|
'approval-body',
|
||||||
|
|||||||
@@ -346,6 +346,25 @@
|
|||||||
return button;
|
return button;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function wrapMappingControl(labelText, control) {
|
||||||
|
var wrapper = document.createElement('label');
|
||||||
|
wrapper.className = 'mapping-field';
|
||||||
|
var label = document.createElement('span');
|
||||||
|
label.className = 'mapping-field-label';
|
||||||
|
label.textContent = labelText;
|
||||||
|
wrapper.appendChild(label);
|
||||||
|
wrapper.appendChild(control);
|
||||||
|
return wrapper;
|
||||||
|
}
|
||||||
|
|
||||||
|
function makeMappingArrow() {
|
||||||
|
var arrow = document.createElement('span');
|
||||||
|
arrow.className = 'mapping-arrow';
|
||||||
|
arrow.setAttribute('aria-hidden', 'true');
|
||||||
|
arrow.textContent = '→';
|
||||||
|
return arrow;
|
||||||
|
}
|
||||||
|
|
||||||
function renderRequestRows(rows) {
|
function renderRequestRows(rows) {
|
||||||
var root = field('wizard-request-mapping-rows');
|
var root = field('wizard-request-mapping-rows');
|
||||||
if (!root) return;
|
if (!root) return;
|
||||||
@@ -364,7 +383,10 @@
|
|||||||
|
|
||||||
var input = makeInput(row.input, 'form-input input-mono mapping-source', 'base');
|
var input = makeInput(row.input, 'form-input input-mono mapping-source', 'base');
|
||||||
input.dataset.role = 'input';
|
input.dataset.role = 'input';
|
||||||
item.appendChild(input);
|
input.title = 'Поле, которое MCP клиент передает инструменту';
|
||||||
|
item.appendChild(wrapMappingControl('Из инструмента', input));
|
||||||
|
|
||||||
|
item.appendChild(makeMappingArrow());
|
||||||
|
|
||||||
var select = document.createElement('select');
|
var select = document.createElement('select');
|
||||||
select.className = 'form-select mapping-target';
|
select.className = 'form-select mapping-target';
|
||||||
@@ -378,16 +400,17 @@
|
|||||||
select.appendChild(makeOption(entry[0], entry[1], entry[0] === row.target));
|
select.appendChild(makeOption(entry[0], entry[1], entry[0] === row.target));
|
||||||
});
|
});
|
||||||
select.addEventListener('change', syncVisualMappingsToYaml);
|
select.addEventListener('change', syncVisualMappingsToYaml);
|
||||||
item.appendChild(select);
|
item.appendChild(wrapMappingControl('Куда в API', select));
|
||||||
|
|
||||||
var apiName = makeInput(row.apiName || row.input, 'form-input input-mono mapping-api-name', 'base');
|
var apiName = makeInput(row.apiName || row.input, 'form-input input-mono mapping-api-name', 'base');
|
||||||
apiName.dataset.role = 'apiName';
|
apiName.dataset.role = 'apiName';
|
||||||
item.appendChild(apiName);
|
apiName.title = 'Имя path, query, header или body-поля в API-запросе';
|
||||||
|
item.appendChild(wrapMappingControl('Имя в API', apiName));
|
||||||
|
|
||||||
var defaultValue = makeInput(row.defaultValue, 'form-input input-mono mapping-default-value', 'по умолчанию');
|
var defaultValue = makeInput(row.defaultValue, 'form-input input-mono mapping-default-value', 'если не передано');
|
||||||
defaultValue.dataset.role = 'defaultValue';
|
defaultValue.dataset.role = 'defaultValue';
|
||||||
defaultValue.title = 'Значение по умолчанию, если поле не передано';
|
defaultValue.title = 'Необязательно. Это значение уйдет в API, если агент не передал поле инструмента.';
|
||||||
item.appendChild(defaultValue);
|
item.appendChild(wrapMappingControl('Если пусто', defaultValue));
|
||||||
|
|
||||||
var transform = document.createElement('select');
|
var transform = document.createElement('select');
|
||||||
transform.className = 'form-select mapping-transform';
|
transform.className = 'form-select mapping-transform';
|
||||||
@@ -398,7 +421,7 @@
|
|||||||
});
|
});
|
||||||
transform.title = 'Простое преобразование перед отправкой в API';
|
transform.title = 'Простое преобразование перед отправкой в API';
|
||||||
transform.addEventListener('change', syncVisualMappingsToYaml);
|
transform.addEventListener('change', syncVisualMappingsToYaml);
|
||||||
item.appendChild(transform);
|
item.appendChild(wrapMappingControl('Преобразование', transform));
|
||||||
|
|
||||||
item.appendChild(makeRemoveButton(item));
|
item.appendChild(makeRemoveButton(item));
|
||||||
return item;
|
return item;
|
||||||
@@ -422,11 +445,15 @@
|
|||||||
|
|
||||||
var responsePath = makeInput(row.responsePath, 'form-input input-mono mapping-response-path', 'rates.EUR');
|
var responsePath = makeInput(row.responsePath, 'form-input input-mono mapping-response-path', 'rates.EUR');
|
||||||
responsePath.dataset.role = 'responsePath';
|
responsePath.dataset.role = 'responsePath';
|
||||||
item.appendChild(responsePath);
|
responsePath.title = 'Поле из ответа API';
|
||||||
|
item.appendChild(wrapMappingControl('Из ответа API', responsePath));
|
||||||
|
|
||||||
|
item.appendChild(makeMappingArrow());
|
||||||
|
|
||||||
var output = makeInput(row.output, 'form-input input-mono mapping-output-field', 'rate');
|
var output = makeInput(row.output, 'form-input input-mono mapping-output-field', 'rate');
|
||||||
output.dataset.role = 'output';
|
output.dataset.role = 'output';
|
||||||
item.appendChild(output);
|
output.title = 'Поле результата, которое получит MCP клиент';
|
||||||
|
item.appendChild(wrapMappingControl('В результат инструмента', output));
|
||||||
item.appendChild(makeRemoveButton(item));
|
item.appendChild(makeRemoveButton(item));
|
||||||
return item;
|
return item;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -831,7 +831,7 @@ test('wizard edit mode preserves explicit request mapping targets on save', asyn
|
|||||||
await page.evaluate(() => window.CrankWizardShell.doGoToStep(3));
|
await page.evaluate(() => window.CrankWizardShell.doGoToStep(3));
|
||||||
await expect(page.locator('#approval-required')).toBeChecked();
|
await expect(page.locator('#approval-required')).toBeChecked();
|
||||||
await expect(page.locator('#approval-config-fields')).toBeVisible();
|
await expect(page.locator('#approval-config-fields')).toBeVisible();
|
||||||
await expect(page.locator('#approval-risk-level')).toHaveValue('financial');
|
await expect(page.locator('#approval-risk-level')).toHaveCount(0);
|
||||||
await expect(page.locator('#approval-ttl-seconds')).toHaveValue('180');
|
await expect(page.locator('#approval-ttl-seconds')).toHaveValue('180');
|
||||||
await expect(page.locator('#approval-title')).toHaveValue('Подтвердите обмен валюты');
|
await expect(page.locator('#approval-title')).toHaveValue('Подтвердите обмен валюты');
|
||||||
await expect(page.locator('#approval-body')).toHaveValue('Проверьте валюты и подтвердите выполнение операции.');
|
await expect(page.locator('#approval-body')).toHaveValue('Проверьте валюты и подтвердите выполнение операции.');
|
||||||
@@ -863,7 +863,7 @@ test('wizard edit mode preserves explicit request mapping targets on save', asyn
|
|||||||
streaming: null,
|
streaming: null,
|
||||||
approval_policy: {
|
approval_policy: {
|
||||||
required: true,
|
required: true,
|
||||||
risk_level: 'financial',
|
risk_level: 'normal',
|
||||||
confirmation_title: 'Подтвердите обмен валюты',
|
confirmation_title: 'Подтвердите обмен валюты',
|
||||||
confirmation_body_template: 'Проверьте валюты и подтвердите выполнение операции.',
|
confirmation_body_template: 'Проверьте валюты и подтвердите выполнение операции.',
|
||||||
ttl_seconds: 180,
|
ttl_seconds: 180,
|
||||||
|
|||||||
Reference in New Issue
Block a user