Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: integrate initial aggregates data on analytics v2 page #1293

Merged
merged 1 commit into from
Sep 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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}
jajjibhai008 marked this conversation as resolved.
Show resolved Hide resolved
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}
jajjibhai008 marked this conversation as resolved.
Show resolved Hide resolved
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) {
jajjibhai008 marked this conversation as resolved.
Show resolved Hide resolved
return (

Check warning on line 14 in src/components/AdvanceAnalyticsV2/Stats.jsx

View check run for this annotation

Codecov / codecov/patch

src/components/AdvanceAnalyticsV2/Stats.jsx#L14

Added line #L14 was not covered by tests
<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 @@
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 @@
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 @@
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 @@
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 @@
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 = ({
jajjibhai008 marked this conversation as resolved.
Show resolved Hide resolved
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;
}
jajjibhai008 marked this conversation as resolved.
Show resolved Hide resolved
}

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
Loading