import { createMemo, createResource, createSignal, For, type JSX, type ResourceReturn, Show, } from "solid-js"; import { ApiError, api } from "../api"; import { CONFIG } from "../config"; import { fmtAge } from "../format"; import { runTrigger } from "../state"; import type { BrowseResponse } from "../types"; import { GlassCard } from "./GlassCard"; import { Pill } from "./Pill"; interface BrowseQuery { state: string; refreshCount: number; } const EMPTY_RESPONSE: BrowseResponse = { issues: [], errors: [], repos: [], cache: { hit: false, fetched_at: 0 }, }; export function Browse(): JSX.Element { const [state, setState] = createSignal("open"); const [refreshCount, setRefreshCount] = createSignal(0); const [filter, setFilter] = createSignal(""); const [hideProcessed, setHideProcessed] = createSignal(true); const fetchBrowse = async (query: BrowseQuery): Promise => { return api.browse(query.state, query.refreshCount > 0); }; const tuple: ResourceReturn = createResource( () => ({ state: state(), refreshCount: refreshCount() }), fetchBrowse, ); const [browseResource] = tuple; const data = createMemo(() => browseResource.latest ?? EMPTY_RESPONSE); const filtered = createMemo(() => { const all = data().issues; const needle = filter().trim().toLowerCase(); const hidden = hideProcessed(); const list = hidden ? all.filter((i) => !i.processed) : all; if (!needle) return list; return list.filter((i) => `${i.repo} ${i.title} #${i.number}`.toLowerCase().includes(needle)); }); const processedCount = createMemo(() => data().issues.filter((i) => i.processed).length); const errorMessage = (): string | null => { const err = browseResource.error; if (!err) return null; if (err instanceof ApiError) return `error ${err.status}: ${err.message}`; if (err instanceof Error) return err.message; return String(err); }; const meta = (): string => { const d = data(); const totalRepos = d.repos.length ? d.repos.join(", ") : "(allowlist empty)"; const ageSeconds = d.cache.fetched_at > 0 ? Math.max(0, (Date.now() - d.cache.fetched_at * 1000) / 1000) : 0; const cacheInfo = d.cache.fetched_at > 0 ? ` · ${d.cache.hit ? "cached" : "loaded"} ${ageSeconds.toFixed(0)}s ago` : ""; const hidden = hideProcessed() && processedCount() > 0 ? ` · ${processedCount()} processed hidden` : ""; return `${filtered().length}/${d.issues.length} from ${totalRepos}${cacheInfo}${hidden}`; }; const triggerFor = (mode: "triage" | "retry", repo: string, number: number): void => { void runTrigger({ mode, issue: `${repo}#${number}` }); }; return ( {meta()}}> issue browser disabled — same gate as the trigger surface. } >
setFilter(ev.currentTarget.value)} />
{errorMessage()}
{(err) => (
{err.repo} {err.error}
)}
{hideProcessed() && processedCount() > 0 && processedCount() === data().issues.length ? `all ${processedCount()} issues already processed — uncheck "hide processed" to see them` : "no issues"}
} > {(issue) => (
{issue.state} processed by {issue.author || "—"} updated {fmtAge(issue.updated_at)} {issue.comments} comments {(label) => {label}}
)}
); }