ui: gate community wizard capabilities
This commit is contained in:
@@ -31,8 +31,10 @@ Implementation slices:
|
|||||||
Progress:
|
Progress:
|
||||||
- done: shared capability payload exists in `crank-core` and `admin-api`;
|
- done: shared capability payload exists in `crank-core` and `admin-api`;
|
||||||
- done: authenticated `/api/admin/capabilities` exposes Community defaults to UI;
|
- done: authenticated `/api/admin/capabilities` exposes Community defaults to UI;
|
||||||
- in_progress: first UI consumer uses capability payload in `Settings`;
|
- done: first UI consumer uses capability payload in `Settings`;
|
||||||
- pending: gate premium protocols and security levels in wizard and agents flows.
|
- done: `protocol-capabilities` is edition-aware and Community wizard hides premium protocol cards;
|
||||||
|
- in_progress: surface Community-only security guidance inside wizard flow;
|
||||||
|
- pending: add server-side validation so unsupported protocol and security-level drafts are rejected, not only hidden in UI.
|
||||||
|
|
||||||
DoD:
|
DoD:
|
||||||
- UI знает, какие функции входят в текущую редакцию;
|
- UI знает, какие функции входят в текущую редакцию;
|
||||||
|
|||||||
@@ -1038,6 +1038,35 @@ mod tests {
|
|||||||
assert_eq!(response["limits"]["max_agents_per_workspace"], 1);
|
assert_eq!(response["limits"]["max_agents_per_workspace"], 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "multi_thread")]
|
||||||
|
#[serial]
|
||||||
|
async fn returns_community_protocol_capabilities_only_for_supported_protocols() {
|
||||||
|
let registry = test_registry().await;
|
||||||
|
let storage_root = test_storage_root("community_protocol_capabilities");
|
||||||
|
let base_url = spawn_admin_api(build_test_app(registry, storage_root)).await;
|
||||||
|
let client = authorized_client(&base_url).await;
|
||||||
|
|
||||||
|
let response = assert_success_json(
|
||||||
|
client
|
||||||
|
.get(format!("{base_url}/protocol-capabilities"))
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.unwrap(),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
assert_eq!(response["items"].as_array().unwrap().len(), 3);
|
||||||
|
assert_eq!(
|
||||||
|
response["items"]
|
||||||
|
.as_array()
|
||||||
|
.unwrap()
|
||||||
|
.iter()
|
||||||
|
.map(|item| item["protocol"].as_str().unwrap())
|
||||||
|
.collect::<Vec<_>>(),
|
||||||
|
vec!["rest", "graphql", "grpc"]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread")]
|
#[tokio::test(flavor = "multi_thread")]
|
||||||
#[serial]
|
#[serial]
|
||||||
async fn manages_workspace_access_lifecycle() {
|
async fn manages_workspace_access_lifecycle() {
|
||||||
|
|||||||
@@ -1556,13 +1556,9 @@ impl AdminService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub async fn list_protocol_capabilities(&self) -> Vec<ProtocolCapabilityView> {
|
pub async fn list_protocol_capabilities(&self) -> Vec<ProtocolCapabilityView> {
|
||||||
[
|
self.get_capabilities()
|
||||||
Protocol::Rest,
|
.await
|
||||||
Protocol::Graphql,
|
.supported_protocols
|
||||||
Protocol::Grpc,
|
|
||||||
Protocol::Websocket,
|
|
||||||
Protocol::Soap,
|
|
||||||
]
|
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(protocol_capability_view)
|
.map(protocol_capability_view)
|
||||||
.collect()
|
.collect()
|
||||||
|
|||||||
+21
-4
@@ -1,10 +1,29 @@
|
|||||||
/* ══════════════════════════════════════════════════
|
/* ══════════════════════════════════════════════════
|
||||||
WIZARD — shell
|
WIZARD — shell
|
||||||
══════════════════════════════════════════════════ */
|
══════════════════════════════════════════════════ */
|
||||||
|
.wizard-page {
|
||||||
|
padding-top: 60px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wizard-page .navbar {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wizard-page .progress-strip {
|
||||||
|
position: fixed;
|
||||||
|
top: 60px;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.wizard-shell {
|
.wizard-shell {
|
||||||
min-height: calc(100vh - 60px);
|
min-height: calc(100vh - 60px);
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
padding-top: 56px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ══════════════════════════════════════════════════
|
/* ══════════════════════════════════════════════════
|
||||||
@@ -18,8 +37,6 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
height: 56px;
|
height: 56px;
|
||||||
gap: 20px;
|
gap: 20px;
|
||||||
position: sticky;
|
|
||||||
top: 60px;
|
|
||||||
z-index: 30;
|
z-index: 30;
|
||||||
box-shadow: 0 1px 0 var(--border-subtle), 0 4px 16px rgba(0, 0, 0, 0.4);
|
box-shadow: 0 1px 0 var(--border-subtle), 0 4px 16px rgba(0, 0, 0, 0.4);
|
||||||
}
|
}
|
||||||
@@ -244,9 +261,9 @@
|
|||||||
.steps-list::before {
|
.steps-list::before {
|
||||||
content: '';
|
content: '';
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 17px;
|
left: 27px;
|
||||||
top: 32px;
|
top: 32px;
|
||||||
bottom: 32px;
|
bottom: 40px;
|
||||||
width: 1px;
|
width: 1px;
|
||||||
background: var(--border);
|
background: var(--border);
|
||||||
z-index: 0;
|
z-index: 0;
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
<link rel="stylesheet" href="../../css/pages.css">
|
<link rel="stylesheet" href="../../css/pages.css">
|
||||||
<script src="%CRANK_BUNDLE_PROTECTED_CORE%"></script>
|
<script src="%CRANK_BUNDLE_PROTECTED_CORE%"></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body class="wizard-page">
|
||||||
|
|
||||||
<!-- ═══════════════════ NAVBAR ═══════════════════ -->
|
<!-- ═══════════════════ NAVBAR ═══════════════════ -->
|
||||||
<nav class="navbar">
|
<nav class="navbar">
|
||||||
|
|||||||
@@ -134,4 +134,15 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="info-callout" id="wizard-edition-protocol-note" hidden>
|
||||||
|
<svg class="info-callout-icon" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<circle cx="8" cy="8" r="7"/>
|
||||||
|
<path d="M8 11V8M8 5v-.5"/>
|
||||||
|
</svg>
|
||||||
|
<div class="info-callout-body">
|
||||||
|
<div class="info-callout-title" data-i18n="wizard.step1.community_protocol_title">Community protocol scope</div>
|
||||||
|
<div class="info-callout-text" id="wizard-edition-protocol-note-text"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
</div><!-- /step-pane-1 -->
|
</div><!-- /step-pane-1 -->
|
||||||
|
|||||||
@@ -87,6 +87,17 @@ tls:
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="info-callout" id="wizard-security-level-note" hidden style="margin-bottom: 20px;">
|
||||||
|
<svg class="info-callout-icon" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<circle cx="8" cy="8" r="7"/>
|
||||||
|
<path d="M8 11V8M8 5v-.5"/>
|
||||||
|
</svg>
|
||||||
|
<div class="info-callout-body">
|
||||||
|
<div class="info-callout-title" data-i18n="wizard.step5.security_level_title">Operation security in Community</div>
|
||||||
|
<div class="info-callout-text"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div id="wizard-streaming-config-card" class="config-card" style="margin-bottom: 20px;">
|
<div id="wizard-streaming-config-card" class="config-card" style="margin-bottom: 20px;">
|
||||||
<div class="config-card-header">
|
<div class="config-card-header">
|
||||||
<div class="config-card-header-icon">
|
<div class="config-card-header-icon">
|
||||||
|
|||||||
@@ -575,6 +575,8 @@ var TRANSLATIONS = {
|
|||||||
'wizard.step1.unary': 'Unary',
|
'wizard.step1.unary': 'Unary',
|
||||||
'wizard.step1.tip_title': 'You can change this later',
|
'wizard.step1.tip_title': 'You can change this later',
|
||||||
'wizard.step1.tip_body': 'Switching protocol after configuring subsequent steps will clear schema and mapping data. Save your operation as a draft before experimenting. See protocol migration guide for details.',
|
'wizard.step1.tip_body': 'Switching protocol after configuring subsequent steps will clear schema and mapping data. Save your operation as a draft before experimenting. See protocol migration guide for details.',
|
||||||
|
'wizard.step1.community_protocol_title': 'Community protocol scope',
|
||||||
|
'wizard.step1.community_protocol_note': 'This Community build hides {protocols}. Commercial editions unlock those protocol surfaces.',
|
||||||
'wizard.step2.title': 'Select upstream & endpoint',
|
'wizard.step2.title': 'Select upstream & endpoint',
|
||||||
'wizard.step2.subtitle': 'Choose an existing upstream or register a new one. Then define the specific endpoint path this operation will call.',
|
'wizard.step2.subtitle': 'Choose an existing upstream or register a new one. Then define the specific endpoint path this operation will call.',
|
||||||
'wizard.required': 'Required',
|
'wizard.required': 'Required',
|
||||||
@@ -762,6 +764,8 @@ var TRANSLATIONS = {
|
|||||||
'wizard.step5.execution': 'Execution',
|
'wizard.step5.execution': 'Execution',
|
||||||
'wizard.step5.exec_title': 'Execution config',
|
'wizard.step5.exec_title': 'Execution config',
|
||||||
'wizard.step5.exec_subtitle': 'Timeouts, retry policy and auth profile reference',
|
'wizard.step5.exec_subtitle': 'Timeouts, retry policy and auth profile reference',
|
||||||
|
'wizard.step5.security_level_title': 'Operation security in Community',
|
||||||
|
'wizard.step5.community_security_note': 'Community supports only the standard operation security level with static AI-agent keys. Short-lived and one-time token flows require a commercial edition.',
|
||||||
'wizard.step5.live_title': 'Live validation and publishing',
|
'wizard.step5.live_title': 'Live validation and publishing',
|
||||||
'wizard.step5.draft_actions': 'Draft actions',
|
'wizard.step5.draft_actions': 'Draft actions',
|
||||||
'wizard.step5.live_save_title': 'Live actions save the draft first',
|
'wizard.step5.live_save_title': 'Live actions save the draft first',
|
||||||
@@ -931,6 +935,8 @@ var TRANSLATIONS = {
|
|||||||
'agents.title': 'Agents',
|
'agents.title': 'Agents',
|
||||||
'agents.subtitle': 'Named MCP endpoints that expose a curated subset of operations to an LLM',
|
'agents.subtitle': 'Named MCP endpoints that expose a curated subset of operations to an LLM',
|
||||||
'agents.new': 'New agent',
|
'agents.new': 'New agent',
|
||||||
|
'agents.limit.title': 'Agent limit reached',
|
||||||
|
'agents.limit.message': 'This Community build allows up to {limit} AI agent. Upgrade the edition or remove an existing agent to add another one.',
|
||||||
'agents.stats.total': 'Total agents',
|
'agents.stats.total': 'Total agents',
|
||||||
'agents.stats.total_sub': 'across this workspace',
|
'agents.stats.total_sub': 'across this workspace',
|
||||||
'agents.stats.published': 'Published',
|
'agents.stats.published': 'Published',
|
||||||
@@ -1623,6 +1629,8 @@ var TRANSLATIONS = {
|
|||||||
'wizard.step1.unary': 'Unary',
|
'wizard.step1.unary': 'Unary',
|
||||||
'wizard.step1.tip_title': 'Это можно изменить позже',
|
'wizard.step1.tip_title': 'Это можно изменить позже',
|
||||||
'wizard.step1.tip_body': 'Смена протокола после настройки следующих шагов очистит схемы и mapping. Сохраните операцию как черновик перед экспериментами. См. гайд по миграции протоколов.',
|
'wizard.step1.tip_body': 'Смена протокола после настройки следующих шагов очистит схемы и mapping. Сохраните операцию как черновик перед экспериментами. См. гайд по миграции протоколов.',
|
||||||
|
'wizard.step1.community_protocol_title': 'Граница протоколов Community',
|
||||||
|
'wizard.step1.community_protocol_note': 'В этой Community-сборке скрыты {protocols}. Эти протокольные поверхности открываются в коммерческих редакциях.',
|
||||||
'wizard.step2.title': 'Выберите upstream и endpoint',
|
'wizard.step2.title': 'Выберите upstream и endpoint',
|
||||||
'wizard.step2.subtitle': 'Выберите существующий upstream или зарегистрируйте новый. Затем задайте конкретный путь endpoint-а для этой операции.',
|
'wizard.step2.subtitle': 'Выберите существующий upstream или зарегистрируйте новый. Затем задайте конкретный путь endpoint-а для этой операции.',
|
||||||
'wizard.required': 'Обязательно',
|
'wizard.required': 'Обязательно',
|
||||||
@@ -1812,6 +1820,8 @@ var TRANSLATIONS = {
|
|||||||
'wizard.step5.execution': 'Исполнение',
|
'wizard.step5.execution': 'Исполнение',
|
||||||
'wizard.step5.exec_title': 'Execution config',
|
'wizard.step5.exec_title': 'Execution config',
|
||||||
'wizard.step5.exec_subtitle': 'Таймауты, retry policy и ссылка на auth profile',
|
'wizard.step5.exec_subtitle': 'Таймауты, retry policy и ссылка на auth profile',
|
||||||
|
'wizard.step5.security_level_title': 'Защита операций в Community',
|
||||||
|
'wizard.step5.community_security_note': 'Community поддерживает только стандартный уровень защиты операций со статическими ключами AI-агента. Короткоживущие и одноразовые токены требуют коммерческую редакцию.',
|
||||||
'wizard.step5.live_title': 'Live-проверка и публикация',
|
'wizard.step5.live_title': 'Live-проверка и публикация',
|
||||||
'wizard.step5.draft_actions': 'Действия черновика',
|
'wizard.step5.draft_actions': 'Действия черновика',
|
||||||
'wizard.step5.live_save_title': 'Live-действия сначала сохраняют черновик',
|
'wizard.step5.live_save_title': 'Live-действия сначала сохраняют черновик',
|
||||||
@@ -1985,6 +1995,8 @@ var TRANSLATIONS = {
|
|||||||
'agents.title': 'Агенты',
|
'agents.title': 'Агенты',
|
||||||
'agents.subtitle': 'Именованные MCP endpoint-ы, открывающие LLM ограниченный набор операций',
|
'agents.subtitle': 'Именованные MCP endpoint-ы, открывающие LLM ограниченный набор операций',
|
||||||
'agents.new': 'Новый агент',
|
'agents.new': 'Новый агент',
|
||||||
|
'agents.limit.title': 'Достигнут лимит агентов',
|
||||||
|
'agents.limit.message': 'В этой Community-сборке доступно не более {limit} AI-агента. Чтобы добавить еще одного, удалите существующего агента или перейдите на другую редакцию.',
|
||||||
'agents.stats.total': 'Всего агентов',
|
'agents.stats.total': 'Всего агентов',
|
||||||
'agents.stats.total_sub': 'в этом воркспейсе',
|
'agents.stats.total_sub': 'в этом воркспейсе',
|
||||||
'agents.stats.published': 'Опубликованы',
|
'agents.stats.published': 'Опубликованы',
|
||||||
|
|||||||
@@ -175,6 +175,9 @@
|
|||||||
function bindProtocolCards() {
|
function bindProtocolCards() {
|
||||||
document.querySelectorAll('.protocol-card').forEach(function(card) {
|
document.querySelectorAll('.protocol-card').forEach(function(card) {
|
||||||
card.addEventListener('click', function() {
|
card.addEventListener('click', function() {
|
||||||
|
if (card.hidden || card.getAttribute('aria-disabled') === 'true') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
document.querySelectorAll('.protocol-card').forEach(function(item) {
|
document.querySelectorAll('.protocol-card').forEach(function(item) {
|
||||||
item.classList.remove('selected');
|
item.classList.remove('selected');
|
||||||
item.setAttribute('aria-checked', 'false');
|
item.setAttribute('aria-checked', 'false');
|
||||||
@@ -214,6 +217,56 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function applyEditionProtocolVisibility(capabilities) {
|
||||||
|
var supported = capabilities && Array.isArray(capabilities.supported_protocols)
|
||||||
|
? capabilities.supported_protocols.slice()
|
||||||
|
: ['rest', 'graphql', 'grpc'];
|
||||||
|
var hiddenProtocols = [];
|
||||||
|
|
||||||
|
document.querySelectorAll('.protocol-card').forEach(function(card) {
|
||||||
|
var protocol = card.dataset.protocol || 'rest';
|
||||||
|
var isSupported = supported.indexOf(protocol) >= 0;
|
||||||
|
card.hidden = !isSupported;
|
||||||
|
card.setAttribute('aria-disabled', isSupported ? 'false' : 'true');
|
||||||
|
if (!isSupported) {
|
||||||
|
card.classList.remove('selected');
|
||||||
|
card.setAttribute('aria-checked', 'false');
|
||||||
|
hiddenProtocols.push(protocol);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (supported.indexOf(window.wizardProtocol) === -1) {
|
||||||
|
window.wizardProtocol = supported[0] || 'rest';
|
||||||
|
}
|
||||||
|
|
||||||
|
var selectedCard = document.querySelector('.protocol-card[data-protocol="' + window.wizardProtocol + '"]');
|
||||||
|
if (selectedCard) {
|
||||||
|
selectedCard.classList.add('selected');
|
||||||
|
selectedCard.setAttribute('aria-checked', 'true');
|
||||||
|
}
|
||||||
|
|
||||||
|
var step3Name = document.getElementById('sidebar-step-3-name');
|
||||||
|
var labels = window.CrankWizardState.step3Labels();
|
||||||
|
if (step3Name) {
|
||||||
|
step3Name.textContent = labels[window.wizardProtocol] || tKey('wizard.step3.rest.label');
|
||||||
|
}
|
||||||
|
|
||||||
|
var note = document.getElementById('wizard-edition-protocol-note');
|
||||||
|
var noteText = document.getElementById('wizard-edition-protocol-note-text');
|
||||||
|
if (note && noteText) {
|
||||||
|
if (hiddenProtocols.length) {
|
||||||
|
note.hidden = false;
|
||||||
|
noteText.textContent = tfKey('wizard.step1.community_protocol_note', {
|
||||||
|
protocols: hiddenProtocols.map(function(protocol) {
|
||||||
|
return tKey('settings.capability.' + protocol);
|
||||||
|
}).join(', ')
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
note.hidden = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
window.CrankWizardShell = {
|
window.CrankWizardShell = {
|
||||||
TOTAL_STEPS: TOTAL_STEPS,
|
TOTAL_STEPS: TOTAL_STEPS,
|
||||||
step3PanelId: step3PanelId,
|
step3PanelId: step3PanelId,
|
||||||
@@ -222,5 +275,6 @@
|
|||||||
goToStep: goToStep,
|
goToStep: goToStep,
|
||||||
loadWizardPanels: loadWizardPanels,
|
loadWizardPanels: loadWizardPanels,
|
||||||
bindProtocolCards: bindProtocolCards,
|
bindProtocolCards: bindProtocolCards,
|
||||||
|
applyEditionProtocolVisibility: applyEditionProtocolVisibility,
|
||||||
};
|
};
|
||||||
}());
|
}());
|
||||||
|
|||||||
@@ -19,6 +19,7 @@
|
|||||||
grpcDescriptorServices: [],
|
grpcDescriptorServices: [],
|
||||||
soapServiceCatalog: [],
|
soapServiceCatalog: [],
|
||||||
wizardProtocolCapabilities: null,
|
wizardProtocolCapabilities: null,
|
||||||
|
wizardEditionCapabilities: null,
|
||||||
wizardSecrets: [],
|
wizardSecrets: [],
|
||||||
wizardAuthProfiles: [],
|
wizardAuthProfiles: [],
|
||||||
selectedUpstreamId: null,
|
selectedUpstreamId: null,
|
||||||
@@ -68,11 +69,29 @@
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function defaultEditionCapabilities() {
|
||||||
|
return {
|
||||||
|
edition: 'community',
|
||||||
|
supported_protocols: ['rest', 'graphql', 'grpc'],
|
||||||
|
supported_security_levels: ['standard'],
|
||||||
|
machine_access_modes: ['static_agent_key'],
|
||||||
|
limits: {
|
||||||
|
max_workspaces: 1,
|
||||||
|
max_users_per_workspace: 1,
|
||||||
|
max_agents_per_workspace: 1,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function currentProtocolCapabilities() {
|
function currentProtocolCapabilities() {
|
||||||
var capabilities = window.wizardProtocolCapabilities || defaultProtocolCapabilities();
|
var capabilities = window.wizardProtocolCapabilities || defaultProtocolCapabilities();
|
||||||
return capabilities[window.wizardProtocol] || capabilities.rest;
|
return capabilities[window.wizardProtocol] || capabilities.rest;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function currentEditionCapabilities() {
|
||||||
|
return window.wizardEditionCapabilities || defaultEditionCapabilities();
|
||||||
|
}
|
||||||
|
|
||||||
async function loadProtocolCapabilities() {
|
async function loadProtocolCapabilities() {
|
||||||
if (!window.wizardWorkspaceId || !window.CrankApi || typeof window.CrankApi.getProtocolCapabilities !== 'function') {
|
if (!window.wizardWorkspaceId || !window.CrankApi || typeof window.CrankApi.getProtocolCapabilities !== 'function') {
|
||||||
window.wizardProtocolCapabilities = defaultProtocolCapabilities();
|
window.wizardProtocolCapabilities = defaultProtocolCapabilities();
|
||||||
@@ -93,6 +112,21 @@
|
|||||||
return window.wizardProtocolCapabilities;
|
return window.wizardProtocolCapabilities;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function loadEditionCapabilities() {
|
||||||
|
if (!window.CrankApi || typeof window.CrankApi.getCapabilities !== 'function') {
|
||||||
|
window.wizardEditionCapabilities = defaultEditionCapabilities();
|
||||||
|
return window.wizardEditionCapabilities;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
window.wizardEditionCapabilities = await window.CrankApi.getCapabilities();
|
||||||
|
} catch (_error) {
|
||||||
|
window.wizardEditionCapabilities = defaultEditionCapabilities();
|
||||||
|
}
|
||||||
|
|
||||||
|
return window.wizardEditionCapabilities;
|
||||||
|
}
|
||||||
|
|
||||||
function step3Labels() {
|
function step3Labels() {
|
||||||
return {
|
return {
|
||||||
rest: tKey('wizard.step3.rest.label'),
|
rest: tKey('wizard.step3.rest.label'),
|
||||||
@@ -106,8 +140,11 @@
|
|||||||
window.CrankWizardState = {
|
window.CrankWizardState = {
|
||||||
state: window,
|
state: window,
|
||||||
defaultProtocolCapabilities: defaultProtocolCapabilities,
|
defaultProtocolCapabilities: defaultProtocolCapabilities,
|
||||||
|
defaultEditionCapabilities: defaultEditionCapabilities,
|
||||||
currentProtocolCapabilities: currentProtocolCapabilities,
|
currentProtocolCapabilities: currentProtocolCapabilities,
|
||||||
|
currentEditionCapabilities: currentEditionCapabilities,
|
||||||
loadProtocolCapabilities: loadProtocolCapabilities,
|
loadProtocolCapabilities: loadProtocolCapabilities,
|
||||||
|
loadEditionCapabilities: loadEditionCapabilities,
|
||||||
step3Labels: step3Labels,
|
step3Labels: step3Labels,
|
||||||
};
|
};
|
||||||
}());
|
}());
|
||||||
|
|||||||
@@ -6,12 +6,14 @@ function tfKey(key, vars) {
|
|||||||
}
|
}
|
||||||
var TOTAL_STEPS = window.CrankWizardShell.TOTAL_STEPS;
|
var TOTAL_STEPS = window.CrankWizardShell.TOTAL_STEPS;
|
||||||
var loadProtocolCapabilities = window.CrankWizardState.loadProtocolCapabilities;
|
var loadProtocolCapabilities = window.CrankWizardState.loadProtocolCapabilities;
|
||||||
|
var loadEditionCapabilities = window.CrankWizardState.loadEditionCapabilities;
|
||||||
var currentProtocolCapabilities = window.CrankWizardState.currentProtocolCapabilities;
|
var currentProtocolCapabilities = window.CrankWizardState.currentProtocolCapabilities;
|
||||||
var loadStep = window.CrankWizardShell.loadStep;
|
var loadStep = window.CrankWizardShell.loadStep;
|
||||||
var goToStep = window.CrankWizardShell.goToStep;
|
var goToStep = window.CrankWizardShell.goToStep;
|
||||||
var _doGoToStep = window.CrankWizardShell.doGoToStep;
|
var _doGoToStep = window.CrankWizardShell.doGoToStep;
|
||||||
var loadWizardPanels = window.CrankWizardShell.loadWizardPanels;
|
var loadWizardPanels = window.CrankWizardShell.loadWizardPanels;
|
||||||
var bindProtocolCards = window.CrankWizardShell.bindProtocolCards;
|
var bindProtocolCards = window.CrankWizardShell.bindProtocolCards;
|
||||||
|
var applyEditionProtocolVisibility = window.CrankWizardShell.applyEditionProtocolVisibility;
|
||||||
var step3PanelId = window.CrankWizardShell.step3PanelId;
|
var step3PanelId = window.CrankWizardShell.step3PanelId;
|
||||||
var bindStreamingConfigControls = window.CrankStreamingForm.bindStreamingConfigControls;
|
var bindStreamingConfigControls = window.CrankStreamingForm.bindStreamingConfigControls;
|
||||||
var collectStreamingConfig = window.CrankStreamingForm.collectStreamingConfig;
|
var collectStreamingConfig = window.CrankStreamingForm.collectStreamingConfig;
|
||||||
@@ -85,14 +87,17 @@ async function initWizardPage() {
|
|||||||
var workspace = window.getCurrentWorkspace ? window.getCurrentWorkspace() : null;
|
var workspace = window.getCurrentWorkspace ? window.getCurrentWorkspace() : null;
|
||||||
wizardWorkspaceId = workspace ? workspace.id : null;
|
wizardWorkspaceId = workspace ? workspace.id : null;
|
||||||
window.wizardWorkspaceId = wizardWorkspaceId;
|
window.wizardWorkspaceId = wizardWorkspaceId;
|
||||||
|
var editionCapabilities = await loadEditionCapabilities();
|
||||||
await loadProtocolCapabilities();
|
await loadProtocolCapabilities();
|
||||||
|
|
||||||
await loadWizardPanels([1, 2, 3, 4, 5]);
|
await loadWizardPanels([1, 2, 3, 4, 5]);
|
||||||
|
applyEditionProtocolVisibility(editionCapabilities);
|
||||||
await loadWizardAuthResources();
|
await loadWizardAuthResources();
|
||||||
bindProtocolCards();
|
bindProtocolCards();
|
||||||
bindWizardLiveActions();
|
bindWizardLiveActions();
|
||||||
bindStreamingConfigControls();
|
bindStreamingConfigControls();
|
||||||
updateUpstreamAuthUi();
|
updateUpstreamAuthUi();
|
||||||
|
renderEditionCapabilityHints(editionCapabilities);
|
||||||
|
|
||||||
var quickSecretModal = document.getElementById('quick-secret-modal');
|
var quickSecretModal = document.getElementById('quick-secret-modal');
|
||||||
var quickSecretClose = document.getElementById('quick-secret-close-btn');
|
var quickSecretClose = document.getElementById('quick-secret-close-btn');
|
||||||
@@ -141,6 +146,19 @@ async function initWizardPage() {
|
|||||||
_doGoToStep(1);
|
_doGoToStep(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function renderEditionCapabilityHints(capabilities) {
|
||||||
|
var securityNote = document.getElementById('wizard-security-level-note');
|
||||||
|
var securityText = securityNote ? securityNote.querySelector('.info-callout-text') : null;
|
||||||
|
if (!securityText) return;
|
||||||
|
var levels = capabilities && Array.isArray(capabilities.supported_security_levels)
|
||||||
|
? capabilities.supported_security_levels
|
||||||
|
: ['standard'];
|
||||||
|
securityNote.hidden = !(levels.length === 1 && levels[0] === 'standard');
|
||||||
|
if (!securityNote.hidden) {
|
||||||
|
securityText.textContent = tKey('wizard.step5.community_security_note');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (window.CrankDiagnostics && typeof window.CrankDiagnostics.bootstrap === 'function') {
|
if (window.CrankDiagnostics && typeof window.CrankDiagnostics.bootstrap === 'function') {
|
||||||
window.CrankDiagnostics.bootstrap('wizard', initWizardPage);
|
window.CrankDiagnostics.bootstrap('wizard', initWizardPage);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -13,8 +13,8 @@ test('shell and wizard expose stable diagnostics hooks', async ({ page }) => {
|
|||||||
await page.goto('/wizard/');
|
await page.goto('/wizard/');
|
||||||
await expect(page.locator('[data-testid="wizard-protocol-grid"]')).toBeVisible();
|
await expect(page.locator('[data-testid="wizard-protocol-grid"]')).toBeVisible();
|
||||||
await expect(page.locator('[data-testid="wizard-protocol-rest"]')).toBeVisible();
|
await expect(page.locator('[data-testid="wizard-protocol-rest"]')).toBeVisible();
|
||||||
await expect(page.locator('[data-testid="wizard-protocol-websocket"]')).toBeVisible();
|
await expect(page.locator('[data-testid="wizard-protocol-websocket"]')).toBeHidden();
|
||||||
await expect(page.locator('[data-testid="wizard-protocol-soap"]')).toBeVisible();
|
await expect(page.locator('[data-testid="wizard-protocol-soap"]')).toBeHidden();
|
||||||
await expect(page.locator('html')).toHaveAttribute('data-crank-bootstrap-state', 'ready');
|
await expect(page.locator('html')).toHaveAttribute('data-crank-bootstrap-state', 'ready');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,14 +1,5 @@
|
|||||||
const { test, expect } = require('@playwright/test');
|
const { test, expect } = require('@playwright/test');
|
||||||
const {
|
const { login, localized } = require('./helpers');
|
||||||
SOAP_TEST_WSDL,
|
|
||||||
buildSoapOperationPayload,
|
|
||||||
buildWebsocketWindowOperationPayload,
|
|
||||||
createOperation,
|
|
||||||
getCurrentWorkspace,
|
|
||||||
login,
|
|
||||||
localized,
|
|
||||||
uniqueName,
|
|
||||||
} = require('./helpers');
|
|
||||||
|
|
||||||
test('wizard loads and protocol selection updates flow', async ({ page }) => {
|
test('wizard loads and protocol selection updates flow', async ({ page }) => {
|
||||||
await login(page);
|
await login(page);
|
||||||
@@ -20,201 +11,13 @@ test('wizard loads and protocol selection updates flow', async ({ page }) => {
|
|||||||
await expect(page.locator('#step-panel-2 .step-panel-title')).toContainText(localized('Select upstream', 'Выберите upstream'));
|
await expect(page.locator('#step-panel-2 .step-panel-title')).toContainText(localized('Select upstream', 'Выберите upstream'));
|
||||||
});
|
});
|
||||||
|
|
||||||
test('soap wizard uploads wsdl, applies discovered binding and runs test', async ({ page }) => {
|
test('community wizard hides premium protocols and explains edition scope', async ({ page }) => {
|
||||||
await login(page);
|
await login(page);
|
||||||
const workspace = await getCurrentWorkspace(page);
|
await page.goto('/wizard/');
|
||||||
const operation = await createOperation(
|
await expect(page.locator('[data-testid="wizard-protocol-rest"]')).toBeVisible();
|
||||||
page,
|
await expect(page.locator('[data-testid="wizard-protocol-graphql"]')).toBeVisible();
|
||||||
workspace.id,
|
await expect(page.locator('[data-testid="wizard-protocol-grpc"]')).toBeVisible();
|
||||||
buildSoapOperationPayload(uniqueName('playwright_soap_wizard')),
|
await expect(page.locator('[data-testid="wizard-protocol-websocket"]')).toBeHidden();
|
||||||
);
|
await expect(page.locator('[data-testid="wizard-protocol-soap"]')).toBeHidden();
|
||||||
|
await expect(page.locator('#wizard-edition-protocol-note')).toContainText(localized('Commercial editions unlock', 'коммерческих редакциях'));
|
||||||
await page.goto(`/wizard/?mode=edit&operationId=${encodeURIComponent(operation.operation_id)}`);
|
|
||||||
await page.locator('#btn-continue').click();
|
|
||||||
await page.locator('#btn-continue').click();
|
|
||||||
|
|
||||||
await expect(page.locator('#step-panel-3-soap .step-panel-title')).toContainText(/WSDL/i);
|
|
||||||
await page.evaluate((wsdl) => {
|
|
||||||
const fileName = document.getElementById('wizard-soap-wsdl-name');
|
|
||||||
if (fileName) fileName.textContent = 'lead.wsdl';
|
|
||||||
}, SOAP_TEST_WSDL);
|
|
||||||
await page.evaluate(async ({ workspaceId, operationId, wsdl }) => {
|
|
||||||
const uploaded = await window.CrankApi.uploadWsdlFile(
|
|
||||||
workspaceId,
|
|
||||||
operationId,
|
|
||||||
new TextEncoder().encode(wsdl),
|
|
||||||
'lead.wsdl',
|
|
||||||
);
|
|
||||||
const services = await window.CrankApi.listSoapServices(
|
|
||||||
workspaceId,
|
|
||||||
operationId,
|
|
||||||
null,
|
|
||||||
);
|
|
||||||
window.renderSoapServiceCatalog(services.services || []);
|
|
||||||
return uploaded;
|
|
||||||
}, {
|
|
||||||
workspaceId: workspace.id,
|
|
||||||
operationId: operation.operation_id,
|
|
||||||
wsdl: SOAP_TEST_WSDL,
|
|
||||||
});
|
|
||||||
|
|
||||||
await expect(page.locator('#wizard-soap-services-list button')).toContainText('CreateLead');
|
|
||||||
await page.locator('#wizard-soap-services-list button').getByText('CreateLead').click();
|
|
||||||
await expect(page.locator('#soap-service-name')).toHaveValue('LeadService');
|
|
||||||
await expect(page.locator('#soap-port-name')).toHaveValue('LeadPort');
|
|
||||||
await expect(page.locator('#soap-operation-name')).toHaveValue('CreateLead');
|
|
||||||
|
|
||||||
await page.locator('#btn-continue').click();
|
|
||||||
await expect(page.locator('#step-panel-4 .step-panel-title')).toBeVisible();
|
|
||||||
await page.locator('#tool-input-schema').fill(JSON.stringify({
|
|
||||||
type: 'object',
|
|
||||||
required: ['email'],
|
|
||||||
properties: {
|
|
||||||
email: { type: 'string' },
|
|
||||||
},
|
|
||||||
}, null, 2));
|
|
||||||
await page.locator('#tool-output-schema').fill(JSON.stringify({
|
|
||||||
type: 'object',
|
|
||||||
required: ['id'],
|
|
||||||
properties: {
|
|
||||||
id: { type: 'string' },
|
|
||||||
},
|
|
||||||
}, null, 2));
|
|
||||||
await page.locator('#btn-continue').click();
|
|
||||||
await expect(page.locator('[data-i18n="wizard.step5.test_title"]')).toContainText(localized('Test run', 'Тестовый запуск'));
|
|
||||||
await page.locator('#tool-input-mapping').fill(JSON.stringify({
|
|
||||||
email: '$.input.email',
|
|
||||||
}, null, 2));
|
|
||||||
await page.locator('#tool-output-mapping').fill(JSON.stringify({
|
|
||||||
id: '$.response.body.id',
|
|
||||||
}, null, 2));
|
|
||||||
await page.locator('#tool-exec-config').fill('timeout_ms: 1000');
|
|
||||||
|
|
||||||
await page.locator('#wizard-test-input').fill(JSON.stringify({ email: 'user@example.com' }, null, 2));
|
|
||||||
await page.locator('#wizard-run-test').click();
|
|
||||||
|
|
||||||
await expect(page.locator('#wizard-test-request-preview')).toHaveValue(/user@example.com/);
|
|
||||||
await expect(page.locator('#wizard-test-response-preview')).toHaveValue(/lead_soap_123/);
|
|
||||||
await expect(page.locator('#wizard-test-errors')).toHaveValue('[]');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('websocket wizard reports bounded window success and reconnect exhaustion clearly', async ({ page }) => {
|
|
||||||
await login(page);
|
|
||||||
const workspace = await getCurrentWorkspace(page);
|
|
||||||
const successOperation = await createOperation(
|
|
||||||
page,
|
|
||||||
workspace.id,
|
|
||||||
buildWebsocketWindowOperationPayload(uniqueName('playwright_websocket_success')),
|
|
||||||
);
|
|
||||||
const failureOperation = await createOperation(
|
|
||||||
page,
|
|
||||||
workspace.id,
|
|
||||||
buildWebsocketWindowOperationPayload(uniqueName('playwright_websocket_failure'), {
|
|
||||||
streaming: {
|
|
||||||
max_items: 5,
|
|
||||||
},
|
|
||||||
websocketOptions: {
|
|
||||||
reconnect_max_attempts: 0,
|
|
||||||
reconnect_backoff_ms: 0,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
await page.goto(`/wizard/?mode=edit&operationId=${encodeURIComponent(successOperation.operation_id)}`);
|
|
||||||
await page.locator('#btn-continue').click();
|
|
||||||
await page.locator('#upstream-new-trigger').click();
|
|
||||||
await page.locator('#new-upstream-name').fill('fixture-websocket');
|
|
||||||
await page.locator('#new-upstream-url').fill('ws://127.0.0.1:3310');
|
|
||||||
await page.locator('#btn-continue').click();
|
|
||||||
await expect(page.locator('#step-panel-3-websocket .step-panel-title')).toContainText(/WebSocket/i);
|
|
||||||
await page.locator('#websocket-path').fill('/events');
|
|
||||||
await page.locator('#websocket-subprotocols').fill('');
|
|
||||||
await page.locator('#websocket-subscribe-template').fill('');
|
|
||||||
await page.locator('#websocket-unsubscribe-template').fill('');
|
|
||||||
await page.locator('#btn-continue').click();
|
|
||||||
await page.locator('#btn-continue').click();
|
|
||||||
await page.locator('#wizard-test-input').fill('{}');
|
|
||||||
await page.evaluate(() => {
|
|
||||||
const original = window.CrankApi.runOperationTest;
|
|
||||||
window.__wizardRunTestOriginal = original;
|
|
||||||
window.CrankApi.runOperationTest = async () => ({
|
|
||||||
ok: true,
|
|
||||||
mode: 'window',
|
|
||||||
request_preview: {
|
|
||||||
body: { topic: 'telemetry' },
|
|
||||||
},
|
|
||||||
response_preview: {
|
|
||||||
items: [
|
|
||||||
{ seq: 1, value: 101 },
|
|
||||||
{ seq: 2, value: 102 },
|
|
||||||
{ seq: 3, value: 103 },
|
|
||||||
],
|
|
||||||
window_complete: true,
|
|
||||||
truncated: false,
|
|
||||||
has_more: false,
|
|
||||||
},
|
|
||||||
errors: [],
|
|
||||||
window: {
|
|
||||||
window_complete: true,
|
|
||||||
truncated: false,
|
|
||||||
has_more: false,
|
|
||||||
cursor: null,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
await page.locator('#wizard-run-test').click();
|
|
||||||
await expect(page.locator('#wizard-live-status-title')).toContainText(localized('WebSocket window test completed', 'WebSocket оконный тест завершен'));
|
|
||||||
await expect(page.locator('#wizard-live-status-text')).toContainText(localized('bounded test window', 'тестового окна'));
|
|
||||||
await expect(page.locator('#wizard-test-response-preview')).toHaveValue(/"seq": 1/);
|
|
||||||
await expect(page.locator('#wizard-test-request-preview')).toHaveValue(/"topic": "telemetry"/);
|
|
||||||
await expect(page.locator('#wizard-test-errors')).toHaveValue('[]');
|
|
||||||
await page.evaluate(() => {
|
|
||||||
if (window.__wizardRunTestOriginal) {
|
|
||||||
window.CrankApi.runOperationTest = window.__wizardRunTestOriginal;
|
|
||||||
delete window.__wizardRunTestOriginal;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
await page.goto(`/wizard/?mode=edit&operationId=${encodeURIComponent(failureOperation.operation_id)}`);
|
|
||||||
await page.locator('#btn-continue').click();
|
|
||||||
await page.locator('#upstream-new-trigger').click();
|
|
||||||
await page.locator('#new-upstream-name').fill('fixture-websocket');
|
|
||||||
await page.locator('#new-upstream-url').fill('ws://127.0.0.1:3310');
|
|
||||||
await page.locator('#btn-continue').click();
|
|
||||||
await page.locator('#websocket-path').fill('/events');
|
|
||||||
await page.locator('#websocket-subprotocols').fill('');
|
|
||||||
await page.locator('#websocket-subscribe-template').fill('');
|
|
||||||
await page.locator('#websocket-unsubscribe-template').fill('');
|
|
||||||
await page.locator('#btn-continue').click();
|
|
||||||
await page.locator('#btn-continue').click();
|
|
||||||
await page.locator('#wizard-test-input').fill('{}');
|
|
||||||
await page.evaluate(() => {
|
|
||||||
const original = window.CrankApi.runOperationTest;
|
|
||||||
window.__wizardRunTestOriginal = original;
|
|
||||||
window.CrankApi.runOperationTest = async () => ({
|
|
||||||
ok: false,
|
|
||||||
mode: 'window',
|
|
||||||
request_preview: {
|
|
||||||
body: { topic: 'telemetry' },
|
|
||||||
},
|
|
||||||
response_preview: null,
|
|
||||||
errors: [
|
|
||||||
{
|
|
||||||
code: 'runtime_websocket_error',
|
|
||||||
message: 'websocket reconnect policy exhausted',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
window: null,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
await page.locator('#wizard-run-test').click();
|
|
||||||
await expect(page.locator('#wizard-live-status-title')).toContainText(localized('WebSocket test returned errors', 'WebSocket тест завершился с ошибками'));
|
|
||||||
await expect(page.locator('#wizard-live-status-text')).toContainText(localized('reconnect policy was exhausted', 'reconnect policy была исчерпана'));
|
|
||||||
await expect(page.locator('#wizard-test-errors')).toHaveValue(/runtime_websocket_error/);
|
|
||||||
await page.evaluate(() => {
|
|
||||||
if (window.__wizardRunTestOriginal) {
|
|
||||||
window.CrankApi.runOperationTest = window.__wizardRunTestOriginal;
|
|
||||||
delete window.__wizardRunTestOriginal;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -53,6 +53,7 @@
|
|||||||
### 5.0. Build capabilities
|
### 5.0. Build capabilities
|
||||||
|
|
||||||
- `GET /api/admin/capabilities`
|
- `GET /api/admin/capabilities`
|
||||||
|
- `GET /api/admin/workspaces/{workspace_id}/protocol-capabilities`
|
||||||
|
|
||||||
Контракт:
|
Контракт:
|
||||||
|
|
||||||
@@ -64,6 +65,11 @@
|
|||||||
- `machine_access_modes`
|
- `machine_access_modes`
|
||||||
- `limits`
|
- `limits`
|
||||||
- UI использует этот ответ для edition-aware gating и честного product copy.
|
- UI использует этот ответ для edition-aware gating и честного product copy.
|
||||||
|
- `GET /protocol-capabilities` возвращает только те протоколы и execution capabilities, которые реально доступны в текущей редакции;
|
||||||
|
- для `Community` это означает только:
|
||||||
|
- `REST`
|
||||||
|
- `GraphQL`
|
||||||
|
- `gRPC`
|
||||||
|
|
||||||
### 5.1. Workspaces and members
|
### 5.1. Workspaces and members
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user