mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-09 22:31:41 +00:00
[Fix] UI: Team table refresh, infinite team dropdown, leftnav for dashboard routes
- OldTeams: refresh table via fetchTeamsV2 after team create instead of appending - TeamDropdown: rewrite with useInfiniteTeams for paginated fetch, scroll-to-load, and debounced search - Update all TeamDropdown consumers to use the new self-fetching API - Dashboard layout: switch from Sidebar2 to SidebarProvider (leftnav) - Leftnav: add MIGRATED_PAGES routing for path-based navigation (api-reference) - Navbar: remove chat button Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
9963b31e07
commit
aea8e32048
12 changed files with 238 additions and 114 deletions
|
|
@ -1,4 +1,4 @@
|
||||||
import { keepPreviousData, useQuery, useQueryClient, UseQueryResult } from "@tanstack/react-query";
|
import { keepPreviousData, useInfiniteQuery, useQuery, useQueryClient, UseQueryResult } from "@tanstack/react-query";
|
||||||
import { Team } from "@/components/key_team_helpers/key_list";
|
import { Team } from "@/components/key_team_helpers/key_list";
|
||||||
import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized";
|
import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized";
|
||||||
import { fetchTeams } from "@/app/(dashboard)/networking";
|
import { fetchTeams } from "@/app/(dashboard)/networking";
|
||||||
|
|
@ -124,6 +124,43 @@ export const useTeam = (teamId?: string) => {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const infiniteTeamKeys = createQueryKeys("infiniteTeams");
|
||||||
|
|
||||||
|
export const useInfiniteTeams = (
|
||||||
|
pageSize: number = 50,
|
||||||
|
search?: string,
|
||||||
|
organizationId?: string | null,
|
||||||
|
) => {
|
||||||
|
const { accessToken, userId, userRole } = useAuthorized();
|
||||||
|
const isAdmin = userRole === "Admin" || userRole === "Admin Viewer";
|
||||||
|
|
||||||
|
return useInfiniteQuery<TeamsResponse>({
|
||||||
|
queryKey: infiniteTeamKeys.list({
|
||||||
|
filters: {
|
||||||
|
pageSize,
|
||||||
|
...(search && { search }),
|
||||||
|
...(organizationId && { organizationId }),
|
||||||
|
...(userId && { userId }),
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
queryFn: async ({ pageParam }) => {
|
||||||
|
return await teamListCall(accessToken!, pageParam as number, pageSize, {
|
||||||
|
team_alias: search || undefined,
|
||||||
|
organizationID: organizationId,
|
||||||
|
userID: !isAdmin ? userId : undefined,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
initialPageParam: 1,
|
||||||
|
getNextPageParam: (lastPage) => {
|
||||||
|
if (lastPage.page < lastPage.total_pages) {
|
||||||
|
return lastPage.page + 1;
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
},
|
||||||
|
enabled: Boolean(accessToken),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const deletedTeamListCall = async (
|
const deletedTeamListCall = async (
|
||||||
accessToken: string,
|
accessToken: string,
|
||||||
page: number,
|
page: number,
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
import React, { Suspense, useEffect, useState } from "react";
|
import React, { Suspense, useEffect, useState } from "react";
|
||||||
import Navbar from "@/components/navbar";
|
import Navbar from "@/components/navbar";
|
||||||
import { ThemeProvider } from "@/contexts/ThemeContext";
|
import { ThemeProvider } from "@/contexts/ThemeContext";
|
||||||
import Sidebar2 from "@/app/(dashboard)/components/Sidebar2";
|
import SidebarProvider from "@/app/(dashboard)/components/SidebarProvider";
|
||||||
import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized";
|
import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized";
|
||||||
import { useRouter, useSearchParams } from "next/navigation";
|
import { useRouter, useSearchParams } from "next/navigation";
|
||||||
import { DebugWarningBanner } from "@/components/DebugWarningBanner";
|
import { DebugWarningBanner } from "@/components/DebugWarningBanner";
|
||||||
|
|
@ -23,6 +23,17 @@ function withBase(path: string): string {
|
||||||
}
|
}
|
||||||
/** -------------------------------- */
|
/** -------------------------------- */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pages that have been migrated to path-based routing under (dashboard)/.
|
||||||
|
* When the leftnav triggers one of these, navigate to the path route instead
|
||||||
|
* of the legacy query-param root page.
|
||||||
|
*
|
||||||
|
* Key = legacy page id used in leftnav, Value = route segment under (dashboard)/
|
||||||
|
*/
|
||||||
|
const MIGRATED_PAGES: Record<string, string> = {
|
||||||
|
"api-reference": "api-reference",
|
||||||
|
};
|
||||||
|
|
||||||
function LayoutContent({ children }: { children: React.ReactNode }) {
|
function LayoutContent({ children }: { children: React.ReactNode }) {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const searchParams = useSearchParams();
|
const searchParams = useSearchParams();
|
||||||
|
|
@ -32,10 +43,17 @@ function LayoutContent({ children }: { children: React.ReactNode }) {
|
||||||
return searchParams.get("page") || "api-keys";
|
return searchParams.get("page") || "api-keys";
|
||||||
});
|
});
|
||||||
|
|
||||||
const updatePage = (newPage: string) => {
|
const handleSetPage = (newPage: string) => {
|
||||||
const newSearchParams = new URLSearchParams(searchParams);
|
// If the page has been migrated to path routing, navigate there
|
||||||
newSearchParams.set("page", newPage);
|
const migratedRoute = MIGRATED_PAGES[newPage];
|
||||||
router.push(withBase(`/?${newSearchParams.toString()}`)); // always under BASE
|
if (migratedRoute) {
|
||||||
|
router.push(withBase(migratedRoute));
|
||||||
|
setPage(newPage);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise, navigate back to the legacy root page with query params
|
||||||
|
router.push(withBase(`?page=${newPage}`));
|
||||||
setPage(newPage);
|
setPage(newPage);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -65,7 +83,11 @@ function LayoutContent({ children }: { children: React.ReactNode }) {
|
||||||
<DebugWarningBanner />
|
<DebugWarningBanner />
|
||||||
<div className="flex flex-1 overflow-auto">
|
<div className="flex flex-1 overflow-auto">
|
||||||
<div className="mt-2">
|
<div className="mt-2">
|
||||||
<Sidebar2 defaultSelectedKey={page} accessToken={accessToken} userRole={userRole} />
|
<SidebarProvider
|
||||||
|
setPage={handleSetPage}
|
||||||
|
defaultSelectedKey={page}
|
||||||
|
sidebarCollapsed={sidebarCollapsed}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<main className="flex-1">{children}</main>
|
<main className="flex-1">{children}</main>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -210,9 +210,7 @@ export const CreateUserButton: React.FC<CreateuserProps> = ({
|
||||||
</Select2>
|
</Select2>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item label="Team" name="team_id">
|
<Form.Item label="Team" name="team_id">
|
||||||
<Select placeholder="Select Team" style={{ width: "100%" }}>
|
<TeamDropdown />
|
||||||
<TeamDropdown teams={availableTeams} />
|
|
||||||
</Select>
|
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
<Form.Item label="Metadata" name="metadata">
|
<Form.Item label="Metadata" name="metadata">
|
||||||
|
|
@ -294,7 +292,7 @@ export const CreateUserButton: React.FC<CreateuserProps> = ({
|
||||||
name="team_id"
|
name="team_id"
|
||||||
help="If selected, user will be added as a 'user' role to the team."
|
help="If selected, user will be added as a 'user' role to the team."
|
||||||
>
|
>
|
||||||
<TeamDropdown teams={availableTeams} />
|
<TeamDropdown />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
<Form.Item
|
<Form.Item
|
||||||
|
|
|
||||||
|
|
@ -579,14 +579,12 @@ const Teams: React.FC<TeamProps> = ({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const response: any = await teamCreateCall(accessToken, formValues);
|
await teamCreateCall(accessToken, formValues);
|
||||||
if (teams !== null) {
|
|
||||||
setTeams([...teams, response]);
|
|
||||||
} else {
|
|
||||||
setTeams([response]);
|
|
||||||
}
|
|
||||||
console.log(`response for team create call: ${response}`);
|
|
||||||
NotificationsManager.success("Team created");
|
NotificationsManager.success("Team created");
|
||||||
|
await fetchTeamsV2({
|
||||||
|
page: currentPage,
|
||||||
|
size: pageSize,
|
||||||
|
});
|
||||||
form.resetFields();
|
form.resetFields();
|
||||||
setLoggingSettings([]);
|
setLoggingSettings([]);
|
||||||
setModelAliases({});
|
setModelAliases({});
|
||||||
|
|
|
||||||
|
|
@ -387,7 +387,6 @@ export function ToolDetail({ toolName, onBack, accessToken }: ToolDetailProps) {
|
||||||
</span>
|
</span>
|
||||||
{blockScope === "team" ? (
|
{blockScope === "team" ? (
|
||||||
<TeamDropdown
|
<TeamDropdown
|
||||||
teams={teams}
|
|
||||||
value={blockTeamId ?? undefined}
|
value={blockTeamId ?? undefined}
|
||||||
onChange={(id) => setBlockTeamId(id || null)}
|
onChange={(id) => setBlockTeamId(id || null)}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -131,7 +131,6 @@ const AddModelForm: React.FC<AddModelFormProps> = ({
|
||||||
tooltip="Select the team for which you want to add this model"
|
tooltip="Select the team for which you want to add this model"
|
||||||
>
|
>
|
||||||
<TeamDropdown
|
<TeamDropdown
|
||||||
teams={teams}
|
|
||||||
onChange={(value) => {
|
onChange={(value) => {
|
||||||
setTeamAdminSelectedTeam(value);
|
setTeamAdminSelectedTeam(value);
|
||||||
}}
|
}}
|
||||||
|
|
@ -325,7 +324,7 @@ const AddModelForm: React.FC<AddModelFormProps> = ({
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<TeamDropdown teams={teams} disabled={!premiumUser} />
|
<TeamDropdown disabled={!premiumUser} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
)}
|
)}
|
||||||
{isAdmin && (
|
{isAdmin && (
|
||||||
|
|
|
||||||
|
|
@ -723,10 +723,7 @@ const AddAgentForm: React.FC<AddAgentFormProps> = ({
|
||||||
name="team_id"
|
name="team_id"
|
||||||
tooltip="Optionally assign this agent to a team. The agent and its key will belong to the selected team."
|
tooltip="Optionally assign this agent to a team. The agent and its key will belong to the selected team."
|
||||||
>
|
>
|
||||||
<TeamDropdown
|
<TeamDropdown />
|
||||||
teams={teams}
|
|
||||||
loading={!teams}
|
|
||||||
/>
|
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
<Divider className="my-4" />
|
<Divider className="my-4" />
|
||||||
|
|
|
||||||
|
|
@ -1,46 +1,120 @@
|
||||||
import React from "react";
|
import React, { useMemo, useState, type UIEvent } from "react";
|
||||||
import { Select } from "antd";
|
import { Select } from "antd";
|
||||||
|
import { LoadingOutlined } from "@ant-design/icons";
|
||||||
|
import { useDebouncedState } from "@tanstack/react-pacer/debouncer";
|
||||||
|
import { useInfiniteTeams } from "@/app/(dashboard)/hooks/teams/useTeams";
|
||||||
import { Team } from "../key_team_helpers/key_list";
|
import { Team } from "../key_team_helpers/key_list";
|
||||||
|
|
||||||
interface TeamDropdownProps {
|
interface TeamDropdownProps {
|
||||||
teams?: Team[] | null;
|
|
||||||
value?: string;
|
value?: string;
|
||||||
onChange?: (value: string) => void;
|
onChange?: (value: string) => void;
|
||||||
|
/** Callback with the full Team object (or null on clear). */
|
||||||
|
onTeamSelect?: (team: Team | null) => void;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
loading?: boolean;
|
/** Filter teams by organization. */
|
||||||
|
organizationId?: string | null;
|
||||||
|
pageSize?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
const TeamDropdown: React.FC<TeamDropdownProps> = ({ teams, value, onChange, disabled, loading }) => {
|
const SCROLL_THRESHOLD = 0.8;
|
||||||
|
const DEBOUNCE_MS = 300;
|
||||||
|
|
||||||
|
const TeamDropdown: React.FC<TeamDropdownProps> = ({
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
onTeamSelect,
|
||||||
|
disabled,
|
||||||
|
organizationId,
|
||||||
|
pageSize = 50,
|
||||||
|
}) => {
|
||||||
|
const [searchInput, setSearchInput] = useState("");
|
||||||
|
const [debouncedSearch, setDebouncedSearch] = useDebouncedState("", {
|
||||||
|
wait: DEBOUNCE_MS,
|
||||||
|
});
|
||||||
|
|
||||||
|
const {
|
||||||
|
data,
|
||||||
|
fetchNextPage,
|
||||||
|
hasNextPage,
|
||||||
|
isFetchingNextPage,
|
||||||
|
isLoading,
|
||||||
|
} = useInfiniteTeams(
|
||||||
|
pageSize,
|
||||||
|
debouncedSearch || undefined,
|
||||||
|
organizationId,
|
||||||
|
);
|
||||||
|
|
||||||
|
const teams = useMemo(() => {
|
||||||
|
if (!data?.pages) return [];
|
||||||
|
const seen = new Set<string>();
|
||||||
|
const result: Team[] = [];
|
||||||
|
for (const page of data.pages) {
|
||||||
|
for (const team of page.teams) {
|
||||||
|
if (seen.has(team.team_id)) continue;
|
||||||
|
seen.add(team.team_id);
|
||||||
|
result.push(team);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}, [data]);
|
||||||
|
|
||||||
|
const options = useMemo(
|
||||||
|
() =>
|
||||||
|
teams.map((team) => ({
|
||||||
|
label: `${team.team_alias} (${team.team_id})`,
|
||||||
|
value: team.team_id,
|
||||||
|
})),
|
||||||
|
[teams],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handlePopupScroll = (e: UIEvent<HTMLDivElement>) => {
|
||||||
|
const target = e.currentTarget;
|
||||||
|
const scrollRatio =
|
||||||
|
(target.scrollTop + target.clientHeight) / target.scrollHeight;
|
||||||
|
if (scrollRatio >= SCROLL_THRESHOLD && hasNextPage && !isFetchingNextPage) {
|
||||||
|
fetchNextPage();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSearch = (val: string) => {
|
||||||
|
setSearchInput(val);
|
||||||
|
setDebouncedSearch(val);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleChange = (teamId: string | undefined) => {
|
||||||
|
onChange?.(teamId ?? "");
|
||||||
|
if (onTeamSelect) {
|
||||||
|
const team = teamId ? teams.find((t) => t.team_id === teamId) ?? null : null;
|
||||||
|
onTeamSelect(team);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Select
|
<Select
|
||||||
showSearch
|
showSearch
|
||||||
placeholder="Search or select a team"
|
placeholder="Search or select a team"
|
||||||
value={value}
|
value={value || undefined}
|
||||||
onChange={onChange}
|
onChange={handleChange}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
loading={loading}
|
|
||||||
allowClear
|
allowClear
|
||||||
filterOption={(input, option) => {
|
filterOption={false}
|
||||||
if (!option) return false;
|
onSearch={handleSearch}
|
||||||
// Get team data from the option key
|
searchValue={searchInput}
|
||||||
const team = teams?.find((t) => t.team_id === option.key);
|
onPopupScroll={handlePopupScroll}
|
||||||
if (!team) return false;
|
loading={isLoading}
|
||||||
|
notFoundContent={isLoading ? <LoadingOutlined spin /> : "No teams found"}
|
||||||
const searchTerm = input.toLowerCase().trim();
|
options={options}
|
||||||
const teamAlias = (team.team_alias || "").toLowerCase();
|
popupRender={(menu) => (
|
||||||
const teamId = (team.team_id || "").toLowerCase();
|
<>
|
||||||
|
{menu}
|
||||||
// Search in both team alias and team ID
|
{isFetchingNextPage && (
|
||||||
return teamAlias.includes(searchTerm) || teamId.includes(searchTerm);
|
<div style={{ textAlign: "center", padding: 8 }}>
|
||||||
}}
|
<LoadingOutlined spin />
|
||||||
optionFilterProp="children"
|
</div>
|
||||||
>
|
)}
|
||||||
{teams?.map((team) => (
|
</>
|
||||||
<Select.Option key={team.team_id} value={team.team_id}>
|
)}
|
||||||
<span className="font-medium">{team.team_alias}</span> <span className="text-gray-500">({team.team_id})</span>
|
/>
|
||||||
</Select.Option>
|
|
||||||
))}
|
|
||||||
</Select>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,6 @@ import {
|
||||||
FolderOutlined,
|
FolderOutlined,
|
||||||
KeyOutlined,
|
KeyOutlined,
|
||||||
LineChartOutlined,
|
LineChartOutlined,
|
||||||
MessageOutlined,
|
|
||||||
PlayCircleOutlined,
|
PlayCircleOutlined,
|
||||||
RobotOutlined,
|
RobotOutlined,
|
||||||
SafetyOutlined,
|
SafetyOutlined,
|
||||||
|
|
@ -36,8 +35,34 @@ import { all_admin_roles, internalUserRoles, isAdminRole, isUserTeamAdminForAnyT
|
||||||
import NewBadge from "./common_components/NewBadge";
|
import NewBadge from "./common_components/NewBadge";
|
||||||
import type { Organization } from "./networking";
|
import type { Organization } from "./networking";
|
||||||
import UsageIndicator from "./UsageIndicator";
|
import UsageIndicator from "./UsageIndicator";
|
||||||
|
import { serverRootPath } from "./networking";
|
||||||
const { Sider } = Layout;
|
const { Sider } = Layout;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pages migrated to path-based routing under (dashboard)/.
|
||||||
|
* Key = legacy page id, Value = route segment.
|
||||||
|
* Keep in sync with MIGRATED_PAGES in (dashboard)/layout.tsx and
|
||||||
|
* LEGACY_REDIRECTS in app/page.tsx.
|
||||||
|
*/
|
||||||
|
const MIGRATED_PAGES: Record<string, string> = {
|
||||||
|
"api-reference": "api-reference",
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Build an absolute href for a migrated page, respecting base URL + serverRootPath. */
|
||||||
|
function migratedHref(routeSegment: string): string {
|
||||||
|
const raw = process.env.NEXT_PUBLIC_BASE_URL ?? "";
|
||||||
|
const trimmed = raw.replace(/^\/+|\/+$/g, "");
|
||||||
|
let base = trimmed ? `/${trimmed}/` : "/";
|
||||||
|
|
||||||
|
if (serverRootPath && serverRootPath !== "/") {
|
||||||
|
const cleanRoot = serverRootPath.replace(/\/+$/, "");
|
||||||
|
const cleanBase = base.replace(/^\/+/, "");
|
||||||
|
base = `${cleanRoot}/${cleanBase}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${base}${routeSegment}`;
|
||||||
|
}
|
||||||
|
|
||||||
// Define the props type
|
// Define the props type
|
||||||
interface SidebarProps {
|
interface SidebarProps {
|
||||||
setPage: (page: string) => void;
|
setPage: (page: string) => void;
|
||||||
|
|
@ -379,6 +404,11 @@ const Sidebar: React.FC<SidebarProps> = ({ setPage, defaultSelectedKey, collapse
|
||||||
|
|
||||||
// Navigate to page helper
|
// Navigate to page helper
|
||||||
const navigateToPage = (page: string) => {
|
const navigateToPage = (page: string) => {
|
||||||
|
// For migrated pages, just call setPage — the parent layout handles routing
|
||||||
|
if (MIGRATED_PAGES[page]) {
|
||||||
|
setPage(page);
|
||||||
|
return;
|
||||||
|
}
|
||||||
const newSearchParams = new URLSearchParams(window.location.search);
|
const newSearchParams = new URLSearchParams(window.location.search);
|
||||||
newSearchParams.set("page", page);
|
newSearchParams.set("page", page);
|
||||||
window.history.pushState(null, "", `?${newSearchParams.toString()}`);
|
window.history.pushState(null, "", `?${newSearchParams.toString()}`);
|
||||||
|
|
@ -405,9 +435,11 @@ const Sidebar: React.FC<SidebarProps> = ({ setPage, defaultSelectedKey, collapse
|
||||||
</a>
|
</a>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
const params = new URLSearchParams(window.location.search);
|
// For migrated pages, generate a path-based href for right-click "Open in new tab"
|
||||||
params.set("page", page);
|
const migratedRoute = MIGRATED_PAGES[page];
|
||||||
const href = `?${params.toString()}`;
|
const href = migratedRoute
|
||||||
|
? migratedHref(migratedRoute)
|
||||||
|
: (() => { const params = new URLSearchParams(window.location.search); params.set("page", page); return `?${params.toString()}`; })();
|
||||||
return (
|
return (
|
||||||
<a
|
<a
|
||||||
href={href}
|
href={href}
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,11 @@
|
||||||
import { useHealthReadiness } from "@/app/(dashboard)/hooks/healthReadiness/useHealthReadiness";
|
import { useHealthReadiness } from "@/app/(dashboard)/hooks/healthReadiness/useHealthReadiness";
|
||||||
import { useDisableBouncingIcon } from "@/app/(dashboard)/hooks/useDisableBouncingIcon";
|
import { useDisableBouncingIcon } from "@/app/(dashboard)/hooks/useDisableBouncingIcon";
|
||||||
import { getProxyBaseUrl } from "@/components/networking";
|
import { getProxyBaseUrl } from "@/components/networking";
|
||||||
import { useUIConfig } from "@/app/(dashboard)/hooks/uiConfig/useUIConfig";
|
|
||||||
import { useTheme } from "@/contexts/ThemeContext";
|
import { useTheme } from "@/contexts/ThemeContext";
|
||||||
import { clearTokenCookies } from "@/utils/cookieUtils";
|
import { clearTokenCookies } from "@/utils/cookieUtils";
|
||||||
import { clearStoredReturnUrl } from "@/utils/returnUrlUtils";
|
import { clearStoredReturnUrl } from "@/utils/returnUrlUtils";
|
||||||
import { fetchProxySettings } from "@/utils/proxyUtils";
|
import { fetchProxySettings } from "@/utils/proxyUtils";
|
||||||
import { MenuFoldOutlined, MenuUnfoldOutlined, MessageOutlined, MoonOutlined, SunOutlined } from "@ant-design/icons";
|
import { MenuFoldOutlined, MenuUnfoldOutlined, MoonOutlined, SunOutlined } from "@ant-design/icons";
|
||||||
import { Button, Switch, Tag } from "antd";
|
import { Button, Switch, Tag } from "antd";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
|
|
@ -46,11 +45,6 @@ const Navbar: React.FC<NavbarProps> = ({
|
||||||
}) => {
|
}) => {
|
||||||
const baseUrl = getProxyBaseUrl();
|
const baseUrl = getProxyBaseUrl();
|
||||||
const [logoutUrl, setLogoutUrl] = useState("");
|
const [logoutUrl, setLogoutUrl] = useState("");
|
||||||
const { data: uiConfig } = useUIConfig();
|
|
||||||
const uiRoot = uiConfig?.server_root_path && uiConfig.server_root_path !== "/"
|
|
||||||
? uiConfig.server_root_path.replace(/\/+$/, "")
|
|
||||||
: "";
|
|
||||||
const chatHref = `${uiRoot}/ui/chat`;
|
|
||||||
const { logoUrl } = useTheme();
|
const { logoUrl } = useTheme();
|
||||||
const { data: healthData } = useHealthReadiness();
|
const { data: healthData } = useHealthReadiness();
|
||||||
const version = healthData?.litellm_version;
|
const version = healthData?.litellm_version;
|
||||||
|
|
@ -146,41 +140,6 @@ const Navbar: React.FC<NavbarProps> = ({
|
||||||
</div>
|
</div>
|
||||||
{/* Right side nav items */}
|
{/* Right side nav items */}
|
||||||
<div className="flex items-center space-x-5 ml-auto">
|
<div className="flex items-center space-x-5 ml-auto">
|
||||||
{/* Chat CTA — always visible, opens in new tab */}
|
|
||||||
<a
|
|
||||||
href={chatHref}
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
style={{
|
|
||||||
display: "inline-flex",
|
|
||||||
alignItems: "center",
|
|
||||||
gap: 6,
|
|
||||||
padding: "6px 14px",
|
|
||||||
borderRadius: 8,
|
|
||||||
background: "#1677ff",
|
|
||||||
color: "#fff",
|
|
||||||
fontSize: 13,
|
|
||||||
fontWeight: 600,
|
|
||||||
textDecoration: "none",
|
|
||||||
whiteSpace: "nowrap",
|
|
||||||
}}
|
|
||||||
onMouseEnter={(e) => { (e.currentTarget as HTMLAnchorElement).style.background = "#0958d9"; }}
|
|
||||||
onMouseLeave={(e) => { (e.currentTarget as HTMLAnchorElement).style.background = "#1677ff"; }}
|
|
||||||
>
|
|
||||||
<MessageOutlined style={{ fontSize: 14 }} />
|
|
||||||
Chat
|
|
||||||
<span style={{
|
|
||||||
fontSize: 9,
|
|
||||||
fontWeight: 700,
|
|
||||||
background: "#fff",
|
|
||||||
color: "#1677ff",
|
|
||||||
borderRadius: 3,
|
|
||||||
padding: "1px 4px",
|
|
||||||
letterSpacing: "0.05em",
|
|
||||||
}}>
|
|
||||||
NEW
|
|
||||||
</span>
|
|
||||||
</a>
|
|
||||||
<WorkerDropdown onWorkerSwitch={handleWorkerSwitch} />
|
<WorkerDropdown onWorkerSwitch={handleWorkerSwitch} />
|
||||||
<CommunityEngagementButtons />
|
<CommunityEngagementButtons />
|
||||||
{/* Dark mode is currently a work in progress. To test, you can change 'false' to 'true' below.
|
{/* Dark mode is currently a work in progress. To test, you can change 'false' to 'true' below.
|
||||||
|
|
|
||||||
|
|
@ -214,19 +214,30 @@ vi.mock("../common_components/PassThroughRoutesSelector", () => ({ default: () =
|
||||||
vi.mock("../common_components/PremiumLoggingSettings", () => ({ default: () => null }));
|
vi.mock("../common_components/PremiumLoggingSettings", () => ({ default: () => null }));
|
||||||
vi.mock("../common_components/RateLimitTypeFormItem", () => ({ default: () => null }));
|
vi.mock("../common_components/RateLimitTypeFormItem", () => ({ default: () => null }));
|
||||||
vi.mock("../common_components/RouterSettingsAccordion", () => ({ default: () => null }));
|
vi.mock("../common_components/RouterSettingsAccordion", () => ({ default: () => null }));
|
||||||
|
vi.mock("@/app/(dashboard)/hooks/teams/useTeams", () => ({
|
||||||
|
useInfiniteTeams: () => ({
|
||||||
|
data: {
|
||||||
|
pages: [{ teams: [
|
||||||
|
{ team_id: "team-1", team_alias: "Team One" },
|
||||||
|
{ team_id: "team-2", team_alias: "Team Two" },
|
||||||
|
], total: 2, page: 1, page_size: 50, total_pages: 1 }],
|
||||||
|
},
|
||||||
|
fetchNextPage: vi.fn(),
|
||||||
|
hasNextPage: false,
|
||||||
|
isFetchingNextPage: false,
|
||||||
|
isLoading: false,
|
||||||
|
}),
|
||||||
|
}));
|
||||||
vi.mock("../common_components/team_dropdown", () => ({
|
vi.mock("../common_components/team_dropdown", () => ({
|
||||||
default: ({ teams, onChange, disabled }: { teams?: any[]; onChange?: (v: string) => void; disabled?: boolean }) => (
|
default: ({ onChange, disabled }: { onChange?: (v: string) => void; disabled?: boolean }) => (
|
||||||
<select
|
<select
|
||||||
data-testid="team-dropdown"
|
data-testid="team-dropdown"
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
onChange={(e) => onChange?.(e.target.value)}
|
onChange={(e) => onChange?.(e.target.value)}
|
||||||
>
|
>
|
||||||
<option value="">Select team</option>
|
<option value="">Select team</option>
|
||||||
{teams?.map((t: any) => (
|
<option value="team-1">Team One</option>
|
||||||
<option key={t.team_id} value={t.team_id}>
|
<option value="team-2">Team Two</option>
|
||||||
{t.team_alias}
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</select>
|
</select>
|
||||||
),
|
),
|
||||||
}));
|
}));
|
||||||
|
|
|
||||||
|
|
@ -810,19 +810,17 @@ const CreateKey: React.FC<CreateKeyProps> = ({ team, teams, data, addKey, autoOp
|
||||||
help={keyOwner === "service_account" ? "required" : ""}
|
help={keyOwner === "service_account" ? "required" : ""}
|
||||||
>
|
>
|
||||||
<TeamDropdown
|
<TeamDropdown
|
||||||
teams={selectedOrganizationId ? teams?.filter((t) => t.organization_id === selectedOrganizationId) : teams}
|
|
||||||
disabled={selectedProjectId !== null}
|
disabled={selectedProjectId !== null}
|
||||||
loading={!teams}
|
organizationId={selectedOrganizationId}
|
||||||
onChange={(teamId) => {
|
onTeamSelect={(team) => {
|
||||||
const selectedTeam = teams?.find((t) => t.team_id === teamId) || null;
|
setSelectedCreateKeyTeam(team);
|
||||||
setSelectedCreateKeyTeam(selectedTeam);
|
|
||||||
setSelectedProjectId(null);
|
setSelectedProjectId(null);
|
||||||
form.setFieldValue("project_id", undefined);
|
form.setFieldValue("project_id", undefined);
|
||||||
// Auto-populate org from team for non-admin users
|
// Auto-populate org from team for non-admin users
|
||||||
if (selectedTeam?.organization_id) {
|
if (team?.organization_id) {
|
||||||
setSelectedOrganizationId(selectedTeam.organization_id);
|
setSelectedOrganizationId(team.organization_id);
|
||||||
form.setFieldValue("organization_id", selectedTeam.organization_id);
|
form.setFieldValue("organization_id", team.organization_id);
|
||||||
} else if (!teamId) {
|
} else if (!team) {
|
||||||
setSelectedOrganizationId(null);
|
setSelectedOrganizationId(null);
|
||||||
form.setFieldValue("organization_id", undefined);
|
form.setFieldValue("organization_id", undefined);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue