diff --git a/src/App.tsx b/src/App.tsx
index e8df44e..cdbd6a7 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -1,7 +1,7 @@
import * as React from 'react';
import {Link as RouterLink, Route, Routes} from 'react-router-dom';
-import {styled} from '@mui/material/styles';
+import {createTheme, styled, ThemeProvider} from '@mui/material/styles';
import Link from '@mui/material/Link';
import MuiDrawer from '@mui/material/Drawer';
import Box from '@mui/material/Box';
@@ -37,6 +37,19 @@ import ReadTag from './pages/tags/Read';
import ReadUser from './pages/users/Read';
import {useCurrentUser} from './authentication';
import ReadRequest from './pages/requests/Read';
+import {
+ alpha,
+ CssBaseline,
+ PaletteMode,
+ Stack,
+ ToggleButton,
+ ToggleButtonGroup,
+ Tooltip,
+ useMediaQuery,
+ useTheme,
+} from '@mui/material';
+import {DarkMode, LightMode, Monitor} from '@mui/icons-material';
+import {lightGreen, red, yellow} from '@mui/material/colors';
const drawerWidth: number = 240;
@@ -88,14 +101,72 @@ const Drawer = styled(MuiDrawer, {
},
}));
-function Dashboard() {
+function ThemeToggle({setThemeMode, condensed}: {setThemeMode: (theme: PaletteMode) => void; condensed: boolean}) {
+ const [storedTheme, setStoredTheme] = React.useState(
+ localStorage.getItem('user-set-color-scheme') as 'light' | 'dark' | null,
+ );
+ const currentTheme = useTheme();
+ const systemTheme = useMediaQuery('(prefers-color-scheme: dark)') ? 'dark' : 'light';
+
+ const handleThemeOverride = (theme: PaletteMode) => {
+ setThemeMode(theme);
+ localStorage.setItem('user-set-color-scheme', theme);
+ setStoredTheme(theme);
+ };
+
+ const handleSystemDefault = () => {
+ setThemeMode(systemTheme);
+ localStorage.removeItem('user-set-color-scheme');
+ setStoredTheme(null);
+ };
+
+ return (
+
+ {(currentTheme.palette.mode != 'light' || !condensed) && (
+
+ handleThemeOverride('light')}
+ aria-label="Light mode">
+
+
+
+ )}
+ {!condensed && (
+
+
+
+
+
+ )}
+ {(currentTheme.palette.mode != 'dark' || !condensed) && (
+
+ handleThemeOverride('dark')}
+ aria-label="Dark mode">
+
+
+
+ )}
+
+ );
+}
+
+function Dashboard({setThemeMode}: {setThemeMode: (theme: PaletteMode) => void}) {
const [open, setOpen] = React.useState(true);
const toggleDrawer = () => {
setOpen(!open);
};
return (
-
+
-
+
ACCESS
@@ -149,12 +220,15 @@ function Dashboard() {
+
+
+
- theme.palette.mode === 'light' ? theme.palette.grey[100] : theme.palette.grey[900],
+ theme.palette.mode === 'light' ? theme.palette.grey[200] : theme.palette.grey[800],
flexGrow: 1,
height: '100vh',
overflow: 'auto',
@@ -189,6 +263,81 @@ function Dashboard() {
}
export default function App() {
+ const storedTheme = localStorage.getItem('user-set-color-scheme') as 'light' | 'dark' | null;
+ const systemTheme = useMediaQuery('(prefers-color-scheme: dark)') ? 'dark' : 'light';
+ const initialMode = storedTheme ?? systemTheme;
+ const [mode, setMode] = React.useState(initialMode);
+
+ // See https://discord.com/branding
+ let theme = React.useMemo(() => {
+ const base = createTheme({
+ palette: {
+ mode,
+ primary: {
+ main: '#5865F2',
+ light: '#A5B2FF',
+ },
+ secondary: {
+ main: '#EB459E',
+ },
+ error: {
+ main: '#ED4245',
+ },
+ warning: {
+ main: '#FEE75C',
+ },
+ success: {
+ main: '#57F287',
+ },
+ text: {
+ accent: mode === 'light' ? '#5865F2' : '#A5B2FF',
+ },
+ },
+ components: {
+ MuiChip: {
+ styleOverrides: {
+ colorPrimary: ({ownerState, theme}) => ({
+ ...(ownerState.variant === 'outlined' &&
+ ownerState.color === 'primary' && {
+ color: theme.palette.text.accent,
+ borderColor: theme.palette.text.accent,
+ }),
+ }),
+ deleteIcon: ({ownerState, theme}) => ({
+ ...(ownerState.variant === 'outlined' &&
+ ownerState.color === 'primary' && {
+ color: theme.palette.text.accent,
+ }),
+ }),
+ },
+ },
+ },
+ });
+ return createTheme(base, {
+ palette: {
+ highlight: {
+ success: base.palette.augmentColor({
+ color: {main: mode === 'light' ? lightGreen[100] : alpha(lightGreen[500], 0.3)},
+ name: 'success',
+ }),
+ warning: base.palette.augmentColor({
+ color: {main: mode === 'light' ? yellow[100] : alpha(yellow[500], 0.3)},
+ name: 'warning',
+ }),
+ danger: base.palette.augmentColor({
+ color: {main: mode === 'light' ? red[100] : alpha(red[500], 0.3)},
+ name: 'danger',
+ }),
+ },
+ },
+ });
+ }, [mode]);
+
useCurrentUser();
- return ;
+ return (
+
+
+
+
+ );
}
diff --git a/src/components/AvatarButton.tsx b/src/components/AvatarButton.tsx
new file mode 100644
index 0000000..cba0b74
--- /dev/null
+++ b/src/components/AvatarButton.tsx
@@ -0,0 +1,33 @@
+import {Avatar, ButtonBase, Typography} from '@mui/material';
+import {ReactNode} from 'react';
+
+interface AvatarButtonProps {
+ icon: ReactNode;
+ text?: string;
+ strikethrough?: boolean;
+ onClick?: () => void;
+}
+
+export default function AvatarButton({icon, text, strikethrough, onClick}: AvatarButtonProps) {
+ return (
+
+ {icon}
+ {text && (
+
+ {text}
+
+ )}
+
+ );
+}
diff --git a/src/components/Breadcrumbs.tsx b/src/components/Breadcrumbs.tsx
index 2f60a8b..c199dce 100644
--- a/src/components/Breadcrumbs.tsx
+++ b/src/components/Breadcrumbs.tsx
@@ -1,7 +1,7 @@
import Breadcrumbs from '@mui/material/Breadcrumbs';
import Typography from '@mui/material/Typography';
-import Link, {LinkProps} from '@mui/material/Link';
+import Link from '@mui/material/Link';
import {Link as RouterLink, useLocation} from 'react-router-dom';
export default function Crumbs() {
diff --git a/src/components/BulkRenewalDataGrid.tsx b/src/components/BulkRenewalDataGrid.tsx
new file mode 100644
index 0000000..94c11be
--- /dev/null
+++ b/src/components/BulkRenewalDataGrid.tsx
@@ -0,0 +1,44 @@
+import {darken, lighten, PaletteColor, styled} from '@mui/material';
+import {DataGrid} from '@mui/x-data-grid';
+
+const getHoverBackgroundColor = (color: PaletteColor, mode: string) => (mode === 'dark' ? color.dark : color.light);
+
+const getSelectedBackgroundColor = (color: PaletteColor, mode: string) =>
+ mode === 'dark' ? darken(color.dark, 0.5) : lighten(color.light, 0.5);
+
+const getSelectedHoverBackgroundColor = (color: PaletteColor, mode: string) =>
+ mode === 'dark' ? darken(color.dark, 0.4) : lighten(color.light, 0.4);
+
+const BulkRenewalDataGrid = styled(DataGrid)(
+ ({
+ theme: {
+ palette: {highlight, mode},
+ },
+ }) => ({
+ '& .super-app-theme--Expired': {
+ backgroundColor: highlight.danger.main,
+ '&:hover': {
+ backgroundColor: getHoverBackgroundColor(highlight.danger, mode),
+ },
+ '&.Mui-selected': {
+ backgroundColor: getSelectedBackgroundColor(highlight.danger, mode),
+ '&:hover': {
+ backgroundColor: getSelectedHoverBackgroundColor(highlight.danger, mode),
+ },
+ },
+ },
+ '& .super-app-theme--Soon': {
+ backgroundColor: highlight.warning.main,
+ '&:hover': {
+ backgroundColor: getHoverBackgroundColor(highlight.warning, mode),
+ },
+ '&.Mui-selected': {
+ backgroundColor: getSelectedBackgroundColor(highlight.warning, mode),
+ '&:hover': {
+ backgroundColor: getSelectedHoverBackgroundColor(highlight.warning, mode),
+ },
+ },
+ },
+ }),
+);
+export default BulkRenewalDataGrid;
diff --git a/src/components/DateRange.tsx b/src/components/DateRange.tsx
index 852627c..c0a76f5 100644
--- a/src/components/DateRange.tsx
+++ b/src/components/DateRange.tsx
@@ -10,12 +10,10 @@ import EventIcon from '@mui/icons-material/Event';
import {Dayjs} from 'dayjs';
import {DatePicker, DatePickerProps} from '@mui/x-date-pickers/DatePicker';
-import {PickersDay, PickersDayProps, pickersDayClasses} from '@mui/x-date-pickers/PickersDay';
+import {PickersDay, PickersDayProps} from '@mui/x-date-pickers/PickersDay';
import {UseDateFieldProps} from '@mui/x-date-pickers/DateField';
import {BaseSingleInputFieldProps, DateValidationError, FieldSection} from '@mui/x-date-pickers/models';
-import {grey} from '@mui/material/colors';
-
function HighlightDay(props: PickersDayProps & {startDate?: Dayjs; endDate?: Dayjs; rangeSelected: boolean}) {
const {startDate, endDate, rangeSelected, ...rest} = props;
@@ -35,22 +33,26 @@ function HighlightDay(props: PickersDayProps & {startDate?: Dayjs; endDat
let selectedClass = '';
let style = {};
+ let dayStyle = {};
if (rangeSelected && props.day.isSame(endDate!, 'day')) {
selectedClass = 'Mui-selected';
}
if (isSelected) {
- style = {backgroundColor: 'primary_extra_light.main'};
+ style = {backgroundColor: theme.palette.primary.light};
+ dayStyle = {color: theme.palette.common.black};
} else if (start) {
- style = {background: `linear-gradient(90deg, white 50%, ${theme.palette.primary_extra_light.main} 50%)`};
+ style = {
+ background: `linear-gradient(90deg, transparent 50%, ${theme.palette.primary.light} 50%)`,
+ };
} else if (end) {
- style = {background: `linear-gradient(90deg, ${theme.palette.primary_extra_light.main} 50%, white 50%)`};
+ style = {background: `linear-gradient(90deg, ${theme.palette.primary.light} 50%, transparent 50%)`};
}
return (
-
+
);
}
@@ -78,6 +80,7 @@ function ButtonField(props: ButtonFieldProps) {
inputProps: {'aria-label': ariaLabel} = {},
} = props;
+ const theme = useTheme();
let displayString = '';
if (rangeSelected) {
@@ -88,38 +91,38 @@ function ButtonField(props: ButtonFieldProps) {
return (
-
-
-
+
Ending Date Range
diff --git a/src/components/EmptyListEntry.tsx b/src/components/EmptyListEntry.tsx
new file mode 100644
index 0000000..c00f2c3
--- /dev/null
+++ b/src/components/EmptyListEntry.tsx
@@ -0,0 +1,18 @@
+import {TableRow, TableCell, Typography, TableCellProps} from '@mui/material';
+
+interface EmptyListEntryProps {
+ cellProps?: TableCellProps;
+ customText?: string;
+}
+
+export const EmptyListEntry: React.FC = ({cellProps, customText}) => {
+ return (
+
+
+
+ {customText || 'None'}
+
+
+
+ );
+};
diff --git a/src/components/NumberInput.tsx b/src/components/NumberInput.tsx
index b66decc..30132fa 100644
--- a/src/components/NumberInput.tsx
+++ b/src/components/NumberInput.tsx
@@ -5,8 +5,7 @@ import {
NumberInputProps as NumInputProps,
numberInputClasses,
} from '@mui/base/Unstable_NumberInput';
-import {styled} from '@mui/system';
-import {grey} from '@mui/material/colors';
+import {styled} from '@mui/material/styles';
const NumInput = React.forwardRef(function CustomNumberInput(
props: NumInputProps,
@@ -66,7 +65,7 @@ const InputAdornment = styled('div')(
align-items: center;
justify-content: center;
grid-row: 1/3;
- color: ${theme.palette.mode === 'dark' ? grey[500] : grey[700]};
+ color: ${theme.palette.text.secondary};
`,
);
@@ -74,11 +73,8 @@ const InputRoot = styled('div')(
({theme}) => `
font-family: 'IBM Plex Sans', sans-serif;
font-weight: 400;
- border-radius: 8px;
- color: ${theme.palette.mode === 'dark' ? grey[300] : grey[900]};
- background: ${theme.palette.mode === 'dark' ? grey[900] : '#fff'};
- border: 1px solid ${theme.palette.mode === 'dark' ? grey[700] : grey[200]};
- box-shadow: 0px 2px 4px ${theme.palette.mode === 'dark' ? 'rgba(0,0,0, 0.5)' : 'rgba(0,0,0, 0.05)'};
+ border-radius: 4px;
+ border: 1px solid ${theme.palette.action.disabled};
display: grid;
grid-template-columns: auto 1fr auto 19px;
grid-template-rows: 1fr 1fr;
@@ -86,12 +82,12 @@ const InputRoot = styled('div')(
padding: 4px;
&.${numberInputClasses.focused} {
- border-color: ${theme.palette.primary.main};
- box-shadow: 0 0 0 3px ${theme.palette.primary_extra_light.main};
+ border: 1px solid transparent;
+ outline: 2px solid ${theme.palette.primary.main};
}
- &:hover {
- border-color: ${theme.palette.primary.main};
+ &:hover:not(.${numberInputClasses.focused}) {
+ border-color: ${theme.palette.action.active};
}
// firefox
@@ -108,7 +104,7 @@ const InputElement = styled('input')(
font-weight: 400;
line-height: 1.5;
grid-row: 1/3;
- color: ${theme.palette.mode === 'dark' ? grey[300] : grey[900]};
+ color: ${theme.palette.text.primary};
background: inherit;
border: none;
border-radius: inherit;
@@ -131,58 +127,30 @@ const Button = styled('button')(
font-size: 0.875rem;
line-height: 1;
box-sizing: border-box;
- background: ${theme.palette.mode === 'dark' ? grey[900] : 'white'};
- border: 0;
- color: ${theme.palette.mode === 'dark' ? grey[300] : grey[900]};
+ border: 1px solid ${theme.palette.action.disabled};
transition-property: all;
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
transition-duration: 120ms;
+ color: ${theme.palette.text.primary};
+ background-color: transparent;
+ grid-column: 4/5;
&:hover {
- background: ${theme.palette.mode === 'dark' ? grey[800] : grey[50]};
- border-color: ${theme.palette.mode === 'dark' ? grey[600] : grey[300]};
+ background: ${theme.palette.action.hover};
cursor: pointer;
}
&.${numberInputClasses.incrementButton} {
- grid-column: 4/5;
grid-row: 1/2;
border-top-left-radius: 4px;
border-top-right-radius: 4px;
- border: 1px solid;
- border-bottom: 0;
- border-color: ${grey[200]};
- background: ${theme.palette.mode === 'dark' ? grey[900] : undefined};
- color: ${theme.palette.mode === 'dark' ? grey[200] : grey[600]};
-
- &:hover {
- cursor: pointer;
- color: #FFF;
- background: ${theme.palette.primary.main};
- border-color: ${theme.palette.primary.main};
- }
+ border-bottom: 0px;
}
&.${numberInputClasses.decrementButton} {
- grid-column: 4/5;
grid-row: 2/3;
border-bottom-left-radius: 4px;
border-bottom-right-radius: 4px;
- border: 1px solid;
- border-color: ${theme.palette.mode === 'dark' ? grey[700] : grey[200]};
- background: ${theme.palette.mode === 'dark' ? grey[900] : undefined};
- color: ${theme.palette.mode === 'dark' ? grey[200] : grey[600]};
-
- &:hover {
- cursor: pointer;
- color: #FFF;
- background: ${theme.palette.primary.main};
- border-color: ${theme.palette.primary.main};
- }
- }
-
- & .arrow {
- transform: translateY(-1px);
}
& .arrow {
diff --git a/src/components/TableTopBar.tsx b/src/components/TableTopBar.tsx
new file mode 100644
index 0000000..1532201
--- /dev/null
+++ b/src/components/TableTopBar.tsx
@@ -0,0 +1,75 @@
+import {Launch} from '@mui/icons-material';
+import {Autocomplete, AutocompleteProps, Box, Grid, IconButton, Stack, TextField, Typography} from '@mui/material';
+
+import * as React from 'react';
+import {useNavigate} from 'react-router-dom';
+
+export function renderUserOption(props: React.HTMLAttributes, option: any) {
+ const [displayName, email] = option.split(';');
+ return (
+
+
+
+ {displayName}
+
+ {email}
+
+
+
+
+ );
+}
+
+export function TableTopBarAutocomplete({
+ defaultValue,
+ filterOptions = (x) => x,
+ ...restProps
+}: Omit, 'renderInput'>) {
+ return (
+ }
+ {...restProps}
+ />
+ );
+}
+
+interface TableTopBarProps {
+ title: string;
+ link?: string;
+ children?: React.ReactNode;
+}
+
+export default function TableTopBar({title, link, children}: TableTopBarProps) {
+ const navigate = useNavigate();
+ return (
+
+
+
+ {title}
+
+ {link != null && (
+ navigate(link)}>
+
+
+ )}
+
+
+ {children}
+
+
+ );
+}
diff --git a/src/helpers.tsx b/src/helpers.tsx
index fd9ab1a..b99eda6 100644
--- a/src/helpers.tsx
+++ b/src/helpers.tsx
@@ -1,4 +1,4 @@
-import {PolymorphicGroup, OktaUser, Tag, OktaGroupTagMap} from './api/apiSchemas';
+import {PolymorphicGroup, OktaUser, Tag, OktaGroupTagMap, OktaUserGroupMember} from './api/apiSchemas';
export const perPage: Array = [5, 10, 20, 50, {label: 'All', value: -1}];
@@ -120,3 +120,28 @@ export function ownerCantAddSelf(tags: Tag[] | undefined, owner: boolean) {
export function ownerCantAddSelfGroups(groups: PolymorphicGroup[], owner: boolean) {
return ownerCantAddSelf(getActiveTagsFromGroups(groups), owner);
}
+
+export function sortGroupMembers(
+ [aUserId, aUsers]: [string, Array],
+ [bUserId, bUsers]: [string, Array],
+): number {
+ let aEmail = aUsers[0].active_user?.email ?? '';
+ let bEmail = bUsers[0].active_user?.email ?? '';
+ return aEmail.localeCompare(bEmail);
+}
+
+export function sortGroupMemberRecords(users: Record): OktaUser[] {
+ const usersArray = Object.values(users); // Convert the object to an array
+ usersArray.sort((a, b) => {
+ const nameA = `${a.first_name} ${a.last_name}`;
+ const nameB = `${b.first_name} ${b.last_name}`;
+ return nameA.localeCompare(nameB);
+ });
+ return usersArray;
+}
+
+export function groupMemberships(
+ memberships: Array | undefined,
+): Map> {
+ return groupBy(memberships ?? [], 'active_user.id');
+}
diff --git a/src/index.tsx b/src/index.tsx
index 070fb55..5ec4ccc 100644
--- a/src/index.tsx
+++ b/src/index.tsx
@@ -2,8 +2,6 @@ import * as React from 'react';
import {QueryClient, QueryClientProvider} from '@tanstack/react-query';
import {createRoot} from 'react-dom/client';
import {BrowserRouter} from 'react-router-dom';
-import {ThemeProvider, createTheme} from '@mui/material/styles';
-import CssBaseline from '@mui/material/CssBaseline';
import {AdapterDayjs} from '@mui/x-date-pickers/AdapterDayjs';
import {LocalizationProvider} from '@mui/x-date-pickers';
import * as Sentry from '@sentry/react';
@@ -17,46 +15,6 @@ const queryClient = new QueryClient({
},
});
-declare module '@mui/material/styles' {
- interface Palette {
- primary_extra_light: Palette['primary'];
- }
-
- interface PaletteOptions {
- primary_extra_light?: PaletteOptions['primary'];
- }
-}
-
-declare module '@mui/material/Button' {
- interface ButtonPropsColorOverrides {
- primary_extra_light: true;
- }
-}
-
-// See https://discord.com/branding
-let theme = createTheme({
- palette: {
- primary: {
- main: '#5865F2',
- },
- secondary: {
- main: '#EB459E',
- },
- error: {
- main: '#ED4245',
- },
- warning: {
- main: '#FEE75C',
- },
- success: {
- main: '#57F287',
- },
- primary_extra_light: {
- main: '#A5B2FF',
- },
- },
-});
-
if (['production', 'staging'].includes(process.env.NODE_ENV)) {
// Use a placeholder DSN as we'll be using the tunnel to proxy all Sentry React errors
Sentry.init({
@@ -74,14 +32,9 @@ createRoot(document.getElementById('root')!).render(
} showDialog>
-
-
-
-
-
-
-
-
+
+
+
diff --git a/src/mui.d.ts b/src/mui.d.ts
new file mode 100644
index 0000000..376f39e
--- /dev/null
+++ b/src/mui.d.ts
@@ -0,0 +1,19 @@
+import '@mui/material/styles';
+
+declare module '@mui/material/styles' {
+ interface Palette {
+ highlight: {
+ [variant: string]: Palette['primary'];
+ };
+ }
+
+ interface PaletteOptions {
+ highlight?: {
+ [variant: string]: PaletteOptions['primary'];
+ };
+ }
+
+ interface TypeText {
+ accent: string;
+ }
+}
diff --git a/src/pages/ComingSoon.tsx b/src/pages/ComingSoon.tsx
index 8a17c3d..304bd1b 100644
--- a/src/pages/ComingSoon.tsx
+++ b/src/pages/ComingSoon.tsx
@@ -26,7 +26,7 @@ export default function ComingSoon() {
-
+
Coming Soon!
diff --git a/src/pages/Error.tsx b/src/pages/Error.tsx
index 767baef..b359077 100644
--- a/src/pages/Error.tsx
+++ b/src/pages/Error.tsx
@@ -24,7 +24,7 @@ export default function NotFound() {
-
+
An Unrecoverable Error Occurred
diff --git a/src/pages/Home.tsx b/src/pages/Home.tsx
index 5caca6a..276b60c 100644
--- a/src/pages/Home.tsx
+++ b/src/pages/Home.tsx
@@ -18,8 +18,6 @@ import PeopleLeadIcon from '@mui/icons-material/ContentPaste';
import FAQIcon from '@mui/icons-material/TipsAndUpdates';
import UserIcon from '@mui/icons-material/AccountBox';
-import {grey} from '@mui/material/colors';
-
const sections: Record = {
// section shorthand --> [guide title, button title, icon]
general: ['Welcome to Access!', 'Overview', ],
@@ -119,18 +117,24 @@ interface AccordionMakerProps {
}
function AccordionMaker(props: AccordionMakerProps) {
+ const [expanded, setExpanded] = React.useState(false);
+
+ const handleChange = (panel: string) => (event: React.SyntheticEvent, newExpanded: boolean) => {
+ setExpanded(newExpanded ? panel : false);
+ };
+
return (
<>
-
+
{sections[props.which][0]}
{Object.entries(guide[props.which]).map(([key, value]: [string, string]) => (
-
+
}
aria-controls={'panel-content' + key}
id={'panel-header' + key}
- sx={{fontWeight: 500, color: grey[900]}}>
+ sx={{fontWeight: 500}}>
{key}
@@ -168,14 +172,14 @@ export default function Home() {
-
+
Access User Guides
{Object.entries(sections).map(([key, [title, buttonTitle, icon]]) => (
-
+
- createData(row))}
rowHeight={40}
columns={columns}
diff --git a/src/pages/groups/Expiring.tsx b/src/pages/groups/Expiring.tsx
index 946483a..3e97b9c 100644
--- a/src/pages/groups/Expiring.tsx
+++ b/src/pages/groups/Expiring.tsx
@@ -19,7 +19,6 @@ import TextField from '@mui/material/TextField';
import ToggleButton from '@mui/material/ToggleButton';
import ToggleButtonGroup from '@mui/material/ToggleButtonGroup';
import Typography from '@mui/material/Typography';
-import {red, yellow} from '@mui/material/colors';
import dayjs, {Dayjs} from 'dayjs';
@@ -35,6 +34,7 @@ import Loading from '../../components/Loading';
import Started from '../../components/Started';
import TablePaginationActions from '../../components/actions/TablePaginationActions';
import {displayUserName, perPage} from '../../helpers';
+import TableTopBar, {renderUserOption, TableTopBarAutocomplete} from '../../components/TableTopBar';
type OrderBy = 'moniker' | 'ended_at';
type OrderDirection = 'asc' | 'desc';
@@ -206,229 +206,197 @@ export default function ExpiringGroups() {
};
return (
-
-
-
-
-
+
+
+ canManageGroup(currentUser, row.group))}
+ ownAccess={userId == '@me' || userId == currentUser.id}
+ />
+
+ Active
+ Inactive
+
+ ,
+ }}
+ />
+ displayUserName(row) + ';' + row.email.toLowerCase())}
+ onChange={handleSearchSubmit}
+ onInputChange={(event, newInputValue) => {
+ setSearchInput(newInputValue?.split(';')[0] ?? '');
+ }}
+ defaultValue={searchQuery}
+ renderOption={renderUserOption}
+ />
+
+
+
+
+
+
+ User Name
+
+
+ User Email
+ Group Name
+ Member or Owner
+
+ Started
+
+ Added by
+
+
+ Ending
+
+
+
+
+
+ {rows.map((row) => (
+
+ dayjs(row.ended_at).isAfter(dayjs()) && dayjs(row.ended_at).isBefore(dayjs().add(7, 'day'))
+ ? highlight.warning.main
+ : dayjs(row.ended_at).isBefore(dayjs())
+ ? highlight.danger.main
+ : null,
+ }}>
-
- Expiring Groups
-
+ {(row.user?.deleted_at ?? null) != null ? (
+
+ {displayUserName(row.user)}
+
+ ) : (
+
+ {displayUserName(row.user)}
+
+ )}
- canManageGroup(currentUser, row.group))}
- ownAccess={userId == '@me' || userId == currentUser.id}
- />
+ {(row.user?.deleted_at ?? null) != null ? (
+
+ {row.user?.email.toLowerCase()}
+
+ ) : (
+
+ {row.user?.email.toLowerCase()}
+
+ )}
-
- Active
- Inactive
-
-
-
- ,
- }}
- />
-
-
- x}
- options={searchRows.map((row) => displayUserName(row) + ';' + row.email.toLowerCase())}
- onChange={handleSearchSubmit}
- onInputChange={(event, newInputValue) => {
- setSearchInput(newInputValue?.split(';')[0] ?? '');
- }}
- defaultValue={searchQuery}
- key={searchQuery}
- renderInput={(params) => }
- renderOption={(props, option, state) => {
- const [displayName, email] = option.split(';');
- return (
-
-
-
- {displayName}
-
- {email}
-
-
-
-
- );
- }}
- />
+ {(row.group?.deleted_at ?? null) != null ? (
+
+ {row.group?.name ?? ''}
+
+ ) : (
+
+ {row.group?.name ?? ''}
+
+ )}
-
-
+ {row.is_owner ? 'Owner' : 'Member'}
-
- User Name
-
+
- User Email
- Group Name
- Member or Owner
- Started
+ {(row.created_actor?.deleted_at ?? null) != null ? (
+
+ {displayUserName(row.created_actor)}
+
+ ) : (
+
+ {displayUserName(row.created_actor)}
+
+ )}
- Added by
-
-
- Ending
-
+
+
-
-
-
- {rows.map((row) => (
-
-
- {(row.user?.deleted_at ?? null) != null ? (
-
- {displayUserName(row.user)}
-
- ) : (
-
- {displayUserName(row.user)}
-
- )}
-
-
- {(row.user?.deleted_at ?? null) != null ? (
-
- {row.user?.email.toLowerCase()}
-
- ) : (
-
- {row.user?.email.toLowerCase()}
-
- )}
-
-
- {(row.group?.deleted_at ?? null) != null ? (
-
- {row.group?.name ?? ''}
-
- ) : (
-
- {row.group?.name ?? ''}
-
- )}
+ {userId == '@me' || currentUser.id == row.user.id ? (
+
+
- {row.is_owner ? 'Owner' : 'Member'}
-
-
+ ) : ownerId == '@me' || canManageGroup(currentUser, row.group) ? (
+
+ canManageGroup(currentUser, row.group))} select={row.id} />
-
- {(row.created_actor?.deleted_at ?? null) != null ? (
-
- {displayUserName(row.created_actor)}
-
- ) : (
-
- {displayUserName(row.created_actor)}
-
- )}
-
-
-
-
- {userId == '@me' || currentUser.id == row.user.id ? (
-
-
-
- ) : ownerId == '@me' || canManageGroup(currentUser, row.group) ? (
-
- canManageGroup(currentUser, row.group))} select={row.id} />
-
- ) : (
-
- )}
-
- ))}
- {emptyRows > 0 && (
-
-
-
- )}
-
-
-
-
+ ) : (
+
+ )}
+
+ ))}
+ {emptyRows > 0 && (
+
+
-
-
-
-
+ )}
+
+
+
+
+
+
+
+
);
}
diff --git a/src/pages/groups/ExternallyManaged.tsx b/src/pages/groups/ExternallyManaged.tsx
index bb60045..cee8289 100644
--- a/src/pages/groups/ExternallyManaged.tsx
+++ b/src/pages/groups/ExternallyManaged.tsx
@@ -10,30 +10,19 @@ import DialogActions from '@mui/material/DialogActions';
import DialogContent from '@mui/material/DialogContent';
import DialogTitle from '@mui/material/DialogTitle';
import Link from '@mui/material/Avatar';
-import ListItemAvatar from '@mui/material/ListItemAvatar';
-import ListItemButton from '@mui/material/ListItemButton';
-import ListItemText from '@mui/material/ListItemText';
import ExternalIcon from '@mui/icons-material/Outbound';
import {useGetGroupById} from '../../api/apiComponents';
import {PolymorphicGroup} from '../../api/apiSchemas';
+import AvatarButton from '../../components/AvatarButton';
interface ExternallyManagedButtonProps {
setOpen(open: boolean): any;
}
function ExternallyManagedButton(props: ExternallyManagedButtonProps) {
- return (
- props.setOpen(true)}>
-
-
-
-
-
-
-
- );
+ return } onClick={() => props.setOpen(true)} text="Managed Externally" />;
}
interface RuleLinksProps {
diff --git a/src/pages/groups/List.tsx b/src/pages/groups/List.tsx
index 83dc096..131937e 100644
--- a/src/pages/groups/List.tsx
+++ b/src/pages/groups/List.tsx
@@ -16,13 +16,13 @@ import TableHead from '@mui/material/TableHead';
import TablePagination from '@mui/material/TablePagination';
import TableRow from '@mui/material/TableRow';
import TextField from '@mui/material/TextField';
-import Typography from '@mui/material/Typography';
import {useCurrentUser} from '../../authentication';
import CreateUpdateGroup from './CreateUpdate';
import {displayGroupType, perPage} from '../../helpers';
import {useGetGroups} from '../../api/apiComponents';
import TablePaginationActions from '../../components/actions/TablePaginationActions';
+import TableTopBar, {TableTopBarAutocomplete} from '../../components/TableTopBar';
export default function ListGroups() {
const navigate = useNavigate();
@@ -104,102 +104,76 @@ export default function ListGroups() {
};
return (
-
-
-
-
-
+
+
+ navigate('/tags/')} endIcon={}>
+ Tags
+
+
+ row.name)}
+ onChange={handleSearchSubmit}
+ onInputChange={(event, newInputValue) => setSearchInput(newInputValue)}
+ defaultValue={searchQuery}
+ />
+
+
+
+
+ Name
+ Type
+ Description
+
+
+
+ {rows.map((row) => (
+
-
- Groups
-
+
+ {row.name}
+
- navigate('/tags/')} endIcon={}>
- Tags
-
+
+ {displayGroupType(row)}
+
-
-
+
+
+ {(row.description?.length ?? 0) > 115
+ ? row.description?.substring(0, 114) + '...' ?? ''
+ : row.description}
+
-
- x}
- options={searchRows.map((row) => row.name)}
- onChange={handleSearchSubmit}
- onInputChange={(event, newInputValue) => setSearchInput(newInputValue)}
- defaultValue={searchQuery}
- key={searchQuery}
- renderInput={(params) => }
- />
-
-
-
- Name
- Type
- Description
-
-
- {rows.map((row) => (
-
-
-
- {row.name}
-
-
-
-
- {displayGroupType(row)}
-
-
-
-
- {(row.description?.length ?? 0) > 115
- ? row.description?.substring(0, 114) + '...' ?? ''
- : row.description}
-
-
-
- ))}
- {emptyRows > 0 && (
-
-
-
- )}
-
-
-
-
+ ))}
+ {emptyRows > 0 && (
+
+
-
-
-
-
+ )}
+
+
+
+
+
+
+
+
);
}
diff --git a/src/pages/groups/Read.tsx b/src/pages/groups/Read.tsx
index 2dc2f53..5f31cc6 100644
--- a/src/pages/groups/Read.tsx
+++ b/src/pages/groups/Read.tsx
@@ -1,7 +1,6 @@
import React from 'react';
import {Link as RouterLink, useParams, useNavigate} from 'react-router-dom';
-import Avatar from '@mui/material/Avatar';
import Box from '@mui/material/Box';
import Chip from '@mui/material/Chip';
import Container from '@mui/material/Container';
@@ -9,10 +8,6 @@ import Divider from '@mui/material/Divider';
import Grid from '@mui/material/Grid';
import IconButton from '@mui/material/IconButton';
import Link from '@mui/material/Link';
-import List from '@mui/material/List';
-import ListItem from '@mui/material/ListItem';
-import ListItemAvatar from '@mui/material/ListItemAvatar';
-import ListItemText from '@mui/material/ListItemText';
import Paper from '@mui/material/Paper';
import Stack from '@mui/material/Stack';
import Table from '@mui/material/Table';
@@ -25,15 +20,12 @@ import TableFooter from '@mui/material/TableFooter';
import Tooltip from '@mui/material/Tooltip';
import Typography from '@mui/material/Typography';
-import AppIcon from '@mui/icons-material/AppShortcut';
import AuditGroupIcon from '@mui/icons-material/History';
import AuditRoleIcon from '@mui/icons-material/Diversity2';
import DeleteIcon from '@mui/icons-material/Close';
import GroupIcon from '@mui/icons-material/People';
import TagIcon from '@mui/icons-material/LocalOffer';
-import {grey} from '@mui/material/colors';
-
import {useCurrentUser} from '../../authentication';
import CreateUpdateGroup from './CreateUpdate';
import DeleteGroup from './Delete';
@@ -59,6 +51,10 @@ import {
GroupMember,
} from '../../api/apiSchemas';
import {canManageGroup} from '../../authorization';
+import {EmptyListEntry} from '../../components/EmptyListEntry';
+import {Diversity3 as RoleIcon} from '@mui/icons-material';
+import AppLinkButton from './AppLinkButton';
+import AvatarButton from '../../components/AvatarButton';
function sortGroupMembers(
[aUserId, aUsers]: [string, Array],
@@ -214,151 +210,88 @@ export default function ReadGroup() {
return (
-
+
-
-
-
-
-
- {group.deleted_at != null ? (
- <>
- {group.name} is Deleted
- >
- ) : (
- group.name
- )}
-
- {group.description}
-
- {group.active_group_tags?.map((tagMap) => (
- navigate(`/tags/${tagMap.active_tag!.name}`)}
- variant={tagMap.active_app_tag_mapping ? 'outlined' : 'filled'}
- icon={}
- sx={{
- margin: '10px 2px 0 2px',
- bgcolor: tagMap.active_tag!.enabled ? 'primary' : grey[500],
- }}
- />
- ))}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {group.type == 'role_group' ? (
-
-
-
-
-
- ) : null}
+
+
+
+
+
+ : }
+ text={displayGroupType(group)}
+ />
+ {group.type == 'app_group' && }
+ {!group.is_managed && }
-
-
-
-
-
-
-
-
-
-
-
-
- {group.type == 'app_group' ? (
- ((group ?? {}) as AppGroup).app?.deleted_at != null ? (
-
-
-
-
-
-
-
-
+
+
+ {group.deleted_at != null ? (
+ <>
+ {group.name} is Deleted
+ >
) : (
-
-
-
-
-
-
-
-
- )
- ) : (
- ''
- )}
- {!group.is_managed ? : ''}
-
-
-
+ group.name
+ )}
+
+
+ {group.description}
+
+
+ {group.active_group_tags?.map((tagMap) => (
+ navigate(`/tags/${tagMap.active_tag!.name}`)}
+ variant={tagMap.active_app_tag_mapping ? 'outlined' : 'filled'}
+ icon={}
+ sx={{
+ margin: '10px 2px 0 2px',
+ bgcolor: (theme) =>
+ tagMap.active_tag!.enabled ? 'primary' : theme.palette.action.disabled,
+ }}
+ />
+ ))}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {group.type == 'role_group' ? (
+
+
+
+
+
+ ) : null}
+
+
{group.type == 'role_group' ? (
@@ -369,7 +302,7 @@ export default function ReadGroup() {
-
+
Groups Owned by Role Membership
@@ -437,13 +370,7 @@ export default function ReadGroup() {
))
) : (
-
-
-
- None
-
-
-
+
)}
@@ -458,7 +385,7 @@ export default function ReadGroup() {
-
+
Groups with Members granted by Role Membership
@@ -528,7 +455,7 @@ export default function ReadGroup() {
) : (
-
+
None
@@ -550,10 +477,10 @@ export default function ReadGroup() {
-
+
{group.type == 'role_group' ? 'Role Owners' : 'Group Owners'}
-
+
{group.type == 'role_group'
? 'Can manage description and membership of Role'
: 'Can manage description and membership of Group'}
@@ -714,7 +641,7 @@ export default function ReadGroup() {
) : (
-
+
None
@@ -734,10 +661,10 @@ export default function ReadGroup() {
-
+
{group.type == 'role_group' ? 'Role Members' : 'Group Members'}
-
+
{group.type == 'role_group' ? 'Members of Okta Group for Role' : 'Members of Okta Group'}
@@ -821,7 +748,13 @@ export default function ReadGroup() {
{group.type != 'role_group' ? (
-
+
{users.sort(sortOktaUserGroupMembers).map((user) =>
user.active_role_group_mapping == null ? (
-
+
None
diff --git a/src/pages/requests/Create.tsx b/src/pages/requests/Create.tsx
index db55ce1..e0bf49f 100644
--- a/src/pages/requests/Create.tsx
+++ b/src/pages/requests/Create.tsx
@@ -330,7 +330,7 @@ function CreateRequestContainer(props: CreateRequestContainerProps) {
Request
-
+
{timeLimit
? (owner ? 'Ownership of ' : 'Membership to ') +
'this group is limited to ' +
diff --git a/src/pages/requests/List.tsx b/src/pages/requests/List.tsx
index 8335333..153fedc 100644
--- a/src/pages/requests/List.tsx
+++ b/src/pages/requests/List.tsx
@@ -18,7 +18,6 @@ import TextField from '@mui/material/TextField';
import Box from '@mui/material/Box';
import Grid from '@mui/material/Grid';
import Autocomplete from '@mui/material/Autocomplete';
-import {lightGreen, red} from '@mui/material/colors';
import dayjs from 'dayjs';
import RelativeTime from 'dayjs/plugin/relativeTime';
@@ -28,6 +27,7 @@ import CreateRequest from './Create';
import {useGetRequests} from '../../api/apiComponents';
import {displayUserName, perPage} from '../../helpers';
import TablePaginationActions from '../../components/actions/TablePaginationActions';
+import TableTopBar, {TableTopBarAutocomplete} from '../../components/TableTopBar';
dayjs.extend(RelativeTime);
@@ -125,184 +125,168 @@ export default function ListRequests() {
};
return (
-
-
-
-
-
+
+
+
+
+ row.id +
+ ';' +
+ displayUserName(row.requester) +
+ ';' +
+ row.request_ownership +
+ ';' +
+ (row.requested_group?.name ?? '') +
+ ';' +
+ (row.status ?? '') +
+ ';' +
+ displayUserName(row.resolver),
+ )}
+ onChange={handleSearchSubmit}
+ onInputChange={(event, newInputValue) => {
+ setSearchInput(newInputValue?.split(';')[0] ?? '');
+ }}
+ defaultValue={searchQuery}
+ renderOption={(props, option, state) => {
+ const [id, displayName, ownership, group, status, resolver] = option.split(';');
+ return (
+
+
+
+
+ {displayName} {ownership == 'true' ? 'ownership of' : 'membership to'} {group}
+
+
+ {status} {status == 'PENDING' || resolver == '' ? '' : 'by ' + resolver}
+
+
+
+
+ );
+ }}
+ />
+
+
+
+
+ Requester
+ Request
+ Resolver
+ Status
+ Created
+
+
+
+ {rows.map((row) => (
+
+ row.status == 'APPROVED'
+ ? highlight.success.main
+ : row.status == 'REJECTED'
+ ? highlight.danger.main
+ : 'inherit',
+ }}>
-
- Access Requests
-
+ {(row.requester?.deleted_at ?? null) != null ? (
+
+ {displayUserName(row.requester)}
+
+ ) : (
+
+ {displayUserName(row.requester)}
+
+ )}
-
-
-
- x}
- options={searchRows.map(
- (row) =>
- row.id +
- ';' +
- displayUserName(row.requester) +
- ';' +
- row.request_ownership +
- ';' +
- (row.requested_group?.name ?? '') +
- ';' +
- (row.status ?? '') +
- ';' +
- displayUserName(row.resolver),
- )}
- onChange={handleSearchSubmit}
- onInputChange={(event, newInputValue) => {
- setSearchInput(newInputValue?.split(';')[0] ?? '');
- }}
- defaultValue={searchQuery}
- key={searchQuery}
- renderInput={(params) => }
- renderOption={(props, option, state) => {
- const [id, displayName, ownership, group, status, resolver] = option.split(';');
- return (
-
-
-
-
- {displayName} {ownership == 'true' ? 'ownership of' : 'membership to'} {group}
-
-
- {status} {status == 'PENDING' || resolver == '' ? '' : 'by ' + resolver}
-
-
-
-
- );
- }}
- />
-
-
-
- Requester
- Request
- Resolver
- Status
- Created
-
-
-
- {rows.map((row) => (
-
-
- {(row.requester?.deleted_at ?? null) != null ? (
-
- {displayUserName(row.requester)}
-
- ) : (
-
- {displayUserName(row.requester)}
-
- )}
-
-
- {row.request_ownership ? 'Ownership of ' : 'Membership to '}
- {(row.requested_group?.deleted_at ?? null) != null ? (
-
- {row.requested_group?.name ?? ''}
-
- ) : (
-
- {row.requested_group?.name ?? ''}
-
- )}
-
-
- {row.resolver == null && row.status != 'PENDING' ? (
- 'Access'
- ) : (row.resolver?.deleted_at ?? null) != null ? (
-
- {displayUserName(row.resolver)}
-
- ) : (
-
- {displayUserName(row.resolver)}
-
- )}
-
-
+ {row.request_ownership ? 'Ownership of ' : 'Membership to '}
+ {(row.requested_group?.deleted_at ?? null) != null ? (
+
+ {row.requested_group?.name ?? ''}
+
+ ) : (
- {row.status}
+ {row.requested_group?.name ?? ''}
-
-
+ )}
+
+
+ {row.resolver == null && row.status != 'PENDING' ? (
+ 'Access'
+ ) : (row.resolver?.deleted_at ?? null) != null ? (
+ {displayUserName(row.resolver)}
+
+ ) : (
+
- {dayjs(row.created_at).startOf('second').fromNow()}
+ {displayUserName(row.resolver)}
-
-
-
- View
-
-
-
- ))}
- {emptyRows > 0 && (
-
-
-
- )}
-
-
-
-
+ )}
+
+
+
+ {row.status}
+
+
+
+
+ {dayjs(row.created_at).startOf('second').fromNow()}
+
+
+
+
+ View
+
+
+
+ ))}
+ {emptyRows > 0 && (
+
+
-
-
-
-
+ )}
+
+
+
+
+
+
+
+
);
}
diff --git a/src/pages/requests/Read.tsx b/src/pages/requests/Read.tsx
index 0efb29b..74d9a86 100644
--- a/src/pages/requests/Read.tsx
+++ b/src/pages/requests/Read.tsx
@@ -72,6 +72,7 @@ import {
import NotFound from '../NotFound';
import Loading from '../../components/Loading';
+import {EmptyListEntry} from '../../components/EmptyListEntry';
dayjs.extend(RelativeTime);
dayjs.extend(IsSameOrBefore);
@@ -408,6 +409,7 @@ export default function ReadRequest() {
xs={8}
sx={{
textAlign: 'center',
+ wordBreak: 'break-word',
}}>
{(accessRequest.requester?.deleted_at ?? null) != null ? (
@@ -604,7 +606,7 @@ export default function ReadRequest() {
-
+
{timeLimit
? (accessRequest.request_ownership ? 'Ownership of ' : 'Membership to ') +
'this group is limited to ' +
@@ -720,7 +722,7 @@ export default function ReadRequest() {
-
+
{accessRequest.requested_group?.name}{' '}
{accessRequest.requested_group?.type == 'role_group'
? 'Owners'
@@ -775,13 +777,7 @@ export default function ReadRequest() {
))
) : (
-
-
-
- None
-
-
-
+
)}
@@ -793,7 +789,7 @@ export default function ReadRequest() {
-
+
{((accessRequest.requested_group ?? {}) as AppGroup).app?.name}
{' App Owners'}
@@ -848,7 +844,7 @@ export default function ReadRequest() {
) : (
-
+
None
@@ -865,7 +861,7 @@ export default function ReadRequest() {
-
+
{ACCESS_APP_RESERVED_NAME}
{' Admins'}
@@ -920,7 +916,7 @@ export default function ReadRequest() {
) : (
-
+
None
diff --git a/src/pages/roles/AddGroups.tsx b/src/pages/roles/AddGroups.tsx
index 164e4eb..38d67c9 100644
--- a/src/pages/roles/AddGroups.tsx
+++ b/src/pages/roles/AddGroups.tsx
@@ -233,7 +233,7 @@ function AddGroupsDialog(props: AddGroupsDialogProps) {
onSuccess={(formData) => submit(formData)}>
Add {addGroupsText}
-
+
{timeLimit
? (props.owner ? 'Ownership of ' : 'Membership to ') +
'one or more selected groups is limited to ' +
@@ -241,7 +241,7 @@ function AddGroupsDialog(props: AddGroupsDialogProps) {
' days.'
: null}
-
+
{disallowedGroups.length != 0 && !isAccessAdmin(currentUser)
? 'Some groups may not be added due to group tag constraints.'
: null}
diff --git a/src/pages/roles/Audit.tsx b/src/pages/roles/Audit.tsx
index f3a1a82..c832f37 100644
--- a/src/pages/roles/Audit.tsx
+++ b/src/pages/roles/Audit.tsx
@@ -18,7 +18,6 @@ import TextField from '@mui/material/TextField';
import Autocomplete from '@mui/material/Autocomplete';
import ToggleButtonGroup from '@mui/material/ToggleButtonGroup';
import ToggleButton from '@mui/material/ToggleButton';
-import {lightGreen, red} from '@mui/material/colors';
import dayjs from 'dayjs';
@@ -32,6 +31,7 @@ import Loading from '../../components/Loading';
import Started from '../../components/Started';
import Ending from '../../components/Ending';
import TablePaginationActions from '../../components/actions/TablePaginationActions';
+import TableTopBar, {TableTopBarAutocomplete} from '../../components/TableTopBar';
type OrderBy = 'moniker' | 'created_at' | 'ended_at';
type OrderDirection = 'asc' | 'desc';
@@ -188,202 +188,173 @@ export default function AuditRole() {
};
return (
-
-
-
-
-
+
+
+
+ Active
+ Inactive
+
+
+ Member
+ Owner
+
+ row.name)}
+ onChange={handleSearchSubmit}
+ onInputChange={(event, newInputValue) => setSearchInput(newInputValue)}
+ defaultValue={searchQuery}
+ />
+
+
+
+
+
+
+ Group Name
+
+
+ Group Type
+ Member or Owner
+
+
+ Started
+
+
+ Added by
+
+
+ Ending
+
+
+ Removed by
+ Justification
+
+
+
+ {rows.map((row) => (
+
+ row.ended_at == null || dayjs().isBefore(dayjs(row.ended_at))
+ ? highlight.success.main
+ : highlight.danger.main,
+ }}>
-
- {(role.deleted_at ?? null) != null ? (
-
- {role.name}
-
- ) : (
-
- {role.name}
-
- )}{' '}
- Role Audit
-
+ {(row.group?.deleted_at ?? null) != null ? (
+
+ {row.group?.name ?? ''}
+
+ ) : (
+
+ {row.group?.name ?? ''}
+
+ )}
-
- Active
- Inactive
-
-
-
-
- Member
- Owner
-
+ {(row.group?.deleted_at ?? null) != null ? (
+ displayGroupType(row.group)
+ ) : (
+
+ {displayGroupType(row.group)}
+
+ )}
-
- x}
- options={searchRows.map((row) => row.name)}
- onChange={handleSearchSubmit}
- onInputChange={(event, newInputValue) => setSearchInput(newInputValue)}
- defaultValue={searchQuery}
- key={searchQuery}
- renderInput={(params) => }
- />
-
-
-
+ {row.is_owner ? 'Owner' : 'Member'}
-
- Group Name
-
+
- Group Type
- Member or Owner
-
- Started
-
+ {(row.created_actor?.deleted_at ?? null) != null ? (
+
+ {displayUserName(row.created_actor)}
+
+ ) : (
+
+ {displayUserName(row.created_actor)}
+
+ )}
- Added by
-
- Ending
-
+
- Removed by
- Justification
-
-
-
- {rows.map((row) => (
-
-
- {(row.group?.deleted_at ?? null) != null ? (
-
- {row.group?.name ?? ''}
-
- ) : (
-
- {row.group?.name ?? ''}
-
- )}
-
-
- {(row.group?.deleted_at ?? null) != null ? (
- displayGroupType(row.group)
- ) : (
-
- {displayGroupType(row.group)}
-
- )}
-
- {row.is_owner ? 'Owner' : 'Member'}
-
-
-
-
- {(row.created_actor?.deleted_at ?? null) != null ? (
+
+ {row.ended_at != null && dayjs().isAfter(dayjs(row.ended_at)) ? (
+ (row.ended_actor?.deleted_at ?? null) != null ? (
- {displayUserName(row.created_actor)}
+ {displayUserName(row.ended_actor)}
) : (
- {displayUserName(row.created_actor)}
+ {displayUserName(row.ended_actor)}
- )}
-
-
-
-
-
- {row.ended_at != null && dayjs().isAfter(dayjs(row.ended_at)) ? (
- (row.ended_actor?.deleted_at ?? null) != null ? (
-
- {displayUserName(row.ended_actor)}
-
- ) : (
-
- {displayUserName(row.ended_actor)}
-
- )
- ) : (
- ''
- )}
-
-
- {row.created_reason ? : null}
-
-
- ))}
- {emptyRows > 0 && (
-
-
-
- )}
-
-
-
-
+ )
+ ) : (
+ ''
+ )}
+
+ {row.created_reason ? : null}
+
+ ))}
+ {emptyRows > 0 && (
+
+
-
-
-
-
+ )}
+
+
+
+
+
+
+
+
);
}
diff --git a/src/pages/roles/BulkRenewal.tsx b/src/pages/roles/BulkRenewal.tsx
index 1256166..fc24c36 100644
--- a/src/pages/roles/BulkRenewal.tsx
+++ b/src/pages/roles/BulkRenewal.tsx
@@ -17,14 +17,11 @@ import Select from '@mui/material/Select';
import Tooltip from '@mui/material/Tooltip';
import Typography from '@mui/material/Typography';
-import {red, yellow} from '@mui/material/colors';
-import {darken, lighten, styled} from '@mui/material/styles';
-
import AccessRequestIcon from '@mui/icons-material/MoreTime';
import {FormContainer, DatePickerElement, TextFieldElement} from 'react-hook-form-mui';
-import {DataGrid, GridColDef, GridRowParams, GridRowSelectionModel} from '@mui/x-data-grid';
+import {GridColDef, GridRowParams, GridRowSelectionModel} from '@mui/x-data-grid';
import dayjs, {Dayjs} from 'dayjs';
@@ -35,6 +32,7 @@ import {useCurrentUser} from '../../authentication';
import {usePutRoleMembersById, PutRoleMembersByIdError, PutRoleMembersByIdVariables} from '../../api/apiComponents';
import {RoleMember, RoleGroupMap, OktaGroup, AppGroup} from '../../api/apiSchemas';
import {isAccessAdmin} from '../../authorization';
+import BulkRenewalDataGrid from '../../components/BulkRenewalDataGrid';
interface Data {
id: number;
@@ -95,45 +93,6 @@ const UNTIL_OPTIONS = Object.entries(UNTIL_ID_TO_LABELS).map(([id, label], index
const RFC822_FORMAT = 'ddd, DD MMM YYYY HH:mm:ss ZZ';
-const getBackgroundColor = (color: string, mode: string) =>
- mode === 'dark' ? darken(color, 0.7) : lighten(color, 0.7);
-
-const getHoverBackgroundColor = (color: string, mode: string) =>
- mode === 'dark' ? darken(color, 0.6) : lighten(color, 0.6);
-
-const getSelectedBackgroundColor = (color: string, mode: string) =>
- mode === 'dark' ? darken(color, 0.5) : lighten(color, 0.5);
-
-const getSelectedHoverBackgroundColor = (color: string, mode: string) =>
- mode === 'dark' ? darken(color, 0.4) : lighten(color, 0.4);
-
-const StyledDataGrid = styled(DataGrid)(({theme}) => ({
- '& .super-app-theme--Expired': {
- backgroundColor: getBackgroundColor(red[200], theme.palette.mode),
- '&:hover': {
- backgroundColor: getHoverBackgroundColor(red[200], theme.palette.mode),
- },
- '&.Mui-selected': {
- backgroundColor: getSelectedBackgroundColor(red[200], theme.palette.mode),
- '&:hover': {
- backgroundColor: getSelectedHoverBackgroundColor(red[200], theme.palette.mode),
- },
- },
- },
- '& .super-app-theme--Soon': {
- backgroundColor: getBackgroundColor(yellow[200], theme.palette.mode),
- '&:hover': {
- backgroundColor: getHoverBackgroundColor(yellow[200], theme.palette.mode),
- },
- '&.Mui-selected': {
- backgroundColor: getSelectedBackgroundColor(yellow[200], theme.palette.mode),
- '&:hover': {
- backgroundColor: getSelectedHoverBackgroundColor(yellow[200], theme.palette.mode),
- },
- },
- },
-}));
-
interface BulkRenewalDialogProps {
setOpen(open: boolean): any;
rows: RoleGroupMap[];
@@ -427,12 +386,12 @@ function BulkRenewalDialog(props: BulkRenewalDialogProps) {
onSuccess={(formData) => submit(formData)}>
Bulk Renew Role Access
-
+
{timeLimit
? 'Access to one or more selected groups is limited to ' + Math.floor(timeLimit / 86400) + ' days.'
: null}
-
+
{display_owner_add_constraint
? 'Due to group constraints, some roles may not be renewed since you are both a member of the role and an owner of the group. Please reach out to another group owner to renew the role membership to the group.'
: null}
@@ -497,7 +456,7 @@ function BulkRenewalDialog(props: BulkRenewalDialogProps) {
) : null}
- createData(row))}
rowHeight={40}
columns={columns}
diff --git a/src/pages/roles/Expiring.tsx b/src/pages/roles/Expiring.tsx
index 3fc2c41..0005a61 100644
--- a/src/pages/roles/Expiring.tsx
+++ b/src/pages/roles/Expiring.tsx
@@ -18,7 +18,6 @@ import ToggleButton from '@mui/material/ToggleButton';
import ToggleButtonGroup from '@mui/material/ToggleButtonGroup';
import Tooltip from '@mui/material/Tooltip';
import Typography from '@mui/material/Typography';
-import {red, yellow} from '@mui/material/colors';
import dayjs, {Dayjs} from 'dayjs';
@@ -33,6 +32,7 @@ import Loading from '../../components/Loading';
import Started from '../../components/Started';
import TablePaginationActions from '../../components/actions/TablePaginationActions';
import {displayUserName, perPage} from '../../helpers';
+import TableTopBar, {TableTopBarAutocomplete} from '../../components/TableTopBar';
type OrderBy = 'moniker' | 'ended_at';
type OrderDirection = 'asc' | 'desc';
@@ -210,211 +210,192 @@ export default function ExpiringRoless() {
};
return (
-
-
-
-
-
+
+
+ canManageGroup(currentUser, row.group))} />
+
+ Active
+ Inactive
+
+ {ownerId ? (
+
+
+
+ Default Owner
+
+
+
+
+ All Owned
+
+
+
+ ) : null}
+ ,
+ }}
+ />
+ row.name)}
+ onChange={handleSearchSubmit}
+ onInputChange={(event, newInputValue) => setSearchInput(newInputValue)}
+ defaultValue={searchQuery}
+ />
+
+
+
+
+ Role Name
+
+
+ Group Name
+
+
+ Group Type
+ Member or Owner
+
+ Started
+
+ Added by
+
+
+ Ending
+
+
+
+
+
+ {rows.map((row) => (
+
+ dayjs(row.ended_at).isAfter(dayjs()) && dayjs(row.ended_at).isBefore(dayjs().add(7, 'day'))
+ ? highlight.warning.main
+ : dayjs(row.ended_at).isBefore(dayjs())
+ ? highlight.danger.main
+ : null,
+ }}>
-
- Expiring Roles
-
+ {(row.group?.deleted_at ?? null) != null ? (
+
+ {row.role_group?.name ?? ''}
+
+ ) : (
+
+ {row.role_group?.name ?? ''}
+
+ )}
- canManageGroup(currentUser, row.group))} />
+ {(row.group?.deleted_at ?? null) != null ? (
+
+ {row.group?.name ?? ''}
+
+ ) : (
+
+ {row.group?.name ?? ''}
+
+ )}
-
- Active
- Inactive
-
-
- {ownerId ? (
-
-
-
-
- Default Owner
-
-
-
-
- All Owned
-
-
-
-
- ) : null}
-
- ,
- }}
- />
-
-
- x}
- options={searchRows.map((row) => row.name)}
- onChange={handleSearchSubmit}
- onInputChange={(event, newInputValue) => setSearchInput(newInputValue)}
- defaultValue={searchQuery}
- key={searchQuery}
- renderInput={(params) => }
- />
+ {row.group?.type == 'okta_group' ? 'Group' : 'app_group' ? 'App Group' : 'Role Group'}
-
-
- Role Name
+ {row.is_owner ? 'Owner' : 'Member'}
-
- Group Name
-
+
- Group Type
- Member or Owner
- Started
+ {(row.created_actor?.deleted_at ?? null) != null ? (
+
+ {displayUserName(row.created_actor)}
+
+ ) : (
+
+ {displayUserName(row.created_actor)}
+
+ )}
- Added by
-
-
- Ending
-
+
+
-
-
-
- {rows.map((row) => (
-
-
- {(row.group?.deleted_at ?? null) != null ? (
-
- {row.role_group?.name ?? ''}
-
- ) : (
-
- {row.role_group?.name ?? ''}
-
- )}
-
-
- {(row.group?.deleted_at ?? null) != null ? (
-
- {row.group?.name ?? ''}
-
- ) : (
-
- {row.group?.name ?? ''}
-
- )}
-
-
- {row.group?.type == 'okta_group' ? 'Group' : 'app_group' ? 'App Group' : 'Role Group'}
-
- {row.is_owner ? 'Owner' : 'Member'}
-
-
+ {ownerId == '@me' || canManageGroup(currentUser, row.group) ? (
+
+ canManageGroup(currentUser, row.group))} select={row.id} />
-
- {(row.created_actor?.deleted_at ?? null) != null ? (
-
- {displayUserName(row.created_actor)}
-
- ) : (
-
- {displayUserName(row.created_actor)}
-
- )}
-
-
-
-
- {ownerId == '@me' || canManageGroup(currentUser, row.group) ? (
-
- canManageGroup(currentUser, row.group))} select={row.id} />
-
- ) : (
-
- )}
-
- ))}
- {emptyRows > 0 && (
-
-
-
- )}
-
-
-
-
+ ) : (
+
+ )}
+
+ ))}
+ {emptyRows > 0 && (
+
+
-
-
-
-
+ )}
+
+
+
+
+
+
+
+
);
}
diff --git a/src/pages/roles/List.tsx b/src/pages/roles/List.tsx
index ac5a783..37607a9 100644
--- a/src/pages/roles/List.tsx
+++ b/src/pages/roles/List.tsx
@@ -24,6 +24,7 @@ import CreateUpdateGroup from '../groups/CreateUpdate';
import {perPage} from '../../helpers';
import {useGetRoles} from '../../api/apiComponents';
import TablePaginationActions from '../../components/actions/TablePaginationActions';
+import TableTopBar, {TableTopBarAutocomplete} from '../../components/TableTopBar';
export default function ListRoles() {
const navigate = useNavigate();
@@ -105,102 +106,70 @@ export default function ListRoles() {
};
return (
-
-
-
-
-
+
+
+ navigate('/tags/')} endIcon={}>
+ Tags
+
+
+ row.name)}
+ onChange={handleSearchSubmit}
+ onInputChange={(event, newInputValue) => setSearchInput(newInputValue)}
+ defaultValue={searchQuery}
+ />
+
+
+
+
+ Name
+ Description
+
+
+
+ {rows.map((row) => (
+
-
- Roles
-
+
+ {row.name}
+
-
-
-
- navigate('/tags/')} endIcon={}>
- Tags
-
-
-
-
-
- x}
- options={searchRows.map((row) => row.name)}
- onChange={handleSearchSubmit}
- onInputChange={(event, newInputValue) => setSearchInput(newInputValue)}
- defaultValue={searchQuery}
- key={searchQuery}
- renderInput={(params) => }
- />
-
+
+
+ {(row.description?.length ?? 0) > 115
+ ? row.description?.substring(0, 114) + '...' ?? ''
+ : row.description}
+
-
- Name
- Description
-
-
-
- {rows.map((row) => (
-
-
-
- {row.name}
-
-
-
-
- {(row.description?.length ?? 0) > 115
- ? row.description?.substring(0, 114) + '...' ?? ''
- : row.description}
-
-
-
- ))}
- {emptyRows > 0 && (
-
-
-
- )}
-
-
-
-
+ ))}
+ {emptyRows > 0 && (
+
+
-
-
-
-
+ )}
+
+
+
+
+
+
+
+
);
}
diff --git a/src/pages/tags/List.tsx b/src/pages/tags/List.tsx
index f105bcf..4e9db00 100644
--- a/src/pages/tags/List.tsx
+++ b/src/pages/tags/List.tsx
@@ -22,6 +22,7 @@ import CreateUpdateTag from './CreateUpdate';
import {perPage} from '../../helpers';
import {useGetTags} from '../../api/apiComponents';
import TablePaginationActions from '../../components/actions/TablePaginationActions';
+import TableTopBar, {TableTopBarAutocomplete} from '../../components/TableTopBar';
export default function ListTags() {
const navigate = useNavigate();
@@ -103,91 +104,67 @@ export default function ListTags() {
};
return (
-
-
-
-
-
+
+
+
+ row.name)}
+ onChange={handleSearchSubmit}
+ onInputChange={(event, newInputValue) => setSearchInput(newInputValue)}
+ defaultValue={searchQuery}
+ />
+
+
+
+
+ Name
+ Description
+
+
+
+ {rows.map((row) => (
+
-
- Tags
-
+
+ {row.name}
+
-
-
-
-
-
- x}
- options={searchRows.map((row) => row.name)}
- onChange={handleSearchSubmit}
- onInputChange={(event, newInputValue) => setSearchInput(newInputValue)}
- defaultValue={searchQuery}
- key={searchQuery}
- renderInput={(params) => }
- />
-
+
+
+ {(row.description ?? '').length > 115
+ ? row.description?.substring(0, 114) + '...'
+ : row.description ?? ''}
+
-
- Name
- Description
-
-
-
- {rows.map((row) => (
-
-
-
- {row.name}
-
-
-
-
- {(row.description ?? '').length > 115
- ? row.description?.substring(0, 114) + '...'
- : row.description ?? ''}
-
-
-
- ))}
- {emptyRows > 0 && (
-
-
-
- )}
-
-
-
-
+ ))}
+ {emptyRows > 0 && (
+
+
-
-
-
-
+ )}
+
+
+
+
+
+
+
+
);
}
diff --git a/src/pages/tags/Read.tsx b/src/pages/tags/Read.tsx
index 323a531..b5412c7 100644
--- a/src/pages/tags/Read.tsx
+++ b/src/pages/tags/Read.tsx
@@ -25,8 +25,6 @@ import DeleteIcon from '@mui/icons-material/Close';
import Disabled from '@mui/icons-material/PauseCircle';
import Enabled from '@mui/icons-material/TaskAlt';
-import {grey} from '@mui/material/colors';
-
import {useGetTagById, usePutGroupById, usePutAppById} from '../../api/apiComponents';
import {App, AppGroup, PolymorphicGroup, Tag} from '../../api/apiSchemas';
@@ -38,6 +36,7 @@ import CreateUpdateTag from './CreateUpdate';
import NotFound from '../NotFound';
import Loading from '../../components/Loading';
import DeleteTag from './Delete';
+import {EmptyListEntry} from '../../components/EmptyListEntry';
export default function ReadTag() {
const currentUser = useCurrentUser();
@@ -116,55 +115,55 @@ export default function ReadTag() {
disallow_self_add_membership: 'Owners may not add selves as members?',
};
+ const hasActions = tag != null && tag.deleted_at == null && isAccessAdmin(currentUser);
return (
-
+
-
-
- {tag.name}
-
-
- {tag.description}
-
- {tag.enabled ? (
-
- } label="Enabled" sx={{marginTop: '10px'}} />
-
- ) : (
-
- }
- label="Disabled"
- sx={{
- marginTop: '10px',
- bgcolor: grey[500],
- }}
- />
-
- )}
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+ {tag.name}
+
+
+ {tag.description}
+
+ {tag.enabled ? (
+
+ } label="Enabled" sx={{marginTop: '10px'}} />
+
+ ) : (
+
+ }
+ label="Disabled"
+ sx={{
+ marginTop: '10px',
+ bgcolor: (theme) => theme.palette.action.disabledBackground,
+ }}
+ />
+
+ )}
+
+ {hasActions && (
+ <>
+
+
+
+
+
+
+
+
+
+
+
+
+
+ >
+ )}
@@ -175,7 +174,7 @@ export default function ReadTag() {
-
+
Tag Constraints
@@ -218,17 +217,11 @@ export default function ReadTag() {
))
) : (
-
-
-
- None
-
-
-
+
)}
-
+
@@ -240,7 +233,7 @@ export default function ReadTag() {
-
+
Apps with Tag
@@ -295,17 +288,11 @@ export default function ReadTag() {
))
) : (
-
-
-
- None
-
-
-
+
)}
-
+
@@ -317,7 +304,7 @@ export default function ReadTag() {
-
+
Groups with Tag
@@ -398,17 +385,11 @@ export default function ReadTag() {
))
) : (
-
-
-
- None
-
-
-
+
)}
-
+
diff --git a/src/pages/users/Audit.tsx b/src/pages/users/Audit.tsx
index c9ea075..595fdca 100644
--- a/src/pages/users/Audit.tsx
+++ b/src/pages/users/Audit.tsx
@@ -20,7 +20,6 @@ import TextField from '@mui/material/TextField';
import Autocomplete from '@mui/material/Autocomplete';
import ToggleButtonGroup from '@mui/material/ToggleButtonGroup';
import ToggleButton from '@mui/material/ToggleButton';
-import {lightGreen, red} from '@mui/material/colors';
import dayjs from 'dayjs';
@@ -33,6 +32,7 @@ import Loading from '../../components/Loading';
import Started from '../../components/Started';
import Ending from '../../components/Ending';
import TablePaginationActions from '../../components/actions/TablePaginationActions';
+import TableTopBar, {TableTopBarAutocomplete} from '../../components/TableTopBar';
type OrderBy = 'moniker' | 'created_at' | 'ended_at';
type OrderDirection = 'asc' | 'desc';
@@ -190,226 +190,199 @@ export default function AuditUser() {
};
return (
-
-
-
-
-
-
-
- {(user.deleted_at ?? null) != null ? (
-
- {displayUserName(user)}
-
- ) : (
-
- {displayUserName(user)}
-
- )}{' '}
- Audit
-
-
+
+
+
+ Member
+ Owner
+
+
+ Active
+ Inactive
+
+ row.name)}
+ onChange={handleSearchSubmit}
+ onInputChange={(event, newInputValue) => setSearchInput(newInputValue)}
+ defaultValue={searchQuery}
+ />
+
+
+
+
+
+
+ Group Name
+
+
+ Group Type
+ Member or Owner
+ Direct or via Role
+
+
+ Started
+
+
+ Added by
+
+
+ Ending
+
+
+ Removed by
+ Access Request
+ Justification
+
+
+
+ {rows.map((row) => (
+
+ row.ended_at == null || dayjs().isBefore(dayjs(row.ended_at))
+ ? highlight.success.main
+ : highlight.danger.main,
+ }}>
-
- Member
- Owner
-
+ {(row.group?.deleted_at ?? null) != null ? (
+
+ {row.group?.name ?? ''}
+
+ ) : (
+
+ {row.group?.name ?? ''}
+
+ )}
-
-
- Active
- Inactive
-
+
+ {(row.group?.deleted_at ?? null) != null ? (
+ displayGroupType(row.group)
+ ) : (
+
+ {displayGroupType(row.group)}
+
+ )}
-
- x}
- options={searchRows.map((row) => row.name)}
- onChange={handleSearchSubmit}
- onInputChange={(event, newInputValue) => setSearchInput(newInputValue)}
- defaultValue={searchQuery}
- key={searchQuery}
- renderInput={(params) => }
- />
+ {row.is_owner ? 'Owner' : 'Member'}
+
+ {row.role_group_mapping == null ? (
+
+ ) : (
+ navigate(`/roles/${row.role_group_mapping?.role_group?.name}`)}
+ />
+ )}
-
-
-
- Group Name
-
+
- Group Type
- Member or Owner
- Direct or via Role
-
- Started
-
+ {(row.created_actor?.deleted_at ?? null) != null ? (
+
+ {displayUserName(row.created_actor)}
+
+ ) : (
+
+ {displayUserName(row.created_actor)}
+
+ )}
- Added by
-
- Ending
-
+
- Removed by
- Access Request
- Justification
-
-
-
- {rows.map((row) => (
-
-
- {(row.group?.deleted_at ?? null) != null ? (
-
- {row.group?.name ?? ''}
-
- ) : (
-
- {row.group?.name ?? ''}
-
- )}
-
-
- {(row.group?.deleted_at ?? null) != null ? (
- displayGroupType(row.group)
- ) : (
-
- {displayGroupType(row.group)}
-
- )}
-
- {row.is_owner ? 'Owner' : 'Member'}
-
- {row.role_group_mapping == null ? (
-
- ) : (
- navigate(`/roles/${row.role_group_mapping?.role_group?.name}`)}
- />
- )}
-
-
-
-
-
- {(row.created_actor?.deleted_at ?? null) != null ? (
+
+ {row.ended_at != null && dayjs().isAfter(dayjs(row.ended_at)) ? (
+ (row.ended_actor?.deleted_at ?? null) != null ? (
- {displayUserName(row.created_actor)}
+ {displayUserName(row.ended_actor)}
) : (
- {displayUserName(row.created_actor)}
+ {displayUserName(row.ended_actor)}
- )}
-
-
-
-
-
- {row.ended_at != null && dayjs().isAfter(dayjs(row.ended_at)) ? (
- (row.ended_actor?.deleted_at ?? null) != null ? (
-
- {displayUserName(row.ended_actor)}
-
- ) : (
-
- {displayUserName(row.ended_actor)}
-
- )
- ) : (
- ''
- )}
-
-
- {row.access_request != null ? (
-
- View
-
- ) : null}
-
-
- {row.created_reason ? : null}
-
-
- ))}
- {emptyRows > 0 && (
-
-
-
- )}
-
-
-
-
+ )
+ ) : (
+ ''
+ )}
+
+
+ {row.access_request != null ? (
+
+ View
+
+ ) : null}
+
+
+ {row.created_reason ? : null}
+
+
+ ))}
+ {emptyRows > 0 && (
+
+
-
-
-
-
+ )}
+
+
+
+
+
+
+
+
);
}
diff --git a/src/pages/users/List.tsx b/src/pages/users/List.tsx
index f49ae1a..20a04c3 100644
--- a/src/pages/users/List.tsx
+++ b/src/pages/users/List.tsx
@@ -22,6 +22,8 @@ import {useGetUsers} from '../../api/apiComponents';
import TablePaginationActions from '../../components/actions/TablePaginationActions';
import UserAvatar from './UserAvatar';
import {displayUserName, perPage} from '../../helpers';
+import {Stack} from '@mui/material';
+import TableTopBar, {renderUserOption, TableTopBarAutocomplete} from '../../components/TableTopBar';
export default function ListUsers() {
const navigate = useNavigate();
@@ -102,110 +104,83 @@ export default function ListUsers() {
};
return (
-
-
-
-
-
+
+
+ displayUserName(row) + ';' + row.email.toLowerCase())}
+ onInputChange={(event, newInputValue) => {
+ setSearchInput(newInputValue?.split(';')[0] ?? '');
+ }}
+ onChange={handleSearchSubmit}
+ defaultValue={searchQuery}
+ key={searchQuery}
+ renderOption={renderUserOption}
+ />
+
+
+
+
+
+ Name
+ Email
+
+
+
+ {rows.map((row) => (
+
-
- Users
-
+
+
+
-
-
- x}
- options={searchRows.map((row) => displayUserName(row) + ';' + row.email.toLowerCase())}
- onInputChange={(event, newInputValue) => {
- setSearchInput(newInputValue?.split(';')[0] ?? '');
- }}
- onChange={handleSearchSubmit}
- defaultValue={searchQuery}
- key={searchQuery}
- renderInput={(params) => }
- renderOption={(props, option, state) => {
- const [displayName, email] = option.split(';');
- return (
-
-
-
- {displayName}
-
- {email}
-
-
-
-
- );
- }}
- />
+
+
+ {displayUserName(row)}
+
+
+
+
+ {row.email.toLowerCase()}
+
-
-
- Name
- Email
-
-
-
- {rows.map((row) => (
-
-
-
-
-
-
-
-
- {displayUserName(row)}
-
-
-
-
- {row.email.toLowerCase()}
-
-
-
- ))}
- {emptyRows > 0 && (
-
-
-
- )}
-
-
-
-
+ ))}
+ {emptyRows > 0 && (
+
+
-
-
-
-
+ )}
+
+
+
+
+
+
+
+
);
}
diff --git a/src/pages/users/Read.tsx b/src/pages/users/Read.tsx
index fd642e2..a70020d 100644
--- a/src/pages/users/Read.tsx
+++ b/src/pages/users/Read.tsx
@@ -38,6 +38,7 @@ import RemoveGroupsDialog, {RemoveGroupsDialogParameters} from '../roles/RemoveG
import RemoveOwnDirectAccessDialog, {RemoveOwnDirectAccessDialogParameters} from '../groups/RemoveOwnDirectAccess';
import {groupBy, displayUserName, displayGroupType} from '../../helpers';
import {canManageGroup, isGroupOwner} from '../../authorization';
+import {EmptyListEntry} from '../../components/EmptyListEntry';
function sortUserGroups(
[aGroupId, aGroups]: [string, Array],
@@ -66,7 +67,7 @@ function ProfileToCard({user}: ProfileToCardProps) {
{Object.entries(user.profile)
.filter(([_, v]) => v != null)
.map(([key, value]: [string, string]) => (
-
+
))}
@@ -92,14 +93,14 @@ function ReportingToCard({user}: ReportingToCardProps) {
+ sx={{textDecoration: 'none', color: 'inherit', padding: 0}}>
) : (
-
+
)}
@@ -140,7 +141,7 @@ function OwnerTable({user, ownerships, onClickRemoveGroupFromRole, onClickRemove
-
+
Owner of Group or Roles
@@ -235,17 +236,11 @@ function OwnerTable({user, ownerships, onClickRemoveGroupFromRole, onClickRemove
))
) : (
-
-
-
- None
-
-
-
+
)}
-
+
@@ -283,7 +278,7 @@ function MemberTable({user, memberships, onClickRemoveGroupFromRole, onClickRemo
-
+
Member of Groups or Roles
@@ -380,7 +375,7 @@ function MemberTable({user, memberships, onClickRemoveGroupFromRole, onClickRemo
) : (
-
+
None
@@ -453,43 +448,14 @@ export default function ReadUser() {
return (
-
+
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
{user.deleted_at != null ? (
<>
{displayUserName(user)} is Deleted
@@ -498,16 +464,19 @@ export default function ReadUser() {
displayUserName(user)
)}
- {user.email?.toLowerCase()}
-
-
-
-
-
-
-
-
-
+
+ {user.email?.toLowerCase()}
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/pages/users/UserAvatar.tsx b/src/pages/users/UserAvatar.tsx
index 7ebd176..ac89bab 100644
--- a/src/pages/users/UserAvatar.tsx
+++ b/src/pages/users/UserAvatar.tsx
@@ -1,24 +1,14 @@
+import {PaletteMode, useTheme} from '@mui/material';
import Avatar from '@mui/material/Avatar';
import Typography from '@mui/material/Typography';
-function stringToColor(string: string) {
- let hash = 0;
- let i;
-
- /* eslint-disable no-bitwise */
- for (i = 0; i < string.length; i += 1) {
- hash = string.charCodeAt(i) + ((hash << 5) - hash);
+function stringToColor(string: string, mode: PaletteMode) {
+ const hue = string.split('').reduce((acc, curr) => curr.charCodeAt(0) + acc, 0) % 360;
+ if (mode === 'dark') {
+ return `hsl(${hue}, 65%, 70%)`;
+ } else {
+ return `hsl(${hue}, 65%, 55%)`;
}
-
- let color = '#';
-
- for (i = 0; i < 3; i += 1) {
- const value = (hash >> (i * 8)) & 0xff;
- color += `00${value.toString(16)}`.slice(-2);
- }
- /* eslint-enable no-bitwise */
-
- return color;
}
interface UserAvatarProps {
@@ -28,13 +18,16 @@ interface UserAvatarProps {
}
export default function UserAvatar(props: UserAvatarProps) {
+ const {
+ palette: {mode},
+ } = useTheme();
const splitName = props.name.split(' ');
return (