Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Data Export #462

Merged
merged 2 commits into from
Feb 15, 2024
Merged
Show file tree
Hide file tree
Changes from all 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
2 changes: 2 additions & 0 deletions csm_web/frontend/src/components/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { emptyRoles, Roles } from "../utils/user";
import CourseMenu from "./CourseMenu";
import Home from "./Home";
import Policies from "./Policies";
import { DataExport } from "./data_export/DataExport";
import { EnrollmentMatcher } from "./enrollment_automation/EnrollmentMatcher";
import { Resources } from "./resource_aggregation/Resources";
import Section from "./section/Section";
Expand Down Expand Up @@ -39,6 +40,7 @@ const App = () => {
<Route path="resources/*" element={<Resources />} />
<Route path="matcher/*" element={<EnrollmentMatcher />} />
<Route path="policies/*" element={<Policies />} />
<Route path="export/*" element={<DataExport />} />
<Route path="*" element={<NotFound />} />
</Route>
</Routes>
Expand Down
48 changes: 48 additions & 0 deletions csm_web/frontend/src/components/AutoGrid.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import React from "react";

import "../css/base/autogrid.scss";

interface AutoColumnsProps {
children?: React.ReactNode[];
}

/**
* Automatically format children in balanced columns.
*/
export const AutoGrid = ({ children }: AutoColumnsProps) => {
const gridSize = Math.ceil(Math.sqrt(children?.length ?? 0));

if (children == null) {
return null;
}

const raw_table: React.ReactNode[][] = [];
children.forEach((item, idx) => {
if (idx % gridSize == 0) {
raw_table.push([item]);
} else {
raw_table[raw_table.length - 1].push(item);
}
});

// transpose table
const table = raw_table[0].map((_, colIndex) => raw_table.map(row => row[colIndex]));

return (
<div className="auto-grid-container">
<table className="auto-grid-table">
<tbody>
{table.map((row, rowIdx) => (
<tr key={rowIdx} className="auto-grid-row">
{row.map((item, itemIdx) => (
<td key={itemIdx} className="auto-grid-item">
{item}
</td>
))}
</tr>
))}
</tbody>
</table>
</div>
);
};
26 changes: 21 additions & 5 deletions csm_web/frontend/src/components/Home.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { useCourses } from "../utils/queries/courses";
import { Profile, Course, Role } from "../utils/types";
import LoadingSpinner from "./LoadingSpinner";

import FileExport from "../../static/frontend/img/file-export.svg";
import PlusIcon from "../../static/frontend/img/plus.svg";

import scssColors from "../css/base/colors-export.module.scss";
Expand All @@ -17,6 +18,7 @@ const Home = () => {
const { data: courses, isSuccess: coursesLoaded, isError: coursesLoadError } = useCourses();

let content = null;
let headingRight = null;
if (profilesLoaded && coursesLoaded) {
// loaded, no error
const coursesById: Map<number, Course> = new Map();
Expand Down Expand Up @@ -52,6 +54,17 @@ const Home = () => {
})}
</div>
);

const isCoordinator = profiles!.some(profile => profile.role === Role.COORDINATOR);

if (isCoordinator) {
headingRight = (
<Link className="primary-btn" to="/export">
<FileExport className="icon" />
Export
</Link>
);
}
} else if (profilesLoadError) {
// error during load
content = <h3>Profiles not found</h3>;
Expand All @@ -66,11 +79,14 @@ const Home = () => {
return (
<div id="home-courses">
<div id="home-courses-heading">
<h3 className="page-title">My courses</h3>
<Link className="primary-btn" to="/courses">
<PlusIcon className="icon inline-plus-icon" />
Add Course
</Link>
<div id="home-courses-heading-left">
<h3 className="page-title">My courses</h3>
<Link className="primary-btn" to="/courses">
<PlusIcon className="icon inline-plus-icon" />
Add Course
</Link>
</div>
<div id="home-courses-heading-right">{headingRight}</div>
</div>
{content}
</div>
Expand Down
15 changes: 1 addition & 14 deletions csm_web/frontend/src/components/course/Course.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import { useCourseSections } from "../../utils/queries/courses";
import { Course as CourseType } from "../../utils/types";
import LoadingSpinner from "../LoadingSpinner";
import { CreateSectionModal } from "./CreateSectionModal";
import { DataExportModal } from "./DataExportModal";
import { SectionCard } from "./SectionCard";
import { SettingsModal } from "./SettingsModal";
import { WhitelistModal } from "./WhitelistModal";
Expand All @@ -27,7 +26,6 @@ const DAY_OF_WEEK_ABREVIATIONS: { [day: string]: string } = Object.freeze({
});

const COURSE_MODAL_TYPE = Object.freeze({
exportData: "csv",
createSection: "mksec",
whitelist: "whitelist",
settings: "settings"
Expand Down Expand Up @@ -83,9 +81,7 @@ const Course = ({ courses, priorityEnrollment, enrollmentTimes }: CourseProps):
* Render the currently chosen modal.
*/
const renderModal = (): React.ReactElement | null => {
if (whichModal == COURSE_MODAL_TYPE.exportData) {
return <DataExportModal closeModal={() => setShowModal(false)} />;
} else if (whichModal == COURSE_MODAL_TYPE.createSection) {
if (whichModal == COURSE_MODAL_TYPE.createSection) {
return (
<CreateSectionModal
reloadSections={reloadSections}
Expand Down Expand Up @@ -168,15 +164,6 @@ const Course = ({ courses, priorityEnrollment, enrollmentTimes }: CourseProps):
<PlusIcon className="icon" />
Create Section
</button>
<button
className="primary-btn"
onClick={() => {
setShowModal(true);
setWhichModal(COURSE_MODAL_TYPE.exportData);
}}
>
Export Data
</button>
<button
className="primary-btn"
onClick={() => {
Expand Down
112 changes: 0 additions & 112 deletions csm_web/frontend/src/components/course/DataExportModal.tsx

This file was deleted.

44 changes: 44 additions & 0 deletions csm_web/frontend/src/components/data_export/DataExport.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +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">
<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>
);
};
Loading
Loading