mirror of
https://github.com/fabro-sh/fabro.git
synced 2026-08-28 05:27:41 +00:00
feat(api): expose workflow graph source as raw DOT
Add GET /api/v1/runs/{id}/graph/source returning text/vnd.graphviz so
the run graph can be inspected as the original Graphviz DOT in addition
to the rendered SVG. Refactor get_graph to share DOT loading with the
new handler. The web run-graph view gains a Graph | Source toggle that
lazy-loads and displays the DOT with a copy button.
This commit is contained in:
parent
cdc43e05fc
commit
fd1087fe2d
7 changed files with 311 additions and 49 deletions
|
|
@ -91,6 +91,13 @@ export function useRunGraph(id: string | undefined, direction?: "LR" | "TB") {
|
|||
);
|
||||
}
|
||||
|
||||
export function useRunGraphSource(id: string | undefined, enabled: boolean) {
|
||||
return useSWR<string | null>(
|
||||
id && enabled ? queryKeys.runs.graphSource(id) : null,
|
||||
apiNullableTextFetcher,
|
||||
);
|
||||
}
|
||||
|
||||
export function useRunLogs(id: string | undefined, refreshInterval?: number) {
|
||||
return useSWR<string | null>(
|
||||
id ? queryKeys.runs.logs(id) : null,
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@ export const queryKeys = {
|
|||
stages: (id: string) => `/api/v1/runs/${pathSegment(id)}/stages`,
|
||||
graph: (id: string, direction?: "LR" | "TB") =>
|
||||
withQuery(`/api/v1/runs/${pathSegment(id)}/graph`, { direction }),
|
||||
graphSource: (id: string) => `/api/v1/runs/${pathSegment(id)}/graph/source`,
|
||||
settings: (id: string) => `/api/v1/runs/${pathSegment(id)}/settings`,
|
||||
logs: (id: string) => `/api/v1/runs/${pathSegment(id)}/logs`,
|
||||
billing: (id: string) => `/api/v1/runs/${pathSegment(id)}/billing`,
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useParams } from "react-router";
|
||||
import { graphTheme } from "../lib/graph-theme";
|
||||
import { useRunGraph, useRunStages } from "../lib/queries";
|
||||
import { useRunGraph, useRunGraphSource, useRunStages } from "../lib/queries";
|
||||
import { CopyButton } from "../components/ui";
|
||||
import { LoadingState } from "../components/state";
|
||||
import { StageSidebar } from "../components/stage-sidebar";
|
||||
import {
|
||||
GRAPH_DEFAULT_ZOOM_INDEX,
|
||||
|
|
@ -69,11 +71,15 @@ function stripGraphTitle(svg: SVGSVGElement) {
|
|||
title.remove();
|
||||
}
|
||||
|
||||
type View = "graph" | "source";
|
||||
|
||||
export default function RunGraph() {
|
||||
const { id } = useParams();
|
||||
const [direction, setDirection] = useState<Direction>("LR");
|
||||
const [view, setView] = useState<View>("graph");
|
||||
const stagesQuery = useRunStages(id);
|
||||
const graphQuery = useRunGraph(id, direction);
|
||||
const sourceQuery = useRunGraphSource(id, view === "source");
|
||||
const stages = useMemo(
|
||||
() => mapRunStagesToSidebarStages(stagesQuery.data),
|
||||
[stagesQuery.data],
|
||||
|
|
@ -184,35 +190,94 @@ export default function RunGraph() {
|
|||
<div className="flex gap-6">
|
||||
<StageSidebar stages={stages} runId={id!} activeLink="graph" />
|
||||
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="graph-svg relative rounded-md border border-line bg-panel-alt">
|
||||
<GraphToolbar
|
||||
direction={direction}
|
||||
setDirection={setDirection}
|
||||
fitToWindow={fitToWindow}
|
||||
zoomIndex={zoomIndex}
|
||||
setZoomIndex={setZoomIndex}
|
||||
/>
|
||||
<div className="min-w-0 flex-1 space-y-3">
|
||||
<ViewToggle view={view} setView={setView} />
|
||||
|
||||
{view === "graph" ? (
|
||||
<div className="graph-svg relative rounded-md border border-line bg-panel-alt">
|
||||
<GraphToolbar
|
||||
direction={direction}
|
||||
setDirection={setDirection}
|
||||
fitToWindow={fitToWindow}
|
||||
zoomIndex={zoomIndex}
|
||||
setZoomIndex={setZoomIndex}
|
||||
/>
|
||||
|
||||
<div
|
||||
ref={containerRef}
|
||||
className="overflow-hidden p-6"
|
||||
style={{ cursor: dragState.current ? "grabbing" : "grab" }}
|
||||
onPointerDown={onPointerDown}
|
||||
onPointerMove={onPointerMove}
|
||||
onPointerUp={onPointerUp}
|
||||
onPointerCancel={onPointerUp}
|
||||
>
|
||||
<div
|
||||
ref={innerRef}
|
||||
className="flex items-center justify-center"
|
||||
style={{ transform: `translate(${pan.x}px, ${pan.y}px) scale(${zoom / 100})`, transformOrigin: "center center" }}
|
||||
ref={containerRef}
|
||||
className="overflow-hidden p-6"
|
||||
style={{ cursor: dragState.current ? "grabbing" : "grab" }}
|
||||
onPointerDown={onPointerDown}
|
||||
onPointerMove={onPointerMove}
|
||||
onPointerUp={onPointerUp}
|
||||
onPointerCancel={onPointerUp}
|
||||
>
|
||||
<p className="text-sm text-fg-muted">Loading diagram...</p>
|
||||
<div
|
||||
ref={innerRef}
|
||||
className="flex items-center justify-center"
|
||||
style={{ transform: `translate(${pan.x}px, ${pan.y}px) scale(${zoom / 100})`, transformOrigin: "center center" }}
|
||||
>
|
||||
<p className="text-sm text-fg-muted">Loading diagram...</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<SourcePanel source={sourceQuery.data} loading={sourceQuery.data === undefined && !sourceQuery.error} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ViewToggle({ view, setView }: { view: View; setView: (v: View) => void }) {
|
||||
const btn =
|
||||
"rounded px-3 py-1.5 text-xs font-medium transition-colors";
|
||||
return (
|
||||
<div role="group" aria-label="Graph view" className="inline-flex rounded-md border border-line bg-panel/80 p-0.5">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setView("graph")}
|
||||
aria-pressed={view === "graph"}
|
||||
className={`${btn} ${view === "graph" ? "bg-overlay text-teal-500" : "text-fg-muted hover:text-fg-3"}`}
|
||||
>
|
||||
Graph
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setView("source")}
|
||||
aria-pressed={view === "source"}
|
||||
className={`${btn} ${view === "source" ? "bg-overlay text-teal-500" : "text-fg-muted hover:text-fg-3"}`}
|
||||
>
|
||||
Source
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function SourcePanel({ source, loading }: { source: string | null | undefined; loading: boolean }) {
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="rounded-md border border-line bg-panel-alt p-4">
|
||||
<LoadingState label="Loading graph source…" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
if (!source) {
|
||||
return (
|
||||
<div className="rounded-md border border-line bg-panel-alt p-4">
|
||||
<p className="text-sm text-fg-muted">No graph source available for this run.</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div className="rounded-md border border-line bg-panel-alt">
|
||||
<div className="flex items-center justify-between gap-3 border-b border-line px-3 py-2">
|
||||
<span className="font-mono text-xs text-fg-muted">workflow.fabro</span>
|
||||
<CopyButton value={source} label="Copy graph source" />
|
||||
</div>
|
||||
<pre className="max-h-[70vh] overflow-auto whitespace-pre p-4 font-mono text-xs leading-5 text-fg-2">
|
||||
{source}
|
||||
</pre>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1049,6 +1049,28 @@ paths:
|
|||
schema:
|
||||
$ref: "#/components/schemas/ErrorResponse"
|
||||
|
||||
/api/v1/runs/{id}/graph/source:
|
||||
get:
|
||||
operationId: retrieveRunGraphSource
|
||||
tags: [Runs]
|
||||
summary: Retrieve Graphviz DOT source
|
||||
description: Returns the raw Graphviz DOT source for the workflow graph (the contents of the workflow's `.fabro` file).
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/RunId"
|
||||
responses:
|
||||
"200":
|
||||
description: Graphviz DOT source
|
||||
content:
|
||||
text/vnd.graphviz:
|
||||
schema:
|
||||
type: string
|
||||
"404":
|
||||
description: Run not found
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ErrorResponse"
|
||||
|
||||
/api/v1/runs/{id}/checkpoint:
|
||||
get:
|
||||
operationId: retrieveRunCheckpoint
|
||||
|
|
|
|||
|
|
@ -414,15 +414,27 @@ pub(crate) async fn unpause_stub(
|
|||
(StatusCode::OK, Json(serde_json::json!({"id": id, "status": "running", "created_at": "2026-03-06T14:30:00Z"}))).into_response()
|
||||
}
|
||||
|
||||
const DEMO_GRAPH_DOT: &str = "digraph demo {\n graph [goal=\"Demo\"]\n rankdir=LR\n start [shape=Mdiamond, label=\"Start\"]\n detect [label=\"Detect\\nDrift\"]\n exit [shape=Msquare, label=\"Exit\"]\n propose [label=\"Propose\\nChanges\"]\n review [label=\"Review\\nChanges\"]\n apply [label=\"Apply\\nChanges\"]\n start -> detect\n detect -> exit [label=\"No drift\"]\n detect -> propose [label=\"Drift found\"]\n propose -> review\n review -> propose [label=\"Revise\"]\n review -> apply [label=\"Accept\"]\n apply -> exit\n}";
|
||||
|
||||
pub(crate) async fn get_run_graph(
|
||||
_auth: AuthenticatedService,
|
||||
State(_state): State<Arc<AppState>>,
|
||||
Path(_id): Path<String>,
|
||||
) -> Response {
|
||||
// Use graphviz to render the demo DOT source
|
||||
let dot_source = "digraph demo {\n graph [goal=\"Demo\"]\n rankdir=LR\n start [shape=Mdiamond, label=\"Start\"]\n detect [label=\"Detect\\nDrift\"]\n exit [shape=Msquare, label=\"Exit\"]\n propose [label=\"Propose\\nChanges\"]\n review [label=\"Review\\nChanges\"]\n apply [label=\"Apply\\nChanges\"]\n start -> detect\n detect -> exit [label=\"No drift\"]\n detect -> propose [label=\"Drift found\"]\n propose -> review\n review -> propose [label=\"Revise\"]\n review -> apply [label=\"Accept\"]\n apply -> exit\n}";
|
||||
crate::server::render_graph_bytes(DEMO_GRAPH_DOT).await
|
||||
}
|
||||
|
||||
crate::server::render_graph_bytes(dot_source).await
|
||||
pub(crate) async fn get_run_graph_source(
|
||||
_auth: AuthenticatedService,
|
||||
State(_state): State<Arc<AppState>>,
|
||||
Path(_id): Path<String>,
|
||||
) -> Response {
|
||||
(
|
||||
StatusCode::OK,
|
||||
[("content-type", "text/vnd.graphviz")],
|
||||
DEMO_GRAPH_DOT,
|
||||
)
|
||||
.into_response()
|
||||
}
|
||||
|
||||
pub(crate) async fn list_secrets(
|
||||
|
|
|
|||
|
|
@ -1138,6 +1138,7 @@ fn demo_routes() -> Router<Arc<AppState>> {
|
|||
.route("/runs/{id}/pause", post(demo::pause_stub))
|
||||
.route("/runs/{id}/unpause", post(demo::unpause_stub))
|
||||
.route("/runs/{id}/graph", get(demo::get_run_graph))
|
||||
.route("/runs/{id}/graph/source", get(demo::get_run_graph_source))
|
||||
.route("/runs/{id}/stages", get(demo::get_run_stages))
|
||||
.route("/runs/{id}/artifacts", get(demo::list_run_artifacts_stub))
|
||||
.route("/runs/{id}/files", get(demo::list_run_files_stub))
|
||||
|
|
@ -1238,6 +1239,7 @@ fn real_routes() -> Router<Arc<AppState>> {
|
|||
.route("/runs/{id}/timeline", get(run_timeline))
|
||||
.route("/runs/{id}/unarchive", post(unarchive_run))
|
||||
.route("/runs/{id}/graph", get(get_graph))
|
||||
.route("/runs/{id}/graph/source", get(get_graph_source))
|
||||
.route("/runs/{id}/stages", get(list_run_stages))
|
||||
.route("/runs/{id}/artifacts", get(list_run_artifacts))
|
||||
.route("/runs/{id}/files", get(list_run_files))
|
||||
|
|
@ -7931,6 +7933,33 @@ struct GraphParams {
|
|||
direction: Option<String>,
|
||||
}
|
||||
|
||||
async fn load_run_dot_source(state: &AppState, id: &RunId) -> Result<String, Response> {
|
||||
let live_dot_source = {
|
||||
let runs = state.runs.lock().expect("runs lock poisoned");
|
||||
runs.get(id).map(|managed_run| managed_run.dot_source.clone())
|
||||
};
|
||||
|
||||
let dot_source = if let Some(dot) = live_dot_source.filter(|d| !d.is_empty()) {
|
||||
Some(dot)
|
||||
} else {
|
||||
match state.store.open_run_reader(id).await {
|
||||
Ok(run_store) => match run_store.state().await {
|
||||
Ok(run_state) => run_state.graph_source,
|
||||
Err(err) => {
|
||||
return Err(
|
||||
ApiError::new(StatusCode::BAD_GATEWAY, err.to_string()).into_response(),
|
||||
);
|
||||
}
|
||||
},
|
||||
Err(_) => return Err(ApiError::not_found("Run not found.").into_response()),
|
||||
}
|
||||
};
|
||||
|
||||
dot_source.ok_or_else(|| {
|
||||
ApiError::new(StatusCode::NOT_FOUND, "Graph not found.").into_response()
|
||||
})
|
||||
}
|
||||
|
||||
async fn get_graph(
|
||||
_auth: AuthenticatedService,
|
||||
State(state): State<Arc<AppState>>,
|
||||
|
|
@ -7942,28 +7971,9 @@ async fn get_graph(
|
|||
Err(response) => return response,
|
||||
};
|
||||
|
||||
let live_dot_source = {
|
||||
let runs = state.runs.lock().expect("runs lock poisoned");
|
||||
runs.get(&id)
|
||||
.map(|managed_run| managed_run.dot_source.clone())
|
||||
};
|
||||
|
||||
let dot_source = if let Some(dot) = live_dot_source.filter(|d| !d.is_empty()) {
|
||||
Some(dot)
|
||||
} else {
|
||||
match state.store.open_run_reader(&id).await {
|
||||
Ok(run_store) => match run_store.state().await {
|
||||
Ok(run_state) => run_state.graph_source,
|
||||
Err(err) => {
|
||||
return ApiError::new(StatusCode::BAD_GATEWAY, err.to_string()).into_response();
|
||||
}
|
||||
},
|
||||
Err(_) => return ApiError::not_found("Run not found.").into_response(),
|
||||
}
|
||||
};
|
||||
|
||||
let Some(dot) = dot_source else {
|
||||
return ApiError::new(StatusCode::NOT_FOUND, "Graph not found.").into_response();
|
||||
let dot = match load_run_dot_source(&state, &id).await {
|
||||
Ok(dot) => dot,
|
||||
Err(response) => return response,
|
||||
};
|
||||
|
||||
let dot = match params.direction.as_deref() {
|
||||
|
|
@ -7977,6 +7987,22 @@ async fn get_graph(
|
|||
render_graph_bytes(&dot).await
|
||||
}
|
||||
|
||||
async fn get_graph_source(
|
||||
_auth: AuthenticatedService,
|
||||
State(state): State<Arc<AppState>>,
|
||||
Path(id): Path<String>,
|
||||
) -> Response {
|
||||
let id = match parse_run_id_path(&id) {
|
||||
Ok(id) => id,
|
||||
Err(response) => return response,
|
||||
};
|
||||
|
||||
match load_run_dot_source(&state, &id).await {
|
||||
Ok(dot) => (StatusCode::OK, [("content-type", "text/vnd.graphviz")], dot).into_response(),
|
||||
Err(response) => response,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[expect(
|
||||
clippy::disallowed_methods,
|
||||
|
|
@ -11784,6 +11810,7 @@ slug = "fabro"
|
|||
(Method::POST, format!("/runs/{run_id}/archive")),
|
||||
(Method::POST, format!("/runs/{run_id}/unarchive")),
|
||||
(Method::GET, format!("/runs/{run_id}/graph")),
|
||||
(Method::GET, format!("/runs/{run_id}/graph/source")),
|
||||
(Method::GET, format!("/runs/{run_id}/stages")),
|
||||
(Method::GET, format!("/runs/{run_id}/artifacts")),
|
||||
(Method::GET, format!("/runs/{run_id}/files")),
|
||||
|
|
@ -12120,6 +12147,60 @@ slug = "fabro"
|
|||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn get_graph_source_returns_dot() {
|
||||
let state = create_app_state();
|
||||
let app = build_router(Arc::clone(&state), AuthMode::Disabled);
|
||||
|
||||
let req = Request::builder()
|
||||
.method("POST")
|
||||
.uri(api("/runs"))
|
||||
.header("content-type", "application/json")
|
||||
.body(Body::from(
|
||||
serde_json::to_string(&serde_json::json!({
|
||||
"version": 1,
|
||||
"cwd": "/tmp",
|
||||
"target": {
|
||||
"identifier": "workflow.fabro",
|
||||
"path": "workflow.fabro",
|
||||
},
|
||||
"workflows": {
|
||||
"workflow.fabro": {
|
||||
"source": MINIMAL_DOT,
|
||||
"files": {},
|
||||
},
|
||||
},
|
||||
}))
|
||||
.unwrap(),
|
||||
))
|
||||
.unwrap();
|
||||
|
||||
let response = app.clone().oneshot(req).await.unwrap();
|
||||
let body = body_json(response.into_body()).await;
|
||||
let run_id = body["id"].as_str().unwrap().parse::<RunId>().unwrap();
|
||||
|
||||
let req = Request::builder()
|
||||
.method("GET")
|
||||
.uri(api(&format!("/runs/{run_id}/graph/source")))
|
||||
.body(Body::empty())
|
||||
.unwrap();
|
||||
|
||||
let response = app.oneshot(req).await.unwrap();
|
||||
let response = checked_response!(response, StatusCode::OK).await;
|
||||
|
||||
let content_type = response
|
||||
.headers()
|
||||
.get("content-type")
|
||||
.expect("content-type header should be present")
|
||||
.to_str()
|
||||
.unwrap();
|
||||
assert_eq!(content_type, "text/vnd.graphviz");
|
||||
|
||||
let bytes = to_bytes(response.into_body(), usize::MAX).await.unwrap();
|
||||
let dot = String::from_utf8(bytes.to_vec()).unwrap();
|
||||
assert_eq!(dot, MINIMAL_DOT);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn render_graph_from_manifest_returns_svg() {
|
||||
let app = test_app_with();
|
||||
|
|
|
|||
|
|
@ -786,6 +786,46 @@ export const RunsApiAxiosParamCreator = function (configuration?: Configuration)
|
|||
options: localVarRequestOptions,
|
||||
};
|
||||
},
|
||||
/**
|
||||
* Returns the raw Graphviz DOT source for the workflow graph (the contents of the workflow\'s `.fabro` file).
|
||||
* @summary Retrieve Graphviz DOT source
|
||||
* @param {string} id Unique run identifier (ULID).
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
retrieveRunGraphSource: async (id: string, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {
|
||||
// verify required parameter 'id' is not null or undefined
|
||||
assertParamExists('retrieveRunGraphSource', 'id', id)
|
||||
const localVarPath = `/api/v1/runs/{id}/graph/source`
|
||||
.replace(`{${"id"}}`, encodeURIComponent(String(id)));
|
||||
// use dummy base URL string because the URL constructor only accepts absolute URLs.
|
||||
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
|
||||
let baseOptions;
|
||||
if (configuration) {
|
||||
baseOptions = configuration.baseOptions;
|
||||
}
|
||||
|
||||
const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};
|
||||
const localVarHeaderParameter = {} as any;
|
||||
const localVarQueryParameter = {} as any;
|
||||
|
||||
// authentication SessionCookie required
|
||||
|
||||
// authentication BearerAuth required
|
||||
// http bearer authentication required
|
||||
await setBearerAuthToObject(localVarHeaderParameter, configuration)
|
||||
|
||||
localVarHeaderParameter['Accept'] = 'text/vnd.graphviz,application/json';
|
||||
|
||||
setSearchParams(localVarUrlObj, localVarQueryParameter);
|
||||
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
|
||||
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
|
||||
|
||||
return {
|
||||
url: toPathString(localVarUrlObj),
|
||||
options: localVarRequestOptions,
|
||||
};
|
||||
},
|
||||
/**
|
||||
* Creates a new run from an earlier checkpoint of a terminal source run, archives the source run, and records `run.superseded_by` on the source after archive succeeds. Returns 207 when the new run was created but the source archive step failed.
|
||||
* @summary Rewind Run
|
||||
|
|
@ -1230,6 +1270,19 @@ export const RunsApiFp = function(configuration?: Configuration) {
|
|||
const localVarOperationServerBasePath = operationServerMap['RunsApi.retrieveRunGraph']?.[localVarOperationServerIndex]?.url;
|
||||
return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);
|
||||
},
|
||||
/**
|
||||
* Returns the raw Graphviz DOT source for the workflow graph (the contents of the workflow\'s `.fabro` file).
|
||||
* @summary Retrieve Graphviz DOT source
|
||||
* @param {string} id Unique run identifier (ULID).
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
async retrieveRunGraphSource(id: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<string>> {
|
||||
const localVarAxiosArgs = await localVarAxiosParamCreator.retrieveRunGraphSource(id, options);
|
||||
const localVarOperationServerIndex = configuration?.serverIndex ?? 0;
|
||||
const localVarOperationServerBasePath = operationServerMap['RunsApi.retrieveRunGraphSource']?.[localVarOperationServerIndex]?.url;
|
||||
return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);
|
||||
},
|
||||
/**
|
||||
* Creates a new run from an earlier checkpoint of a terminal source run, archives the source run, and records `run.superseded_by` on the source after archive succeeds. Returns 207 when the new run was created but the source archive step failed.
|
||||
* @summary Rewind Run
|
||||
|
|
@ -1483,6 +1536,16 @@ export const RunsApiFactory = function (configuration?: Configuration, basePath?
|
|||
retrieveRunGraph(id: string, options?: RawAxiosRequestConfig): AxiosPromise<string> {
|
||||
return localVarFp.retrieveRunGraph(id, options).then((request) => request(axios, basePath));
|
||||
},
|
||||
/**
|
||||
* Returns the raw Graphviz DOT source for the workflow graph (the contents of the workflow\'s `.fabro` file).
|
||||
* @summary Retrieve Graphviz DOT source
|
||||
* @param {string} id Unique run identifier (ULID).
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
retrieveRunGraphSource(id: string, options?: RawAxiosRequestConfig): AxiosPromise<string> {
|
||||
return localVarFp.retrieveRunGraphSource(id, options).then((request) => request(axios, basePath));
|
||||
},
|
||||
/**
|
||||
* Creates a new run from an earlier checkpoint of a terminal source run, archives the source run, and records `run.superseded_by` on the source after archive succeeds. Returns 207 when the new run was created but the source archive step failed.
|
||||
* @summary Rewind Run
|
||||
|
|
@ -1736,6 +1799,17 @@ export class RunsApi extends BaseAPI {
|
|||
return RunsApiFp(this.configuration).retrieveRunGraph(id, options).then((request) => request(this.axios, this.basePath));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the raw Graphviz DOT source for the workflow graph (the contents of the workflow\'s `.fabro` file).
|
||||
* @summary Retrieve Graphviz DOT source
|
||||
* @param {string} id Unique run identifier (ULID).
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
public retrieveRunGraphSource(id: string, options?: RawAxiosRequestConfig) {
|
||||
return RunsApiFp(this.configuration).retrieveRunGraphSource(id, options).then((request) => request(this.axios, this.basePath));
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new run from an earlier checkpoint of a terminal source run, archives the source run, and records `run.superseded_by` on the source after archive succeeds. Returns 207 when the new run was created but the source archive step failed.
|
||||
* @summary Rewind Run
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue