mirror of
https://github.com/0xWheatyz/handler.git
synced 2026-08-30 10:36:24 +00:00
7399315185
Rebuild the bundled web UI as a Next.js (React + TypeScript) static export implementing the Claude Activity Dashboard design: a left-nav "Control Center" hub over Runs, Repositories, Agents, Approvals, Git Servers, Activity, and Shared, styled with the Leeworks design-system tokens (flat, dark, border-led). The dashboard is a pure client of the existing API (same contract as curl): the browser prompts for the token once, stores it in localStorage, attaches it to every call, and renders all API values as React text so agent-authored strings stay inert. Control actions enqueue a command and poll it to a terminal state, matching the worker model. The build output is committed to src/handler/api/static/ so the wheel ships it and FastAPI serves it same-origin. app.py now mounts the export at "/" after the API routers (a non-shadowing fallback: unmatched paths 404, no SPA rewrite). UI-serving tests updated for the export; frontend source lives in frontend/. Claude-Session: https://claude.ai/code/session_01ATgVWRjFzG8nHEnwgZpJWD Co-authored-by: Claude <noreply@anthropic.com>
666 lines
20 KiB
TypeScript
666 lines
20 KiB
TypeScript
/* Dashboard state: one store owns the API client, the loaded data for every section,
|
|
* the 5s polling loop, and all mutating actions. Control actions enqueue a command and
|
|
* poll it to done/failed, surfacing the outcome in the command banner (`cmd`). */
|
|
"use client";
|
|
|
|
import {
|
|
createContext,
|
|
useCallback,
|
|
useContext,
|
|
useEffect,
|
|
useMemo,
|
|
useRef,
|
|
useState,
|
|
type ReactNode,
|
|
} from "react";
|
|
import {
|
|
AuthError,
|
|
createClient,
|
|
type Agent,
|
|
type ApiError,
|
|
type Approval,
|
|
type Checkmark,
|
|
type Command,
|
|
type Host,
|
|
type LogEntry,
|
|
type Project,
|
|
type SharedContext,
|
|
} from "@/lib/api";
|
|
|
|
export type Section =
|
|
| "runs"
|
|
| "repositories"
|
|
| "agents"
|
|
| "approvals"
|
|
| "servers"
|
|
| "activity"
|
|
| "shared";
|
|
|
|
export interface RunAgent extends Agent {}
|
|
|
|
export interface CmdState {
|
|
text: string;
|
|
error: boolean;
|
|
busy: boolean;
|
|
}
|
|
|
|
const LOG_LIMIT = 100;
|
|
const POLL_MS = 5000;
|
|
|
|
interface StoreValue {
|
|
section: Section;
|
|
setSection: (s: Section) => void;
|
|
|
|
projects: Project[];
|
|
agents: RunAgent[]; // every agent across every project
|
|
selectedProjectId: string;
|
|
selectProject: (id: string) => void;
|
|
|
|
// Runs inbox
|
|
selectedRun: { projectId: string; name: string } | null;
|
|
selectRun: (projectId: string, name: string) => void;
|
|
checkmark: Checkmark | null;
|
|
checkmarkMissing: boolean;
|
|
log: LogEntry[];
|
|
logOffset: number;
|
|
pageLog: (dir: 1 | -1) => void;
|
|
|
|
approvals: Approval[];
|
|
hosts: Host[];
|
|
commands: Command[];
|
|
shared: { log: LogEntry[]; context: SharedContext[] };
|
|
|
|
cmd: CmdState;
|
|
lastError: string;
|
|
loading: boolean;
|
|
|
|
refresh: () => void;
|
|
|
|
// actions
|
|
spawnAgent: (body: SpawnBody) => Promise<boolean>;
|
|
killAgent: (projectId: string, name: string) => Promise<void>;
|
|
deleteAgent: (projectId: string, name: string) => Promise<void>;
|
|
submitAnswer: (answer: string, resume: boolean) => Promise<boolean>;
|
|
createProject: (b: ProjectBody) => Promise<boolean>;
|
|
updateProject: (id: string, b: Omit<ProjectBody, "id">) => Promise<boolean>;
|
|
deleteProject: (id: string) => Promise<void>;
|
|
submitApproval: (b: ApprovalBody) => Promise<void>;
|
|
createHost: (b: HostBody) => Promise<boolean>;
|
|
updateHost: (hostname: string, b: Omit<HostBody, "hostname">) => Promise<boolean>;
|
|
deleteHost: (hostname: string) => Promise<void>;
|
|
pollCi: () => Promise<void>;
|
|
setSharedKey: (key: string, value: string) => Promise<boolean>;
|
|
}
|
|
|
|
export interface SpawnBody {
|
|
name: string;
|
|
role: string;
|
|
placement: "worktree" | "subdir";
|
|
worktree: string;
|
|
subdir: string;
|
|
task: string;
|
|
}
|
|
export interface ProjectBody {
|
|
id: string;
|
|
root_dir: string;
|
|
git_remote: string;
|
|
credential_ref: string;
|
|
}
|
|
export interface ApprovalBody {
|
|
branch: string;
|
|
status: string;
|
|
agent_name: string;
|
|
sha: string;
|
|
note: string;
|
|
}
|
|
export interface HostBody {
|
|
hostname: string;
|
|
forge_type: string;
|
|
token_env_var: string;
|
|
base_url: string;
|
|
}
|
|
|
|
const Ctx = createContext<StoreValue | null>(null);
|
|
|
|
export function useDashboard(): StoreValue {
|
|
const v = useContext(Ctx);
|
|
if (!v) throw new Error("useDashboard outside provider");
|
|
return v;
|
|
}
|
|
|
|
export function DashboardProvider({
|
|
token,
|
|
onUnauthorized,
|
|
children,
|
|
}: {
|
|
token: string;
|
|
onUnauthorized: () => void;
|
|
children: ReactNode;
|
|
}) {
|
|
const client = useMemo(() => createClient(token, onUnauthorized), [token, onUnauthorized]);
|
|
const clientRef = useRef(client);
|
|
clientRef.current = client;
|
|
|
|
const [section, setSectionRaw] = useState<Section>("runs");
|
|
const [projects, setProjects] = useState<Project[]>([]);
|
|
const [agents, setAgents] = useState<RunAgent[]>([]);
|
|
const [selectedProjectId, setSelectedProjectId] = useState<string>("");
|
|
const [selectedRun, setSelectedRun] = useState<{ projectId: string; name: string } | null>(null);
|
|
const [checkmark, setCheckmark] = useState<Checkmark | null>(null);
|
|
const [checkmarkMissing, setCheckmarkMissing] = useState(false);
|
|
const [log, setLog] = useState<LogEntry[]>([]);
|
|
const [logOffset, setLogOffset] = useState(0);
|
|
const [approvals, setApprovals] = useState<Approval[]>([]);
|
|
const [hosts, setHosts] = useState<Host[]>([]);
|
|
const [commands, setCommands] = useState<Command[]>([]);
|
|
const [shared, setShared] = useState<{ log: LogEntry[]; context: SharedContext[] }>({
|
|
log: [],
|
|
context: [],
|
|
});
|
|
const [cmd, setCmd] = useState<CmdState>({ text: "", error: false, busy: false });
|
|
const [lastError, setLastError] = useState("");
|
|
const [loading, setLoading] = useState(true);
|
|
|
|
// Keep polling loop reading fresh values without re-subscribing every render.
|
|
const sectionRef = useRef(section);
|
|
sectionRef.current = section;
|
|
const selectedProjectRef = useRef(selectedProjectId);
|
|
selectedProjectRef.current = selectedProjectId;
|
|
const selectedRunRef = useRef(selectedRun);
|
|
selectedRunRef.current = selectedRun;
|
|
const logOffsetRef = useRef(logOffset);
|
|
logOffsetRef.current = logOffset;
|
|
|
|
const swallow = (e: unknown) => {
|
|
if (!(e instanceof AuthError)) setLastError((e as Error).message);
|
|
};
|
|
|
|
const loadProjects = useCallback(async () => {
|
|
try {
|
|
const ps = await clientRef.current.api<Project[]>("/projects");
|
|
setProjects(ps);
|
|
setLastError("");
|
|
setSelectedProjectId((cur) => cur || (ps[0]?.id ?? ""));
|
|
} catch (e) {
|
|
swallow(e);
|
|
}
|
|
}, []);
|
|
|
|
const loadAgents = useCallback(async (projectList: Project[]) => {
|
|
try {
|
|
const results = await Promise.all(
|
|
projectList.map((p) =>
|
|
clientRef.current
|
|
.api<Agent[]>(`/projects/${encodeURIComponent(p.id)}/agents`)
|
|
.catch(() => [] as Agent[]),
|
|
),
|
|
);
|
|
setAgents(results.flat());
|
|
} catch (e) {
|
|
swallow(e);
|
|
}
|
|
}, []);
|
|
|
|
const loadRun = useCallback(async (projectId: string, name: string) => {
|
|
const path = `/projects/${encodeURIComponent(projectId)}/agents/${encodeURIComponent(name)}`;
|
|
try {
|
|
const cm = await clientRef.current.api<Checkmark>(`${path}/checkmark`);
|
|
setCheckmark(cm);
|
|
setCheckmarkMissing(false);
|
|
} catch (e) {
|
|
if (e instanceof AuthError) return;
|
|
if ((e as ApiError).status === 404) {
|
|
setCheckmark(null);
|
|
setCheckmarkMissing(true);
|
|
} else swallow(e);
|
|
}
|
|
try {
|
|
const entries = await clientRef.current.api<LogEntry[]>(
|
|
`${path}/log?limit=${LOG_LIMIT}&offset=${logOffsetRef.current}`,
|
|
);
|
|
setLog(entries);
|
|
} catch (e) {
|
|
swallow(e);
|
|
}
|
|
}, []);
|
|
|
|
const loadApprovals = useCallback(async (projectId: string) => {
|
|
if (!projectId) {
|
|
setApprovals([]);
|
|
return;
|
|
}
|
|
try {
|
|
setApprovals(
|
|
await clientRef.current.api<Approval[]>(
|
|
`/projects/${encodeURIComponent(projectId)}/approvals`,
|
|
),
|
|
);
|
|
} catch (e) {
|
|
swallow(e);
|
|
}
|
|
}, []);
|
|
|
|
const loadHosts = useCallback(async () => {
|
|
try {
|
|
setHosts(await clientRef.current.api<Host[]>("/hosts"));
|
|
} catch (e) {
|
|
swallow(e);
|
|
}
|
|
}, []);
|
|
|
|
const loadCommands = useCallback(async () => {
|
|
try {
|
|
setCommands(await clientRef.current.api<Command[]>("/commands?limit=50"));
|
|
} catch (e) {
|
|
swallow(e);
|
|
}
|
|
}, []);
|
|
|
|
const loadShared = useCallback(async () => {
|
|
try {
|
|
const [logRows, context] = await Promise.all([
|
|
clientRef.current.api<LogEntry[]>("/shared/log"),
|
|
clientRef.current.api<SharedContext[]>("/shared/context"),
|
|
]);
|
|
setShared({ log: logRows, context });
|
|
} catch (e) {
|
|
swallow(e);
|
|
}
|
|
}, []);
|
|
|
|
/* One refresh cycle for whatever section is active (plus always-cheap projects/agents
|
|
* so the nav counts and inbox stay live). */
|
|
const tick = useCallback(async () => {
|
|
const ps = await clientRef.current
|
|
.api<Project[]>("/projects")
|
|
.catch((e) => {
|
|
swallow(e);
|
|
return null;
|
|
});
|
|
if (ps) {
|
|
setProjects(ps);
|
|
setSelectedProjectId((cur) => cur || (ps[0]?.id ?? ""));
|
|
await loadAgents(ps);
|
|
}
|
|
const s = sectionRef.current;
|
|
const run = selectedRunRef.current;
|
|
if (run) await loadRun(run.projectId, run.name);
|
|
if (s === "approvals") await loadApprovals(selectedProjectRef.current);
|
|
if (s === "servers") await loadHosts();
|
|
if (s === "activity") await loadCommands();
|
|
if (s === "shared") await loadShared();
|
|
}, [loadAgents, loadRun, loadApprovals, loadHosts, loadCommands, loadShared]);
|
|
|
|
// Initial load + polling loop. The first tick populates projects *and* agents (and the
|
|
// active section) up front, so the Runs inbox is filled without waiting a poll interval.
|
|
useEffect(() => {
|
|
let alive = true;
|
|
(async () => {
|
|
setLoading(true);
|
|
await tick();
|
|
if (alive) setLoading(false);
|
|
})();
|
|
const id = setInterval(() => {
|
|
if (!document.hidden) void tick();
|
|
}, POLL_MS);
|
|
return () => {
|
|
alive = false;
|
|
clearInterval(id);
|
|
};
|
|
}, [tick]);
|
|
|
|
const setSection = useCallback(
|
|
(s: Section) => {
|
|
setSectionRaw(s);
|
|
setCmd({ text: "", error: false, busy: false });
|
|
if (s === "approvals") void loadApprovals(selectedProjectRef.current);
|
|
if (s === "servers") void loadHosts();
|
|
if (s === "activity") void loadCommands();
|
|
if (s === "shared") void loadShared();
|
|
},
|
|
[loadApprovals, loadHosts, loadCommands, loadShared],
|
|
);
|
|
|
|
const selectProject = useCallback(
|
|
(id: string) => {
|
|
setSelectedProjectId(id);
|
|
if (sectionRef.current === "approvals") void loadApprovals(id);
|
|
},
|
|
[loadApprovals],
|
|
);
|
|
|
|
const selectRun = useCallback(
|
|
(projectId: string, name: string) => {
|
|
setSelectedRun({ projectId, name });
|
|
setLogOffset(0);
|
|
logOffsetRef.current = 0;
|
|
setCheckmark(null);
|
|
setCheckmarkMissing(false);
|
|
setLog([]);
|
|
void loadRun(projectId, name);
|
|
},
|
|
[loadRun],
|
|
);
|
|
|
|
const pageLog = useCallback(
|
|
(dir: 1 | -1) => {
|
|
const next = Math.max(0, logOffset + dir * LOG_LIMIT);
|
|
if (next === logOffset) return;
|
|
setLogOffset(next);
|
|
logOffsetRef.current = next;
|
|
const run = selectedRunRef.current;
|
|
if (run) void loadRun(run.projectId, run.name);
|
|
},
|
|
[logOffset, loadRun],
|
|
);
|
|
|
|
const refresh = useCallback(() => {
|
|
void tick();
|
|
}, [tick]);
|
|
|
|
// ---- control actions (enqueue + track) ----
|
|
const enqueueAndTrack = useCallback(
|
|
async (path: string, body: unknown, label: string): Promise<Command | null> => {
|
|
setCmd({ text: `${label}: queued…`, error: false, busy: true });
|
|
try {
|
|
const command = await clientRef.current.api<Command>(path, { method: "POST", body });
|
|
const final = await clientRef.current.trackCommand(command.id);
|
|
if (!final) {
|
|
setCmd({
|
|
text: `${label}: still running (see Activity). Is the worker up?`,
|
|
error: false,
|
|
busy: false,
|
|
});
|
|
return null;
|
|
}
|
|
const ok = final.status === "done";
|
|
const detail = final.error || (final.result ? JSON.stringify(final.result) : "");
|
|
setCmd({
|
|
text: `${label} ${ok ? "done" : "failed"}${detail ? " — " + detail : ""}`,
|
|
error: !ok,
|
|
busy: false,
|
|
});
|
|
return final;
|
|
} catch (e) {
|
|
if (e instanceof AuthError) return null;
|
|
setCmd({ text: `${label} failed: ${(e as Error).message}`, error: true, busy: false });
|
|
return null;
|
|
}
|
|
},
|
|
[],
|
|
);
|
|
|
|
const spawnAgent = useCallback(
|
|
async (f: SpawnBody) => {
|
|
const body: Record<string, unknown> = {
|
|
name: f.name.trim(),
|
|
role: f.role || null,
|
|
task: f.task.trim() || null,
|
|
};
|
|
if (f.placement === "worktree" && f.worktree.trim()) body.worktree = f.worktree.trim();
|
|
if (f.placement === "subdir" && f.subdir.trim()) body.subdir = f.subdir.trim();
|
|
const p = encodeURIComponent(selectedProjectRef.current);
|
|
const final = await enqueueAndTrack(`/projects/${p}/agents/spawn`, body, `spawn ${body.name}`);
|
|
await loadAgents(projects);
|
|
return final?.status === "done";
|
|
},
|
|
[enqueueAndTrack, loadAgents, projects],
|
|
);
|
|
|
|
const killAgent = useCallback(
|
|
async (projectId: string, name: string) => {
|
|
const p = encodeURIComponent(projectId);
|
|
await enqueueAndTrack(`/projects/${p}/agents/${encodeURIComponent(name)}/kill`, undefined, `kill ${name}`);
|
|
await loadAgents(projects);
|
|
},
|
|
[enqueueAndTrack, loadAgents, projects],
|
|
);
|
|
|
|
const deleteAgent = useCallback(
|
|
async (projectId: string, name: string) => {
|
|
const p = encodeURIComponent(projectId);
|
|
try {
|
|
await clientRef.current.api(`/projects/${p}/agents/${encodeURIComponent(name)}`, {
|
|
method: "DELETE",
|
|
});
|
|
setCmd({ text: `agent '${name}' row deleted`, error: false, busy: false });
|
|
if (selectedRunRef.current?.name === name) setSelectedRun(null);
|
|
await loadAgents(projects);
|
|
} catch (e) {
|
|
if (e instanceof AuthError) return;
|
|
setCmd({ text: (e as Error).message, error: true, busy: false });
|
|
}
|
|
},
|
|
[loadAgents, projects],
|
|
);
|
|
|
|
const submitAnswer = useCallback(
|
|
async (answer: string, resume: boolean) => {
|
|
const run = selectedRunRef.current;
|
|
if (!run) return false;
|
|
const path = `/projects/${encodeURIComponent(run.projectId)}/agents/${encodeURIComponent(run.name)}`;
|
|
try {
|
|
await clientRef.current.api(`${path}/answer`, { method: "POST", body: { answer } });
|
|
if (resume) {
|
|
await enqueueAndTrack(`${path}/resume`, { answer }, "resume");
|
|
} else {
|
|
setCmd({ text: "Answer saved (agent still paused).", error: false, busy: false });
|
|
}
|
|
await loadAgents(projects);
|
|
await loadRun(run.projectId, run.name);
|
|
return true;
|
|
} catch (e) {
|
|
if (e instanceof AuthError) return false;
|
|
setCmd({ text: (e as Error).message, error: true, busy: false });
|
|
return false;
|
|
}
|
|
},
|
|
[enqueueAndTrack, loadAgents, loadRun, projects],
|
|
);
|
|
|
|
const createProject = useCallback(
|
|
async (b: ProjectBody) => {
|
|
try {
|
|
await clientRef.current.api("/projects", {
|
|
method: "POST",
|
|
body: {
|
|
id: b.id.trim(),
|
|
root_dir: b.root_dir.trim(),
|
|
git_remote: b.git_remote.trim() || null,
|
|
credential_ref: b.credential_ref.trim() || null,
|
|
},
|
|
});
|
|
setCmd({ text: `repository '${b.id}' registered`, error: false, busy: false });
|
|
await loadProjects();
|
|
return true;
|
|
} catch (e) {
|
|
if (e instanceof AuthError) return false;
|
|
setCmd({ text: (e as Error).message, error: true, busy: false });
|
|
return false;
|
|
}
|
|
},
|
|
[loadProjects],
|
|
);
|
|
|
|
const updateProject = useCallback(
|
|
async (id: string, b: Omit<ProjectBody, "id">) => {
|
|
try {
|
|
await clientRef.current.api(`/projects/${encodeURIComponent(id)}`, {
|
|
method: "PATCH",
|
|
body: {
|
|
root_dir: b.root_dir.trim(),
|
|
git_remote: b.git_remote.trim() || null,
|
|
credential_ref: b.credential_ref.trim() || null,
|
|
},
|
|
});
|
|
setCmd({ text: `repository '${id}' updated`, error: false, busy: false });
|
|
await loadProjects();
|
|
return true;
|
|
} catch (e) {
|
|
if (e instanceof AuthError) return false;
|
|
setCmd({ text: (e as Error).message, error: true, busy: false });
|
|
return false;
|
|
}
|
|
},
|
|
[loadProjects],
|
|
);
|
|
|
|
const deleteProject = useCallback(
|
|
async (id: string) => {
|
|
try {
|
|
await clientRef.current.api(`/projects/${encodeURIComponent(id)}`, { method: "DELETE" });
|
|
setCmd({ text: `repository '${id}' removed`, error: false, busy: false });
|
|
setSelectedProjectId((cur) => (cur === id ? "" : cur));
|
|
await loadProjects();
|
|
} catch (e) {
|
|
if (e instanceof AuthError) return;
|
|
setCmd({ text: (e as Error).message, error: true, busy: false });
|
|
}
|
|
},
|
|
[loadProjects],
|
|
);
|
|
|
|
const submitApproval = useCallback(
|
|
async (b: ApprovalBody) => {
|
|
const p = encodeURIComponent(selectedProjectRef.current);
|
|
await enqueueAndTrack(
|
|
`/projects/${p}/approvals`,
|
|
{
|
|
branch: b.branch.trim(),
|
|
status: b.status,
|
|
agent_name: b.agent_name.trim() || null,
|
|
sha: b.sha.trim() || null,
|
|
note: b.note.trim() || null,
|
|
},
|
|
`${b.status} ${b.branch}`,
|
|
);
|
|
await loadApprovals(selectedProjectRef.current);
|
|
},
|
|
[enqueueAndTrack, loadApprovals],
|
|
);
|
|
|
|
const createHost = useCallback(
|
|
async (b: HostBody) => {
|
|
try {
|
|
await clientRef.current.api("/hosts", {
|
|
method: "POST",
|
|
body: {
|
|
hostname: b.hostname.trim(),
|
|
forge_type: b.forge_type,
|
|
token_env_var: b.token_env_var.trim() || null,
|
|
base_url: b.base_url.trim() || null,
|
|
},
|
|
});
|
|
setCmd({ text: `git server '${b.hostname}' added`, error: false, busy: false });
|
|
await loadHosts();
|
|
return true;
|
|
} catch (e) {
|
|
if (e instanceof AuthError) return false;
|
|
setCmd({ text: (e as Error).message, error: true, busy: false });
|
|
return false;
|
|
}
|
|
},
|
|
[loadHosts],
|
|
);
|
|
|
|
const updateHost = useCallback(
|
|
async (hostname: string, b: Omit<HostBody, "hostname">) => {
|
|
try {
|
|
await clientRef.current.api(`/hosts/${encodeURIComponent(hostname)}`, {
|
|
method: "PATCH",
|
|
body: {
|
|
forge_type: b.forge_type,
|
|
token_env_var: b.token_env_var.trim() || null,
|
|
base_url: b.base_url.trim() || null,
|
|
},
|
|
});
|
|
setCmd({ text: `git server '${hostname}' updated`, error: false, busy: false });
|
|
await loadHosts();
|
|
return true;
|
|
} catch (e) {
|
|
if (e instanceof AuthError) return false;
|
|
setCmd({ text: (e as Error).message, error: true, busy: false });
|
|
return false;
|
|
}
|
|
},
|
|
[loadHosts],
|
|
);
|
|
|
|
const deleteHost = useCallback(
|
|
async (hostname: string) => {
|
|
try {
|
|
await clientRef.current.api(`/hosts/${encodeURIComponent(hostname)}`, { method: "DELETE" });
|
|
setCmd({ text: `git server '${hostname}' removed`, error: false, busy: false });
|
|
await loadHosts();
|
|
} catch (e) {
|
|
if (e instanceof AuthError) return;
|
|
setCmd({ text: (e as Error).message, error: true, busy: false });
|
|
}
|
|
},
|
|
[loadHosts],
|
|
);
|
|
|
|
const pollCi = useCallback(async () => {
|
|
await enqueueAndTrack("/poll-ci", undefined, "poll-ci (all projects)");
|
|
await loadCommands();
|
|
}, [enqueueAndTrack, loadCommands]);
|
|
|
|
const setSharedKey = useCallback(
|
|
async (key: string, value: string) => {
|
|
try {
|
|
await clientRef.current.api(`/shared/context/${encodeURIComponent(key)}`, {
|
|
method: "PUT",
|
|
body: { value },
|
|
});
|
|
setCmd({ text: `shared context '${key}' set`, error: false, busy: false });
|
|
await loadShared();
|
|
return true;
|
|
} catch (e) {
|
|
if (e instanceof AuthError) return false;
|
|
setCmd({ text: (e as Error).message, error: true, busy: false });
|
|
return false;
|
|
}
|
|
},
|
|
[loadShared],
|
|
);
|
|
|
|
const value: StoreValue = {
|
|
section,
|
|
setSection,
|
|
projects,
|
|
agents,
|
|
selectedProjectId,
|
|
selectProject,
|
|
selectedRun,
|
|
selectRun,
|
|
checkmark,
|
|
checkmarkMissing,
|
|
log,
|
|
logOffset,
|
|
pageLog,
|
|
approvals,
|
|
hosts,
|
|
commands,
|
|
shared,
|
|
cmd,
|
|
lastError,
|
|
loading,
|
|
refresh,
|
|
spawnAgent,
|
|
killAgent,
|
|
deleteAgent,
|
|
submitAnswer,
|
|
createProject,
|
|
updateProject,
|
|
deleteProject,
|
|
submitApproval,
|
|
createHost,
|
|
updateHost,
|
|
deleteHost,
|
|
pollCi,
|
|
setSharedKey,
|
|
};
|
|
|
|
return <Ctx.Provider value={value}>{children}</Ctx.Provider>;
|
|
}
|