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,
}
}