diff --git a/src/components/bulk-email-tool/bulk-email-task-manager/BulkEmailContentHistory.jsx b/src/components/bulk-email-tool/bulk-email-task-manager/BulkEmailContentHistory.jsx index 6a70151..243f1eb 100644 --- a/src/components/bulk-email-tool/bulk-email-task-manager/BulkEmailContentHistory.jsx +++ b/src/components/bulk-email-tool/bulk-email-task-manager/BulkEmailContentHistory.jsx @@ -4,25 +4,35 @@ import { useParams } from 'react-router-dom'; import { injectIntl, intlShape } from '@edx/frontend-platform/i18n'; import { - Alert, Button, DataTable, Modal, + Button, Icon, Modal, StatefulButton, } from '@edx/paragon'; +import { SpinnerSimple } from '@edx/paragon/icons'; import messages from './messages'; import { getSentEmailHistory } from './api'; +import BulkEmailTaskManagerTable from './BulkEmailHistoryTable'; export function BulkEmailContentHistory({ intl }) { const { courseId } = useParams(); + const BUTTON_STATE = { + DEFAULT: 'default', + PENDING: 'pending', + COMPLETE: 'complete', + }; const [emailHistoryData, setEmailHistoryData] = useState(); const [errorRetrievingData, setErrorRetrievingData] = useState(false); const [showHistoricalEmailContentTable, setShowHistoricalEmailContentTable] = useState(false); const [isMessageModalOpen, setIsMessageModalOpen] = useState(false); const [messageContent, setMessageContent] = useState(); + const [buttonState, setButtonState] = useState(BUTTON_STATE.DEFAULT); /** * Async function that makes a REST API call to retrieve historical email message data sent by the bulk course email * tool from edx-platform. */ async function fetchSentEmailHistoryData() { + setButtonState(BUTTON_STATE.PENDING); + let data = null; try { data = await getSentEmailHistory(courseId); @@ -35,6 +45,24 @@ export function BulkEmailContentHistory({ intl }) { setEmailHistoryData(emails); setShowHistoricalEmailContentTable(true); } + + setButtonState(BUTTON_STATE.COMPLETE); + } + + /** + * This utility function transforms the data stored in `emailHistoryData` to make it easier to display in the Paragon + * DataTable component. Some of the information we want displayed is in an inner object so we extract it and move it + * up a level (the `subject` field). We also convert the `sent_to` data to be a String rather than an array to fix a + * display bug in the table. + */ + function transformDataForTable() { + const tableData = emailHistoryData.map((item) => ({ + ...item, + subject: item.email.subject, + sent_to: item.sent_to.join(', '), + })); + + return tableData; } /** @@ -47,34 +75,6 @@ export function BulkEmailContentHistory({ intl }) { setIsMessageModalOpen(true); }; - /** - * Render function for the email content history table. If an error occurs while attempting to fetch data from - * edx-platform we will render this error instead of the table. - */ - const renderError = () => ( -
- -

- {intl.formatMessage(messages.errorFetchingData)} -

-
-
- ); - - /** - * Render function for the email content history table. If there is no data to display in our table we will render - * this informative message instead. - */ - const renderEmpty = () => ( -
- -

- {intl.formatMessage(messages.noEmailData)} -

-
-
- ); - /** * Renders a modal that will display the contents of a single historical email message sent via the bulk course email * tool to a user. @@ -132,83 +132,52 @@ export function BulkEmailContentHistory({ intl }) { ); + const tableColumns = [ + { + Header: `${intl.formatMessage(messages.emailHistoryTableColumnHeaderSubject)}`, + accessor: 'subject', + }, + { + Header: `${intl.formatMessage(messages.emailHistoryTableColumnHeaderAuthor)}`, + accessor: 'requester', + }, + { + Header: `${intl.formatMessage(messages.emailHistoryTableColumnHeaderRecipients)}`, + accessor: 'sent_to', + }, + { + Header: `${intl.formatMessage(messages.emailHistoryTableColumnHeaderTimeSent)}`, + accessor: 'created', + }, + { + Header: `${intl.formatMessage(messages.emailHistoryTableColumnHeaderNumberSent)}`, + accessor: 'number_sent', + }, + ]; + /** - * Render function for the email content history table. This function is responsible for displaying data inside of - * the table when the `Show Sent Email History` button is pressed on the page. + * Paragon's DataTable supports the ability to add extra columns that might not directly coincide with the data being + * represented in the table. We are using an additional column to embed a button that will open a Modal to display the + * contents of a previously sent message. */ - const renderTable = () => { - // Do a little data manipulation to make it easier to display what we want in the table. Pull the email subject out - // of the email data. Transforms the `sent_to` array to a string for easier display in our table. - const tableData = emailHistoryData.map((item) => ({ - ...item, - subject: item.email.subject, - sent_to: item.sent_to.join(', '), - })); + const additionalColumns = () => { + const tableData = transformDataForTable(); return ( -
-

- {intl.formatMessage(messages.emailHistoryTableViewMessageInstructions)} -

- ( - - ), - }, - ]} - /> -
+ [ + { + id: 'view_message', + Header: `${intl.formatMessage(messages.emailHistoryTableColumnHeaderViewMessage)}`, + Cell: ({ row }) => ( + + ), + }, + ] ); }; - /** - * Today there can be three states which the renderTableData function will handle: - * 1. There was an error retrieving data from edx-platform and we can't display anything (for now). - * 2. There is no email history for this course-run and we have nothing to display to the end user. - * 3. We were able to receive historical email content and it will be presented in a table. - */ - const renderTableData = () => { - if (errorRetrievingData) { - return renderError(); - } - - if (!emailHistoryData.length) { - return renderEmpty(); - } - - return renderTable(); - }; - return (
@@ -218,10 +187,35 @@ export function BulkEmailContentHistory({ intl }) {

{intl.formatMessage(messages.emailHistoryTableSectionButtonHeader)}

- - {showHistoricalEmailContentTable && renderTableData()} + + { showHistoricalEmailContentTable && ( + + )}
); diff --git a/src/components/bulk-email-tool/bulk-email-task-manager/BulkEmailHistoryTable.jsx b/src/components/bulk-email-tool/bulk-email-task-manager/BulkEmailHistoryTable.jsx new file mode 100644 index 0000000..7f7e8e4 --- /dev/null +++ b/src/components/bulk-email-tool/bulk-email-task-manager/BulkEmailHistoryTable.jsx @@ -0,0 +1,86 @@ +import { Alert, DataTable } from '@edx/paragon'; +import PropTypes from 'prop-types'; +import React from 'react'; + +export default function BulkEmailTaskManagerTable(props) { + const { + errorRetrievingData, + tableData, + tableDescription, + alertWarningMessage, + alertErrorMessage, + columns, + additionalColumns, + } = props; + + /** + * Sub-render function that creates an Alert component with a specific type and message for display to a user. + */ + const renderAlert = (alertType, alertMessage) => ( +
+ +

+ {`${alertMessage}`} +

+
+
+ ); + + /** + * Responsible for rendering the tables used by the BulkEmailContentHistory, BulkEmailTaskManager, and + * BulkEmailTaskHistory components. Conditionally renders a table description as well. + */ + const renderTable = () => ( +
+ {tableDescription && ( +

+ {tableDescription} +

+ )} + +
+ ); + + /** + * Sub-render function that determines if we can render the DataTable. If not, we will render an Alert component to + * inform the user why the data/table cannot be displayed. + */ + const canRenderTable = () => { + if (errorRetrievingData) { + return renderAlert('danger', alertErrorMessage); + } + + if (!tableData.length) { + return renderAlert('warning', alertWarningMessage); + } + + return renderTable(); + }; + + return ( +
+ {canRenderTable()} +
+ ); +} + +BulkEmailTaskManagerTable.propTypes = { + errorRetrievingData: PropTypes.bool.isRequired, + tableData: PropTypes.arrayOf(PropTypes.object), + tableDescription: PropTypes.string, + alertWarningMessage: PropTypes.string.isRequired, + alertErrorMessage: PropTypes.string.isRequired, + columns: PropTypes.arrayOf(PropTypes.object).isRequired, + additionalColumns: PropTypes.arrayOf(PropTypes.object), +}; + +BulkEmailTaskManagerTable.defaultProps = { + tableData: [], + tableDescription: '', + additionalColumns: [], +}; diff --git a/src/components/bulk-email-tool/bulk-email-task-manager/BulkEmailPendingTasks.jsx b/src/components/bulk-email-tool/bulk-email-task-manager/BulkEmailPendingTasks.jsx index e29bc82..0557e37 100644 --- a/src/components/bulk-email-tool/bulk-email-task-manager/BulkEmailPendingTasks.jsx +++ b/src/components/bulk-email-tool/bulk-email-task-manager/BulkEmailPendingTasks.jsx @@ -1,32 +1,91 @@ -import React, { useState, useEffect } from 'react'; +import React, { useState } from 'react'; import { useParams } from 'react-router-dom'; -import { FormattedMessage } from '@edx/frontend-platform/i18n'; +import { injectIntl, intlShape } from '@edx/frontend-platform/i18n'; import { getInstructorTasks } from './api'; +import messages from './messages'; +import useInterval from '../../../utils/useInterval'; +import BulkEmailTaskManagerTable from './BulkEmailHistoryTable'; -export default function BulkEmailPendingTasks() { +export function BulkEmailPendingTasks({ intl }) { const { courseId } = useParams(); - const [instructorTaskData, setInstructorTaskData] = useState(); // eslint-disable-line no-unused-vars + const [instructorTaskData, setInstructorTaskData] = useState(); + const [errorRetrievingData, setErrorRetrievingData] = useState(false); - useEffect(() => { + /** + * We use a custom hook (`useInterval`) here to setup a timer that will refresh the pending instructor task data + * displayed in the table of this component. + */ + useInterval(() => { async function fetchPendingInstructorTasksData() { const data = await getInstructorTasks(courseId); const { tasks } = data; setInstructorTaskData(tasks); } - fetchPendingInstructorTasksData(); - }, []); + + try { + fetchPendingInstructorTasksData(); + } catch (error) { + setErrorRetrievingData(true); + } + }, 30000); + + const tableColumns = [ + { + Header: `${intl.formatMessage(messages.taskHistoryTableColumnHeaderTaskType)}`, + accessor: 'task_type', + }, + { + Header: `${intl.formatMessage(messages.taskHistoryTableColumnHeaderTaskInputs)}`, + accessor: 'task_input', + }, + { + Header: `${intl.formatMessage(messages.taskHistoryTableColumnHeaderTaskId)}`, + accessor: 'task_id', + }, + { + Header: `${intl.formatMessage(messages.taskHistoryTableColumnHeaderTaskRequester)}`, + accessor: 'requester', + }, + { + Header: `${intl.formatMessage(messages.taskHistoryTableColumnHeaderTaskSubmittedDate)}`, + accessor: 'created', + }, + { + Header: `${intl.formatMessage(messages.taskHistoryTableColumnHeaderTaskDuration)}`, + accessor: 'duration_sec', + }, + { + Header: `${intl.formatMessage(messages.taskHistoryTableColumnHeaderTaskState)}`, + accessor: 'task_state', + }, + { + Header: `${intl.formatMessage(messages.taskHistoryTableColumnHeaderTaskStatus)}`, + accessor: 'status', + }, + { + Header: `${intl.formatMessage(messages.taskHistoryTableColumnHeaderTaskProgress)}`, + accessor: 'task_message', + }, + ]; return ( -
-

- -

+
+
); } + +BulkEmailPendingTasks.propTypes = { + intl: intlShape.isRequired, +}; + +export default injectIntl(BulkEmailPendingTasks); diff --git a/src/components/bulk-email-tool/bulk-email-task-manager/BulkEmailTaskHistory.jsx b/src/components/bulk-email-tool/bulk-email-task-manager/BulkEmailTaskHistory.jsx index c139495..750dc13 100644 --- a/src/components/bulk-email-tool/bulk-email-task-manager/BulkEmailTaskHistory.jsx +++ b/src/components/bulk-email-tool/bulk-email-task-manager/BulkEmailTaskHistory.jsx @@ -1,41 +1,129 @@ -import React, { useState, useEffect } from 'react'; +import React, { useState } from 'react'; import { useParams } from 'react-router-dom'; +import { injectIntl, intlShape } from '@edx/frontend-platform/i18n'; -import { FormattedMessage } from '@edx/frontend-platform/i18n'; +import { Icon, StatefulButton } from '@edx/paragon'; +import { SpinnerSimple } from '@edx/paragon/icons'; import { getEmailTaskHistory } from './api'; +import messages from './messages'; -export default function BulkEmailTaskHistory() { +import BulkEmailTaskManagerTable from './BulkEmailHistoryTable'; + +export function BulkEmailTaskHistory({ intl }) { const { courseId } = useParams(); + const BUTTON_STATE = { + DEFAULT: 'default', + PENDING: 'pending', + COMPLETE: 'complete', + }; - const [emailTaskHistoryData, setEmailTaskHistoryData] = useState(); // eslint-disable-line no-unused-vars + const [emailTaskHistoryData, setEmailTaskHistoryData] = useState(); + const [showHistoricalTaskContentTable, setShowHistoricalTaskContentTable] = useState(false); + const [errorRetrievingData, setErrorRetrievingData] = useState(false); + const [buttonState, setButtonState] = useState(BUTTON_STATE.DEFAULT); - useEffect(() => { - async function fetchEmailTaskHistoryData() { - const data = await getEmailTaskHistory(courseId); + /** + * Async function that makes a REST API call to retrieve historical bulk email (Instructor) task data for display + * within this component. + */ + async function fetchEmailTaskHistoryData() { + setButtonState(BUTTON_STATE.PENDING); + + let data = null; + try { + data = await getEmailTaskHistory(courseId); + } catch (error) { + setErrorRetrievingData(true); + } + + if (data) { const { tasks } = data; setEmailTaskHistoryData(tasks); + setShowHistoricalTaskContentTable(true); } - fetchEmailTaskHistoryData(); - }, []); + + setButtonState(BUTTON_STATE.COMPLETE); + } + + const tableColumns = [ + { + Header: `${intl.formatMessage(messages.taskHistoryTableColumnHeaderTaskType)}`, + accessor: 'task_type', + }, + { + Header: `${intl.formatMessage(messages.taskHistoryTableColumnHeaderTaskInputs)}`, + accessor: 'task_input', + }, + { + Header: `${intl.formatMessage(messages.taskHistoryTableColumnHeaderTaskId)}`, + accessor: 'task_id', + }, + { + Header: `${intl.formatMessage(messages.taskHistoryTableColumnHeaderTaskRequester)}`, + accessor: 'requester', + }, + { + Header: `${intl.formatMessage(messages.taskHistoryTableColumnHeaderTaskSubmittedDate)}`, + accessor: 'created', + }, + { + Header: `${intl.formatMessage(messages.taskHistoryTableColumnHeaderTaskDuration)}`, + accessor: 'duration_sec', + }, + { + Header: `${intl.formatMessage(messages.taskHistoryTableColumnHeaderTaskState)}`, + accessor: 'task_state', + }, + { + Header: `${intl.formatMessage(messages.taskHistoryTableColumnHeaderTaskStatus)}`, + accessor: 'status', + }, + { + Header: `${intl.formatMessage(messages.taskHistoryTableColumnHeaderTaskProgress)}`, + accessor: 'task_message', + }, + ]; return (

- + {intl.formatMessage(messages.emailTaskHistoryTableSectionButtonHeader)}

- + )}
); } + +BulkEmailTaskHistory.propTypes = { + intl: intlShape.isRequired, +}; + +export default injectIntl(BulkEmailTaskHistory); diff --git a/src/components/bulk-email-tool/bulk-email-task-manager/BulkEmailTaskManager.jsx b/src/components/bulk-email-tool/bulk-email-task-manager/BulkEmailTaskManager.jsx index 535c7f6..10f7e4b 100644 --- a/src/components/bulk-email-tool/bulk-email-task-manager/BulkEmailTaskManager.jsx +++ b/src/components/bulk-email-tool/bulk-email-task-manager/BulkEmailTaskManager.jsx @@ -1,30 +1,23 @@ import React from 'react'; -import { FormattedMessage } from '@edx/frontend-platform/i18n'; +import { injectIntl, intlShape } from '@edx/frontend-platform/i18n'; import BulkEmailContentHistory from './BulkEmailContentHistory'; import BulkEmailPendingTasks from './BulkEmailPendingTasks'; import BulkEmailTaskHistory from './BulkEmailTaskHistory'; +import messages from './messages'; -export default function BulkEmailTaskManager() { +export function BulkEmailTaskManager({ intl }) { return (

- + {intl.formatMessage(messages.pendingTasksHeader)}

- + {intl.formatMessage(messages.emailTaskHistoryHeader)}

@@ -34,3 +27,9 @@ export default function BulkEmailTaskManager() {
); } + +BulkEmailTaskManager.propTypes = { + intl: intlShape.isRequired, +}; + +export default injectIntl(BulkEmailTaskManager); diff --git a/src/components/bulk-email-tool/bulk-email-task-manager/messages.js b/src/components/bulk-email-tool/bulk-email-task-manager/messages.js index 684bcf8..146620b 100644 --- a/src/components/bulk-email-tool/bulk-email-task-manager/messages.js +++ b/src/components/bulk-email-tool/bulk-email-task-manager/messages.js @@ -2,13 +2,13 @@ import { defineMessages } from '@edx/frontend-platform/i18n'; const messages = defineMessages({ /* BulkEmailContentHistory.jsx Messages */ - errorFetchingData: { + errorFetchingEmailHistoryData: { id: 'bulk.email.content.history.table.alert.errorFetchingData', defaultMessage: 'An error occurred retrieving email history data for this course. Please try again later.', }, noEmailData: { id: 'bulk.email.content.history.table.alert.noEmailData', - defaultMessage: 'There is no email history for this course', + defaultMessage: 'There is no email history for this course.', }, buttonViewMessage: { id: 'bulk.email.content.history.table.button.viewMessage', @@ -71,8 +71,81 @@ const messages = defineMessages({ defaultMessage: 'Show Sent Email History', }, /* BulkEmailTaskManager.jsx messages */ + pendingTasksHeader: { + id: 'bulk.email.pending.tasks.header', + defaultMessage: 'Pending Tasks', + }, + emailTaskHistoryHeader: { + id: 'bulk.email.email.task.history.header', + defaultMessage: 'Email Task History', + }, /* BulkEmailPendingTasks.jsx messages */ + pendingTaskSectionInfo: { + id: 'bulk.email.pending.tasks.section.info', + defaultMessage: 'Email actions run in the background. The status for any active tasks - including email tasks - appears in the table below.', + }, + errorFetchingPendingTaskData: { + id: 'bulk.email.pending.tasks.table.alert.errorFetchingData', + defaultMessage: 'Error fetching Instructor Task data. This request will be retried automatically.', + }, + noPendingTaskData: { + id: 'bulk.email.pending.tasks.table.alert.noTaskData', + defaultMessage: 'No tasks currently running.', + }, /* BulkEmailTaskHistory.jsx messages */ + emailTaskHistoryTableSectionButtonHeader: { + id: 'bulk.email.task.history.table.button.header', + defaultMessage: 'To see the status for all email tasks submitted for this course, click this button:', + }, + emailTaskHistoryTableSectionButton: { + id: 'bulk.email.task.history.table.button', + defaultMessage: 'Show Email Task History', + }, + errorFetchingTaskHistoryData: { + id: 'bulk.email.task.history.table.alert.errorFetchingData', + defaultMessage: 'Error fetching email task history data for this course. Please try again later.', + }, + noTaskHistoryData: { + id: 'bulk.email.task.history.table.alert.noTaskData', + defaultMessage: 'There is no email task history for this course.', + }, + /* Common Messages */ + taskHistoryTableColumnHeaderTaskType: { + id: 'bulk.email.task.history.table.column.header.taskType', + defaultMessage: 'Task Type', + }, + taskHistoryTableColumnHeaderTaskInputs: { + id: 'bulk.email.task.history.table.column.header.taskInputs', + defaultMessage: 'Task Inputs', + }, + taskHistoryTableColumnHeaderTaskId: { + id: 'bulk.email.task.history.table.column.header.taskId', + defaultMessage: 'Task Id', + }, + taskHistoryTableColumnHeaderTaskRequester: { + id: 'bulk.email.task.history.table.column.header.taskRequester', + defaultMessage: 'Requester', + }, + taskHistoryTableColumnHeaderTaskSubmittedDate: { + id: 'bulk.email.task.history.table.column.header.taskSubmittedDate', + defaultMessage: 'Submitted', + }, + taskHistoryTableColumnHeaderTaskDuration: { + id: 'bulk.email.task.history.table.column.header.taskDuration', + defaultMessage: 'Duration (seconds)', + }, + taskHistoryTableColumnHeaderTaskState: { + id: 'bulk.email.task.history.table.column.header.taskState', + defaultMessage: 'State', + }, + taskHistoryTableColumnHeaderTaskStatus: { + id: 'bulk.email.task.history.table.column.header.taskStatus', + defaultMessage: 'Status', + }, + taskHistoryTableColumnHeaderTaskProgress: { + id: 'bulk.email.task.history.table.column.header.taskProgress', + defaultMessage: 'Task Progress', + }, }); export default messages; diff --git a/src/utils/useInterval.js b/src/utils/useInterval.js new file mode 100644 index 0000000..ea9e96e --- /dev/null +++ b/src/utils/useInterval.js @@ -0,0 +1,28 @@ +import { useEffect, useRef } from 'react'; + +/** + * A custom hook used by the BulkEmailPendingTasks component to periodically make an API call on a regular interval. + * This is lifted from: https://overreacted.io/making-setinterval-declarative-with-react-hooks/. + */ +export default function useInterval(callback, delay) { + const savedCallback = useRef(); + + // Remember the latest callback + useEffect(() => { + savedCallback.current = callback; + }, [callback]); + + // Set up the interval + useEffect(() => { + function tick() { + savedCallback.current(); + } + + if (delay !== null) { + const id = setInterval(tick, delay); + return () => clearInterval(id); + } + + return null; + }, [delay]); +}