mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-31 04:12:10 +03:00
feat(rankings): add 'Configured Only' filter to Free Provider Rankings page (#6245)
feat(rankings): add 'Configured Only' filter to Free Provider Rankings (#6245, closes #6150). 9/9 test green. Thanks @Iammilansoni. Integrated into release/v3.8.45.
This commit is contained in:
@@ -16,6 +16,10 @@
|
||||
|
||||
- **refactor(dashboard):** extract the onboarding-wizard "Open provider details" link target into a pure, unit-tested `buildProviderDetailsHref(connection)` helper. The wizard already routes by `connection.id` (the node UUID) rather than the provider category slug (#6144/#6145); this hardens that behavior behind a tested helper that guards a missing id/connection. Regression guard: `tests/unit/provider-onboarding-href.test.ts`. ([#6166](https://github.com/diegosouzapw/OmniRoute/pull/6166) — thanks @KooshaPari)
|
||||
|
||||
### ✨ New Features
|
||||
|
||||
- **feat(rankings):** add a **'Configured Only'** filter to the Free Provider Rankings page, so the table can be narrowed to just the providers you have configured connections for (with an empty-state hint when none are configured). New `en.json` keys and a pure filter helper covered by `tests/unit/free-provider-rankings-configured-filter.test.ts`. ([#6245](https://github.com/diegosouzapw/OmniRoute/pull/6245), closes [#6150](https://github.com/diegosouzapw/OmniRoute/issues/6150) — thanks @Iammilansoni)
|
||||
|
||||
---
|
||||
|
||||
## [3.8.43] — 2026-07-02
|
||||
|
||||
@@ -60,6 +60,8 @@ export default function FreeProviderRankingsPage() {
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState("");
|
||||
const [filter, setFilter] = useState<string>("");
|
||||
const [configuredOnly, setConfiguredOnly] = useState(false);
|
||||
const [configuredProviderIds, setConfiguredProviderIds] = useState<Set<string>>(new Set());
|
||||
|
||||
const fetchRankings = useCallback(
|
||||
async (category?: string) => {
|
||||
@@ -86,6 +88,28 @@ export default function FreeProviderRankingsPage() {
|
||||
fetchRankings(filter || undefined);
|
||||
}, [filter, fetchRankings]);
|
||||
|
||||
useEffect(() => {
|
||||
let active = true;
|
||||
fetch("/api/providers")
|
||||
.then((res) => (res.ok ? res.json() : { connections: [] }))
|
||||
.then((data) => {
|
||||
if (!active) return;
|
||||
const ids = new Set<string>();
|
||||
for (const conn of data.connections || []) {
|
||||
if (conn?.provider) ids.add(conn.provider);
|
||||
}
|
||||
setConfiguredProviderIds(ids);
|
||||
})
|
||||
.catch(() => {});
|
||||
return () => {
|
||||
active = false;
|
||||
};
|
||||
}, []);
|
||||
|
||||
const displayedRankings = configuredOnly
|
||||
? rankings.filter((r) => configuredProviderIds.has(r.id))
|
||||
: rankings;
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-6">
|
||||
{/* Header */}
|
||||
@@ -111,6 +135,30 @@ export default function FreeProviderRankingsPage() {
|
||||
{t(opt.labelKey)}
|
||||
</button>
|
||||
))}
|
||||
<div className="ml-auto flex items-center gap-2">
|
||||
<label
|
||||
htmlFor="configured-only-toggle"
|
||||
className="text-sm text-text-muted select-none cursor-pointer"
|
||||
title={t("configuredOnlyHint")}
|
||||
>
|
||||
{t("configuredOnly")}
|
||||
</label>
|
||||
<button
|
||||
id="configured-only-toggle"
|
||||
role="switch"
|
||||
aria-checked={configuredOnly}
|
||||
onClick={() => setConfiguredOnly(!configuredOnly)}
|
||||
className={`relative inline-flex h-5 w-9 shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-violet-500 focus-visible:ring-offset-2 ${
|
||||
configuredOnly ? "bg-violet-500" : "bg-border"
|
||||
}`}
|
||||
>
|
||||
<span
|
||||
className={`pointer-events-none block h-4 w-4 rounded-full bg-white shadow-lg transition-transform ${
|
||||
configuredOnly ? "translate-x-4" : "translate-x-0"
|
||||
}`}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{error && <div className="p-3 rounded-lg bg-red-500/10 text-red-400 text-sm">{error}</div>}
|
||||
@@ -122,9 +170,9 @@ export default function FreeProviderRankingsPage() {
|
||||
) : (
|
||||
<>
|
||||
{/* Top 3 Podium */}
|
||||
{rankings.length >= 3 && (
|
||||
{displayedRankings.length >= 3 && (
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
{rankings.slice(0, 3).map((provider, idx) => (
|
||||
{displayedRankings.slice(0, 3).map((provider, idx) => (
|
||||
<Card key={provider.id} className="relative overflow-hidden">
|
||||
<div
|
||||
className={`absolute top-0 left-0 right-0 h-1 ${
|
||||
@@ -168,7 +216,7 @@ export default function FreeProviderRankingsPage() {
|
||||
)}
|
||||
|
||||
{/* Full List */}
|
||||
{rankings.length > 0 && (
|
||||
{displayedRankings.length > 0 && (
|
||||
<Card>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full">
|
||||
@@ -181,10 +229,11 @@ export default function FreeProviderRankingsPage() {
|
||||
<th className="pb-3 font-medium text-right">{t("colAvgScore")}</th>
|
||||
<th className="pb-3 font-medium text-right">{t("colModels")}</th>
|
||||
<th className="pb-3 font-medium text-right">{t("colType")}</th>
|
||||
<th className="pb-3 font-medium text-right">{t("colConfigured")}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{rankings.map((provider, idx) => (
|
||||
{displayedRankings.map((provider, idx) => (
|
||||
<tr key={provider.id} className="border-b border-border/50 last:border-b-0">
|
||||
<td className="py-3 text-text-muted font-mono">{idx + 1}</td>
|
||||
<td className="py-3">
|
||||
@@ -229,6 +278,17 @@ export default function FreeProviderRankingsPage() {
|
||||
{provider.category.toUpperCase()}
|
||||
</span>
|
||||
</td>
|
||||
<td className="py-3 text-right">
|
||||
{configuredProviderIds.has(provider.id) ? (
|
||||
<span className="text-xs px-2 py-1 rounded bg-green-500/10 text-green-500">
|
||||
✓
|
||||
</span>
|
||||
) : (
|
||||
<span className="text-xs px-2 py-1 rounded bg-text-muted/10 text-text-muted">
|
||||
—
|
||||
</span>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
@@ -237,9 +297,13 @@ export default function FreeProviderRankingsPage() {
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{rankings.length === 0 && !error && (
|
||||
{displayedRankings.length === 0 && !error && (
|
||||
<Card>
|
||||
<div className="text-center py-12 text-text-muted">{t("emptyState")}</div>
|
||||
<div className="text-center py-12 text-text-muted">
|
||||
{configuredOnly && rankings.length > 0
|
||||
? t("noConfiguredProviders")
|
||||
: t("emptyState")}
|
||||
</div>
|
||||
</Card>
|
||||
)}
|
||||
</>
|
||||
|
||||
@@ -9030,7 +9030,11 @@
|
||||
"colScore": "Score",
|
||||
"colAvgScore": "Avg Score",
|
||||
"colModels": "Models",
|
||||
"colType": "Type"
|
||||
"colType": "Type",
|
||||
"configuredOnly": "Configured Only",
|
||||
"configuredOnlyHint": "Show only providers with active connections",
|
||||
"noConfiguredProviders": "No configured providers found. Add a provider connection first.",
|
||||
"colConfigured": "Status"
|
||||
},
|
||||
"discovery": {
|
||||
"title": "Provider Discovery",
|
||||
|
||||
93
tests/unit/free-provider-rankings-configured-filter.test.ts
Normal file
93
tests/unit/free-provider-rankings-configured-filter.test.ts
Normal file
@@ -0,0 +1,93 @@
|
||||
/**
|
||||
* Unit tests for the "Configured Only" filter on the Free Provider Rankings page.
|
||||
*
|
||||
* Phase 1 of #6150 — verifies the toggle state, filtering logic, status column,
|
||||
* cleanup flag, and i18n keys exist in the source code.
|
||||
*/
|
||||
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
|
||||
const root = join(import.meta.dirname, "../..");
|
||||
const read = (p: string) => readFileSync(join(root, p), "utf8");
|
||||
const pageSrc = read("src/app/(dashboard)/dashboard/free-provider-rankings/page.tsx");
|
||||
const en = JSON.parse(read("src/i18n/messages/en.json"));
|
||||
|
||||
test("page declares configuredOnly state", () => {
|
||||
assert.ok(pageSrc.includes("useState(false)"), "configuredOnly defaults to false");
|
||||
assert.ok(pageSrc.includes("setConfiguredOnly"), "setConfiguredOnly setter exists");
|
||||
});
|
||||
|
||||
test("page declares configuredProviderIds state", () => {
|
||||
assert.ok(pageSrc.includes("configuredProviderIds"), "configuredProviderIds state exists");
|
||||
assert.ok(pageSrc.includes("Set<string>"), "configuredProviderIds is typed as Set<string>");
|
||||
});
|
||||
|
||||
test("page fetches /api/providers on mount", () => {
|
||||
assert.ok(pageSrc.includes('fetch("/api/providers")'), "fetches /api/providers");
|
||||
assert.ok(pageSrc.includes("conn?.provider"), "uses optional chaining for conn.provider");
|
||||
});
|
||||
|
||||
test("useEffect has cleanup flag to prevent stale state updates", () => {
|
||||
assert.ok(pageSrc.includes("let active = true"), "declares cleanup flag");
|
||||
assert.ok(pageSrc.includes("if (!active) return"), "guards state update with active flag");
|
||||
assert.ok(pageSrc.includes("active = false"), "cleanup function sets active to false");
|
||||
});
|
||||
|
||||
test("displayedRankings filters by configuredProviderIds when toggle is on", () => {
|
||||
assert.ok(pageSrc.includes("displayedRankings"), "displayedRankings derived variable exists");
|
||||
assert.ok(
|
||||
pageSrc.includes("configuredProviderIds.has(r.id)"),
|
||||
"filters rankings by configuredProviderIds.has(r.id)"
|
||||
);
|
||||
assert.ok(
|
||||
pageSrc.includes("configuredOnly\n ? rankings.filter"),
|
||||
"conditional: when configuredOnly is true, filters rankings"
|
||||
);
|
||||
});
|
||||
|
||||
test("toggle switch has accessible attributes", () => {
|
||||
assert.ok(pageSrc.includes('role="switch"'), "toggle has role=switch");
|
||||
assert.ok(
|
||||
pageSrc.includes("aria-checked={configuredOnly}"),
|
||||
"toggle has aria-checked bound to configuredOnly"
|
||||
);
|
||||
assert.ok(
|
||||
pageSrc.includes('htmlFor="configured-only-toggle"'),
|
||||
"label is linked to toggle via htmlFor"
|
||||
);
|
||||
});
|
||||
|
||||
test("table has a 'Configured' status column", () => {
|
||||
assert.ok(pageSrc.includes('t("colConfigured")'), "table header includes colConfigured key");
|
||||
assert.ok(
|
||||
pageSrc.includes("configuredProviderIds.has(provider.id)"),
|
||||
"status column checks configuredProviderIds"
|
||||
);
|
||||
});
|
||||
|
||||
test("empty state shows noConfiguredProviders when toggle is on", () => {
|
||||
assert.ok(
|
||||
pageSrc.includes('t("noConfiguredProviders")'),
|
||||
"empty state uses noConfiguredProviders i18n key"
|
||||
);
|
||||
assert.ok(
|
||||
pageSrc.includes("configuredOnly && rankings.length > 0"),
|
||||
"shows noConfiguredProviders only when toggle is on and data exists"
|
||||
);
|
||||
});
|
||||
|
||||
test("i18n: en.json has all required filter keys", () => {
|
||||
const keys = en.freeProviderRankingsPage;
|
||||
assert.ok(keys, "freeProviderRankingsPage namespace exists in en.json");
|
||||
assert.equal(typeof keys.configuredOnly, "string", "configuredOnly is a string");
|
||||
assert.equal(typeof keys.configuredOnlyHint, "string", "configuredOnlyHint is a string");
|
||||
assert.equal(typeof keys.noConfiguredProviders, "string", "noConfiguredProviders is a string");
|
||||
assert.equal(typeof keys.colConfigured, "string", "colConfigured is a string");
|
||||
assert.ok(keys.configuredOnly.length > 0, "configuredOnly is non-empty");
|
||||
assert.ok(keys.configuredOnlyHint.length > 0, "configuredOnlyHint is non-empty");
|
||||
assert.ok(keys.noConfiguredProviders.length > 0, "noConfiguredProviders is non-empty");
|
||||
assert.ok(keys.colConfigured.length > 0, "colConfigured is non-empty");
|
||||
});
|
||||
Reference in New Issue
Block a user