Skip to content

Commit

Permalink
Merge pull request #1275 from openedx/eahmadjaved/ENT-9261
Browse files Browse the repository at this point in the history
feat: integrate csv download button on all plotly v2 charts and tables
  • Loading branch information
jajjibhai008 authored Aug 27, 2024
2 parents ac64f8c + 57afd7c commit 7388dfc
Show file tree
Hide file tree
Showing 14 changed files with 570 additions and 28 deletions.
68 changes: 51 additions & 17 deletions src/components/AdvanceAnalyticsV2/AnalyticsV2Page.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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();

Expand Down Expand Up @@ -52,6 +55,8 @@ const AnalyticsV2Page = () => {
</Form.Label>
<Form.Control
type="date"
value={startDate}
onChange={(e) => setStartDate(e.target.value)}
/>
</Form.Group>
</div>
Expand All @@ -66,10 +71,12 @@ const AnalyticsV2Page = () => {
</Form.Label>
<Form.Control
type="date"
value={endDate}
onChange={(e) => setEndDate(e.target.value)}
/>
</Form.Group>
</div>
<div className="col">
<div className="col" data-testid="granularity-select">
<Form.Group>
<Form.Label>
<FormattedMessage
Expand All @@ -83,28 +90,28 @@ const AnalyticsV2Page = () => {
value={granularity}
onChange={(e) => setGranularity(e.target.value)}
>
<option value="daily">
<option value="Daily">
{intl.formatMessage({
id: 'advance.analytics.filter.granularity.option.daily',
defaultMessage: 'Daily',
description: 'Advance analytics granularity filter daily option',
})}
</option>
<option value="weekly">
<option value="Weekly">
{intl.formatMessage({
id: 'advance.analytics.filter.granularity.option.weekly',
defaultMessage: 'Weekly',
description: 'Advance analytics granularity filter weekly option',
})}
</option>
<option value="monthly">
<option value="Monthly">
{intl.formatMessage({
id: 'advance.analytics.filter.granularity.option.monthly',
defaultMessage: 'Monthly',
description: 'Advance analytics granularity filter monthly option',
})}
</option>
<option value="quarterly">
<option value="Quarterly">
{intl.formatMessage({
id: 'advance.analytics.filter.granularity.option.quarterly',
defaultMessage: 'Quarterly',
Expand All @@ -128,28 +135,28 @@ const AnalyticsV2Page = () => {
value={calculation}
onChange={(e) => setCalculation(e.target.value)}
>
<option value="total">
<option value="Total">
{intl.formatMessage({
id: 'advance.analytics.filter.calculation.option.total',
defaultMessage: 'Total',
description: 'Advance analytics calculation filter total option',
})}
</option>
<option value="running_total">
<option value="Running Total">
{intl.formatMessage({
id: 'advance.analytics.filter.calculation.option.running.total',
defaultMessage: 'Running Total',
description: 'Advance analytics calculation filter running total option',
})}
</option>
<option value="average_3">
<option value="Moving Average (3 Period)">
{intl.formatMessage({
id: 'advance.analytics.filter.calculation.option.average.3',
defaultMessage: 'Moving Average (3 Period)',
description: 'Advance analytics calculation filter moving average 3 period option',
})}
</option>
<option value="average_7">
<option value="Moving Average (7 Period)">
{intl.formatMessage({
id: 'advance.analytics.filter.calculation.option.average.7',
defaultMessage: 'Moving Average (7 Period)',
Expand Down Expand Up @@ -187,7 +194,13 @@ const AnalyticsV2Page = () => {
description: 'Title for the enrollments tab in advance analytics.',
})}
>
<Enrollments />
<Enrollments
startDate={startDate}
endDate={endDate}
granularity={granularity}
calculation={calculation}
enterpriseId={enterpriseId}
/>
</Tab>
<Tab
eventKey="engagements"
Expand All @@ -197,7 +210,11 @@ const AnalyticsV2Page = () => {
description: 'Title for the engagements tab in advance analytics.',
})}
>
<Engagements />
<Engagements
startDate={startDate}
endDate={endDate}
enterpriseId={enterpriseId}
/>
</Tab>
<Tab
eventKey="completions"
Expand All @@ -207,7 +224,13 @@ const AnalyticsV2Page = () => {
description: 'Title for the completions tab in advance analytics.',
})}
>
<Completions />
<Completions
startDate={startDate}
endDate={endDate}
granularity={granularity}
calculation={calculation}
enterpriseId={enterpriseId}
/>
</Tab>
<Tab
eventKey="leaderboard"
Expand All @@ -217,7 +240,11 @@ const AnalyticsV2Page = () => {
description: 'Title for the leaderboard tab in advance analytics.',
})}
>
<Leaderboard />
<Leaderboard
startDate={startDate}
endDate={endDate}
enterpriseId={enterpriseId}
/>
</Tab>
<Tab
eventKey="skills"
Expand All @@ -227,7 +254,11 @@ const AnalyticsV2Page = () => {
description: 'Title for the skills tab in advance analytics.',
})}
>
<Skills />
<Skills
startDate={startDate}
endDate={endDate}
enterpriseId={enterpriseId}
/>
</Tab>
</Tabs>
</div>
Expand All @@ -236,4 +267,7 @@ const AnalyticsV2Page = () => {
);
};

