-
Notifications
You must be signed in to change notification settings - Fork 8.2k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
CSV panel actions for SavedSearch embedabbles #33709
Changes from all commits
d307898
0ab1034
446f6ce
428c831
fcda1dc
e826e28
30360a8
d658e75
4cf525e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,120 @@ | ||
/* | ||
* 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 { API_BASE_URL_V1 } from '../../common/constants'; | ||
|
||
const API_BASE_URL = `${API_BASE_URL_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(); | ||
} | ||
|
||
// @TODO: Clean this up once we update SavedSearch's interface | ||
// 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, containerState } = panelActionAPI; | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Had to go with duck-type-typing here since it was proving to be impossible to import the SavedSearchEmbedable component :/ There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ok, so the actual class we want is from In that case, I think this could use a comment to marker where we might be able to improve things down the road. |
||
return ( | ||
containerState.viewMode !== 'edit' && !!embeddable && embeddable.hasOwnProperty('savedSearch') | ||
); | ||
}; | ||
|
||
// 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 { | ||
timeRange: { from, to }, | ||
} = embeddable; | ||
|
||
if (!embeddable) { | ||
return; | ||
} | ||
|
||
const searchEmbeddable = embeddable; | ||
const state = await this.generateJobParams({ searchEmbeddable }); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think this needs to be wrapped in There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Oh, this is just a comment of something we can figure out later, because I'm not sure what we need at the moment. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Another thing (that doesn't need to be figured out right now) - this object doesn't seem to contain the columns.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sounds good! |
||
|
||
const id = `search:${embeddable.savedSearch.id}`; | ||
const filename = embeddable.savedSearch.title; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe next, we should work on exposing the SearchEmbeddable interface here, because There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In addition to the other headaches with the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah, I think I'm going to add another issue where we do some cleanup/migration of this component so we can better use it in our x-pack code. |
||
const timezone = embeddable.$rootScope.chrome.getUiSettingsClient().get('dateFormat:tz'); | ||
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, | ||
}, | ||
state, | ||
}); | ||
|
||
await kfetch({ method: 'POST', pathname: `${API_BASE_URL}${id}`, body }, { parseJson: false }) | ||
.then(r => r.text()) | ||
.then(csv => { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Kind of a nasty client-side mechanism to do CSV downloads browser-side. Might want to consolidate this into some sort of util elsewhere. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Looks like typical JS to me 😁 |
||
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({ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 💚 |
||
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()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Some reason panel actions are duped everytime you re-enter a page when client-side routing occurs. This fixes that.