Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.
Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.
Path: blob/master/src/packages/frontend/course/students/students-panel.tsx
Views: 687
/*1* This file is part of CoCalc: Copyright © 2020 Sagemath, Inc.2* License: MS-RSL – see LICENSE.md for details3*/45import { Alert, Col, Input, Row } from "antd";6import { Set } from "immutable";7import { isEqual } from "lodash";8import { useEffect, useMemo, useState } from "react";9import { FormattedMessage, useIntl } from "react-intl";1011import { AppRedux, useRedux } from "@cocalc/frontend/app-framework";12import { Gap, Icon, Tip } from "@cocalc/frontend/components";13import ScrollableList from "@cocalc/frontend/components/scrollable-list";14import { course, labels } from "@cocalc/frontend/i18n";15import { ProjectMap, UserMap } from "@cocalc/frontend/todo-types";16import { search_match, search_split } from "@cocalc/util/misc";17import type { CourseActions } from "../actions";18import {19AssignmentsMap,20IsGradingMap,21NBgraderRunInfo,22SortDescription,23StudentRecord,24StudentsMap,25} from "../store";26import * as util from "../util";27import AddStudents from "./add-students";28import { Student, StudentNameDescription } from "./students-panel-student";2930interface StudentsPanelReactProps {31frame_id?: string; // used for state caching32actions: CourseActions;33name: string;34redux: AppRedux;35project_id: string;36students: StudentsMap;37user_map: UserMap;38project_map: ProjectMap;39assignments: AssignmentsMap;40frameActions;41}4243interface StudentList {44students: any[];45num_omitted: number;46num_deleted: number;47}4849export function StudentsPanel({50actions,51frame_id,52name,53redux,54project_id,55students,56user_map,57project_map,58assignments,59frameActions,60}: StudentsPanelReactProps) {61const intl = useIntl();6263const expanded_students: Set<string> | undefined = useRedux(64name,65"expanded_students",66);67const active_student_sort: SortDescription | undefined = useRedux(68name,69"active_student_sort",70);71const active_feedback_edits: IsGradingMap = useRedux(72name,73"active_feedback_edits",74);75const nbgrader_run_info: NBgraderRunInfo | undefined = useRedux(76name,77"nbgrader_run_info",78);79const assignmentFilter = useRedux(name, "assignmentFilter");80const pageFilter = useRedux(name, "pageFilter");81const filter = pageFilter?.get("students") ?? "";82const setFilter = (filter: string) => {83actions.setPageFilter("students", filter);84};8586// the type is copy/paste from what TS infers in the util.parse_students function87const [students_unordered, set_students_unordered] = useState<88{89create_project?: number;90account_id?: string;91student_id: string;92first_name?: string;93last_name?: string;94last_active?: number;95hosting?: string;96email_address?: string;97project_id?: string;98deleted?: boolean;99deleted_account?: boolean;100note?: string;101last_email_invite?: number;102}[]103>([]);104const [show_deleted, set_show_deleted] = useState<boolean>(false);105106// this updates a JS list from the ever changing user_map immutableMap107useEffect(() => {108const v = util.parse_students(students, user_map, redux, intl);109if (!isEqual(v, students_unordered)) {110set_students_unordered(v);111}112}, [students, user_map]);113114// student_list not a list, but has one, plus some extra info.115const student_list: StudentList = useMemo(() => {116// turn map of students into a list117// account_id : "bed84c9e-98e0-494f-99a1-ad9203f752cb" # Student's CoCalc account ID118// email_address : "[email protected]" # Email the instructor signed the student up with.119// first_name : "Rachel" # Student's first name they use for CoCalc120// last_name : "Florence" # Student's last name they use for CoCalc121// project_id : "6bea25c7-da96-4e92-aa50-46ebee1994ca" # Student's project ID for this course122// student_id : "920bdad2-9c3a-40ab-b5c0-eb0b3979e212" # Student's id for this course123// last_active : 2357025124// create_project : number -- server timestamp of when create started125// deleted : False126// note : "Is younger sister of Abby Florence (TA)"127128const students_ordered = [...students_unordered];129130if (active_student_sort != null) {131students_ordered.sort(132util.pick_student_sorter(active_student_sort.toJS()),133);134if (active_student_sort.get("is_descending")) {135students_ordered.reverse();136}137}138139// Deleted and non-deleted students140const deleted: any[] = [];141const non_deleted: any[] = [];142for (const x of students_ordered) {143if (x.deleted) {144deleted.push(x);145} else {146non_deleted.push(x);147}148}149const num_deleted = deleted.length;150151const students_shown = show_deleted152? non_deleted.concat(deleted) // show deleted ones at the end...153: non_deleted;154155let num_omitted = 0;156const students_next = (function () {157if (filter) {158const words = search_split(filter.toLowerCase());159const students_filtered: any[] = [];160for (const x of students_shown) {161const target = [162x.first_name ?? "",163x.last_name ?? "",164x.email_address ?? "",165]166.join(" ")167.toLowerCase();168if (search_match(target, words)) {169students_filtered.push(x);170} else {171num_omitted += 1;172}173}174return students_filtered;175} else {176return students_shown;177}178})();179180return { students: students_next, num_omitted, num_deleted };181}, [students, students_unordered, show_deleted, filter, active_student_sort]);182183function render_header(num_omitted) {184// TODO: get rid of all of the bootstrap form crap below. I'm basically185// using inline styles to undo the spacing screwups they cause, so it doesn't186// look like total crap.187188return (189<div>190<Row>191<Col md={6}>192<Input.Search193allowClear194placeholder={intl.formatMessage({195id: "course.students-panel.filter_students.placeholder",196defaultMessage: "Filter existing students...",197})}198value={filter}199onChange={(e) => setFilter(e.target.value)}200/>201</Col>202<Col md={6}>203{num_omitted ? (204<h5 style={{ marginLeft: "15px" }}>205{intl.formatMessage(206{207id: "course.students-panel.filter_students.info",208defaultMessage: "(Omitting {num_omitted} students)",209},210{ num_omitted },211)}212</h5>213) : undefined}214</Col>215<Col md={11}>216<AddStudents217name={name}218students={students}219user_map={user_map}220project_id={project_id}221/>222</Col>223</Row>224</div>225);226}227228function render_sort_icon(column_name: string) {229if (230active_student_sort == null ||231active_student_sort.get("column_name") != column_name232)233return;234return (235<Icon236style={{ marginRight: "10px" }}237name={238active_student_sort.get("is_descending") ? "caret-up" : "caret-down"239}240/>241);242}243244function render_sort_link(column_name: string, display_name: string) {245return (246<a247href=""248onClick={(e) => {249e.preventDefault();250actions.students.set_active_student_sort(column_name);251}}252>253{display_name}254<Gap />255{render_sort_icon(column_name)}256</a>257);258}259260function render_student_table_header(num_deleted: number) {261// HACK: that marginRight is to get things to line up with students.262const firstName = intl.formatMessage(labels.account_first_name);263const lastName = intl.formatMessage(labels.account_last_name);264const lastActive = intl.formatMessage(labels.last_active);265const projectStatus = intl.formatMessage(labels.project_status);266const emailAddress = intl.formatMessage(labels.email_address);267268return (269<div>270<Row style={{ marginRight: 0 }}>271<Col md={6}>272<div style={{ display: "inline-block", width: "50%" }}>273{render_sort_link("first_name", firstName)}274</div>275<div style={{ display: "inline-block" }}>276{render_sort_link("last_name", lastName)}277</div>278</Col>279<Col md={4}>{render_sort_link("email", emailAddress)}</Col>280<Col md={8}>{render_sort_link("last_active", lastActive)}</Col>281<Col md={3}>{render_sort_link("hosting", projectStatus)}</Col>282<Col md={3}>283{num_deleted ? render_show_deleted(num_deleted) : undefined}284</Col>285</Row>286</div>287);288}289290function get_student(id: string): StudentRecord {291const student = students.get(id);292if (student == null) {293console.warn(`Tried to access undefined student ${id}`);294}295return student as StudentRecord;296}297298function render_student(student_id: string, index: number) {299const x = student_list.students[index];300if (x == null) return null;301const store = actions.get_store();302if (store == null) return null;303const studentName: StudentNameDescription = {304full: store.get_student_name(x.student_id),305first: x.first_name,306last: x.last_name,307};308const student = get_student(student_id);309if (student == null) {310// temporary and better than crashing311return null;312}313return (314<Student315background={index % 2 === 0 ? "#eee" : undefined}316key={student_id}317student_id={student_id}318student={student}319user_map={user_map}320redux={redux}321name={name}322project_map={project_map}323assignments={assignments}324is_expanded={expanded_students?.has(student_id) ?? false}325student_name={studentName}326display_account_name={true}327active_feedback_edits={active_feedback_edits}328nbgrader_run_info={nbgrader_run_info}329assignmentFilter={assignmentFilter?.get(student_id)}330/>331);332}333334function render_students(students) {335if (students.length == 0) {336return render_no_students();337}338return (339<ScrollableList340virtualize341rowCount={students.length}342rowRenderer={({ key, index }) => render_student(key, index)}343rowKey={(index) =>344students[index] != null ? students[index].student_id : undefined345}346cacheId={`course-student-${name}-${frame_id}`}347/>348);349}350351function render_no_students() {352return (353<div>354<Alert355type="info"356style={{357margin: "15px auto",358fontSize: "12pt",359maxWidth: "800px",360}}361message={362<b>363<a onClick={() => frameActions.setModal("add-students")}>364<FormattedMessage365id="course.students-panel.no_students.title"366defaultMessage="Add Students to your Course"367/>368</a>369</b>370}371description={372<div>373<FormattedMessage374id="course.students-panel.no_students.descr"375defaultMessage={`<A>Add some students</A> to your course376by entering their email addresses in the box in the upper right,377then click on Search.`}378values={{379A: (c) => (380<a onClick={() => frameActions.setModal("add-students")}>381{c}382</a>383),384}}385/>386</div>387}388/>389</div>390);391}392393function render_show_deleted(num_deleted: number) {394if (show_deleted) {395return (396<a onClick={() => set_show_deleted(false)}>397<Tip398placement="left"399title="Hide deleted"400tip={intl.formatMessage(course.show_deleted_students_tooltip, {401show: false,402})}403>404{intl.formatMessage(course.show_deleted_students_msg, {405num_deleted,406show: false,407})}408</Tip>409</a>410);411} else {412return (413<a414onClick={() => {415set_show_deleted(true);416setFilter("");417}}418>419<Tip420placement="left"421title="Show deleted"422tip={intl.formatMessage(course.show_deleted_students_tooltip, {423show: true,424})}425>426{intl.formatMessage(course.show_deleted_students_msg, {427num_deleted,428show: true,429})}430</Tip>431</a>432);433}434}435436function render_student_info(students, num_deleted) {437/* The "|| num_deleted > 0" below is because we show438header even if no non-deleted students if there are deleted439students, since it's important to show the link to show440deleted students if there are any. */441return (442<div className="smc-vfill">443{students.length > 0 || num_deleted > 0444? render_student_table_header(num_deleted)445: undefined}446{render_students(students)}447</div>448);449}450451{452const { students, num_omitted, num_deleted } = student_list;453return (454<div className="smc-vfill" style={{ margin: "0" }}>455{render_header(num_omitted)}456{render_student_info(students, num_deleted)}457</div>458);459}460}461462463