trying to run a gitea workflow
Some checks failed
CI / Security Advisory Checks (push) Waiting to run
CI / Rust Blocking Checks (push) Failing after 47s
CI / Web Blocking Checks (push) Failing after 46s
CI / Security Blocking Checks (push) Failing after 8s
CI / Web Advisory Checks (push) Failing after 9s

This commit is contained in:
2026-03-04 22:36:16 -06:00
parent 7438f92502
commit 67a1c02543
25 changed files with 1129 additions and 83 deletions

6
web/knip.json Normal file
View File

@@ -0,0 +1,6 @@
{
"$schema": "https://unpkg.com/knip@latest/schema.json",
"entry": ["src/main.tsx", "vite.config.ts"],
"project": ["src/**/*.{ts,tsx}", "scripts/**/*.js"],
"ignore": ["src/api/**", "dist/**", "node_modules/**"]
}

View File

@@ -6,7 +6,9 @@
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build",
"typecheck": "tsc -b --pretty false",
"lint": "eslint .",
"knip": "npx --yes knip --config knip.json --production",
"preview": "vite preview",
"generate:api": "curl -s http://localhost:8080/api-spec/openapi.json > openapi.json && npx openapi-typescript-codegen --input ./openapi.json --output ./src/api --client axios --useOptions"
},

View File

