Bulk Edit Users on UI (#12763)

* feat(bulk_edit_user.tsx): initial working ui for editing users in bulk on the ui

easier to give access / assign to a default team

* feat(team_endpoints-+-bulk_edit_users.tsx): add bulk adding users to teams

make it easier to add existing users to a default team

* fix(bulk_edit_user.tsx): fix ui linting error

* fix: fix linting error
This commit is contained in:
Krish Dholakia 2025-07-19 11:04:23 -07:00 committed by GitHub
parent 96f7eb6f78
commit d72b3389a1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 842 additions and 25 deletions

View file

@ -58,7 +58,7 @@ def decrypt_value_helper(
verbose_proxy_logger.debug(error_message)
return None
verbose_proxy_logger.error(error_message)
verbose_proxy_logger.exception(error_message)
# [Non-Blocking Exception. - this should not block decrypting other values]
return None
@ -98,7 +98,12 @@ def decrypt_value(value: bytes, signing_key: str) -> str:
box = nacl.secret.SecretBox(hash_bytes)
# Convert the bytes object to a string
plaintext = box.decrypt(value)
try:
if len(value) == 0:
return ""
plaintext = plaintext.decode("utf-8") # type: ignore
return plaintext # type: ignore
plaintext = box.decrypt(value)
plaintext = plaintext.decode("utf-8") # type: ignore
return plaintext # type: ignore
except Exception as e:
raise e

View file

@ -92,8 +92,11 @@ from litellm.types.proxy.management_endpoints.common_daily_activity import (
SpendAnalyticsPaginatedResponse,
)
from litellm.types.proxy.management_endpoints.team_endpoints import (
BulkTeamMemberAddRequest,
BulkTeamMemberAddResponse,
GetTeamMemberPermissionsResponse,
TeamListResponse,
TeamMemberAddResult,
UpdateTeamMemberPermissionsRequest,
)
@ -1541,6 +1544,154 @@ async def team_member_update(
)
def _create_results_from_response(
members: List[Member],
response: TeamAddMemberResponse,
) -> List[TeamMemberAddResult]:
"""
Convert TeamAddMemberResponse into individual TeamMemberAddResult objects
"""
results: List[TeamMemberAddResult] = []
for member in members:
# Find corresponding updated user
updated_user = None
for user in response.updated_users:
if (member.user_id and user.user_id == member.user_id) or (
member.user_email and user.user_email == member.user_email
):
updated_user = user.model_dump()
break
# Find corresponding updated team membership
updated_team_membership = None
for tm in response.updated_team_memberships:
if member.user_id and tm.user_id == member.user_id:
updated_team_membership = tm.model_dump()
break
results.append(
TeamMemberAddResult(
user_id=member.user_id,
user_email=member.user_email,
success=True,
updated_user=updated_user,
updated_team_membership=updated_team_membership,
)
)
return results
@router.post(
"/team/bulk_member_add",
tags=["team management"],
dependencies=[Depends(user_api_key_auth)],
response_model=BulkTeamMemberAddResponse,
)
@management_endpoint_wrapper
async def bulk_team_member_add(
data: BulkTeamMemberAddRequest,
user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth),
):
"""
Bulk add multiple members to a team at once.
This endpoint reuses the same logic as /team/member_add but provides a bulk-friendly response format.
Parameters:
- team_id: str - The ID of the team to add members to
- members: List[Member] - List of members to add to the team
- max_budget_in_team: Optional[float] - Maximum budget allocated to each user within the team
Returns:
- results: List of individual member addition results
- total_requested: Total number of members requested for addition
- successful_additions: Number of successful additions
- failed_additions: Number of failed additions
- updated_team: The updated team object
Example request:
```bash
curl --location 'http://0.0.0.0:4000/team/bulk_member_add' \
--header 'Authorization: Bearer sk-1234' \
--header 'Content-Type: application/json' \
--data '{
"team_id": "team-1234",
"members": [
{
"user_id": "user1",
"role": "user"
},
{
"user_email": "user2@example.com",
"role": "admin"
}
],
"max_budget_in_team": 100.0
}'
```
"""
if not data.members:
raise HTTPException(
status_code=400,
detail={"error": "At least one member is required"},
)
# Limit batch size to prevent overwhelming the system
MAX_BATCH_SIZE = 100
if len(data.members) > MAX_BATCH_SIZE:
raise HTTPException(
status_code=400,
detail={"error": f"Maximum {MAX_BATCH_SIZE} members can be added at once"},
)
try:
# Reuse the existing team_member_add logic directly
response = await team_member_add(
data=TeamMemberAddRequest(
team_id=data.team_id,
member=data.members, # Pass the entire list
max_budget_in_team=data.max_budget_in_team,
),
user_api_key_dict=user_api_key_dict,
)
# Convert to bulk response format
results = _create_results_from_response(data.members, response)
return BulkTeamMemberAddResponse(
team_id=data.team_id,
results=results,
total_requested=len(data.members),
successful_additions=len(results), # All succeeded if we got here
failed_additions=0,
updated_team=response.model_dump(),
)
except Exception as e:
# If the entire operation fails, mark all members as failed
error_message = str(e)
results = [
TeamMemberAddResult(
user_id=member.user_id,
user_email=member.user_email,
success=False,
error=error_message,
)
for member in data.members
]
return BulkTeamMemberAddResponse(
team_id=data.team_id,
results=results,
total_requested=len(data.members),
successful_additions=0,
failed_additions=len(data.members),
updated_team=None,
)
@router.post(
"/team/delete", tags=["team management"], dependencies=[Depends(user_api_key_auth)]
)

View file

@ -1,8 +1,13 @@
from typing import List, Optional
from typing import Any, Dict, List, Optional
from pydantic import BaseModel
from litellm.proxy._types import LiteLLM_TeamTable
from litellm.proxy._types import (
LiteLLM_TeamMembership,
LiteLLM_TeamTable,
LiteLLM_UserTable,
Member,
)
class GetTeamMemberPermissionsRequest(BaseModel):
@ -45,3 +50,33 @@ class TeamListResponse(BaseModel):
page: int
page_size: int
total_pages: int
class BulkTeamMemberAddRequest(BaseModel):
"""Request for bulk team member addition"""
team_id: str
members: List[Member] # List of members to add
max_budget_in_team: Optional[float] = None
class TeamMemberAddResult(BaseModel):
"""Result of a single team member add operation"""
user_id: Optional[str] = None
user_email: Optional[str] = None
success: bool
error: Optional[str] = None
updated_user: Optional[Dict[str, Any]] = None
updated_team_membership: Optional[Dict[str, Any]] = None
class BulkTeamMemberAddResponse(BaseModel):
"""Response for bulk team member add operations"""
team_id: str
results: List[TeamMemberAddResult]
total_requested: int
successful_additions: int
failed_additions: int
updated_team: Optional[Dict[str, Any]] = None

View file

@ -0,0 +1,344 @@
import React, { useState } from 'react';
import {
Button as Button2,
Modal,
Typography,
Divider,
message,
Table,
Select,
Form,
InputNumber,
Card,
Space,
Checkbox,
} from "antd";
import { Button } from '@tremor/react';
import { userBulkUpdateUserCall, teamBulkMemberAddCall } from "./networking";
import { UserEditView } from "./user_edit_view";
const { Text, Title } = Typography;
interface BulkEditUserModalProps {
visible: boolean;
onCancel: () => void;
selectedUsers: any[];
possibleUIRoles: Record<string, Record<string, string>> | null;
accessToken: string | null;
onSuccess: () => void;
teams: any[] | null;
userRole: string | null;
userModels: string[];
}
const BulkEditUserModal: React.FC<BulkEditUserModalProps> = ({
visible,
onCancel,
selectedUsers,
possibleUIRoles,
accessToken,
onSuccess,
teams,
userRole,
userModels,
}) => {
const [loading, setLoading] = useState(false);
const [selectedTeams, setSelectedTeams] = useState<string[]>([]);
const [teamBudget, setTeamBudget] = useState<number | null>(null);
const [addToTeams, setAddToTeams] = useState(false);
const handleCancel = () => {
// Reset team management state
setSelectedTeams([]);
setTeamBudget(null);
setAddToTeams(false);
onCancel();
};
// Create a mock userData object for the UserEditView
const mockUserData = {
user_id: "bulk_edit",
user_info: {
user_email: "",
user_role: "",
teams: [],
models: [],
max_budget: null,
spend: 0,
metadata: {},
created_at: null,
updated_at: null,
},
keys: [],
teams: teams || [],
};
const handleSubmit = async (formValues: any) => {
if (!accessToken) {
message.error("Access token not found");
return;
}
setLoading(true);
try {
const userIds = selectedUsers.map(user => user.user_id);
// Build the update payload - only include fields that have been changed from default/empty values
const updatePayload: any = {};
if (formValues.user_role && formValues.user_role !== "") {
updatePayload.user_role = formValues.user_role;
}
if (formValues.max_budget !== null && formValues.max_budget !== undefined) {
updatePayload.max_budget = formValues.max_budget;
}
if (formValues.models && formValues.models.length > 0) {
updatePayload.models = formValues.models;
}
if (formValues.metadata && Object.keys(formValues.metadata).length > 0) {
updatePayload.metadata = formValues.metadata;
}
// Check if any operations were requested
const hasUserUpdates = Object.keys(updatePayload).length > 0;
const hasTeamAdditions = addToTeams && selectedTeams.length > 0;
if (!hasUserUpdates && !hasTeamAdditions) {
message.error("Please modify at least one field or select teams to add users to");
return;
}
let successMessages: string[] = [];
// Handle user property updates
if (hasUserUpdates) {
await userBulkUpdateUserCall(accessToken, updatePayload, userIds);
successMessages.push(`Updated ${userIds.length} user(s)`);
}
// Handle team additions
if (hasTeamAdditions) {
const teamResults: any[] = [];
for (const teamId of selectedTeams) {
try {
// Create member objects for bulk add
const members = selectedUsers.map(user => ({
user_id: user.user_id,
role: "user" as const, // Default role for bulk add
user_email: user.user_email || null,
}));
const result = await teamBulkMemberAddCall(
accessToken,
teamId,
members,
teamBudget || undefined
);
teamResults.push({
teamId,
success: true,
successfulAdditions: result.successful_additions,
failedAdditions: result.failed_additions,
});
} catch (error) {
console.error(`Failed to add users to team ${teamId}:`, error);
teamResults.push({
teamId,
success: false,
error: error,
});
}
}
// Generate team success message
const successfulTeams = teamResults.filter(r => r.success);
const failedTeams = teamResults.filter(r => !r.success);
if (successfulTeams.length > 0) {
const totalAdditions = successfulTeams.reduce((sum, r) => sum + r.successfulAdditions, 0);
successMessages.push(`Added users to ${successfulTeams.length} team(s) (${totalAdditions} total additions)`);
}
if (failedTeams.length > 0) {
message.warning(`Failed to add users to ${failedTeams.length} team(s)`);
}
}
if (successMessages.length > 0) {
message.success(successMessages.join('. '));
}
// Reset team management state
setSelectedTeams([]);
setTeamBudget(null);
setAddToTeams(false);
onSuccess();
onCancel();
} catch (error) {
console.error("Bulk operation failed:", error);
message.error("Failed to perform bulk operations");
} finally {
setLoading(false);
}
};
return (
<Modal
visible={visible}
onCancel={handleCancel}
footer={null}
title={`Bulk Edit ${selectedUsers.length} User(s)`}
width={800}
>
<div className="mb-4">
<Title level={5}>Selected Users ({selectedUsers.length}):</Title>
<Table
size="small"
bordered
dataSource={selectedUsers}
pagination={false}
scroll={{ y: 200 }}
rowKey="user_id"
columns={[
{
title: 'User ID',
dataIndex: 'user_id',
key: 'user_id',
width: '30%',
render: (text: string) => (
<Text strong style={{ fontSize: '12px' }}>
{text.length > 20 ? `${text.slice(0, 20)}...` : text}
</Text>
),
},
{
title: 'Email',
dataIndex: 'user_email',
key: 'user_email',
width: '25%',
render: (text: string) => (
<Text type="secondary" style={{ fontSize: '12px' }}>
{text || 'No email'}
</Text>
),
},
{
title: 'Current Role',
dataIndex: 'user_role',
key: 'user_role',
width: '25%',
render: (role: string) => (
<Text style={{ fontSize: '12px' }}>
{possibleUIRoles?.[role]?.ui_label || role}
</Text>
),
},
{
title: 'Budget',
dataIndex: 'max_budget',
key: 'max_budget',
width: '20%',
render: (budget: number | null) => (
<Text style={{ fontSize: '12px' }}>
{budget !== null ? `$${budget}` : 'Unlimited'}
</Text>
),
},
]}
/>
</div>
<Divider />
<div className="mb-4">
<Text>
<strong>Instructions:</strong> Fill in the fields below with the values you want to apply to all selected users.
You can bulk edit: role, budget, models, and metadata. You can also add users to teams.
</Text>
</div>
{/* Team Management Section */}
<Card
title="Team Management"
size="small"
className="mb-4"
style={{ backgroundColor: '#fafafa' }}
>
<Space direction="vertical" style={{ width: '100%' }}>
<Checkbox
checked={addToTeams}
onChange={(e) => setAddToTeams(e.target.checked)}
>
Add selected users to teams
</Checkbox>
{addToTeams && (
<>
<div>
<Text strong>Select Teams:</Text>
<Select
mode="multiple"
placeholder="Select teams to add users to"
value={selectedTeams}
onChange={setSelectedTeams}
style={{ width: '100%', marginTop: 8 }}
options={teams?.map(team => ({
label: team.team_alias || team.team_id,
value: team.team_id,
})) || []}
/>
</div>
<div>
<Text strong>Team Budget (Optional):</Text>
<InputNumber
placeholder="Max budget per user in team"
value={teamBudget}
onChange={(value) => setTeamBudget(value)}
style={{ width: '100%', marginTop: 8 }}
min={0}
step={0.01}
precision={2}
/>
<Text type="secondary" style={{ fontSize: '12px' }}>
Leave empty for unlimited budget within team limits
</Text>
</div>
<Text type="secondary" style={{ fontSize: '12px' }}>
Users will be added with &quot;user&quot; role by default. All users will be added to each selected team.
</Text>
</>
)}
</Space>
</Card>
<UserEditView
userData={mockUserData}
onCancel={handleCancel}
onSubmit={handleSubmit}
teams={teams}
accessToken={accessToken}
userID="bulk_edit"
userRole={userRole}
userModels={userModels}
possibleUIRoles={possibleUIRoles}
isBulkEdit={true}
/>
{loading && (
<div style={{ textAlign: "center", marginTop: "10px" }}>
<Text>Updating {selectedUsers.length} user(s)...</Text>
</div>
)}
</Modal>
);
};
export default BulkEditUserModal;

View file

@ -3674,6 +3674,64 @@ export const teamMemberAddCall = async (
}
};
export const teamBulkMemberAddCall = async (
accessToken: string,
teamId: string,
members: Member[],
maxBudgetInTeam?: number
) => {
try {
console.log("Bulk add team members:", { teamId, members, maxBudgetInTeam });
const url = proxyBaseUrl
? `${proxyBaseUrl}/team/bulk_member_add`
: `/team/bulk_member_add`;
const requestBody: any = {
team_id: teamId,
members: members,
};
if (maxBudgetInTeam !== undefined && maxBudgetInTeam !== null) {
requestBody.max_budget_in_team = maxBudgetInTeam;
}
const response = await fetch(url, {
method: "POST",
headers: {
[globalLitellmHeaderName]: `Bearer ${accessToken}`,
"Content-Type": "application/json",
},
body: JSON.stringify(requestBody),
});
if (!response.ok) {
// Read and parse JSON error body
const errorText = await response.text();
let parsedError: any = {};
try {
parsedError = JSON.parse(errorText);
} catch (e) {
console.warn("Failed to parse error body as JSON:", errorText);
}
const rawMessage =
parsedError?.detail?.error || "Failed to bulk add team members";
const err = new Error(rawMessage);
(err as any).raw = parsedError;
throw err;
}
const data = await response.json();
console.log("Bulk team member add API Response:", data);
return data;
} catch (error) {
console.error("Failed to bulk add team members:", error);
throw error;
}
};
export const teamMemberUpdateCall = async (
accessToken: string,
teamId: string,
@ -3933,6 +3991,57 @@ export const userUpdateUserCall = async (
}
};
export const userBulkUpdateUserCall = async (
accessToken: string,
formValues: any, // Assuming formValues is an object
userIds: string[]
) => {
try {
console.log("Form Values in userUpdateUserCall:", formValues); // Log the form values before making the API call
const url = proxyBaseUrl
? `${proxyBaseUrl}/user/bulk_update`
: `/user/bulk_update`;
let request_body = []
for (const user_id of userIds) {
request_body.push({
user_id: user_id,
...formValues,
});
}
let request_body_json = JSON.stringify({
users: request_body,
});
const response = await fetch(url, {
method: "POST",
headers: {
[globalLitellmHeaderName]: `Bearer ${accessToken}`,
"Content-Type": "application/json",
},
body: request_body_json,
});
if (!response.ok) {
const errorData = await response.text();
handleError(errorData);
console.error("Error response from the server:", errorData);
throw new Error("Network response was not ok");
}
const data = (await response.json()) as {
user_id: string;
data: UserInfo;
};
console.log("API Response:", data);
//message.success("User role updated");
return data;
// Handle success - you might want to update some state or UI based on the created key
} catch (error) {
console.error("Failed to create key:", error);
throw error;
}
};
export const PredictedSpendLogsCall = async (
accessToken: string,
requestData: any

View file

@ -15,6 +15,7 @@ interface UserEditViewProps {
userRole: string | null;
userModels: string[];
possibleUIRoles: Record<string, Record<string, string>> | null;
isBulkEdit?: boolean;
}
export function UserEditView({
@ -27,6 +28,7 @@ export function UserEditView({
userRole,
userModels,
possibleUIRoles,
isBulkEdit = false,
}: UserEditViewProps) {
const [form] = Form.useForm();
@ -62,19 +64,23 @@ export function UserEditView({
onFinish={handleSubmit}
layout="vertical"
>
<Form.Item
label="User ID"
name="user_id"
>
<TextInput disabled />
</Form.Item>
{!isBulkEdit && (
<Form.Item
label="User ID"
name="user_id"
>
<TextInput disabled />
</Form.Item>
)}
<Form.Item
label="Email"
name="user_email"
>
<TextInput />
</Form.Item>
{!isBulkEdit && (
<Form.Item
label="Email"
name="user_email"
>
<TextInput />
</Form.Item>
)}
<Form.Item label={
<span>

View file

@ -17,8 +17,9 @@ import CreateUser from "./create_user_button"
import EditUserModal from "./edit_user"
import OnboardingModal from "./onboarding_link"
import { InvitationLink } from "./onboarding_link"
import BulkEditUserModal from "./bulk_edit_user"
import { userDeleteCall } from "./networking"
import { userDeleteCall, modelAvailableCall } from "./networking"
import { columns } from "./view_users/columns"
import { UserDataTable } from "./view_users/table"
import { UserInfo } from "./view_users/types"
@ -80,6 +81,10 @@ const ViewUserDashboard: React.FC<ViewUserDashboardProps> = ({ accessToken, toke
const [isInvitationLinkModalVisible, setIsInvitationLinkModalVisible] = useState(false)
const [invitationLinkData, setInvitationLinkData] = useState<InvitationLink | null>(null)
const [baseUrl, setBaseUrl] = useState<string | null>(null)
const [selectedUsers, setSelectedUsers] = useState<UserInfo[]>([])
const [isBulkEditModalVisible, setIsBulkEditModalVisible] = useState(false)
const [selectionMode, setSelectionMode] = useState(false)
const [userModels, setUserModels] = useState<string[]>([])
const handleDelete = (userId: string) => {
setUserToDelete(userId)
@ -96,6 +101,28 @@ const ViewUserDashboard: React.FC<ViewUserDashboardProps> = ({ accessToken, toke
setBaseUrl(getProxyBaseUrl())
}, [])
// Fetch available models for bulk edit
useEffect(() => {
const fetchUserModels = async () => {
try {
if (!userID || !userRole || !accessToken) {
return
}
const model_available = await modelAvailableCall(accessToken, userID, userRole)
let available_model_names = model_available["data"].map(
(element: { id: string }) => element.id
)
console.log("available_model_names:", available_model_names)
setUserModels(available_model_names)
} catch (error) {
console.error("Error fetching user models:", error)
}
}
fetchUserModels()
}, [accessToken, userID, userRole])
const updateFilters = (update: Partial<FilterState>) => {
setFilters((previousFilters) => {
const newFilters = { ...previousFilters, ...update }
@ -189,6 +216,30 @@ const ViewUserDashboard: React.FC<ViewUserDashboardProps> = ({ accessToken, toke
setCurrentPage(newPage)
}
const handleToggleSelectionMode = () => {
setSelectionMode(!selectionMode)
setSelectedUsers([])
}
const handleSelectionChange = (users: UserInfo[]) => {
setSelectedUsers(users)
}
const handleBulkEdit = () => {
if (selectedUsers.length === 0) {
message.error("Please select users to edit")
return
}
setIsBulkEditModalVisible(true)
}
const handleBulkEditSuccess = () => {
// Refresh the user list
queryClient.invalidateQueries({ queryKey: ["userList"] })
setSelectedUsers([])
setSelectionMode(false)
}
const userListQuery = useQuery({
queryKey: ["userList", { debouncedFilter: debouncedFilters, currentPage }],
queryFn: async () => {
@ -247,6 +298,24 @@ const ViewUserDashboard: React.FC<ViewUserDashboardProps> = ({ accessToken, toke
<div className="flex items-center justify-between mb-4">
<div className="flex space-x-3">
<CreateUser userID={userID} accessToken={accessToken} teams={teams} possibleUIRoles={possibleUIRoles} />
<Button
onClick={handleToggleSelectionMode}
variant={selectionMode ? "primary" : "secondary"}
className="flex items-center"
>
{selectionMode ? "Cancel Selection" : "Select Users"}
</Button>
{selectionMode && (
<Button
onClick={handleBulkEdit}
disabled={selectedUsers.length === 0}
className="flex items-center"
>
Bulk Edit ({selectedUsers.length} selected)
</Button>
)}
</div>
</div>
@ -454,6 +523,9 @@ const ViewUserDashboard: React.FC<ViewUserDashboardProps> = ({ accessToken, toke
}}
handleDelete={handleDelete}
handleResetPassword={handleResetPassword}
enableSelection={selectionMode}
selectedUsers={selectedUsers}
onSelectionChange={handleSelectionChange}
/>
</div>
</TabPanel>
@ -522,6 +594,18 @@ const ViewUserDashboard: React.FC<ViewUserDashboardProps> = ({ accessToken, toke
invitationLinkData={invitationLinkData}
modalType="resetPassword"
/>
<BulkEditUserModal
visible={isBulkEditModalVisible}
onCancel={() => setIsBulkEditModalVisible(false)}
selectedUsers={selectedUsers}
possibleUIRoles={possibleUIRoles}
accessToken={accessToken}
onSuccess={handleBulkEditSuccess}
teams={teams}
userRole={userRole}
userModels={userModels}
/>
</div>
)
}

View file

@ -1,17 +1,28 @@
import { ColumnDef } from "@tanstack/react-table";
import { Badge, Grid, Icon } from "@tremor/react";
import { Tooltip } from "antd";
import { Tooltip, Checkbox } from "antd";
import { UserInfo } from "./types";
import { PencilAltIcon, TrashIcon, InformationCircleIcon, RefreshIcon } from "@heroicons/react/outline";
import { formatNumberWithCommas } from "@/utils/dataUtils";
interface SelectionOptions {
selectedUsers: UserInfo[];
onSelectUser: (user: UserInfo, isSelected: boolean) => void;
onSelectAll: (isSelected: boolean) => void;
isUserSelected: (user: UserInfo) => boolean;
isAllSelected: boolean;
isIndeterminate: boolean;
}
export const columns = (
possibleUIRoles: Record<string, Record<string, string>>,
handleEdit: (user: UserInfo) => void,
handleDelete: (userId: string) => void,
handleResetPassword: (userId: string) => void,
handleUserClick: (userId: string, openInEditMode?: boolean) => void
): ColumnDef<UserInfo>[] => [
handleUserClick: (userId: string, openInEditMode?: boolean) => void,
selectionOptions?: SelectionOptions
): ColumnDef<UserInfo>[] => {
const baseColumns: ColumnDef<UserInfo>[] = [
{
header: "User ID",
accessorKey: "user_id",
@ -137,4 +148,34 @@ export const columns = (
</div>
),
},
];
];
// Add selection column if selection is enabled
if (selectionOptions) {
const { onSelectUser, onSelectAll, isUserSelected, isAllSelected, isIndeterminate } = selectionOptions;
return [
{
id: "select",
header: () => (
<Checkbox
indeterminate={isIndeterminate}
checked={isAllSelected}
onChange={(e) => onSelectAll(e.target.checked)}
onClick={(e) => e.stopPropagation()}
/>
),
cell: ({ row }) => (
<Checkbox
checked={isUserSelected(row.original)}
onChange={(e) => onSelectUser(row.original, e.target.checked)}
onClick={(e) => e.stopPropagation()}
/>
),
},
...baseColumns,
];
}
return baseColumns;
};

View file

@ -36,6 +36,9 @@ interface UserDataTableProps {
handleEdit: (user: UserInfo) => void;
handleDelete: (userId: string) => void;
handleResetPassword: (userId: string) => void;
selectedUsers?: UserInfo[];
onSelectionChange?: (selectedUsers: UserInfo[]) => void;
enableSelection?: boolean;
}
export function UserDataTable({
@ -50,6 +53,9 @@ export function UserDataTable({
handleEdit,
handleDelete,
handleResetPassword,
selectedUsers = [],
onSelectionChange,
enableSelection = false,
}: UserDataTableProps) {
const [sorting, setSorting] = React.useState<SortingState>([
{
@ -70,6 +76,34 @@ export function UserDataTable({
setOpenInEditMode(false);
};
// Selection handlers
const handleSelectUser = (user: UserInfo, isSelected: boolean) => {
if (!onSelectionChange) return;
if (isSelected) {
onSelectionChange([...selectedUsers, user]);
} else {
onSelectionChange(selectedUsers.filter(u => u.user_id !== user.user_id));
}
};
const handleSelectAll = (isSelected: boolean) => {
if (!onSelectionChange) return;
if (isSelected) {
onSelectionChange(data);
} else {
onSelectionChange([]);
}
};
const isUserSelected = (user: UserInfo) => {
return selectedUsers.some(u => u.user_id === user.user_id);
};
const isAllSelected = data.length > 0 && selectedUsers.length === data.length;
const isIndeterminate = selectedUsers.length > 0 && selectedUsers.length < data.length;
// Create columns with the handleUserClick function
const columns = React.useMemo(() => {
if (possibleUIRoles) {
@ -78,11 +112,19 @@ export function UserDataTable({
handleEdit,
handleDelete,
handleResetPassword,
handleUserClick
handleUserClick,
enableSelection ? {
selectedUsers,
onSelectUser: handleSelectUser,
onSelectAll: handleSelectAll,
isUserSelected,
isAllSelected,
isIndeterminate,
} : undefined
);
}
return originalColumns;
}, [possibleUIRoles, handleEdit, handleDelete, handleResetPassword, handleUserClick, originalColumns]);
}, [possibleUIRoles, handleEdit, handleDelete, handleResetPassword, handleUserClick, originalColumns, enableSelection, selectedUsers, isAllSelected, isIndeterminate]);
const table = useReactTable({
data,