AnalyticsV2Page.propTypes = {
enterpriseId: PropTypes.string.isRequired,
};
export default AnalyticsV2Page;
119 changes: 119 additions & 0 deletions src/components/AdvanceAnalyticsV2/DownloadCSV.jsx
Original file line number Diff line number Diff line change
@@ -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 (
<div className="d-flex justify-content-end">
{ isOpen
&& (
<Toast onClose={close} show={isOpen}>
{toastText}
</Toast>
)}
<StatefulButton
state={buttonState}
variant={buttonState === 'error' ? 'danger' : 'primary'}
data-testid="plotly-charts-download-csv-button"
labels={{
default: intl.formatMessage({
id: 'adminPortal.LPRV2.downloadCSV.button.default',
defaultMessage: 'Download CSV',
description: 'Label for the download button in the module activity report page.',
}),
pending: intl.formatMessage({
id: 'adminPortal.LPRV2.downloadCSV.button.pending',
defaultMessage: 'Downloading CSV',
description: 'Label for the download button in the module activity report page when the download is in progress.',
}),
complete: intl.formatMessage({
id: 'adminPortal.LPRV2.downloadCSV.button.complete',
defaultMessage: 'CSV Downloaded',
description: 'Label for the download button in the module activity report page when the download is complete.',
}),
error: intl.formatMessage({
id: 'adminPortal.LPRV2.downloadCSV.button.error',
defaultMessage: 'Error',
description: 'Label for the download button in the module activity report page when the download fails.',
}),
}}
icons={{
default: <Icon src={Download} />,
pending: <Spinner animation="border" variant="light" size="sm" />,
complete: <Icon src={Check} />,
error: <Icon src={Close} variant="light" size="sm" />,
}}
disabledStates={['pending']}
onClick={downloadCsv}
/>
</div>
);
};

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;
38 changes: 34 additions & 4 deletions src/components/AdvanceAnalyticsV2/Header.jsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,50 @@
import React from 'react';
import PropTypes from 'prop-types';
import DownloadCSV from './DownloadCSV';

const Header = ({ title, subtitle }) => (
<div className="analytics-header">
<h2 className="analytics-header-title">{title}</h2>
{subtitle && <p className="analytics-header-subtitle">{subtitle}</p>}
const Header = ({
title, subtitle, startDate, endDate, isDownloadCSV, activeTab, granularity, calculation, chartType, enterpriseId,
}) => (
<div className="analytics-header d-flex justify-content-between row">
<div className="col-8">
<h2 className="analytics-header-title">{title}</h2>
{subtitle && <p className="analytics-header-subtitle">{subtitle}</p>}
</div>
{isDownloadCSV && (
<div className="col-3 mr-0">
<DownloadCSV
enterpriseId={enterpriseId}
startDate={startDate}
endDate={endDate}
activeTab={activeTab}
granularity={granularity}
calculation={calculation}
chartType={chartType}
/>
</div>
)}
</div>
);

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;
Loading

0 comments on commit 7388dfc

Please sign in to comment.