@@ -1,7 +1,7 @@
import { memo, useEffect } from "react";
import { Link } from "react-router-dom";
import { X, ExternalLink, Loader2 } from "lucide-react";
import { useExecution } from "@/hooks/useExecutions";
import { X, ExternalLink, Loader2, XCircle } from "lucide-react";
import { useExecution, useCancelExecution } from "@/hooks/useExecutions";
import { useExecutionStream } from "@/hooks/useExecutionStream";
import { formatDistanceToNow } from "date-fns";
import type { ExecutionStatus } from "@/api";
@@ -51,6 +51,7 @@ const ExecutionPreviewPanel = memo(function ExecutionPreviewPanel({
}: ExecutionPreviewPanelProps) {
const { data, isLoading, error } = useExecution(executionId);
const execution = data?.data;
const cancelExecution = useCancelExecution();
// Subscribe to real-time updates for this execution
useExecutionStream({ executionId, enabled: true });
@@ -70,6 +71,8 @@ const ExecutionPreviewPanel = memo(function ExecutionPreviewPanel({
execution?.status === "scheduled" ||
execution?.status === "requested";
const isCancellable = isRunning || execution?.status === "canceling";
const startedAt = execution?.started_at
? new Date(execution.started_at)
: null;
@@ -100,6 +103,28 @@ const ExecutionPreviewPanel = memo(function ExecutionPreviewPanel({
)}
</div>
<div className="flex items-center gap-1 flex-shrink-0">
{isCancellable && (
<button
onClick={() => {
if (
window.confirm(
`Are you sure you want to cancel execution #${executionId}?`,
)
) {
cancelExecution.mutate(executionId);
}
}}
disabled={cancelExecution.isPending}
className="p-1.5 text-gray-400 hover:text-red-600 rounded hover:bg-red-50 transition-colors"
title="Cancel execution"
>
{cancelExecution.isPending ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : (
<XCircle className="h-4 w-4" />
)}
</button>
)}
<Link
to={`/executions/${executionId}`}
className="p-1.5 text-gray-400 hover:text-blue-600 rounded hover:bg-gray-100 transition-colors"

View File

@@ -8,6 +8,7 @@ import { ExecutionsService } from "@/api";
import type { ExecutionStatus } from "@/api";
import { OpenAPI } from "@/api/core/OpenAPI";
import { request as __request } from "@/api/core/request";
import type { ExecutionResponse } from "@/api";
interface ExecutionsQueryParams {
page?: number;
@@ -112,6 +113,33 @@ export function useRequestExecution() {
});
}
/**
* Cancel a running or pending execution.
*
* Calls POST /api/v1/executions/{id}/cancel. For workflow executions this
* cascades to all incomplete child task executions on the server side.
*/
export function useCancelExecution() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (executionId: number) => {
const response = await __request(OpenAPI, {
method: "POST",
url: "/api/v1/executions/{id}/cancel",
path: { id: executionId },
mediaType: "application/json",
});
return response as { data: ExecutionResponse };
},
onSuccess: (_data, executionId) => {
// Invalidate the specific execution and the list
queryClient.invalidateQueries({ queryKey: ["executions", executionId] });
queryClient.invalidateQueries({ queryKey: ["executions"] });
},
});
}
export function useChildExecutions(parentId: number | undefined) {
return useQuery({
queryKey: ["executions", { parent: parentId }],

View File

@@ -12,14 +12,14 @@ function formatDuration(ms: number): string {
const remainMins = mins % 60;
return `${hrs}h ${remainMins}m`;
}
import { useExecution } from "@/hooks/useExecutions";
import { useExecution, useCancelExecution } from "@/hooks/useExecutions";
import { useAction } from "@/hooks/useActions";
import { useExecutionStream } from "@/hooks/useExecutionStream";
import { useExecutionHistory } from "@/hooks/useHistory";
import { formatDistanceToNow } from "date-fns";
import { ExecutionStatus } from "@/api";
import { useState, useMemo } from "react";
import { RotateCcw, Loader2 } from "lucide-react";
import { RotateCcw, Loader2, XCircle } from "lucide-react";
import ExecuteActionModal from "@/components/common/ExecuteActionModal";
import EntityHistoryPanel from "@/components/common/EntityHistoryPanel";
import ExecutionArtifactsPanel from "@/components/executions/ExecutionArtifactsPanel";
@@ -123,6 +123,7 @@ export default function ExecutionDetailPage() {
const isWorkflow = !!actionData?.data?.workflow_def;
const [showRerunModal, setShowRerunModal] = useState(false);
const cancelExecution = useCancelExecution();
// Fetch status history for the timeline
const { data: historyData, isLoading: historyLoading } = useExecutionHistory(
@@ -200,6 +201,9 @@ export default function ExecutionDetailPage() {
execution.status === ExecutionStatus.SCHEDULED ||
execution.status === ExecutionStatus.REQUESTED;
const isCancellable =
isRunning || execution.status === ExecutionStatus.CANCELING;
return (
<div className="p-6 max-w-7xl mx-auto">
{/* Header */}
@@ -236,19 +240,44 @@ export default function ExecutionDetailPage() {
</div>
)}
</div>
<button
onClick={() => setShowRerunModal(true)}
disabled={!actionData?.data}
className="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed flex items-center gap-2"
title={
!actionData?.data
? "Loading action details..."
: "Re-run this action with the same parameters"
}
>
<RotateCcw className="h-4 w-4" />
Re-Run
</button>
<div className="flex items-center gap-2">
{isCancellable && (
<button
onClick={() => {
if (
window.confirm(
`Are you sure you want to cancel execution #${execution.id}?`,
)
) {
cancelExecution.mutate(execution.id);
}
}}
disabled={cancelExecution.isPending}
className="px-4 py-2 bg-red-600 text-white rounded hover:bg-red-700 disabled:opacity-50 disabled:cursor-not-allowed flex items-center gap-2"
title="Cancel this execution"
>
{cancelExecution.isPending ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : (
<XCircle className="h-4 w-4" />
)}
Cancel
</button>
)}
<button
onClick={() => setShowRerunModal(true)}
disabled={!actionData?.data}
className="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed flex items-center gap-2"
title={
!actionData?.data
? "Loading action details..."
: "Re-run this action with the same parameters"
}
>
<RotateCcw className="h-4 w-4" />
Re-Run
</button>
</div>
</div>
<p className="text-gray-600 mt-2">
<Link