WIP
This commit is contained in:
@@ -1,32 +1,32 @@
|
||||
/* generated using openapi-typescript-codegen -- do not edit */
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
import type { ApiRequestOptions } from './ApiRequestOptions';
|
||||
|
||||
import type { ApiRequestOptions } from "./ApiRequestOptions";
|
||||
|
||||
type Resolver<T> = (options: ApiRequestOptions) => Promise<T>;
|
||||
type Headers = Record<string, string>;
|
||||
|
||||
export type OpenAPIConfig = {
|
||||
BASE: string;
|
||||
VERSION: string;
|
||||
WITH_CREDENTIALS: boolean;
|
||||
CREDENTIALS: 'include' | 'omit' | 'same-origin';
|
||||
TOKEN?: string | Resolver<string> | undefined;
|
||||
USERNAME?: string | Resolver<string> | undefined;
|
||||
PASSWORD?: string | Resolver<string> | undefined;
|
||||
HEADERS?: Headers | Resolver<Headers> | undefined;
|
||||
ENCODE_PATH?: ((path: string) => string) | undefined;
|
||||
BASE: string;
|
||||
VERSION: string;
|
||||
WITH_CREDENTIALS: boolean;
|
||||
CREDENTIALS: "include" | "omit" | "same-origin";
|
||||
TOKEN?: string | Resolver<string> | undefined;
|
||||
USERNAME?: string | Resolver<string> | undefined;
|
||||
PASSWORD?: string | Resolver<string> | undefined;
|
||||
HEADERS?: Headers | Resolver<Headers> | undefined;
|
||||
ENCODE_PATH?: ((path: string) => string) | undefined;
|
||||
};
|
||||
|
||||
export const OpenAPI: OpenAPIConfig = {
|
||||
BASE: 'http://localhost:8080',
|
||||
VERSION: '0.1.0',
|
||||
WITH_CREDENTIALS: false,
|
||||
CREDENTIALS: 'include',
|
||||
TOKEN: undefined,
|
||||
USERNAME: undefined,
|
||||
PASSWORD: undefined,
|
||||
HEADERS: undefined,
|
||||
ENCODE_PATH: undefined,
|
||||
BASE: "http://localhost:8080",
|
||||
VERSION: "0.1.0",
|
||||
WITH_CREDENTIALS: false,
|
||||
CREDENTIALS: "include",
|
||||
TOKEN: undefined,
|
||||
USERNAME: undefined,
|
||||
PASSWORD: undefined,
|
||||
HEADERS: undefined,
|
||||
ENCODE_PATH: undefined,
|
||||
};
|
||||
|
||||
@@ -1,124 +1,124 @@
|
||||
/* generated using openapi-typescript-codegen -- do not edit */
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
export { ApiError } from './core/ApiError';
|
||||
export { CancelablePromise, CancelError } from './core/CancelablePromise';
|
||||
export { OpenAPI } from './core/OpenAPI';
|
||||
export type { OpenAPIConfig } from './core/OpenAPI';
|
||||
|
||||
export type { ActionResponse } from './models/ActionResponse';
|
||||
export type { ActionSummary } from './models/ActionSummary';
|
||||
export type { ApiResponse_ActionResponse } from './models/ApiResponse_ActionResponse';
|
||||
export type { ApiResponse_CurrentUserResponse } from './models/ApiResponse_CurrentUserResponse';
|
||||
export type { ApiResponse_EnforcementResponse } from './models/ApiResponse_EnforcementResponse';
|
||||
export type { ApiResponse_EventResponse } from './models/ApiResponse_EventResponse';
|
||||
export type { ApiResponse_ExecutionResponse } from './models/ApiResponse_ExecutionResponse';
|
||||
export type { ApiResponse_InquiryResponse } from './models/ApiResponse_InquiryResponse';
|
||||
export type { ApiResponse_KeyResponse } from './models/ApiResponse_KeyResponse';
|
||||
export type { ApiResponse_PackInstallResponse } from './models/ApiResponse_PackInstallResponse';
|
||||
export type { ApiResponse_PackResponse } from './models/ApiResponse_PackResponse';
|
||||
export type { ApiResponse_QueueStatsResponse } from './models/ApiResponse_QueueStatsResponse';
|
||||
export type { ApiResponse_RuleResponse } from './models/ApiResponse_RuleResponse';
|
||||
export type { ApiResponse_SensorResponse } from './models/ApiResponse_SensorResponse';
|
||||
export type { ApiResponse_String } from './models/ApiResponse_String';
|
||||
export type { ApiResponse_TokenResponse } from './models/ApiResponse_TokenResponse';
|
||||
export type { ApiResponse_TriggerResponse } from './models/ApiResponse_TriggerResponse';
|
||||
export type { ApiResponse_WebhookReceiverResponse } from './models/ApiResponse_WebhookReceiverResponse';
|
||||
export type { ApiResponse_WorkflowResponse } from './models/ApiResponse_WorkflowResponse';
|
||||
export type { ChangePasswordRequest } from './models/ChangePasswordRequest';
|
||||
export type { CreateActionRequest } from './models/CreateActionRequest';
|
||||
export type { CreateInquiryRequest } from './models/CreateInquiryRequest';
|
||||
export type { CreateKeyRequest } from './models/CreateKeyRequest';
|
||||
export type { CreatePackRequest } from './models/CreatePackRequest';
|
||||
export type { CreateRuleRequest } from './models/CreateRuleRequest';
|
||||
export type { CreateSensorRequest } from './models/CreateSensorRequest';
|
||||
export type { CreateTriggerRequest } from './models/CreateTriggerRequest';
|
||||
export type { CreateWorkflowRequest } from './models/CreateWorkflowRequest';
|
||||
export type { CurrentUserResponse } from './models/CurrentUserResponse';
|
||||
export { EnforcementCondition } from './models/EnforcementCondition';
|
||||
export type { EnforcementResponse } from './models/EnforcementResponse';
|
||||
export { EnforcementStatus } from './models/EnforcementStatus';
|
||||
export type { EnforcementSummary } from './models/EnforcementSummary';
|
||||
export type { EventResponse } from './models/EventResponse';
|
||||
export type { EventSummary } from './models/EventSummary';
|
||||
export type { ExecutionResponse } from './models/ExecutionResponse';
|
||||
export { ExecutionStatus } from './models/ExecutionStatus';
|
||||
export type { ExecutionSummary } from './models/ExecutionSummary';
|
||||
export type { HealthResponse } from './models/HealthResponse';
|
||||
export type { i64 } from './models/i64';
|
||||
export type { InquiryRespondRequest } from './models/InquiryRespondRequest';
|
||||
export type { InquiryResponse } from './models/InquiryResponse';
|
||||
export { InquiryStatus } from './models/InquiryStatus';
|
||||
export type { InquirySummary } from './models/InquirySummary';
|
||||
export type { InstallPackRequest } from './models/InstallPackRequest';
|
||||
export type { KeyResponse } from './models/KeyResponse';
|
||||
export type { KeySummary } from './models/KeySummary';
|
||||
export type { LoginRequest } from './models/LoginRequest';
|
||||
export { OwnerType } from './models/OwnerType';
|
||||
export type { PackInstallResponse } from './models/PackInstallResponse';
|
||||
export type { PackResponse } from './models/PackResponse';
|
||||
export type { PackSummary } from './models/PackSummary';
|
||||
export type { PackTestExecution } from './models/PackTestExecution';
|
||||
export type { PackTestResult } from './models/PackTestResult';
|
||||
export type { PackTestSummary } from './models/PackTestSummary';
|
||||
export type { PackWorkflowSyncResponse } from './models/PackWorkflowSyncResponse';
|
||||
export type { PackWorkflowValidationResponse } from './models/PackWorkflowValidationResponse';
|
||||
export type { PaginatedResponse_ActionSummary } from './models/PaginatedResponse_ActionSummary';
|
||||
export type { PaginatedResponse_EnforcementSummary } from './models/PaginatedResponse_EnforcementSummary';
|
||||
export type { PaginatedResponse_EventSummary } from './models/PaginatedResponse_EventSummary';
|
||||
export type { PaginatedResponse_ExecutionSummary } from './models/PaginatedResponse_ExecutionSummary';
|
||||
export type { PaginatedResponse_InquirySummary } from './models/PaginatedResponse_InquirySummary';
|
||||
export type { PaginatedResponse_KeySummary } from './models/PaginatedResponse_KeySummary';
|
||||
export type { PaginatedResponse_PackSummary } from './models/PaginatedResponse_PackSummary';
|
||||
export type { PaginatedResponse_PackTestSummary } from './models/PaginatedResponse_PackTestSummary';
|
||||
export type { PaginatedResponse_RuleSummary } from './models/PaginatedResponse_RuleSummary';
|
||||
export type { PaginatedResponse_SensorSummary } from './models/PaginatedResponse_SensorSummary';
|
||||
export type { PaginatedResponse_TriggerSummary } from './models/PaginatedResponse_TriggerSummary';
|
||||
export type { PaginatedResponse_WorkflowSummary } from './models/PaginatedResponse_WorkflowSummary';
|
||||
export type { PaginationMeta } from './models/PaginationMeta';
|
||||
export type { QueueStatsResponse } from './models/QueueStatsResponse';
|
||||
export type { RefreshTokenRequest } from './models/RefreshTokenRequest';
|
||||
export type { RegisterPackRequest } from './models/RegisterPackRequest';
|
||||
export type { RegisterRequest } from './models/RegisterRequest';
|
||||
export type { RuleResponse } from './models/RuleResponse';
|
||||
export type { RuleSummary } from './models/RuleSummary';
|
||||
export type { SensorResponse } from './models/SensorResponse';
|
||||
export type { SensorSummary } from './models/SensorSummary';
|
||||
export type { SuccessResponse } from './models/SuccessResponse';
|
||||
export type { TestCaseResult } from './models/TestCaseResult';
|
||||
export { TestStatus } from './models/TestStatus';
|
||||
export type { TestSuiteResult } from './models/TestSuiteResult';
|
||||
export type { TokenResponse } from './models/TokenResponse';
|
||||
export type { TriggerResponse } from './models/TriggerResponse';
|
||||
export type { TriggerSummary } from './models/TriggerSummary';
|
||||
export type { UpdateActionRequest } from './models/UpdateActionRequest';
|
||||
export type { UpdateInquiryRequest } from './models/UpdateInquiryRequest';
|
||||
export type { UpdateKeyRequest } from './models/UpdateKeyRequest';
|
||||
export type { UpdatePackRequest } from './models/UpdatePackRequest';
|
||||
export type { UpdateRuleRequest } from './models/UpdateRuleRequest';
|
||||
export type { UpdateSensorRequest } from './models/UpdateSensorRequest';
|
||||
export type { UpdateTriggerRequest } from './models/UpdateTriggerRequest';
|
||||
export type { UpdateWorkflowRequest } from './models/UpdateWorkflowRequest';
|
||||
export type { UserInfo } from './models/UserInfo';
|
||||
export type { Value } from './models/Value';
|
||||
export type { WebhookReceiverRequest } from './models/WebhookReceiverRequest';
|
||||
export type { WebhookReceiverResponse } from './models/WebhookReceiverResponse';
|
||||
export type { WorkflowResponse } from './models/WorkflowResponse';
|
||||
export type { WorkflowSummary } from './models/WorkflowSummary';
|
||||
export type { WorkflowSyncResult } from './models/WorkflowSyncResult';
|
||||
export { ApiError } from "./core/ApiError";
|
||||
export { CancelablePromise, CancelError } from "./core/CancelablePromise";
|
||||
export { OpenAPI } from "./core/OpenAPI";
|
||||
export type { OpenAPIConfig } from "./core/OpenAPI";
|
||||
|
||||
export { ActionsService } from './services/ActionsService';
|
||||
export { AuthService } from './services/AuthService';
|
||||
export { EnforcementsService } from './services/EnforcementsService';
|
||||
export { EventsService } from './services/EventsService';
|
||||
export { ExecutionsService } from './services/ExecutionsService';
|
||||
export { HealthService } from './services/HealthService';
|
||||
export { InquiriesService } from './services/InquiriesService';
|
||||
export { PacksService } from './services/PacksService';
|
||||
export { RulesService } from './services/RulesService';
|
||||
export { SecretsService } from './services/SecretsService';
|
||||
export { SensorsService } from './services/SensorsService';
|
||||
export { TriggersService } from './services/TriggersService';
|
||||
export { WebhooksService } from './services/WebhooksService';
|
||||
export { WorkflowsService } from './services/WorkflowsService';
|
||||
export type { ActionResponse } from "./models/ActionResponse";
|
||||
export type { ActionSummary } from "./models/ActionSummary";
|
||||
export type { ApiResponse_ActionResponse } from "./models/ApiResponse_ActionResponse";
|
||||
export type { ApiResponse_CurrentUserResponse } from "./models/ApiResponse_CurrentUserResponse";
|
||||
export type { ApiResponse_EnforcementResponse } from "./models/ApiResponse_EnforcementResponse";
|
||||
export type { ApiResponse_EventResponse } from "./models/ApiResponse_EventResponse";
|
||||
export type { ApiResponse_ExecutionResponse } from "./models/ApiResponse_ExecutionResponse";
|
||||
export type { ApiResponse_InquiryResponse } from "./models/ApiResponse_InquiryResponse";
|
||||
export type { ApiResponse_KeyResponse } from "./models/ApiResponse_KeyResponse";
|
||||
export type { ApiResponse_PackInstallResponse } from "./models/ApiResponse_PackInstallResponse";
|
||||
export type { ApiResponse_PackResponse } from "./models/ApiResponse_PackResponse";
|
||||
export type { ApiResponse_QueueStatsResponse } from "./models/ApiResponse_QueueStatsResponse";
|
||||
export type { ApiResponse_RuleResponse } from "./models/ApiResponse_RuleResponse";
|
||||
export type { ApiResponse_SensorResponse } from "./models/ApiResponse_SensorResponse";
|
||||
export type { ApiResponse_String } from "./models/ApiResponse_String";
|
||||
export type { ApiResponse_TokenResponse } from "./models/ApiResponse_TokenResponse";
|
||||
export type { ApiResponse_TriggerResponse } from "./models/ApiResponse_TriggerResponse";
|
||||
export type { ApiResponse_WebhookReceiverResponse } from "./models/ApiResponse_WebhookReceiverResponse";
|
||||
export type { ApiResponse_WorkflowResponse } from "./models/ApiResponse_WorkflowResponse";
|
||||
export type { ChangePasswordRequest } from "./models/ChangePasswordRequest";
|
||||
export type { CreateActionRequest } from "./models/CreateActionRequest";
|
||||
export type { CreateInquiryRequest } from "./models/CreateInquiryRequest";
|
||||
export type { CreateKeyRequest } from "./models/CreateKeyRequest";
|
||||
export type { CreatePackRequest } from "./models/CreatePackRequest";
|
||||
export type { CreateRuleRequest } from "./models/CreateRuleRequest";
|
||||
export type { CreateSensorRequest } from "./models/CreateSensorRequest";
|
||||
export type { CreateTriggerRequest } from "./models/CreateTriggerRequest";
|
||||
export type { CreateWorkflowRequest } from "./models/CreateWorkflowRequest";
|
||||
export type { CurrentUserResponse } from "./models/CurrentUserResponse";
|
||||
export { EnforcementCondition } from "./models/EnforcementCondition";
|
||||
export type { EnforcementResponse } from "./models/EnforcementResponse";
|
||||
export { EnforcementStatus } from "./models/EnforcementStatus";
|
||||
export type { EnforcementSummary } from "./models/EnforcementSummary";
|
||||
export type { EventResponse } from "./models/EventResponse";
|
||||
export type { EventSummary } from "./models/EventSummary";
|
||||
export type { ExecutionResponse } from "./models/ExecutionResponse";
|
||||
export { ExecutionStatus } from "./models/ExecutionStatus";
|
||||
export type { ExecutionSummary } from "./models/ExecutionSummary";
|
||||
export type { HealthResponse } from "./models/HealthResponse";
|
||||
export type { i64 } from "./models/i64";
|
||||
export type { InquiryRespondRequest } from "./models/InquiryRespondRequest";
|
||||
export type { InquiryResponse } from "./models/InquiryResponse";
|
||||
export { InquiryStatus } from "./models/InquiryStatus";
|
||||
export type { InquirySummary } from "./models/InquirySummary";
|
||||
export type { InstallPackRequest } from "./models/InstallPackRequest";
|
||||
export type { KeyResponse } from "./models/KeyResponse";
|
||||
export type { KeySummary } from "./models/KeySummary";
|
||||
export type { LoginRequest } from "./models/LoginRequest";
|
||||
export { OwnerType } from "./models/OwnerType";
|
||||
export type { PackInstallResponse } from "./models/PackInstallResponse";
|
||||
export type { PackResponse } from "./models/PackResponse";
|
||||
export type { PackSummary } from "./models/PackSummary";
|
||||
export type { PackTestExecution } from "./models/PackTestExecution";
|
||||
export type { PackTestResult } from "./models/PackTestResult";
|
||||
export type { PackTestSummary } from "./models/PackTestSummary";
|
||||
export type { PackWorkflowSyncResponse } from "./models/PackWorkflowSyncResponse";
|
||||
export type { PackWorkflowValidationResponse } from "./models/PackWorkflowValidationResponse";
|
||||
export type { PaginatedResponse_ActionSummary } from "./models/PaginatedResponse_ActionSummary";
|
||||
export type { PaginatedResponse_EnforcementSummary } from "./models/PaginatedResponse_EnforcementSummary";
|
||||
export type { PaginatedResponse_EventSummary } from "./models/PaginatedResponse_EventSummary";
|
||||
export type { PaginatedResponse_ExecutionSummary } from "./models/PaginatedResponse_ExecutionSummary";
|
||||
export type { PaginatedResponse_InquirySummary } from "./models/PaginatedResponse_InquirySummary";
|
||||
export type { PaginatedResponse_KeySummary } from "./models/PaginatedResponse_KeySummary";
|
||||
export type { PaginatedResponse_PackSummary } from "./models/PaginatedResponse_PackSummary";
|
||||
export type { PaginatedResponse_PackTestSummary } from "./models/PaginatedResponse_PackTestSummary";
|
||||
export type { PaginatedResponse_RuleSummary } from "./models/PaginatedResponse_RuleSummary";
|
||||
export type { PaginatedResponse_SensorSummary } from "./models/PaginatedResponse_SensorSummary";
|
||||
export type { PaginatedResponse_TriggerSummary } from "./models/PaginatedResponse_TriggerSummary";
|
||||
export type { PaginatedResponse_WorkflowSummary } from "./models/PaginatedResponse_WorkflowSummary";
|
||||
export type { PaginationMeta } from "./models/PaginationMeta";
|
||||
export type { QueueStatsResponse } from "./models/QueueStatsResponse";
|
||||
export type { RefreshTokenRequest } from "./models/RefreshTokenRequest";
|
||||
export type { RegisterPackRequest } from "./models/RegisterPackRequest";
|
||||
export type { RegisterRequest } from "./models/RegisterRequest";
|
||||
export type { RuleResponse } from "./models/RuleResponse";
|
||||
export type { RuleSummary } from "./models/RuleSummary";
|
||||
export type { SensorResponse } from "./models/SensorResponse";
|
||||
export type { SensorSummary } from "./models/SensorSummary";
|
||||
export type { SuccessResponse } from "./models/SuccessResponse";
|
||||
export type { TestCaseResult } from "./models/TestCaseResult";
|
||||
export { TestStatus } from "./models/TestStatus";
|
||||
export type { TestSuiteResult } from "./models/TestSuiteResult";
|
||||
export type { TokenResponse } from "./models/TokenResponse";
|
||||
export type { TriggerResponse } from "./models/TriggerResponse";
|
||||
export type { TriggerSummary } from "./models/TriggerSummary";
|
||||
export type { UpdateActionRequest } from "./models/UpdateActionRequest";
|
||||
export type { UpdateInquiryRequest } from "./models/UpdateInquiryRequest";
|
||||
export type { UpdateKeyRequest } from "./models/UpdateKeyRequest";
|
||||
export type { UpdatePackRequest } from "./models/UpdatePackRequest";
|
||||
export type { UpdateRuleRequest } from "./models/UpdateRuleRequest";
|
||||
export type { UpdateSensorRequest } from "./models/UpdateSensorRequest";
|
||||
export type { UpdateTriggerRequest } from "./models/UpdateTriggerRequest";
|
||||
export type { UpdateWorkflowRequest } from "./models/UpdateWorkflowRequest";
|
||||
export type { UserInfo } from "./models/UserInfo";
|
||||
export type { Value } from "./models/Value";
|
||||
export type { WebhookReceiverRequest } from "./models/WebhookReceiverRequest";
|
||||
export type { WebhookReceiverResponse } from "./models/WebhookReceiverResponse";
|
||||
export type { WorkflowResponse } from "./models/WorkflowResponse";
|
||||
export type { WorkflowSummary } from "./models/WorkflowSummary";
|
||||
export type { WorkflowSyncResult } from "./models/WorkflowSyncResult";
|
||||
|
||||
export { ActionsService } from "./services/ActionsService";
|
||||
export { AuthService } from "./services/AuthService";
|
||||
export { EnforcementsService } from "./services/EnforcementsService";
|
||||
export { EventsService } from "./services/EventsService";
|
||||
export { ExecutionsService } from "./services/ExecutionsService";
|
||||
export { HealthService } from "./services/HealthService";
|
||||
export { InquiriesService } from "./services/InquiriesService";
|
||||
export { PacksService } from "./services/PacksService";
|
||||
export { RulesService } from "./services/RulesService";
|
||||
export { SecretsService } from "./services/SecretsService";
|
||||
export { SensorsService } from "./services/SensorsService";
|
||||
export { TriggersService } from "./services/TriggersService";
|
||||
export { WebhooksService } from "./services/WebhooksService";
|
||||
export { WorkflowsService } from "./services/WorkflowsService";
|
||||
|
||||
612
web/src/components/executions/ExecutionArtifactsPanel.tsx
Normal file
612
web/src/components/executions/ExecutionArtifactsPanel.tsx
Normal file
@@ -0,0 +1,612 @@
|
||||
import { useState, useMemo, useEffect, useCallback } from "react";
|
||||
import { formatDistanceToNow } from "date-fns";
|
||||
import {
|
||||
ChevronDown,
|
||||
ChevronRight,
|
||||
FileText,
|
||||
FileImage,
|
||||
File,
|
||||
BarChart3,
|
||||
Link as LinkIcon,
|
||||
Table2,
|
||||
Package,
|
||||
Loader2,
|
||||
Download,
|
||||
Eye,
|
||||
X,
|
||||
} from "lucide-react";
|
||||
import {
|
||||
useExecutionArtifacts,
|
||||
useArtifact,
|
||||
type ArtifactSummary,
|
||||
type ArtifactType,
|
||||
} from "@/hooks/useArtifacts";
|
||||
import { OpenAPI } from "@/api/core/OpenAPI";
|
||||
|
||||
interface ExecutionArtifactsPanelProps {
|
||||
executionId: number;
|
||||
/** Whether the execution is still running (enables polling) */
|
||||
isRunning?: boolean;
|
||||
defaultCollapsed?: boolean;
|
||||
}
|
||||
|
||||
function getArtifactTypeIcon(type: ArtifactType) {
|
||||
switch (type) {
|
||||
case "file_text":
|
||||
return <FileText className="h-4 w-4 text-blue-500" />;
|
||||
case "file_image":
|
||||
return <FileImage className="h-4 w-4 text-purple-500" />;
|
||||
case "file_binary":
|
||||
return <File className="h-4 w-4 text-gray-500" />;
|
||||
case "file_datatable":
|
||||
return <Table2 className="h-4 w-4 text-green-500" />;
|
||||
case "progress":
|
||||
return <BarChart3 className="h-4 w-4 text-amber-500" />;
|
||||
case "url":
|
||||
return <LinkIcon className="h-4 w-4 text-cyan-500" />;
|
||||
case "other":
|
||||
default:
|
||||
return <Package className="h-4 w-4 text-gray-400" />;
|
||||
}
|
||||
}
|
||||
|
||||
function getArtifactTypeBadge(type: ArtifactType): {
|
||||
label: string;
|
||||
classes: string;
|
||||
} {
|
||||
switch (type) {
|
||||
case "file_text":
|
||||
return { label: "Text File", classes: "bg-blue-100 text-blue-800" };
|
||||
case "file_image":
|
||||
return { label: "Image", classes: "bg-purple-100 text-purple-800" };
|
||||
case "file_binary":
|
||||
return { label: "Binary", classes: "bg-gray-100 text-gray-800" };
|
||||
case "file_datatable":
|
||||
return { label: "Data Table", classes: "bg-green-100 text-green-800" };
|
||||
case "progress":
|
||||
return { label: "Progress", classes: "bg-amber-100 text-amber-800" };
|
||||
case "url":
|
||||
return { label: "URL", classes: "bg-cyan-100 text-cyan-800" };
|
||||
case "other":
|
||||
default:
|
||||
return { label: "Other", classes: "bg-gray-100 text-gray-700" };
|
||||
}
|
||||
}
|
||||
|
||||
function formatBytes(bytes: number | null): string {
|
||||
if (bytes == null || bytes === 0) return "—";
|
||||
if (bytes < 1024) return `${bytes} B`;
|
||||
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
||||
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
||||
}
|
||||
|
||||
/** Download the latest version of an artifact using a fetch with auth token. */
|
||||
async function downloadArtifact(artifactId: number, artifactRef: string) {
|
||||
const token = localStorage.getItem("access_token");
|
||||
const url = `${OpenAPI.BASE}/api/v1/artifacts/${artifactId}/download`;
|
||||
|
||||
const response = await fetch(url, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
console.error(`Download failed: ${response.status} ${response.statusText}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Extract filename from Content-Disposition header or fall back to ref
|
||||
const disposition = response.headers.get("Content-Disposition");
|
||||
let filename = artifactRef.replace(/\./g, "_") + ".bin";
|
||||
if (disposition) {
|
||||
const match = disposition.match(/filename="?([^"]+)"?/);
|
||||
if (match) filename = match[1];
|
||||
}
|
||||
|
||||
const blob = await response.blob();
|
||||
const blobUrl = URL.createObjectURL(blob);
|
||||
const a = document.createElement("a");
|
||||
a.href = blobUrl;
|
||||
a.download = filename;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(blobUrl);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Text File Artifact Detail
|
||||
// ============================================================================
|
||||
|
||||
interface TextFileDetailProps {
|
||||
artifactId: number;
|
||||
artifactName: string | null;
|
||||
isRunning?: boolean;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
function TextFileDetail({
|
||||
artifactId,
|
||||
artifactName,
|
||||
isRunning = false,
|
||||
onClose,
|
||||
}: TextFileDetailProps) {
|
||||
const [content, setContent] = useState<string | null>(null);
|
||||
const [loadError, setLoadError] = useState<string | null>(null);
|
||||
const [isLoadingContent, setIsLoadingContent] = useState(true);
|
||||
|
||||
const fetchContent = useCallback(async () => {
|
||||
const token = localStorage.getItem("access_token");
|
||||
const url = `${OpenAPI.BASE}/api/v1/artifacts/${artifactId}/download`;
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
});
|
||||
if (!response.ok) {
|
||||
setLoadError(`HTTP ${response.status}: ${response.statusText}`);
|
||||
setIsLoadingContent(false);
|
||||
return;
|
||||
}
|
||||
const text = await response.text();
|
||||
setContent(text);
|
||||
setLoadError(null);
|
||||
} catch (e) {
|
||||
setLoadError(e instanceof Error ? e.message : "Unknown error");
|
||||
} finally {
|
||||
setIsLoadingContent(false);
|
||||
}
|
||||
}, [artifactId]);
|
||||
|
||||
// Initial load
|
||||
useEffect(() => {
|
||||
fetchContent();
|
||||
}, [fetchContent]);
|
||||
|
||||
// Poll while running to pick up new file versions
|
||||
useEffect(() => {
|
||||
if (!isRunning) return;
|
||||
const interval = setInterval(fetchContent, 3000);
|
||||
return () => clearInterval(interval);
|
||||
}, [isRunning, fetchContent]);
|
||||
|
||||
return (
|
||||
<div className="border border-blue-200 bg-blue-50/50 rounded-lg p-4 mt-2">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<h4 className="text-sm font-semibold text-blue-900 flex items-center gap-2">
|
||||
<FileText className="h-4 w-4" />
|
||||
{artifactName ?? "Text File"}
|
||||
</h4>
|
||||
<div className="flex items-center gap-2">
|
||||
{isRunning && (
|
||||
<div className="flex items-center gap-1 text-xs text-blue-600">
|
||||
<Loader2 className="h-3 w-3 animate-spin" />
|
||||
<span>Live</span>
|
||||
</div>
|
||||
)}
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="text-gray-400 hover:text-gray-600 p-1 rounded"
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{isLoadingContent && (
|
||||
<div className="flex items-center gap-2 py-2 text-sm text-gray-500">
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
Loading content…
|
||||
</div>
|
||||
)}
|
||||
|
||||
{loadError && (
|
||||
<p className="text-xs text-red-600 italic">Error: {loadError}</p>
|
||||
)}
|
||||
|
||||
{!isLoadingContent && !loadError && content !== null && (
|
||||
<pre className="max-h-64 overflow-y-auto bg-gray-900 text-gray-100 rounded p-3 text-xs font-mono whitespace-pre-wrap break-all">
|
||||
{content || <span className="text-gray-500 italic">(empty)</span>}
|
||||
</pre>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Progress Artifact Detail
|
||||
// ============================================================================
|
||||
|
||||
interface ProgressDetailProps {
|
||||
artifactId: number;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
function ProgressDetail({ artifactId, onClose }: ProgressDetailProps) {
|
||||
const { data: artifactData, isLoading } = useArtifact(artifactId);
|
||||
const artifact = artifactData?.data;
|
||||
|
||||
const progressEntries = useMemo(() => {
|
||||
if (!artifact?.data || !Array.isArray(artifact.data)) return [];
|
||||
return artifact.data as Array<Record<string, unknown>>;
|
||||
}, [artifact]);
|
||||
|
||||
const latestEntry =
|
||||
progressEntries.length > 0
|
||||
? progressEntries[progressEntries.length - 1]
|
||||
: null;
|
||||
const latestPercent =
|
||||
latestEntry && typeof latestEntry.percent === "number"
|
||||
? latestEntry.percent
|
||||
: null;
|
||||
|
||||
return (
|
||||
<div className="border border-amber-200 bg-amber-50/50 rounded-lg p-4 mt-2">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<h4 className="text-sm font-semibold text-amber-900 flex items-center gap-2">
|
||||
<BarChart3 className="h-4 w-4" />
|
||||
{artifact?.name ?? "Progress"}
|
||||
</h4>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="text-gray-400 hover:text-gray-600 p-1 rounded"
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{isLoading && (
|
||||
<div className="flex items-center gap-2 py-2 text-sm text-gray-500">
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
Loading progress…
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!isLoading && latestPercent != null && (
|
||||
<div className="mb-3">
|
||||
<div className="flex items-center justify-between text-xs text-gray-600 mb-1">
|
||||
<span>
|
||||
{latestEntry?.message
|
||||
? String(latestEntry.message)
|
||||
: `${latestPercent}%`}
|
||||
</span>
|
||||
<span className="font-mono">{latestPercent}%</span>
|
||||
</div>
|
||||
<div className="w-full bg-gray-200 rounded-full h-2.5">
|
||||
<div
|
||||
className="bg-amber-500 h-2.5 rounded-full transition-all duration-300"
|
||||
style={{ width: `${Math.min(latestPercent, 100)}%` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!isLoading && progressEntries.length > 0 && (
|
||||
<div className="max-h-48 overflow-y-auto">
|
||||
<table className="w-full text-xs">
|
||||
<thead>
|
||||
<tr className="text-left text-gray-500 border-b border-amber-200">
|
||||
<th className="pb-1 pr-2">#</th>
|
||||
<th className="pb-1 pr-2">%</th>
|
||||
<th className="pb-1 pr-2">Message</th>
|
||||
<th className="pb-1">Time</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{progressEntries.map((entry, idx) => (
|
||||
<tr
|
||||
key={idx}
|
||||
className="border-b border-amber-100 last:border-0"
|
||||
>
|
||||
<td className="py-1 pr-2 text-gray-400 font-mono">
|
||||
{typeof entry.iteration === "number"
|
||||
? entry.iteration
|
||||
: idx + 1}
|
||||
</td>
|
||||
<td className="py-1 pr-2 font-mono">
|
||||
{typeof entry.percent === "number"
|
||||
? `${entry.percent}%`
|
||||
: "—"}
|
||||
</td>
|
||||
<td className="py-1 pr-2 text-gray-700 truncate max-w-[200px]">
|
||||
{entry.message ? String(entry.message) : "—"}
|
||||
</td>
|
||||
<td className="py-1 text-gray-400 whitespace-nowrap">
|
||||
{entry.timestamp
|
||||
? formatDistanceToNow(new Date(String(entry.timestamp)), {
|
||||
addSuffix: true,
|
||||
})
|
||||
: "—"}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!isLoading && progressEntries.length === 0 && (
|
||||
<p className="text-xs text-gray-500 italic">No progress entries yet.</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Main Panel
|
||||
// ============================================================================
|
||||
|
||||
export default function ExecutionArtifactsPanel({
|
||||
executionId,
|
||||
isRunning = false,
|
||||
defaultCollapsed = false,
|
||||
}: ExecutionArtifactsPanelProps) {
|
||||
const [isCollapsed, setIsCollapsed] = useState(defaultCollapsed);
|
||||
const [expandedProgressId, setExpandedProgressId] = useState<number | null>(
|
||||
null,
|
||||
);
|
||||
const [expandedTextFileId, setExpandedTextFileId] = useState<number | null>(
|
||||
null,
|
||||
);
|
||||
|
||||
const { data, isLoading, error } = useExecutionArtifacts(
|
||||
executionId,
|
||||
isRunning,
|
||||
);
|
||||
|
||||
const artifacts: ArtifactSummary[] = useMemo(() => {
|
||||
return data?.data ?? [];
|
||||
}, [data]);
|
||||
|
||||
const summary = useMemo(() => {
|
||||
const total = artifacts.length;
|
||||
const files = artifacts.filter((a) =>
|
||||
["file_text", "file_binary", "file_image", "file_datatable"].includes(
|
||||
a.type,
|
||||
),
|
||||
).length;
|
||||
const progress = artifacts.filter((a) => a.type === "progress").length;
|
||||
const other = total - files - progress;
|
||||
return { total, files, progress, other };
|
||||
}, [artifacts]);
|
||||
|
||||
// Don't render anything if there are no artifacts and we're not loading
|
||||
if (!isLoading && artifacts.length === 0 && !error) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="bg-white shadow rounded-lg">
|
||||
{/* Header */}
|
||||
<button
|
||||
onClick={() => setIsCollapsed(!isCollapsed)}
|
||||
className="w-full flex items-center justify-between p-6 text-left hover:bg-gray-50 rounded-lg transition-colors"
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
{isCollapsed ? (
|
||||
<ChevronRight className="h-5 w-5 text-gray-400" />
|
||||
) : (
|
||||
<ChevronDown className="h-5 w-5 text-gray-400" />
|
||||
)}
|
||||
<Package className="h-5 w-5 text-indigo-500" />
|
||||
<h2 className="text-xl font-semibold">Artifacts</h2>
|
||||
{!isLoading && (
|
||||
<span className="text-sm text-gray-500">
|
||||
({summary.total} artifact{summary.total !== 1 ? "s" : ""})
|
||||
</span>
|
||||
)}
|
||||
{isRunning && (
|
||||
<div className="flex items-center gap-1.5 text-xs text-blue-600">
|
||||
<Loader2 className="h-3 w-3 animate-spin" />
|
||||
<span>Live</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Summary badges */}
|
||||
<div className="flex items-center gap-2">
|
||||
{summary.files > 0 && (
|
||||
<span className="inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800">
|
||||
<FileText className="h-3 w-3" />
|
||||
{summary.files}
|
||||
</span>
|
||||
)}
|
||||
{summary.progress > 0 && (
|
||||
<span className="inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-xs font-medium bg-amber-100 text-amber-800">
|
||||
<BarChart3 className="h-3 w-3" />
|
||||
{summary.progress}
|
||||
</span>
|
||||
)}
|
||||
{summary.other > 0 && (
|
||||
<span className="inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-xs font-medium bg-gray-100 text-gray-700">
|
||||
{summary.other}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</button>
|
||||
|
||||
{/* Content */}
|
||||
{!isCollapsed && (
|
||||
<div className="px-6 pb-6">
|
||||
{isLoading && (
|
||||
<div className="flex items-center justify-center py-8">
|
||||
<Loader2 className="h-5 w-5 animate-spin text-gray-400" />
|
||||
<span className="ml-2 text-sm text-gray-500">
|
||||
Loading artifacts…
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{error && (
|
||||
<div className="bg-red-50 border border-red-200 text-red-700 px-4 py-3 rounded text-sm">
|
||||
Error loading artifacts:{" "}
|
||||
{error instanceof Error ? error.message : "Unknown error"}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!isLoading && !error && artifacts.length > 0 && (
|
||||
<div className="space-y-2">
|
||||
{/* Column headers */}
|
||||
<div className="grid grid-cols-12 gap-3 px-3 py-2 text-xs font-medium text-gray-500 uppercase tracking-wider border-b border-gray-100">
|
||||
<div className="col-span-1">Type</div>
|
||||
<div className="col-span-4">Name</div>
|
||||
<div className="col-span-3">Ref</div>
|
||||
<div className="col-span-1">Size</div>
|
||||
<div className="col-span-2">Created</div>
|
||||
<div className="col-span-1">Actions</div>
|
||||
</div>
|
||||
|
||||
{/* Artifact rows */}
|
||||
{artifacts.map((artifact) => {
|
||||
const badge = getArtifactTypeBadge(artifact.type);
|
||||
const isProgress = artifact.type === "progress";
|
||||
const isTextFile = artifact.type === "file_text";
|
||||
const isFile = [
|
||||
"file_text",
|
||||
"file_binary",
|
||||
"file_image",
|
||||
"file_datatable",
|
||||
].includes(artifact.type);
|
||||
const isProgressExpanded = expandedProgressId === artifact.id;
|
||||
const isTextExpanded = expandedTextFileId === artifact.id;
|
||||
|
||||
return (
|
||||
<div key={artifact.id}>
|
||||
<div
|
||||
className={`grid grid-cols-12 gap-3 px-3 py-3 rounded-lg hover:bg-gray-50 transition-colors items-center ${
|
||||
isProgress || isTextFile ? "cursor-pointer" : ""
|
||||
}`}
|
||||
onClick={() => {
|
||||
if (isProgress) {
|
||||
setExpandedProgressId(
|
||||
isProgressExpanded ? null : artifact.id,
|
||||
);
|
||||
setExpandedTextFileId(null);
|
||||
} else if (isTextFile) {
|
||||
setExpandedTextFileId(
|
||||
isTextExpanded ? null : artifact.id,
|
||||
);
|
||||
setExpandedProgressId(null);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{/* Type icon */}
|
||||
<div className="col-span-1 flex items-center">
|
||||
{getArtifactTypeIcon(artifact.type)}
|
||||
</div>
|
||||
|
||||
{/* Name */}
|
||||
<div className="col-span-4 flex items-center gap-2 min-w-0">
|
||||
<span
|
||||
className="text-sm font-medium text-gray-900 truncate"
|
||||
title={artifact.name ?? artifact.ref}
|
||||
>
|
||||
{artifact.name ?? artifact.ref}
|
||||
</span>
|
||||
<span
|
||||
className={`inline-flex px-1.5 py-0.5 rounded text-[10px] font-medium flex-shrink-0 ${badge.classes}`}
|
||||
>
|
||||
{badge.label}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Ref */}
|
||||
<div className="col-span-3 min-w-0">
|
||||
<span
|
||||
className="text-xs text-gray-500 truncate block font-mono"
|
||||
title={artifact.ref}
|
||||
>
|
||||
{artifact.ref}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Size */}
|
||||
<div className="col-span-1 text-sm text-gray-500">
|
||||
{formatBytes(artifact.size_bytes)}
|
||||
</div>
|
||||
|
||||
{/* Created */}
|
||||
<div className="col-span-2 text-xs text-gray-500">
|
||||
{formatDistanceToNow(new Date(artifact.created), {
|
||||
addSuffix: true,
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Actions */}
|
||||
<div
|
||||
className="col-span-1 flex items-center gap-1"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
{isProgress && (
|
||||
<button
|
||||
onClick={() => {
|
||||
setExpandedProgressId(
|
||||
isProgressExpanded ? null : artifact.id,
|
||||
);
|
||||
setExpandedTextFileId(null);
|
||||
}}
|
||||
className="p-1 rounded hover:bg-gray-200 text-gray-500 hover:text-amber-600"
|
||||
title="View progress"
|
||||
>
|
||||
<Eye className="h-4 w-4" />
|
||||
</button>
|
||||
)}
|
||||
{isTextFile && (
|
||||
<button
|
||||
onClick={() => {
|
||||
setExpandedTextFileId(
|
||||
isTextExpanded ? null : artifact.id,
|
||||
);
|
||||
setExpandedProgressId(null);
|
||||
}}
|
||||
className="p-1 rounded hover:bg-gray-200 text-gray-500 hover:text-blue-600"
|
||||
title="Preview text content"
|
||||
>
|
||||
<Eye className="h-4 w-4" />
|
||||
</button>
|
||||
)}
|
||||
{isFile && (
|
||||
<button
|
||||
onClick={() =>
|
||||
downloadArtifact(artifact.id, artifact.ref)
|
||||
}
|
||||
className="p-1 rounded hover:bg-gray-200 text-gray-500 hover:text-blue-600"
|
||||
title="Download latest version"
|
||||
>
|
||||
<Download className="h-4 w-4" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Expanded progress detail */}
|
||||
{isProgress && isProgressExpanded && (
|
||||
<div className="px-3">
|
||||
<ProgressDetail
|
||||
artifactId={artifact.id}
|
||||
onClose={() => setExpandedProgressId(null)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Expanded text file preview */}
|
||||
{isTextFile && isTextExpanded && (
|
||||
<div className="px-3">
|
||||
<TextFileDetail
|
||||
artifactId={artifact.id}
|
||||
artifactName={artifact.name}
|
||||
isRunning={isRunning}
|
||||
onClose={() => setExpandedTextFileId(null)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -175,17 +175,20 @@ export default function MainLayout() {
|
||||
});
|
||||
const [showUserMenu, setShowUserMenu] = useState(false);
|
||||
|
||||
// Persist collapsed state to localStorage
|
||||
// Persist collapsed state to localStorage and close user menu when expanding
|
||||
useEffect(() => {
|
||||
localStorage.setItem("sidebar-collapsed", isCollapsed.toString());
|
||||
}, [isCollapsed]);
|
||||
|
||||
// Close user menu when expanding sidebar
|
||||
useEffect(() => {
|
||||
if (!isCollapsed) {
|
||||
setShowUserMenu(false);
|
||||
}
|
||||
}, [isCollapsed]);
|
||||
const handleToggleCollapse = () => {
|
||||
setIsCollapsed((prev) => {
|
||||
const next = !prev;
|
||||
if (!next) {
|
||||
setShowUserMenu(false);
|
||||
}
|
||||
return next;
|
||||
});
|
||||
};
|
||||
|
||||
const handleLogout = () => {
|
||||
logout();
|
||||
@@ -248,7 +251,7 @@ export default function MainLayout() {
|
||||
{/* Toggle Button */}
|
||||
<div className="px-4 py-3">
|
||||
<button
|
||||
onClick={() => setIsCollapsed(!isCollapsed)}
|
||||
onClick={handleToggleCollapse}
|
||||
className="flex items-center w-full px-3 py-2 text-gray-400 hover:text-white hover:bg-gray-800 rounded-md transition-colors whitespace-nowrap"
|
||||
title={isCollapsed ? "Expand sidebar" : "Collapse sidebar"}
|
||||
>
|
||||
|
||||
131
web/src/hooks/useArtifacts.ts
Normal file
131
web/src/hooks/useArtifacts.ts
Normal file
@@ -0,0 +1,131 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { OpenAPI } from "@/api/core/OpenAPI";
|
||||
import { request as __request } from "@/api/core/request";
|
||||
|
||||
// Artifact types matching the backend ArtifactType enum
|
||||
export type ArtifactType =
|
||||
| "file_binary"
|
||||
| "file_datatable"
|
||||
| "file_image"
|
||||
| "file_text"
|
||||
| "other"
|
||||
| "progress"
|
||||
| "url";
|
||||
|
||||
export type OwnerType = "system" | "pack" | "action" | "sensor" | "rule";
|
||||
|
||||
export type RetentionPolicyType = "versions" | "days" | "hours" | "minutes";
|
||||
|
||||
export interface ArtifactSummary {
|
||||
id: number;
|
||||
ref: string;
|
||||
type: ArtifactType;
|
||||
name: string | null;
|
||||
content_type: string | null;
|
||||
size_bytes: number | null;
|
||||
execution: number | null;
|
||||
scope: OwnerType;
|
||||
owner: string;
|
||||
created: string;
|
||||
updated: string;
|
||||
}
|
||||
|
||||
export interface ArtifactResponse {
|
||||
id: number;
|
||||
ref: string;
|
||||
scope: OwnerType;
|
||||
owner: string;
|
||||
type: ArtifactType;
|
||||
retention_policy: RetentionPolicyType;
|
||||
retention_limit: number;
|
||||
name: string | null;
|
||||
description: string | null;
|
||||
content_type: string | null;
|
||||
size_bytes: number | null;
|
||||
execution: number | null;
|
||||
data?: unknown;
|
||||
created: string;
|
||||
updated: string;
|
||||
}
|
||||
|
||||
export interface ArtifactVersionSummary {
|
||||
id: number;
|
||||
version: number;
|
||||
content_type: string | null;
|
||||
size_bytes: number | null;
|
||||
created_by: string | null;
|
||||
created: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch all artifacts for a given execution ID.
|
||||
*
|
||||
* Uses the GET /api/v1/executions/{execution_id}/artifacts endpoint.
|
||||
*/
|
||||
export function useExecutionArtifacts(
|
||||
executionId: number | undefined,
|
||||
isRunning = false,
|
||||
) {
|
||||
return useQuery({
|
||||
queryKey: ["artifacts", "execution", executionId],
|
||||
queryFn: async () => {
|
||||
const response = await __request<{ data: ArtifactSummary[] }>(OpenAPI, {
|
||||
method: "GET",
|
||||
url: "/api/v1/executions/{execution_id}/artifacts",
|
||||
path: {
|
||||
execution_id: executionId!,
|
||||
},
|
||||
});
|
||||
return response;
|
||||
},
|
||||
enabled: !!executionId,
|
||||
staleTime: isRunning ? 3000 : 10000,
|
||||
refetchInterval: isRunning ? 3000 : 10000,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch a single artifact by ID (includes data field for progress artifacts).
|
||||
*/
|
||||
export function useArtifact(id: number | undefined) {
|
||||
return useQuery({
|
||||
queryKey: ["artifacts", id],
|
||||
queryFn: async () => {
|
||||
const response = await __request<{ data: ArtifactResponse }>(OpenAPI, {
|
||||
method: "GET",
|
||||
url: "/api/v1/artifacts/{id}",
|
||||
path: {
|
||||
id: id!,
|
||||
},
|
||||
});
|
||||
return response;
|
||||
},
|
||||
enabled: !!id,
|
||||
staleTime: 3000,
|
||||
refetchInterval: 3000,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch versions for a given artifact ID.
|
||||
*/
|
||||
export function useArtifactVersions(artifactId: number | undefined) {
|
||||
return useQuery({
|
||||
queryKey: ["artifacts", artifactId, "versions"],
|
||||
queryFn: async () => {
|
||||
const response = await __request<{ data: ArtifactVersionSummary[] }>(
|
||||
OpenAPI,
|
||||
{
|
||||
method: "GET",
|
||||
url: "/api/v1/artifacts/{id}/versions",
|
||||
path: {
|
||||
id: artifactId!,
|
||||
},
|
||||
},
|
||||
);
|
||||
return response;
|
||||
},
|
||||
enabled: !!artifactId,
|
||||
staleTime: 10000,
|
||||
});
|
||||
}
|
||||
@@ -23,6 +23,7 @@ import { RotateCcw, Loader2 } from "lucide-react";
|
||||
import ExecuteActionModal from "@/components/common/ExecuteActionModal";
|
||||
import EntityHistoryPanel from "@/components/common/EntityHistoryPanel";
|
||||
import WorkflowTasksPanel from "@/components/common/WorkflowTasksPanel";
|
||||
import ExecutionArtifactsPanel from "@/components/executions/ExecutionArtifactsPanel";
|
||||
|
||||
const getStatusColor = (status: string) => {
|
||||
switch (status) {
|
||||
@@ -539,6 +540,14 @@ export default function ExecutionDetailPage() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Artifacts */}
|
||||
<div className="mt-6">
|
||||
<ExecutionArtifactsPanel
|
||||
executionId={execution.id}
|
||||
isRunning={isRunning}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Change History */}
|
||||
<div className="mt-6">
|
||||
<EntityHistoryPanel
|
||||
|
||||
Reference in New Issue
Block a user