From 21dadc8724cff5a24a5c81877281d904e973c0d9 Mon Sep 17 00:00:00 2001 From: null Date: Tue, 19 May 2026 20:31:05 -0500 Subject: [PATCH] feat(dashboard): issue tracking widgets (#27) --- frontend/src/app/dashboard/page.tsx | 298 +++++++++++++----- frontend/src/app/git-projects/issues/page.tsx | 159 +++++++++- .../git/ForgejoIssueMetricCards.tsx | 287 +++++++++++++++++ frontend/src/lib/api-forgejo.ts | 13 +- 4 files changed, 661 insertions(+), 96 deletions(-) create mode 100644 frontend/src/components/git/ForgejoIssueMetricCards.tsx diff --git a/frontend/src/app/dashboard/page.tsx b/frontend/src/app/dashboard/page.tsx index de72b89..5ac9755 100644 --- a/frontend/src/app/dashboard/page.tsx +++ b/frontend/src/app/dashboard/page.tsx @@ -22,14 +22,13 @@ import { DashboardSidebar } from "@/components/organisms/DashboardSidebar"; import { DashboardShell } from "@/components/templates/DashboardShell"; import { Markdown } from "@/components/atoms/Markdown"; import { SignedOutPanel } from "@/components/auth/SignedOutPanel"; +import { ForgejoIssueMetricCards } from "@/components/git/ForgejoIssueMetricCards"; import { ApiError } from "@/api/mutator"; import { type dashboardMetricsApiV1MetricsDashboardGetResponse, useDashboardMetricsApiV1MetricsDashboardGet, } from "@/api/generated/metrics/metrics"; -import { - gatewaysStatusApiV1GatewaysStatusGet, -} from "@/api/generated/gateways/gateways"; +import { gatewaysStatusApiV1GatewaysStatusGet } from "@/api/generated/gateways/gateways"; import type { GatewaysStatusResponse } from "@/api/generated/model/gatewaysStatusResponse"; import { type listAgentsApiV1AgentsGetResponse, @@ -44,6 +43,12 @@ import { useListActivityApiV1ActivityGet, } from "@/api/generated/activity/activity"; import type { ActivityEventRead } from "@/api/generated/model"; +import { + getForgejoMetrics, + getForgejoRepositories, + type ForgejoIssueMetrics, + type ForgejoRepository, +} from "@/lib/api-forgejo"; import { formatRelativeTimestamp, formatTimestamp, @@ -189,9 +194,13 @@ const readTimestampFromRecords = ( return null; }; -const sessionIdentifiers = (record: Record | null): string[] => { +const sessionIdentifiers = ( + record: Record | null, +): string[] => { if (!record) return []; - const ids = SESSION_ID_KEYS.map((key) => readString(record, [key])).filter(Boolean) as string[]; + const ids = SESSION_ID_KEYS.map((key) => readString(record, [key])).filter( + Boolean, + ) as string[]; return [...new Set(ids)]; }; @@ -210,13 +219,16 @@ const compactNumber = (value: number): string => { }; const formatCount = (value: number): string => - Number.isFinite(value) ? numberFormatter.format(Math.max(0, Math.round(value))) : "0"; + Number.isFinite(value) + ? numberFormatter.format(Math.max(0, Math.round(value))) + : "0"; const formatPercent = (value: number): string => Number.isFinite(value) ? `${value.toFixed(1)}%` : DASH; const formatPerDay = (total: number, days: number): string => { - if (!Number.isFinite(total) || !Number.isFinite(days) || days <= 0) return DASH; + if (!Number.isFinite(total) || !Number.isFinite(days) || days <= 0) + return DASH; return `${(total / days).toFixed(1)}/day`; }; @@ -224,15 +236,15 @@ const toSessionSummaries = ( sessions: unknown[] | null | undefined, mainSession: unknown, ): SessionSummary[] => { - const sessionRecords = (sessions ?? []).map(toRecord).filter(Boolean) as Array< - Record - >; + const sessionRecords = (sessions ?? []) + .map(toRecord) + .filter(Boolean) as Array>; const mainRecord = toRecord(mainSession); const mainIdentifiers = sessionIdentifiers(mainRecord); if (mainRecord && mainIdentifiers.length > 0) { - const exists = sessionRecords.some( - (entry) => sharesSessionIdentity(sessionIdentifiers(entry), mainIdentifiers), + const exists = sessionRecords.some((entry) => + sharesSessionIdentity(sessionIdentifiers(entry), mainIdentifiers), ); if (!exists) sessionRecords.unshift(mainRecord); } @@ -242,7 +254,10 @@ const toSessionSummaries = ( for (const entry of sessionRecords) { const identifiers = sessionIdentifiers(entry); - if (identifiers.length > 0 && identifiers.some((value) => seenIdentifiers.has(value))) { + if ( + identifiers.length > 0 && + identifiers.some((value) => seenIdentifiers.has(value)) + ) { continue; } uniqueRecords.push(entry); @@ -258,17 +273,29 @@ const toSessionSummaries = ( const identifiers = sessionIdentifiers(entry); const key = - readString(entry, ["key", "session_key", "sessionKey", "id", "sessionId"]) ?? - `session-${index}`; + readString(entry, [ + "key", + "session_key", + "sessionKey", + "id", + "sessionId", + ]) ?? `session-${index}`; const label = readString(entry, ["label", "name", "title"]) ?? key; - const channel = readStringFromRecords([entry, originRecord], [ - "channel", - "source", - "kind", - "chatType", + const channel = readStringFromRecords( + [entry, originRecord], + ["channel", "source", "kind", "chatType"], + ); + const model = readString(entry, [ + "model", + "model_name", + "provider", + "engine", + ]); + const modelProvider = readString(entry, [ + "modelProvider", + "model_provider", + "provider", ]); - const model = readString(entry, ["model", "model_name", "provider", "engine"]); - const modelProvider = readString(entry, ["modelProvider", "model_provider", "provider"]); const lastSeenAt = readTimestampFromRecords(candidateRecords, [ "updated_at", "updatedAt", @@ -336,10 +363,15 @@ const toSessionSummaries = ( : DASH; const subtitleBits = [channel, model].filter(Boolean) as string[]; - const subtitle = subtitleBits.length > 0 ? subtitleBits.join(" · ") : "Session"; + const subtitle = + subtitleBits.length > 0 ? subtitleBits.join(" · ") : "Session"; const modelWithProvider = - modelProvider && model && modelProvider !== model ? `${model} · ${modelProvider}` : model; - const subtitleWithProvider = [channel, modelWithProvider].filter(Boolean).join(" · "); + modelProvider && model && modelProvider !== model + ? `${model} · ${modelProvider}` + : model; + const subtitleWithProvider = [channel, modelWithProvider] + .filter(Boolean) + .join(" · "); return { key, @@ -397,15 +429,15 @@ function TopMetricCard({ ) : null}
-

{value}

+

+ {value} +

{secondary ? (

{secondary}

) : null}
-
- {icon} -
+
{icon}
); @@ -453,7 +485,10 @@ function InfoBlock({
{rows.map((row) => ( -
+
{row.label} ( + const boardsQuery = useListBoardsApiV1BoardsGet< + listBoardsApiV1BoardsGetResponse, + ApiError + >( { limit: 200 }, { query: { @@ -490,7 +528,10 @@ export default function DashboardPage() { }, ); - const agentsQuery = useListAgentsApiV1AgentsGet( + const agentsQuery = useListAgentsApiV1AgentsGet< + listAgentsApiV1AgentsGetResponse, + ApiError + >( { limit: 200 }, { query: { @@ -519,7 +560,10 @@ export default function DashboardPage() { }, ); - const activityQuery = useListActivityApiV1ActivityGet( + const activityQuery = useListActivityApiV1ActivityGet< + listActivityApiV1ActivityGetResponse, + ApiError + >( { limit: 200 }, { query: { @@ -530,10 +574,46 @@ export default function DashboardPage() { }, ); + const forgejoRepositoriesQuery = useQuery({ + queryKey: ["dashboard", "forgejo", "repositories"], + enabled: Boolean(isSignedIn), + refetchInterval: 60_000, + refetchOnMount: "always", + queryFn: () => getForgejoRepositories(), + }); + + const forgejoRepositories = useMemo( + () => forgejoRepositoriesQuery.data ?? [], + [forgejoRepositoriesQuery.data], + ); + const forgejoOrganizationId = useMemo( + () => + forgejoRepositories.find((repository) => repository.organization_id) + ?.organization_id ?? null, + [forgejoRepositories], + ); + + const forgejoMetricsQuery = useQuery({ + queryKey: ["dashboard", "forgejo", "metrics", forgejoOrganizationId], + enabled: Boolean( + isSignedIn && + !forgejoRepositoriesQuery.isLoading && + !forgejoRepositoriesQuery.error, + ), + refetchInterval: 60_000, + refetchOnMount: "always", + queryFn: () => { + if (!forgejoOrganizationId) return Promise.resolve(null); + return getForgejoMetrics({ organization_id: forgejoOrganizationId }); + }, + }); + const boards = useMemo( () => boardsQuery.data?.status === 200 - ? [...(boardsQuery.data.data.items ?? [])].sort((a, b) => a.name.localeCompare(b.name)) + ? [...(boardsQuery.data.data.items ?? [])].sort((a, b) => + a.name.localeCompare(b.name), + ) : [], [boardsQuery.data], ); @@ -541,15 +621,20 @@ export default function DashboardPage() { const agents = useMemo( () => agentsQuery.data?.status === 200 - ? [...(agentsQuery.data.data.items ?? [])].sort((a, b) => a.name.localeCompare(b.name)) + ? [...(agentsQuery.data.data.items ?? [])].sort((a, b) => + a.name.localeCompare(b.name), + ) : [], [agentsQuery.data], ); - const metrics = metricsQuery.data?.status === 200 ? metricsQuery.data.data : null; + const metrics = + metricsQuery.data?.status === 200 ? metricsQuery.data.data : null; const onlineAgents = useMemo( - () => agents.filter((agent) => (agent.status ?? "").toLowerCase() === "online").length, + () => + agents.filter((agent) => (agent.status ?? "").toLowerCase() === "online") + .length, [agents], ); const gatewayTargets = useMemo(() => { @@ -564,7 +649,9 @@ export default function DashboardPage() { boardName: board.name, }); } - return [...byGateway.values()].sort((a, b) => a.boardName.localeCompare(b.boardName)); + return [...byGateway.values()].sort((a, b) => + a.boardName.localeCompare(b.boardName), + ); }, [boards]); const hasConfiguredGateways = gatewayTargets.length > 0; @@ -622,7 +709,9 @@ export default function DashboardPage() { mainSessionError: null, error: null, requestError: - error instanceof Error ? error.message : "Gateway status request failed.", + error instanceof Error + ? error.message + : "Gateway status request failed.", }; } }), @@ -639,11 +728,13 @@ export default function DashboardPage() { gatewaySnapshots.flatMap((snapshot) => { if (snapshot.requestError) return []; const sourceLabel = snapshot.gatewayUrl || snapshot.boardName; - return toSessionSummaries(snapshot.sessions, snapshot.mainSession).map((session) => ({ - ...session, - key: `${snapshot.gatewayId}:${session.key}`, - subtitle: `${sourceLabel} · ${session.subtitle}`, - })); + return toSessionSummaries(snapshot.sessions, snapshot.mainSession).map( + (session) => ({ + ...session, + key: `${snapshot.gatewayId}:${session.key}`, + subtitle: `${sourceLabel} · ${session.subtitle}`, + }), + ); }), [gatewaySnapshots], ); @@ -669,7 +760,9 @@ export default function DashboardPage() { const recentLogs = orderedActivityEvents.slice(0, 8); const latestThroughputPoint = - metrics?.throughput.primary.points?.[metrics.throughput.primary.points.length - 1] ?? null; + metrics?.throughput.primary.points?.[ + metrics.throughput.primary.points.length - 1 + ] ?? null; const throughputTotal = (metrics?.throughput.primary.points ?? []).reduce( (sum, point) => sum + Number(point.value ?? 0), 0, @@ -685,11 +778,18 @@ export default function DashboardPage() { const doneTasksMetric = metrics?.kpis.done_tasks ?? 0; const activeAgentsMetric = onlineAgents; - const tasksTotal = inboxTasksMetric + inProgressTasksMetric + reviewTasksMetric + doneTasksMetric; - const tasksInProgressMetric = metrics?.kpis.tasks_in_progress ?? inProgressTasksMetric; + const tasksTotal = + inboxTasksMetric + + inProgressTasksMetric + + reviewTasksMetric + + doneTasksMetric; + const tasksInProgressMetric = + metrics?.kpis.tasks_in_progress ?? inProgressTasksMetric; const errorRateMetric = Number(metrics?.kpis.error_rate_pct ?? 0); const reviewBacklogRatio = - inProgressTasksMetric > 0 ? reviewTasksMetric / inProgressTasksMetric : null; + inProgressTasksMetric > 0 + ? reviewTasksMetric / inProgressTasksMetric + : null; const gatewayConnectedCount = gatewaySnapshots.filter( (snapshot) => !snapshot.requestError && snapshot.connected, @@ -697,11 +797,11 @@ export default function DashboardPage() { const gatewayDisconnectedCount = gatewaySnapshots.filter( (snapshot) => !snapshot.requestError && !snapshot.connected, ).length; - const gatewayUnavailableCount = gatewaySnapshots.filter( - (snapshot) => Boolean(snapshot.requestError), + const gatewayUnavailableCount = gatewaySnapshots.filter((snapshot) => + Boolean(snapshot.requestError), ).length; - const gatewayHealthErrorCount = gatewaySnapshots.filter( - (snapshot) => Boolean(snapshot.error || snapshot.mainSessionError), + const gatewayHealthErrorCount = gatewaySnapshots.filter((snapshot) => + Boolean(snapshot.error || snapshot.mainSessionError), ).length; const countedSessions = gatewaySnapshots.reduce( @@ -732,9 +832,11 @@ export default function DashboardPage() { const gatewayStatusTone: SummaryRow["tone"] = gatewayStatusLabel === "All connected" ? "success" - : gatewayStatusLabel === "Checking" || gatewayStatusLabel === "Not configured" + : gatewayStatusLabel === "Checking" || + gatewayStatusLabel === "Not configured" ? "default" - : gatewayStatusLabel === "Partially connected" || gatewayStatusLabel === "Disconnected" + : gatewayStatusLabel === "Partially connected" || + gatewayStatusLabel === "Disconnected" ? "warning" : "danger"; @@ -768,7 +870,10 @@ export default function DashboardPage() { label: "Completed tasks", value: formatCount(throughputTotal), }, - { label: "Average throughput", value: formatPerDay(throughputTotal, DASHBOARD_RANGE_DAYS) }, + { + label: "Average throughput", + value: formatPerDay(throughputTotal, DASHBOARD_RANGE_DAYS), + }, { label: "Error rate", value: formatPercent(errorRateMetric), @@ -777,7 +882,10 @@ export default function DashboardPage() { { label: "Completion consistency", value: `${formatCount(completionDaysCount)} active days`, - tone: completionDaysCount >= Math.ceil(DASHBOARD_RANGE_DAYS * 0.75) ? "success" : "default", + tone: + completionDaysCount >= Math.ceil(DASHBOARD_RANGE_DAYS * 0.75) + ? "success" + : "default", }, { label: "Review backlog ratio", @@ -799,7 +907,11 @@ export default function DashboardPage() { ]; const gatewayRows: SummaryRow[] = [ - { label: "Gateway status", value: gatewayStatusLabel, tone: gatewayStatusTone }, + { + label: "Gateway status", + value: gatewayStatusLabel, + tone: gatewayStatusTone, + }, { label: "Configured gateways", value: formatCount(gatewayTargets.length) }, { label: "Connected gateways", @@ -814,13 +926,23 @@ export default function DashboardPage() { { label: "Gateways with issues", value: formatCount(gatewayHealthErrorCount + gatewayDisconnectedCount), - tone: gatewayHealthErrorCount + gatewayDisconnectedCount > 0 ? "warning" : "success", + tone: + gatewayHealthErrorCount + gatewayDisconnectedCount > 0 + ? "warning" + : "success", }, ]; const pendingApprovalItems = metrics?.pending_approvals.items ?? []; const pendingApprovalsTotal = metrics?.pending_approvals.total ?? 0; const hasPendingApprovals = pendingApprovalItems.length > 0; const activityFeedHref = "/activity"; + const forgejoIssueMetrics = forgejoMetricsQuery.data ?? null; + const forgejoIssueMetricsError = + forgejoRepositoriesQuery.error?.message ?? + forgejoMetricsQuery.error?.message ?? + null; + const forgejoIssueMetricsLoading = + forgejoRepositoriesQuery.isLoading || forgejoMetricsQuery.isLoading; const shouldIgnoreRowNavigation = (target: EventTarget | null): boolean => { if (!(target instanceof HTMLElement)) return false; @@ -940,11 +1062,17 @@ export default function DashboardPage() { />
-
- + +
+ +
+
-

Pending Approvals

+

+ Pending Approvals +

{pendingApprovalsTotal > pendingApprovalItems.length ? (

- Showing latest {formatCount(pendingApprovalItems.length)} of{" "} - {formatCount(pendingApprovalsTotal)} pending approvals. + Showing latest {formatCount(pendingApprovalItems.length)}{" "} + of {formatCount(pendingApprovalsTotal)} pending approvals.

) : null}
@@ -1020,8 +1150,12 @@ export default function DashboardPage() {
-

Sessions

- {formatCount(activeSessions)} +

+ Sessions +

+ + {formatCount(activeSessions)} +
{!hasConfiguredGateways ? ( @@ -1037,8 +1171,8 @@ export default function DashboardPage() { {gatewayUnavailableCount > 0 ? (
{formatCount(gatewayUnavailableCount)} gateway - {gatewayUnavailableCount === 1 ? "" : "s"} unavailable; showing sessions - from reachable gateways. + {gatewayUnavailableCount === 1 ? "" : "s"}{" "} + unavailable; showing sessions from reachable gateways.
) : null} {sessionSummaries.map((session) => ( @@ -1051,16 +1185,22 @@ export default function DashboardPage() {

{session.title}

-

{session.subtitle}

+

+ {session.subtitle} +

- {session.usage === DASH ? "Usage unavailable" : session.usage} + {session.usage === DASH + ? "Usage unavailable" + : session.usage}

{session.lastSeenAt @@ -1086,7 +1226,9 @@ export default function DashboardPage() {

-

Recent Activity

+

+ Recent Activity +

handleLogRowClick(interactionEvent, eventHref) } @@ -1117,7 +1259,9 @@ export default function DashboardPage() {
@@ -1137,7 +1281,9 @@ export default function DashboardPage() {
No activity yet -

Activity appears here when events are emitted.

+

+ Activity appears here when events are emitted. +

)}
diff --git a/frontend/src/app/git-projects/issues/page.tsx b/frontend/src/app/git-projects/issues/page.tsx index 965a703..63afd15 100644 --- a/frontend/src/app/git-projects/issues/page.tsx +++ b/frontend/src/app/git-projects/issues/page.tsx @@ -2,7 +2,8 @@ export const dynamic = "force-dynamic"; -import { useState, useEffect } from "react"; +import { useState, useEffect, useMemo } from "react"; +import { useSearchParams } from "next/navigation"; import { AlertCircle } from "lucide-react"; import { DashboardPageLayout } from "@/components/templates/DashboardPageLayout"; @@ -16,16 +17,67 @@ import { import { ForgejoIssueFilters } from "@/components/git/ForgejoIssueFilters"; import { ForgejoIssuesTable } from "@/components/git/ForgejoIssuesTable"; +const STALE_ISSUE_DAYS = 14; +const STALE_ISSUE_MS = STALE_ISSUE_DAYS * 24 * 60 * 60 * 1000; +const RECENT_CLOSED_DAYS = 7; +const RECENT_CLOSED_MS = RECENT_CLOSED_DAYS * 24 * 60 * 60 * 1000; +const ISSUE_STATES = new Set(["all", "open", "closed"]); + +const normalizeStateFilter = (value: string | null): string => + value && ISSUE_STATES.has(value) ? value : "open"; + +const parsePositiveInteger = (value: string | null): number => { + const parsed = Number.parseInt(value ?? "", 10); + return Number.isFinite(parsed) && parsed > 0 ? parsed : 1; +}; + +const isStaleOpenIssue = (issue: ForgejoIssue, cutoffMs: number): boolean => { + if (issue.state !== "open") return false; + const updatedAt = new Date(issue.forgejo_updated_at || issue.updated_at); + if (Number.isNaN(updatedAt.getTime())) return false; + return updatedAt.getTime() < cutoffMs; +}; + +const isRecentlyClosedIssue = ( + issue: ForgejoIssue, + cutoffMs: number, +): boolean => { + if (issue.state !== "closed") return false; + const closedAt = new Date( + issue.forgejo_closed_at || issue.forgejo_updated_at || issue.updated_at, + ); + if (Number.isNaN(closedAt.getTime())) return false; + return closedAt.getTime() >= cutoffMs; +}; + export default function GitIssuesPage() { + const searchParams = useSearchParams(); + const initialStaleOnly = searchParams.get("stale") === "1"; + const initialRecentClosedOnly = + !initialStaleOnly && searchParams.get("recent") === "7d"; const [issues, setIssues] = useState([]); const [repos, setRepos] = useState([]); const [total, setTotal] = useState(0); const [isLoadingIssues, setIsLoadingIssues] = useState(true); const [error, setError] = useState(null); - const [stateFilter, setStateFilter] = useState("open"); - const [repoFilter, setRepoFilter] = useState("all"); - const [search, setSearch] = useState(""); - const [page, setPage] = useState(1); + const [stateFilter, setStateFilter] = useState(() => + initialStaleOnly + ? "open" + : initialRecentClosedOnly + ? "closed" + : normalizeStateFilter(searchParams.get("state")), + ); + const [repoFilter, setRepoFilter] = useState( + () => searchParams.get("repository_id") || "all", + ); + const [search, setSearch] = useState(() => searchParams.get("search") ?? ""); + const [staleOnly, setStaleOnly] = useState(initialStaleOnly); + const [recentClosedOnly, setRecentClosedOnly] = useState( + initialRecentClosedOnly, + ); + const [page, setPage] = useState(() => + parsePositiveInteger(searchParams.get("page")), + ); const limit = 30; useEffect(() => { @@ -45,11 +97,17 @@ export default function GitIssuesPage() { try { setIsLoadingIssues(true); const result = await getForgejoIssues({ - state: stateFilter !== "all" ? stateFilter : undefined, + state: staleOnly + ? "open" + : recentClosedOnly + ? "closed" + : stateFilter !== "all" + ? stateFilter + : undefined, repository_id: repoFilter !== "all" ? repoFilter : undefined, search: search || undefined, - page, - limit, + page: staleOnly || recentClosedOnly ? 1 : page, + limit: staleOnly || recentClosedOnly ? 100 : limit, }); setIssues(result.items); setTotal(result.total); @@ -66,17 +124,23 @@ export default function GitIssuesPage() { } })(); return () => controller.abort(); - }, [stateFilter, repoFilter, search, page]); + }, [stateFilter, repoFilter, search, staleOnly, recentClosedOnly, page]); const handleRefresh = async () => { try { setIsLoadingIssues(true); const result = await getForgejoIssues({ - state: stateFilter !== "all" ? stateFilter : undefined, + state: staleOnly + ? "open" + : recentClosedOnly + ? "closed" + : stateFilter !== "all" + ? stateFilter + : undefined, repository_id: repoFilter !== "all" ? repoFilter : undefined, search: search || undefined, - page, - limit, + page: staleOnly || recentClosedOnly ? 1 : page, + limit: staleOnly || recentClosedOnly ? 100 : limit, }); setIssues(result.items); setTotal(result.total); @@ -92,7 +156,28 @@ export default function GitIssuesPage() { } }; - const totalPages = Math.ceil(total / limit); + const staleCutoffMs = useMemo(() => Date.now() - STALE_ISSUE_MS, []); + const recentClosedCutoffMs = useMemo(() => Date.now() - RECENT_CLOSED_MS, []); + const visibleIssues = useMemo(() => { + if (staleOnly) { + return issues.filter((issue) => isStaleOpenIssue(issue, staleCutoffMs)); + } + if (recentClosedOnly) { + return issues.filter((issue) => + isRecentlyClosedIssue(issue, recentClosedCutoffMs), + ); + } + return issues; + }, [ + issues, + recentClosedCutoffMs, + recentClosedOnly, + staleCutoffMs, + staleOnly, + ]); + const isClientFiltered = staleOnly || recentClosedOnly; + const visibleTotal = isClientFiltered ? visibleIssues.length : total; + const totalPages = isClientFiltered ? 1 : Math.ceil(total / limit); return ( { setStateFilter(v); + setStaleOnly(false); + setRecentClosedOnly(false); setPage(1); }} repoFilter={repoFilter} @@ -124,6 +211,46 @@ export default function GitIssuesPage() { repos={repos} /> + {staleOnly ? ( +
+ + Showing open issues not updated in {STALE_ISSUE_DAYS}+ days. + + +
+ ) : null} + + {recentClosedOnly ? ( +
+ + Showing issues closed in the last {RECENT_CLOSED_DAYS} days. + + +
+ ) : null} + {error ? (
@@ -132,7 +259,7 @@ export default function GitIssuesPage() { ) : null} @@ -140,7 +267,7 @@ export default function GitIssuesPage() { {totalPages > 1 && (
- Page {page} of {totalPages} ({total} total) + Page {page} of {totalPages} ({visibleTotal} total)