From 072e31091d58e9546fdfa82637b49206e113c104 Mon Sep 17 00:00:00 2001 From: deniz kusefoglu Date: Mon, 9 Oct 2017 15:09:27 -0700 Subject: [PATCH 01/31] Refrain from coercing data to a date if column name is not time --- ui/src/data_explorer/components/VisHeader.js | 6 +++- ui/src/shared/parsing/resultsToCSV.js | 30 +++++++++++++------- 2 files changed, 25 insertions(+), 11 deletions(-) diff --git a/ui/src/data_explorer/components/VisHeader.js b/ui/src/data_explorer/components/VisHeader.js index 0457460fe7..6e2c85cb01 100644 --- a/ui/src/data_explorer/components/VisHeader.js +++ b/ui/src/data_explorer/components/VisHeader.js @@ -9,7 +9,11 @@ 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') diff --git a/ui/src/shared/parsing/resultsToCSV.js b/ui/src/shared/parsing/resultsToCSV.js index 47596b16ec..18db204c82 100644 --- a/ui/src/shared/parsing/resultsToCSV.js +++ b/ui/src/shared/parsing/resultsToCSV.js @@ -5,19 +5,29 @@ 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 + 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 From b87befa0b31d2eeb651602a21b112798dc7812aa Mon Sep 17 00:00:00 2001 From: Nathan Haugo Date: Wed, 11 Oct 2017 13:14:44 -0700 Subject: [PATCH 02/31] Add support for millisecond groupbys --- chronograf.go | 17 +++++++++++++---- influx/templates_test.go | 4 ++-- 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/chronograf.go b/chronograf.go index bd8b2755b8..ca8ae070e5 100644 --- a/chronograf.go +++ b/chronograf.go @@ -298,11 +298,20 @@ func (g *GroupByVar) parseAbsolute(fragment string) (time.Duration, error) { } func (g *GroupByVar) String() string { - duration := int64(g.Duration/time.Second) / int64(g.Resolution) * 3 - if duration == 0 { - duration = 1 + // The function is: ((total_seconds * millisecond_converstion) / group_by) = pixels / 3 + // Number of points given the pixels + pixels := float64(g.Resolution) / 3.0 + // Move the milliseconds to the other side of the equation + pixels = pixels / 1000.0 + // Remove the number of pixels from the number of seconds + groupby := float64(g.Duration/time.Second) / pixels + if groupby < 1000.0 { + // If groupby is less than 1 second + return "time(" + strconv.Itoa(int(groupby)) + "ms)" } - return "time(" + strconv.Itoa(int(duration)) + "s)" + // If groupby is more than 1 second round to the second + seconds := int(groupby) / 1000 + return "time(" + strconv.Itoa(seconds) + "s)" } func (g *GroupByVar) Name() string { diff --git a/influx/templates_test.go b/influx/templates_test.go index d1d7670b65..e87ec2c84b 100644 --- a/influx/templates_test.go +++ b/influx/templates_test.go @@ -274,7 +274,7 @@ func TestGroupByVarString(t *testing.T) { ReportingInterval: 10 * time.Second, Duration: 24 * time.Hour, }, - want: "time(369s)", + want: "time(370s)", }, { name: "String() outputs a minimum of 1s intervals", @@ -283,7 +283,7 @@ func TestGroupByVarString(t *testing.T) { ReportingInterval: 10 * time.Second, Duration: time.Hour, }, - want: "time(1s)", + want: "time(107ms)", }, } for _, tt := range tests { From 4d6f7628d8e51b88d04711d5e0be303715161e2f Mon Sep 17 00:00:00 2001 From: deniz kusefoglu Date: Mon, 9 Oct 2017 15:09:27 -0700 Subject: [PATCH 03/31] Refrain from coercing data to a date if column name is not time --- ui/src/data_explorer/components/VisHeader.js | 6 +++- ui/src/shared/parsing/resultsToCSV.js | 30 +++++++++++++------- 2 files changed, 25 insertions(+), 11 deletions(-) diff --git a/ui/src/data_explorer/components/VisHeader.js b/ui/src/data_explorer/components/VisHeader.js index 0457460fe7..6e2c85cb01 100644 --- a/ui/src/data_explorer/components/VisHeader.js +++ b/ui/src/data_explorer/components/VisHeader.js @@ -9,7 +9,11 @@ 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') diff --git a/ui/src/shared/parsing/resultsToCSV.js b/ui/src/shared/parsing/resultsToCSV.js index 47596b16ec..18db204c82 100644 --- a/ui/src/shared/parsing/resultsToCSV.js +++ b/ui/src/shared/parsing/resultsToCSV.js @@ -5,19 +5,29 @@ 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 + 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 From 3a07ded85c04b08314e81f7831a78dc3f0162a56 Mon Sep 17 00:00:00 2001 From: deniz kusefoglu Date: Wed, 11 Oct 2017 13:36:59 -0700 Subject: [PATCH 04/31] WIP adds a console.logging csv button to each dashboard cell --- ui/src/shared/components/Layout.js | 7 ++++ ui/src/shared/components/LayoutCell.js | 40 ++++++++++++++++++++++ ui/src/shared/components/LayoutCellMenu.js | 8 ++++- 3 files changed, 54 insertions(+), 1 deletion(-) diff --git a/ui/src/shared/components/Layout.js b/ui/src/shared/components/Layout.js index b1f3970e5c..9e60e5409a 100644 --- a/ui/src/shared/components/Layout.js +++ b/ui/src/shared/components/Layout.js @@ -38,6 +38,13 @@ const Layout = ( ) => async () => { + console.log(cell.name) + const qs = this.props.queries + const templates = this.props.templates + const resolution = 740 + + if (!qs.length) { + console.log('empty!') // TODO + } + + const timeSeriesPromises = qs.map(query => { + const {host, database, rp} = query + const templatesWithResolution = templates.map(temp => { + if (temp.tempVar === ':interval:') { + if (resolution) { + return {...temp, resolution} + } + return {...temp, resolution: 1000} + } + return {...temp} + }) + return fetchTimeSeriesAsync({ + source: host, + db: database, + rp, + query, + tempVars: removeUnselectedTemplateValues(templatesWithResolution), + resolution, + }) + }) + Promise.all(timeSeriesPromises).then(timeSeries => { + console.log('zomG successfull') + const newSeries = timeSeries.map(response => ({response})) + console.log(newSeries) + }) + } + render() { const {cell, children, isEditable} = this.props @@ -46,6 +85,7 @@ class LayoutCell extends Component { onEdit={this.handleSummonOverlay} handleClickOutside={this.closeMenu} onDeleteClick={this.handleDeleteClick} + onDataDownload={this.handleDataDownload} /> + ({isDeleting, onEdit, onDeleteClick, onDelete, onDataDownload, cell}) =>
+
+ +
{isDeleting ?
From 5a1e4d11c5ee1cafa0d8e078ee8bf674fc9bc17f Mon Sep 17 00:00:00 2001 From: deniz kusefoglu Date: Thu, 12 Oct 2017 14:41:24 -0700 Subject: [PATCH 05/31] Create a seperate function to handle csv downloads from dashboard cells --- ui/spec/shared/parsing/resultsToCSVSpec.js | 2 +- ui/src/data_explorer/components/VisHeader.js | 2 +- ui/src/shared/components/LayoutCell.js | 35 +++++++++++--------- ui/src/shared/components/LayoutCellMenu.js | 24 ++++++++++---- ui/src/shared/parsing/resultsToCSV.js | 24 ++++++++++++-- 5 files changed, 61 insertions(+), 26 deletions(-) diff --git a/ui/spec/shared/parsing/resultsToCSVSpec.js b/ui/spec/shared/parsing/resultsToCSVSpec.js index e2e2bae2b1..18c0b8843e 100644 --- a/ui/spec/shared/parsing/resultsToCSVSpec.js +++ b/ui/spec/shared/parsing/resultsToCSVSpec.js @@ -1,4 +1,4 @@ -import resultsToCSV, {formatDate} from 'shared/parsing/resultsToCSV' +import {resultsToCSV, formatDate} from 'shared/parsing/resultsToCSV' describe('formatDate', () => { it('converts timestamp to an excel compatible date string', () => { diff --git a/ui/src/data_explorer/components/VisHeader.js b/ui/src/data_explorer/components/VisHeader.js index 6e2c85cb01..2d3325456e 100644 --- a/ui/src/data_explorer/components/VisHeader.js +++ b/ui/src/data_explorer/components/VisHeader.js @@ -3,7 +3,7 @@ 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 () => { diff --git a/ui/src/shared/components/LayoutCell.js b/ui/src/shared/components/LayoutCell.js index 9ab96c6174..18e4c99ddd 100644 --- a/ui/src/shared/components/LayoutCell.js +++ b/ui/src/shared/components/LayoutCell.js @@ -5,6 +5,9 @@ import LayoutCellMenu from 'shared/components/LayoutCellMenu' import LayoutCellHeader from 'shared/components/LayoutCellHeader' import {fetchTimeSeriesAsync} from 'shared/actions/timeSeries' import {removeUnselectedTemplateValues} from 'src/dashboards/constants' +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) { @@ -32,17 +35,12 @@ class LayoutCell extends Component { this.props.onSummonOverlayTechnologies(cell) } - handleDataDownload = cell => async () => { - console.log(cell.name) - const qs = this.props.queries - const templates = this.props.templates - const resolution = 740 + handleCSVDownload = cell => () => { + const {queries, templates} = this.props + const joined_name = cell.name.split(' ').join('_') + const resolution = undefined // TODO - if (!qs.length) { - console.log('empty!') // TODO - } - - const timeSeriesPromises = qs.map(query => { + const timeSeriesPromises = queries.map(query => { const {host, database, rp} = query const templatesWithResolution = templates.map(temp => { if (temp.tempVar === ':interval:') { @@ -62,10 +60,16 @@ class LayoutCell extends Component { resolution, }) }) - Promise.all(timeSeriesPromises).then(timeSeries => { - console.log('zomG successfull') - const newSeries = timeSeries.map(response => ({response})) - console.log(newSeries) + + Promise.all(timeSeriesPromises).then(results => { + console.log(results) + const CSVString = dashboardtoCSV(results) + try { + download(CSVString, `${joined_name}.csv`, 'text/plain') + } catch (error) { + errorThrown(error, 'Unable to download .csv file') + console.error(error) + } }) } @@ -79,13 +83,14 @@ class LayoutCell extends Component {
+ ({ + isDeleting, + onEdit, + onDeleteClick, + onDelete, + onCSVDownload, + queriesExist, + cell, + }) =>
-
- -
+ {queriesExist + ?
+ +
+ : null} {isDeleting ?
diff --git a/ui/src/shared/parsing/resultsToCSV.js b/ui/src/shared/parsing/resultsToCSV.js index 18db204c82..ff7a2191d6 100644 --- a/ui/src/shared/parsing/resultsToCSV.js +++ b/ui/src/shared/parsing/resultsToCSV.js @@ -4,7 +4,7 @@ import moment from 'moment' export const formatDate = timestamp => moment(timestamp).format('M/D/YYYY h:mm:ss A') -const resultsToCSV = results => { +export const resultsToCSV = results => { if (!_.get(results, ['0', 'series', '0'])) { return {flag: 'no_data', name: '', CSVString: ''} } @@ -30,4 +30,24 @@ const resultsToCSV = results => { 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')) // TODO default? + ) + 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')) // TODO default? + 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 +} From ecc1d23c5b59b0214c37f6c164133b82cfa0efbe Mon Sep 17 00:00:00 2001 From: deniz kusefoglu Date: Fri, 13 Oct 2017 12:16:44 -0700 Subject: [PATCH 06/31] Hoist graph data up to be downloaded upon csv dl push --- ui/src/shared/components/AutoRefresh.js | 5 +- ui/src/shared/components/LayoutCell.js | 62 ++++++++------------- ui/src/shared/components/LayoutCellMenu.js | 7 ++- ui/src/shared/components/RefreshingGraph.js | 3 + ui/src/shared/parsing/resultsToCSV.js | 4 +- 5 files changed, 34 insertions(+), 47 deletions(-) diff --git a/ui/src/shared/components/AutoRefresh.js b/ui/src/shared/components/AutoRefresh.js index d833c3ccf5..9f8685a299 100644 --- a/ui/src/shared/components/AutoRefresh.js +++ b/ui/src/shared/components/AutoRefresh.js @@ -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,12 @@ 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, }) + grabDataForDownload(timeSeries) }) }, diff --git a/ui/src/shared/components/LayoutCell.js b/ui/src/shared/components/LayoutCell.js index 18e4c99ddd..6da54921ba 100644 --- a/ui/src/shared/components/LayoutCell.js +++ b/ui/src/shared/components/LayoutCell.js @@ -3,8 +3,6 @@ import _ from 'lodash' import LayoutCellMenu from 'shared/components/LayoutCellMenu' import LayoutCellHeader from 'shared/components/LayoutCellHeader' -import {fetchTimeSeriesAsync} from 'shared/actions/timeSeries' -import {removeUnselectedTemplateValues} from 'src/dashboards/constants' import {errorThrown} from 'shared/actions/errors' import {dashboardtoCSV} from 'shared/parsing/resultsToCSV' import download from 'src/external/download.js' @@ -14,6 +12,7 @@ class LayoutCell extends Component { super(props) this.state = { isDeleting: false, + celldata: [], } } @@ -35,55 +34,32 @@ class LayoutCell extends Component { this.props.onSummonOverlayTechnologies(cell) } + grabDataForDownload = celldata => { + this.setState({celldata}) + } + handleCSVDownload = cell => () => { - const {queries, templates} = this.props - const joined_name = cell.name.split(' ').join('_') - const resolution = undefined // TODO - - const timeSeriesPromises = queries.map(query => { - const {host, database, rp} = query - const templatesWithResolution = templates.map(temp => { - if (temp.tempVar === ':interval:') { - if (resolution) { - return {...temp, resolution} - } - return {...temp, resolution: 1000} - } - return {...temp} - }) - return fetchTimeSeriesAsync({ - source: host, - db: database, - rp, - query, - tempVars: removeUnselectedTemplateValues(templatesWithResolution), - resolution, - }) - }) - - Promise.all(timeSeriesPromises).then(results => { - console.log(results) - const CSVString = dashboardtoCSV(results) - try { - download(CSVString, `${joined_name}.csv`, 'text/plain') - } catch (error) { - errorThrown(error, 'Unable to download .csv file') - console.error(error) - } - }) + 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 (
{queries.length - ? children + ? React.Children.map(children, child => { + if (child && child.props && child.props.autoRefresh) { + return React.cloneElement(child, { + grabDataForDownload: this.grabDataForDownload, + }) + } + }) :