Merge pull request #3381 from influxdata/feature/groupby-data-to-csv
flatten groupby data for csv downloads in dashboard and data explorerpull/3389/head
commit
68c7765aa0
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -118,6 +118,7 @@ const RefreshingGraph = ({
|
|||
decimalPlaces={decimalPlaces}
|
||||
editQueryStatus={editQueryStatus}
|
||||
resizerTopHeight={resizerTopHeight}
|
||||
grabDataForDownload={grabDataForDownload}
|
||||
handleSetHoverTime={handleSetHoverTime}
|
||||
isInCEO={isInCEO}
|
||||
/>
|
||||
|
|
|
@ -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')
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
})
|
||||
})
|
|
@ -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)
|
||||
})
|
||||
})
|
Loading…
Reference in New Issue