diff --git a/client/src/Conductor.jsx b/client/src/Conductor.jsx index 1d1e8c6c..ba962786 100644 --- a/client/src/Conductor.jsx +++ b/client/src/Conductor.jsx @@ -55,8 +55,6 @@ const ProjectsFlagged = lazy(() => import('./screens/conductor/Projects/Projects import ProjectTimeline from './components/projects/ProjectTimeline'; import ProjectView from './components/projects/ProjectView'; const Search = lazy(() => import('./screens/conductor/Search')); -import UserDetails from './components/controlpanel/UserDetails'; -const UsersManager = lazy(() => import('./screens/conductor/controlpanel/UsersManager')); import LoadingSpinner from './components/LoadingSpinner'; const CentralIdentity = lazy(() => import('./screens/conductor/controlpanel/CentralIdentity')); const CentralIdentityInstructorVerifications = lazy(() => import('./screens/conductor/controlpanel/CentralIdentity/CentralIdentityInstructorVerifications')); @@ -150,8 +148,6 @@ const Conductor = () => { - - diff --git a/client/src/components/controlpanel/ControlPanel.tsx b/client/src/components/controlpanel/ControlPanel.tsx index d554f585..c9c6df7d 100644 --- a/client/src/components/controlpanel/ControlPanel.tsx +++ b/client/src/components/controlpanel/ControlPanel.tsx @@ -143,13 +143,6 @@ const ControlPanel = () => { description: "Manage branding settings for your Conductor instance and your Campus Commons", }, - { - url: "/controlpanel/usersmanager", - icon: "users", - title: "Users Manager", - description: - "See which Conductor users have access to your instance and manage their permissions", - }, ]; if (org.FEAT_AssetTagsManager) { diff --git a/client/src/components/controlpanel/UserDetails.tsx b/client/src/components/controlpanel/UserDetails.tsx deleted file mode 100644 index a5137c80..00000000 --- a/client/src/components/controlpanel/UserDetails.tsx +++ /dev/null @@ -1,320 +0,0 @@ -import "./ControlPanel.css"; - -import { - Grid, - Header, - Segment, - Table, - Breadcrumb, - Image, -} from "semantic-ui-react"; -import { useEffect, useState, useCallback } from "react"; -import { Link } from "react-router-dom"; -import { RouteComponentProps } from "react-router-dom"; -import axios from "axios"; -import { format, parseISO } from "date-fns"; - -import { - getClassificationText, - getVisibilityText, -} from "../util/ProjectHelpers.js"; -import { truncateString } from "../util/HelperFunctions.js"; -import useGlobalError from "../error/ErrorHooks"; -import { Project, User } from "../../types"; - -interface MatchParams { - uuid: string; -} - -const UserDetails = (props: RouteComponentProps) => { - // Global State - const { handleGlobalError } = useGlobalError(); - - // Data - const [userData, setUserData] = useState({} as User); - const [userProjects, setUserProjects] = useState([]); - - // UI - const [loadedData, setLoadedData] = useState(false); - - /** - * Retrieves a list of the user's Projects from the server. - */ - const getUserProjects = useCallback(() => { - setLoadedData(false); - axios - .get("/user/projects", { - params: { - uuid: props.match.params.uuid, - }, - }) - .then((res) => { - if (!res.data.err) { - if (Array.isArray(res.data.projects)) { - let sorted = res.data.projects.sort((a: Project, b: Project) => { - let aNormal = a.title.toLowerCase(); - let bNormal = b.title.toLowerCase(); - if (aNormal < bNormal) { - return -1; - } - if (aNormal > bNormal) { - return 1; - } - return 0; - }); - setUserProjects(sorted); - } - } else { - handleGlobalError(res.data.errMsg); - } - setLoadedData(true); - }) - .catch((err) => { - setLoadedData(true); - handleGlobalError(err); - }); - }, [props.match, setLoadedData, handleGlobalError]); - - /** - * Retrieve the user's basic information from the server. - */ - const getUser = useCallback(() => { - if ( - typeof props.match?.params?.uuid === "string" && - props.match?.params?.uuid.length > 0 - ) { - setLoadedData(false); - axios - .get("/user/admininfo", { - params: { - uuid: props.match.params.uuid, - }, - }) - .then((res) => { - if (!res.data.err) { - if (typeof res.data.user === "object") { - const createdDate = new Date(res.data.user.createdAt); - setUserData({ - ...res.data.user, - createdAt: createdDate.toDateString(), - }); - getUserProjects(); - } - } else { - handleGlobalError(res.data.errMsg); - } - setLoadedData(true); - }) - .catch((err) => { - setLoadedData(true); - handleGlobalError(err); - }); - } - }, [ - props.match, - setLoadedData, - setUserData, - getUserProjects, - handleGlobalError, - ]); - - /** - * Set page title and retrieve user information on initial load. - */ - useEffect(() => { - document.title = "LibreTexts Conductor | User Details"; - getUser(); - }, [getUser]); - - return ( - - - -
User Details
-
-
- - - - - - - Control Panel - - - - Users Manager - - - User Details - - - - - - - - - -
- {userData.firstName} {userData.lastName} -
- - - -
Email
- {userData.email ? ( -

{userData.email}

- ) : ( -

- Unknown -

- )} -
-
-
-
-
-
-
- User Projects -
- - - - -
Title
-
- -
Progress (C/PR/A11Y)
-
- -
Classification
-
- -
Visibility
-
- -
Lead
-
- -
Last Updated
-
-
-
- - {userProjects.length > 0 && - userProjects.map((item, index) => { - let projectLead = "Unknown"; - if (item.leads && Array.isArray(item.leads)) { - item.leads.forEach((lead, leadIdx) => { - if (lead.firstName && lead.lastName) { - if (leadIdx > 0) - projectLead += `, ${lead.firstName} ${lead.lastName}`; - else if (leadIdx === 0) - projectLead = `${lead.firstName} ${lead.lastName}`; - } - }); - } - if (!item.hasOwnProperty("peerProgress")) - item.peerProgress = 0; - if (!item.hasOwnProperty("a11yProgress")) - item.a11yProgress = 0; - return ( - - -

- - - {truncateString(item.title, 100)} - - -

-
- -
-
- {item.currentProgress}% -
-
- - / - -
-
- {item.peerProgress}% -
-
- - / - -
-
- {item.a11yProgress}% -
-
-
- - {item.classification ? ( -

- {getClassificationText(item.classification)} -

- ) : ( -

- Unclassified -

- )} -
- - {typeof item.visibility === "string" && - item.visibility ? ( -

{getVisibilityText(item.visibility)}

- ) : ( -

- Unknown -

- )} -
- -

{truncateString(projectLead, 50)}

-
- -

- {format( - parseISO(item.updatedAt ?? ""), - "MM/dd/yyyy" - )}{" "} - at{" "} - {format( - parseISO(item.updatedAt ?? ""), - "h:mm aa" - )} -

-
-
- ); - })} - {userProjects.length === 0 && ( - - -

- No results found. -

-
-
- )} -
-
-
-
-
-
-
- ); -}; - -export default UserDetails; diff --git a/client/src/components/controlpanel/UsersManager/ManageUserRolesModal.tsx b/client/src/components/controlpanel/UsersManager/ManageUserRolesModal.tsx deleted file mode 100644 index 1693f810..00000000 --- a/client/src/components/controlpanel/UsersManager/ManageUserRolesModal.tsx +++ /dev/null @@ -1,193 +0,0 @@ -import axios from "axios"; -import { Button, Dropdown, List, Loader, Modal, Icon } from "semantic-ui-react"; -import { Organization } from "../../../types"; -import React, { useEffect, useState } from "react"; -import useGlobalError from "../../error/ErrorHooks"; -import { UserRoleOptions } from "../../../utils/userHelpers"; -import api from "../../../api"; - -type ManageUserRolesModalProps = { - firstName: string; - isSuperAdmin: boolean; - lastName: string; - onClose: () => void; - orgID: string; - show: boolean; - uuid: string; -}; - -const ManageUserRolesModal: React.FC = ({ - firstName, - isSuperAdmin, - lastName, - onClose, - orgID, - show, - uuid, - ...props -}) => { - const { handleGlobalError } = useGlobalError(); - const allRoleOpts = UserRoleOptions; - const roleOpts = UserRoleOptions.filter((o) => o.value !== "superadmin"); - - const [allOrganizations, setAllOrganizations] = useState([]); - const [loading, setLoading] = useState(false); - const [userRoles, setUserRoles] = useState< - { org: Organization; role: string; roleInternal: string }[] - >([]); - - async function getAllOrganizations() { - try { - setLoading(true); - const res = await axios.get("/orgs"); - if (res.data?.errMsg) { - handleGlobalError(res.data.errMsg); - return; - } - if (!Array.isArray(res.data?.orgs)) return; - const orgs = (res.data.orgs as Organization[]).sort((a, b) => - a.name.localeCompare(b.name) - ); - setAllOrganizations( - isSuperAdmin ? orgs : orgs.filter((org) => org.orgID === orgID) - ); - } catch (err) { - handleGlobalError(err); - } finally { - setLoading(false); - } - } - - async function getUserRoles() { - try { - setLoading(true); - const res = await axios.get("/user/roles", { - params: { - uuid, - }, - }); - if (res.data?.errMsg) { - handleGlobalError(res.data.errMsg); - return; - } - if (!res.data?.user || !Array.isArray(res.data?.user?.roles)) return; - setUserRoles(res.data.user.roles); - } catch (err) { - } finally { - setLoading(false); - } - } - - useEffect(() => { - if (show) { - getAllOrganizations(); - getUserRoles(); - } else { - setAllOrganizations([]); - setUserRoles([]); - } - }, [show]); - - async function updateUserRole(newRole: string, orgToUpdateID: string) { - try { - setLoading(true); - const res = await axios.put("/user/role/update", { - orgID: orgToUpdateID, - role: newRole, - uuid, - }); - if (res.data?.errMsg) { - handleGlobalError(res.data.errMsg); - return; - } - await getUserRoles(); - } catch (err) { - handleGlobalError(err); - } finally { - setLoading(false); - } - } - - async function deleteUserRole(orgID: string) { - try { - setLoading(true); - const res = await api.deleteUserRole(orgID, uuid); - await getUserRoles(); - } catch (err) { - handleGlobalError(err); - } finally { - setLoading(false); - } - } - - return ( - - - - {firstName} {lastName}: - {" "} - Manage User Roles - - - {loading && } - - {allOrganizations.map((org) => { - const currentRole = userRoles.find( - (r) => r.org?.orgID === org.orgID - )?.roleInternal; - const hasRole = !!currentRole; - return ( - -
- -
- { - if (typeof value !== "string") return; - const current = userRoles.find( - (r) => r.org?.orgID === org.orgID - )?.roleInternal; - if (value === current) return; - updateUserRole(value, org.orgID); - }} - options={ - org.orgID === "libretexts" ? allRoleOpts : roleOpts - } - placeholder="No role set" - selection - value={currentRole || ""} - /> - -
-
-
- ); - })} -
-
- - - -
- ); -}; - -export default ManageUserRolesModal; diff --git a/client/src/screens/conductor/controlpanel/UsersManager/index.tsx b/client/src/screens/conductor/controlpanel/UsersManager/index.tsx deleted file mode 100644 index 35339ff2..00000000 --- a/client/src/screens/conductor/controlpanel/UsersManager/index.tsx +++ /dev/null @@ -1,262 +0,0 @@ -import '../../../../components/controlpanel/ControlPanel.css'; - -import { - Grid, - Header, - Segment, - Table, - Button, - Dropdown, - Icon, - Input, - Breadcrumb, -} from 'semantic-ui-react'; -import { useEffect, useState } from 'react'; -import { Link } from 'react-router-dom'; -import { useTypedSelector } from '../../../../state/hooks.js'; - -import { itemsPerPageOptions } from '../../../../components/util/PaginationOptions.js'; -import useGlobalError from '../../../../components/error/ErrorHooks.js'; -import { User } from '../../../../types'; -import ManageUserRolesModal from '../../../../components/controlpanel/UsersManager/ManageUserRolesModal.js'; -import { useQuery } from '@tanstack/react-query'; -import api from '../../../../api'; -import ConductorPagination from '../../../../components/util/ConductorPagination'; -import useDebounce from '../../../../hooks/useDebounce'; -import { useModals } from '../../../../context/ModalContext'; - -const UsersManager = () => { - - // Global State & Hooks - const { handleGlobalError } = useGlobalError(); - const org = useTypedSelector((state) => state.org); - const isSuperAdmin = useTypedSelector((state) => state.user.isSuperAdmin); - const { debounce } = useDebounce(); - const { openModal, closeAllModals } = useModals(); - - // UI - const [page, setPage] = useState(1); - const [itemsPerPage, setItemsPerPage] = useState(10); - const [sortChoice, setSortChoice] = useState('first'); - const [searchString, setSearchString] = useState(''); // for debounce, this is the value that will be used - const [searchInput, setSearchInput] = useState(''); // for debounce, this is the input value - - const { data, isFetching: loading } = useQuery({ - queryKey: ['users', searchString, sortChoice, page, itemsPerPage], - queryFn: () => getUsers({ query: searchString, limit: itemsPerPage, page, sort: sortChoice }), - refetchOnWindowFocus: false, - }) - - const sortOptions = [ - { key: 'first', text: 'Sort by First Name', value: 'first' }, - { key: 'last', text: 'Sort by Last Name', value: 'last' }, - { key: 'email', text: 'Sort by Email', value: 'email' }, - ]; - - - useEffect(() => { - document.title = "LibreTexts Conductor | Users Manager"; - }, []); - - async function getUsers({ query, limit, page, sort }: { query?: string, limit?: number, page?: number, sort?: string }): Promise<{ - results: User[], - total_items: number - }> { - try { - const res = await api.getUsers({ query, limit, page, sort }) - if (res.data.err) { - throw new Error(res.data.errMsg) - } - - return { - results: res.data.results, - total_items: res.data.total_items - } - } catch (err) { - handleGlobalError(err); - return { - results: [], - total_items: 0 - } - } - } - - const debouncedSearch = debounce((newVal: string) => { - setSearchString(newVal); - }, 200); - - const openManageUserModal = (uuid: string, firstName: string, lastName: string) => { - if (!uuid) return; - - openModal( - - ) - }; - - return ( - - - -
Users Manager
-
-
- - - - - - - Control Panel - - - - Users Manager - - - - - - - - { setSortChoice(value as string) }} - value={sortChoice} - /> - - - { - setSearchInput(e.target.value); - debouncedSearch(e.target.value); - }} - value={searchInput} - fluid - /> - - - - - -
-
- Displaying - { - setItemsPerPage(value as number); - }} - value={itemsPerPage} - /> - items per page of {Number(data?.total_items || 0).toLocaleString()} results. -
-
- 0 ? Math.ceil(data?.total_items / itemsPerPage) : 1} - onPageChange={(e, { activePage }) => { - setPage(activePage as number); - }} - /> -
-
-
- - - - - - {(sortChoice === 'first') - ? First Name - : First Name - } - - - {(sortChoice === 'last') - ? Last Name - : Last Name - } - - - {(sortChoice === 'email') - ? Email - : Email - } - - - Actions - - - - - {(data && data.results.length > 0) && - data.results.map((item, index) => { - return ( - - -

{item.firstName}

-
- -

{item.lastName}

-
- -

{item.email}

-
- - - - - - -
- ) - }) - } - {(!data || data.results.length === 0) && - - -

No results found.

-
-
- } -
-
-
-
-
-
-
- ) - -} - -export default UsersManager;