Skip to content
Draft
Show file tree
Hide file tree
Changes from 3 commits
Commits
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
3 changes: 2 additions & 1 deletion src/common/orgs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,4 +27,5 @@ export const CommitteeList = [
"Corporate Committee",
"Marketing Committee",
] as const;
export const OrganizationList = ["ACM", ...SIGList, ...CommitteeList];

export const OrganizationList = ["ACM", ...SIGList, ...CommitteeList] as const;
29 changes: 27 additions & 2 deletions src/common/types/iam.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { OrganizationList } from "../orgs.js";
import { AppRoles } from "../roles.js";
import { z } from "zod";

Expand All @@ -23,7 +24,8 @@ export type InviteUserPostRequest = z.infer<typeof invitePostRequestSchema>;

export const groupMappingCreatePostSchema = z.object({
roles: z.union([
z.array(z.nativeEnum(AppRoles))
z
.array(z.nativeEnum(AppRoles))
.min(1)
.refine((items) => new Set(items).size === items.length, {
message: "All roles must be unique, no duplicate values allowed",
Expand All @@ -32,7 +34,6 @@ export const groupMappingCreatePostSchema = z.object({
]),
});


export type GroupMappingCreatePostRequest = z.infer<
typeof groupMappingCreatePostSchema
>;
Expand Down Expand Up @@ -65,3 +66,27 @@ export const entraGroupMembershipListResponse = z.array(
export type GroupMemberGetResponse = z.infer<
typeof entraGroupMembershipListResponse
>;

const userOrgSchema = z.object({
netid: z.string().min(1),
org: z.enum(OrganizationList),
});
const userOrgsSchema = z.array(userOrgSchema);

const userNameSchema = z.object({
netid: z.string().min(1),
firstName: z.string().min(1),
middleName: z.string().optional(),
lastName: z.string().min(1),
});
const userNamesSchema = z.array(userNameSchema);

const userSchema = userNameSchema.merge(userOrgSchema);
const usersSchema = z.array(userSchema);

export type UserOrg = z.infer<typeof userOrgSchema>;
export type UserOrgs = z.infer<typeof userOrgsSchema>;
export type UserName = z.infer<typeof userNameSchema>;
export type UserNames = z.infer<typeof userNamesSchema>;
export type User = z.infer<typeof userSchema>;
export type Users = z.infer<typeof usersSchema>;
5 changes: 5 additions & 0 deletions src/ui/Router.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { ScanTicketsPage } from './pages/tickets/ScanTickets.page';
import { SelectTicketsPage } from './pages/tickets/SelectEventId.page';
import { ViewTicketsPage } from './pages/tickets/ViewTickets.page';
import { ManageIamPage } from './pages/iam/ManageIam.page';
import { ScreenPage } from './pages/screen/Screen.page';

// Component to handle redirects to login with return path
const LoginRedirect: React.FC = () => {
Expand Down Expand Up @@ -119,6 +120,10 @@ const authenticatedRouter = createBrowserRouter([
path: '/tickets/manage/:eventId',
element: <ViewTicketsPage />,
},
{
path: '/iam/leads',
element: <ScreenPage />,
},
// Catch-all route for authenticated users shows 404 page
{
path: '*',
Expand Down
239 changes: 239 additions & 0 deletions src/ui/pages/screen/Screen.page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,239 @@
import { Text, Button, Table, Modal, Group, Transition, ButtonGroup } from '@mantine/core';
import { useDisclosure } from '@mantine/hooks';
import { notifications } from '@mantine/notifications';
import { IconPlus, IconTrash } from '@tabler/icons-react';
// import dayjs from 'dayjs';
import React, { useEffect, useState } from 'react';
// import { useNavigate } from 'react-router-dom';
import { z } from 'zod';

// import { capitalizeFirstLetter } from './ManageEvent.page.js';
import FullScreenLoader from '@ui/components/AuthContext/LoadingScreen';
import { AuthGuard } from '@ui/components/AuthGuard';
import { useApi } from '@ui/util/api';
import { AppRoles } from '@common/roles.js';
import { OrganizationList } from '@common/orgs';
import { User, UserNames, UserOrgs, Users } from '@common/types/iam';

// const repeatOptions = ['weekly', 'biweekly'] as const;

// export type EventGetResponse = z.infer<typeof getEventSchema>;
// const getEventsSchema = z.array(getEventSchema);
// export type EventsGetResponse = z.infer<typeof getEventsSchema>;

export const ScreenPage: React.FC = () => {
const [userList, setUserList] = useState<Users>([]);
const api = useApi('core');
const [opened, { open, close }] = useDisclosure(false);
// const [showPrevious, { toggle: togglePrevious }] = useDisclosure(false); // Changed default to false
const [userRemoved, setRemoveUser] = useState<User | null>(null);
// const navigate = useNavigate();

const renderTableRow = (user: User) => {
// const shouldShow = event.upcoming || (!event.upcoming && showPrevious);

return (
// <Transition mounted={shouldShow} transition="fade" duration={400} timingFunction="ease">
<Transition mounted={true} transition="fade" duration={400} timingFunction="ease">
{(styles) => (
// <tr style={{ ...styles, display: shouldShow ? 'table-row' : 'none' }}>
<tr style={{ ...styles, display: 'table-row' }}>
<Table.Td>{user.netid}</Table.Td>
<Table.Td>{user.firstName}</Table.Td>
<Table.Td>{user.middleName}</Table.Td>
<Table.Td>{user.lastName}</Table.Td>
<Table.Td>{user.org}</Table.Td>
{/* <Table.Td>{dayjs(event.start).format('MMM D YYYY hh:mm')}</Table.Td>
<Table.Td>{event.end ? dayjs(event.end).format('MMM D YYYY hh:mm') : 'N/A'}</Table.Td>
<Table.Td>{event.location}</Table.Td>
<Table.Td>{event.description}</Table.Td>
<Table.Td>{event.host}</Table.Td>
<Table.Td>{event.featured ? 'Yes' : 'No'}</Table.Td> */}
{/* <Table.Td>{capitalizeFirstLetter(event.repeats || 'Never')}</Table.Td> */}
<Table.Td>
<ButtonGroup>
{/* <Button component="a">Edit</Button> */}
<Button
color="red"
onClick={() => {
setRemoveUser(user);
open();
}}
>
Remove User
</Button>
</ButtonGroup>
</Table.Td>
</tr>
)}
</Transition>
);
};

useEffect(() => {
const getUsers = async () => {
// const response = await api.get('/api/v1/events');
// const upcomingEvents = await api.get('/api/v1/events?upcomingOnly=true');
// const upcomingEventsSet = new Set(upcomingEvents.data.map((x: EventGetResponse) => x.id));
// const events = response.data;
// events.sort((a: User, b: User) => {
// return a.start.localeCompare(b.start);
// });
// const enrichedResponse = response.data.map((item: EventGetResponse) => {
// if (upcomingEventsSet.has(item.id)) {
// return { ...item, upcoming: true };
// }
// return { ...item, upcoming: false };
// });

// get request for user orgs
const userOrgsResponse: UserOrgs = [
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

UserOrgs -> UserOrg[]

{ netid: 'johnd01', org: 'SIGMusic' },
{ netid: 'miker44', org: 'SIGPLAN' },
{ netid: 'chrisb19', org: 'SIGCHI' },
{ netid: 'ethanw12', org: 'SIGecom' },
{ netid: 'emilyh54', org: 'SIGRobotics' },
{ netid: 'juliel08', org: 'SIGGRAPH' },
{ netid: 'rachelb03', org: 'GameBuilders' },
{ netid: 'ashleyc28', org: 'SIGNLL' },
{ netid: 'briand77', org: 'SIGma' },
{ netid: 'meganf65', org: 'SIGPolicy' },
{ netid: 'danielh04', org: 'SIGARCH' },
{ netid: 'lindam29', org: 'SIGMobile' },
{ netid: 'paulf31', org: 'SIGMusic' },
{ netid: 'markl13', org: 'SIGCHI' },
{ netid: 'carolynb59', org: 'ACM' },
{ netid: 'nataliep71', org: 'SIGPolicy' },

{ netid: 'ethanc12', org: 'Infrastructure Committee' },
{ netid: 'sarahg23', org: 'SIGQuantum' },
{ netid: 'annaw02', org: 'SIGMobile' },
{ netid: 'laurenp87', org: 'SIGPwny' },
{ netid: 'kevink11', org: 'Infrastructure Committee' },
{ netid: 'mattt92', org: 'SIGtricity' },
{ netid: 'stephenj45', org: 'SIGAIDA' },
{ netid: 'victorc16', org: 'GLUG' },
{ netid: 'susana80', org: 'SIGPwny' },
{ netid: 'patrickh37', org: 'SIGQuantum' },
];

// retrieve from azure active directory (aad)
const userNamesResponse: UserNames = [
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same thing here

{ netid: 'johnd01', firstName: 'John', lastName: 'Doe' },
{ netid: 'miker44', firstName: 'Michael', lastName: 'Roberts' },
{ netid: 'chrisb19', firstName: 'Christopher', lastName: 'Brown' },
{ netid: 'ethanw12', firstName: 'Ethan', lastName: 'Wong' },
{ netid: 'emilyh54', firstName: 'Emily', lastName: 'Hernandez' },
{ netid: 'juliel08', firstName: 'Julie', lastName: 'Lopez' },
{ netid: 'rachelb03', firstName: 'Rachel', lastName: 'Bell' },
{ netid: 'ashleyc28', firstName: 'Ashley', lastName: 'Clark' },
{ netid: 'briand77', firstName: 'Brian', lastName: 'Davis' },
{ netid: 'meganf65', firstName: 'Megan', lastName: 'Flores' },
{ netid: 'danielh04', firstName: 'Daniel', lastName: 'Hughes' },
{ netid: 'lindam29', firstName: 'Linda', lastName: 'Martinez' },
{ netid: 'paulf31', firstName: 'Paul', lastName: 'Fisher' },
{ netid: 'markl13', firstName: 'Mark', lastName: 'Lewis' },
{ netid: 'carolynb59', firstName: 'Carolyn', lastName: 'Barnes' },
{ netid: 'nataliep71', firstName: 'Natalie', lastName: 'Price' },

{ netid: 'ethanc12', firstName: 'Ethan', middleName: 'Yuting', lastName: 'Chang' },
{ netid: 'sarahg23', firstName: 'Sarah', middleName: 'Grace', lastName: 'Gonzalez' },
{ netid: 'annaw02', firstName: 'Anna', middleName: 'Marie', lastName: 'Williams' },
{ netid: 'laurenp87', firstName: 'Lauren', middleName: 'Patricia', lastName: 'Perez' },
{ netid: 'kevink11', firstName: 'Kevin', middleName: 'Lee', lastName: 'Kim' },
{ netid: 'mattt92', firstName: 'Matthew', middleName: 'Thomas', lastName: 'Taylor' },
{ netid: 'stephenj45', firstName: 'Stephen', middleName: 'James', lastName: 'Johnson' },
{ netid: 'victorc16', firstName: 'Victor', middleName: 'Charles', lastName: 'Carter' },
{ netid: 'susana80', firstName: 'Susan', middleName: 'Ann', lastName: 'Anderson' },
{ netid: 'patrickh37', firstName: 'Patrick', middleName: 'Henry', lastName: 'Hill' },
];

const mergedResponse: Users = userOrgsResponse.map((orgObj) => {
const nameObj = userNamesResponse.find((name) => name.netid === orgObj.netid);
return { ...orgObj, ...nameObj } as User;
});

setUserList(mergedResponse);
};
getUsers();
}, []);

const removeUser = async (netid: string) => {
try {
// await api.delete(`/api/v1/events/${eventId}`);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we would probably call something like PATCH /api/v1/iam/:orgName/leads with a body {remove: [email]}

setUserList((prevUsers) => prevUsers.filter((u) => u.netid !== netid));
notifications.show({
title: 'User removed',
message: 'The user was successfully removed.',
});
close();
} catch (error) {
console.error(error);
notifications.show({
title: 'Error removing user',
message: `${error}`,
color: 'red',
});
}
};

if (userList.length === 0) {
return <FullScreenLoader />;
}

return (
<AuthGuard resourceDef={{ service: 'core', validRoles: [AppRoles.IAM_ADMIN] }}>
{userRemoved && (
<Modal
opened={opened}
onClose={() => {
setRemoveUser(null);
close();
}}
title="Confirm action"
>
<Text>
Are you sure you want to remove the user <i>{userRemoved?.netid}</i>?
</Text>
<hr />
<Group>
<Button
leftSection={<IconTrash />}
onClick={() => {
removeUser(userRemoved?.netid);
}}
>
Delete
</Button>
</Group>
</Modal>
)}
{/* <div style={{ display: 'flex', columnGap: '1vw', verticalAlign: 'middle' }}>
<Button
leftSection={<IconPlus size={14} />}
onClick={() => {
navigate('/events/add');
}}
>
New Calendar Event
</Button>
<Button onClick={togglePrevious}>
{showPrevious ? 'Hide Previous Events' : 'Show Previous Events'}
</Button>
</div> */}
<Table style={{ tableLayout: 'fixed', width: '100%' }} data-testid="users-table">
<Table.Thead>
<Table.Tr>
<Table.Th>NetID</Table.Th>
<Table.Th>First Name</Table.Th>
<Table.Th>Middle Name</Table.Th>
<Table.Th>Last Name</Table.Th>
<Table.Th>Organization</Table.Th>
<Table.Th>Actions</Table.Th>
</Table.Tr>
</Table.Thead>
<Table.Tbody>{userList.map(renderTableRow)}</Table.Tbody>
</Table>
</AuthGuard>
);
};
Loading