diff --git a/src/components/AdvanceAnalyticsV2/AnalyticsV2Page.jsx b/src/components/AdvanceAnalyticsV2/AnalyticsV2Page.jsx index f2db473b9e..6040d706e1 100644 --- a/src/components/AdvanceAnalyticsV2/AnalyticsV2Page.jsx +++ b/src/components/AdvanceAnalyticsV2/AnalyticsV2Page.jsx @@ -3,6 +3,7 @@ import { Form, Tabs, Tab, } from '@openedx/paragon'; import { Helmet } from 'react-helmet'; +import PropTypes from 'prop-types'; import { FormattedMessage, useIntl } from '@edx/frontend-platform/i18n'; import Hero from '../Hero'; @@ -15,10 +16,12 @@ import Skills from './tabs/Skills'; const PAGE_TITLE = 'AnalyticsV2'; -const AnalyticsV2Page = () => { +const AnalyticsV2Page = ({ enterpriseId }) => { const [activeTab, setActiveTab] = useState('enrollments'); - const [granularity, setGranularity] = useState('daily'); - const [calculation, setCalculation] = useState('total'); + const [granularity, setGranularity] = useState('Daily'); + const [calculation, setCalculation] = useState('Total'); + const [startDate, setStartDate] = useState(''); + const [endDate, setEndDate] = useState(''); const dataRefreshDate = ''; const intl = useIntl(); @@ -52,6 +55,8 @@ const AnalyticsV2Page = () => { setStartDate(e.target.value)} /> @@ -66,10 +71,12 @@ const AnalyticsV2Page = () => { setEndDate(e.target.value)} /> -
+
{ value={granularity} onChange={(e) => setGranularity(e.target.value)} > - - - - - - -
@@ -236,4 +267,7 @@ const AnalyticsV2Page = () => { ); }; +AnalyticsV2Page.propTypes = { + enterpriseId: PropTypes.string.isRequired, +}; export default AnalyticsV2Page; diff --git a/src/components/AdvanceAnalyticsV2/DownloadCSV.jsx b/src/components/AdvanceAnalyticsV2/DownloadCSV.jsx new file mode 100644 index 0000000000..38cd389263 --- /dev/null +++ b/src/components/AdvanceAnalyticsV2/DownloadCSV.jsx @@ -0,0 +1,119 @@ +import React, { useState } from 'react'; +import { saveAs } from 'file-saver'; +import { useIntl } from '@edx/frontend-platform/i18n'; +import PropTypes from 'prop-types'; +import { logError } from '@edx/frontend-platform/logging'; +import { + Toast, StatefulButton, Icon, Spinner, useToggle, +} from '@openedx/paragon'; +import { Download, Check, Close } from '@openedx/paragon/icons'; +import EnterpriseDataApiService from '../../data/services/EnterpriseDataApiService'; +import simulateURL from './data/utils'; + +const DownloadCSV = ({ + startDate, endDate, chartType, activeTab, granularity, calculation, enterpriseId, +}) => { + const [buttonState, setButtonState] = useState('default'); + const [isOpen, open, close] = useToggle(false); + const intl = useIntl(); + + const getFileName = (contentDisposition) => { + let filename = `${activeTab} from (${startDate}-${endDate}).csv`; // Default filename + + // Extract the filename from the content-disposition header if it exists + if (contentDisposition && contentDisposition.indexOf('attachment') !== -1) { + const matches = /filename="([^"]+)"/.exec(contentDisposition); + if (matches != null && matches[1]) { + [, filename] = matches; + } + } + return filename; + }; + + const downloadCsv = () => { + setButtonState('pending'); + const chartUrl = simulateURL(activeTab, chartType); + EnterpriseDataApiService.fetchPlotlyChartsCSV(enterpriseId, chartUrl, { + start_date: startDate, + end_date: endDate, + granularity, + calculation, + chart_type: chartType, + response_type: 'csv', + }).then((response) => { + const contentDisposition = response.headers['content-disposition']; + const filename = getFileName(contentDisposition); + + const blob = new Blob([response.data], { type: 'text/csv' }); + saveAs(blob, filename); + open(); + setButtonState('complete'); + }).catch((error) => { + setButtonState('error'); + logError(error); + }); + }; + + const toastText = intl.formatMessage({ + id: 'adminPortal.LPRV2.downloadCSV.toast', + defaultMessage: 'CSV Downloaded', + description: 'Toast message for the download button in the LPR V2 page.', + }); + return ( +
+ { isOpen + && ( + + {toastText} + + )} + , + pending: , + complete: , + error: , + }} + disabledStates={['pending']} + onClick={downloadCsv} + /> +
+ ); +}; + +DownloadCSV.propTypes = { + startDate: PropTypes.string.isRequired, + endDate: PropTypes.string.isRequired, + chartType: PropTypes.string.isRequired, + activeTab: PropTypes.string.isRequired, + granularity: PropTypes.string.isRequired, + calculation: PropTypes.string.isRequired, + enterpriseId: PropTypes.string.isRequired, +}; + +export default DownloadCSV; diff --git a/src/components/AdvanceAnalyticsV2/Header.jsx b/src/components/AdvanceAnalyticsV2/Header.jsx index 5436e24245..c9f5a13264 100644 --- a/src/components/AdvanceAnalyticsV2/Header.jsx +++ b/src/components/AdvanceAnalyticsV2/Header.jsx @@ -1,20 +1,50 @@ import React from 'react'; import PropTypes from 'prop-types'; +import DownloadCSV from './DownloadCSV'; -const Header = ({ title, subtitle }) => ( -
-

{title}

- {subtitle &&

{subtitle}

} +const Header = ({ + title, subtitle, startDate, endDate, isDownloadCSV, activeTab, granularity, calculation, chartType, enterpriseId, +}) => ( +
+
+

{title}

+ {subtitle &&

{subtitle}

} +
+ {isDownloadCSV && ( +
+ +
+ )}
); Header.defaultProps = { subtitle: undefined, + isDownloadCSV: false, + granularity: 'Daily', + calculation: 'Total', + chartType: '', }; Header.propTypes = { title: PropTypes.string.isRequired, subtitle: PropTypes.string, + isDownloadCSV: PropTypes.bool, + startDate: PropTypes.string.isRequired, + endDate: PropTypes.string.isRequired, + activeTab: PropTypes.string.isRequired, + enterpriseId: PropTypes.string.isRequired, + chartType: PropTypes.string, + granularity: PropTypes.string, + calculation: PropTypes.string, }; export default Header; diff --git a/src/components/AdvanceAnalyticsV2/data/constants.js b/src/components/AdvanceAnalyticsV2/data/constants.js new file mode 100644 index 0000000000..1e591db955 --- /dev/null +++ b/src/components/AdvanceAnalyticsV2/data/constants.js @@ -0,0 +1,35 @@ +export const advancedAnalyticsV2QueryKeys = { + chartCSV: (enterpriseId, chartName) => [...advancedAnalyticsV2QueryKeys.all, 'chartCSV', enterpriseId, chartName], +}; + +export const CHART_TYPES = { + // Charts types for Skills tab + BUBBLE: 'bubble', + TOP_SKILLS_ENROLLMENT: 'top_skills_enrollment', + TOP_SKILLS_COMPLETION: 'top_skills_completion', + + // Charts types for Completions tab + COMPLETIONS_OVER_TIME: 'completions_over_time', + TOP_COURSES_BY_COMPLETIONS: 'top_courses_by_completions', + TOP_SUBJECTS_BY_COMPLETIONS: 'top_subjects_by_completions', + + // Charts types for Enrollments tab + ENROLLMENTS_OVER_TIME: 'enrollments_over_time', + TOP_COURSES_BY_ENROLLMENTS: 'top_courses_by_enrollments', + TOP_SUBJECTS_BY_ENROLLMENTS: 'top_subjects_by_enrollments', + INDIVIDUAL_ENROLLMENTS: 'individual_enrollments', + + // Charts types for Engagements tab + ENGAGEMENTS_OVER_TIME: 'engagements_over_time', + TOP_COURSES_BY_ENGAGEMENTS: 'top_courses_by_engagements', + TOP_SUBJECTS_BY_ENGAGEMENTS: 'top_subjects_by_engagements', + INDIVIDUAL_ENGAGEMENTS: 'individual_engagements', +}; + +export const ANALYTICS_TABS = { + SKILLS: 'skills', + COMPLETIONS: 'completions', + ENROLLMENTS: 'enrollments', + LEADERBOARD: 'leaderboard', + ENGAGEMENTS: 'engagements', +}; diff --git a/src/components/AdvanceAnalyticsV2/data/utils.js b/src/components/AdvanceAnalyticsV2/data/utils.js new file mode 100644 index 0000000000..d7bfb0250d --- /dev/null +++ b/src/components/AdvanceAnalyticsV2/data/utils.js @@ -0,0 +1,10 @@ +import { CHART_TYPES } from './constants'; + +const simulateURL = (activeTab, chartType) => { + if (!Object.values(CHART_TYPES).includes(chartType)) { + return activeTab; + } + return `${activeTab}/stats`; +}; + +export default simulateURL; diff --git a/src/components/AdvanceAnalyticsV2/tabs/Completions.jsx b/src/components/AdvanceAnalyticsV2/tabs/Completions.jsx index d672e0e103..4d68d74a58 100644 --- a/src/components/AdvanceAnalyticsV2/tabs/Completions.jsx +++ b/src/components/AdvanceAnalyticsV2/tabs/Completions.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 Completions = () => { +const Completions = ({ + startDate, endDate, granularity, calculation, enterpriseId, +}) => { const intl = useIntl(); return ( @@ -20,6 +24,14 @@ const Completions = () => { defaultMessage: 'See the course completions that result in a passing grade over time.', description: 'Subtitle for the completions over time chart.', })} + startDate={startDate} + endDate={endDate} + activeTab={ANALYTICS_TABS.COMPLETIONS} + granularity={granularity} + calculation={calculation} + chartType={CHART_TYPES.COMPLETIONS_OVER_TIME} + enterpriseId={enterpriseId} + isDownloadCSV />
@@ -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;