Skip to content

Commit

Permalink
Merge pull request #1293 from openedx/eahmadjaved/ENT-9401
Browse files Browse the repository at this point in the history
feat: integrate initial aggregates data on analytics v2 page
  • Loading branch information
jajjibhai008 authored Sep 10, 2024
2 parents 24478cf + 6a25b4b commit 2465202
Show file tree
Hide file tree
Showing 7 changed files with 122 additions and 52 deletions.
38 changes: 21 additions & 17 deletions src/components/AdvanceAnalyticsV2/AnalyticsV2Page.jsx
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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';

Expand All @@ -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 (
<>
<Helmet title={PAGE_TITLE} />
<Hero title={PAGE_TITLE} />
<div className="container-fluid w-100">
<div className="row data-refresh-msg-container mb-4">
<Stack className="container-fluid w-100" gap={4}>
<div className="row data-refresh-msg-container">
<div className="col">
<span>
<FormattedMessage
id="advance.analytics.data.refresh.msg"
defaultMessage="Data updated on {date}"
description="Data refresh message"
values={{ date: dataRefreshDate }}
values={{ date: data?.lastUpdatedAt || '' }}
/>
</span>
</div>
</div>

<div className="row filter-container mb-4">
<div className="row filter-container">
<div className="col">
<Form.Group>
<Form.Label>
Expand All @@ -55,7 +59,8 @@ const AnalyticsV2Page = ({ enterpriseId }) => {
</Form.Label>
<Form.Control
type="date"
value={startDate}
value={startDate || data?.minEnrollmentDate}
min={data?.minEnrollmentDate}
onChange={(e) => setStartDate(e.target.value)}
/>
</Form.Group>
Expand All @@ -71,7 +76,8 @@ const AnalyticsV2Page = ({ enterpriseId }) => {
</Form.Label>
<Form.Control
type="date"
value={endDate}
value={endDate || data?.maxEnrollmentDate}
max={data?.maxEnrollmentDate}
onChange={(e) => setEndDate(e.target.value)}
/>
</Form.Group>
Expand Down Expand Up @@ -168,13 +174,11 @@ const AnalyticsV2Page = ({ enterpriseId }) => {
</div>
</div>

<div className="row stats-container mb-4">
<div className="row stats-container d-flex justify-content-center">
<Stats
enrollments={0}
distinctCourses={0}
dailySessions={0}
learningHours={0}
completions={0}
data={data}
isFetching={isFetching}
isError={isError}
/>
</div>

Expand Down Expand Up @@ -213,9 +217,9 @@ const AnalyticsV2Page = ({ enterpriseId }) => {
<Engagements
startDate={startDate}
endDate={endDate}
enterpriseId={enterpriseId}
granularity={granularity}
calculation={calculation}
enterpriseId={enterpriseId}
/>
</Tab>
<Tab
Expand Down Expand Up @@ -264,7 +268,7 @@ const AnalyticsV2Page = ({ enterpriseId }) => {
</Tab>
</Tabs>
</div>
</div>
</Stack>
</>
);
};
Expand Down
48 changes: 35 additions & 13 deletions src/components/AdvanceAnalyticsV2/Stats.jsx
Original file line number Diff line number Diff line change
@@ -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 (
<FormattedMessage
id="advance.analytics.stats.aggregates.notFound.errorMesssage"
defaultMessage="No Matching Data Found"
description="Error message when no data is found."
/>
);
}
return (
<div className="container-fluid analytics-stats">
<div className={classNames('container-fluid analytics-stats stats-container', { 'is-fetching': isFetching })}>
{isFetching && (
<div className="spinner-centered">
<Spinner animation="border" />
</div>
)}
<div className="row">
<div className="col d-flex flex-column justify-content-center align-items-center">
<p className="mb-0 small title-enrollments">
Expand All @@ -18,7 +35,7 @@ const Stats = ({
description="Title for the enrollments stat."
/>
</p>
<p className="font-weight-bolder analytics-stat-number value-enrollments">{formatter.format(enrollments)}</p>
<p className="font-weight-bolder analytics-stat-number value-enrollments">{formatter.format(data?.enrolls || 0)}</p>
</div>
<div className="col d-flex flex-column justify-content-center align-items-center">
<p className="mb-0 small title-distinct-courses">
Expand All @@ -28,7 +45,7 @@ const Stats = ({
description="Title for the distinct courses stat."
/>
</p>
<p className="font-weight-bolder analytics-stat-number value-distinct-courses">{formatter.format(distinctCourses)}</p>
<p className="font-weight-bolder analytics-stat-number value-distinct-courses">{formatter.format(data?.courses || 0)}</p>
</div>
<div className="col d-flex flex-column justify-content-center align-items-center">
<p className="mb-0 small title-daily-sessions">
Expand All @@ -38,7 +55,7 @@ const Stats = ({
description="Title for the daily sessions stat."
/>
</p>
<p className="font-weight-bolder analytics-stat-number value-daily-sessions">{formatter.format(dailySessions)}</p>
<p className="font-weight-bolder analytics-stat-number value-daily-sessions">{formatter.format(data?.sessions || 0)}</p>
</div>
<div className="col d-flex flex-column justify-content-center align-items-center">
<p className="mb-0 small title-learning-hours">
Expand All @@ -48,7 +65,7 @@ const Stats = ({
description="Title for the learning hours stat."
/>
</p>
<p className="font-weight-bolder analytics-stat-number value-learning-hours">{formatter.format(learningHours)}</p>
<p className="font-weight-bolder analytics-stat-number value-learning-hours">{formatter.format(data?.hours || 0)}</p>
</div>
<div className="col d-flex flex-column justify-content-center align-items-center">
<p className="mb-0 small title-completions">
Expand All @@ -58,19 +75,24 @@ const Stats = ({
description="Title for the completions stat."
/>
</p>
<p className="font-weight-bolder analytics-stat-number value-completions">{formatter.format(completions)}</p>
<p className="font-weight-bolder analytics-stat-number value-completions">{formatter.format(data?.completions || 0)}</p>
</div>
</div>
</div>
);
};

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;
3 changes: 3 additions & 0 deletions src/components/AdvanceAnalyticsV2/data/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down
22 changes: 22 additions & 0 deletions src/components/AdvanceAnalyticsV2/data/hooks.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
});
};
39 changes: 24 additions & 15 deletions src/components/AdvanceAnalyticsV2/styles/index.scss
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}

15 changes: 8 additions & 7 deletions src/components/AdvanceAnalyticsV2/tests/Stats.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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(
<IntlProvider locale="en">
<Stats
enrollments={150400}
distinctCourses={365}
dailySessions={1892}
learningHours={25349876}
completions={265400}
/>
<Stats data={data} isFetching={false} isError={false} />
</IntlProvider>,
);

Expand Down
9 changes: 9 additions & 0 deletions src/data/services/EnterpriseDataApiService.js
Original file line number Diff line number Diff line change
Expand Up @@ -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}`;
Expand Down

0 comments on commit 2465202

Please sign in to comment.