Merge pull request #1334 from influxdata/feature/cont-queries

Feature/cont queries
pull/10616/head
Andrew Watkins 2017-04-28 12:47:56 -07:00 committed by GitHub
commit d9653fd3f0
7 changed files with 385 additions and 175 deletions

View File

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

View File

@ -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 <div className="generic-empty-state">Please add a query below</div>
}
if (!isLoading && !values.length) {
return <div className="generic-empty-state">Your query returned no data</div>
if (isLoading) {
return <div className="generic-empty-state">Loading...</div>
}
return (
<Table
onColumnResizeEndCallback={this.handleColumnResize}
isColumnResizing={false}
rowHeight={rowHeight}
rowsCount={values.length}
width={containerWidth}
ownerHeight={styleAdjustedHeight}
height={styleAdjustedHeight}
headerHeight={headerHeight}>
{columns.map((columnName, colIndex) => {
return (
<Column
isResizable={true}
key={columnName}
columnKey={columnName}
header={<Cell>{columnName}</Cell>}
cell={({rowIndex}) => {
return <CustomCell columnName={columnName} data={values[rowIndex][colIndex]} />
}}
width={columnWidths[columnName] || width}
minWidth={minWidth}
/>
)
})}
</Table>
<div style={{width: '100%', height: '100%', position: 'relative'}}>
{series.length < maximumTabsCount
? <div className="table--tabs">
{series.map(({name}, i) => (
<TabItem
isActive={i === activeSeriesIndex}
key={i}
name={name}
index={i}
onClickTab={this.handleClickTab}
/>
))}
</div>
: <Dropdown
className="dropdown-160 table--tabs-dropdown"
items={series.map((s, index) => ({...s, text: s.name, index}))}
onChoose={this.handleClickDropdown}
selected={series[activeSeriesIndex].name}
buttonSize="btn-xs"
/>}
<div className="table--tabs-content">
{(columns && !columns.length) || (values && !values.length)
? <div className="generic-empty-state">
This series is empty
</div>
: <Table
onColumnResizeEndCallback={this.handleColumnResize}
isColumnResizing={false}
rowHeight={rowHeight}
rowsCount={values.length}
width={containerWidth}
ownerHeight={styleAdjustedHeight}
height={styleAdjustedHeight}
headerHeight={headerHeight}
>
{columns.map((columnName, colIndex) => {
return (
<Column
isResizable={true}
key={columnName}
columnKey={columnName}
header={<Cell>{columnName}</Cell>}
cell={({rowIndex}) => (
<CustomCell
columnName={columnName}
data={values[rowIndex][colIndex]}
/>
)}
width={columnWidths[columnName] || width}
minWidth={minWidth}
/>
)
})}
</Table>}
</div>
</div>
)
},
})
const TabItem = ({name, index, onClickTab, isActive}) => (
<div
className={classNames('table--tab', {active: isActive})}
onClick={() => onClickTab(index)}
>
{name}
</div>
)
TabItem.propTypes = {
name: string,
onClickTab: func.isRequired,
index: number.isRequired,
isActive: bool,
}
export default Dimensions({elementResize: true})(ChronoTable)

View File

@ -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'},
]

View File

@ -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 (
<div onClick={this.toggleMenu} className={classnames(`dropdown ${className}`, {open: isOpen})}>
<div
onClick={this.toggleMenu}
className={classnames(`dropdown ${className}`, {open: isOpen})}
>
<div className={`btn dropdown-toggle ${buttonSize} ${buttonColor}`}>
{iconName ? <span className={classnames('icon', {[iconName]: true})}></span> : null}
{iconName
? <span className={classnames('icon', {[iconName]: true})} />
: null}
<span className="dropdown-selected">{selected}</span>
<span className="caret" />
</div>
{isOpen ?
<ul className="dropdown-menu" style={{width: menuWidth}}>
{items.map((item, i) => {
return (
<li className="dropdown-item" key={i}>
<a href="#" onClick={() => this.handleSelection(item)}>
{item.text}
</a>
{actions.length > 0 ?
<div className="dropdown-item__actions">
{actions.map((action) => {
return (
<button key={action.text} className="dropdown-item__action" onClick={(e) => this.handleAction(e, action, item)}>
<span title={action.text} className={`icon ${action.icon}`}></span>
</button>
)
})}
</div>
: null}
</li>
)
})}
{
addNew ?
<li>
<Link to={addNew.url}>
{addNew.text}
</Link>
</li> :
null
}
</ul>
{isOpen
? <ul className="dropdown-menu" style={{width: menuWidth}}>
{items.map((item, i) => {
if (item.text === 'SEPARATOR') {
return <li key={i} role="separator" className="divider" />
}
return (
<li className="dropdown-item" key={i}>
<a href="#" onClick={() => this.handleSelection(item)}>
{item.text}
</a>
{actions.length > 0
? <div className="dropdown-item__actions">
{actions.map(action => {
return (
<button
key={action.text}
className="dropdown-item__action"
onClick={e =>
this.handleAction(e, action, item)}
>
<span
title={action.text}
className={`icon ${action.icon}`}
/>
</button>
)
})}
</div>
: null}
</li>
)
})}
{addNew
? <li>
<Link to={addNew.url}>
{addNew.text}
</Link>
</li>
: null}
</ul>
: null}
</div>
)
}
}
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,

View File

@ -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;
}
}
.divider {
background: linear-gradient(to right, #00C9FF 0%, #22ADF6 100%);
}
}

View File

@ -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
----------------------------------------------

View File

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