Merge pull request #2116 from influxdata/feature/dashboard-download-csv

Feature/dashboard download csv
pull/2117/merge
Deniz Kusefoglu 2017-10-16 10:49:02 -07:00 committed by GitHub
commit f9031dc72d
8 changed files with 167 additions and 21 deletions

View File

@ -1,9 +1,11 @@
## v1.3.10.0 [unreleased]
### Bug Fixes
1. [#2095](https://github.com/influxdata/chronograf/pull/2095): Improve the copy in the retention policy edit page
1. [#2093](https://github.com/influxdata/chronograf/pull/2093): Fix when exporting `SHOW DATABASES` CSV has bad data
### Features
1. [#2083](https://github.com/influxdata/chronograf/pull/2083): Every dashboard can now have its own time range
1. [#2045](https://github.com/influxdata/chronograf/pull/2045): Add CSV download option in dashboard cells
### UI Improvements
1. [#2111](https://github.com/influxdata/chronograf/pull/2111): Increase size of Cell Editor query tabs to reveal more of their query strings

View File

@ -1,4 +1,8 @@
import resultsToCSV, {formatDate} from 'shared/parsing/resultsToCSV'
import {
resultsToCSV,
formatDate,
dashboardtoCSV,
} from 'shared/parsing/resultsToCSV'
describe('formatDate', () => {
it('converts timestamp to an excel compatible date string', () => {
@ -29,6 +33,7 @@ describe('resultsToCSV', () => {
]
const response = resultsToCSV(results)
const expected = {
flag: 'ok',
name: 'procstat',
CSVString: `date,mean_cpu_usage\n${formatDate(
1505262600000
@ -36,10 +41,65 @@ describe('resultsToCSV', () => {
1505264400000
)},2.616484718180463\n${formatDate(1505266200000)},1.6174323943535571`,
}
expect(response).to.have.all.keys('name', 'CSVString')
expect(response).to.have.all.keys('flag', 'name', 'CSVString')
expect(response.flag).to.be.a('string')
expect(response.name).to.be.a('string')
expect(response.CSVString).to.be.a('string')
expect(response.flag).to.equal(expected.flag)
expect(response.name).to.equal(expected.name)
expect(response.CSVString).to.equal(expected.CSVString)
})
})
describe('dashboardtoCSV', () => {
it('parses the array of timeseries data displayed by the dashboard cell to a CSVstring for download', () => {
const data = [
{
results: [
{
statement_id: 0,
series: [
{
name: 'procstat',
columns: ['time', 'mean_cpu_usage'],
values: [
[1505262600000, 0.06163066773148772],
[1505264400000, 2.616484718180463],
[1505266200000, 1.6174323943535571],
],
},
],
},
],
},
{
results: [
{
statement_id: 0,
series: [
{
name: 'procstat',
columns: ['not-time', 'mean_cpu_usage'],
values: [
[1505262600000, 0.06163066773148772],
[1505264400000, 2.616484718180463],
[1505266200000, 1.6174323943535571],
],
},
],
},
],
},
]
const result = dashboardtoCSV(data)
const expected = `time,mean_cpu_usage,not-time,mean_cpu_usage\n${formatDate(
1505262600000
)},0.06163066773148772,1505262600000,0.06163066773148772\n${formatDate(
1505264400000
)},2.616484718180463,1505264400000,2.616484718180463\n${formatDate(
1505266200000
)},1.6174323943535571,1505266200000,1.6174323943535571`
expect(result).to.be.a('string')
expect(result).to.equal(expected)
})
})

View File

@ -3,13 +3,17 @@ import classnames from 'classnames'
import _ from 'lodash'
import {fetchTimeSeriesAsync} from 'shared/actions/timeSeries'
import resultsToCSV from 'src/shared/parsing/resultsToCSV.js'
import {resultsToCSV} from 'src/shared/parsing/resultsToCSV.js'
import download from 'src/external/download.js'
const getCSV = (query, errorThrown) => async () => {
try {
const {results} = await fetchTimeSeriesAsync({source: query.host, query})
const {name, CSVString} = resultsToCSV(results)
const {flag, name, CSVString} = resultsToCSV(results)
if (flag === 'no_data') {
errorThrown('no data', 'There are no data to download.')
return
}
download(CSVString, `${name}.csv`, 'text/plain')
} catch (error) {
errorThrown(error, 'Unable to download .csv file')

View File

@ -51,6 +51,7 @@ const AutoRefresh = ComposedComponent => {
}),
}),
editQueryStatus: func,
grabDataForDownload: func,
},
getInitialState() {
@ -111,7 +112,7 @@ const AutoRefresh = ComposedComponent => {
},
executeQueries(queries, templates = []) {
const {editQueryStatus} = this.props
const {editQueryStatus, grabDataForDownload} = this.props
const {resolution} = this.state
if (!queries.length) {
@ -150,12 +151,14 @@ const AutoRefresh = ComposedComponent => {
Promise.all(timeSeriesPromises).then(timeSeries => {
const newSeries = timeSeries.map(response => ({response}))
const lastQuerySuccessful = !this._noResultsForQuery(newSeries)
this.setState({
timeSeries: newSeries,
lastQuerySuccessful,
isFetching: false,
})
if (grabDataForDownload) {
grabDataForDownload(timeSeries)
}
})
},

View File

@ -3,12 +3,16 @@ import _ from 'lodash'
import LayoutCellMenu from 'shared/components/LayoutCellMenu'
import LayoutCellHeader from 'shared/components/LayoutCellHeader'
import {errorThrown} from 'shared/actions/errors'
import {dashboardtoCSV} from 'shared/parsing/resultsToCSV'
import download from 'src/external/download.js'
class LayoutCell extends Component {
constructor(props) {
super(props)
this.state = {
isDeleting: false,
celldata: [],
}
}
@ -30,22 +34,39 @@ class LayoutCell extends Component {
this.props.onSummonOverlayTechnologies(cell)
}
grabDataForDownload = celldata => {
this.setState({celldata})
}
handleCSVDownload = cell => () => {
const joinedName = cell.name.split(' ').join('_')
const {celldata} = this.state
try {
download(dashboardtoCSV(celldata), `${joinedName}.csv`, 'text/plain')
} catch (error) {
errorThrown(error, 'Unable to download .csv file')
console.error(error)
}
}
render() {
const {cell, children, isEditable} = this.props
const {isDeleting} = this.state
const {isDeleting, celldata} = this.state
const queries = _.get(cell, ['queries'], [])
return (
<div className="dash-graph">
<LayoutCellMenu
cell={cell}
dataExists={!!celldata.length}
isDeleting={isDeleting}
isEditable={isEditable}
onDelete={this.handleDeleteCell}
onEdit={this.handleSummonOverlay}
handleClickOutside={this.closeMenu}
onDeleteClick={this.handleDeleteClick}
onCSVDownload={this.handleCSVDownload}
/>
<LayoutCellHeader
queries={queries}
@ -54,7 +75,13 @@ class LayoutCell extends Component {
/>
<div className="dash-graph--container">
{queries.length
? children
? React.Children.map(children, child => {
if (child && child.props && child.props.autoRefresh) {
return React.cloneElement(child, {
grabDataForDownload: this.grabDataForDownload,
})
}
})
: <div className="graph-empty">
<button
className="no-query--button btn btn-md btn-primary"

View File

@ -2,7 +2,15 @@ import React, {PropTypes} from 'react'
import OnClickOutside from 'react-onclickoutside'
const LayoutCellMenu = OnClickOutside(
({isDeleting, onEdit, onDeleteClick, onDelete, cell}) =>
({
isDeleting,
onEdit,
onDeleteClick,
onDelete,
onCSVDownload,
dataExists,
cell,
}) =>
<div
className={
isDeleting
@ -13,6 +21,14 @@ const LayoutCellMenu = OnClickOutside(
<div className="dash-graph-context--button" onClick={onEdit(cell)}>
<span className="icon pencil" />
</div>
{dataExists
? <div
className="dash-graph-context--button"
onClick={onCSVDownload(cell)}
>
<span className="icon download" />
</div>
: null}
{isDeleting
? <div className="dash-graph-context--button active">
<span className="icon trash" />
@ -46,6 +62,7 @@ LayoutCellMenuContainer.propTypes = {
onDeleteClick: func,
cell: shape(),
isEditable: bool,
dataExists: bool,
}
LayoutCellMenu.propTypes = LayoutCellMenuContainer.propTypes

View File

@ -21,6 +21,7 @@ const RefreshingGraph = ({
synchronizer,
resizeCoords,
editQueryStatus,
grabDataForDownload,
}) => {
if (!queries.length) {
return (
@ -53,6 +54,7 @@ const RefreshingGraph = ({
axes={axes}
onZoom={onZoom}
queries={queries}
grabDataForDownload={grabDataForDownload}
templates={templates}
timeRange={timeRange}
autoRefresh={autoRefresh}
@ -82,6 +84,7 @@ RefreshingGraph.propTypes = {
editQueryStatus: func,
onZoom: func,
resizeCoords: shape(),
grabDataForDownload: func,
}
export default RefreshingGraph

View File

@ -4,20 +4,50 @@ import moment from 'moment'
export const formatDate = timestamp =>
moment(timestamp).format('M/D/YYYY h:mm:ss A')
const resultsToCSV = results => {
const {name, columns, values} = _.get(results, ['0', 'series', '0'], {})
const [, ...cols] = columns
export const resultsToCSV = results => {
if (!_.get(results, ['0', 'series', '0'])) {
return {flag: 'no_data', name: '', CSVString: ''}
}
const CSVString = [['date', ...cols].join(',')]
.concat(
values.map(([timestamp, ...measurements]) =>
// MS Excel format
[formatDate(timestamp), ...measurements].join(',')
const {name, columns, values} = _.get(results, ['0', 'series', '0'])
if (columns[0] === 'time') {
const [, ...cols] = columns
const CSVString = [['date', ...cols].join(',')]
.concat(
values.map(([timestamp, ...measurements]) =>
// MS Excel format
[formatDate(timestamp), ...measurements].join(',')
)
)
)
.join('\n')
.join('\n')
return {flag: 'ok', name, CSVString}
}
return {name, CSVString}
const CSVString = [columns.join(',')]
.concat(values.map(row => row.join(',')))
.join('\n')
return {flag: 'ok', name, CSVString}
}
export default resultsToCSV
export const dashboardtoCSV = data => {
const columnNames = _.flatten(
data.map(r => _.get(r, 'results[0].series[0].columns', []))
)
const timeIndices = columnNames
.map((e, i) => (e === 'time' ? i : -1))
.filter(e => e >= 0)
let values = data.map(r => _.get(r, 'results[0].series[0].values', []))
values = _.unzip(values).map(v => _.flatten(v))
if (timeIndices) {
values.map(v => {
timeIndices.forEach(i => (v[i] = formatDate(v[i])))
return v
})
}
const CSVString = [columnNames.join(',')]
.concat(values.map(v => v.join(',')))
.join('\n')
return CSVString
}