refactor(ui): remove dead App Router scaffolding in (dashboard)/* (#28891)
Deletes 39 files across 12 unused subdirs under src/app/(dashboard)/. None of these routes are reachable: LEGACY_REDIRECTS in src/app/page.tsx is empty, the live UI renders everything via the legacy ?page=X switch, and no other code statically imports from these dirs. Kept api-reference, models-and-endpoints, organizations, playground, virtual-keys because they ARE imported by the legacy switch in src/app/page.tsx — they will be migrated properly in the App Router migration. Kept shared infra: layout.tsx, hooks/, components/, networking.ts, README.md — these are imported by live code. Clean baseline before the App Router migration (LIT-3128).
This commit is contained in:
parent
3d0e0cee56
commit
13512e7abd
@ -1,12 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import TransformRequestPanel from "@/components/transform_request";
|
|
||||||
import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized";
|
|
||||||
|
|
||||||
const APIPlaygroundPage = () => {
|
|
||||||
const { accessToken } = useAuthorized();
|
|
||||||
|
|
||||||
return <TransformRequestPanel accessToken={accessToken} />;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default APIPlaygroundPage;
|
|
||||||
@ -1,12 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import BudgetPanel from "@/components/budgets/budget_panel";
|
|
||||||
import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized";
|
|
||||||
|
|
||||||
const BudgetsPage = () => {
|
|
||||||
const { accessToken } = useAuthorized();
|
|
||||||
|
|
||||||
return <BudgetPanel accessToken={accessToken} />;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default BudgetsPage;
|
|
||||||
@ -1,20 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import CacheDashboard from "@/components/cache_dashboard";
|
|
||||||
import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized";
|
|
||||||
|
|
||||||
const CachingPage = () => {
|
|
||||||
const { token, accessToken, userRole, userId, premiumUser } = useAuthorized();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<CacheDashboard
|
|
||||||
accessToken={accessToken}
|
|
||||||
token={token}
|
|
||||||
userRole={userRole}
|
|
||||||
userID={userId}
|
|
||||||
premiumUser={premiumUser}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default CachingPage;
|
|
||||||
@ -1,17 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import ClaudeCodePluginsPanel from "@/components/claude_code_plugins";
|
|
||||||
import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized";
|
|
||||||
|
|
||||||
const ClaudeCodePluginsPage = () => {
|
|
||||||
const { accessToken, userRole } = useAuthorized();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ClaudeCodePluginsPanel
|
|
||||||
accessToken={accessToken}
|
|
||||||
userRole={userRole}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ClaudeCodePluginsPage;
|
|
||||||
@ -1,23 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import Usage from "@/components/usage";
|
|
||||||
import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized";
|
|
||||||
import { useState } from "react";
|
|
||||||
|
|
||||||
const OldUsagePage = () => {
|
|
||||||
const { accessToken, token, userRole, userId, premiumUser } = useAuthorized();
|
|
||||||
const [keys, setKeys] = useState<null | any[]>([]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Usage
|
|
||||||
accessToken={accessToken}
|
|
||||||
token={token}
|
|
||||||
userRole={userRole}
|
|
||||||
userID={userId}
|
|
||||||
keys={keys}
|
|
||||||
premiumUser={premiumUser}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default OldUsagePage;
|
|
||||||
@ -1,12 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import PromptsPanel from "@/components/prompts";
|
|
||||||
import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized";
|
|
||||||
|
|
||||||
const PromptsPage = () => {
|
|
||||||
const { accessToken } = useAuthorized();
|
|
||||||
|
|
||||||
return <PromptsPanel accessToken={accessToken} />;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default PromptsPage;
|
|
||||||
@ -1,12 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import TagManagement from "@/components/tag_management";
|
|
||||||
import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized";
|
|
||||||
|
|
||||||
const TagManagementPage = () => {
|
|
||||||
const { accessToken, userId, userRole } = useAuthorized();
|
|
||||||
|
|
||||||
return <TagManagement accessToken={accessToken} userID={userId} userRole={userRole} />;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default TagManagementPage;
|
|
||||||
@ -1,12 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import GuardrailsPanel from "@/components/guardrails";
|
|
||||||
import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized";
|
|
||||||
|
|
||||||
const GuardrailsPage = () => {
|
|
||||||
const { accessToken } = useAuthorized();
|
|
||||||
|
|
||||||
return <GuardrailsPanel accessToken={accessToken} />;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default GuardrailsPage;
|
|
||||||
@ -1,20 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import SpendLogsTable from "@/components/view_logs";
|
|
||||||
import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized";
|
|
||||||
|
|
||||||
const LogsPage = () => {
|
|
||||||
const { accessToken, token, userRole, userId, premiumUser } = useAuthorized();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<SpendLogsTable
|
|
||||||
accessToken={accessToken}
|
|
||||||
token={token}
|
|
||||||
userRole={userRole}
|
|
||||||
userID={userId}
|
|
||||||
premiumUser={premiumUser}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default LogsPage;
|
|
||||||
@ -1,12 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import ModelHubTable from "@/components/AIHub/ModelHubTable";
|
|
||||||
import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized";
|
|
||||||
|
|
||||||
const ModelHubPage = () => {
|
|
||||||
const { accessToken, premiumUser, userRole } = useAuthorized();
|
|
||||||
|
|
||||||
return <ModelHubTable accessToken={accessToken} publicPage={false} premiumUser={premiumUser} userRole={userRole} />;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ModelHubPage;
|
|
||||||
@ -1,17 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import PoliciesPanel from "@/components/policies";
|
|
||||||
import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized";
|
|
||||||
|
|
||||||
const PoliciesPage = () => {
|
|
||||||
const { accessToken, userRole } = useAuthorized();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<PoliciesPanel
|
|
||||||
accessToken={accessToken}
|
|
||||||
userRole={userRole}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default PoliciesPage;
|
|
||||||
@ -1,13 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import AdminPanel from "@/components/AdminPanel";
|
|
||||||
|
|
||||||
const AdminSettings = () => {
|
|
||||||
|
|
||||||
return (
|
|
||||||
<AdminPanel
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default AdminSettings;
|
|
||||||
@ -1,12 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import Settings from "@/components/settings";
|
|
||||||
import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized";
|
|
||||||
|
|
||||||
const LoggingAndAlertsPage = () => {
|
|
||||||
const { accessToken, userRole, userId, premiumUser } = useAuthorized();
|
|
||||||
|
|
||||||
return <Settings accessToken={accessToken} userRole={userRole} userID={userId} premiumUser={premiumUser} />;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default LoggingAndAlertsPage;
|
|
||||||
@ -1,12 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import GeneralSettings from "@/components/general_settings";
|
|
||||||
import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized";
|
|
||||||
|
|
||||||
const RouterSettingsPage = () => {
|
|
||||||
const { accessToken, userRole, userId } = useAuthorized();
|
|
||||||
|
|
||||||
return <GeneralSettings accessToken={accessToken} userRole={userRole} userID={userId} modelData={{}} />;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default RouterSettingsPage;
|
|
||||||
@ -1,12 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import UIThemeSettings from "@/components/ui_theme_settings";
|
|
||||||
import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized";
|
|
||||||
|
|
||||||
const UIThemePage = () => {
|
|
||||||
const { userId, userRole, accessToken } = useAuthorized();
|
|
||||||
|
|
||||||
return <UIThemeSettings userID={userId} userRole={userRole} accessToken={accessToken} />;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default UIThemePage;
|
|
||||||
@ -1,17 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import ClaudeCodePluginsPanel from "@/components/claude_code_plugins";
|
|
||||||
import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized";
|
|
||||||
|
|
||||||
const SkillsPage = () => {
|
|
||||||
const { accessToken, userRole } = useAuthorized();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ClaudeCodePluginsPanel
|
|
||||||
accessToken={accessToken}
|
|
||||||
userRole={userRole}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default SkillsPage;
|
|
||||||
@ -1,370 +0,0 @@
|
|||||||
import React, { useState, useEffect } from "react";
|
|
||||||
import { useQueryClient } from "@tanstack/react-query";
|
|
||||||
import { organizationKeys } from "@/app/(dashboard)/hooks/organizations/useOrganizations";
|
|
||||||
import { teamDeleteCall, Organization } from "@/components/networking";
|
|
||||||
import { fetchTeams } from "@/components/common_components/fetch_teams";
|
|
||||||
import { Form } from "antd";
|
|
||||||
import TeamInfoView from "@/components/team/TeamInfo";
|
|
||||||
import TeamSSOSettings from "@/components/TeamSSOSettings";
|
|
||||||
import { isAdminRole } from "@/utils/roles";
|
|
||||||
import { Card, Button, Col, Text, Grid, TabPanel } from "@tremor/react";
|
|
||||||
import AvailableTeamsPanel from "@/components/team/available_teams";
|
|
||||||
import type { KeyResponse, Team } from "@/components/key_team_helpers/key_list";
|
|
||||||
|
|
||||||
import { Member, v2TeamListCall } from "@/components/networking";
|
|
||||||
import { updateExistingKeys } from "@/utils/dataUtils";
|
|
||||||
import TeamsHeaderTabs from "@/app/(dashboard)/teams/components/TeamsHeaderTabs";
|
|
||||||
import TeamsFilters from "@/app/(dashboard)/teams/components/TeamsFilters";
|
|
||||||
import useFetchTeams from "@/app/(dashboard)/teams/hooks/useFetchTeams";
|
|
||||||
import TeamsTable from "@/app/(dashboard)/teams/components/TeamsTable/TeamsTable";
|
|
||||||
import DeleteTeamModal from "@/app/(dashboard)/teams/components/modals/DeleteTeamModal";
|
|
||||||
import CreateTeamModal from "@/app/(dashboard)/teams/components/modals/CreateTeamModal";
|
|
||||||
|
|
||||||
interface TeamProps {
|
|
||||||
teams: Team[] | null;
|
|
||||||
accessToken: string | null;
|
|
||||||
setTeams: React.Dispatch<React.SetStateAction<Team[] | null>>;
|
|
||||||
userID: string | null;
|
|
||||||
userRole: string | null;
|
|
||||||
organizations: Organization[] | null;
|
|
||||||
premiumUser?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface FilterState {
|
|
||||||
team_id: string;
|
|
||||||
team_alias: string;
|
|
||||||
organization_id: string;
|
|
||||||
sort_by: string;
|
|
||||||
sort_order: "asc" | "desc";
|
|
||||||
}
|
|
||||||
|
|
||||||
interface TeamInfo {
|
|
||||||
members_with_roles: Member[];
|
|
||||||
}
|
|
||||||
|
|
||||||
interface PerTeamInfo {
|
|
||||||
keys: KeyResponse[];
|
|
||||||
team_info: TeamInfo;
|
|
||||||
}
|
|
||||||
|
|
||||||
const TeamsView: React.FC<TeamProps> = ({
|
|
||||||
teams,
|
|
||||||
accessToken,
|
|
||||||
setTeams,
|
|
||||||
userID,
|
|
||||||
userRole,
|
|
||||||
organizations,
|
|
||||||
premiumUser = false,
|
|
||||||
}) => {
|
|
||||||
const queryClient = useQueryClient();
|
|
||||||
const [currentOrg, setCurrentOrg] = useState<Organization | null>(null);
|
|
||||||
const [showFilters, setShowFilters] = useState(false);
|
|
||||||
const [filters, setFilters] = useState<FilterState>({
|
|
||||||
team_id: "",
|
|
||||||
team_alias: "",
|
|
||||||
organization_id: "",
|
|
||||||
sort_by: "created_at",
|
|
||||||
sort_order: "desc",
|
|
||||||
});
|
|
||||||
|
|
||||||
const [form] = Form.useForm();
|
|
||||||
const [memberForm] = Form.useForm();
|
|
||||||
|
|
||||||
const [selectedTeamId, setSelectedTeamId] = useState<string | null>(null);
|
|
||||||
const [editTeam, setEditTeam] = useState<boolean>(false);
|
|
||||||
|
|
||||||
const [isTeamModalVisible, setIsTeamModalVisible] = useState(false);
|
|
||||||
const [isAddMemberModalVisible, setIsAddMemberModalVisible] = useState(false);
|
|
||||||
const [isEditMemberModalVisible, setIsEditMemberModalVisible] = useState(false);
|
|
||||||
const [userModels, setUserModels] = useState<string[]>([]);
|
|
||||||
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
|
||||||
const [teamToDelete, setTeamToDelete] = useState<string | null>(null);
|
|
||||||
const [perTeamInfo, setPerTeamInfo] = useState<Record<string, PerTeamInfo>>({});
|
|
||||||
|
|
||||||
const [loggingSettings, setLoggingSettings] = useState<any[]>([]);
|
|
||||||
const [modelAliases, setModelAliases] = useState<{ [key: string]: string }>({});
|
|
||||||
const { lastRefreshed, onRefreshClick: handleRefreshClick } = useFetchTeams({ currentOrg, setTeams });
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const fetchTeamInfo = () => {
|
|
||||||
if (!teams) return;
|
|
||||||
|
|
||||||
const newPerTeamInfo = teams.reduce(
|
|
||||||
(acc, team) => {
|
|
||||||
acc[team.team_id] = {
|
|
||||||
keys: team.keys || [],
|
|
||||||
team_info: {
|
|
||||||
members_with_roles: team.members_with_roles || [],
|
|
||||||
},
|
|
||||||
};
|
|
||||||
return acc;
|
|
||||||
},
|
|
||||||
{} as Record<string, PerTeamInfo>,
|
|
||||||
);
|
|
||||||
|
|
||||||
setPerTeamInfo(newPerTeamInfo);
|
|
||||||
};
|
|
||||||
|
|
||||||
fetchTeamInfo();
|
|
||||||
}, [teams]);
|
|
||||||
|
|
||||||
const handleOk = () => {
|
|
||||||
setIsTeamModalVisible(false);
|
|
||||||
form.resetFields();
|
|
||||||
setLoggingSettings([]);
|
|
||||||
setModelAliases({});
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleMemberOk = () => {
|
|
||||||
setIsAddMemberModalVisible(false);
|
|
||||||
setIsEditMemberModalVisible(false);
|
|
||||||
memberForm.resetFields();
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleCancel = () => {
|
|
||||||
setIsTeamModalVisible(false);
|
|
||||||
form.resetFields();
|
|
||||||
setLoggingSettings([]);
|
|
||||||
setModelAliases({});
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDelete = async (team_id: string) => {
|
|
||||||
// Set the team to delete and open the confirmation modal
|
|
||||||
setTeamToDelete(team_id);
|
|
||||||
setIsDeleteModalOpen(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
const confirmDelete = async () => {
|
|
||||||
if (teamToDelete == null || teams == null || accessToken == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
await teamDeleteCall(accessToken, teamToDelete);
|
|
||||||
queryClient.invalidateQueries({ queryKey: organizationKeys.all });
|
|
||||||
// Successfully completed the deletion. Update the state to trigger a rerender.
|
|
||||||
fetchTeams(accessToken, userID, userRole, currentOrg, setTeams);
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error deleting the team:", error);
|
|
||||||
// Handle any error situations, such as displaying an error message to the user.
|
|
||||||
}
|
|
||||||
|
|
||||||
// Close the confirmation modal and reset the teamToDelete
|
|
||||||
setIsDeleteModalOpen(false);
|
|
||||||
setTeamToDelete(null);
|
|
||||||
};
|
|
||||||
|
|
||||||
const cancelDelete = () => {
|
|
||||||
// Close the confirmation modal and reset the teamToDelete
|
|
||||||
setIsDeleteModalOpen(false);
|
|
||||||
setTeamToDelete(null);
|
|
||||||
};
|
|
||||||
|
|
||||||
const is_team_admin = (team: any) => {
|
|
||||||
if (team == null || team.members_with_roles == null) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
for (let i = 0; i < team.members_with_roles.length; i++) {
|
|
||||||
let member = team.members_with_roles[i];
|
|
||||||
if (member.user_id == userID && member.role == "admin") {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleFilterChange = (key: keyof FilterState, value: string) => {
|
|
||||||
const newFilters = { ...filters, [key]: value };
|
|
||||||
setFilters(newFilters);
|
|
||||||
// Call teamListCall with the new filters
|
|
||||||
if (accessToken) {
|
|
||||||
v2TeamListCall(
|
|
||||||
accessToken,
|
|
||||||
newFilters.organization_id || null,
|
|
||||||
null,
|
|
||||||
newFilters.team_id || null,
|
|
||||||
newFilters.team_alias || null,
|
|
||||||
)
|
|
||||||
.then((response) => {
|
|
||||||
if (response && response.teams) {
|
|
||||||
setTeams(response.teams);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
console.error("Error fetching teams:", error);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSortChange = (sortBy: string, sortOrder: "asc" | "desc") => {
|
|
||||||
const newFilters = {
|
|
||||||
...filters,
|
|
||||||
sort_by: sortBy,
|
|
||||||
sort_order: sortOrder,
|
|
||||||
};
|
|
||||||
setFilters(newFilters);
|
|
||||||
// Call teamListCall with the new sort parameters
|
|
||||||
if (accessToken) {
|
|
||||||
v2TeamListCall(
|
|
||||||
accessToken,
|
|
||||||
filters.organization_id || null,
|
|
||||||
null,
|
|
||||||
filters.team_id || null,
|
|
||||||
filters.team_alias || null,
|
|
||||||
)
|
|
||||||
.then((response) => {
|
|
||||||
if (response && response.teams) {
|
|
||||||
setTeams(response.teams);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
console.error("Error fetching teams:", error);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleFilterReset = () => {
|
|
||||||
setFilters({
|
|
||||||
team_id: "",
|
|
||||||
team_alias: "",
|
|
||||||
organization_id: "",
|
|
||||||
sort_by: "created_at",
|
|
||||||
sort_order: "desc",
|
|
||||||
});
|
|
||||||
// Reset teams list
|
|
||||||
if (accessToken) {
|
|
||||||
v2TeamListCall(accessToken, null, userID || null, null, null)
|
|
||||||
.then((response) => {
|
|
||||||
if (response && response.teams) {
|
|
||||||
setTeams(response.teams);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
console.error("Error fetching teams:", error);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="w-full mx-4 h-[75vh]">
|
|
||||||
<Grid numItems={1} className="gap-2 p-8 w-full mt-2">
|
|
||||||
<Col numColSpan={1} className="flex flex-col gap-2">
|
|
||||||
{(userRole == "Admin" || userRole == "Org Admin") && (
|
|
||||||
<Button className="w-fit" onClick={() => setIsTeamModalVisible(true)}>
|
|
||||||
+ Create New Team
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
{selectedTeamId ? (
|
|
||||||
<TeamInfoView
|
|
||||||
teamId={selectedTeamId}
|
|
||||||
onUpdate={(data) => {
|
|
||||||
setTeams((teams) => {
|
|
||||||
if (teams == null) {
|
|
||||||
return teams;
|
|
||||||
}
|
|
||||||
const updated = teams.map((team) => {
|
|
||||||
if (data.team_id === team.team_id) {
|
|
||||||
return updateExistingKeys(team, data);
|
|
||||||
}
|
|
||||||
return team;
|
|
||||||
});
|
|
||||||
// Minimal fix: refresh the full team list after an update
|
|
||||||
if (accessToken) {
|
|
||||||
fetchTeams(accessToken, userID, userRole, currentOrg, setTeams);
|
|
||||||
}
|
|
||||||
return updated;
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
onClose={() => {
|
|
||||||
setSelectedTeamId(null);
|
|
||||||
setEditTeam(false);
|
|
||||||
}}
|
|
||||||
accessToken={accessToken}
|
|
||||||
is_team_admin={is_team_admin(teams?.find((team) => team.team_id === selectedTeamId))}
|
|
||||||
is_proxy_admin={userRole == "Admin"}
|
|
||||||
is_org_admin={(() => {
|
|
||||||
const team = teams?.find((t) => t.team_id === selectedTeamId);
|
|
||||||
if (!team?.organization_id || !organizations || !userID) return false;
|
|
||||||
const org = organizations.find((o) => o.organization_id === team.organization_id);
|
|
||||||
return org?.members?.some((m: any) => m.user_id === userID && m.user_role === "org_admin") ?? false;
|
|
||||||
})()}
|
|
||||||
userModels={userModels}
|
|
||||||
editTeam={editTeam}
|
|
||||||
premiumUser={premiumUser}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<TeamsHeaderTabs lastRefreshed={lastRefreshed} onRefresh={handleRefreshClick} userRole={userRole}>
|
|
||||||
<TabPanel>
|
|
||||||
<Text>
|
|
||||||
Click on “Team ID” to view team details <b>and</b> manage team members.
|
|
||||||
</Text>
|
|
||||||
<Grid numItems={1} className="gap-2 pt-2 pb-2 h-[75vh] w-full mt-2">
|
|
||||||
<Col numColSpan={1}>
|
|
||||||
<Card className="w-full mx-auto flex-auto overflow-hidden overflow-y-auto max-h-[50vh]">
|
|
||||||
<div className="border-b px-6 py-4">
|
|
||||||
<div className="flex flex-col space-y-4">
|
|
||||||
<TeamsFilters
|
|
||||||
filters={filters}
|
|
||||||
organizations={organizations}
|
|
||||||
showFilters={showFilters}
|
|
||||||
onToggleFilters={setShowFilters}
|
|
||||||
onChange={handleFilterChange}
|
|
||||||
onReset={handleFilterReset}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<TeamsTable
|
|
||||||
teams={teams}
|
|
||||||
currentOrg={currentOrg}
|
|
||||||
perTeamInfo={perTeamInfo}
|
|
||||||
userRole={userRole}
|
|
||||||
userId={userID}
|
|
||||||
setSelectedTeamId={setSelectedTeamId}
|
|
||||||
setEditTeam={setEditTeam}
|
|
||||||
onDeleteTeam={handleDelete}
|
|
||||||
/>
|
|
||||||
{isDeleteModalOpen && (
|
|
||||||
<DeleteTeamModal
|
|
||||||
teams={teams}
|
|
||||||
teamToDelete={teamToDelete}
|
|
||||||
onCancel={cancelDelete}
|
|
||||||
onConfirm={confirmDelete}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Card>
|
|
||||||
</Col>
|
|
||||||
</Grid>
|
|
||||||
</TabPanel>
|
|
||||||
<TabPanel>
|
|
||||||
<AvailableTeamsPanel accessToken={accessToken} userID={userID} />
|
|
||||||
</TabPanel>
|
|
||||||
{isAdminRole(userRole || "") && (
|
|
||||||
<TabPanel>
|
|
||||||
<TeamSSOSettings accessToken={accessToken} userID={userID || ""} userRole={userRole || ""} />
|
|
||||||
</TabPanel>
|
|
||||||
)}
|
|
||||||
</TeamsHeaderTabs>
|
|
||||||
)}
|
|
||||||
{(userRole == "Admin" || userRole == "Org Admin") && (
|
|
||||||
<CreateTeamModal
|
|
||||||
isTeamModalVisible={isTeamModalVisible}
|
|
||||||
handleOk={handleOk}
|
|
||||||
handleCancel={handleCancel}
|
|
||||||
currentOrg={currentOrg}
|
|
||||||
organizations={organizations}
|
|
||||||
teams={teams}
|
|
||||||
setTeams={setTeams}
|
|
||||||
modelAliases={modelAliases}
|
|
||||||
setModelAliases={setModelAliases}
|
|
||||||
loggingSettings={loggingSettings}
|
|
||||||
setLoggingSettings={setLoggingSettings}
|
|
||||||
setIsTeamModalVisible={setIsTeamModalVisible}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Col>
|
|
||||||
</Grid>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default TeamsView;
|
|
||||||
@ -1,151 +0,0 @@
|
|||||||
import { render, screen, within } from "@testing-library/react";
|
|
||||||
import userEvent from "@testing-library/user-event";
|
|
||||||
import React from "react";
|
|
||||||
import { describe, expect, it, vi } from "vitest";
|
|
||||||
import { Organization } from "@/components/networking";
|
|
||||||
import TeamsFilters from "./TeamsFilters";
|
|
||||||
|
|
||||||
type FilterState = {
|
|
||||||
team_id: string;
|
|
||||||
team_alias: string;
|
|
||||||
organization_id: string;
|
|
||||||
sort_by: string;
|
|
||||||
sort_order: "asc" | "desc";
|
|
||||||
};
|
|
||||||
|
|
||||||
const emptyFilters: FilterState = {
|
|
||||||
team_alias: "",
|
|
||||||
team_id: "",
|
|
||||||
organization_id: "",
|
|
||||||
sort_by: "",
|
|
||||||
sort_order: "asc",
|
|
||||||
};
|
|
||||||
|
|
||||||
const mockOrganizations: Organization[] = [
|
|
||||||
{ organization_id: "org-1", organization_alias: "Acme Corp" } as Organization,
|
|
||||||
{ organization_id: "org-2", organization_alias: "Globex" } as Organization,
|
|
||||||
];
|
|
||||||
|
|
||||||
const renderFilters = (overrides: Partial<Parameters<typeof TeamsFilters>[0]> = {}) => {
|
|
||||||
const defaults = {
|
|
||||||
filters: emptyFilters,
|
|
||||||
organizations: mockOrganizations,
|
|
||||||
showFilters: false,
|
|
||||||
onToggleFilters: vi.fn(),
|
|
||||||
onChange: vi.fn(),
|
|
||||||
onReset: vi.fn(),
|
|
||||||
};
|
|
||||||
return render(<TeamsFilters {...defaults} {...overrides} />);
|
|
||||||
};
|
|
||||||
|
|
||||||
describe("TeamsFilters", () => {
|
|
||||||
it("should render the team name search input, Filters button, and Reset Filters button", () => {
|
|
||||||
renderFilters();
|
|
||||||
|
|
||||||
expect(screen.getByPlaceholderText("Search by Team Name...")).toBeInTheDocument();
|
|
||||||
expect(screen.getByRole("button", { name: /^filters$/i })).toBeInTheDocument();
|
|
||||||
expect(screen.getByRole("button", { name: /reset filters/i })).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should reflect the current team_alias filter value in the search input", () => {
|
|
||||||
renderFilters({ filters: { ...emptyFilters, team_alias: "Platform" } });
|
|
||||||
|
|
||||||
expect(screen.getByPlaceholderText("Search by Team Name...")).toHaveValue("Platform");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should call onChange with 'team_alias' key when the search input changes", async () => {
|
|
||||||
const user = userEvent.setup();
|
|
||||||
const onChange = vi.fn();
|
|
||||||
renderFilters({ onChange });
|
|
||||||
|
|
||||||
await user.type(screen.getByPlaceholderText("Search by Team Name..."), "Dev");
|
|
||||||
|
|
||||||
expect(onChange).toHaveBeenCalledWith("team_alias", expect.stringContaining("D"));
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should call onToggleFilters with the inverted boolean when the Filters button is clicked", async () => {
|
|
||||||
const user = userEvent.setup();
|
|
||||||
const onToggleFilters = vi.fn();
|
|
||||||
renderFilters({ showFilters: false, onToggleFilters });
|
|
||||||
|
|
||||||
await user.click(screen.getByRole("button", { name: /^filters$/i }));
|
|
||||||
|
|
||||||
expect(onToggleFilters).toHaveBeenCalledWith(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should call onToggleFilters(false) when filters are currently expanded", async () => {
|
|
||||||
const user = userEvent.setup();
|
|
||||||
const onToggleFilters = vi.fn();
|
|
||||||
renderFilters({ showFilters: true, onToggleFilters });
|
|
||||||
|
|
||||||
await user.click(screen.getByRole("button", { name: /^filters$/i }));
|
|
||||||
|
|
||||||
expect(onToggleFilters).toHaveBeenCalledWith(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should call onReset when the Reset Filters button is clicked", async () => {
|
|
||||||
const user = userEvent.setup();
|
|
||||||
const onReset = vi.fn();
|
|
||||||
renderFilters({ onReset });
|
|
||||||
|
|
||||||
await user.click(screen.getByRole("button", { name: /reset filters/i }));
|
|
||||||
|
|
||||||
expect(onReset).toHaveBeenCalledTimes(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should not show the Team ID input when showFilters is false", () => {
|
|
||||||
renderFilters({ showFilters: false });
|
|
||||||
|
|
||||||
expect(screen.queryByPlaceholderText("Enter Team ID")).not.toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should show the Team ID input when showFilters is true", () => {
|
|
||||||
renderFilters({ showFilters: true });
|
|
||||||
|
|
||||||
expect(screen.getByPlaceholderText("Enter Team ID")).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should call onChange with 'team_id' key when the Team ID input changes", async () => {
|
|
||||||
const user = userEvent.setup();
|
|
||||||
const onChange = vi.fn();
|
|
||||||
renderFilters({ showFilters: true, onChange });
|
|
||||||
|
|
||||||
await user.type(screen.getByPlaceholderText("Enter Team ID"), "abc");
|
|
||||||
|
|
||||||
expect(onChange).toHaveBeenCalledWith("team_id", expect.stringContaining("a"));
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should reflect the current team_id filter value in the Team ID input", () => {
|
|
||||||
renderFilters({ showFilters: true, filters: { ...emptyFilters, team_id: "team-xyz" } });
|
|
||||||
|
|
||||||
expect(screen.getByPlaceholderText("Enter Team ID")).toHaveValue("team-xyz");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should show the active filter indicator on the Filters button when team_alias is set", () => {
|
|
||||||
renderFilters({ filters: { ...emptyFilters, team_alias: "Platform" } });
|
|
||||||
|
|
||||||
const filtersButton = screen.getByRole("button", { name: /^filters$/i });
|
|
||||||
expect(within(filtersButton).getByTestId("active-filter-indicator")).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should show the active filter indicator on the Filters button when team_id is set", () => {
|
|
||||||
renderFilters({ filters: { ...emptyFilters, team_id: "team-123" } });
|
|
||||||
|
|
||||||
const filtersButton = screen.getByRole("button", { name: /^filters$/i });
|
|
||||||
expect(within(filtersButton).getByTestId("active-filter-indicator")).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should show the active filter indicator on the Filters button when organization_id is set", () => {
|
|
||||||
renderFilters({ filters: { ...emptyFilters, organization_id: "org-1" } });
|
|
||||||
|
|
||||||
const filtersButton = screen.getByRole("button", { name: /^filters$/i });
|
|
||||||
expect(within(filtersButton).getByTestId("active-filter-indicator")).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should not show the active filter indicator when all filters are empty", () => {
|
|
||||||
renderFilters({ filters: emptyFilters });
|
|
||||||
|
|
||||||
const filtersButton = screen.getByRole("button", { name: /^filters$/i });
|
|
||||||
expect(within(filtersButton).queryByTestId("active-filter-indicator")).not.toBeInTheDocument();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@ -1,141 +0,0 @@
|
|||||||
import { Select, SelectItem } from "@tremor/react";
|
|
||||||
import React from "react";
|
|
||||||
import { Organization } from "@/components/networking";
|
|
||||||
|
|
||||||
interface TeamsFiltersProps {
|
|
||||||
filters: FilterState;
|
|
||||||
organizations: Organization[] | null;
|
|
||||||
showFilters: boolean;
|
|
||||||
onToggleFilters: (toggle: boolean) => void;
|
|
||||||
onChange: <K extends keyof FilterState>(key: K, value: FilterState[K]) => void;
|
|
||||||
onReset: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
type FilterState = {
|
|
||||||
team_id: string;
|
|
||||||
team_alias: string;
|
|
||||||
organization_id: string;
|
|
||||||
sort_by: string;
|
|
||||||
sort_order: "asc" | "desc";
|
|
||||||
};
|
|
||||||
|
|
||||||
const TeamsFilters = ({
|
|
||||||
filters,
|
|
||||||
organizations,
|
|
||||||
showFilters,
|
|
||||||
onToggleFilters,
|
|
||||||
onChange,
|
|
||||||
onReset,
|
|
||||||
}: TeamsFiltersProps) => {
|
|
||||||
return (
|
|
||||||
<div className="flex flex-col space-y-4">
|
|
||||||
{/* Search and Filter Controls */}
|
|
||||||
<div className="flex flex-wrap items-center gap-3">
|
|
||||||
{/* Team Alias Search */}
|
|
||||||
<div className="relative w-64">
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
placeholder="Search by Team Name..."
|
|
||||||
className="w-full px-3 py-2 pl-8 border rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
|
||||||
value={filters.team_alias}
|
|
||||||
onChange={(e) => onChange("team_alias", e.target.value)}
|
|
||||||
/>
|
|
||||||
<svg
|
|
||||||
className="absolute left-2.5 top-2.5 h-4 w-4 text-gray-500"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
strokeWidth={2}
|
|
||||||
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Filter Button */}
|
|
||||||
<button
|
|
||||||
className={`px-3 py-2 text-sm border rounded-md hover:bg-gray-50 flex items-center gap-2 ${showFilters ? "bg-gray-100" : ""}`}
|
|
||||||
onClick={() => onToggleFilters(!showFilters)}
|
|
||||||
>
|
|
||||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
strokeWidth={2}
|
|
||||||
d="M3 4a1 1 0 011-1h16a1 1 0 011 1v2.586a1 1 0 01-.293.707l-6.414 6.414a1 1 0 00-.293.707V17l-4 4v-6.586a1 1 0 00-.293-.707L3.293 7.293A1 1 0 013 6.586V4z"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
Filters
|
|
||||||
{(filters.team_id || filters.team_alias || filters.organization_id) && (
|
|
||||||
<span data-testid="active-filter-indicator" className="w-2 h-2 rounded-full bg-blue-500"></span>
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
|
|
||||||
{/* Reset Filters Button */}
|
|
||||||
<button
|
|
||||||
className="px-3 py-2 text-sm border rounded-md hover:bg-gray-50 flex items-center gap-2"
|
|
||||||
onClick={onReset}
|
|
||||||
>
|
|
||||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
strokeWidth={2}
|
|
||||||
d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
Reset Filters
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Additional Filters */}
|
|
||||||
{showFilters && (
|
|
||||||
<div className="flex flex-wrap items-center gap-3 mt-3">
|
|
||||||
{/* Team ID Search */}
|
|
||||||
<div className="relative w-64">
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
placeholder="Enter Team ID"
|
|
||||||
className="w-full px-3 py-2 pl-8 border rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
|
||||||
value={filters.team_id}
|
|
||||||
onChange={(e) => onChange("team_id", e.target.value)}
|
|
||||||
/>
|
|
||||||
<svg
|
|
||||||
className="absolute left-2.5 top-2.5 h-4 w-4 text-gray-500"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
strokeWidth={2}
|
|
||||||
d="M5.121 17.804A13.937 13.937 0 0112 16c2.5 0 4.847.655 6.879 1.804M15 10a3 3 0 11-6 0 3 3 0 016 0zm6 2a9 9 0 11-18 0 9 9 0 0118 0z"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Organization Dropdown */}
|
|
||||||
<div className="w-64">
|
|
||||||
<Select
|
|
||||||
value={filters.organization_id || ""}
|
|
||||||
onValueChange={(value) => onChange("organization_id", value)}
|
|
||||||
placeholder="Select Organization"
|
|
||||||
>
|
|
||||||
{organizations?.map((org) => (
|
|
||||||
<SelectItem key={org.organization_id} value={org.organization_id || ""}>
|
|
||||||
{org.organization_alias || org.organization_id}
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default TeamsFilters;
|
|
||||||
@ -1,54 +0,0 @@
|
|||||||
import { render, screen } from "@testing-library/react";
|
|
||||||
import React from "react";
|
|
||||||
import { describe, expect, it, vi } from "vitest";
|
|
||||||
import TeamsHeaderTabs from "./TeamsHeaderTabs";
|
|
||||||
|
|
||||||
vi.mock("@tremor/react", () => ({
|
|
||||||
TabGroup: ({ children, ...props }: any) => <div data-testid="tab-group" {...props}>{children}</div>,
|
|
||||||
TabList: ({ children, ...props }: any) => <div data-testid="tab-list" {...props}>{children}</div>,
|
|
||||||
Tab: ({ children, ...props }: any) => <button {...props}>{children}</button>,
|
|
||||||
TabPanels: ({ children, ...props }: any) => <div data-testid="tab-panels" {...props}>{children}</div>,
|
|
||||||
Text: ({ children, ...props }: any) => <span {...props}>{children}</span>,
|
|
||||||
Icon: ({ onClick, ...props }: any) => <button data-testid="refresh-icon" onClick={onClick} />,
|
|
||||||
}));
|
|
||||||
|
|
||||||
vi.mock("@heroicons/react/outline", () => ({
|
|
||||||
RefreshIcon: () => <svg data-testid="refresh-svg" />,
|
|
||||||
}));
|
|
||||||
|
|
||||||
const renderTabs = (props: Partial<Parameters<typeof TeamsHeaderTabs>[0]> = {}) => {
|
|
||||||
const defaults = {
|
|
||||||
lastRefreshed: "",
|
|
||||||
onRefresh: vi.fn(),
|
|
||||||
userRole: "Internal User",
|
|
||||||
children: <div data-testid="panel-content">Panel</div>,
|
|
||||||
};
|
|
||||||
return render(<TeamsHeaderTabs {...defaults} {...props} />);
|
|
||||||
};
|
|
||||||
|
|
||||||
describe("TeamsHeaderTabs", () => {
|
|
||||||
it("should render 'Your Teams' and 'Available Teams' tabs", () => {
|
|
||||||
renderTabs();
|
|
||||||
|
|
||||||
expect(screen.getByText("Your Teams")).toBeInTheDocument();
|
|
||||||
expect(screen.getByText("Available Teams")).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should render 'Default Team Settings' tab when user is Admin", () => {
|
|
||||||
renderTabs({ userRole: "Admin" });
|
|
||||||
|
|
||||||
expect(screen.getByText("Default Team Settings")).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should not render 'Default Team Settings' tab for non-admin users", () => {
|
|
||||||
renderTabs({ userRole: "Internal User" });
|
|
||||||
|
|
||||||
expect(screen.queryByText("Default Team Settings")).not.toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should display last refreshed time when provided", () => {
|
|
||||||
renderTabs({ lastRefreshed: "2024-06-01 12:00:00" });
|
|
||||||
|
|
||||||
expect(screen.getByText("Last Refreshed: 2024-06-01 12:00:00")).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@ -1,38 +0,0 @@
|
|||||||
import { Icon, Tab, TabGroup, TabList, TabPanels, Text } from "@tremor/react";
|
|
||||||
import { isAdminRole } from "@/utils/roles";
|
|
||||||
import { RefreshIcon } from "@heroicons/react/outline";
|
|
||||||
import React from "react";
|
|
||||||
|
|
||||||
type TeamsHeaderTabsProps = {
|
|
||||||
lastRefreshed: string;
|
|
||||||
onRefresh: () => void;
|
|
||||||
userRole: string | null;
|
|
||||||
children: React.ReactNode;
|
|
||||||
};
|
|
||||||
|
|
||||||
const TeamsHeaderTabs = ({ lastRefreshed, onRefresh, userRole, children }: TeamsHeaderTabsProps) => {
|
|
||||||
return (
|
|
||||||
<TabGroup className="gap-2 h-[75vh] w-full">
|
|
||||||
<TabList className="flex justify-between mt-2 w-full items-center">
|
|
||||||
<div className="flex">
|
|
||||||
<Tab>Your Teams</Tab>
|
|
||||||
<Tab>Available Teams</Tab>
|
|
||||||
{isAdminRole(userRole || "") && <Tab>Default Team Settings</Tab>}
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center space-x-2">
|
|
||||||
{lastRefreshed && <Text>Last Refreshed: {lastRefreshed}</Text>}
|
|
||||||
<Icon
|
|
||||||
icon={RefreshIcon} // Modify as necessary for correct icon name
|
|
||||||
variant="shadow"
|
|
||||||
size="xs"
|
|
||||||
className="self-center"
|
|
||||||
onClick={onRefresh}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</TabList>
|
|
||||||
<TabPanels>{children}</TabPanels>
|
|
||||||
</TabGroup>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default TeamsHeaderTabs;
|
|
||||||
@ -1,146 +0,0 @@
|
|||||||
import { act, render, screen } from "@testing-library/react";
|
|
||||||
import React from "react";
|
|
||||||
import { describe, expect, it, vi } from "vitest";
|
|
||||||
import { Team } from "@/components/key_team_helpers/key_list";
|
|
||||||
import ModelsCell from "./ModelsCell";
|
|
||||||
|
|
||||||
// The Icon component from @tremor/react does not forward onClick to the rendered element
|
|
||||||
// by default in the test environment, so we stub it with a clickable button so accordion
|
|
||||||
// interaction can be tested end-to-end.
|
|
||||||
vi.mock("@tremor/react", async (importOriginal) => {
|
|
||||||
const actual = await importOriginal<typeof import("@tremor/react")>();
|
|
||||||
// Re-apply the global Button/Tooltip overrides from tests/setupTests.ts. A file-level
|
|
||||||
// vi.mock fully replaces the setup-level mock, so without this the real Tremor Button
|
|
||||||
// leaks through and its useTooltip(300) schedules a native setTimeout that can fire
|
|
||||||
// post-teardown -> "window is not defined".
|
|
||||||
return {
|
|
||||||
...actual,
|
|
||||||
Icon: ({ onClick, "aria-label": ariaLabel }: { onClick?: () => void; "aria-label"?: string }) =>
|
|
||||||
React.createElement("button", { onClick, "aria-label": ariaLabel ?? "accordion-toggle", type: "button" }),
|
|
||||||
Button: React.forwardRef<HTMLButtonElement, any>(({ children, ...props }, ref) =>
|
|
||||||
React.createElement("button", { ...props, ref }, children),
|
|
||||||
),
|
|
||||||
Tooltip: ({ children }: { children?: React.ReactNode }) => React.createElement(React.Fragment, null, children),
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
const makeTeam = (models: string[], overrides: Partial<Team> = {}): Team => ({
|
|
||||||
team_id: "team-1",
|
|
||||||
team_alias: "Engineering",
|
|
||||||
models,
|
|
||||||
max_budget: null,
|
|
||||||
budget_duration: null,
|
|
||||||
tpm_limit: null,
|
|
||||||
rpm_limit: null,
|
|
||||||
organization_id: "org-1",
|
|
||||||
created_at: "2024-01-01T00:00:00Z",
|
|
||||||
keys: [],
|
|
||||||
members_with_roles: [],
|
|
||||||
spend: 0,
|
|
||||||
...overrides,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Wrap in a table so the <td> from TableCell renders without HTML warnings.
|
|
||||||
const renderModelsCell = (team: Team) =>
|
|
||||||
render(
|
|
||||||
<table>
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<ModelsCell team={team} />
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>,
|
|
||||||
);
|
|
||||||
|
|
||||||
describe("ModelsCell", () => {
|
|
||||||
it("should show 'All Proxy Models' badge when the models array is empty", () => {
|
|
||||||
renderModelsCell(makeTeam([]));
|
|
||||||
|
|
||||||
expect(screen.getByText("All Proxy Models")).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should show an 'All Proxy Models' badge when the model value is 'all-proxy-models'", () => {
|
|
||||||
renderModelsCell(makeTeam(["all-proxy-models"]));
|
|
||||||
|
|
||||||
expect(screen.getByText("All Proxy Models")).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should display individual model badges for up to 3 models without an accordion", () => {
|
|
||||||
renderModelsCell(makeTeam(["gpt-4", "gpt-3.5-turbo", "claude-3"]));
|
|
||||||
|
|
||||||
expect(screen.getByText("gpt-4")).toBeInTheDocument();
|
|
||||||
expect(screen.getByText("gpt-3.5-turbo")).toBeInTheDocument();
|
|
||||||
expect(screen.getByText("claude-3")).toBeInTheDocument();
|
|
||||||
expect(screen.queryByRole("button", { name: /accordion/i })).not.toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should truncate model names longer than 30 characters with an ellipsis", () => {
|
|
||||||
const longName = "a-very-long-model-name-exceeding-thirty-chars";
|
|
||||||
renderModelsCell(makeTeam([longName]));
|
|
||||||
|
|
||||||
const badge = screen.getByText((text) => text.endsWith("..."));
|
|
||||||
expect(badge).toBeInTheDocument();
|
|
||||||
expect(badge.textContent!.length).toBeLessThanOrEqual(33); // 30 chars + "..."
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should show the first 3 models and a '+N more models' badge when there are more than 3 models", () => {
|
|
||||||
renderModelsCell(makeTeam(["m1", "m2", "m3", "m4", "m5"]));
|
|
||||||
|
|
||||||
expect(screen.getByText("m1")).toBeInTheDocument();
|
|
||||||
expect(screen.getByText("m2")).toBeInTheDocument();
|
|
||||||
expect(screen.getByText("m3")).toBeInTheDocument();
|
|
||||||
expect(screen.getByText("+2 more models")).toBeInTheDocument();
|
|
||||||
expect(screen.queryByText("m4")).not.toBeInTheDocument();
|
|
||||||
expect(screen.queryByText("m5")).not.toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should use singular 'more model' when there is exactly 1 overflow model", () => {
|
|
||||||
renderModelsCell(makeTeam(["m1", "m2", "m3", "m4"]));
|
|
||||||
|
|
||||||
expect(screen.getByText("+1 more model")).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should show the accordion toggle button when there are more than 3 models", () => {
|
|
||||||
renderModelsCell(makeTeam(["m1", "m2", "m3", "m4"]));
|
|
||||||
|
|
||||||
expect(screen.getByRole("button", { name: /accordion/i })).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should expand to show all models when the accordion toggle is clicked", () => {
|
|
||||||
renderModelsCell(makeTeam(["m1", "m2", "m3", "m4", "m5"]));
|
|
||||||
|
|
||||||
act(() => {
|
|
||||||
screen.getByRole("button", { name: /accordion/i }).click();
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(screen.getByText("m4")).toBeInTheDocument();
|
|
||||||
expect(screen.getByText("m5")).toBeInTheDocument();
|
|
||||||
expect(screen.queryByText("+2 more models")).not.toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should collapse back to show the overflow badge after a second click on the toggle", () => {
|
|
||||||
renderModelsCell(makeTeam(["m1", "m2", "m3", "m4", "m5"]));
|
|
||||||
|
|
||||||
const toggle = screen.getByRole("button", { name: /accordion/i });
|
|
||||||
act(() => {
|
|
||||||
toggle.click();
|
|
||||||
});
|
|
||||||
act(() => {
|
|
||||||
toggle.click();
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(screen.queryByText("m4")).not.toBeInTheDocument();
|
|
||||||
expect(screen.getByText("+2 more models")).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should collapse to a single 'All Proxy Models' badge when the models list includes 'all-proxy-models'", () => {
|
|
||||||
renderModelsCell(makeTeam(["m1", "m2", "m3", "all-proxy-models"]));
|
|
||||||
|
|
||||||
// When all-proxy-models is present, all individual models are hidden and no accordion is shown
|
|
||||||
expect(screen.getByText("All Proxy Models")).toBeInTheDocument();
|
|
||||||
expect(screen.queryByText("m1")).not.toBeInTheDocument();
|
|
||||||
expect(screen.queryByText("m2")).not.toBeInTheDocument();
|
|
||||||
expect(screen.queryByText("m3")).not.toBeInTheDocument();
|
|
||||||
expect(screen.queryByRole("button", { name: /accordion/i })).not.toBeInTheDocument();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@ -1,107 +0,0 @@
|
|||||||
import { Badge, Icon, TableCell, Text } from "@tremor/react";
|
|
||||||
import { ChevronDownIcon, ChevronRightIcon } from "@heroicons/react/outline";
|
|
||||||
import { getModelDisplayName } from "@/components/key_team_helpers/fetch_available_models_team_key";
|
|
||||||
import React, { useMemo, useState } from "react";
|
|
||||||
import { Team } from "@/components/key_team_helpers/key_list";
|
|
||||||
|
|
||||||
interface ModelsCellProps {
|
|
||||||
team: Team;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ModelEntry {
|
|
||||||
name: string;
|
|
||||||
source: "direct" | "access_group";
|
|
||||||
}
|
|
||||||
|
|
||||||
const ModelsCell = ({ team }: ModelsCellProps) => {
|
|
||||||
const [expandedAccordion, setExpandedAccordion] = useState<boolean>(false);
|
|
||||||
|
|
||||||
const isAllModels = !team.models || team.models.length === 0 || team.models.includes("all-proxy-models");
|
|
||||||
|
|
||||||
const modelEntries: ModelEntry[] = useMemo(() => {
|
|
||||||
if (isAllModels) return [];
|
|
||||||
const entries: ModelEntry[] = team.models.map((m) => ({
|
|
||||||
name: m,
|
|
||||||
source: "direct" as const,
|
|
||||||
}));
|
|
||||||
for (const m of team.access_group_models || []) {
|
|
||||||
entries.push({ name: m, source: "access_group" });
|
|
||||||
}
|
|
||||||
return entries;
|
|
||||||
}, [team.models, team.access_group_models, isAllModels]);
|
|
||||||
|
|
||||||
const renderBadge = (entry: ModelEntry, index: number) => {
|
|
||||||
if (entry.name === "all-proxy-models") {
|
|
||||||
return (
|
|
||||||
<Badge key={index} size={"xs"} color="red">
|
|
||||||
<Text>All Proxy Models</Text>
|
|
||||||
</Badge>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
const displayName = getModelDisplayName(entry.name);
|
|
||||||
const truncated = displayName.length > 30 ? `${displayName.slice(0, 30)}...` : displayName;
|
|
||||||
return (
|
|
||||||
<Badge
|
|
||||||
key={index}
|
|
||||||
size={"xs"}
|
|
||||||
color={entry.source === "access_group" ? "green" : "blue"}
|
|
||||||
title={entry.source === "access_group" ? "From access group" : "Direct assignment"}
|
|
||||||
>
|
|
||||||
<Text>{truncated}</Text>
|
|
||||||
</Badge>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<TableCell
|
|
||||||
style={{
|
|
||||||
maxWidth: "8-x",
|
|
||||||
whiteSpace: "pre-wrap",
|
|
||||||
overflow: "hidden",
|
|
||||||
}}
|
|
||||||
className={modelEntries.length > 3 ? "px-0" : ""}
|
|
||||||
>
|
|
||||||
<div className="flex flex-col">
|
|
||||||
{modelEntries.length === 0 ? (
|
|
||||||
<Badge size={"xs"} className="mb-1" color="red">
|
|
||||||
<Text>All Proxy Models</Text>
|
|
||||||
</Badge>
|
|
||||||
) : (
|
|
||||||
<div className="flex flex-col">
|
|
||||||
<div className="flex items-start">
|
|
||||||
{modelEntries.length > 3 && (
|
|
||||||
<div>
|
|
||||||
<Icon
|
|
||||||
icon={expandedAccordion ? ChevronDownIcon : ChevronRightIcon}
|
|
||||||
className="cursor-pointer"
|
|
||||||
size="xs"
|
|
||||||
onClick={() => {
|
|
||||||
setExpandedAccordion((prev) => !prev);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div className="flex flex-wrap gap-1">
|
|
||||||
{modelEntries.slice(0, 3).map((entry, index) => renderBadge(entry, index))}
|
|
||||||
{modelEntries.length > 3 && !expandedAccordion && (
|
|
||||||
<Badge size={"xs"} color="gray" className="cursor-pointer">
|
|
||||||
<Text>
|
|
||||||
+{modelEntries.length - 3} {modelEntries.length - 3 === 1 ? "more model" : "more models"}
|
|
||||||
</Text>
|
|
||||||
</Badge>
|
|
||||||
)}
|
|
||||||
{expandedAccordion && (
|
|
||||||
<div className="flex flex-wrap gap-1">
|
|
||||||
{modelEntries.slice(3).map((entry, index) => renderBadge(entry, index + 3))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</TableCell>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ModelsCell;
|
|
||||||
@ -1,129 +0,0 @@
|
|||||||
import { render, screen } from "@testing-library/react";
|
|
||||||
import userEvent from "@testing-library/user-event";
|
|
||||||
import React from "react";
|
|
||||||
import { describe, expect, it, vi } from "vitest";
|
|
||||||
import { Team } from "@/components/key_team_helpers/key_list";
|
|
||||||
import TeamsTable from "./TeamsTable";
|
|
||||||
|
|
||||||
vi.mock("@tremor/react", () => ({
|
|
||||||
Button: React.forwardRef<HTMLButtonElement, any>(({ children, ...props }, ref) =>
|
|
||||||
React.createElement("button", { ...props, ref }, children),
|
|
||||||
),
|
|
||||||
Icon: ({ onClick, ...props }: any) => <button data-testid={props["data-testid"] || "icon-btn"} onClick={onClick} aria-label={props["aria-label"]} />,
|
|
||||||
Table: ({ children }: any) => <table>{children}</table>,
|
|
||||||
TableHead: ({ children }: any) => <thead>{children}</thead>,
|
|
||||||
TableBody: ({ children }: any) => <tbody>{children}</tbody>,
|
|
||||||
TableRow: ({ children }: any) => <tr>{children}</tr>,
|
|
||||||
TableHeaderCell: ({ children }: any) => <th>{children}</th>,
|
|
||||||
TableCell: ({ children, ...props }: any) => <td {...props}>{children}</td>,
|
|
||||||
Text: ({ children }: any) => <span>{children}</span>,
|
|
||||||
}));
|
|
||||||
|
|
||||||
vi.mock("antd", () => ({
|
|
||||||
Tooltip: ({ children }: any) => <>{children}</>,
|
|
||||||
}));
|
|
||||||
|
|
||||||
vi.mock("@heroicons/react/outline", () => ({
|
|
||||||
PencilAltIcon: () => <svg data-testid="pencil-icon" />,
|
|
||||||
TrashIcon: () => <svg data-testid="trash-icon" />,
|
|
||||||
}));
|
|
||||||
|
|
||||||
vi.mock("@/utils/dataUtils", () => ({
|
|
||||||
formatNumberWithCommas: (val: number, decimals: number) =>
|
|
||||||
val != null ? val.toFixed(decimals) : "N/A",
|
|
||||||
}));
|
|
||||||
|
|
||||||
vi.mock("@/app/(dashboard)/teams/components/TeamsTable/ModelsCell", () => ({
|
|
||||||
default: ({ team }: any) => <td data-testid="models-cell">{team.models.join(",")}</td>,
|
|
||||||
}));
|
|
||||||
|
|
||||||
vi.mock("@/app/(dashboard)/teams/components/TeamsTable/YourRoleCell/YourRoleCell", () => ({
|
|
||||||
default: ({ team }: any) => <td data-testid="role-cell">{team.team_id}</td>,
|
|
||||||
}));
|
|
||||||
|
|
||||||
const makeTeam = (overrides: Partial<Team> = {}): Team => ({
|
|
||||||
team_id: "team-abc1234",
|
|
||||||
team_alias: "Platform",
|
|
||||||
models: ["gpt-4"],
|
|
||||||
max_budget: 500,
|
|
||||||
budget_duration: null,
|
|
||||||
tpm_limit: null,
|
|
||||||
rpm_limit: null,
|
|
||||||
organization_id: "org-1",
|
|
||||||
created_at: "2024-06-01T00:00:00Z",
|
|
||||||
keys: [],
|
|
||||||
members_with_roles: [],
|
|
||||||
spend: 123.4567,
|
|
||||||
...overrides,
|
|
||||||
});
|
|
||||||
|
|
||||||
const defaultPerTeamInfo = {
|
|
||||||
"team-abc1234": {
|
|
||||||
keys: [{ token: "tok-1" } as any, { token: "tok-2" } as any],
|
|
||||||
team_info: {
|
|
||||||
members_with_roles: [{ user_id: "u1", role: "admin" } as any],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const renderTable = (overrides: Partial<Parameters<typeof TeamsTable>[0]> = {}) => {
|
|
||||||
const defaults = {
|
|
||||||
teams: [makeTeam()],
|
|
||||||
currentOrg: null,
|
|
||||||
perTeamInfo: defaultPerTeamInfo,
|
|
||||||
userRole: "Admin",
|
|
||||||
userId: "user-1",
|
|
||||||
setSelectedTeamId: vi.fn(),
|
|
||||||
setEditTeam: vi.fn(),
|
|
||||||
onDeleteTeam: vi.fn(),
|
|
||||||
};
|
|
||||||
return render(<TeamsTable {...defaults} {...overrides} />);
|
|
||||||
};
|
|
||||||
|
|
||||||
describe("TeamsTable", () => {
|
|
||||||
it("should render table headers", () => {
|
|
||||||
renderTable();
|
|
||||||
|
|
||||||
expect(screen.getByText("Team Name")).toBeInTheDocument();
|
|
||||||
expect(screen.getByText("Team ID")).toBeInTheDocument();
|
|
||||||
expect(screen.getByText("Created")).toBeInTheDocument();
|
|
||||||
expect(screen.getByText("Spend (USD)")).toBeInTheDocument();
|
|
||||||
expect(screen.getByText("Budget (USD)")).toBeInTheDocument();
|
|
||||||
expect(screen.getByText("Models")).toBeInTheDocument();
|
|
||||||
expect(screen.getByText("Organization")).toBeInTheDocument();
|
|
||||||
expect(screen.getByText("Your Role")).toBeInTheDocument();
|
|
||||||
expect(screen.getByText("Info")).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should render team rows with team data", () => {
|
|
||||||
renderTable();
|
|
||||||
|
|
||||||
expect(screen.getByText("Platform")).toBeInTheDocument();
|
|
||||||
expect(screen.getByText("team-ab...")).toBeInTheDocument();
|
|
||||||
expect(screen.getByText("org-1")).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should show edit and delete icons for Admin users", () => {
|
|
||||||
renderTable({ userRole: "Admin" });
|
|
||||||
|
|
||||||
expect(screen.getAllByTestId("icon-btn").length).toBeGreaterThanOrEqual(2);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should not show edit and delete icons for non-Admin users", () => {
|
|
||||||
renderTable({ userRole: "Internal User" });
|
|
||||||
|
|
||||||
// Only the team ID button should be present, no icon-btn for edit/delete
|
|
||||||
const iconBtns = screen.queryAllByTestId("icon-btn");
|
|
||||||
expect(iconBtns).toHaveLength(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should call setSelectedTeamId when team ID button is clicked", async () => {
|
|
||||||
const user = userEvent.setup();
|
|
||||||
const setSelectedTeamId = vi.fn();
|
|
||||||
renderTable({ setSelectedTeamId });
|
|
||||||
|
|
||||||
await user.click(screen.getByText("team-ab..."));
|
|
||||||
|
|
||||||
expect(setSelectedTeamId).toHaveBeenCalledWith("team-abc1234");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@ -1,166 +0,0 @@
|
|||||||
import { Button, Icon, Table, TableBody, TableCell, TableHead, TableHeaderCell, TableRow, Text } from "@tremor/react";
|
|
||||||
import { Tooltip } from "antd";
|
|
||||||
import { formatNumberWithCommas } from "@/utils/dataUtils";
|
|
||||||
import { PencilAltIcon, TrashIcon } from "@heroicons/react/outline";
|
|
||||||
import React from "react";
|
|
||||||
import { type KeyResponse, Team } from "@/components/key_team_helpers/key_list";
|
|
||||||
import { Member, Organization } from "@/components/networking";
|
|
||||||
import ModelsCell from "@/app/(dashboard)/teams/components/TeamsTable/ModelsCell";
|
|
||||||
import YourRoleCell from "@/app/(dashboard)/teams/components/TeamsTable/YourRoleCell/YourRoleCell";
|
|
||||||
|
|
||||||
type TeamsTableProps = {
|
|
||||||
teams: Team[] | null;
|
|
||||||
currentOrg: Organization | null;
|
|
||||||
perTeamInfo: Record<string, PerTeamInfo>;
|
|
||||||
userRole: string | null;
|
|
||||||
userId: string | null;
|
|
||||||
setSelectedTeamId: (teamId: string) => void;
|
|
||||||
setEditTeam: (editTeam: boolean) => void;
|
|
||||||
onDeleteTeam: (teamId: string) => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
interface TeamInfo {
|
|
||||||
members_with_roles: Member[];
|
|
||||||
}
|
|
||||||
|
|
||||||
interface PerTeamInfo {
|
|
||||||
keys: KeyResponse[];
|
|
||||||
team_info: TeamInfo;
|
|
||||||
}
|
|
||||||
|
|
||||||
const TeamsTable = ({
|
|
||||||
teams,
|
|
||||||
currentOrg,
|
|
||||||
setSelectedTeamId,
|
|
||||||
perTeamInfo,
|
|
||||||
userRole,
|
|
||||||
userId,
|
|
||||||
setEditTeam,
|
|
||||||
onDeleteTeam,
|
|
||||||
}: TeamsTableProps) => {
|
|
||||||
return (
|
|
||||||
<Table>
|
|
||||||
<TableHead>
|
|
||||||
<TableRow>
|
|
||||||
<TableHeaderCell>Team Name</TableHeaderCell>
|
|
||||||
<TableHeaderCell>Team ID</TableHeaderCell>
|
|
||||||
<TableHeaderCell>Created</TableHeaderCell>
|
|
||||||
<TableHeaderCell>Spend (USD)</TableHeaderCell>
|
|
||||||
<TableHeaderCell>Budget (USD)</TableHeaderCell>
|
|
||||||
<TableHeaderCell>Models</TableHeaderCell>
|
|
||||||
<TableHeaderCell>Organization</TableHeaderCell>
|
|
||||||
<TableHeaderCell>Your Role</TableHeaderCell>
|
|
||||||
<TableHeaderCell>Info</TableHeaderCell>
|
|
||||||
</TableRow>
|
|
||||||
</TableHead>
|
|
||||||
|
|
||||||
<TableBody>
|
|
||||||
{teams && teams.length > 0
|
|
||||||
? teams
|
|
||||||
.filter((team) => {
|
|
||||||
if (!currentOrg) return true;
|
|
||||||
return team.organization_id === currentOrg.organization_id;
|
|
||||||
})
|
|
||||||
.sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime())
|
|
||||||
.map((team: any) => (
|
|
||||||
<TableRow key={team.team_id}>
|
|
||||||
<TableCell
|
|
||||||
style={{
|
|
||||||
maxWidth: "4px",
|
|
||||||
whiteSpace: "pre-wrap",
|
|
||||||
overflow: "hidden",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{team["team_alias"]}
|
|
||||||
</TableCell>
|
|
||||||
<TableCell>
|
|
||||||
<div className="overflow-hidden">
|
|
||||||
<Tooltip title={team.team_id}>
|
|
||||||
<Button
|
|
||||||
size="xs"
|
|
||||||
variant="light"
|
|
||||||
className="font-mono text-blue-500 bg-blue-50 hover:bg-blue-100 text-xs font-normal px-2 py-0.5 text-left overflow-hidden truncate max-w-[200px]"
|
|
||||||
data-testid="team-id-cell"
|
|
||||||
onClick={() => {
|
|
||||||
// Add click handler
|
|
||||||
setSelectedTeamId(team.team_id);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{team.team_id.slice(0, 7)}...
|
|
||||||
</Button>
|
|
||||||
</Tooltip>
|
|
||||||
</div>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell
|
|
||||||
style={{
|
|
||||||
maxWidth: "4px",
|
|
||||||
whiteSpace: "pre-wrap",
|
|
||||||
overflow: "hidden",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{team.created_at ? new Date(team.created_at).toLocaleDateString() : "N/A"}
|
|
||||||
</TableCell>
|
|
||||||
<TableCell
|
|
||||||
style={{
|
|
||||||
maxWidth: "4px",
|
|
||||||
whiteSpace: "pre-wrap",
|
|
||||||
overflow: "hidden",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{formatNumberWithCommas(team["spend"], 4)}
|
|
||||||
</TableCell>
|
|
||||||
<TableCell
|
|
||||||
style={{
|
|
||||||
maxWidth: "4px",
|
|
||||||
whiteSpace: "pre-wrap",
|
|
||||||
overflow: "hidden",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{team["max_budget"] !== null && team["max_budget"] !== undefined ? team["max_budget"] : "No limit"}
|
|
||||||
</TableCell>
|
|
||||||
<ModelsCell team={team} />
|
|
||||||
<TableCell>{team.organization_id}</TableCell>
|
|
||||||
<YourRoleCell team={team} userId={userId} />
|
|
||||||
<TableCell>
|
|
||||||
<Text>
|
|
||||||
{perTeamInfo &&
|
|
||||||
team.team_id &&
|
|
||||||
perTeamInfo[team.team_id] &&
|
|
||||||
perTeamInfo[team.team_id].keys &&
|
|
||||||
perTeamInfo[team.team_id].keys.length}{" "}
|
|
||||||
Keys
|
|
||||||
</Text>
|
|
||||||
<Text>
|
|
||||||
{perTeamInfo &&
|
|
||||||
team.team_id &&
|
|
||||||
perTeamInfo[team.team_id] &&
|
|
||||||
perTeamInfo[team.team_id].team_info &&
|
|
||||||
perTeamInfo[team.team_id].team_info.members_with_roles &&
|
|
||||||
perTeamInfo[team.team_id].team_info.members_with_roles.length}{" "}
|
|
||||||
Members
|
|
||||||
</Text>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell>
|
|
||||||
{userRole == "Admin" ? (
|
|
||||||
<>
|
|
||||||
<Icon
|
|
||||||
icon={PencilAltIcon}
|
|
||||||
size="sm"
|
|
||||||
onClick={() => {
|
|
||||||
setSelectedTeamId(team.team_id);
|
|
||||||
setEditTeam(true);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Icon onClick={() => onDeleteTeam(team.team_id)} icon={TrashIcon} size="sm" />
|
|
||||||
</>
|
|
||||||
) : null}
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
))
|
|
||||||
: null}
|
|
||||||
</TableBody>
|
|
||||||
</Table>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default TeamsTable;
|
|
||||||
@ -1,38 +0,0 @@
|
|||||||
import React from "react";
|
|
||||||
import { describe, it, expect } from "vitest";
|
|
||||||
import { render, screen } from "@testing-library/react";
|
|
||||||
import "@testing-library/jest-dom";
|
|
||||||
import TeamRoleBadge from "./TeamRoleBadge";
|
|
||||||
|
|
||||||
const renderBadge = (role: string | null) => render(<div data-testid="wrap">{TeamRoleBadge(role)}</div>);
|
|
||||||
|
|
||||||
describe("TeamRoleBadge", () => {
|
|
||||||
it("renders admin badge with correct label, base classes, styles, and an icon", () => {
|
|
||||||
renderBadge("admin");
|
|
||||||
const label = screen.getByText("Admin");
|
|
||||||
const badge = label.closest("span")!;
|
|
||||||
expect(badge).toHaveClass("inline-flex", "items-center", "border", "text-xs", "font-medium");
|
|
||||||
expect(badge).toHaveStyle({
|
|
||||||
backgroundColor: "#EEF2FF",
|
|
||||||
color: "#3730A3",
|
|
||||||
borderColor: "#C7D2FE",
|
|
||||||
});
|
|
||||||
expect(badge.querySelector("svg")).toBeInTheDocument(); // ShieldIcon renders as an SVG
|
|
||||||
});
|
|
||||||
|
|
||||||
it.each<[string | null]>([["user"], [null], ["viewer" as unknown as string]])(
|
|
||||||
"renders member badge for non-admin role (%p) with correct styles",
|
|
||||||
(role) => {
|
|
||||||
renderBadge(role);
|
|
||||||
const label = screen.getByText("Member");
|
|
||||||
const badge = label.closest("span")!;
|
|
||||||
expect(badge).toHaveClass("inline-flex", "items-center", "border", "text-xs", "font-medium");
|
|
||||||
expect(badge).toHaveStyle({
|
|
||||||
backgroundColor: "#F3F4F6",
|
|
||||||
color: "#4B5563",
|
|
||||||
borderColor: "#E5E7EB",
|
|
||||||
});
|
|
||||||
expect(badge.querySelector("svg")).toBeInTheDocument(); // UserIcon renders as an SVG
|
|
||||||
},
|
|
||||||
);
|
|
||||||
});
|
|
||||||
@ -1,47 +0,0 @@
|
|||||||
import { ShieldIcon, UserIcon } from "lucide-react";
|
|
||||||
|
|
||||||
const MEMBER_BADGE_BG = "#F3F4F6"; // gray-100
|
|
||||||
const MEMBER_BADGE_TEXT = "#4B5563"; // gray-600
|
|
||||||
const MEMBER_BADGE_BORDER = "#E5E7EB"; // gray-200
|
|
||||||
|
|
||||||
const ADMIN_BADGE_BG = "#EEF2FF"; // indigo-50
|
|
||||||
const ADMIN_BADGE_TEXT = "#3730A3"; // indigo-800
|
|
||||||
const ADMIN_BADGE_BORDER = "#C7D2FE"; // indigo-200
|
|
||||||
|
|
||||||
const TeamRoleBadge = (role: string | null) => {
|
|
||||||
const base = "inline-flex items-center px-2.5 py-0.5 rounded-md text-xs font-medium border";
|
|
||||||
|
|
||||||
switch (role) {
|
|
||||||
case "admin":
|
|
||||||
return (
|
|
||||||
<span
|
|
||||||
className={base}
|
|
||||||
style={{
|
|
||||||
backgroundColor: ADMIN_BADGE_BG,
|
|
||||||
color: ADMIN_BADGE_TEXT,
|
|
||||||
borderColor: ADMIN_BADGE_BORDER,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<ShieldIcon className="h-3 w-3 mr-1" />
|
|
||||||
Admin
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
case "user":
|
|
||||||
default:
|
|
||||||
return (
|
|
||||||
<span
|
|
||||||
className={base}
|
|
||||||
style={{
|
|
||||||
backgroundColor: MEMBER_BADGE_BG,
|
|
||||||
color: MEMBER_BADGE_TEXT,
|
|
||||||
borderColor: MEMBER_BADGE_BORDER,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<UserIcon className="h-3 w-3 mr-1" />
|
|
||||||
Member
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export default TeamRoleBadge;
|
|
||||||
@ -1,43 +0,0 @@
|
|||||||
import React from "react";
|
|
||||||
import { describe, it, expect, vi } from "vitest";
|
|
||||||
import { render, screen } from "@testing-library/react";
|
|
||||||
import "@testing-library/jest-dom";
|
|
||||||
import type { Team } from "@/components/key_team_helpers/key_list";
|
|
||||||
import YourRoleCell from "./YourRoleCell";
|
|
||||||
|
|
||||||
// Lightweight mocks for stable, focused tests
|
|
||||||
vi.mock("@tremor/react", () => ({
|
|
||||||
TableCell: ({ children }: { children: React.ReactNode }) => <div data-testid="cell">{children}</div>,
|
|
||||||
}));
|
|
||||||
|
|
||||||
// The component invokes TeamRoleBadge as a function, so mock it as such
|
|
||||||
vi.mock("@/app/(dashboard)/teams/components/TeamsTable/YourRoleCell/TeamRoleBadge", () => ({
|
|
||||||
__esModule: true,
|
|
||||||
default: (role: string | null) => <span data-testid="badge">{role === "admin" ? "Admin" : "Member"}</span>,
|
|
||||||
}));
|
|
||||||
|
|
||||||
const team = (members?: Array<{ user_id: string; role: "admin" | "user" }>): Team =>
|
|
||||||
({ members_with_roles: members }) as unknown as Team;
|
|
||||||
|
|
||||||
describe("YourRoleCell", () => {
|
|
||||||
it("renders Admin when the user is an admin of the team", () => {
|
|
||||||
render(<YourRoleCell team={team([{ user_id: "u1", role: "admin" }])} userId="u1" />);
|
|
||||||
expect(screen.getByTestId("cell")).toBeInTheDocument();
|
|
||||||
expect(screen.getByTestId("badge")).toHaveTextContent("Admin");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("renders Member when the user is a regular member", () => {
|
|
||||||
render(<YourRoleCell team={team([{ user_id: "u2", role: "user" }])} userId="u2" />);
|
|
||||||
expect(screen.getByTestId("badge")).toHaveTextContent("Member");
|
|
||||||
});
|
|
||||||
|
|
||||||
it.each<[string, Team, string | null]>([
|
|
||||||
["userId is null", team([{ user_id: "u3", role: "admin" }]), null],
|
|
||||||
["user not in team", team([{ user_id: "x", role: "user" }]), "y"],
|
|
||||||
["team has no members", team([]), "u4"],
|
|
||||||
["members field undefined", team(undefined), "u5"],
|
|
||||||
])("falls back to Member when no role can be determined (%s)", (_label, t, uid) => {
|
|
||||||
render(<YourRoleCell team={t} userId={uid} />);
|
|
||||||
expect(screen.getByTestId("badge")).toHaveTextContent("Member");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@ -1,22 +0,0 @@
|
|||||||
import { TableCell } from "@tremor/react";
|
|
||||||
import { Team } from "@/components/key_team_helpers/key_list";
|
|
||||||
import TeamRoleBadge from "@/app/(dashboard)/teams/components/TeamsTable/YourRoleCell/TeamRoleBadge";
|
|
||||||
|
|
||||||
interface YourRoleCellProps {
|
|
||||||
team: Team;
|
|
||||||
userId: string | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const getUserRole = (team: Team, userId: string | null): string | null => {
|
|
||||||
if (!userId) return null;
|
|
||||||
const member = team.members_with_roles?.find((m) => m.user_id === userId);
|
|
||||||
return member?.role ?? null;
|
|
||||||
};
|
|
||||||
|
|
||||||
const YourRoleCell = ({ team, userId }: YourRoleCellProps) => {
|
|
||||||
const roleBadge = TeamRoleBadge(getUserRole(team, userId));
|
|
||||||
|
|
||||||
return <TableCell>{roleBadge}</TableCell>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default YourRoleCell;
|
|
||||||
@ -1,822 +0,0 @@
|
|||||||
import { Button as Button2, Form, Input, Modal, Select as Select2, Switch, Tooltip } from "antd";
|
|
||||||
import { Accordion, AccordionBody, AccordionHeader, Text, TextInput } from "@tremor/react";
|
|
||||||
import { InfoCircleOutlined } from "@ant-design/icons";
|
|
||||||
import {
|
|
||||||
fetchAvailableModelsForTeamOrKey,
|
|
||||||
getModelDisplayName,
|
|
||||||
unfurlWildcardModelsInList,
|
|
||||||
} from "@/components/key_team_helpers/fetch_available_models_team_key";
|
|
||||||
import NumericalInput from "@/components/shared/numerical_input";
|
|
||||||
import VectorStoreSelector from "@/components/vector_store_management/VectorStoreSelector";
|
|
||||||
import MCPServerSelector from "@/components/mcp_server_management/MCPServerSelector";
|
|
||||||
import AgentSelector from "@/components/agent_management/AgentSelector";
|
|
||||||
import PremiumLoggingSettings from "@/components/common_components/PremiumLoggingSettings";
|
|
||||||
import ModelAliasManager from "@/components/common_components/ModelAliasManager";
|
|
||||||
import React, { useEffect, useState } from "react";
|
|
||||||
import { useQueryClient } from "@tanstack/react-query";
|
|
||||||
import NotificationsManager from "@/components/molecules/notifications_manager";
|
|
||||||
import {
|
|
||||||
fetchMCPAccessGroups,
|
|
||||||
getGuardrailsList,
|
|
||||||
getPoliciesList,
|
|
||||||
Organization,
|
|
||||||
Team,
|
|
||||||
teamCreateCall,
|
|
||||||
} from "@/components/networking";
|
|
||||||
import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized";
|
|
||||||
import { organizationKeys } from "@/app/(dashboard)/hooks/organizations/useOrganizations";
|
|
||||||
import MCPToolPermissions from "@/components/mcp_server_management/MCPToolPermissions";
|
|
||||||
import SearchToolSelector from "@/components/SearchTools/SearchToolSelector";
|
|
||||||
|
|
||||||
interface ModelAliases {
|
|
||||||
[key: string]: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface CreateTeamModalProps {
|
|
||||||
isTeamModalVisible: boolean;
|
|
||||||
handleOk: () => void;
|
|
||||||
handleCancel: () => void;
|
|
||||||
currentOrg: Organization | null;
|
|
||||||
organizations: Organization[] | null;
|
|
||||||
teams: Team[] | null;
|
|
||||||
setTeams: (teams: Team[] | null) => void;
|
|
||||||
modelAliases: ModelAliases;
|
|
||||||
setModelAliases: (modelAliases: ModelAliases) => void;
|
|
||||||
loggingSettings: any[];
|
|
||||||
setLoggingSettings: (loggingSettings: any[]) => void;
|
|
||||||
setIsTeamModalVisible: (isTeamModalVisible: boolean) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
const getOrganizationModels = (organization: Organization | null, userModels: string[]) => {
|
|
||||||
let tempModelsToPick = [];
|
|
||||||
|
|
||||||
if (organization) {
|
|
||||||
if (organization.models.length > 0) {
|
|
||||||
console.log(`organization.models: ${organization.models}`);
|
|
||||||
tempModelsToPick = organization.models;
|
|
||||||
} else {
|
|
||||||
// show all available models if the team has no models set
|
|
||||||
tempModelsToPick = userModels;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// no team set, show all available models
|
|
||||||
tempModelsToPick = userModels;
|
|
||||||
}
|
|
||||||
|
|
||||||
return unfurlWildcardModelsInList(tempModelsToPick, userModels);
|
|
||||||
};
|
|
||||||
|
|
||||||
const CreateTeamModal = ({
|
|
||||||
isTeamModalVisible,
|
|
||||||
handleOk,
|
|
||||||
handleCancel,
|
|
||||||
currentOrg,
|
|
||||||
organizations,
|
|
||||||
teams,
|
|
||||||
setTeams,
|
|
||||||
modelAliases,
|
|
||||||
setModelAliases,
|
|
||||||
loggingSettings,
|
|
||||||
setLoggingSettings,
|
|
||||||
setIsTeamModalVisible,
|
|
||||||
}: CreateTeamModalProps) => {
|
|
||||||
const { userId: userID, userRole, accessToken, premiumUser } = useAuthorized();
|
|
||||||
const queryClient = useQueryClient();
|
|
||||||
const [form] = Form.useForm();
|
|
||||||
const [userModels, setUserModels] = useState<string[]>([]);
|
|
||||||
const [currentOrgForCreateTeam, setCurrentOrgForCreateTeam] = useState<Organization | null>(null);
|
|
||||||
const [modelsToPick, setModelsToPick] = useState<string[]>([]);
|
|
||||||
const [guardrailsList, setGuardrailsList] = useState<string[]>([]);
|
|
||||||
const [policiesList, setPoliciesList] = useState<string[]>([]);
|
|
||||||
const [mcpAccessGroups, setMcpAccessGroups] = useState<string[]>([]);
|
|
||||||
const [mcpAccessGroupsLoaded, setMcpAccessGroupsLoaded] = useState(false);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const fetchUserModels = async () => {
|
|
||||||
try {
|
|
||||||
if (userID === null || userRole === null || accessToken === null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const models = await fetchAvailableModelsForTeamOrKey(userID, userRole, accessToken);
|
|
||||||
if (models) {
|
|
||||||
setUserModels(models);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error fetching user models:", error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
fetchUserModels();
|
|
||||||
}, [accessToken, userID, userRole, teams]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
console.log(`currentOrgForCreateTeam: ${currentOrgForCreateTeam}`);
|
|
||||||
const models = getOrganizationModels(currentOrgForCreateTeam, userModels);
|
|
||||||
console.log(`models: ${models}`);
|
|
||||||
setModelsToPick(models);
|
|
||||||
form.setFieldValue("models", []);
|
|
||||||
}, [currentOrgForCreateTeam, userModels, form]);
|
|
||||||
|
|
||||||
const fetchMcpAccessGroups = async () => {
|
|
||||||
try {
|
|
||||||
if (accessToken == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const groups = await fetchMCPAccessGroups(accessToken);
|
|
||||||
setMcpAccessGroups(groups);
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Failed to fetch MCP access groups:", error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
fetchMcpAccessGroups();
|
|
||||||
}, [accessToken, fetchMcpAccessGroups]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const fetchGuardrails = async () => {
|
|
||||||
try {
|
|
||||||
if (accessToken == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const response = await getGuardrailsList(accessToken);
|
|
||||||
const guardrailNames = response.guardrails.map((g: { guardrail_name: string }) => g.guardrail_name);
|
|
||||||
setGuardrailsList(guardrailNames);
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Failed to fetch guardrails:", error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const fetchPolicies = async () => {
|
|
||||||
try {
|
|
||||||
if (accessToken == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const response = await getPoliciesList(accessToken);
|
|
||||||
const policyNames = response.policies.map((p: { policy_name: string }) => p.policy_name);
|
|
||||||
setPoliciesList(policyNames);
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Failed to fetch policies:", error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
fetchGuardrails();
|
|
||||||
fetchPolicies();
|
|
||||||
}, [accessToken]);
|
|
||||||
|
|
||||||
const handleCreate = async (formValues: Record<string, any>) => {
|
|
||||||
try {
|
|
||||||
console.log(`formValues: ${JSON.stringify(formValues)}`);
|
|
||||||
if (accessToken != null) {
|
|
||||||
const newTeamAlias = formValues?.team_alias;
|
|
||||||
const existingTeamAliases = teams?.map((t) => t.team_alias) ?? [];
|
|
||||||
let organizationId = formValues?.organization_id || currentOrg?.organization_id;
|
|
||||||
if (organizationId === "" || typeof organizationId !== "string") {
|
|
||||||
formValues.organization_id = null;
|
|
||||||
} else {
|
|
||||||
formValues.organization_id = organizationId.trim();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove guardrails from top level since it's now in metadata
|
|
||||||
if (existingTeamAliases.includes(newTeamAlias)) {
|
|
||||||
throw new Error(`Team alias ${newTeamAlias} already exists, please pick another alias`);
|
|
||||||
}
|
|
||||||
|
|
||||||
NotificationsManager.info("Creating Team");
|
|
||||||
|
|
||||||
// Handle logging settings in metadata
|
|
||||||
if (loggingSettings.length > 0) {
|
|
||||||
let metadata = {};
|
|
||||||
if (formValues.metadata) {
|
|
||||||
try {
|
|
||||||
metadata = JSON.parse(formValues.metadata);
|
|
||||||
} catch (e) {
|
|
||||||
console.warn("Invalid JSON in metadata field, starting with empty object");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add logging settings to metadata
|
|
||||||
metadata = {
|
|
||||||
...metadata,
|
|
||||||
logging: loggingSettings.filter((config) => config.callback_name), // Only include configs with callback_name
|
|
||||||
};
|
|
||||||
|
|
||||||
formValues.metadata = JSON.stringify(metadata);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (formValues.secret_manager_settings) {
|
|
||||||
if (typeof formValues.secret_manager_settings === "string") {
|
|
||||||
if (formValues.secret_manager_settings.trim() === "") {
|
|
||||||
delete formValues.secret_manager_settings;
|
|
||||||
} else {
|
|
||||||
try {
|
|
||||||
formValues.secret_manager_settings = JSON.parse(formValues.secret_manager_settings);
|
|
||||||
} catch (e) {
|
|
||||||
throw new Error("Failed to parse secret manager settings: " + e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Transform integrations into object_permission (vector stores, MCP, agents, search tools)
|
|
||||||
const hasAgents =
|
|
||||||
formValues.allowed_agents_and_groups &&
|
|
||||||
((formValues.allowed_agents_and_groups.agents?.length ?? 0) > 0 ||
|
|
||||||
(formValues.allowed_agents_and_groups.accessGroups?.length ?? 0) > 0);
|
|
||||||
const hasSearchTools =
|
|
||||||
Array.isArray(formValues.object_permission_search_tools) &&
|
|
||||||
formValues.object_permission_search_tools.length > 0;
|
|
||||||
|
|
||||||
if (
|
|
||||||
(formValues.allowed_vector_store_ids && formValues.allowed_vector_store_ids.length > 0) ||
|
|
||||||
(formValues.allowed_mcp_servers_and_groups &&
|
|
||||||
(formValues.allowed_mcp_servers_and_groups.servers?.length > 0 ||
|
|
||||||
formValues.allowed_mcp_servers_and_groups.accessGroups?.length > 0 ||
|
|
||||||
formValues.allowed_mcp_servers_and_groups.toolPermissions)) ||
|
|
||||||
hasAgents ||
|
|
||||||
hasSearchTools
|
|
||||||
) {
|
|
||||||
if (!formValues.object_permission) {
|
|
||||||
formValues.object_permission = {};
|
|
||||||
}
|
|
||||||
if (formValues.allowed_vector_store_ids && formValues.allowed_vector_store_ids.length > 0) {
|
|
||||||
formValues.object_permission.vector_stores = formValues.allowed_vector_store_ids;
|
|
||||||
delete formValues.allowed_vector_store_ids;
|
|
||||||
}
|
|
||||||
if (formValues.allowed_mcp_servers_and_groups) {
|
|
||||||
const { servers, accessGroups } = formValues.allowed_mcp_servers_and_groups;
|
|
||||||
if (servers && servers.length > 0) {
|
|
||||||
formValues.object_permission.mcp_servers = servers;
|
|
||||||
}
|
|
||||||
if (accessGroups && accessGroups.length > 0) {
|
|
||||||
formValues.object_permission.mcp_access_groups = accessGroups;
|
|
||||||
}
|
|
||||||
delete formValues.allowed_mcp_servers_and_groups;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add tool permissions separately
|
|
||||||
if (formValues.mcp_tool_permissions && Object.keys(formValues.mcp_tool_permissions).length > 0) {
|
|
||||||
formValues.object_permission.mcp_tool_permissions = formValues.mcp_tool_permissions;
|
|
||||||
delete formValues.mcp_tool_permissions;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle agent permissions
|
|
||||||
if (formValues.allowed_agents_and_groups) {
|
|
||||||
const { agents, accessGroups } = formValues.allowed_agents_and_groups;
|
|
||||||
if (agents && agents.length > 0) {
|
|
||||||
formValues.object_permission.agents = agents;
|
|
||||||
}
|
|
||||||
if (accessGroups && accessGroups.length > 0) {
|
|
||||||
formValues.object_permission.agent_access_groups = accessGroups;
|
|
||||||
}
|
|
||||||
delete formValues.allowed_agents_and_groups;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (hasSearchTools) {
|
|
||||||
formValues.object_permission.search_tools = formValues.object_permission_search_tools;
|
|
||||||
delete formValues.object_permission_search_tools;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Transform allowed_mcp_access_groups into object_permission
|
|
||||||
if (formValues.allowed_mcp_access_groups && formValues.allowed_mcp_access_groups.length > 0) {
|
|
||||||
if (!formValues.object_permission) {
|
|
||||||
formValues.object_permission = {};
|
|
||||||
}
|
|
||||||
formValues.object_permission.mcp_access_groups = formValues.allowed_mcp_access_groups;
|
|
||||||
delete formValues.allowed_mcp_access_groups;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add model_aliases if any are defined
|
|
||||||
if (Object.keys(modelAliases).length > 0) {
|
|
||||||
formValues.model_aliases = modelAliases;
|
|
||||||
}
|
|
||||||
|
|
||||||
const response: any = await teamCreateCall(accessToken, formValues);
|
|
||||||
queryClient.invalidateQueries({ queryKey: organizationKeys.all });
|
|
||||||
if (teams !== null) {
|
|
||||||
setTeams([...teams, response]);
|
|
||||||
} else {
|
|
||||||
setTeams([response]);
|
|
||||||
}
|
|
||||||
console.log(`response for team create call: ${response}`);
|
|
||||||
NotificationsManager.success("Team created");
|
|
||||||
form.resetFields();
|
|
||||||
setLoggingSettings([]);
|
|
||||||
setModelAliases({});
|
|
||||||
setIsTeamModalVisible(false);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error creating the team:", error);
|
|
||||||
NotificationsManager.fromBackend("Error creating the team: " + error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Modal
|
|
||||||
title="Create Team"
|
|
||||||
open={isTeamModalVisible}
|
|
||||||
width={1000}
|
|
||||||
footer={null}
|
|
||||||
onOk={handleOk}
|
|
||||||
onCancel={handleCancel}
|
|
||||||
>
|
|
||||||
<Form form={form} onFinish={handleCreate} labelCol={{ span: 8 }} wrapperCol={{ span: 16 }} labelAlign="left">
|
|
||||||
<>
|
|
||||||
<Form.Item
|
|
||||||
label="Team Name"
|
|
||||||
name="team_alias"
|
|
||||||
rules={[
|
|
||||||
{
|
|
||||||
required: true,
|
|
||||||
message: "Please input a team name",
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<TextInput placeholder="" data-testid="team-name-input" />
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item
|
|
||||||
label={
|
|
||||||
<span>
|
|
||||||
Organization{" "}
|
|
||||||
<Tooltip
|
|
||||||
title={
|
|
||||||
<span>
|
|
||||||
Organizations can have multiple teams. Learn more about{" "}
|
|
||||||
<a
|
|
||||||
href="https://docs.litellm.ai/docs/proxy/user_management_heirarchy"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
style={{
|
|
||||||
color: "#1890ff",
|
|
||||||
textDecoration: "underline",
|
|
||||||
}}
|
|
||||||
onClick={(e) => e.stopPropagation()}
|
|
||||||
>
|
|
||||||
user management hierarchy
|
|
||||||
</a>
|
|
||||||
</span>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<InfoCircleOutlined style={{ marginLeft: "4px" }} />
|
|
||||||
</Tooltip>
|
|
||||||
</span>
|
|
||||||
}
|
|
||||||
name="organization_id"
|
|
||||||
initialValue={currentOrg ? currentOrg.organization_id : null}
|
|
||||||
className="mt-8"
|
|
||||||
>
|
|
||||||
<Select2
|
|
||||||
showSearch
|
|
||||||
allowClear
|
|
||||||
placeholder="Search or select an Organization"
|
|
||||||
onChange={(value) => {
|
|
||||||
form.setFieldValue("organization_id", value);
|
|
||||||
setCurrentOrgForCreateTeam(organizations?.find((org) => org.organization_id === value) || null);
|
|
||||||
}}
|
|
||||||
filterOption={(input, option) => {
|
|
||||||
if (!option) return false;
|
|
||||||
const optionValue = option.children?.toString() || "";
|
|
||||||
return optionValue.toLowerCase().includes(input.toLowerCase());
|
|
||||||
}}
|
|
||||||
optionFilterProp="children"
|
|
||||||
>
|
|
||||||
{organizations?.map((org) => (
|
|
||||||
<Select2.Option key={org.organization_id} value={org.organization_id}>
|
|
||||||
<span className="font-medium">{org.organization_alias}</span>{" "}
|
|
||||||
<span className="text-gray-500">({org.organization_id})</span>
|
|
||||||
</Select2.Option>
|
|
||||||
))}
|
|
||||||
</Select2>
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item
|
|
||||||
label={
|
|
||||||
<span>
|
|
||||||
Models{" "}
|
|
||||||
<Tooltip title="These are the models that your selected team has access to">
|
|
||||||
<InfoCircleOutlined style={{ marginLeft: "4px" }} />
|
|
||||||
</Tooltip>
|
|
||||||
</span>
|
|
||||||
}
|
|
||||||
name="models"
|
|
||||||
>
|
|
||||||
<Select2 mode="multiple" placeholder="Select models" style={{ width: "100%" }} data-testid="team-models-select">
|
|
||||||
<Select2.Option key="all-proxy-models" value="all-proxy-models">
|
|
||||||
All Proxy Models
|
|
||||||
</Select2.Option>
|
|
||||||
{modelsToPick.map((model) => (
|
|
||||||
<Select2.Option key={model} value={model}>
|
|
||||||
{getModelDisplayName(model)}
|
|
||||||
</Select2.Option>
|
|
||||||
))}
|
|
||||||
</Select2>
|
|
||||||
</Form.Item>
|
|
||||||
|
|
||||||
<Accordion className="mt-8 mb-8">
|
|
||||||
<AccordionHeader>
|
|
||||||
<b>Team Member Settings</b>
|
|
||||||
</AccordionHeader>
|
|
||||||
<AccordionBody>
|
|
||||||
<Text className="text-xs text-gray-500 mb-4">
|
|
||||||
Optional defaults applied when members join this team. All fields can be overridden per member.
|
|
||||||
</Text>
|
|
||||||
<Form.Item
|
|
||||||
noStyle
|
|
||||||
shouldUpdate={(prev, cur) => prev.models !== cur.models}
|
|
||||||
>
|
|
||||||
{({ getFieldValue }) => {
|
|
||||||
const teamModels: string[] = getFieldValue("models") || [];
|
|
||||||
const opts = teamModels.length > 0 ? teamModels : modelsToPick;
|
|
||||||
return (
|
|
||||||
<Form.Item
|
|
||||||
label={
|
|
||||||
<span>
|
|
||||||
Default Model Access{" "}
|
|
||||||
<Tooltip title="Optional. If set, new members can only access these models by default. Must be a subset of the team's models. Leave empty to give all members access to all team models.">
|
|
||||||
<InfoCircleOutlined style={{ marginLeft: "4px" }} />
|
|
||||||
</Tooltip>
|
|
||||||
</span>
|
|
||||||
}
|
|
||||||
name="default_team_member_models"
|
|
||||||
>
|
|
||||||
<Select2
|
|
||||||
mode="multiple"
|
|
||||||
placeholder="Leave empty — all team models accessible to every member"
|
|
||||||
style={{ width: "100%" }}
|
|
||||||
>
|
|
||||||
{opts.map((m) => (
|
|
||||||
<Select2.Option key={m} value={m}>
|
|
||||||
{getModelDisplayName(m)}
|
|
||||||
</Select2.Option>
|
|
||||||
))}
|
|
||||||
</Select2>
|
|
||||||
</Form.Item>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item
|
|
||||||
label="Default Member Budget (USD)"
|
|
||||||
name="team_member_budget"
|
|
||||||
normalize={(value) => (value ? Number(value) : undefined)}
|
|
||||||
tooltip="Default spend budget for each member in this team."
|
|
||||||
>
|
|
||||||
<NumericalInput step={0.01} precision={2} width={200} />
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item
|
|
||||||
label="Default Key Duration (eg: 1d, 1mo)"
|
|
||||||
name="team_member_key_duration"
|
|
||||||
tooltip="Set a limit to the duration of a team member's key. Format: 30s (seconds), 30m (minutes), 30h (hours), 30d (days), 1mo (month)"
|
|
||||||
>
|
|
||||||
<TextInput placeholder="e.g., 30d" />
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item
|
|
||||||
label="Default RPM Limit"
|
|
||||||
name="team_member_rpm_limit"
|
|
||||||
tooltip="Default requests per minute limit for each member. Can be overridden per member."
|
|
||||||
>
|
|
||||||
<NumericalInput step={1} width={400} />
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item
|
|
||||||
label="Default TPM Limit"
|
|
||||||
name="team_member_tpm_limit"
|
|
||||||
tooltip="Default tokens per minute limit for each member. Can be overridden per member."
|
|
||||||
>
|
|
||||||
<NumericalInput step={1} width={400} />
|
|
||||||
</Form.Item>
|
|
||||||
</AccordionBody>
|
|
||||||
</Accordion>
|
|
||||||
|
|
||||||
<Form.Item label="Max Budget (USD)" name="max_budget">
|
|
||||||
<NumericalInput step={0.01} precision={2} width={200} />
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item className="mt-8" label="Reset Budget" name="budget_duration">
|
|
||||||
<Select2 defaultValue={null} placeholder="n/a">
|
|
||||||
<Select2.Option value="24h">daily</Select2.Option>
|
|
||||||
<Select2.Option value="7d">weekly</Select2.Option>
|
|
||||||
<Select2.Option value="30d">monthly</Select2.Option>
|
|
||||||
</Select2>
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item label="Tokens per minute Limit (TPM)" name="tpm_limit">
|
|
||||||
<NumericalInput step={1} width={400} />
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item label="Requests per minute Limit (RPM)" name="rpm_limit">
|
|
||||||
<NumericalInput step={1} width={400} />
|
|
||||||
</Form.Item>
|
|
||||||
|
|
||||||
<Accordion
|
|
||||||
className="mt-8 mb-8"
|
|
||||||
onClick={() => {
|
|
||||||
if (!mcpAccessGroupsLoaded) {
|
|
||||||
fetchMcpAccessGroups();
|
|
||||||
setMcpAccessGroupsLoaded(true);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<AccordionHeader>
|
|
||||||
<b>Additional Settings</b>
|
|
||||||
</AccordionHeader>
|
|
||||||
<AccordionBody>
|
|
||||||
<Form.Item
|
|
||||||
label="Team ID"
|
|
||||||
name="team_id"
|
|
||||||
help="ID of the team you want to create. If not provided, it will be generated automatically."
|
|
||||||
>
|
|
||||||
<TextInput
|
|
||||||
onChange={(e) => {
|
|
||||||
e.target.value = e.target.value.trim();
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item
|
|
||||||
label="Metadata"
|
|
||||||
name="metadata"
|
|
||||||
help="Additional team metadata. Enter metadata as JSON object."
|
|
||||||
>
|
|
||||||
<Input.TextArea rows={4} />
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item
|
|
||||||
label="Secret Manager Settings"
|
|
||||||
name="secret_manager_settings"
|
|
||||||
help={
|
|
||||||
premiumUser
|
|
||||||
? "Enter secret manager configuration as a JSON object."
|
|
||||||
: "Premium feature - Upgrade to manage secret manager settings."
|
|
||||||
}
|
|
||||||
rules={[
|
|
||||||
{
|
|
||||||
validator: async (_, value) => {
|
|
||||||
if (!value) {
|
|
||||||
return Promise.resolve();
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
JSON.parse(value);
|
|
||||||
return Promise.resolve();
|
|
||||||
} catch (error) {
|
|
||||||
return Promise.reject(new Error("Please enter valid JSON"));
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<Input.TextArea
|
|
||||||
rows={4}
|
|
||||||
placeholder='{"namespace": "admin", "mount": "secret", "path_prefix": "litellm"}'
|
|
||||||
disabled={!premiumUser}
|
|
||||||
/>
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item
|
|
||||||
label={
|
|
||||||
<span>
|
|
||||||
Guardrails{" "}
|
|
||||||
<Tooltip title="Setup your first guardrail">
|
|
||||||
<a
|
|
||||||
href="https://docs.litellm.ai/docs/proxy/guardrails/quick_start"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
onClick={(e) => e.stopPropagation()}
|
|
||||||
>
|
|
||||||
<InfoCircleOutlined style={{ marginLeft: "4px" }} />
|
|
||||||
</a>
|
|
||||||
</Tooltip>
|
|
||||||
</span>
|
|
||||||
}
|
|
||||||
name="guardrails"
|
|
||||||
className="mt-8"
|
|
||||||
help="Select existing guardrails or enter new ones"
|
|
||||||
>
|
|
||||||
<Select2
|
|
||||||
mode="tags"
|
|
||||||
style={{ width: "100%" }}
|
|
||||||
placeholder="Select or enter guardrails"
|
|
||||||
options={guardrailsList.map((name) => ({
|
|
||||||
value: name,
|
|
||||||
label: name,
|
|
||||||
}))}
|
|
||||||
/>
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item
|
|
||||||
label={
|
|
||||||
<span>
|
|
||||||
Disable Global Guardrails{" "}
|
|
||||||
<Tooltip title="When enabled, this team will bypass any guardrails configured to run on every request (global guardrails)">
|
|
||||||
<InfoCircleOutlined style={{ marginLeft: "4px" }} />
|
|
||||||
</Tooltip>
|
|
||||||
</span>
|
|
||||||
}
|
|
||||||
name="disable_global_guardrails"
|
|
||||||
className="mt-4"
|
|
||||||
valuePropName="checked"
|
|
||||||
help="Bypass global guardrails for this team"
|
|
||||||
>
|
|
||||||
<Switch
|
|
||||||
checkedChildren="Yes"
|
|
||||||
unCheckedChildren="No"
|
|
||||||
/>
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item
|
|
||||||
label={
|
|
||||||
<span>
|
|
||||||
Policies{" "}
|
|
||||||
<Tooltip title="Apply policies to this team to control guardrails and other settings">
|
|
||||||
<a
|
|
||||||
href="https://docs.litellm.ai/docs/proxy/guardrails/guardrail_policies"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
onClick={(e) => e.stopPropagation()}
|
|
||||||
>
|
|
||||||
<InfoCircleOutlined style={{ marginLeft: "4px" }} />
|
|
||||||
</a>
|
|
||||||
</Tooltip>
|
|
||||||
</span>
|
|
||||||
}
|
|
||||||
name="policies"
|
|
||||||
className="mt-8"
|
|
||||||
help="Select existing policies or enter new ones"
|
|
||||||
>
|
|
||||||
<Select2
|
|
||||||
mode="tags"
|
|
||||||
style={{ width: "100%" }}
|
|
||||||
placeholder="Select or enter policies"
|
|
||||||
options={policiesList.map((name) => ({
|
|
||||||
value: name,
|
|
||||||
label: name,
|
|
||||||
}))}
|
|
||||||
/>
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item
|
|
||||||
label={
|
|
||||||
<span>
|
|
||||||
Allowed Vector Stores{" "}
|
|
||||||
<Tooltip title="Select which vector stores this team can access by default. Leave empty for access to all vector stores">
|
|
||||||
<InfoCircleOutlined style={{ marginLeft: "4px" }} />
|
|
||||||
</Tooltip>
|
|
||||||
</span>
|
|
||||||
}
|
|
||||||
name="allowed_vector_store_ids"
|
|
||||||
className="mt-8"
|
|
||||||
help="Select vector stores this team can access. Leave empty for access to all vector stores"
|
|
||||||
>
|
|
||||||
<VectorStoreSelector
|
|
||||||
onChange={(values: string[]) => form.setFieldValue("allowed_vector_store_ids", values)}
|
|
||||||
value={form.getFieldValue("allowed_vector_store_ids")}
|
|
||||||
accessToken={accessToken || ""}
|
|
||||||
placeholder="Select vector stores (optional)"
|
|
||||||
/>
|
|
||||||
</Form.Item>
|
|
||||||
</AccordionBody>
|
|
||||||
</Accordion>
|
|
||||||
|
|
||||||
<Accordion className="mt-8 mb-8">
|
|
||||||
<AccordionHeader>
|
|
||||||
<b>MCP Settings</b>
|
|
||||||
</AccordionHeader>
|
|
||||||
<AccordionBody>
|
|
||||||
<Form.Item
|
|
||||||
label={
|
|
||||||
<span>
|
|
||||||
Allowed MCP Servers{" "}
|
|
||||||
<Tooltip title="Select which MCP servers or access groups this team can access">
|
|
||||||
<InfoCircleOutlined style={{ marginLeft: "4px" }} />
|
|
||||||
</Tooltip>
|
|
||||||
</span>
|
|
||||||
}
|
|
||||||
name="allowed_mcp_servers_and_groups"
|
|
||||||
className="mt-4"
|
|
||||||
help="Select MCP servers or access groups this team can access"
|
|
||||||
>
|
|
||||||
<MCPServerSelector
|
|
||||||
onChange={(val: any) => form.setFieldValue("allowed_mcp_servers_and_groups", val)}
|
|
||||||
value={form.getFieldValue("allowed_mcp_servers_and_groups")}
|
|
||||||
accessToken={accessToken || ""}
|
|
||||||
placeholder="Select MCP servers or access groups (optional)"
|
|
||||||
/>
|
|
||||||
</Form.Item>
|
|
||||||
|
|
||||||
{/* Hidden field to register mcp_tool_permissions with the form */}
|
|
||||||
<Form.Item name="mcp_tool_permissions" initialValue={{}} hidden>
|
|
||||||
<Input type="hidden" />
|
|
||||||
</Form.Item>
|
|
||||||
|
|
||||||
<Form.Item
|
|
||||||
noStyle
|
|
||||||
shouldUpdate={(prevValues, currentValues) =>
|
|
||||||
prevValues.allowed_mcp_servers_and_groups !== currentValues.allowed_mcp_servers_and_groups ||
|
|
||||||
prevValues.mcp_tool_permissions !== currentValues.mcp_tool_permissions
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{() => (
|
|
||||||
<div className="mt-6">
|
|
||||||
<MCPToolPermissions
|
|
||||||
accessToken={accessToken || ""}
|
|
||||||
selectedServers={form.getFieldValue("allowed_mcp_servers_and_groups")?.servers || []}
|
|
||||||
toolPermissions={form.getFieldValue("mcp_tool_permissions") || {}}
|
|
||||||
onChange={(toolPerms) => form.setFieldsValue({ mcp_tool_permissions: toolPerms })}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</Form.Item>
|
|
||||||
</AccordionBody>
|
|
||||||
</Accordion>
|
|
||||||
|
|
||||||
<Accordion className="mt-8 mb-8">
|
|
||||||
<AccordionHeader>
|
|
||||||
<b>Agent Settings</b>
|
|
||||||
</AccordionHeader>
|
|
||||||
<AccordionBody>
|
|
||||||
<Form.Item
|
|
||||||
label={
|
|
||||||
<span>
|
|
||||||
Allowed Agents{" "}
|
|
||||||
<Tooltip title="Select which agents or access groups this team can access">
|
|
||||||
<InfoCircleOutlined style={{ marginLeft: "4px" }} />
|
|
||||||
</Tooltip>
|
|
||||||
</span>
|
|
||||||
}
|
|
||||||
name="allowed_agents_and_groups"
|
|
||||||
className="mt-4"
|
|
||||||
help="Select agents or access groups this team can access"
|
|
||||||
>
|
|
||||||
<AgentSelector
|
|
||||||
onChange={(val: any) => form.setFieldValue("allowed_agents_and_groups", val)}
|
|
||||||
value={form.getFieldValue("allowed_agents_and_groups")}
|
|
||||||
accessToken={accessToken || ""}
|
|
||||||
placeholder="Select agents or access groups (optional)"
|
|
||||||
/>
|
|
||||||
</Form.Item>
|
|
||||||
</AccordionBody>
|
|
||||||
</Accordion>
|
|
||||||
|
|
||||||
<Accordion className="mt-8 mb-8">
|
|
||||||
<AccordionHeader>
|
|
||||||
<b>Search Tool Settings</b>
|
|
||||||
</AccordionHeader>
|
|
||||||
<AccordionBody>
|
|
||||||
<Form.Item
|
|
||||||
label={
|
|
||||||
<span>
|
|
||||||
Allowed Search Tools{" "}
|
|
||||||
<Tooltip title="Select which search tools this team can access. Leave empty to allow all search tools.">
|
|
||||||
<InfoCircleOutlined style={{ marginLeft: "4px" }} />
|
|
||||||
</Tooltip>
|
|
||||||
</span>
|
|
||||||
}
|
|
||||||
name="object_permission_search_tools"
|
|
||||||
className="mt-4"
|
|
||||||
help="Restrict which configured search tools keys on this team may call."
|
|
||||||
>
|
|
||||||
<SearchToolSelector
|
|
||||||
onChange={(vals: string[]) => form.setFieldValue("object_permission_search_tools", vals)}
|
|
||||||
value={form.getFieldValue("object_permission_search_tools")}
|
|
||||||
accessToken={accessToken || ""}
|
|
||||||
placeholder="Select search tools (optional, empty = all allowed)"
|
|
||||||
/>
|
|
||||||
</Form.Item>
|
|
||||||
</AccordionBody>
|
|
||||||
</Accordion>
|
|
||||||
|
|
||||||
<Accordion className="mt-8 mb-8">
|
|
||||||
<AccordionHeader>
|
|
||||||
<b>Logging Settings</b>
|
|
||||||
</AccordionHeader>
|
|
||||||
<AccordionBody>
|
|
||||||
<div className="mt-4">
|
|
||||||
<PremiumLoggingSettings
|
|
||||||
value={loggingSettings}
|
|
||||||
onChange={setLoggingSettings}
|
|
||||||
premiumUser={premiumUser}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</AccordionBody>
|
|
||||||
</Accordion>
|
|
||||||
|
|
||||||
<Accordion className="mt-8 mb-8">
|
|
||||||
<AccordionHeader>
|
|
||||||
<b>Model Aliases</b>
|
|
||||||
</AccordionHeader>
|
|
||||||
<AccordionBody>
|
|
||||||
<div className="mt-4">
|
|
||||||
<Text className="text-sm text-gray-600 mb-4">
|
|
||||||
Create custom aliases for models that can be used by team members in API calls. This allows you to
|
|
||||||
create shortcuts for specific models.
|
|
||||||
</Text>
|
|
||||||
<ModelAliasManager
|
|
||||||
accessToken={accessToken || ""}
|
|
||||||
initialModelAliases={modelAliases}
|
|
||||||
onAliasUpdate={setModelAliases}
|
|
||||||
showExampleConfig={false}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</AccordionBody>
|
|
||||||
</Accordion>
|
|
||||||
</>
|
|
||||||
<div style={{ textAlign: "right", marginTop: "10px" }}>
|
|
||||||
<Button2 htmlType="submit" data-testid="create-team-submit">Create Team</Button2>
|
|
||||||
</div>
|
|
||||||
</Form>
|
|
||||||
</Modal>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default CreateTeamModal;
|
|
||||||
@ -1,171 +0,0 @@
|
|||||||
import { render, screen } from "@testing-library/react";
|
|
||||||
import userEvent from "@testing-library/user-event";
|
|
||||||
import React from "react";
|
|
||||||
import { describe, expect, it, vi } from "vitest";
|
|
||||||
import { Team } from "@/components/key_team_helpers/key_list";
|
|
||||||
import DeleteTeamModal from "./DeleteTeamModal";
|
|
||||||
|
|
||||||
const makeTeam = (overrides: Partial<Team> = {}): Team => ({
|
|
||||||
team_id: "team-1",
|
|
||||||
team_alias: "Engineering",
|
|
||||||
models: [],
|
|
||||||
max_budget: null,
|
|
||||||
budget_duration: null,
|
|
||||||
tpm_limit: null,
|
|
||||||
rpm_limit: null,
|
|
||||||
organization_id: "org-1",
|
|
||||||
created_at: "2024-01-01T00:00:00Z",
|
|
||||||
keys: [],
|
|
||||||
members_with_roles: [],
|
|
||||||
spend: 0,
|
|
||||||
...overrides,
|
|
||||||
});
|
|
||||||
|
|
||||||
const renderModal = (props: Partial<Parameters<typeof DeleteTeamModal>[0]> = {}) => {
|
|
||||||
const defaults = {
|
|
||||||
teams: [makeTeam()],
|
|
||||||
teamToDelete: "team-1",
|
|
||||||
onCancel: vi.fn(),
|
|
||||||
onConfirm: vi.fn(),
|
|
||||||
};
|
|
||||||
return render(<DeleteTeamModal {...defaults} {...props} />);
|
|
||||||
};
|
|
||||||
|
|
||||||
describe("DeleteTeamModal", () => {
|
|
||||||
it("should render the title, team name label, and confirmation input", () => {
|
|
||||||
renderModal();
|
|
||||||
|
|
||||||
expect(screen.getByText("Delete Team")).toBeInTheDocument();
|
|
||||||
expect(screen.getByText("Engineering")).toBeInTheDocument();
|
|
||||||
expect(screen.getByPlaceholderText("Enter team name exactly")).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should render Cancel and Force Delete buttons", () => {
|
|
||||||
renderModal();
|
|
||||||
|
|
||||||
expect(screen.getByRole("button", { name: /^cancel$/i })).toBeInTheDocument();
|
|
||||||
expect(screen.getByRole("button", { name: /force delete/i })).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should not show the warning banner when the team has no keys", () => {
|
|
||||||
renderModal({ teams: [makeTeam({ keys: [] })] });
|
|
||||||
|
|
||||||
expect(screen.queryByText(/Warning/i)).not.toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should show a warning with singular 'key' when the team has exactly 1 key", () => {
|
|
||||||
const team = makeTeam({ keys: [{ token: "tok-1" } as any] });
|
|
||||||
renderModal({ teams: [team] });
|
|
||||||
|
|
||||||
expect(screen.getByText(/This team has 1 associated key\./)).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should show a warning with plural 'keys' when the team has multiple keys", () => {
|
|
||||||
const team = makeTeam({
|
|
||||||
keys: [{ token: "tok-1" } as any, { token: "tok-2" } as any, { token: "tok-3" } as any],
|
|
||||||
});
|
|
||||||
renderModal({ teams: [team] });
|
|
||||||
|
|
||||||
expect(screen.getByText(/This team has 3 associated keys\./)).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should note that associated keys will also be deleted in the warning", () => {
|
|
||||||
const team = makeTeam({ keys: [{ token: "tok-1" } as any] });
|
|
||||||
renderModal({ teams: [team] });
|
|
||||||
|
|
||||||
expect(screen.getByText(/Deleting the team will also delete all associated keys/)).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should disable Force Delete when the input is empty", () => {
|
|
||||||
renderModal();
|
|
||||||
|
|
||||||
expect(screen.getByRole("button", { name: /force delete/i })).toBeDisabled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should keep Force Delete disabled when the input does not exactly match the team name", async () => {
|
|
||||||
const user = userEvent.setup();
|
|
||||||
renderModal();
|
|
||||||
|
|
||||||
await user.type(screen.getByPlaceholderText("Enter team name exactly"), "engineer");
|
|
||||||
|
|
||||||
expect(screen.getByRole("button", { name: /force delete/i })).toBeDisabled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should enable Force Delete only after typing the exact team name (case-sensitive)", async () => {
|
|
||||||
const user = userEvent.setup();
|
|
||||||
renderModal();
|
|
||||||
|
|
||||||
const input = screen.getByPlaceholderText("Enter team name exactly");
|
|
||||||
|
|
||||||
await user.type(input, "Engineering");
|
|
||||||
|
|
||||||
expect(screen.getByRole("button", { name: /force delete/i })).toBeEnabled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should call onConfirm when Force Delete is clicked with a valid input", async () => {
|
|
||||||
const user = userEvent.setup();
|
|
||||||
const onConfirm = vi.fn();
|
|
||||||
renderModal({ onConfirm });
|
|
||||||
|
|
||||||
await user.type(screen.getByPlaceholderText("Enter team name exactly"), "Engineering");
|
|
||||||
await user.click(screen.getByRole("button", { name: /force delete/i }));
|
|
||||||
|
|
||||||
expect(onConfirm).toHaveBeenCalledTimes(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should not call onConfirm when Force Delete is clicked with an invalid input", async () => {
|
|
||||||
const user = userEvent.setup();
|
|
||||||
const onConfirm = vi.fn();
|
|
||||||
renderModal({ onConfirm });
|
|
||||||
|
|
||||||
// Button is disabled so click has no effect
|
|
||||||
await user.click(screen.getByRole("button", { name: /force delete/i }));
|
|
||||||
|
|
||||||
expect(onConfirm).not.toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should call onCancel when the Cancel button is clicked", async () => {
|
|
||||||
const user = userEvent.setup();
|
|
||||||
const onCancel = vi.fn();
|
|
||||||
renderModal({ onCancel });
|
|
||||||
|
|
||||||
await user.click(screen.getByRole("button", { name: /^cancel$/i }));
|
|
||||||
|
|
||||||
expect(onCancel).toHaveBeenCalledTimes(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should call onCancel when the Close button is clicked", async () => {
|
|
||||||
const user = userEvent.setup();
|
|
||||||
const onCancel = vi.fn();
|
|
||||||
renderModal({ onCancel });
|
|
||||||
|
|
||||||
await user.click(screen.getByRole("button", { name: /^close$/i }));
|
|
||||||
|
|
||||||
expect(onCancel).toHaveBeenCalledTimes(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should reset the confirmation input when Cancel is clicked", async () => {
|
|
||||||
const user = userEvent.setup();
|
|
||||||
renderModal();
|
|
||||||
|
|
||||||
const input = screen.getByPlaceholderText("Enter team name exactly");
|
|
||||||
await user.type(input, "Engineering");
|
|
||||||
expect(input).toHaveValue("Engineering");
|
|
||||||
|
|
||||||
await user.click(screen.getByRole("button", { name: /^cancel$/i }));
|
|
||||||
|
|
||||||
expect(input).toHaveValue("");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should reset the confirmation input when the Close button is clicked", async () => {
|
|
||||||
const user = userEvent.setup();
|
|
||||||
renderModal();
|
|
||||||
|
|
||||||
const input = screen.getByPlaceholderText("Enter team name exactly");
|
|
||||||
await user.type(input, "Engineering");
|
|
||||||
|
|
||||||
await user.click(screen.getByRole("button", { name: /^close$/i }));
|
|
||||||
|
|
||||||
expect(input).toHaveValue("");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@ -1,96 +0,0 @@
|
|||||||
import { AlertTriangleIcon, XIcon } from "lucide-react";
|
|
||||||
import React, { useState } from "react";
|
|
||||||
import { Team } from "@/components/key_team_helpers/key_list";
|
|
||||||
|
|
||||||
interface DeleteTeamModalProps {
|
|
||||||
teams: Team[] | null;
|
|
||||||
teamToDelete: string | null;
|
|
||||||
onCancel: () => void;
|
|
||||||
onConfirm: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
const DeleteTeamModal = ({ teams, teamToDelete, onCancel, onConfirm }: DeleteTeamModalProps) => {
|
|
||||||
const [deleteConfirmInput, setDeleteConfirmInput] = useState("");
|
|
||||||
|
|
||||||
const team = teams?.find((t) => t.team_id === teamToDelete);
|
|
||||||
const teamName = team?.team_alias || "";
|
|
||||||
const keyCount = team?.keys?.length || 0;
|
|
||||||
const isValid = deleteConfirmInput === teamName;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
|
||||||
<div className="bg-white rounded-lg shadow-xl w-full max-w-2xl min-h-[380px] py-6 overflow-hidden transform transition-all flex flex-col justify-between">
|
|
||||||
<div>
|
|
||||||
<div className="flex items-center justify-between px-6 py-4 border-b border-gray-200">
|
|
||||||
<h3 className="text-lg font-semibold text-gray-900">Delete Team</h3>
|
|
||||||
<button
|
|
||||||
aria-label="Close"
|
|
||||||
onClick={() => {
|
|
||||||
onCancel();
|
|
||||||
setDeleteConfirmInput("");
|
|
||||||
}}
|
|
||||||
className="text-gray-400 hover:text-gray-500 focus:outline-none"
|
|
||||||
>
|
|
||||||
<XIcon size={20} />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div className="px-6 py-4">
|
|
||||||
{keyCount > 0 && (
|
|
||||||
<div className="flex items-start gap-3 p-4 bg-red-50 border border-red-100 rounded-md mb-5">
|
|
||||||
<div className="text-red-500 mt-0.5">
|
|
||||||
<AlertTriangleIcon size={20} />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<p className="text-base font-medium text-red-600">
|
|
||||||
Warning: This team has {keyCount} associated key{keyCount > 1 ? "s" : ""}.
|
|
||||||
</p>
|
|
||||||
<p className="text-base text-red-600 mt-2">
|
|
||||||
Deleting the team will also delete all associated keys. This action is irreversible.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<p className="text-base text-gray-600 mb-5">
|
|
||||||
Are you sure you want to force delete this team and all its keys?
|
|
||||||
</p>
|
|
||||||
<div className="mb-5">
|
|
||||||
<label className="block text-base font-medium text-gray-700 mb-2">
|
|
||||||
{`Type `}
|
|
||||||
<span className="underline">{teamName}</span>
|
|
||||||
{` to confirm deletion:`}
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
value={deleteConfirmInput}
|
|
||||||
onChange={(e) => setDeleteConfirmInput(e.target.value)}
|
|
||||||
placeholder="Enter team name exactly"
|
|
||||||
className="w-full px-4 py-3 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 text-base"
|
|
||||||
autoFocus
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="px-6 py-4 bg-gray-50 flex justify-end gap-4">
|
|
||||||
<button
|
|
||||||
onClick={() => {
|
|
||||||
onCancel();
|
|
||||||
setDeleteConfirmInput("");
|
|
||||||
}}
|
|
||||||
className="px-5 py-3 bg-white border border-gray-300 rounded-md text-base font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
|
|
||||||
>
|
|
||||||
Cancel
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={onConfirm}
|
|
||||||
disabled={!isValid}
|
|
||||||
className={`px-5 py-3 rounded-md text-base font-medium text-white focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500 ${isValid ? "bg-red-600 hover:bg-red-700" : "bg-red-300 cursor-not-allowed"}`}
|
|
||||||
>
|
|
||||||
Force Delete
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default DeleteTeamModal;
|
|
||||||
@ -1,30 +0,0 @@
|
|||||||
import { useCallback, useEffect, useState } from "react";
|
|
||||||
import { fetchTeams } from "@/components/common_components/fetch_teams";
|
|
||||||
import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized";
|
|
||||||
import { Organization, Team } from "@/components/networking";
|
|
||||||
|
|
||||||
interface useFetchTeamsProps {
|
|
||||||
currentOrg: Organization | null;
|
|
||||||
setTeams: (teams: Team[] | null) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
const useFetchTeams = ({ currentOrg, setTeams }: useFetchTeamsProps) => {
|
|
||||||
const [lastRefreshed, setLastRefreshed] = useState("");
|
|
||||||
const { accessToken, userId, userRole } = useAuthorized();
|
|
||||||
|
|
||||||
const onRefreshClick = useCallback(() => {
|
|
||||||
const currentDate = new Date();
|
|
||||||
setLastRefreshed(currentDate.toLocaleString());
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (accessToken) {
|
|
||||||
fetchTeams(accessToken, userId, userRole, currentOrg, setTeams).then();
|
|
||||||
}
|
|
||||||
onRefreshClick();
|
|
||||||
}, [accessToken, currentOrg, lastRefreshed, onRefreshClick, setTeams, userId, userRole]);
|
|
||||||
|
|
||||||
return { lastRefreshed, setLastRefreshed, onRefreshClick };
|
|
||||||
};
|
|
||||||
|
|
||||||
export default useFetchTeams;
|
|
||||||
@ -1,31 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import TeamsView from "@/app/(dashboard)/teams/TeamsView";
|
|
||||||
import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized";
|
|
||||||
import useTeams from "@/app/(dashboard)/hooks/useTeams";
|
|
||||||
import { useEffect, useState } from "react";
|
|
||||||
import { Organization } from "@/components/networking";
|
|
||||||
import { fetchOrganizations } from "@/components/organizations";
|
|
||||||
|
|
||||||
const TeamsPage = () => {
|
|
||||||
const { accessToken, userId, userRole } = useAuthorized();
|
|
||||||
const { teams, setTeams } = useTeams();
|
|
||||||
const [organizations, setOrganizations] = useState<Organization[]>([]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
fetchOrganizations(accessToken, setOrganizations).then(() => {});
|
|
||||||
}, [accessToken]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<TeamsView
|
|
||||||
teams={teams}
|
|
||||||
accessToken={accessToken}
|
|
||||||
setTeams={setTeams}
|
|
||||||
userID={userId}
|
|
||||||
userRole={userRole}
|
|
||||||
organizations={organizations}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default TeamsPage;
|
|
||||||
@ -1,45 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import ChatUI from "@/components/playground/chat_ui/ChatUI";
|
|
||||||
import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized";
|
|
||||||
import { useState, useEffect } from "react";
|
|
||||||
import { fetchProxySettings } from "@/utils/proxyUtils";
|
|
||||||
|
|
||||||
interface ProxySettings {
|
|
||||||
PROXY_BASE_URL?: string;
|
|
||||||
LITELLM_UI_API_DOC_BASE_URL?: string | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const TestKeyPage = () => {
|
|
||||||
const { token, accessToken, userRole, userId, disabledPersonalKeyCreation } = useAuthorized();
|
|
||||||
const [proxySettings, setProxySettings] = useState<ProxySettings | undefined>(undefined);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const initializeProxySettings = async () => {
|
|
||||||
if (accessToken) {
|
|
||||||
const settings = await fetchProxySettings(accessToken);
|
|
||||||
if (settings) {
|
|
||||||
setProxySettings({
|
|
||||||
PROXY_BASE_URL: settings.PROXY_BASE_URL || undefined,
|
|
||||||
LITELLM_UI_API_DOC_BASE_URL: settings.LITELLM_UI_API_DOC_BASE_URL,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
initializeProxySettings();
|
|
||||||
}, [accessToken]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ChatUI
|
|
||||||
accessToken={accessToken}
|
|
||||||
token={token}
|
|
||||||
userRole={userRole}
|
|
||||||
userID={userId}
|
|
||||||
disabledPersonalKeyCreation={disabledPersonalKeyCreation}
|
|
||||||
proxySettings={proxySettings}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default TestKeyPage;
|
|
||||||
@ -1,12 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import { MCPServers } from "@/components/mcp_tools";
|
|
||||||
import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized";
|
|
||||||
|
|
||||||
const MCPServersPage = () => {
|
|
||||||
const { accessToken, userRole, userId } = useAuthorized();
|
|
||||||
|
|
||||||
return <MCPServers accessToken={accessToken} userRole={userRole} userID={userId} />;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default MCPServersPage;
|
|
||||||
@ -1,12 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import VectorStoreManagement from "@/components/vector_store_management";
|
|
||||||
import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized";
|
|
||||||
|
|
||||||
const VectorStoresPage = () => {
|
|
||||||
const { accessToken, userId, userRole } = useAuthorized();
|
|
||||||
|
|
||||||
return <VectorStoreManagement accessToken={accessToken} userID={userId} userRole={userRole} />;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default VectorStoresPage;
|
|
||||||
@ -1,14 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import UsagePageView from "@/components/UsagePage/components/UsagePageView";
|
|
||||||
import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized";
|
|
||||||
import useTeams from "@/app/(dashboard)/hooks/useTeams";
|
|
||||||
|
|
||||||
const UsagePage = () => {
|
|
||||||
const { accessToken, userRole, userId, premiumUser } = useAuthorized();
|
|
||||||
const { teams } = useTeams();
|
|
||||||
|
|
||||||
return <UsagePageView teams={teams ?? []} organizations={[]} />;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default UsagePage;
|
|
||||||
@ -1,53 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import ViewUserDashboard from "@/components/view_users";
|
|
||||||
import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized";
|
|
||||||
import useTeams from "@/app/(dashboard)/hooks/useTeams";
|
|
||||||
import { useOrganizations } from "@/app/(dashboard)/hooks/organizations/useOrganizations";
|
|
||||||
import { isProxyAdminRole } from "@/utils/roles";
|
|
||||||
import { useState, useMemo } from "react";
|
|
||||||
import { Organization } from "@/components/networking";
|
|
||||||
|
|
||||||
const UsersPage = () => {
|
|
||||||
const { accessToken, userRole, userId, token } = useAuthorized();
|
|
||||||
const [keys, setKeys] = useState<null | any[]>([]);
|
|
||||||
|
|
||||||
const { teams } = useTeams();
|
|
||||||
const { data: organizations, isLoading: isOrgsLoading } = useOrganizations();
|
|
||||||
|
|
||||||
// Three states:
|
|
||||||
// - undefined: org data still loading (non-proxy-admin) — query should wait
|
|
||||||
// - null: proxy admin or no org filtering needed — query runs unfiltered
|
|
||||||
// - Array<{organization_id, organization_alias}>: org admin orgs — query runs filtered
|
|
||||||
const orgAdminOrgIds = useMemo((): Array<{organization_id: string, organization_alias: string}> | null | undefined => {
|
|
||||||
if (!userId || !userRole) return null;
|
|
||||||
// Proxy admins see all users — no org filtering
|
|
||||||
if (isProxyAdminRole(userRole)) return null;
|
|
||||||
|
|
||||||
// Still loading org data — signal "not ready yet"
|
|
||||||
if (isOrgsLoading || !organizations) return undefined;
|
|
||||||
|
|
||||||
const adminOrgs = organizations
|
|
||||||
.filter((org: Organization) =>
|
|
||||||
org.members?.some((member) => member.user_id === userId && member.user_role === "org_admin")
|
|
||||||
)
|
|
||||||
.map((org: Organization) => ({ organization_id: org.organization_id, organization_alias: org.organization_alias }));
|
|
||||||
|
|
||||||
return adminOrgs.length > 0 ? adminOrgs : null;
|
|
||||||
}, [userId, organizations, userRole, isOrgsLoading]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ViewUserDashboard
|
|
||||||
accessToken={accessToken}
|
|
||||||
token={token}
|
|
||||||
keys={keys}
|
|
||||||
userRole={userRole}
|
|
||||||
userID={userId}
|
|
||||||
teams={teams as any}
|
|
||||||
setKeys={setKeys}
|
|
||||||
orgAdminOrgIds={orgAdminOrgIds}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default UsersPage;
|
|
||||||
Loading…
Reference in New Issue
Block a user