diff --git a/ui/.eslintrc b/ui/.eslintrc index 996fed43ae..517f82ebf5 100644 --- a/ui/.eslintrc +++ b/ui/.eslintrc @@ -212,7 +212,6 @@ 'react/jsx-boolean-value': [2, 'always'], 'react/jsx-curly-spacing': [2, 'never'], 'react/jsx-equals-spacing': [2, 'never'], - 'react/jsx-indent-props': [2, 2], 'react/jsx-key': 2, 'react/jsx-no-duplicate-props': 2, 'react/jsx-no-undef': 2, diff --git a/ui/src/data_explorer/components/Table.js b/ui/src/data_explorer/components/Table.js index 1a2aef38c3..40ae051bfd 100644 --- a/ui/src/data_explorer/components/Table.js +++ b/ui/src/data_explorer/components/Table.js @@ -3,26 +3,17 @@ import React, {PropTypes} from 'react' import Dimensions from 'react-dimensions' import _ from 'lodash' import moment from 'moment' +import classNames from 'classnames' +import Dropdown from 'shared/components/Dropdown' import {fetchTimeSeriesAsync} from 'shared/actions/timeSeries' import {Table, Column, Cell} from 'fixed-data-table' -const { - arrayOf, - func, - number, - oneOfType, - shape, - string, -} = PropTypes - -const emptyCells = { - columns: [], - values: [], -} +const {arrayOf, bool, func, number, oneOfType, shape, string} = PropTypes const defaultTableHeight = 1000 +const emptySeries = {columns: [], values: []} const CustomCell = React.createClass({ propTypes: { @@ -57,8 +48,9 @@ const ChronoTable = React.createClass({ getInitialState() { return { - cellData: emptyCells, + series: [emptySeries], columnWidths: {}, + activeSeriesIndex: 0, } }, @@ -80,7 +72,6 @@ const ChronoTable = React.createClass({ this.fetchCellData(nextProps.query) }, - async fetchCellData(query) { if (!query || !query.text) { return @@ -92,87 +83,147 @@ const ChronoTable = React.createClass({ const {results} = await fetchTimeSeriesAsync({source: query.host, query}) this.setState({isLoading: false}) - if (!results) { - return this.setState({cellData: emptyCells}) + let series = _.get(results, ['0', 'series'], []) + + if (!series.length) { + return this.setState({series: []}) } - const cellData = _.get(results, ['0', 'series', '0'], false) - - if (!cellData) { - return this.setState({cellData: emptyCells}) - } - - this.setState({cellData}) + series = series.map(s => (s.values ? s : {...s, values: []})) + this.setState({series}) } catch (error) { this.setState({ isLoading: false, - cellData: emptyCells, + series: [], }) throw error } }, handleColumnResize(newColumnWidth, columnKey) { - this.setState(({columnWidths}) => ({ - columnWidths: Object.assign({}, columnWidths, { - [columnKey]: newColumnWidth, - }), - })) + const columnWidths = { + ...this.state.columnWidths, + [columnKey]: newColumnWidth, + } + + this.setState({ + columnWidths, + }) + }, + + handleClickTab(activeSeriesIndex) { + this.setState({activeSeriesIndex}) + }, + + handleClickDropdown(item) { + this.setState({activeSeriesIndex: item.index}) }, - // Table data as a list of array. render() { const {containerWidth, height, query} = this.props - const {cellData, columnWidths, isLoading} = this.state - const {columns, values} = cellData + const {series, columnWidths, isLoading, activeSeriesIndex} = this.state + const {columns, values} = _.get( + series, + [`${activeSeriesIndex}`], + emptySeries + ) + const maximumTabsCount = 11 // adjust height to proper value by subtracting the heights of the UI around it // tab height, graph-container vertical padding, graph-heading height, multitable-header height const stylePixelOffset = 136 - const rowHeight = 34 const defaultColumnWidth = 200 - const width = columns.length > 1 ? defaultColumnWidth : containerWidth const headerHeight = 30 const minWidth = 70 const styleAdjustedHeight = height - stylePixelOffset + const width = columns && columns.length > 1 + ? defaultColumnWidth + : containerWidth if (!query) { return
Please add a query below
} - if (!isLoading && !values.length) { - return
Your query returned no data
+ if (isLoading) { + return
Loading...
} return ( - - {columns.map((columnName, colIndex) => { - return ( - {columnName}} - cell={({rowIndex}) => { - return - }} - width={columnWidths[columnName] || width} - minWidth={minWidth} - /> - ) - })} -
+
+ {series.length < maximumTabsCount + ?
+ {series.map(({name}, i) => ( + + ))} +
+ : ({...s, text: s.name, index}))} + onChoose={this.handleClickDropdown} + selected={series[activeSeriesIndex].name} + buttonSize="btn-xs" + />} +
+ {(columns && !columns.length) || (values && !values.length) + ?
+ This series is empty +
+ : + {columns.map((columnName, colIndex) => { + return ( + {columnName}} + cell={({rowIndex}) => ( + + )} + width={columnWidths[columnName] || width} + minWidth={minWidth} + /> + ) + })} +
} +
+
) }, }) +const TabItem = ({name, index, onClickTab, isActive}) => ( +
onClickTab(index)} + > + {name} +
+) + +TabItem.propTypes = { + name: string, + onClickTab: func.isRequired, + index: number.isRequired, + isActive: bool, +} + export default Dimensions({elementResize: true})(ChronoTable) diff --git a/ui/src/data_explorer/constants/index.js b/ui/src/data_explorer/constants/index.js index 05cf3e517d..885b4cce66 100644 --- a/ui/src/data_explorer/constants/index.js +++ b/ui/src/data_explorer/constants/index.js @@ -11,22 +11,60 @@ export const INFLUXQL_FUNCTIONS = [ 'stddev', ] +const SEPARATOR = 'SEPARATOR' + export const QUERY_TEMPLATES = [ {text: 'Show Databases', query: 'SHOW DATABASES'}, {text: 'Create Database', query: 'CREATE DATABASE "db_name"'}, {text: 'Drop Database', query: 'DROP DATABASE "db_name"'}, + {text: `${SEPARATOR}`}, {text: 'Show Measurements', query: 'SHOW MEASUREMENTS ON "db_name"'}, - {text: 'Show Tag Keys', query: 'SHOW TAG KEYS ON "db_name" FROM "measurement_name"'}, - {text: 'Show Tag Values', query: 'SHOW TAG VALUES ON "db_name" FROM "measurement_name" WITH KEY = "tag_key"'}, - {text: 'Show Retention Policies', query: 'SHOW RETENTION POLICIES on "db_name"'}, - {text: 'Create Retention Policy', query: 'CREATE RETENTION POLICY "rp_name" ON "db_name" DURATION 30d REPLICATION 1 DEFAULT'}, - {text: 'Drop Retention Policy', query: 'DROP RETENTION POLICY "rp_name" ON "db_name"'}, - {text: 'Create Continuous Query', query: 'CREATE CONTINUOUS QUERY "cq_name" ON "db_name" BEGIN SELECT min("field") INTO "target_measurement" FROM "current_measurement" GROUP BY time(30m) END'}, - {text: 'Drop Continuous Query', query: 'DROP CONTINUOUS QUERY "cq_name" ON "db_name"'}, + { + text: 'Show Tag Keys', + query: 'SHOW TAG KEYS ON "db_name" FROM "measurement_name"', + }, + { + text: 'Show Tag Values', + query: 'SHOW TAG VALUES ON "db_name" FROM "measurement_name" WITH KEY = "tag_key"', + }, + {text: `${SEPARATOR}`}, + { + text: 'Show Retention Policies', + query: 'SHOW RETENTION POLICIES on "db_name"', + }, + { + text: 'Create Retention Policy', + query: 'CREATE RETENTION POLICY "rp_name" ON "db_name" DURATION 30d REPLICATION 1 DEFAULT', + }, + { + text: 'Drop Retention Policy', + query: 'DROP RETENTION POLICY "rp_name" ON "db_name"', + }, + {text: `${SEPARATOR}`}, + { + text: 'Show Continuos Queries', + query: 'SHOW CONTINUOUS QUERIES', + }, + { + text: 'Create Continuous Query', + query: 'CREATE CONTINUOUS QUERY "cq_name" ON "db_name" BEGIN SELECT min("field") INTO "target_measurement" FROM "current_measurement" GROUP BY time(30m) END', + }, + { + text: 'Drop Continuous Query', + query: 'DROP CONTINUOUS QUERY "cq_name" ON "db_name"', + }, + {text: `${SEPARATOR}`}, {text: 'Show Users', query: 'SHOW USERS'}, - {text: 'Create User', query: 'CREATE USER "username" WITH PASSWORD \'password\''}, - {text: 'Create Admin User', query: 'CREATE USER "username" WITH PASSWORD \'password\' WITH ALL PRIVILEGES'}, + { + text: 'Create User', + query: 'CREATE USER "username" WITH PASSWORD \'password\'', + }, + { + text: 'Create Admin User', + query: 'CREATE USER "username" WITH PASSWORD \'password\' WITH ALL PRIVILEGES', + }, {text: 'Drop User', query: 'DROP USER "username"'}, + {text: `${SEPARATOR}`}, {text: 'Show Stats', query: 'SHOW STATS'}, {text: 'Show Diagnostics', query: 'SHOW DIAGNOSTICS'}, ] diff --git a/ui/src/shared/components/Dropdown.js b/ui/src/shared/components/Dropdown.js index a18589cd84..6f3441bbff 100644 --- a/ui/src/shared/components/Dropdown.js +++ b/ui/src/shared/components/Dropdown.js @@ -46,70 +46,93 @@ class Dropdown extends Component { } render() { - const {items, selected, className, iconName, actions, addNew, buttonSize, buttonColor, menuWidth} = this.props + const { + items, + selected, + className, + iconName, + actions, + addNew, + buttonSize, + buttonColor, + menuWidth, + } = this.props const {isOpen} = this.state return ( -
+
- {iconName ? : null} + {iconName + ? + : null} {selected}
- {isOpen ? - + {isOpen + ? : null}
) } } -const { - arrayOf, - shape, - string, - func, -} = PropTypes +const {arrayOf, shape, string, func} = PropTypes Dropdown.propTypes = { - actions: arrayOf(shape({ - icon: string.isRequired, - text: string.isRequired, - handler: func.isRequired, - })), - items: arrayOf(shape({ - text: string.isRequired, - })).isRequired, + actions: arrayOf( + shape({ + icon: string.isRequired, + text: string.isRequired, + handler: func.isRequired, + }) + ), + items: arrayOf( + shape({ + text: string.isRequired, + }) + ).isRequired, onChoose: func.isRequired, addNew: shape({ url: string.isRequired, diff --git a/ui/src/style/components/query-editor.scss b/ui/src/style/components/query-editor.scss index b55a015fc0..bb95febc18 100644 --- a/ui/src/style/components/query-editor.scss +++ b/ui/src/style/components/query-editor.scss @@ -21,7 +21,7 @@ border-color 0.25s ease; border: 2px solid $query-editor--bg; background-color: $query-editor--field-bg; - + } .query-editor--field { font-size: 12px; @@ -103,4 +103,7 @@ min-width: $query-editor--templates-menu-width; max-width: $query-editor--templates-menu-width; } -} \ No newline at end of file + .divider { + background: linear-gradient(to right, #00C9FF 0%, #22ADF6 100%); + } +} diff --git a/ui/src/style/components/tables.scss b/ui/src/style/components/tables.scss index 2d99e41abe..a17acdd30c 100644 --- a/ui/src/style/components/tables.scss +++ b/ui/src/style/components/tables.scss @@ -131,6 +131,58 @@ table .monotype { } } +/* + Table Tabs + ---------------------------------------------- +*/ +$table-tab-height: 30px; +$table-tab-scrollbar-height: 6px; + +.table--tabs { + display: flex; + height: $table-tab-height; + align-items: center; +} +.table--tab { + font-size: 12px; + font-weight: 600; + @include no-user-select(); + height: $table-tab-height; + border-radius: $radius-small $radius-small 0 0; + line-height: $table-tab-height; + padding: 0 6px; + background-color: $g4-onyx; + color: $g11-sidewalk; + margin-right: 2px; + transition: + color 0.25s ease, + background-color 0.25s ease; + + &:hover { + background-color: $g5-pepper; + color: $g15-platinum; + cursor: pointer; + } + &.active { + background-color: $g6-smoke; + color: $g18-cloud; + } +} +.table--tabs-dropdown { + display: inline-block; +} +.table--tabs-content { + width: 100%; + height: calc(100% - #{$table-tab-height}); + position: absolute; + top: $table-tab-height; +} + +.table--tabs + .table--tabs-content > .generic-empty-state { + background-color: $g6-smoke !important; + border-radius: 0 $radius-small $radius-small $radius-small; +} + /* Responsive Tables ---------------------------------------------- diff --git a/ui/src/utils/timeSeriesToDygraph.js b/ui/src/utils/timeSeriesToDygraph.js index 3359ba0305..c848456b2d 100644 --- a/ui/src/utils/timeSeriesToDygraph.js +++ b/ui/src/utils/timeSeriesToDygraph.js @@ -16,67 +16,102 @@ const cells = { } // activeQueryIndex is an optional argument that indicated which query's series we want highlighted. -export default function timeSeriesToDygraph(raw = [], activeQueryIndex, isInDataExplorer) { +export default function timeSeriesToDygraph( + raw = [], + activeQueryIndex, + isInDataExplorer +) { // 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] - }, []) + 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 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.length) { - return acc + (columns.length - 1) * values.length - } - return acc - }, 0) + 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}`, + forEach( + serieses, + ({ + name: measurement, + columns, + values, + index: seriesIndex, responseIndex, - seriesIndex, - })) - labels = concat(labels, unsortedLabels) + tags = {}, + }) => { + const rows = map(values || [], vals => ({ + vals, + })) - forEach(rows, ({vals}) => { - const [time, ...rowValues] = 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(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 + 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 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++) { @@ -97,23 +132,32 @@ export default function timeSeriesToDygraph(raw = [], activeQueryIndex, isInData tsMemo[time] = existingRowIndex } - timeSeries[existingRowIndex].values[labelsToValueIndex[label + seriesIndex]] = value + timeSeries[existingRowIndex].values[ + labelsToValueIndex[label + seriesIndex] + ] = value } const sortedTimeSeries = _.sortBy(timeSeries, 'time') - const dygraphSeries = reduce(sortedLabels, (acc, {label, responseIndex}) => { - if (!isInDataExplorer) { - acc[label] = { - axis: responseIndex === 0 ? 'y' : 'y2', + const dygraphSeries = reduce( + sortedLabels, + (acc, {label, responseIndex}) => { + if (!isInDataExplorer) { + acc[label] = { + axis: responseIndex === 0 ? 'y' : 'y2', + } } - } - return acc - }, {}) + return acc + }, + {} + ) return { labels: ['time', ...map(sortedLabels, ({label}) => label)], - timeSeries: map(sortedTimeSeries, ({time, values}) => ([new Date(time), ...values])), + timeSeries: map(sortedTimeSeries, ({time, values}) => [ + new Date(time), + ...values, + ]), dygraphSeries, } }