From ae7e2502a9aebf837121c06148eda212402d899b Mon Sep 17 00:00:00 2001 From: CD Cabrera Date: Sat, 22 Jun 2024 17:13:24 -0400 Subject: [PATCH] refactor(toolbarFieldExport): sw-2353 move notifications to hooks --- .env | 6 +- .env.test | 2 + public/locales/en-US.json | 27 +- src/common/README.md | 45 +++ .../__snapshots__/helpers.test.js.snap | 9 + src/common/helpers.js | 24 ++ src/components/README.md | 217 ++++++++--- .../__tests__/__snapshots__/i18n.test.js.snap | 161 +++++++++ .../productViewContext.test.js.snap | 8 + .../__tests__/productViewContext.test.js | 6 + .../productView/productViewContext.js | 29 ++ .../toolbarFieldExport.test.js.snap | 98 +---- .../toolbarFieldExportContext.test.js.snap | 187 ++++++++++ .../toolbarFieldSelectCategory.test.js.snap | 8 + .../__tests__/toolbarFieldExport.test.js | 167 +-------- .../toolbarFieldExportContext.test.js | 143 ++++++++ src/components/toolbar/toolbarFieldExport.js | 192 +++------- .../toolbar/toolbarFieldExportContext.js | 336 ++++++++++++++++++ .../toolbar/toolbarFieldSelectCategory.js | 10 + src/redux/README.md | 51 +-- .../platformActions.test.js.snap | 43 ++- .../actions/__tests__/platformActions.test.js | 42 +-- src/redux/actions/platformActions.js | 103 +++--- .../__snapshots__/appReducer.test.js.snap | 58 ++- .../reducers/__tests__/appReducer.test.js | 27 +- src/redux/reducers/appReducer.js | 20 +- .../__snapshots__/index.test.js.snap | 20 +- src/redux/types/index.js | 14 +- src/services/README.md | 77 +++- .../platformConstants.test.js.snap | 48 ++- .../platformServices.test.js.snap | 13 + .../platformTransformers.test.js.snap | 33 +- .../__tests__/platformServices.test.js | 27 +- .../__tests__/platformTransformers.test.js | 24 +- src/services/platform/platformConstants.js | 24 +- src/services/platform/platformServices.js | 299 ++++++++++------ src/services/platform/platformTransformers.js | 50 ++- src/styles/_toolbar.scss | 4 + src/styles/index.scss | 2 +- 39 files changed, 1929 insertions(+), 725 deletions(-) create mode 100644 src/components/toolbar/__tests__/__snapshots__/toolbarFieldExportContext.test.js.snap create mode 100644 src/components/toolbar/__tests__/toolbarFieldExportContext.test.js create mode 100644 src/components/toolbar/toolbarFieldExportContext.js diff --git a/.env b/.env index f1e89d173..989fbc3c7 100644 --- a/.env +++ b/.env @@ -26,9 +26,13 @@ REACT_APP_UI_WINDOW_ID=curiosity REACT_APP_AJAX_TIMEOUT=60000 REACT_APP_AJAX_CACHE=15000 -REACT_APP_AJAX_POLL_INTERVAL=15000 +REACT_APP_AJAX_POLL_INTERVAL=5000 REACT_APP_SELECTOR_CACHE=120000 +REACT_APP_CONFIG_EXPORT_EXPIRE=86400000 +REACT_APP_CONFIG_EXPORT_FILENAME=swatch_report_{0} +REACT_APP_CONFIG_EXPORT_SERVICE_NAME_PREFIX=swatch + REACT_APP_CONFIG_SERVICE_LOCALES_COOKIE=rh_locale REACT_APP_CONFIG_SERVICE_LOCALES_DEFAULT_LNG=en-US REACT_APP_CONFIG_SERVICE_LOCALES_DEFAULT_LNG_DESC=English diff --git a/.env.test b/.env.test index f771e5ada..c9e7aa68a 100644 --- a/.env.test +++ b/.env.test @@ -2,5 +2,7 @@ REACT_APP_ENV=test REACT_APP_UI_VERSION=0.0.0.0000000 +REACT_APP_AJAX_POLL_INTERVAL=1 + REACT_APP_CONFIG_SERVICE_LOCALES=./locales/locales.json REACT_APP_CONFIG_SERVICE_LOCALES_PATH=./locales/{{lng}}.json diff --git a/public/locales/en-US.json b/public/locales/en-US.json index e970ea8f3..69010deb8 100644 --- a/public/locales/en-US.json +++ b/public/locales/en-US.json @@ -297,6 +297,8 @@ }, "curiosity-toolbar": { "button_displayName": "Search button for name", + "button_yes": "Yes", + "button_no": "No", "clearFilters": "Reset filters", "label": "", "label_billing_provider": "", @@ -360,9 +362,32 @@ "label_usage_Development/Test": "Development/Test", "label_usage_Disaster Recovery": "Disaster Recovery", "label_usage_Production": "Production", + "notifications_export_completed_description": "Downloading file {{fileName}}", + "notifications_export_completed_description_existing_completed": "{{completed}} completed report(s) are available. Would you like to continue and download?", + "notifications_export_completed_description_existing_completed_one": "{{completed}} completed report is available. Would you like to continue and download?", + "notifications_export_completed_description_existing_completed_other": "{{completed}} completed reports are available. Would you like to continue and download?", + "notifications_export_completed_description_existing_completed_pending": "{{completed}} completed and {{pending}} pending reports are available. Would you like to continue and download?", + "notifications_export_completed_description_existing_pending": "{{pending}} pending report(s) are available. Would you like to continue and download?", + "notifications_export_completed_description_existing_pending_one": "{{pending}} pending report is available. Would you like to continue and download?", + "notifications_export_completed_description_existing_pending_other": "{{pending}} pending reports are available. Would you like to continue and download?", + "notifications_export_completed_descriptionGlobal": "Download in progress", + "notifications_export_completed_descriptionGlobal_one": "{{count}} file download in progress", + "notifications_export_completed_descriptionGlobal_other": "{{count}} file downloads in progress", + "notifications_export_completed_title": "Report ready", + "notifications_export_completed_title_existing": "{{count}} existing report(s) are available", + "notifications_export_completed_title_existing_one": "{{count}} existing report is available", + "notifications_export_completed_title_existing_other": "{{count}} existing reports are available", + "notifications_export_completed_titleGlobal": "Existing report(s) ready", + "notifications_export_completed_titleGlobal_one": "Existing report ready", + "notifications_export_completed_titleGlobal_other": "Existing reports ready", + "notifications_export_error_description": "Closing report generator", + "notifications_export_error_title": "Report failed", + "notifications_export_pending_title": "Generating report in progress", + "notifications_export_pending_titleGlobal": "Continuing reports download", "placeholder": "Select", "placeholder_billing_provider": "Select purchased through", - "placeholder_export": "Export", + "placeholder_export": "Export data", + "placeholder_export_loading": "Export data is loading", "placeholder_granularity": "Select date range", "placeholder_groupVariant": "Select a variant", "placeholder_rangedMonthly": "Select a month", diff --git a/src/common/README.md b/src/common/README.md index 943463c58..33c1170b6 100644 --- a/src/common/README.md +++ b/src/common/README.md @@ -30,6 +30,7 @@ * [~setStartOfDay(date)](#Helpers.module_Dates..setStartOfDay) ⇒ Date * [~setEndOfMonth(date)](#Helpers.module_Dates..setEndOfMonth) ⇒ Date * [~setRangedDateTime(params)](#Helpers.module_Dates..setRangedDateTime) ⇒ Object + * [~setMillisecondsFromDate(params)](#Helpers.module_Dates..setMillisecondsFromDate) ⇒ Date * [~getRangedDateTime(granularity)](#Helpers.module_Dates..getRangedDateTime) ⇒ Object * [~getRangedMonthDateTime(month, defaultLocale)](#Helpers.module_Dates..getRangedMonthDateTime) ⇒ Object \| \* \| undefined @@ -193,6 +194,29 @@ Set a date range based on a granularity type. + + +### Dates~setMillisecondsFromDate(params) ⇒ Date +Set milliseconds from date. Defaults to internal current date. + +**Kind**: inner method of [Dates](#Helpers.module_Dates) + + + + + + + + + + + + + + +
ParamTypeDescription
paramsobject
params.dateDate

The date to add milliseconds towards. Defaults to current date.

+
params.msnumber
+ ### Dates~getRangedDateTime(granularity) ⇒ Object @@ -282,6 +306,9 @@ Download the debug log file. * [~PROD_MODE](#Helpers.module_General..PROD_MODE) : boolean * [~REVIEW_MODE](#Helpers.module_General..REVIEW_MODE) : boolean * [~TEST_MODE](#Helpers.module_General..TEST_MODE) : boolean + * [~CONFIG_EXPORT_EXPIRE](#Helpers.module_General..CONFIG_EXPORT_EXPIRE) : string + * [~CONFIG_EXPORT_FILENAME](#Helpers.module_General..CONFIG_EXPORT_FILENAME) : string + * [~CONFIG_EXPORT_SERVICE_NAME_PREFIX](#Helpers.module_General..CONFIG_EXPORT_SERVICE_NAME_PREFIX) : string * [~UI_DEPLOY_PATH_PREFIX](#Helpers.module_General..UI_DEPLOY_PATH_PREFIX) : string * [~UI_DEPLOY_PATH_LINK_PREFIX](#Helpers.module_General..UI_DEPLOY_PATH_LINK_PREFIX) : string * [~UI_DISABLED](#Helpers.module_General..UI_DISABLED) : boolean @@ -399,6 +426,24 @@ Associated with using the NPM script "start:proxy". See dotenv config files for Is test mode active. Associated with running unit tests. See dotenv config files for activation. +**Kind**: inner constant of [General](#Helpers.module_General) + + +### General~CONFIG\_EXPORT\_EXPIRE : string +CONFIG export download file expiration. + +**Kind**: inner constant of [General](#Helpers.module_General) + + +### General~CONFIG\_EXPORT\_FILENAME : string +CONFIG export download file name. Extension is handled at the service level. + +**Kind**: inner constant of [General](#Helpers.module_General) + + +### General~CONFIG\_EXPORT\_SERVICE\_NAME\_PREFIX : string +CONFIG export "post" download name prefix, for consistency. + **Kind**: inner constant of [General](#Helpers.module_General) diff --git a/src/common/__tests__/__snapshots__/helpers.test.js.snap b/src/common/__tests__/__snapshots__/helpers.test.js.snap index 566a9e93e..538390107 100644 --- a/src/common/__tests__/__snapshots__/helpers.test.js.snap +++ b/src/common/__tests__/__snapshots__/helpers.test.js.snap @@ -8,6 +8,9 @@ G { exports[`Helpers should expose a window object: limited window object 1`] = ` { + "CONFIG_EXPORT_EXPIRE": "86400000", + "CONFIG_EXPORT_FILENAME": "swatch_report_{0}", + "CONFIG_EXPORT_SERVICE_NAME_PREFIX": "swatch", "DEV_MODE": false, "PROD_MODE": false, "REVIEW_MODE": false, @@ -55,6 +58,9 @@ exports[`Helpers should expose a window object: limited window object 1`] = ` exports[`Helpers should expose a window object: window object 1`] = ` { + "CONFIG_EXPORT_EXPIRE": "86400000", + "CONFIG_EXPORT_FILENAME": "swatch_report_{0}", + "CONFIG_EXPORT_SERVICE_NAME_PREFIX": "swatch", "DEV_MODE": false, "PROD_MODE": false, "REVIEW_MODE": false, @@ -114,6 +120,9 @@ exports[`Helpers should handle use aggregate error, or fallback: emulated aggreg exports[`Helpers should have specific functions: helpers 1`] = ` { + "CONFIG_EXPORT_EXPIRE": "86400000", + "CONFIG_EXPORT_FILENAME": "swatch_report_{0}", + "CONFIG_EXPORT_SERVICE_NAME_PREFIX": "swatch", "DEV_MODE": false, "PROD_MODE": false, "REVIEW_MODE": false, diff --git a/src/common/helpers.js b/src/common/helpers.js index 066d1ddfa..a9a303709 100644 --- a/src/common/helpers.js +++ b/src/common/helpers.js @@ -238,6 +238,27 @@ const REVIEW_MODE = process.env.REACT_APP_ENV === 'review'; */ const TEST_MODE = process.env.REACT_APP_ENV === 'test'; +/** + * CONFIG export download file expiration. + * + * @type {string} + */ +const CONFIG_EXPORT_EXPIRE = process.env.REACT_APP_CONFIG_EXPORT_EXPIRE; + +/** + * CONFIG export download file name. Extension is handled at the service level. + * + * @type {string} + */ +const CONFIG_EXPORT_FILENAME = process.env.REACT_APP_CONFIG_EXPORT_FILENAME; + +/** + * CONFIG export "post" download name prefix, for consistency. + * + * @type {string} + */ +const CONFIG_EXPORT_SERVICE_NAME_PREFIX = process.env.REACT_APP_CONFIG_EXPORT_SERVICE_NAME_PREFIX; + /** * Apply a path prefix for routing. * Typically associated with applying a "beta" path prefix. See dotenv config files for updating. @@ -463,6 +484,9 @@ const helpers = { PROD_MODE, REVIEW_MODE, TEST_MODE, + CONFIG_EXPORT_EXPIRE, + CONFIG_EXPORT_FILENAME, + CONFIG_EXPORT_SERVICE_NAME_PREFIX, UI_DEPLOY_PATH_PREFIX, UI_DEPLOY_PATH_LINK_PREFIX, UI_DISABLED, diff --git a/src/components/README.md b/src/components/README.md index 37c6b191c..c514eebc8 100644 --- a/src/components/README.md +++ b/src/components/README.md @@ -161,8 +161,10 @@ recreate the core component.

A standalone Display Name input filter.

ToolbarFieldExport
-

A standalone export select/dropdown filter.

+

A standalone export select/dropdown filter and download hooks.

+
ToolbarFieldExportContext
+
ToolbarFieldGranularity

A standalone Granularity select filter.

@@ -4743,6 +4745,7 @@ Default props. * [~useProductInventoryHostsConfig(options)](#ProductView.module_ProductViewContext..useProductInventoryHostsConfig) ⇒ Object * [~useProductInventorySubscriptionsConfig(options)](#ProductView.module_ProductViewContext..useProductInventorySubscriptionsConfig) ⇒ Object * [~useProductToolbarConfig(options)](#ProductView.module_ProductViewContext..useProductToolbarConfig) ⇒ Object + * [~useProductExportQuery(options)](#ProductView.module_ProductViewContext..useProductExportQuery) ⇒ Object @@ -5088,6 +5091,32 @@ Return primary toolbar configuration. + + +### ProductViewContext~useProductExportQuery(options) ⇒ Object +Return an export query for subscriptions. + +**Kind**: inner method of [ProductViewContext](#ProductView.module_ProductViewContext) + + + + + + + + + + + + + + + + + + +
ParamType
optionsobject
options.useProductfunction
options.schemaCheckobject
options.useProductToolbarQueryfunction
options.optionsobject
+ ## ProductViewMissing @@ -6591,14 +6620,11 @@ On enter submit value, on type submit value, and on esc ignore (clear value at c ## ToolbarFieldExport -A standalone export select/dropdown filter. +A standalone export select/dropdown filter and download hooks. * [ToolbarFieldExport](#Toolbar.module_ToolbarFieldExport) * [~toolbarFieldOptions](#Toolbar.module_ToolbarFieldExport..toolbarFieldOptions) : Array.<{title: React.ReactNode, value: string, selected: boolean}> - * [~useExportStatus(options)](#Toolbar.module_ToolbarFieldExport..useExportStatus) ⇒ Object - * [~useExport(options)](#Toolbar.module_ToolbarFieldExport..useExport) ⇒ function - * [~validate](#Toolbar.module_ToolbarFieldExport..useExport..validate) : function * [~useOnSelect(options)](#Toolbar.module_ToolbarFieldExport..useOnSelect) ⇒ function * [~ToolbarFieldExport(props)](#Toolbar.module_ToolbarFieldExport..ToolbarFieldExport) ⇒ React.ReactNode * [.propTypes](#Toolbar.module_ToolbarFieldExport..ToolbarFieldExport.propTypes) : Object @@ -6610,10 +6636,10 @@ A standalone export select/dropdown filter. Select field options. **Kind**: inner constant of [ToolbarFieldExport](#Toolbar.module_ToolbarFieldExport) - + -### ToolbarFieldExport~useExportStatus(options) ⇒ Object -Aggregated export status +### ToolbarFieldExport~useOnSelect(options) ⇒ function +On select create/post an export. **Kind**: inner method of [ToolbarFieldExport](#Toolbar.module_ToolbarFieldExport) @@ -6626,18 +6652,21 @@ Aggregated export status + + - +
optionsobject
options.useExportfunction
options.useProductfunction
options.useSelectorfunctionoptions.useProductExportQueryfunction
- + -### ToolbarFieldExport~useExport(options) ⇒ function -Apply a centralized export hook for, post/put, polling status, and download. +### ToolbarFieldExport~ToolbarFieldExport(props) ⇒ React.ReactNode +Display an export/download field with options. Check and download available exports. **Kind**: inner method of [ToolbarFieldExport](#Toolbar.module_ToolbarFieldExport) +**Emits**: [onSelect](#event_onSelect) @@ -6646,32 +6675,56 @@ Apply a centralized export hook for, post/put, polling status, and download. - + - + - + - + - + + + - +
optionsobjectpropsobject
options.createExportfunctionprops.optionsArray
options.getExportfunctionprops.positionstring
options.getExportStatusfunctionprops.tfunction
options.useDispatchfunctionprops.useExistingExportsfunction
props.useExportStatusfunction
options.useExportStatusfunctionprops.useOnSelectfunction
- -#### useExport~validate : function -A polling response validator +* [~ToolbarFieldExport(props)](#Toolbar.module_ToolbarFieldExport..ToolbarFieldExport) ⇒ React.ReactNode + * [.propTypes](#Toolbar.module_ToolbarFieldExport..ToolbarFieldExport.propTypes) : Object + * [.defaultProps](#Toolbar.module_ToolbarFieldExport..ToolbarFieldExport.defaultProps) : Object -**Kind**: inner constant of [useExport](#Toolbar.module_ToolbarFieldExport..useExport) - + -### ToolbarFieldExport~useOnSelect(options) ⇒ function -On select update export. +#### ToolbarFieldExport.propTypes : Object +Prop types. -**Kind**: inner method of [ToolbarFieldExport](#Toolbar.module_ToolbarFieldExport) +**Kind**: static property of [ToolbarFieldExport](#Toolbar.module_ToolbarFieldExport..ToolbarFieldExport) + + +#### ToolbarFieldExport.defaultProps : Object +Default props. + +**Kind**: static property of [ToolbarFieldExport](#Toolbar.module_ToolbarFieldExport..ToolbarFieldExport) + + +## ToolbarFieldExportContext + +* [ToolbarFieldExportContext](#ToolbarFieldExport.module_ToolbarFieldExportContext) + * [~useExportConfirmation(options)](#ToolbarFieldExport.module_ToolbarFieldExportContext..useExportConfirmation) ⇒ function + * [~useExport(options)](#ToolbarFieldExport.module_ToolbarFieldExportContext..useExport) ⇒ function + * [~useExistingExportsConfirmation(options)](#ToolbarFieldExport.module_ToolbarFieldExportContext..useExistingExportsConfirmation) ⇒ function + * [~useExistingExports(options)](#ToolbarFieldExport.module_ToolbarFieldExportContext..useExistingExports) + * [~useExportStatus(options)](#ToolbarFieldExport.module_ToolbarFieldExportContext..useExportStatus) ⇒ Object + + + +### ToolbarFieldExportContext~useExportConfirmation(options) ⇒ function +Return a polling status callback. Used when creating an export. + +**Kind**: inner method of [ToolbarFieldExportContext](#ToolbarFieldExport.module_ToolbarFieldExportContext) @@ -6682,21 +6735,22 @@ On select update export. - + - + - + + +
optionsobject
options.useExportfunctionoptions.addNotificationfunction
options.useProductfunctionoptions.tfunction
options.useProductInventoryQueryfunctionoptions.useDispatchfunction
options.useProductfunction
- + -### ToolbarFieldExport~ToolbarFieldExport(props) ⇒ React.ReactNode -Display an export/download field with options. +### ToolbarFieldExportContext~useExport(options) ⇒ function +Apply an export hook for an export post. The service automatically sets up polling, then force downloads the file. -**Kind**: inner method of [ToolbarFieldExport](#Toolbar.module_ToolbarFieldExport) -**Emits**: [onSelect](#event_onSelect) +**Kind**: inner method of [ToolbarFieldExportContext](#ToolbarFieldExport.module_ToolbarFieldExportContext) @@ -6705,39 +6759,98 @@ Display an export/download field with options. - + - + - + - + + + + +
propsobjectoptionsobject
props.optionsArrayoptions.createExportfunction
props.positionstringoptions.tfunction
props.tfunctionoptions.useDispatchfunction
options.useExportConfirmationfunction
+ + + +### ToolbarFieldExportContext~useExistingExportsConfirmation(options) ⇒ function +User confirmation results when existing exports are detected. + +**Kind**: inner method of [ToolbarFieldExportContext](#ToolbarFieldExport.module_ToolbarFieldExportContext) + + + + + + + + + - + - + - + + + + +
ParamType
optionsobject
props.useExportfunctionoptions.deleteExistingExportsfunction
props.useExportStatusfunctionoptions.getExistingExportsfunction
props.useOnSelectfunctionoptions.removeNotificationfunction
options.tfunction
options.useDispatchfunction
+ -* [~ToolbarFieldExport(props)](#Toolbar.module_ToolbarFieldExport..ToolbarFieldExport) ⇒ React.ReactNode - * [.propTypes](#Toolbar.module_ToolbarFieldExport..ToolbarFieldExport.propTypes) : Object - * [.defaultProps](#Toolbar.module_ToolbarFieldExport..ToolbarFieldExport.defaultProps) : Object +### ToolbarFieldExportContext~useExistingExports(options) +Apply an existing exports hook for user abandoned reports. Allow bulk polling status with download. - +**Kind**: inner method of [ToolbarFieldExportContext](#ToolbarFieldExport.module_ToolbarFieldExportContext) + + + + + + + + + + + + + + + + + + + + + + +
ParamType
optionsobject
options.addNotificationfunction
options.getExistingExportsStatusfunction
options.tfunction
options.useDispatchfunction
options.useExistingExportsConfirmationfunction
options.useSelectorsResponsefunction
-#### ToolbarFieldExport.propTypes : Object -Prop types. + -**Kind**: static property of [ToolbarFieldExport](#Toolbar.module_ToolbarFieldExport..ToolbarFieldExport) - +### ToolbarFieldExportContext~useExportStatus(options) ⇒ Object +Aggregated export status -#### ToolbarFieldExport.defaultProps : Object -Default props. +**Kind**: inner method of [ToolbarFieldExportContext](#ToolbarFieldExport.module_ToolbarFieldExportContext) + + + + + + + + + + + + + + +
ParamType
optionsobject
options.useProductfunction
options.useSelectorfunction
-**Kind**: static property of [ToolbarFieldExport](#Toolbar.module_ToolbarFieldExport..ToolbarFieldExport) ## ToolbarFieldGranularity diff --git a/src/components/i18n/__tests__/__snapshots__/i18n.test.js.snap b/src/components/i18n/__tests__/__snapshots__/i18n.test.js.snap index 9a11dd273..34576c85b 100644 --- a/src/components/i18n/__tests__/__snapshots__/i18n.test.js.snap +++ b/src/components/i18n/__tests__/__snapshots__/i18n.test.js.snap @@ -352,6 +352,87 @@ exports[`I18n Component should generate a predictable locale key output snapshot "key": "curiosity-toolbar.placeholder", "match": "t('curiosity-toolbar.placeholder', { context: 'export' })", }, + { + "key": "curiosity-toolbar.placeholder", + "match": "t('curiosity-toolbar.placeholder', { context: 'export' })", + }, + ], + }, + { + "file": "src/components/toolbar/toolbarFieldExportContext.js", + "keys": [ + { + "key": "curiosity-toolbar.notifications", + "match": "t('curiosity-toolbar.notifications', { context: ['export', 'completed', 'title'] })", + }, + { + "key": "curiosity-toolbar.notifications", + "match": "t('curiosity-toolbar.notifications', { context: ['export', 'completed', 'description'], fileName: completed?.[0]?.fileName })", + }, + { + "key": "curiosity-toolbar.notifications", + "match": "t('curiosity-toolbar.notifications', { context: ['export', 'error', 'title'] })", + }, + { + "key": "curiosity-toolbar.notifications", + "match": "t('curiosity-toolbar.notifications', { context: ['export', 'error', 'description'] })", + }, + { + "key": "curiosity-toolbar.notifications", + "match": "t('curiosity-toolbar.notifications', { context: ['export', 'pending', 'title', id] })", + }, + { + "key": "curiosity-toolbar.notifications", + "match": "t('curiosity-toolbar.notifications', { context: ['export', 'error', 'title'] })", + }, + { + "key": "curiosity-toolbar.notifications", + "match": "t('curiosity-toolbar.notifications', { context: ['export', 'error', 'description'] })", + }, + { + "key": "curiosity-toolbar.notifications", + "match": "t('curiosity-toolbar.notifications', { context: ['export', 'error', 'title'] })", + }, + { + "key": "curiosity-toolbar.notifications", + "match": "t('curiosity-toolbar.notifications', { context: ['export', 'error', 'description'] })", + }, + { + "key": "curiosity-toolbar.notifications", + "match": "t('curiosity-toolbar.notifications', { context: ['export', 'pending', 'titleGlobal'] })", + }, + { + "key": "curiosity-toolbar.notifications", + "match": "t('curiosity-toolbar.notifications', { context: ['export', 'completed', 'titleGlobal'], count: allResults.length })", + }, + { + "key": "curiosity-toolbar.notifications", + "match": "t('curiosity-toolbar.notifications', { context: ['export', 'completed', 'descriptionGlobal'], count: allResults.length })", + }, + { + "key": "curiosity-toolbar.notifications", + "match": "t('curiosity-toolbar.notifications', { context: ['export', 'error', 'title'] })", + }, + { + "key": "curiosity-toolbar.notifications", + "match": "t('curiosity-toolbar.notifications', { context: ['export', 'error', 'description'] })", + }, + { + "key": "curiosity-toolbar.notifications", + "match": "t('curiosity-toolbar.notifications', { context: ['export', 'completed', 'title', 'existing'], count: totalResults })", + }, + { + "key": "curiosity-toolbar.notifications", + "match": "t('curiosity-toolbar.notifications', { context: [ 'export', 'completed', 'description', 'existing', completed.length && 'completed', pending.length && 'pending' ], count: totalResults, completed: completed.length, pending: pending.length })", + }, + { + "key": "curiosity-toolbar.button", + "match": "t('curiosity-toolbar.button', { context: 'yes' })", + }, + { + "key": "curiosity-toolbar.button", + "match": "t('curiosity-toolbar.button', { context: 'no' })", + }, ], }, { @@ -436,6 +517,10 @@ exports[`I18n Component should generate a predictable locale key output snapshot "key": "curiosity-toolbar.label", "match": "translate('curiosity-toolbar.label', { context: ['filter', RHSM_API_QUERY_SET_TYPES.DISPLAY_NAME] })", }, + { + "key": "curiosity-toolbar.label", + "match": "translate('curiosity-toolbar.label', { context: ['filter', 'export'] })", + }, { "key": "curiosity-toolbar.placeholder", "match": "t('curiosity-toolbar.placeholder', { context: ['filter'] })", @@ -975,6 +1060,78 @@ exports[`I18n Component should have locale keys that exist in the default langua "file": "src/components/toolbar/toolbarFieldExport.js", "key": "curiosity-toolbar.label", }, + { + "file": "src/components/toolbar/toolbarFieldExportContext.js", + "key": "curiosity-toolbar.notifications", + }, + { + "file": "src/components/toolbar/toolbarFieldExportContext.js", + "key": "curiosity-toolbar.notifications", + }, + { + "file": "src/components/toolbar/toolbarFieldExportContext.js", + "key": "curiosity-toolbar.notifications", + }, + { + "file": "src/components/toolbar/toolbarFieldExportContext.js", + "key": "curiosity-toolbar.notifications", + }, + { + "file": "src/components/toolbar/toolbarFieldExportContext.js", + "key": "curiosity-toolbar.notifications", + }, + { + "file": "src/components/toolbar/toolbarFieldExportContext.js", + "key": "curiosity-toolbar.notifications", + }, + { + "file": "src/components/toolbar/toolbarFieldExportContext.js", + "key": "curiosity-toolbar.notifications", + }, + { + "file": "src/components/toolbar/toolbarFieldExportContext.js", + "key": "curiosity-toolbar.notifications", + }, + { + "file": "src/components/toolbar/toolbarFieldExportContext.js", + "key": "curiosity-toolbar.notifications", + }, + { + "file": "src/components/toolbar/toolbarFieldExportContext.js", + "key": "curiosity-toolbar.notifications", + }, + { + "file": "src/components/toolbar/toolbarFieldExportContext.js", + "key": "curiosity-toolbar.notifications", + }, + { + "file": "src/components/toolbar/toolbarFieldExportContext.js", + "key": "curiosity-toolbar.notifications", + }, + { + "file": "src/components/toolbar/toolbarFieldExportContext.js", + "key": "curiosity-toolbar.notifications", + }, + { + "file": "src/components/toolbar/toolbarFieldExportContext.js", + "key": "curiosity-toolbar.notifications", + }, + { + "file": "src/components/toolbar/toolbarFieldExportContext.js", + "key": "curiosity-toolbar.notifications", + }, + { + "file": "src/components/toolbar/toolbarFieldExportContext.js", + "key": "curiosity-toolbar.notifications", + }, + { + "file": "src/components/toolbar/toolbarFieldExportContext.js", + "key": "curiosity-toolbar.button", + }, + { + "file": "src/components/toolbar/toolbarFieldExportContext.js", + "key": "curiosity-toolbar.button", + }, { "file": "src/components/toolbar/toolbarFieldGranularity.js", "key": "curiosity-toolbar.label", @@ -1015,6 +1172,10 @@ exports[`I18n Component should have locale keys that exist in the default langua "file": "src/components/toolbar/toolbarFieldSelectCategory.js", "key": "curiosity-toolbar.label", }, + { + "file": "src/components/toolbar/toolbarFieldSelectCategory.js", + "key": "curiosity-toolbar.label", + }, { "file": "src/components/toolbar/toolbarFieldSla.js", "key": "curiosity-toolbar.label", diff --git a/src/components/productView/__tests__/__snapshots__/productViewContext.test.js.snap b/src/components/productView/__tests__/__snapshots__/productViewContext.test.js.snap index 3e67dd64b..23a7a47de 100644 --- a/src/components/productView/__tests__/__snapshots__/productViewContext.test.js.snap +++ b/src/components/productView/__tests__/__snapshots__/productViewContext.test.js.snap @@ -19,6 +19,13 @@ exports[`ProductViewContext should apply a hook for retrieving product context: } `; +exports[`ProductViewContext should apply hooks for retrieving specific api queries: exportQuery 1`] = ` +{ + "product_id": undefined, + "sla": "testSla", +} +`; + exports[`ProductViewContext should apply hooks for retrieving specific api queries: graphTallyQuery 1`] = ` { "granularity": "testGranularity", @@ -177,6 +184,7 @@ exports[`ProductViewContext should return specific properties: specific properti "useInventorySubscriptionsQuery": [Function], "useProduct": [Function], "useProductContext": [Function], + "useProductExportQuery": [Function], "useQuery": [Function], "useQueryFactory": [Function], "useToolbarConfig": [Function], diff --git a/src/components/productView/__tests__/productViewContext.test.js b/src/components/productView/__tests__/productViewContext.test.js index eb1ca956f..35afc610b 100644 --- a/src/components/productView/__tests__/productViewContext.test.js +++ b/src/components/productView/__tests__/productViewContext.test.js @@ -2,6 +2,7 @@ import { context, useProductQueryFactory, useProductQuery, + useProductExportQuery, useProductGraphTallyQuery, useProductInventoryGuestsQuery, useProductInventoryHostsQuery, @@ -76,6 +77,11 @@ describe('ProductViewContext', () => { useProductToolbarQuery({ options: { useProductViewContext: () => mockContextValue } }) ); expect(toolbarQuery).toMatchSnapshot('toolbarQuery'); + + const { result: exportQuery } = await renderHook(() => + useProductExportQuery({ options: { useProductViewContext: () => mockContextValue } }) + ); + expect(exportQuery).toMatchSnapshot('exportQuery'); }); it('should apply a hook for retrieving product context', async () => { diff --git a/src/components/productView/productViewContext.js b/src/components/productView/productViewContext.js index 48b50be60..4c596de98 100644 --- a/src/components/productView/productViewContext.js +++ b/src/components/productView/productViewContext.js @@ -2,6 +2,7 @@ import React, { useContext } from 'react'; import { reduxHelpers } from '../../redux/common'; import { storeHooks } from '../../redux/hooks'; import { rhsmConstants } from '../../services/rhsm/rhsmConstants'; +import { platformConstants } from '../../services/platform/platformConstants'; import { helpers } from '../../common/helpers'; /** @@ -308,6 +309,32 @@ const useProductToolbarConfig = ({ useProductContext: useAliasProductContext = u }; }; +/** + * Return an export query for subscriptions. + * + * @param {object} options + * @param {Function} options.useProduct + * @param {object} options.schemaCheck + * @param {Function} options.useProductToolbarQuery + * @param {object} options.options + * @returns {{}} + */ +const useProductExportQuery = ({ + useProduct: useAliasProduct = useProduct, + schemaCheck = platformConstants.PLATFORM_API_EXPORT_POST_SUBSCRIPTIONS_FILTER_TYPES, + useProductToolbarQuery: useAliasProductToolbarQuery = useProductToolbarQuery, + options +} = {}) => { + const { productId } = useAliasProduct(); + return reduxHelpers.setApiQuery( + { + ...useAliasProductToolbarQuery({ options }), + [platformConstants.PLATFORM_API_EXPORT_POST_SUBSCRIPTIONS_FILTER_TYPES.PRODUCT_ID]: productId + }, + schemaCheck + ); +}; + const context = { ProductViewContext, DEFAULT_CONTEXT, @@ -319,6 +346,7 @@ const context = { useInventoryHostsQuery: useProductInventoryHostsQuery, useInventorySubscriptionsQuery: useProductInventorySubscriptionsQuery, useProduct, + useProductExportQuery, useGraphConfig: useProductGraphConfig, useInventoryGuestsConfig: useProductInventoryGuestsConfig, useInventoryHostsConfig: useProductInventoryHostsConfig, @@ -340,6 +368,7 @@ export { useProductInventoryHostsQuery, useProductInventorySubscriptionsQuery, useProduct, + useProductExportQuery, useProductGraphConfig, useProductInventoryGuestsConfig, useProductInventoryHostsConfig, diff --git a/src/components/toolbar/__tests__/__snapshots__/toolbarFieldExport.test.js.snap b/src/components/toolbar/__tests__/__snapshots__/toolbarFieldExport.test.js.snap index 9f17bced2..723c29a9e 100644 --- a/src/components/toolbar/__tests__/__snapshots__/toolbarFieldExport.test.js.snap +++ b/src/components/toolbar/__tests__/__snapshots__/toolbarFieldExport.test.js.snap @@ -1,90 +1,35 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`ToolbarFieldExport Component should aggregate export service calls: createExport 1`] = ` -[ - [ - { - "lorem": "ipsum", - }, - { - "poll": { - "validate": [Function], - }, - }, - ], -] -`; - -exports[`ToolbarFieldExport Component should aggregate export service calls: getExport 1`] = ` +exports[`ToolbarFieldExport Component should export select options: toolbarFieldOptions 1`] = ` [ - [ - "dolorSit", - ], + { + "selected": false, + "title": "t(curiosity-toolbar.label_export, {"context":"json"})", + "value": "json", + }, ] `; -exports[`ToolbarFieldExport Component should aggregate export service calls: getStatus 1`] = ` +exports[`ToolbarFieldExport Component should handle updating export through redux action with hook: dispatch, hook 1`] = ` [ [ + undefined, { - "poll": { - "validate": [Function], - }, + "expires_at": "2019-07-21T00:00:00.000Z", + "format": "dolor sit", + "name": "swatch-undefined", + "sources": [ + { + "application": "subscriptions", + "filters": {}, + "resource": "subscriptions", + }, + ], }, ], ] `; -exports[`ToolbarFieldExport Component should aggregate export service calls: getStatus, existing polling 1`] = `[]`; - -exports[`ToolbarFieldExport Component should aggregate export status, polling status with a hook: status, basic 1`] = ` -{ - "isCompleted": undefined, - "isPolling": undefined, - "isProductPolling": false, - "productPollingFormats": [], -} -`; - -exports[`ToolbarFieldExport Component should aggregate export status, polling status with a hook: status, completed 1`] = ` -{ - "isCompleted": undefined, - "isPolling": undefined, - "isProductPolling": false, - "productPollingFormats": [], -} -`; - -exports[`ToolbarFieldExport Component should aggregate export status, polling status with a hook: status, polling 1`] = ` -{ - "isCompleted": undefined, - "isPolling": true, - "isProductPolling": true, - "productPollingFormats": [ - "dolorSit", - ], -} -`; - -exports[`ToolbarFieldExport Component should export select options: toolbarFieldOptions 1`] = ` -[ - { - "selected": false, - "title": "t(curiosity-toolbar.label_export, {"context":"csv"})", - "value": "csv", - }, - { - "selected": false, - "title": "t(curiosity-toolbar.label_export, {"context":"json"})", - "value": "json", - }, -] -`; - -exports[`ToolbarFieldExport Component should handle updating export through redux state with component: dispatch, component 1`] = `[]`; - -exports[`ToolbarFieldExport Component should handle updating export through redux state with hook: dispatch, hook 1`] = `[]`; - exports[`ToolbarFieldExport Component should render a basic component: basic 1`] = ` { + const { productId } = useAliasProduct(); + const dispatch = useAliasDispatch(); + + return useCallback( + successResponse => { + const { completed = [], isCompleted, pending = [] } = successResponse?.data?.data?.products?.[productId] || {}; + const isPending = !isCompleted; + + if (isCompleted) { + addAliasNotification({ + variant: 'success', + id: `swatch-create-export-${productId}`, + title: t('curiosity-toolbar.notifications', { + context: ['export', 'completed', 'title'] + }), + description: t('curiosity-toolbar.notifications', { + context: ['export', 'completed', 'description'], + fileName: completed?.[0]?.fileName + }), + dismissable: true + })(dispatch); + } + + dispatch({ + type: reduxTypes.platform.SET_PLATFORM_EXPORT_STATUS, + id: productId, + isPending, + pending + }); + }, + [addAliasNotification, dispatch, productId, t] + ); +}; + +/** + * Apply an export hook for an export post. The service automatically sets up polling, then force downloads the file. + * + * @param {object} options + * @param {Function} options.createExport + * @param {Function} options.t + * @param {Function} options.useDispatch + * @param {Function} options.useExportConfirmation + * @returns {Function} + */ +const useExport = ({ + createExport: createAliasExport = reduxActions.platform.createExport, + t = translate, + useDispatch: useAliasDispatch = storeHooks.reactRedux.useDispatch, + useExportConfirmation: useAliasExportConfirmation = useExportConfirmation +} = {}) => { + const statusConfirmation = useAliasExportConfirmation(); + const dispatch = useAliasDispatch(); + + return useCallback( + (id, data) => { + dispatch({ + type: reduxTypes.platform.SET_PLATFORM_EXPORT_STATUS, + id, + isPending: true + }); + + createAliasExport( + id, + data, + { poll: { status: statusConfirmation } }, + { + rejected: { + variant: 'warning', + title: t('curiosity-toolbar.notifications', { + context: ['export', 'error', 'title'] + }), + description: t('curiosity-toolbar.notifications', { + context: ['export', 'error', 'description'] + }), + dismissable: true + }, + pending: { + variant: 'info', + id: `swatch-create-export-${id}`, + title: t('curiosity-toolbar.notifications', { + context: ['export', 'pending', 'title', id] + }), + dismissable: true + } + } + )(dispatch); + }, + [createAliasExport, dispatch, statusConfirmation, t] + ); +}; + +/** + * User confirmation results when existing exports are detected. + * + * @param {object} options + * @param {Function} options.deleteExistingExports + * @param {Function} options.getExistingExports + * @param {Function} options.removeNotification + * @param {Function} options.t + * @param {Function} options.useDispatch + * @returns {Function} + */ +const useExistingExportsConfirmation = ({ + deleteExistingExports: deleteAliasExistingExports = reduxActions.platform.deleteExistingExports, + getExistingExports: getAliasExistingExports = reduxActions.platform.getExistingExports, + removeNotification: removeAliasNotification = reduxActions.platform.removeNotification, + t = translate, + useDispatch: useAliasDispatch = storeHooks.reactRedux.useDispatch +} = {}) => { + const dispatch = useAliasDispatch(); + + return useCallback( + (confirmation, allResults) => { + dispatch(removeAliasNotification('swatch-exports-status')); + + if (confirmation === 'no') { + return deleteAliasExistingExports(allResults, { + rejected: { + variant: 'warning', + title: t('curiosity-toolbar.notifications', { context: ['export', 'error', 'title'] }), + description: t('curiosity-toolbar.notifications', { context: ['export', 'error', 'description'] }), + dismissable: true + } + })(dispatch); + } + + return getAliasExistingExports(allResults, { + rejected: { + variant: 'warning', + title: t('curiosity-toolbar.notifications', { context: ['export', 'error', 'title'] }), + description: t('curiosity-toolbar.notifications', { context: ['export', 'error', 'description'] }), + dismissable: true + }, + pending: { + variant: 'info', + title: t('curiosity-toolbar.notifications', { context: ['export', 'pending', 'titleGlobal'] }), + dismissable: true + }, + fulfilled: { + variant: 'success', + title: t('curiosity-toolbar.notifications', { + context: ['export', 'completed', 'titleGlobal'], + count: allResults.length + }), + description: t('curiosity-toolbar.notifications', { + context: ['export', 'completed', 'descriptionGlobal'], + count: allResults.length + }), + dismissable: true + } + })(dispatch); + }, + [dispatch, deleteAliasExistingExports, getAliasExistingExports, removeAliasNotification, t] + ); +}; + +/** + * Apply an existing exports hook for user abandoned reports. Allow bulk polling status with download. + * + * @param {object} options + * @param {Function} options.addNotification + * @param {Function} options.getExistingExportsStatus + * @param {Function} options.t + * @param {Function} options.useDispatch + * @param {Function} options.useExistingExportsConfirmation + * @param {Function} options.useSelectorsResponse + */ +const useExistingExports = ({ + addNotification: addAliasNotification = reduxActions.platform.addNotification, + getExistingExportsStatus: getAliasExistingExportsStatus = reduxActions.platform.getExistingExportsStatus, + t = translate, + useDispatch: useAliasDispatch = storeHooks.reactRedux.useDispatch, + useExistingExportsConfirmation: useAliasExistingExportsConfirmation = useExistingExportsConfirmation, + useSelectorsResponse: useAliasSelectorsResponse = storeHooks.reactRedux.useSelectorsResponse +} = {}) => { + const [isConfirmation, setIsConfirmation] = useState(false); + const dispatch = useAliasDispatch(); + const onConfirmation = useAliasExistingExportsConfirmation(); + const { data, fulfilled } = useAliasSelectorsResponse(({ app }) => app?.exportsExisting); + const { completed = [], isAnythingPending, isAnythingCompleted, pending = [] } = data?.[0]?.data || {}; + + useMount(() => { + if (!isConfirmation) { + getAliasExistingExportsStatus({ + rejected: { + variant: 'warning', + title: t('curiosity-toolbar.notifications', { context: ['export', 'error', 'title'] }), + description: t('curiosity-toolbar.notifications', { context: ['export', 'error', 'description'] }), + dismissable: true + } + })(dispatch); + } + }); + + useEffect(() => { + if (!fulfilled || isConfirmation) { + return; + } + + const isAnythingAvailable = isAnythingPending || isAnythingCompleted || false; + const totalResults = completed.length + pending.length; + + if (isAnythingAvailable && totalResults) { + addAliasNotification({ + id: `swatch-exports-status`, + title: t('curiosity-toolbar.notifications', { + context: ['export', 'completed', 'title', 'existing'], + count: totalResults + }), + description: ( +
+ {t('curiosity-toolbar.notifications', { + context: [ + 'export', + 'completed', + 'description', + 'existing', + completed.length && 'completed', + pending.length && 'pending' + ], + count: totalResults, + completed: completed.length, + pending: pending.length + })} +

+ {' '} + +

+
+ ), + autoDismiss: false, + dismissable: false + })(dispatch); + + setIsConfirmation(true); + } + }, [ + addAliasNotification, + completed, + dispatch, + fulfilled, + isAnythingCompleted, + isAnythingPending, + isConfirmation, + onConfirmation, + pending, + t + ]); +}; + +/** + * Aggregated export status + * + * @param {object} options + * @param {Function} options.useProduct + * @param {Function} options.useSelector + * @returns {{isProductPending: boolean, productPendingFormats: Array}} + */ +const useExportStatus = ({ + useProduct: useAliasProduct = useProduct, + useSelector: useAliasSelector = storeHooks.reactRedux.useSelector +} = {}) => { + const { productId } = useAliasProduct(); + const { isPending, pending } = useAliasSelector(({ app }) => app?.exports?.[productId], {}); + + const pendingProductFormats = []; + const isProductPending = isPending || false; + + if (isProductPending && Array.isArray(pending)) { + pendingProductFormats.push(...pending.map(({ format: productFormat }) => productFormat)); + } + + return { + isProductPending, + pendingProductFormats + }; +}; + +const context = { + useExport, + useExportConfirmation, + useExportStatus, + useExistingExports, + useExistingExportsConfirmation +}; + +export { + context as default, + context, + useExport, + useExportConfirmation, + useExportStatus, + useExistingExports, + useExistingExportsConfirmation +}; diff --git a/src/components/toolbar/toolbarFieldSelectCategory.js b/src/components/toolbar/toolbarFieldSelectCategory.js index 79ac4e1a5..7e94f812b 100644 --- a/src/components/toolbar/toolbarFieldSelectCategory.js +++ b/src/components/toolbar/toolbarFieldSelectCategory.js @@ -13,6 +13,7 @@ import { } from './toolbarFieldBillingProvider'; import { ToolbarFieldCategory } from './toolbarFieldCategory'; import { ToolbarFieldDisplayName } from './toolbarFieldDisplayName'; +import { ToolbarFieldExport } from './toolbarFieldExport'; import { ToolbarFieldGranularity, toolbarFieldOptions as granularityOptions } from './toolbarFieldGranularity'; import { ToolbarFieldRangedMonthly, toolbarFieldOptions as rangedMonthlyOptions } from './toolbarFieldRangedMonthly'; import { ToolbarFieldSla, toolbarFieldOptions as slaOptions } from './toolbarFieldSla'; @@ -92,6 +93,15 @@ const toolbarFieldOptions = [ }, options: null, isClearable: true + }, + { + title: translate('curiosity-toolbar.label', { context: ['filter', 'export'] }), + value: 'export', + component: function Export(props) { + return ; + }, + options: [], + isClearable: false } ].map(option => ({ ...option, diff --git a/src/redux/README.md b/src/redux/README.md index 94df37d08..67f7c1ca9 100644 --- a/src/redux/README.md +++ b/src/redux/README.md @@ -65,10 +65,10 @@ Platform service wrappers for dispatch, state update. * [~removeNotification(id)](#Actions.module_PlatformActions..removeNotification) ⇒ \* * [~clearNotifications()](#Actions.module_PlatformActions..clearNotifications) ⇒ \* * [~authorizeUser(appName)](#Actions.module_PlatformActions..authorizeUser) ⇒ function - * [~getExport(id)](#Actions.module_PlatformActions..getExport) ⇒ function - * [~setExportStatus(dispatch)](#Actions.module_PlatformActions..setExportStatus) ⇒ function - * [~getExportStatus(options)](#Actions.module_PlatformActions..getExportStatus) ⇒ function - * [~createExport(data, options)](#Actions.module_PlatformActions..createExport) ⇒ function + * [~getExistingExports(existingExports, notifications)](#Actions.module_PlatformActions..getExistingExports) ⇒ function + * [~deleteExistingExports(existingExports, notifications)](#Actions.module_PlatformActions..deleteExistingExports) ⇒ function + * [~getExistingExportsStatus(notifications)](#Actions.module_PlatformActions..getExistingExportsStatus) ⇒ function + * [~createExport(id, data, options, notifications)](#Actions.module_PlatformActions..createExport) ⇒ function * [~hideGlobalFilter(isHidden)](#Actions.module_PlatformActions..hideGlobalFilter) ⇒ Object @@ -131,28 +131,31 @@ Get an emulated and combined API response from the platforms "getUser" and "getU - + -### PlatformActions~getExport(id) ⇒ function -Get a specific export download package. +### PlatformActions~getExistingExports(existingExports, notifications) ⇒ function +Get all existing exports, if pending poll, and when complete download. Includes toast notifications. **Kind**: inner method of [PlatformActions](#Actions.module_PlatformActions) - + - + + +
ParamTypeParamTypeDescription
idstringexistingExportsArray
notificationsobject

Apply notification options

+
- + -### PlatformActions~setExportStatus(dispatch) ⇒ function -Return a "dispatch ready" export poll status check. +### PlatformActions~deleteExistingExports(existingExports, notifications) ⇒ function +Delete all existing exports. Includes toast notifications. **Kind**: inner method of [PlatformActions](#Actions.module_PlatformActions) @@ -163,33 +166,35 @@ Return a "dispatch ready" export poll status check. - + + +
dispatchfunctionexistingExportsArray.<{id: string}>
notificationsobject
- + -### PlatformActions~getExportStatus(options) ⇒ function -Get a specific, or all, export status. +### PlatformActions~getExistingExportsStatus(notifications) ⇒ function +Get a status from any existing exports. Display a confirmation for downloading, or ignoring, the exports. +Includes toast notifications. **Kind**: inner method of [PlatformActions](#Actions.module_PlatformActions) - + - +
ParamTypeDescriptionParamType
optionsobject

Apply polling options

-
notificationsobject
-### PlatformActions~createExport(data, options) ⇒ function -Create an export for download. +### PlatformActions~createExport(id, data, options, notifications) ⇒ function +Create an export for download. Includes toast notifications. **Kind**: inner method of [PlatformActions](#Actions.module_PlatformActions) @@ -200,10 +205,14 @@ Create an export for download. + + + +
idstring
dataobject
optionsobject

Apply polling options

notificationsobject
diff --git a/src/redux/actions/__tests__/__snapshots__/platformActions.test.js.snap b/src/redux/actions/__tests__/__snapshots__/platformActions.test.js.snap index b8e2f2f71..b586d9b1e 100644 --- a/src/redux/actions/__tests__/__snapshots__/platformActions.test.js.snap +++ b/src/redux/actions/__tests__/__snapshots__/platformActions.test.js.snap @@ -7,23 +7,58 @@ exports[`PlatformActions Should return a dispatch object for the hideGlobalFilte } `; -exports[`PlatformActions Should return response content for getExport method: dispatch object 1`] = ` +exports[`PlatformActions Should return response content for createExport method: dispatch object 1`] = ` [ [ { + "meta": { + "id": undefined, + "notifications": {}, + }, "payload": Promise {}, - "type": "GET_PLATFORM_EXPORT", + "type": "SET_PLATFORM_EXPORT_CREATE", }, ], ] `; -exports[`PlatformActions Should return response content for setExportStatus method: dispatch object 1`] = ` +exports[`PlatformActions Should return response content for getExistingExports method: dispatch object 1`] = ` [ [ { + "meta": { + "notifications": {}, + }, "payload": Promise {}, - "type": "SET_PLATFORM_EXPORT_STATUS", + "type": "GET_PLATFORM_EXPORT_EXISTING", + }, + ], +] +`; + +exports[`PlatformActions Should return response content for getExistingExportsStatus method: dispatch object 1`] = ` +[ + [ + { + "meta": { + "notifications": undefined, + }, + "payload": Promise {}, + "type": "SET_PLATFORM_EXPORT_EXISTING_STATUS", + }, + ], +] +`; + +exports[`PlatformActions Should return response content for removeExistingExports method: dispatch object 1`] = ` +[ + [ + { + "meta": { + "notifications": undefined, + }, + "payload": Promise {}, + "type": "DELETE_PLATFORM_EXPORT_EXISTING", }, ], ] diff --git a/src/redux/actions/__tests__/platformActions.test.js b/src/redux/actions/__tests__/platformActions.test.js index 36e571828..6269e9edc 100644 --- a/src/redux/actions/__tests__/platformActions.test.js +++ b/src/redux/actions/__tests__/platformActions.test.js @@ -1,7 +1,7 @@ import { applyMiddleware, combineReducers, legacy_createStore as createStore } from 'redux'; import moxios from 'moxios'; import { promiseMiddleware } from '../../middleware'; -import { platformActions, setExportStatus } from '../platformActions'; +import { platformActions } from '../platformActions'; import { appReducer } from '../../reducers'; describe('PlatformActions', () => { @@ -43,41 +43,33 @@ describe('PlatformActions', () => { }); }); - it('Should return response content for createExport method', done => { - const store = generateStore(); - const dispatcher = platformActions.createExport(); - - dispatcher(store.dispatch).then(() => { - const response = store.getState().app; - expect(response.exports.fulfilled).toBe(true); - done(); - }); - }); - - it('Should return response content for getExport method', () => { + it('Should return response content for createExport method', () => { const mockDispatch = jest.fn(); - platformActions.getExport()(mockDispatch); + platformActions.createExport(undefined, undefined, { + poll: { location: undefined, status: undefined, validate: undefined } + })(mockDispatch); expect(mockDispatch.mock.calls).toMatchSnapshot('dispatch object'); }); - it('Should return response content for setExportStatus method', () => { + it('Should return response content for getExistingExports method', () => { const mockDispatch = jest.fn(); - setExportStatus(mockDispatch)(); + platformActions.getExistingExports([])(mockDispatch); expect(mockDispatch.mock.calls).toMatchSnapshot('dispatch object'); }); - it('Should return response content for getExportStatus method', done => { - const store = generateStore(); - const dispatcher = platformActions.getExportStatus(); - - dispatcher(store.dispatch).then(() => { - const response = store.getState().app; - expect(response.exports.fulfilled).toBe(true); - done(); - }); + it('Should return response content for getExistingExportsStatus method', () => { + const mockDispatch = jest.fn(); + platformActions.getExistingExportsStatus()(mockDispatch); + expect(mockDispatch.mock.calls).toMatchSnapshot('dispatch object'); }); it('Should return a dispatch object for the hideGlobalFilter method', () => { expect(platformActions.hideGlobalFilter()).toMatchSnapshot('dispatch object'); }); + + it('Should return response content for removeExistingExports method', () => { + const mockDispatch = jest.fn(); + platformActions.deleteExistingExports([])(mockDispatch); + expect(mockDispatch.mock.calls).toMatchSnapshot('dispatch object'); + }); }); diff --git a/src/redux/actions/platformActions.js b/src/redux/actions/platformActions.js index 5bb2492c0..7a83d65eb 100644 --- a/src/redux/actions/platformActions.js +++ b/src/redux/actions/platformActions.js @@ -19,7 +19,12 @@ import { platformServices } from '../../services/platform/platformServices'; * @param {object} data * @returns {*} */ -const addNotification = data => RcsAddNotification(data); +const addNotification = data => dispatch => { + if (data.id) { + dispatch(RcsRemoveNotification(data.id)); + } + return dispatch(RcsAddNotification(data)); +}; /** * Remove a platform plugin toast notification. @@ -27,14 +32,14 @@ const addNotification = data => RcsAddNotification(data); * @param {string} id * @returns {*} */ -const removeNotification = id => RcsRemoveNotification(id); +const removeNotification = id => dispatch => dispatch(RcsRemoveNotification(id)); /** * Clear all platform plugin toast notifications. * * @returns {*} */ -const clearNotifications = () => RcsClearNotifications(); +const clearNotifications = () => dispatch => dispatch(RcsClearNotifications()); /** * Get an emulated and combined API response from the platforms "getUser" and "getUserPermissions" global methods. @@ -49,64 +54,74 @@ const authorizeUser = appName => dispatch => }); /** - * Get a specific export download package. + * Get all existing exports, if pending poll, and when complete download. Includes toast notifications. * - * @param {string} id + * @param {Array} existingExports + * @param {object} notifications Apply notification options * @returns {Function} */ -const getExport = id => dispatch => - dispatch({ - type: platformTypes.GET_PLATFORM_EXPORT, - payload: platformServices.getExport(id) - }); +const getExistingExports = + (existingExports, notifications = {}) => + dispatch => + dispatch({ + type: platformTypes.GET_PLATFORM_EXPORT_EXISTING, + payload: platformServices.getExistingExports(existingExports), + meta: { + notifications + } + }); /** - * Return a "dispatch ready" export poll status check. + * Delete all existing exports. Includes toast notifications. * - * @param {Function} dispatch + * @param {Array<{ id: string }>} existingExports + * @param {object} notifications * @returns {Function} */ -const setExportStatus = - dispatch => - (success = {}, error) => - dispatch({ - type: platformTypes.SET_PLATFORM_EXPORT_STATUS, - payload: (error && Promise.reject(error)) || Promise.resolve(success) - }); +const deleteExistingExports = (existingExports, notifications) => dispatch => + dispatch({ + type: platformTypes.DELETE_PLATFORM_EXPORT_EXISTING, + payload: Promise.all(existingExports.map(({ id }) => platformServices.deleteExport(id))), + meta: { + notifications + } + }); /** - * Get a specific, or all, export status. + * Get a status from any existing exports. Display a confirmation for downloading, or ignoring, the exports. + * Includes toast notifications. * - * @param {object} options Apply polling options + * @param {object} notifications * @returns {Function} */ -const getExportStatus = - (options = {}) => - dispatch => - dispatch({ - type: platformTypes.SET_PLATFORM_EXPORT_STATUS, - payload: platformServices.getExportStatus(undefined, undefined, { - ...options, - poll: { ...options.poll, status: setExportStatus(dispatch) } - }) - }); +const getExistingExportsStatus = notifications => dispatch => + dispatch({ + type: platformTypes.SET_PLATFORM_EXPORT_EXISTING_STATUS, + payload: platformServices.getExistingExportsStatus(), + meta: { + notifications + } + }); /** - * Create an export for download. + * Create an export for download. Includes toast notifications. * + * @param {string} id * @param {object} data * @param {object} options Apply polling options + * @param {object} notifications * @returns {Function} */ const createExport = - (data = {}, options = {}) => + (id, data = {}, options = {}, notifications = {}) => dispatch => dispatch({ - type: platformTypes.SET_PLATFORM_EXPORT_STATUS, - payload: platformServices.postExport(data, { - ...options, - poll: { ...options.poll, status: setExportStatus(dispatch) } - }) + type: platformTypes.SET_PLATFORM_EXPORT_CREATE, + payload: platformServices.postExport(data, options), + meta: { + id, + notifications + } }); /** @@ -126,9 +141,9 @@ const platformActions = { clearNotifications, authorizeUser, createExport, - getExport, - setExportStatus, - getExportStatus, + deleteExistingExports, + getExistingExports, + getExistingExportsStatus, hideGlobalFilter }; @@ -140,8 +155,8 @@ export { clearNotifications, authorizeUser, createExport, - getExport, - setExportStatus, - getExportStatus, + deleteExistingExports, + getExistingExports, + getExistingExportsStatus, hideGlobalFilter }; diff --git a/src/redux/reducers/__tests__/__snapshots__/appReducer.test.js.snap b/src/redux/reducers/__tests__/__snapshots__/appReducer.test.js.snap index caf968bda..9aa6f5cc6 100644 --- a/src/redux/reducers/__tests__/__snapshots__/appReducer.test.js.snap +++ b/src/redux/reducers/__tests__/__snapshots__/appReducer.test.js.snap @@ -6,6 +6,7 @@ exports[`UserReducer should handle all defined error types: rejected types DELET "auth": {}, "errors": {}, "exports": {}, + "exportsExisting": {}, "locale": {}, "optin": { "error": true, @@ -26,6 +27,7 @@ exports[`UserReducer should handle all defined error types: rejected types GET_U "auth": {}, "errors": {}, "exports": {}, + "exportsExisting": {}, "locale": {}, "optin": { "error": true, @@ -53,6 +55,7 @@ exports[`UserReducer should handle all defined error types: rejected types PLATF }, "errors": {}, "exports": {}, + "exportsExisting": {}, "locale": {}, "optin": {}, }, @@ -60,12 +63,13 @@ exports[`UserReducer should handle all defined error types: rejected types PLATF } `; -exports[`UserReducer should handle all defined error types: rejected types SET_PLATFORM_EXPORT_STATUS 1`] = ` +exports[`UserReducer should handle all defined error types: rejected types SET_PLATFORM_EXPORT_EXISTING_STATUS 1`] = ` { "result": { "auth": {}, "errors": {}, - "exports": { + "exports": {}, + "exportsExisting": { "error": true, "errorMessage": "MESSAGE", "fulfilled": false, @@ -76,7 +80,7 @@ exports[`UserReducer should handle all defined error types: rejected types SET_P "locale": {}, "optin": {}, }, - "type": "SET_PLATFORM_EXPORT_STATUS_REJECTED", + "type": "SET_PLATFORM_EXPORT_EXISTING_STATUS_REJECTED", } `; @@ -86,6 +90,7 @@ exports[`UserReducer should handle all defined error types: rejected types UPDAT "auth": {}, "errors": {}, "exports": {}, + "exportsExisting": {}, "locale": {}, "optin": { "error": true, @@ -106,6 +111,7 @@ exports[`UserReducer should handle all defined error types: rejected types USER_ "auth": {}, "errors": {}, "exports": {}, + "exportsExisting": {}, "locale": { "error": true, "errorMessage": "MESSAGE", @@ -126,6 +132,7 @@ exports[`UserReducer should handle all defined fulfilled types: fulfilled types "auth": {}, "errors": {}, "exports": {}, + "exportsExisting": {}, "locale": {}, "optin": { "data": { @@ -158,6 +165,7 @@ exports[`UserReducer should handle all defined fulfilled types: fulfilled types "auth": {}, "errors": {}, "exports": {}, + "exportsExisting": {}, "locale": {}, "optin": { "data": { @@ -209,6 +217,7 @@ exports[`UserReducer should handle all defined fulfilled types: fulfilled types }, "errors": {}, "exports": {}, + "exportsExisting": {}, "locale": {}, "optin": {}, }, @@ -216,12 +225,13 @@ exports[`UserReducer should handle all defined fulfilled types: fulfilled types } `; -exports[`UserReducer should handle all defined fulfilled types: fulfilled types SET_PLATFORM_EXPORT_STATUS 1`] = ` +exports[`UserReducer should handle all defined fulfilled types: fulfilled types SET_PLATFORM_EXPORT_EXISTING_STATUS 1`] = ` { "result": { "auth": {}, "errors": {}, - "exports": { + "exports": {}, + "exportsExisting": { "data": { "permissions": [], "test": "success", @@ -244,7 +254,7 @@ exports[`UserReducer should handle all defined fulfilled types: fulfilled types "locale": {}, "optin": {}, }, - "type": "SET_PLATFORM_EXPORT_STATUS_FULFILLED", + "type": "SET_PLATFORM_EXPORT_EXISTING_STATUS_FULFILLED", } `; @@ -254,6 +264,7 @@ exports[`UserReducer should handle all defined fulfilled types: fulfilled types "auth": {}, "errors": {}, "exports": {}, + "exportsExisting": {}, "locale": {}, "optin": { "data": { @@ -286,6 +297,7 @@ exports[`UserReducer should handle all defined fulfilled types: fulfilled types "auth": {}, "errors": {}, "exports": {}, + "exportsExisting": {}, "locale": { "data": { "permissions": [], @@ -318,6 +330,7 @@ exports[`UserReducer should handle all defined pending types: pending types DELE "auth": {}, "errors": {}, "exports": {}, + "exportsExisting": {}, "locale": {}, "optin": { "error": false, @@ -337,6 +350,7 @@ exports[`UserReducer should handle all defined pending types: pending types GET_ "auth": {}, "errors": {}, "exports": {}, + "exportsExisting": {}, "locale": {}, "optin": { "error": false, @@ -362,6 +376,7 @@ exports[`UserReducer should handle all defined pending types: pending types PLAT }, "errors": {}, "exports": {}, + "exportsExisting": {}, "locale": {}, "optin": {}, }, @@ -369,12 +384,13 @@ exports[`UserReducer should handle all defined pending types: pending types PLAT } `; -exports[`UserReducer should handle all defined pending types: pending types SET_PLATFORM_EXPORT_STATUS 1`] = ` +exports[`UserReducer should handle all defined pending types: pending types SET_PLATFORM_EXPORT_EXISTING_STATUS 1`] = ` { "result": { "auth": {}, "errors": {}, - "exports": { + "exports": {}, + "exportsExisting": { "error": false, "errorMessage": "", "fulfilled": false, @@ -384,7 +400,7 @@ exports[`UserReducer should handle all defined pending types: pending types SET_ "locale": {}, "optin": {}, }, - "type": "SET_PLATFORM_EXPORT_STATUS_PENDING", + "type": "SET_PLATFORM_EXPORT_EXISTING_STATUS_PENDING", } `; @@ -394,6 +410,7 @@ exports[`UserReducer should handle all defined pending types: pending types UPDA "auth": {}, "errors": {}, "exports": {}, + "exportsExisting": {}, "locale": {}, "optin": { "error": false, @@ -413,6 +430,7 @@ exports[`UserReducer should handle all defined pending types: pending types USER "auth": {}, "errors": {}, "exports": {}, + "exportsExisting": {}, "locale": { "error": false, "errorMessage": "", @@ -426,12 +444,32 @@ exports[`UserReducer should handle all defined pending types: pending types USER } `; +exports[`UserReducer should handle specific defined types: defined type SET_PLATFORM_EXPORT_STATUS 1`] = ` +{ + "result": { + "auth": {}, + "errors": {}, + "exports": { + "test_id": { + "isPending": true, + "pending": [], + }, + }, + "exportsExisting": {}, + "locale": {}, + "optin": {}, + }, + "type": "SET_PLATFORM_EXPORT_STATUS", +} +`; + exports[`UserReducer should handle specific http status types: http status 4XX 400 1`] = ` { "result": { "auth": {}, "errors": {}, "exports": {}, + "exportsExisting": {}, "locale": {}, "optin": {}, }, @@ -452,6 +490,7 @@ exports[`UserReducer should handle specific http status types: http status 4XX 4 "status": 401, }, "exports": {}, + "exportsExisting": {}, "locale": {}, "optin": {}, }, @@ -472,6 +511,7 @@ exports[`UserReducer should handle specific http status types: http status 4XX 4 "status": 403, }, "exports": {}, + "exportsExisting": {}, "locale": {}, "optin": {}, }, diff --git a/src/redux/reducers/__tests__/appReducer.test.js b/src/redux/reducers/__tests__/appReducer.test.js index 6038396ea..444391841 100644 --- a/src/redux/reducers/__tests__/appReducer.test.js +++ b/src/redux/reducers/__tests__/appReducer.test.js @@ -43,10 +43,31 @@ describe('UserReducer', () => { }); }); + it('should handle specific defined types', () => { + const specificTypes = [platformTypes.SET_PLATFORM_EXPORT_STATUS]; + + specificTypes.forEach(value => { + if (!value) { + return; + } + + const dispatched = { + type: value, + id: 'test_id', + isPending: true, + pending: [] + }; + + const resultState = appReducer(undefined, dispatched); + + expect({ type: value, result: resultState }).toMatchSnapshot(`defined type ${value}`); + }); + }); + it('should handle all defined error types', () => { const specificTypes = [ platformTypes.PLATFORM_USER_AUTH, - platformTypes.SET_PLATFORM_EXPORT_STATUS, + platformTypes.SET_PLATFORM_EXPORT_EXISTING_STATUS, types.USER_LOCALE, types.DELETE_USER_OPTIN, types.GET_USER_OPTIN, @@ -80,7 +101,7 @@ describe('UserReducer', () => { it('should handle all defined pending types', () => { const specificTypes = [ platformTypes.PLATFORM_USER_AUTH, - platformTypes.SET_PLATFORM_EXPORT_STATUS, + platformTypes.SET_PLATFORM_EXPORT_EXISTING_STATUS, types.USER_LOCALE, types.DELETE_USER_OPTIN, types.GET_USER_OPTIN, @@ -103,7 +124,7 @@ describe('UserReducer', () => { it('should handle all defined fulfilled types', () => { const specificTypes = [ platformTypes.PLATFORM_USER_AUTH, - platformTypes.SET_PLATFORM_EXPORT_STATUS, + platformTypes.SET_PLATFORM_EXPORT_EXISTING_STATUS, types.USER_LOCALE, types.DELETE_USER_OPTIN, types.GET_USER_OPTIN, diff --git a/src/redux/reducers/appReducer.js b/src/redux/reducers/appReducer.js index 09e8daa97..6eed96eb7 100644 --- a/src/redux/reducers/appReducer.js +++ b/src/redux/reducers/appReducer.js @@ -14,12 +14,13 @@ import { reduxHelpers } from '../common'; * Initial state. * * @private - * @type {{auth: {}, exports: {}, optin: {}, locale: {}, errors: {}}} + * @type {{auth: {}, exports: {}, exportsExisting: {}, optin: {}, locale: {}, errors: {}}} */ const initialState = { auth: {}, errors: {}, exports: {}, + exportsExisting: {}, locale: {}, optin: {} }; @@ -55,13 +56,26 @@ const appReducer = (state = initialState, action) => { } return state; - + case platformTypes.SET_PLATFORM_EXPORT_STATUS: + return reduxHelpers.setStateProp( + 'exports', + { + [action.id]: { + isPending: action.isPending, + pending: action.pending + } + }, + { + state, + initialState + } + ); default: return reduxHelpers.generatedPromiseActionReducer( [ { ref: 'locale', type: appTypes.USER_LOCALE }, { ref: 'optin', type: [appTypes.DELETE_USER_OPTIN, appTypes.GET_USER_OPTIN, appTypes.UPDATE_USER_OPTIN] }, - { ref: 'exports', type: platformTypes.SET_PLATFORM_EXPORT_STATUS }, + { ref: 'exportsExisting', type: platformTypes.SET_PLATFORM_EXPORT_EXISTING_STATUS }, { ref: 'auth', type: platformTypes.PLATFORM_USER_AUTH } ], state, diff --git a/src/redux/types/__tests__/__snapshots__/index.test.js.snap b/src/redux/types/__tests__/__snapshots__/index.test.js.snap index 7d4a7792e..a56c47b25 100644 --- a/src/redux/types/__tests__/__snapshots__/index.test.js.snap +++ b/src/redux/types/__tests__/__snapshots__/index.test.js.snap @@ -36,12 +36,15 @@ exports[`ReduxTypes should have specific type properties: all redux types 1`] = "SET_BANNER_MESSAGES": "SET_BANNER_MESSAGES", }, "platform": { - "GET_PLATFORM_EXPORT": "GET_PLATFORM_EXPORT", + "DELETE_PLATFORM_EXPORT_EXISTING": "DELETE_PLATFORM_EXPORT_EXISTING", + "GET_PLATFORM_EXPORT_EXISTING": "GET_PLATFORM_EXPORT_EXISTING", "PLATFORM_ADD_NOTIFICATION": "@@INSIGHTS-CORE/NOTIFICATIONS/ADD_NOTIFICATION", "PLATFORM_CLEAR_NOTIFICATIONS": "@@INSIGHTS-CORE/NOTIFICATIONS/CLEAR_NOTIFICATIONS", "PLATFORM_GLOBAL_FILTER_HIDE": "PLATFORM_GLOBAL_FILTER_HIDE", "PLATFORM_REMOVE_NOTIFICATION": "@@INSIGHTS-CORE/NOTIFICATIONS/REMOVE_NOTIFICATION", "PLATFORM_USER_AUTH": "PLATFORM_USER_AUTH", + "SET_PLATFORM_EXPORT_CREATE": "SET_PLATFORM_EXPORT_CREATE", + "SET_PLATFORM_EXPORT_EXISTING_STATUS": "SET_PLATFORM_EXPORT_EXISTING_STATUS", "SET_PLATFORM_EXPORT_STATUS": "SET_PLATFORM_EXPORT_STATUS", }, "query": { @@ -78,12 +81,15 @@ exports[`ReduxTypes should have specific type properties: all redux types 1`] = "SET_BANNER_MESSAGES": "SET_BANNER_MESSAGES", }, "platformTypes": { - "GET_PLATFORM_EXPORT": "GET_PLATFORM_EXPORT", + "DELETE_PLATFORM_EXPORT_EXISTING": "DELETE_PLATFORM_EXPORT_EXISTING", + "GET_PLATFORM_EXPORT_EXISTING": "GET_PLATFORM_EXPORT_EXISTING", "PLATFORM_ADD_NOTIFICATION": "@@INSIGHTS-CORE/NOTIFICATIONS/ADD_NOTIFICATION", "PLATFORM_CLEAR_NOTIFICATIONS": "@@INSIGHTS-CORE/NOTIFICATIONS/CLEAR_NOTIFICATIONS", "PLATFORM_GLOBAL_FILTER_HIDE": "PLATFORM_GLOBAL_FILTER_HIDE", "PLATFORM_REMOVE_NOTIFICATION": "@@INSIGHTS-CORE/NOTIFICATIONS/REMOVE_NOTIFICATION", "PLATFORM_USER_AUTH": "PLATFORM_USER_AUTH", + "SET_PLATFORM_EXPORT_CREATE": "SET_PLATFORM_EXPORT_CREATE", + "SET_PLATFORM_EXPORT_EXISTING_STATUS": "SET_PLATFORM_EXPORT_EXISTING_STATUS", "SET_PLATFORM_EXPORT_STATUS": "SET_PLATFORM_EXPORT_STATUS", }, "queryTypes": { @@ -120,12 +126,15 @@ exports[`ReduxTypes should have specific type properties: all redux types 1`] = "SET_BANNER_MESSAGES": "SET_BANNER_MESSAGES", }, "platform": { - "GET_PLATFORM_EXPORT": "GET_PLATFORM_EXPORT", + "DELETE_PLATFORM_EXPORT_EXISTING": "DELETE_PLATFORM_EXPORT_EXISTING", + "GET_PLATFORM_EXPORT_EXISTING": "GET_PLATFORM_EXPORT_EXISTING", "PLATFORM_ADD_NOTIFICATION": "@@INSIGHTS-CORE/NOTIFICATIONS/ADD_NOTIFICATION", "PLATFORM_CLEAR_NOTIFICATIONS": "@@INSIGHTS-CORE/NOTIFICATIONS/CLEAR_NOTIFICATIONS", "PLATFORM_GLOBAL_FILTER_HIDE": "PLATFORM_GLOBAL_FILTER_HIDE", "PLATFORM_REMOVE_NOTIFICATION": "@@INSIGHTS-CORE/NOTIFICATIONS/REMOVE_NOTIFICATION", "PLATFORM_USER_AUTH": "PLATFORM_USER_AUTH", + "SET_PLATFORM_EXPORT_CREATE": "SET_PLATFORM_EXPORT_CREATE", + "SET_PLATFORM_EXPORT_EXISTING_STATUS": "SET_PLATFORM_EXPORT_EXISTING_STATUS", "SET_PLATFORM_EXPORT_STATUS": "SET_PLATFORM_EXPORT_STATUS", }, "query": { @@ -189,12 +198,15 @@ exports[`ReduxTypes should have specific type properties: specific types 1`] = ` "SET_BANNER_MESSAGES": "SET_BANNER_MESSAGES", }, "platform": { - "GET_PLATFORM_EXPORT": "GET_PLATFORM_EXPORT", + "DELETE_PLATFORM_EXPORT_EXISTING": "DELETE_PLATFORM_EXPORT_EXISTING", + "GET_PLATFORM_EXPORT_EXISTING": "GET_PLATFORM_EXPORT_EXISTING", "PLATFORM_ADD_NOTIFICATION": "@@INSIGHTS-CORE/NOTIFICATIONS/ADD_NOTIFICATION", "PLATFORM_CLEAR_NOTIFICATIONS": "@@INSIGHTS-CORE/NOTIFICATIONS/CLEAR_NOTIFICATIONS", "PLATFORM_GLOBAL_FILTER_HIDE": "PLATFORM_GLOBAL_FILTER_HIDE", "PLATFORM_REMOVE_NOTIFICATION": "@@INSIGHTS-CORE/NOTIFICATIONS/REMOVE_NOTIFICATION", "PLATFORM_USER_AUTH": "PLATFORM_USER_AUTH", + "SET_PLATFORM_EXPORT_CREATE": "SET_PLATFORM_EXPORT_CREATE", + "SET_PLATFORM_EXPORT_EXISTING_STATUS": "SET_PLATFORM_EXPORT_EXISTING_STATUS", "SET_PLATFORM_EXPORT_STATUS": "SET_PLATFORM_EXPORT_STATUS", }, "query": { diff --git a/src/redux/types/index.js b/src/redux/types/index.js index 329be66d9..8d7ea5202 100644 --- a/src/redux/types/index.js +++ b/src/redux/types/index.js @@ -59,9 +59,10 @@ const messageTypes = { /** * Platform action, reducer types. * - * @type {{PLATFORM_USER_AUTH: string, SET_PLATFORM_EXPORT_STATUS: string, PLATFORM_GLOBAL_FILTER_HIDE: string, - * PLATFORM_CLEAR_NOTIFICATIONS: string, PLATFORM_ADD_NOTIFICATION: string, PLATFORM_REMOVE_NOTIFICATION: string, - * GET_PLATFORM_EXPORT: string}} + * @type {{PLATFORM_USER_AUTH: string, DELETE_PLATFORM_EXPORT_EXISTING: string, SET_PLATFORM_EXPORT_EXISTING_STATUS: + * string, SET_PLATFORM_EXPORT_STATUS: string, PLATFORM_GLOBAL_FILTER_HIDE: string, PLATFORM_CLEAR_NOTIFICATIONS: + * string, GET_PLATFORM_EXPORT_EXISTING: string, PLATFORM_ADD_NOTIFICATION: string, PLATFORM_REMOVE_NOTIFICATION: + * string, SET_PLATFORM_EXPORT_CREATE: string}} */ const platformTypes = { PLATFORM_ADD_NOTIFICATION: ADD_NOTIFICATION, @@ -69,8 +70,11 @@ const platformTypes = { PLATFORM_CLEAR_NOTIFICATIONS: CLEAR_NOTIFICATIONS, PLATFORM_GLOBAL_FILTER_HIDE: 'PLATFORM_GLOBAL_FILTER_HIDE', PLATFORM_USER_AUTH: 'PLATFORM_USER_AUTH', - GET_PLATFORM_EXPORT: 'GET_PLATFORM_EXPORT', - SET_PLATFORM_EXPORT_STATUS: 'SET_PLATFORM_EXPORT_STATUS' + DELETE_PLATFORM_EXPORT_EXISTING: 'DELETE_PLATFORM_EXPORT_EXISTING', + GET_PLATFORM_EXPORT_EXISTING: 'GET_PLATFORM_EXPORT_EXISTING', + SET_PLATFORM_EXPORT_EXISTING_STATUS: 'SET_PLATFORM_EXPORT_EXISTING_STATUS', + SET_PLATFORM_EXPORT_STATUS: 'SET_PLATFORM_EXPORT_STATUS', + SET_PLATFORM_EXPORT_CREATE: 'SET_PLATFORM_EXPORT_CREATE' }; /** diff --git a/src/services/README.md b/src/services/README.md index b7712a8bc..4f7d16888 100644 --- a/src/services/README.md +++ b/src/services/README.md @@ -253,6 +253,7 @@ page or wait the "maxAge". * [~PLATFORM_API_EXPORT_STATUS_TYPES](#Platform.module_PlatformConstants..PLATFORM_API_EXPORT_STATUS_TYPES) : Object * [~PLATFORM_API_EXPORT_SOURCE_TYPES](#Platform.module_PlatformConstants..PLATFORM_API_EXPORT_SOURCE_TYPES) : Object * [~PLATFORM_API_EXPORT_POST_TYPES](#Platform.module_PlatformConstants..PLATFORM_API_EXPORT_POST_TYPES) : Object + * [~PLATFORM_API_EXPORT_POST_SUBSCRIPTIONS_FILTER_TYPES](#Platform.module_PlatformConstants..PLATFORM_API_EXPORT_POST_SUBSCRIPTIONS_FILTER_TYPES) : Object * [~PLATFORM_API_EXPORT_RESPONSE_TYPES](#Platform.module_PlatformConstants..PLATFORM_API_EXPORT_RESPONSE_TYPES) : Object * [~PLATFORM_API_RESPONSE_USER_ENTITLEMENTS](#Platform.module_PlatformConstants..PLATFORM_API_RESPONSE_USER_ENTITLEMENTS) : string * [~PLATFORM_API_RESPONSE_USER_ENTITLEMENTS_APP_TYPES](#Platform.module_PlatformConstants..PLATFORM_API_RESPONSE_USER_ENTITLEMENTS_APP_TYPES) : Object @@ -314,6 +315,12 @@ Platform Export, available response, POST source types. ### PlatformConstants~PLATFORM\_API\_EXPORT\_POST\_TYPES : Object Platform Export, available POST types. +**Kind**: inner constant of [PlatformConstants](#Platform.module_PlatformConstants) + + +### PlatformConstants~PLATFORM\_API\_EXPORT\_POST\_SUBSCRIPTIONS\_FILTER\_TYPES : Object +Platform Export, available SUBSCRIPTION FILTER POST types. + **Kind**: inner constant of [PlatformConstants](#Platform.module_PlatformConstants) @@ -419,8 +426,10 @@ Emulated service calls for platform globals. * [~getUser(options)](#Platform.module_PlatformServices..getUser) ⇒ Promise.<\*> * [~getUserPermissions(appName, options)](#Platform.module_PlatformServices..getUserPermissions) ⇒ Promise.<\*> * [~hideGlobalFilter(isHidden)](#Platform.module_PlatformServices..hideGlobalFilter) ⇒ Promise.<\*> + * [~deleteExport(id, options)](#Platform.module_PlatformServices..deleteExport) ⇒ Promise.<\*> + * [~getExistingExportsStatus(id, params, options)](#Platform.module_PlatformServices..getExistingExportsStatus) ⇒ Promise.<\*> * [~getExport(id, options)](#Platform.module_PlatformServices..getExport) ⇒ Promise.<\*> - * [~getExportStatus(id, params, options)](#Platform.module_PlatformServices..getExportStatus) ⇒ Promise.<\*> + * [~getExistingExports(idList, params, options)](#Platform.module_PlatformServices..getExistingExports) ⇒ Promise.<\*> * [~postExport(data, options)](#Platform.module_PlatformServices..postExport) ⇒ Promise.<\*> @@ -479,6 +488,58 @@ Disables the Platform's global filter display. + + +### PlatformServices~deleteExport(id, options) ⇒ Promise.<\*> +Delete an export. Useful for clean up. Helps avoid having to deal with export lists and most recent exports. + +**Kind**: inner method of [PlatformServices](#Platform.module_PlatformServices) + + + + + + + + + + + + + + + + +
ParamTypeDescription
idstring

ID of export to delete

+
optionsobject
options.cancelboolean
options.cancelIdstring
+ + + +### PlatformServices~getExistingExportsStatus(id, params, options) ⇒ Promise.<\*> +Get multiple export status, or a single status after setup. + +**Kind**: inner method of [PlatformServices](#Platform.module_PlatformServices) + + + + + + + + + + + + + + + + + + +
ParamTypeDescription
idstring | undefined | null

Export ID

+
paramsobject
optionsobject
options.cancelboolean
options.cancelIdstring
+ ### PlatformServices~getExport(id, options) ⇒ Promise.<\*> @@ -501,13 +562,17 @@ Get an export after setup. options.cancelboolean options.cancelIdstring + + options.fileNamestring + + options.fileTypestring - + -### PlatformServices~getExportStatus(id, params, options) ⇒ Promise.<\*> -Get multiple export status, or a single status after setup. +### PlatformServices~getExistingExports(idList, params, options) ⇒ Promise.<\*> +Convenience wrapper for setting up global export status with status polling, and download with clean-up. **Kind**: inner method of [PlatformServices](#Platform.module_PlatformServices) @@ -518,7 +583,7 @@ Get multiple export status, or a single status after setup. - @@ -534,7 +599,7 @@ Get multiple export status, or a single status after setup. ### PlatformServices~postExport(data, options) ⇒ Promise.<\*> -Post to create an export. +Convenience wrapper for posting to create an export with status polling, then performing a download with clean-up. **Kind**: inner method of [PlatformServices](#Platform.module_PlatformServices)
idstring | undefined | null

Export ID

+
idListArray.<{id: string, fileName: string}>

A list of export IDs to finish

paramsobject
diff --git a/src/services/platform/__tests__/__snapshots__/platformConstants.test.js.snap b/src/services/platform/__tests__/__snapshots__/platformConstants.test.js.snap index af4282025..b615a5646 100644 --- a/src/services/platform/__tests__/__snapshots__/platformConstants.test.js.snap +++ b/src/services/platform/__tests__/__snapshots__/platformConstants.test.js.snap @@ -6,10 +6,18 @@ exports[`Platform Constants should have specific properties: all exported consta "SUBSCRIPTIONS": "subscriptions", }, "PLATFORM_API_EXPORT_CONTENT_TYPES": { - "CSV": "csv", "JSON": "json", }, "PLATFORM_API_EXPORT_FILENAME_PREFIX": "swatch", + "PLATFORM_API_EXPORT_POST_SUBSCRIPTIONS_FILTER_TYPES": { + "BILLING_ACCOUNT_ID": "billing_account_id", + "BILLING_PROVIDER": "billing_provider", + "CATEGORY": "category", + "METRIC_ID": "metric_id", + "PRODUCT_ID": "product_id", + "SLA": "sla", + "USAGE": "usage", + }, "PLATFORM_API_EXPORT_POST_TYPES": { "EXPIRES_AT": "expires_at", "FORMAT": "format", @@ -35,7 +43,7 @@ exports[`Platform Constants should have specific properties: all exported consta "RESOURCE": "resource", }, "PLATFORM_API_EXPORT_STATUS_TYPES": { - "COMPLETED": "completed", + "COMPLETE": "complete", "FAILED": "failed", "PARTIAL": "partial", "PENDING": "pending", @@ -72,10 +80,18 @@ exports[`Platform Constants should have specific properties: all exported consta "SUBSCRIPTIONS": "subscriptions", }, "PLATFORM_API_EXPORT_CONTENT_TYPES": { - "CSV": "csv", "JSON": "json", }, "PLATFORM_API_EXPORT_FILENAME_PREFIX": "swatch", + "PLATFORM_API_EXPORT_POST_SUBSCRIPTIONS_FILTER_TYPES": { + "BILLING_ACCOUNT_ID": "billing_account_id", + "BILLING_PROVIDER": "billing_provider", + "CATEGORY": "category", + "METRIC_ID": "metric_id", + "PRODUCT_ID": "product_id", + "SLA": "sla", + "USAGE": "usage", + }, "PLATFORM_API_EXPORT_POST_TYPES": { "EXPIRES_AT": "expires_at", "FORMAT": "format", @@ -101,7 +117,7 @@ exports[`Platform Constants should have specific properties: all exported consta "RESOURCE": "resource", }, "PLATFORM_API_EXPORT_STATUS_TYPES": { - "COMPLETED": "completed", + "COMPLETE": "complete", "FAILED": "failed", "PARTIAL": "partial", "PENDING": "pending", @@ -139,10 +155,18 @@ exports[`Platform Constants should have specific properties: all exported consta "SUBSCRIPTIONS": "subscriptions", }, "PLATFORM_API_EXPORT_CONTENT_TYPES": { - "CSV": "csv", "JSON": "json", }, "PLATFORM_API_EXPORT_FILENAME_PREFIX": "swatch", + "PLATFORM_API_EXPORT_POST_SUBSCRIPTIONS_FILTER_TYPES": { + "BILLING_ACCOUNT_ID": "billing_account_id", + "BILLING_PROVIDER": "billing_provider", + "CATEGORY": "category", + "METRIC_ID": "metric_id", + "PRODUCT_ID": "product_id", + "SLA": "sla", + "USAGE": "usage", + }, "PLATFORM_API_EXPORT_POST_TYPES": { "EXPIRES_AT": "expires_at", "FORMAT": "format", @@ -168,7 +192,7 @@ exports[`Platform Constants should have specific properties: all exported consta "RESOURCE": "resource", }, "PLATFORM_API_EXPORT_STATUS_TYPES": { - "COMPLETED": "completed", + "COMPLETE": "complete", "FAILED": "failed", "PARTIAL": "partial", "PENDING": "pending", @@ -210,10 +234,18 @@ exports[`Platform Constants should have specific properties: specific constants "SUBSCRIPTIONS": "subscriptions", }, "PLATFORM_API_EXPORT_CONTENT_TYPES": { - "CSV": "csv", "JSON": "json", }, "PLATFORM_API_EXPORT_FILENAME_PREFIX": "swatch", + "PLATFORM_API_EXPORT_POST_SUBSCRIPTIONS_FILTER_TYPES": { + "BILLING_ACCOUNT_ID": "billing_account_id", + "BILLING_PROVIDER": "billing_provider", + "CATEGORY": "category", + "METRIC_ID": "metric_id", + "PRODUCT_ID": "product_id", + "SLA": "sla", + "USAGE": "usage", + }, "PLATFORM_API_EXPORT_POST_TYPES": { "EXPIRES_AT": "expires_at", "FORMAT": "format", @@ -239,7 +271,7 @@ exports[`Platform Constants should have specific properties: specific constants "RESOURCE": "resource", }, "PLATFORM_API_EXPORT_STATUS_TYPES": { - "COMPLETED": "completed", + "COMPLETE": "complete", "FAILED": "failed", "PARTIAL": "partial", "PENDING": "pending", diff --git a/src/services/platform/__tests__/__snapshots__/platformServices.test.js.snap b/src/services/platform/__tests__/__snapshots__/platformServices.test.js.snap index 681d3f34b..828416de1 100644 --- a/src/services/platform/__tests__/__snapshots__/platformServices.test.js.snap +++ b/src/services/platform/__tests__/__snapshots__/platformServices.test.js.snap @@ -1,5 +1,18 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`PlatformServices should export a specific properties, methods and classes: properties 1`] = ` +{ + "deleteExport": [Function], + "getExistingExports": [Function], + "getExistingExportsStatus": [Function], + "getExport": [Function], + "getUser": [Function], + "getUserPermissions": [Function], + "hideGlobalFilter": [Function], + "postExport": [Function], +} +`; + exports[`PlatformServices should return a failed getUser: failed authorized user 1`] = ` { "data": undefined, diff --git a/src/services/platform/__tests__/__snapshots__/platformTransformers.test.js.snap b/src/services/platform/__tests__/__snapshots__/platformTransformers.test.js.snap index 4d10fabd6..068dbec7e 100644 --- a/src/services/platform/__tests__/__snapshots__/platformTransformers.test.js.snap +++ b/src/services/platform/__tests__/__snapshots__/platformTransformers.test.js.snap @@ -69,7 +69,13 @@ exports[`Platform Transformers should attempt to parse a user response: user, pa exports[`Platform Transformers should attempt to parse an exports response: exports, default 1`] = ` { - "data": {}, + "data": { + "completed": [], + "isAnythingCompleted": false, + "isAnythingPending": false, + "pending": [], + "products": {}, + }, "meta": {}, } `; @@ -77,15 +83,36 @@ exports[`Platform Transformers should attempt to parse an exports response: expo exports[`Platform Transformers should attempt to parse an exports response: exports, parsed single 1`] = ` { "data": { - "RHEL for x86": [ + "completed": [], + "isAnythingCompleted": false, + "isAnythingPending": true, + "pending": [ { + "fileName": "20190720_000000_swatch_report_rhel_for_x_86", "format": undefined, "id": "0123456789", "name": "swatch-RHEL for x86", + "productId": "RHEL for x86", "status": "pending", }, ], - "isAnythingPending": true, + "products": { + "RHEL for x86": { + "completed": [], + "isCompleted": false, + "isPending": true, + "pending": [ + { + "fileName": "20190720_000000_swatch_report_rhel_for_x_86", + "format": undefined, + "id": "0123456789", + "name": "swatch-RHEL for x86", + "productId": "RHEL for x86", + "status": "pending", + }, + ], + }, + }, }, "meta": {}, } diff --git a/src/services/platform/__tests__/platformServices.test.js b/src/services/platform/__tests__/platformServices.test.js index 58bf75323..426b3ab56 100644 --- a/src/services/platform/__tests__/platformServices.test.js +++ b/src/services/platform/__tests__/platformServices.test.js @@ -29,25 +29,24 @@ describe('PlatformServices', () => { moxios.uninstall(); }); - it('should export a specific number of methods and classes', () => { - expect(Object.keys(platformServices)).toHaveLength(6); - }); - - it('should have specific methods', () => { - expect(platformServices.getUser).toBeDefined(); - expect(platformServices.getUserPermissions).toBeDefined(); - expect(platformServices.hideGlobalFilter).toBeDefined(); - expect(platformServices.postExport).toBeDefined(); - expect(platformServices.getExport).toBeDefined(); - expect(platformServices.getExportStatus).toBeDefined(); + it('should export a specific properties, methods and classes', () => { + expect(platformServices).toMatchSnapshot('properties'); }); /** - * timeout errors associated with this test sometimes stem from endpoint - * settings or missing globals, see "before" above, or the "setupTests" config + * Notes: + * - Timeout errors associated with this test sometimes stem from endpoint + * settings or missing globals, see "before" above, or the "setupTests" config + * - Reset polling for testing */ it('should return async for most methods and resolve successfully', async () => { - const promises = Object.keys(platformServices).map(value => platformServices[value]()); + const promises = Object.keys(platformServices).map(value => + platformServices[value]( + undefined, + { poll: { location: undefined, status: undefined, validate: undefined } }, + { poll: { location: undefined, status: undefined, validate: undefined } } + ) + ); const response = await Promise.all(promises); expect(response.length).toEqual(Object.keys(platformServices).length); diff --git a/src/services/platform/__tests__/platformTransformers.test.js b/src/services/platform/__tests__/platformTransformers.test.js index a0690a711..93ddc39f8 100644 --- a/src/services/platform/__tests__/platformTransformers.test.js +++ b/src/services/platform/__tests__/platformTransformers.test.js @@ -18,23 +18,25 @@ describe('Platform Transformers', () => { const parsedSingle = platformTransformers.exports({ [platformConstants.PLATFORM_API_EXPORT_RESPONSE_TYPES.NAME]: - `${platformConstants.PLATFORM_API_EXPORT_FILENAME_PREFIX}-${rhsmConstants.RHSM_API_PATH_PRODUCT_TYPES.RHEL_X86}`, + `${helpers.CONFIG_EXPORT_SERVICE_NAME_PREFIX}-${rhsmConstants.RHSM_API_PATH_PRODUCT_TYPES.RHEL_X86}`, [platformConstants.PLATFORM_API_EXPORT_RESPONSE_TYPES.EXPIRES_AT]: '2019-07-14T00:00:00Z', [platformConstants.PLATFORM_API_EXPORT_RESPONSE_TYPES.ID]: '0123456789', [platformConstants.PLATFORM_API_EXPORT_RESPONSE_TYPES.STATUS]: platformConstants.PLATFORM_API_EXPORT_STATUS_TYPES.PENDING }); - const parsedArray = platformTransformers.exports([ - { - [platformConstants.PLATFORM_API_EXPORT_RESPONSE_TYPES.NAME]: - `${platformConstants.PLATFORM_API_EXPORT_FILENAME_PREFIX}-${rhsmConstants.RHSM_API_PATH_PRODUCT_TYPES.RHEL_X86}`, - [platformConstants.PLATFORM_API_EXPORT_RESPONSE_TYPES.EXPIRES_AT]: '2019-07-14T00:00:00Z', - [platformConstants.PLATFORM_API_EXPORT_RESPONSE_TYPES.ID]: '0123456789', - [platformConstants.PLATFORM_API_EXPORT_RESPONSE_TYPES.STATUS]: - platformConstants.PLATFORM_API_EXPORT_STATUS_TYPES.PENDING - } - ]); + const parsedArray = platformTransformers.exports({ + [platformConstants.PLATFORM_API_EXPORT_RESPONSE_DATA]: [ + { + [platformConstants.PLATFORM_API_EXPORT_RESPONSE_TYPES.NAME]: + `${helpers.CONFIG_EXPORT_SERVICE_NAME_PREFIX}-${rhsmConstants.RHSM_API_PATH_PRODUCT_TYPES.RHEL_X86}`, + [platformConstants.PLATFORM_API_EXPORT_RESPONSE_TYPES.EXPIRES_AT]: '2019-07-14T00:00:00Z', + [platformConstants.PLATFORM_API_EXPORT_RESPONSE_TYPES.ID]: '0123456789', + [platformConstants.PLATFORM_API_EXPORT_RESPONSE_TYPES.STATUS]: + platformConstants.PLATFORM_API_EXPORT_STATUS_TYPES.PENDING + } + ] + }); expect(parsedSingle).toMatchSnapshot('exports, parsed single'); expect(parsedSingle).toMatchObject(parsedArray); diff --git a/src/services/platform/platformConstants.js b/src/services/platform/platformConstants.js index e71361ed3..d9c228ee0 100644 --- a/src/services/platform/platformConstants.js +++ b/src/services/platform/platformConstants.js @@ -42,7 +42,7 @@ const PLATFORM_API_EXPORT_RESOURCE_TYPES = { * @type {{CSV: string, JSON: string}} */ const PLATFORM_API_EXPORT_CONTENT_TYPES = { - CSV: 'csv', + // CSV: 'csv', JSON: 'json' }; @@ -56,11 +56,11 @@ const PLATFORM_API_EXPORT_FILENAME_PREFIX = 'swatch'; /** * Platform Export, available status types. * - * @type {{COMPLETED: string, FAILED: string, RUNNING: string, PARTIAL: string, PENDING: string}} + * @type {{COMPLETE: string, FAILED: string, RUNNING: string, PARTIAL: string, PENDING: string}} */ const PLATFORM_API_EXPORT_STATUS_TYPES = { FAILED: 'failed', - COMPLETED: 'completed', + COMPLETE: 'complete', PARTIAL: 'partial', PENDING: 'pending', RUNNING: 'running' @@ -89,6 +89,22 @@ const PLATFORM_API_EXPORT_POST_TYPES = { SOURCES: 'sources' }; +/** + * Platform Export, available SUBSCRIPTION FILTER POST types. + * + * @type {{BILLING_ACCOUNT_ID: string, USAGE: string, CATEGORY: string, METRIC_ID: string, SLA: string, + * BILLING_PROVIDER: string, PRODUCT_ID: string}} + */ +const PLATFORM_API_EXPORT_POST_SUBSCRIPTIONS_FILTER_TYPES = { + BILLING_PROVIDER: 'billing_provider', + BILLING_ACCOUNT_ID: 'billing_account_id', + CATEGORY: 'category', + METRIC_ID: 'metric_id', + PRODUCT_ID: 'product_id', + SLA: 'sla', + USAGE: 'usage' +}; + /** * Platform Export, available response types. * @@ -177,6 +193,7 @@ const platformConstants = { PLATFORM_API_EXPORT_CONTENT_TYPES, PLATFORM_API_EXPORT_FILENAME_PREFIX, PLATFORM_API_EXPORT_POST_TYPES, + PLATFORM_API_EXPORT_POST_SUBSCRIPTIONS_FILTER_TYPES, PLATFORM_API_EXPORT_RESOURCE_TYPES, PLATFORM_API_EXPORT_RESPONSE_DATA, PLATFORM_API_EXPORT_RESPONSE_META, @@ -201,6 +218,7 @@ export { PLATFORM_API_EXPORT_CONTENT_TYPES, PLATFORM_API_EXPORT_FILENAME_PREFIX, PLATFORM_API_EXPORT_POST_TYPES, + PLATFORM_API_EXPORT_POST_SUBSCRIPTIONS_FILTER_TYPES, PLATFORM_API_EXPORT_RESOURCE_TYPES, PLATFORM_API_EXPORT_RESPONSE_DATA, PLATFORM_API_EXPORT_RESPONSE_META, diff --git a/src/services/platform/platformServices.js b/src/services/platform/platformServices.js index 0325191fe..0b43fdbbc 100644 --- a/src/services/platform/platformServices.js +++ b/src/services/platform/platformServices.js @@ -109,21 +109,15 @@ const hideGlobalFilter = async (isHidden = true) => { }; /** - * @api {get} /api/export/v1/exports/:id - * @apiDescription Get an export by id + * @apiMock {ForceStatus} 202 + * @api {delete} /api/export/v1/exports/:id + * @apiDescription Create an export * * Reference [EXPORTS API](https://github.com/RedHatInsights/export-service-go/blob/main/static/spec/openapi.yaml) * - * @apiSuccessExample {zip} Success-Response: - * HTTP/1.1 200 OK - * Success - * - * @apiErrorExample {json} Error-Response: - * HTTP/1.1 400 Bad Request - * { - * message: "'---' is not a valid export UUID", - * code: 400 - * } + * @apiSuccessExample {json} Success-Response: + * HTTP/1.1 202 OK + * {} * * @apiErrorExample {json} Error-Response: * HTTP/1.1 500 Internal Server Error @@ -131,31 +125,23 @@ const hideGlobalFilter = async (isHidden = true) => { * } */ /** - * Get an export after setup. + * Delete an export. Useful for clean up. Helps avoid having to deal with export lists and most recent exports. * - * @param {string} id Export ID + * @param {string} id ID of export to delete * @param {object} options * @param {boolean} options.cancel * @param {string} options.cancelId * @returns {Promise<*>} */ -const getExport = (id, options = {}) => { +const deleteExport = (id, options = {}) => { const { cache = false, cancel = true, cancelId } = options; return axiosServiceCall({ url: `${process.env.REACT_APP_SERVICES_PLATFORM_EXPORT}/${id}`, - responseType: 'blob', + method: 'delete', cache, cancel, cancelId - }).then( - success => - (helpers.TEST_MODE && success.data) || - downloadHelpers.downloadData({ - data: success.data, - fileName: `swatch_report_${id}.tar.gz`, - fileType: 'application/gzip' - }) - ); + }); }; /** @@ -177,16 +163,7 @@ const getExport = (id, options = {}) => { * "completed_at": "2024-01-24T16:20:31.229Z", * "expires_at": "2024-01-24T16:20:31.229Z", * "format": "json", - * "status": "partial", - * "sources": [ - * { - * "application": "subscriptions", - * "resource": "instances", - * "filters": {}, - * "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", - * "status": "pending" - * } - * ] + * "status": "pending" * }, * { * "id": "x123456-5717-4562-b3fc-2c963f66afa6", @@ -195,16 +172,7 @@ const getExport = (id, options = {}) => { * "completed_at": "2024-01-24T16:20:31.229Z", * "expires_at": "2024-01-24T16:20:31.229Z", * "format": "json", - * "status": "completed", - * "sources": [ - * { - * "application": "subscriptions", - * "resource": "subscriptions", - * "filters": {}, - * "id": "x123456-5717-4562-b3fc-2c963f66afa6", - * "status": "completed" - * } - * ] + * "status": "complete" * } * ] * } @@ -220,16 +188,7 @@ const getExport = (id, options = {}) => { * "completed_at": "2024-01-24T16:20:31.229Z", * "expires_at": "2024-01-24T16:20:31.229Z", * "format": "json", - * "status": "partial", - * "sources": [ - * { - * "application": "subscriptions", - * "resource": "instances", - * "filters": {}, - * "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", - * "status": "pending" - * } - * ] + * "status": "pending" * }, * { * "id": "x123456-5717-4562-b3fc-2c963f66afa6", @@ -238,16 +197,7 @@ const getExport = (id, options = {}) => { * "completed_at": "2024-01-24T16:20:31.229Z", * "expires_at": "2024-01-24T16:20:31.229Z", * "format": "json", - * "status": "partial", - * "sources": [ - * { - * "application": "subscriptions", - * "resource": "subscriptions", - * "filters": {}, - * "id": "x123456-5717-4562-b3fc-2c963f66afa6", - * "status": "pending" - * } - * ] + * "status": "pending" * } * ] * } @@ -263,16 +213,7 @@ const getExport = (id, options = {}) => { * "completed_at": "2024-01-24T16:20:31.229Z", * "expires_at": "2024-01-24T16:20:31.229Z", * "format": "json", - * "status": "completed", - * "sources": [ - * { - * "application": "subscriptions", - * "resource": "instances", - * "filters": {}, - * "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", - * "status": "completed" - * } - * ] + * "status": "complete" * }, * { * "id": "x123456-5717-4562-b3fc-2c963f66afa6", @@ -281,16 +222,7 @@ const getExport = (id, options = {}) => { * "completed_at": "2024-01-24T16:20:31.229Z", * "expires_at": "2024-01-24T16:20:31.229Z", * "format": "json", - * "status": "completed", - * "sources": [ - * { - * "application": "subscriptions", - * "resource": "subscriptions", - * "filters": {}, - * "id": "x123456-5717-4562-b3fc-2c963f66afa6", - * "status": "completed" - * } - * ] + * "status": "complete" * }, * { * "id": "x123456-5717-4562-b3fc-2c963f66afa6", @@ -299,16 +231,7 @@ const getExport = (id, options = {}) => { * "completed_at": "2024-01-24T16:20:31.229Z", * "expires_at": "2024-01-24T16:20:31.229Z", * "format": "json", - * "status": "partial", - * "sources": [ - * { - * "application": "subscriptions", - * "resource": "subscriptions", - * "filters": {}, - * "id": "x123456-5717-4562-b3fc-2c963f66afa6", - * "status": "pending" - * } - * ] + * "status": "partial" * } * ] * } @@ -340,16 +263,7 @@ const getExport = (id, options = {}) => { * "completed_at": "2024-01-24T16:20:31.229Z", * "expires_at": "2024-01-24T16:20:31.229Z", * "format": "json", - * "status": "partial", - * "sources": [ - * { - * "application": "subscriptions", - * "resource": "instances", - * "filters": {}, - * "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", - * "status": "pending" - * } - * ] + * "status": "partial" * } * * @apiErrorExample {json} Error-Response: @@ -374,11 +288,10 @@ const getExport = (id, options = {}) => { * @param {string} options.cancelId * @returns {Promise<*>} */ -const getExportStatus = (id, params = {}, options = {}) => { +const getExistingExportsStatus = (id, params = {}, options = {}) => { const { cache = false, cancel = true, - // cancelId = 'export-status', cancelId, schema = [platformSchemas.exports], transform = [platformTransformers.exports], @@ -398,6 +311,122 @@ const getExportStatus = (id, params = {}, options = {}) => { }); }; +/** + * @api {get} /api/export/v1/exports/:id + * @apiDescription Get an export by id + * + * Reference [EXPORTS API](https://github.com/RedHatInsights/export-service-go/blob/main/static/spec/openapi.yaml) + * + * @apiSuccessExample {zip} Success-Response: + * HTTP/1.1 200 OK + * Success + * + * @apiErrorExample {json} Error-Response: + * HTTP/1.1 400 Bad Request + * { + * message: "'---' is not a valid export UUID", + * code: 400 + * } + * + * @apiErrorExample {json} Error-Response: + * HTTP/1.1 500 Internal Server Error + * { + * } + */ +/** + * Get an export after setup. + * + * @param {string} id Export ID + * @param {object} options + * @param {boolean} options.cancel + * @param {string} options.cancelId + * @param {string} options.fileName + * @param {string} options.fileType + * @returns {Promise<*>} + */ +const getExport = (id, options = {}) => { + const { + cache = false, + cancel = true, + cancelId, + fileName = `swatch_report_${id}`, + fileType = 'application/gzip' + } = options; + return axiosServiceCall({ + url: `${process.env.REACT_APP_SERVICES_PLATFORM_EXPORT}/${id}`, + responseType: 'blob', + cache, + cancel, + cancelId + }) + .then( + success => + (helpers.TEST_MODE && success.data) || + downloadHelpers.downloadData({ + data: success.data, + fileName: `${fileName}.tar.gz`, + fileType + }) + ) + .then(() => deleteExport(id)); +}; + +/** + * Convenience wrapper for setting up global export status with status polling, and download with clean-up. + * + * @param {Array<{id: string, fileName: string}>} idList A list of export IDs to finish + * @param {object} params + * @param {object} options + * @param {boolean} options.cancel + * @param {string} options.cancelId + * @returns {Promise<*>} + */ +const getExistingExports = (idList, params = {}, options = {}) => { + const { + cache = false, + cancel = true, + cancelId = 'all-exports', + poll, + schema = [platformSchemas.exports], + transform = [platformTransformers.exports], + ...restOptions + } = options; + + return axiosServiceCall({ + ...restOptions, + poll: { + location: { + url: process.env.REACT_APP_SERVICES_PLATFORM_EXPORT, + ...poll?.location + }, + validate: response => { + const completedResults = response?.data?.data?.completed; + const isIdListCompleted = + idList.filter(({ id }) => completedResults.find(({ id: completedId }) => completedId === id) !== undefined) + .length === idList.length; + + if (isIdListCompleted && completedResults.length > 0) { + Promise.all(idList.map(({ id, fileName }) => getExport(id, { fileName }))); + } + + return isIdListCompleted; + }, + ...poll + }, + url: process.env.REACT_APP_SERVICES_PLATFORM_EXPORT, + params, + cache, + cancel, + cancelId, + schema, + transform + }); +}; + +/** + * Note: 202 status appears to be only response that returns a sources list, OR it's variable depending on + * partial/pending status. + */ /** * @apiMock {ForceStatus} 202 * @api {post} /api/export/v1/exports @@ -432,7 +461,7 @@ const getExportStatus = (id, params = {}, options = {}) => { * } */ /** - * Post to create an export. + * Convenience wrapper for posting to create an export with status polling, then performing a download with clean-up. * * @param {object} data JSON data to submit * @param {object} options @@ -440,17 +469,50 @@ const getExportStatus = (id, params = {}, options = {}) => { * @param {string} options.cancelId * @returns {Promise<*>} */ -const postExport = (data = {}, options = {}) => { +const postExport = async (data = {}, options = {}) => { const { cache = false, - cancel = true, - cancelId, // = 'export-status', + cancel = false, + cancelId, + poll, schema = [platformSchemas.exports], - transform = [platformTransformers.exports], + transform = [], ...restOptions } = options; - return axiosServiceCall({ + + let downloadId; + const postResponse = await axiosServiceCall({ ...restOptions, + poll: { + ...poll, + location: { + url: process.env.REACT_APP_SERVICES_PLATFORM_EXPORT, + config: { + cache: false, + cancel: false, + schema: [platformSchemas.exports], + transform: [platformTransformers.exports] + }, + ...poll?.location + }, + status: (successResponse, ...args) => { + if (typeof poll?.status === 'function') { + poll.status.call(null, successResponse, ...args); + } + }, + validate: response => { + const foundDownload = response?.data?.data?.completed.find( + ({ id }) => downloadId !== undefined && id === downloadId + ); + + if (foundDownload) { + const { id, fileName } = foundDownload; + getExport(id, { fileName }); + } + + return foundDownload !== undefined; + } + }, method: 'post', url: process.env.REACT_APP_SERVICES_PLATFORM_EXPORT, data, @@ -460,11 +522,16 @@ const postExport = (data = {}, options = {}) => { schema, transform }); + + downloadId = postResponse.data.id; + return postResponse; }; const platformServices = { + deleteExport, + getExistingExports, + getExistingExportsStatus, getExport, - getExportStatus, getUser, getUserPermissions, hideGlobalFilter, @@ -479,8 +546,10 @@ helpers.browserExpose({ platformServices }); export { platformServices as default, platformServices, + deleteExport, + getExistingExports, + getExistingExportsStatus, getExport, - getExportStatus, getUser, getUserPermissions, hideGlobalFilter, diff --git a/src/services/platform/platformTransformers.js b/src/services/platform/platformTransformers.js index d83d2b8d6..8125331b9 100644 --- a/src/services/platform/platformTransformers.js +++ b/src/services/platform/platformTransformers.js @@ -1,12 +1,13 @@ +import moment from 'moment'; +import _snakeCase from 'lodash/snakeCase'; import { rbacConfig } from '../../config'; import { platformConstants, PLATFORM_API_EXPORT_STATUS_TYPES, - PLATFORM_API_EXPORT_FILENAME_PREFIX as EXPORT_PREFIX, PLATFORM_API_RESPONSE_USER_PERMISSION_OPERATION_TYPES as OPERATION_TYPES, PLATFORM_API_RESPONSE_USER_PERMISSION_RESOURCE_TYPES as RESOURCE_TYPES } from './platformConstants'; -import { helpers } from '../../common'; +import { helpers, dateHelpers } from '../../common'; /** * Transform export responses. Combines multiple exports, or a single export, @@ -32,6 +33,12 @@ const exports = response => { [platformConstants.PLATFORM_API_EXPORT_RESPONSE_TYPES.STATUS]: status } = response || {}; + updatedResponse.data.isAnythingPending = false; + updatedResponse.data.isAnythingCompleted = false; + updatedResponse.data.pending ??= []; + updatedResponse.data.completed ??= []; + updatedResponse.data.products = {}; + /** * Pull a product id from an export name. Fallback filtering for product identifiers. * @@ -40,7 +47,7 @@ const exports = response => { */ const getProductId = str => { const updatedStr = str; - const attemptId = updatedStr?.replace(`${EXPORT_PREFIX}-`, '')?.trim(); + const attemptId = updatedStr?.replace(`${helpers.CONFIG_EXPORT_SERVICE_NAME_PREFIX}-`, '')?.trim(); if (attemptId === updatedStr) { return undefined; @@ -61,7 +68,7 @@ const exports = response => { if ( updatedStr === PLATFORM_API_EXPORT_STATUS_TYPES.FAILED || - updatedStr === PLATFORM_API_EXPORT_STATUS_TYPES.COMPLETED + updatedStr === PLATFORM_API_EXPORT_STATUS_TYPES.COMPLETE ) { updatedStatus = updatedStr; } @@ -82,23 +89,32 @@ const exports = response => { const productId = getProductId(exportName); const focusedStatus = getStatus(exportStatus); - if (updatedResponse.data.isAnythingPending !== true) { - updatedResponse.data.isAnythingPending = focusedStatus === PLATFORM_API_EXPORT_STATUS_TYPES.PENDING; - } - - updatedResponse.data[productId] ??= []; - updatedResponse.data[productId].push({ + const updatedExportData = { + fileName: `${moment.utc(dateHelpers.getCurrentDate()).format('YYYYMMDD_HHmmss')}_${helpers.CONFIG_EXPORT_FILENAME.replace('{0}', _snakeCase(productId))}`, format: exportFormat, id: exportId, name: exportName, + productId, status: focusedStatus - }); + }; + + updatedResponse.data.products[productId] ??= {}; + updatedResponse.data.products[productId].pending ??= []; + updatedResponse.data.products[productId].completed ??= []; + + if (focusedStatus === PLATFORM_API_EXPORT_STATUS_TYPES.PENDING) { + updatedResponse.data.pending.push(updatedExportData); + updatedResponse.data.products[productId].pending.push(updatedExportData); + } else if (focusedStatus === PLATFORM_API_EXPORT_STATUS_TYPES.COMPLETE) { + updatedResponse.data.completed.push(updatedExportData); + updatedResponse.data.products[productId].completed.push(updatedExportData); + } }; if (Array.isArray(data)) { data .filter(({ [platformConstants.PLATFORM_API_EXPORT_RESPONSE_TYPES.NAME]: exportName }) => - new RegExp(`^${EXPORT_PREFIX}`, 'i').test(exportName) + new RegExp(`^${helpers.CONFIG_EXPORT_SERVICE_NAME_PREFIX}`, 'i').test(exportName) ) .forEach( ({ @@ -110,10 +126,18 @@ const exports = response => { restructureResponse({ exportName, exportStatus, exportFormat, exportId }); } ); - } else if (id && status && new RegExp(`^${EXPORT_PREFIX}`, 'i').test(name)) { + } else if (id && status && new RegExp(`^${helpers.CONFIG_EXPORT_SERVICE_NAME_PREFIX}`, 'i').test(name)) { restructureResponse({ exportName: name, exportStatus: status, exportFormat: format, exportId: id }); } + updatedResponse.data.isAnythingPending = updatedResponse.data.pending.length > 0; + updatedResponse.data.isAnythingCompleted = updatedResponse.data.completed.length > 0; + + Object.entries(updatedResponse.data.products).forEach(([productId, { pending, completed }]) => { + updatedResponse.data.products[productId].isPending = pending.length > 0; + updatedResponse.data.products[productId].isCompleted = completed.length > 0; + }); + return updatedResponse; }; diff --git a/src/styles/_toolbar.scss b/src/styles/_toolbar.scss index 963fd6620..9eea89ea4 100644 --- a/src/styles/_toolbar.scss +++ b/src/styles/_toolbar.scss @@ -6,6 +6,10 @@ padding-left: 0 !important; padding-right: 0 !important; } + + &__export-confirmation > p { + padding-top: var(--pf-global--spacer--md); + } } .curiosity-page-columns-column { diff --git a/src/styles/index.scss b/src/styles/index.scss index 5a7e6b4b1..3d76f9d92 100644 --- a/src/styles/index.scss +++ b/src/styles/index.scss @@ -7,7 +7,7 @@ $pf-global--breakpoint--sm: $pf-v5-global--breakpoint--sm; $pf-global--breakpoint--md: $pf-v5-global--breakpoint--md; $pf-global--breakpoint--lg: $pf-v5-global--breakpoint--lg; -.curiosity { +.curiosity, .curiosity-vars { --pf-global--BackgroundColor--dark-100: var(--pf-v5-global--BackgroundColor--dark-100); --pf-global--BackgroundColor--light-300: var(--pf-v5-global--BackgroundColor--light-300); --pf-global--BackgroundColor--100: var(--pf-v5-global--BackgroundColor--100);