mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-26 09:52:11 +03:00
Fix Providers empty state blocking first provider setup
This commit is contained in:
@@ -31,6 +31,8 @@ import {
|
||||
buildMergedOAuthProviderEntries,
|
||||
buildStaticProviderEntries,
|
||||
filterConfiguredProviderEntries,
|
||||
shouldApplyConfiguredOnlyFilter,
|
||||
shouldShowFirstProviderHint,
|
||||
} from "./providerPageUtils";
|
||||
import type { ProviderEntry } from "./providerPageUtils";
|
||||
import { readConfiguredOnlyPreference, writeConfiguredOnlyPreference } from "./providerPageStorage";
|
||||
@@ -423,6 +425,11 @@ export default function ProvidersPage() {
|
||||
textIcon: "CC",
|
||||
}));
|
||||
|
||||
const effectiveShowConfiguredOnly = shouldApplyConfiguredOnlyFilter(
|
||||
showConfiguredOnly,
|
||||
connections.length
|
||||
);
|
||||
|
||||
const oauthProviderEntriesAll = buildMergedOAuthProviderEntries(
|
||||
OAUTH_PROVIDERS,
|
||||
FREE_PROVIDERS,
|
||||
@@ -430,7 +437,7 @@ export default function ProvidersPage() {
|
||||
);
|
||||
const oauthProviderEntries = filterConfiguredProviderEntries(
|
||||
oauthProviderEntriesAll,
|
||||
showConfiguredOnly,
|
||||
effectiveShowConfiguredOnly,
|
||||
searchQuery,
|
||||
showFreeOnly
|
||||
);
|
||||
@@ -445,37 +452,37 @@ export default function ProvidersPage() {
|
||||
!VIDEO_PROVIDER_IDS.has(entry.providerId) &&
|
||||
!EMBEDDING_RERANK_PROVIDER_IDS.has(entry.providerId)
|
||||
),
|
||||
showConfiguredOnly,
|
||||
effectiveShowConfiguredOnly,
|
||||
searchQuery,
|
||||
showFreeOnly
|
||||
);
|
||||
const aggregatorProviderEntries = filterConfiguredProviderEntries(
|
||||
apiKeyProviderEntriesAll.filter((entry) => AGGREGATOR_PROVIDER_IDS.has(entry.providerId)),
|
||||
showConfiguredOnly,
|
||||
effectiveShowConfiguredOnly,
|
||||
searchQuery,
|
||||
showFreeOnly
|
||||
);
|
||||
const imageProviderEntries = filterConfiguredProviderEntries(
|
||||
apiKeyProviderEntriesAll.filter((entry) => IMAGE_ONLY_PROVIDER_IDS.has(entry.providerId)),
|
||||
showConfiguredOnly,
|
||||
effectiveShowConfiguredOnly,
|
||||
searchQuery,
|
||||
showFreeOnly
|
||||
);
|
||||
const enterpriseProviderEntries = filterConfiguredProviderEntries(
|
||||
apiKeyProviderEntriesAll.filter((entry) => ENTERPRISE_CLOUD_PROVIDER_IDS.has(entry.providerId)),
|
||||
showConfiguredOnly,
|
||||
effectiveShowConfiguredOnly,
|
||||
searchQuery,
|
||||
showFreeOnly
|
||||
);
|
||||
const videoProviderEntries = filterConfiguredProviderEntries(
|
||||
apiKeyProviderEntriesAll.filter((entry) => VIDEO_PROVIDER_IDS.has(entry.providerId)),
|
||||
showConfiguredOnly,
|
||||
effectiveShowConfiguredOnly,
|
||||
searchQuery,
|
||||
showFreeOnly
|
||||
);
|
||||
const embeddingRerankProviderEntries = filterConfiguredProviderEntries(
|
||||
apiKeyProviderEntriesAll.filter((entry) => EMBEDDING_RERANK_PROVIDER_IDS.has(entry.providerId)),
|
||||
showConfiguredOnly,
|
||||
effectiveShowConfiguredOnly,
|
||||
searchQuery,
|
||||
showFreeOnly
|
||||
);
|
||||
@@ -483,7 +490,7 @@ export default function ProvidersPage() {
|
||||
const webCookieProviderEntriesAll = buildStaticProviderEntries("web-cookie", getProviderStats);
|
||||
const webCookieProviderEntries = filterConfiguredProviderEntries(
|
||||
webCookieProviderEntriesAll,
|
||||
showConfiguredOnly,
|
||||
effectiveShowConfiguredOnly,
|
||||
searchQuery,
|
||||
showFreeOnly
|
||||
);
|
||||
@@ -491,7 +498,7 @@ export default function ProvidersPage() {
|
||||
const localProviderEntriesAll = buildStaticProviderEntries("local", getProviderStats);
|
||||
const localProviderEntries = filterConfiguredProviderEntries(
|
||||
localProviderEntriesAll,
|
||||
showConfiguredOnly,
|
||||
effectiveShowConfiguredOnly,
|
||||
searchQuery,
|
||||
showFreeOnly
|
||||
);
|
||||
@@ -499,7 +506,7 @@ export default function ProvidersPage() {
|
||||
const searchProviderEntriesAll = buildStaticProviderEntries("search", getProviderStats);
|
||||
const searchProviderEntries = filterConfiguredProviderEntries(
|
||||
searchProviderEntriesAll,
|
||||
showConfiguredOnly,
|
||||
effectiveShowConfiguredOnly,
|
||||
searchQuery,
|
||||
showFreeOnly
|
||||
);
|
||||
@@ -507,7 +514,7 @@ export default function ProvidersPage() {
|
||||
const audioProviderEntriesAll = buildStaticProviderEntries("audio", getProviderStats);
|
||||
const audioProviderEntries = filterConfiguredProviderEntries(
|
||||
audioProviderEntriesAll,
|
||||
showConfiguredOnly,
|
||||
effectiveShowConfiguredOnly,
|
||||
searchQuery,
|
||||
showFreeOnly
|
||||
);
|
||||
@@ -515,7 +522,7 @@ export default function ProvidersPage() {
|
||||
const cloudAgentProviderEntriesAll = buildStaticProviderEntries("cloud-agent", getProviderStats);
|
||||
const cloudAgentProviderEntries = filterConfiguredProviderEntries(
|
||||
cloudAgentProviderEntriesAll,
|
||||
showConfiguredOnly,
|
||||
effectiveShowConfiguredOnly,
|
||||
searchQuery,
|
||||
showFreeOnly
|
||||
);
|
||||
@@ -523,7 +530,7 @@ export default function ProvidersPage() {
|
||||
const upstreamProxyEntriesAll = buildStaticProviderEntries("upstream-proxy", getProviderStats);
|
||||
const upstreamProxyEntries = filterConfiguredProviderEntries(
|
||||
upstreamProxyEntriesAll,
|
||||
showConfiguredOnly,
|
||||
effectiveShowConfiguredOnly,
|
||||
searchQuery,
|
||||
showFreeOnly
|
||||
);
|
||||
@@ -553,7 +560,7 @@ export default function ProvidersPage() {
|
||||
];
|
||||
const compatibleProviderEntries = filterConfiguredProviderEntries(
|
||||
compatibleProviderEntriesAll,
|
||||
showConfiguredOnly,
|
||||
effectiveShowConfiguredOnly,
|
||||
searchQuery,
|
||||
showFreeOnly
|
||||
);
|
||||
@@ -592,7 +599,7 @@ export default function ProvidersPage() {
|
||||
);
|
||||
const freeSectionEntries = filterConfiguredProviderEntries(
|
||||
freeSectionEntriesAll,
|
||||
showConfiguredOnly,
|
||||
effectiveShowConfiguredOnly,
|
||||
searchQuery
|
||||
);
|
||||
|
||||
@@ -618,40 +625,37 @@ export default function ProvidersPage() {
|
||||
);
|
||||
}
|
||||
|
||||
const totalConfigured =
|
||||
oauthProviderEntriesAll.filter((e) => Number(e.stats?.total || 0) > 0).length +
|
||||
apiKeyProviderEntriesAll.filter((e) => Number(e.stats?.total || 0) > 0).length +
|
||||
webCookieProviderEntriesAll.filter((e) => Number(e.stats?.total || 0) > 0).length +
|
||||
localProviderEntriesAll.filter((e) => Number(e.stats?.total || 0) > 0).length;
|
||||
|
||||
if (totalConfigured === 0 && !searchQuery && !showAllProviders) {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center py-20 text-center">
|
||||
<div className="flex items-center justify-center size-16 rounded-full bg-primary/10 mb-4">
|
||||
<span className="material-symbols-outlined text-[32px] text-primary">dns</span>
|
||||
</div>
|
||||
<h2 className="text-xl font-semibold text-text-main">{t("addFirstProvider")}</h2>
|
||||
<p className="text-sm text-text-muted mt-2 max-w-md">{t("addFirstProviderDesc")}</p>
|
||||
<div className="flex items-center gap-3 mt-4">
|
||||
<Button icon="add" onClick={() => setShowAllProviders(true)}>
|
||||
{t("addProvider")}
|
||||
</Button>
|
||||
<a
|
||||
href="https://docs.omniroute.io/providers"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="inline-flex items-center gap-1.5 px-4 py-2 text-sm font-medium rounded-lg border border-border text-text-muted hover:text-text-main hover:bg-bg-subtle transition-colors"
|
||||
>
|
||||
<span className="material-symbols-outlined text-[16px]">help</span>
|
||||
{t("learnMore")}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
const showFirstProviderHint =
|
||||
shouldShowFirstProviderHint(connections.length, searchQuery) && !showAllProviders;
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-6">
|
||||
{showFirstProviderHint && (
|
||||
<Card padding="lg">
|
||||
<div className="flex flex-col items-center justify-center text-center">
|
||||
<div className="flex items-center justify-center size-16 rounded-full bg-primary/10 mb-4">
|
||||
<span className="material-symbols-outlined text-[32px] text-primary">dns</span>
|
||||
</div>
|
||||
<h2 className="text-xl font-semibold text-text-main">
|
||||
{t("addFirstProvider") || "Add your first provider"}
|
||||
</h2>
|
||||
<p className="text-sm text-text-muted mt-2 max-w-md">
|
||||
{t("addFirstProviderDesc") ||
|
||||
"Connect an AI provider to start routing requests through OmniRoute. You can use free providers, API keys, or OAuth accounts."}
|
||||
</p>
|
||||
<a
|
||||
href="https://docs.omniroute.io/providers"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="inline-flex items-center gap-1.5 px-4 py-2 text-sm font-medium rounded-lg border border-border text-text-muted hover:text-text-main hover:bg-bg-subtle transition-colors mt-4"
|
||||
>
|
||||
<span className="material-symbols-outlined text-[16px]">help</span>
|
||||
{t("learnMore") || "Learn more"}
|
||||
</a>
|
||||
</div>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* Provider Summary Card */}
|
||||
<Card padding="sm">
|
||||
<div className="flex flex-col gap-3">
|
||||
@@ -678,9 +682,10 @@ export default function ProvidersPage() {
|
||||
</div>
|
||||
<Toggle
|
||||
size="sm"
|
||||
checked={showConfiguredOnly}
|
||||
checked={effectiveShowConfiguredOnly}
|
||||
onChange={setShowConfiguredOnly}
|
||||
label={t("showConfiguredOnly")}
|
||||
disabled={connections.length === 0}
|
||||
className="rounded-lg border border-border bg-bg-subtle px-3 py-1.5"
|
||||
/>
|
||||
<button
|
||||
@@ -953,9 +958,10 @@ export default function ProvidersPage() {
|
||||
/>
|
||||
<Toggle
|
||||
size="sm"
|
||||
checked={showConfiguredOnly}
|
||||
checked={effectiveShowConfiguredOnly}
|
||||
onChange={setShowConfiguredOnly}
|
||||
label={t("showConfiguredOnly")}
|
||||
disabled={connections.length === 0}
|
||||
className="rounded-lg border border-border bg-bg-subtle px-3 py-1.5"
|
||||
/>
|
||||
<button
|
||||
|
||||
@@ -21,6 +21,20 @@ export interface ProviderEntry<TProvider = Record<string, unknown>> {
|
||||
toggleAuthType: "oauth" | "free" | "apikey";
|
||||
}
|
||||
|
||||
export function shouldApplyConfiguredOnlyFilter(
|
||||
showConfiguredOnly: boolean,
|
||||
connectionCount: number
|
||||
): boolean {
|
||||
return showConfiguredOnly && connectionCount > 0;
|
||||
}
|
||||
|
||||
export function shouldShowFirstProviderHint(
|
||||
connectionCount: number,
|
||||
searchQuery?: string
|
||||
): boolean {
|
||||
return connectionCount === 0 && !searchQuery?.trim();
|
||||
}
|
||||
|
||||
type ProviderRecord<TProvider = Record<string, unknown>> = Record<string, TProvider>;
|
||||
|
||||
type GetProviderStats = (
|
||||
|
||||
@@ -189,6 +189,19 @@ test("configured-only preference parser only enables explicit true values", () =
|
||||
assert.equal(providerPageStorage.parseConfiguredOnlyPreference(undefined), false);
|
||||
});
|
||||
|
||||
test("configured-only filter is ignored before the first provider is connected", () => {
|
||||
assert.equal(providerPageUtils.shouldApplyConfiguredOnlyFilter(true, 0), false);
|
||||
assert.equal(providerPageUtils.shouldApplyConfiguredOnlyFilter(false, 0), false);
|
||||
assert.equal(providerPageUtils.shouldApplyConfiguredOnlyFilter(true, 1), true);
|
||||
});
|
||||
|
||||
test("first-provider hint is shown only when no providers are connected and search is empty", () => {
|
||||
assert.equal(providerPageUtils.shouldShowFirstProviderHint(0, ""), true);
|
||||
assert.equal(providerPageUtils.shouldShowFirstProviderHint(0, " "), true);
|
||||
assert.equal(providerPageUtils.shouldShowFirstProviderHint(0, "codex"), false);
|
||||
assert.equal(providerPageUtils.shouldShowFirstProviderHint(1, ""), false);
|
||||
});
|
||||
|
||||
test("configured-only preference storage round-trips correctly", () => {
|
||||
const storage = new Map();
|
||||
const mockStorage = {
|
||||
|
||||
Reference in New Issue
Block a user