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