mirror of
https://github.com/fabro-sh/fabro.git
synced 2026-10-09 03:20:56 +00:00
271 lines
8.6 KiB
TypeScript
271 lines
8.6 KiB
TypeScript
import { useMemo, useState } from "react";
|
|
import { useParams } from "react-router";
|
|
import type { WorkflowSettings } from "@qltysh/fabro-api-client";
|
|
import { CollapsibleFile } from "../components/collapsible-file";
|
|
import { StageSidebar } from "../components/stage-sidebar";
|
|
import {
|
|
Badge,
|
|
Count,
|
|
Mono,
|
|
Muted,
|
|
Panel,
|
|
PanelSkeleton,
|
|
Row,
|
|
type SettingsView,
|
|
Toggle,
|
|
ViewToggle,
|
|
} from "../components/settings-panel";
|
|
import { useRunSettings, useRunStages } from "../lib/queries";
|
|
import { mapRunStagesToSidebarStages } from "../lib/stage-sidebar";
|
|
import {
|
|
type UnknownRecord,
|
|
getArray,
|
|
getBool,
|
|
getObject,
|
|
getString,
|
|
objectKeyCount,
|
|
} from "../lib/run-settings-snapshot";
|
|
|
|
export const handle = { wide: true };
|
|
|
|
export default function RunSettingsPage() {
|
|
const { id } = useParams();
|
|
const stagesQuery = useRunStages(id);
|
|
const settingsQuery = useRunSettings<WorkflowSettings>(id);
|
|
const stages = useMemo(
|
|
() => mapRunStagesToSidebarStages(stagesQuery.data),
|
|
[stagesQuery.data],
|
|
);
|
|
const [view, setView] = useState<SettingsView>("settings");
|
|
const snapshot = settingsQuery.data;
|
|
|
|
return (
|
|
<div className="flex gap-6">
|
|
<StageSidebar stages={stages} runId={id!} activeLink="settings" />
|
|
|
|
<div className="min-w-0 flex-1 space-y-6">
|
|
<PageIntro view={view} setView={setView} />
|
|
|
|
{!snapshot ? (
|
|
<>
|
|
<PanelSkeleton />
|
|
<PanelSkeleton />
|
|
<PanelSkeleton />
|
|
<PanelSkeleton />
|
|
</>
|
|
) : view === "settings" ? (
|
|
<>
|
|
<WorkflowPanel snapshot={snapshot} />
|
|
<EnvironmentPanel snapshot={snapshot} />
|
|
<GitPanel snapshot={snapshot} />
|
|
<ArtifactsPanel snapshot={snapshot} />
|
|
</>
|
|
) : (
|
|
<CollapsibleFile
|
|
file={{
|
|
name: "settings.json",
|
|
contents: JSON.stringify(snapshot, null, 2),
|
|
lang: "json",
|
|
}}
|
|
/>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function PageIntro({
|
|
view,
|
|
setView,
|
|
}: {
|
|
view: SettingsView;
|
|
setView: (v: SettingsView) => void;
|
|
}) {
|
|
return (
|
|
<div className="flex items-start justify-between gap-6">
|
|
<div>
|
|
<h2 className="text-base font-semibold text-fg">Run settings</h2>
|
|
<p className="mt-1 text-sm/6 text-fg-3">
|
|
Frozen settings snapshot used by this run.
|
|
</p>
|
|
</div>
|
|
<ViewToggle view={view} setView={setView} />
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function WorkflowPanel({ snapshot }: { snapshot: WorkflowSettings }) {
|
|
const workflow = getObject(snapshot, "workflow");
|
|
const run = getObject(snapshot, "run");
|
|
const name = getString(workflow, "name");
|
|
const description = getString(workflow, "description");
|
|
const goal = getObject(run, "goal");
|
|
return (
|
|
<Panel title="Workflow">
|
|
<Row title="Name" help="Workflow identifier used by this run.">
|
|
{name ? <Mono>{name}</Mono> : <Muted>Unnamed</Muted>}
|
|
</Row>
|
|
{description ? (
|
|
<Row title="Description">
|
|
<span className="text-fg-2">{description}</span>
|
|
</Row>
|
|
) : null}
|
|
<Row title="Goal" help="Goal text or file used for this run.">
|
|
<GoalValue goal={goal} />
|
|
</Row>
|
|
<Row title="Inputs" help="Run input variables.">
|
|
<Count
|
|
n={objectKeyCount(run, "inputs")}
|
|
singular="value"
|
|
plural="values"
|
|
/>
|
|
</Row>
|
|
<Row title="Metadata" help="Combined run / workflow / project labels.">
|
|
<Count
|
|
n={
|
|
objectKeyCount(run, "metadata") +
|
|
objectKeyCount(workflow, "metadata") +
|
|
objectKeyCount(getObject(snapshot, "project"), "metadata")
|
|
}
|
|
singular="label"
|
|
plural="labels"
|
|
/>
|
|
</Row>
|
|
</Panel>
|
|
);
|
|
}
|
|
|
|
function EnvironmentPanel({ snapshot }: { snapshot: WorkflowSettings }) {
|
|
const environment = getObject(getObject(snapshot, "run"), "environment");
|
|
const provider = getString(environment, "provider");
|
|
const environmentId = getString(environment, "id");
|
|
const image = getObject(environment, "image");
|
|
const imageRef = getString(image, "ref");
|
|
const lifecycle = getObject(environment, "lifecycle");
|
|
return (
|
|
<Panel title="Environment">
|
|
<Row title="Name" help="Named environment selected for this run.">
|
|
{environmentId ? <Mono>{environmentId}</Mono> : <Muted>Default</Muted>}
|
|
</Row>
|
|
<Row title="Provider" help="Sandbox provider for this environment.">
|
|
{provider ? <Badge>{provider}</Badge> : <Muted>Unknown</Muted>}
|
|
</Row>
|
|
{imageRef ? (
|
|
<Row title="Image" help="Image or snapshot used for this environment.">
|
|
<Mono>{imageRef}</Mono>
|
|
</Row>
|
|
) : null}
|
|
<Row title="Preserve" help="Keep the sandbox after the run completes.">
|
|
<Toggle on={getBool(lifecycle, "preserve") ?? false} />
|
|
</Row>
|
|
<Row title="Stop on terminal" help="Stop the sandbox when the run reaches a terminal state.">
|
|
<Toggle on={getBool(lifecycle, "stop_on_terminal") ?? false} />
|
|
</Row>
|
|
<Row title="Env" help="Environment variables injected into the sandbox.">
|
|
<Count n={objectKeyCount(environment, "env")} singular="var" plural="vars" />
|
|
</Row>
|
|
</Panel>
|
|
);
|
|
}
|
|
|
|
function GitPanel({ snapshot }: { snapshot: WorkflowSettings }) {
|
|
const run = getObject(snapshot, "run");
|
|
const author = getObject(getObject(run, "git"), "author");
|
|
const authorName = getString(author, "name");
|
|
const authorEmail = getString(author, "email");
|
|
const scm = getObject(run, "scm");
|
|
const scmProvider = getString(scm, "provider");
|
|
const scmOwner = getString(scm, "owner");
|
|
const scmRepo = getString(scm, "repository");
|
|
const repoLabel = scmOwner && scmRepo ? `${scmOwner}/${scmRepo}` : scmRepo ?? scmOwner;
|
|
const pr = getObject(run, "pull_request");
|
|
return (
|
|
<Panel title="Git">
|
|
<Row title="Author" help="Identity used for commits made during the run.">
|
|
<AuthorValue name={authorName} email={authorEmail} />
|
|
</Row>
|
|
<Row title="SCM" help="Source-control provider and repository.">
|
|
{scmProvider ? (
|
|
<span className="inline-flex flex-wrap items-center gap-x-2 gap-y-1">
|
|
<Badge>{scmProvider}</Badge>
|
|
{repoLabel ? <Mono>{repoLabel}</Mono> : null}
|
|
</span>
|
|
) : (
|
|
<Muted>None</Muted>
|
|
)}
|
|
</Row>
|
|
<Row title="Pull request" help="Whether this run opens a PR on completion.">
|
|
<PullRequestValue pr={pr} />
|
|
</Row>
|
|
</Panel>
|
|
);
|
|
}
|
|
|
|
function ArtifactsPanel({ snapshot }: { snapshot: WorkflowSettings }) {
|
|
const artifacts = getObject(getObject(snapshot, "run"), "artifacts");
|
|
const include = getArray(artifacts, "include");
|
|
return (
|
|
<Panel title="Artifacts">
|
|
<Row title="Include" help="Workspace-relative globs collected after each stage.">
|
|
{include && include.length > 0 ? (
|
|
<GlobList globs={include.filter((e): e is string => typeof e === "string")} />
|
|
) : (
|
|
<Muted>None</Muted>
|
|
)}
|
|
</Row>
|
|
</Panel>
|
|
);
|
|
}
|
|
|
|
function GoalValue({ goal }: { goal: UnknownRecord | undefined }) {
|
|
if (!goal) return <Muted>None</Muted>;
|
|
const type = getString(goal, "type");
|
|
const value = getString(goal, "value");
|
|
if (!value) return <Muted>None</Muted>;
|
|
return (
|
|
<span className="inline-flex flex-wrap items-center gap-x-2 gap-y-1">
|
|
{type ? <Badge>{type}</Badge> : null}
|
|
<Mono>{value}</Mono>
|
|
</span>
|
|
);
|
|
}
|
|
|
|
function AuthorValue({ name, email }: { name?: string; email?: string }) {
|
|
if (!name && !email) return <Muted>Default</Muted>;
|
|
if (name && email) {
|
|
return (
|
|
<span className="text-fg-2">
|
|
{name} <span className="text-fg-muted"><{email}></span>
|
|
</span>
|
|
);
|
|
}
|
|
return <span className="text-fg-2">{name ?? email}</span>;
|
|
}
|
|
|
|
function PullRequestValue({ pr }: { pr: UnknownRecord | undefined }) {
|
|
const enabled = getBool(pr, "enabled") ?? false;
|
|
if (!enabled) return <Toggle on={false} />;
|
|
const draft = getBool(pr, "draft") ?? false;
|
|
const autoMerge = getBool(pr, "auto_merge") ?? false;
|
|
const strategy = getString(pr, "merge_strategy");
|
|
return (
|
|
<span className="inline-flex flex-wrap items-center gap-x-2 gap-y-1">
|
|
<Toggle on={true} />
|
|
{draft ? <Badge>draft</Badge> : null}
|
|
{autoMerge ? <Badge>auto-merge</Badge> : null}
|
|
{strategy ? (
|
|
<span className="font-mono text-xs text-fg-3">{strategy}</span>
|
|
) : null}
|
|
</span>
|
|
);
|
|
}
|
|
|
|
function GlobList({ globs }: { globs: string[] }) {
|
|
return (
|
|
<span className="inline-flex flex-wrap items-center gap-1.5">
|
|
{globs.map((g) => (
|
|
<Badge key={g}>{g}</Badge>
|
|
))}
|
|
</span>
|
|
);
|
|
}
|