diff --git a/ui/src/dashboards/components/CellEditorOverlay.tsx b/ui/src/dashboards/components/CellEditorOverlay.tsx index b49c7f7113..1617474219 100644 --- a/ui/src/dashboards/components/CellEditorOverlay.tsx +++ b/ui/src/dashboards/components/CellEditorOverlay.tsx @@ -82,7 +82,10 @@ const createWorkingDraft = (source: string, query: CellQuery): Query => { return draft } -const createWorkingDrafts = (source: string, queries: CellQuery[]): Query[] => +const createWorkingDrafts = ( + source: string, + queries: CellQuery[] = [] +): Query[] => _.cloneDeep( queries.map((query: CellQuery) => createWorkingDraft(source, query)) ) @@ -179,6 +182,7 @@ class CellEditorOverlay extends Component { queryConfigs={queriesWorkingDraft} editQueryStatus={editQueryStatus} staticLegend={isStaticLegend} + isInCEO={true} /> void moveField: (dragIndex: number, hoverIndex: number) => void } -const GraphOptionsCustomizeFields: SFC = ({ - fields, - onFieldUpdate, - moveField, -}) => { - return ( -
- -
- {fields.map((field, i) => ( - - ))} + +class GraphOptionsCustomizeFields extends PureComponent { + public render() { + const {fields, onFieldUpdate, moveField} = this.props + + return ( +
+ +
+ {fields.map((field, i) => ( + + ))} +
-
- ) + ) + } } export default DragDropContext(HTML5Backend)(GraphOptionsCustomizeFields) diff --git a/ui/src/dashboards/components/GraphOptionsTimeFormat.tsx b/ui/src/dashboards/components/GraphOptionsTimeFormat.tsx index 7c6ad9f966..5e44af6cf1 100644 --- a/ui/src/dashboards/components/GraphOptionsTimeFormat.tsx +++ b/ui/src/dashboards/components/GraphOptionsTimeFormat.tsx @@ -4,7 +4,7 @@ import QuestionMarkTooltip from 'src/shared/components/QuestionMarkTooltip' import { FORMAT_OPTIONS, TIME_FORMAT_CUSTOM, - TIME_FORMAT_DEFAULT, + DEFAULT_TIME_FORMAT, TIME_FORMAT_TOOLTIP_LINK, } from 'src/shared/constants/tableGraph' import {ErrorHandling} from 'src/shared/decorators/errors' @@ -29,7 +29,7 @@ class GraphOptionsTimeFormat extends PureComponent { super(props) this.state = { customFormat: false, - format: this.props.timeFormat || TIME_FORMAT_DEFAULT, + format: this.props.timeFormat || DEFAULT_TIME_FORMAT, } } diff --git a/ui/src/dashboards/components/TableOptions.tsx b/ui/src/dashboards/components/TableOptions.tsx index 12a034f1d2..cdaab75c04 100644 --- a/ui/src/dashboards/components/TableOptions.tsx +++ b/ui/src/dashboards/components/TableOptions.tsx @@ -52,24 +52,6 @@ export class TableOptions extends Component { this.moveField = this.moveField.bind(this) } - public componentWillMount() { - const {handleUpdateTableOptions, tableOptions} = this.props - handleUpdateTableOptions({ - ...tableOptions, - fieldNames: this.computedFieldNames, - }) - } - - public shouldComponentUpdate(nextProps) { - const {tableOptions} = this.props - const tableOptionsDifferent = !_.isEqual( - tableOptions, - nextProps.tableOptions - ) - - return tableOptionsDifferent - } - public render() { const { tableOptions: {timeFormat, fieldNames, verticalTimeAxis, fixFirstColumn}, @@ -122,28 +104,14 @@ export class TableOptions extends Component { ) } - private get fieldNames() { - const { - tableOptions: {fieldNames}, - } = this.props - return fieldNames || [] - } - - private get timeField() { - return ( - this.fieldNames.find(f => f.internalName === 'time') || TIME_FIELD_DEFAULT - ) - } - private moveField(dragIndex, hoverIndex) { const {handleUpdateTableOptions, tableOptions} = this.props const {fieldNames} = tableOptions - const fields = fieldNames.length > 1 ? fieldNames : this.computedFieldNames - const dragField = fields[dragIndex] + const dragField = fieldNames[dragIndex] const removedFields = _.concat( - _.slice(fields, 0, dragIndex), - _.slice(fields, dragIndex + 1) + _.slice(fieldNames, 0, dragIndex), + _.slice(fieldNames, dragIndex + 1) ) const addedFields = _.concat( _.slice(removedFields, 0, hoverIndex), @@ -156,23 +124,6 @@ export class TableOptions extends Component { }) } - private get computedFieldNames() { - const {queryConfigs} = this.props - const queryFields = _.flatten( - queryConfigs.map(({measurement, fields}) => { - return fields.map(({alias}) => { - const internalName = `${measurement}.${alias}` - const existing = this.fieldNames.find( - c => c.internalName === internalName - ) - return existing || {internalName, displayName: '', visible: true} - }) - }) - ) - - return [this.timeField, ...queryFields] - } - private handleChooseSortBy = (option: Option) => { const {tableOptions, handleUpdateTableOptions} = this.props const sortBy = { diff --git a/ui/src/dashboards/components/Visualization.js b/ui/src/dashboards/components/Visualization.js index 96606fa228..fe8b994aca 100644 --- a/ui/src/dashboards/components/Visualization.js +++ b/ui/src/dashboards/components/Visualization.js @@ -24,6 +24,7 @@ const DashVisualization = ( staticLegend, thresholdsListColors, tableOptions, + isInCEO, }, { source: { @@ -53,6 +54,7 @@ const DashVisualization = ( editQueryStatus={editQueryStatus} resizerTopHeight={resizerTopHeight} staticLegend={staticLegend} + isInCEO={isInCEO} />
@@ -82,6 +84,7 @@ DashVisualization.propTypes = { gaugeColors: colorsNumberSchema, lineColors: colorsStringSchema, staticLegend: bool, + isInCEO: bool, } DashVisualization.contextTypes = { diff --git a/ui/src/dashboards/containers/DashboardPage.js b/ui/src/dashboards/containers/DashboardPage.js index a0976bfe2c..e8153319bd 100644 --- a/ui/src/dashboards/containers/DashboardPage.js +++ b/ui/src/dashboards/containers/DashboardPage.js @@ -363,7 +363,6 @@ class DashboardPage extends Component { name: d.name, link: `/sources/${sourceID}/dashboards/${d.id}`, })) - return (
{isTemplating ? ( @@ -566,6 +565,7 @@ const mapStateToProps = (state, {params: {dashboardID}}) => { const dashboard = dashboards.find( d => d.id === idNormalizer(TYPE_ID, dashboardID) ) + const selectedCell = cell return { diff --git a/ui/src/dashboards/utils/tableGraph.ts b/ui/src/dashboards/utils/tableGraph.ts index 7b853f422b..a31e3af9dd 100644 --- a/ui/src/dashboards/utils/tableGraph.ts +++ b/ui/src/dashboards/utils/tableGraph.ts @@ -1,11 +1,11 @@ import calculateSize from 'calculate-size' import _ from 'lodash' -import {reduce} from 'fast.js' +import {map, reduce, filter} from 'fast.js' import { CELL_HORIZONTAL_PADDING, TIME_FIELD_DEFAULT, - TIME_FORMAT_DEFAULT, + DEFAULT_TIME_FORMAT, } from 'src/shared/constants/tableGraph' const calculateTimeColumnWidth = timeFormat => { @@ -77,6 +77,28 @@ const updateMaxWidths = ( ) } +export const computeFieldNames = (existingFieldNames, sortedLabels) => { + const timeField = + existingFieldNames.find(f => f.internalName === 'time') || + TIME_FIELD_DEFAULT + let astNames = [timeField] + + sortedLabels.forEach(({label}) => { + const field = {internalName: label, displayName: '', visible: true} + astNames = [...astNames, field] + }) + + const intersection = existingFieldNames.filter(f => { + return astNames.find(a => a.internalName === f.internalName) + }) + + const newFields = astNames.filter(a => { + return !existingFieldNames.find(f => f.internalName === a.internalName) + }) + + return [...intersection, ...newFields] +} + export const calculateColumnWidths = ( data, fieldNames, @@ -84,7 +106,7 @@ export const calculateColumnWidths = ( verticalTimeAxis ) => { const timeFormatWidth = calculateTimeColumnWidth( - timeFormat === '' ? TIME_FORMAT_DEFAULT : timeFormat + timeFormat === '' ? DEFAULT_TIME_FORMAT : timeFormat ) return reduce( data, @@ -102,3 +124,50 @@ export const calculateColumnWidths = ( {widths: {}, totalWidths: 0} ) } + +export const filterTableColumns = (data, fieldNames) => { + const visibility = {} + const filteredData = map(data, (row, i) => { + return filter(row, (col, j) => { + if (i === 0) { + const foundField = fieldNames.find(field => field.internalName === col) + visibility[j] = foundField ? foundField.visible : true + } + return visibility[j] + }) + }) + return filteredData[0].length ? filteredData : [[]] +} + +export const orderTableColumns = (data, fieldNames) => { + const fieldsSortOrder = fieldNames.map(fieldName => { + return _.findIndex(data[0], dataLabel => { + return dataLabel === fieldName.internalName + }) + }) + const filteredFieldSortOrder = filter(fieldsSortOrder, f => f !== -1) + const orderedData = map(data, row => { + return row.map((v, j, arr) => arr[filteredFieldSortOrder[j]] || v) + }) + return orderedData[0].length ? orderedData : [[]] +} + +export const transformTableData = (data, sort, fieldNames, tableOptions) => { + const {verticalTimeAxis, timeFormat} = tableOptions + const sortIndex = _.indexOf(data[0], sort.field) + const sortedData = [ + data[0], + ..._.orderBy(_.drop(data, 1), sortIndex, [sort.direction]), + ] + const sortedTimeVals = map(sortedData, r => r[0]) + const filteredData = filterTableColumns(sortedData, fieldNames) + const orderedData = orderTableColumns(filteredData, fieldNames) + const transformedData = verticalTimeAxis ? orderedData : _.unzip(orderedData) + const {widths: columnWidths, totalWidths} = calculateColumnWidths( + transformedData, + fieldNames, + timeFormat, + verticalTimeAxis + ) + return {transformedData, sortedTimeVals, columnWidths, totalWidths} +} diff --git a/ui/src/shared/components/AutoRefresh.js b/ui/src/shared/components/AutoRefresh.js index e3fdce8b0f..6e4033593e 100644 --- a/ui/src/shared/components/AutoRefresh.js +++ b/ui/src/shared/components/AutoRefresh.js @@ -5,6 +5,7 @@ import _ from 'lodash' import {fetchTimeSeriesAsync} from 'shared/actions/timeSeries' import {removeUnselectedTemplateValues} from 'src/dashboards/constants' import {intervalValuesPoints} from 'src/shared/constants' +import {getQueryConfig} from 'shared/apis' const AutoRefresh = ComposedComponent => { class wrapper extends Component { @@ -14,12 +15,17 @@ const AutoRefresh = ComposedComponent => { lastQuerySuccessful: true, timeSeries: [], resolution: null, + queryASTs: [], } } - componentDidMount() { - const {queries, templates, autoRefresh} = this.props + async componentDidMount() { + const {queries, templates, autoRefresh, type} = this.props this.executeQueries(queries, templates) + if (type === 'table') { + const queryASTs = await this.getQueryASTs(queries, templates) + this.setState({queryASTs}) + } if (autoRefresh) { this.intervalID = setInterval( () => this.executeQueries(queries, templates), @@ -28,7 +34,19 @@ const AutoRefresh = ComposedComponent => { } } - componentWillReceiveProps(nextProps) { + getQueryASTs = async (queries, templates) => { + return await Promise.all( + queries.map(async q => { + const host = _.isArray(q.host) ? q.host[0] : q.host + const url = host.replace('proxy', 'queries') + const text = q.text + const {data} = await getQueryConfig(url, [{query: text}], templates) + return data.queries[0].queryAST + }) + ) + } + + async componentWillReceiveProps(nextProps) { const inViewDidUpdate = this.props.inView !== nextProps.inView const queriesDidUpdate = this.queryDifference( @@ -45,6 +63,14 @@ const AutoRefresh = ComposedComponent => { queriesDidUpdate || tempVarsDidUpdate || inViewDidUpdate if (shouldRefetch) { + if (this.props.type === 'table') { + const queryASTs = await this.getQueryASTs( + nextProps.queries, + nextProps.templates + ) + this.setState({queryASTs}) + } + this.executeQueries( nextProps.queries, nextProps.templates, @@ -169,8 +195,7 @@ const AutoRefresh = ComposedComponent => { } render() { - const {timeSeries} = this.state - + const {timeSeries, queryASTs} = this.state if (this.state.isFetching && this.state.lastQuerySuccessful) { return ( { setResolution={this.setResolution} isFetchingInitially={false} isRefreshing={true} + queryASTs={queryASTs} /> ) } @@ -188,6 +214,7 @@ const AutoRefresh = ComposedComponent => { {...this.props} data={timeSeries} setResolution={this.setResolution} + queryASTs={queryASTs} /> ) } @@ -221,6 +248,7 @@ const AutoRefresh = ComposedComponent => { } = PropTypes wrapper.propTypes = { + type: string.isRequired, children: element, autoRefresh: number.isRequired, inView: bool, diff --git a/ui/src/shared/components/LineGraph.js b/ui/src/shared/components/LineGraph.js index f18ce1a7ad..f64411e6db 100644 --- a/ui/src/shared/components/LineGraph.js +++ b/ui/src/shared/components/LineGraph.js @@ -3,7 +3,7 @@ import PropTypes from 'prop-types' import Dygraph from 'shared/components/Dygraph' import SingleStat from 'src/shared/components/SingleStat' -import timeSeriesToDygraph from 'utils/timeSeriesTransformers' +import {timeSeriesToDygraph} from 'utils/timeSeriesTransformers' import {colorsStringSchema} from 'shared/schemas' import {ErrorHandling} from 'src/shared/decorators/errors' diff --git a/ui/src/shared/components/RefreshingGraph.js b/ui/src/shared/components/RefreshingGraph.js index 83ccfea5e8..12c935ae1a 100644 --- a/ui/src/shared/components/RefreshingGraph.js +++ b/ui/src/shared/components/RefreshingGraph.js @@ -40,6 +40,7 @@ const RefreshingGraph = ({ editQueryStatus, handleSetHoverTime, grabDataForDownload, + isInCEO, }) => { const prefix = (axes && axes.y.prefix) || '' const suffix = (axes && axes.y.suffix) || '' @@ -55,6 +56,7 @@ const RefreshingGraph = ({ if (type === 'single-stat') { return ( ) } @@ -114,6 +119,7 @@ const RefreshingGraph = ({ return ( { + const {isInCEO} = this.props + if (!isInCEO) { + return + } + this.props.handleUpdateTableOptions({...tableOptions, fieldNames}) + } + componentWillReceiveProps(nextProps) { - const {data} = timeSeriesToTableGraph(nextProps.data) + const updatedProps = _.keys(nextProps).filter( + k => !_.isEqual(this.props[k], nextProps[k]) + ) + const {tableOptions} = nextProps + + let result = {} + + if (_.includes(updatedProps, 'data')) { + result = timeSeriesToTableGraph(nextProps.data, nextProps.queryASTs) + } + + const data = _.get(result, 'data', this.state.data) + const sortedLabels = _.get(result, 'sortedLabels', this.state.sortedLabels) + const fieldNames = computeFieldNames(tableOptions.fieldNames, sortedLabels) + + if (_.includes(updatedProps, 'data')) { + this.handleUpdateTableOptions(fieldNames, tableOptions) + } + if (_.isEmpty(data[0])) { return } - const {sortField, sortDirection} = this.state - const { - tableOptions: { - sortBy: {internalName}, - fieldNames, - verticalTimeAxis, - timeFormat, - }, - } = nextProps - - let direction, sortFieldName + const {sort} = this.state + const internalName = _.get( + nextProps, + ['tableOptions', 'sortBy', 'internalName'], + '' + ) if ( - _.get(this.props, ['tableOptions', 'sortBy', 'internalName'], '') === + !_.get(this.props, ['tableOptions', 'sortBy', 'internalName'], '') === internalName ) { - direction = sortDirection - sortFieldName = sortField - } else { - direction = DEFAULT_SORT - sortFieldName = internalName + sort.direction = DEFAULT_SORT_DIRECTION + sort.field = internalName } - const { - processedData, - sortedTimeVals, - columnWidths, - totalWidths, - } = processTableData( - data, - sortFieldName, - direction, - verticalTimeAxis, - fieldNames, - timeFormat - ) + if ( + _.includes(updatedProps, 'data') || + _.includes(updatedProps, 'tableOptions') + ) { + const { + transformedData, + sortedTimeVals, + columnWidths, + totalWidths, + } = transformTableData(data, sort, fieldNames, tableOptions) - this.setState({ - data, - processedData, - sortedTimeVals, - sortField: sortFieldName, - sortDirection: direction, - columnWidths, - totalColumnWidths: totalWidths, - }) + this.setState({ + data, + sortedLabels, + transformedData, + sortedTimeVals, + sort, + columnWidths, + totalColumnWidths: totalWidths, + }) + } } calcScrollToColRow = () => { @@ -167,6 +188,7 @@ class TableGraph extends Component { handleClickFieldName = fieldName => () => { const {tableOptions} = this.props + const {timeFormat} = tableOptions const {data, sortField, sortDirection} = this.state const verticalTimeAxis = _.get(tableOptions, 'verticalTimeAxis', true) const fieldNames = _.get(tableOptions, 'fieldNames', [TIME_FIELD_DEFAULT]) @@ -175,19 +197,20 @@ class TableGraph extends Component { if (fieldName === sortField) { direction = sortDirection === ASCENDING ? DESCENDING : ASCENDING } else { - direction = DEFAULT_SORT + direction = DEFAULT_SORT_DIRECTION } - const {processedData, sortedTimeVals} = processTableData( + const {transformedData, sortedTimeVals} = transformTableData( data, fieldName, direction, verticalTimeAxis, - fieldNames + fieldNames, + timeFormat ) this.setState({ - processedData, + transformedData, sortedTimeVals, sortField: fieldName, sortDirection: direction, @@ -199,9 +222,9 @@ class TableGraph extends Component { const { tableOptions: {fixFirstColumn}, } = this.props - const {processedData, columnWidths, totalColumnWidths} = this.state - const columnCount = _.get(processedData, ['0', 'length'], 0) - const columnLabel = processedData[0][index] + const {transformedData, columnWidths, totalColumnWidths} = this.state + const columnCount = _.get(transformedData, ['0', 'length'], 0) + const columnLabel = transformedData[0][index] let adjustedColumnSizerWidth = columnWidths[columnLabel] @@ -227,20 +250,20 @@ class TableGraph extends Component { const { hoveredColumnIndex, hoveredRowIndex, - processedData, + transformedData, sortField, sortDirection, } = this.state const {tableOptions, colors} = parent.props const { - timeFormat = TIME_FORMAT_DEFAULT, + timeFormat = DEFAULT_TIME_FORMAT, verticalTimeAxis = VERTICAL_TIME_AXIS_DEFAULT, fixFirstColumn = FIX_FIRST_COLUMN_DEFAULT, fieldNames = [TIME_FIELD_DEFAULT], } = tableOptions - const cellData = processedData[rowIndex][columnIndex] + const cellData = transformedData[rowIndex][columnIndex] const timeFieldIndex = fieldNames.findIndex( field => field.internalName === TIME_FIELD_DEFAULT.internalName @@ -302,7 +325,7 @@ class TableGraph extends Component { const cellContents = isTimeData ? `${moment(cellData).format( - timeFormat === '' ? TIME_FORMAT_DEFAULT : timeFormat + timeFormat === '' ? DEFAULT_TIME_FORMAT : timeFormat )}` : fieldName || `${cellData}` @@ -332,12 +355,12 @@ class TableGraph extends Component { timeColumnWidth, sortField, sortDirection, - processedData, + transformedData, } = this.state const {hoverTime, tableOptions, colors} = this.props const {fixFirstColumn = FIX_FIRST_COLUMN_DEFAULT} = tableOptions - const columnCount = _.get(processedData, ['0', 'length'], 0) - const rowCount = columnCount === 0 ? 0 : processedData.length + const columnCount = _.get(transformedData, ['0', 'length'], 0) + const rowCount = columnCount === 0 ? 0 : transformedData.length const COLUMN_MIN_WIDTH = 100 const COLUMN_MAX_WIDTH = 1000 @@ -348,7 +371,6 @@ class TableGraph extends Component { const tableWidth = _.get(this, ['gridContainer', 'clientWidth'], 0) const tableHeight = _.get(this, ['gridContainer', 'clientHeight'], 0) const {scrollToColumn, scrollToRow} = this.calcScrollToColRow() - return (
({ + handleUpdateTableOptions: bindActionCreators(updateTableOptions, dispatch), +}) + +export default connect(null, mapDispatchToProps)(TableGraph) diff --git a/ui/src/shared/constants/tableGraph.js b/ui/src/shared/constants/tableGraph.js index 04ba2ede50..471586361f 100644 --- a/ui/src/shared/constants/tableGraph.js +++ b/ui/src/shared/constants/tableGraph.js @@ -13,18 +13,18 @@ export const TIME_FIELD_DEFAULT = { export const ASCENDING = 'asc' export const DESCENDING = 'desc' -export const DEFAULT_SORT = ASCENDING +export const DEFAULT_SORT_DIRECTION = ASCENDING export const FIX_FIRST_COLUMN_DEFAULT = true export const VERTICAL_TIME_AXIS_DEFAULT = true export const CELL_HORIZONTAL_PADDING = 30 -export const TIME_FORMAT_DEFAULT = 'MM/DD/YYYY HH:mm:ss' +export const DEFAULT_TIME_FORMAT = 'MM/DD/YYYY HH:mm:ss' export const TIME_FORMAT_CUSTOM = 'Custom' export const FORMAT_OPTIONS = [ - {text: TIME_FORMAT_DEFAULT}, + {text: DEFAULT_TIME_FORMAT}, {text: 'MM/DD/YYYY HH:mm:ss.SSS'}, {text: 'YYYY-MM-DD HH:mm:ss'}, {text: 'HH:mm:ss'}, @@ -36,7 +36,7 @@ export const FORMAT_OPTIONS = [ export const DEFAULT_TABLE_OPTIONS = { verticalTimeAxis: VERTICAL_TIME_AXIS_DEFAULT, - timeFormat: TIME_FORMAT_DEFAULT, + timeFormat: DEFAULT_TIME_FORMAT, sortBy: TIME_FIELD_DEFAULT, wrapping: 'truncate', fieldNames: [TIME_FIELD_DEFAULT], diff --git a/ui/src/utils/groupBy.js b/ui/src/utils/groupBy.js new file mode 100644 index 0000000000..74e9e7b09a --- /dev/null +++ b/ui/src/utils/groupBy.js @@ -0,0 +1,242 @@ +import _ from 'lodash' +import {shiftDate} from 'shared/query/helpers' +import {map, reduce, forEach, concat, clone} from 'fast.js' + +const groupByMap = (responses, responseIndex, groupByColumns) => { + const firstColumns = _.get(responses, [0, 'series', 0, 'columns']) + const accum = [ + { + responseIndex, + series: [ + { + columns: [ + firstColumns[0], + ...groupByColumns, + ...firstColumns.slice(1), + ], + groupByColumns, + name: _.get(responses, [0, 'series', 0, 'name']), + values: [], + }, + ], + }, + ] + + const seriesArray = _.get(responses, [0, 'series']) + seriesArray.forEach(s => { + const prevValues = accum[0].series[0].values + const tagsToAdd = groupByColumns.map(gb => s.tags[gb]) + const newValues = s.values.map(v => [v[0], ...tagsToAdd, ...v.slice(1)]) + accum[0].series[0].values = [...prevValues, ...newValues] + }) + return accum +} + +const constructResults = (raw, groupBys) => { + return _.flatten( + map(raw, (response, index) => { + const responses = _.get(response, 'response.results', []) + + if (groupBys[index]) { + return groupByMap(responses, index, groupBys[index]) + } + + return map(responses, r => ({...r, responseIndex: index})) + }) + ) +} + +const constructSerieses = results => { + return reduce( + results, + (acc, {series = [], responseIndex}) => { + return [ + ...acc, + ...map(series, (item, index) => ({ + ...item, + responseIndex, + seriesIndex: index, + })), + ] + }, + [] + ) +} + +const constructCells = serieses => { + let cellIndex = 0 + let labels = [] + const seriesLabels = [] + const cells = { + label: [], + value: [], + time: [], + seriesIndex: [], + responseIndex: [], + } + forEach( + serieses, + ( + { + name: measurement, + columns, + groupByColumns, + values, + seriesIndex, + responseIndex, + tags = {}, + }, + ind + ) => { + const rows = map(values || [], vals => ({ + vals, + })) + + const unsortedLabels = map(columns.slice(1), (field, i) => ({ + label: + groupByColumns && i <= groupByColumns.length - 1 + ? `${field}` + : `${measurement}.${field}`, + responseIndex, + seriesIndex, + })) + seriesLabels[ind] = unsortedLabels + labels = concat(labels, unsortedLabels) + + forEach(rows, ({vals}) => { + const [time, ...rowValues] = vals + forEach(rowValues, (value, i) => { + cells.label[cellIndex] = unsortedLabels[i].label + cells.value[cellIndex] = value + cells.time[cellIndex] = time + cells.seriesIndex[cellIndex] = seriesIndex + cells.responseIndex[cellIndex] = responseIndex + cellIndex++ // eslint-disable-line no-plusplus + }) + }) + } + ) + const sortedLabels = _.sortBy(labels, 'label') + return {cells, sortedLabels, seriesLabels} +} + +const insertGroupByValues = ( + serieses, + groupBys, + seriesLabels, + labelsToValueIndex, + sortedLabels +) => { + const dashArray = Array(sortedLabels.length).fill('-') + const timeSeries = [] + let existingRowIndex + forEach(serieses, (s, sind) => { + if (groupBys[s.responseIndex]) { + forEach(s.values, vs => { + timeSeries.push({time: vs[0], values: clone(dashArray)}) + existingRowIndex = timeSeries.length - 1 + forEach(vs.slice(1), (v, i) => { + const label = seriesLabels[sind][i].label + timeSeries[existingRowIndex].values[ + labelsToValueIndex[label + s.responseIndex + s.seriesIndex] + ] = v + }) + }) + } + }) + + return timeSeries +} + +const constructTimeSeries = ( + serieses, + cells, + sortedLabels, + groupBys, + seriesLabels +) => { + const nullArray = Array(sortedLabels.length).fill(null) + + const labelsToValueIndex = reduce( + sortedLabels, + (acc, {label, responseIndex, seriesIndex}, i) => { + // adding series index prevents overwriting of two distinct labels that have the same field and measurements + acc[label + responseIndex + seriesIndex] = i + return acc + }, + {} + ) + + const tsMemo = {} + + const timeSeries = insertGroupByValues( + serieses, + groupBys, + seriesLabels, + labelsToValueIndex, + sortedLabels + ) + + let existingRowIndex + + for (let i = 0; i < _.get(cells, ['value', 'length'], 0); i++) { + let time + time = cells.time[i] + const value = cells.value[i] + const label = cells.label[i] + const seriesIndex = cells.seriesIndex[i] + const responseIndex = cells.responseIndex[i] + + if (groupBys[cells.responseIndex[i]]) { + // we've already inserted GroupByValues + continue + } + + if (label.includes('_shifted__')) { + const [, quantity, duration] = label.split('__') + time = +shiftDate(time, quantity, duration).format('x') + } + + existingRowIndex = tsMemo[time] + + if (existingRowIndex === undefined) { + timeSeries.push({ + time, + values: clone(nullArray), + }) + + existingRowIndex = timeSeries.length - 1 + tsMemo[time] = existingRowIndex + } + + timeSeries[existingRowIndex].values[ + labelsToValueIndex[label + responseIndex + seriesIndex] + ] = value + } + + return _.sortBy(timeSeries, 'time') +} + +export const groupByTimeSeriesTransform = (raw, groupBys) => { + if (!groupBys) { + groupBys = Array(raw.length).fill(false) + } + const results = constructResults(raw, groupBys) + + const serieses = constructSerieses(results) + + const {cells, sortedLabels, seriesLabels} = constructCells(serieses) + + const sortedTimeSeries = constructTimeSeries( + serieses, + cells, + sortedLabels, + groupBys, + seriesLabels + ) + + return { + sortedLabels, + sortedTimeSeries, + } +} diff --git a/ui/src/utils/timeSeriesTransformers.js b/ui/src/utils/timeSeriesTransformers.js index f1bebd87d0..0ee22c8e7b 100644 --- a/ui/src/utils/timeSeriesTransformers.js +++ b/ui/src/utils/timeSeriesTransformers.js @@ -1,153 +1,9 @@ import _ from 'lodash' -import {shiftDate} from 'shared/query/helpers' -import {map, reduce, filter, forEach, concat, clone} from 'fast.js' -import {calculateColumnWidths} from 'src/dashboards/utils/tableGraph' - -/** - * Accepts an array of raw influxdb responses and returns a format - * that Dygraph understands. - **/ - -const DEFAULT_SIZE = 0 -const cells = { - label: new Array(DEFAULT_SIZE), - value: new Array(DEFAULT_SIZE), - time: new Array(DEFAULT_SIZE), - seriesIndex: new Array(DEFAULT_SIZE), - responseIndex: new Array(DEFAULT_SIZE), -} - -const timeSeriesTransform = (raw = []) => { - // collect results from each influx response - const results = reduce( - raw, - (acc, rawResponse, responseIndex) => { - const responses = _.get(rawResponse, 'response.results', []) - const indexedResponses = map(responses, response => ({ - ...response, - responseIndex, - })) - return [...acc, ...indexedResponses] - }, - [] - ) - - // collect each series - const serieses = reduce( - results, - (acc, {series = [], responseIndex}, index) => { - return [...acc, ...map(series, item => ({...item, responseIndex, index}))] - }, - [] - ) - - const size = reduce( - serieses, - (acc, {columns, values}) => { - if (columns.length && (values && values.length)) { - return acc + (columns.length - 1) * values.length - } - return acc - }, - 0 - ) - - // convert series into cells with rows and columns - let cellIndex = 0 - let labels = [] - - forEach( - serieses, - ({ - name: measurement, - columns, - values, - index: seriesIndex, - responseIndex, - tags = {}, - }) => { - const rows = map(values || [], vals => ({ - vals, - })) - - // tagSet is each tag key and value for a series - const tagSet = map(Object.keys(tags), tag => `[${tag}=${tags[tag]}]`) - .sort() - .join('') - const unsortedLabels = map(columns.slice(1), field => ({ - label: `${measurement}.${field}${tagSet}`, - responseIndex, - seriesIndex, - })) - labels = concat(labels, unsortedLabels) - - forEach(rows, ({vals}) => { - const [time, ...rowValues] = vals - - forEach(rowValues, (value, i) => { - cells.label[cellIndex] = unsortedLabels[i].label - cells.value[cellIndex] = value - cells.time[cellIndex] = time - cells.seriesIndex[cellIndex] = seriesIndex - cells.responseIndex[cellIndex] = responseIndex - cellIndex++ // eslint-disable-line no-plusplus - }) - }) - } - ) - - const sortedLabels = _.sortBy(labels, 'label') - const tsMemo = {} - const nullArray = Array(sortedLabels.length).fill(null) - - const labelsToValueIndex = reduce( - sortedLabels, - (acc, {label, seriesIndex}, i) => { - // adding series index prevents overwriting of two distinct labels that have the same field and measurements - acc[label + seriesIndex] = i - return acc - }, - {} - ) - - const timeSeries = [] - for (let i = 0; i < size; i++) { - let time = cells.time[i] - const value = cells.value[i] - const label = cells.label[i] - const seriesIndex = cells.seriesIndex[i] - - if (label.includes('_shifted__')) { - const [, quantity, duration] = label.split('__') - time = +shiftDate(time, quantity, duration).format('x') - } - - let existingRowIndex = tsMemo[time] - - if (existingRowIndex === undefined) { - timeSeries.push({ - time, - values: clone(nullArray), - }) - - existingRowIndex = timeSeries.length - 1 - tsMemo[time] = existingRowIndex - } - - timeSeries[existingRowIndex].values[ - labelsToValueIndex[label + seriesIndex] - ] = value - } - const sortedTimeSeries = _.sortBy(timeSeries, 'time') - - return { - sortedLabels, - sortedTimeSeries, - } -} +import {map, reduce} from 'fast.js' +import {groupByTimeSeriesTransform} from 'src/utils/groupBy.js' export const timeSeriesToDygraph = (raw = [], isInDataExplorer) => { - const {sortedLabels, sortedTimeSeries} = timeSeriesTransform(raw) + const {sortedLabels, sortedTimeSeries} = groupByTimeSeriesTransform(raw) const dygraphSeries = reduce( sortedLabels, @@ -172,8 +28,17 @@ export const timeSeriesToDygraph = (raw = [], isInDataExplorer) => { } } -export const timeSeriesToTableGraph = raw => { - const {sortedLabels, sortedTimeSeries} = timeSeriesTransform(raw) +const computeGroupBys = queryASTs => { + return queryASTs.map(queryAST => { + return _.get(queryAST, ['groupBy', 'tags'], false) + }) +} + +export const timeSeriesToTableGraph = (raw, queryASTs) => { + const {sortedLabels, sortedTimeSeries} = groupByTimeSeriesTransform( + raw, + computeGroupBys(queryASTs) + ) const labels = ['time', ...map(sortedLabels, ({label}) => label)] @@ -181,60 +46,6 @@ export const timeSeriesToTableGraph = raw => { const data = tableData.length ? [labels, ...tableData] : [[]] return { data, + sortedLabels, } } - -export const filterTableColumns = (data, fieldNames) => { - const visibility = {} - const filteredData = map(data, (row, i) => { - return filter(row, (col, j) => { - if (i === 0) { - const foundField = fieldNames.find(field => field.internalName === col) - visibility[j] = foundField ? foundField.visible : true - } - return visibility[j] - }) - }) - return filteredData[0].length ? filteredData : [[]] -} - -export const orderTableColumns = (data, fieldNames) => { - const fieldsSortOrder = fieldNames.map(fieldName => { - return _.findIndex(data[0], dataLabel => { - return dataLabel === fieldName.internalName - }) - }) - const filteredFieldSortOrder = filter(fieldsSortOrder, f => f !== -1) - const orderedData = map(data, row => { - return row.map((v, j, arr) => arr[filteredFieldSortOrder[j]]) - }) - return orderedData[0].length ? orderedData : [[]] -} - -export const processTableData = ( - data, - sortFieldName, - direction, - verticalTimeAxis, - fieldNames, - timeFormat -) => { - const sortIndex = _.indexOf(data[0], sortFieldName) - const sortedData = [ - data[0], - ..._.orderBy(_.drop(data, 1), sortIndex, [direction]), - ] - const sortedTimeVals = map(sortedData, r => r[0]) - const filteredData = filterTableColumns(sortedData, fieldNames) - const orderedData = orderTableColumns(filteredData, fieldNames) - const processedData = verticalTimeAxis ? orderedData : _.unzip(orderedData) - const {widths: columnWidths, totalWidths} = calculateColumnWidths( - processedData, - fieldNames, - timeFormat, - verticalTimeAxis - ) - return {processedData, sortedTimeVals, columnWidths, totalWidths} -} - -export default timeSeriesToDygraph diff --git a/ui/test/dashboards/components/TableOptions.test.tsx b/ui/test/dashboards/components/TableOptions.test.tsx index e65fbe0310..1c4923056a 100644 --- a/ui/test/dashboards/components/TableOptions.test.tsx +++ b/ui/test/dashboards/components/TableOptions.test.tsx @@ -22,6 +22,7 @@ const defaultProps = { timeFormat: '', verticalTimeAxis: true, }, + queryASTs: [], } const setup = (override = {}) => { diff --git a/ui/test/utils/timeSeriesTransformers.test.js b/ui/test/utils/timeSeriesTransformers.test.js index b50a20d599..f4d44e8e6f 100644 --- a/ui/test/utils/timeSeriesTransformers.test.js +++ b/ui/test/utils/timeSeriesTransformers.test.js @@ -1,9 +1,17 @@ -import timeSeriesToDygraph, { +import { + timeSeriesToDygraph, timeSeriesToTableGraph, - filterTableColumns, - processTableData, } from 'src/utils/timeSeriesTransformers' -import {DEFAULT_SORT} from 'src/shared/constants/tableGraph' + +import { + filterTableColumns, + transformTableData, +} from 'src/dashboards/utils/tableGraph' + +import { + DEFAULT_SORT_DIRECTION, + DEFAULT_TIME_FORMAT, +} from 'src/shared/constants/tableGraph' describe('timeSeriesToDygraph', () => { it('parses a raw InfluxDB response into a dygraph friendly data format', () => { @@ -178,7 +186,6 @@ describe('timeSeriesToDygraph', () => { }, }, ] - const actual = timeSeriesToDygraph(influxResponse) const expected = { @@ -341,13 +348,45 @@ describe('timeSeriesToTableGraph', () => { }, ] - const actual = timeSeriesToTableGraph(influxResponse) + const qASTs = [ + { + groupBy: { + time: { + interval: '2s', + }, + }, + }, + { + groupBy: { + time: { + interval: '2s', + }, + }, + }, + { + groupBy: { + time: { + interval: '2s', + }, + }, + }, + { + groupBy: { + time: { + interval: '2s', + }, + }, + }, + ] + + const actual = timeSeriesToTableGraph(influxResponse, qASTs) const expected = [ ['time', 'ma.f1', 'mb.f1', 'mc.f1', 'mc.f2'], [1000, 1, 1, null, null], [2000, 2, 2, 3, 3], [4000, null, null, 4, 4], ] + expect(actual.data).toEqual(expected) }) @@ -397,7 +436,38 @@ describe('timeSeriesToTableGraph', () => { }, ] - const actual = timeSeriesToTableGraph(influxResponse) + const qASTs = [ + { + groupBy: { + time: { + interval: '2s', + }, + }, + }, + { + groupBy: { + time: { + interval: '2s', + }, + }, + }, + { + groupBy: { + time: { + interval: '2s', + }, + }, + }, + { + groupBy: { + time: { + interval: '2s', + }, + }, + }, + ] + + const actual = timeSeriesToTableGraph(influxResponse, qASTs) const expected = ['time', 'ma.f1', 'mb.f1', 'mc.f1', 'mc.f2'] expect(actual.data[0]).toEqual(expected) @@ -406,7 +476,9 @@ describe('timeSeriesToTableGraph', () => { it('returns an array of an empty array if there is an empty response', () => { const influxResponse = [] - const actual = timeSeriesToTableGraph(influxResponse) + const qASTs = [] + + const actual = timeSeriesToTableGraph(influxResponse, qASTs) const expected = [[]] expect(actual.data).toEqual(expected) @@ -453,7 +525,7 @@ describe('filterTableColumns', () => { }) }) -describe('processTableData', () => { +describe('transformTableData', () => { it('sorts the data based on the provided sortFieldName', () => { const data = [ ['time', 'f1', 'f2'], @@ -461,9 +533,11 @@ describe('processTableData', () => { [2000, 1000, 3000], [3000, 2000, 1000], ] - const sortFieldName = 'f1' - const direction = DEFAULT_SORT - const verticalTimeAxis = true + const sort = {field: 'f1', direction: DEFAULT_SORT_DIRECTION} + const tableOptions = { + verticalTimeAxis: true, + timeFormat: DEFAULT_TIME_FORMAT, + } const fieldNames = [ {internalName: 'time', displayName: 'Time', visible: true}, @@ -471,13 +545,7 @@ describe('processTableData', () => { {internalName: 'f2', displayName: 'F2', visible: true}, ] - const actual = processTableData( - data, - sortFieldName, - direction, - verticalTimeAxis, - fieldNames - ) + const actual = transformTableData(data, sort, fieldNames, tableOptions) const expected = [ ['time', 'f1', 'f2'], [2000, 1000, 3000], @@ -485,7 +553,7 @@ describe('processTableData', () => { [1000, 3000, 2000], ] - expect(actual.processedData).toEqual(expected) + expect(actual.transformedData).toEqual(expected) }) it('filters out columns that should not be visible', () => { @@ -495,9 +563,13 @@ describe('processTableData', () => { [2000, 1000, 3000], [3000, 2000, 1000], ] - const sortFieldName = 'time' - const direction = DEFAULT_SORT - const verticalTimeAxis = true + + const sort = {field: 'time', direction: DEFAULT_SORT_DIRECTION} + + const tableOptions = { + verticalTimeAxis: true, + timeFormat: DEFAULT_TIME_FORMAT, + } const fieldNames = [ {internalName: 'time', displayName: 'Time', visible: true}, @@ -505,16 +577,11 @@ describe('processTableData', () => { {internalName: 'f2', displayName: 'F2', visible: true}, ] - const actual = processTableData( - data, - sortFieldName, - direction, - verticalTimeAxis, - fieldNames - ) + const actual = transformTableData(data, sort, fieldNames, tableOptions) + const expected = [['time', 'f2'], [1000, 2000], [2000, 3000], [3000, 1000]] - expect(actual.processedData).toEqual(expected) + expect(actual.transformedData).toEqual(expected) }) it('filters out invisible columns after sorting', () => { @@ -524,9 +591,13 @@ describe('processTableData', () => { [2000, 1000, 3000], [3000, 2000, 1000], ] - const sortFieldName = 'f1' - const direction = DEFAULT_SORT - const verticalTimeAxis = true + + const sort = {field: 'f1', direction: DEFAULT_SORT_DIRECTION} + + const tableOptions = { + verticalTimeAxis: true, + timeFormat: DEFAULT_TIME_FORMAT, + } const fieldNames = [ {internalName: 'time', displayName: 'Time', visible: true}, @@ -534,16 +605,11 @@ describe('processTableData', () => { {internalName: 'f2', displayName: 'F2', visible: true}, ] - const actual = processTableData( - data, - sortFieldName, - direction, - verticalTimeAxis, - fieldNames - ) + const actual = transformTableData(data, sort, fieldNames, tableOptions) + const expected = [['time', 'f2'], [2000, 3000], [3000, 1000], [1000, 2000]] - expect(actual.processedData).toEqual(expected) + expect(actual.transformedData).toEqual(expected) }) }) @@ -555,9 +621,13 @@ describe('if verticalTimeAxis is false', () => { [2000, 1000, 3000], [3000, 2000, 1000], ] - const sortFieldName = 'time' - const direction = DEFAULT_SORT - const verticalTimeAxis = false + + const sort = {field: 'time', direction: DEFAULT_SORT_DIRECTION} + + const tableOptions = { + verticalTimeAxis: false, + timeFormat: DEFAULT_TIME_FORMAT, + } const fieldNames = [ {internalName: 'time', displayName: 'Time', visible: true}, @@ -565,20 +635,15 @@ describe('if verticalTimeAxis is false', () => { {internalName: 'f2', displayName: 'F2', visible: true}, ] - const actual = processTableData( - data, - sortFieldName, - direction, - verticalTimeAxis, - fieldNames - ) + const actual = transformTableData(data, sort, fieldNames, tableOptions) + const expected = [ ['time', 1000, 2000, 3000], ['f1', 3000, 1000, 2000], ['f2', 2000, 3000, 1000], ] - expect(actual.processedData).toEqual(expected) + expect(actual.transformedData).toEqual(expected) }) it('transforms data after filtering out invisible columns', () => { @@ -588,9 +653,13 @@ describe('if verticalTimeAxis is false', () => { [2000, 1000, 3000], [3000, 2000, 1000], ] - const sortFieldName = 'f1' - const direction = DEFAULT_SORT - const verticalTimeAxis = false + + const sort = {field: 'f1', direction: DEFAULT_SORT_DIRECTION} + + const tableOptions = { + verticalTimeAxis: false, + timeFormat: DEFAULT_TIME_FORMAT, + } const fieldNames = [ {internalName: 'time', displayName: 'Time', visible: true}, @@ -598,15 +667,10 @@ describe('if verticalTimeAxis is false', () => { {internalName: 'f2', displayName: 'F2', visible: true}, ] - const actual = processTableData( - data, - sortFieldName, - direction, - verticalTimeAxis, - fieldNames - ) + const actual = transformTableData(data, sort, fieldNames, tableOptions) + const expected = [['time', 2000, 3000, 1000], ['f2', 3000, 1000, 2000]] - expect(actual.processedData).toEqual(expected) + expect(actual.transformedData).toEqual(expected) }) })