Expose approval requests in admin logs
This commit is contained in:
@@ -19,7 +19,10 @@ use crate::{
|
|||||||
auth_profiles::{create_auth_profile, get_auth_profile, list_auth_profiles},
|
auth_profiles::{create_auth_profile, get_auth_profile, list_auth_profiles},
|
||||||
capabilities::get_capabilities,
|
capabilities::get_capabilities,
|
||||||
imports::{create_openapi_import, preview_openapi_import},
|
imports::{create_openapi_import, preview_openapi_import},
|
||||||
observability::{get_agent_usage, get_log, get_operation_usage, get_usage, list_logs},
|
observability::{
|
||||||
|
get_agent_usage, get_approval, get_log, get_operation_usage, get_usage, list_approvals,
|
||||||
|
list_logs,
|
||||||
|
},
|
||||||
operations::{
|
operations::{
|
||||||
analyze_operation_quality, archive_operation, create_operation, create_version,
|
analyze_operation_quality, archive_operation, create_operation, create_version,
|
||||||
delete_operation, export_operation, generate_draft, get_operation,
|
delete_operation, export_operation, generate_draft, get_operation,
|
||||||
@@ -123,6 +126,8 @@ pub fn build_app(state: AppState) -> Router {
|
|||||||
.route("/export", get(export_workspace))
|
.route("/export", get(export_workspace))
|
||||||
.route("/logs", get(list_logs))
|
.route("/logs", get(list_logs))
|
||||||
.route("/logs/{log_id}", get(get_log))
|
.route("/logs/{log_id}", get(get_log))
|
||||||
|
.route("/approvals", get(list_approvals))
|
||||||
|
.route("/approvals/{approval_id}", get(get_approval))
|
||||||
.route("/usage", get(get_usage))
|
.route("/usage", get(get_usage))
|
||||||
.route("/usage/operations/{operation_id}", get(get_operation_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));
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
use crank_core::{
|
use crank_core::{
|
||||||
AgentId, AgentStatus, AuthConfig, AuthKind, ExecutionMode, ExportMode, GeneratedDraft,
|
AgentId, AgentStatus, ApprovalRequestStatus, AuthConfig, AuthKind, ExecutionMode, ExportMode,
|
||||||
InvocationLevel, InvocationSource, InvocationStatus, OperationSecurityLevel, OperationStatus,
|
GeneratedDraft, InvocationLevel, InvocationSource, InvocationStatus, OperationSecurityLevel,
|
||||||
PlatformApiKeyKind, PlatformApiKeyScope, Protocol, SecretKind, Target, UsagePeriod,
|
OperationStatus, PlatformApiKeyKind, PlatformApiKeyScope, Protocol, SecretKind, Target,
|
||||||
WizardState, WorkspaceId, WorkspaceStatus,
|
UsagePeriod, WizardState, WorkspaceId, WorkspaceStatus,
|
||||||
};
|
};
|
||||||
use crank_mapping::MappingSet;
|
use crank_mapping::MappingSet;
|
||||||
use crank_registry::{
|
use crank_registry::{
|
||||||
@@ -250,6 +250,12 @@ pub struct LogsQuery {
|
|||||||
pub limit: Option<u32>,
|
pub limit: Option<u32>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Deserialize)]
|
||||||
|
pub struct ApprovalsQuery {
|
||||||
|
pub status: Option<ApprovalRequestStatus>,
|
||||||
|
pub limit: Option<u32>,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Deserialize)]
|
#[derive(Clone, Debug, Deserialize)]
|
||||||
pub struct UsageRequestQuery {
|
pub struct UsageRequestQuery {
|
||||||
pub period: Option<UsagePeriod>,
|
pub period: Option<UsagePeriod>,
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ use serde_json::{Value, json};
|
|||||||
use crate::{
|
use crate::{
|
||||||
error::ApiError,
|
error::ApiError,
|
||||||
routes::access::WorkspacePath,
|
routes::access::WorkspacePath,
|
||||||
service::{LogsQuery, UsageRequestQuery},
|
service::{ApprovalsQuery, LogsQuery, UsageRequestQuery},
|
||||||
state::AppState,
|
state::AppState,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -17,6 +17,12 @@ pub struct WorkspaceLogPath {
|
|||||||
pub log_id: String,
|
pub log_id: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(serde::Deserialize)]
|
||||||
|
pub struct WorkspaceApprovalPath {
|
||||||
|
pub workspace_id: String,
|
||||||
|
pub approval_id: String,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(serde::Deserialize)]
|
#[derive(serde::Deserialize)]
|
||||||
pub struct WorkspaceOperationUsagePath {
|
pub struct WorkspaceOperationUsagePath {
|
||||||
pub workspace_id: String,
|
pub workspace_id: String,
|
||||||
@@ -55,6 +61,32 @@ pub async fn get_log(
|
|||||||
Ok(Json(json!(item)))
|
Ok(Json(json!(item)))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn list_approvals(
|
||||||
|
Path(path): Path<WorkspacePath>,
|
||||||
|
Query(query): Query<ApprovalsQuery>,
|
||||||
|
State(state): State<AppState>,
|
||||||
|
) -> Result<Json<Value>, ApiError> {
|
||||||
|
let items = state
|
||||||
|
.service
|
||||||
|
.list_approvals(&path.workspace_id.as_str().into(), query)
|
||||||
|
.await?;
|
||||||
|
Ok(Json(json!({ "items": items })))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_approval(
|
||||||
|
Path(path): Path<WorkspaceApprovalPath>,
|
||||||
|
State(state): State<AppState>,
|
||||||
|
) -> Result<Json<Value>, ApiError> {
|
||||||
|
let item = state
|
||||||
|
.service
|
||||||
|
.get_approval(
|
||||||
|
&path.workspace_id.as_str().into(),
|
||||||
|
&path.approval_id.as_str().into(),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
Ok(Json(json!(item)))
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn get_usage(
|
pub async fn get_usage(
|
||||||
Path(path): Path<WorkspacePath>,
|
Path(path): Path<WorkspacePath>,
|
||||||
Query(query): Query<UsageRequestQuery>,
|
Query(query): Query<UsageRequestQuery>,
|
||||||
|
|||||||
@@ -1,11 +1,21 @@
|
|||||||
use crank_core::{AgentId, InvocationLogId, OperationId, UsagePeriod, WorkspaceId};
|
use crank_core::{
|
||||||
use crank_registry::{InvocationLogRecord, ListInvocationLogsQuery, UsageQuery, UsageRollupRecord};
|
AgentId, ApprovalRequestId, ApprovalRequestStatus, InvocationLogId, OperationId, UsagePeriod,
|
||||||
|
WorkspaceId,
|
||||||
|
};
|
||||||
|
use crank_registry::{
|
||||||
|
ApprovalRequestRecord, ExpireApprovalRequest, InvocationLogRecord, ListApprovalRequestsQuery,
|
||||||
|
ListInvocationLogsQuery, UsageQuery, UsageRollupRecord,
|
||||||
|
};
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
|
use time::OffsetDateTime;
|
||||||
use tracing::instrument;
|
use tracing::instrument;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
error::ApiError,
|
error::ApiError,
|
||||||
service::{AdminService, LogsQuery, UsageOverviewResponse, UsageRequestQuery, usage_window},
|
service::{
|
||||||
|
AdminService, ApprovalsQuery, LogsQuery, UsageOverviewResponse, UsageRequestQuery,
|
||||||
|
usage_window,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
impl AdminService {
|
impl AdminService {
|
||||||
@@ -52,6 +62,76 @@ impl AdminService {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[instrument(skip(self))]
|
||||||
|
pub async fn list_approvals(
|
||||||
|
&self,
|
||||||
|
workspace_id: &WorkspaceId,
|
||||||
|
query: ApprovalsQuery,
|
||||||
|
) -> Result<Vec<ApprovalRequestRecord>, ApiError> {
|
||||||
|
self.ensure_workspace_exists(workspace_id).await?;
|
||||||
|
let records = self
|
||||||
|
.registry
|
||||||
|
.list_approval_requests(ListApprovalRequestsQuery {
|
||||||
|
workspace_id,
|
||||||
|
status: query.status,
|
||||||
|
limit: query.limit.unwrap_or(50).clamp(1, 200),
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let mut normalized = Vec::with_capacity(records.len());
|
||||||
|
for record in records {
|
||||||
|
let record = self.normalize_approval_record(record).await?;
|
||||||
|
if query.status.is_none() || record.approval.status == query.status.unwrap() {
|
||||||
|
normalized.push(record);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(normalized)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[instrument(skip(self))]
|
||||||
|
pub async fn get_approval(
|
||||||
|
&self,
|
||||||
|
workspace_id: &WorkspaceId,
|
||||||
|
approval_id: &ApprovalRequestId,
|
||||||
|
) -> Result<ApprovalRequestRecord, ApiError> {
|
||||||
|
self.ensure_workspace_exists(workspace_id).await?;
|
||||||
|
let record = self
|
||||||
|
.registry
|
||||||
|
.get_approval_request(workspace_id, approval_id)
|
||||||
|
.await?
|
||||||
|
.ok_or_else(|| {
|
||||||
|
ApiError::not_found_with_context(
|
||||||
|
format!("approval request {} was not found", approval_id.as_str()),
|
||||||
|
json!({ "approval_id": approval_id.as_str() }),
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
self.normalize_approval_record(record).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn normalize_approval_record(
|
||||||
|
&self,
|
||||||
|
record: ApprovalRequestRecord,
|
||||||
|
) -> Result<ApprovalRequestRecord, ApiError> {
|
||||||
|
if record.approval.status != ApprovalRequestStatus::Pending
|
||||||
|
|| record.approval.expires_at > OffsetDateTime::now_utc()
|
||||||
|
{
|
||||||
|
return Ok(record);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(self
|
||||||
|
.registry
|
||||||
|
.expire_approval_request(ExpireApprovalRequest {
|
||||||
|
workspace_id: &record.approval.workspace_id,
|
||||||
|
agent_id: &record.approval.agent_id,
|
||||||
|
approval_id: &record.approval.id,
|
||||||
|
expired_at: OffsetDateTime::now_utc(),
|
||||||
|
})
|
||||||
|
.await?
|
||||||
|
.unwrap_or(record))
|
||||||
|
}
|
||||||
|
|
||||||
#[instrument(skip(self))]
|
#[instrument(skip(self))]
|
||||||
pub async fn get_usage_overview(
|
pub async fn get_usage_overview(
|
||||||
&self,
|
&self,
|
||||||
|
|||||||
@@ -134,3 +134,188 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.refresh-btn:hover { color: var(--text-secondary); background: var(--bg-muted); }
|
.refresh-btn:hover { color: var(--text-secondary); background: var(--bg-muted); }
|
||||||
|
|
||||||
|
.approval-panel {
|
||||||
|
margin-bottom: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.approval-panel-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 16px;
|
||||||
|
padding: 16px 20px;
|
||||||
|
border-bottom: 1px solid var(--border-subtle);
|
||||||
|
}
|
||||||
|
|
||||||
|
.approval-panel-title {
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 650;
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.approval-panel-subtitle {
|
||||||
|
margin-top: 4px;
|
||||||
|
font-size: 12.5px;
|
||||||
|
line-height: 1.45;
|
||||||
|
color: var(--text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.approval-refresh-btn {
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.approval-list {
|
||||||
|
display: grid;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 16px 20px 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.approval-empty {
|
||||||
|
padding: 20px;
|
||||||
|
border: 1px dashed var(--border);
|
||||||
|
border-radius: 10px;
|
||||||
|
background: var(--bg-canvas);
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.approval-empty-error {
|
||||||
|
border-color: rgba(248, 81, 73, 0.35);
|
||||||
|
color: var(--red);
|
||||||
|
}
|
||||||
|
|
||||||
|
.approval-item {
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 12px;
|
||||||
|
background: var(--bg-canvas);
|
||||||
|
padding: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.approval-pending {
|
||||||
|
border-color: rgba(210, 153, 34, 0.45);
|
||||||
|
background: linear-gradient(180deg, rgba(210, 153, 34, 0.08), var(--bg-canvas) 46%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.approval-completed {
|
||||||
|
border-color: rgba(63, 185, 80, 0.28);
|
||||||
|
}
|
||||||
|
|
||||||
|
.approval-failed,
|
||||||
|
.approval-denied,
|
||||||
|
.approval-expired {
|
||||||
|
border-color: rgba(248, 81, 73, 0.26);
|
||||||
|
}
|
||||||
|
|
||||||
|
.approval-item-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.approval-item-title {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 650;
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.approval-item-meta,
|
||||||
|
.approval-timing,
|
||||||
|
.approval-note {
|
||||||
|
margin-top: 5px;
|
||||||
|
font-size: 11.5px;
|
||||||
|
color: var(--text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.approval-item-body {
|
||||||
|
margin: 10px 0 0;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.55;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.approval-status {
|
||||||
|
flex-shrink: 0;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 999px;
|
||||||
|
padding: 3px 9px;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 700;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.35px;
|
||||||
|
color: var(--text-muted);
|
||||||
|
background: var(--bg-overlay);
|
||||||
|
}
|
||||||
|
|
||||||
|
.approval-status-pending {
|
||||||
|
color: var(--amber);
|
||||||
|
border-color: rgba(210, 153, 34, 0.45);
|
||||||
|
background: rgba(210, 153, 34, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.approval-status-completed {
|
||||||
|
color: var(--green);
|
||||||
|
border-color: rgba(63, 185, 80, 0.35);
|
||||||
|
background: rgba(63, 185, 80, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.approval-status-denied,
|
||||||
|
.approval-status-expired,
|
||||||
|
.approval-status-failed {
|
||||||
|
color: var(--red);
|
||||||
|
border-color: rgba(248, 81, 73, 0.35);
|
||||||
|
background: rgba(248, 81, 73, 0.09);
|
||||||
|
}
|
||||||
|
|
||||||
|
.approval-payload-grid {
|
||||||
|
display: grid;
|
||||||
|
gap: 10px;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
|
||||||
|
margin-top: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.approval-payload-label {
|
||||||
|
margin-bottom: 5px;
|
||||||
|
font-size: 10.5px;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.45px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: var(--text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.approval-payload-code {
|
||||||
|
margin: 0;
|
||||||
|
max-height: 180px;
|
||||||
|
overflow: auto;
|
||||||
|
border: 1px solid var(--border-subtle);
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #161b22;
|
||||||
|
padding: 10px;
|
||||||
|
color: #c9d1d9;
|
||||||
|
font-family: 'JetBrains Mono', monospace;
|
||||||
|
font-size: 11.5px;
|
||||||
|
line-height: 1.55;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 720px) {
|
||||||
|
.approval-panel-header {
|
||||||
|
align-items: stretch;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.approval-refresh-btn {
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.approval-item-header {
|
||||||
|
align-items: stretch;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.approval-status {
|
||||||
|
align-self: flex-start;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -78,6 +78,20 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="section-card approval-panel">
|
||||||
|
<div class="approval-panel-header">
|
||||||
|
<div>
|
||||||
|
<div class="approval-panel-title" data-i18n="approvals.title">Human confirmations</div>
|
||||||
|
<div class="approval-panel-subtitle" data-i18n="approvals.subtitle">Requests waiting for an external user decision and recent results.</div>
|
||||||
|
</div>
|
||||||
|
<button class="refresh-btn approval-refresh-btn" id="approval-refresh-btn" type="button">
|
||||||
|
<svg width="12" height="12" viewBox="0 0 16 16" fill="currentColor"><path d="M1.705 8.005a.75.75 0 01.834.656 5.5 5.5 0 009.592 2.97l-1.204-1.204a.25.25 0 01.177-.427h3.646a.25.25 0 01.25.25v3.646a.25.25 0 01-.427.177l-1.38-1.38A7.001 7.001 0 011.05 8.84a.75.75 0 01.656-.834zM8 2.5a5.487 5.487 0 00-4.131 1.869l1.204 1.204A.25.25 0 014.896 6H1.25A.25.25 0 011 5.75V2.104a.25.25 0 01.427-.177l1.38 1.38A7.001 7.001 0 0114.95 7.16a.75.75 0 01-1.49.178A5.501 5.501 0 008 2.5z"/></svg>
|
||||||
|
<span data-i18n="approvals.refresh">Refresh</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="approval-list" id="approval-list"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="section-card">
|
<div class="section-card">
|
||||||
<div class="log-toolbar">
|
<div class="log-toolbar">
|
||||||
<div class="live-dot"></div>
|
<div class="live-dot"></div>
|
||||||
|
|||||||
@@ -327,6 +327,12 @@
|
|||||||
getLog: function(workspaceId, logId) {
|
getLog: function(workspaceId, logId) {
|
||||||
return get('/workspaces/' + encodeURIComponent(workspaceId) + '/logs/' + encodeURIComponent(logId));
|
return get('/workspaces/' + encodeURIComponent(workspaceId) + '/logs/' + encodeURIComponent(logId));
|
||||||
},
|
},
|
||||||
|
listApprovals: function(workspaceId, params) {
|
||||||
|
return get('/workspaces/' + encodeURIComponent(workspaceId) + '/approvals' + query(params));
|
||||||
|
},
|
||||||
|
getApproval: function(workspaceId, approvalId) {
|
||||||
|
return get('/workspaces/' + encodeURIComponent(workspaceId) + '/approvals/' + encodeURIComponent(approvalId));
|
||||||
|
},
|
||||||
getUsageOverview: function(workspaceId, params) {
|
getUsageOverview: function(workspaceId, params) {
|
||||||
return get('/workspaces/' + encodeURIComponent(workspaceId) + '/usage' + query(params));
|
return get('/workspaces/' + encodeURIComponent(workspaceId) + '/usage' + query(params));
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -281,6 +281,27 @@ var TRANSLATIONS = {
|
|||||||
'logs.live.off.body': 'Automatic polling is paused.',
|
'logs.live.off.body': 'Automatic polling is paused.',
|
||||||
'logs.refresh.title': 'Logs refreshed',
|
'logs.refresh.title': 'Logs refreshed',
|
||||||
'logs.refresh.body': 'The latest invocation records were loaded for the current workspace.',
|
'logs.refresh.body': 'The latest invocation records were loaded for the current workspace.',
|
||||||
|
'approvals.title': 'Human confirmations',
|
||||||
|
'approvals.subtitle': 'Requests waiting for an external user decision and recent results.',
|
||||||
|
'approvals.refresh': 'Refresh',
|
||||||
|
'approvals.refresh.title': 'Confirmations refreshed',
|
||||||
|
'approvals.refresh.body': 'The latest confirmation requests were loaded.',
|
||||||
|
'approvals.loading': 'Loading confirmation requests…',
|
||||||
|
'approvals.empty': 'There are no confirmation requests yet.',
|
||||||
|
'approvals.error.load': 'Failed to load confirmation requests',
|
||||||
|
'approvals.untitled': 'Confirmation request',
|
||||||
|
'approvals.operation': 'Operation',
|
||||||
|
'approvals.agent': 'Agent',
|
||||||
|
'approvals.expires_at': 'Expires',
|
||||||
|
'approvals.updated_at': 'Updated',
|
||||||
|
'approvals.request': 'Request',
|
||||||
|
'approvals.response': 'Result',
|
||||||
|
'approvals.status.pending': 'Pending',
|
||||||
|
'approvals.status.approved': 'Approved',
|
||||||
|
'approvals.status.denied': 'Denied',
|
||||||
|
'approvals.status.expired': 'Expired',
|
||||||
|
'approvals.status.completed': 'Completed',
|
||||||
|
'approvals.status.failed': 'Failed',
|
||||||
|
|
||||||
// Usage page
|
// Usage page
|
||||||
'usage.title': 'Usage',
|
'usage.title': 'Usage',
|
||||||
@@ -1127,6 +1148,27 @@ var TRANSLATIONS = {
|
|||||||
'logs.live.off.body': 'Автоматический опрос остановлен.',
|
'logs.live.off.body': 'Автоматический опрос остановлен.',
|
||||||
'logs.refresh.title': 'Логи обновлены',
|
'logs.refresh.title': 'Логи обновлены',
|
||||||
'logs.refresh.body': 'Получены последние записи вызовов для текущего воркспейса.',
|
'logs.refresh.body': 'Получены последние записи вызовов для текущего воркспейса.',
|
||||||
|
'approvals.title': 'Подтверждения человеком',
|
||||||
|
'approvals.subtitle': 'Заявки, которые ожидают решения пользователя, и последние результаты.',
|
||||||
|
'approvals.refresh': 'Обновить',
|
||||||
|
'approvals.refresh.title': 'Подтверждения обновлены',
|
||||||
|
'approvals.refresh.body': 'Получены последние заявки на подтверждение.',
|
||||||
|
'approvals.loading': 'Загрузка заявок на подтверждение…',
|
||||||
|
'approvals.empty': 'Заявок на подтверждение пока нет.',
|
||||||
|
'approvals.error.load': 'Не удалось загрузить заявки на подтверждение',
|
||||||
|
'approvals.untitled': 'Заявка на подтверждение',
|
||||||
|
'approvals.operation': 'Операция',
|
||||||
|
'approvals.agent': 'Агент',
|
||||||
|
'approvals.expires_at': 'Истекает',
|
||||||
|
'approvals.updated_at': 'Обновлено',
|
||||||
|
'approvals.request': 'Запрос',
|
||||||
|
'approvals.response': 'Результат',
|
||||||
|
'approvals.status.pending': 'Ожидает',
|
||||||
|
'approvals.status.approved': 'Подтверждено',
|
||||||
|
'approvals.status.denied': 'Отклонено',
|
||||||
|
'approvals.status.expired': 'Истекло',
|
||||||
|
'approvals.status.completed': 'Выполнено',
|
||||||
|
'approvals.status.failed': 'Ошибка',
|
||||||
|
|
||||||
// Usage page
|
// Usage page
|
||||||
'usage.title': 'Использование',
|
'usage.title': 'Использование',
|
||||||
|
|||||||
+174
-4
@@ -11,9 +11,14 @@ document.addEventListener('DOMContentLoaded', function () {
|
|||||||
workspaceId: null,
|
workspaceId: null,
|
||||||
loading: false,
|
loading: false,
|
||||||
loadError: '',
|
loadError: '',
|
||||||
|
approvals: [],
|
||||||
|
approvalsLoading: false,
|
||||||
|
approvalsError: '',
|
||||||
};
|
};
|
||||||
|
|
||||||
var logList = document.getElementById('log-list');
|
var logList = document.getElementById('log-list');
|
||||||
|
var approvalList = document.getElementById('approval-list');
|
||||||
|
var approvalRefreshBtn = document.getElementById('approval-refresh-btn');
|
||||||
var logSearch = document.getElementById('log-search');
|
var logSearch = document.getElementById('log-search');
|
||||||
var refreshBtn = document.getElementById('refresh-btn');
|
var refreshBtn = document.getElementById('refresh-btn');
|
||||||
var timeRangeSel = document.getElementById('time-range');
|
var timeRangeSel = document.getElementById('time-range');
|
||||||
@@ -54,6 +59,19 @@ document.addEventListener('DOMContentLoaded', function () {
|
|||||||
return date.toISOString().slice(11, 23);
|
return date.toISOString().slice(11, 23);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function formatDateTime(timestamp) {
|
||||||
|
if (!timestamp) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
var date = new Date(timestamp);
|
||||||
|
return date.toLocaleString(window.CrankLocale || undefined, {
|
||||||
|
day: '2-digit',
|
||||||
|
month: 'short',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function element(tag, className, text) {
|
function element(tag, className, text) {
|
||||||
var node = document.createElement(tag);
|
var node = document.createElement(tag);
|
||||||
if (className) node.className = className;
|
if (className) node.className = className;
|
||||||
@@ -111,6 +129,115 @@ document.addEventListener('DOMContentLoaded', function () {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizeApproval(record) {
|
||||||
|
var approval = record.approval || record;
|
||||||
|
return {
|
||||||
|
id: approval.id,
|
||||||
|
agentId: approval.agent_id,
|
||||||
|
operationId: approval.operation_id,
|
||||||
|
operationVersion: approval.operation_version,
|
||||||
|
status: approval.status,
|
||||||
|
riskLevel: approval.risk_level,
|
||||||
|
title: approval.confirmation_title,
|
||||||
|
body: approval.confirmation_body,
|
||||||
|
requestPayload: approval.request_payload,
|
||||||
|
responsePayload: approval.response_payload,
|
||||||
|
createdAt: approval.created_at,
|
||||||
|
expiresAt: approval.expires_at,
|
||||||
|
decidedAt: approval.decided_at,
|
||||||
|
note: approval.decision_note,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function approvalStatusLabel(status) {
|
||||||
|
var key = 'approvals.status.' + status;
|
||||||
|
var translated = tKey(key);
|
||||||
|
return translated === key ? status : translated;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderApprovals() {
|
||||||
|
if (!approvalList) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
approvalList.innerHTML = '';
|
||||||
|
|
||||||
|
if (state.approvalsLoading && state.approvals.length === 0) {
|
||||||
|
var loading = element('div', 'approval-empty', tKey('approvals.loading'));
|
||||||
|
approvalList.appendChild(loading);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state.approvalsError) {
|
||||||
|
var error = element('div', 'approval-empty approval-empty-error', state.approvalsError);
|
||||||
|
approvalList.appendChild(error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!state.approvals.length) {
|
||||||
|
var empty = element('div', 'approval-empty', tKey('approvals.empty'));
|
||||||
|
approvalList.appendChild(empty);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var fragment = document.createDocumentFragment();
|
||||||
|
state.approvals.forEach(function (item) {
|
||||||
|
var card = element('article', 'approval-item approval-' + item.status);
|
||||||
|
|
||||||
|
var header = element('div', 'approval-item-header');
|
||||||
|
var titleWrap = element('div', 'approval-item-title-wrap');
|
||||||
|
titleWrap.appendChild(element('div', 'approval-item-title', item.title || tKey('approvals.untitled')));
|
||||||
|
|
||||||
|
var meta = element('div', 'approval-item-meta');
|
||||||
|
meta.textContent = [
|
||||||
|
tKey('approvals.operation') + ': ' + item.operationId + ' v' + item.operationVersion,
|
||||||
|
tKey('approvals.agent') + ': ' + item.agentId,
|
||||||
|
].join(' · ');
|
||||||
|
titleWrap.appendChild(meta);
|
||||||
|
header.appendChild(titleWrap);
|
||||||
|
|
||||||
|
var badge = element('span', 'approval-status approval-status-' + item.status, approvalStatusLabel(item.status));
|
||||||
|
header.appendChild(badge);
|
||||||
|
card.appendChild(header);
|
||||||
|
|
||||||
|
if (item.body) {
|
||||||
|
card.appendChild(element('p', 'approval-item-body', item.body));
|
||||||
|
}
|
||||||
|
|
||||||
|
var timing = element('div', 'approval-timing');
|
||||||
|
timing.textContent = item.status === 'pending'
|
||||||
|
? tKey('approvals.expires_at') + ': ' + formatDateTime(item.expiresAt)
|
||||||
|
: tKey('approvals.updated_at') + ': ' + formatDateTime(item.decidedAt || item.createdAt);
|
||||||
|
card.appendChild(timing);
|
||||||
|
|
||||||
|
var payloadGrid = element('div', 'approval-payload-grid');
|
||||||
|
var requestBlock = element('div', 'approval-payload');
|
||||||
|
requestBlock.appendChild(element('div', 'approval-payload-label', tKey('approvals.request')));
|
||||||
|
var requestPre = element('pre', 'approval-payload-code');
|
||||||
|
requestPre.textContent = formatJson(item.requestPayload);
|
||||||
|
requestBlock.appendChild(requestPre);
|
||||||
|
payloadGrid.appendChild(requestBlock);
|
||||||
|
|
||||||
|
if (item.responsePayload !== null && item.responsePayload !== undefined) {
|
||||||
|
var responseBlock = element('div', 'approval-payload');
|
||||||
|
responseBlock.appendChild(element('div', 'approval-payload-label', tKey('approvals.response')));
|
||||||
|
var responsePre = element('pre', 'approval-payload-code');
|
||||||
|
responsePre.textContent = formatJson(item.responsePayload);
|
||||||
|
responseBlock.appendChild(responsePre);
|
||||||
|
payloadGrid.appendChild(responseBlock);
|
||||||
|
}
|
||||||
|
card.appendChild(payloadGrid);
|
||||||
|
|
||||||
|
if (item.note) {
|
||||||
|
card.appendChild(element('div', 'approval-note', item.note));
|
||||||
|
}
|
||||||
|
|
||||||
|
fragment.appendChild(card);
|
||||||
|
});
|
||||||
|
|
||||||
|
approvalList.appendChild(fragment);
|
||||||
|
}
|
||||||
|
|
||||||
function renderEmpty(title, message) {
|
function renderEmpty(title, message) {
|
||||||
logList.innerHTML = '';
|
logList.innerHTML = '';
|
||||||
var empty = element('div', 'empty-state');
|
var empty = element('div', 'empty-state');
|
||||||
@@ -306,6 +433,39 @@ document.addEventListener('DOMContentLoaded', function () {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function loadApprovals() {
|
||||||
|
if (!window.CrankApi) {
|
||||||
|
state.approvalsError = tKey('logs.error.api');
|
||||||
|
renderApprovals();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
state.workspaceId = currentWorkspaceId();
|
||||||
|
if (!state.workspaceId) {
|
||||||
|
state.approvalsError = tKey('logs.error.workspace');
|
||||||
|
renderApprovals();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
state.approvalsLoading = true;
|
||||||
|
state.approvalsError = '';
|
||||||
|
renderApprovals();
|
||||||
|
|
||||||
|
try {
|
||||||
|
var response = await window.CrankApi.listApprovals(state.workspaceId, { limit: 20 });
|
||||||
|
state.approvals = (response && response.items ? response.items : []).map(normalizeApproval);
|
||||||
|
} catch (error) {
|
||||||
|
state.approvalsError = error.message || tKey('approvals.error.load');
|
||||||
|
} finally {
|
||||||
|
state.approvalsLoading = false;
|
||||||
|
renderApprovals();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function refreshOperationalData() {
|
||||||
|
await Promise.all([loadLogs(), loadApprovals()]);
|
||||||
|
}
|
||||||
|
|
||||||
async function loadLogDetail(logId) {
|
async function loadLogDetail(logId) {
|
||||||
if (!window.CrankApi || !state.workspaceId || state.details[logId]) {
|
if (!window.CrankApi || !state.workspaceId || state.details[logId]) {
|
||||||
return;
|
return;
|
||||||
@@ -343,7 +503,7 @@ document.addEventListener('DOMContentLoaded', function () {
|
|||||||
if (!state.liveMode) {
|
if (!state.liveMode) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
state.timer = setInterval(loadLogs, 4000);
|
state.timer = setInterval(refreshOperationalData, 4000);
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleLive() {
|
function toggleLive() {
|
||||||
@@ -386,6 +546,16 @@ document.addEventListener('DOMContentLoaded', function () {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (approvalRefreshBtn) {
|
||||||
|
approvalRefreshBtn.addEventListener('click', function () {
|
||||||
|
loadApprovals().then(function () {
|
||||||
|
if (!state.approvalsError && window.CrankUi) {
|
||||||
|
window.CrankUi.info(tKey('approvals.refresh.body'), tKey('approvals.refresh.title'));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (timeRangeSel) {
|
if (timeRangeSel) {
|
||||||
timeRangeSel.value = state.period;
|
timeRangeSel.value = state.period;
|
||||||
timeRangeSel.addEventListener('change', function () {
|
timeRangeSel.addEventListener('change', function () {
|
||||||
@@ -405,15 +575,15 @@ document.addEventListener('DOMContentLoaded', function () {
|
|||||||
window.addEventListener('crank:workspacechange', function () {
|
window.addEventListener('crank:workspacechange', function () {
|
||||||
state.details = {};
|
state.details = {};
|
||||||
state.openId = null;
|
state.openId = null;
|
||||||
loadLogs();
|
refreshOperationalData();
|
||||||
});
|
});
|
||||||
|
|
||||||
setLiveState();
|
setLiveState();
|
||||||
startPolling();
|
startPolling();
|
||||||
|
|
||||||
if (window.whenWorkspacesReady) {
|
if (window.whenWorkspacesReady) {
|
||||||
window.whenWorkspacesReady().finally(loadLogs);
|
window.whenWorkspacesReady().finally(refreshOperationalData);
|
||||||
} else {
|
} else {
|
||||||
loadLogs();
|
refreshOperationalData();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -28,10 +28,10 @@ pub mod requests {
|
|||||||
CreatePlatformApiKeyRequest, CreateSecretRequest, CreateVersionRequest,
|
CreatePlatformApiKeyRequest, CreateSecretRequest, CreateVersionRequest,
|
||||||
CreateWorkspaceRequest, CreateYamlImportJobRequest, DecideApprovalRequest,
|
CreateWorkspaceRequest, CreateYamlImportJobRequest, DecideApprovalRequest,
|
||||||
ExpireApprovalRequest, FinishApprovalRequest, FinishImportJobRequest,
|
ExpireApprovalRequest, FinishApprovalRequest, FinishImportJobRequest,
|
||||||
ListInvocationLogsQuery, PublishAgentRequest, PublishRequest, RotateSecretRequest,
|
ListApprovalRequestsQuery, ListInvocationLogsQuery, PublishAgentRequest, PublishRequest,
|
||||||
SaveAgentBindingsRequest, SaveAuthProfileRequest, SaveDescriptorMetadataRequest,
|
RotateSecretRequest, SaveAgentBindingsRequest, SaveAuthProfileRequest,
|
||||||
SaveSampleMetadataRequest, SaveWorkspaceUpstreamRequest, UpdateWorkspaceRequest,
|
SaveDescriptorMetadataRequest, SaveSampleMetadataRequest, SaveWorkspaceUpstreamRequest,
|
||||||
UsageQuery,
|
UpdateWorkspaceRequest, UsageQuery,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -47,15 +47,16 @@ pub use model::{
|
|||||||
CreatePlatformApiKeyRequest, CreateSecretRequest, CreateVersionRequest, CreateWorkspaceRequest,
|
CreatePlatformApiKeyRequest, CreateSecretRequest, CreateVersionRequest, CreateWorkspaceRequest,
|
||||||
CreateYamlImportJobRequest, DecideApprovalRequest, DescriptorKind, DescriptorMetadata,
|
CreateYamlImportJobRequest, DecideApprovalRequest, DescriptorKind, DescriptorMetadata,
|
||||||
ExpireApprovalRequest, FinishApprovalRequest, FinishImportJobRequest, ImportJob, ImportJobId,
|
ExpireApprovalRequest, FinishApprovalRequest, FinishImportJobRequest, ImportJob, ImportJobId,
|
||||||
ImportJobKind, ImportJobStatus, InvitationRecord, InvocationLogRecord, ListInvocationLogsQuery,
|
ImportJobKind, ImportJobStatus, InvitationRecord, InvocationLogRecord,
|
||||||
MembershipRecord, OperationAgentRef, OperationSampleMetadata, OperationSummary,
|
ListApprovalRequestsQuery, ListInvocationLogsQuery, MembershipRecord, OperationAgentRef,
|
||||||
OperationUsageSummary, OperationVersionRecord, Page, PlatformApiKeyRecord, PublishAgentRequest,
|
OperationSampleMetadata, OperationSummary, OperationUsageSummary, OperationVersionRecord, Page,
|
||||||
PublishRequest, PublishedAgentTool, RegistryOperation, RotateSecretRequest, SampleKind,
|
PlatformApiKeyRecord, PublishAgentRequest, PublishRequest, PublishedAgentTool,
|
||||||
SaveAgentBindingsRequest, SaveAuthProfileRequest, SaveDescriptorMetadataRequest,
|
RegistryOperation, RotateSecretRequest, SampleKind, SaveAgentBindingsRequest,
|
||||||
SaveSampleMetadataRequest, SaveWorkspaceUpstreamRequest, SecretRecord, SecretVersionRecord,
|
SaveAuthProfileRequest, SaveDescriptorMetadataRequest, SaveSampleMetadataRequest,
|
||||||
SessionRecord, UpdateWorkspaceRequest, UsageAgentBreakdown, UsageBucket,
|
SaveWorkspaceUpstreamRequest, SecretRecord, SecretVersionRecord, SessionRecord,
|
||||||
UsageOperationBreakdown, UsageQuery, UsageRollupRecord, UsageSummary, UsageTimelinePoint,
|
UpdateWorkspaceRequest, UsageAgentBreakdown, UsageBucket, UsageOperationBreakdown, UsageQuery,
|
||||||
WorkspaceMembershipRecord, WorkspaceRecord, WorkspaceUpstream, WorkspaceUpstreamId,
|
UsageRollupRecord, UsageSummary, UsageTimelinePoint, WorkspaceMembershipRecord,
|
||||||
YamlImportJob, YamlImportJobCompletion, YamlImportJobId, YamlImportJobStatus,
|
WorkspaceRecord, WorkspaceUpstream, WorkspaceUpstreamId, YamlImportJob,
|
||||||
|
YamlImportJobCompletion, YamlImportJobId, YamlImportJobStatus,
|
||||||
};
|
};
|
||||||
pub use postgres::{PostgresPoolConfig, PostgresPoolConfigError, PostgresRegistry};
|
pub use postgres::{PostgresPoolConfig, PostgresPoolConfigError, PostgresRegistry};
|
||||||
|
|||||||
@@ -524,6 +524,13 @@ pub struct CreateApprovalRequest<'a> {
|
|||||||
pub approval: &'a ApprovalRequest,
|
pub approval: &'a ApprovalRequest,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct ListApprovalRequestsQuery<'a> {
|
||||||
|
pub workspace_id: &'a WorkspaceId,
|
||||||
|
pub status: Option<ApprovalRequestStatus>,
|
||||||
|
pub limit: u32,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct DecideApprovalRequest<'a> {
|
pub struct DecideApprovalRequest<'a> {
|
||||||
pub workspace_id: &'a WorkspaceId,
|
pub workspace_id: &'a WorkspaceId,
|
||||||
|
|||||||
@@ -140,6 +140,83 @@ impl PostgresRegistry {
|
|||||||
row.map(map_approval_request_row).transpose()
|
row.map(map_approval_request_row).transpose()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn list_approval_requests(
|
||||||
|
&self,
|
||||||
|
query: ListApprovalRequestsQuery<'_>,
|
||||||
|
) -> Result<Vec<ApprovalRequestRecord>, RegistryError> {
|
||||||
|
let status = query
|
||||||
|
.status
|
||||||
|
.map(|status| serialize_enum_text(&status, "approval_status"))
|
||||||
|
.transpose()?;
|
||||||
|
let rows = sqlx::query(
|
||||||
|
"select
|
||||||
|
id,
|
||||||
|
workspace_id,
|
||||||
|
agent_id,
|
||||||
|
operation_id,
|
||||||
|
operation_version,
|
||||||
|
status,
|
||||||
|
risk_level,
|
||||||
|
confirmation_title,
|
||||||
|
confirmation_body,
|
||||||
|
request_payload_json,
|
||||||
|
response_payload_json,
|
||||||
|
created_at,
|
||||||
|
expires_at,
|
||||||
|
decided_at,
|
||||||
|
decided_by_key_id,
|
||||||
|
decision_note
|
||||||
|
from approval_requests
|
||||||
|
where workspace_id = $1
|
||||||
|
and ($2::text is null or status = $2)
|
||||||
|
order by created_at desc
|
||||||
|
limit $3",
|
||||||
|
)
|
||||||
|
.bind(query.workspace_id.as_str())
|
||||||
|
.bind(status.as_deref())
|
||||||
|
.bind(i64::from(query.limit))
|
||||||
|
.fetch_all(&self.pool)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
rows.into_iter().map(map_approval_request_row).collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_approval_request(
|
||||||
|
&self,
|
||||||
|
workspace_id: &WorkspaceId,
|
||||||
|
approval_id: &ApprovalRequestId,
|
||||||
|
) -> Result<Option<ApprovalRequestRecord>, RegistryError> {
|
||||||
|
let row = sqlx::query(
|
||||||
|
"select
|
||||||
|
id,
|
||||||
|
workspace_id,
|
||||||
|
agent_id,
|
||||||
|
operation_id,
|
||||||
|
operation_version,
|
||||||
|
status,
|
||||||
|
risk_level,
|
||||||
|
confirmation_title,
|
||||||
|
confirmation_body,
|
||||||
|
request_payload_json,
|
||||||
|
response_payload_json,
|
||||||
|
created_at,
|
||||||
|
expires_at,
|
||||||
|
decided_at,
|
||||||
|
decided_by_key_id,
|
||||||
|
decision_note
|
||||||
|
from approval_requests
|
||||||
|
where workspace_id = $1
|
||||||
|
and id = $2
|
||||||
|
limit 1",
|
||||||
|
)
|
||||||
|
.bind(workspace_id.as_str())
|
||||||
|
.bind(approval_id.as_str())
|
||||||
|
.fetch_optional(&self.pool)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
row.map(map_approval_request_row).transpose()
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn decide_approval_request(
|
pub async fn decide_approval_request(
|
||||||
&self,
|
&self,
|
||||||
request: DecideApprovalRequest<'_>,
|
request: DecideApprovalRequest<'_>,
|
||||||
|
|||||||
@@ -36,16 +36,16 @@ use crate::{
|
|||||||
CreatePlatformApiKeyRequest, CreateSecretRequest, CreateVersionRequest,
|
CreatePlatformApiKeyRequest, CreateSecretRequest, CreateVersionRequest,
|
||||||
CreateWorkspaceRequest, CreateYamlImportJobRequest, DecideApprovalRequest,
|
CreateWorkspaceRequest, CreateYamlImportJobRequest, DecideApprovalRequest,
|
||||||
DescriptorMetadata, ExpireApprovalRequest, FinishApprovalRequest, FinishImportJobRequest,
|
DescriptorMetadata, ExpireApprovalRequest, FinishApprovalRequest, FinishImportJobRequest,
|
||||||
ImportJob, ImportJobId, InvitationRecord, InvocationLogRecord, ListInvocationLogsQuery,
|
ImportJob, ImportJobId, InvitationRecord, InvocationLogRecord, ListApprovalRequestsQuery,
|
||||||
MembershipRecord, OperationAgentRef, OperationSampleMetadata, OperationSummary,
|
ListInvocationLogsQuery, MembershipRecord, OperationAgentRef, OperationSampleMetadata,
|
||||||
OperationUsageSummary, OperationVersionRecord, PlatformApiKeyRecord, PublishAgentRequest,
|
OperationSummary, OperationUsageSummary, OperationVersionRecord, PlatformApiKeyRecord,
|
||||||
PublishRequest, PublishedAgentTool, RegistryOperation, RotateSecretRequest,
|
PublishAgentRequest, PublishRequest, PublishedAgentTool, RegistryOperation,
|
||||||
SaveAgentBindingsRequest, SaveAuthProfileRequest, SaveDescriptorMetadataRequest,
|
RotateSecretRequest, SaveAgentBindingsRequest, SaveAuthProfileRequest,
|
||||||
SaveSampleMetadataRequest, SaveWorkspaceUpstreamRequest, SecretRecord, SecretVersionRecord,
|
SaveDescriptorMetadataRequest, SaveSampleMetadataRequest, SaveWorkspaceUpstreamRequest,
|
||||||
SessionRecord, UpdateWorkspaceRequest, UsageAgentBreakdown, UsageOperationBreakdown,
|
SecretRecord, SecretVersionRecord, SessionRecord, UpdateWorkspaceRequest,
|
||||||
UsageQuery, UsageRollupRecord, UsageSummary, UsageTimelinePoint, WorkspaceMembershipRecord,
|
UsageAgentBreakdown, UsageOperationBreakdown, UsageQuery, UsageRollupRecord, UsageSummary,
|
||||||
WorkspaceRecord, WorkspaceUpstream, YamlImportJob, YamlImportJobCompletion,
|
UsageTimelinePoint, WorkspaceMembershipRecord, WorkspaceRecord, WorkspaceUpstream,
|
||||||
YamlImportJobId, YamlImportJobStatus,
|
YamlImportJob, YamlImportJobCompletion, YamlImportJobId, YamlImportJobStatus,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -23,11 +23,11 @@ use crank_registry::{
|
|||||||
CreateAgentRequest, CreateApprovalRequest, CreateInvocationLogRequest,
|
CreateAgentRequest, CreateApprovalRequest, CreateInvocationLogRequest,
|
||||||
CreatePlatformApiKeyRequest, CreateVersionRequest, CreateWorkspaceRequest,
|
CreatePlatformApiKeyRequest, CreateVersionRequest, CreateWorkspaceRequest,
|
||||||
CreateYamlImportJobRequest, DecideApprovalRequest, DescriptorKind, DescriptorMetadata,
|
CreateYamlImportJobRequest, DecideApprovalRequest, DescriptorKind, DescriptorMetadata,
|
||||||
FinishApprovalRequest, OperationSampleMetadata, PlatformApiKeyRecord, PostgresRegistry,
|
FinishApprovalRequest, ListApprovalRequestsQuery, OperationSampleMetadata,
|
||||||
PublishAgentRequest, PublishRequest, RegistryError, RegistryOperation, SampleKind,
|
PlatformApiKeyRecord, PostgresRegistry, PublishAgentRequest, PublishRequest, RegistryError,
|
||||||
SaveAuthProfileRequest, SaveDescriptorMetadataRequest, SaveSampleMetadataRequest,
|
RegistryOperation, SampleKind, SaveAuthProfileRequest, SaveDescriptorMetadataRequest,
|
||||||
UpdateWorkspaceRequest, WorkspaceRecord, YamlImportJobCompletion, YamlImportJobId,
|
SaveSampleMetadataRequest, UpdateWorkspaceRequest, WorkspaceRecord, YamlImportJobCompletion,
|
||||||
YamlImportJobStatus,
|
YamlImportJobId, YamlImportJobStatus,
|
||||||
};
|
};
|
||||||
|
|
||||||
fn test_workspace_id() -> WorkspaceId {
|
fn test_workspace_id() -> WorkspaceId {
|
||||||
@@ -531,6 +531,24 @@ async fn manages_approval_request_lifecycle() {
|
|||||||
assert_eq!(pending.len(), 1);
|
assert_eq!(pending.len(), 1);
|
||||||
assert_eq!(pending[0].approval, approval);
|
assert_eq!(pending[0].approval, approval);
|
||||||
|
|
||||||
|
let workspace_approvals = registry
|
||||||
|
.list_approval_requests(ListApprovalRequestsQuery {
|
||||||
|
workspace_id: &workspace_id,
|
||||||
|
status: None,
|
||||||
|
limit: 10,
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(workspace_approvals.len(), 1);
|
||||||
|
assert_eq!(workspace_approvals[0].approval.id, approval.id);
|
||||||
|
|
||||||
|
let fetched = registry
|
||||||
|
.get_approval_request(&workspace_id, &approval.id)
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(fetched.approval, approval);
|
||||||
|
|
||||||
let decided = registry
|
let decided = registry
|
||||||
.decide_approval_request(DecideApprovalRequest {
|
.decide_approval_request(DecideApprovalRequest {
|
||||||
workspace_id: &workspace_id,
|
workspace_id: &workspace_id,
|
||||||
@@ -575,6 +593,20 @@ async fn manages_approval_request_lifecycle() {
|
|||||||
Some(json!({"id": "lead_123"}))
|
Some(json!({"id": "lead_123"}))
|
||||||
);
|
);
|
||||||
|
|
||||||
|
let completed_by_status = registry
|
||||||
|
.list_approval_requests(ListApprovalRequestsQuery {
|
||||||
|
workspace_id: &workspace_id,
|
||||||
|
status: Some(ApprovalRequestStatus::Completed),
|
||||||
|
limit: 10,
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(completed_by_status.len(), 1);
|
||||||
|
assert_eq!(
|
||||||
|
completed_by_status[0].approval.status,
|
||||||
|
ApprovalRequestStatus::Completed
|
||||||
|
);
|
||||||
|
|
||||||
let pending_after_decision = registry
|
let pending_after_decision = registry
|
||||||
.list_pending_approval_requests_for_agent(&workspace_id, &agent.id)
|
.list_pending_approval_requests_for_agent(&workspace_id, &agent.id)
|
||||||
.await
|
.await
|
||||||
|
|||||||
Reference in New Issue
Block a user