import { useState } from "react"; import { Link } from "react-router"; import { useSWRConfig } from "swr"; import { Menu, MenuButton, MenuItem, MenuItems } from "@headlessui/react"; import { ChevronDownIcon, PlusIcon } from "@heroicons/react/16/solid"; import { EllipsisVerticalIcon } from "@heroicons/react/20/solid"; import type { Environment } from "@qltysh/fabro-api-client"; import { ApiError, apiData, environmentsApi } from "../lib/api-client"; import { useEnvironments, useServerSettings } from "../lib/queries"; import { queryKeys } from "../lib/query-keys"; import { CREATABLE_PROVIDERS } from "../components/environment-form"; import { Badge, Muted, Panel, PanelSkeleton, SettingsPageIntro, } from "../components/settings-panel"; import { ConfirmDialog } from "../components/ui"; import { useToast } from "../components/toast"; // `local` is a reserved, in-memory environment the server includes only when // the local sandbox provider is enabled. It has no configurable settings, so it // gets its own panel instead of a row in the managed environments list. const RESERVED_ID = "local"; const MENU_ITEM_CLASS = "flex w-full items-center gap-2 px-3 py-2 text-left text-sm text-fg-3 transition-colors data-focus:bg-overlay data-focus:text-fg data-focus:outline-hidden disabled:cursor-not-allowed disabled:opacity-60"; const MENU_ITEM_DANGER_CLASS = "flex w-full items-center gap-2 px-3 py-2 text-left text-sm text-coral transition-colors data-focus:bg-coral/10 data-focus:text-coral data-focus:outline-hidden disabled:cursor-not-allowed disabled:opacity-60"; export function meta() { return [{ title: "Environments — Fabro" }]; } const DESCRIPTION = "Environments are server-managed runtime definitions — provider, image, resources, network, and lifecycle — that workflow runs select by id. They are operator policy stored on this Fabro server."; export default function SettingsEnvironments() { const query = useEnvironments(); return (
} /> {query.data ? ( ) : query.error ? (
Couldn't load environments. Please try again.
) : ( )}
); } const NEW_BUTTON_CLASS = "inline-flex items-center gap-1.5 rounded-md border border-line bg-panel/80 px-2.5 py-1 text-sm font-medium text-fg-3 transition-colors hover:border-line-strong hover:bg-panel hover:text-fg disabled:cursor-not-allowed disabled:opacity-60 disabled:hover:border-line disabled:hover:bg-panel/80 disabled:hover:text-fg-3"; function providerLabel(provider: string): string { return provider.charAt(0).toUpperCase() + provider.slice(1); } // "New environment" is a provider picker: each enabled sandbox provider opens // the create form pre-set to that provider, which is then fixed for the // environment's lifetime. `local` is never offered (it's reserved/in-memory). function NewEnvironmentMenu() { const { data } = useServerSettings(); const providers = data ? CREATABLE_PROVIDERS.filter((provider) => data.server.sandbox.providers[provider].enabled) : []; if (providers.length === 0) { return ( ); } return ( {providers.map((provider) => ( {providerLabel(provider)} ))} ); } function EnvironmentsContent({ environments }: { environments: Environment[] }) { const local = environments.find((environment) => environment.id === RESERVED_ID); const managed = environments.filter((environment) => environment.id !== RESERVED_ID); return ( <> {local ? : null} ); } function LocalEnvironmentPanel({ environment }: { environment: Environment }) { return (
{environment.id} {environment.provider}

Built-in environment that runs tools directly on the host. Available because the local sandbox provider is enabled; it has no configurable settings and can't be edited or deleted.

reserved
); } function EnvironmentsPanel({ environments }: { environments: Environment[] }) { const { mutate } = useSWRConfig(); const toast = useToast(); const [pendingDelete, setPendingDelete] = useState(null); const [deleting, setDeleting] = useState(false); async function confirmDelete() { if (!pendingDelete) return; const target = pendingDelete; setDeleting(true); try { await apiData(() => environmentsApi.deleteEnvironment(target.id, target.revision)); await mutate(queryKeys.environments.list()); toast.push({ message: `Environment “${target.id}” deleted.` }); setPendingDelete(null); } catch (cause) { toast.push({ tone: "error", message: cause instanceof ApiError && cause.message ? cause.message : "Couldn't delete the environment. Please try again.", }); } finally { setDeleting(false); } } return ( <> {environments.length === 0 ? (
No environments defined yet.
) : ( environments.map((environment) => ( setPendingDelete(environment)} /> )) )}
Delete{" "} {pendingDelete?.id}? Runs that select this environment will fail until it is recreated. } confirmLabel="Delete" pendingLabel="Deleting…" pending={deleting} onConfirm={confirmDelete} onCancel={() => { if (!deleting) setPendingDelete(null); }} /> ); } function EnvironmentRow({ environment, disabled, onDelete, }: { environment: Environment; disabled: boolean; onDelete: () => void; }) { return (
{environment.id} {environment.provider}
{resourcesSummary(environment)}
{imageSummary(environment) ?? No image}
); } function imageSummary(environment: Environment): string | null { if (environment.image.docker) return environment.image.docker; if (environment.image.dockerfile) return "Dockerfile (inline)"; return null; } function resourcesSummary(environment: Environment): string { const parts = [ environment.resources.cpu === null ? null : `${environment.resources.cpu} CPU`, environment.resources.memory, environment.resources.disk, ].filter((part): part is string => Boolean(part)); return parts.length > 0 ? parts.join(" · ") : "Default resources"; } function StatusTag({ children }: { children: string }) { return ( {children} ); } function RowMenu({ environment, disabled, onDelete, }: { environment: Environment; disabled: boolean; onDelete: () => void; }) { return ( Edit
); }