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-boolean-value': [2, 'always'],
'react/jsx-curly-spacing': [2, 'never'], 'react/jsx-curly-spacing': [2, 'never'],
'react/jsx-equals-spacing': [2, 'never'], 'react/jsx-equals-spacing': [2, 'never'],
'react/jsx-indent-props': [2, 2],
'react/jsx-key': 2, 'react/jsx-key': 2,
'react/jsx-no-duplicate-props': 2, 'react/jsx-no-duplicate-props': 2,
'react/jsx-no-undef': 2, 'react/jsx-no-undef': 2,

View File

@ -3,26 +3,17 @@ import React, {PropTypes} from 'react'
import Dimensions from 'react-dimensions' import Dimensions from 'react-dimensions'
import _ from 'lodash' import _ from 'lodash'
import moment from 'moment' import moment from 'moment'
import classNames from 'classnames'
import Dropdown from 'shared/components/Dropdown'
import {fetchTimeSeriesAsync} from 'shared/actions/timeSeries' import {fetchTimeSeriesAsync} from 'shared/actions/timeSeries'
import {Table, Column, Cell} from 'fixed-data-table' import {Table, Column, Cell} from 'fixed-data-table'
const { const {arrayOf, bool, func, number, oneOfType, shape, string} = PropTypes
arrayOf,
func,
number,
oneOfType,
shape,
string,
} = PropTypes
const emptyCells = {
columns: [],
values: [],
}
const defaultTableHeight = 1000 const defaultTableHeight = 1000
const emptySeries = {columns: [], values: []}
const CustomCell = React.createClass({ const CustomCell = React.createClass({
propTypes: { propTypes: {
@ -57,8 +48,9 @@ const ChronoTable = React.createClass({
getInitialState() { getInitialState() {
return { return {
cellData: emptyCells, series: [emptySeries],
columnWidths: {}, columnWidths: {},
activeSeriesIndex: 0,
} }
}, },
@ -80,7 +72,6 @@ const ChronoTable = React.createClass({
this.fetchCellData(nextProps.query) this.fetchCellData(nextProps.query)
}, },
async fetchCellData(query) { async fetchCellData(query) {
if (!query || !query.text) { if (!query || !query.text) {
return return
@ -92,87 +83,147 @@ const ChronoTable = React.createClass({
const {results} = await fetchTimeSeriesAsync({source: query.host, query}) const {results} = await fetchTimeSeriesAsync({source: query.host, query})
this.setState({isLoading: false}) this.setState({isLoading: false})
if (!results) { let series = _.get(results, ['0', 'series'], [])
return this.setState({cellData: emptyCells})
if (!series.length) {
return this.setState({series: []})
} }
const cellData = _.get(results, ['0', 'series', '0'], false) series = series.map(s => (s.values ? s : {...s, values: []}))
this.setState({series})
if (!cellData) {
return this.setState({cellData: emptyCells})
}
this.setState({cellData})
} catch (error) { } catch (error) {
this.setState({ this.setState({
isLoading: false, isLoading: false,
cellData: emptyCells, series: [],
}) })
throw error throw error
} }
}, },
handleColumnResize(newColumnWidth, columnKey) { handleColumnResize(newColumnWidth, columnKey) {
this.setState(({columnWidths}) => ({ const columnWidths = {
columnWidths: Object.assign({}, columnWidths, { ...this.state.columnWidths,
[columnKey]: newColumnWidth, [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() { render() {
const {containerWidth, height, query} = this.props const {containerWidth, height, query} = this.props
const {cellData, columnWidths, isLoading} = this.state const {series, columnWidths, isLoading, activeSeriesIndex} = this.state
const {columns, values} = cellData const {columns, values} = _.get(
series,
[`${activeSeriesIndex}`],
emptySeries
)
const maximumTabsCount = 11
// adjust height to proper value by subtracting the heights of the UI around it // 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 // tab height, graph-container vertical padding, graph-heading height, multitable-header height
const stylePixelOffset = 136 const stylePixelOffset = 136
const rowHeight = 34 const rowHeight = 34
const defaultColumnWidth = 200 const defaultColumnWidth = 200
const width = columns.length > 1 ? defaultColumnWidth : containerWidth
const headerHeight = 30 const headerHeight = 30
const minWidth = 70 const minWidth = 70
const styleAdjustedHeight = height - stylePixelOffset const styleAdjustedHeight = height - stylePixelOffset
const width = columns && columns.length > 1
? defaultColumnWidth
: containerWidth
if (!query) { if (!query) {
return <div className="generic-empty-state">Please add a query below</div> return <div className="generic-empty-state">Please add a query below</div>
} }
if (!isLoading && !values.length) { if (isLoading) {
return <div className="generic-empty-state">Your query returned no data</div> return <div className="generic-empty-state">Loading...</div>
} }
return ( return (
<Table <div style={{width: '100%', height: '100%', position: 'relative'}}>
onColumnResizeEndCallback={this.handleColumnResize} {series.length < maximumTabsCount
isColumnResizing={false} ? <div className="table--tabs">
rowHeight={rowHeight} {series.map(({name}, i) => (
rowsCount={values.length} <TabItem
width={containerWidth} isActive={i === activeSeriesIndex}
ownerHeight={styleAdjustedHeight} key={i}
height={styleAdjustedHeight} name={name}
headerHeight={headerHeight}> index={i}
{columns.map((columnName, colIndex) => { onClickTab={this.handleClickTab}
return ( />
<Column ))}
isResizable={true} </div>
key={columnName} : <Dropdown
columnKey={columnName} className="dropdown-160 table--tabs-dropdown"
header={<Cell>{columnName}</Cell>} items={series.map((s, index) => ({...s, text: s.name, index}))}
cell={({rowIndex}) => { onChoose={this.handleClickDropdown}
return <CustomCell columnName={columnName} data={values[rowIndex][colIndex]} /> selected={series[activeSeriesIndex].name}
}} buttonSize="btn-xs"
width={columnWidths[columnName] || width} />}
minWidth={minWidth} <div className="table--tabs-content">
/> {(columns && !columns.length) || (values && !values.length)
) ? <div className="generic-empty-state">
})} This series is empty
</Table> </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) export default Dimensions({elementResize: true})(ChronoTable)

View File

@ -11,22 +11,60 @@ export const INFLUXQL_FUNCTIONS = [
'stddev', 'stddev',
] ]
const SEPARATOR = 'SEPARATOR'
export const QUERY_TEMPLATES = [ export const QUERY_TEMPLATES = [
{text: 'Show Databases', query: 'SHOW DATABASES'}, {text: 'Show Databases', query: 'SHOW DATABASES'},
{text: 'Create Database', query: 'CREATE DATABASE "db_name"'}, {text: 'Create Database', query: 'CREATE DATABASE "db_name"'},
{text: 'Drop Database', query: 'DROP DATABASE "db_name"'}, {text: 'Drop Database', query: 'DROP DATABASE "db_name"'},
{text: `${SEPARATOR}`},
{text: 'Show Measurements', query: 'SHOW MEASUREMENTS ON "db_name"'}, {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 Tag Keys',
{text: 'Show Retention Policies', query: 'SHOW RETENTION POLICIES on "db_name"'}, query: 'SHOW TAG KEYS ON "db_name" FROM "measurement_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: 'Show Tag Values',
{text: 'Drop Continuous Query', query: 'DROP CONTINUOUS QUERY "cq_name" ON "db_name"'}, 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: '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: 'Drop User', query: 'DROP USER "username"'},
{text: `${SEPARATOR}`},
{text: 'Show Stats', query: 'SHOW STATS'}, {text: 'Show Stats', query: 'SHOW STATS'},
{text: 'Show Diagnostics', query: 'SHOW DIAGNOSTICS'}, {text: 'Show Diagnostics', query: 'SHOW DIAGNOSTICS'},
] ]

View File

@ -46,70 +46,93 @@ class Dropdown extends Component {
} }
render() { 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 const {isOpen} = this.state
return ( 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}`}> <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="dropdown-selected">{selected}</span>
<span className="caret" /> <span className="caret" />
</div> </div>
{isOpen ? {isOpen
<ul className="dropdown-menu" style={{width: menuWidth}}> ? <ul className="dropdown-menu" style={{width: menuWidth}}>
{items.map((item, i) => { {items.map((item, i) => {
return ( if (item.text === 'SEPARATOR') {
<li className="dropdown-item" key={i}> return <li key={i} role="separator" className="divider" />
<a href="#" onClick={() => this.handleSelection(item)}> }
{item.text} return (
</a> <li className="dropdown-item" key={i}>
{actions.length > 0 ? <a href="#" onClick={() => this.handleSelection(item)}>
<div className="dropdown-item__actions"> {item.text}
{actions.map((action) => { </a>
return ( {actions.length > 0
<button key={action.text} className="dropdown-item__action" onClick={(e) => this.handleAction(e, action, item)}> ? <div className="dropdown-item__actions">
<span title={action.text} className={`icon ${action.icon}`}></span> {actions.map(action => {
</button> return (
) <button
})} key={action.text}
</div> className="dropdown-item__action"
: null} onClick={e =>
</li> this.handleAction(e, action, item)}
) >
})} <span
{ title={action.text}
addNew ? className={`icon ${action.icon}`}
<li> />
<Link to={addNew.url}> </button>
{addNew.text} )
</Link> })}
</li> : </div>
null : null}
} </li>
</ul> )
})}
{addNew
? <li>
<Link to={addNew.url}>
{addNew.text}
</Link>
</li>
: null}
</ul>
: null} : null}
</div> </div>
) )
} }
} }
const { const {arrayOf, shape, string, func} = PropTypes
arrayOf,
shape,
string,
func,
} = PropTypes
Dropdown.propTypes = { Dropdown.propTypes = {
actions: arrayOf(shape({ actions: arrayOf(
icon: string.isRequired, shape({
text: string.isRequired, icon: string.isRequired,
handler: func.isRequired, text: string.isRequired,
})), handler: func.isRequired,
items: arrayOf(shape({ })
text: string.isRequired, ),
})).isRequired, items: arrayOf(
shape({
text: string.isRequired,
})
).isRequired,
onChoose: func.isRequired, onChoose: func.isRequired,
addNew: shape({ addNew: shape({
url: string.isRequired, url: string.isRequired,

View File

@ -21,7 +21,7 @@
border-color 0.25s ease; border-color 0.25s ease;
border: 2px solid $query-editor--bg; border: 2px solid $query-editor--bg;
background-color: $query-editor--field-bg; background-color: $query-editor--field-bg;
} }
.query-editor--field { .query-editor--field {
font-size: 12px; font-size: 12px;
@ -103,4 +103,7 @@
min-width: $query-editor--templates-menu-width; min-width: $query-editor--templates-menu-width;
max-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 Responsive Tables
---------------------------------------------- ----------------------------------------------

View File

@ -16,67 +16,102 @@ const cells = {
} }
// activeQueryIndex is an optional argument that indicated which query's series we want highlighted. // 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 // collect results from each influx response
const results = reduce(raw, (acc, rawResponse, responseIndex) => { const results = reduce(
const responses = _.get(rawResponse, 'response.results', []) raw,
const indexedResponses = map(responses, (response) => ({...response, responseIndex})) (acc, rawResponse, responseIndex) => {
return [...acc, ...indexedResponses] const responses = _.get(rawResponse, 'response.results', [])
}, []) const indexedResponses = map(responses, response => ({
...response,
responseIndex,
}))
return [...acc, ...indexedResponses]
},
[]
)
// collect each series // collect each series
const serieses = reduce(results, (acc, {series = [], responseIndex}, index) => { const serieses = reduce(
return [...acc, ...map(series, (item) => ({...item, responseIndex, index}))] results,
}, []) (acc, {series = [], responseIndex}, index) => {
return [...acc, ...map(series, item => ({...item, responseIndex, index}))]
},
[]
)
const size = reduce(serieses, (acc, {columns, values}) => { const size = reduce(
if (columns.length && values.length) { serieses,
return acc + (columns.length - 1) * values.length (acc, {columns, values}) => {
} if (columns.length && (values && values.length)) {
return acc return acc + (columns.length - 1) * values.length
}, 0) }
return acc
},
0
)
// convert series into cells with rows and columns // convert series into cells with rows and columns
let cellIndex = 0 let cellIndex = 0
let labels = [] let labels = []
forEach(serieses, ({name: measurement, columns, values, index: seriesIndex, responseIndex, tags = {}}) => { forEach(
const rows = map(values, (vals) => ({ serieses,
vals, ({
})) name: measurement,
columns,
// tagSet is each tag key and value for a series values,
const tagSet = map(Object.keys(tags), (tag) => `[${tag}=${tags[tag]}]`).sort().join('') index: seriesIndex,
const unsortedLabels = map(columns.slice(1), (field) => ({
label: `${measurement}.${field}${tagSet}`,
responseIndex, responseIndex,
seriesIndex, tags = {},
})) }) => {
labels = concat(labels, unsortedLabels) const rows = map(values || [], vals => ({
vals,
}))
forEach(rows, ({vals}) => { // tagSet is each tag key and value for a series
const [time, ...rowValues] = vals 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) => { forEach(rows, ({vals}) => {
cells.label[cellIndex] = unsortedLabels[i].label const [time, ...rowValues] = vals
cells.value[cellIndex] = value
cells.time[cellIndex] = time forEach(rowValues, (value, i) => {
cells.seriesIndex[cellIndex] = seriesIndex cells.label[cellIndex] = unsortedLabels[i].label
cells.responseIndex[cellIndex] = responseIndex cells.value[cellIndex] = value
cellIndex++ // eslint-disable-line no-plusplus cells.time[cellIndex] = time
cells.seriesIndex[cellIndex] = seriesIndex
cells.responseIndex[cellIndex] = responseIndex
cellIndex++ // eslint-disable-line no-plusplus
})
}) })
}) }
}) )
const sortedLabels = _.sortBy(labels, 'label') const sortedLabels = _.sortBy(labels, 'label')
const tsMemo = {} const tsMemo = {}
const nullArray = Array(sortedLabels.length).fill(null) const nullArray = Array(sortedLabels.length).fill(null)
const labelsToValueIndex = reduce(sortedLabels, (acc, {label, seriesIndex}, i) => { const labelsToValueIndex = reduce(
// adding series index prevents overwriting of two distinct labels that have the same field and measurements sortedLabels,
acc[label + seriesIndex] = i (acc, {label, seriesIndex}, i) => {
return acc // adding series index prevents overwriting of two distinct labels that have the same field and measurements
}, {}) acc[label + seriesIndex] = i
return acc
},
{}
)
const timeSeries = [] const timeSeries = []
for (let i = 0; i < size; i++) { for (let i = 0; i < size; i++) {
@ -97,23 +132,32 @@ export default function timeSeriesToDygraph(raw = [], activeQueryIndex, isInData
tsMemo[time] = existingRowIndex tsMemo[time] = existingRowIndex
} }
timeSeries[existingRowIndex].values[labelsToValueIndex[label + seriesIndex]] = value timeSeries[existingRowIndex].values[
labelsToValueIndex[label + seriesIndex]
] = value
} }
const sortedTimeSeries = _.sortBy(timeSeries, 'time') const sortedTimeSeries = _.sortBy(timeSeries, 'time')
const dygraphSeries = reduce(sortedLabels, (acc, {label, responseIndex}) => { const dygraphSeries = reduce(
if (!isInDataExplorer) { sortedLabels,
acc[label] = { (acc, {label, responseIndex}) => {
axis: responseIndex === 0 ? 'y' : 'y2', if (!isInDataExplorer) {
acc[label] = {
axis: responseIndex === 0 ? 'y' : 'y2',
}
} }
}
return acc return acc
}, {}) },
{}
)
return { return {
labels: ['time', ...map(sortedLabels, ({label}) => label)], 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, dygraphSeries,
} }
} }