diff --git a/app/controllers/gradebooks_controller.rb b/app/controllers/gradebooks_controller.rb index 9b72dc75ce9..15f09b2aad0 100644 --- a/app/controllers/gradebooks_controller.rb +++ b/app/controllers/gradebooks_controller.rb @@ -233,6 +233,11 @@ class GradebooksController < ApplicationController @course_is_concluded = @context.completed? @post_grades_tools = post_grades_tools + # Optimize initial data loading + if Account.site_admin.feature_enabled?(:prefetch_gradebook_user_ids) + prefetch_xhr(user_ids_course_gradebook_url(@context), id: 'user_ids') + end + render_gradebook end end @@ -339,6 +344,8 @@ class GradebooksController < ApplicationController last_exported_attachment = @last_exported_gradebook_csv.try(:attachment) grading_standard = @context.grading_standard_or_default { + prefetch_gradebook_user_ids: Account.site_admin.feature_enabled?(:prefetch_gradebook_user_ids), + GRADEBOOK_OPTIONS: { api_max_per_page: per_page, chunk_size: Setting.get('gradebook2.submissions_chunk_size', '10').to_i, diff --git a/app/jsx/gradebook/DataLoader.js b/app/jsx/gradebook/DataLoader.js index 9b3d765a61c..2db58a24fc2 100644 --- a/app/jsx/gradebook/DataLoader.js +++ b/app/jsx/gradebook/DataLoader.js @@ -18,10 +18,25 @@ import $ from 'jquery' +import {asJson, consumePrefetchedXHR} from '@instructure/js-utils' + import NaiveRequestDispatch from '../shared/network/NaiveRequestDispatch' import StudentContentDataLoader from './default_gradebook/DataLoader/StudentContentDataLoader' function getStudentIds(courseId) { + if (ENV.prefetch_gradebook_user_ids) { + /* + * When user ids have been prefetched, the data is only known valid for the + * first request. Consume it by pulling it out of the prefetch store, which + * will force all subsequent requests for user ids to call through the + * network. + */ + const promise = consumePrefetchedXHR('user_ids') + if (promise) { + return asJson(promise) + } + } + const url = `/courses/${courseId}/gradebook/user_ids` return $.ajaxJSON(url, 'GET', {}) } diff --git a/app/jsx/gradebook/default_gradebook/DataLoader/__tests__/DataLoader.test.js b/app/jsx/gradebook/default_gradebook/DataLoader/__tests__/DataLoader.test.js index 295f5bb35ec..26a2093cf98 100644 --- a/app/jsx/gradebook/default_gradebook/DataLoader/__tests__/DataLoader.test.js +++ b/app/jsx/gradebook/default_gradebook/DataLoader/__tests__/DataLoader.test.js @@ -18,6 +18,8 @@ import sinon from 'sinon' +import {clearPrefetchedXHRs, getPrefetchedXHR, setPrefetchedXHR} from '@instructure/js-utils' + import FakeServer, { paramsFromRequest, pathFromRequest @@ -179,11 +181,41 @@ describe('Gradebook DataLoader', () => { }) it('resolves .gotStudentIds with the user ids', async () => { - let loadedStudentIds await loadGradebookData() - dataLoader.gotStudentIds.then(studentIds => (loadedStudentIds = studentIds)) + const loadedStudentIds = await dataLoader.gotStudentIds expect(loadedStudentIds).toEqual({user_ids: exampleData.studentIds}) }) + + describe('when student ids have been prefetched', () => { + beforeEach(() => { + ENV.prefetch_gradebook_user_ids = true + const jsonString = JSON.stringify({user_ids: exampleData.studentIds}) + const response = new Response(jsonString) + setPrefetchedXHR('user_ids', Promise.resolve(response)) + }) + + afterEach(() => { + clearPrefetchedXHRs() + delete ENV.prefetch_gradebook_user_ids + }) + + it('does not sends the request using the given course id', async () => { + await loadGradebookData() + const requests = server.filterRequests(urls.userIds) + expect(requests).toHaveLength(0) + }) + + it('resolves .gotStudentIds with the user ids', async () => { + await loadGradebookData() + const loadedStudentIds = await dataLoader.gotStudentIds + expect(loadedStudentIds).toEqual({user_ids: exampleData.studentIds}) + }) + + it('removes the prefetch request', async () => { + await loadGradebookData() + expect(getPrefetchedXHR('user_ids')).toBeUndefined() + }) + }) }) describe('loading grading period assignments', () => { diff --git a/config/feature_flags/apogee_release_flags.yml b/config/feature_flags/apogee_release_flags.yml index 3ea82eb8863..78a739dc2b5 100644 --- a/config/feature_flags/apogee_release_flags.yml +++ b/config/feature_flags/apogee_release_flags.yml @@ -56,3 +56,8 @@ include_speed_grader_in_assignment_header_menu: applies_to: SiteAdmin display_name: Include SpeedGrader in Assignment Header Menu description: Includes a link to SpeedGrader in the assignment header menu in Gradebook. +prefetch_gradebook_user_ids: + state: hidden + applies_to: SiteAdmin + display_name: Prefetch User IDs in Gradebook + description: Load user ids upon page load so that Gradebook loads more quickly. diff --git a/packages/js-utils/src/prefetched_xhrs.js b/packages/js-utils/src/prefetched_xhrs.js index 0408aef9a14..7057ad8e033 100644 --- a/packages/js-utils/src/prefetched_xhrs.js +++ b/packages/js-utils/src/prefetched_xhrs.js @@ -18,10 +18,61 @@ // These are helpful methods you can use along side the ruby ApplicationHelper::prefetch_xhr helper method in canvas +/** + * Retrieve the fetch response promise assigned to the given id. This will + * return `undefined` if there are no prefetched requests or there is no + * request assigned to the given id. When a request is assigned to the given + * id, it will remain stored for subsequent retrievals, if needed. + * + * @param {String} id + * @returns {Promise} fetchResponsePromise + */ export function getPrefetchedXHR(id) { return window.prefetched_xhrs && window.prefetched_xhrs[id] } +/** + * Retrieve the fetch response promise assigned to the given id. This will + * return `undefined` if there are no prefetched requests or there is no + * request assigned to the given id. When a request is assigned to the given + * id, it will be removed from request storage. This is important for requests + * which are expected to return fresh results upon each request. + * + * @param {String} id + * @returns {Promise} fetchResponsePromise + */ +export function consumePrefetchedXHR(id) { + if (window.prefetched_xhrs) { + const value = window.prefetched_xhrs[id] + delete window.prefetched_xhrs[id] + return value + } + + return undefined +} + +/** + * Store a fetch response promise assigned to the given id. This function is + * intended for specs so that they do not need awareness of the implementation + * details for how prefetched requests are managed. + * + * @param {String} id + * @param {Promise} fetchResponsePromise + */ +export function setPrefetchedXHR(id, fetchResponsePromise) { + window.prefetched_xhrs = window.prefetched_xhrs || {} + window.prefetched_xhrs[id] = fetchResponsePromise +} + +/** + * Remove all prefetched requests from storage. This function is intended for + * specs so that they can clean up after assertions without needing awareness + * of the implementation details for how prefetched requests are managed. + */ +export function clearPrefetchedXHRs() { + delete window.prefetched_xhrs +} + /** * Transforms a `fetch` request into something that looks like an `axios` response * with a `.data` and `.headers` property, so you can pass it to our parseLinkHeaders stuff