Merge pull request #3381 from influxdata/feature/groupby-data-to-csv

flatten groupby data for csv downloads in dashboard and data explorer
pull/3389/head
Deniz Kusefoglu 2018-05-07 10:09:32 -07:00 committed by GitHub
commit 68c7765aa0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 97 additions and 176 deletions

View File

@ -2,25 +2,29 @@ import React from 'react'
import PropTypes from 'prop-types'
import classnames from 'classnames'
import _ from 'lodash'
import moment from 'moment'
import {fetchTimeSeriesAsync} from 'shared/actions/timeSeries'
import {resultsToCSV} from 'src/shared/parsing/resultsToCSV.js'
import {timeSeriesToTableGraph} from 'src/utils/timeSeriesTransformers'
import {dataToCSV} from 'src/shared/parsing/dataToCSV'
import download from 'src/external/download.js'
import {TEMPLATES} from 'src/shared/constants'
const getCSV = (query, errorThrown) => async () => {
const getDataForCSV = (query, errorThrown) => async () => {
try {
const {results} = await fetchTimeSeriesAsync({
const response = await fetchTimeSeriesAsync({
source: query.host,
query,
tempVars: TEMPLATES,
})
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')
const {data} = timeSeriesToTableGraph([{response}])
const db = _.get(query, ['queryConfig', 'database'], '')
const rp = _.get(query, ['queryConfig', 'retentionPolicy'], '')
const measurement = _.get(query, ['queryConfig', 'measurement'], '')
const timestring = moment().format('YYYY-MM-DD-HH-mm')
const name = `${db}.${rp}.${measurement}.${timestring}`
download(dataToCSV(data), `${name}.csv`, 'text/plain')
} catch (error) {
errorThrown(error, 'Unable to download .csv file')
console.error(error)
@ -46,7 +50,7 @@ const VisHeader = ({views, view, onToggleView, query, errorThrown}) => (
{query ? (
<div
className="btn btn-sm btn-default dlcsv"
onClick={getCSV(query, errorThrown)}
onClick={getDataForCSV(query, errorThrown)}
>
<span className="icon download dlcsv" />
.csv

View File

@ -4,6 +4,7 @@ import _ from 'lodash'
import {fetchTimeSeries} from 'src/shared/apis/query'
import {DEFAULT_TIME_SERIES} from 'src/shared/constants/series'
import {TimeSeriesServerResponse, TimeSeriesResponse} from 'src/types/series'
import {timeSeriesToTableGraph} from 'src/utils/timeSeriesTransformers'
interface Axes {
bounds: {
@ -130,7 +131,8 @@ const AutoRefresh = (
})
if (grabDataForDownload) {
grabDataForDownload(newSeries)
const {data} = timeSeriesToTableGraph(newSeries)
grabDataForDownload(data)
}
} catch (err) {
console.error(err)

View File

@ -23,7 +23,7 @@ const getSource = (cell, source, sources, defaultSource) => {
@ErrorHandling
class LayoutState extends Component {
state = {
celldata: [],
celldata: [[]],
}
grabDataForDownload = celldata => {
@ -122,7 +122,7 @@ const Layout = (
</LayoutCell>
)
const {arrayOf, bool, func, number, shape, string} = PropTypes
const {array, arrayOf, bool, func, number, shape, string} = PropTypes
Layout.contextTypes = {
source: shape(),
@ -200,7 +200,7 @@ LayoutState.propTypes = {...propTypes}
Layout.propTypes = {
...propTypes,
grabDataForDownload: func,
celldata: arrayOf(shape()),
celldata: arrayOf(array),
}
export default LayoutState

View File

@ -8,9 +8,9 @@ import LayoutCellMenu from 'shared/components/LayoutCellMenu'
import LayoutCellHeader from 'shared/components/LayoutCellHeader'
import {notify} from 'src/shared/actions/notifications'
import {notifyCSVDownloadFailed} from 'src/shared/copy/notifications'
import {dashboardtoCSV} from 'shared/parsing/resultsToCSV'
import download from 'src/external/download.js'
import {ErrorHandling} from 'src/shared/decorators/errors'
import {dataToCSV} from 'src/shared/parsing/dataToCSV'
@ErrorHandling
class LayoutCell extends Component {
@ -26,7 +26,7 @@ class LayoutCell extends Component {
const joinedName = cell.name.split(' ').join('_')
const {celldata} = this.props
try {
download(dashboardtoCSV(celldata), `${joinedName}.csv`, 'text/plain')
download(dataToCSV(celldata), `${joinedName}.csv`, 'text/plain')
} catch (error) {
notify(notifyCSVDownloadFailed())
console.error(error)
@ -79,7 +79,7 @@ class LayoutCell extends Component {
}
}
const {arrayOf, bool, func, node, number, shape, string} = PropTypes
const {array, arrayOf, bool, func, node, number, shape, string} = PropTypes
LayoutCell.propTypes = {
cell: shape({
@ -96,7 +96,7 @@ LayoutCell.propTypes = {
onSummonOverlayTechnologies: func,
isEditable: bool,
onCancelEditCell: func,
celldata: arrayOf(shape()),
celldata: arrayOf(array),
}
export default LayoutCell

View File

@ -118,6 +118,7 @@ const RefreshingGraph = ({
decimalPlaces={decimalPlaces}
editQueryStatus={editQueryStatus}
resizerTopHeight={resizerTopHeight}
grabDataForDownload={grabDataForDownload}
handleSetHoverTime={handleSetHoverTime}
isInCEO={isInCEO}
/>

View File

@ -0,0 +1,26 @@
import _ from 'lodash'
import moment from 'moment'
import {map} from 'fast.js'
export const formatDate = timestamp =>
moment(timestamp).format('M/D/YYYY h:mm:ss.SSSSSSSSS A')
export const dataToCSV = ([titleRow, ...valueRows]) => {
if (_.isEmpty(titleRow)) {
return ''
}
if (_.isEmpty(valueRows)) {
return ['date', titleRow.slice(1)].join(',')
}
if (titleRow[0] === 'time') {
const titlesString = ['date', titleRow.slice(1)].join(',')
const valuesString = map(valueRows, ([timestamp, ...values]) => [
[formatDate(timestamp), ...values].join(','),
]).join('\n')
return `${titlesString}\n${valuesString}`
}
const allRows = [titleRow, ...valueRows]
const allRowsStringArray = map(allRows, r => r.join(','))
return allRowsStringArray.join('\n')
}

View File

@ -1,53 +0,0 @@
import _ from 'lodash'
import moment from 'moment'
export const formatDate = timestamp =>
moment(timestamp).format('M/D/YYYY h:mm:ss.SSSSSSSSS A')
export const resultsToCSV = results => {
if (!_.get(results, ['0', 'series', '0'])) {
return {flag: 'no_data', name: '', CSVString: ''}
}
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')
return {flag: 'ok', name, CSVString}
}
const CSVString = [columns.join(',')]
.concat(values.map(row => row.join(',')))
.join('\n')
return {flag: 'ok', name, CSVString}
}
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
}

View File

@ -0,0 +1,46 @@
import {dataToCSV, formatDate} from 'shared/parsing/dataToCSV'
import moment from 'moment'
describe('formatDate', () => {
it('converts timestamp to an excel compatible date string', () => {
const timestamp = 1000000000000
const result = formatDate(timestamp)
expect(moment(result, 'M/D/YYYY h:mm:ss.SSSSSSSSS A').valueOf()).toBe(
timestamp
)
})
})
describe('dataToCSV', () => {
it('parses data, an array of arrays, to a csv string', () => {
const data = [[1, 2], [3, 4], [5, 6], [7, 8]]
const returned = dataToCSV(data)
const expected = `1,2\n3,4\n5,6\n7,8`
expect(returned).toEqual(expected)
})
it('converts values to dates if title of first column is time.', () => {
const data = [
['time', 'something'],
[1505262600000, 0.06163066773148772],
[1505264400000, 2.616484718180463],
[1505266200000, 1.6174323943535571],
]
const returned = dataToCSV(data)
const expected = `date,something\n${formatDate(
1505262600000
)},0.06163066773148772\n${formatDate(
1505264400000
)},2.616484718180463\n${formatDate(1505266200000)},1.6174323943535571`
expect(returned).toEqual(expected)
})
it('returns an empty string if data is empty', () => {
const data = [[]]
const returned = dataToCSV(data)
const expected = ''
expect(returned).toEqual(expected)
})
})

View File

@ -1,105 +0,0 @@
import {
resultsToCSV,
formatDate,
dashboardtoCSV,
} from 'shared/parsing/resultsToCSV'
import moment from 'moment'
describe('formatDate', () => {
it('converts timestamp to an excel compatible date string', () => {
const timestamp = 1000000000000
const result = formatDate(timestamp)
expect(moment(result, 'M/D/YYYY h:mm:ss.SSSSSSSSS A').valueOf()).toBe(
timestamp
)
})
})
describe('resultsToCSV', () => {
it('parses results, a time series data structure, to an object with name and CSVString keys', () => {
const results = [
{
statement_id: 0,
series: [
{
name: 'procstat',
columns: ['time', 'mean_cpu_usage'],
values: [
[1505262600000, 0.06163066773148772],
[1505264400000, 2.616484718180463],
[1505266200000, 1.6174323943535571],
],
},
],
},
]
const response = resultsToCSV(results)
const expected = {
flag: 'ok',
name: 'procstat',
CSVString: `date,mean_cpu_usage\n${formatDate(
1505262600000
)},0.06163066773148772\n${formatDate(
1505264400000
)},2.616484718180463\n${formatDate(1505266200000)},1.6174323943535571`,
}
expect(Object.keys(response).sort()).toEqual(
['flag', 'name', 'CSVString'].sort()
)
expect(response.flag).toBe(expected.flag)
expect(response.name).toBe(expected.name)
expect(response.CSVString).toBe(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).toBe(expected)
})
})