feat: expose operation target summaries for alpine ui
This commit is contained in:
@@ -12,6 +12,7 @@ DoD:
|
||||
- UI продолжает раздаваться как статический контейнер через текущий deployment path
|
||||
- legacy React/Vite артефакты удалены из `apps/ui`
|
||||
- backend для `Operations/Wizard` поддерживает server-side `PATCH`, `DELETE`, `archive`, `category`, `usage_summary`, `agent_refs`
|
||||
- backend summary для `Operations` отдает `target_url` и `target_action`
|
||||
- первые экраны Alpine UI можно сажать на реальные catalog/detail/mutation endpoints без расширения backend-контракта
|
||||
- `Operations` page читает live catalog из `admin-api` и удаляет операции через backend
|
||||
- `Wizard` использует live create/get/update flow вместо `localStorage` и `sessionStorage`
|
||||
|
||||
@@ -210,6 +210,11 @@ mod tests {
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(listed["items"][0]["name"], "crm_create_lead");
|
||||
assert_eq!(
|
||||
listed["items"][0]["target_url"],
|
||||
format!("{upstream_base_url}/crm/leads")
|
||||
);
|
||||
assert_eq!(listed["items"][0]["target_action"], "POST");
|
||||
assert_eq!(published["published_version"], 1);
|
||||
assert_eq!(test_run["ok"], true);
|
||||
assert_eq!(
|
||||
@@ -251,6 +256,11 @@ mod tests {
|
||||
.unwrap();
|
||||
assert_eq!(listed["total"], 1);
|
||||
assert_eq!(listed["items"][0]["category"], "sales");
|
||||
assert_eq!(
|
||||
listed["items"][0]["target_url"],
|
||||
format!("{upstream_base_url}/crm/leads")
|
||||
);
|
||||
assert_eq!(listed["items"][0]["target_action"], "POST");
|
||||
|
||||
let updated = client
|
||||
.patch(format!("{base_url}/operations/{operation_id}"))
|
||||
|
||||
@@ -291,6 +291,8 @@ pub struct OperationSummaryView {
|
||||
pub display_name: String,
|
||||
pub category: String,
|
||||
pub protocol: Protocol,
|
||||
pub target_url: String,
|
||||
pub target_action: String,
|
||||
pub status: OperationStatus,
|
||||
pub current_draft_version: u32,
|
||||
pub latest_published_version: Option<u32>,
|
||||
@@ -2070,6 +2072,8 @@ fn enrich_operation_summary(
|
||||
display_name: summary.display_name,
|
||||
category: summary.category,
|
||||
protocol: summary.protocol,
|
||||
target_url: summary.target_url,
|
||||
target_action: summary.target_action,
|
||||
status: summary.status,
|
||||
current_draft_version: summary.current_draft_version,
|
||||
latest_published_version: summary.latest_published_version,
|
||||
|
||||
@@ -40,8 +40,8 @@ function mapOperation(item) {
|
||||
created_at: item.created_at,
|
||||
updated_at: item.updated_at,
|
||||
published_at: item.published_at,
|
||||
target_url: '',
|
||||
method: '',
|
||||
target_url: item.target_url || '',
|
||||
method: item.target_action || '',
|
||||
usage_summary: item.usage_summary || {
|
||||
calls_today: 0,
|
||||
error_rate_pct: 0,
|
||||
|
||||
@@ -157,6 +157,8 @@ pub struct OperationSummary {
|
||||
pub display_name: String,
|
||||
pub category: String,
|
||||
pub protocol: Protocol,
|
||||
pub target_url: String,
|
||||
pub target_action: String,
|
||||
pub status: OperationStatus,
|
||||
pub current_draft_version: u32,
|
||||
pub latest_published_version: Option<u32>,
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
use crank_core::{
|
||||
AgentId, AgentOperationBinding, AgentStatus, AgentVersion, AuthProfile, InvitationId,
|
||||
InvitationToken, InvocationLog, InvocationLogId, OperationId, OperationStatus, PlatformApiKey,
|
||||
PlatformApiKeyId, PlatformApiKeyStatus, UsageRollup, User, UserId, Workspace, WorkspaceId,
|
||||
AgentId, AgentOperationBinding, AgentStatus, AgentVersion, AuthProfile, GraphqlOperationType,
|
||||
HttpMethod, InvitationId, InvitationToken, InvocationLog, InvocationLogId, OperationId,
|
||||
OperationStatus, PlatformApiKey, PlatformApiKeyId, PlatformApiKeyStatus, Target, UsageRollup,
|
||||
User, UserId, Workspace, WorkspaceId,
|
||||
};
|
||||
use serde::{Serialize, de::DeserializeOwned};
|
||||
use serde_json::Value;
|
||||
@@ -1490,21 +1491,25 @@ impl PostgresRegistry {
|
||||
) -> Result<Vec<OperationSummary>, RegistryError> {
|
||||
let rows = sqlx::query(
|
||||
"select
|
||||
id,
|
||||
workspace_id,
|
||||
name,
|
||||
display_name,
|
||||
category,
|
||||
protocol,
|
||||
status,
|
||||
current_draft_version,
|
||||
latest_published_version,
|
||||
to_char(created_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as created_at,
|
||||
to_char(updated_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as updated_at,
|
||||
to_char(published_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as published_at
|
||||
operations.id,
|
||||
operations.workspace_id,
|
||||
operations.name,
|
||||
operations.display_name,
|
||||
operations.category,
|
||||
operations.protocol,
|
||||
ov.target_json,
|
||||
operations.status,
|
||||
operations.current_draft_version,
|
||||
operations.latest_published_version,
|
||||
to_char(operations.created_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as created_at,
|
||||
to_char(operations.updated_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as updated_at,
|
||||
to_char(operations.published_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as published_at
|
||||
from operations
|
||||
where workspace_id = $1
|
||||
order by name asc",
|
||||
join operation_versions ov
|
||||
on ov.operation_id = operations.id
|
||||
and ov.version = operations.current_draft_version
|
||||
where operations.workspace_id = $1
|
||||
order by operations.name asc",
|
||||
)
|
||||
.bind(workspace_id.as_str())
|
||||
.fetch_all(&self.pool)
|
||||
@@ -1571,20 +1576,24 @@ impl PostgresRegistry {
|
||||
) -> Result<Option<OperationSummary>, RegistryError> {
|
||||
let row = sqlx::query(
|
||||
"select
|
||||
id,
|
||||
workspace_id,
|
||||
name,
|
||||
display_name,
|
||||
category,
|
||||
protocol,
|
||||
status,
|
||||
current_draft_version,
|
||||
latest_published_version,
|
||||
to_char(created_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as created_at,
|
||||
to_char(updated_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as updated_at,
|
||||
to_char(published_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as published_at
|
||||
operations.id,
|
||||
operations.workspace_id,
|
||||
operations.name,
|
||||
operations.display_name,
|
||||
operations.category,
|
||||
operations.protocol,
|
||||
ov.target_json,
|
||||
operations.status,
|
||||
operations.current_draft_version,
|
||||
operations.latest_published_version,
|
||||
to_char(operations.created_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as created_at,
|
||||
to_char(operations.updated_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as updated_at,
|
||||
to_char(operations.published_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as published_at
|
||||
from operations
|
||||
where workspace_id = $1 and id = $2",
|
||||
join operation_versions ov
|
||||
on ov.operation_id = operations.id
|
||||
and ov.version = operations.current_draft_version
|
||||
where operations.workspace_id = $1 and operations.id = $2",
|
||||
)
|
||||
.bind(workspace_id.as_str())
|
||||
.bind(operation_id.as_str())
|
||||
@@ -2447,6 +2456,9 @@ fn map_agent_summary(row: &PgRow) -> Result<AgentSummary, RegistryError> {
|
||||
}
|
||||
|
||||
fn map_operation_summary(row: &PgRow) -> Result<OperationSummary, RegistryError> {
|
||||
let target: Target = deserialize_json_value(row.try_get::<Json<Value>, _>("target_json")?.0)?;
|
||||
let (target_url, target_action) = target_summary(&target);
|
||||
|
||||
Ok(OperationSummary {
|
||||
id: OperationId::new(row.try_get::<String, _>("id")?),
|
||||
workspace_id: WorkspaceId::new(row.try_get::<String, _>("workspace_id")?),
|
||||
@@ -2454,6 +2466,8 @@ fn map_operation_summary(row: &PgRow) -> Result<OperationSummary, RegistryError>
|
||||
display_name: row.try_get("display_name")?,
|
||||
category: row.try_get("category")?,
|
||||
protocol: deserialize_enum_text(&row.try_get::<String, _>("protocol")?, "protocol")?,
|
||||
target_url,
|
||||
target_action,
|
||||
status: deserialize_enum_text(&row.try_get::<String, _>("status")?, "status")?,
|
||||
current_draft_version: from_db_version(
|
||||
row.try_get("current_draft_version")?,
|
||||
@@ -2478,6 +2492,52 @@ fn map_operation_usage_summary(row: &PgRow) -> Result<OperationUsageSummary, Reg
|
||||
})
|
||||
}
|
||||
|
||||
fn target_summary(target: &Target) -> (String, String) {
|
||||
match target {
|
||||
Target::Rest(rest) => (
|
||||
join_target_url(&rest.base_url, &rest.path_template),
|
||||
match rest.method {
|
||||
HttpMethod::Get => "GET",
|
||||
HttpMethod::Post => "POST",
|
||||
HttpMethod::Put => "PUT",
|
||||
HttpMethod::Patch => "PATCH",
|
||||
HttpMethod::Delete => "DELETE",
|
||||
}
|
||||
.to_owned(),
|
||||
),
|
||||
Target::Graphql(graphql) => (
|
||||
graphql.endpoint.clone(),
|
||||
match graphql.operation_type {
|
||||
GraphqlOperationType::Query => "QUERY",
|
||||
GraphqlOperationType::Mutation => "MUTATION",
|
||||
}
|
||||
.to_owned(),
|
||||
),
|
||||
Target::Grpc(grpc) => (grpc.server_addr.clone(), grpc.method.clone()),
|
||||
}
|
||||
}
|
||||
|
||||
fn join_target_url(base: &str, path: &str) -> String {
|
||||
if path.is_empty() {
|
||||
return base.to_owned();
|
||||
}
|
||||
|
||||
if path.starts_with("http://") || path.starts_with("https://") || path.starts_with("grpc://") {
|
||||
return path.to_owned();
|
||||
}
|
||||
|
||||
let trimmed_base = base.trim_end_matches('/');
|
||||
let trimmed_path = path.trim_start_matches('/');
|
||||
|
||||
if trimmed_base.is_empty() {
|
||||
format!("/{trimmed_path}")
|
||||
} else if trimmed_path.is_empty() {
|
||||
trimmed_base.to_owned()
|
||||
} else {
|
||||
format!("{trimmed_base}/{trimmed_path}")
|
||||
}
|
||||
}
|
||||
|
||||
fn map_operation_agent_ref(row: &PgRow) -> Result<OperationAgentRef, RegistryError> {
|
||||
Ok(OperationAgentRef {
|
||||
operation_id: OperationId::new(row.try_get::<String, _>("operation_id")?),
|
||||
|
||||
@@ -73,7 +73,7 @@ UI-файлы:
|
||||
- эту страницу можно интегрировать первой;
|
||||
- backend уже отдает server-side category, usage summary и agent refs;
|
||||
- каталог уже может работать через реальные `list/delete/edit` вызовы;
|
||||
- следующий шаг здесь только в richer summary shape, если нужно вернуть `target_url` и server-side paging.
|
||||
- следующий шаг здесь только в server-side paging и остальных catalog actions; `target_url` и `target_action` теперь должны приходить из backend summary.
|
||||
|
||||
### 4.2. Wizard
|
||||
|
||||
|
||||
@@ -183,6 +183,8 @@ Query params:
|
||||
"protocol": "rest",
|
||||
"status": "draft",
|
||||
"category": "sales",
|
||||
"target_url": "https://api.example.com/v1/leads",
|
||||
"target_action": "POST",
|
||||
"current_draft_version": 3,
|
||||
"latest_published_version": 2,
|
||||
"updated_at": "2026-03-29T12:00:00Z",
|
||||
|
||||
Reference in New Issue
Block a user