feat: add streaming ui configuration
This commit is contained in:
@@ -2,20 +2,25 @@
|
||||
|
||||
## Current
|
||||
|
||||
### `feat/session-and-job-tools`
|
||||
### `feat/streaming-e2e`
|
||||
|
||||
Status: completed
|
||||
Status: pending
|
||||
|
||||
DoD:
|
||||
- session and async_job tool families are published in the MCP tool catalog
|
||||
- generated `start/poll/stop` and `start/status/result/cancel` tool calls work through JSON-RPC
|
||||
- stream sessions and async jobs are persisted with explicit status transitions
|
||||
- session and async_job tool calls are logged through observability
|
||||
- MCP integration tests cover `session start -> poll -> stop` and `async job start -> status -> result`
|
||||
- local fixture stack includes:
|
||||
- REST SSE server
|
||||
- gRPC server-streaming server
|
||||
- WebSocket event server
|
||||
- Playwright covers:
|
||||
- REST window flow
|
||||
- gRPC streaming flow
|
||||
- session tool flow
|
||||
- async job flow
|
||||
- CI runs streaming e2e against the local fixture stack
|
||||
|
||||
## Next
|
||||
|
||||
- `feat/streaming-ui-config`
|
||||
- `feat/websocket-upstream-adapter`
|
||||
|
||||
## Backlog
|
||||
|
||||
|
||||
@@ -28,6 +28,10 @@ use crate::{
|
||||
upload_descriptor_set, upload_input_json, upload_output_json, upload_proto_descriptor,
|
||||
},
|
||||
secrets::{create_secret, delete_secret, get_secret, list_secrets, rotate_secret},
|
||||
streaming::{
|
||||
cancel_async_job, get_async_job, get_async_job_result, get_stream_session,
|
||||
list_async_jobs, list_protocol_capabilities, list_stream_sessions, stop_stream_session,
|
||||
},
|
||||
workspaces::{create_workspace, get_workspace, list_workspaces, update_workspace},
|
||||
},
|
||||
state::AppState,
|
||||
@@ -133,7 +137,18 @@ pub fn build_app(state: AppState) -> Router {
|
||||
.route("/logs/{log_id}", get(get_log))
|
||||
.route("/usage", get(get_usage))
|
||||
.route("/usage/operations/{operation_id}", get(get_operation_usage))
|
||||
.route("/usage/agents/{agent_id}", get(get_agent_usage));
|
||||
.route("/usage/agents/{agent_id}", get(get_agent_usage))
|
||||
.route("/protocol-capabilities", get(list_protocol_capabilities))
|
||||
.route("/stream-sessions", get(list_stream_sessions))
|
||||
.route("/stream-sessions/{session_id}", get(get_stream_session))
|
||||
.route(
|
||||
"/stream-sessions/{session_id}/stop",
|
||||
post(stop_stream_session),
|
||||
)
|
||||
.route("/async-jobs", get(list_async_jobs))
|
||||
.route("/async-jobs/{job_id}", get(get_async_job))
|
||||
.route("/async-jobs/{job_id}/cancel", post(cancel_async_job))
|
||||
.route("/async-jobs/{job_id}/result", get(get_async_job_result));
|
||||
|
||||
let workspace_root_router = Router::new()
|
||||
.route("/workspaces", get(list_workspaces).post(create_workspace))
|
||||
|
||||
@@ -5,6 +5,7 @@ pub mod auth_profiles;
|
||||
pub mod observability;
|
||||
pub mod operations;
|
||||
pub mod secrets;
|
||||
pub mod streaming;
|
||||
pub mod workspaces;
|
||||
|
||||
use axum::Json;
|
||||
|
||||
@@ -0,0 +1,137 @@
|
||||
use axum::{
|
||||
Json,
|
||||
extract::{Path, Query, State},
|
||||
};
|
||||
use serde_json::{Value, json};
|
||||
|
||||
use crate::{
|
||||
error::ApiError,
|
||||
routes::access::WorkspacePath,
|
||||
service::{AsyncJobsQuery, StreamSessionsQuery},
|
||||
state::AppState,
|
||||
};
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
pub struct WorkspaceStreamSessionPath {
|
||||
pub workspace_id: String,
|
||||
pub session_id: String,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
pub struct WorkspaceAsyncJobPath {
|
||||
pub workspace_id: String,
|
||||
pub job_id: String,
|
||||
}
|
||||
|
||||
pub async fn list_protocol_capabilities(
|
||||
Path(_path): Path<WorkspacePath>,
|
||||
State(state): State<AppState>,
|
||||
) -> Result<Json<Value>, ApiError> {
|
||||
Ok(Json(json!({
|
||||
"items": state.service.list_protocol_capabilities().await
|
||||
})))
|
||||
}
|
||||
|
||||
pub async fn list_stream_sessions(
|
||||
Path(path): Path<WorkspacePath>,
|
||||
Query(query): Query<StreamSessionsQuery>,
|
||||
State(state): State<AppState>,
|
||||
) -> Result<Json<Value>, ApiError> {
|
||||
let page = state
|
||||
.service
|
||||
.list_stream_sessions(&path.workspace_id.as_str().into(), query.clone())
|
||||
.await?;
|
||||
Ok(Json(json!({
|
||||
"items": page.items,
|
||||
"page": query.page.unwrap_or(1),
|
||||
"page_size": query.page_size.unwrap_or(20),
|
||||
"total": page.total,
|
||||
})))
|
||||
}
|
||||
|
||||
pub async fn get_stream_session(
|
||||
Path(path): Path<WorkspaceStreamSessionPath>,
|
||||
State(state): State<AppState>,
|
||||
) -> Result<Json<Value>, ApiError> {
|
||||
let session = state
|
||||
.service
|
||||
.get_stream_session(
|
||||
&path.workspace_id.as_str().into(),
|
||||
&path.session_id.as_str().into(),
|
||||
)
|
||||
.await?;
|
||||
Ok(Json(json!(session)))
|
||||
}
|
||||
|
||||
pub async fn stop_stream_session(
|
||||
Path(path): Path<WorkspaceStreamSessionPath>,
|
||||
State(state): State<AppState>,
|
||||
) -> Result<Json<Value>, ApiError> {
|
||||
let session = state
|
||||
.service
|
||||
.stop_stream_session(
|
||||
&path.workspace_id.as_str().into(),
|
||||
&path.session_id.as_str().into(),
|
||||
)
|
||||
.await?;
|
||||
Ok(Json(json!(session)))
|
||||
}
|
||||
|
||||
pub async fn list_async_jobs(
|
||||
Path(path): Path<WorkspacePath>,
|
||||
Query(query): Query<AsyncJobsQuery>,
|
||||
State(state): State<AppState>,
|
||||
) -> Result<Json<Value>, ApiError> {
|
||||
let page = state
|
||||
.service
|
||||
.list_async_jobs(&path.workspace_id.as_str().into(), query.clone())
|
||||
.await?;
|
||||
Ok(Json(json!({
|
||||
"items": page.items,
|
||||
"page": query.page.unwrap_or(1),
|
||||
"page_size": query.page_size.unwrap_or(20),
|
||||
"total": page.total,
|
||||
})))
|
||||
}
|
||||
|
||||
pub async fn get_async_job(
|
||||
Path(path): Path<WorkspaceAsyncJobPath>,
|
||||
State(state): State<AppState>,
|
||||
) -> Result<Json<Value>, ApiError> {
|
||||
let job = state
|
||||
.service
|
||||
.get_async_job(
|
||||
&path.workspace_id.as_str().into(),
|
||||
&path.job_id.as_str().into(),
|
||||
)
|
||||
.await?;
|
||||
Ok(Json(json!(job)))
|
||||
}
|
||||
|
||||
pub async fn cancel_async_job(
|
||||
Path(path): Path<WorkspaceAsyncJobPath>,
|
||||
State(state): State<AppState>,
|
||||
) -> Result<Json<Value>, ApiError> {
|
||||
let job = state
|
||||
.service
|
||||
.cancel_async_job(
|
||||
&path.workspace_id.as_str().into(),
|
||||
&path.job_id.as_str().into(),
|
||||
)
|
||||
.await?;
|
||||
Ok(Json(json!(job)))
|
||||
}
|
||||
|
||||
pub async fn get_async_job_result(
|
||||
Path(path): Path<WorkspaceAsyncJobPath>,
|
||||
State(state): State<AppState>,
|
||||
) -> Result<Json<Value>, ApiError> {
|
||||
let result = state
|
||||
.service
|
||||
.get_async_job_result(
|
||||
&path.workspace_id.as_str().into(),
|
||||
&path.job_id.as_str().into(),
|
||||
)
|
||||
.await?;
|
||||
Ok(Json(json!(result)))
|
||||
}
|
||||
+384
-11
@@ -7,26 +7,27 @@ use base64::{
|
||||
};
|
||||
use crank_adapter_grpc::test_support as grpc_test_support;
|
||||
use crank_core::{
|
||||
Agent, AgentId, AgentOperationBinding, AgentStatus, AgentVersion, AuthConfig, AuthKind,
|
||||
AuthProfile, AuthProfileId, ConfigExport, ExportMode, GeneratedDraft, GeneratedDraftStatus,
|
||||
InvitationId, InvitationStatus, InvitationToken, InvocationLevel, InvocationLog,
|
||||
InvocationLogId, InvocationSource, InvocationStatus, MembershipRole, OperationId,
|
||||
OperationStatus, PlatformApiKey, PlatformApiKeyId, PlatformApiKeyScope, PlatformApiKeyStatus,
|
||||
Protocol, SampleId, Samples, Secret, SecretId, SecretKind, SecretStatus, Target, UsagePeriod,
|
||||
UserId, UserSessionId, Workspace, WorkspaceId, WorkspaceStatus,
|
||||
Agent, AgentId, AgentOperationBinding, AgentStatus, AgentVersion, AggregationMode,
|
||||
AsyncJobHandle, AuthConfig, AuthKind, AuthProfile, AuthProfileId, ConfigExport, ExecutionMode,
|
||||
ExportMode, GeneratedDraft, GeneratedDraftStatus, InvitationId, InvitationStatus,
|
||||
InvitationToken, InvocationLevel, InvocationLog, InvocationLogId, InvocationSource,
|
||||
InvocationStatus, JobStatus, MembershipRole, OperationId, OperationStatus, PlatformApiKey,
|
||||
PlatformApiKeyId, PlatformApiKeyScope, PlatformApiKeyStatus, Protocol, SampleId, Samples,
|
||||
Secret, SecretId, SecretKind, SecretStatus, StreamSession, StreamStatus, Target,
|
||||
TransportBehavior, UsagePeriod, UserId, UserSessionId, Workspace, WorkspaceId, WorkspaceStatus,
|
||||
};
|
||||
use crank_mapping::{JsonPathRoot, MappingSet, infer_mapping_from_samples};
|
||||
use crank_proto::{ProtoService, services_from_descriptor_set_bytes};
|
||||
use crank_registry::{
|
||||
AgentSummary, AgentVersionRecord, CreateAgentRequest, CreateInvitationRequest,
|
||||
AgentSummary, AgentVersionRecord, AsyncJobFilter, CreateAgentRequest, CreateInvitationRequest,
|
||||
CreateInvocationLogRequest, CreatePlatformApiKeyRequest, CreateSecretRequest,
|
||||
CreateVersionRequest, CreateWorkspaceRequest, InvitationRecord, InvocationLogRecord,
|
||||
ListInvocationLogsQuery, MembershipRecord, OperationAgentRef, OperationSampleMetadata,
|
||||
OperationSummary, OperationUsageSummary, OperationVersionRecord, PlatformApiKeyRecord,
|
||||
OperationSummary, OperationUsageSummary, OperationVersionRecord, Page, PlatformApiKeyRecord,
|
||||
PostgresRegistry, PublishAgentRequest, PublishRequest, RegistryOperation, RotateSecretRequest,
|
||||
SampleKind, SaveAgentBindingsRequest, SaveAuthProfileRequest, SaveDescriptorMetadataRequest,
|
||||
SaveSampleMetadataRequest, UpdateWorkspaceRequest, UsageAgentBreakdown, UsageBucket,
|
||||
UsageOperationBreakdown, UsageQuery, UsageSummary, UsageTimelinePoint,
|
||||
SaveSampleMetadataRequest, StreamSessionFilter, UpdateWorkspaceRequest, UsageAgentBreakdown,
|
||||
UsageBucket, UsageOperationBreakdown, UsageQuery, UsageSummary, UsageTimelinePoint,
|
||||
WorkspaceMembershipRecord, WorkspaceRecord,
|
||||
};
|
||||
use crank_runtime::{PreparedRequest, RuntimeError, RuntimeExecutor, RuntimeOperation};
|
||||
@@ -303,6 +304,78 @@ pub struct UsageOverviewResponse {
|
||||
pub agents: Vec<UsageAgentBreakdown>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
pub struct ProtocolCapabilityView {
|
||||
pub protocol: Protocol,
|
||||
pub supports_execution_modes: Vec<ExecutionMode>,
|
||||
pub supports_transport_behaviors: Vec<TransportBehavior>,
|
||||
pub supports_auth_kinds: Vec<String>,
|
||||
pub supports_upload_artifacts: Vec<String>,
|
||||
pub supports_cursor_path: bool,
|
||||
pub supports_done_path: bool,
|
||||
pub supports_aggregation_mode: Vec<AggregationMode>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
pub struct StreamSessionsQuery {
|
||||
pub operation_id: Option<String>,
|
||||
pub agent_id: Option<String>,
|
||||
pub status: Option<StreamStatus>,
|
||||
pub mode: Option<ExecutionMode>,
|
||||
pub page: Option<u32>,
|
||||
pub page_size: Option<u32>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
pub struct StreamSessionSummaryView {
|
||||
pub id: String,
|
||||
pub operation_id: String,
|
||||
pub agent_id: Option<String>,
|
||||
pub protocol: Protocol,
|
||||
pub mode: ExecutionMode,
|
||||
pub status: StreamStatus,
|
||||
pub expires_at: String,
|
||||
pub last_poll_at: Option<String>,
|
||||
pub created_at: String,
|
||||
pub closed_at: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
pub struct StreamSessionDetailView {
|
||||
#[serde(flatten)]
|
||||
pub summary: StreamSessionSummaryView,
|
||||
pub cursor: Option<Value>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
pub struct AsyncJobsQuery {
|
||||
pub operation_id: Option<String>,
|
||||
pub agent_id: Option<String>,
|
||||
pub status: Option<JobStatus>,
|
||||
pub page: Option<u32>,
|
||||
pub page_size: Option<u32>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
pub struct AsyncJobSummaryView {
|
||||
pub id: String,
|
||||
pub operation_id: String,
|
||||
pub agent_id: Option<String>,
|
||||
pub status: JobStatus,
|
||||
pub progress: Value,
|
||||
pub created_at: String,
|
||||
pub updated_at: String,
|
||||
pub finished_at: Option<String>,
|
||||
pub expires_at: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
pub struct AsyncJobDetailView {
|
||||
#[serde(flatten)]
|
||||
pub summary: AsyncJobSummaryView,
|
||||
pub error: Option<Value>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
pub struct GenerateDraftPayload {
|
||||
#[serde(default)]
|
||||
@@ -1273,6 +1346,185 @@ impl AdminService {
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn list_protocol_capabilities(&self) -> Vec<ProtocolCapabilityView> {
|
||||
[Protocol::Rest, Protocol::Graphql, Protocol::Grpc]
|
||||
.into_iter()
|
||||
.map(protocol_capability_view)
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[instrument(skip(self))]
|
||||
pub async fn list_stream_sessions(
|
||||
&self,
|
||||
workspace_id: &WorkspaceId,
|
||||
query: StreamSessionsQuery,
|
||||
) -> Result<Page<StreamSessionSummaryView>, ApiError> {
|
||||
self.ensure_workspace_exists(workspace_id).await?;
|
||||
let operation_id = query.operation_id.as_deref().map(OperationId::new);
|
||||
let agent_id = query.agent_id.as_deref().map(AgentId::new);
|
||||
let limit = query.page_size.unwrap_or(20).max(1);
|
||||
let page = self
|
||||
.registry
|
||||
.list_stream_sessions(StreamSessionFilter {
|
||||
workspace_id,
|
||||
agent_id: agent_id.as_ref(),
|
||||
operation_id: operation_id.as_ref(),
|
||||
status: query.status,
|
||||
mode: query.mode,
|
||||
limit,
|
||||
})
|
||||
.await?;
|
||||
|
||||
Ok(Page {
|
||||
items: page
|
||||
.items
|
||||
.into_iter()
|
||||
.map(stream_session_summary_view)
|
||||
.collect(),
|
||||
total: page.total,
|
||||
})
|
||||
}
|
||||
|
||||
#[instrument(skip(self))]
|
||||
pub async fn get_stream_session(
|
||||
&self,
|
||||
workspace_id: &WorkspaceId,
|
||||
session_id: &crank_core::StreamSessionId,
|
||||
) -> Result<StreamSessionDetailView, ApiError> {
|
||||
self.ensure_workspace_exists(workspace_id).await?;
|
||||
let session = self
|
||||
.registry
|
||||
.get_stream_session(session_id)
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
ApiError::not_found(format!(
|
||||
"stream session {} was not found",
|
||||
session_id.as_str()
|
||||
))
|
||||
})?;
|
||||
ensure_stream_session_workspace(&session, workspace_id)?;
|
||||
|
||||
Ok(stream_session_detail_view(session))
|
||||
}
|
||||
|
||||
#[instrument(skip(self))]
|
||||
pub async fn stop_stream_session(
|
||||
&self,
|
||||
workspace_id: &WorkspaceId,
|
||||
session_id: &crank_core::StreamSessionId,
|
||||
) -> Result<StreamSessionDetailView, ApiError> {
|
||||
self.ensure_workspace_exists(workspace_id).await?;
|
||||
let session = self
|
||||
.registry
|
||||
.get_stream_session(session_id)
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
ApiError::not_found(format!(
|
||||
"stream session {} was not found",
|
||||
session_id.as_str()
|
||||
))
|
||||
})?;
|
||||
ensure_stream_session_workspace(&session, workspace_id)?;
|
||||
self.registry
|
||||
.close_stream_session(session_id, &now_string()?)
|
||||
.await?;
|
||||
let updated = self
|
||||
.registry
|
||||
.get_stream_session(session_id)
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
ApiError::not_found(format!(
|
||||
"stream session {} was not found",
|
||||
session_id.as_str()
|
||||
))
|
||||
})?;
|
||||
|
||||
Ok(stream_session_detail_view(updated))
|
||||
}
|
||||
|
||||
#[instrument(skip(self))]
|
||||
pub async fn list_async_jobs(
|
||||
&self,
|
||||
workspace_id: &WorkspaceId,
|
||||
query: AsyncJobsQuery,
|
||||
) -> Result<Page<AsyncJobSummaryView>, ApiError> {
|
||||
self.ensure_workspace_exists(workspace_id).await?;
|
||||
let operation_id = query.operation_id.as_deref().map(OperationId::new);
|
||||
let agent_id = query.agent_id.as_deref().map(AgentId::new);
|
||||
let limit = query.page_size.unwrap_or(20).max(1);
|
||||
let page = self
|
||||
.registry
|
||||
.list_async_jobs(AsyncJobFilter {
|
||||
workspace_id,
|
||||
agent_id: agent_id.as_ref(),
|
||||
operation_id: operation_id.as_ref(),
|
||||
status: query.status,
|
||||
limit,
|
||||
})
|
||||
.await?;
|
||||
|
||||
Ok(Page {
|
||||
items: page.items.into_iter().map(async_job_summary_view).collect(),
|
||||
total: page.total,
|
||||
})
|
||||
}
|
||||
|
||||
#[instrument(skip(self))]
|
||||
pub async fn get_async_job(
|
||||
&self,
|
||||
workspace_id: &WorkspaceId,
|
||||
job_id: &crank_core::AsyncJobId,
|
||||
) -> Result<AsyncJobDetailView, ApiError> {
|
||||
self.ensure_workspace_exists(workspace_id).await?;
|
||||
let job = self.registry.get_async_job(job_id).await?.ok_or_else(|| {
|
||||
ApiError::not_found(format!("async job {} was not found", job_id.as_str()))
|
||||
})?;
|
||||
ensure_async_job_workspace(&job, workspace_id)?;
|
||||
|
||||
Ok(async_job_detail_view(job))
|
||||
}
|
||||
|
||||
#[instrument(skip(self))]
|
||||
pub async fn cancel_async_job(
|
||||
&self,
|
||||
workspace_id: &WorkspaceId,
|
||||
job_id: &crank_core::AsyncJobId,
|
||||
) -> Result<AsyncJobDetailView, ApiError> {
|
||||
self.ensure_workspace_exists(workspace_id).await?;
|
||||
let job = self.registry.get_async_job(job_id).await?.ok_or_else(|| {
|
||||
ApiError::not_found(format!("async job {} was not found", job_id.as_str()))
|
||||
})?;
|
||||
ensure_async_job_workspace(&job, workspace_id)?;
|
||||
self.registry
|
||||
.cancel_async_job(job_id, &now_string()?)
|
||||
.await?;
|
||||
let updated = self.registry.get_async_job(job_id).await?.ok_or_else(|| {
|
||||
ApiError::not_found(format!("async job {} was not found", job_id.as_str()))
|
||||
})?;
|
||||
|
||||
Ok(async_job_detail_view(updated))
|
||||
}
|
||||
|
||||
#[instrument(skip(self))]
|
||||
pub async fn get_async_job_result(
|
||||
&self,
|
||||
workspace_id: &WorkspaceId,
|
||||
job_id: &crank_core::AsyncJobId,
|
||||
) -> Result<Value, ApiError> {
|
||||
self.ensure_workspace_exists(workspace_id).await?;
|
||||
let job = self.registry.get_async_job(job_id).await?.ok_or_else(|| {
|
||||
ApiError::not_found(format!("async job {} was not found", job_id.as_str()))
|
||||
})?;
|
||||
ensure_async_job_workspace(&job, workspace_id)?;
|
||||
|
||||
match job.status {
|
||||
JobStatus::Completed => Ok(job.result.unwrap_or(Value::Null)),
|
||||
JobStatus::Failed => Err(ApiError::conflict("async job failed")),
|
||||
JobStatus::Cancelled => Err(ApiError::conflict("async job was cancelled")),
|
||||
_ => Err(ApiError::conflict("async job result is not ready")),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn list_operations(
|
||||
&self,
|
||||
workspace_id: &WorkspaceId,
|
||||
@@ -3762,6 +4014,127 @@ 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();
|
||||
let supports_upload_artifacts = match protocol {
|
||||
Protocol::Rest | Protocol::Graphql => Vec::new(),
|
||||
Protocol::Grpc => vec!["proto".to_owned(), "descriptor_set".to_owned()],
|
||||
};
|
||||
|
||||
ProtocolCapabilityView {
|
||||
protocol,
|
||||
supports_execution_modes,
|
||||
supports_transport_behaviors,
|
||||
supports_auth_kinds: vec![
|
||||
"none".to_owned(),
|
||||
"bearer".to_owned(),
|
||||
"basic".to_owned(),
|
||||
"api_key_header".to_owned(),
|
||||
"api_key_query".to_owned(),
|
||||
],
|
||||
supports_upload_artifacts,
|
||||
supports_cursor_path: !matches!(protocol, Protocol::Graphql),
|
||||
supports_done_path: !matches!(protocol, Protocol::Graphql),
|
||||
supports_aggregation_mode: vec![
|
||||
AggregationMode::RawItems,
|
||||
AggregationMode::SummaryOnly,
|
||||
AggregationMode::SummaryPlusSamples,
|
||||
AggregationMode::Stats,
|
||||
AggregationMode::LatestState,
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
fn stream_session_summary_view(session: StreamSession) -> StreamSessionSummaryView {
|
||||
StreamSessionSummaryView {
|
||||
id: session.id.as_str().to_owned(),
|
||||
operation_id: session.operation_id.as_str().to_owned(),
|
||||
agent_id: session.agent_id.map(|value| value.as_str().to_owned()),
|
||||
protocol: session.protocol,
|
||||
mode: session.mode,
|
||||
status: session.status,
|
||||
expires_at: session.expires_at,
|
||||
last_poll_at: session.last_poll_at,
|
||||
created_at: session.created_at,
|
||||
closed_at: session.closed_at,
|
||||
}
|
||||
}
|
||||
|
||||
fn stream_session_detail_view(session: StreamSession) -> StreamSessionDetailView {
|
||||
StreamSessionDetailView {
|
||||
cursor: session.cursor.clone(),
|
||||
summary: stream_session_summary_view(session),
|
||||
}
|
||||
}
|
||||
|
||||
fn async_job_summary_view(job: AsyncJobHandle) -> AsyncJobSummaryView {
|
||||
AsyncJobSummaryView {
|
||||
id: job.id.as_str().to_owned(),
|
||||
operation_id: job.operation_id.as_str().to_owned(),
|
||||
agent_id: job.agent_id.map(|value| value.as_str().to_owned()),
|
||||
status: job.status,
|
||||
progress: job.progress,
|
||||
created_at: job.created_at,
|
||||
updated_at: job.updated_at,
|
||||
finished_at: job.finished_at,
|
||||
expires_at: job.expires_at,
|
||||
}
|
||||
}
|
||||
|
||||
fn async_job_detail_view(job: AsyncJobHandle) -> AsyncJobDetailView {
|
||||
AsyncJobDetailView {
|
||||
error: job.error.clone(),
|
||||
summary: async_job_summary_view(job),
|
||||
}
|
||||
}
|
||||
|
||||
fn ensure_stream_session_workspace(
|
||||
session: &StreamSession,
|
||||
workspace_id: &WorkspaceId,
|
||||
) -> Result<(), ApiError> {
|
||||
if &session.workspace_id == workspace_id {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
Err(ApiError::not_found(format!(
|
||||
"stream session {} was not found in workspace {}",
|
||||
session.id.as_str(),
|
||||
workspace_id.as_str()
|
||||
)))
|
||||
}
|
||||
|
||||
fn ensure_async_job_workspace(
|
||||
job: &AsyncJobHandle,
|
||||
workspace_id: &WorkspaceId,
|
||||
) -> Result<(), ApiError> {
|
||||
if &job.workspace_id == workspace_id {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
Err(ApiError::not_found(format!(
|
||||
"async job {} was not found in workspace {}",
|
||||
job.id.as_str(),
|
||||
workspace_id.as_str()
|
||||
)))
|
||||
}
|
||||
|
||||
fn usage_window(period: UsagePeriod) -> Result<(UsagePeriod, String, UsageBucket), ApiError> {
|
||||
let now = OffsetDateTime::now_utc();
|
||||
let (start, bucket) = match period {
|
||||
|
||||
@@ -1550,3 +1550,174 @@
|
||||
width: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.list-toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
margin-bottom: 16px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.list-toolbar-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.page-select {
|
||||
min-height: 36px;
|
||||
padding: 8px 12px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 10px;
|
||||
background: var(--bg-overlay);
|
||||
color: var(--text-secondary);
|
||||
font-size: 13px;
|
||||
font-family: 'Inter', sans-serif;
|
||||
}
|
||||
|
||||
.resource-list {
|
||||
display: grid;
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.resource-card {
|
||||
background: var(--bg-overlay);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 14px;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.resource-card-header {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 14px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.resource-card-title {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.resource-card-subtitle {
|
||||
margin-top: 4px;
|
||||
font-size: 12px;
|
||||
color: var(--text-muted);
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.resource-card-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.resource-meta-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
|
||||
gap: 12px;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.resource-meta-item {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.resource-meta-label {
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.04em;
|
||||
text-transform: uppercase;
|
||||
color: var(--text-muted);
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.resource-meta-value {
|
||||
font-size: 13px;
|
||||
color: var(--text-primary);
|
||||
line-height: 1.5;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.resource-detail-block {
|
||||
margin-top: 12px;
|
||||
border-top: 1px solid var(--border-subtle);
|
||||
padding-top: 12px;
|
||||
}
|
||||
|
||||
.resource-detail-title {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.resource-detail-pre {
|
||||
margin: 0;
|
||||
padding: 12px 14px;
|
||||
border-radius: 12px;
|
||||
background: rgba(12, 17, 24, 0.8);
|
||||
border: 1px solid var(--border-subtle);
|
||||
color: var(--text-secondary);
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
overflow: auto;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.resource-pill-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.resource-status-pill {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 5px 9px;
|
||||
border-radius: 999px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
border: 1px solid var(--border);
|
||||
background: rgba(255, 255, 255, 0.04);
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.resource-status-pill.active,
|
||||
.resource-status-pill.running,
|
||||
.resource-status-pill.succeeded {
|
||||
border-color: rgba(63, 185, 80, 0.28);
|
||||
background: rgba(63, 185, 80, 0.12);
|
||||
color: #71dd8a;
|
||||
}
|
||||
|
||||
.resource-status-pill.idle,
|
||||
.resource-status-pill.pending {
|
||||
border-color: rgba(110, 118, 129, 0.28);
|
||||
background: rgba(110, 118, 129, 0.12);
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.resource-status-pill.failed,
|
||||
.resource-status-pill.cancelled,
|
||||
.resource-status-pill.stopped {
|
||||
border-color: rgba(248, 81, 73, 0.28);
|
||||
background: rgba(248, 81, 73, 0.12);
|
||||
color: #ff8a83;
|
||||
}
|
||||
|
||||
.resource-status-pill.completed {
|
||||
border-color: rgba(56, 189, 248, 0.28);
|
||||
background: rgba(56, 189, 248, 0.12);
|
||||
color: #79d8ff;
|
||||
}
|
||||
|
||||
@@ -137,6 +137,38 @@
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.checkbox-pill {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
min-height: 38px;
|
||||
padding: 8px 12px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 10px;
|
||||
background: var(--bg-overlay);
|
||||
color: var(--text-secondary);
|
||||
font-size: 13px;
|
||||
cursor: pointer;
|
||||
transition: border-color 0.15s ease, background 0.15s ease, color 0.15s ease;
|
||||
}
|
||||
|
||||
.checkbox-pill:hover {
|
||||
border-color: var(--bg-muted);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.checkbox-pill input {
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
accent-color: var(--accent);
|
||||
}
|
||||
|
||||
.checkbox-pill:has(input:checked) {
|
||||
border-color: rgba(13, 148, 136, 0.35);
|
||||
background: rgba(13, 148, 136, 0.12);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
/* ══════════════════════════════════════════════════
|
||||
STEP SIDEBAR
|
||||
══════════════════════════════════════════════════ */
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<base href="../">
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Crank — Async Jobs</title>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="css/variables.css">
|
||||
<link rel="stylesheet" href="css/layout.css">
|
||||
<link rel="stylesheet" href="css/pages.css">
|
||||
<script src="js/config.js"></script>
|
||||
<script src="js/i18n.js"></script>
|
||||
<script src="js/api.js"></script>
|
||||
<script src="js/ui-feedback.js"></script>
|
||||
<script src="js/workspace.js"></script>
|
||||
<script src="js/auth.js"></script>
|
||||
<script>window.CrankAuth.guardProtectedPage();</script>
|
||||
</head>
|
||||
<body>
|
||||
<nav class="navbar">
|
||||
<a href="/" class="nav-logo">
|
||||
<div class="nav-logo-mark"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 28 28" fill="white"><path fill-rule="evenodd" d="M 10.000,1.800 L 12.287,3.962 L 15.408,3.557 L 15.987,6.650 L 18.750,8.157 L 17.400,11.000 L 18.750,13.843 L 15.987,15.350 L 15.408,18.443 L 12.287,18.038 L 10.000,20.200 L 7.713,18.038 L 4.592,18.443 L 4.013,15.350 L 1.250,13.843 L 2.600,11.000 L 1.250,8.157 L 4.013,6.650 L 4.592,3.557 L 7.713,3.962 Z M 12.800,11.000 A 2.8 2.8 0 1 0 7.200,11.000 A 2.8 2.8 0 1 0 12.800,11.000 Z"/><path fill-rule="evenodd" d="M 11.791,15.604 Q 16.083,17.179 21.375,21.154 A 2.00 2.00 0 1 1 23.625,17.846 Q 19.782,15.610 14.940,10.973 Z M 24.300,19.500 A 1.8 1.8 0 1 0 20.700,19.500 A 1.8 1.8 0 1 0 24.300,19.500 Z"/><path d="M 10.0,9.7 L 11.3,11.0 L 10.0,12.3 L 8.7,11.0 Z" fill="rgba(0,0,0,0.3)"/></svg></div>
|
||||
<span class="nav-logo-text">Crank</span>
|
||||
</a>
|
||||
<div class="ws-switcher" id="ws-switcher">
|
||||
<button class="ws-switcher-trigger" onclick="toggleWsSwitcher(event)">
|
||||
<div class="ws-dot" id="ws-dot">A</div>
|
||||
<span class="ws-current-name" id="ws-current-name">workspace</span>
|
||||
<svg width="11" height="11" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 6l4 4 4-4"/></svg>
|
||||
</button>
|
||||
<div class="ws-dropdown" id="ws-dropdown" style="display:none">
|
||||
<div class="ws-dropdown-label" data-i18n="nav.workspaces">Your workspaces</div>
|
||||
<div id="ws-dropdown-list"></div>
|
||||
<div class="ws-dropdown-footer">
|
||||
<button class="ws-dropdown-create" onclick="window.location.href='/workspace-setup?mode=create'">
|
||||
<svg width="12" height="12" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round"><path d="M8 3v10M3 8h10"/></svg>
|
||||
<span data-i18n="nav.create_workspace">Create workspace</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="nav-links">
|
||||
<a href="/" class="nav-link" data-i18n="nav.operations">Operations</a>
|
||||
<a href="/agents" class="nav-link" data-i18n="nav.agents">Agents</a>
|
||||
<a href="/api-keys" class="nav-link" data-i18n="nav.apikeys">API Keys</a>
|
||||
<a href="/logs" class="nav-link" data-i18n="nav.logs">Logs</a>
|
||||
<a href="/usage" class="nav-link" data-i18n="nav.usage">Usage</a>
|
||||
</div>
|
||||
<div class="nav-right">
|
||||
<button class="nav-hamburger" data-i18n-title="nav.menu" data-i18n-aria-label="nav.menu" aria-label="Menu"><span></span><span></span><span></span></button>
|
||||
<div class="nav-kbd"><svg width="12" height="12"><use href="icons/general/grid.svg#icon"/></svg><span>⌘K</span></div>
|
||||
<button class="nav-icon-btn" data-i18n-title="nav.notifications" title="Notifications"><svg width="15" height="15"><use href="icons/general/bell.svg#icon"/></svg></button>
|
||||
<div class="nav-divider"></div>
|
||||
<div class="user-menu">
|
||||
<div class="nav-avatar" data-i18n-title="nav.account" title="Account">AT</div>
|
||||
<div class="user-dropdown">
|
||||
<div class="user-dropdown-header"><div class="user-dropdown-name">Crank</div><div class="user-dropdown-role">—</div></div>
|
||||
<button class="user-dropdown-item" data-action="settings-link"><svg width="13" height="13"><use href="icons/general/settings.svg#icon"/></svg><span data-i18n="nav.settings">Settings</span></button>
|
||||
<div class="dropdown-divider"></div>
|
||||
<button class="user-dropdown-item danger" data-action="logout"><svg width="13" height="13"><use href="icons/general/logout.svg#icon"/></svg><span data-i18n="nav.logout">Log out</span></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div class="mobile-nav">
|
||||
<a href="/" class="mobile-nav-link" data-i18n="nav.operations">Operations</a>
|
||||
<a href="/agents" class="mobile-nav-link" data-i18n="nav.agents">Agents</a>
|
||||
<a href="/api-keys" class="mobile-nav-link" data-i18n="nav.apikeys">API Keys</a>
|
||||
<a href="/logs" class="mobile-nav-link" data-i18n="nav.logs">Logs</a>
|
||||
<a href="/usage" class="mobile-nav-link" data-i18n="nav.usage">Usage</a>
|
||||
</div>
|
||||
|
||||
<div class="page">
|
||||
<div class="page-header">
|
||||
<div class="page-header-text">
|
||||
<h1 class="page-title" data-i18n="async_jobs.title">Async jobs</h1>
|
||||
<p class="page-subtitle" data-i18n="async_jobs.subtitle">Inspect deferred jobs started by streaming MCP tool families.</p>
|
||||
</div>
|
||||
<div class="page-header-actions">
|
||||
<button class="btn-secondary" id="async-jobs-refresh" type="button" data-i18n="async_jobs.refresh">Refresh</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section-card">
|
||||
<div class="section-card-body">
|
||||
<div class="list-toolbar">
|
||||
<div class="list-toolbar-group">
|
||||
<select class="page-select" id="async-jobs-status">
|
||||
<option value="" data-i18n="async_jobs.filter.all_statuses">All statuses</option>
|
||||
<option value="created" data-i18n="async_jobs.status.created">Created</option>
|
||||
<option value="running" data-i18n="async_jobs.status.running">Running</option>
|
||||
<option value="completed" data-i18n="async_jobs.status.completed">Completed</option>
|
||||
<option value="failed" data-i18n="async_jobs.status.failed">Failed</option>
|
||||
<option value="cancelled" data-i18n="async_jobs.status.cancelled">Cancelled</option>
|
||||
<option value="expired" data-i18n="async_jobs.status.expired">Expired</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="list-toolbar-group">
|
||||
<span class="section-card-subtitle" id="async-jobs-summary"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="resource-list" id="async-jobs-list"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="js/nav.js"></script>
|
||||
<script src="js/async-jobs.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,117 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<base href="../">
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Crank — Stream Sessions</title>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="css/variables.css">
|
||||
<link rel="stylesheet" href="css/layout.css">
|
||||
<link rel="stylesheet" href="css/pages.css">
|
||||
<script src="js/config.js"></script>
|
||||
<script src="js/i18n.js"></script>
|
||||
<script src="js/api.js"></script>
|
||||
<script src="js/ui-feedback.js"></script>
|
||||
<script src="js/workspace.js"></script>
|
||||
<script src="js/auth.js"></script>
|
||||
<script>window.CrankAuth.guardProtectedPage();</script>
|
||||
</head>
|
||||
<body>
|
||||
<nav class="navbar">
|
||||
<a href="/" class="nav-logo">
|
||||
<div class="nav-logo-mark"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 28 28" fill="white"><path fill-rule="evenodd" d="M 10.000,1.800 L 12.287,3.962 L 15.408,3.557 L 15.987,6.650 L 18.750,8.157 L 17.400,11.000 L 18.750,13.843 L 15.987,15.350 L 15.408,18.443 L 12.287,18.038 L 10.000,20.200 L 7.713,18.038 L 4.592,18.443 L 4.013,15.350 L 1.250,13.843 L 2.600,11.000 L 1.250,8.157 L 4.013,6.650 L 4.592,3.557 L 7.713,3.962 Z M 12.800,11.000 A 2.8 2.8 0 1 0 7.200,11.000 A 2.8 2.8 0 1 0 12.800,11.000 Z"/><path fill-rule="evenodd" d="M 11.791,15.604 Q 16.083,17.179 21.375,21.154 A 2.00 2.00 0 1 1 23.625,17.846 Q 19.782,15.610 14.940,10.973 Z M 24.300,19.500 A 1.8 1.8 0 1 0 20.700,19.500 A 1.8 1.8 0 1 0 24.300,19.500 Z"/><path d="M 10.0,9.7 L 11.3,11.0 L 10.0,12.3 L 8.7,11.0 Z" fill="rgba(0,0,0,0.3)"/></svg></div>
|
||||
<span class="nav-logo-text">Crank</span>
|
||||
</a>
|
||||
<div class="ws-switcher" id="ws-switcher">
|
||||
<button class="ws-switcher-trigger" onclick="toggleWsSwitcher(event)">
|
||||
<div class="ws-dot" id="ws-dot">A</div>
|
||||
<span class="ws-current-name" id="ws-current-name">workspace</span>
|
||||
<svg width="11" height="11" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 6l4 4 4-4"/></svg>
|
||||
</button>
|
||||
<div class="ws-dropdown" id="ws-dropdown" style="display:none">
|
||||
<div class="ws-dropdown-label" data-i18n="nav.workspaces">Your workspaces</div>
|
||||
<div id="ws-dropdown-list"></div>
|
||||
<div class="ws-dropdown-footer">
|
||||
<button class="ws-dropdown-create" onclick="window.location.href='/workspace-setup?mode=create'">
|
||||
<svg width="12" height="12" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round"><path d="M8 3v10M3 8h10"/></svg>
|
||||
<span data-i18n="nav.create_workspace">Create workspace</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="nav-links">
|
||||
<a href="/" class="nav-link" data-i18n="nav.operations">Operations</a>
|
||||
<a href="/agents" class="nav-link" data-i18n="nav.agents">Agents</a>
|
||||
<a href="/api-keys" class="nav-link" data-i18n="nav.apikeys">API Keys</a>
|
||||
<a href="/logs" class="nav-link" data-i18n="nav.logs">Logs</a>
|
||||
<a href="/usage" class="nav-link" data-i18n="nav.usage">Usage</a>
|
||||
</div>
|
||||
<div class="nav-right">
|
||||
<button class="nav-hamburger" data-i18n-title="nav.menu" data-i18n-aria-label="nav.menu" aria-label="Menu"><span></span><span></span><span></span></button>
|
||||
<div class="nav-kbd"><svg width="12" height="12"><use href="icons/general/grid.svg#icon"/></svg><span>⌘K</span></div>
|
||||
<button class="nav-icon-btn" data-i18n-title="nav.notifications" title="Notifications"><svg width="15" height="15"><use href="icons/general/bell.svg#icon"/></svg></button>
|
||||
<div class="nav-divider"></div>
|
||||
<div class="user-menu">
|
||||
<div class="nav-avatar" data-i18n-title="nav.account" title="Account">AT</div>
|
||||
<div class="user-dropdown">
|
||||
<div class="user-dropdown-header"><div class="user-dropdown-name">Crank</div><div class="user-dropdown-role">—</div></div>
|
||||
<button class="user-dropdown-item" data-action="settings-link"><svg width="13" height="13"><use href="icons/general/settings.svg#icon"/></svg><span data-i18n="nav.settings">Settings</span></button>
|
||||
<div class="dropdown-divider"></div>
|
||||
<button class="user-dropdown-item danger" data-action="logout"><svg width="13" height="13"><use href="icons/general/logout.svg#icon"/></svg><span data-i18n="nav.logout">Log out</span></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div class="mobile-nav">
|
||||
<a href="/" class="mobile-nav-link" data-i18n="nav.operations">Operations</a>
|
||||
<a href="/agents" class="mobile-nav-link" data-i18n="nav.agents">Agents</a>
|
||||
<a href="/api-keys" class="mobile-nav-link" data-i18n="nav.apikeys">API Keys</a>
|
||||
<a href="/logs" class="mobile-nav-link" data-i18n="nav.logs">Logs</a>
|
||||
<a href="/usage" class="mobile-nav-link" data-i18n="nav.usage">Usage</a>
|
||||
</div>
|
||||
|
||||
<div class="page">
|
||||
<div class="page-header">
|
||||
<div class="page-header-text">
|
||||
<h1 class="page-title" data-i18n="stream_sessions.title">Stream sessions</h1>
|
||||
<p class="page-subtitle" data-i18n="stream_sessions.subtitle">Inspect stateful streaming sessions created by generated MCP tool families.</p>
|
||||
</div>
|
||||
<div class="page-header-actions">
|
||||
<button class="btn-secondary" id="stream-sessions-refresh" type="button" data-i18n="stream_sessions.refresh">Refresh</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section-card">
|
||||
<div class="section-card-body">
|
||||
<div class="list-toolbar">
|
||||
<div class="list-toolbar-group">
|
||||
<select class="page-select" id="stream-sessions-status">
|
||||
<option value="" data-i18n="stream_sessions.filter.all_statuses">All statuses</option>
|
||||
<option value="created" data-i18n="stream_sessions.status.created">Created</option>
|
||||
<option value="running" data-i18n="stream_sessions.status.running">Running</option>
|
||||
<option value="failed" data-i18n="stream_sessions.status.failed">Failed</option>
|
||||
<option value="stopped" data-i18n="stream_sessions.status.stopped">Stopped</option>
|
||||
<option value="expired" data-i18n="stream_sessions.status.expired">Expired</option>
|
||||
</select>
|
||||
<select class="page-select" id="stream-sessions-mode">
|
||||
<option value="" data-i18n="stream_sessions.filter.all_modes">All modes</option>
|
||||
<option value="window">window</option>
|
||||
<option value="session">session</option>
|
||||
<option value="async_job">async_job</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="list-toolbar-group">
|
||||
<span class="section-card-subtitle" id="stream-sessions-summary"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="resource-list" id="stream-sessions-list"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="js/nav.js"></script>
|
||||
<script src="js/stream-sessions.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -235,6 +235,8 @@
|
||||
|
||||
</div><!-- /wizard-shell -->
|
||||
|
||||
<script src="../../js/streaming-form.js"></script>
|
||||
<script src="../../js/stream-test-run.js"></script>
|
||||
<script src="../../js/wizard.js"></script>
|
||||
<script src="../../js/nav.js"></script>
|
||||
|
||||
|
||||
@@ -87,6 +87,164 @@ tls:
|
||||
</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">
|
||||
<svg width="13" height="13" viewBox="0 0 16 16" fill="none" stroke="var(--text-secondary)" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M3 4h10M3 8h10M3 12h6"/>
|
||||
<circle cx="12" cy="12" r="1.5"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<div class="config-card-title" data-i18n="wizard.streaming.title">Streaming execution</div>
|
||||
<div class="config-card-subtitle" data-i18n="wizard.streaming.subtitle">Bounded stream, session and async job settings for MCP tool families.</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="config-card-body" style="gap: 16px;">
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label class="form-label" data-i18n="wizard.streaming.execution_mode">Execution mode</label>
|
||||
<select id="streaming-mode" class="form-select">
|
||||
<option value="unary">Unary</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label" data-i18n="wizard.streaming.transport_behavior">Transport behavior</label>
|
||||
<select id="streaming-transport-behavior" class="form-select">
|
||||
<option value="request_response">Request / response</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label" data-i18n="wizard.streaming.aggregation_mode">Aggregation mode</label>
|
||||
<select id="streaming-aggregation-mode" class="form-select">
|
||||
<option value="raw_items" data-i18n="wizard.streaming.aggregation.raw_items">Raw items</option>
|
||||
<option value="summary_only" data-i18n="wizard.streaming.aggregation.summary_only">Summary only</option>
|
||||
<option value="summary_plus_samples" data-i18n="wizard.streaming.aggregation.summary_plus_samples">Summary + samples</option>
|
||||
<option value="stats" data-i18n="wizard.streaming.aggregation.stats">Stats</option>
|
||||
<option value="latest_state" data-i18n="wizard.streaming.aggregation.latest_state">Latest state</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="streaming-mode-help" class="form-hint" data-i18n="wizard.streaming.help.unary">Unary keeps the existing request-response model. Other modes create bounded stream-aware MCP tools.</div>
|
||||
|
||||
<div id="streaming-limits-grid" class="form-row">
|
||||
<div class="form-group">
|
||||
<label class="form-label" data-i18n="wizard.streaming.window_duration">Window duration (ms)</label>
|
||||
<input id="streaming-window-duration-ms" class="form-input" type="number" min="1" step="1" placeholder="5000">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label" data-i18n="wizard.streaming.poll_interval">Poll interval (ms)</label>
|
||||
<input id="streaming-poll-interval-ms" class="form-input" type="number" min="1" step="1" placeholder="2000">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label" data-i18n="wizard.streaming.upstream_timeout">Upstream timeout (ms)</label>
|
||||
<input id="streaming-upstream-timeout-ms" class="form-input" type="number" min="1" step="1" placeholder="10000">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label class="form-label" data-i18n="wizard.streaming.idle_timeout">Idle timeout (ms)</label>
|
||||
<input id="streaming-idle-timeout-ms" class="form-input" type="number" min="1" step="1" placeholder="30000">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label" data-i18n="wizard.streaming.session_lifetime">Session lifetime (ms)</label>
|
||||
<input id="streaming-session-lifetime-ms" class="form-input" type="number" min="1" step="1" placeholder="300000">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label" data-i18n="wizard.streaming.max_items">Max items</label>
|
||||
<input id="streaming-max-items" class="form-input" type="number" min="1" step="1" placeholder="100">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label class="form-label" data-i18n="wizard.streaming.max_bytes">Max bytes</label>
|
||||
<input id="streaming-max-bytes" class="form-input" type="number" min="1" step="1" placeholder="65536">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label" data-i18n="wizard.streaming.max_field_length">Max field length</label>
|
||||
<input id="streaming-max-field-length" class="form-input" type="number" min="1" step="1" placeholder="512">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label" data-i18n="wizard.streaming.sampling_rate">Sampling rate</label>
|
||||
<input id="streaming-sampling-rate" class="form-input" type="number" min="0" max="1" step="0.1" placeholder="1.0">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label class="form-label" data-i18n="wizard.streaming.items_path">Items path</label>
|
||||
<input id="streaming-items-path" class="form-input input-mono" type="text" placeholder="$.items">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label" data-i18n="wizard.streaming.summary_path">Summary path</label>
|
||||
<input id="streaming-summary-path" class="form-input input-mono" type="text" placeholder="$.summary">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label" data-i18n="wizard.streaming.done_path">Done path</label>
|
||||
<input id="streaming-done-path" class="form-input input-mono" type="text" placeholder="$.done">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label class="form-label" data-i18n="wizard.streaming.cursor_path">Cursor path</label>
|
||||
<input id="streaming-cursor-path" class="form-input input-mono" type="text" placeholder="$.cursor">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label" data-i18n="wizard.streaming.status_path">Status path</label>
|
||||
<input id="streaming-status-path" class="form-input input-mono" type="text" placeholder="$.status">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label" data-i18n="wizard.streaming.redacted_paths">Redacted paths</label>
|
||||
<textarea id="streaming-redacted-paths" class="form-textarea code-textarea" rows="4" spellcheck="false" placeholder="$.items[*].token $.summary.secret"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<label class="checkbox-pill"><input id="streaming-truncate-item-fields" type="checkbox"> <span data-i18n="wizard.streaming.truncate_item_fields">Truncate item fields</span></label>
|
||||
<label class="checkbox-pill"><input id="streaming-drop-duplicates" type="checkbox"> <span data-i18n="wizard.streaming.drop_duplicates">Drop duplicates</span></label>
|
||||
</div>
|
||||
|
||||
<div id="streaming-tool-family-block" style="display:none;">
|
||||
<div class="section-divider" style="margin: 8px 0 16px;">
|
||||
<span class="section-divider-label" data-i18n="wizard.streaming.tool_family">Tool family</span>
|
||||
<div class="section-divider-line"></div>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label class="form-label" data-i18n="wizard.streaming.start_tool_name">Start tool name</label>
|
||||
<input id="streaming-start-tool-name" class="form-input input-mono" type="text" placeholder="logs_follow_start">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label" data-i18n="wizard.streaming.poll_tool_name">Poll tool name</label>
|
||||
<input id="streaming-poll-tool-name" class="form-input input-mono" type="text" placeholder="logs_follow_poll">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label" data-i18n="wizard.streaming.stop_tool_name">Stop tool name</label>
|
||||
<input id="streaming-stop-tool-name" class="form-input input-mono" type="text" placeholder="logs_follow_stop">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-row" id="streaming-async-tool-family-row" style="display:none;">
|
||||
<div class="form-group">
|
||||
<label class="form-label" data-i18n="wizard.streaming.status_tool_name">Status tool name</label>
|
||||
<input id="streaming-status-tool-name" class="form-input input-mono" type="text" placeholder="deploy_status">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label" data-i18n="wizard.streaming.result_tool_name">Result tool name</label>
|
||||
<input id="streaming-result-tool-name" class="form-input input-mono" type="text" placeholder="deploy_result">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label" data-i18n="wizard.streaming.cancel_tool_name">Cancel tool name</label>
|
||||
<input id="streaming-cancel-tool-name" class="form-input input-mono" type="text" placeholder="deploy_cancel">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section-divider" style="margin-bottom: 16px;">
|
||||
<span class="section-divider-label" data-i18n="wizard.step5.live_title">Live validation and publishing</span>
|
||||
<div class="section-divider-line"></div>
|
||||
|
||||
@@ -332,6 +332,30 @@
|
||||
getUsageOverview: function(workspaceId, params) {
|
||||
return get('/workspaces/' + encodeURIComponent(workspaceId) + '/usage' + query(params));
|
||||
},
|
||||
getProtocolCapabilities: function(workspaceId) {
|
||||
return get('/workspaces/' + encodeURIComponent(workspaceId) + '/protocol-capabilities');
|
||||
},
|
||||
listStreamSessions: function(workspaceId, params) {
|
||||
return get('/workspaces/' + encodeURIComponent(workspaceId) + '/stream-sessions' + query(params));
|
||||
},
|
||||
getStreamSession: function(workspaceId, sessionId) {
|
||||
return get('/workspaces/' + encodeURIComponent(workspaceId) + '/stream-sessions/' + encodeURIComponent(sessionId));
|
||||
},
|
||||
stopStreamSession: function(workspaceId, sessionId) {
|
||||
return post('/workspaces/' + encodeURIComponent(workspaceId) + '/stream-sessions/' + encodeURIComponent(sessionId) + '/stop', {});
|
||||
},
|
||||
listAsyncJobs: function(workspaceId, params) {
|
||||
return get('/workspaces/' + encodeURIComponent(workspaceId) + '/async-jobs' + query(params));
|
||||
},
|
||||
getAsyncJob: function(workspaceId, jobId) {
|
||||
return get('/workspaces/' + encodeURIComponent(workspaceId) + '/async-jobs/' + encodeURIComponent(jobId));
|
||||
},
|
||||
cancelAsyncJob: function(workspaceId, jobId) {
|
||||
return post('/workspaces/' + encodeURIComponent(workspaceId) + '/async-jobs/' + encodeURIComponent(jobId) + '/cancel', {});
|
||||
},
|
||||
getAsyncJobResult: function(workspaceId, jobId) {
|
||||
return get('/workspaces/' + encodeURIComponent(workspaceId) + '/async-jobs/' + encodeURIComponent(jobId) + '/result');
|
||||
},
|
||||
getOperationUsage: function(workspaceId, operationId, params) {
|
||||
return get(
|
||||
'/workspaces/' + encodeURIComponent(workspaceId) + '/usage/operations/' + encodeURIComponent(operationId) + query(params)
|
||||
|
||||
@@ -0,0 +1,181 @@
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
var state = {
|
||||
items: [],
|
||||
workspaceId: null,
|
||||
loading: false,
|
||||
error: '',
|
||||
openId: null,
|
||||
};
|
||||
|
||||
var list = document.getElementById('async-jobs-list');
|
||||
var summary = document.getElementById('async-jobs-summary');
|
||||
var refreshButton = document.getElementById('async-jobs-refresh');
|
||||
var statusFilter = document.getElementById('async-jobs-status');
|
||||
|
||||
function tKey(key, vars) {
|
||||
if (!window.t) return key;
|
||||
return t(key, vars);
|
||||
}
|
||||
|
||||
function currentWorkspaceId() {
|
||||
var workspace = window.getCurrentWorkspace ? window.getCurrentWorkspace() : null;
|
||||
return workspace ? workspace.id : null;
|
||||
}
|
||||
|
||||
function formatJson(value) {
|
||||
if (value === null || value === undefined) return '';
|
||||
if (typeof value === 'string') return value;
|
||||
return JSON.stringify(value, null, 2);
|
||||
}
|
||||
|
||||
function formatDateTime(value) {
|
||||
if (!value) return '—';
|
||||
return new Date(value).toLocaleString();
|
||||
}
|
||||
|
||||
function renderEmpty(title, body) {
|
||||
list.innerHTML = '<div class="empty-state"><div class="empty-state-title">' + title + '</div><div class="empty-state-text">' + body + '</div></div>';
|
||||
}
|
||||
|
||||
function render() {
|
||||
summary.textContent = tKey('async_jobs.summary', { count: state.items.length });
|
||||
|
||||
if (state.loading && state.items.length === 0) {
|
||||
renderEmpty(tKey('async_jobs.loading.title'), tKey('async_jobs.loading.body'));
|
||||
return;
|
||||
}
|
||||
|
||||
if (state.error) {
|
||||
renderEmpty(tKey('async_jobs.error.title'), state.error);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!state.items.length) {
|
||||
renderEmpty(tKey('async_jobs.empty.title'), tKey('async_jobs.empty.body'));
|
||||
return;
|
||||
}
|
||||
|
||||
list.innerHTML = state.items.map(function(item) {
|
||||
var open = state.openId === item.id;
|
||||
var statusLabel = tKey('async_jobs.status.' + item.status);
|
||||
return [
|
||||
'<div class="resource-card" data-job-id="' + item.id + '">',
|
||||
' <div class="resource-card-header">',
|
||||
' <div>',
|
||||
' <div class="resource-card-title">' + item.id + '</div>',
|
||||
' <div class="resource-card-subtitle">' + tKey('async_jobs.operation', { operation: item.operation_id }) + '</div>',
|
||||
' <div class="resource-pill-row">',
|
||||
' <span class="resource-status-pill ' + String(item.status || '').toLowerCase() + '">' + statusLabel + '</span>',
|
||||
' </div>',
|
||||
' </div>',
|
||||
' <div class="resource-card-actions">',
|
||||
item.status === 'created' || item.status === 'running'
|
||||
? ' <button class="btn-secondary" data-action="cancel" data-job-id="' + item.id + '">' + tKey('async_jobs.cancel') + '</button>'
|
||||
: '',
|
||||
' <button class="btn-secondary" data-action="toggle" data-job-id="' + item.id + '">' + (open ? tKey('async_jobs.hide_details') : tKey('async_jobs.show_details')) + '</button>',
|
||||
' </div>',
|
||||
' </div>',
|
||||
' <div class="resource-meta-grid">',
|
||||
' <div class="resource-meta-item"><div class="resource-meta-label">' + tKey('async_jobs.meta.created') + '</div><div class="resource-meta-value">' + formatDateTime(item.created_at) + '</div></div>',
|
||||
' <div class="resource-meta-item"><div class="resource-meta-label">' + tKey('async_jobs.meta.updated') + '</div><div class="resource-meta-value">' + formatDateTime(item.updated_at) + '</div></div>',
|
||||
' <div class="resource-meta-item"><div class="resource-meta-label">' + tKey('async_jobs.meta.finished') + '</div><div class="resource-meta-value">' + formatDateTime(item.finished_at) + '</div></div>',
|
||||
' <div class="resource-meta-item"><div class="resource-meta-label">' + tKey('async_jobs.meta.expires') + '</div><div class="resource-meta-value">' + formatDateTime(item.expires_at) + '</div></div>',
|
||||
' </div>',
|
||||
open
|
||||
? ' <div class="resource-detail-block"><div class="resource-detail-title">' + tKey('async_jobs.progress') + '</div><pre class="resource-detail-pre">' + formatJson(item.progress) + '</pre></div>'
|
||||
: '',
|
||||
open
|
||||
? ' <div class="resource-detail-block"><div class="resource-detail-title">' + tKey('async_jobs.error_preview') + '</div><pre class="resource-detail-pre">' + formatJson(item.error) + '</pre></div>'
|
||||
: '',
|
||||
open && (item.status === 'completed' || item.status === 'failed' || item.status === 'cancelled' || item.status === 'expired')
|
||||
? ' <div class="resource-detail-block"><div class="resource-detail-title">' + tKey('async_jobs.result') + '</div><pre class="resource-detail-pre" data-result-for="' + item.id + '">' + tKey('async_jobs.result_loading') + '</pre></div>'
|
||||
: '',
|
||||
'</div>'
|
||||
].join('');
|
||||
}).join('');
|
||||
|
||||
list.querySelectorAll('[data-action="toggle"]').forEach(function(button) {
|
||||
button.addEventListener('click', function () {
|
||||
var jobId = this.getAttribute('data-job-id');
|
||||
state.openId = state.openId === jobId ? null : jobId;
|
||||
if (state.openId) {
|
||||
void loadDetail(jobId);
|
||||
} else {
|
||||
render();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
list.querySelectorAll('[data-action="cancel"]').forEach(function(button) {
|
||||
button.addEventListener('click', async function () {
|
||||
try {
|
||||
var jobId = this.getAttribute('data-job-id');
|
||||
await window.CrankApi.cancelAsyncJob(state.workspaceId, jobId);
|
||||
if (window.CrankUi) {
|
||||
window.CrankUi.success(tKey('async_jobs.toast.cancel.body'), tKey('async_jobs.toast.cancel.title'));
|
||||
}
|
||||
await load();
|
||||
} catch (error) {
|
||||
if (window.CrankUi) {
|
||||
window.CrankUi.error(error.message || tKey('async_jobs.toast.cancel_error.body'), tKey('async_jobs.toast.cancel_error.title'));
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function loadResult(jobId) {
|
||||
try {
|
||||
var result = await window.CrankApi.getAsyncJobResult(state.workspaceId, jobId);
|
||||
var node = document.querySelector('[data-result-for="' + jobId + '"]');
|
||||
if (node) node.textContent = formatJson(result);
|
||||
} catch (error) {
|
||||
var failedNode = document.querySelector('[data-result-for="' + jobId + '"]');
|
||||
if (failedNode) failedNode.textContent = error.message || tKey('async_jobs.result_error');
|
||||
}
|
||||
}
|
||||
|
||||
async function loadDetail(jobId) {
|
||||
var detail = await window.CrankApi.getAsyncJob(state.workspaceId, jobId);
|
||||
state.items = state.items.map(function(item) {
|
||||
return item.id === jobId ? detail : item;
|
||||
});
|
||||
render();
|
||||
if (detail.status === 'completed' || detail.status === 'failed' || detail.status === 'cancelled' || detail.status === 'expired') {
|
||||
await loadResult(jobId);
|
||||
}
|
||||
}
|
||||
|
||||
async function load() {
|
||||
state.workspaceId = currentWorkspaceId();
|
||||
if (!state.workspaceId) {
|
||||
state.items = [];
|
||||
state.error = tKey('async_jobs.error.workspace');
|
||||
render();
|
||||
return;
|
||||
}
|
||||
|
||||
state.loading = true;
|
||||
state.error = '';
|
||||
render();
|
||||
|
||||
try {
|
||||
var response = await window.CrankApi.listAsyncJobs(state.workspaceId, {
|
||||
status: statusFilter.value || null,
|
||||
page: 1,
|
||||
page_size: 50,
|
||||
});
|
||||
state.items = response.items || [];
|
||||
} catch (error) {
|
||||
state.error = error.message || tKey('async_jobs.error.load');
|
||||
} finally {
|
||||
state.loading = false;
|
||||
render();
|
||||
}
|
||||
}
|
||||
|
||||
refreshButton.addEventListener('click', load);
|
||||
statusFilter.addEventListener('change', load);
|
||||
document.addEventListener('workspace:changed', load);
|
||||
void load();
|
||||
});
|
||||
@@ -8,6 +8,8 @@
|
||||
apiKeys: '/api-keys',
|
||||
logs: '/logs',
|
||||
usage: '/usage',
|
||||
streamSessions: '/stream-sessions',
|
||||
asyncJobs: '/async-jobs',
|
||||
settings: '/settings',
|
||||
workspaceSetup: '/workspace-setup',
|
||||
wizard: '/wizard/'
|
||||
|
||||
@@ -1569,6 +1569,240 @@ var TRANSLATIONS = {
|
||||
}
|
||||
};
|
||||
|
||||
Object.assign(TRANSLATIONS.en, {
|
||||
'wizard.streaming.title': 'Streaming execution',
|
||||
'wizard.streaming.subtitle': 'Bounded stream, session and async job settings for MCP tool families.',
|
||||
'wizard.streaming.execution_mode': 'Execution mode',
|
||||
'wizard.streaming.transport_behavior': 'Transport behavior',
|
||||
'wizard.streaming.aggregation_mode': 'Aggregation mode',
|
||||
'wizard.streaming.window_duration': 'Window duration (ms)',
|
||||
'wizard.streaming.poll_interval': 'Poll interval (ms)',
|
||||
'wizard.streaming.upstream_timeout': 'Upstream timeout (ms)',
|
||||
'wizard.streaming.idle_timeout': 'Idle timeout (ms)',
|
||||
'wizard.streaming.session_lifetime': 'Session lifetime (ms)',
|
||||
'wizard.streaming.max_items': 'Max items',
|
||||
'wizard.streaming.max_bytes': 'Max bytes',
|
||||
'wizard.streaming.max_field_length': 'Max field length',
|
||||
'wizard.streaming.sampling_rate': 'Sampling rate',
|
||||
'wizard.streaming.items_path': 'Items path',
|
||||
'wizard.streaming.summary_path': 'Summary path',
|
||||
'wizard.streaming.done_path': 'Done path',
|
||||
'wizard.streaming.cursor_path': 'Cursor path',
|
||||
'wizard.streaming.status_path': 'Status path',
|
||||
'wizard.streaming.redacted_paths': 'Redacted paths',
|
||||
'wizard.streaming.truncate_item_fields': 'Truncate item fields',
|
||||
'wizard.streaming.drop_duplicates': 'Drop duplicates',
|
||||
'wizard.streaming.tool_family': 'Tool family',
|
||||
'wizard.streaming.start_tool_name': 'Start tool name',
|
||||
'wizard.streaming.poll_tool_name': 'Poll tool name',
|
||||
'wizard.streaming.stop_tool_name': 'Stop tool name',
|
||||
'wizard.streaming.status_tool_name': 'Status tool name',
|
||||
'wizard.streaming.result_tool_name': 'Result tool name',
|
||||
'wizard.streaming.cancel_tool_name': 'Cancel tool name',
|
||||
'wizard.streaming.mode.unary': 'Unary',
|
||||
'wizard.streaming.mode.window': 'Window',
|
||||
'wizard.streaming.mode.session': 'Session',
|
||||
'wizard.streaming.mode.async_job': 'Async job',
|
||||
'wizard.streaming.transport.request_response': 'Request / response',
|
||||
'wizard.streaming.transport.server_stream': 'Server stream',
|
||||
'wizard.streaming.transport.stateful_session': 'Stateful session',
|
||||
'wizard.streaming.transport.deferred_result': 'Deferred result',
|
||||
'wizard.streaming.aggregation.raw_items': 'Raw items',
|
||||
'wizard.streaming.aggregation.summary_only': 'Summary only',
|
||||
'wizard.streaming.aggregation.summary_plus_samples': 'Summary + samples',
|
||||
'wizard.streaming.aggregation.stats': 'Stats',
|
||||
'wizard.streaming.aggregation.latest_state': 'Latest state',
|
||||
'wizard.streaming.help.unary': 'Unary keeps the existing request-response model. Other modes create bounded stream-aware MCP tools.',
|
||||
'wizard.streaming.help.window': 'Window mode collects a bounded slice of data and returns a compact result.',
|
||||
'wizard.streaming.help.session': 'Session mode publishes start/poll/stop tool families backed by persisted stream sessions.',
|
||||
'wizard.streaming.help.async_job': 'Async job mode publishes start/status/result/cancel tools and stores runtime job state.',
|
||||
|
||||
'stream_sessions.title': 'Stream sessions',
|
||||
'stream_sessions.subtitle': 'Inspect stateful streaming sessions created by generated MCP tool families.',
|
||||
'stream_sessions.refresh': 'Refresh',
|
||||
'stream_sessions.summary': '{count} sessions',
|
||||
'stream_sessions.filter.all_statuses': 'All statuses',
|
||||
'stream_sessions.filter.all_modes': 'All modes',
|
||||
'stream_sessions.loading.title': 'Loading stream sessions…',
|
||||
'stream_sessions.loading.body': 'Fetching persisted session state for the current workspace.',
|
||||
'stream_sessions.error.title': 'Unable to load stream sessions',
|
||||
'stream_sessions.error.workspace': 'Workspace is not selected',
|
||||
'stream_sessions.error.load': 'Failed to load stream sessions',
|
||||
'stream_sessions.empty.title': 'No stream sessions found',
|
||||
'stream_sessions.empty.body': 'Session-mode MCP tools will create entries here after start/poll/stop flows.',
|
||||
'stream_sessions.operation': 'Operation: {operation}',
|
||||
'stream_sessions.meta.created': 'Created',
|
||||
'stream_sessions.meta.last_poll': 'Last poll',
|
||||
'stream_sessions.meta.expires': 'Expires',
|
||||
'stream_sessions.meta.agent': 'Agent',
|
||||
'stream_sessions.cursor': 'Cursor',
|
||||
'stream_sessions.stop': 'Stop session',
|
||||
'stream_sessions.show_details': 'Show details',
|
||||
'stream_sessions.hide_details': 'Hide details',
|
||||
'stream_sessions.toast.stop.title': 'Session stopped',
|
||||
'stream_sessions.toast.stop.body': 'The stream session was stopped.',
|
||||
'stream_sessions.toast.stop_error.title': 'Stop failed',
|
||||
'stream_sessions.toast.stop_error.body': 'Failed to stop stream session',
|
||||
'stream_sessions.status.created': 'Created',
|
||||
'stream_sessions.status.running': 'Running',
|
||||
'stream_sessions.status.failed': 'Failed',
|
||||
'stream_sessions.status.stopped': 'Stopped',
|
||||
'stream_sessions.status.expired': 'Expired',
|
||||
|
||||
'async_jobs.title': 'Async jobs',
|
||||
'async_jobs.subtitle': 'Inspect deferred jobs started by streaming MCP tool families.',
|
||||
'async_jobs.refresh': 'Refresh',
|
||||
'async_jobs.summary': '{count} jobs',
|
||||
'async_jobs.filter.all_statuses': 'All statuses',
|
||||
'async_jobs.loading.title': 'Loading async jobs…',
|
||||
'async_jobs.loading.body': 'Fetching persisted job state for the current workspace.',
|
||||
'async_jobs.error.title': 'Unable to load async jobs',
|
||||
'async_jobs.error.workspace': 'Workspace is not selected',
|
||||
'async_jobs.error.load': 'Failed to load async jobs',
|
||||
'async_jobs.empty.title': 'No async jobs found',
|
||||
'async_jobs.empty.body': 'Async job MCP tools will create entries here after start/status/result flows.',
|
||||
'async_jobs.operation': 'Operation: {operation}',
|
||||
'async_jobs.meta.created': 'Created',
|
||||
'async_jobs.meta.updated': 'Updated',
|
||||
'async_jobs.meta.finished': 'Finished',
|
||||
'async_jobs.meta.expires': 'Expires',
|
||||
'async_jobs.progress': 'Progress payload',
|
||||
'async_jobs.error_preview': 'Error preview',
|
||||
'async_jobs.result': 'Result payload',
|
||||
'async_jobs.result_loading': 'Loading result…',
|
||||
'async_jobs.result_error': 'Failed to load result',
|
||||
'async_jobs.cancel': 'Cancel job',
|
||||
'async_jobs.show_details': 'Show details',
|
||||
'async_jobs.hide_details': 'Hide details',
|
||||
'async_jobs.toast.cancel.title': 'Job cancelled',
|
||||
'async_jobs.toast.cancel.body': 'The async job was cancelled.',
|
||||
'async_jobs.toast.cancel_error.title': 'Cancel failed',
|
||||
'async_jobs.toast.cancel_error.body': 'Failed to cancel async job',
|
||||
'async_jobs.status.created': 'Created',
|
||||
'async_jobs.status.running': 'Running',
|
||||
'async_jobs.status.completed': 'Completed',
|
||||
'async_jobs.status.failed': 'Failed',
|
||||
'async_jobs.status.cancelled': 'Cancelled',
|
||||
'async_jobs.status.expired': 'Expired',
|
||||
});
|
||||
|
||||
Object.assign(TRANSLATIONS.ru, {
|
||||
'wizard.streaming.title': 'Потоковое выполнение',
|
||||
'wizard.streaming.subtitle': 'Ограниченные настройки стрима, сессий и async job для семейств MCP-инструментов.',
|
||||
'wizard.streaming.execution_mode': 'Режим выполнения',
|
||||
'wizard.streaming.transport_behavior': 'Поведение транспорта',
|
||||
'wizard.streaming.aggregation_mode': 'Режим агрегации',
|
||||
'wizard.streaming.window_duration': 'Длительность окна (мс)',
|
||||
'wizard.streaming.poll_interval': 'Интервал poll (мс)',
|
||||
'wizard.streaming.upstream_timeout': 'Таймаут upstream (мс)',
|
||||
'wizard.streaming.idle_timeout': 'Таймаут простоя (мс)',
|
||||
'wizard.streaming.session_lifetime': 'Время жизни сессии (мс)',
|
||||
'wizard.streaming.max_items': 'Максимум элементов',
|
||||
'wizard.streaming.max_bytes': 'Максимум байт',
|
||||
'wizard.streaming.max_field_length': 'Максимальная длина поля',
|
||||
'wizard.streaming.sampling_rate': 'Частота выборки',
|
||||
'wizard.streaming.items_path': 'Путь к items',
|
||||
'wizard.streaming.summary_path': 'Путь к summary',
|
||||
'wizard.streaming.done_path': 'Путь к done',
|
||||
'wizard.streaming.cursor_path': 'Путь к cursor',
|
||||
'wizard.streaming.status_path': 'Путь к status',
|
||||
'wizard.streaming.redacted_paths': 'Скрываемые пути',
|
||||
'wizard.streaming.truncate_item_fields': 'Обрезать поля элементов',
|
||||
'wizard.streaming.drop_duplicates': 'Удалять дубликаты',
|
||||
'wizard.streaming.tool_family': 'Семейство инструментов',
|
||||
'wizard.streaming.start_tool_name': 'Имя start-инструмента',
|
||||
'wizard.streaming.poll_tool_name': 'Имя poll-инструмента',
|
||||
'wizard.streaming.stop_tool_name': 'Имя stop-инструмента',
|
||||
'wizard.streaming.status_tool_name': 'Имя status-инструмента',
|
||||
'wizard.streaming.result_tool_name': 'Имя result-инструмента',
|
||||
'wizard.streaming.cancel_tool_name': 'Имя cancel-инструмента',
|
||||
'wizard.streaming.mode.unary': 'Unary',
|
||||
'wizard.streaming.mode.window': 'Window',
|
||||
'wizard.streaming.mode.session': 'Session',
|
||||
'wizard.streaming.mode.async_job': 'Async job',
|
||||
'wizard.streaming.transport.request_response': 'Запрос / ответ',
|
||||
'wizard.streaming.transport.server_stream': 'Серверный стрим',
|
||||
'wizard.streaming.transport.stateful_session': 'Состояние сессии',
|
||||
'wizard.streaming.transport.deferred_result': 'Отложенный результат',
|
||||
'wizard.streaming.aggregation.raw_items': 'Сырые элементы',
|
||||
'wizard.streaming.aggregation.summary_only': 'Только summary',
|
||||
'wizard.streaming.aggregation.summary_plus_samples': 'Summary + примеры',
|
||||
'wizard.streaming.aggregation.stats': 'Статистика',
|
||||
'wizard.streaming.aggregation.latest_state': 'Последнее состояние',
|
||||
'wizard.streaming.help.unary': 'Unary сохраняет текущую модель запрос-ответ. Остальные режимы создают ограниченные потоковые MCP-инструменты.',
|
||||
'wizard.streaming.help.window': 'Режим window собирает ограниченный срез данных и возвращает компактный результат.',
|
||||
'wizard.streaming.help.session': 'Режим session публикует семейство start/poll/stop и опирается на сохраненные stream sessions.',
|
||||
'wizard.streaming.help.async_job': 'Режим async job публикует start/status/result/cancel и хранит состояние фоновой задачи.',
|
||||
|
||||
'stream_sessions.title': 'Потоковые сессии',
|
||||
'stream_sessions.subtitle': 'Просмотр stateful streaming-сессий, созданных сгенерированными семействами MCP-инструментов.',
|
||||
'stream_sessions.refresh': 'Обновить',
|
||||
'stream_sessions.summary': '{count} сессий',
|
||||
'stream_sessions.filter.all_statuses': 'Все статусы',
|
||||
'stream_sessions.filter.all_modes': 'Все режимы',
|
||||
'stream_sessions.loading.title': 'Загрузка потоковых сессий…',
|
||||
'stream_sessions.loading.body': 'Получаем сохраненное состояние сессий для текущего workspace.',
|
||||
'stream_sessions.error.title': 'Не удалось загрузить потоковые сессии',
|
||||
'stream_sessions.error.workspace': 'Workspace не выбран',
|
||||
'stream_sessions.error.load': 'Не удалось загрузить потоковые сессии',
|
||||
'stream_sessions.empty.title': 'Потоковых сессий пока нет',
|
||||
'stream_sessions.empty.body': 'Сессии появятся здесь после вызовов start/poll/stop для session-mode инструментов.',
|
||||
'stream_sessions.operation': 'Операция: {operation}',
|
||||
'stream_sessions.meta.created': 'Создана',
|
||||
'stream_sessions.meta.last_poll': 'Последний poll',
|
||||
'stream_sessions.meta.expires': 'Истекает',
|
||||
'stream_sessions.meta.agent': 'Агент',
|
||||
'stream_sessions.cursor': 'Cursor',
|
||||
'stream_sessions.stop': 'Остановить сессию',
|
||||
'stream_sessions.show_details': 'Показать детали',
|
||||
'stream_sessions.hide_details': 'Скрыть детали',
|
||||
'stream_sessions.toast.stop.title': 'Сессия остановлена',
|
||||
'stream_sessions.toast.stop.body': 'Потоковая сессия остановлена.',
|
||||
'stream_sessions.toast.stop_error.title': 'Не удалось остановить',
|
||||
'stream_sessions.toast.stop_error.body': 'Не удалось остановить потоковую сессию',
|
||||
'stream_sessions.status.created': 'Создана',
|
||||
'stream_sessions.status.running': 'Выполняется',
|
||||
'stream_sessions.status.failed': 'Ошибка',
|
||||
'stream_sessions.status.stopped': 'Остановлена',
|
||||
'stream_sessions.status.expired': 'Истекла',
|
||||
|
||||
'async_jobs.title': 'Асинхронные задачи',
|
||||
'async_jobs.subtitle': 'Просмотр отложенных задач, запущенных потоковыми семействами MCP-инструментов.',
|
||||
'async_jobs.refresh': 'Обновить',
|
||||
'async_jobs.summary': '{count} задач',
|
||||
'async_jobs.filter.all_statuses': 'Все статусы',
|
||||
'async_jobs.loading.title': 'Загрузка async job…',
|
||||
'async_jobs.loading.body': 'Получаем сохраненное состояние задач для текущего workspace.',
|
||||
'async_jobs.error.title': 'Не удалось загрузить async job',
|
||||
'async_jobs.error.workspace': 'Workspace не выбран',
|
||||
'async_jobs.error.load': 'Не удалось загрузить async job',
|
||||
'async_jobs.empty.title': 'Асинхронных задач пока нет',
|
||||
'async_jobs.empty.body': 'Задачи появятся здесь после вызовов start/status/result для async job инструментов.',
|
||||
'async_jobs.operation': 'Операция: {operation}',
|
||||
'async_jobs.meta.created': 'Создана',
|
||||
'async_jobs.meta.updated': 'Обновлена',
|
||||
'async_jobs.meta.finished': 'Завершена',
|
||||
'async_jobs.meta.expires': 'Истекает',
|
||||
'async_jobs.progress': 'Payload прогресса',
|
||||
'async_jobs.error_preview': 'Предпросмотр ошибки',
|
||||
'async_jobs.result': 'Payload результата',
|
||||
'async_jobs.result_loading': 'Загрузка результата…',
|
||||
'async_jobs.result_error': 'Не удалось загрузить результат',
|
||||
'async_jobs.cancel': 'Отменить задачу',
|
||||
'async_jobs.show_details': 'Показать детали',
|
||||
'async_jobs.hide_details': 'Скрыть детали',
|
||||
'async_jobs.toast.cancel.title': 'Задача отменена',
|
||||
'async_jobs.toast.cancel.body': 'Асинхронная задача отменена.',
|
||||
'async_jobs.toast.cancel_error.title': 'Не удалось отменить',
|
||||
'async_jobs.toast.cancel_error.body': 'Не удалось отменить async job',
|
||||
'async_jobs.status.created': 'Создана',
|
||||
'async_jobs.status.running': 'Выполняется',
|
||||
'async_jobs.status.completed': 'Завершена',
|
||||
'async_jobs.status.failed': 'Ошибка',
|
||||
'async_jobs.status.cancelled': 'Отменена',
|
||||
'async_jobs.status.expired': 'Истекла',
|
||||
});
|
||||
|
||||
function t(key) {
|
||||
var lang = localStorage.getItem('crank_lang') || 'en';
|
||||
var tr = TRANSLATIONS[lang] || TRANSLATIONS.en;
|
||||
|
||||
@@ -0,0 +1,166 @@
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
var state = {
|
||||
items: [],
|
||||
workspaceId: null,
|
||||
loading: false,
|
||||
error: '',
|
||||
openId: null,
|
||||
};
|
||||
|
||||
var list = document.getElementById('stream-sessions-list');
|
||||
var summary = document.getElementById('stream-sessions-summary');
|
||||
var refreshButton = document.getElementById('stream-sessions-refresh');
|
||||
var statusFilter = document.getElementById('stream-sessions-status');
|
||||
var modeFilter = document.getElementById('stream-sessions-mode');
|
||||
|
||||
function tKey(key, vars) {
|
||||
if (!window.t) return key;
|
||||
return t(key, vars);
|
||||
}
|
||||
|
||||
function currentWorkspaceId() {
|
||||
var workspace = window.getCurrentWorkspace ? window.getCurrentWorkspace() : null;
|
||||
return workspace ? workspace.id : null;
|
||||
}
|
||||
|
||||
function formatJson(value) {
|
||||
if (value === null || value === undefined) return '';
|
||||
if (typeof value === 'string') return value;
|
||||
return JSON.stringify(value, null, 2);
|
||||
}
|
||||
|
||||
function formatDateTime(value) {
|
||||
if (!value) return '—';
|
||||
return new Date(value).toLocaleString();
|
||||
}
|
||||
|
||||
function renderEmpty(title, body) {
|
||||
list.innerHTML = '<div class="empty-state"><div class="empty-state-title">' + title + '</div><div class="empty-state-text">' + body + '</div></div>';
|
||||
}
|
||||
|
||||
function render() {
|
||||
summary.textContent = tKey('stream_sessions.summary', { count: state.items.length });
|
||||
|
||||
if (state.loading && state.items.length === 0) {
|
||||
renderEmpty(tKey('stream_sessions.loading.title'), tKey('stream_sessions.loading.body'));
|
||||
return;
|
||||
}
|
||||
|
||||
if (state.error) {
|
||||
renderEmpty(tKey('stream_sessions.error.title'), state.error);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!state.items.length) {
|
||||
renderEmpty(tKey('stream_sessions.empty.title'), tKey('stream_sessions.empty.body'));
|
||||
return;
|
||||
}
|
||||
|
||||
list.innerHTML = state.items.map(function(item) {
|
||||
var open = state.openId === item.id;
|
||||
var statusLabel = tKey('stream_sessions.status.' + item.status);
|
||||
return [
|
||||
'<div class="resource-card" data-session-id="' + item.id + '">',
|
||||
' <div class="resource-card-header">',
|
||||
' <div>',
|
||||
' <div class="resource-card-title">' + item.id + '</div>',
|
||||
' <div class="resource-card-subtitle">' + tKey('stream_sessions.operation', { operation: item.operation_id }) + '</div>',
|
||||
' <div class="resource-pill-row">',
|
||||
' <span class="resource-status-pill ' + String(item.status || '').toLowerCase() + '">' + statusLabel + '</span>',
|
||||
' <span class="resource-status-pill">' + item.mode + '</span>',
|
||||
' <span class="resource-status-pill">' + item.protocol + '</span>',
|
||||
' </div>',
|
||||
' </div>',
|
||||
' <div class="resource-card-actions">',
|
||||
item.status === 'created' || item.status === 'running'
|
||||
? ' <button class="btn-secondary" data-action="stop" data-session-id="' + item.id + '">' + tKey('stream_sessions.stop') + '</button>'
|
||||
: '',
|
||||
' <button class="btn-secondary" data-action="toggle" data-session-id="' + item.id + '">' + (open ? tKey('stream_sessions.hide_details') : tKey('stream_sessions.show_details')) + '</button>',
|
||||
' </div>',
|
||||
' </div>',
|
||||
' <div class="resource-meta-grid">',
|
||||
' <div class="resource-meta-item"><div class="resource-meta-label">' + tKey('stream_sessions.meta.created') + '</div><div class="resource-meta-value">' + formatDateTime(item.created_at) + '</div></div>',
|
||||
' <div class="resource-meta-item"><div class="resource-meta-label">' + tKey('stream_sessions.meta.last_poll') + '</div><div class="resource-meta-value">' + formatDateTime(item.last_poll_at) + '</div></div>',
|
||||
' <div class="resource-meta-item"><div class="resource-meta-label">' + tKey('stream_sessions.meta.expires') + '</div><div class="resource-meta-value">' + formatDateTime(item.expires_at) + '</div></div>',
|
||||
' <div class="resource-meta-item"><div class="resource-meta-label">' + tKey('stream_sessions.meta.agent') + '</div><div class="resource-meta-value">' + (item.agent_id || '—') + '</div></div>',
|
||||
' </div>',
|
||||
open
|
||||
? ' <div class="resource-detail-block"><div class="resource-detail-title">' + tKey('stream_sessions.cursor') + '</div><pre class="resource-detail-pre">' + formatJson(item.cursor) + '</pre></div>'
|
||||
: '',
|
||||
'</div>'
|
||||
].join('');
|
||||
}).join('');
|
||||
|
||||
list.querySelectorAll('[data-action="toggle"]').forEach(function(button) {
|
||||
button.addEventListener('click', function () {
|
||||
var sessionId = this.getAttribute('data-session-id');
|
||||
state.openId = state.openId === sessionId ? null : sessionId;
|
||||
if (state.openId) {
|
||||
void loadDetail(sessionId);
|
||||
} else {
|
||||
render();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
list.querySelectorAll('[data-action="stop"]').forEach(function(button) {
|
||||
button.addEventListener('click', async function () {
|
||||
try {
|
||||
var sessionId = this.getAttribute('data-session-id');
|
||||
await window.CrankApi.stopStreamSession(state.workspaceId, sessionId);
|
||||
if (window.CrankUi) {
|
||||
window.CrankUi.success(tKey('stream_sessions.toast.stop.body'), tKey('stream_sessions.toast.stop.title'));
|
||||
}
|
||||
await load();
|
||||
} catch (error) {
|
||||
if (window.CrankUi) {
|
||||
window.CrankUi.error(error.message || tKey('stream_sessions.toast.stop_error.body'), tKey('stream_sessions.toast.stop_error.title'));
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function loadDetail(sessionId) {
|
||||
var detail = await window.CrankApi.getStreamSession(state.workspaceId, sessionId);
|
||||
state.items = state.items.map(function(item) {
|
||||
return item.id === sessionId ? detail : item;
|
||||
});
|
||||
render();
|
||||
}
|
||||
|
||||
async function load() {
|
||||
state.workspaceId = currentWorkspaceId();
|
||||
if (!state.workspaceId) {
|
||||
state.items = [];
|
||||
state.error = tKey('stream_sessions.error.workspace');
|
||||
render();
|
||||
return;
|
||||
}
|
||||
|
||||
state.loading = true;
|
||||
state.error = '';
|
||||
render();
|
||||
|
||||
try {
|
||||
var response = await window.CrankApi.listStreamSessions(state.workspaceId, {
|
||||
status: statusFilter.value || null,
|
||||
mode: modeFilter.value || null,
|
||||
page: 1,
|
||||
page_size: 50,
|
||||
});
|
||||
state.items = response.items || [];
|
||||
} catch (error) {
|
||||
state.error = error.message || tKey('stream_sessions.error.load');
|
||||
} finally {
|
||||
state.loading = false;
|
||||
render();
|
||||
}
|
||||
}
|
||||
|
||||
refreshButton.addEventListener('click', load);
|
||||
statusFilter.addEventListener('change', load);
|
||||
modeFilter.addEventListener('change', load);
|
||||
document.addEventListener('workspace:changed', load);
|
||||
void load();
|
||||
});
|
||||
@@ -0,0 +1,47 @@
|
||||
(function() {
|
||||
function currentWorkspaceId() {
|
||||
var workspace = window.getCurrentWorkspace ? window.getCurrentWorkspace() : null;
|
||||
return workspace ? workspace.id : null;
|
||||
}
|
||||
|
||||
async function startWindowTest(operationId, payload) {
|
||||
if (!window.CrankApi || !operationId) {
|
||||
throw new Error('Window test requires an operation id.');
|
||||
}
|
||||
return window.CrankApi.runOperationTest(currentWorkspaceId(), operationId, payload);
|
||||
}
|
||||
|
||||
async function startSessionTest(workspaceId, operationId, payload) {
|
||||
return window.CrankApi.runOperationTest(workspaceId || currentWorkspaceId(), operationId, payload);
|
||||
}
|
||||
|
||||
async function pollSessionTest(workspaceId, sessionId) {
|
||||
return window.CrankApi.getStreamSession(workspaceId || currentWorkspaceId(), sessionId);
|
||||
}
|
||||
|
||||
async function stopSessionTest(workspaceId, sessionId) {
|
||||
return window.CrankApi.stopStreamSession(workspaceId || currentWorkspaceId(), sessionId);
|
||||
}
|
||||
|
||||
async function startAsyncJobTest(workspaceId, operationId, payload) {
|
||||
return window.CrankApi.runOperationTest(workspaceId || currentWorkspaceId(), operationId, payload);
|
||||
}
|
||||
|
||||
async function refreshAsyncJobStatus(workspaceId, jobId) {
|
||||
return window.CrankApi.getAsyncJob(workspaceId || currentWorkspaceId(), jobId);
|
||||
}
|
||||
|
||||
async function loadAsyncJobResult(workspaceId, jobId) {
|
||||
return window.CrankApi.getAsyncJobResult(workspaceId || currentWorkspaceId(), jobId);
|
||||
}
|
||||
|
||||
window.CrankStreamTestRun = {
|
||||
startWindowTest: startWindowTest,
|
||||
startSessionTest: startSessionTest,
|
||||
pollSessionTest: pollSessionTest,
|
||||
stopSessionTest: stopSessionTest,
|
||||
startAsyncJobTest: startAsyncJobTest,
|
||||
refreshAsyncJobStatus: refreshAsyncJobStatus,
|
||||
loadAsyncJobResult: loadAsyncJobResult,
|
||||
};
|
||||
}());
|
||||
@@ -0,0 +1,127 @@
|
||||
(function() {
|
||||
function text(id) {
|
||||
var element = document.getElementById(id);
|
||||
return element ? String(element.value || '').trim() : '';
|
||||
}
|
||||
|
||||
function number(id) {
|
||||
var value = text(id);
|
||||
if (!value) return null;
|
||||
var parsed = Number(value);
|
||||
return Number.isFinite(parsed) ? parsed : null;
|
||||
}
|
||||
|
||||
function lines(id) {
|
||||
var value = text(id);
|
||||
return value
|
||||
? value.split('\n').map(function(item) { return item.trim(); }).filter(Boolean)
|
||||
: [];
|
||||
}
|
||||
|
||||
function checked(id) {
|
||||
var element = document.getElementById(id);
|
||||
return !!(element && element.checked);
|
||||
}
|
||||
|
||||
function selectedMode() {
|
||||
return text('streaming-mode') || 'unary';
|
||||
}
|
||||
|
||||
function serializeStreamingConfig() {
|
||||
if (selectedMode() === 'unary') {
|
||||
return null;
|
||||
}
|
||||
|
||||
var config = {
|
||||
mode: selectedMode(),
|
||||
transport_behavior: text('streaming-transport-behavior') || 'request_response',
|
||||
aggregation_mode: text('streaming-aggregation-mode') || 'summary_plus_samples',
|
||||
window_duration_ms: number('streaming-window-duration-ms'),
|
||||
poll_interval_ms: number('streaming-poll-interval-ms'),
|
||||
upstream_timeout_ms: number('streaming-upstream-timeout-ms'),
|
||||
idle_timeout_ms: number('streaming-idle-timeout-ms'),
|
||||
max_session_lifetime_ms: number('streaming-session-lifetime-ms'),
|
||||
max_items: number('streaming-max-items'),
|
||||
max_bytes: number('streaming-max-bytes'),
|
||||
max_field_length: number('streaming-max-field-length'),
|
||||
sampling_rate: number('streaming-sampling-rate'),
|
||||
items_path: text('streaming-items-path') || null,
|
||||
summary_path: text('streaming-summary-path') || null,
|
||||
done_path: text('streaming-done-path') || null,
|
||||
cursor_path: text('streaming-cursor-path') || null,
|
||||
status_path: text('streaming-status-path') || null,
|
||||
redacted_paths: lines('streaming-redacted-paths'),
|
||||
truncate_item_fields: checked('streaming-truncate-item-fields'),
|
||||
drop_duplicates: checked('streaming-drop-duplicates'),
|
||||
tool_family: {},
|
||||
};
|
||||
|
||||
if (config.mode === 'session') {
|
||||
config.tool_family.start_tool_name = text('streaming-start-tool-name') || null;
|
||||
config.tool_family.poll_tool_name = text('streaming-poll-tool-name') || null;
|
||||
config.tool_family.stop_tool_name = text('streaming-stop-tool-name') || null;
|
||||
} else if (config.mode === 'async_job') {
|
||||
config.tool_family.start_tool_name = text('streaming-start-tool-name') || null;
|
||||
config.tool_family.status_tool_name = text('streaming-status-tool-name') || null;
|
||||
config.tool_family.result_tool_name = text('streaming-result-tool-name') || null;
|
||||
config.tool_family.cancel_tool_name = text('streaming-cancel-tool-name') || null;
|
||||
}
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
function validateStreamingConfig(capabilities) {
|
||||
var config = serializeStreamingConfig();
|
||||
if (!config) {
|
||||
return { valid: true, errors: [] };
|
||||
}
|
||||
|
||||
var errors = [];
|
||||
var capability = (capabilities || []).find(function(item) {
|
||||
return item.protocol === window.wizardProtocol;
|
||||
});
|
||||
|
||||
if (capability && Array.isArray(capability.supports_execution_modes)) {
|
||||
var supported = capability.supports_execution_modes.map(String);
|
||||
if (supported.indexOf(config.mode) === -1) {
|
||||
errors.push('Selected execution mode is not supported by the current protocol.');
|
||||
}
|
||||
}
|
||||
|
||||
if (!config.max_items || config.max_items <= 0) {
|
||||
errors.push('Max items must be a positive integer.');
|
||||
}
|
||||
|
||||
if (!config.max_bytes || config.max_bytes <= 0) {
|
||||
errors.push('Max bytes must be a positive integer.');
|
||||
}
|
||||
|
||||
if (config.mode === 'window' && (!config.window_duration_ms || config.window_duration_ms <= 0)) {
|
||||
errors.push('Window duration must be set for window mode.');
|
||||
}
|
||||
|
||||
if (config.mode === 'session' && (!config.poll_interval_ms || config.poll_interval_ms <= 0)) {
|
||||
errors.push('Poll interval must be set for session mode.');
|
||||
}
|
||||
|
||||
if (config.mode === 'async_job' && (!config.max_session_lifetime_ms || config.max_session_lifetime_ms <= 0)) {
|
||||
errors.push('Session lifetime must be set for async job mode.');
|
||||
}
|
||||
|
||||
return {
|
||||
valid: errors.length === 0,
|
||||
errors: errors,
|
||||
config: config,
|
||||
};
|
||||
}
|
||||
|
||||
window.CrankStreamingForm = {
|
||||
serializeStreamingConfig: serializeStreamingConfig,
|
||||
deserializeStreamingConfig: function(streaming) {
|
||||
if (typeof window.applyStreamingConfig === 'function') {
|
||||
window.applyStreamingConfig(streaming);
|
||||
}
|
||||
},
|
||||
validateStreamingConfig: validateStreamingConfig,
|
||||
};
|
||||
}());
|
||||
@@ -10,6 +10,7 @@ var wizardProtoUpload = null;
|
||||
var wizardDescriptorSetUpload = null;
|
||||
var wizardTestResponsePreview = null;
|
||||
var grpcDescriptorServices = [];
|
||||
var wizardProtocolCapabilities = null;
|
||||
function tKey(key) {
|
||||
return typeof t === 'function' ? t(key) : key;
|
||||
}
|
||||
@@ -17,6 +18,48 @@ function tfKey(key, vars) {
|
||||
return typeof tf === 'function' ? tf(key, vars) : key;
|
||||
}
|
||||
|
||||
function defaultProtocolCapabilities() {
|
||||
return {
|
||||
rest: {
|
||||
supports_execution_modes: ['unary', 'window', 'session', 'async_job'],
|
||||
supports_transport_behaviors: ['request_response', 'server_stream', 'deferred_result'],
|
||||
},
|
||||
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'],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function currentProtocolCapabilities() {
|
||||
var capabilities = wizardProtocolCapabilities || defaultProtocolCapabilities();
|
||||
return capabilities[wizardProtocol] || capabilities.rest;
|
||||
}
|
||||
|
||||
async function loadProtocolCapabilities() {
|
||||
if (!wizardWorkspaceId || !window.CrankApi || typeof window.CrankApi.getProtocolCapabilities !== 'function') {
|
||||
wizardProtocolCapabilities = defaultProtocolCapabilities();
|
||||
return wizardProtocolCapabilities;
|
||||
}
|
||||
|
||||
try {
|
||||
var response = await window.CrankApi.getProtocolCapabilities(wizardWorkspaceId);
|
||||
var mapped = {};
|
||||
(response.items || []).forEach(function(item) {
|
||||
mapped[item.protocol] = item;
|
||||
});
|
||||
wizardProtocolCapabilities = mapped;
|
||||
} catch (_error) {
|
||||
wizardProtocolCapabilities = defaultProtocolCapabilities();
|
||||
}
|
||||
|
||||
return wizardProtocolCapabilities;
|
||||
}
|
||||
|
||||
/* ── Dynamic step loading ── */
|
||||
function _stepFile(n) {
|
||||
return (n === 3) ? 'step3-' + wizardProtocol + '.html' : 'step' + n + '.html';
|
||||
@@ -222,10 +265,12 @@ document.addEventListener('DOMContentLoaded', async function() {
|
||||
await (window.whenWorkspacesReady ? window.whenWorkspacesReady() : Promise.resolve());
|
||||
var workspace = window.getCurrentWorkspace ? window.getCurrentWorkspace() : null;
|
||||
wizardWorkspaceId = workspace ? workspace.id : null;
|
||||
await loadProtocolCapabilities();
|
||||
|
||||
await loadWizardPanels([1, 2, 3, 4, 5]);
|
||||
bindProtocolCards();
|
||||
bindWizardLiveActions();
|
||||
bindStreamingConfigControls();
|
||||
|
||||
var params = new URLSearchParams(window.location.search);
|
||||
if (params.get('mode') === 'edit' && params.get('operationId')) {
|
||||
@@ -1092,6 +1137,12 @@ function buildMappingSet(rawValue, mode) {
|
||||
}
|
||||
|
||||
function parseExecutionConfig(text) {
|
||||
if (window.CrankStreamingForm && typeof window.CrankStreamingForm.validateStreamingConfig === 'function') {
|
||||
var validation = window.CrankStreamingForm.validateStreamingConfig([currentProtocolCapabilities()]);
|
||||
if (!validation.valid) {
|
||||
throw new Error(validation.errors[0]);
|
||||
}
|
||||
}
|
||||
var value = parseStructuredText(text);
|
||||
var retry = value.retry || value.retry_policy || null;
|
||||
var headers = value.headers && typeof value.headers === 'object' ? value.headers : {};
|
||||
@@ -1113,6 +1164,8 @@ function parseExecutionConfig(text) {
|
||||
config.protocol_options = { grpc: { use_tls: useTls } };
|
||||
}
|
||||
|
||||
config.streaming = collectStreamingConfig();
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
@@ -1236,6 +1289,264 @@ function parseHeaderMap(text) {
|
||||
return value && typeof value === 'object' ? value : {};
|
||||
}
|
||||
|
||||
function numericValueOrNull(id) {
|
||||
var value = textValue(id);
|
||||
if (!value) return null;
|
||||
var parsed = Number(value);
|
||||
return Number.isFinite(parsed) && parsed > 0 ? parsed : null;
|
||||
}
|
||||
|
||||
function stringValueOrNull(id) {
|
||||
var value = textValue(id);
|
||||
return value ? value : null;
|
||||
}
|
||||
|
||||
function textareaLines(id) {
|
||||
var value = textValue(id);
|
||||
if (!value) return [];
|
||||
return value
|
||||
.split(/\r?\n/)
|
||||
.map(function(line) { return line.trim(); })
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
function selectedStreamingMode() {
|
||||
var element = document.getElementById('streaming-mode');
|
||||
return element ? element.value : 'unary';
|
||||
}
|
||||
|
||||
function transportOptionsForMode(mode) {
|
||||
var capabilities = currentProtocolCapabilities();
|
||||
var behaviors = (capabilities.supports_transport_behaviors || []).slice();
|
||||
if (mode === 'unary') return behaviors.filter(function(value) { return value === 'request_response'; });
|
||||
if (mode === 'window') return behaviors.filter(function(value) {
|
||||
return value === 'request_response' || value === 'server_stream';
|
||||
});
|
||||
if (mode === 'session') return behaviors.filter(function(value) {
|
||||
return value === 'server_stream' || value === 'stateful_session';
|
||||
});
|
||||
if (mode === 'async_job') return behaviors.filter(function(value) {
|
||||
return value === 'deferred_result';
|
||||
});
|
||||
return behaviors;
|
||||
}
|
||||
|
||||
function modeOptionsForProtocol() {
|
||||
var capabilities = currentProtocolCapabilities();
|
||||
return (capabilities.supports_execution_modes || ['unary']).filter(function(mode) {
|
||||
return transportOptionsForMode(mode).length > 0 || mode === 'unary';
|
||||
});
|
||||
}
|
||||
|
||||
function updateStreamingTransportOptions(preferredValue) {
|
||||
var element = document.getElementById('streaming-transport-behavior');
|
||||
if (!element) return;
|
||||
var options = transportOptionsForMode(selectedStreamingMode());
|
||||
element.innerHTML = '';
|
||||
options.forEach(function(value) {
|
||||
var option = document.createElement('option');
|
||||
option.value = value;
|
||||
option.textContent = tKey('wizard.streaming.transport.' + value);
|
||||
element.appendChild(option);
|
||||
});
|
||||
if (preferredValue && options.indexOf(preferredValue) >= 0) {
|
||||
element.value = preferredValue;
|
||||
} else if (options.length) {
|
||||
element.value = options[0];
|
||||
}
|
||||
}
|
||||
|
||||
function updateStreamingModeOptions(preferredValue) {
|
||||
var element = document.getElementById('streaming-mode');
|
||||
if (!element) return;
|
||||
var options = modeOptionsForProtocol();
|
||||
element.innerHTML = '';
|
||||
options.forEach(function(value) {
|
||||
var option = document.createElement('option');
|
||||
option.value = value;
|
||||
option.textContent = tKey('wizard.streaming.mode.' + value);
|
||||
element.appendChild(option);
|
||||
});
|
||||
if (preferredValue && options.indexOf(preferredValue) >= 0) {
|
||||
element.value = preferredValue;
|
||||
} else {
|
||||
element.value = options.indexOf('unary') >= 0 ? 'unary' : (options[0] || 'unary');
|
||||
}
|
||||
updateStreamingTransportOptions();
|
||||
}
|
||||
|
||||
function ensureStreamingToolFamilyDefaults(mode) {
|
||||
var base = textValue('tool-name') || 'stream_tool';
|
||||
if (mode === 'session') {
|
||||
if (!textValue('streaming-start-tool-name')) setValue('streaming-start-tool-name', base + '_start');
|
||||
if (!textValue('streaming-poll-tool-name')) setValue('streaming-poll-tool-name', base + '_poll');
|
||||
if (!textValue('streaming-stop-tool-name')) setValue('streaming-stop-tool-name', base + '_stop');
|
||||
}
|
||||
if (mode === 'async_job') {
|
||||
if (!textValue('streaming-start-tool-name')) setValue('streaming-start-tool-name', base + '_start');
|
||||
if (!textValue('streaming-status-tool-name')) setValue('streaming-status-tool-name', base + '_status');
|
||||
if (!textValue('streaming-result-tool-name')) setValue('streaming-result-tool-name', base + '_result');
|
||||
if (!textValue('streaming-cancel-tool-name')) setValue('streaming-cancel-tool-name', base + '_cancel');
|
||||
}
|
||||
}
|
||||
|
||||
function updateStreamingConfigVisibility() {
|
||||
var mode = selectedStreamingMode();
|
||||
var card = document.getElementById('wizard-streaming-config-card');
|
||||
var help = document.getElementById('streaming-mode-help');
|
||||
var toolFamily = document.getElementById('streaming-tool-family-block');
|
||||
var asyncRow = document.getElementById('streaming-async-tool-family-row');
|
||||
if (!card) return;
|
||||
|
||||
var isUnary = mode === 'unary';
|
||||
var isWindow = mode === 'window';
|
||||
var isSession = mode === 'session';
|
||||
var isAsyncJob = mode === 'async_job';
|
||||
|
||||
[
|
||||
'streaming-window-duration-ms',
|
||||
'streaming-poll-interval-ms',
|
||||
'streaming-upstream-timeout-ms',
|
||||
'streaming-idle-timeout-ms',
|
||||
'streaming-session-lifetime-ms',
|
||||
'streaming-max-items',
|
||||
'streaming-max-bytes',
|
||||
'streaming-max-field-length',
|
||||
'streaming-sampling-rate',
|
||||
'streaming-items-path',
|
||||
'streaming-summary-path',
|
||||
'streaming-done-path',
|
||||
'streaming-cursor-path',
|
||||
'streaming-status-path',
|
||||
'streaming-redacted-paths',
|
||||
'streaming-truncate-item-fields',
|
||||
'streaming-drop-duplicates'
|
||||
].forEach(function(id) {
|
||||
var node = document.getElementById(id);
|
||||
if (!node) return;
|
||||
var group = node.closest('.form-group') || node.closest('.checkbox-pill');
|
||||
if (group) {
|
||||
group.style.display = isUnary ? 'none' : '';
|
||||
}
|
||||
});
|
||||
|
||||
if (help) {
|
||||
help.textContent = isUnary
|
||||
? tKey('wizard.streaming.help.unary')
|
||||
: (isSession
|
||||
? tKey('wizard.streaming.help.session')
|
||||
: (isAsyncJob
|
||||
? tKey('wizard.streaming.help.async_job')
|
||||
: tKey('wizard.streaming.help.window')));
|
||||
}
|
||||
|
||||
if (toolFamily) toolFamily.style.display = (isSession || isAsyncJob) ? '' : 'none';
|
||||
if (asyncRow) asyncRow.style.display = isAsyncJob ? '' : 'none';
|
||||
|
||||
ensureStreamingToolFamilyDefaults(mode);
|
||||
}
|
||||
|
||||
function bindStreamingConfigControls() {
|
||||
updateStreamingModeOptions();
|
||||
updateStreamingConfigVisibility();
|
||||
|
||||
var mode = document.getElementById('streaming-mode');
|
||||
if (mode) {
|
||||
mode.addEventListener('change', function() {
|
||||
updateStreamingTransportOptions();
|
||||
updateStreamingConfigVisibility();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function collectStreamingConfig() {
|
||||
var mode = selectedStreamingMode();
|
||||
if (mode === 'unary') return null;
|
||||
|
||||
var config = {
|
||||
mode: mode,
|
||||
transport_behavior: stringValueOrNull('streaming-transport-behavior') || 'request_response',
|
||||
window_duration_ms: numericValueOrNull('streaming-window-duration-ms'),
|
||||
poll_interval_ms: numericValueOrNull('streaming-poll-interval-ms'),
|
||||
upstream_timeout_ms: numericValueOrNull('streaming-upstream-timeout-ms'),
|
||||
idle_timeout_ms: numericValueOrNull('streaming-idle-timeout-ms'),
|
||||
max_session_lifetime_ms: numericValueOrNull('streaming-session-lifetime-ms'),
|
||||
max_items: numericValueOrNull('streaming-max-items'),
|
||||
max_bytes: numericValueOrNull('streaming-max-bytes'),
|
||||
aggregation_mode: stringValueOrNull('streaming-aggregation-mode') || 'summary_plus_samples',
|
||||
summary_path: stringValueOrNull('streaming-summary-path'),
|
||||
items_path: stringValueOrNull('streaming-items-path'),
|
||||
cursor_path: stringValueOrNull('streaming-cursor-path'),
|
||||
status_path: stringValueOrNull('streaming-status-path'),
|
||||
done_path: stringValueOrNull('streaming-done-path'),
|
||||
redacted_paths: textareaLines('streaming-redacted-paths'),
|
||||
truncate_item_fields: !!document.getElementById('streaming-truncate-item-fields').checked,
|
||||
max_field_length: numericValueOrNull('streaming-max-field-length'),
|
||||
drop_duplicates: !!document.getElementById('streaming-drop-duplicates').checked,
|
||||
sampling_rate: (function() {
|
||||
var value = textValue('streaming-sampling-rate');
|
||||
if (!value) return null;
|
||||
var parsed = Number(value);
|
||||
return Number.isFinite(parsed) ? parsed : null;
|
||||
}()),
|
||||
tool_family: {},
|
||||
};
|
||||
|
||||
if (mode === 'session') {
|
||||
config.tool_family.start_tool_name = stringValueOrNull('streaming-start-tool-name');
|
||||
config.tool_family.poll_tool_name = stringValueOrNull('streaming-poll-tool-name');
|
||||
config.tool_family.stop_tool_name = stringValueOrNull('streaming-stop-tool-name');
|
||||
} else if (mode === 'async_job') {
|
||||
config.tool_family.start_tool_name = stringValueOrNull('streaming-start-tool-name');
|
||||
config.tool_family.status_tool_name = stringValueOrNull('streaming-status-tool-name');
|
||||
config.tool_family.result_tool_name = stringValueOrNull('streaming-result-tool-name');
|
||||
config.tool_family.cancel_tool_name = stringValueOrNull('streaming-cancel-tool-name');
|
||||
}
|
||||
|
||||
Object.keys(config).forEach(function(key) {
|
||||
if (config[key] === null) delete config[key];
|
||||
});
|
||||
if (!config.redacted_paths.length) delete config.redacted_paths;
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
function applyStreamingConfig(streaming) {
|
||||
updateStreamingModeOptions(streaming && streaming.mode ? streaming.mode : 'unary');
|
||||
if (!streaming) {
|
||||
updateStreamingConfigVisibility();
|
||||
return;
|
||||
}
|
||||
|
||||
setValue('streaming-window-duration-ms', streaming.window_duration_ms || '');
|
||||
setValue('streaming-poll-interval-ms', streaming.poll_interval_ms || '');
|
||||
setValue('streaming-upstream-timeout-ms', streaming.upstream_timeout_ms || '');
|
||||
setValue('streaming-idle-timeout-ms', streaming.idle_timeout_ms || '');
|
||||
setValue('streaming-session-lifetime-ms', streaming.max_session_lifetime_ms || '');
|
||||
setValue('streaming-max-items', streaming.max_items || '');
|
||||
setValue('streaming-max-bytes', streaming.max_bytes || '');
|
||||
setValue('streaming-max-field-length', streaming.max_field_length || '');
|
||||
setValue('streaming-sampling-rate', streaming.sampling_rate || '');
|
||||
setValue('streaming-items-path', streaming.items_path || '');
|
||||
setValue('streaming-summary-path', streaming.summary_path || '');
|
||||
setValue('streaming-done-path', streaming.done_path || '');
|
||||
setValue('streaming-cursor-path', streaming.cursor_path || '');
|
||||
setValue('streaming-status-path', streaming.status_path || '');
|
||||
setValue('streaming-redacted-paths', (streaming.redacted_paths || []).join('\n'));
|
||||
document.getElementById('streaming-truncate-item-fields').checked = !!streaming.truncate_item_fields;
|
||||
document.getElementById('streaming-drop-duplicates').checked = !!streaming.drop_duplicates;
|
||||
updateStreamingTransportOptions(streaming.transport_behavior);
|
||||
setValue('streaming-start-tool-name', streaming.tool_family && streaming.tool_family.start_tool_name ? streaming.tool_family.start_tool_name : '');
|
||||
setValue('streaming-poll-tool-name', streaming.tool_family && streaming.tool_family.poll_tool_name ? streaming.tool_family.poll_tool_name : '');
|
||||
setValue('streaming-stop-tool-name', streaming.tool_family && streaming.tool_family.stop_tool_name ? streaming.tool_family.stop_tool_name : '');
|
||||
setValue('streaming-status-tool-name', streaming.tool_family && streaming.tool_family.status_tool_name ? streaming.tool_family.status_tool_name : '');
|
||||
setValue('streaming-result-tool-name', streaming.tool_family && streaming.tool_family.result_tool_name ? streaming.tool_family.result_tool_name : '');
|
||||
setValue('streaming-cancel-tool-name', streaming.tool_family && streaming.tool_family.cancel_tool_name ? streaming.tool_family.cancel_tool_name : '');
|
||||
var aggregation = document.getElementById('streaming-aggregation-mode');
|
||||
if (aggregation && streaming.aggregation_mode) aggregation.value = streaming.aggregation_mode;
|
||||
updateStreamingConfigVisibility();
|
||||
}
|
||||
|
||||
function buildToolDescription() {
|
||||
return {
|
||||
title: textValue('tool-title') || textValue('tool-display-name') || textValue('tool-name'),
|
||||
@@ -1439,6 +1750,7 @@ function prefillWizardFromEdit(detail, versionDocument) {
|
||||
setValue('tool-input-mapping', mappingSetToEditorValue(versionDocument.input_mapping, 'input', detail.protocol));
|
||||
setValue('tool-output-mapping', mappingSetToEditorValue(versionDocument.output_mapping, 'output', detail.protocol));
|
||||
setValue('tool-exec-config', executionConfigToEditorValue(versionDocument.execution_config));
|
||||
applyStreamingConfig(versionDocument.execution_config ? versionDocument.execution_config.streaming : null);
|
||||
|
||||
var toolNameInput = document.getElementById('tool-name');
|
||||
if (toolNameInput) toolNameInput.disabled = true;
|
||||
@@ -1525,6 +1837,8 @@ async function runWizardLiveAction(button, busyLabel, handler) {
|
||||
function updateWizardProtocolVisibility() {
|
||||
var grpcTools = document.getElementById('wizard-grpc-live-tools');
|
||||
if (grpcTools) grpcTools.style.display = wizardProtocol === 'grpc' ? '' : 'none';
|
||||
updateStreamingModeOptions();
|
||||
updateStreamingConfigVisibility();
|
||||
}
|
||||
|
||||
function showWizardLiveStatus(title, text, isError) {
|
||||
|
||||
@@ -34,6 +34,14 @@ server {
|
||||
try_files /html/usage.html =404;
|
||||
}
|
||||
|
||||
location = /stream-sessions {
|
||||
try_files /html/stream-sessions.html =404;
|
||||
}
|
||||
|
||||
location = /async-jobs {
|
||||
try_files /html/async-jobs.html =404;
|
||||
}
|
||||
|
||||
location = /settings {
|
||||
try_files /html/settings.html =404;
|
||||
}
|
||||
@@ -75,6 +83,14 @@ server {
|
||||
return 302 /usage;
|
||||
}
|
||||
|
||||
location = /html/stream-sessions.html {
|
||||
return 302 /stream-sessions;
|
||||
}
|
||||
|
||||
location = /html/async-jobs.html {
|
||||
return 302 /async-jobs;
|
||||
}
|
||||
|
||||
location = /html/settings.html {
|
||||
return 302 /settings;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user