mirror of
https://github.com/tiennm99/goclaw.git
synced 2026-10-13 08:15:29 +00:00
refactor(ui): Phase 4 complete — provider-pool adapter eliminates DRY violation
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).
This commit is contained in:
1 parent
1313f02cf4
commit
2ed810b854
4 files changed
+173
-100
No files matched your search
@@ -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<string, { availability: ChatGPTOAuthAvailability }>,
|
||||
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<string, PoolProviderInfo>,
|
||||
statusByName: Map<string, { availability: ChatGPTOAuthAvailability }>,
|
||||
quotaByName: Map<string, ChatGPTOAuthProviderQuota | null>,
|
||||
): 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<string, PoolProviderInfo>,
|
||||
statusByName: Map<string, { availability: ChatGPTOAuthAvailability }>,
|
||||
quotaByName: Map<string, ChatGPTOAuthProviderQuota | null>,
|
||||
): 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<string, PoolProviderInfo>,
|
||||
statusByName: Map<string, { availability: ChatGPTOAuthAvailability }>,
|
||||
quotaByName: Map<string, ChatGPTOAuthProviderQuota | null>,
|
||||
): 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<string, PoolProviderInfo>,
|
||||
statusByName: Map<string, { availability: ChatGPTOAuthAvailability }>,
|
||||
quotaByName: Map<string, ChatGPTOAuthProviderQuota | null>,
|
||||
): 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<string, PoolProviderInfo>,
|
||||
statusByName: Map<string, { availability: ChatGPTOAuthAvailability }>,
|
||||
quotaByName: Map<string, ChatGPTOAuthProviderQuota | null>,
|
||||
): 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,
|
||||
};
|
||||
});
|
||||
}
|
||||
@@ -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<string, { availability: ChatGPTOAuthAvailability }>,
|
||||
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(
|
||||
|
||||
@@ -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<CodexPoolEntry[]>(() => {
|
||||
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);
|
||||
|
||||
@@ -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<CodexPoolEntry[]>(() => {
|
||||
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;
|
||||
|
||||
|
||||
Reference in new issue
Block a user