Merge pull request #2316 from oyi77/feat/ui-rework

feat(ui): simple/advanced mode for Caveman & RTK + newbie UX improvements

Adapts oyi77's UX rework on top of our refactor/pages overhaul. Layout
priority: our 9-section sidebar restructure stays; PR's additions
(subtitles, intros, simple/advanced toggles, empty states, error labels)
are integrated into our structure.

Conflict resolution:

- index.tsx: union — exports InfoTooltip, PresetSlider (new shared
  components from PR) alongside our NoAuthProviderCard.
- en.json: union — kept our "OmniSkills"/"AgentSkills" labels and
  "API Key Manager" naming; added PR's subtitleKey strings, settings
  intro keys, and empty state keys.
- sidebarVisibility.ts: kept our 9-section structure; added subtitleKey?:
  string to SidebarItemDefinition and mapped subtitle keys onto the 7
  matching items (endpoints, api-manager, combos, batch, context-caveman,
  context-rtk, webhooks).
- Sidebar.tsx: kept our collapsible-section rendering; integrated PR's
  subtitle support into resolveItem() and renderNavLink() label area.
- CavemanContextPageClient.tsx: took PR's version — adds SegmentedControl
  for simple/advanced mode (gates full settings tab in advanced).
- RtkContextPageClient.tsx: took PR's version — adds SegmentedControl +
  Collapsible filter catalog.
- settings/page.tsx: kept our redirect (we converted tabs→pages). Ported
  PR's intro text paragraphs to /settings/ai, /settings/routing, and
  /settings/resilience subpages using the auto-merged i18n keys.
- HomePageClient.tsx: kept ours — we removed Providers Overview card in
  the refactor, and PR's empty state for that card is now redundant.
  PR's equivalent empty state at /dashboard/providers (in
  providers/page.tsx) auto-merged cleanly and serves the same purpose.

Closes #2316
This commit is contained in:
diegosouzapw
2026-05-17 02:13:22 -03:00
12 changed files with 357 additions and 87 deletions

View File

@@ -2,6 +2,7 @@
import { useEffect, useMemo, useState } from "react";
import { useTranslations } from "next-intl";
import { SegmentedControl } from "@/shared/components";
import CompressionSettingsTab from "@/app/(dashboard)/dashboard/settings/components/CompressionSettingsTab";
type AnalyticsSummary = {
@@ -44,6 +45,7 @@ export default function CavemanContextPageClient() {
const [settings, setSettings] = useState<CompressionSettings | null>(null);
const [languagePacks, setLanguagePacks] = useState<LanguagePack[]>([]);
const [saving, setSaving] = useState(false);
const [viewMode, setViewMode] = useState<"simple" | "advanced">("simple");
const refreshSettings = () => {
fetch("/api/context/caveman/config")
@@ -116,7 +118,27 @@ export default function CavemanContextPageClient() {
const previewPrompt = `[OmniRoute Caveman Output Mode]\n${t(`preview.${outputMode.intensity}`)}`;
return (
<div className="flex flex-col gap-6">
<div className="mx-auto flex max-w-6xl flex-col gap-6">
<header className="flex flex-col gap-2">
<div className="flex items-center justify-between gap-3">
<div className="flex items-center gap-3">
<span className="material-symbols-outlined text-[30px] text-primary">compress</span>
<div>
<h1 className="text-2xl font-bold text-text-main">{t("title")}</h1>
<p className="text-sm text-text-muted">{t("description")}</p>
</div>
</div>
<SegmentedControl
value={viewMode}
onChange={(v) => setViewMode(v as "simple" | "advanced")}
options={[
{ value: "simple", label: t("simpleMode") || "Simple" },
{ value: "advanced", label: t("advancedMode") || "Advanced" },
]}
/>
</div>
</header>
<section className="grid grid-cols-1 gap-3 sm:grid-cols-4">
{statCards.map(([label, value]) => (
<div key={label} className="rounded-lg border border-border bg-surface p-4">
@@ -246,7 +268,7 @@ export default function CavemanContextPageClient() {
</div>
</section>
<CompressionSettingsTab />
{viewMode === "advanced" && <CompressionSettingsTab />}
</div>
);
}

View File

@@ -2,6 +2,7 @@
import { useEffect, useMemo, useState } from "react";
import { useTranslations } from "next-intl";
import { SegmentedControl, Collapsible } from "@/shared/components";
type RtkFilter = {
id: string;
@@ -66,6 +67,7 @@ export default function RtkContextPageClient() {
const [sample, setSample] = useState(SAMPLE_OUTPUT);
const [preview, setPreview] = useState<PreviewResult | null>(null);
const [saving, setSaving] = useState(false);
const [viewMode, setViewMode] = useState<"simple" | "advanced">("simple");
useEffect(() => {
fetch("/api/context/rtk/filters")
@@ -138,7 +140,27 @@ export default function RtkContextPageClient() {
];
return (
<div className="flex flex-col gap-6">
<div className="mx-auto flex max-w-6xl flex-col gap-6">
<header className="flex flex-col gap-2">
<div className="flex items-center justify-between gap-3">
<div className="flex items-center gap-3">
<span className="material-symbols-outlined text-[30px] text-primary">filter_alt</span>
<div>
<h1 className="text-2xl font-bold text-text-main">{t("title")}</h1>
<p className="text-sm text-text-muted">{t("description")}</p>
</div>
</div>
<SegmentedControl
value={viewMode}
onChange={(v) => setViewMode(v as "simple" | "advanced")}
options={[
{ value: "simple", label: t("simpleMode") || "Simple" },
{ value: "advanced", label: t("advancedMode") || "Advanced" },
]}
/>
</div>
</header>
<section className="grid grid-cols-1 gap-3 sm:grid-cols-2 xl:grid-cols-4">
{statCards.map(([label, value]) => (
<div key={label} className="rounded-lg border border-border bg-surface p-4">
@@ -268,72 +290,84 @@ export default function RtkContextPageClient() {
</section>
)}
<section className="grid grid-cols-1 gap-4 xl:grid-cols-[1fr_1fr]">
<div className="rounded-lg border border-border bg-surface p-4">
<div className="mb-3 flex items-center justify-between">
<h2 className="text-sm font-semibold text-text-main">{t("filterTesting")}</h2>
<button
onClick={runPreview}
className="rounded-lg bg-primary px-3 py-2 text-sm font-medium text-white"
>
{t("run")}
</button>
</div>
<textarea
value={sample}
onChange={(event) => setSample(event.target.value)}
placeholder={t("pasteOutput")}
className="h-72 w-full rounded-lg border border-border bg-bg p-3 font-mono text-xs text-text-main"
/>
</div>
<div className="rounded-lg border border-border bg-surface p-4">
<h2 className="mb-3 text-sm font-semibold text-text-main">{t("result")}</h2>
{preview?.detection && (
<p className="mb-2 text-xs text-text-muted">
{t("detected")}: {preview.detection.type} (
{Math.round(preview.detection.confidence * 100)}%)
</p>
)}
<pre className="h-72 overflow-auto rounded-lg border border-border bg-bg p-3 text-xs text-text-main">
{preview ? JSON.stringify(preview, null, 2) : t("previewEmpty")}
</pre>
</div>
</section>
<section className="grid grid-cols-1 gap-3 md:grid-cols-2 xl:grid-cols-3">
{Object.entries(groupedFilters).map(([category, items]) => (
<div key={category} className="rounded-lg border border-border bg-surface p-4">
<h2 className="text-sm font-semibold capitalize text-text-main">{category}</h2>
<div className="mt-3 space-y-3">
{items.map((filter) => {
const enabled = config ? !config.disabledFilters.includes(filter.id) : true;
return (
<div
key={filter.id}
className="border-t border-border pt-3 first:border-t-0 first:pt-0"
>
<div className="flex items-start justify-between gap-3">
<div className="min-w-0">
<p className="text-sm font-medium text-text-main">{filter.name}</p>
<p className="mt-1 text-xs text-text-muted">{filter.description}</p>
</div>
<input
type="checkbox"
checked={enabled}
disabled={!config || saving}
onChange={(event) => toggleFilter(filter.id, event.target.checked)}
/>
</div>
<p className="mt-2 font-mono text-[11px] text-text-muted">
{filter.commandTypes.join(", ")}
</p>
</div>
);
})}
{viewMode === "advanced" && (
<section className="grid grid-cols-1 gap-4 xl:grid-cols-[1fr_1fr]">
<div className="rounded-lg border border-border bg-surface p-4">
<div className="mb-3 flex items-center justify-between">
<h2 className="text-sm font-semibold text-text-main">{t("filterTesting")}</h2>
<button
onClick={runPreview}
className="rounded-lg bg-primary px-3 py-2 text-sm font-medium text-white"
>
{t("run")}
</button>
</div>
<textarea
value={sample}
onChange={(event) => setSample(event.target.value)}
placeholder={t("pasteOutput")}
className="h-72 w-full rounded-lg border border-border bg-bg p-3 font-mono text-xs text-text-main"
/>
</div>
))}
</section>
<div className="rounded-lg border border-border bg-surface p-4">
<h2 className="mb-3 text-sm font-semibold text-text-main">{t("result")}</h2>
{preview?.detection && (
<p className="mb-2 text-xs text-text-muted">
{t("detected")}: {preview.detection.type} (
{Math.round(preview.detection.confidence * 100)}%)
</p>
)}
<pre className="h-72 overflow-auto rounded-lg border border-border bg-bg p-3 text-xs text-text-main">
{preview ? JSON.stringify(preview, null, 2) : t("previewEmpty")}
</pre>
</div>
</section>
)}
<Collapsible
title={t("filterCatalog") || "Filter Catalog"}
subtitle={t("filterCatalogDesc") || "Available output filters by category"}
icon="filter_list"
trailing={
<span className="text-xs text-text-muted">
{Object.values(groupedFilters).flat().length} {t("filtersActive") || "filters"}
</span>
}
defaultOpen={viewMode === "advanced"}
>
<div className="grid grid-cols-1 gap-3 md:grid-cols-2 xl:grid-cols-3">
{Object.entries(groupedFilters).map(([category, items]) => (
<div key={category} className="rounded-lg border border-border bg-bg p-3">
<h3 className="text-xs font-semibold capitalize text-text-main">{category}</h3>
<div className="mt-2 space-y-2">
{items.map((filter) => {
const enabled = config ? !config.disabledFilters.includes(filter.id) : true;
return (
<div
key={filter.id}
className="border-t border-border pt-2 first:border-t-0 first:pt-0"
>
<div className="flex items-start justify-between gap-2">
<div className="min-w-0">
<p className="text-xs font-medium text-text-main">{filter.name}</p>
<p className="mt-0.5 text-[11px] text-text-muted">{filter.description}</p>
</div>
<input
type="checkbox"
checked={enabled}
disabled={!config || saving}
onChange={(event) => toggleFilter(filter.id, event.target.checked)}
className="mt-0.5"
/>
</div>
</div>
);
})}
</div>
</div>
))}
</div>
</Collapsible>
</div>
);
}

View File

@@ -65,18 +65,18 @@ function getConnectionErrorTag(connection) {
if (!connection) return null;
const explicitType = connection.lastErrorType;
if (explicitType === "runtime_error") return "RUNTIME";
if (explicitType === "runtime_error") return "Runtime";
if (
explicitType === "upstream_auth_error" ||
explicitType === "auth_missing" ||
explicitType === "token_refresh_failed" ||
explicitType === "token_expired"
) {
return "AUTH";
return "Auth";
}
if (explicitType === "upstream_rate_limited") return "429";
if (explicitType === "upstream_unavailable") return "5XX";
if (explicitType === "network_error") return "NET";
if (explicitType === "upstream_rate_limited") return "Rate limited";
if (explicitType === "upstream_unavailable") return "Server error";
if (explicitType === "network_error") return "Network";
const numericCode = Number(connection.errorCode);
if (Number.isFinite(numericCode) && numericCode >= 400) {
@@ -84,19 +84,19 @@ function getConnectionErrorTag(connection) {
}
const fromMessage = getErrorCode(connection.lastError);
if (fromMessage === "401" || fromMessage === "403") return "AUTH";
if (fromMessage === "401" || fromMessage === "403") return "Auth";
if (fromMessage && fromMessage !== "ERR") return fromMessage;
const msg = (connection.lastError || "").toLowerCase();
if (msg.includes("runtime") || msg.includes("not runnable") || msg.includes("not installed"))
return "RUNTIME";
return "Runtime";
if (
msg.includes("invalid api key") ||
msg.includes("token invalid") ||
msg.includes("revoked") ||
msg.includes("unauthorized")
)
return "AUTH";
return "Auth";
return "ERR";
}
@@ -592,6 +592,40 @@ 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) {
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") || "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>
<div className="flex items-center gap-3 mt-4">
<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") || "Learn more"}
</a>
</div>
</div>
);
}
return (
<div className="flex flex-col gap-6">
{/* Provider Summary Card */}

View File

@@ -1,5 +1,6 @@
"use client";
import { useTranslations } from "next-intl";
import ThinkingBudgetTab from "../components/ThinkingBudgetTab";
import VisionBridgeSettingsTab from "../components/VisionBridgeSettingsTab";
import SystemPromptTab from "../components/SystemPromptTab";
@@ -7,8 +8,10 @@ import MemorySkillsTab from "../components/MemorySkillsTab";
import ModelsDevSyncTab from "../components/ModelsDevSyncTab";
export default function SettingsAiPage() {
const t = useTranslations("settings");
return (
<div className="space-y-6">
<p className="text-sm text-text-muted">{t("aiSettingsIntro")}</p>
<ThinkingBudgetTab />
<VisionBridgeSettingsTab />
<SystemPromptTab />

View File

@@ -1,7 +1,14 @@
"use client";
import { useTranslations } from "next-intl";
import ResilienceTab from "../components/ResilienceTab";
export default function SettingsResiliencePage() {
return <ResilienceTab />;
const t = useTranslations("settings");
return (
<div className="space-y-6">
<p className="text-sm text-text-muted">{t("resilienceSettingsIntro")}</p>
<ResilienceTab />
</div>
);
}

View File

@@ -1,5 +1,6 @@
"use client";
import { useTranslations } from "next-intl";
import RoutingTab from "../components/RoutingTab";
import ModelRoutingSection from "@/shared/components/ModelRoutingSection";
import ComboDefaultsTab from "../components/ComboDefaultsTab";
@@ -7,8 +8,10 @@ import ModelAliasesUnified from "../components/ModelAliasesUnified";
import BackgroundDegradationTab from "../components/BackgroundDegradationTab";
export default function SettingsRoutingPage() {
const t = useTranslations("settings");
return (
<div className="space-y-6">
<p className="text-sm text-text-muted">{t("routingSettingsIntro")}</p>
<RoutingTab />
<ModelRoutingSection />
<ComboDefaultsTab />

View File

@@ -365,6 +365,10 @@
"apiKeyForCheck": "Api Key For Check",
"cloudRequestTimeout": "Cloud Request Timeout",
"showConfiguredOnly": "Show Configured Only",
"showFreeOnly": "Free only",
"addFirstProvider": "Add your first provider",
"addFirstProviderDesc": "Connect an AI provider to start routing requests through OmniRoute. You can use free providers, API keys, or OAuth accounts.",
"learnMore": "Learn more",
"includeDomains": "Include Domains",
"promptCache": "Prompt Cache",
"cloudConnected": "Cloud Connected",
@@ -405,7 +409,7 @@
"expirationBannerExpiringSoonDesc": "Expiration Banner Expiring Soon Desc",
"source": "Source",
"failed": "Failed",
"apiKeyMgmt": "Api Key Mgmt",
"apiKeyMgmt": "API Key Management",
"mcpQuickStartStep1": "Mcp Quick Start Step1",
"runTest": "Run Test",
"loadingFallbackChains": "Loading Fallback Chains",
@@ -680,9 +684,16 @@
"docs": "Docs",
"issues": "Issues",
"endpoints": "Endpoints",
"endpointsSubtitle": "Your AI connection URLs",
"apiManager": "API Key Manager",
"apiManagerSubtitle": "Manage API keys and access",
"logs": "Logs",
"webhooks": "Webhooks",
"webhooksSubtitle": "Get notified of events",
"combosSubtitle": "Group providers for failover",
"batchSubtitle": "Process multiple requests",
"contextCavemanSubtitle": "Prompt compression",
"contextRtkSubtitle": "Output filtering",
"auditLog": "Audit Log",
"shutdown": "Shutdown",
"restart": "Restart",
@@ -1013,6 +1024,8 @@
"providersOverview": "Providers Overview",
"configuredOf": "{configured} configured of {total} available providers",
"noModelsAvailable": "No models available for this provider.",
"noProvidersConfigured": "No providers configured yet",
"addProvider": "Add a provider",
"configureFirst": "Configure a connection first in {providers}",
"configureProvider": "Configure Provider",
"modelAvailable": "{count} model available",
@@ -2734,7 +2747,6 @@
"welcomeDesc": "OmniRoute is your local AI API proxy. It routes requests to multiple AI providers with load balancing, failover, and usage tracking.",
"multiProvider": "Multi-Provider",
"usageTracking": "Usage Tracking",
"apiKeyMgmt": "API Key Mgmt",
"securityDesc": "Set a password to protect your dashboard, or skip for now.",
"providerDesc": "Connect your first AI provider. You can add more later.",
"apiKeyRequired": "API Key (required)",
@@ -2753,7 +2765,8 @@
"failedSetPassword": "Failed to set password. Try again.",
"failedAddProvider": "Failed to add provider. Try again.",
"connectionError": "Connection error. Please try again.",
"provider": "Provider"
"provider": "Provider",
"apiKeyHelp": "An API key is a password for AI services. Get one from your provider's website (e.g., platform.openai.com, console.anthropic.com)."
},
"providers": {
"title": "Providers",
@@ -3268,6 +3281,9 @@
"routing": "Routing",
"cache": "Cache",
"resilience": "Resilience",
"routingSettingsIntro": "Controls how your requests are routed, transformed, and sent to AI providers.",
"resilienceSettingsIntro": "Automatic retry, cooldown, and fallback when providers fail.",
"aiSettingsIntro": "AI-specific settings for thinking budget, model behavior, and compression.",
"systemPrompt": "System Prompt",
"thinkingBudget": "Thinking Budget",
"proxy": "Proxy",
@@ -3981,7 +3997,11 @@
"tokensFiltered": "Tokens filtered",
"filtersActive": "Filters active",
"requests": "Requests",
"avgSavings": "Avg savings"
"avgSavings": "Avg savings",
"simpleMode": "Simple",
"advancedMode": "Advanced",
"filterCatalog": "Filter Catalog",
"filterCatalogDesc": "Available output filters by category"
},
"contextCombos": {
"title": "Compression Combos",
@@ -4024,6 +4044,8 @@
"autoClarity": "Auto-Clarity Bypass",
"bypassConditions": "Bypass conditions",
"bypassConditionsList": "security, irreversible, clarification, order-sensitive",
"simpleMode": "Simple",
"advancedMode": "Advanced",
"preview": {
"lite": "Respond concise. Preserve technical terms, code, errors, URLs, and identifiers.",
"full": "Respond terse and compact. Preserve all technical substance.",

View File

@@ -0,0 +1,34 @@
"use client";
import { cn } from "@/shared/utils/cn";
interface InfoTooltipProps {
text: string;
className?: string;
}
export default function InfoTooltip({ text, className }: InfoTooltipProps) {
return (
<span className={cn("relative inline-flex group", className)}>
<span
className="material-symbols-outlined text-[16px] text-text-muted cursor-help"
aria-label={text}
>
info
</span>
<span
role="tooltip"
className={cn(
"absolute bottom-full left-1/2 -translate-x-1/2 mb-2",
"px-2.5 py-1.5 text-xs font-medium text-white bg-gray-900/95 rounded-md shadow-lg",
"whitespace-nowrap pointer-events-none",
"border border-white/10",
"opacity-0 group-hover:opacity-100 transition-opacity duration-150",
"z-50"
)}
>
{text}
</span>
</span>
);
}

View File

@@ -0,0 +1,64 @@
"use client";
import { cn } from "@/shared/utils/cn";
interface PresetSliderProps {
value: number;
onChange: (value: number) => void;
presets: { label: string; value: number }[];
min?: number;
max?: number;
step?: number;
className?: string;
}
export default function PresetSlider({
value,
onChange,
presets,
min = 0,
max = 100,
step = 1,
className,
}: PresetSliderProps) {
return (
<div className={cn("flex flex-col gap-3", className)}>
<div className="flex flex-wrap gap-2">
{presets.map((preset) => (
<button
key={preset.value}
type="button"
onClick={() => onChange(preset.value)}
className={cn(
"px-3 py-1.5 text-xs font-medium rounded-full transition-colors",
"border",
value === preset.value
? "bg-primary text-white border-primary"
: "bg-transparent text-text-muted border-black/10 dark:border-white/10 hover:border-primary/30 hover:text-text-main"
)}
>
{preset.label}
</button>
))}
</div>
<input
type="range"
min={min}
max={max}
step={step}
value={value}
onChange={(e) => onChange(Number(e.target.value))}
className={cn(
"w-full h-1.5 rounded-full appearance-none cursor-pointer",
"bg-black/10 dark:bg-white/10",
"accent-primary"
)}
/>
<div className="flex justify-between text-[10px] text-text-muted">
<span>{min}</span>
<span className="font-medium text-text-main">{value}</span>
<span>{max}</span>
</div>
</div>
);
}

View File

@@ -148,7 +148,12 @@ export default function Sidebar({
const resolveItem = (item: SidebarItemDefinition, hidden: Set<string>) => {
if (hidden.has(item.id)) return null;
return { ...item, label: getSidebarLabel(item.i18nKey, item.id) };
const subtitle = item.subtitleKey ? getSidebarLabel(item.subtitleKey, "") : undefined;
return {
...item,
label: getSidebarLabel(item.i18nKey, item.id),
subtitle: subtitle || undefined,
};
};
const hiddenSidebarSet = new Set(hiddenSidebarItems);
@@ -321,7 +326,14 @@ export default function Sidebar({
const content = (
<>
<span className={iconClassName}>{item.icon}</span>
{!collapsed && <span className="text-sm font-medium truncate">{item.label}</span>}
{!collapsed && (
<div className="flex min-w-0 flex-col">
<span className="truncate text-sm font-medium">{item.label}</span>
{item.subtitle && (
<span className="truncate text-[10px] text-text-muted/60">{item.subtitle}</span>
)}
</div>
)}
</>
);
const sharedProps = {

View File

@@ -34,6 +34,8 @@ export { default as FilterBar } from "./FilterBar";
export { default as ColumnToggle } from "./ColumnToggle";
export { default as DataTable } from "./DataTable";
export { default as NoAuthProviderCard } from "./NoAuthProviderCard";
export { default as InfoTooltip } from "./InfoTooltip";
export { default as PresetSlider } from "./PresetSlider";
// Layouts
export * from "./layouts";

View File

@@ -91,6 +91,7 @@ export interface SidebarItemDefinition {
id: HideableSidebarItemId;
href: string;
i18nKey: string;
subtitleKey?: string;
icon: string;
exact?: boolean;
external?: boolean;
@@ -131,10 +132,28 @@ const HOME_ITEMS: readonly SidebarItemDefinition[] = [
];
const OMNI_PROXY_ITEMS: readonly SidebarItemDefinition[] = [
{ id: "endpoints", href: "/dashboard/endpoint", i18nKey: "endpoints", icon: "api" },
{ id: "api-manager", href: "/dashboard/api-manager", i18nKey: "apiManager", icon: "vpn_key" },
{
id: "endpoints",
href: "/dashboard/endpoint",
i18nKey: "endpoints",
subtitleKey: "endpointsSubtitle",
icon: "api",
},
{
id: "api-manager",
href: "/dashboard/api-manager",
i18nKey: "apiManager",
subtitleKey: "apiManagerSubtitle",
icon: "vpn_key",
},
{ id: "providers", href: "/dashboard/providers", i18nKey: "providers", icon: "dns" },
{ id: "combos", href: "/dashboard/combos", i18nKey: "combos", icon: "layers" },
{
id: "combos",
href: "/dashboard/combos",
i18nKey: "combos",
subtitleKey: "combosSubtitle",
icon: "layers",
},
{ id: "limits", href: "/dashboard/limits", i18nKey: "quotaTracker", icon: "tune" },
];
@@ -148,12 +167,14 @@ const COMPRESSION_CONTEXT_GROUP: SidebarItemGroup = {
id: "context-caveman",
href: "/dashboard/context/caveman",
i18nKey: "contextCaveman",
subtitleKey: "contextCavemanSubtitle",
icon: "compress",
},
{
id: "context-rtk",
href: "/dashboard/context/rtk",
i18nKey: "contextRtk",
subtitleKey: "contextRtkSubtitle",
icon: "filter_alt",
},
{
@@ -184,7 +205,13 @@ const INTEGRATIONS_GROUP: SidebarItemGroup = {
titleFallback: "Integrations",
items: [
{ id: "api-endpoints", href: "/dashboard/api-endpoints", i18nKey: "apiEndpoints", icon: "api" },
{ id: "webhooks", href: "/dashboard/webhooks", i18nKey: "webhooks", icon: "webhook" },
{
id: "webhooks",
href: "/dashboard/webhooks",
i18nKey: "webhooks",
subtitleKey: "webhooksSubtitle",
icon: "webhook",
},
],
};
@@ -319,7 +346,13 @@ const BATCH_GROUP: SidebarItemGroup = {
titleKey: "batchGroup",
titleFallback: "Batch",
items: [
{ id: "batch", href: "/dashboard/batch", i18nKey: "batch", icon: "view_list" },
{
id: "batch",
href: "/dashboard/batch",
i18nKey: "batch",
subtitleKey: "batchSubtitle",
icon: "view_list",
},
{ id: "batch-files", href: "/dashboard/batch/files", i18nKey: "batchFiles", icon: "folder" },
],
};