Skip to content

Commit

Permalink
chore(fe): cleanup leftover api requests (#1166)
Browse files Browse the repository at this point in the history
* feat: re-enable backend tests in ci

* hotfix: moved the env setup in test utilities above the connections

* fix: moved out /courses/getCourse, /courses/getPathFrom and /courses/courseChildren

Also deleted a unused helper.

* fix: moved out timetable request

* fix: moved out coursesUnlockedWhenTaken, and consolidated courses.ts into course.ts

* fix: move out /programs/getPrograms and fold programApi.ts into programsApi.ts

* fix: move specialisation api calls into specsApi.ts, upgrading useEffects to useQuerys in degree wizard.

* fix: move /user/updateDegreeLength, /user/toggleSummerTerm, /user/updateStartYear, /planner/toggleTermLocked into api folder

* fix: moved out ctf axios (lol) and finished last few others

* chore: rename some fe api request files and shuffled some functions

* fix: final useEffect in ProgressionChecker

* fix: move out unwrapSettled api helper

* fix: moved Prereq tree to use queries

Also explored size fixing, left a TODO comment with my findings, so we can revisit later.
  • Loading branch information
ollibowers authored Jul 30, 2024
1 parent fe22a61 commit cd736b4
Show file tree
Hide file tree
Showing 34 changed files with 330 additions and 394 deletions.
3 changes: 1 addition & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,6 @@ jobs:
backend_tests:
name: Backend Unit Tests
runs-on: ubuntu-latest
if: false
permissions: # required for artifacts
checks: write
pull-requests: write
Expand Down Expand Up @@ -111,7 +110,7 @@ jobs:
docker_build:
name: Build docker containers
runs-on: ubuntu-latest
needs: [backend_lint, backend_typecheck, frontend_lint, frontend_typecheck] # TODO: add test jobs when they get unskipped
needs: [backend_lint, backend_typecheck, backend_tests, frontend_lint, frontend_typecheck] # TODO: add test jobs when they get unskipped
steps:
- name: Checking out repository
uses: actions/checkout@v4
Expand Down
8 changes: 5 additions & 3 deletions backend/server/tests/user/utility.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
# pylint: disable=wrong-import-position
import os

import requests
from dotenv import load_dotenv

load_dotenv("../env/backend.env") # must be before the connection imports as they require the env
os.environ["SESSIONSDB_SERVICE_HOSTNAME"] = "localhost"
os.environ["MONGODB_SERVICE_HOSTNAME"] = "localhost"

from server.db.mongo.setup import setup_user_related_collections
from server.db.redis.setup import setup_redis_sessionsdb

load_dotenv("../env/backend.env")
os.environ["MONGODB_SERVICE_HOSTNAME"] = "localhost"
os.environ["SESSIONSDB_SERVICE_HOSTNAME"] = "localhost"



Expand Down
2 changes: 1 addition & 1 deletion frontend/src/components/Auth/IdentityProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import React, { useCallback, useEffect, useState } from 'react';
import { Outlet } from 'react-router-dom';
import { useQueryClient } from '@tanstack/react-query';
import { isAxiosError } from 'axios';
import { refreshTokens } from 'utils/api/auth';
import { refreshTokens } from 'utils/api/authApi';
import PageLoading from 'components/PageLoading';
import { useAppDispatch, useAppSelector } from 'hooks';
import { selectIdentity, unsetIdentity, updateIdentityWithAPIRes } from 'reducers/identitySlice';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,24 +2,30 @@ import React from 'react';
import { useLocation } from 'react-router-dom';
import { useQuery } from '@tanstack/react-query';
import { Typography } from 'antd';
import axios from 'axios';
import { Course, CoursePathFrom, CoursesUnlockedWhenTaken } from 'types/api';
import { CourseTimetable } from 'types/courseCapacity';
import { CoursesResponse, DegreeResponse, PlannerResponse } from 'types/userResponse';
import { getCourseInfo, getCoursePrereqs, getCoursesUnlockedWhenTaken } from 'utils/api/coursesApi';
import { getCourseTimetable } from 'utils/api/timetableApi';
import getEnrolmentCapacity from 'utils/getEnrolmentCapacity';
import prepareUserPayload from 'utils/prepareUserPayload';
import { unwrapSettledPromise } from 'utils/queryUtils';
import {
LoadingCourseDescriptionPanel,
LoadingCourseDescriptionPanelSidebar
} from 'components/LoadingSkeleton';
import PlannerButton from 'components/PlannerButton';
import { TIMETABLE_API_URL } from 'config/constants';
import CourseAttributes from './CourseAttributes';
import CourseInfoDrawers from './CourseInfoDrawers';
import S from './styles';

const { Title, Text } = Typography;

const getCourseExtendedInfo = async (courseCode: string) => {
return Promise.allSettled([
getCourseInfo(courseCode),
getCoursePrereqs(courseCode),
getCourseTimetable(courseCode)
]);
};

type CourseDescriptionPanelProps = {
className?: string;
courseCode: string;
Expand All @@ -37,45 +43,18 @@ const CourseDescriptionPanel = ({
courses,
degree
}: CourseDescriptionPanelProps) => {
const getCoursesUnlocked = React.useCallback(async () => {
if (!degree || !planner || !courses)
return Promise.reject(new Error('degree, planner or courses undefined'));
const res = await axios.post<CoursesUnlockedWhenTaken>(
`/courses/coursesUnlockedWhenTaken/${courseCode}`,
JSON.stringify(prepareUserPayload(degree, planner, courses))
);
return res.data;
}, [courseCode, degree, planner, courses]);

const getCourseInfo = React.useCallback(async () => {
return Promise.allSettled([
axios.get<Course>(`/courses/getCourse/${courseCode}`),
axios.get<CoursePathFrom>(`/courses/getPathFrom/${courseCode}`),
axios.get<CourseTimetable>(`${TIMETABLE_API_URL}/${courseCode}`)
]);
}, [courseCode]);

const coursesUnlockedQuery = useQuery({
queryKey: ['coursesUnlocked', courseCode, degree, planner, courses],
queryFn: getCoursesUnlocked,
queryFn: () => getCoursesUnlockedWhenTaken(degree!, planner!, courses!, courseCode),
enabled: !!degree && !!planner && !!courses
});

const { pathname } = useLocation();
const sidebar = pathname === '/course-selector';

function unwrap<T>(res: PromiseSettledResult<T>): T | undefined {
if (res.status === 'rejected') {
// eslint-disable-next-line no-console
console.error('Rejected request at unwrap', res.reason);
return undefined;
}
return res.value;
}

const courseInfoQuery = useQuery({
queryKey: ['courseInfo', courseCode],
queryFn: getCourseInfo
queryFn: () => getCourseExtendedInfo(courseCode)
});

const loadingWrapper = (
Expand All @@ -87,9 +66,9 @@ const CourseDescriptionPanel = ({
if (courseInfoQuery.isPending || !courseInfoQuery.isSuccess) return loadingWrapper;

const [courseRes, pathFromRes, courseCapRes] = courseInfoQuery.data;
const course = unwrap(courseRes)?.data;
const coursesPathFrom = unwrap(pathFromRes)?.data.courses;
const courseCapacity = getEnrolmentCapacity(unwrap(courseCapRes)?.data);
const course = unwrapSettledPromise(courseRes);
const coursesPathFrom = unwrapSettledPromise(pathFromRes)?.courses;
const courseCapacity = getEnrolmentCapacity(unwrapSettledPromise(courseCapRes));

// course wasn't fetchable (fatal; should do proper error handling instead of indefinitely loading)
if (!course) return loadingWrapper;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { Flex, Select, Spin, Typography } from 'antd';
import { SearchCourse } from 'types/api';
import { CoursesResponse } from 'types/userResponse';
import { useDebounce } from 'use-debounce';
import { searchCourse } from 'utils/api/courseApi';
import { searchCourse } from 'utils/api/coursesApi';
import { addToUnplanned, removeCourse } from 'utils/api/plannerApi';
import QuickAddCartButton from 'components/QuickAddCartButton';
import useToken from 'hooks/useToken';
Expand Down
95 changes: 42 additions & 53 deletions frontend/src/components/PrerequisiteTree/PrerequisiteTree.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import React, { useEffect, useRef, useState } from 'react';
import type { Item, TreeGraph, TreeGraphData } from '@antv/g6';
import axios from 'axios';
import { CourseChildren, CoursePathFrom } from 'types/api';
import { CourseList } from 'types/courses';
import { useQuery } from '@tanstack/react-query';
import { getCourseChildren, getCoursePrereqs } from 'utils/api/coursesApi';
import Spinner from 'components/Spinner';
import GRAPH_STYLE from './config';
import TREE_CONSTANTS from './constants';
Expand All @@ -15,15 +14,25 @@ type Props = {
};

const PrerequisiteTree = ({ courseCode, onCourseClick }: Props) => {
const [loading, setLoading] = useState(true);
const [initGraph, setInitGraph] = useState(true);
const graphRef = useRef<TreeGraph | null>(null);
const [courseUnlocks, setCourseUnlocks] = useState<CourseList>([]);
const [coursesRequires, setCoursesRequires] = useState<CourseList>([]);
const ref = useRef<HTMLDivElement | null>(null);

const childrenQuery = useQuery({
queryKey: ['course', 'children', courseCode], // TODO: make this key reasonable when we rework all keys
queryFn: () => getCourseChildren(courseCode),
select: (data) => data.courses
});

const prereqsQuery = useQuery({
queryKey: ['course', 'prereqs', courseCode],
queryFn: () => getCoursePrereqs(courseCode),
select: (data) => data.courses
});

useEffect(() => {
// if the course code changes, force a reload
setLoading(true);
// if the course code changes, force a reload (REQUIRED)
setInitGraph(true);
}, [courseCode]);

useEffect(() => {
Expand Down Expand Up @@ -56,76 +65,56 @@ const PrerequisiteTree = ({ courseCode, onCourseClick }: Props) => {
const node = event.item as Item;
if (onCourseClick) onCourseClick(node.getModel().label as string);
});

setInitGraph(false);
};

// NOTE: This is for hot reloading in development as new graph will instantiate every time
const updateTreeGraph = (graphData: TreeGraphData) => {
// TODO: fix sizing change here (will need to use graph.changeSize with the scroll height/width)
// Doing so has weird interactions where it grows in width each update,
// which gets influenced by the margins from here and Collapsible, and by the loading spinner size (= 142px)
if (!graphRef.current) return;
graphRef.current.changeData(graphData);
bringEdgeLabelsToFront(graphRef.current);
};

/* REQUESTS */
const getCourseUnlocks = async (code: string) => {
try {
const res = await axios.get<CourseChildren>(`/courses/courseChildren/${code}`);
return res.data.courses;
} catch (e) {
// eslint-disable-next-line no-console
console.error('Error at getCourseUnlocks', e);
return [];
}
};

const getCoursePrereqs = async (code: string) => {
try {
const res = await axios.get<CoursePathFrom>(`/courses/getPathFrom/${code}`);
return res.data.courses;
} catch (e) {
// eslint-disable-next-line no-console
console.error('Error at getCoursePrereqs', e);
return [];
}
setInitGraph(false);
};

/* MAIN */
const setupGraph = async (c: string) => {
setLoading(true);

const unlocks = await getCourseUnlocks(c);
if (unlocks) setCourseUnlocks(unlocks);
const prereqs = await getCoursePrereqs(c);
if (prereqs) setCoursesRequires(prereqs);

// create graph data
if (initGraph && !childrenQuery.isPending && !prereqsQuery.isPending) {
const unlocks = childrenQuery.data ?? [];
const prereqs = prereqsQuery.data ?? [];
const graphData = {
id: 'root',
label: courseCode,
children: prereqs
?.map((child) => handleNodeData(child, TREE_CONSTANTS.PREREQ))
.concat(unlocks?.map((child) => handleNodeData(child, TREE_CONSTANTS.UNLOCKS)))
.map((child) => handleNodeData(child, TREE_CONSTANTS.PREREQ))
.concat(unlocks.map((child) => handleNodeData(child, TREE_CONSTANTS.UNLOCKS)))
};

// render graph
if (!graphRef.current && graphData.children.length !== 0) {
generateTreeGraph(graphData);
} else {
// NOTE: This is for hot reloading in development as new graph will instantiate every time
updateTreeGraph(graphData);
}
setLoading(false);
};

if (loading) {
setupGraph(courseCode);
setLoading(false);
}
}, [courseCode, loading, onCourseClick]);
}, [
courseCode,
initGraph,
onCourseClick,
childrenQuery.data,
prereqsQuery.data,
childrenQuery.isPending,
prereqsQuery.isPending
]);

const loading = initGraph || childrenQuery.isPending || prereqsQuery.isPending;
const height = calcHeight(prereqsQuery.data ?? [], childrenQuery.data ?? []);

return (
<S.PrereqTreeContainer ref={ref} $height={calcHeight(coursesRequires, courseUnlocks)}>
<S.PrereqTreeContainer ref={ref} $height={height}>
{loading && <Spinner text="Loading tree..." />}
{!loading && graphRef.current && !graphRef.current.getEdges().length && (
{!loading && graphRef.current && graphRef.current.getEdges().length === 0 && (
<p> No prerequisite visualisation is needed for this course </p>
)}
</S.PrereqTreeContainer>
Expand Down
5 changes: 2 additions & 3 deletions frontend/src/config/migrations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,9 @@
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-nocheck
import axios from 'axios';
import type { MigrationManifest } from 'redux-persist';
import { createMigrate } from 'redux-persist';
import { Course } from 'types/api';
import { getCourseInfo } from 'utils/api/coursesApi';

/**
* IMPORTANT NOTE:
Expand Down Expand Up @@ -43,7 +42,7 @@ const persistMigrations: MigrationManifest = {
await Promise.all(
courses.map(async (course) => {
try {
const res = await axios.get<Course>(`/courses/getCourse/${course}`);
const res = await getCourseInfo(course);
if (res.status === 200) {
const courseData = res.data;
newState.planner.courses[courseData.code].isMultiterm = courseData.is_multiterm;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import React from 'react';
import { useQuery } from '@tanstack/react-query';
import { Typography } from 'antd';
import { CoursesResponse } from 'types/userResponse';
import { fetchAllDegrees } from 'utils/api/programApi';
import { fetchAllDegrees } from 'utils/api/programsApi';
import { getUserDegree } from 'utils/api/userApi';
import CourseSearchBar from 'components/CourseSearchBar';
import { useAppDispatch } from 'hooks';
Expand Down
26 changes: 9 additions & 17 deletions frontend/src/pages/DegreeWizard/DegreeStep/DegreeStep.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import React, { useEffect, useState } from 'react';
import React, { useState } from 'react';
import { animated, useSpring } from '@react-spring/web';
import { useQuery } from '@tanstack/react-query';
import { Input, Menu, Typography } from 'antd';
import axios from 'axios';
import { fuzzy } from 'fast-fuzzy';
import { Programs } from 'types/api';
import { DegreeWizardPayload } from 'types/degreeWizard';
import { fetchAllDegrees } from 'utils/api/programsApi';
import springProps from '../common/spring';
import Steps from '../common/steps';
import CS from '../common/styles';
Expand All @@ -22,21 +22,13 @@ type Props = {
const DegreeStep = ({ incrementStep, degreeInfo, setDegreeInfo }: Props) => {
const [input, setInput] = useState('');
const [options, setOptions] = useState<string[]>([]);
const [allDegrees, setAllDegrees] = useState<Record<string, string>>({});

const fetchAllDegrees = async () => {
try {
const res = await axios.get<Programs>('/programs/getPrograms');
setAllDegrees(res.data.programs);
} catch (e) {
// eslint-disable-next-line no-console
console.error('Error at fetchAllDegrees', e);
}
};

useEffect(() => {
fetchAllDegrees();
}, []);
const allDegreesQuery = useQuery({
queryKey: ['programs'],
queryFn: fetchAllDegrees,
select: (data) => data.programs
});
const allDegrees = allDegreesQuery.data ?? {};

const onDegreeChange = async ({ key }: { key: string }) => {
setInput(key);
Expand Down
Loading

0 comments on commit cd736b4

Please sign in to comment.