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 csv download button on all plotly v2 charts and tables #1275

Merged
merged 1 commit into from
Aug 27, 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
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();

jajjibhai008 marked this conversation as resolved.
Show resolved Hide resolved
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
Loading