diff --git a/src/components/AdvanceAnalyticsV2/AnalyticsV2Page.jsx b/src/components/AdvanceAnalyticsV2/AnalyticsV2Page.jsx index 7b78fc5b4e..4348b020c1 100644 --- a/src/components/AdvanceAnalyticsV2/AnalyticsV2Page.jsx +++ b/src/components/AdvanceAnalyticsV2/AnalyticsV2Page.jsx @@ -1,6 +1,6 @@ import React, { useState } from 'react'; import { - Form, Tabs, Tab, + Form, Tabs, Tab, Stack, } from '@openedx/paragon'; import { Helmet } from 'react-helmet'; import PropTypes from 'prop-types'; @@ -13,6 +13,7 @@ import Engagements from './tabs/Engagements'; import Completions from './tabs/Completions'; import Leaderboard from './tabs/Leaderboard'; import Skills from './tabs/Skills'; +import { useEnterpriseAnalyticsAggregatesData } from './data/hooks'; const PAGE_TITLE = 'AnalyticsV2'; @@ -22,28 +23,31 @@ const AnalyticsV2Page = ({ enterpriseId }) => { const [calculation, setCalculation] = useState('Total'); const [startDate, setStartDate] = useState(''); const [endDate, setEndDate] = useState(''); - const dataRefreshDate = ''; const intl = useIntl(); - + const { isFetching, isError, data } = useEnterpriseAnalyticsAggregatesData({ + enterpriseCustomerUUID: enterpriseId, + startDate, + endDate, + }); return ( <> -
-
+ +
-
+
@@ -55,7 +59,8 @@ const AnalyticsV2Page = ({ enterpriseId }) => { setStartDate(e.target.value)} /> @@ -71,7 +76,8 @@ const AnalyticsV2Page = ({ enterpriseId }) => { setEndDate(e.target.value)} /> @@ -168,13 +174,11 @@ const AnalyticsV2Page = ({ enterpriseId }) => {
-
+
@@ -213,9 +217,9 @@ const AnalyticsV2Page = ({ enterpriseId }) => { {
-
+
); }; diff --git a/src/components/AdvanceAnalyticsV2/Stats.jsx b/src/components/AdvanceAnalyticsV2/Stats.jsx index 81c89ae91e..a30b55bf7d 100644 --- a/src/components/AdvanceAnalyticsV2/Stats.jsx +++ b/src/components/AdvanceAnalyticsV2/Stats.jsx @@ -1,14 +1,31 @@ import React from 'react'; import PropTypes from 'prop-types'; import { FormattedMessage } from '@edx/frontend-platform/i18n'; +import { + Spinner, +} from '@openedx/paragon'; +import classNames from 'classnames'; const Stats = ({ - enrollments, distinctCourses, dailySessions, learningHours, completions, + isFetching, isError, data, }) => { const formatter = Intl.NumberFormat('en', { notation: 'compact', maximumFractionDigits: 2 }); - + if (isError) { + return ( + + ); + } return ( -
+
+ {isFetching && ( +
+ +
+ )}

@@ -18,7 +35,7 @@ const Stats = ({ description="Title for the enrollments stat." />

-

{formatter.format(enrollments)}

+

{formatter.format(data?.enrolls || 0)}

@@ -28,7 +45,7 @@ const Stats = ({ description="Title for the distinct courses stat." />

-

{formatter.format(distinctCourses)}

+

{formatter.format(data?.courses || 0)}

@@ -38,7 +55,7 @@ const Stats = ({ description="Title for the daily sessions stat." />

-

{formatter.format(dailySessions)}

+

{formatter.format(data?.sessions || 0)}

@@ -48,7 +65,7 @@ const Stats = ({ description="Title for the learning hours stat." />

-

{formatter.format(learningHours)}

+

{formatter.format(data?.hours || 0)}

@@ -58,7 +75,7 @@ const Stats = ({ description="Title for the completions stat." />

-

{formatter.format(completions)}

+

{formatter.format(data?.completions || 0)}

@@ -66,11 +83,16 @@ const Stats = ({ }; Stats.propTypes = { - enrollments: PropTypes.number.isRequired, - distinctCourses: PropTypes.number.isRequired, - dailySessions: PropTypes.number.isRequired, - learningHours: PropTypes.number.isRequired, - completions: PropTypes.number.isRequired, + data: PropTypes.shape({ + enrolls: PropTypes.number, + courses: PropTypes.number, + sessions: PropTypes.number, + hours: PropTypes.number, + completions: PropTypes.number, + }).isRequired, + isFetching: PropTypes.bool.isRequired, + isError: PropTypes.bool.isRequired, + }; export default Stats; diff --git a/src/components/AdvanceAnalyticsV2/data/constants.js b/src/components/AdvanceAnalyticsV2/data/constants.js index 1fa630a9ca..846daefa15 100644 --- a/src/components/AdvanceAnalyticsV2/data/constants.js +++ b/src/components/AdvanceAnalyticsV2/data/constants.js @@ -77,6 +77,9 @@ export const advanceAnalyticsQueryKeys = { leaderboardTable: (enterpriseUUID, requestOptions) => ( generateKey(analyticsDataTableKeys.leaderboard, enterpriseUUID, requestOptions) ), + aggregates: (enterpriseUUID, requestOptions) => ( + generateKey('aggregates', enterpriseUUID, requestOptions) + ), }; export const skillsColorMap = { diff --git a/src/components/AdvanceAnalyticsV2/data/hooks.js b/src/components/AdvanceAnalyticsV2/data/hooks.js index fd8ffdb77f..4e710e4def 100644 --- a/src/components/AdvanceAnalyticsV2/data/hooks.js +++ b/src/components/AdvanceAnalyticsV2/data/hooks.js @@ -47,3 +47,25 @@ export const usePaginatedData = (data) => useMemo(() => { data: [], }; }, [data]); + +export const useEnterpriseAnalyticsAggregatesData = ({ + enterpriseCustomerUUID, + startDate, + endDate, + queryOptions = {}, +}) => { + const requestOptions = { + startDate, endDate, + }; + return useQuery({ + queryKey: advanceAnalyticsQueryKeys.aggregates(enterpriseCustomerUUID, requestOptions), + queryFn: () => EnterpriseDataApiService.fetchAdminAggregatesData( + enterpriseCustomerUUID, + requestOptions, + ), + staleTime: 0.5 * (1000 * 60 * 60), // 30 minutes. The time in milliseconds after data is considered stale. + cacheTime: 0.75 * (1000 * 60 * 60), // 45 minutes. Cache data will be garbage collected after this duration. + keepPreviousData: true, + ...queryOptions, + }); +}; diff --git a/src/components/AdvanceAnalyticsV2/styles/index.scss b/src/components/AdvanceAnalyticsV2/styles/index.scss index 21748bd525..bc64ffc5a3 100644 --- a/src/components/AdvanceAnalyticsV2/styles/index.scss +++ b/src/components/AdvanceAnalyticsV2/styles/index.scss @@ -2,26 +2,35 @@ font-size: 2.5rem; } -.analytics-chart-container { +@mixin fetching-overlay { + content: ""; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-color: rgba($white, 0.7); + z-index: 1; +} + +@mixin spinner-centered { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + z-index: 2; +} + +.analytics-chart-container, +.stats-container { position: relative; - min-height: 40vh; &.is-fetching::before { - content: ""; - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; - background-color: rgba($white, .7); - z-index: 1; + @include fetching-overlay; } .spinner-centered { - position: absolute; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - z-index: 2; + @include spinner-centered; } } + diff --git a/src/components/AdvanceAnalyticsV2/tests/Stats.test.jsx b/src/components/AdvanceAnalyticsV2/tests/Stats.test.jsx index 340f6ab9cc..4c3b8fc03f 100644 --- a/src/components/AdvanceAnalyticsV2/tests/Stats.test.jsx +++ b/src/components/AdvanceAnalyticsV2/tests/Stats.test.jsx @@ -3,17 +3,18 @@ import { mount } from 'enzyme'; import { IntlProvider } from '@edx/frontend-platform/i18n'; import Stats from '../Stats'; +const data = { + enrolls: 150400, + courses: 365, + sessions: 1892, + hours: 25349876, + completions: 265400, +}; describe('Stats', () => { it('renders the correct values for each statistic', () => { const wrapper = mount( - + , ); diff --git a/src/data/services/EnterpriseDataApiService.js b/src/data/services/EnterpriseDataApiService.js index a64a26ae7a..79f3d04884 100644 --- a/src/data/services/EnterpriseDataApiService.js +++ b/src/data/services/EnterpriseDataApiService.js @@ -167,6 +167,15 @@ class EnterpriseDataApiService { return EnterpriseDataApiService.apiClient().get(url).then((response) => camelCaseObject(response.data)); } + static fetchAdminAggregatesData(enterpriseCustomerUUID, options) { + const baseURL = EnterpriseDataApiService.enterpriseAdminAnalyticsV2BaseUrl; + const enterpriseUUID = EnterpriseDataApiService.getEnterpriseUUID(enterpriseCustomerUUID); + const transformOptions = omitBy(snakeCaseObject(options), isFalsy); + const queryParams = new URLSearchParams(transformOptions); + const url = `${baseURL}${enterpriseUUID}?${queryParams.toString()}`; + return EnterpriseDataApiService.apiClient().get(url).then((response) => camelCaseObject(response.data)); + } + static fetchDashboardInsights(enterpriseId) { const enterpriseUUID = EnterpriseDataApiService.getEnterpriseUUID(enterpriseId); const url = `${EnterpriseDataApiService.enterpriseAdminBaseUrl}insights/${enterpriseUUID}`;