Skip to content

Commit

Permalink
Restyle export page, fix various bugs
Browse files Browse the repository at this point in the history
Add automatic preview refresh, restyle and add tooltip
Rearrange export page sections into one page
Fix section id query bugs, rework cases where no courses are selected
Add additional fields to export types, fix download data with no courses
Add statuses for export page
  • Loading branch information
smartspot2 committed Feb 15, 2024
1 parent 3dd7785 commit 4d1732f
Show file tree
Hide file tree
Showing 7 changed files with 275 additions and 137 deletions.
40 changes: 31 additions & 9 deletions csm_web/frontend/src/components/data_export/DataExport.tsx
Original file line number Diff line number Diff line change
@@ -1,22 +1,44 @@
import React, { useState } from "react";
import { useProfiles } from "../../utils/queries/base";
import { Role } from "../../utils/types";
import LoadingSpinner from "../LoadingSpinner";
import { ExportType } from "./DataExportTypes";
import { ExportPage } from "./ExportPage";
import { ExportSelector } from "./ExportSelector";

export const DataExport = () => {
const [dataExportType, setDataExportType] = useState<ExportType | null>(null);
const { data: profiles, isSuccess: profilesLoaded, isError: profilesError } = useProfiles();

if (profilesError) {
return <b>Error loading user profiles.</b>;
} else if (!profilesLoaded) {
return <LoadingSpinner className="spinner-centered" />;
} else if (profilesLoaded && !profiles.some(profile => profile.role === Role.COORDINATOR)) {
return <b>Permission denied; you are not a coordinator for any course.</b>;
}

return (
<div className="data-export-container">
{dataExportType === null ? (
<ExportSelector
onContinue={(exportType: ExportType) => {
setDataExportType(exportType);
}}
/>
) : (
<ExportPage dataExportType={dataExportType} onBack={() => setDataExportType(null)} />
)}
<div className="data-export-header">
<h2 className="data-export-page-title">Export Data</h2>
</div>
<div className="data-export-body">
<div className="data-export-sidebar">
<ExportSelector
onSelect={(exportType: ExportType) => {
setDataExportType(exportType);
}}
/>
</div>
<div className="data-export-content">
{dataExportType === null ? (
<div>Select export type to start.</div>
) : (
<ExportPage dataExportType={dataExportType!} />
)}
</div>
</div>
</div>
);
};
16 changes: 10 additions & 6 deletions csm_web/frontend/src/components/data_export/DataExportTypes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,10 @@ export enum ExportType {
* Object for displaying export types in the UI
*/
export const EXPORT_TYPE_DATA = new Map<ExportType, string>([
[ExportType.STUDENT_DATA, "Student data"],
[ExportType.ATTENDANCE_DATA, "Attendance data"],
[ExportType.SECTION_DATA, "Section data"],
[ExportType.COURSE_DATA, "Course data"]
[ExportType.STUDENT_DATA, "Student"],
[ExportType.ATTENDANCE_DATA, "Attendance"],
[ExportType.SECTION_DATA, "Section"],
[ExportType.COURSE_DATA, "Course"]
]);

export const EXPORT_COLUMNS: {
Expand All @@ -27,14 +27,18 @@ export const EXPORT_COLUMNS: {
[ExportType.ATTENDANCE_DATA]: {
required: {
student_email: "Student email",
student_name: "Student name"
student_name: "Student name",
attendance_data: "Attendance data"
},
optional: {
course_name: "Course name",
active: "Active",
section_id: "Section ID",
mentor_name: "Mentor name",
mentor_email: "Mentor email"
mentor_email: "Mentor email",
num_present: "Present attendance count",
num_excused: "Excused absence count",
num_unexcused: "Unexcused absence count"
}
},
[ExportType.COURSE_DATA]: {
Expand Down
144 changes: 83 additions & 61 deletions csm_web/frontend/src/components/data_export/ExportPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,16 @@ import { useDataExportMutation, useDataExportPreviewMutation } from "../../utils
import { Role } from "../../utils/types";
import { AutoGrid } from "../AutoGrid";
import LoadingSpinner from "../LoadingSpinner";
import { Tooltip } from "../Tooltip";
import { ExportType, EXPORT_COLUMNS } from "./DataExportTypes";

import RefreshIcon from "../../../static/frontend/img/refresh.svg";

interface ExportPageProps {
dataExportType: ExportType;
onBack: () => void;
}

export const ExportPage = ({ dataExportType, onBack }: ExportPageProps) => {
export const ExportPage = ({ dataExportType }: ExportPageProps) => {
const { data: profiles, isSuccess: profilesLoaded, isError: profilesError } = useProfiles();
const [includedCourses, setIncludedCourses] = useState<number[]>([]);
const [includedFields, setIncludedFields] = useState<string[]>(
Expand All @@ -29,6 +29,10 @@ export const ExportPage = ({ dataExportType, onBack }: ExportPageProps) => {
}
}, [profilesLoaded, profiles]);

useEffect(() => {
setIncludedFields(Array.from(Object.keys(EXPORT_COLUMNS[dataExportType].optional)));
}, [dataExportType]);

if (profilesError) {
return <h3>Profiles not found</h3>;
} else if (!profilesLoaded) {
Expand Down Expand Up @@ -111,6 +115,11 @@ export const ExportPage = ({ dataExportType, onBack }: ExportPageProps) => {
* Download the data; open a new page with the data
*/
const downloadData = () => {
if (includedCourses.length === 0) {
// no data to download
return;
}

dataExportMutation.mutate({
courses: includedCourses,
fields: includedFields,
Expand All @@ -120,19 +129,13 @@ export const ExportPage = ({ dataExportType, onBack }: ExportPageProps) => {

return (
<div className="export-page-container">
<div className="export-page-header">
<h3 className="page-title">Export Data</h3>
<button className="secondary-btn" onClick={onBack}>
Back
</button>
</div>
<div className="export-page-config">
<div className="export-page-sidebar sidebar-left">{courseSelection}</div>
<div className="export-page-sidebar sidebar-right">{columnSelection}</div>
</div>
<ExportPagePreview courses={includedCourses} fields={includedFields} exportType={dataExportType} preview={3} />
<ExportPagePreview courses={includedCourses} fields={includedFields} exportType={dataExportType} />
<div className="export-page-footer">
<button className="primary-btn" onClick={downloadData}>
<button className="primary-btn" onClick={downloadData} disabled={includedCourses.length === 0}>
Download Data
</button>
</div>
Expand All @@ -146,7 +149,6 @@ interface ExportPagePreviewProps {
courses: number[];
fields: string[];
exportType: ExportType;
preview: number;
}

/**
Expand All @@ -157,11 +159,12 @@ const ExportPagePreview = ({ exportType, courses, fields }: ExportPagePreviewPro
const [preview, setPreview] = useState<number>(10);
const [data, setData] = useState<string[][]>([]);

const refreshPreview = () => {
if (courses.length == 0) {
return;
}
// automatically refresh on change
useEffect(() => {
refreshPreview();
}, [courses, fields, preview]);

const refreshPreview = () => {
dataExportPreviewMutation.mutate(
{
courses: courses,
Expand All @@ -182,54 +185,73 @@ const ExportPagePreview = ({ exportType, courses, fields }: ExportPagePreviewPro
setPreview(selectedValue);
};

let dataTable = null;

if (data?.length > 1) {
// if has header and at least one row of content, display it
dataTable = (
<table className="export-preview-table">
<thead>
<tr className="export-preview-table-header">
{data?.length > 0 &&
data[0].map((cell, cellIdx) => (
<th key={cellIdx} className="export-preview-table-header-item">
{cell}
</th>
))}
</tr>
</thead>
<tbody>
{data.map(
(row, rowIdx) =>
rowIdx > 0 && (
<tr key={rowIdx}>
{row.map((cell, cellIdx) => (
<td key={cellIdx} className="export-preview-table-item">
{cell}
</td>
))}
</tr>
)
)}
{data?.length >= preview && (
<tr className="export-preview-table-more-row">
<td className="export-preview-table-more-row-item" colSpan={data[0].length}>
<i>(More rows clipped)</i>
</td>
</tr>
)}
</tbody>
</table>
);
} else {
// not enough data
dataTable = <i>Preview query returned no data.</i>;
}

return (
<div className="export-page-preview-container">
<h3>Preview</h3>
<div className="export-preview-header">
Rows:
<select className="export-preview-select form-select" onChange={handlePreviewSelect} value={preview}>
{PREVIEW_OPTIONS.map(count => (
<option key={count} value={count}>
{count}
</option>
))}
</select>
<RefreshIcon className="export-preview-icon icon" onClick={refreshPreview} />
</div>
<div className="export-preview-table-container">
<table className="export-preview-table">
<thead>
<tr className="export-preview-table-header">
{data?.length > 0 &&
data[0].map((cell, cellIdx) => (
<th key={cellIdx} className="export-preview-table-header-item">
{cell}
</th>
))}
</tr>
</thead>
<tbody>
{data.map(
(row, rowIdx) =>
rowIdx > 0 && (
<tr key={rowIdx}>
{row.map((cell, cellIdx) => (
<td key={cellIdx} className="export-preview-table-item">
{cell}
</td>
))}
</tr>
)
)}
{data?.length >= preview && (
<tr className="export-preview-table-more-row">
<td className="export-preview-table-more-row-item" colSpan={data[0].length}>
<i>(More rows clipped)</i>
</td>
</tr>
)}
</tbody>
</table>
<h3 className="export-page-preview-title">Preview</h3>
<div className="export-page-preview-wrapper">
<div className="export-preview-header">
Rows:
<select className="export-preview-select form-select" onChange={handlePreviewSelect} value={preview}>
{PREVIEW_OPTIONS.map(count => (
<option key={count} value={count}>
{count}
</option>
))}
</select>
<div className="export-preview-refresh-tooltip-container">
<Tooltip
source={<RefreshIcon className="export-preview-icon icon" onClick={refreshPreview} />}
placement="right"
>
Refresh
</Tooltip>
</div>
</div>
<div className="export-preview-table-container">{dataTable}</div>
</div>
</div>
);
Expand Down
48 changes: 18 additions & 30 deletions csm_web/frontend/src/components/data_export/ExportSelector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,47 +5,35 @@ import { ExportType, EXPORT_TYPE_DATA } from "./DataExportTypes";
import "../../css/data-export.scss";

interface ExportSelectorProps {
onContinue: (exportType: ExportType) => void;
onSelect: (exportType: ExportType) => void;
}

/**
* Component for selecting the courses to include in the export,
* along with the export data config selection.
*/
export const ExportSelector = ({ onContinue }: ExportSelectorProps) => {
const [dataExportType, setDataExportType] = useState<ExportType>(ExportType.ATTENDANCE_DATA);
export const ExportSelector = ({ onSelect }: ExportSelectorProps) => {
const [dataExportType, setDataExportType] = useState<ExportType | null>(null);

const handleContinue = () => {
onContinue(dataExportType);
const handleSelect = (exportType: ExportType) => {
onSelect(exportType);
setDataExportType(exportType);
};

return (
<div className="export-selector-container">
<div className="export-selector-section">
<h3 className="page-title center-title">Select Export Data</h3>
<div className="export-selector-data-type-container">
<div className="export-selector-data-type-options">
{Array.from(EXPORT_TYPE_DATA.entries())
.sort()
.map(([exportType, description]) => (
<label key={exportType} className="export-selector-data-type-label">
<input
className="export-selector-data-type-input"
name="export-data-type"
type="radio"
checked={dataExportType === exportType}
onChange={() => setDataExportType(exportType)}
/>
{description}
</label>
))}
</div>
</div>
</div>
<div className="export-selector-footer">
<div className="primary-btn" onClick={handleContinue}>
Continue
</div>
<div className="export-selector-data-type-options">
{Array.from(EXPORT_TYPE_DATA.entries())
.sort()
.map(([exportType, description]) => (
<div
key={exportType}
className={`export-selector-data-type-label ${dataExportType === exportType ? "active" : ""}`}
onClick={() => handleSelect(exportType)}
>
{description}
</div>
))}
</div>
</div>
);
Expand Down
Loading

0 comments on commit 4d1732f

Please sign in to comment.