Skip to content

[Apps] New accordion groups, autocomplete search to filter by existing set of all users with App permissions #172

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 52 commits into from
Jun 30, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
52 commits
Select commit Hold shift + click to select a range
45292a0
Access user guide: collapse everything but current accordion on click…
savathoon Oct 28, 2024
66b3d65
break out app group header component for readability
savathoon Oct 28, 2024
0ed08ee
update paths, set min width on header to prevent icon overlap on smal…
savathoon Oct 28, 2024
c440947
Break out admin action button group into sub component on apps page, …
savathoon Oct 28, 2024
2f7c44d
App detail: remove duplicate components, replace with EmptyListEntry …
savathoon Oct 29, 2024
f63e77d
replace duplicate empty list entries with emptylistentry component
savathoon Oct 29, 2024
d4a6ec0
Add props for adding custom empty list text
savathoon Oct 29, 2024
1719825
App page: initial demo accordion, fix missing admin group button
savathoon Oct 29, 2024
813fcd4
[feat] dark mode and theme toggle (#174)
amyjchen Oct 29, 2024
3173e61
make theme toggle minimize when nav is minimized
amyjchen Oct 29, 2024
fc557e5
figure out why table rows are not 100% width with accordion component
savathoon Oct 30, 2024
082cbd1
full-width accordion list
savathoon Oct 30, 2024
7367f5f
add demo of owner group card for accordion list
savathoon Oct 30, 2024
4ed8ee6
Merge branch '2024-hackweek' into sava/more-components
savathoon Oct 30, 2024
3c51142
[Group Detail] Update the groups member page to add spacing and wrap …
savathoon Oct 30, 2024
fed33e7
Merge branch '2024-hackweek' into sava/more-components
savathoon Oct 30, 2024
ecac23f
[feat] Table Top Bar Component (#177)
amyjchen Oct 30, 2024
fa37b58
figure out why this thing hates being full width of the parent compon…
savathoon Oct 30, 2024
cdfa264
Merge branch '2024-hackweek' into sava/more-components
savathoon Oct 30, 2024
1aa0aa8
update owner group accordion on app group page
savathoon Oct 30, 2024
a50ceaa
[fix] update user avatars to have adequate contrast across themes (#178)
amyjchen Oct 31, 2024
01f5b77
[fix] dark mode bugs (#179)
amyjchen Oct 31, 2024
9015a9c
fix missing key
amyjchen Oct 31, 2024
2a3f548
v0 of trying to filter members down to unique in list groups
savathoon Oct 31, 2024
8809feb
merge
savathoon Oct 31, 2024
c0988d4
flex jank
savathoon Oct 31, 2024
d1158ad
merge back changes in app/detail/index
savathoon Oct 31, 2024
166b4d2
controlled accordion but only kinda
savathoon Oct 31, 2024
69727e4
fix imports
savathoon Nov 1, 2024
f4b3dbc
autocomplete with members
savathoon Nov 1, 2024
fe70d9c
autocomplete
savathoon Nov 1, 2024
437bdae
add back dedupe
savathoon Nov 1, 2024
418f806
add optional header description to accordion list group
savathoon Nov 1, 2024
be55f39
[feat] Update Headers (#180)
amyjchen Nov 1, 2024
f43bcfd
[Groups] Add styles to wrap group name in header (#183)
savathoon Nov 1, 2024
fb5bc98
merge
savathoon Nov 1, 2024
e5108aa
merge conflict fixings
savathoon Nov 1, 2024
8e46b5d
Apps: user filter options now sorted
savathoon Nov 1, 2024
fc0e6e0
Hide title and description if accordion list is empty
savathoon Nov 1, 2024
3bdc926
update copy
savathoon Nov 1, 2024
dfaf96c
oops
savathoon Nov 1, 2024
387a776
merge main
savathoon Jan 3, 2025
8245737
Update type signature of groupMemberships function, clean up imports …
savathoon Jan 3, 2025
db56d3e
update formatting, add freeSolo to autocomplete
savathoon Jan 3, 2025
1bc5905
Merge branch 'main' into sava/more-components
savathoon Jun 26, 2025
a96e4e1
add expand/collapse all button
savathoon Jun 26, 2025
c024374
fix search parsing after removing email
savathoon Jun 26, 2025
5804703
Merge branch 'main' into sava/more-components
savathoon Jun 30, 2025
0e9b959
PR: move email extract from display name to helpers
savathoon Jun 30, 2025
b611f38
Update src/pages/apps/components/AppsAccordionListGroup.tsx
savathoon Jun 30, 2025
2a5b06a
Update src/pages/tags/Read.tsx
savathoon Jun 30, 2025
8ce2383
prettier
savathoon Jun 30, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions src/helpers.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,14 @@ export function displayUserName(user: OktaUser | undefined) {
return user.display_name != null ? user.display_name : user.first_name + ' ' + user.last_name;
}

export function extractEmailFromDisplayName(displayName: string | null) {
if (!!displayName) {
const emailMatch = displayName.match(/\(([^)]+)\)/);
return emailMatch ? emailMatch[1].toLowerCase() : '';
}
return '';
}

// https://stackoverflow.com/a/34890276
export function groupBy<T>(xs: T[] | undefined, keyFn: (item: T) => string | undefined) {
return (xs ?? []).reduce(
Expand Down
466 changes: 43 additions & 423 deletions src/pages/apps/Read.tsx

Large diffs are not rendered by default.

233 changes: 233 additions & 0 deletions src/pages/apps/components/AppsAccordionListGroup.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,233 @@
import {
Accordion,
AccordionDetails,
AccordionSummary,
Box,
Divider,
Grid,
Link,
Paper,
Stack,
Table,
TableBody,
TableCell,
TableContainer,
TableFooter,
TableHead,
TableRow,
Typography,
} from '@mui/material';
import {App, AppGroup, OktaUserGroupMember} from '../../../api/apiSchemas';
import React from 'react';
import {displayUserName, groupBy, groupMemberships, sortGroupMembers} from '../../../helpers';
import {EmptyListEntry} from '../../../components/EmptyListEntry';
import Ending from '../../../components/Ending';
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
import {Link as RouterLink, useParams} from 'react-router-dom';

interface GroupDetailListProps {
member_list: any[];
title?: string;
}

const GroupDetailList: React.FC<GroupDetailListProps> = ({member_list, title}) => {
return (
<Stack direction="column" spacing={1}>
{title && (
<Typography variant="body1" component={'div'}>
{title}
</Typography>
)}

<TableContainer component={Paper}>
<Table sx={{minWidth: 325}} size="small" aria-label="app owners">
<TableHead>
<TableRow>
<TableCell>Name</TableCell>
<TableCell>Email</TableCell>
<TableCell>Ending</TableCell>
</TableRow>
</TableHead>
<TableBody>
{member_list.length > 0 ? (
member_list.map((member: OktaUserGroupMember) => (
<TableRow key={member.active_user?.id}>
<TableCell>
<Link
to={`/users/${member.active_user?.email.toLowerCase()}`}
sx={{
textDecoration: 'none',
color: 'inherit',
}}
component={RouterLink}>
{displayUserName(member.active_user)}
</Link>
</TableCell>
<TableCell>
<Link
to={`/users/${member.active_user?.email.toLowerCase()}`}
sx={{
textDecoration: 'none',
color: 'inherit',
}}
component={RouterLink}>
{member.active_user?.email.toLowerCase()}
</Link>
</TableCell>
<TableCell>
<Ending memberships={member_list} />
</TableCell>
</TableRow>
))
) : (
<EmptyListEntry cellProps={{colSpan: 3}} />
)}
</TableBody>

<TableFooter>
<TableRow />
</TableFooter>
</Table>
</TableContainer>
</Stack>
);
};

interface AppAccordionListGroupProps {
app_group: AppGroup[];
list_group_title?: string;
list_group_description?: string;
isExpanded?: boolean;
}

export const AppsAccordionListGroup: React.FC<AppAccordionListGroupProps> = ({
app_group,
list_group_title,
list_group_description,
isExpanded = false,
}) => {
const [expanded, setExpanded] = React.useState<Record<string, boolean>>(() => {
if (isExpanded && app_group) {
const initialExpanded: Record<string, boolean> = {};
app_group.forEach((group) => {
initialExpanded[group.name] = true;
});
return initialExpanded;
}
return {};
});

// Sync internal state with isExpanded prop changes
React.useEffect(() => {
if (app_group) {
const newExpanded: Record<string, boolean> = {};
app_group.forEach((group) => {
newExpanded[group.name] = isExpanded;
});
setExpanded(newExpanded);
}
}, [isExpanded, app_group]);

const handleChange = (id: string) => (event: React.SyntheticEvent, newExpanded: boolean) => {
setExpanded({...expanded, [id]: newExpanded});
};

return (
<React.Fragment>
<Grid
item
xs={12}
sx={{
display: 'flex',
flexDirection: 'column',
gap: '1.25rem',
}}>
{(app_group?.length || 0) > 0 && list_group_title && (
<Typography variant="h5" component={'div'}>
{list_group_title}
</Typography>
)}
{(app_group?.length || 0) > 0 && list_group_description && (
<Typography variant="body1" component={'div'}>
{list_group_description}
</Typography>
)}
{app_group &&
app_group?.map((appGroup) => {
const owners = Object.entries(groupMemberships(appGroup.active_user_ownerships))
.sort(sortGroupMembers)
.map((memberList) => memberList[1][0]);

const members = Object.entries(groupMemberships(appGroup.active_user_memberships))
.sort(sortGroupMembers)
.map((memberList) => memberList[1][0]);

return (
<TableContainer key={appGroup.id} component={Paper}>
<Accordion expanded={expanded[appGroup.name] || isExpanded} onChange={handleChange(appGroup.name)}>
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
<Box
sx={{
display: 'inline-flex',
flexGrow: 1,
}}>
<Stack
direction="column"
spacing={1}
sx={{
flexGrow: 0.95,
}}>
<Typography variant="h6" color="primary">
<Link
to={`/groups/${appGroup.name}`}
sx={{
textDecoration: 'none',
color: 'inherit',
}}
component={RouterLink}>
{appGroup.name}
</Link>
</Typography>
<Typography variant="body1" color="grey">
{appGroup.description}
</Typography>
</Stack>
<Box
sx={{
display: 'flex',
justifyContent: 'flex-end',
alignItems: 'right',
}}>
<Divider sx={{mx: 2}} orientation="vertical" flexItem />
Owners: {owners.length || 0} <br />
Members: {members.length || 0}
</Box>
</Box>
</AccordionSummary>
<AccordionDetails>
<Table aria-label="app group owners">
<TableBody className="accordion-body">
<TableRow>
<TableCell colSpan={2}>
<Stack
direction="row"
useFlexGap
flexWrap={'wrap'}
justifyContent={'space-between'}
gap={'2rem'}>
<GroupDetailList member_list={owners} title={'Group Owners'} />
<GroupDetailList member_list={members} title={'Members'} />
</Stack>
</TableCell>
</TableRow>
</TableBody>
</Table>
</AccordionDetails>
</Accordion>
</TableContainer>
);
})}
</Grid>
</React.Fragment>
);
};
92 changes: 92 additions & 0 deletions src/pages/apps/components/AppsAdminActionGroup.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import {Autocomplete, Button, Grid, Paper, Stack, TextField} from '@mui/material';
import CreateUpdateGroup from '../../groups/CreateUpdate';
import {OktaUser, App, AppGroup} from '../../../api/apiSchemas';
import {renderUserOption} from '../../../components/TableTopBar';
import {displayUserName, extractEmailFromDisplayName, sortGroupMemberRecords, sortGroupMembers} from '../../../helpers';
import React from 'react';

interface AppsAdminActionGroupProps {
currentUser: OktaUser;
app: App;
onSearchSubmit?: (appGroup: AppGroup[]) => void;
onToggleExpand?: (expanded: boolean) => void;
isExpanded?: boolean;
}

export const AppsAdminActionGroup: React.FC<AppsAdminActionGroupProps> = ({
currentUser,
app,
onSearchSubmit,
onToggleExpand,
isExpanded = true,
}) => {
const allMembers: Record<string, OktaUser> = {};
const memberGroups: Record<string, AppGroup[]> = {};
(app.active_non_owner_app_groups ?? []).forEach((appGroup) => {
[appGroup.active_user_ownerships, appGroup.active_user_memberships].forEach((memberList) => {
(memberList ?? []).forEach((member) => {
const activeUser = member.active_user;
if (activeUser) {
allMembers[activeUser.id] = activeUser;
const groups = (memberGroups[activeUser.email.toLowerCase()] ||= []);
if (
!groups.find((g) => {
return g.id === appGroup.id;
})
) {
groups.push(appGroup);
}
}
});
});
});

const handleSearchSubmit = (_: unknown, newValue: string | null) => {
const email = extractEmailFromDisplayName(newValue);
const appGroups = memberGroups[email] ?? app.active_non_owner_app_groups;
if (!!onSearchSubmit) {
onSearchSubmit(appGroups);
}
};

const handleToggleExpand = () => {
if (!!onToggleExpand) {
onToggleExpand(!isExpanded);
}
};

return (
<Grid item xs={12} className={'app-detail app-detail-admin-action-group'}>
<Paper
sx={{
p: 2,
display: 'flex',
alignItems: 'center',
}}>
<Stack direction="row" width="100%" justifyContent="space-between" alignItems="center">
<CreateUpdateGroup defaultGroupType={'app_group'} currentUser={currentUser} app={app} />
<Stack direction="row" spacing={2} alignItems="center">
<Autocomplete
size="small"
sx={{width: 320}}
renderInput={(params) => <TextField {...params} label="Search" />}
options={sortGroupMemberRecords(allMembers).map(
(row) => `${displayUserName(row)} (${row.email.toLowerCase()})`,
)}
onChange={handleSearchSubmit}
renderOption={renderUserOption}
autoHighlight
autoSelect
clearOnEscape
freeSolo
autoFocus
/>
<Button variant="outlined" size="small" onClick={handleToggleExpand}>
{isExpanded ? 'Collapse All' : 'Expand All'}
</Button>
</Stack>
</Stack>
</Paper>
</Grid>
);
};
Loading