ui: trim community streaming surface

This commit is contained in:
a.tolmachev
2026-05-03 18:03:23 +00:00
parent 82b7b7278e
commit 20598ff5dc
11 changed files with 104 additions and 40 deletions
+1 -1
View File
@@ -34,7 +34,7 @@ Progress:
- done: Community wizard protocol list hides premium protocol cards; - done: Community wizard protocol list hides premium protocol cards;
- done: Community security note is shown inside wizard flow; - done: Community security note is shown inside wizard flow;
- done: server-side Community enforcement rejects unsupported protocols and premium security levels; - 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 границе. - pending: привести Agents/Settings copy к окончательной open-core границе.
DoD: DoD:
+7
View File
@@ -1065,6 +1065,13 @@ mod tests {
.collect::<Vec<_>>(), .collect::<Vec<_>>(),
vec!["rest", "graphql", "grpc"] 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")] #[tokio::test(flavor = "multi_thread")]
+17 -8
View File
@@ -1562,11 +1562,11 @@ impl AdminService {
} }
pub async fn list_protocol_capabilities(&self) -> Vec<ProtocolCapabilityView> { pub async fn list_protocol_capabilities(&self) -> Vec<ProtocolCapabilityView> {
self.get_capabilities() let capabilities = self.get_capabilities().await;
.await capabilities
.supported_protocols .supported_protocols
.into_iter() .into_iter()
.map(protocol_capability_view) .map(|protocol| protocol_capability_view(protocol, capabilities.edition))
.collect() .collect()
} }
@@ -5124,8 +5124,12 @@ fn runtime_error_code(error: &RuntimeError) -> &'static str {
} }
} }
fn protocol_capability_view(protocol: Protocol) -> ProtocolCapabilityView { fn protocol_capability_view(protocol: Protocol, edition: ProductEdition) -> ProtocolCapabilityView {
let supports_execution_modes = [ let community_build = matches!(edition, ProductEdition::Community);
let supports_execution_modes = if community_build {
vec![ExecutionMode::Unary]
} else {
[
ExecutionMode::Unary, ExecutionMode::Unary,
ExecutionMode::Window, ExecutionMode::Window,
ExecutionMode::Session, ExecutionMode::Session,
@@ -5133,8 +5137,12 @@ fn protocol_capability_view(protocol: Protocol) -> ProtocolCapabilityView {
] ]
.into_iter() .into_iter()
.filter(|mode| protocol.supports_execution_mode(*mode)) .filter(|mode| protocol.supports_execution_mode(*mode))
.collect(); .collect()
let supports_transport_behaviors = [ };
let supports_transport_behaviors = if community_build {
vec![TransportBehavior::RequestResponse]
} else {
[
TransportBehavior::RequestResponse, TransportBehavior::RequestResponse,
TransportBehavior::ServerStream, TransportBehavior::ServerStream,
TransportBehavior::StatefulSession, TransportBehavior::StatefulSession,
@@ -5142,7 +5150,8 @@ fn protocol_capability_view(protocol: Protocol) -> ProtocolCapabilityView {
] ]
.into_iter() .into_iter()
.filter(|behavior| protocol.supports_transport_behavior(*behavior)) .filter(|behavior| protocol.supports_transport_behavior(*behavior))
.collect(); .collect()
};
let supports_upload_artifacts = match protocol { let supports_upload_artifacts = match protocol {
Protocol::Rest | Protocol::Graphql | Protocol::Websocket => Vec::new(), Protocol::Rest | Protocol::Graphql | Protocol::Websocket => Vec::new(),
Protocol::Grpc => vec!["proto".to_owned(), "descriptor_set".to_owned()], Protocol::Grpc => vec!["proto".to_owned(), "descriptor_set".to_owned()],
+1 -4
View File
@@ -69,12 +69,9 @@
</svg> </svg>
</div> </div>
<div class="protocol-card-name" data-i18n="wizard.step1.grpc_name">gRPC</div> <div class="protocol-card-name" data-i18n="wizard.step1.grpc_name">gRPC</div>
<div class="protocol-card-tagline" data-i18n="wizard.step1.grpc_tagline">High-performance binary RPC over HTTP/2 with unary and bounded streaming flows for low-latency internal services.</div> <div class="protocol-card-tagline" data-i18n="wizard.step1.grpc_tagline">High-performance binary RPC over HTTP/2 for unary calls to low-latency internal services.</div>
<div class="protocol-card-tags"> <div class="protocol-card-tags">
<span class="protocol-tag" data-i18n="wizard.step1.unary">Unary</span> <span class="protocol-tag" data-i18n="wizard.step1.unary">Unary</span>
<span class="protocol-tag" data-i18n="wizard.streaming.transport.server_stream">Server stream</span>
<span class="protocol-tag" data-i18n="wizard.streaming.mode.window">Window</span>
<span class="protocol-tag" data-i18n="wizard.streaming.mode.session">Session</span>
<span class="protocol-tag">Protobuf</span> <span class="protocol-tag">Protobuf</span>
</div> </div>
</div> </div>
+11
View File
@@ -98,6 +98,17 @@ tls:
</div> </div>
</div> </div>
<div class="info-callout" id="wizard-streaming-capability-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.streaming_title">Streaming execution 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">
+6 -2
View File
@@ -565,7 +565,7 @@ var TRANSLATIONS = {
'wizard.step1.graphql_name': 'GraphQL', '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.graphql_tagline': 'Query and mutate a GraphQL schema. Supports queries and mutations over one fixed operation.',
'wizard.step1.grpc_name': 'gRPC', '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_name': 'WebSocket',
'wizard.step1.websocket_tagline': 'Stateful event streams and push-oriented integrations normalized into bounded MCP streaming tools.', 'wizard.step1.websocket_tagline': 'Stateful event streams and push-oriented integrations normalized into bounded MCP streaming tools.',
'wizard.step1.soap_name': 'SOAP', '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.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_title': 'Community protocol scope',
'wizard.step1.community_protocol_note': 'This Community build hides {protocols}. Commercial editions unlock those protocol surfaces.', '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.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',
@@ -1619,7 +1621,7 @@ var TRANSLATIONS = {
'wizard.step1.graphql_name': 'GraphQL', 'wizard.step1.graphql_name': 'GraphQL',
'wizard.step1.graphql_tagline': 'Операции по GraphQL-схеме. Поддерживаются Query и Mutation для одной фиксированной операции.', 'wizard.step1.graphql_tagline': 'Операции по GraphQL-схеме. Поддерживаются Query и Mutation для одной фиксированной операции.',
'wizard.step1.grpc_name': 'gRPC', '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_name': 'WebSocket',
'wizard.step1.websocket_tagline': 'Состояние соединения, event streams и push-oriented интеграции, нормализованные в bounded MCP streaming tools.', 'wizard.step1.websocket_tagline': 'Состояние соединения, event streams и push-oriented интеграции, нормализованные в bounded MCP streaming tools.',
'wizard.step1.soap_name': 'SOAP', 'wizard.step1.soap_name': 'SOAP',
@@ -1631,6 +1633,8 @@ var TRANSLATIONS = {
'wizard.step1.tip_body': 'Смена протокола после настройки следующих шагов очистит схемы и mapping. Сохраните операцию как черновик перед экспериментами. См. гайд по миграции протоколов.', 'wizard.step1.tip_body': 'Смена протокола после настройки следующих шагов очистит схемы и mapping. Сохраните операцию как черновик перед экспериментами. См. гайд по миграции протоколов.',
'wizard.step1.community_protocol_title': 'Граница протоколов Community', 'wizard.step1.community_protocol_title': 'Граница протоколов Community',
'wizard.step1.community_protocol_note': 'В этой Community-сборке скрыты {protocols}. Эти протокольные поверхности открываются в коммерческих редакциях.', '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.title': 'Выберите upstream и endpoint',
'wizard.step2.subtitle': 'Выберите существующий upstream или зарегистрируйте новый. Затем задайте конкретный путь endpoint-а для этой операции.', 'wizard.step2.subtitle': 'Выберите существующий upstream или зарегистрируйте новый. Затем задайте конкретный путь endpoint-а для этой операции.',
'wizard.required': 'Обязательно', 'wizard.required': 'Обязательно',
+5
View File
@@ -187,6 +187,11 @@ function updateWizardProtocolVisibility() {
if (grpcTools) grpcTools.hidden = wizardProtocol !== 'grpc'; if (grpcTools) grpcTools.hidden = wizardProtocol !== 'grpc';
updateStreamingModeOptions(); updateStreamingModeOptions();
updateStreamingConfigVisibility(); updateStreamingConfigVisibility();
if (typeof window.renderEditionCapabilityHints === 'function'
&& window.CrankWizardState
&& typeof window.CrankWizardState.currentEditionCapabilities === 'function') {
window.renderEditionCapabilityHints(window.CrankWizardState.currentEditionCapabilities());
}
} }
function showWizardLiveStatus(title, text, isError) { function showWizardLiveStatus(title, text, isError) {
+8 -8
View File
@@ -47,24 +47,24 @@
function defaultProtocolCapabilities() { function defaultProtocolCapabilities() {
return { return {
rest: { rest: {
supports_execution_modes: ['unary', 'window', 'session', 'async_job'], supports_execution_modes: ['unary'],
supports_transport_behaviors: ['request_response', 'server_stream', 'deferred_result'], supports_transport_behaviors: ['request_response'],
}, },
graphql: { graphql: {
supports_execution_modes: ['unary'], supports_execution_modes: ['unary'],
supports_transport_behaviors: ['request_response'], supports_transport_behaviors: ['request_response'],
}, },
grpc: { grpc: {
supports_execution_modes: ['unary', 'window', 'session', 'async_job'], supports_execution_modes: ['unary'],
supports_transport_behaviors: ['request_response', 'server_stream'], supports_transport_behaviors: ['request_response'],
}, },
websocket: { websocket: {
supports_execution_modes: ['window', 'session', 'async_job'], supports_execution_modes: ['unary'],
supports_transport_behaviors: ['server_stream', 'stateful_session', 'deferred_result'], supports_transport_behaviors: ['request_response'],
}, },
soap: { soap: {
supports_execution_modes: ['unary', 'async_job'], supports_execution_modes: ['unary'],
supports_transport_behaviors: ['request_response', 'server_stream', 'deferred_result'], supports_transport_behaviors: ['request_response'],
}, },
}; };
} }
+24
View File
@@ -149,6 +149,9 @@ async function initWizardPage() {
function renderEditionCapabilityHints(capabilities) { function renderEditionCapabilityHints(capabilities) {
var securityNote = document.getElementById('wizard-security-level-note'); var securityNote = document.getElementById('wizard-security-level-note');
var securityText = securityNote ? securityNote.querySelector('.info-callout-text') : null; 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; if (!securityText) return;
var levels = capabilities && Array.isArray(capabilities.supported_security_levels) var levels = capabilities && Array.isArray(capabilities.supported_security_levels)
? capabilities.supported_security_levels ? capabilities.supported_security_levels
@@ -157,8 +160,29 @@ function renderEditionCapabilityHints(capabilities) {
if (!securityNote.hidden) { if (!securityNote.hidden) {
securityText.textContent = tKey('wizard.step5.community_security_note'); 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') { if (window.CrankDiagnostics && typeof window.CrankDiagnostics.bootstrap === 'function') {
window.CrankDiagnostics.bootstrap('wizard', initWizardPage); window.CrankDiagnostics.bootstrap('wizard', initWizardPage);
} else { } else {
+9
View File
@@ -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-websocket"]')).toBeHidden();
await expect(page.locator('[data-testid="wizard-protocol-soap"]')).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 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', 'коммерческую редакцию'));
}); });
+1 -3
View File
@@ -270,9 +270,7 @@
- upstream auth selector; - upstream auth selector;
- quick-create secret / auth profile modal. - quick-create secret / auth profile modal.
- execution mode selector; - execution mode selector;
- streaming config blocks; - streaming config blocks, stream test-runs и tool family preview только там, где это разрешает capability model редакции;
- stream test-runs;
- tool family preview.
Детальные DTO и response shapes для экранов `Operations` и `Wizard` зафиксированы отдельно в: Детальные DTO и response shapes для экранов `Operations` и `Wizard` зафиксированы отдельно в: