From 0f443cb2f2c9086d59a41fa5deebfbab16cc680c Mon Sep 17 00:00:00 2001 From: Jill Date: Thu, 5 Jun 2025 09:54:07 -0700 Subject: [PATCH 1/3] part 1 --- src/pages/users/DiffUsers.tsx | 278 ++++++++++++++++++++++++++++++++++ 1 file changed, 278 insertions(+) create mode 100644 src/pages/users/DiffUsers.tsx diff --git a/src/pages/users/DiffUsers.tsx b/src/pages/users/DiffUsers.tsx new file mode 100644 index 00000000..fa094014 --- /dev/null +++ b/src/pages/users/DiffUsers.tsx @@ -0,0 +1,278 @@ +import React from 'react'; +import { + Box, + Card, + CardContent, + Typography, + List, + ListItem, + ListItemText, + Chip, + CircularProgress, + Alert, + Divider, + Autocomplete, + TextField, +} from '@mui/material'; +import {useQuery} from '@tanstack/react-query'; +import {useGetAllUsers} from '../../api/apiComponents'; +import {OktaUser, OktaUserGroupMember} from '../../api/apiSchemas'; + +interface User { + id: string; + name: string; + email: string; + memberships: Membership[]; +} + +interface Membership { + id: string; + name: string; + type: 'group' | 'role'; + description?: string; +} + +const fetchUser = async (userId: string): Promise => { + const response = await fetch(`/api/users/${userId}`); + if (!response.ok) { + throw new Error(`Failed to fetch user ${userId}`); + } + return response.json(); +}; + +const findCommonMemberships = (user1: OktaUser, user2: OktaUser): OktaUserGroupMember[] => { + const user1MembershipIds = new Set(user1?.active_group_memberships?.map((m) => m.id) ?? []); + return user2?.active_group_memberships?.filter((membership) => user1MembershipIds.has(membership.id)) ?? []; +}; + +export default function DiffUsers() { + const [userId1, setUserId1] = React.useState(null); + const [userId2, setUserId2] = React.useState(null); + const [selectedUser1, setSelectedUser1] = React.useState(null); + const [selectedUser2, setSelectedUser2] = React.useState(null); + + if (!userId1 || !userId2) { + const {data, isLoading, error} = useGetAllUsers({}); + if (isLoading) { + return ( + + + + ); + } + if (error) { + return ( + Error loading users: {error instanceof Error ? error.message : 'Unknown error'} + ); + } + + return ( + + + Compare Users + + + Select two users to compare their memberships and access. + + + + + + + First User + + `${option.first_name} ${option.last_name} (${option.email})`} + renderInput={(params) => ( + + )} + onChange={(event, value) => { + if (value) { + // Navigate to comparison with selected user + // You'll need to implement navigation logic here + console.log('Selected user:', value); + setUserId1(value.id); + } + }} + renderOption={(props, option) => ( + + + + {option.first_name} {option.last_name} + + + {option.email} + + + + )} + /> + + + + + + + Second User + + `${option.first_name} ${option.last_name} (${option.email})`} + renderInput={(params) => ( + + )} + onChange={(event, value) => { + if (value) { + // Navigate to comparison with selected user + // You'll need to implement navigation logic here + console.log('Selected user:', value); + setUserId2(value.id); + } + }} + renderOption={(props, option) => ( + + + + {option.first_name} {option.last_name} + + + {option.email} + + + + )} + /> + + + + + ); + } + const { + data: fetchUserId1, + isLoading: isLoadingUser1, + error: errorUser1, + } = useQuery({ + queryKey: ['user', userId1], + queryFn: () => fetchUser(userId1!), + enabled: !!userId1, + }); + + const { + data: fetchUserId2, + isLoading: isLoadingUser2, + error: errorUser2, + } = useQuery({ + queryKey: ['user', userId2], + queryFn: () => fetchUser(userId2!), + enabled: !!userId2, + }); + + const isLoading = isLoadingUser1 || isLoadingUser2; + const error = errorUser1 || errorUser2; + + if (isLoading) { + return ( + + + + ); + } + + if (error) { + return ( + Error loading users: {error instanceof Error ? error.message : 'Unknown error'} + ); + } + + const commonMemberships = findCommonMemberships(fetchUserId1, fetchUserId2); + + return ( + + + User Membership Comparison + + + + + + + {selectedUser1?.first_name} {selectedUser1?.last_name} + + + {selectedUser1?.email} + + + Total memberships: {selectedUser1?.active_group_memberships?.length} + + + + + + + + {selectedUser2?.first_name} {selectedUser2?.last_name} + + + {selectedUser2?.email} + + + Total memberships: {selectedUser2?.active_group_memberships?.length} + + + + + + + + + Common Memberships ({commonMemberships.length}) + + + {commonMemberships.length === 0 ? ( + + No common memberships found between these users. + + ) : ( + + {commonMemberships.map((membership, index) => ( + + + + {membership.group.name} + + + } + secondary={membership.group.description} + /> + + {index < commonMemberships.length - 1 && } + + ))} + + )} + + + + ); +} From ae642946508830f5338a1959e7dc26c3cd337c79 Mon Sep 17 00:00:00 2001 From: Jill Date: Thu, 5 Jun 2025 20:02:56 -0700 Subject: [PATCH 2/3] initial commit for diff users page --- src/App.tsx | 3 + src/api/apiComponents.ts | 36 +++ src/components/NavItems.tsx | 3 + src/pages/users/DiffUsers.tsx | 581 ++++++++++++++++++++++++++-------- 4 files changed, 499 insertions(+), 124 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index d95a92fe..177bd38e 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -34,6 +34,7 @@ import {lightGreen, red, yellow} from '@mui/material/colors'; import AuditGroup from './pages/groups/Audit'; import AuditRole from './pages/roles/Audit'; import AuditUser from './pages/users/Audit'; +import DiffUsers from './pages/users/DiffUsers'; import ExpiringGroups from './pages/groups/Expiring'; import ExpiringRoles from './pages/roles/Expiring'; import Home from './pages/Home'; @@ -53,6 +54,7 @@ import ReadUser from './pages/users/Read'; import {useCurrentUser} from './authentication'; import ReadRequest from './pages/requests/Read'; import ReadRoleRequest from './pages/role_requests/Read'; + import * as Sentry from '@sentry/react'; const drawerWidth: number = 240; @@ -243,6 +245,7 @@ function Dashboard({setThemeMode}: {setThemeMode: (theme: PaletteMode) => void}) } /> } /> } /> + } /> } /> } /> } /> diff --git a/src/api/apiComponents.ts b/src/api/apiComponents.ts index ef118ca4..f944edab 100644 --- a/src/api/apiComponents.ts +++ b/src/api/apiComponents.ts @@ -1183,6 +1183,37 @@ export const useGetUsers = ( }); }; +export type GetAllUsersError = Fetcher.ErrorWrapper<{ + status: ClientErrorStatus | ServerErrorStatus; + payload: Schemas.UserPagination; +}>; + +export type GetAllUsersVariables = ApiContext['fetcherOptions']; + +export const fetchGetAllUsers = (variables: GetAllUsersVariables, signal?: AbortSignal) => + apiFetch({ + url: '/api/users', + method: 'get', + ...variables, + signal, + }); + +export const useGetAllUsers = ( + variables: GetAllUsersVariables, + options?: Omit< + reactQuery.UseQueryOptions, + 'queryKey' | 'queryFn' | 'initialData' + >, +) => { + const {fetcherOptions, queryOptions, queryKeyFn} = useApiContext(options); + return reactQuery.useQuery({ + queryKey: queryKeyFn({path: '/api/users', operationId: 'getAllUsers', variables}), + queryFn: ({signal}) => fetchGetAllUsers({...fetcherOptions, ...variables}, signal), + ...options, + ...queryOptions, + }); +}; + export type GetUserByIdPathParams = { userId: string; }; @@ -1310,4 +1341,9 @@ export type QueryOperation = path: '/api/users/{userId}'; operationId: 'getUserById'; variables: GetUserByIdVariables; + } + | { + path: '/api/users'; + operationId: 'getAllUsers'; + variables: GetAllUsersVariables; }; diff --git a/src/components/NavItems.tsx b/src/components/NavItems.tsx index 8163d38a..2f0f9843 100644 --- a/src/components/NavItems.tsx +++ b/src/components/NavItems.tsx @@ -164,6 +164,9 @@ export default function NavItems(props: NavItemsProps) { } sx={{pl: 4}} /> + + + } /> ); } diff --git a/src/pages/users/DiffUsers.tsx b/src/pages/users/DiffUsers.tsx index fa094014..7d30c09a 100644 --- a/src/pages/users/DiffUsers.tsx +++ b/src/pages/users/DiffUsers.tsx @@ -13,59 +13,319 @@ import { Divider, Autocomplete, TextField, + Button, + Grid, } from '@mui/material'; -import {useQuery} from '@tanstack/react-query'; -import {useGetAllUsers} from '../../api/apiComponents'; +import {useGetAllUsers, useGetUserById} from '../../api/apiComponents'; import {OktaUser, OktaUserGroupMember} from '../../api/apiSchemas'; -interface User { - id: string; - name: string; - email: string; - memberships: Membership[]; -} +const groupTypeLabels = { + role_group: 'Role Group', + app_group: 'App Group', + okta_group: 'Okta Group', +}; -interface Membership { - id: string; - name: string; - type: 'group' | 'role'; - description?: string; -} +const findCommonMemberships = (user1: OktaUser, user2: OktaUser): OktaUserGroupMember[] => { + if (user1.id === user2.id) { + return user1?.active_group_memberships || []; + } + + if (!user1?.active_group_memberships?.length || !user2?.active_group_memberships?.length) { + console.log('DEBUG - One user has no memberships, returning empty array'); + return []; + } + + const commonMemberships: OktaUserGroupMember[] = []; + + for (const membership1 of user1.active_group_memberships) { + const groupId1 = membership1.group?.id || membership1.active_group?.id; -const fetchUser = async (userId: string): Promise => { - const response = await fetch(`/api/users/${userId}`); - if (!response.ok) { - throw new Error(`Failed to fetch user ${userId}`); + if (!groupId1) { + continue; + } + // Check if user2 has a membership with the same group ID + const hasCommonGroup = user2.active_group_memberships.some((membership2) => { + const groupId2 = membership2.group?.id || membership2.active_group?.id; + return groupId1 === groupId2; + }); + + if (hasCommonGroup) { + commonMemberships.push(membership1); + } } - return response.json(); + return commonMemberships; }; -const findCommonMemberships = (user1: OktaUser, user2: OktaUser): OktaUserGroupMember[] => { - const user1MembershipIds = new Set(user1?.active_group_memberships?.map((m) => m.id) ?? []); - return user2?.active_group_memberships?.filter((membership) => user1MembershipIds.has(membership.id)) ?? []; +// Function to find groups where both users have ownerships +const findCommonOwnerships = (user1: OktaUser, user2: OktaUser): OktaUserGroupMember[] => { + if (user1.id === user2.id) { + return user1?.active_group_ownerships || []; + } + + if (!user1?.active_group_ownerships?.length || !user2?.active_group_ownerships?.length) { + return []; + } + + const commonOwnerships: OktaUserGroupMember[] = []; + + for (const ownership1 of user1.active_group_ownerships) { + const groupId1 = ownership1.group?.id || ownership1.active_group?.id; + + if (!groupId1) { + continue; + } + + const hasCommonGroup = user2.active_group_ownerships.some((ownership2) => { + const groupId2 = ownership2.group?.id || ownership2.active_group?.id; + return groupId1 === groupId2; + }); + + if (hasCommonGroup) { + commonOwnerships.push(ownership1); + } + } + + return commonOwnerships; }; +// Function to find memberships unique to user1 +const findUniqueMembershipsUser1 = (user1: OktaUser, user2: OktaUser): OktaUserGroupMember[] => { + if (!user1?.active_group_memberships?.length) { + return []; + } + + // If user2 has no memberships, all of user1's memberships are unique + if (!user2?.active_group_memberships?.length) { + return user1.active_group_memberships; + } + + // Collect all group IDs from user2 for comparison + const user2GroupIds = new Set(); + + user2.active_group_memberships.forEach((membership) => { + const groupId = membership.group?.id || membership.active_group?.id; + if (groupId) { + user2GroupIds.add(groupId); + } + }); + + // Return user1's memberships that are not in user2's groups + return user1.active_group_memberships.filter((membership) => { + const groupId = membership.group?.id || membership.active_group?.id; + return groupId && !user2GroupIds.has(groupId); + }); +}; + +// Function to find memberships unique to user2 +const findUniqueMembershipsUser2 = (user1: OktaUser, user2: OktaUser): OktaUserGroupMember[] => { + if (!user2?.active_group_memberships?.length) { + return []; + } + + // If user1 has no memberships, all of user2's memberships are unique + if (!user1?.active_group_memberships?.length) { + return user2.active_group_memberships; + } + + // Collect all group IDs from user1 for comparison + const user1GroupIds = new Set(); + + user1.active_group_memberships.forEach((membership) => { + const groupId = membership.group?.id || membership.active_group?.id; + if (groupId) { + user1GroupIds.add(groupId); + } + }); + + // Return user2's memberships that are not in user1's groups + return user2.active_group_memberships.filter((membership) => { + const groupId = membership.group?.id || membership.active_group?.id; + return groupId && !user1GroupIds.has(groupId); + }); +}; + +// Function to find ownerships unique to user1 +const findUniqueOwnershipsUser1 = (user1: OktaUser, user2: OktaUser): OktaUserGroupMember[] => { + if (!user1?.active_group_ownerships?.length) { + return []; + } + + // If user2 has no ownerships, all of user1's ownerships are unique + if (!user2?.active_group_ownerships?.length) { + return user1.active_group_ownerships; + } + + // Collect all group IDs from user2 for comparison + const user2GroupIds = new Set(); + + user2.active_group_ownerships.forEach((ownership) => { + const groupId = ownership.group?.id || ownership.active_group?.id; + if (groupId) { + user2GroupIds.add(groupId); + } + }); + + // Return user1's ownerships that are not in user2's groups + return user1.active_group_ownerships.filter((ownership) => { + const groupId = ownership.group?.id || ownership.active_group?.id; + return groupId && !user2GroupIds.has(groupId); + }); +}; + +// Function to find ownerships unique to user2 +const findUniqueOwnershipsUser2 = (user1: OktaUser, user2: OktaUser): OktaUserGroupMember[] => { + if (!user2?.active_group_ownerships?.length) { + return []; + } + + // If user1 has no ownerships, all of user2's ownerships are unique + if (!user1?.active_group_ownerships?.length) { + return user2.active_group_ownerships; + } + + // Collect all group IDs from user1 for comparison + const user1GroupIds = new Set(); + + user1.active_group_ownerships.forEach((ownership) => { + const groupId = ownership.group?.id || ownership.active_group?.id; + if (groupId) { + user1GroupIds.add(groupId); + } + }); + + // Return user2's ownerships that are not in user1's groups + return user2.active_group_ownerships.filter((ownership) => { + const groupId = ownership.group?.id || ownership.active_group?.id; + return groupId && !user1GroupIds.has(groupId); + }); +}; + +interface MembershipListProps { + memberships: OktaUserGroupMember[]; + emptyMessage: string; +} + +function MembershipList({memberships, emptyMessage}: MembershipListProps) { + if (memberships.length === 0) { + return ( + + {emptyMessage} + + ); + } + + return ( + + {memberships.map((membership, index) => { + const groupId = membership.group?.id || membership.active_group?.id; + const groupName = membership.group?.name || membership.active_group?.name; + const groupType = membership.group?.type || membership.active_group?.type; + + return ( + + + + {groupName || 'Unnamed Group'} + + + } + secondary={membership.group?.description || membership.active_group?.description || ''} + /> + + {index < memberships.length - 1 && } + + ); + })} + + ); +} + export default function DiffUsers() { - const [userId1, setUserId1] = React.useState(null); - const [userId2, setUserId2] = React.useState(null); + // Store the basic user selections from the dropdown const [selectedUser1, setSelectedUser1] = React.useState(null); const [selectedUser2, setSelectedUser2] = React.useState(null); + const [isSameUser, setIsSameUser] = React.useState(false); + const [tabValue, setTabValue] = React.useState(0); - if (!userId1 || !userId2) { - const {data, isLoading, error} = useGetAllUsers({}); - if (isLoading) { - return ( - - - - ); - } - if (error) { - return ( - Error loading users: {error instanceof Error ? error.message : 'Unknown error'} - ); + // Fetch all users for the dropdown options + const {data: allUsers, isLoading: isLoadingAllUsers, error: errorAllUsers} = useGetAllUsers({}); + + // Fetch detailed user data after selection + const { + data: user1Details, + isLoading: isLoadingUser1, + error: errorUser1, + } = useGetUserById( + { + pathParams: {userId: selectedUser1?.id || ''}, + }, + { + enabled: !!selectedUser1?.id && !!selectedUser2?.id, // Only fetch when both users are selected + }, + ); + + const { + data: user2Details, + isLoading: isLoadingUser2, + error: errorUser2, + } = useGetUserById( + { + pathParams: {userId: selectedUser2?.id || ''}, + }, + { + enabled: !!selectedUser1?.id && !!selectedUser2?.id, // Only fetch when both users are selected + }, + ); + + const isLoading = isLoadingAllUsers || (selectedUser1 && isLoadingUser1) || (selectedUser2 && isLoadingUser2); + const error = errorAllUsers || errorUser1 || errorUser2; + + // Reset the comparison view + const resetComparison = () => { + setSelectedUser1(null); + setSelectedUser2(null); + setIsSameUser(false); + }; + + // Handle tab change + const handleTabChange = (event: React.SyntheticEvent, newValue: number) => { + setTabValue(newValue); + }; + + // Update isSameUser flag whenever selections change + React.useEffect(() => { + if (selectedUser1 && selectedUser2) { + setIsSameUser(selectedUser1.id === selectedUser2.id); + } else { + setIsSameUser(false); } + }, [selectedUser1, selectedUser2]); + + if (error) { + return ( + Error loading users: {error instanceof Error ? error.message : 'Unknown error'} + ); + } + + if (isLoadingAllUsers) { + return ( + + + + ); + } + // Show user selection screen if no users selected or if detailed data is not loaded + if (!selectedUser1 || !selectedUser2 || !user1Details || !user2Details) { return ( @@ -82,7 +342,7 @@ export default function DiffUsers() { First User `${option.first_name} ${option.last_name} (${option.email})`} renderInput={(params) => ( { if (value) { - // Navigate to comparison with selected user - // You'll need to implement navigation logic here - console.log('Selected user:', value); - setUserId1(value.id); + setSelectedUser1(value); } }} renderOption={(props, option) => ( @@ -123,12 +380,12 @@ export default function DiffUsers() { Second User `${option.first_name} ${option.last_name} (${option.email})`} renderInput={(params) => ( { if (value) { - // Navigate to comparison with selected user - // You'll need to implement navigation logic here - console.log('Selected user:', value); - setUserId2(value.id); + setSelectedUser2(value); } }} renderOption={(props, option) => ( @@ -161,30 +415,8 @@ export default function DiffUsers() { ); } - const { - data: fetchUserId1, - isLoading: isLoadingUser1, - error: errorUser1, - } = useQuery({ - queryKey: ['user', userId1], - queryFn: () => fetchUser(userId1!), - enabled: !!userId1, - }); - - const { - data: fetchUserId2, - isLoading: isLoadingUser2, - error: errorUser2, - } = useQuery({ - queryKey: ['user', userId2], - queryFn: () => fetchUser(userId2!), - enabled: !!userId2, - }); - - const isLoading = isLoadingUser1 || isLoadingUser2; - const error = errorUser1 || errorUser2; - if (isLoading) { + if (isLoadingUser1 || isLoadingUser2) { return ( @@ -192,87 +424,188 @@ export default function DiffUsers() { ); } - if (error) { - return ( - Error loading users: {error instanceof Error ? error.message : 'Unknown error'} - ); - } + // Calculate common and unique memberships/ownerships + const commonMemberships = findCommonMemberships(user1Details, user2Details); + const commonOwnerships = findCommonOwnerships(user1Details, user2Details); + + const uniqueMembershipsUser1 = findUniqueMembershipsUser1(user1Details, user2Details); + const uniqueMembershipsUser2 = findUniqueMembershipsUser2(user1Details, user2Details); - const commonMemberships = findCommonMemberships(fetchUserId1, fetchUserId2); + const uniqueOwnershipsUser1 = findUniqueOwnershipsUser1(user1Details, user2Details); + const uniqueOwnershipsUser2 = findUniqueOwnershipsUser2(user1Details, user2Details); + + // Log user properties for debugging + console.log('User comparison data:', { + user1: user1Details, + user2: user2Details, + commonMembershipsCount: commonMemberships.length, + commonOwnershipsCount: commonOwnerships.length, + }); return ( - User Membership Comparison + User Access Comparison + + + {isSameUser && ( + + Same user selected in both dropdowns. Showing all access. + + )} + + - {selectedUser1?.first_name} {selectedUser1?.last_name} + {user1Details.first_name} {user1Details.last_name} - {selectedUser1?.email} - - - Total memberships: {selectedUser1?.active_group_memberships?.length} + {user1Details.email} + + + Group memberships: {user1Details.active_group_memberships?.length || 0} + + + Group ownerships: {user1Details.active_group_ownerships?.length || 0} + + - {selectedUser2?.first_name} {selectedUser2?.last_name} + {user2Details.first_name} {user2Details.last_name} - {selectedUser2?.email} - - - Total memberships: {selectedUser2?.active_group_memberships?.length} + {user2Details.email} + + + Group memberships: {user2Details.active_group_memberships?.length || 0} + + + Group ownerships: {user2Details.active_group_ownerships?.length || 0} + + - - - - Common Memberships ({commonMemberships.length}) - + {/* Common Groups Section */} + + Common Access + + + {/* Common Memberships */} + + + + + {isSameUser ? 'All Memberships' : 'Common Memberships'} ({commonMemberships.length}) + + + + + - {commonMemberships.length === 0 ? ( - - No common memberships found between these users. - - ) : ( - - {commonMemberships.map((membership, index) => ( - - - - {membership.group.name} - - - } - secondary={membership.group.description} - /> - - {index < commonMemberships.length - 1 && } - - ))} - - )} - - + {/* Common Ownerships */} + + + + + {isSameUser ? 'All Ownerships' : 'Common Ownerships'} ({commonOwnerships.length}) + + + + + + + + {/* Unique Memberships Section */} + + Unique Memberships + + + {/* User 1's Unique Memberships */} + + + + + {user1Details.first_name}'s Unique Memberships ({uniqueMembershipsUser1.length}) + + + + + + + {/* User 2's Unique Memberships */} + + + + + {user2Details.first_name}'s Unique Memberships ({uniqueMembershipsUser2.length}) + + + + + + + + {/* Unique Ownerships Section */} + + Unique Ownerships + + + {/* User 1's Unique Ownerships */} + + + + + {user1Details.first_name}'s Unique Ownerships ({uniqueOwnershipsUser1.length}) + + + + + + + {/* User 2's Unique Ownerships */} + + + + + {user2Details.first_name}'s Unique Ownerships ({uniqueOwnershipsUser2.length}) + + + + + + ); } From 1219ab94d37c3ee18839dc9ddd010a2b26f612cf Mon Sep 17 00:00:00 2001 From: Jill Date: Fri, 6 Jun 2025 09:20:53 -0700 Subject: [PATCH 3/3] duplicate code cleanup, updated styles --- src/pages/users/DiffUsers.tsx | 158 ++++++++++++---------------------- 1 file changed, 53 insertions(+), 105 deletions(-) diff --git a/src/pages/users/DiffUsers.tsx b/src/pages/users/DiffUsers.tsx index 7d30c09a..14c5804c 100644 --- a/src/pages/users/DiffUsers.tsx +++ b/src/pages/users/DiffUsers.tsx @@ -4,6 +4,7 @@ import { Card, CardContent, Typography, + Link, List, ListItem, ListItemText, @@ -18,6 +19,7 @@ import { } from '@mui/material'; import {useGetAllUsers, useGetUserById} from '../../api/apiComponents'; import {OktaUser, OktaUserGroupMember} from '../../api/apiSchemas'; +import {Link as RouterLink} from 'react-router-dom'; const groupTypeLabels = { role_group: 'Role Group', @@ -88,115 +90,59 @@ const findCommonOwnerships = (user1: OktaUser, user2: OktaUser): OktaUserGroupMe return commonOwnerships; }; -// Function to find memberships unique to user1 -const findUniqueMembershipsUser1 = (user1: OktaUser, user2: OktaUser): OktaUserGroupMember[] => { - if (!user1?.active_group_memberships?.length) { +// Function to find memberships, left join on primary +const findUniqueMemberships = (primary: OktaUser, compareUser: OktaUser): OktaUserGroupMember[] => { + if (!primary?.active_group_memberships?.length) { return []; } - // If user2 has no memberships, all of user1's memberships are unique - if (!user2?.active_group_memberships?.length) { - return user1.active_group_memberships; + // If compareUser has no memberships, all of primary's memberships are unique + if (!compareUser?.active_group_memberships?.length) { + return primary.active_group_memberships; } - // Collect all group IDs from user2 for comparison - const user2GroupIds = new Set(); + // Collect all group IDs from compareUser for comparison + const compareUserGroupIds = new Set(); - user2.active_group_memberships.forEach((membership) => { + compareUser.active_group_memberships.forEach((membership) => { const groupId = membership.group?.id || membership.active_group?.id; if (groupId) { - user2GroupIds.add(groupId); + compareUserGroupIds.add(groupId); } }); - // Return user1's memberships that are not in user2's groups - return user1.active_group_memberships.filter((membership) => { + // Return primary's memberships that are not in compareUser's groups + return primary.active_group_memberships.filter((membership) => { const groupId = membership.group?.id || membership.active_group?.id; - return groupId && !user2GroupIds.has(groupId); + return groupId && !compareUserGroupIds.has(groupId); }); }; -// Function to find memberships unique to user2 -const findUniqueMembershipsUser2 = (user1: OktaUser, user2: OktaUser): OktaUserGroupMember[] => { - if (!user2?.active_group_memberships?.length) { +// Function to find unique ownerships, left join on primary +const findUniqueOwnerships = (primary: OktaUser, compareUser: OktaUser): OktaUserGroupMember[] => { + if (!primary?.active_group_ownerships?.length) { return []; } - // If user1 has no memberships, all of user2's memberships are unique - if (!user1?.active_group_memberships?.length) { - return user2.active_group_memberships; + // If compareUser has no ownerships, all of primary's ownerships are unique + if (!compareUser?.active_group_ownerships?.length) { + return primary.active_group_ownerships; } - // Collect all group IDs from user1 for comparison - const user1GroupIds = new Set(); + // Collect all group IDs from compareUser for comparison + const compareUserGroupIds = new Set(); - user1.active_group_memberships.forEach((membership) => { - const groupId = membership.group?.id || membership.active_group?.id; - if (groupId) { - user1GroupIds.add(groupId); - } - }); - - // Return user2's memberships that are not in user1's groups - return user2.active_group_memberships.filter((membership) => { - const groupId = membership.group?.id || membership.active_group?.id; - return groupId && !user1GroupIds.has(groupId); - }); -}; - -// Function to find ownerships unique to user1 -const findUniqueOwnershipsUser1 = (user1: OktaUser, user2: OktaUser): OktaUserGroupMember[] => { - if (!user1?.active_group_ownerships?.length) { - return []; - } - - // If user2 has no ownerships, all of user1's ownerships are unique - if (!user2?.active_group_ownerships?.length) { - return user1.active_group_ownerships; - } - - // Collect all group IDs from user2 for comparison - const user2GroupIds = new Set(); - - user2.active_group_ownerships.forEach((ownership) => { - const groupId = ownership.group?.id || ownership.active_group?.id; - if (groupId) { - user2GroupIds.add(groupId); - } - }); - - // Return user1's ownerships that are not in user2's groups - return user1.active_group_ownerships.filter((ownership) => { - const groupId = ownership.group?.id || ownership.active_group?.id; - return groupId && !user2GroupIds.has(groupId); - }); -}; - -// Function to find ownerships unique to user2 -const findUniqueOwnershipsUser2 = (user1: OktaUser, user2: OktaUser): OktaUserGroupMember[] => { - if (!user2?.active_group_ownerships?.length) { - return []; - } - - // If user1 has no ownerships, all of user2's ownerships are unique - if (!user1?.active_group_ownerships?.length) { - return user2.active_group_ownerships; - } - - // Collect all group IDs from user1 for comparison - const user1GroupIds = new Set(); - - user1.active_group_ownerships.forEach((ownership) => { + compareUser.active_group_ownerships.forEach((ownership) => { const groupId = ownership.group?.id || ownership.active_group?.id; if (groupId) { - user1GroupIds.add(groupId); + compareUserGroupIds.add(groupId); } }); - // Return user2's ownerships that are not in user1's groups - return user2.active_group_ownerships.filter((ownership) => { + // Return primary's ownerships that are not in compareUser's groups + return primary.active_group_ownerships.filter((ownership) => { const groupId = ownership.group?.id || ownership.active_group?.id; - return groupId && !user1GroupIds.has(groupId); + return groupId && !compareUserGroupIds.has(groupId); }); }; @@ -205,7 +151,7 @@ interface MembershipListProps { emptyMessage: string; } -function MembershipList({memberships, emptyMessage}: MembershipListProps) { +const MembershipList = ({memberships, emptyMessage}: MembershipListProps) => { if (memberships.length === 0) { return ( @@ -227,7 +173,24 @@ function MembershipList({memberships, emptyMessage}: MembershipListProps) { - {groupName || 'Unnamed Group'} + + + {groupName ?? 'Unnamed Group'} + + ); -} +}; export default function DiffUsers() { // Store the basic user selections from the dropdown const [selectedUser1, setSelectedUser1] = React.useState(null); const [selectedUser2, setSelectedUser2] = React.useState(null); const [isSameUser, setIsSameUser] = React.useState(false); - const [tabValue, setTabValue] = React.useState(0); // Fetch all users for the dropdown options const {data: allUsers, isLoading: isLoadingAllUsers, error: errorAllUsers} = useGetAllUsers({}); @@ -286,7 +248,6 @@ export default function DiffUsers() { }, ); - const isLoading = isLoadingAllUsers || (selectedUser1 && isLoadingUser1) || (selectedUser2 && isLoadingUser2); const error = errorAllUsers || errorUser1 || errorUser2; // Reset the comparison view @@ -296,11 +257,6 @@ export default function DiffUsers() { setIsSameUser(false); }; - // Handle tab change - const handleTabChange = (event: React.SyntheticEvent, newValue: number) => { - setTabValue(newValue); - }; - // Update isSameUser flag whenever selections change React.useEffect(() => { if (selectedUser1 && selectedUser2) { @@ -428,19 +384,11 @@ export default function DiffUsers() { const commonMemberships = findCommonMemberships(user1Details, user2Details); const commonOwnerships = findCommonOwnerships(user1Details, user2Details); - const uniqueMembershipsUser1 = findUniqueMembershipsUser1(user1Details, user2Details); - const uniqueMembershipsUser2 = findUniqueMembershipsUser2(user1Details, user2Details); + const uniqueMembershipsUser1 = findUniqueMemberships(user1Details, user2Details); + const uniqueMembershipsUser2 = findUniqueMemberships(user2Details, user1Details); - const uniqueOwnershipsUser1 = findUniqueOwnershipsUser1(user1Details, user2Details); - const uniqueOwnershipsUser2 = findUniqueOwnershipsUser2(user1Details, user2Details); - - // Log user properties for debugging - console.log('User comparison data:', { - user1: user1Details, - user2: user2Details, - commonMembershipsCount: commonMemberships.length, - commonOwnershipsCount: commonOwnerships.length, - }); + const uniqueOwnershipsUser1 = findUniqueOwnerships(user1Details, user2Details); + const uniqueOwnershipsUser2 = findUniqueOwnerships(user2Details, user1Details); return ( @@ -449,7 +397,7 @@ export default function DiffUsers() { - {isSameUser && (