Beginner UX: Essentials sidebar preset (#11286)

Validated on a 3-PR combined board: sidebar-customization + sidebar-essentials-static 30/30 combined (across the board's 4 focused files), typecheck:core + dashboard-typecheck clean, gates within baseline. New Essentials sidebar preset gives first-time users a short beginner path while advanced tools stay reachable via Command Palette search. Thank you @ignamiranda!
This commit is contained in:
ignamiranda
2026-08-23 18:08:23 -04:00
committed by GitHub
parent d137368fb5
commit 7cec8e32fd
9 changed files with 145 additions and 5 deletions

View File

@@ -0,0 +1 @@
- **feat(dashboard):** add an Essentials sidebar preset that shows only the beginner core path (Home → Endpoints → API Keys → Providers → Health → Settings) while keeping Advanced tools reachable via Command Palette search ([#11286](https://github.com/diegosouzapw/OmniRoute/pull/11286))

View File

@@ -521,6 +521,7 @@ export default function SidebarTab() {
const presetLabels: Record<SidebarPresetId, string> = {
all: getSettingsLabel("presetAll", "All"),
essentials: getSettingsLabel("presetEssentials", "Essentials"),
minimal: getSettingsLabel("presetMinimal", "Minimal"),
developer: getSettingsLabel("presetDeveloper", "Developer"),
admin: getSettingsLabel("presetAdmin", "Admin"),
@@ -528,6 +529,10 @@ export default function SidebarTab() {
const presetDescriptions: Record<SidebarPresetId, string> = {
all: getSettingsLabel("presetAllDesc", "Show everything"),
essentials: getSettingsLabel(
"presetEssentialsDesc",
"Beginner path — Advanced tools stay searchable"
),
minimal: getSettingsLabel("presetMinimalDesc", "Core pages only"),
developer: getSettingsLabel("presetDeveloperDesc", "Dev & proxy tools"),
admin: getSettingsLabel("presetAdminDesc", "Monitoring & audit"),

View File

@@ -6707,6 +6707,18 @@
"sidebarVisibility": "Hide sidebar items",
"sidebarVisibilityDesc": "Hide any sidebar navigation entry to reduce visual clutter without disabling any features",
"sidebarVisibilityHint": "Any sidebar section is hidden automatically when all of its entries are hidden",
"presetAll": "All",
"presetAllDesc": "Show everything",
"presetEssentials": "Essentials",
"presetEssentialsDesc": "Beginner path - Advanced tools stay searchable",
"presetMinimal": "Minimal",
"presetMinimalDesc": "Core pages only",
"presetDeveloper": "Developer",
"presetDeveloperDesc": "Dev & proxy tools",
"presetAdmin": "Admin",
"presetAdminDesc": "Monitoring & audit",
"settingsSidebarTitle": "Sidebar Customization",
"settingsSidebarDesc": "Choose which sidebar items to show. Essentials keeps Advanced tools searchable.",
"hideHealthLogs": "Hide Health Check Logs",
"hideHealthLogsDesc": "When ON, suppress [HealthCheck] messages in server console",
"themeAccent": "Theme color",

View File

@@ -6,8 +6,11 @@ import { useTranslations } from "next-intl";
import {
SIDEBAR_SECTIONS,
HIDDEN_SIDEBAR_ITEMS_SETTING_KEY,
SIDEBAR_PRESET_KEY,
ESSENTIALS_ADVANCED_TOOL_IDS,
normalizeHiddenSidebarItems,
resolveRuntimeSidebarSections,
type HideableSidebarItemId,
type SidebarItemDefinition,
type SidebarSectionChild,
} from "@/shared/constants/sidebarVisibility";
@@ -61,6 +64,7 @@ function CommandPaletteDialog({ onClose }: { onClose: () => void }) {
const [query, setQuery] = useState("");
const [selectedIndex, setSelectedIndex] = useState(0);
const [hiddenItems, setHiddenItems] = useState<Set<string>>(new Set());
const [activePreset, setActivePreset] = useState<string | null>(null);
const [radarAdminUrl, setRadarAdminUrl] = useState<unknown>(null);
useEffect(() => {
@@ -71,6 +75,9 @@ function CommandPaletteDialog({ onClose }: { onClose: () => void }) {
setHiddenItems(
new Set(normalizeHiddenSidebarItems(data?.[HIDDEN_SIDEBAR_ITEMS_SETTING_KEY]))
);
setActivePreset(
typeof data?.[SIDEBAR_PRESET_KEY] === "string" ? data[SIDEBAR_PRESET_KEY] : null
);
setRadarAdminUrl(data?.radarAdminUrl ?? null);
})
.catch(() => {
@@ -104,7 +111,13 @@ function CommandPaletteDialog({ onClose }: { onClose: () => void }) {
if (isSidebarGroup(child)) {
const subgroupLabel = safeTranslate(child.titleKey, child.titleFallback);
return child.items
.filter((item) => !hiddenItems.has(item.id))
.filter((item) => {
if (!hiddenItems.has(item.id)) return true;
return (
activePreset === "essentials" &&
ESSENTIALS_ADVANCED_TOOL_IDS.has(item.id as HideableSidebarItemId)
);
})
.map<PaletteItem>((item) => ({
id: item.id,
href: item.href,
@@ -121,7 +134,12 @@ function CommandPaletteDialog({ onClose }: { onClose: () => void }) {
}));
}
const item = child as SidebarItemDefinition;
if (hiddenItems.has(item.id)) return [];
if (hiddenItems.has(item.id)) {
const keepForEssentials =
activePreset === "essentials" &&
ESSENTIALS_ADVANCED_TOOL_IDS.has(item.id as HideableSidebarItemId);
if (!keepForEssentials) return [];
}
return [
{
id: item.id,

View File

@@ -202,6 +202,36 @@ export const SIDEBAR_ITEM_ORDER_KEY = "sidebarItemOrder";
export const SIDEBAR_PRESET_KEY = "sidebarActivePreset";
export const SIDEBAR_SETTINGS_UPDATED_EVENT = "omniroute:settings-updated";
/** Beginner Essentials: core path only. Advanced tools stay reachable via search. */
const ESSENTIALS_SHOWN: ReadonlySet<HideableSidebarItemId> = new Set([
"home",
"endpoints",
"api-manager",
"providers",
"health",
"settings-general",
"settings-sidebar",
]);
/** Hidden in Essentials sidebar but kept searchable in Command Palette. */
export const ESSENTIALS_ADVANCED_TOOL_IDS: ReadonlySet<HideableSidebarItemId> = new Set([
"playground",
"logs",
"batch",
"translator",
"combos",
"quota",
"analytics",
"costs",
"cache",
"runtime",
"resilience-connections",
"mcp",
"a2a",
"memory",
"skills",
]);
const MINIMAL_SHOWN: ReadonlySet<HideableSidebarItemId> = new Set([
"home",
"endpoints",
@@ -297,6 +327,7 @@ function buildHiddenList(shown: ReadonlySet<HideableSidebarItemId>): HideableSid
export const SIDEBAR_PRESETS: readonly SidebarPresetDefinition[] = [
{ id: "all", icon: "select_all", hiddenItems: [] },
{ id: "essentials", icon: "star", hiddenItems: buildHiddenList(ESSENTIALS_SHOWN) },
{ id: "minimal", icon: "minimize", hiddenItems: buildHiddenList(MINIMAL_SHOWN) },
{ id: "developer", icon: "code", hiddenItems: buildHiddenList(DEVELOPER_SHOWN) },
{ id: "admin", icon: "admin_panel_settings", hiddenItems: buildHiddenList(ADMIN_SHOWN) },

View File

@@ -174,7 +174,7 @@ export interface SidebarSectionDefinition {
defaultPinned?: boolean;
}
export type SidebarPresetId = "all" | "minimal" | "developer" | "admin";
export type SidebarPresetId = "all" | "essentials" | "minimal" | "developer" | "admin";
export interface SidebarPresetDefinition {
id: SidebarPresetId;

View File

@@ -199,7 +199,10 @@ export const updateSettingsSchema = z.object({
.array(z.enum(SIDEBAR_SECTIONS.map((s) => s.id) as [string, ...string[]]))
.optional(),
sidebarItemOrder: z.record(z.string(), z.array(z.string().max(100))).optional(),
sidebarActivePreset: z.enum(["all", "minimal", "developer", "admin"]).nullable().optional(),
sidebarActivePreset: z
.enum(["all", "essentials", "minimal", "developer", "admin"])
.nullable()
.optional(),
comboConfigMode: z.enum(COMBO_CONFIG_MODES).optional(),
codexServiceTier: z
.object({

View File

@@ -89,9 +89,10 @@ test("applyItemOrder ignores unknown IDs in order list", () => {
// ─── SIDEBAR_PRESETS ──────────────────────────────────────────────────────────
test("SIDEBAR_PRESETS contains all four preset IDs", () => {
test("SIDEBAR_PRESETS contains all five preset IDs", () => {
const ids = SIDEBAR_PRESETS.map((p) => p.id);
assert.ok(ids.includes("all"), "expected 'all' preset");
assert.ok(ids.includes("essentials"), "expected 'essentials' preset");
assert.ok(ids.includes("minimal"), "expected 'minimal' preset");
assert.ok(ids.includes("developer"), "expected 'developer' preset");
assert.ok(ids.includes("admin"), "expected 'admin' preset");
@@ -112,6 +113,30 @@ test("SIDEBAR_PRESETS 'all' preset has no hidden items", () => {
assert.deepEqual(allPreset.hiddenItems, []);
});
test("SIDEBAR_PRESETS includes essentials as the beginner path", () => {
assert.equal(SIDEBAR_PRESETS.length, 5);
assert.deepEqual(
SIDEBAR_PRESETS.map((p) => p.id),
["all", "essentials", "minimal", "developer", "admin"]
);
const essentials = SIDEBAR_PRESETS.find((p) => p.id === "essentials");
assert.ok(essentials, "expected 'essentials' preset to exist");
const hidden = new Set(essentials.hiddenItems);
for (const id of [
"home",
"endpoints",
"api-manager",
"providers",
"health",
"settings-general",
"settings-sidebar",
]) {
assert.equal(hidden.has(id as never), false, `${id} should stay visible in essentials`);
}
assert.equal(hidden.has("playground"), true);
assert.equal(hidden.has("logs"), true);
});
test("SIDEBAR_PRESETS non-all presets have at least one hidden item", () => {
for (const preset of SIDEBAR_PRESETS.filter((p) => p.id !== "all")) {
assert.ok(preset.hiddenItems.length > 0, `Preset '${preset.id}' should hide at least one item`);

View File

@@ -0,0 +1,45 @@
import test from "node:test";
import assert from "node:assert/strict";
import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
test("essentials preset is registered in sidebar visibility types and presets", () => {
const types = fs.readFileSync(
path.join(repoRoot, "src/shared/constants/sidebarVisibility/types.ts"),
"utf8"
);
const visibility = fs.readFileSync(
path.join(repoRoot, "src/shared/constants/sidebarVisibility.ts"),
"utf8"
);
const schema = fs.readFileSync(
path.join(repoRoot, "src/shared/validation/settingsSchemas.ts"),
"utf8"
);
assert.match(types, /"essentials"/);
assert.match(visibility, /id:\s*"essentials"/);
assert.match(visibility, /ESSENTIALS_ADVANCED_TOOL_IDS/);
assert.match(schema, /"essentials"/);
});
test("command palette keeps essentials advanced tools searchable", () => {
const source = fs.readFileSync(
path.join(repoRoot, "src/shared/components/CommandPalette.tsx"),
"utf8"
);
assert.match(source, /ESSENTIALS_ADVANCED_TOOL_IDS/);
assert.match(source, /activePreset === "essentials"/);
});
test("essentials i18n keys exist in en.json", () => {
const en = JSON.parse(
fs.readFileSync(path.join(repoRoot, "src/i18n/messages/en.json"), "utf8")
) as { settings: Record<string, string> };
assert.equal(en.settings.presetEssentials, "Essentials");
assert.match(en.settings.presetEssentialsDesc, /Beginner path/i);
assert.match(en.settings.presetEssentialsDesc, /searchable/i);
});