Skip to content

Commit 2465202

Browse files
authored
Merge pull request #1293 from openedx/eahmadjaved/ENT-9401
feat: integrate initial aggregates data on analytics v2 page
2 parents 24478cf + 6a25b4b commit 2465202

File tree

7 files changed

+122
-52
lines changed

7 files changed

+122
-52
lines changed

src/components/AdvanceAnalyticsV2/AnalyticsV2Page.jsx

Lines changed: 21 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import React, { useState } from 'react';
22
import {
3-
Form, Tabs, Tab,
3+
Form, Tabs, Tab, Stack,
44
} from '@openedx/paragon';
55
import { Helmet } from 'react-helmet';
66
import PropTypes from 'prop-types';
@@ -13,6 +13,7 @@ import Engagements from './tabs/Engagements';
1313
import Completions from './tabs/Completions';
1414
import Leaderboard from './tabs/Leaderboard';
1515
import Skills from './tabs/Skills';
16+
import { useEnterpriseAnalyticsAggregatesData } from './data/hooks';
1617

1718
const PAGE_TITLE = 'AnalyticsV2';
1819

@@ -22,28 +23,31 @@ const AnalyticsV2Page = ({ enterpriseId }) => {
2223
const [calculation, setCalculation] = useState('Total');
2324
const [startDate, setStartDate] = useState('');
2425
const [endDate, setEndDate] = useState('');
25-
const dataRefreshDate = '';
2626
const intl = useIntl();
27-
27+
const { isFetching, isError, data } = useEnterpriseAnalyticsAggregatesData({
28+
enterpriseCustomerUUID: enterpriseId,
29+
startDate,
30+
endDate,
31+
});
2832
return (
2933
<>
3034
<Helmet title={PAGE_TITLE} />
3135
<Hero title={PAGE_TITLE} />
32-
<div className="container-fluid w-100">
33-
<div className="row data-refresh-msg-container mb-4">
36+
<Stack className="container-fluid w-100" gap={4}>
37+
<div className="row data-refresh-msg-container">
3438
<div className="col">
3539
<span>
3640
<FormattedMessage
3741
id="advance.analytics.data.refresh.msg"
3842
defaultMessage="Data updated on {date}"
3943
description="Data refresh message"
40-
values={{ date: dataRefreshDate }}
44+
values={{ date: data?.lastUpdatedAt || '' }}
4145
/>
4246
</span>
4347
</div>
4448
</div>
4549

46-
<div className="row filter-container mb-4">
50+
<div className="row filter-container">
4751
<div className="col">
4852
<Form.Group>
4953
<Form.Label>
@@ -55,7 +59,8 @@ const AnalyticsV2Page = ({ enterpriseId }) => {
5559
</Form.Label>
5660
<Form.Control
5761
type="date"
58-
value={startDate}
62+
value={startDate || data?.minEnrollmentDate}
63+
min={data?.minEnrollmentDate}
5964
onChange={(e) => setStartDate(e.target.value)}
6065
/>
6166
</Form.Group>
@@ -71,7 +76,8 @@ const AnalyticsV2Page = ({ enterpriseId }) => {
7176
</Form.Label>
7277
<Form.Control
7378
type="date"
74-
value={endDate}
79+
value={endDate || data?.maxEnrollmentDate}
80+
max={data?.maxEnrollmentDate}
7581
onChange={(e) => setEndDate(e.target.value)}
7682
/>
7783
</Form.Group>
@@ -168,13 +174,11 @@ const AnalyticsV2Page = ({ enterpriseId }) => {
168174
</div>
169175
</div>
170176

171-
<div className="row stats-container mb-4">
177+
<div className="row stats-container d-flex justify-content-center">
172178
<Stats
173-
enrollments={0}
174-
distinctCourses={0}
175-
dailySessions={0}
176-
learningHours={0}
177-
completions={0}
179+
data={data}
180+
isFetching={isFetching}
181+
isError={isError}
178182
/>
179183
</div>
180184

@@ -213,9 +217,9 @@ const AnalyticsV2Page = ({ enterpriseId }) => {
213217
<Engagements
214218
startDate={startDate}
215219
endDate={endDate}
220+
enterpriseId={enterpriseId}
216221
granularity={granularity}
217222
calculation={calculation}
218-
enterpriseId={enterpriseId}
219223
/>
220224
</Tab>
221225
<Tab
@@ -264,7 +268,7 @@ const AnalyticsV2Page = ({ enterpriseId }) => {
264268
</Tab>
265269
</Tabs>
266270
</div>
267-
</div>
271+
</Stack>
268272
</>
269273
);
270274
};
Lines changed: 35 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,31 @@
11
import React from 'react';
22
import PropTypes from 'prop-types';
33
import { FormattedMessage } from '@edx/frontend-platform/i18n';
4+
import {
5+
Spinner,
6+
} from '@openedx/paragon';
7+
import classNames from 'classnames';
48

59
const Stats = ({
6-
enrollments, distinctCourses, dailySessions, learningHours, completions,
10+
isFetching, isError, data,
711
}) => {
812
const formatter = Intl.NumberFormat('en', { notation: 'compact', maximumFractionDigits: 2 });
9-
13+
if (isError) {
14+
return (
15+
<FormattedMessage
16+
id="advance.analytics.stats.aggregates.notFound.errorMesssage"
17+
defaultMessage="No Matching Data Found"
18+
description="Error message when no data is found."
19+
/>
20+
);
21+
}
1022
return (
11-
<div className="container-fluid analytics-stats">
23+
<div className={classNames('container-fluid analytics-stats stats-container', { 'is-fetching': isFetching })}>
24+
{isFetching && (
25+
<div className="spinner-centered">
26+
<Spinner animation="border" />
27+
</div>
28+
)}
1229
<div className="row">
1330
<div className="col d-flex flex-column justify-content-center align-items-center">
1431
<p className="mb-0 small title-enrollments">
@@ -18,7 +35,7 @@ const Stats = ({
1835
description="Title for the enrollments stat."
1936
/>
2037
</p>
21-
<p className="font-weight-bolder analytics-stat-number value-enrollments">{formatter.format(enrollments)}</p>
38+
<p className="font-weight-bolder analytics-stat-number value-enrollments">{formatter.format(data?.enrolls || 0)}</p>
2239
</div>
2340
<div className="col d-flex flex-column justify-content-center align-items-center">
2441
<p className="mb-0 small title-distinct-courses">
@@ -28,7 +45,7 @@ const Stats = ({
2845
description="Title for the distinct courses stat."
2946
/>
3047
</p>
31-
<p className="font-weight-bolder analytics-stat-number value-distinct-courses">{formatter.format(distinctCourses)}</p>
48+
<p className="font-weight-bolder analytics-stat-number value-distinct-courses">{formatter.format(data?.courses || 0)}</p>
3249
</div>
3350
<div className="col d-flex flex-column justify-content-center align-items-center">
3451
<p className="mb-0 small title-daily-sessions">
@@ -38,7 +55,7 @@ const Stats = ({
3855
description="Title for the daily sessions stat."
3956
/>
4057
</p>
41-
<p className="font-weight-bolder analytics-stat-number value-daily-sessions">{formatter.format(dailySessions)}</p>
58+
<p className="font-weight-bolder analytics-stat-number value-daily-sessions">{formatter.format(data?.sessions || 0)}</p>
4259
</div>
4360
<div className="col d-flex flex-column justify-content-center align-items-center">
4461
<p className="mb-0 small title-learning-hours">
@@ -48,7 +65,7 @@ const Stats = ({
4865
description="Title for the learning hours stat."
4966
/>
5067
</p>
51-
<p className="font-weight-bolder analytics-stat-number value-learning-hours">{formatter.format(learningHours)}</p>
68+
<p className="font-weight-bolder analytics-stat-number value-learning-hours">{formatter.format(data?.hours || 0)}</p>
5269
</div>
5370
<div className="col d-flex flex-column justify-content-center align-items-center">
5471
<p className="mb-0 small title-completions">
@@ -58,19 +75,24 @@ const Stats = ({
5875
description="Title for the completions stat."
5976
/>
6077
</p>
61-
<p className="font-weight-bolder analytics-stat-number value-completions">{formatter.format(completions)}</p>
78+
<p className="font-weight-bolder analytics-stat-number value-completions">{formatter.format(data?.completions || 0)}</p>
6279
</div>
6380
</div>
6481
</div>
6582
);
6683
};
6784

6885
Stats.propTypes = {
69-
enrollments: PropTypes.number.isRequired,
70-
distinctCourses: PropTypes.number.isRequired,
71-
dailySessions: PropTypes.number.isRequired,
72-
learningHours: PropTypes.number.isRequired,
73-
completions: PropTypes.number.isRequired,
86+
data: PropTypes.shape({
87+
enrolls: PropTypes.number,
88+
courses: PropTypes.number,
89+
sessions: PropTypes.number,
90+
hours: PropTypes.number,
91+
completions: PropTypes.number,
92+
}).isRequired,
93+
isFetching: PropTypes.bool.isRequired,
94+
isError: PropTypes.bool.isRequired,
95+
7496
};
7597

7698
export default Stats;

src/components/AdvanceAnalyticsV2/data/constants.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,9 @@ export const advanceAnalyticsQueryKeys = {
7777
leaderboardTable: (enterpriseUUID, requestOptions) => (
7878
generateKey(analyticsDataTableKeys.leaderboard, enterpriseUUID, requestOptions)
7979
),
80+
aggregates: (enterpriseUUID, requestOptions) => (
81+
generateKey('aggregates', enterpriseUUID, requestOptions)
82+
),
8083
};
8184

8285
export const skillsColorMap = {

src/components/AdvanceAnalyticsV2/data/hooks.js

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,3 +47,25 @@ export const usePaginatedData = (data) => useMemo(() => {
4747
data: [],
4848
};
4949
}, [data]);
50+
51+
export const useEnterpriseAnalyticsAggregatesData = ({
52+
enterpriseCustomerUUID,
53+
startDate,
54+
endDate,
55+
queryOptions = {},
56+
}) => {
57+
const requestOptions = {
58+
startDate, endDate,
59+
};
60+
return useQuery({
61+
queryKey: advanceAnalyticsQueryKeys.aggregates(enterpriseCustomerUUID, requestOptions),
62+
queryFn: () => EnterpriseDataApiService.fetchAdminAggregatesData(
63+
enterpriseCustomerUUID,
64+
requestOptions,
65+
),
66+
staleTime: 0.5 * (1000 * 60 * 60), // 30 minutes. The time in milliseconds after data is considered stale.
67+
cacheTime: 0.75 * (1000 * 60 * 60), // 45 minutes. Cache data will be garbage collected after this duration.
68+
keepPreviousData: true,
69+
...queryOptions,
70+
});
71+
};

src/components/AdvanceAnalyticsV2/styles/index.scss

Lines changed: 24 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,26 +2,35 @@
22
font-size: 2.5rem;
33
}
44

5-
.analytics-chart-container {
5+
@mixin fetching-overlay {
6+
content: "";
7+
position: absolute;
8+
top: 0;
9+
left: 0;
10+
width: 100%;
11+
height: 100%;
12+
background-color: rgba($white, 0.7);
13+
z-index: 1;
14+
}
15+
16+
@mixin spinner-centered {
17+
position: absolute;
18+
top: 50%;
19+
left: 50%;
20+
transform: translate(-50%, -50%);
21+
z-index: 2;
22+
}
23+
24+
.analytics-chart-container,
25+
.stats-container {
626
position: relative;
7-
min-height: 40vh;
827

928
&.is-fetching::before {
10-
content: "";
11-
position: absolute;
12-
top: 0;
13-
left: 0;
14-
width: 100%;
15-
height: 100%;
16-
background-color: rgba($white, .7);
17-
z-index: 1;
29+
@include fetching-overlay;
1830
}
1931

2032
.spinner-centered {
21-
position: absolute;
22-
top: 50%;
23-
left: 50%;
24-
transform: translate(-50%, -50%);
25-
z-index: 2;
33+
@include spinner-centered;
2634
}
2735
}
36+

src/components/AdvanceAnalyticsV2/tests/Stats.test.jsx

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,18 @@ import { mount } from 'enzyme';
33
import { IntlProvider } from '@edx/frontend-platform/i18n';
44
import Stats from '../Stats';
55

6+
const data = {
7+
enrolls: 150400,
8+
courses: 365,
9+
sessions: 1892,
10+
hours: 25349876,
11+
completions: 265400,
12+
};
613
describe('Stats', () => {
714
it('renders the correct values for each statistic', () => {
815
const wrapper = mount(
916
<IntlProvider locale="en">
10-
<Stats
11-
enrollments={150400}
12-
distinctCourses={365}
13-
dailySessions={1892}
14-
learningHours={25349876}
15-
completions={265400}
16-
/>
17+
<Stats data={data} isFetching={false} isError={false} />
1718
</IntlProvider>,
1819
);
1920

src/data/services/EnterpriseDataApiService.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,15 @@ class EnterpriseDataApiService {
167167
return EnterpriseDataApiService.apiClient().get(url).then((response) => camelCaseObject(response.data));
168168
}
169169

170+
static fetchAdminAggregatesData(enterpriseCustomerUUID, options) {
171+
const baseURL = EnterpriseDataApiService.enterpriseAdminAnalyticsV2BaseUrl;
172+
const enterpriseUUID = EnterpriseDataApiService.getEnterpriseUUID(enterpriseCustomerUUID);
173+
const transformOptions = omitBy(snakeCaseObject(options), isFalsy);
174+
const queryParams = new URLSearchParams(transformOptions);
175+
const url = `${baseURL}${enterpriseUUID}?${queryParams.toString()}`;
176+
return EnterpriseDataApiService.apiClient().get(url).then((response) => camelCaseObject(response.data));
177+
}
178+
170179
static fetchDashboardInsights(enterpriseId) {
171180
const enterpriseUUID = EnterpriseDataApiService.getEnterpriseUUID(enterpriseId);
172181
const url = `${EnterpriseDataApiService.enterpriseAdminBaseUrl}insights/${enterpriseUUID}`;

0 commit comments

Comments
 (0)