From 2ed810b8542bc1528da11fe2d5f7398b7dbd5a11 Mon Sep 17 00:00:00 2001 From: viettranx Date: Fri, 3 Apr 2026 22:59:25 +0700 Subject: [PATCH] =?UTF-8?q?refactor(ui):=20Phase=204=20complete=20?= =?UTF-8?q?=E2=80=94=20provider-pool=20adapter=20eliminates=20DRY=20violat?= =?UTF-8?q?ion?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Create adapters/provider-pool.adapter.ts with 6 pure functions that replace duplicated CodexPoolEntry construction in 3 files: - provider-overview.tsx (inline poolEntries useMemo) - agent-codex-pool-page.tsx (35-line buildEntries callback) - provider-pool-activity-section.tsx (30-line entries useMemo) Also centralizes resolveProviderAvailability (was copied in 3 places). --- ui/web/src/adapters/provider-pool.adapter.ts | 156 ++++++++++++++++++ .../agent-detail/agent-codex-pool-page.tsx | 69 +------- .../provider-detail/provider-overview.tsx | 9 +- .../provider-pool-activity-section.tsx | 39 +---- 4 files changed, 173 insertions(+), 100 deletions(-) create mode 100644 ui/web/src/adapters/provider-pool.adapter.ts diff --git a/ui/web/src/adapters/provider-pool.adapter.ts b/ui/web/src/adapters/provider-pool.adapter.ts new file mode 100644 index 00000000..4f1725d2 --- /dev/null +++ b/ui/web/src/adapters/provider-pool.adapter.ts @@ -0,0 +1,156 @@ +import type { CodexPoolEntry } from "@/pages/agents/agent-detail/codex-pool-entry-types"; +import type { CodexPoolProviderCount } from "@/pages/agents/agent-detail/hooks/use-codex-pool-activity"; +import type { ChatGPTOAuthAvailability } from "@/pages/providers/hooks/use-chatgpt-oauth-provider-statuses"; +import type { ChatGPTOAuthProviderQuota } from "@/pages/providers/hooks/use-chatgpt-oauth-provider-quotas"; + +/** Minimal provider shape needed for pool entry construction. */ +export interface PoolProviderInfo { + id?: string; + display_name?: string; + enabled?: boolean; +} + +/** Resolve OAuth availability from status map with enabled fallback. */ +export function resolveProviderAvailability( + providerName: string, + statusByName: Map, + enabled?: boolean, +): ChatGPTOAuthAvailability { + return ( + statusByName.get(providerName)?.availability ?? + (enabled === false ? "disabled" : "needs_sign_in") + ); +} + +/** + * Build a CodexPoolEntry for a provider name with live activity counts. + * Used when CodexPoolProviderCount data is available (activity views). + */ +export function toPoolEntryWithCounts( + providerName: string, + count: CodexPoolProviderCount, + preferredProviderName: string, + providerByName: Map, + statusByName: Map, + quotaByName: Map, +): CodexPoolEntry { + const provider = providerByName.get(providerName); + return { + name: providerName, + label: provider?.display_name || providerName, + availability: resolveProviderAvailability(providerName, statusByName, provider?.enabled), + role: providerName === preferredProviderName ? "preferred" : "extra", + requestCount: count.request_count, + directSelectionCount: count.direct_selection_count, + failoverServeCount: count.failover_serve_count, + successCount: count.success_count, + failureCount: count.failure_count, + consecutiveFailures: count.consecutive_failures, + successRate: count.success_rate, + healthScore: count.health_score, + healthState: count.health_state ?? "idle", + lastSelectedAt: count.last_selected_at, + lastFailoverAt: count.last_failover_at, + lastUsedAt: count.last_used_at, + lastSuccessAt: count.last_success_at, + lastFailureAt: count.last_failure_at, + providerHref: provider?.id ? `/providers/${provider.id}` : undefined, + quota: quotaByName.get(providerName), + }; +} + +/** + * Build a CodexPoolEntry for a provider name with zeroed counts. + * Used when no activity data is available (pool configuration views). + */ +export function toPoolEntry( + providerName: string, + preferredProviderName: string, + providerByName: Map, + statusByName: Map, + quotaByName: Map, +): CodexPoolEntry { + const provider = providerByName.get(providerName); + return { + name: providerName, + label: provider?.display_name || providerName, + availability: resolveProviderAvailability(providerName, statusByName, provider?.enabled), + role: providerName === preferredProviderName ? "preferred" : "extra", + requestCount: 0, + directSelectionCount: 0, + failoverServeCount: 0, + successCount: 0, + failureCount: 0, + consecutiveFailures: 0, + successRate: 0, + healthScore: 0, + healthState: "idle", + providerHref: provider?.id ? `/providers/${provider.id}` : undefined, + quota: quotaByName.get(providerName), + }; +} + +/** + * Batch-transform a list of provider names into CodexPoolEntry[] with zeroed counts. + * Used in pool configuration views (no activity data). + */ +export function toPoolEntries( + poolNames: string[], + preferredProviderName: string, + providerByName: Map, + statusByName: Map, + quotaByName: Map, +): CodexPoolEntry[] { + return poolNames.map((name) => + toPoolEntry(name, preferredProviderName, providerByName, statusByName, quotaByName), + ); +} + +/** + * Batch-transform CodexPoolProviderCount[] into CodexPoolEntry[] with live counts. + * Used in pool activity views where counts drive the entry list (provider-pool-activity-section). + */ +export function toPoolEntriesWithCounts( + counts: CodexPoolProviderCount[], + preferredProviderName: string, + providerByName: Map, + statusByName: Map, + quotaByName: Map, +): CodexPoolEntry[] { + return counts.map((c) => + toPoolEntryWithCounts( + c.provider_name, + c, + preferredProviderName, + providerByName, + statusByName, + quotaByName, + ), + ); +} + +/** + * Build CodexPoolEntry[] for a list of pool provider names, merging in any available + * activity counts by name. Names with no count data get zeroed fields. + * directSelectionCount falls back to request_count when direct_selection_count is absent. + * Used in agent codex pool page where pool names drive the list, not counts. + */ +export function toPoolEntriesMerged( + poolNames: string[], + counts: CodexPoolProviderCount[], + preferredProviderName: string, + providerByName: Map, + statusByName: Map, + quotaByName: Map, +): CodexPoolEntry[] { + const countsByName = new Map(counts.map((c) => [c.provider_name, c])); + return poolNames.map((providerName) => { + const count = countsByName.get(providerName); + if (!count) return toPoolEntry(providerName, preferredProviderName, providerByName, statusByName, quotaByName); + // directSelectionCount falls back to request_count when field is absent (older API responses) + return { + ...toPoolEntryWithCounts(providerName, count, preferredProviderName, providerByName, statusByName, quotaByName), + directSelectionCount: count.direct_selection_count ?? count.request_count ?? 0, + }; + }); +} diff --git a/ui/web/src/pages/agents/agent-detail/agent-codex-pool-page.tsx b/ui/web/src/pages/agents/agent-detail/agent-codex-pool-page.tsx index ae9bac17..89e621d2 100644 --- a/ui/web/src/pages/agents/agent-detail/agent-codex-pool-page.tsx +++ b/ui/web/src/pages/agents/agent-detail/agent-codex-pool-page.tsx @@ -1,12 +1,9 @@ -import { useCallback, useEffect, useMemo, useRef, useState } from "react"; +import { useEffect, useMemo, useRef, useState } from "react"; import { useNavigate, useParams } from "react-router"; import { useTranslation } from "react-i18next"; import { DetailPageSkeleton } from "@/components/shared/loading-skeleton"; import { useProviders } from "@/pages/providers/hooks/use-providers"; -import { - useChatGPTOAuthProviderStatuses, - type ChatGPTOAuthAvailability, -} from "@/pages/providers/hooks/use-chatgpt-oauth-provider-statuses"; +import { useChatGPTOAuthProviderStatuses } from "@/pages/providers/hooks/use-chatgpt-oauth-provider-statuses"; import { useChatGPTOAuthProviderQuotas } from "@/pages/providers/hooks/use-chatgpt-oauth-provider-quotas"; import { useAuthStore } from "@/stores/use-auth-store"; import type { ChatGPTOAuthRoutingConfig } from "@/types/agent"; @@ -24,27 +21,14 @@ import { } from "./agent-display-utils"; import { getRouteReadiness } from "./chatgpt-oauth-quota-utils"; import { ChatGPTOAuthRoutingSection } from "./config-sections"; -import { - CodexPoolActivityPanel, - type CodexPoolEntry, -} from "./codex-pool-activity-panel"; +import { CodexPoolActivityPanel } from "./codex-pool-activity-panel"; import { CodexPoolPageHeader } from "./codex-pool-page-header"; import { buildDraftRouting, routingDraftSignature, } from "./codex-pool-routing-draft-utils"; import { useCodexPoolActivity } from "./hooks/use-codex-pool-activity"; - -function providerStatus( - providerName: string, - statusByName: Map, - enabled?: boolean, -): ChatGPTOAuthAvailability { - return ( - statusByName.get(providerName)?.availability ?? - (enabled === false ? "disabled" : "needs_sign_in") - ); -} +import { toPoolEntriesMerged } from "@/adapters/provider-pool.adapter"; export function AgentCodexPoolPage() { const { id = "" } = useParams<{ id: string }>(); @@ -171,50 +155,13 @@ export function AgentCodexPoolPage() { refetch: refreshActivity, } = useCodexPoolActivity(agent?.id ?? id, 8, Boolean(agent && isEligible)); - const buildEntries = useCallback( - (poolNames: string[]): CodexPoolEntry[] => { - if (!agent) return []; - const countsByName = new Map( - activity.provider_counts.map((item) => [item.provider_name, item]), - ); - return poolNames.map((providerName) => { - const provider = providerByName.get(providerName); - const count = countsByName.get(providerName); - return { - name: providerName, - label: provider?.display_name || providerName, - availability: providerStatus(providerName, statusByName, provider?.enabled), - role: providerName === agent.provider ? "preferred" : "extra", - requestCount: count?.request_count ?? 0, - directSelectionCount: - count?.direct_selection_count ?? count?.request_count ?? 0, - failoverServeCount: count?.failover_serve_count ?? 0, - successCount: count?.success_count ?? 0, - failureCount: count?.failure_count ?? 0, - consecutiveFailures: count?.consecutive_failures ?? 0, - successRate: count?.success_rate ?? 0, - healthScore: count?.health_score ?? 0, - healthState: count?.health_state ?? "idle", - lastSelectedAt: count?.last_selected_at, - lastFailoverAt: count?.last_failover_at, - lastUsedAt: count?.last_used_at, - lastSuccessAt: count?.last_success_at, - lastFailureAt: count?.last_failure_at, - providerHref: provider?.id ? `/providers/${provider.id}` : undefined, - quota: quotaByName.get(providerName), - }; - }); - }, - [activity.provider_counts, agent, providerByName, quotaByName, statusByName], - ); - const liveEntries = useMemo( - () => buildEntries(savedEffectiveRouting.poolProviderNames), - [buildEntries, savedEffectiveRouting.poolProviderNames], + () => !agent ? [] : toPoolEntriesMerged(savedEffectiveRouting.poolProviderNames, activity.provider_counts, agent.provider, providerByName, statusByName, quotaByName), + [activity.provider_counts, agent, providerByName, quotaByName, savedEffectiveRouting.poolProviderNames, statusByName], ); const draftEntries = useMemo( - () => buildEntries(draftEffectiveRouting.poolProviderNames), - [buildEntries, draftEffectiveRouting.poolProviderNames], + () => !agent ? [] : toPoolEntriesMerged(draftEffectiveRouting.poolProviderNames, activity.provider_counts, agent.provider, providerByName, statusByName, quotaByName), + [activity.provider_counts, agent, draftEffectiveRouting.poolProviderNames, providerByName, quotaByName, statusByName], ); const routeEntries = useMemo( diff --git a/ui/web/src/pages/providers/provider-detail/provider-overview.tsx b/ui/web/src/pages/providers/provider-detail/provider-overview.tsx index 79bce182..84be4f93 100644 --- a/ui/web/src/pages/providers/provider-detail/provider-overview.tsx +++ b/ui/web/src/pages/providers/provider-detail/provider-overview.tsx @@ -29,8 +29,8 @@ import type { ChatGPTOAuthRoutingConfig } from "@/types/agent"; import { useChatGPTOAuthProviderStatuses } from "../hooks/use-chatgpt-oauth-provider-statuses"; import { useChatGPTOAuthProviderQuotas } from "../hooks/use-chatgpt-oauth-provider-quotas"; import { ChatGPTOAuthRoutingSection } from "@/pages/agents/agent-detail/config-sections"; -import type { CodexPoolEntry } from "@/pages/agents/agent-detail/codex-pool-activity-panel"; import { useProviderCodexPoolActivity } from "../hooks/use-provider-codex-pool-activity"; +import { toPoolEntries } from "@/adapters/provider-pool.adapter"; import { NO_API_KEY_TYPES, NO_EMBEDDING_TYPES, @@ -169,12 +169,9 @@ export function ProviderOverview({ provider, onUpdate }: ProviderOverviewProps) return Array.from(new Set(selectedPoolProviderNames.filter((n) => { if (!n) return false; const item = providerByName.get(n); return providerStatus(n, statusByName, item?.enabled) === "ready"; }))); }, [isOAuth, providerByName, selectedPoolProviderNames, statusByName]); const { quotaByName, isLoading: quotasLoading, isFetching: quotasFetching } = useChatGPTOAuthProviderQuotas(quotaProviderNames, isOAuth); - const poolEntries = useMemo(() => { + const poolEntries = useMemo(() => { if (!canEditPoolRouting) return []; - return selectedPoolProviderNames.map((n) => { - const item = providerByName.get(n); - return { name: n, label: item?.display_name || n, availability: providerStatus(n, statusByName, item?.enabled), role: n === provider.name ? "preferred" : "extra", requestCount: 0, directSelectionCount: 0, failoverServeCount: 0, successCount: 0, failureCount: 0, consecutiveFailures: 0, successRate: 0, healthScore: 0, healthState: "idle" as const, providerHref: item?.id ? `/providers/${item.id}` : undefined, quota: quotaByName.get(n) }; - }); + return toPoolEntries(selectedPoolProviderNames, provider.name, providerByName, statusByName, quotaByName); }, [canEditPoolRouting, provider.name, providerByName, quotaByName, selectedPoolProviderNames, statusByName]); const isPoolOwner = canEditPoolRouting && selectedPoolProviderNames.length > 1; const { data: poolActivity, isFetching: poolActivityFetching, refetch: refreshPoolActivity } = useProviderCodexPoolActivity(provider.id, 8, isPoolOwner); diff --git a/ui/web/src/pages/providers/provider-detail/provider-pool-activity-section.tsx b/ui/web/src/pages/providers/provider-detail/provider-pool-activity-section.tsx index 01cc9364..170e55da 100644 --- a/ui/web/src/pages/providers/provider-detail/provider-pool-activity-section.tsx +++ b/ui/web/src/pages/providers/provider-detail/provider-pool-activity-section.tsx @@ -6,15 +6,14 @@ import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { EmptyState } from "@/components/shared/empty-state"; import { cn } from "@/lib/utils"; -import type { CodexPoolProviderCount } from "@/pages/agents/agent-detail/hooks/use-codex-pool-activity"; +import type { CodexPoolProviderCount, CodexPoolRecentRequest } from "@/pages/agents/agent-detail/hooks/use-codex-pool-activity"; import { CodexPoolMemberCard } from "@/pages/agents/agent-detail/codex-pool-member-card"; import { CodexPoolRecentRequestsList } from "@/pages/agents/agent-detail/codex-pool-recent-requests-list"; -import type { CodexPoolEntry } from "@/pages/agents/agent-detail/codex-pool-entry-types"; import type { ChatGPTOAuthAvailability } from "../hooks/use-chatgpt-oauth-provider-statuses"; import type { ChatGPTOAuthProviderQuota } from "../hooks/use-chatgpt-oauth-provider-quotas"; import type { ProviderData } from "../hooks/use-providers"; import type { ProviderCodexPoolAgentCount } from "../hooks/use-provider-codex-pool-activity"; -import type { CodexPoolRecentRequest } from "@/pages/agents/agent-detail/hooks/use-codex-pool-activity"; +import { toPoolEntriesWithCounts } from "@/adapters/provider-pool.adapter"; function MonitorStat({ label, value }: { label: string; value: string }) { return ( @@ -57,36 +56,10 @@ export function ProviderPoolActivitySection({ const { t } = useTranslation("providers"); const { t: ta } = useTranslation("agents"); - const entries = useMemo(() => { - return providerCounts.map((c) => { - const p = providerByName.get(c.provider_name); - const availability: ChatGPTOAuthAvailability = - statusByName.get(c.provider_name)?.availability ?? - (p?.enabled === false ? "disabled" : "needs_sign_in"); - return { - name: c.provider_name, - label: p?.display_name || c.provider_name, - availability, - role: c.provider_name === provider.name ? "preferred" as const : "extra" as const, - requestCount: c.request_count, - directSelectionCount: c.direct_selection_count, - failoverServeCount: c.failover_serve_count, - successCount: c.success_count, - failureCount: c.failure_count, - consecutiveFailures: c.consecutive_failures, - successRate: c.success_rate, - healthScore: c.health_score, - healthState: c.health_state ?? "idle", - lastSelectedAt: c.last_selected_at, - lastFailoverAt: c.last_failover_at, - lastUsedAt: c.last_used_at, - lastSuccessAt: c.last_success_at, - lastFailureAt: c.last_failure_at, - providerHref: p?.id ? `/providers/${p.id}` : undefined, - quota: quotaByName.get(c.provider_name), - }; - }); - }, [provider.name, providerByName, providerCounts, quotaByName, statusByName]); + const entries = useMemo( + () => toPoolEntriesWithCounts(providerCounts, provider.name, providerByName, statusByName, quotaByName), + [provider.name, providerByName, providerCounts, quotaByName, statusByName], + ); const failoverCount = recentRequests.filter((r) => r.used_failover).length;