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 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:
+7
View File
@@ -1065,6 +1065,13 @@ mod tests {
.collect::<Vec<_>>(),
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")]
+31 -22
View File
@@ -1562,11 +1562,11 @@ impl AdminService {
}
pub async fn list_protocol_capabilities(&self) -> Vec<ProtocolCapabilityView> {
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()],
+1 -4
View File
@@ -69,12 +69,9 @@
</svg>
</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">
<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>
</div>
</div>
+11
View File
@@ -98,6 +98,17 @@ tls:
</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 class="config-card-header">
<div class="config-card-header-icon">
+6 -2
View File
@@ -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': 'Обязательно',
+5
View File
@@ -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) {
+8 -8
View File
@@ -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'],
},
};
}
+24
View File
@@ -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 {
+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-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', 'коммерческую редакцию'));
});
+1 -3
View File
@@ -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` зафиксированы отдельно в: