From 0527730d9c7a7743e03061048937c6b3aa611788 Mon Sep 17 00:00:00 2001 From: Joshua Li Date: Tue, 14 Feb 2023 11:23:54 -0800 Subject: [PATCH] Add reporting on-demand menu items back in notebooks (#229) Signed-off-by: Joshua Li --- .gitignore | 1 + .../reporting_context_menu_helper.test.tsx | 25 ++- .../helpers/reporting_context_menu_helper.tsx | 142 ++++++++---------- .../notebooks/components/notebook.tsx | 16 ++ 4 files changed, 90 insertions(+), 94 deletions(-) diff --git a/.gitignore b/.gitignore index bf5586d64..83a6e387b 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ coverage/ .cypress/screenshots .cypress/videos common/query_manager/antlr/output +.eslintcache diff --git a/public/components/notebooks/components/helpers/__tests__/reporting_context_menu_helper.test.tsx b/public/components/notebooks/components/helpers/__tests__/reporting_context_menu_helper.test.tsx index 90b951df2..46fe88de6 100644 --- a/public/components/notebooks/components/helpers/__tests__/reporting_context_menu_helper.test.tsx +++ b/public/components/notebooks/components/helpers/__tests__/reporting_context_menu_helper.test.tsx @@ -6,7 +6,7 @@ import { contextMenuCreateReportDefinition, contextMenuViewReports, - generateInContextReport + generateInContextReport, } from '../reporting_context_menu_helper'; describe('reporting_context_menu_helper tests', () => { @@ -52,7 +52,8 @@ describe('reporting_context_menu_helper tests', () => { global.fetch = jest.fn(() => Promise.resolve({ status, - json: () => Promise.resolve({ filename, fileFormat, data: 'test-data' }), + json: () => + Promise.resolve({ filename, fileFormat, data: 'test-data', reportId: 'test-id' }), text: () => Promise.resolve({ tenant }), }) ); @@ -65,7 +66,7 @@ describe('reporting_context_menu_helper tests', () => { const toggleReportingLoadingModal = jest.fn(); await generateReport(200, 'test.csv', '__user__', setToast, toggleReportingLoadingModal); expect(toggleReportingLoadingModal).toBeCalledWith(true); - expect(setToast).toBeCalledWith('Successfully generated report.', 'success'); + expect(setToast).toBeCalledWith('Please continue report generation in the new tab.', 'success'); }); it('generates pdf for global tenant', async () => { @@ -73,7 +74,7 @@ describe('reporting_context_menu_helper tests', () => { const toggleReportingLoadingModal = jest.fn(); await generateReport(200, 'test.pdf', '', setToast, toggleReportingLoadingModal); expect(toggleReportingLoadingModal).toBeCalledWith(true); - expect(setToast).toBeCalledWith('Successfully generated report.', 'success'); + expect(setToast).toBeCalledWith('Please continue report generation in the new tab.', 'success'); }); it('generates png for custom tenant', async () => { @@ -81,15 +82,7 @@ describe('reporting_context_menu_helper tests', () => { const toggleReportingLoadingModal = jest.fn(); await generateReport(200, 'test.png', 'custom_tenant', setToast, toggleReportingLoadingModal); expect(toggleReportingLoadingModal).toBeCalledWith(true); - expect(setToast).toBeCalledWith('Successfully generated report.', 'success'); - }); - - it('generates png for custom tenant', async () => { - const setToast = jest.fn(); - const toggleReportingLoadingModal = jest.fn(); - await generateReport(200, 'test.png', 'custom_tenant', setToast, toggleReportingLoadingModal); - expect(toggleReportingLoadingModal).toBeCalledWith(true); - expect(setToast).toBeCalledWith('Successfully generated report.', 'success'); + expect(setToast).toBeCalledWith('Please continue report generation in the new tab.', 'success'); }); it('handles 404 error', async () => { @@ -132,7 +125,11 @@ describe('reporting_context_menu_helper tests', () => { global.fetch = jest.fn(() => Promise.reject({ status: 500 })); const setToast = jest.fn(); const toggleReportingLoadingModal = jest.fn(); - await generateInContextReport('csv', { setToast }, toggleReportingLoadingModal); + try { + await generateInContextReport('csv', { setToast }, toggleReportingLoadingModal); + } catch (error) { + expect(error.status).toEqual(500); + } expect(toggleReportingLoadingModal).toBeCalledWith(true); expect(setToast).toBeCalledWith('Tenant error', 'danger', 'Failed to get user tenant.'); }); diff --git a/public/components/notebooks/components/helpers/reporting_context_menu_helper.tsx b/public/components/notebooks/components/helpers/reporting_context_menu_helper.tsx index a174da65b..1269d24c1 100644 --- a/public/components/notebooks/components/helpers/reporting_context_menu_helper.tsx +++ b/public/components/notebooks/components/helpers/reporting_context_menu_helper.tsx @@ -3,11 +3,11 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { parse } from "url"; +import { parse } from 'url'; const getReportSourceURL = (baseURI: string) => { return baseURI.substr(baseURI.lastIndexOf('/') + 1, baseURI.length); -} +}; export const readDataReportToFile = async ( stream: string, @@ -16,7 +16,7 @@ export const readDataReportToFile = async ( ) => { const blob = new Blob([stream]); const url = URL.createObjectURL(blob); - let link = document.createElement('a'); + const link = document.createElement('a'); link.setAttribute('href', url); link.setAttribute('download', fileName); document.body.appendChild(link); @@ -25,22 +25,18 @@ export const readDataReportToFile = async ( }; const getFileFormatPrefix = (fileFormat: string) => { - var fileFormatPrefix = 'data:' + fileFormat + ';base64,'; + const fileFormatPrefix = 'data:' + fileFormat + ';base64,'; return fileFormatPrefix; }; -const readStreamToFile = async ( - stream: string, - fileFormat: string, - fileName: string -) => { - let link = document.createElement('a'); +const readStreamToFile = async (stream: string, fileFormat: string, fileName: string) => { + const link = document.createElement('a'); if (fileName.includes('csv')) { readDataReportToFile(stream, fileFormat, fileName); return; } - let fileFormatPrefix = getFileFormatPrefix(fileFormat); - let url = fileFormatPrefix + stream; + const fileFormatPrefix = getFileFormatPrefix(fileFormat); + const url = fileFormatPrefix + stream; if (typeof link.download !== 'string') { window.open(url, '_blank'); return; @@ -93,8 +89,7 @@ function addTenantToURL(url, userRequestedTenant) { // build fake url from relative url const fakeUrl = `http://opensearch.com${url}`; const tenantKey = 'security_tenant'; - const tenantKeyAndValue = - tenantKey + '=' + encodeURIComponent(userRequestedTenant); + const tenantKeyAndValue = tenantKey + '=' + encodeURIComponent(userRequestedTenant); const { pathname, search } = parse(fakeUrl); const queryDelimiter = !search ? '?' : '&'; @@ -127,15 +122,10 @@ export const generateInContextReport = async ( try { const tenant = await getTenantInfoIfExists(); if (tenant) { - baseUrl = addTenantToURL(baseUrl, tenant) + baseUrl = addTenantToURL(baseUrl, tenant); } } catch (error) { - props.setToast( - 'Tenant error', - 'danger', - 'Failed to get user tenant.' - ); - console.log(`failed to get user tenant: ${error}`); + props.setToast('Tenant error', 'danger', 'Failed to get user tenant.'); } const reportSource = 'Notebook'; @@ -151,7 +141,7 @@ export const generateInContextReport = async ( core_params: { base_url: baseUrl, report_format: fileFormat, - time_duration: 'PT30M', // time duration can be hard-coded + time_duration: 'PT30M', // time duration can be hard-coded ...rest, }, }, @@ -166,71 +156,63 @@ export const generateInContextReport = async ( }, }, }; - fetch( - '../api/reporting/generateReport', - { - headers: { - 'Content-Type': 'application/json', - 'osd-xsrf': 'true', - accept: '*/*', - 'accept-language': 'en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7,zh-TW;q=0.6', - pragma: 'no-cache', - 'sec-fetch-dest': 'empty', - 'sec-fetch-mode': 'cors', - 'sec-fetch-site': 'same-origin', - }, - method: 'POST', - body: JSON.stringify(contextMenuOnDemandReport), - referrerPolicy: 'strict-origin-when-cross-origin', - mode: 'cors', - credentials: 'include', - } - ) - .then((response) => { - toggleReportingLoadingModal(false); - if (response.status === 200) { - // success toast - props.setToast('Successfully generated report.', 'success'); - } else { - if (response.status === 403) { - // permissions failure toast - props.setToast( - 'Error generating report,', - 'danger', - 'Insufficient permissions. Reach out to your OpenSearch Dashboards administrator.' - ); - } else if (response.status === 503) { - // timeout failure - props.setToast( - 'Error generating report.', - 'danger', - 'Timed out generating on-demand report from notebook. Try again later.' - ); + await fetch('../api/reporting/generateReport', { + headers: { + 'Content-Type': 'application/json', + 'osd-xsrf': 'true', + accept: '*/*', + 'accept-language': 'en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7,zh-TW;q=0.6', + pragma: 'no-cache', + 'sec-fetch-dest': 'empty', + 'sec-fetch-mode': 'cors', + 'sec-fetch-site': 'same-origin', + }, + method: 'POST', + body: JSON.stringify(contextMenuOnDemandReport), + referrerPolicy: 'strict-origin-when-cross-origin', + mode: 'cors', + credentials: 'include', + }) + .then(async (response) => [response.status, await response.json()]) + .then(async ([status, data]) => { + toggleReportingLoadingModal(false); + if (status === 200) { + const a = document.createElement('a'); + a.href = window.location.origin + `${data.queryUrl}&visualReportId=${data.reportId}`; + a.target = '_blank'; + a.rel = 'noreferrer'; + a.click(); + // success toast + props.setToast('Please continue report generation in the new tab.', 'success'); } else { - // generic failure - props.setToast( - 'Download error', - 'danger', - 'There was an error generating this report.' - ); + if (status === 403) { + // permissions failure toast + props.setToast( + 'Error generating report,', + 'danger', + 'Insufficient permissions. Reach out to your OpenSearch Dashboards administrator.' + ); + } else if (status === 503) { + // timeout failure + props.setToast( + 'Error generating report.', + 'danger', + 'Timed out generating on-demand report from notebook. Try again later.' + ); + } else { + // generic failure + props.setToast('Download error', 'danger', 'There was an error generating this report.'); + } } - } - return response.json(); - }) - .then(async (data) => { - await readStreamToFile(data.data, fileFormat, data.filename); - }) -} + }); +}; export const contextMenuCreateReportDefinition = (baseURI: string) => { const reportSourceId = getReportSourceURL(baseURI); let reportSource = 'notebook:'; reportSource += reportSourceId.toString(); - window.location.assign( - `reports-dashboards#/create?previous=${reportSource}?timeFrom=0?timeTo=0` - ); + window.location.assign(`reports-dashboards#/create?previous=${reportSource}?timeFrom=0?timeTo=0`); }; -export const contextMenuViewReports = () => - window.location.assign('reports-dashboards#/'); +export const contextMenuViewReports = () => window.location.assign('reports-dashboards#/'); diff --git a/public/components/notebooks/components/notebook.tsx b/public/components/notebooks/components/notebook.tsx index 2d63e61ca..b97ab1723 100644 --- a/public/components/notebooks/components/notebook.tsx +++ b/public/components/notebooks/components/notebook.tsx @@ -858,6 +858,22 @@ export class Notebook extends Component { id: 0, title: 'Reporting', items: [ + { + name: 'Download PDF', + icon: , + onClick: () => { + this.setState({ isReportingActionsPopoverOpen: false }); + generateInContextReport('pdf', this.props, this.toggleReportingLoadingModal); + }, + }, + { + name: 'Download PNG', + icon: , + onClick: () => { + this.setState({ isReportingActionsPopoverOpen: false }); + generateInContextReport('png', this.props, this.toggleReportingLoadingModal); + }, + }, { name: 'Create report definition', icon: ,