mirror of
https://github.com/fabro-sh/fabro.git
synced 2026-10-07 03:00:29 +00:00
## Summary Improves the web UI's React Doctor audit score by separating reusable helpers from React component modules, tightening effect/state ownership, and extracting real component boundaries in the install wizard, stage activity view, run-files diff browser, RunDetail route, and Runs workspace. The branch removes the previously deferred RunDetail and Runs giant-component diagnostics without changing RunDetail UX, route contracts, action ordering, or Runs workspace behavior. | Metric | Main baseline | Initial PR | Current PR | |--------|---------------|------------|------------| | React Doctor score | 63 | 71 | 99 | | React Doctor errors | 123 | 0 | 0 | | React Doctor warnings | 241 | 163 | 3 | | React Doctor diagnostics | 364 | 163 | 3 | ## Changes - Moves exported helper logic out of component files so Fast Refresh/component-export rules no longer dominate the audit. - Adds a targeted React Doctor config exception for React Router route modules, where non-component exports like route metadata are intentional. - Refactors low-risk state/effect patterns: keyed interview question state, reducer-backed editable run title state, event-owned preview opening, route-keyed insights editor initialization, refresh timer ownership, and selection/derived list cleanup. - Reworks `InstallApp` around an install reducer, a controller hook for install lifecycle state, and focused wizard step components for LLM, server, object-store, sandbox, and GitHub setup. - Moves `RunStages` selected-stage activity into a keyed boundary for panel/debug detail state while preserving stage activity filters across navigation. - Extracts the `RunFiles` loaded diff-browser view from route/query coordination so the route owns data/URL state and the loaded view owns rendering. - Splits `RunDetail` into route-local header, actions, tab shell, docked controls, model, and lifecycle-toast modules; the actions menu now uses grouped descriptors instead of a large boolean/callback prop matrix. - Extracts Runs workspace preference ownership into `useRunsWorkspacePreferences` and moves toolbar rendering into `RunsToolbar`, leaving the route focused on data, DnD state, filtering, and view selection. - Guards `InsightsEditor` query execution with a latest-run id and timeout cleanup so stale or unmounted mock query runs cannot overwrite newer results. - Adds regression coverage for archived-run deletion from RunDetail and stale-result handling in InsightsEditor. - Improves semantic/accessibility coverage with labeled controls, native meter/section semantics, decorative status dots, and clearer unavailable copy. - Removes dead UI code and applies local suppressions only where the rule is a documented false positive or an intentional imperative integration boundary. ## Remaining React Doctor warnings Current score is 99 with 0 errors and 3 warnings. The remaining warnings are intentionally left for separate judgment rather than mechanical churn: - `prefer-useReducer` (3): `AutomationsNew`, `InsightsEditor`, and `CreateSecretForm` need reducers only if they encode real coupled transitions, not simple field setters. ## Verification - `cd apps/fabro-web && bun test app/routes/run-detail.test.ts` -> `22 pass`, `0 fail` - `cd apps/fabro-web && bun test app/routes/insights-editor.test.tsx app/routes/runs.preferences.test.tsx` -> `7 pass`, `0 fail` - `cd apps/fabro-web && bun run typecheck` - `cd apps/fabro-web && bun test --isolate` -> `490 pass`, `0 fail` - `cd apps/fabro-web && bunx react-doctor@latest --full --json > /tmp/fabro-react-doctor-runs-insights.json` -> score `99`, `0` errors, `3` warnings - Earlier branch verification also included `cd apps/fabro-web && bun run build` - `git diff --check` --- [](https://github.com/EveryInc/compound-engineering-plugin) 🤖 Generated with GPT-5 (context not reported, default reasoning) via [Codex](https://openai.com/codex)
188 lines
5.4 KiB
TypeScript
188 lines
5.4 KiB
TypeScript
import { useMemo, useState } from "react";
|
|
import { Link } from "react-router";
|
|
import { ChevronDownIcon } from "@heroicons/react/16/solid";
|
|
import { ComputerDesktopIcon } from "@heroicons/react/24/outline";
|
|
import type { ServerSandboxProvidersSettings } from "@qltysh/fabro-api-client";
|
|
import { useServerSettings } from "../lib/queries";
|
|
import {
|
|
Dot,
|
|
Panel,
|
|
PanelSkeleton,
|
|
Row,
|
|
SettingsPageIntro,
|
|
} from "../components/settings-panel";
|
|
import { plural } from "../lib/plural";
|
|
|
|
export function meta() {
|
|
return [{ title: "Sandboxes — Fabro" }];
|
|
}
|
|
|
|
type SandboxProviderId = "local" | "docker" | "daytona";
|
|
|
|
type SandboxProvider = {
|
|
id: SandboxProviderId;
|
|
name: string;
|
|
description: string;
|
|
enabled: boolean;
|
|
secretName?: string;
|
|
};
|
|
|
|
const DESCRIPTION =
|
|
"Runtime environments where workflow stages execute. Configured via settings.toml.";
|
|
|
|
export default function SettingsSandboxes() {
|
|
const query = useServerSettings();
|
|
const settings = query.data;
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
<SettingsPageIntro description={DESCRIPTION} />
|
|
{settings ? <ProvidersPanel settings={settings.server.sandbox.providers} /> : <PanelSkeleton />}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function ProvidersPanel({ settings }: { settings: ServerSandboxProvidersSettings }) {
|
|
const providers: SandboxProvider[] = useMemo(
|
|
() => [
|
|
{
|
|
id: "local",
|
|
name: "Local",
|
|
description: "Run stages directly on the Fabro host.",
|
|
enabled: settings.local.enabled,
|
|
},
|
|
{
|
|
id: "docker",
|
|
name: "Docker",
|
|
description: "Run stages in isolated Docker containers on the host daemon.",
|
|
enabled: settings.docker.enabled,
|
|
},
|
|
{
|
|
id: "daytona",
|
|
name: "Daytona",
|
|
description: "Run stages in cloud sandboxes managed by Daytona.",
|
|
enabled: settings.daytona.enabled,
|
|
secretName: "DAYTONA_API_KEY",
|
|
},
|
|
],
|
|
[settings],
|
|
);
|
|
|
|
const { enabled, disabled } = useMemo(() => {
|
|
const enabled: SandboxProvider[] = [];
|
|
const disabled: SandboxProvider[] = [];
|
|
for (const provider of providers) {
|
|
if (provider.enabled) {
|
|
enabled.push(provider);
|
|
} else {
|
|
disabled.push(provider);
|
|
}
|
|
}
|
|
return { enabled, disabled };
|
|
}, [providers]);
|
|
|
|
const [showDisabled, setShowDisabled] = useState(false);
|
|
const showDisabledRows = enabled.length === 0 || showDisabled;
|
|
|
|
return (
|
|
<Panel title="Providers">
|
|
{enabled.map((provider) => (
|
|
<ProviderRow key={provider.id} provider={provider} />
|
|
))}
|
|
{showDisabledRows
|
|
? disabled.map((provider) => (
|
|
<ProviderRow key={provider.id} provider={provider} />
|
|
))
|
|
: null}
|
|
{enabled.length > 0 && disabled.length > 0 ? (
|
|
<button
|
|
type="button"
|
|
onClick={() => setShowDisabled((v) => !v)}
|
|
aria-expanded={showDisabled}
|
|
className="flex w-full items-center gap-1.5 px-4 py-3 text-left text-xs font-medium text-fg-muted hover:text-fg-3"
|
|
>
|
|
<ChevronDownIcon
|
|
className={`size-4 h-lh shrink-0 transition-transform ${
|
|
showDisabled ? "rotate-180" : ""
|
|
}`}
|
|
/>
|
|
{showDisabled ? "Hide" : "Show"} {disabled.length} disabled{" "}
|
|
{plural(disabled.length, "provider", "providers")}
|
|
</button>
|
|
) : null}
|
|
</Panel>
|
|
);
|
|
}
|
|
|
|
function ProviderRow({ provider }: { provider: SandboxProvider }) {
|
|
return (
|
|
<Row
|
|
title={
|
|
<span className="flex items-center gap-4">
|
|
<ProviderLogo provider={provider} />
|
|
<span className="flex min-w-0 flex-col">
|
|
<span className="text-sm text-fg-2">{provider.name}</span>
|
|
<span className="text-xs/5 text-fg-3">{provider.description}</span>
|
|
</span>
|
|
</span>
|
|
}
|
|
>
|
|
<ProviderStatus provider={provider} />
|
|
</Row>
|
|
);
|
|
}
|
|
|
|
function ProviderLogo({ provider }: { provider: SandboxProvider }) {
|
|
const [failed, setFailed] = useState(false);
|
|
const chip =
|
|
"grid size-10 shrink-0 place-items-center rounded-md bg-ice-50 ring-1 ring-line-strong";
|
|
const dim = provider.enabled ? "" : "opacity-60";
|
|
|
|
if (provider.id === "local") {
|
|
return (
|
|
<span className={`${chip} text-page ${dim}`}>
|
|
<ComputerDesktopIcon className="size-6" aria-hidden="true" />
|
|
</span>
|
|
);
|
|
}
|
|
|
|
if (failed) {
|
|
return (
|
|
<span className={`${chip} text-base font-medium text-page ${dim}`}>
|
|
{provider.name.charAt(0)}
|
|
</span>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<span className={`${chip} text-page ${dim}`}>
|
|
<img
|
|
alt=""
|
|
src={`/images/sandboxes/${provider.id}.svg`}
|
|
onError={() => setFailed(true)}
|
|
className="size-7"
|
|
/>
|
|
</span>
|
|
);
|
|
}
|
|
|
|
function ProviderStatus({ provider }: { provider: SandboxProvider }) {
|
|
return (
|
|
<span className="inline-flex flex-wrap items-center gap-x-2 gap-y-1">
|
|
<span className="inline-flex items-center gap-2">
|
|
<Dot on={provider.enabled} />
|
|
<span className={provider.enabled ? "text-fg" : "text-fg-muted"}>
|
|
{provider.enabled ? "Enabled" : "Disabled"}
|
|
</span>
|
|
</span>
|
|
{!provider.enabled && provider.secretName ? (
|
|
<Link
|
|
to={`/settings/secrets/new?name=${encodeURIComponent(provider.secretName)}`}
|
|
className="text-xs text-teal-500 hover:underline"
|
|
>
|
|
Add secret →
|
|
</Link>
|
|
) : null}
|
|
</span>
|
|
);
|
|
}
|