From 20598ff5dc05c35f255eae74669ef629eaaf8a8e Mon Sep 17 00:00:00 2001 From: "a.tolmachev" Date: Sun, 3 May 2026 18:03:23 +0000 Subject: [PATCH] ui: trim community streaming surface --- TASKS.md | 2 +- apps/admin-api/src/app.rs | 7 +++++ apps/admin-api/src/service.rs | 53 +++++++++++++++++++------------- apps/ui/html/wizard/step1.html | 5 +-- apps/ui/html/wizard/step5.html | 11 +++++++ apps/ui/js/i18n.js | 8 +++-- apps/ui/js/wizard-live.js | 5 +++ apps/ui/js/wizard-state.js | 16 +++++----- apps/ui/js/wizard.js | 24 +++++++++++++++ apps/ui/tests/e2e/wizard.spec.js | 9 ++++++ docs/admin-api.md | 4 +-- 11 files changed, 104 insertions(+), 40 deletions(-) diff --git a/TASKS.md b/TASKS.md index cb75b49..d565ee3 100644 --- a/TASKS.md +++ b/TASKS.md @@ -34,7 +34,7 @@ Progress: - done: Community wizard protocol list hides premium protocol cards; - done: Community security note is shown inside wizard flow; - done: server-side Community enforcement rejects unsupported protocols and premium security levels; -- pending: убрать или честно заблокировать оставшиеся Community-incompatible streaming affordances; +- done: Community protocol capabilities and wizard surface no longer present streaming execution as available; - pending: привести Agents/Settings copy к окончательной open-core границе. DoD: diff --git a/apps/admin-api/src/app.rs b/apps/admin-api/src/app.rs index 7958f08..0c29700 100644 --- a/apps/admin-api/src/app.rs +++ b/apps/admin-api/src/app.rs @@ -1065,6 +1065,13 @@ mod tests { .collect::>(), vec!["rest", "graphql", "grpc"] ); + for item in response["items"].as_array().unwrap() { + assert_eq!(item["supports_execution_modes"], json!(["unary"])); + assert_eq!( + item["supports_transport_behaviors"], + json!(["request_response"]) + ); + } } #[tokio::test(flavor = "multi_thread")] diff --git a/apps/admin-api/src/service.rs b/apps/admin-api/src/service.rs index a2cd375..c618b4f 100644 --- a/apps/admin-api/src/service.rs +++ b/apps/admin-api/src/service.rs @@ -1562,11 +1562,11 @@ impl AdminService { } pub async fn list_protocol_capabilities(&self) -> Vec { - self.get_capabilities() - .await + let capabilities = self.get_capabilities().await; + capabilities .supported_protocols .into_iter() - .map(protocol_capability_view) + .map(|protocol| protocol_capability_view(protocol, capabilities.edition)) .collect() } @@ -5124,25 +5124,34 @@ fn runtime_error_code(error: &RuntimeError) -> &'static str { } } -fn protocol_capability_view(protocol: Protocol) -> ProtocolCapabilityView { - let supports_execution_modes = [ - ExecutionMode::Unary, - ExecutionMode::Window, - ExecutionMode::Session, - ExecutionMode::AsyncJob, - ] - .into_iter() - .filter(|mode| protocol.supports_execution_mode(*mode)) - .collect(); - let supports_transport_behaviors = [ - TransportBehavior::RequestResponse, - TransportBehavior::ServerStream, - TransportBehavior::StatefulSession, - TransportBehavior::DeferredResult, - ] - .into_iter() - .filter(|behavior| protocol.supports_transport_behavior(*behavior)) - .collect(); +fn protocol_capability_view(protocol: Protocol, edition: ProductEdition) -> ProtocolCapabilityView { + let community_build = matches!(edition, ProductEdition::Community); + let supports_execution_modes = if community_build { + vec![ExecutionMode::Unary] + } else { + [ + ExecutionMode::Unary, + ExecutionMode::Window, + ExecutionMode::Session, + ExecutionMode::AsyncJob, + ] + .into_iter() + .filter(|mode| protocol.supports_execution_mode(*mode)) + .collect() + }; + let supports_transport_behaviors = if community_build { + vec![TransportBehavior::RequestResponse] + } else { + [ + TransportBehavior::RequestResponse, + TransportBehavior::ServerStream, + TransportBehavior::StatefulSession, + TransportBehavior::DeferredResult, + ] + .into_iter() + .filter(|behavior| protocol.supports_transport_behavior(*behavior)) + .collect() + }; let supports_upload_artifacts = match protocol { Protocol::Rest | Protocol::Graphql | Protocol::Websocket => Vec::new(), Protocol::Grpc => vec!["proto".to_owned(), "descriptor_set".to_owned()], diff --git a/apps/ui/html/wizard/step1.html b/apps/ui/html/wizard/step1.html index e081405..d59984a 100644 --- a/apps/ui/html/wizard/step1.html +++ b/apps/ui/html/wizard/step1.html @@ -69,12 +69,9 @@
gRPC
-
High-performance binary RPC over HTTP/2 with unary and bounded streaming flows for low-latency internal services.
+
High-performance binary RPC over HTTP/2 for unary calls to low-latency internal services.
Unary - Server stream - Window - Session Protobuf
diff --git a/apps/ui/html/wizard/step5.html b/apps/ui/html/wizard/step5.html index 2f11dbe..dcc27d0 100644 --- a/apps/ui/html/wizard/step5.html +++ b/apps/ui/html/wizard/step5.html @@ -98,6 +98,17 @@ tls: + +
diff --git a/apps/ui/js/i18n.js b/apps/ui/js/i18n.js index 5b47291..2b1f738 100644 --- a/apps/ui/js/i18n.js +++ b/apps/ui/js/i18n.js @@ -565,7 +565,7 @@ var TRANSLATIONS = { 'wizard.step1.graphql_name': 'GraphQL', 'wizard.step1.graphql_tagline': 'Query and mutate a GraphQL schema. Supports queries and mutations over one fixed operation.', 'wizard.step1.grpc_name': 'gRPC', - 'wizard.step1.grpc_tagline': 'High-performance binary RPC over HTTP/2 with unary and bounded streaming flows for low-latency internal services.', + 'wizard.step1.grpc_tagline': 'High-performance binary RPC over HTTP/2 for unary calls to low-latency internal services.', 'wizard.step1.websocket_name': 'WebSocket', 'wizard.step1.websocket_tagline': 'Stateful event streams and push-oriented integrations normalized into bounded MCP streaming tools.', 'wizard.step1.soap_name': 'SOAP', @@ -577,6 +577,8 @@ var TRANSLATIONS = { '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.step5.streaming_title': 'Streaming execution in Community', + 'wizard.step5.community_streaming_note': 'This Community build keeps operations in unary mode. Window, session and async job tool families require a commercial edition.', '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.required': 'Required', @@ -1619,7 +1621,7 @@ var TRANSLATIONS = { 'wizard.step1.graphql_name': 'GraphQL', 'wizard.step1.graphql_tagline': 'Операции по GraphQL-схеме. Поддерживаются Query и Mutation для одной фиксированной операции.', 'wizard.step1.grpc_name': 'gRPC', - 'wizard.step1.grpc_tagline': 'Высокопроизводительный бинарный RPC поверх HTTP/2 с unary и bounded streaming flow для низколатентных внутренних сервисов.', + 'wizard.step1.grpc_tagline': 'Высокопроизводительный бинарный RPC поверх HTTP/2 для unary-вызовов к низколатентным внутренним сервисам.', 'wizard.step1.websocket_name': 'WebSocket', 'wizard.step1.websocket_tagline': 'Состояние соединения, event streams и push-oriented интеграции, нормализованные в bounded MCP streaming tools.', 'wizard.step1.soap_name': 'SOAP', @@ -1631,6 +1633,8 @@ var TRANSLATIONS = { 'wizard.step1.tip_body': 'Смена протокола после настройки следующих шагов очистит схемы и mapping. Сохраните операцию как черновик перед экспериментами. См. гайд по миграции протоколов.', 'wizard.step1.community_protocol_title': 'Граница протоколов Community', 'wizard.step1.community_protocol_note': 'В этой Community-сборке скрыты {protocols}. Эти протокольные поверхности открываются в коммерческих редакциях.', + 'wizard.step5.streaming_title': 'Потоковое выполнение в Community', + 'wizard.step5.community_streaming_note': 'В этой Community-сборке операции остаются в режиме unary. Семейства инструментов window, session и async job требуют коммерческую редакцию.', 'wizard.step2.title': 'Выберите upstream и endpoint', 'wizard.step2.subtitle': 'Выберите существующий upstream или зарегистрируйте новый. Затем задайте конкретный путь endpoint-а для этой операции.', 'wizard.required': 'Обязательно', diff --git a/apps/ui/js/wizard-live.js b/apps/ui/js/wizard-live.js index 3159a3c..cc2e34f 100644 --- a/apps/ui/js/wizard-live.js +++ b/apps/ui/js/wizard-live.js @@ -187,6 +187,11 @@ function updateWizardProtocolVisibility() { if (grpcTools) grpcTools.hidden = wizardProtocol !== 'grpc'; updateStreamingModeOptions(); updateStreamingConfigVisibility(); + if (typeof window.renderEditionCapabilityHints === 'function' + && window.CrankWizardState + && typeof window.CrankWizardState.currentEditionCapabilities === 'function') { + window.renderEditionCapabilityHints(window.CrankWizardState.currentEditionCapabilities()); + } } function showWizardLiveStatus(title, text, isError) { diff --git a/apps/ui/js/wizard-state.js b/apps/ui/js/wizard-state.js index 852bd22..7c60a27 100644 --- a/apps/ui/js/wizard-state.js +++ b/apps/ui/js/wizard-state.js @@ -47,24 +47,24 @@ function defaultProtocolCapabilities() { return { rest: { - supports_execution_modes: ['unary', 'window', 'session', 'async_job'], - supports_transport_behaviors: ['request_response', 'server_stream', 'deferred_result'], + supports_execution_modes: ['unary'], + supports_transport_behaviors: ['request_response'], }, graphql: { supports_execution_modes: ['unary'], supports_transport_behaviors: ['request_response'], }, grpc: { - supports_execution_modes: ['unary', 'window', 'session', 'async_job'], - supports_transport_behaviors: ['request_response', 'server_stream'], + supports_execution_modes: ['unary'], + supports_transport_behaviors: ['request_response'], }, websocket: { - supports_execution_modes: ['window', 'session', 'async_job'], - supports_transport_behaviors: ['server_stream', 'stateful_session', 'deferred_result'], + supports_execution_modes: ['unary'], + supports_transport_behaviors: ['request_response'], }, soap: { - supports_execution_modes: ['unary', 'async_job'], - supports_transport_behaviors: ['request_response', 'server_stream', 'deferred_result'], + supports_execution_modes: ['unary'], + supports_transport_behaviors: ['request_response'], }, }; } diff --git a/apps/ui/js/wizard.js b/apps/ui/js/wizard.js index af599d4..3fc748c 100644 --- a/apps/ui/js/wizard.js +++ b/apps/ui/js/wizard.js @@ -149,6 +149,9 @@ async function initWizardPage() { function renderEditionCapabilityHints(capabilities) { var securityNote = document.getElementById('wizard-security-level-note'); var securityText = securityNote ? securityNote.querySelector('.info-callout-text') : null; + var streamingNote = document.getElementById('wizard-streaming-capability-note'); + var streamingText = streamingNote ? streamingNote.querySelector('.info-callout-text') : null; + var streamingCard = document.getElementById('wizard-streaming-config-card'); if (!securityText) return; var levels = capabilities && Array.isArray(capabilities.supported_security_levels) ? capabilities.supported_security_levels @@ -157,8 +160,29 @@ function renderEditionCapabilityHints(capabilities) { if (!securityNote.hidden) { securityText.textContent = tKey('wizard.step5.community_security_note'); } + + var protocolCapabilities = currentProtocolCapabilities(); + var modes = protocolCapabilities && Array.isArray(protocolCapabilities.supports_execution_modes) + ? protocolCapabilities.supports_execution_modes + : ['unary']; + var hasStreamingModes = modes.some(function(mode) { return mode !== 'unary'; }); + if (streamingCard) { + streamingCard.hidden = !hasStreamingModes; + } + if (streamingNote && streamingText) { + var edition = capabilities && capabilities.edition ? capabilities.edition : 'community'; + var shouldExplainCommunityLimit = edition === 'community' + && !hasStreamingModes + && ['rest', 'grpc'].indexOf(wizardProtocol) >= 0; + streamingNote.hidden = !shouldExplainCommunityLimit; + if (shouldExplainCommunityLimit) { + streamingText.textContent = tKey('wizard.step5.community_streaming_note'); + } + } } +window.renderEditionCapabilityHints = renderEditionCapabilityHints; + if (window.CrankDiagnostics && typeof window.CrankDiagnostics.bootstrap === 'function') { window.CrankDiagnostics.bootstrap('wizard', initWizardPage); } else { diff --git a/apps/ui/tests/e2e/wizard.spec.js b/apps/ui/tests/e2e/wizard.spec.js index 73d82fb..6a082aa 100644 --- a/apps/ui/tests/e2e/wizard.spec.js +++ b/apps/ui/tests/e2e/wizard.spec.js @@ -20,4 +20,13 @@ test('community wizard hides premium protocols and explains edition scope', asyn 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.locator('[data-testid="wizard-protocol-grpc"]').click(); + await page.locator('#btn-continue').click(); + await page.locator('#btn-continue').click(); + await page.locator('#btn-continue').click(); + await page.locator('#btn-continue').click(); + + await expect(page.locator('#wizard-streaming-config-card')).toBeHidden(); + await expect(page.locator('#wizard-streaming-capability-note')).toContainText(localized('commercial edition', 'коммерческую редакцию')); }); diff --git a/docs/admin-api.md b/docs/admin-api.md index 4cbe306..c68a30c 100644 --- a/docs/admin-api.md +++ b/docs/admin-api.md @@ -270,9 +270,7 @@ - upstream auth selector; - quick-create secret / auth profile modal. - execution mode selector; -- streaming config blocks; -- stream test-runs; -- tool family preview. +- streaming config blocks, stream test-runs и tool family preview только там, где это разрешает capability model редакции; Детальные DTO и response shapes для экранов `Operations` и `Wizard` зафиксированы отдельно в: