mirror of
https://github.com/router-for-me/Cli-Proxy-API-Management-Center.git
synced 2026-06-16 21:03:58 +08:00
feat(toolbar): implement provider resource toolbar with sorting and filtering functionality
This commit is contained in:
@@ -180,6 +180,15 @@ export function getProviderTotalStats(
|
||||
return { success: entry.success, failure: entry.failed };
|
||||
}
|
||||
|
||||
export function getProviderRecentWindowStats(
|
||||
usageByProvider: ProviderRecentUsageMap,
|
||||
provider: string,
|
||||
apiKey?: string,
|
||||
baseUrl?: string
|
||||
): { success: number; failure: number } {
|
||||
return sumRecentRequests(getProviderRecentBuckets(usageByProvider, provider, apiKey, baseUrl));
|
||||
}
|
||||
|
||||
const collectOpenAIProviderRecentBuckets = (
|
||||
provider: OpenAIProviderConfig,
|
||||
usageByProvider: ProviderRecentUsageMap
|
||||
|
||||
@@ -5,16 +5,20 @@ import { useHeaderRefresh } from '@/hooks/useHeaderRefresh';
|
||||
import { Skeleton } from '@/components/ui/Skeleton';
|
||||
import { useAuthStore, useNotificationStore } from '@/stores';
|
||||
import { useProviderRecentRequests } from '@/components/providers/hooks/useProviderRecentRequests';
|
||||
import { getOpenAIProviderRecentWindowStats } from '@/components/providers/utils';
|
||||
import {
|
||||
getOpenAIProviderRecentWindowStats,
|
||||
getProviderRecentWindowStats,
|
||||
type ProviderRecentUsageMap,
|
||||
} from '@/components/providers/utils';
|
||||
import type { OpenAIProviderConfig } from '@/types';
|
||||
import { ProviderHeaderCard } from './components/ProviderHeaderCard';
|
||||
import { ProviderCategoryList } from './components/ProviderCategoryList';
|
||||
import { ProviderResourcePanel } from './components/ProviderResourcePanel';
|
||||
import type { OpenAIPanelControls } from './components/ProviderResourcePanel';
|
||||
import type { ProviderPanelControls } from './components/ProviderResourcePanel';
|
||||
import type {
|
||||
OpenAISortBy,
|
||||
ProviderSortBy,
|
||||
SortDir,
|
||||
} from './components/OpenAIBrandToolbar';
|
||||
} from './components/ProviderResourceToolbar';
|
||||
import { ProviderSheet, type ProviderSheetHandle } from './sheets/ProviderSheet';
|
||||
import { useProviderWorkbench } from './useProviderWorkbench';
|
||||
import type { ProviderBrand, ProviderResource } from './types';
|
||||
@@ -59,6 +63,63 @@ const matchesFilter = (r: ProviderResource, normalized: string): boolean => {
|
||||
return haystack.some((v) => v.includes(normalized));
|
||||
};
|
||||
|
||||
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
||||
Boolean(value && typeof value === 'object' && !Array.isArray(value));
|
||||
|
||||
const getResourceModels = (resource: ProviderResource): string[] => {
|
||||
if (!isRecord(resource.raw)) return [];
|
||||
if (resource.brand === 'ampcode') {
|
||||
const mappings = resource.raw.modelMappings;
|
||||
if (!Array.isArray(mappings)) return [];
|
||||
const seen = new Set<string>();
|
||||
mappings.forEach((mapping) => {
|
||||
if (!isRecord(mapping)) return;
|
||||
const from = typeof mapping.from === 'string' ? mapping.from.trim() : '';
|
||||
const to = typeof mapping.to === 'string' ? mapping.to.trim() : '';
|
||||
if (from) seen.add(from);
|
||||
if (to) seen.add(to);
|
||||
});
|
||||
return Array.from(seen);
|
||||
}
|
||||
const models = resource.raw.models;
|
||||
if (!Array.isArray(models)) return [];
|
||||
const seen = new Set<string>();
|
||||
models.forEach((model) => {
|
||||
if (!isRecord(model)) return;
|
||||
const name = typeof model.name === 'string' ? model.name.trim() : '';
|
||||
if (name) seen.add(name);
|
||||
});
|
||||
return Array.from(seen);
|
||||
};
|
||||
|
||||
const getResourcePriority = (resource: ProviderResource): number => {
|
||||
if (!isRecord(resource.raw)) return 0;
|
||||
const priority = resource.raw.priority;
|
||||
return typeof priority === 'number' && Number.isFinite(priority) ? priority : 0;
|
||||
};
|
||||
|
||||
const getResourceSortName = (resource: ProviderResource): string =>
|
||||
(resource.name ?? resource.identifier ?? resource.apiKeyPreview ?? '').toLowerCase();
|
||||
|
||||
const getResourceRecentSuccess = (
|
||||
resource: ProviderResource,
|
||||
usageByProvider: ProviderRecentUsageMap
|
||||
): number => {
|
||||
if (resource.brand === 'openaiCompatibility') {
|
||||
return getOpenAIProviderRecentWindowStats(
|
||||
resource.raw as OpenAIProviderConfig,
|
||||
usageByProvider
|
||||
).success;
|
||||
}
|
||||
if (resource.brand === 'ampcode') return 0;
|
||||
return getProviderRecentWindowStats(
|
||||
usageByProvider,
|
||||
resource.brand,
|
||||
resource.apiKey ?? undefined,
|
||||
resource.baseUrl ?? undefined
|
||||
).success;
|
||||
};
|
||||
|
||||
export function ProvidersWorkbenchPage() {
|
||||
const { t, i18n } = useTranslation();
|
||||
const connectionStatus = useAuthStore((s) => s.connectionStatus);
|
||||
@@ -70,11 +131,9 @@ export function ProvidersWorkbenchPage() {
|
||||
const workbench = useProviderWorkbench();
|
||||
const [activeBrand, setActiveBrand] = useState<ProviderBrand>('gemini');
|
||||
const [filter, setFilter] = useState('');
|
||||
const [openaiSortBy, setOpenaiSortBy] = useState<OpenAISortBy>('name');
|
||||
const [openaiSortDir, setOpenaiSortDir] = useState<SortDir>('asc');
|
||||
const [openaiSelectedModels, setOpenaiSelectedModels] = useState<Set<string>>(
|
||||
() => new Set()
|
||||
);
|
||||
const [providerSortBy, setProviderSortBy] = useState<ProviderSortBy>('name');
|
||||
const [providerSortDir, setProviderSortDir] = useState<SortDir>('asc');
|
||||
const [selectedModels, setSelectedModels] = useState<Set<string>>(() => new Set());
|
||||
const [sheetState, setSheetState] = useState<SheetState>({
|
||||
open: false,
|
||||
brand: 'gemini',
|
||||
@@ -109,85 +168,69 @@ export function ProvidersWorkbenchPage() {
|
||||
return activeGroup.resources.filter((r) => matchesFilter(r, normalized));
|
||||
}, [activeGroup, filter]);
|
||||
|
||||
const isOpenAI = activeGroup?.id === 'openaiCompatibility';
|
||||
|
||||
const availableOpenaiModels = useMemo(() => {
|
||||
if (!isOpenAI || !activeGroup) return [];
|
||||
const availableModels = useMemo(() => {
|
||||
if (!activeGroup) return [];
|
||||
const seen = new Set<string>();
|
||||
activeGroup.resources.forEach((r) => {
|
||||
const cfg = r.raw as OpenAIProviderConfig;
|
||||
cfg.models?.forEach((m) => {
|
||||
const name = (m.name ?? '').trim();
|
||||
if (name) seen.add(name);
|
||||
});
|
||||
getResourceModels(r).forEach((name) => seen.add(name));
|
||||
});
|
||||
return Array.from(seen).sort();
|
||||
}, [activeGroup, isOpenAI]);
|
||||
}, [activeGroup]);
|
||||
|
||||
const visibleResources = useMemo(() => {
|
||||
if (!isOpenAI) return filteredResources;
|
||||
|
||||
let arr = filteredResources;
|
||||
if (openaiSelectedModels.size > 0) {
|
||||
if (selectedModels.size > 0) {
|
||||
arr = arr.filter((r) => {
|
||||
const cfg = r.raw as OpenAIProviderConfig;
|
||||
return Boolean(
|
||||
cfg.models?.some((m) => openaiSelectedModels.has((m.name ?? '').trim()))
|
||||
);
|
||||
const models = getResourceModels(r);
|
||||
return models.some((name) => selectedModels.has(name));
|
||||
});
|
||||
}
|
||||
|
||||
const sorted = [...arr].sort((a, b) => {
|
||||
let diff = 0;
|
||||
if (openaiSortBy === 'name') {
|
||||
const an = (a.name ?? a.identifier ?? '').toLowerCase();
|
||||
const bn = (b.name ?? b.identifier ?? '').toLowerCase();
|
||||
diff = an.localeCompare(bn);
|
||||
} else if (openaiSortBy === 'priority') {
|
||||
const ap = (a.raw as OpenAIProviderConfig).priority ?? 0;
|
||||
const bp = (b.raw as OpenAIProviderConfig).priority ?? 0;
|
||||
if (providerSortBy === 'name') {
|
||||
diff = getResourceSortName(a).localeCompare(getResourceSortName(b));
|
||||
} else if (providerSortBy === 'priority') {
|
||||
const ap = getResourcePriority(a);
|
||||
const bp = getResourcePriority(b);
|
||||
diff = ap - bp;
|
||||
} else {
|
||||
const aStats = getOpenAIProviderRecentWindowStats(
|
||||
a.raw as OpenAIProviderConfig,
|
||||
usageByProvider
|
||||
);
|
||||
const bStats = getOpenAIProviderRecentWindowStats(
|
||||
b.raw as OpenAIProviderConfig,
|
||||
usageByProvider
|
||||
);
|
||||
diff = aStats.success - bStats.success;
|
||||
diff =
|
||||
getResourceRecentSuccess(a, usageByProvider) -
|
||||
getResourceRecentSuccess(b, usageByProvider);
|
||||
}
|
||||
return openaiSortDir === 'asc' ? diff : -diff;
|
||||
if (diff === 0) {
|
||||
diff = a.originalIndex - b.originalIndex;
|
||||
}
|
||||
return providerSortDir === 'asc' ? diff : -diff;
|
||||
});
|
||||
|
||||
return sorted;
|
||||
}, [
|
||||
filteredResources,
|
||||
isOpenAI,
|
||||
openaiSelectedModels,
|
||||
openaiSortBy,
|
||||
openaiSortDir,
|
||||
providerSortBy,
|
||||
providerSortDir,
|
||||
selectedModels,
|
||||
usageByProvider,
|
||||
]);
|
||||
|
||||
const openaiControls = useMemo<OpenAIPanelControls | undefined>(() => {
|
||||
if (!isOpenAI) return undefined;
|
||||
const toolbarControls = useMemo<ProviderPanelControls | undefined>(() => {
|
||||
if (!activeGroup) return undefined;
|
||||
return {
|
||||
sortBy: openaiSortBy,
|
||||
sortDir: openaiSortDir,
|
||||
onSortBy: setOpenaiSortBy,
|
||||
onSortDir: setOpenaiSortDir,
|
||||
availableModels: availableOpenaiModels,
|
||||
selectedModels: openaiSelectedModels,
|
||||
onSelectedModelsChange: setOpenaiSelectedModels,
|
||||
sortBy: providerSortBy,
|
||||
sortDir: providerSortDir,
|
||||
onSortBy: setProviderSortBy,
|
||||
onSortDir: setProviderSortDir,
|
||||
availableModels,
|
||||
selectedModels,
|
||||
onSelectedModelsChange: setSelectedModels,
|
||||
};
|
||||
}, [
|
||||
availableOpenaiModels,
|
||||
isOpenAI,
|
||||
openaiSelectedModels,
|
||||
openaiSortBy,
|
||||
openaiSortDir,
|
||||
activeGroup,
|
||||
availableModels,
|
||||
providerSortBy,
|
||||
providerSortDir,
|
||||
selectedModels,
|
||||
]);
|
||||
|
||||
const totalResources = useMemo(
|
||||
@@ -381,7 +424,7 @@ export function ProvidersWorkbenchPage() {
|
||||
if (!ok) return;
|
||||
setActiveBrand(brand);
|
||||
setFilter('');
|
||||
setOpenaiSelectedModels(new Set());
|
||||
setSelectedModels(new Set());
|
||||
if (isSwitching) {
|
||||
closeSheet();
|
||||
}
|
||||
@@ -396,7 +439,7 @@ export function ProvidersWorkbenchPage() {
|
||||
selectedId={sheetState.open ? sheetState.resource?.id ?? null : null}
|
||||
disableMutations={disableMutations}
|
||||
usageByProvider={usageByProvider}
|
||||
openaiControls={openaiControls}
|
||||
toolbarControls={toolbarControls}
|
||||
onView={openView}
|
||||
onEdit={openEdit}
|
||||
onDelete={handleDelete}
|
||||
|
||||
@@ -10,10 +10,10 @@ import type { ProviderRecentUsageMap } from '@/components/providers/utils';
|
||||
import type { ProviderBrand, ProviderGroup, ProviderResource } from '../types';
|
||||
import { ProviderResourceTable } from './ProviderResourceTable';
|
||||
import {
|
||||
OpenAIBrandToolbar,
|
||||
type OpenAISortBy,
|
||||
ProviderResourceToolbar,
|
||||
type ProviderSortBy,
|
||||
type SortDir,
|
||||
} from './OpenAIBrandToolbar';
|
||||
} from './ProviderResourceToolbar';
|
||||
import styles from './ProviderResourcePanel.module.scss';
|
||||
|
||||
const LOGOS: Record<ProviderBrand, { src: string; invertOnDark?: boolean }> = {
|
||||
@@ -25,10 +25,10 @@ const LOGOS: Record<ProviderBrand, { src: string; invertOnDark?: boolean }> = {
|
||||
ampcode: { src: ampcodeLogo },
|
||||
};
|
||||
|
||||
export interface OpenAIPanelControls {
|
||||
sortBy: OpenAISortBy;
|
||||
export interface ProviderPanelControls {
|
||||
sortBy: ProviderSortBy;
|
||||
sortDir: SortDir;
|
||||
onSortBy: (value: OpenAISortBy) => void;
|
||||
onSortBy: (value: ProviderSortBy) => void;
|
||||
onSortDir: (value: SortDir) => void;
|
||||
availableModels: ReadonlyArray<string>;
|
||||
selectedModels: ReadonlySet<string>;
|
||||
@@ -43,7 +43,7 @@ interface ProviderResourcePanelProps {
|
||||
selectedId: string | null;
|
||||
disableMutations?: boolean;
|
||||
usageByProvider?: ProviderRecentUsageMap;
|
||||
openaiControls?: OpenAIPanelControls;
|
||||
toolbarControls?: ProviderPanelControls;
|
||||
onView: (resource: ProviderResource) => void;
|
||||
onEdit: (resource: ProviderResource) => void;
|
||||
onDelete: (resource: ProviderResource) => void;
|
||||
@@ -59,7 +59,7 @@ export function ProviderResourcePanel({
|
||||
selectedId,
|
||||
disableMutations,
|
||||
usageByProvider,
|
||||
openaiControls,
|
||||
toolbarControls,
|
||||
onView,
|
||||
onEdit,
|
||||
onDelete,
|
||||
@@ -105,16 +105,17 @@ export function ProviderResourcePanel({
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
{openaiControls ? (
|
||||
{toolbarControls ? (
|
||||
<div className={styles.headerToolbarRow}>
|
||||
<OpenAIBrandToolbar
|
||||
sortBy={openaiControls.sortBy}
|
||||
sortDir={openaiControls.sortDir}
|
||||
onSortBy={openaiControls.onSortBy}
|
||||
onSortDir={openaiControls.onSortDir}
|
||||
availableModels={openaiControls.availableModels}
|
||||
selectedModels={openaiControls.selectedModels}
|
||||
onSelectedModelsChange={openaiControls.onSelectedModelsChange}
|
||||
<ProviderResourceToolbar
|
||||
key={group.id}
|
||||
sortBy={toolbarControls.sortBy}
|
||||
sortDir={toolbarControls.sortDir}
|
||||
onSortBy={toolbarControls.onSortBy}
|
||||
onSortDir={toolbarControls.onSortDir}
|
||||
availableModels={toolbarControls.availableModels}
|
||||
selectedModels={toolbarControls.selectedModels}
|
||||
onSelectedModelsChange={toolbarControls.onSelectedModelsChange}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
+8
-8
@@ -7,22 +7,22 @@ import {
|
||||
} from '@/components/ui/icons';
|
||||
import { Select } from '@/components/ui/Select';
|
||||
import { SelectionCheckbox } from '@/components/ui/SelectionCheckbox';
|
||||
import styles from './OpenAIBrandToolbar.module.scss';
|
||||
import styles from './ProviderResourceToolbar.module.scss';
|
||||
|
||||
export type OpenAISortBy = 'name' | 'priority' | 'recent-success';
|
||||
export type ProviderSortBy = 'name' | 'priority' | 'recent-success';
|
||||
export type SortDir = 'asc' | 'desc';
|
||||
|
||||
interface OpenAIBrandToolbarProps {
|
||||
sortBy: OpenAISortBy;
|
||||
interface ProviderResourceToolbarProps {
|
||||
sortBy: ProviderSortBy;
|
||||
sortDir: SortDir;
|
||||
onSortBy: (value: OpenAISortBy) => void;
|
||||
onSortBy: (value: ProviderSortBy) => void;
|
||||
onSortDir: (value: SortDir) => void;
|
||||
availableModels: ReadonlyArray<string>;
|
||||
selectedModels: ReadonlySet<string>;
|
||||
onSelectedModelsChange: (next: Set<string>) => void;
|
||||
}
|
||||
|
||||
export function OpenAIBrandToolbar({
|
||||
export function ProviderResourceToolbar({
|
||||
sortBy,
|
||||
sortDir,
|
||||
onSortBy,
|
||||
@@ -30,7 +30,7 @@ export function OpenAIBrandToolbar({
|
||||
availableModels,
|
||||
selectedModels,
|
||||
onSelectedModelsChange,
|
||||
}: OpenAIBrandToolbarProps) {
|
||||
}: ProviderResourceToolbarProps) {
|
||||
const { t } = useTranslation();
|
||||
const [filterOpen, setFilterOpen] = useState(false);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
@@ -86,7 +86,7 @@ export function OpenAIBrandToolbar({
|
||||
<Select
|
||||
value={sortBy}
|
||||
options={sortOptions}
|
||||
onChange={(value) => onSortBy(value as OpenAISortBy)}
|
||||
onChange={(value) => onSortBy(value as ProviderSortBy)}
|
||||
ariaLabel={t('providersPage.toolbar.sortBy')}
|
||||
size="sm"
|
||||
/>
|
||||
Reference in New Issue
Block a user