Fix Providers empty state blocking first provider setup

This commit is contained in:
Michael
2026-05-17 10:44:36 +00:00
committed by diegosouzapw
parent 1f27c344d4
commit b191173ae1
3 changed files with 81 additions and 48 deletions

View File

@@ -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

View File

@@ -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 = (

View File

@@ -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 = {