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 useAuthorized from "@/app/(dashboard)/hooks/useAuthorized";
|
||||
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 (
|
||||
accessToken: string,
|
||||
page: number,
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
import React, { Suspense, useEffect, useState } from "react";
|
||||
import Navbar from "@/components/navbar";
|
||||
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 { useRouter, useSearchParams } from "next/navigation";
|
||||
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 }) {
|
||||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
|
|
@ -32,10 +43,17 @@ function LayoutContent({ children }: { children: React.ReactNode }) {
|
|||
return searchParams.get("page") || "api-keys";
|
||||
});
|
||||
|
||||
const updatePage = (newPage: string) => {
|
||||
const newSearchParams = new URLSearchParams(searchParams);
|
||||
newSearchParams.set("page", newPage);
|
||||
router.push(withBase(`/?${newSearchParams.toString()}`)); // always under BASE
|
||||
const handleSetPage = (newPage: string) => {
|
||||
// If the page has been migrated to path routing, navigate there
|
||||
const migratedRoute = MIGRATED_PAGES[newPage];
|
||||
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);
|
||||
};
|
||||
|
||||
|
|
@ -65,7 +83,11 @@ function LayoutContent({ children }: { children: React.ReactNode }) {
|
|||
<DebugWarningBanner />
|
||||
<div className="flex flex-1 overflow-auto">
|
||||
<div className="mt-2">
|
||||
<Sidebar2 defaultSelectedKey={page} accessToken={accessToken} userRole={userRole} />
|
||||
<SidebarProvider
|
||||
setPage={handleSetPage}
|
||||
defaultSelectedKey={page}
|
||||
sidebarCollapsed={sidebarCollapsed}
|
||||
/>
|
||||
</div>
|
||||
<main className="flex-1">{children}</main>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -210,9 +210,7 @@ export const CreateUserButton: React.FC<CreateuserProps> = ({
|
|||
</Select2>
|
||||
</Form.Item>
|
||||
<Form.Item label="Team" name="team_id">
|
||||
<Select placeholder="Select Team" style={{ width: "100%" }}>
|
||||
<TeamDropdown teams={availableTeams} />
|
||||
</Select>
|
||||
<TeamDropdown />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label="Metadata" name="metadata">
|
||||
|
|
@ -294,7 +292,7 @@ export const CreateUserButton: React.FC<CreateuserProps> = ({
|
|||
name="team_id"
|
||||
help="If selected, user will be added as a 'user' role to the team."
|
||||
>
|
||||
<TeamDropdown teams={availableTeams} />
|
||||
<TeamDropdown />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
|
|
|
|||
|
|
@ -579,14 +579,12 @@ const Teams: React.FC<TeamProps> = ({
|
|||
}
|
||||
}
|
||||
|
||||
const response: any = await teamCreateCall(accessToken, formValues);
|
||||
if (teams !== null) {
|
||||
setTeams([...teams, response]);
|
||||
} else {
|
||||
setTeams([response]);
|
||||
}
|
||||
console.log(`response for team create call: ${response}`);
|
||||
await teamCreateCall(accessToken, formValues);
|
||||
NotificationsManager.success("Team created");
|
||||
await fetchTeamsV2({
|
||||
page: currentPage,
|
||||
size: pageSize,
|
||||
});
|
||||
form.resetFields();
|
||||
setLoggingSettings([]);
|
||||
setModelAliases({});
|
||||
|
|
|
|||
|
|
@ -387,7 +387,6 @@ export function ToolDetail({ toolName, onBack, accessToken }: ToolDetailProps) {
|
|||
</span>
|
||||
{blockScope === "team" ? (
|
||||
<TeamDropdown
|
||||
teams={teams}
|
||||
value={blockTeamId ?? undefined}
|
||||
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"
|
||||
>
|
||||
<TeamDropdown
|
||||
teams={teams}
|
||||
onChange={(value) => {
|
||||
setTeamAdminSelectedTeam(value);
|
||||
}}
|
||||
|
|
@ -325,7 +324,7 @@ const AddModelForm: React.FC<AddModelFormProps> = ({
|
|||
},
|
||||
]}
|
||||
>
|
||||
<TeamDropdown teams={teams} disabled={!premiumUser} />
|
||||
<TeamDropdown disabled={!premiumUser} />
|
||||
</Form.Item>
|
||||
)}
|
||||
{isAdmin && (
|
||||
|
|
|
|||
|
|
@ -723,10 +723,7 @@ const AddAgentForm: React.FC<AddAgentFormProps> = ({
|
|||
name="team_id"
|
||||
tooltip="Optionally assign this agent to a team. The agent and its key will belong to the selected team."
|
||||
>
|
||||
<TeamDropdown
|
||||
teams={teams}
|
||||
loading={!teams}
|
||||
/>
|
||||
<TeamDropdown />
|
||||
</Form.Item>
|
||||
|
||||
<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 { 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";
|
||||
|
||||
interface TeamDropdownProps {
|
||||
teams?: Team[] | null;
|
||||
value?: string;
|
||||
onChange?: (value: string) => void;
|
||||
/** Callback with the full Team object (or null on clear). */
|
||||
onTeamSelect?: (team: Team | null) => void;
|
||||
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 (
|
||||
<Select
|
||||
showSearch
|
||||
placeholder="Search or select a team"
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
value={value || undefined}
|
||||
onChange={handleChange}
|
||||
disabled={disabled}
|
||||
loading={loading}
|
||||
allowClear
|
||||
filterOption={(input, option) => {
|
||||
if (!option) return false;
|
||||
// Get team data from the option key
|
||||
const team = teams?.find((t) => t.team_id === option.key);
|
||||
if (!team) return false;
|
||||
|
||||
const searchTerm = input.toLowerCase().trim();
|
||||
const teamAlias = (team.team_alias || "").toLowerCase();
|
||||
const teamId = (team.team_id || "").toLowerCase();
|
||||
|
||||
// Search in both team alias and team ID
|
||||
return teamAlias.includes(searchTerm) || teamId.includes(searchTerm);
|
||||
}}
|
||||
optionFilterProp="children"
|
||||
>
|
||||
{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>
|
||||
filterOption={false}
|
||||
onSearch={handleSearch}
|
||||
searchValue={searchInput}
|
||||
onPopupScroll={handlePopupScroll}
|
||||
loading={isLoading}
|
||||
notFoundContent={isLoading ? <LoadingOutlined spin /> : "No teams found"}
|
||||
options={options}
|
||||
popupRender={(menu) => (
|
||||
<>
|
||||
{menu}
|
||||
{isFetchingNextPage && (
|
||||
<div style={{ textAlign: "center", padding: 8 }}>
|
||||
<LoadingOutlined spin />
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -18,7 +18,6 @@ import {
|
|||
FolderOutlined,
|
||||
KeyOutlined,
|
||||
LineChartOutlined,
|
||||
MessageOutlined,
|
||||
PlayCircleOutlined,
|
||||
RobotOutlined,
|
||||
SafetyOutlined,
|
||||
|
|
@ -36,8 +35,34 @@ import { all_admin_roles, internalUserRoles, isAdminRole, isUserTeamAdminForAnyT
|
|||
import NewBadge from "./common_components/NewBadge";
|
||||
import type { Organization } from "./networking";
|
||||
import UsageIndicator from "./UsageIndicator";
|
||||
import { serverRootPath } from "./networking";
|
||||
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
|
||||
interface SidebarProps {
|
||||
setPage: (page: string) => void;
|
||||
|
|
@ -379,6 +404,11 @@ const Sidebar: React.FC<SidebarProps> = ({ setPage, defaultSelectedKey, collapse
|
|||
|
||||
// Navigate to page helper
|
||||
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);
|
||||
newSearchParams.set("page", page);
|
||||
window.history.pushState(null, "", `?${newSearchParams.toString()}`);
|
||||
|
|
@ -405,9 +435,11 @@ const Sidebar: React.FC<SidebarProps> = ({ setPage, defaultSelectedKey, collapse
|
|||
</a>
|
||||
);
|
||||
}
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
params.set("page", page);
|
||||
const href = `?${params.toString()}`;
|
||||
// For migrated pages, generate a path-based href for right-click "Open in new tab"
|
||||
const migratedRoute = MIGRATED_PAGES[page];
|
||||
const href = migratedRoute
|
||||
? migratedHref(migratedRoute)
|
||||
: (() => { const params = new URLSearchParams(window.location.search); params.set("page", page); return `?${params.toString()}`; })();
|
||||
return (
|
||||
<a
|
||||
href={href}
|
||||
|
|
|
|||
|
|
@ -1,12 +1,11 @@
|
|||
import { useHealthReadiness } from "@/app/(dashboard)/hooks/healthReadiness/useHealthReadiness";
|
||||
import { useDisableBouncingIcon } from "@/app/(dashboard)/hooks/useDisableBouncingIcon";
|
||||
import { getProxyBaseUrl } from "@/components/networking";
|
||||
import { useUIConfig } from "@/app/(dashboard)/hooks/uiConfig/useUIConfig";
|
||||
import { useTheme } from "@/contexts/ThemeContext";
|
||||
import { clearTokenCookies } from "@/utils/cookieUtils";
|
||||
import { clearStoredReturnUrl } from "@/utils/returnUrlUtils";
|
||||
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 Link from "next/link";
|
||||
import React, { useEffect, useState } from "react";
|
||||
|
|
@ -46,11 +45,6 @@ const Navbar: React.FC<NavbarProps> = ({
|
|||
}) => {
|
||||
const baseUrl = getProxyBaseUrl();
|
||||
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 { data: healthData } = useHealthReadiness();
|
||||
const version = healthData?.litellm_version;
|
||||
|
|
@ -146,41 +140,6 @@ const Navbar: React.FC<NavbarProps> = ({
|
|||
</div>
|
||||
{/* Right side nav items */}
|
||||
<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} />
|
||||
<CommunityEngagementButtons />
|
||||
{/* 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/RateLimitTypeFormItem", () => ({ 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", () => ({
|
||||
default: ({ teams, onChange, disabled }: { teams?: any[]; onChange?: (v: string) => void; disabled?: boolean }) => (
|
||||
default: ({ onChange, disabled }: { onChange?: (v: string) => void; disabled?: boolean }) => (
|
||||
<select
|
||||
data-testid="team-dropdown"
|
||||
disabled={disabled}
|
||||
onChange={(e) => onChange?.(e.target.value)}
|
||||
>
|
||||
<option value="">Select team</option>
|
||||
{teams?.map((t: any) => (
|
||||
<option key={t.team_id} value={t.team_id}>
|
||||
{t.team_alias}
|
||||
</option>
|
||||
))}
|
||||
<option value="team-1">Team One</option>
|
||||
<option value="team-2">Team Two</option>
|
||||
</select>
|
||||
),
|
||||
}));
|
||||
|
|
|
|||
|
|
@ -810,19 +810,17 @@ const CreateKey: React.FC<CreateKeyProps> = ({ team, teams, data, addKey, autoOp
|
|||
help={keyOwner === "service_account" ? "required" : ""}
|
||||
>
|
||||
<TeamDropdown
|
||||
teams={selectedOrganizationId ? teams?.filter((t) => t.organization_id === selectedOrganizationId) : teams}
|
||||
disabled={selectedProjectId !== null}
|
||||
loading={!teams}
|
||||
onChange={(teamId) => {
|
||||
const selectedTeam = teams?.find((t) => t.team_id === teamId) || null;
|
||||
setSelectedCreateKeyTeam(selectedTeam);
|
||||
organizationId={selectedOrganizationId}
|
||||
onTeamSelect={(team) => {
|
||||
setSelectedCreateKeyTeam(team);
|
||||
setSelectedProjectId(null);
|
||||
form.setFieldValue("project_id", undefined);
|
||||
// Auto-populate org from team for non-admin users
|
||||
if (selectedTeam?.organization_id) {
|
||||
setSelectedOrganizationId(selectedTeam.organization_id);
|
||||
form.setFieldValue("organization_id", selectedTeam.organization_id);
|
||||
} else if (!teamId) {
|
||||
if (team?.organization_id) {
|
||||
setSelectedOrganizationId(team.organization_id);
|
||||
form.setFieldValue("organization_id", team.organization_id);
|
||||
} else if (!team) {
|
||||
setSelectedOrganizationId(null);
|
||||
form.setFieldValue("organization_id", undefined);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue