mirror of
https://github.com/fabro-sh/fabro.git
synced 2026-08-28 05:27:41 +00:00
Add POST /runs/{id}/preview endpoint and Preview button on Run page
Adds a new API endpoint for generating sandbox preview URLs (stubbed to return google.com in demo mode). The Run detail page now shows a Preview button that POSTs to this endpoint and opens the returned URL in a new tab. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
9e74e6f913
commit
47f032e535
9 changed files with 216 additions and 2 deletions
|
|
@ -1,11 +1,12 @@
|
|||
import { useEffect } from "react";
|
||||
import { ChevronDownIcon, ChevronRightIcon } from "@heroicons/react/20/solid";
|
||||
import { Menu, MenuButton, MenuItem, MenuItems } from "@headlessui/react";
|
||||
import { Link, Outlet, useLocation } from "react-router";
|
||||
import { Link, Outlet, useFetcher, useLocation } from "react-router";
|
||||
import { statusColors } from "../data/runs";
|
||||
import type { ColumnStatus } from "../data/runs";
|
||||
import { apiJson } from "../api-client";
|
||||
import { formatElapsedSecs, formatDurationSecs } from "../lib/format";
|
||||
import type { RunListItem } from "@qltysh/arc-api-client";
|
||||
import type { RunListItem, PreviewUrlResponse } from "@qltysh/arc-api-client";
|
||||
import type { Route } from "./+types/run-detail";
|
||||
|
||||
const tabs = [
|
||||
|
|
@ -38,6 +39,18 @@ export async function loader({ params }: Route.LoaderArgs) {
|
|||
};
|
||||
}
|
||||
|
||||
export async function action({ params, request }: Route.ActionArgs) {
|
||||
const formData = await request.formData();
|
||||
const port = formData.get("port");
|
||||
const expiresInSecs = formData.get("expires_in_secs");
|
||||
const result = await apiJson<PreviewUrlResponse>(`/runs/${params.id}/preview`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ port: Number(port), expires_in_secs: Number(expiresInSecs) }),
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
export function meta({ data }: Route.MetaArgs) {
|
||||
const run = data?.run;
|
||||
return [{ title: run ? `${run.title} — Arc` : "Run — Arc" }];
|
||||
|
|
@ -47,6 +60,13 @@ export default function RunDetail({ loaderData, params }: Route.ComponentProps)
|
|||
const { run } = loaderData;
|
||||
const { pathname } = useLocation();
|
||||
const basePath = `/runs/${params.id}`;
|
||||
const previewFetcher = useFetcher<PreviewUrlResponse>();
|
||||
|
||||
useEffect(() => {
|
||||
if (previewFetcher.data?.url) {
|
||||
window.open(previewFetcher.data.url, "_blank");
|
||||
}
|
||||
}, [previewFetcher.data]);
|
||||
|
||||
if (!run) {
|
||||
return <p className="py-8 text-center text-sm text-fg-muted">Run not found.</p>;
|
||||
|
|
@ -90,6 +110,23 @@ export default function RunDetail({ loaderData, params }: Route.ComponentProps)
|
|||
</svg>
|
||||
Open PR
|
||||
</button>
|
||||
{run.sandboxId && (
|
||||
<previewFetcher.Form method="post">
|
||||
<input type="hidden" name="port" value="3000" />
|
||||
<input type="hidden" name="expires_in_secs" value="3600" />
|
||||
<button
|
||||
type="submit"
|
||||
disabled={previewFetcher.state !== "idle"}
|
||||
className="flex shrink-0 items-center gap-1.5 rounded-md border border-teal-500/20 px-3 py-1.5 text-sm font-medium text-teal-500 transition-colors hover:border-teal-500/50 hover:bg-teal-500/10 hover:text-fg disabled:opacity-50"
|
||||
>
|
||||
<svg viewBox="0 0 20 20" fill="currentColor" className="size-3.5" aria-hidden="true">
|
||||
<path d="M10 12.5a2.5 2.5 0 1 0 0-5 2.5 2.5 0 0 0 0 5Z" />
|
||||
<path fillRule="evenodd" d="M.664 10.59a1.651 1.651 0 0 1 0-1.186A10.004 10.004 0 0 1 10 3c4.257 0 7.893 2.66 9.336 6.41.147.381.146.804 0 1.186A10.004 10.004 0 0 1 10 17c-4.257 0-7.893-2.66-9.336-6.41ZM14 10a4 4 0 1 1-8 0 4 4 0 0 1 8 0Z" clipRule="evenodd" />
|
||||
</svg>
|
||||
{previewFetcher.state !== "idle" ? "Opening..." : "Preview"}
|
||||
</button>
|
||||
</previewFetcher.Form>
|
||||
)}
|
||||
{run.sandboxId && (
|
||||
<Menu as="div" className="relative">
|
||||
<MenuButton className="flex shrink-0 items-center gap-1.5 rounded-md border border-teal-500/20 px-3 py-1.5 text-sm font-medium text-teal-500 transition-colors hover:border-teal-500/50 hover:bg-teal-500/10 hover:text-fg">
|
||||
|
|
|
|||
|
|
@ -99,6 +99,14 @@ pub async fn steer_run_stub(
|
|||
(StatusCode::OK, Json(serde_json::json!({"accepted": true}))).into_response()
|
||||
}
|
||||
|
||||
pub async fn generate_preview_url_stub(
|
||||
_auth: AuthenticatedService,
|
||||
State(_state): State<Arc<AppState>>,
|
||||
Path(_id): Path<String>,
|
||||
) -> Response {
|
||||
(StatusCode::OK, Json(serde_json::json!({"url": "https://google.com"}))).into_response()
|
||||
}
|
||||
|
||||
pub async fn get_run_status(
|
||||
_auth: AuthenticatedService,
|
||||
State(_state): State<Arc<AppState>>,
|
||||
|
|
|
|||
|
|
@ -79,6 +79,7 @@ pub fn build_router(state: Arc<AppState>, auth_mode: AuthMode) -> Router {
|
|||
.route("/runs/{id}/verifications", get(crate::demo::get_run_verifications))
|
||||
.route("/runs/{id}/configuration", get(crate::demo::get_run_configuration))
|
||||
.route("/runs/{id}/steer", post(crate::demo::steer_run_stub))
|
||||
.route("/runs/{id}/preview", post(crate::demo::generate_preview_url_stub))
|
||||
.route("/workflows", get(crate::demo::list_workflows))
|
||||
.route("/workflows/{name}", get(crate::demo::get_workflow))
|
||||
.route("/workflows/{name}/runs", get(crate::demo::list_workflow_runs).post(crate::demo::trigger_workflow_run_stub))
|
||||
|
|
@ -115,6 +116,7 @@ pub fn build_router(state: Arc<AppState>, auth_mode: AuthMode) -> Router {
|
|||
.route("/runs/{id}/verifications", get(not_implemented))
|
||||
.route("/runs/{id}/configuration", get(not_implemented))
|
||||
.route("/runs/{id}/steer", post(not_implemented))
|
||||
.route("/runs/{id}/preview", post(not_implemented))
|
||||
.route("/workflows", get(not_implemented))
|
||||
.route("/workflows/{name}", get(not_implemented))
|
||||
.route("/workflows/{name}/runs", get(not_implemented).post(not_implemented))
|
||||
|
|
|
|||
|
|
@ -398,6 +398,29 @@ paths:
|
|||
"404":
|
||||
description: Run not found
|
||||
|
||||
/runs/{id}/preview:
|
||||
post:
|
||||
operationId: generatePreviewUrl
|
||||
tags: [Runs]
|
||||
summary: Generate a sandbox preview URL
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/RunId"
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/PreviewUrlRequest"
|
||||
responses:
|
||||
"200":
|
||||
description: Preview URL generated
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/PreviewUrlResponse"
|
||||
"404":
|
||||
description: Run not found
|
||||
|
||||
# ── Workflows ─────────────────────────────────────────────────────────
|
||||
|
||||
/workflows:
|
||||
|
|
@ -1252,6 +1275,25 @@ components:
|
|||
guidance:
|
||||
type: string
|
||||
|
||||
PreviewUrlRequest:
|
||||
type: object
|
||||
required:
|
||||
- port
|
||||
- expires_in_secs
|
||||
properties:
|
||||
port:
|
||||
type: integer
|
||||
expires_in_secs:
|
||||
type: integer
|
||||
|
||||
PreviewUrlResponse:
|
||||
type: object
|
||||
required:
|
||||
- url
|
||||
properties:
|
||||
url:
|
||||
type: string
|
||||
|
||||
# ── Workflow Schemas ─────────────────────────────────────────────────
|
||||
|
||||
WorkflowListItem:
|
||||
|
|
|
|||
|
|
@ -32,6 +32,8 @@ models/file-checkpoint.ts
|
|||
models/file-diff.ts
|
||||
models/history-entry.ts
|
||||
models/index.ts
|
||||
models/preview-url-request.ts
|
||||
models/preview-url-response.ts
|
||||
models/project.ts
|
||||
models/recent-control-result.ts
|
||||
models/retro-list-item.ts
|
||||
|
|
|
|||
|
|
@ -28,6 +28,10 @@ import type { CancelRun200Response } from '../models';
|
|||
// @ts-ignore
|
||||
import type { ErrorResponse } from '../models';
|
||||
// @ts-ignore
|
||||
import type { PreviewUrlRequest } from '../models';
|
||||
// @ts-ignore
|
||||
import type { PreviewUrlResponse } from '../models';
|
||||
// @ts-ignore
|
||||
import type { RunFiles } from '../models';
|
||||
// @ts-ignore
|
||||
import type { RunListItem } from '../models';
|
||||
|
|
@ -92,6 +96,45 @@ export const RunsApiAxiosParamCreator = function (configuration?: Configuration)
|
|||
options: localVarRequestOptions,
|
||||
};
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @summary Generate a sandbox preview URL
|
||||
* @param {string} id
|
||||
* @param {PreviewUrlRequest} previewUrlRequest
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
generatePreviewUrl: async (id: string, previewUrlRequest: PreviewUrlRequest, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {
|
||||
// verify required parameter 'id' is not null or undefined
|
||||
assertParamExists('generatePreviewUrl', 'id', id)
|
||||
// verify required parameter 'previewUrlRequest' is not null or undefined
|
||||
assertParamExists('generatePreviewUrl', 'previewUrlRequest', previewUrlRequest)
|
||||
const localVarPath = `/runs/{id}/preview`
|
||||
.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: 'POST', ...baseOptions, ...options};
|
||||
const localVarHeaderParameter = {} as any;
|
||||
const localVarQueryParameter = {} as any;
|
||||
|
||||
localVarHeaderParameter['Content-Type'] = 'application/json';
|
||||
localVarHeaderParameter['Accept'] = 'application/json';
|
||||
|
||||
setSearchParams(localVarUrlObj, localVarQueryParameter);
|
||||
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
|
||||
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
|
||||
localVarRequestOptions.data = serializeDataIfNeeded(previewUrlRequest, localVarRequestOptions, configuration)
|
||||
|
||||
return {
|
||||
url: toPathString(localVarUrlObj),
|
||||
options: localVarRequestOptions,
|
||||
};
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @summary Get run checkpoint
|
||||
|
|
@ -712,6 +755,20 @@ export const RunsApiFp = function(configuration?: Configuration) {
|
|||
const localVarOperationServerBasePath = operationServerMap['RunsApi.cancelRun']?.[localVarOperationServerIndex]?.url;
|
||||
return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @summary Generate a sandbox preview URL
|
||||
* @param {string} id
|
||||
* @param {PreviewUrlRequest} previewUrlRequest
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
async generatePreviewUrl(id: string, previewUrlRequest: PreviewUrlRequest, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<PreviewUrlResponse>> {
|
||||
const localVarAxiosArgs = await localVarAxiosParamCreator.generatePreviewUrl(id, previewUrlRequest, options);
|
||||
const localVarOperationServerIndex = configuration?.serverIndex ?? 0;
|
||||
const localVarOperationServerBasePath = operationServerMap['RunsApi.generatePreviewUrl']?.[localVarOperationServerIndex]?.url;
|
||||
return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @summary Get run checkpoint
|
||||
|
|
@ -956,6 +1013,17 @@ export const RunsApiFactory = function (configuration?: Configuration, basePath?
|
|||
cancelRun(id: string, options?: RawAxiosRequestConfig): AxiosPromise<CancelRun200Response> {
|
||||
return localVarFp.cancelRun(id, options).then((request) => request(axios, basePath));
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @summary Generate a sandbox preview URL
|
||||
* @param {string} id
|
||||
* @param {PreviewUrlRequest} previewUrlRequest
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
generatePreviewUrl(id: string, previewUrlRequest: PreviewUrlRequest, options?: RawAxiosRequestConfig): AxiosPromise<PreviewUrlResponse> {
|
||||
return localVarFp.generatePreviewUrl(id, previewUrlRequest, options).then((request) => request(axios, basePath));
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @summary Get run checkpoint
|
||||
|
|
@ -1148,6 +1216,18 @@ export class RunsApi extends BaseAPI {
|
|||
return RunsApiFp(this.configuration).cancelRun(id, options).then((request) => request(this.axios, this.basePath));
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @summary Generate a sandbox preview URL
|
||||
* @param {string} id
|
||||
* @param {PreviewUrlRequest} previewUrlRequest
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
public generatePreviewUrl(id: string, previewUrlRequest: PreviewUrlRequest, options?: RawAxiosRequestConfig) {
|
||||
return RunsApiFp(this.configuration).generatePreviewUrl(id, previewUrlRequest, options).then((request) => request(this.axios, this.basePath));
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @summary Get run checkpoint
|
||||
|
|
|
|||
|
|
@ -18,6 +18,8 @@ export * from './execute-query-response';
|
|||
export * from './file-checkpoint';
|
||||
export * from './file-diff';
|
||||
export * from './history-entry';
|
||||
export * from './preview-url-request';
|
||||
export * from './preview-url-response';
|
||||
export * from './project';
|
||||
export * from './recent-control-result';
|
||||
export * from './retro-list-item';
|
||||
|
|
|
|||
21
packages/arc-api-client/src/models/preview-url-request.ts
Normal file
21
packages/arc-api-client/src/models/preview-url-request.ts
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
/**
|
||||
* Arc Run API
|
||||
* HTTP API for managing Arc workflow run executions.
|
||||
*
|
||||
* The version of the OpenAPI document: 0.1.0
|
||||
*
|
||||
*
|
||||
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
||||
* https://openapi-generator.tech
|
||||
* Do not edit the class manually.
|
||||
*/
|
||||
|
||||
|
||||
|
||||
export interface PreviewUrlRequest {
|
||||
'port': number;
|
||||
'expires_in_secs': number;
|
||||
}
|
||||
|
||||
20
packages/arc-api-client/src/models/preview-url-response.ts
Normal file
20
packages/arc-api-client/src/models/preview-url-response.ts
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
/**
|
||||
* Arc Run API
|
||||
* HTTP API for managing Arc workflow run executions.
|
||||
*
|
||||
* The version of the OpenAPI document: 0.1.0
|
||||
*
|
||||
*
|
||||
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
||||
* https://openapi-generator.tech
|
||||
* Do not edit the class manually.
|
||||
*/
|
||||
|
||||
|
||||
|
||||
export interface PreviewUrlResponse {
|
||||
'url': string;
|
||||
}
|
||||
|
||||
Loading…
Add table
Reference in a new issue