Skip to content

Commit

Permalink
feat(CentralIdentity): initial admin consoles UI (#99)
Browse files Browse the repository at this point in the history
  • Loading branch information
jakeaturner authored Aug 2, 2023
1 parent ec3c0fe commit 705e73f
Show file tree
Hide file tree
Showing 19 changed files with 1,665 additions and 7 deletions.
8 changes: 8 additions & 0 deletions client/src/Conductor.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,10 @@ import Search from './components/search/Search';
import UserDetails from './components/controlpanel/UserDetails';
import UsersManager from './components/controlpanel/UsersManager';
import LoadingSpinner from './components/LoadingSpinner';
const CentralIdentity = lazy(() => import('./screens/conductor/controlpanel/CentralIdentity'));
const CentralIdentityOrgs = lazy(() => import('./screens/conductor/controlpanel/CentralIdentity/CentralIdentityOrgs'));
const CentralIdentityServices = lazy(() => import('./screens/conductor/controlpanel/CentralIdentity/CentralIdentityServices'));
const CentralIdentityUsers = lazy(() => import('./screens/conductor/controlpanel/CentralIdentity/CentralIdentityUsers'));

/* 404 */
import PageNotFound from './components/util/PageNotFound';
Expand Down Expand Up @@ -93,6 +97,10 @@ const Conductor = () => {
<PrivateRoute exact path='/controlpanel/eventsmanager/:mode/:eventID?' component={ManageEvent} />
<PrivateRoute exact path='/controlpanel/harvestingrequests' component={HarvestingRequests} />
<PrivateRoute exact path='/controlpanel/homeworkmanager' component={HomeworkManager} />
<PrivateRoute exact path='/controlpanel/central-identity' component={CentralIdentity} />
<PrivateRoute exact path='/controlpanel/central-identity/orgs' component={CentralIdentityOrgs} />
<PrivateRoute exact path='/controlpanel/central-identity/services' component={CentralIdentityServices} />
<PrivateRoute exact path='/controlpanel/central-identity/users' component={CentralIdentityUsers} />
<PrivateRoute exact path='/controlpanel/orgsmanager' component={OrganizationsManager} />
<PrivateRoute exact path='/controlpanel/peerreviewrubrics' component={PeerReviewRubrics} />
<PrivateRoute exact path='/controlpanel/peerreviewrubrics/:mode/:rubricID?' component={PeerReviewRubricManage} />
Expand Down
60 changes: 60 additions & 0 deletions client/src/components/ControlledInputs/CtlCheckbox.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { FieldValues, FieldPath, Controller } from "react-hook-form";
import {
Checkbox,
CheckboxProps,
Form,
FormInputProps,
} from "semantic-ui-react";
import { ControlledInputProps } from "../../types";

interface CtlCheckboxProps extends CheckboxProps {
label?: string;
labelDirection?: "col" | "row";
required?: boolean;
}

/**
* Semantic UI Checkbox component wrapped in react-hook-form controller
* Fall-through props allow for finer-grained control and styling on a case-by-case basis
*/
export default function CtlCheckbox<
TFieldValues extends FieldValues = FieldValues,
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>
>({
name,
control,
rules,
label,
labelDirection = "row",
required = false,
...rest
}: ControlledInputProps<TFieldValues, TName> & CtlCheckboxProps) {
return (
<Controller
control={control}
name={name}
rules={rules}
render={({
field: { value, onChange, onBlur },
fieldState: { error },
}) => (
<div className={labelDirection === 'row' ? 'flex-row-div' : 'flex-col-div'}>
{label && (
<label
className={`form-field-label ${required ? "form-required" : ""}`}
>
{label}
</label>
)}
<Checkbox
value={value}
onChange={onChange}
onBlur={onBlur}
error={error?.message}
{...rest}
/>
</div>
)}
/>
);
}
262 changes: 262 additions & 0 deletions client/src/components/controlpanel/CentralIdentity/ManageUserModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,262 @@
import "../../../styles/global.css";
import {
Modal,
Button,
Icon,
ModalProps,
Header,
Table,
Feed,
} from "semantic-ui-react";
import { useState, useEffect } from "react";
import { CentralIdentityUser, User } from "../../../types";
import CtlTextInput from "../../ControlledInputs/CtlTextInput";
import { useForm } from "react-hook-form";
import {
accountStatusOptions,
getPrettyAuthSource,
getPrettyUserType,
getPrettyVerficationStatus,
} from "../../../utils/centralIdentityHelpers";
import CtlCheckbox from "../../ControlledInputs/CtlCheckbox";

interface ManageUserModalProps extends ModalProps {
show: boolean;
user: CentralIdentityUser;
onSave: () => void;
onClose: () => void;
}

const ManageUserModal: React.FC<ManageUserModalProps> = ({
show,
user,
onSave,
onClose,
...rest
}) => {
const { control, formState, reset } = useForm<CentralIdentityUser>({
defaultValues: user,
});

// UI
const [editingFirstName, setEditingFirstName] = useState<boolean>(false);
const [editingLastName, setEditingLastName] = useState<boolean>(false);

// Effects

useEffect(() => {
// Reset editing states when modal is closed
if (!show) {
setEditingFirstName(false);
setEditingLastName(false);
}
}, [show]);

// Handlers
function handleCancel() {
reset();
onClose();
}

function handleResetFirstName() {
reset({ first_name: user.first_name });
setEditingFirstName(false);
}

function handleResetLastName() {
reset({ last_name: user.last_name });
setEditingLastName(false);
}

return (
<Modal open={show} onClose={onClose} {...rest} size="fullscreen">
<Modal.Header>Manage User</Modal.Header>
<Modal.Content scrolling id="task-view-content">
<div className="flex-col-div">
<div className="flex-row-div" id="project-task-header">
<div className="task-detail-div">
<Header sub>Avatar</Header>
<Feed.Label>
<img
width={40}
height={40}
src={user.avatar ?? ""}
alt="avatar"
/>
</Feed.Label>
</div>
<div className="task-detail-div">
<Header sub>First Name</Header>
<div className="task-detail-textdiv">
{editingFirstName && (
<div className="mt-2p flex-row-div">
<CtlTextInput
name="first_name"
control={control}
rules={{ required: true }}
/>
<Icon
name="close"
size="small"
onClick={() => handleResetFirstName()}
/>
</div>
)}
{!editingFirstName && (
<p>
{user.first_name}{" "}
<Icon
name="pencil"
size="small"
onClick={() => setEditingFirstName(true)}
/>
</p>
)}
</div>
</div>
<div className="task-detail-div">
<Header sub>Last Name</Header>
<div className="task-detail-textdiv">
{editingLastName && (
<div className="mt-2p flex-row-div">
<CtlTextInput
name="last_name"
control={control}
rules={{ required: true }}
/>
<Icon
name="close"
size="small"
onClick={() => handleResetLastName()}
/>
</div>
)}
{!editingLastName && (
<p>
{user.last_name}{" "}
<Icon
name="pencil"
size="small"
onClick={() => setEditingLastName(true)}
/>
</p>
)}
</div>
</div>
<div className="task-detail-div">
<Header sub>Email</Header>
<p>{user.email}</p>
</div>
<div className="task-detail-div">
<Header sub>Account Status</Header>
<CtlCheckbox name="disabled" control={control} toggle />
</div>
</div>
<div className="flex-row-div" id="project-task-page">
<div id="task-view-left">
<div className="mt-1p mb-4p">
<div className="dividing-header-custom">
<h3>Permissions</h3>
</div>
<div className="flex-col-div">
<div className="flex-row-div mt-2p mb-2p">
<p>
<strong>User Type: </strong>
{getPrettyUserType(user.user_type)}
</p>
</div>
<div className="flex-row-div mb-2p">
<p>
<strong>Verification Status: </strong>
{getPrettyVerficationStatus(user.verify_status)}
</p>
</div>
</div>
</div>
<div className="mt-1p mb-4p">
<div className="dividing-header-custom">
<h3>Authentication & Security Data</h3>
</div>
<div className="flex-col-div">
<div className="flex-row-div mt-1p mb-2p">
<p>
<strong>Authentication Source: </strong>
{user.external_idp
? getPrettyAuthSource(user.external_idp)
: "LibreOne"}
</p>
</div>
<div className="flex-row-div mb-2p">
<p>
<strong>Time of Last Access:</strong> Unknown
</p>
</div>
<div className="flex-row-div">
<p>
<strong>Time of Last Password Change: </strong>
{user.last_password_change ?? "Never"}
</p>
</div>
</div>
</div>
</div>
<div id="task-view-right">
<div id="task-view-chat">
<Table striped celled size="small" compact>
<Table.Header>
<Table.Row key="header1">
<Table.HeaderCell colSpan="2">
<span>Organizations</span>
</Table.HeaderCell>
</Table.Row>
<Table.Row key="header2">
<Table.HeaderCell key="orgNameHeader">
<span>Name</span>
</Table.HeaderCell>
<Table.HeaderCell key="orgSystemHeader">
<span>System (if applicable)</span>
</Table.HeaderCell>
</Table.Row>
</Table.Header>
<Table.Body>
{user.organizations.length > 0 &&
user.organizations.map((org) => {
return (
<Table.Row key={org.id} className="word-break-all">
<Table.Cell>
<span>{org.name}</span>
</Table.Cell>
<Table.Cell>
<span></span>
</Table.Cell>
</Table.Row>
);
})}
{user.organizations.length === 0 && (
<Table.Row textAlign="center">
<Table.Cell colSpan="2">
<em>No associated organizations found.</em>
</Table.Cell>
</Table.Row>
)}
</Table.Body>
</Table>
</div>
</div>
</div>
</div>
</Modal.Content>
<Modal.Actions>
<Button onClick={handleCancel}>Cancel</Button>
{formState.isDirty && (
<Button color="green" onClick={onSave}>
<Icon name="save" />
Save Changes
</Button>
)}
</Modal.Actions>
</Modal>
);
};

export default ManageUserModal;
18 changes: 12 additions & 6 deletions client/src/components/controlpanel/ControlPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,12 @@ const ControlPanel = () => {
title: 'Analytics Access Requests',
description: 'View requests to access LibreTexts textbook analytics feeds'
},
{
url: '/controlpanel/central-identity',
icon: 'key',
title: 'Central Identity',
description: 'View and manage users and organizations on the LibreOne platform'
},
{
url: '/controlpanel/eventsmanager',
icon: 'calendar alternate outline',
Expand All @@ -68,18 +74,18 @@ const ControlPanel = () => {
title: 'Harvesting Requests',
description: 'View and manage OER Integration Requests submitted to the Conductor platform'
},
{
url: '/controlpanel/orgsmanager',
icon: 'building',
title: 'Organizations Manager',
description: 'View and manage Organizations on the Conductor platform'
},
{
url: '/controlpanel/homeworkmanager',
icon: 'tasks',
title: 'Homework Manager',
description: 'View and manage Homework resources listed on the LibreCommons'
},
{
url: '/controlpanel/orgsmanager',
icon: 'sitemap',
title: 'Organizations Manager',
description: 'View and manage Organizations on the Conductor platform'
},
];


Expand Down
Loading

0 comments on commit 705e73f

Please sign in to comment.