@@ -35,6 +47,14 @@ const Completions = () => {
defaultMessage: 'See the courses in which your learners are most often achieving a passing grade.',
description: 'Subtitle for the top 10 courses by completions chart.',
})}
+ startDate={startDate}
+ endDate={endDate}
+ activeTab={ANALYTICS_TABS.COMPLETIONS}
+ granularity={granularity}
+ calculation={calculation}
+ chartType={CHART_TYPES.TOP_COURSES_BY_COMPLETIONS}
+ enterpriseId={enterpriseId}
+ isDownloadCSV
/>
@@ -50,6 +70,14 @@ const Completions = () => {
defaultMessage: 'See the subjects your learners are most often achieving a passing grade.',
description: 'Subtitle for the top 10 subjects by completion chart.',
})}
+ startDate={startDate}
+ endDate={endDate}
+ activeTab={ANALYTICS_TABS.COMPLETIONS}
+ granularity={granularity}
+ calculation={calculation}
+ chartType={CHART_TYPES.TOP_SUBJECTS_BY_COMPLETIONS}
+ enterpriseId={enterpriseId}
+ isDownloadCSV
/>
@@ -65,11 +93,24 @@ const Completions = () => {
defaultMessage: 'See the individual completions from your organization.',
description: 'Subtitle for the individual completions datatable.',
})}
+ startDate={startDate}
+ endDate={endDate}
+ activeTab={ANALYTICS_TABS.COMPLETIONS}
+ granularity={granularity}
+ calculation={calculation}
+ enterpriseId={enterpriseId}
+ isDownloadCSV
/>
);
};
-
+Completions.propTypes = {
+ enterpriseId: PropTypes.string.isRequired,
+ startDate: PropTypes.string.isRequired,
+ endDate: PropTypes.string.isRequired,
+ granularity: PropTypes.string.isRequired,
+ calculation: PropTypes.string.isRequired,
+};
export default Completions;
diff --git a/src/components/AdvanceAnalyticsV2/tabs/Engagements.jsx b/src/components/AdvanceAnalyticsV2/tabs/Engagements.jsx
index aa8c5d175f..13988f06c4 100644
--- a/src/components/AdvanceAnalyticsV2/tabs/Engagements.jsx
+++ b/src/components/AdvanceAnalyticsV2/tabs/Engagements.jsx
@@ -1,9 +1,11 @@
import React from 'react';
import { useIntl } from '@edx/frontend-platform/i18n';
+import PropTypes from 'prop-types';
import EmptyChart from '../charts/EmptyChart';
import Header from '../Header';
+import { ANALYTICS_TABS, CHART_TYPES } from '../data/constants';
-const Engagements = () => {
+const Engagements = ({ startDate, endDate, enterpriseId }) => {
const intl = useIntl();
return (
@@ -20,6 +22,12 @@ const Engagements = () => {
defaultMessage: 'See audit and certificate track hours of learning over time.',
description: 'Subtitle for the learning hours over time chart.',
})}
+ startDate={startDate}
+ endDate={endDate}
+ activeTab={ANALYTICS_TABS.ENGAGEMENTS}
+ chartType={CHART_TYPES.ENGAGEMENTS_OVER_TIME}
+ enterpriseId={enterpriseId}
+ isDownloadCSV
/>
@@ -35,6 +43,12 @@ const Engagements = () => {
defaultMessage: 'See the courses in which your learners spend the most time.',
description: 'Subtitle for the top 10 courses by learning hours chart.',
})}
+ startDate={startDate}
+ endDate={endDate}
+ activeTab={ANALYTICS_TABS.ENGAGEMENTS}
+ chartType={CHART_TYPES.TOP_COURSES_BY_ENGAGEMENTS}
+ enterpriseId={enterpriseId}
+ isDownloadCSV
/>
@@ -50,6 +64,12 @@ const Engagements = () => {
defaultMessage: 'See the subjects your learners are spending the most time in.',
description: 'Subtitle for the top 10 subjects by learning hours chart.',
})}
+ startDate={startDate}
+ endDate={endDate}
+ activeTab={ANALYTICS_TABS.ENGAGEMENTS}
+ chartType={CHART_TYPES.TOP_SUBJECTS_BY_ENGAGEMENTS}
+ enterpriseId={enterpriseId}
+ isDownloadCSV
/>
@@ -65,6 +85,11 @@ const Engagements = () => {
defaultMessage: 'See the engagement levels of learners from your organization.',
description: 'Subtitle for the individual engagements datatable.',
})}
+ startDate={startDate}
+ endDate={endDate}
+ activeTab={ANALYTICS_TABS.ENGAGEMENTS}
+ enterpriseId={enterpriseId}
+ isDownloadCSV
/>
@@ -72,4 +97,9 @@ const Engagements = () => {
);
};
+Engagements.propTypes = {
+ startDate: PropTypes.string.isRequired,
+ endDate: PropTypes.string.isRequired,
+ enterpriseId: PropTypes.string.isRequired,
+};
export default Engagements;
diff --git a/src/components/AdvanceAnalyticsV2/tabs/Enrollments.jsx b/src/components/AdvanceAnalyticsV2/tabs/Enrollments.jsx
index e1c377e7cf..36f41b0319 100644
--- a/src/components/AdvanceAnalyticsV2/tabs/Enrollments.jsx
+++ b/src/components/AdvanceAnalyticsV2/tabs/Enrollments.jsx
@@ -1,9 +1,13 @@
import React from 'react';
import { useIntl } from '@edx/frontend-platform/i18n';
+import PropTypes from 'prop-types';
import EmptyChart from '../charts/EmptyChart';
import Header from '../Header';
+import { ANALYTICS_TABS, CHART_TYPES } from '../data/constants';
-const Enrollments = () => {
+const Enrollments = ({
+ startDate, endDate, granularity, calculation, enterpriseId,
+}) => {
const intl = useIntl();
return (
@@ -20,6 +24,14 @@ const Enrollments = () => {
defaultMessage: 'See audit and certificate track enrollments over time.',
description: 'Subtitle for the enrollments over time chart.',
})}
+ startDate={startDate}
+ endDate={endDate}
+ granularity={granularity}
+ calculation={calculation}
+ activeTab={ANALYTICS_TABS.ENROLLMENTS}
+ chartType={CHART_TYPES.ENROLLMENTS_OVER_TIME}
+ enterpriseId={enterpriseId}
+ isDownloadCSV
/>
@@ -35,6 +47,14 @@ const Enrollments = () => {
defaultMessage: 'See the most popular courses at your organization.',
description: 'Subtitle for the top 10 courses by enrollment chart.',
})}
+ startDate={startDate}
+ endDate={endDate}
+ granularity={granularity}
+ calculation={calculation}
+ activeTab={ANALYTICS_TABS.ENROLLMENTS}
+ chartType={CHART_TYPES.TOP_COURSES_BY_ENROLLMENTS}
+ enterpriseId={enterpriseId}
+ isDownloadCSV
/>
@@ -50,6 +70,14 @@ const Enrollments = () => {
defaultMessage: 'See the most popular subjects at your organization.',
description: 'Subtitle for the top 10 subjects by enrollment chart.',
})}
+ startDate={startDate}
+ endDate={endDate}
+ granularity={granularity}
+ calculation={calculation}
+ activeTab={ANALYTICS_TABS.ENROLLMENTS}
+ chartType={CHART_TYPES.TOP_SUBJECTS_BY_ENROLLMENTS}
+ enterpriseId={enterpriseId}
+ isDownloadCSV
/>
@@ -65,6 +93,13 @@ const Enrollments = () => {
defaultMessage: 'See the individual enrollments from your organization.',
description: 'Subtitle for the individual enrollments datatable.',
})}
+ startDate={startDate}
+ endDate={endDate}
+ granularity={granularity}
+ calculation={calculation}
+ activeTab={ANALYTICS_TABS.ENROLLMENTS}
+ enterpriseId={enterpriseId}
+ isDownloadCSV
/>
@@ -72,4 +107,12 @@ const Enrollments = () => {
);
};
+Enrollments.propTypes = {
+ enterpriseId: PropTypes.string.isRequired,
+ startDate: PropTypes.string.isRequired,
+ endDate: PropTypes.string.isRequired,
+ granularity: PropTypes.string.isRequired,
+ calculation: PropTypes.string.isRequired,
+};
+
export default Enrollments;
diff --git a/src/components/AdvanceAnalyticsV2/tabs/Leaderboard.jsx b/src/components/AdvanceAnalyticsV2/tabs/Leaderboard.jsx
index 940e779631..0eee4b8180 100644
--- a/src/components/AdvanceAnalyticsV2/tabs/Leaderboard.jsx
+++ b/src/components/AdvanceAnalyticsV2/tabs/Leaderboard.jsx
@@ -1,9 +1,11 @@
import React from 'react';
import { useIntl } from '@edx/frontend-platform/i18n';
+import PropTypes from 'prop-types';
import EmptyChart from '../charts/EmptyChart';
import Header from '../Header';
+import { ANALYTICS_TABS } from '../data/constants';
-const Leaderboard = () => {
+const Leaderboard = ({ startDate, endDate, enterpriseId }) => {
const intl = useIntl();
return (
@@ -20,6 +22,11 @@ const Leaderboard = () => {
defaultMessage: 'See the top learners by different measures of engagement. The results are defaulted to sort by learning hours. Download the full CSV below to sort by other metrics.',
description: 'Subtitle for the leaderboard datatable.',
})}
+ startDate={startDate}
+ endDate={endDate}
+ activeTab={ANALYTICS_TABS.LEADERBOARD}
+ enterpriseId={enterpriseId}
+ isDownloadCSV
/>
@@ -27,4 +34,10 @@ const Leaderboard = () => {
);
};
+Leaderboard.propTypes = {
+ enterpriseId: PropTypes.string.isRequired,
+ startDate: PropTypes.string.isRequired,
+ endDate: PropTypes.string.isRequired,
+};
+
export default Leaderboard;
diff --git a/src/components/AdvanceAnalyticsV2/tabs/Skills.jsx b/src/components/AdvanceAnalyticsV2/tabs/Skills.jsx
index d7b85ccbb7..19694ca07d 100644
--- a/src/components/AdvanceAnalyticsV2/tabs/Skills.jsx
+++ b/src/components/AdvanceAnalyticsV2/tabs/Skills.jsx
@@ -1,9 +1,11 @@
import React from 'react';
import { useIntl } from '@edx/frontend-platform/i18n';
+import PropTypes from 'prop-types';
import Header from '../Header';
import EmptyChart from '../charts/EmptyChart';
+import { ANALYTICS_TABS, CHART_TYPES } from '../data/constants';
-const Skills = () => {
+const Skills = ({ startDate, endDate, enterpriseId }) => {
const intl = useIntl();
return (
@@ -20,6 +22,12 @@ const Skills = () => {
defaultMessage: 'See the top skills that are the most in demand in your organization, based on enrollments and completions.',
description: 'Subtitle for the top skills chart.',
})}
+ startDate={startDate}
+ endDate={endDate}
+ activeTab={ANALYTICS_TABS.SKILLS}
+ chartType={CHART_TYPES.BUBBLE}
+ enterpriseId={enterpriseId}
+ isDownloadCSV
/>
@@ -53,4 +61,10 @@ const Skills = () => {
);
};
+Skills.propTypes = {
+ startDate: PropTypes.string.isRequired,
+ endDate: PropTypes.string.isRequired,
+ enterpriseId: PropTypes.string.isRequired,
+};
+
export default Skills;
diff --git a/src/components/AdvanceAnalyticsV2/tests/AnalyticsV2Page.test.jsx b/src/components/AdvanceAnalyticsV2/tests/AnalyticsV2Page.test.jsx
new file mode 100644
index 0000000000..0cba1d8df1
--- /dev/null
+++ b/src/components/AdvanceAnalyticsV2/tests/AnalyticsV2Page.test.jsx
@@ -0,0 +1,77 @@
+import React from 'react';
+import { render, fireEvent, within } from '@testing-library/react';
+import '@testing-library/jest-dom/extend-expect';
+import { IntlProvider } from '@edx/frontend-platform/i18n';
+import { Provider } from 'react-redux';
+import configureMockStore from 'redux-mock-store';
+import thunk from 'redux-thunk';
+import AnalyticsV2Page from '../AnalyticsV2Page';
+import '@testing-library/jest-dom';
+
+const mockStore = configureMockStore([thunk]);
+const store = mockStore({
+ portalConfiguration: {
+ enterpriseId: 'test-enterprise-id',
+ enterpriseFeatures: {
+ topDownAssignmentRealTimeLcm: true,
+ },
+ },
+ table: {},
+ csv: {},
+ dashboardAnalytics: {
+ active_learners: {
+ past_month: 1,
+ past_week: 1,
+ },
+ enrolled_learners: 1,
+ number_of_users: 3,
+ course_completions: 1,
+ },
+ dashboardInsights: {
+ loading: null,
+ insights: null,
+ },
+});
+
+const renderWithProviders = (component) => render(
+
+
+ {component}
+
+ ,
+);
+
+describe('AnalyticsV2Page', () => {
+ test('verifies the granularity select label, options, and values', () => {
+ const { container } = renderWithProviders();
+
+ // Get the div by class name
+ const granularityDiv = container.querySelector('[data-testid="granularity-select"]');
+ expect(granularityDiv).toBeInTheDocument();
+
+ // Verify the label within the div
+ const granularityLabel = within(granularityDiv).getByText('Date granularity');
+ expect(granularityLabel).toBeInTheDocument();
+
+ // Verift the onChange event on startDate is working
+ });
+ test('verifies the start date and end date input fields', () => {
+ const { getByLabelText } = renderWithProviders();
+
+ // Get start date input field by its label text
+ const startDateInput = getByLabelText('Start Date');
+ expect(startDateInput).toBeInTheDocument();
+ expect(startDateInput).toHaveAttribute('type', 'date');
+
+ // Get end date input field by its label text
+ const endDateInput = getByLabelText('End Date');
+ expect(endDateInput).toBeInTheDocument();
+ expect(endDateInput).toHaveAttribute('type', 'date');
+
+ fireEvent.change(startDateInput, { target: { value: '2024-06-01' } });
+ expect(startDateInput.value).toBe('2024-06-01');
+
+ fireEvent.change(endDateInput, { target: { value: '2025-06-01' } });
+ expect(endDateInput.value).toBe('2025-06-01');
+ });
+});
diff --git a/src/components/AdvanceAnalyticsV2/tests/DownloadCSV.test.jsx b/src/components/AdvanceAnalyticsV2/tests/DownloadCSV.test.jsx
new file mode 100644
index 0000000000..dc03ae5a3f
--- /dev/null
+++ b/src/components/AdvanceAnalyticsV2/tests/DownloadCSV.test.jsx
@@ -0,0 +1,89 @@
+import React from 'react';
+import { IntlProvider } from '@edx/frontend-platform/i18n';
+import { render, screen } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import { saveAs } from 'file-saver';
+import { logError } from '@edx/frontend-platform/logging';
+import DownloadCSV from '../DownloadCSV';
+import '@testing-library/jest-dom/extend-expect';
+import EnterpriseDataApiService from '../../../data/services/EnterpriseDataApiService';
+
+jest.mock('file-saver', () => ({
+ ...jest.requireActual('file-saver'),
+ saveAs: jest.fn(),
+}));
+
+jest.mock('@edx/frontend-platform/logging', () => ({
+ ...jest.requireActual('@edx/frontend-platform/logging'),
+ logError: jest.fn(),
+}));
+
+jest.mock('../../../data/services/EnterpriseDataApiService', () => ({
+ fetchPlotlyChartsCSV: jest.fn(),
+}));
+
+const DEFAULT_PROPS = {
+ startDate: '2021-01-01',
+ endDate: '2025-01-31',
+ chartType: 'completions-over-time',
+ activeTab: 'completions',
+ granularity: 'Daily',
+ calculation: 'Total',
+ enterpriseId: 'test_enterprise_id',
+};
+describe('DownloadCSV', () => {
+ const flushPromises = () => new Promise(setImmediate);
+
+ it('renders download csv button correctly', async () => {
+ render(
+
+
+ ,
+ );
+
+ expect(screen.getByTestId('plotly-charts-download-csv-button')).toBeInTheDocument();
+ });
+
+ it('handles successful CSV download', async () => {
+ const mockResponse = {
+ headers: {
+ 'content-disposition': 'attachment; filename="completions.csv"',
+ },
+ data: 'CSV data',
+ };
+ EnterpriseDataApiService.fetchPlotlyChartsCSV.mockResolvedValueOnce(mockResponse);
+
+ render(
+
+
+ ,
+ );
+
+ // Click the download button.
+ userEvent.click(screen.getByTestId('plotly-charts-download-csv-button'));
+ await flushPromises();
+
+ expect(saveAs).toHaveBeenCalledWith(
+ new Blob([mockResponse.data], { type: 'text/csv' }),
+ 'completions.csv',
+ );
+ });
+
+ it('handles error during CSV download', async () => {
+ const mockError = new Error('Failed to download CSV');
+ EnterpriseDataApiService.fetchPlotlyChartsCSV.mockRejectedValueOnce(mockError);
+
+ render(
+
+
+ ,
+ );
+
+ // Click the download button.
+ userEvent.click(screen.getByTestId('plotly-charts-download-csv-button'));
+ await flushPromises();
+
+ expect(logError).toHaveBeenCalledWith(mockError);
+ expect(screen.getByText('Error')).toBeInTheDocument();
+ });
+});
diff --git a/src/components/EnterpriseApp/EnterpriseAppRoutes.jsx b/src/components/EnterpriseApp/EnterpriseAppRoutes.jsx
index e1d5d56c88..12d466c89d 100644
--- a/src/components/EnterpriseApp/EnterpriseAppRoutes.jsx
+++ b/src/components/EnterpriseApp/EnterpriseAppRoutes.jsx
@@ -89,7 +89,7 @@ const EnterpriseAppRoutes = ({
}
+ element={}
/>
)}
diff --git a/src/data/services/EnterpriseDataApiService.js b/src/data/services/EnterpriseDataApiService.js
index 170254f8e3..b95fb84111 100644
--- a/src/data/services/EnterpriseDataApiService.js
+++ b/src/data/services/EnterpriseDataApiService.js
@@ -12,6 +12,8 @@ class EnterpriseDataApiService {
static enterpriseAdminBaseUrl = `${configuration.DATA_API_BASE_URL}/enterprise/api/v1/admin/`;
+ static enterpriseAdminAnalyticsV2BaseUrl = `${configuration.DATA_API_BASE_URL}/enterprise/api/v1/admin/analytics/`;
+
static getEnterpriseUUID(enterpriseId) {
const { enableDemoData } = store.getState().portalConfiguration;
return enableDemoData ? configuration.DEMO_ENTEPRISE_UUID : enterpriseId;
@@ -160,6 +162,11 @@ class EnterpriseDataApiService {
const url = `${EnterpriseDataApiService.enterpriseBaseUrl}${enterpriseUUID}/${endpoint}/?${queryParams.toString()}`;
return EnterpriseDataApiService.apiClient().get(url);
}
+
+ static fetchPlotlyChartsCSV(enterpriseId, chartUrl, options) {
+ const url = `${EnterpriseDataApiService.enterpriseAdminAnalyticsV2BaseUrl}${enterpriseId}/${chartUrl}?${new URLSearchParams(options).toString()}`;
+ return EnterpriseDataApiService.apiClient().get(url);
+ }
}
export default EnterpriseDataApiService;