From d30789811300273636be684fadf8513b79ae2a5c Mon Sep 17 00:00:00 2001 From: Joel Griffith Date: Fri, 1 Mar 2019 09:28:03 -0800 Subject: [PATCH 1/9] Adding CSV panel actions for SavedSearch embedabbles --- .../dashboard/store/panel_actions_store.ts | 4 +- src/legacy/ui/public/kfetch/kfetch.ts | 11 +- .../panel_actions/get_csv_panel_action.tsx | 116 ++++++++++++++++++ 3 files changed, 127 insertions(+), 4 deletions(-) create mode 100644 x-pack/plugins/reporting/public/panel_actions/get_csv_panel_action.tsx diff --git a/src/legacy/core_plugins/kibana/public/dashboard/store/panel_actions_store.ts b/src/legacy/core_plugins/kibana/public/dashboard/store/panel_actions_store.ts index 449125d0ecfa4..69a9a93b1828a 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/store/panel_actions_store.ts +++ b/src/legacy/core_plugins/kibana/public/dashboard/store/panel_actions_store.ts @@ -28,7 +28,9 @@ class PanelActionsStore { */ public initializeFromRegistry(panelActionsRegistry: ContextMenuAction[]) { panelActionsRegistry.forEach(panelAction => { - this.actions.push(panelAction); + if (!this.actions.includes(panelAction)) { + this.actions.push(panelAction); + } }); } } diff --git a/src/legacy/ui/public/kfetch/kfetch.ts b/src/legacy/ui/public/kfetch/kfetch.ts index 086692a66261b..61663c0f4ed59 100644 --- a/src/legacy/ui/public/kfetch/kfetch.ts +++ b/src/legacy/ui/public/kfetch/kfetch.ts @@ -35,6 +35,7 @@ export interface KFetchOptions extends RequestInit { export interface KFetchKibanaOptions { prependBasePath?: boolean; + parseJson?: boolean; } export interface Interceptor { @@ -50,7 +51,7 @@ export const addInterceptor = (interceptor: Interceptor) => interceptors.push(in export async function kfetch( options: KFetchOptions, - { prependBasePath = true }: KFetchKibanaOptions = {} + { prependBasePath = true, parseJson = true }: KFetchKibanaOptions = {} ) { const combinedOptions = withDefaultOptions(options); const promise = requestInterceptors(combinedOptions).then( @@ -61,10 +62,14 @@ export async function kfetch( }); return window.fetch(fullUrl, restOptions).then(async res => { - const body = await getBodyAsJson(res); + const body = parseJson ? await getBodyAsJson(res) : null; if (res.ok) { - return body; + if (parseJson) { + return body; + } + return res; } + throw new KFetchError(res, body); }); } diff --git a/x-pack/plugins/reporting/public/panel_actions/get_csv_panel_action.tsx b/x-pack/plugins/reporting/public/panel_actions/get_csv_panel_action.tsx new file mode 100644 index 0000000000000..828b2c8ee814f --- /dev/null +++ b/x-pack/plugins/reporting/public/panel_actions/get_csv_panel_action.tsx @@ -0,0 +1,116 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import dateMath from '@elastic/datemath'; +import { i18n } from '@kbn/i18n'; + +import { ContextMenuAction, ContextMenuActionsRegistryProvider } from 'ui/embeddable'; +import { PanelActionAPI } from 'ui/embeddable/context_menu_actions/types'; +import { kfetch } from 'ui/kfetch'; +import { toastNotifications } from 'ui/notify'; +import { SearchEmbeddable } from '../../../../../src/legacy/core_plugins/kibana/public/discover/embeddable/search_embeddable'; + +const API_BASE_URL = '/api/reporting/v1/generate/immediate/csv/saved-object/'; + +class GetCsvReportPanelAction extends ContextMenuAction { + constructor() { + super( + { + displayName: i18n.translate('xpack.reporting.dashboard.downloadCsvPanelTitle', { + defaultMessage: 'Download CSV', + }), + id: 'downloadCsvReport', + parentPanelId: 'mainMenu', + }, + { + icon: 'document', + } + ); + } + + public async generateJobParams({ searchEmbeddable }: { searchEmbeddable: any }) { + const adapters = searchEmbeddable.getInspectorAdapters(); + if (!adapters) { + return {}; + } + + if (adapters.requests.requests.length === 0) { + return {}; + } + + return searchEmbeddable.searchScope.searchSource.getSearchRequestBody(); + } + + public isVisible = (panelActionAPI: PanelActionAPI): boolean => { + const { embeddable } = panelActionAPI; + + if (embeddable && embeddable instanceof SearchEmbeddable) { + return true; + } + + return false; + }; + + public onClick = async (panelActionAPI: PanelActionAPI) => { + const { embeddable } = panelActionAPI as any; + const { + timeRange: { from, to }, + } = embeddable; + + if (!embeddable) { + return; + } + + const searchEmbeddable = embeddable as SearchEmbeddable; + const state = await this.generateJobParams({ searchEmbeddable }); + + const id = `search:${embeddable.savedSearch.id}`; + const filename = embeddable.savedSearch.title; + const fromTime = dateMath.parse(from); + const toTime = dateMath.parse(to); + + if (!fromTime || !toTime) { + return this.onGenerationFail( + new Error(`Invalid time range: From: ${fromTime}, To: ${toTime}`) + ); + } + + const body = JSON.stringify({ + timerange: { + min: fromTime.valueOf(), + max: toTime.valueOf(), + timezone: 'PST', + }, + state, + }); + + await kfetch({ method: 'POST', pathname: `${API_BASE_URL}${id}`, body }, { parseJson: false }) + .then(r => r.text()) + .then(csv => { + const blob = new Blob([csv], { type: 'text/csv' }); + const a = window.document.createElement('a'); + const downloadObject = window.URL.createObjectURL(blob); + a.href = downloadObject; + a.download = `${filename}.csv`; + a.click(); + window.URL.revokeObjectURL(downloadObject); + }) + .catch(this.onGenerationFail); + }; + + private onGenerationFail(error: Error) { + toastNotifications.addDanger({ + title: i18n.translate('xpack.reporting.dashboard.failedCsvDownloadTitle', { + defaultMessage: `CSV download failed`, + }), + text: i18n.translate('xpack.reporting.dashboard.failedCsvDownloadMessage', { + defaultMessage: `We couldn't download your CSV at this time.`, + }), + 'data-test-subj': 'downloadCsvFail', + }); + } +} + +ContextMenuActionsRegistryProvider.register(() => new GetCsvReportPanelAction()); From 0ab1034d1da28e169d55edc978b102fb584b54b2 Mon Sep 17 00:00:00 2001 From: Joel Griffith Date: Tue, 12 Mar 2019 15:58:09 -0700 Subject: [PATCH 2/9] WIP: Implementing PR feedback --- src/legacy/ui/public/kfetch/kfetch.ts | 5 +---- .../reporting/public/panel_actions/get_csv_panel_action.tsx | 2 +- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/src/legacy/ui/public/kfetch/kfetch.ts b/src/legacy/ui/public/kfetch/kfetch.ts index 61663c0f4ed59..3192a7bf44b98 100644 --- a/src/legacy/ui/public/kfetch/kfetch.ts +++ b/src/legacy/ui/public/kfetch/kfetch.ts @@ -64,10 +64,7 @@ export async function kfetch( return window.fetch(fullUrl, restOptions).then(async res => { const body = parseJson ? await getBodyAsJson(res) : null; if (res.ok) { - if (parseJson) { - return body; - } - return res; + return parseJson ? body : res; } throw new KFetchError(res, body); diff --git a/x-pack/plugins/reporting/public/panel_actions/get_csv_panel_action.tsx b/x-pack/plugins/reporting/public/panel_actions/get_csv_panel_action.tsx index 828b2c8ee814f..3536847f582a2 100644 --- a/x-pack/plugins/reporting/public/panel_actions/get_csv_panel_action.tsx +++ b/x-pack/plugins/reporting/public/panel_actions/get_csv_panel_action.tsx @@ -6,11 +6,11 @@ import dateMath from '@elastic/datemath'; import { i18n } from '@kbn/i18n'; +import { SearchEmbeddable } from 'src/legacy/core_plugins/kibana/public/discover/embeddable/search_embeddable'; import { ContextMenuAction, ContextMenuActionsRegistryProvider } from 'ui/embeddable'; import { PanelActionAPI } from 'ui/embeddable/context_menu_actions/types'; import { kfetch } from 'ui/kfetch'; import { toastNotifications } from 'ui/notify'; -import { SearchEmbeddable } from '../../../../../src/legacy/core_plugins/kibana/public/discover/embeddable/search_embeddable'; const API_BASE_URL = '/api/reporting/v1/generate/immediate/csv/saved-object/'; From 446f6ce9733476fb226e705febb826613b87a1e3 Mon Sep 17 00:00:00 2001 From: Joel Griffith Date: Mon, 18 Mar 2019 15:31:35 -0700 Subject: [PATCH 3/9] Fixing imports, timzeone config, and more for exporting CSV --- x-pack/plugins/reporting/index.js | 3 +++ .../reporting/public/panel_actions/get_csv_panel_action.tsx | 5 +++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/reporting/index.js b/x-pack/plugins/reporting/index.js index 30f9b62e941ca..265d095511ceb 100644 --- a/x-pack/plugins/reporting/index.js +++ b/x-pack/plugins/reporting/index.js @@ -36,6 +36,9 @@ export const reporting = (kibana) => { 'plugins/reporting/share_context_menu/register_csv_reporting', 'plugins/reporting/share_context_menu/register_reporting', ], + contextMenuActions: [ + 'plugins/reporting/panel_actions/get_csv_panel_action', + ], hacks: ['plugins/reporting/hacks/job_completion_notifier'], home: ['plugins/reporting/register_feature'], managementSections: ['plugins/reporting/views/management'], diff --git a/x-pack/plugins/reporting/public/panel_actions/get_csv_panel_action.tsx b/x-pack/plugins/reporting/public/panel_actions/get_csv_panel_action.tsx index 3536847f582a2..25606f373f9a8 100644 --- a/x-pack/plugins/reporting/public/panel_actions/get_csv_panel_action.tsx +++ b/x-pack/plugins/reporting/public/panel_actions/get_csv_panel_action.tsx @@ -6,7 +6,7 @@ import dateMath from '@elastic/datemath'; import { i18n } from '@kbn/i18n'; -import { SearchEmbeddable } from 'src/legacy/core_plugins/kibana/public/discover/embeddable/search_embeddable'; +import { SearchEmbeddable } from 'plugins/kibana/discover/embeddable/search_embeddable'; import { ContextMenuAction, ContextMenuActionsRegistryProvider } from 'ui/embeddable'; import { PanelActionAPI } from 'ui/embeddable/context_menu_actions/types'; import { kfetch } from 'ui/kfetch'; @@ -68,6 +68,7 @@ class GetCsvReportPanelAction extends ContextMenuAction { const id = `search:${embeddable.savedSearch.id}`; const filename = embeddable.savedSearch.title; + const timezone = embeddable.$rootScope.chrome.getUiSettingsClient().get('dateFormat:tz'); const fromTime = dateMath.parse(from); const toTime = dateMath.parse(to); @@ -81,7 +82,7 @@ class GetCsvReportPanelAction extends ContextMenuAction { timerange: { min: fromTime.valueOf(), max: toTime.valueOf(), - timezone: 'PST', + timezone, }, state, }); From 428c831d185a40aece3f0d6a9d8ed5e32aaece3c Mon Sep 17 00:00:00 2001 From: Joel Griffith Date: Wed, 20 Mar 2019 13:53:29 -0700 Subject: [PATCH 4/9] Duck typing vs instanceof checks --- .../reporting/public/panel_actions/get_csv_panel_action.tsx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/x-pack/plugins/reporting/public/panel_actions/get_csv_panel_action.tsx b/x-pack/plugins/reporting/public/panel_actions/get_csv_panel_action.tsx index 25606f373f9a8..a566bbc656f08 100644 --- a/x-pack/plugins/reporting/public/panel_actions/get_csv_panel_action.tsx +++ b/x-pack/plugins/reporting/public/panel_actions/get_csv_panel_action.tsx @@ -6,7 +6,6 @@ import dateMath from '@elastic/datemath'; import { i18n } from '@kbn/i18n'; -import { SearchEmbeddable } from 'plugins/kibana/discover/embeddable/search_embeddable'; import { ContextMenuAction, ContextMenuActionsRegistryProvider } from 'ui/embeddable'; import { PanelActionAPI } from 'ui/embeddable/context_menu_actions/types'; import { kfetch } from 'ui/kfetch'; @@ -46,7 +45,7 @@ class GetCsvReportPanelAction extends ContextMenuAction { public isVisible = (panelActionAPI: PanelActionAPI): boolean => { const { embeddable } = panelActionAPI; - if (embeddable && embeddable instanceof SearchEmbeddable) { + if (embeddable && embeddable.hasOwnProperty('savedSearch')) { return true; } @@ -63,7 +62,7 @@ class GetCsvReportPanelAction extends ContextMenuAction { return; } - const searchEmbeddable = embeddable as SearchEmbeddable; + const searchEmbeddable = embeddable; const state = await this.generateJobParams({ searchEmbeddable }); const id = `search:${embeddable.savedSearch.id}`; From fcda1dccc8d55cc3b35dcf2811486198083fbd4b Mon Sep 17 00:00:00 2001 From: Joel Griffith Date: Thu, 28 Mar 2019 07:41:40 -0700 Subject: [PATCH 5/9] Moving base URL over to constants --- .../reporting/public/panel_actions/get_csv_panel_action.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/reporting/public/panel_actions/get_csv_panel_action.tsx b/x-pack/plugins/reporting/public/panel_actions/get_csv_panel_action.tsx index a566bbc656f08..2bb87cd46630d 100644 --- a/x-pack/plugins/reporting/public/panel_actions/get_csv_panel_action.tsx +++ b/x-pack/plugins/reporting/public/panel_actions/get_csv_panel_action.tsx @@ -10,8 +10,9 @@ import { ContextMenuAction, ContextMenuActionsRegistryProvider } from 'ui/embedd import { PanelActionAPI } from 'ui/embeddable/context_menu_actions/types'; import { kfetch } from 'ui/kfetch'; import { toastNotifications } from 'ui/notify'; +import { API_BASE_URL_V1 } from '../../common/constants'; -const API_BASE_URL = '/api/reporting/v1/generate/immediate/csv/saved-object/'; +const API_BASE_URL = `${API_BASE_URL_V1}/generate/immediate/csv/saved-object/`; class GetCsvReportPanelAction extends ContextMenuAction { constructor() { From e826e2885fcaf1029cf338e75ac3cae3d69f7282 Mon Sep 17 00:00:00 2001 From: Joel Griffith Date: Thu, 28 Mar 2019 09:07:42 -0700 Subject: [PATCH 6/9] Spec for kfetch --- src/legacy/ui/public/kfetch/kfetch.test.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/legacy/ui/public/kfetch/kfetch.test.ts b/src/legacy/ui/public/kfetch/kfetch.test.ts index 8f8cc807911a3..f9e05802d4ff4 100644 --- a/src/legacy/ui/public/kfetch/kfetch.test.ts +++ b/src/legacy/ui/public/kfetch/kfetch.test.ts @@ -78,12 +78,19 @@ describe('kfetch', () => { }); }); - it('should return response', async () => { + it('should return JSON responses by default', async () => { fetchMock.get('*', { foo: 'bar' }); const res = await kfetch({ pathname: 'my/path' }); expect(res).toEqual({ foo: 'bar' }); }); + it('should not return JSON responses by defaul when `parseJson` is `false`', async () => { + fetchMock.get('*', { foo: 'bar' }); + const raw = await kfetch({ pathname: 'my/path' }, { parseJson: false }); + const res = await raw.text(); + expect(res).toEqual('{"foo":"bar"}'); + }); + it('should prepend url with basepath by default', async () => { fetchMock.get('*', {}); await kfetch({ pathname: 'my/path' }); From 30360a85630a48d7e64c690e4f6d2d4dee96d705 Mon Sep 17 00:00:00 2001 From: Joel Griffith Date: Thu, 28 Mar 2019 13:23:27 -0700 Subject: [PATCH 7/9] Some placemarkers for things to cleanup once we get SavedSearch updated --- .../reporting/public/panel_actions/get_csv_panel_action.tsx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/x-pack/plugins/reporting/public/panel_actions/get_csv_panel_action.tsx b/x-pack/plugins/reporting/public/panel_actions/get_csv_panel_action.tsx index 2bb87cd46630d..708c64d9a2cfb 100644 --- a/x-pack/plugins/reporting/public/panel_actions/get_csv_panel_action.tsx +++ b/x-pack/plugins/reporting/public/panel_actions/get_csv_panel_action.tsx @@ -43,6 +43,8 @@ class GetCsvReportPanelAction extends ContextMenuAction { return searchEmbeddable.searchScope.searchSource.getSearchRequestBody(); } + // @TODO: Clean this up once we update SavedSearch's interface + // and location in the file-system public isVisible = (panelActionAPI: PanelActionAPI): boolean => { const { embeddable } = panelActionAPI; @@ -53,6 +55,8 @@ class GetCsvReportPanelAction extends ContextMenuAction { return false; }; + // TODO: Need to expose some of the interface here from SavedSearch + // as well as pass things like columns into the API call public onClick = async (panelActionAPI: PanelActionAPI) => { const { embeddable } = panelActionAPI as any; const { From d658e7513fb9d2fe8632b744fb8d4729067c4e8c Mon Sep 17 00:00:00 2001 From: Joel Griffith Date: Thu, 28 Mar 2019 13:52:57 -0700 Subject: [PATCH 8/9] Don't allow exports in edit mode --- .../public/panel_actions/get_csv_panel_action.tsx | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/x-pack/plugins/reporting/public/panel_actions/get_csv_panel_action.tsx b/x-pack/plugins/reporting/public/panel_actions/get_csv_panel_action.tsx index 708c64d9a2cfb..24228fc5fdc8f 100644 --- a/x-pack/plugins/reporting/public/panel_actions/get_csv_panel_action.tsx +++ b/x-pack/plugins/reporting/public/panel_actions/get_csv_panel_action.tsx @@ -44,15 +44,14 @@ class GetCsvReportPanelAction extends ContextMenuAction { } // @TODO: Clean this up once we update SavedSearch's interface - // and location in the file-system + // and location in the file-system. `viewMode` also has an enum + // buried inside of the dashboard folder we could use vs a bare string public isVisible = (panelActionAPI: PanelActionAPI): boolean => { - const { embeddable } = panelActionAPI; + const { embeddable, containerState } = panelActionAPI; - if (embeddable && embeddable.hasOwnProperty('savedSearch')) { - return true; - } - - return false; + return ( + containerState.viewMode !== 'edit' && !!embeddable && embeddable.hasOwnProperty('savedSearch') + ); }; // TODO: Need to expose some of the interface here from SavedSearch From 4cf525ec6f4720816ec093f5e8c2892f6b0a941c Mon Sep 17 00:00:00 2001 From: Joel Griffith Date: Thu, 28 Mar 2019 13:56:52 -0700 Subject: [PATCH 9/9] Fixing expectjs import --- x-pack/test/reporting/api/generate/csv_saved_search.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/test/reporting/api/generate/csv_saved_search.ts b/x-pack/test/reporting/api/generate/csv_saved_search.ts index e5db3e8727b27..afee6422dc0d8 100644 --- a/x-pack/test/reporting/api/generate/csv_saved_search.ts +++ b/x-pack/test/reporting/api/generate/csv_saved_search.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import expect from 'expect.js'; +import expect from '@kbn/expect'; import supertest from 'supertest'; import { CSV_RESULT_TIMEBASED, CSV_RESULT_TIMELESS } from './fixtures';