mirror of
https://github.com/BerriAI/litellm.git
synced 2025-12-06 11:33:26 +08:00
hidden dashboard routing, dev and build env files
This commit is contained in:
2
ui/litellm-dashboard/.env.development
Normal file
2
ui/litellm-dashboard/.env.development
Normal file
@@ -0,0 +1,2 @@
|
||||
NODE_ENV=development
|
||||
NEXT_PUBLIC_BASE_URL=""
|
||||
2
ui/litellm-dashboard/.env.production
Normal file
2
ui/litellm-dashboard/.env.production
Normal file
@@ -0,0 +1,2 @@
|
||||
NODE_ENV=production
|
||||
NEXT_PUBLIC_BASE_URL="ui/"
|
||||
@@ -46,6 +46,21 @@ interface MenuItem {
|
||||
icon?: React.ReactNode;
|
||||
}
|
||||
|
||||
/** ---------- Base URL helpers ---------- */
|
||||
/** Normalizes NEXT_PUBLIC_BASE_URL into either "" or "/something" (no trailing slash). */
|
||||
const getBasePath = () => {
|
||||
const raw = process.env.NEXT_PUBLIC_BASE_URL ?? "";
|
||||
const trimmed = raw.replace(/^\/+|\/+$/g, ""); // strip leading/trailing slashes
|
||||
return trimmed ? `/${trimmed}` : "";
|
||||
};
|
||||
|
||||
/** Joins base path with a relative path like "virtual-keys" or "/virtual-keys" -> "/base/virtual-keys" */
|
||||
const withBase = (relativePath: string) => {
|
||||
const base = getBasePath(); // "" or "/ui" (no trailing slash)
|
||||
const rel = relativePath.replace(/^\/+/, ""); // drop any leading slash
|
||||
return `${base}/${rel}`.replace(/\/{2,}/g, "/"); // collapse accidental doubles
|
||||
};
|
||||
|
||||
const Sidebar2: React.FC<SidebarProps> = ({ accessToken, userRole, defaultSelectedKey, collapsed = false }) => {
|
||||
const menuItems: MenuItem[] = [
|
||||
{ key: "1", page: "api-keys", label: "Virtual Keys", icon: <KeyOutlined style={{ fontSize: 18 }} /> },
|
||||
@@ -217,8 +232,11 @@ const Sidebar2: React.FC<SidebarProps> = ({ accessToken, userRole, defaultSelect
|
||||
return "1";
|
||||
};
|
||||
|
||||
// Match Virtual Keys path with base prefix (e.g., "/virtual-keys" or "/ui/virtual-keys")
|
||||
const virtualKeysPath = withBase("virtual-keys");
|
||||
|
||||
const selectedMenuKey =
|
||||
pathname === "/dashboard/virtual-keys"
|
||||
pathname === virtualKeysPath
|
||||
? "1"
|
||||
: pageParam
|
||||
? findMenuItemKey(pageParam)
|
||||
@@ -226,16 +244,19 @@ const Sidebar2: React.FC<SidebarProps> = ({ accessToken, userRole, defaultSelect
|
||||
? findMenuItemKey(defaultSelectedKey)
|
||||
: "1";
|
||||
|
||||
// Root-only routing helper: always replace everything after the domain
|
||||
const rootWithPage = (p: string) => ({ pathname: "/", query: { page: p } });
|
||||
// Root-only routing helper: always replace everything after the domain, honoring base path
|
||||
const rootWithPage = (p: string) => ({
|
||||
pathname: getBasePath() || "/",
|
||||
query: { page: p },
|
||||
});
|
||||
|
||||
// Convert to AntD Menu items:
|
||||
// - "Virtual Keys" still routes to /virtual-keys
|
||||
// - All other items (and children) route to "/?page=<page>"
|
||||
// - "Virtual Keys" routes to "/<BASE>/virtual-keys"
|
||||
// - All other items (and children) route to "/<BASE>/?page=<page>"
|
||||
const antdItems = filteredMenuItems.map((item) => {
|
||||
const isVirtualKeys = item.key === "1";
|
||||
const label = isVirtualKeys ? (
|
||||
<Link href="/virtual-keys">Virtual Keys</Link>
|
||||
<Link href={withBase("virtual-keys")}>Virtual Keys</Link>
|
||||
) : (
|
||||
<Link href={rootWithPage(item.page)}>{item.label}</Link>
|
||||
);
|
||||
@@ -0,0 +1,32 @@
|
||||
"use client";
|
||||
|
||||
import Sidebar2 from "@/app/(dashboard)/components/Sidebar2";
|
||||
import Sidebar from "@/components/leftnav";
|
||||
import React from "react";
|
||||
import useFeatureFlags from "@/hooks/useFeatureFlags";
|
||||
import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized";
|
||||
|
||||
interface SidebarProviderProps {
|
||||
defaultSelectedKey: string;
|
||||
sidebarCollapsed: boolean;
|
||||
setPage: (page: string) => void;
|
||||
}
|
||||
|
||||
const SidebarProvider = ({ defaultSelectedKey, sidebarCollapsed, setPage }: SidebarProviderProps) => {
|
||||
const { accessToken, userRole } = useAuthorized();
|
||||
const { refactoredUIFlag, setRefactoredUIFlag } = useFeatureFlags();
|
||||
|
||||
return refactoredUIFlag ? (
|
||||
<Sidebar2 accessToken={accessToken} userRole={userRole} />
|
||||
) : (
|
||||
<Sidebar
|
||||
accessToken={accessToken}
|
||||
setPage={setPage}
|
||||
userRole={userRole}
|
||||
defaultSelectedKey={defaultSelectedKey}
|
||||
collapsed={sidebarCollapsed}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default SidebarProvider;
|
||||
@@ -29,9 +29,9 @@ import {
|
||||
} from "@/components/networking";
|
||||
import { getModelDisplayName } from "@/components/key_team_helpers/fetch_available_models_team_key";
|
||||
import BulkCreateUsersButton from "@/components/bulk_create_users_button";
|
||||
import { fetchTeams } from "@/app/dashboard/virtual-keys/networking";
|
||||
import useTeams from "@/app/dashboard/virtual-keys/hooks/useTeams";
|
||||
import useAuthorized from "@/app/dashboard/hooks/useAuthorized";
|
||||
import { fetchTeams } from "@/app/(dashboard)/virtual-keys/networking";
|
||||
import useTeams from "@/app/(dashboard)/virtual-keys/hooks/useTeams";
|
||||
import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized";
|
||||
|
||||
// Helper function to generate UUID compatible across all environments
|
||||
const generateUUID = (): string => {
|
||||
@@ -3,8 +3,8 @@
|
||||
import React from "react";
|
||||
import Navbar from "@/components/navbar";
|
||||
import { ThemeProvider } from "@/contexts/ThemeContext";
|
||||
import Sidebar2 from "@/app/dashboard/components/Sidebar2";
|
||||
import useAuthorized from "@/app/dashboard/hooks/useAuthorized";
|
||||
import Sidebar2 from "@/app/(dashboard)/components/Sidebar2";
|
||||
import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized";
|
||||
|
||||
export default function Layout({ children }: { children: React.ReactNode }) {
|
||||
const { accessToken, userRole } = useAuthorized();
|
||||
@@ -6,9 +6,9 @@ import { Modal, Form } from "antd";
|
||||
import { getPossibleUserRoles, Organization } from "@/components/networking";
|
||||
import { rolesWithWriteAccess } from "@/utils/roles";
|
||||
import { Team } from "@/components/key_team_helpers/key_list";
|
||||
import CreateKeyModal from "@/app/dashboard/virtual-keys/components/CreateKeyModal/CreateKeyModal";
|
||||
import CreateUserModal from "@/app/dashboard/components/modals/CreateUserModal";
|
||||
import useAuthorized from "@/app/dashboard/hooks/useAuthorized";
|
||||
import CreateKeyModal from "@/app/(dashboard)/virtual-keys/components/CreateKeyModal/CreateKeyModal";
|
||||
import CreateUserModal from "@/app/(dashboard)/components/modals/CreateUserModal";
|
||||
import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized";
|
||||
|
||||
interface CreateKeyProps {
|
||||
team: Team | null;
|
||||
@@ -17,7 +17,7 @@ import SchemaFormFields from "@/components/common_components/check_openapi_schem
|
||||
import { Team } from "@/components/key_team_helpers/key_list";
|
||||
import React from "react";
|
||||
import { DefaultOptionType } from "rc-select/lib/Select";
|
||||
import { ModelAliases } from "@/app/dashboard/virtual-keys/components/CreateKeyModal/types";
|
||||
import { ModelAliases } from "@/app/(dashboard)/virtual-keys/components/CreateKeyModal/types";
|
||||
|
||||
export interface OptionalSettingsSectionProps {
|
||||
form: FormInstance;
|
||||
@@ -6,7 +6,7 @@ import { InfoCircleOutlined } from "@ant-design/icons";
|
||||
import TeamDropdown from "@/components/common_components/team_dropdown";
|
||||
import React from "react";
|
||||
import { Team } from "@/components/key_team_helpers/key_list";
|
||||
import { UserOption } from "@/app/dashboard/virtual-keys/components/CreateKeyModal/types";
|
||||
import { UserOption } from "@/app/(dashboard)/virtual-keys/components/CreateKeyModal/types";
|
||||
|
||||
interface OwnershipSectionProps {
|
||||
team: Team | null;
|
||||
@@ -5,7 +5,7 @@ import { Text } from "@tremor/react";
|
||||
import { keyCreateCall, keyCreateServiceAccountCall } from "@/components/networking";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { Team } from "@/components/key_team_helpers/key_list";
|
||||
import SaveKeyModal from "@/app/dashboard/virtual-keys/components/SaveKeyModal";
|
||||
import SaveKeyModal from "@/app/(dashboard)/virtual-keys/components/SaveKeyModal";
|
||||
import NotificationsManager from "@/components/molecules/notifications_manager";
|
||||
import {
|
||||
useGuardrailsAndPrompts,
|
||||
@@ -13,15 +13,15 @@ import {
|
||||
useTeamModels,
|
||||
useUserModels,
|
||||
useUserSearch,
|
||||
} from "@/app/dashboard/virtual-keys/components/CreateKeyModal/hooks";
|
||||
import OwnershipSection from "@/app/dashboard/virtual-keys/components/CreateKeyModal/CreateKeyForm/OwnershipSection";
|
||||
import KeyDetailsSection from "@/app/dashboard/virtual-keys/components/CreateKeyModal/CreateKeyForm/KeyDetailsSection";
|
||||
import OptionalSettingsSection from "@/app/dashboard/virtual-keys/components/CreateKeyModal/CreateKeyForm/OptionalSettingsSection";
|
||||
import { ModelAliases } from "@/app/dashboard/virtual-keys/components/CreateKeyModal/types";
|
||||
import { getPredefinedTags, prepareFormValues } from "@/app/dashboard/virtual-keys/components/CreateKeyModal/utils";
|
||||
import { fetchTeams } from "@/app/dashboard/virtual-keys/networking";
|
||||
import useTeams from "@/app/dashboard/virtual-keys/hooks/useTeams";
|
||||
import useAuthorized from "@/app/dashboard/hooks/useAuthorized";
|
||||
} from "@/app/(dashboard)/virtual-keys/components/CreateKeyModal/hooks";
|
||||
import OwnershipSection from "@/app/(dashboard)/virtual-keys/components/CreateKeyModal/CreateKeyForm/OwnershipSection";
|
||||
import KeyDetailsSection from "@/app/(dashboard)/virtual-keys/components/CreateKeyModal/CreateKeyForm/KeyDetailsSection";
|
||||
import OptionalSettingsSection from "@/app/(dashboard)/virtual-keys/components/CreateKeyModal/CreateKeyForm/OptionalSettingsSection";
|
||||
import { ModelAliases } from "@/app/(dashboard)/virtual-keys/components/CreateKeyModal/types";
|
||||
import { getPredefinedTags, prepareFormValues } from "@/app/(dashboard)/virtual-keys/components/CreateKeyModal/utils";
|
||||
import { fetchTeams } from "@/app/(dashboard)/virtual-keys/networking";
|
||||
import useTeams from "@/app/(dashboard)/virtual-keys/hooks/useTeams";
|
||||
import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized";
|
||||
|
||||
export interface CreateKeyModalProps {
|
||||
isModalVisible: boolean;
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { fetchGuardrails, fetchPrompts } from "@/app/dashboard/virtual-keys/components/CreateKeyModal/networking";
|
||||
import useAuthorized from "@/app/dashboard/hooks/useAuthorized";
|
||||
import { fetchGuardrails, fetchPrompts } from "@/app/(dashboard)/virtual-keys/components/CreateKeyModal/networking";
|
||||
import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized";
|
||||
|
||||
export const useGuardrailsAndPrompts = () => {
|
||||
const [guardrails, setGuardrails] = useState<string[]>([]);
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { getMCPAccessGroups } from "@/app/dashboard/virtual-keys/components/CreateKeyModal/networking";
|
||||
import useAuthorized from "@/app/dashboard/hooks/useAuthorized";
|
||||
import { getMCPAccessGroups } from "@/app/(dashboard)/virtual-keys/components/CreateKeyModal/networking";
|
||||
import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized";
|
||||
|
||||
export const useMcpAccessGroups = () => {
|
||||
const [mcpAccessGroups, setMcpAccessGroups] = useState<string[]>([]);
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import type { Team } from "@/components/key_team_helpers/key_list";
|
||||
import { fetchTeamModels } from "@/app/dashboard/virtual-keys/components/CreateKeyModal/networking";
|
||||
import useAuthorized from "@/app/dashboard/hooks/useAuthorized";
|
||||
import { fetchTeamModels } from "@/app/(dashboard)/virtual-keys/components/CreateKeyModal/networking";
|
||||
import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized";
|
||||
|
||||
export const useTeamModels = (selectedTeam: Team | null) => {
|
||||
const { userId: userID, userRole, accessToken } = useAuthorized();
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { getUserModelNames } from "@/app/dashboard/virtual-keys/components/CreateKeyModal/networking";
|
||||
import useAuthorized from "@/app/dashboard/hooks/useAuthorized";
|
||||
import { getUserModelNames } from "@/app/(dashboard)/virtual-keys/components/CreateKeyModal/networking";
|
||||
import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized";
|
||||
|
||||
export const useUserModels = () => {
|
||||
const { userId: userID, userRole, accessToken } = useAuthorized();
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { debounce } from "lodash";
|
||||
import { searchUserOptionsByEmail } from "@/app/dashboard/virtual-keys/components/CreateKeyModal/networking";
|
||||
import useAuthorized from "@/app/dashboard/hooks/useAuthorized";
|
||||
import { searchUserOptionsByEmail } from "@/app/(dashboard)/virtual-keys/components/CreateKeyModal/networking";
|
||||
import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized";
|
||||
|
||||
type User = { user_id: string; user_email: string; role?: string };
|
||||
export interface UserOption {
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
User,
|
||||
userFilterUICall,
|
||||
} from "@/components/networking";
|
||||
import { ModelAvailableResponse, UserOption } from "@/app/dashboard/virtual-keys/components/CreateKeyModal/types";
|
||||
import { ModelAvailableResponse, UserOption } from "@/app/(dashboard)/virtual-keys/components/CreateKeyModal/types";
|
||||
|
||||
export const fetchGuardrails = async (accessToken: string) => {
|
||||
try {
|
||||
@@ -1,5 +1,5 @@
|
||||
import { mapDisplayToInternalNames } from "@/components/callback_info_helpers";
|
||||
import { ModelAliases } from "@/app/dashboard/virtual-keys/components/CreateKeyModal/types";
|
||||
import { ModelAliases } from "@/app/(dashboard)/virtual-keys/components/CreateKeyModal/types";
|
||||
|
||||
export const getPredefinedTags = (data: any[] | null) => {
|
||||
let allTags = [];
|
||||
@@ -16,9 +16,9 @@ import { Organization, userListCall } from "@/components/networking";
|
||||
import { getModelDisplayName } from "@/components/key_team_helpers/fetch_available_models_team_key";
|
||||
import FilterComponent, { FilterOption } from "@/components/molecules/filter";
|
||||
import { useFilterLogic } from "@/components/key_team_helpers/filter_logic";
|
||||
import useTeams from "@/app/dashboard/virtual-keys/hooks/useTeams";
|
||||
import useAuthorized from "@/app/dashboard/hooks/useAuthorized";
|
||||
import KeyInfoView from "@/app/dashboard/virtual-keys/components/KeyInfoView";
|
||||
import useTeams from "@/app/(dashboard)/virtual-keys/hooks/useTeams";
|
||||
import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized";
|
||||
import KeyInfoView from "@/app/(dashboard)/virtual-keys/components/KeyInfoView";
|
||||
|
||||
interface AllKeysTableProps {
|
||||
keys: KeyResponse[];
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { fetchTeams } from "@/app/dashboard/virtual-keys/networking";
|
||||
import { fetchTeams } from "@/app/(dashboard)/virtual-keys/networking";
|
||||
import { Team } from "@/components/key_team_helpers/key_list";
|
||||
import useAuthorized from "@/app/dashboard/hooks/useAuthorized";
|
||||
import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized";
|
||||
|
||||
const useTeams = () => {
|
||||
const [teams, setTeams] = useState<Team[]>([]);
|
||||
@@ -1,15 +1,15 @@
|
||||
"use client";
|
||||
|
||||
import VirtualKeysTable from "@/app/dashboard/virtual-keys/components/VirtualKeysTable/VirtualKeysTable";
|
||||
import VirtualKeysTable from "@/app/(dashboard)/virtual-keys/components/VirtualKeysTable/VirtualKeysTable";
|
||||
import { useState } from "react";
|
||||
import useKeyList from "@/components/key_team_helpers/key_list";
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import { Col, Grid } from "@tremor/react";
|
||||
import CreateKey from "@/app/dashboard/virtual-keys/components/CreateKey";
|
||||
import useAuthorized from "@/app/dashboard/hooks/useAuthorized"
|
||||
import CreateKey from "@/app/(dashboard)/virtual-keys/components/CreateKey";
|
||||
import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized";
|
||||
|
||||
const VirtualKeysPage = () => {
|
||||
const {accessToken, userRole} = useAuthorized();
|
||||
const { accessToken, userRole } = useAuthorized();
|
||||
const [createClicked, setCreateClicked] = useState<boolean>(false);
|
||||
|
||||
const queryClient = new QueryClient();
|
||||
@@ -1,36 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import Sidebar2 from "@/app/dashboard/components/Sidebar2";
|
||||
import Sidebar from "@/components/leftnav";
|
||||
import React from "react";
|
||||
import useFeatureFlags from "@/hooks/useFeatureFlags";
|
||||
import useAuthorized from "@/app/dashboard/hooks/useAuthorized";
|
||||
|
||||
interface SidebarProviderProps {
|
||||
defaultSelectedKey: string;
|
||||
sidebarCollapsed: boolean;
|
||||
setPage: (page: string) => void;
|
||||
}
|
||||
|
||||
const SidebarProvider = ({defaultSelectedKey, sidebarCollapsed, setPage}: SidebarProviderProps) => {
|
||||
|
||||
const {accessToken, userRole} = useAuthorized();
|
||||
const { refactoredUIFlag, setRefactoredUIFlag } = useFeatureFlags();
|
||||
|
||||
return (
|
||||
refactoredUIFlag ? (
|
||||
<Sidebar2 accessToken={accessToken} userRole={userRole} />
|
||||
) : (
|
||||
<Sidebar
|
||||
accessToken={accessToken}
|
||||
setPage={setPage}
|
||||
userRole={userRole}
|
||||
defaultSelectedKey={defaultSelectedKey}
|
||||
collapsed={sidebarCollapsed}
|
||||
/>
|
||||
)
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
export default SidebarProvider;
|
||||
@@ -40,8 +40,8 @@ import UIThemeSettings from "@/components/ui_theme_settings";
|
||||
import { UiLoadingSpinner } from "@/components/ui/ui-loading-spinner";
|
||||
import { cx } from "@/lib/cva.config";
|
||||
import useFeatureFlags, { FeatureFlagsProvider } from "@/hooks/useFeatureFlags";
|
||||
import Sidebar2 from "@/app/dashboard/components/Sidebar2";
|
||||
import SidebarProvider from "@/app/dashboard/components/SidebarProvider";
|
||||
import Sidebar2 from "@/app/(dashboard)/components/Sidebar2";
|
||||
import SidebarProvider from "@/app/(dashboard)/components/SidebarProvider";
|
||||
|
||||
function getCookie(name: string) {
|
||||
const cookieValue = document.cookie.split("; ").find((row) => row.startsWith(name + "="));
|
||||
@@ -126,6 +126,10 @@ export default function CreateKeyPage() {
|
||||
return searchParams.get("page") || "api-keys";
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
setPage(searchParams.get("page") || "api-keys");
|
||||
}, [searchParams]);
|
||||
|
||||
// Custom setPage function that updates URL
|
||||
const updatePage = (newPage: string) => {
|
||||
// Update URL without full page reload
|
||||
|
||||
@@ -56,6 +56,17 @@ interface MenuItem {
|
||||
icon?: React.ReactNode;
|
||||
}
|
||||
|
||||
/** ---------- Base URL helpers ---------- */
|
||||
/**
|
||||
* Normalizes NEXT_PUBLIC_BASE_URL to either "/" or "/ui/" (always with a trailing slash).
|
||||
* Supported env values: "" or "ui/".
|
||||
*/
|
||||
const getBasePath = () => {
|
||||
const raw = process.env.NEXT_PUBLIC_BASE_URL ?? "";
|
||||
const trimmed = raw.replace(/^\/+|\/+$/g, ""); // strip leading/trailing slashes
|
||||
return trimmed ? `/${trimmed}/` : "/"; // ensure trailing slash
|
||||
};
|
||||
|
||||
const Sidebar: React.FC<SidebarProps> = ({ accessToken, setPage, userRole, defaultSelectedKey, collapsed = false }) => {
|
||||
// Note: If a menu item does not have a role, it is visible to all roles.
|
||||
const menuItems: MenuItem[] = [
|
||||
@@ -214,6 +225,7 @@ const Sidebar: React.FC<SidebarProps> = ({ accessToken, setPage, userRole, defau
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
// Find the menu item that matches the default page, including in submenus
|
||||
const findMenuItemKey = (page: string): string => {
|
||||
// Check top-level items
|
||||
@@ -248,6 +260,15 @@ const Sidebar: React.FC<SidebarProps> = ({ accessToken, setPage, userRole, defau
|
||||
return true;
|
||||
});
|
||||
|
||||
// Centralized navigation that prefixes the base path ("/" or "/ui/")
|
||||
const goTo = (page: string) => {
|
||||
const base = getBasePath(); // "/" or "/ui/"
|
||||
const newSearchParams = new URLSearchParams(window.location.search);
|
||||
newSearchParams.set("page", page);
|
||||
window.history.pushState(null, "", `${base}?${newSearchParams.toString()}`); // e.g. "/ui/?page=..."
|
||||
setPage(page);
|
||||
};
|
||||
|
||||
return (
|
||||
<Layout style={{ minHeight: "100vh" }}>
|
||||
<Sider
|
||||
@@ -291,21 +312,9 @@ const Sidebar: React.FC<SidebarProps> = ({ accessToken, setPage, userRole, defau
|
||||
key: child.key,
|
||||
icon: child.icon,
|
||||
label: child.label,
|
||||
onClick: () => {
|
||||
const newSearchParams = new URLSearchParams(window.location.search);
|
||||
newSearchParams.set("page", child.page);
|
||||
window.history.pushState(null, "", `?${newSearchParams.toString()}`);
|
||||
setPage(child.page);
|
||||
},
|
||||
onClick: () => goTo(child.page),
|
||||
})),
|
||||
onClick: !item.children
|
||||
? () => {
|
||||
const newSearchParams = new URLSearchParams(window.location.search);
|
||||
newSearchParams.set("page", item.page);
|
||||
window.history.pushState(null, "", `?${newSearchParams.toString()}`);
|
||||
setPage(item.page);
|
||||
}
|
||||
: undefined,
|
||||
onClick: !item.children ? () => goTo(item.page) : undefined,
|
||||
}))}
|
||||
/>
|
||||
</ConfigProvider>
|
||||
|
||||
@@ -131,11 +131,10 @@ export const fetchUserModels = async (
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* ─────────────────────────────────────────────────────────────────────────
|
||||
* @deprecated
|
||||
* This component is being DEPRECATED in favor of src/app/dashboard/virtual-keys/components/CreateKey.tsx
|
||||
* This component is being DEPRECATED in favor of src/app/(dashboard)/virtual-keys/components/CreateKey.tsx
|
||||
* Please contribute to the new refactor.
|
||||
* ─────────────────────────────────────────────────────────────────────────
|
||||
*/
|
||||
|
||||
@@ -50,7 +50,7 @@ interface KeyInfoViewProps {
|
||||
/**
|
||||
* ─────────────────────────────────────────────────────────────────────────
|
||||
* @deprecated
|
||||
* This component is being DEPRECATED in favor of src/app/dashboard/virtual-keys/components/KeyInfoView.tsx
|
||||
* This component is being DEPRECATED in favor of src/app/(dashboard)/virtual-keys/components/KeyInfoView.tsx
|
||||
* Please contribute to the new refactor.
|
||||
* ─────────────────────────────────────────────────────────────────────────
|
||||
*/
|
||||
|
||||
@@ -138,7 +138,7 @@ interface CombinedLimits {
|
||||
/**
|
||||
* ─────────────────────────────────────────────────────────────────────────
|
||||
* @deprecated
|
||||
* This component is being DEPRECATED in favor of src/app/dashboard/virtual-keys/components/VirtualKeysTable/
|
||||
* This component is being DEPRECATED in favor of src/app/(dashboard)/virtual-keys/components/VirtualKeysTable/
|
||||
* Please contribute to the new refactor.
|
||||
* ─────────────────────────────────────────────────────────────────────────
|
||||
*/
|
||||
|
||||
@@ -1,26 +1,87 @@
|
||||
'use client';
|
||||
import React, {createContext, useContext, useState} from 'react';
|
||||
"use client";
|
||||
import React, { createContext, useContext, useEffect, useState } from "react";
|
||||
|
||||
type Flags = {
|
||||
refactoredUIFlag: boolean;
|
||||
setRefactoredUIFlag: (v: boolean) => void;
|
||||
};
|
||||
|
||||
const STORAGE_KEY = "feature.refactoredUIFlag";
|
||||
|
||||
const FeatureFlagsCtx = createContext<Flags | null>(null);
|
||||
|
||||
export const FeatureFlagsProvider = ({ children }: { children: React.ReactNode }) => {
|
||||
const [refactoredUIFlag, setRefactoredUIFlag] = useState(false);
|
||||
return (
|
||||
<FeatureFlagsCtx.Provider value={{ refactoredUIFlag, setRefactoredUIFlag }}>
|
||||
{children}
|
||||
</FeatureFlagsCtx.Provider>
|
||||
);
|
||||
/** Safely read the flag from localStorage. If anything goes wrong, reset to false. */
|
||||
function readFlagSafely(): boolean {
|
||||
try {
|
||||
const raw = localStorage.getItem(STORAGE_KEY);
|
||||
if (raw === null) {
|
||||
localStorage.setItem(STORAGE_KEY, "false");
|
||||
return false;
|
||||
}
|
||||
|
||||
const v = raw.trim().toLowerCase();
|
||||
if (v === "true" || v === "1") return true;
|
||||
if (v === "false" || v === "0") return false;
|
||||
|
||||
// Last chance: try JSON.parse in case something odd was stored.
|
||||
const parsed = JSON.parse(raw);
|
||||
if (typeof parsed === "boolean") return parsed;
|
||||
|
||||
// Malformed → reset to false
|
||||
localStorage.setItem(STORAGE_KEY, "false");
|
||||
return false;
|
||||
} catch {
|
||||
// If even accessing localStorage throws, best effort reset then default to false
|
||||
try {
|
||||
localStorage.setItem(STORAGE_KEY, "false");
|
||||
} catch {}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function writeFlagSafely(v: boolean) {
|
||||
try {
|
||||
localStorage.setItem(STORAGE_KEY, String(v));
|
||||
} catch {
|
||||
// Ignore write errors; state will still reflect the intended value.
|
||||
}
|
||||
}
|
||||
|
||||
export const FeatureFlagsProvider = ({ children }: { children: React.ReactNode }) => {
|
||||
// Lazy init reads from localStorage only on the client
|
||||
const [refactoredUIFlag, setRefactoredUIFlagState] = useState<boolean>(() => readFlagSafely());
|
||||
|
||||
const setRefactoredUIFlag = (v: boolean) => {
|
||||
setRefactoredUIFlagState(v);
|
||||
writeFlagSafely(v);
|
||||
};
|
||||
|
||||
// Keep this flag in sync across tabs/windows.
|
||||
useEffect(() => {
|
||||
const onStorage = (e: StorageEvent) => {
|
||||
if (e.key === STORAGE_KEY && e.newValue != null) {
|
||||
const next = e.newValue.trim().toLowerCase();
|
||||
setRefactoredUIFlagState(next === "true" || next === "1");
|
||||
}
|
||||
// If the key was cleared elsewhere, self-heal to false.
|
||||
if (e.key === STORAGE_KEY && e.newValue === null) {
|
||||
writeFlagSafely(false);
|
||||
setRefactoredUIFlagState(false);
|
||||
}
|
||||
};
|
||||
window.addEventListener("storage", onStorage);
|
||||
return () => window.removeEventListener("storage", onStorage);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<FeatureFlagsCtx.Provider value={{ refactoredUIFlag, setRefactoredUIFlag }}>{children}</FeatureFlagsCtx.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
const useFeatureFlags = () => {
|
||||
const ctx = useContext(FeatureFlagsCtx);
|
||||
if (!ctx) throw new Error('useFeatureFlags must be used within FeatureFlagsProvider');
|
||||
if (!ctx) throw new Error("useFeatureFlags must be used within FeatureFlagsProvider");
|
||||
return ctx;
|
||||
}
|
||||
};
|
||||
|
||||
export default useFeatureFlags;
|
||||
|
||||
Reference in New Issue
Block a user