Merge branch 'master' into nls/drag-fixes

pull/10616/head
Andrew Watkins 2017-08-14 15:30:57 -07:00 committed by GitHub
commit a5e8c5ba2e
58 changed files with 1351 additions and 772 deletions

1
.gitignore vendored
View File

@ -17,3 +17,4 @@ npm-debug.log
.jssrc
.dev-jssrc
.bindata
ui/reports

View File

@ -1,9 +1,13 @@
## v1.3.7.0 [unreleased]
### Bug Fixes
1. [#1795](https://github.com/influxdata/chronograf/pull/1795): Fix uptime status on Windows hosts running Telegraf
1. [#1715](https://github.com/influxdata/chronograf/pull/1715): Chronograf now renders on IE11.
1. [#1845](https://github.com/influxdata/chronograf/pull/1845): Fix no-scroll bar appearing in the Data Explorer table
1. [#1864](https://github.com/influxdata/chronograf/pull/1864): Fix Write Data form upload button and add `onDragExit` handler
1. [#1866](https://github.com/influxdata/chronograf/pull/1866): Fix missing cell type (and consequently single-stat)
### Features
1. [#1863](https://github.com/influxdata/chronograf/pull/1863): Improve 'new-sources' server flag example by adding 'type' key
### UI Improvements
1. [#1846](https://github.com/influxdata/chronograf/pull/1846): Increase screen real estate of Query Maker in the Cell Editor Overlay

View File

@ -8,6 +8,7 @@ import (
"fmt"
"io"
"net/http"
"regexp"
"strconv"
"strings"
"time"
@ -193,32 +194,103 @@ type GroupByVar struct {
// Exec is responsible for extracting the Duration from the query
func (g *GroupByVar) Exec(query string) {
whereClause := "WHERE time > now() - "
whereClause := "WHERE"
start := strings.Index(query, whereClause)
if start == -1 {
// no where clause
return
}
// reposition start to the END of the where clause
// reposition start to after the 'where' keyword
durStr := query[start+len(whereClause):]
// advance to next space
// attempt to parse out a relative time range
dur, err := g.parseRelative(durStr)
if err == nil {
// we parsed relative duration successfully
g.Duration = dur
return
}
dur, err = g.parseAbsolute(durStr)
if err == nil {
// we found an absolute time range
g.Duration = dur
}
}
// parseRelative locates and extracts a duration value from a fragment of an
// InfluxQL query following the "where" keyword. For example, in the fragment
// "time > now() - 180d GROUP BY :interval:", parseRelative would return a
// duration equal to 180d
func (g *GroupByVar) parseRelative(fragment string) (time.Duration, error) {
// locate duration literal start
prefix := "time > now() - "
start := strings.Index(fragment, prefix)
if start == -1 {
return time.Duration(0), errors.New("not a relative duration")
}
// reposition to duration literal
durFragment := fragment[start+len(prefix):]
// init counters
pos := 0
for pos < len(durStr) {
rn, _ := utf8.DecodeRuneInString(durStr[pos:])
// locate end of duration literal
for pos < len(durFragment) {
rn, _ := utf8.DecodeRuneInString(durFragment[pos:])
if unicode.IsSpace(rn) {
break
}
pos++
}
dur, err := influxql.ParseDuration(durStr[:pos])
// attempt to parse what we suspect is a duration literal
dur, err := influxql.ParseDuration(durFragment[:pos])
if err != nil {
return
return dur, err
}
g.Duration = dur
return dur, nil
}
// parseAbsolute will determine the duration between two absolute timestamps
// found within an InfluxQL fragment following the "where" keyword. For
// example, the fragement "time > '1985-10-25T00:01:21-0800 and time <
// '1985-10-25T00:01:22-0800'" would yield a duration of 1m'
func (g *GroupByVar) parseAbsolute(fragment string) (time.Duration, error) {
timePtn := `time\s[>|<]\s'([0-9\-TZ\:]+)'` // Playground: http://gobular.com/x/41a45095-c384-46ea-b73c-54ef91ab93af
re, err := regexp.Compile(timePtn)
if err != nil {
// this is a developer error and should complain loudly
panic("Bad Regex: err:" + err.Error())
}
if !re.Match([]byte(fragment)) {
return time.Duration(0), errors.New("absolute duration not found")
}
// extract at most two times
matches := re.FindAll([]byte(fragment), 2)
// parse out absolute times
durs := make([]time.Time, 0, 2)
for _, match := range matches {
durStr := re.FindSubmatch(match)
if tm, err := time.Parse(time.RFC3339Nano, string(durStr[1])); err == nil {
durs = append(durs, tm)
}
}
// reject more than 2 times found
if len(durs) != 2 {
return time.Duration(0), errors.New("must provide exactly two absolute times")
}
dur := durs[1].Sub(durs[0])
return dur, nil
}
func (g *GroupByVar) String() string {

49
chronograf_test.go Normal file
View File

@ -0,0 +1,49 @@
package chronograf_test
import (
"testing"
"time"
"github.com/influxdata/chronograf"
)
func Test_GroupByVar(t *testing.T) {
gbvTests := []struct {
name string
query string
expected time.Duration
resolution uint // the screen resolution to render queries into
reportingInterval time.Duration
}{
{
"relative time",
"SELECT mean(usage_idle) FROM cpu WHERE time > now() - 180d GROUP BY :interval:",
4320 * time.Hour,
1000,
10 * time.Second,
},
{
"absolute time",
"SELECT mean(usage_idle) FROM cpu WHERE time > '1985-10-25T00:01:00Z' and time < '1985-10-25T00:02:00Z' GROUP BY :interval:",
1 * time.Minute,
1000,
10 * time.Second,
},
}
for _, test := range gbvTests {
t.Run(test.name, func(t *testing.T) {
gbv := chronograf.GroupByVar{
Var: ":interval:",
Resolution: test.resolution,
ReportingInterval: test.reportingInterval,
}
gbv.Exec(test.query)
if gbv.Duration != test.expected {
t.Fatalf("%q - durations not equal! Want: %s, Got: %s", test.name, test.expected, gbv.Duration)
}
})
}
}

View File

@ -45,6 +45,7 @@ func newCellResponses(dID chronograf.DashboardID, dcells []chronograf.DashboardC
newCell.H = cell.H
newCell.Name = cell.Name
newCell.ID = cell.ID
newCell.Type = cell.Type
for _, lbl := range labels {
if axis, found := cell.Axes[lbl]; !found {

View File

@ -105,6 +105,7 @@ func Test_Service_DashboardCells(t *testing.T) {
W: 4,
H: 4,
Name: "CPU",
Type: "bar",
Queries: []chronograf.DashboardQuery{},
Axes: map[string]chronograf.Axis{},
},
@ -117,6 +118,7 @@ func Test_Service_DashboardCells(t *testing.T) {
W: 4,
H: 4,
Name: "CPU",
Type: "bar",
Queries: []chronograf.DashboardQuery{},
Axes: map[string]chronograf.Axis{
"x": chronograf.Axis{

View File

@ -50,7 +50,7 @@ type Server struct {
KapacitorUsername string `long:"kapacitor-username" description:"Username of your Kapacitor instance" env:"KAPACITOR_USERNAME"`
KapacitorPassword string `long:"kapacitor-password" description:"Password of your Kapacitor instance" env:"KAPACITOR_PASSWORD"`
NewSources string `long:"new-sources" description:"Config for adding a new InfluxDB source and Kapacitor server, in JSON as an array of objects, and surrounded by single quotes. E.g. --new-sources='[{\"influxdb\":{\"name\":\"Influx 1\",\"username\":\"user1\",\"password\":\"pass1\",\"url\":\"http://localhost:8086\",\"metaUrl\":\"http://metaurl.com\",\"insecureSkipVerify\":false,\"default\":true,\"telegraf\":\"telegraf\",\"sharedSecret\":\"hunter2\"},\"kapacitor\":{\"name\":\"Kapa 1\",\"url\":\"http://localhost:9092\",\"active\":true}}]'" env:"NEW_SOURCES" hidden:"true"`
NewSources string `long:"new-sources" description:"Config for adding a new InfluxDB source and Kapacitor server, in JSON as an array of objects, and surrounded by single quotes. E.g. --new-sources='[{\"influxdb\":{\"name\":\"Influx 1\",\"username\":\"user1\",\"password\":\"pass1\",\"url\":\"http://localhost:8086\",\"metaUrl\":\"http://metaurl.com\",\"type\":\"influx-enterprise\",\"insecureSkipVerify\":false,\"default\":true,\"telegraf\":\"telegraf\",\"sharedSecret\":\"cubeapples\"},\"kapacitor\":{\"name\":\"Kapa 1\",\"url\":\"http://localhost:9092\",\"active\":true}}]'" env:"NEW_SOURCES" hidden:"true"`
Develop bool `short:"d" long:"develop" description:"Run server in develop mode."`
BoltPath string `short:"b" long:"bolt-path" description:"Full path to boltDB file (e.g. './chronograf-v1.db')" env:"BOLT_PATH" default:"chronograf-v1.db"`

View File

@ -102,7 +102,7 @@
'no-iterator': 2,
'no-lone-blocks': 2,
'no-loop-func': 2,
'no-magic-numbers': [2, {ignore: [-1, 0, 1, 2]}],
'no-magic-numbers': [0, {ignore: [-1, 0, 1, 2]}],
'no-multi-spaces': 2,
'no-multi-str': 2,
'no-native-reassign': 2,

View File

@ -1,6 +1,8 @@
## Packages
We are using [yarn](https://yarnpkg.com/en/docs/install) 0.19.1.
Run `yarn run` to see a list of available tasks.
### Adding new packages
To add a new package, run

37
ui/nightwatch.json Normal file
View File

@ -0,0 +1,37 @@
{
"src_folders": ["tests"],
"output_folder": "reports",
"custom_commands_path": "",
"custom_assertions_path": "",
"page_objects_path": "",
"globals_path": "",
"selenium": {
"start_process": false,
"host": "hub-cloud.browserstack.com",
"port": 80
},
"live_output" : true,
"test_settings": {
"default": {
"selenium_port": 80,
"selenium_host": "hub-cloud.browserstack.com",
"silent": false,
"screenshots": {
"enabled": true,
"path": "screenshots"
},
"desiredCapabilities": {
"browser": "chrome",
"build": "nightwatch-browserstack",
"browserstack.user": "${BROWSERSTACK_USER}",
"browserstack.key": "${BROWSERSTACK_KEY}",
"browserstack.debug": true,
"browserstack.local": true,
"resolution": "1280x1024"
}
}
}
}

View File

@ -9,15 +9,16 @@
"url": "github:influxdata/chronograf"
},
"scripts": {
"build": "yarn run clean && env NODE_ENV=production node_modules/webpack/bin/webpack.js -p --config ./webpack/prodConfig.js",
"build:dev": "node_modules/webpack/bin/webpack.js --config ./webpack/devConfig.js",
"start": "node_modules/webpack/bin/webpack.js -w --config ./webpack/devConfig.js",
"lint": "node_modules/eslint/bin/eslint.js src/",
"build": "yarn run clean && env NODE_ENV=production webpack --optimize-minimize --config ./webpack/prodConfig.js",
"build:dev": "webpack --config ./webpack/devConfig.js",
"start": "webpack --watch --config ./webpack/devConfig.js",
"lint": "esw src/",
"test": "karma start",
"test:integration": "nightwatch tests --skip",
"test:lint": "yarn run lint; yarn run test",
"test:dev": "nodemon --exec yarn run test:lint",
"test:dev": "concurrently \"yarn run lint -- --watch\" \"yarn run test -- --no-single-run --reporters=verbose\"",
"clean": "rm -rf build",
"storybook": "node ./storybook",
"storybook": "node ./storybook.js",
"prettier": "prettier --single-quote --trailing-comma es5 --bracket-spacing false --semi false --write \"{src,spec}/**/*.js\"; eslint src --fix"
},
"author": "",
@ -45,14 +46,16 @@
"babel-runtime": "^6.5.0",
"bower": "^1.7.7",
"chai": "^3.5.0",
"concurrently": "^3.5.0",
"core-js": "^2.1.3",
"css-loader": "^0.23.1",
"envify": "^3.4.0",
"enzyme": "^2.4.1",
"eslint": "3.9.1",
"eslint": "^3.14.1",
"eslint-loader": "1.6.1",
"eslint-plugin-prettier": "^2.1.2",
"eslint-plugin-react": "6.6.0",
"eslint-watch": "^3.1.2",
"express": "^4.14.0",
"extract-text-webpack-plugin": "^1.0.1",
"file-loader": "^0.8.5",

View File

@ -4,8 +4,8 @@ import _ from 'lodash'
import uuid from 'node-uuid'
import ResizeContainer from 'shared/components/ResizeContainer'
import QueryMaker from 'src/data_explorer/components/QueryMaker'
import Visualization from 'src/data_explorer/components/Visualization'
import QueryMaker from 'src/dashboards/components/QueryMaker'
import Visualization from 'src/dashboards/components/Visualization'
import OverlayControls from 'src/dashboards/components/OverlayControls'
import DisplayOptions from 'src/dashboards/components/DisplayOptions'
@ -34,6 +34,7 @@ class CellEditorOverlay extends Component {
this.handleEditRawText = ::this.handleEditRawText
this.handleSetYAxisBounds = ::this.handleSetYAxisBounds
this.handleSetLabel = ::this.handleSetLabel
this.getActiveQuery = ::this.getActiveQuery
const {cell: {name, type, queries, axes}} = props
@ -111,10 +112,17 @@ class CellEditorOverlay extends Component {
e.preventDefault()
}
handleAddQuery(options) {
const newQuery = Object.assign({}, defaultQueryConfig(uuid.v4()), options)
const nextQueries = this.state.queriesWorkingDraft.concat(newQuery)
this.setState({queriesWorkingDraft: nextQueries})
handleAddQuery() {
const {queriesWorkingDraft} = this.state
const newIndex = queriesWorkingDraft.length
this.setState({
queriesWorkingDraft: [
...queriesWorkingDraft,
defaultQueryConfig(uuid.v4()),
],
})
this.handleSetActiveQueryIndex(newIndex)
}
handleDeleteQuery(index) {
@ -167,6 +175,14 @@ class CellEditorOverlay extends Component {
this.setState({activeQueryIndex})
}
getActiveQuery() {
const {queriesWorkingDraft, activeQueryIndex} = this.state
const activeQuery = queriesWorkingDraft[activeQueryIndex]
const defaultQuery = queriesWorkingDraft[0]
return activeQuery || defaultQuery
}
async handleEditRawText(url, id, text) {
const templates = removeUnselectedTemplateValues(this.props.templates)
@ -203,7 +219,6 @@ class CellEditorOverlay extends Component {
} = this.state
const queryActions = {
addQuery: this.handleAddQuery,
editRawTextAsync: this.handleEditRawText,
..._.mapValues(queryModifiers, qm => this.queryStateReducer(qm)),
}
@ -222,16 +237,14 @@ class CellEditorOverlay extends Component {
initialBottomHeight={INITIAL_HEIGHTS.queryMaker}
>
<Visualization
autoRefresh={autoRefresh}
axes={axes}
type={cellWorkingType}
name={cellWorkingName}
timeRange={timeRange}
templates={templates}
autoRefresh={autoRefresh}
queryConfigs={queriesWorkingDraft}
activeQueryIndex={0}
cellType={cellWorkingType}
cellName={cellWorkingName}
editQueryStatus={editQueryStatus}
axes={axes}
views={[]}
/>
<div className="overlay-technology--editor">
<OverlayControls
@ -256,9 +269,11 @@ class CellEditorOverlay extends Component {
actions={queryActions}
autoRefresh={autoRefresh}
timeRange={timeRange}
setActiveQueryIndex={this.handleSetActiveQueryIndex}
onDeleteQuery={this.handleDeleteQuery}
onAddQuery={this.handleAddQuery}
activeQueryIndex={activeQueryIndex}
activeQuery={this.getActiveQuery()}
setActiveQueryIndex={this.handleSetActiveQueryIndex}
/>}
</div>
</ResizeContainer>

View File

@ -38,7 +38,7 @@ class DashboardEditHeader extends Component {
<div className="page-header__container">
<form
className="page-header__left"
style={{flex: '1 0 0'}}
style={{flex: '1 0 0%'}}
onSubmit={this.handleFormSubmit}
>
<input

View File

@ -0,0 +1,94 @@
import React, {PropTypes} from 'react'
import EmptyQuery from 'src/shared/components/EmptyQuery'
import QueryTabList from 'src/shared/components/QueryTabList'
import QueryTextArea from 'src/dashboards/components/QueryTextArea'
import SchemaExplorer from 'src/shared/components/SchemaExplorer'
import buildInfluxQLQuery from 'utils/influxql'
const TEMPLATE_RANGE = {upper: null, lower: ':dashboardTime:'}
const rawTextBinder = (links, id, action) => text =>
action(links.queries, id, text)
const buildText = q =>
q.rawText || buildInfluxQLQuery(q.range || TEMPLATE_RANGE, q) || ''
const QueryMaker = ({
source: {links},
actions,
queries,
timeRange,
templates,
onAddQuery,
activeQuery,
onDeleteQuery,
activeQueryIndex,
setActiveQueryIndex,
}) =>
<div className="query-maker query-maker--panel">
<QueryTabList
queries={queries}
timeRange={timeRange}
onAddQuery={onAddQuery}
onDeleteQuery={onDeleteQuery}
activeQueryIndex={activeQueryIndex}
setActiveQueryIndex={setActiveQueryIndex}
/>
{activeQuery && activeQuery.id
? <div className="query-maker--tab-contents">
<QueryTextArea
query={buildText(activeQuery)}
config={activeQuery}
onUpdate={rawTextBinder(
links,
activeQuery.id,
actions.editRawTextAsync
)}
templates={templates}
/>
<SchemaExplorer
query={activeQuery}
actions={actions}
onAddQuery={onAddQuery}
/>
</div>
: <EmptyQuery onAddQuery={onAddQuery} />}
</div>
const {arrayOf, bool, func, number, shape, string} = PropTypes
QueryMaker.propTypes = {
source: shape({
links: shape({
queries: string.isRequired,
}).isRequired,
}).isRequired,
queries: arrayOf(shape({})).isRequired,
timeRange: shape({
upper: string,
lower: string,
}).isRequired,
isInDataExplorer: bool,
actions: shape({
chooseNamespace: func.isRequired,
chooseMeasurement: func.isRequired,
chooseTag: func.isRequired,
groupByTag: func.isRequired,
toggleField: func.isRequired,
groupByTime: func.isRequired,
toggleTagAcceptance: func.isRequired,
applyFuncsToField: func.isRequired,
editRawTextAsync: func.isRequired,
}).isRequired,
setActiveQueryIndex: func.isRequired,
onDeleteQuery: func.isRequired,
activeQueryIndex: number,
activeQuery: shape({}),
onAddQuery: func.isRequired,
templates: arrayOf(
shape({
tempVar: string.isRequired,
})
).isRequired,
}
export default QueryMaker

View File

@ -0,0 +1,263 @@
import React, {PropTypes, Component} from 'react'
import _ from 'lodash'
import classnames from 'classnames'
import TemplateDrawer from 'shared/components/TemplateDrawer'
import QueryStatus from 'shared/components/QueryStatus'
import {
MATCH_INCOMPLETE_TEMPLATES,
applyMasks,
insertTempVar,
unMask,
} from 'src/dashboards/constants'
class QueryTextArea extends Component {
constructor(props) {
super(props)
this.state = {
value: this.props.query,
isTemplating: false,
selectedTemplate: {
tempVar: _.get(this.props.templates, ['0', 'tempVar'], ''),
},
filteredTemplates: this.props.templates,
}
this.handleKeyDown = ::this.handleKeyDown
this.handleChange = ::this.handleChange
this.handleUpdate = ::this.handleUpdate
this.handleChooseTemplate = ::this.handleChooseTemplate
this.handleCloseDrawer = ::this.handleCloseDrawer
this.findTempVar = ::this.findTempVar
this.handleTemplateReplace = ::this.handleTemplateReplace
this.handleMouseOverTempVar = ::this.handleMouseOverTempVar
this.handleClickTempVar = ::this.handleClickTempVar
this.closeDrawer = ::this.closeDrawer
}
componentWillReceiveProps(nextProps) {
if (this.props.query !== nextProps.query) {
this.setState({value: nextProps.query})
}
}
handleCloseDrawer() {
this.setState({isTemplating: false})
}
handleMouseOverTempVar(template) {
this.handleTemplateReplace(template)
}
handleClickTempVar(template) {
// Clicking a tempVar does the same thing as hitting 'Enter'
this.handleTemplateReplace(template, true)
this.closeDrawer()
}
closeDrawer() {
this.setState({
isTemplating: false,
selectedTemplate: {
tempVar: _.get(this.props.templates, ['0', 'tempVar'], ''),
},
})
}
handleKeyDown(e) {
const {isTemplating, value} = this.state
if (isTemplating) {
switch (e.key) {
case 'Tab':
case 'ArrowRight':
case 'ArrowDown':
e.preventDefault()
return this.handleTemplateReplace(this.findTempVar('next'))
case 'ArrowLeft':
case 'ArrowUp':
e.preventDefault()
return this.handleTemplateReplace(this.findTempVar('previous'))
case 'Enter':
e.preventDefault()
this.handleTemplateReplace(this.state.selectedTemplate, true)
return this.closeDrawer()
case 'Escape':
e.preventDefault()
return this.closeDrawer()
}
} else if (e.key === 'Escape') {
e.preventDefault()
this.setState({value, isTemplating: false})
} else if (e.key === 'Enter') {
e.preventDefault()
this.handleUpdate()
}
}
handleTemplateReplace(selectedTemplate, replaceWholeTemplate) {
const {selectionStart, value} = this.editor
const {tempVar} = selectedTemplate
const newTempVar = replaceWholeTemplate
? tempVar
: tempVar.substring(0, tempVar.length - 1)
// mask matches that will confuse our regex
const masked = applyMasks(value)
const matched = masked.match(MATCH_INCOMPLETE_TEMPLATES)
let templatedValue
if (matched) {
templatedValue = insertTempVar(masked, newTempVar)
templatedValue = unMask(templatedValue)
}
const enterModifier = replaceWholeTemplate ? 0 : -1
const diffInLength =
tempVar.length - _.get(matched, '0', []).length + enterModifier
this.setState({value: templatedValue, selectedTemplate}, () =>
this.editor.setSelectionRange(
selectionStart + diffInLength,
selectionStart + diffInLength
)
)
}
findTempVar(direction) {
const {filteredTemplates: templates} = this.state
const {selectedTemplate} = this.state
const i = _.findIndex(templates, selectedTemplate)
const lastIndex = templates.length - 1
if (i >= 0) {
if (direction === 'next') {
return templates[(i + 1) % templates.length]
}
if (direction === 'previous') {
if (i === 0) {
return templates[lastIndex]
}
return templates[i - 1]
}
}
return templates[0]
}
handleChange() {
const {templates} = this.props
const {selectedTemplate} = this.state
const value = this.editor.value
// mask matches that will confuse our regex
const masked = applyMasks(value)
const matched = masked.match(MATCH_INCOMPLETE_TEMPLATES)
if (matched && !_.isEmpty(templates)) {
// maintain cursor poition
const start = this.editor.selectionStart
const end = this.editor.selectionEnd
const filterText = matched[0].substr(1).toLowerCase()
const filteredTemplates = templates.filter(t =>
t.tempVar.toLowerCase().includes(filterText)
)
const found = filteredTemplates.find(
t => t.tempVar === selectedTemplate && selectedTemplate.tempVar
)
const newTemplate = found ? found : filteredTemplates[0]
this.setState({
isTemplating: true,
selectedTemplate: newTemplate,
filteredTemplates,
value,
})
this.editor.setSelectionRange(start, end)
} else {
this.setState({isTemplating: false, value})
}
}
handleUpdate() {
this.props.onUpdate(this.state.value)
}
handleChooseTemplate(template) {
this.setState({value: template.query})
}
handleSelectTempVar(tempVar) {
this.setState({selectedTemplate: tempVar})
}
render() {
const {config: {status}} = this.props
const {
value,
isTemplating,
selectedTemplate,
filteredTemplates,
} = this.state
return (
<div className="query-editor">
<textarea
className="query-editor--field"
onChange={this.handleChange}
onKeyDown={this.handleKeyDown}
onBlur={this.handleUpdate}
ref={editor => (this.editor = editor)}
value={value}
placeholder="Enter a query or select database, measurement, and field below and have us build one for you..."
autoComplete="off"
spellCheck="false"
data-test="query-editor-field"
/>
<div
className={classnames('varmoji', {'varmoji-rotated': isTemplating})}
>
<div className="varmoji-container">
<div className="varmoji-front">
<QueryStatus status={status} />
</div>
<div className="varmoji-back">
{isTemplating
? <TemplateDrawer
onClickTempVar={this.handleClickTempVar}
templates={filteredTemplates}
selected={selectedTemplate}
onMouseOverTempVar={this.handleMouseOverTempVar}
handleClickOutside={this.handleCloseDrawer}
/>
: null}
</div>
</div>
</div>
</div>
)
}
}
const {arrayOf, bool, func, shape, string} = PropTypes
QueryTextArea.propTypes = {
query: string.isRequired,
onUpdate: func.isRequired,
config: shape().isRequired,
isInDataExplorer: bool,
templates: arrayOf(
shape({
tempVar: string.isRequired,
})
),
}
export default QueryTextArea

View File

@ -0,0 +1,69 @@
import React, {PropTypes} from 'react'
import RefreshingGraph from 'shared/components/RefreshingGraph'
import buildQueries from 'utils/buildQueriesForGraphs'
const DashVisualization = (
{
axes,
type,
name,
templates,
timeRange,
autoRefresh,
queryConfigs,
editQueryStatus,
},
{source: {links: {proxy}}}
) =>
<div className="graph">
<div className="graph-heading">
<div className="graph-title">
{name}
</div>
</div>
<div className="graph-container">
<RefreshingGraph
axes={axes}
type={type}
queries={buildQueries(proxy, queryConfigs, timeRange)}
templates={templates}
autoRefresh={autoRefresh}
editQueryStatus={editQueryStatus}
/>
</div>
</div>
const {arrayOf, func, number, shape, string} = PropTypes
DashVisualization.defaultProps = {
name: '',
type: '',
}
DashVisualization.propTypes = {
name: string,
type: string,
autoRefresh: number.isRequired,
templates: arrayOf(shape()),
timeRange: shape({
upper: string,
lower: string,
}).isRequired,
queryConfigs: arrayOf(shape({})).isRequired,
editQueryStatus: func.isRequired,
axes: shape({
y: shape({
bounds: arrayOf(string),
}),
}),
}
DashVisualization.contextTypes = {
source: PropTypes.shape({
links: PropTypes.shape({
proxy: PropTypes.string.isRequired,
}).isRequired,
}).isRequired,
}
export default DashVisualization

View File

@ -64,6 +64,7 @@ const FieldListItem = React.createClass({
active: isSelected,
})}
onClick={_.wrap(fieldFunc, this.handleToggleField)}
data-test={`query-builder-list-item-field-${fieldText}`}
>
<span>
<div className="query-builder--checkbox" />
@ -77,6 +78,7 @@ const FieldListItem = React.createClass({
'btn-primary': fieldFunc.funcs.length,
})}
onClick={this.toggleFunctionsMenu}
data-test={`query-builder-list-item-function-${fieldText}`}
>
{fieldFuncsLabel}
</div>

View File

@ -1,177 +0,0 @@
import React, {PropTypes} from 'react'
import DatabaseList from './DatabaseList'
import DatabaseDropdown from 'shared/components/DatabaseDropdown'
import MeasurementList from './MeasurementList'
import FieldList from './FieldList'
import QueryEditor from './QueryEditor'
import buildInfluxQLQuery from 'utils/influxql'
const {arrayOf, bool, func, shape, string} = PropTypes
const QueryBuilder = React.createClass({
propTypes: {
source: shape({
links: shape({
queries: string.isRequired,
}).isRequired,
}).isRequired,
query: shape({
id: string,
}).isRequired,
timeRange: shape({
upper: string,
lower: string,
}).isRequired,
templates: arrayOf(
shape({
tempVar: string.isRequired,
})
),
isInDataExplorer: bool,
actions: shape({
chooseNamespace: func.isRequired,
chooseMeasurement: func.isRequired,
applyFuncsToField: func.isRequired,
chooseTag: func.isRequired,
groupByTag: func.isRequired,
toggleField: func.isRequired,
groupByTime: func.isRequired,
toggleTagAcceptance: func.isRequired,
editRawTextAsync: func.isRequired,
}).isRequired,
layout: string,
},
handleChooseNamespace(namespace) {
this.props.actions.chooseNamespace(this.props.query.id, namespace)
},
handleChooseMeasurement(measurement) {
this.props.actions.chooseMeasurement(this.props.query.id, measurement)
},
handleToggleField(field) {
this.props.actions.toggleFieldWithGroupByInterval(
this.props.query.id,
field
)
},
handleGroupByTime(time) {
this.props.actions.groupByTime(this.props.query.id, time)
},
handleApplyFuncsToField(fieldFunc) {
this.props.actions.applyFuncsToField(
this.props.query.id,
fieldFunc,
this.props.isInDataExplorer
)
},
handleChooseTag(tag) {
this.props.actions.chooseTag(this.props.query.id, tag)
},
handleToggleTagAcceptance() {
this.props.actions.toggleTagAcceptance(this.props.query.id)
},
handleGroupByTag(tagKey) {
this.props.actions.groupByTag(this.props.query.id, tagKey)
},
handleEditRawText(text) {
const {source: {links}, query} = this.props
this.props.actions.editRawTextAsync(links.queries, query.id, text)
},
render() {
const {query, templates, isInDataExplorer} = this.props
// DE does not understand templating. :dashboardTime: is specific to dashboards
let timeRange
if (isInDataExplorer) {
timeRange = this.props.timeRange
} else {
timeRange = query.range || {upper: null, lower: ':dashboardTime:'}
}
const q = query.rawText || buildInfluxQLQuery(timeRange, query) || ''
return (
<div className="query-maker--tab-contents">
<QueryEditor
query={q}
config={query}
onUpdate={this.handleEditRawText}
templates={templates}
isInDataExplorer={isInDataExplorer}
/>
{this.renderLists()}
</div>
)
},
renderLists() {
const {query, layout, isInDataExplorer} = this.props
// Panel layout uses a dropdown instead of a list for database selection
// Also groups measurements & fields into their own container so they
// can be stacked vertically.
// TODO: Styles to make all this look proper
if (layout === 'panel') {
return (
<div className="query-builder--panel">
<DatabaseDropdown
query={query}
onChooseNamespace={this.handleChooseNamespace}
/>
<div className="query-builder">
<MeasurementList
query={query}
onChooseMeasurement={this.handleChooseMeasurement}
onChooseTag={this.handleChooseTag}
onToggleTagAcceptance={this.handleToggleTagAcceptance}
onGroupByTag={this.handleGroupByTag}
/>
<FieldList
query={query}
onToggleField={this.handleToggleField}
onGroupByTime={this.handleGroupByTime}
applyFuncsToField={this.handleApplyFuncsToField}
isInDataExplorer={isInDataExplorer}
/>
</div>
</div>
)
}
return (
<div className="query-builder">
<DatabaseList
query={query}
onChooseNamespace={this.handleChooseNamespace}
/>
<MeasurementList
query={query}
onChooseMeasurement={this.handleChooseMeasurement}
onChooseTag={this.handleChooseTag}
onToggleTagAcceptance={this.handleToggleTagAcceptance}
onGroupByTag={this.handleGroupByTag}
/>
<FieldList
query={query}
onToggleField={this.handleToggleField}
onGroupByTime={this.handleGroupByTime}
applyFuncsToField={this.handleApplyFuncsToField}
isInDataExplorer={isInDataExplorer}
/>
</div>
)
},
})
export default QueryBuilder

View File

@ -1,40 +1,20 @@
import React, {PropTypes, Component} from 'react'
import _ from 'lodash'
import classnames from 'classnames'
import Dropdown from 'shared/components/Dropdown'
import LoadingDots from 'shared/components/LoadingDots'
import TemplateDrawer from 'shared/components/TemplateDrawer'
import {QUERY_TEMPLATES} from 'src/data_explorer/constants'
import {
MATCH_INCOMPLETE_TEMPLATES,
applyMasks,
insertTempVar,
unMask,
} from 'src/dashboards/constants'
import QueryStatus from 'shared/components/QueryStatus'
class QueryEditor extends Component {
constructor(props) {
super(props)
this.state = {
value: this.props.query,
isTemplating: false,
selectedTemplate: {
tempVar: _.get(this.props.templates, ['0', 'tempVar'], ''),
},
filteredTemplates: this.props.templates,
}
this.handleKeyDown = ::this.handleKeyDown
this.handleChange = ::this.handleChange
this.handleUpdate = ::this.handleUpdate
this.handleChooseTemplate = ::this.handleChooseTemplate
this.handleCloseDrawer = ::this.handleCloseDrawer
this.findTempVar = ::this.findTempVar
this.handleTemplateReplace = ::this.handleTemplateReplace
this.handleMouseOverTempVar = ::this.handleMouseOverTempVar
this.handleClickTempVar = ::this.handleClickTempVar
this.closeDrawer = ::this.closeDrawer
this.handleChooseMetaQuery = ::this.handleChooseMetaQuery
}
componentWillReceiveProps(nextProps) {
@ -43,170 +23,35 @@ class QueryEditor extends Component {
}
}
handleCloseDrawer() {
this.setState({isTemplating: false})
}
handleMouseOverTempVar(template) {
this.handleTemplateReplace(template)
}
handleClickTempVar(template) {
// Clicking a tempVar does the same thing as hitting 'Enter'
this.handleTemplateReplace(template, true)
this.closeDrawer()
}
closeDrawer() {
this.setState({
isTemplating: false,
selectedTemplate: {
tempVar: _.get(this.props.templates, ['0', 'tempVar'], ''),
},
})
}
handleKeyDown(e) {
const {isTemplating, value} = this.state
const {value} = this.state
if (isTemplating) {
switch (e.key) {
case 'Tab':
case 'ArrowRight':
case 'ArrowDown':
e.preventDefault()
return this.handleTemplateReplace(this.findTempVar('next'))
case 'ArrowLeft':
case 'ArrowUp':
e.preventDefault()
return this.handleTemplateReplace(this.findTempVar('previous'))
case 'Enter':
e.preventDefault()
this.handleTemplateReplace(this.state.selectedTemplate, true)
return this.closeDrawer()
case 'Escape':
e.preventDefault()
return this.closeDrawer()
}
} else if (e.key === 'Escape') {
if (e.key === 'Escape') {
e.preventDefault()
this.setState({value, isTemplating: false})
} else if (e.key === 'Enter') {
this.setState({value})
}
if (e.key === 'Enter') {
e.preventDefault()
this.handleUpdate()
}
}
handleTemplateReplace(selectedTemplate, replaceWholeTemplate) {
const {selectionStart, value} = this.editor
const {tempVar} = selectedTemplate
const newTempVar = replaceWholeTemplate
? tempVar
: tempVar.substring(0, tempVar.length - 1)
// mask matches that will confuse our regex
const masked = applyMasks(value)
const matched = masked.match(MATCH_INCOMPLETE_TEMPLATES)
let templatedValue
if (matched) {
templatedValue = insertTempVar(masked, newTempVar)
templatedValue = unMask(templatedValue)
}
const enterModifier = replaceWholeTemplate ? 0 : -1
const diffInLength =
tempVar.length - _.get(matched, '0', []).length + enterModifier
this.setState({value: templatedValue, selectedTemplate}, () =>
this.editor.setSelectionRange(
selectionStart + diffInLength,
selectionStart + diffInLength
)
)
}
findTempVar(direction) {
const {filteredTemplates: templates} = this.state
const {selectedTemplate} = this.state
const i = _.findIndex(templates, selectedTemplate)
const lastIndex = templates.length - 1
if (i >= 0) {
if (direction === 'next') {
return templates[(i + 1) % templates.length]
}
if (direction === 'previous') {
if (i === 0) {
return templates[lastIndex]
}
return templates[i - 1]
}
}
return templates[0]
}
handleChange() {
const {templates} = this.props
const {selectedTemplate} = this.state
const value = this.editor.value
// mask matches that will confuse our regex
const masked = applyMasks(value)
const matched = masked.match(MATCH_INCOMPLETE_TEMPLATES)
if (matched && !_.isEmpty(templates)) {
// maintain cursor poition
const start = this.editor.selectionStart
const end = this.editor.selectionEnd
const filterText = matched[0].substr(1).toLowerCase()
const filteredTemplates = templates.filter(t =>
t.tempVar.toLowerCase().includes(filterText)
)
const found = filteredTemplates.find(
t => t.tempVar === selectedTemplate && selectedTemplate.tempVar
)
const newTemplate = found ? found : filteredTemplates[0]
this.setState({
isTemplating: true,
selectedTemplate: newTemplate,
filteredTemplates,
value,
})
this.editor.setSelectionRange(start, end)
} else {
this.setState({isTemplating: false, value})
}
this.setState({value: this.editor.value})
}
handleUpdate() {
this.props.onUpdate(this.state.value)
}
handleChooseTemplate(template) {
handleChooseMetaQuery(template) {
this.setState({value: template.query})
}
handleSelectTempVar(tempVar) {
this.setState({selectedTemplate: tempVar})
}
render() {
const {config: {status}} = this.props
const {
value,
isTemplating,
selectedTemplate,
filteredTemplates,
} = this.state
const {value} = this.state
return (
<div className="query-editor">
@ -220,111 +65,34 @@ class QueryEditor extends Component {
placeholder="Enter a query or select database, measurement, and field below and have us build one for you..."
autoComplete="off"
spellCheck="false"
data-test="query-editor-field"
/>
<div
className={classnames('varmoji', {'varmoji-rotated': isTemplating})}
>
<div className="varmoji">
<div className="varmoji-container">
<div className="varmoji-front">
{this.renderStatus(status)}
</div>
<div className="varmoji-back">
{isTemplating
? <TemplateDrawer
onClickTempVar={this.handleClickTempVar}
templates={filteredTemplates}
selected={selectedTemplate}
onMouseOverTempVar={this.handleMouseOverTempVar}
handleClickOutside={this.handleCloseDrawer}
/>
: null}
<QueryStatus status={status}>
<Dropdown
items={QUERY_TEMPLATES}
selected={'Query Templates'}
onChoose={this.handleChooseMetaQuery}
className="dropdown-140 query-editor--templates"
buttonSize="btn-xs"
/>
</QueryStatus>
</div>
</div>
</div>
</div>
)
}
renderStatus(status) {
const {isInDataExplorer} = this.props
if (!status) {
return (
<div className="query-editor--status">
{isInDataExplorer
? <Dropdown
items={QUERY_TEMPLATES}
selected={'Query Templates'}
onChoose={this.handleChooseTemplate}
className="dropdown-140 query-editor--templates"
buttonSize="btn-xs"
/>
: null}
</div>
)
}
if (status.loading) {
return (
<div className="query-editor--status">
<LoadingDots />
{isInDataExplorer
? <Dropdown
items={QUERY_TEMPLATES}
selected={'Query Templates'}
onChoose={this.handleChooseTemplate}
className="dropdown-140 query-editor--templates"
buttonSize="btn-xs"
/>
: null}
</div>
)
}
return (
<div className="query-editor--status">
<span
className={classnames('query-status-output', {
'query-status-output--error': status.error,
'query-status-output--success': status.success,
'query-status-output--warning': status.warn,
})}
>
<span
className={classnames('icon', {
stop: status.error,
checkmark: status.success,
'alert-triangle': status.warn,
})}
/>
{status.error || status.warn || status.success}
</span>
{isInDataExplorer
? <Dropdown
items={QUERY_TEMPLATES}
selected={'Query Templates'}
onChoose={this.handleChooseTemplate}
className="dropdown-140 query-editor--templates"
buttonSize="btn-xs"
/>
: null}
</div>
)
}
}
const {arrayOf, bool, func, shape, string} = PropTypes
const {func, shape, string} = PropTypes
QueryEditor.propTypes = {
query: string.isRequired,
onUpdate: func.isRequired,
config: shape().isRequired,
isInDataExplorer: bool,
templates: arrayOf(
shape({
tempVar: string.isRequired,
})
),
}
export default QueryEditor

View File

@ -1,181 +1,87 @@
import React, {PropTypes} from 'react'
import QueryBuilder from './QueryBuilder'
import QueryMakerTab from './QueryMakerTab'
import QueryEditor from './QueryEditor'
import EmptyQuery from 'src/shared/components/EmptyQuery'
import QueryTabList from 'src/shared/components/QueryTabList'
import SchemaExplorer from 'src/shared/components/SchemaExplorer'
import buildInfluxQLQuery from 'utils/influxql'
import classnames from 'classnames'
const {arrayOf, bool, func, node, number, shape, string} = PropTypes
const rawTextBinder = (links, id, action) => text =>
action(links.queries, id, text)
const QueryMaker = React.createClass({
propTypes: {
source: shape({
links: shape({
queries: string.isRequired,
}).isRequired,
}).isRequired,
queries: arrayOf(shape({})).isRequired,
timeRange: shape({
upper: string,
lower: string,
}).isRequired,
templates: arrayOf(
shape({
tempVar: string.isRequired,
})
),
isInDataExplorer: bool,
actions: shape({
chooseNamespace: func.isRequired,
chooseMeasurement: func.isRequired,
chooseTag: func.isRequired,
groupByTag: func.isRequired,
addQuery: func.isRequired,
toggleField: func.isRequired,
groupByTime: func.isRequired,
toggleTagAcceptance: func.isRequired,
applyFuncsToField: func.isRequired,
editRawTextAsync: func.isRequired,
}).isRequired,
height: string,
top: string,
setActiveQueryIndex: func.isRequired,
onDeleteQuery: func.isRequired,
activeQueryIndex: number,
children: node,
layout: string,
},
const buildText = (q, timeRange) =>
q.rawText || buildInfluxQLQuery(q.range || timeRange, q) || ''
handleAddQuery() {
const newIndex = this.props.queries.length
this.props.actions.addQuery()
this.props.setActiveQueryIndex(newIndex)
},
handleAddRawQuery() {
const newIndex = this.props.queries.length
this.props.actions.addQuery({rawText: ''})
this.props.setActiveQueryIndex(newIndex)
},
getActiveQuery() {
const {queries, activeQueryIndex} = this.props
const activeQuery = queries[activeQueryIndex]
const defaultQuery = queries[0]
return activeQuery || defaultQuery
},
render() {
const {height, top, layout} = this.props
return (
<div
className={classnames('query-maker', {
'query-maker--panel': layout === 'panel',
})}
style={{height, top}}
>
{this.renderQueryTabList()}
{this.renderQueryBuilder()}
</div>
)
},
renderQueryBuilder() {
const {
timeRange,
actions,
source,
templates,
layout,
isInDataExplorer,
} = this.props
const query = this.getActiveQuery()
if (!query) {
return (
<div className="query-maker--empty">
<h5>This Graph has no Queries</h5>
<br />
<div
className="btn btn-primary"
role="button"
onClick={this.handleAddQuery}
>
Add a Query
</div>
const QueryMaker = ({
source,
actions,
queries,
timeRange,
onAddQuery,
activeQuery,
onDeleteQuery,
activeQueryIndex,
setActiveQueryIndex,
}) =>
<div className="query-maker query-maker--panel">
<QueryTabList
queries={queries}
timeRange={timeRange}
onAddQuery={onAddQuery}
onDeleteQuery={onDeleteQuery}
activeQueryIndex={activeQueryIndex}
setActiveQueryIndex={setActiveQueryIndex}
/>
{activeQuery && activeQuery.id
? <div className="query-maker--tab-contents">
<QueryEditor
query={buildText(activeQuery, timeRange)}
config={activeQuery}
onUpdate={rawTextBinder(
source.links,
activeQuery.id,
actions.editRawTextAsync
)}
/>
<SchemaExplorer
query={activeQuery}
actions={actions}
onAddQuery={onAddQuery}
/>
</div>
)
}
: <EmptyQuery onAddQuery={onAddQuery} />}
</div>
// NOTE
// the layout prop is intended to toggle between a horizontal and vertical layout
// the layout will be horizontal by default
// vertical layout is known as "panel" layout as it will be used to build
// a "cell editor panel" though that term might change
// Currently, if set to "panel" the only noticeable difference is that the
// DatabaseList becomes DatabaseDropdown (more space efficient in vertical layout)
// and is outside the container with measurements/tags/fields
//
// TODO:
// - perhaps switch to something like "isVertical" and accept boolean instead of string
// - more css/markup work to make the alternate appearance look good
const {arrayOf, func, number, shape, string} = PropTypes
return (
<QueryBuilder
source={source}
timeRange={timeRange}
templates={templates}
query={query}
actions={actions}
onAddQuery={this.handleAddQuery}
layout={layout}
isInDataExplorer={isInDataExplorer}
/>
)
},
renderQueryTabList() {
const {
queries,
activeQueryIndex,
onDeleteQuery,
timeRange,
setActiveQueryIndex,
} = this.props
return (
<div className="query-maker--tabs">
{queries.map((q, i) => {
return (
<QueryMakerTab
isActive={i === activeQueryIndex}
key={i}
queryIndex={i}
query={q}
onSelect={setActiveQueryIndex}
onDelete={onDeleteQuery}
queryTabText={
q.rawText ||
buildInfluxQLQuery(timeRange, q) ||
`Query ${i + 1}`
}
/>
)
})}
{this.props.children}
<div
className="query-maker--new btn btn-sm btn-primary"
onClick={this.handleAddQuery}
>
<span className="icon plus" />
</div>
</div>
)
},
})
QueryMaker.defaultProps = {
layout: 'default',
QueryMaker.propTypes = {
source: shape({
links: shape({
queries: string.isRequired,
}).isRequired,
}).isRequired,
queries: arrayOf(shape({})).isRequired,
timeRange: shape({
upper: string,
lower: string,
}).isRequired,
actions: shape({
chooseNamespace: func.isRequired,
chooseMeasurement: func.isRequired,
chooseTag: func.isRequired,
groupByTag: func.isRequired,
addQuery: func.isRequired,
toggleField: func.isRequired,
groupByTime: func.isRequired,
toggleTagAcceptance: func.isRequired,
applyFuncsToField: func.isRequired,
editRawTextAsync: func.isRequired,
}).isRequired,
setActiveQueryIndex: func.isRequired,
onDeleteQuery: func.isRequired,
onAddQuery: func.isRequired,
activeQuery: shape({}),
activeQueryIndex: number,
}
export default QueryMaker

View File

@ -33,7 +33,11 @@ const QueryMakerTab = React.createClass({
<label>
{this.props.queryTabText}
</label>
<span className="query-maker--delete" onClick={this.handleDelete} />
<span
className="query-maker--delete"
onClick={this.handleDelete}
data-test="query-maker-delete"
/>
</div>
)
},

View File

@ -76,7 +76,12 @@ const TagListItem = React.createClass({
active: selectedTagValues.indexOf(v) > -1,
})
return (
<div className={cx} onClick={_.wrap(v, this.handleChoose)} key={v}>
<div
className={cx}
onClick={_.wrap(v, this.handleChoose)}
key={v}
data-test={`query-builder-list-item-tag-value-${v}`}
>
<span>
<div className="query-builder--checkbox" />
{v}
@ -103,6 +108,7 @@ const TagListItem = React.createClass({
<div
className={classnames('query-builder--list-item', {active: isOpen})}
onClick={this.handleClickKey}
data-test={`query-builder-list-item-tag-${tagKey}`}
>
<span>
<div className="query-builder--caret icon caret-right" />

View File

@ -11,6 +11,7 @@ const VisHeader = ({views, view, onToggleView, name}) =>
key={v}
onClick={() => onToggleView(v)}
className={classnames({active: view === v})}
data-test={`data-${v}`}
>
{_.upperFirst(v)}
</li>

View File

@ -24,6 +24,7 @@ const WriteDataBody = ({
onKeyUp={handleKeyUp}
onChange={handleEdit}
autoFocus={true}
data-test="manual-entry-field"
/>
: <div className="write-data-form--file">
<input

View File

@ -37,6 +37,7 @@ const WriteDataFooter = ({
(!uploadContent && !isManual) ||
isUploading
}
data-test="write-data-submit-button"
>
Write
</button>

View File

@ -27,6 +27,7 @@ const WriteDataHeader = ({
<li
onClick={() => toggleWriteView(true)}
className={isManual ? 'active' : ''}
data-test="manual-entry-button"
>
Manual Entry
</li>

View File

@ -1,4 +1,4 @@
import React, {PropTypes} from 'react'
import React, {PropTypes, Component} from 'react'
import {connect} from 'react-redux'
import {bindActionCreators} from 'redux'
@ -18,64 +18,44 @@ import {setAutoRefresh} from 'shared/actions/app'
import * as dataExplorerActionCreators from 'src/data_explorer/actions/view'
import {writeLineProtocolAsync} from 'src/data_explorer/actions/view/write'
const {arrayOf, func, number, shape, string} = PropTypes
class DataExplorer extends Component {
constructor(props) {
super(props)
const DataExplorer = React.createClass({
propTypes: {
source: shape({
links: shape({
proxy: string.isRequired,
self: string.isRequired,
queries: string.isRequired,
}).isRequired,
}).isRequired,
queryConfigs: arrayOf(shape({})).isRequired,
queryConfigActions: shape({
editQueryStatus: func.isRequired,
}).isRequired,
autoRefresh: number.isRequired,
handleChooseAutoRefresh: func.isRequired,
timeRange: shape({
upper: string,
lower: string,
}).isRequired,
setTimeRange: func.isRequired,
dataExplorer: shape({
queryIDs: arrayOf(string).isRequired,
}).isRequired,
writeLineProtocol: func.isRequired,
errorThrownAction: func.isRequired,
},
childContextTypes: {
source: shape({
links: shape({
proxy: string.isRequired,
self: string.isRequired,
}).isRequired,
}).isRequired,
},
getChildContext() {
return {source: this.props.source}
},
getInitialState() {
return {
this.state = {
activeQueryIndex: 0,
showWriteForm: false,
}
},
}
getChildContext() {
return {source: this.props.source}
}
handleSetActiveQueryIndex(index) {
this.setState({activeQueryIndex: index})
},
}
handleDeleteQuery(index) {
const {queryConfigs} = this.props
const {queryConfigs, queryConfigActions} = this.props
const query = queryConfigs[index]
this.props.queryConfigActions.deleteQuery(query.id)
},
queryConfigActions.deleteQuery(query.id)
}
handleAddQuery() {
const newIndex = this.props.queryConfigs.length
this.props.queryConfigActions.addQuery()
this.handleSetActiveQueryIndex(newIndex)
}
getActiveQuery() {
const {activeQueryIndex} = this.state
const {queryConfigs} = this.props
const activeQuery = queryConfigs[activeQueryIndex]
const defaultQuery = queryConfigs[0]
return activeQuery || defaultQuery
}
render() {
const {
@ -89,6 +69,7 @@ const DataExplorer = React.createClass({
source,
writeLineProtocol,
} = this.props
const {activeQueryIndex, showWriteForm} = this.state
const selectedDatabase = _.get(
queryConfigs,
@ -128,10 +109,11 @@ const DataExplorer = React.createClass({
actions={queryConfigActions}
autoRefresh={autoRefresh}
timeRange={timeRange}
isInDataExplorer={true}
setActiveQueryIndex={this.handleSetActiveQueryIndex}
onDeleteQuery={this.handleDeleteQuery}
setActiveQueryIndex={::this.handleSetActiveQueryIndex}
onDeleteQuery={::this.handleDeleteQuery}
onAddQuery={::this.handleAddQuery}
activeQueryIndex={activeQueryIndex}
activeQuery={::this.getActiveQuery()}
/>
<Visualization
isInDataExplorer={true}
@ -145,10 +127,47 @@ const DataExplorer = React.createClass({
</ResizeContainer>
</div>
)
},
})
}
}
function mapStateToProps(state) {
const {arrayOf, func, number, shape, string} = PropTypes
DataExplorer.propTypes = {
source: shape({
links: shape({
proxy: string.isRequired,
self: string.isRequired,
queries: string.isRequired,
}).isRequired,
}).isRequired,
queryConfigs: arrayOf(shape({})).isRequired,
queryConfigActions: shape({
editQueryStatus: func.isRequired,
}).isRequired,
autoRefresh: number.isRequired,
handleChooseAutoRefresh: func.isRequired,
timeRange: shape({
upper: string,
lower: string,
}).isRequired,
setTimeRange: func.isRequired,
dataExplorer: shape({
queryIDs: arrayOf(string).isRequired,
}).isRequired,
writeLineProtocol: func.isRequired,
errorThrownAction: func.isRequired,
}
DataExplorer.childContextTypes = {
source: shape({
links: shape({
proxy: string.isRequired,
self: string.isRequired,
}).isRequired,
}).isRequired,
}
const mapStateToProps = state => {
const {
app: {persisted: {autoRefresh}},
dataExplorer,
@ -165,7 +184,7 @@ function mapStateToProps(state) {
}
}
function mapDispatchToProps(dispatch) {
const mapDispatchToProps = dispatch => {
return {
handleChooseAutoRefresh: bindActionCreators(setAutoRefresh, dispatch),
errorThrownAction: bindActionCreators(errorThrown, dispatch),

View File

@ -49,7 +49,11 @@ const Header = React.createClass({
<div className="page-header__right">
<GraphTips />
<SourceIndicator sourceName={this.context.source.name} />
<div className="btn btn-sm btn-default" onClick={showWriteForm}>
<div
className="btn btn-sm btn-default"
onClick={showWriteForm}
data-test="write-data-button"
>
<span className="icon pencil" />
Write Data
</div>

View File

@ -10,7 +10,7 @@ export function getCpuAndLoadForHosts(proxyLink, telegrafDB) {
SELECT non_negative_derivative(mean(uptime)) AS deltaUptime FROM "system" WHERE time > now() - 10m GROUP BY host, time(1m) fill(0);
SELECT mean("Percent_Processor_Time") FROM win_cpu WHERE time > now() - 10m GROUP BY host;
SELECT mean("Processor_Queue_Length") FROM win_system WHERE time > now() - 10s GROUP BY host;
SELECT non_negative_derivative(mean("System_Up_Time")) AS deltaUptime FROM "telegraf"."autogen"."win_uptime" WHERE time > now() - 10m GROUP BY host, time(1m) fill(0);
SELECT non_negative_derivative(mean("System_Up_Time")) AS winDeltaUptime FROM "telegraf"."autogen"."win_system" WHERE time > now() - 10m GROUP BY host, time(1m) fill(0);
SHOW TAG VALUES FROM /win_system|system/ WITH KEY = "host"`,
db: telegrafDB,
}).then(resp => {
@ -72,9 +72,11 @@ export function getCpuAndLoadForHosts(proxyLink, telegrafDB) {
})
winUptimeSeries.forEach(s => {
const uptimeIndex = s.columns.findIndex(col => col === 'deltaUptime')
hosts[s.tags.host].deltaUptime =
s.values[s.values.length - 1][uptimeIndex]
const winUptimeIndex = s.columns.findIndex(
col => col === 'winDeltaUptime'
)
hosts[s.tags.host].winDeltaUptime =
s.values[s.values.length - 1][winUptimeIndex]
})
return hosts

View File

@ -209,7 +209,9 @@ const HostRow = React.createClass({
<div
className={classnames(
'table-dot',
host.deltaUptime > 0 ? 'dot-success' : 'dot-critical'
Math.max(host.deltaUptime || 0, host.winDeltaUptime || 0) > 0
? 'dot-success'
: 'dot-critical'
)}
/>
</td>

View File

@ -1,3 +1,5 @@
import 'babel-polyfill'
import React from 'react'
import {render} from 'react-dom'
import {Provider} from 'react-redux'
@ -47,7 +49,8 @@ const errorsQueue = []
const rootNode = document.getElementById('react-root')
const basepath = rootNode.dataset.basepath || ''
// Older method used for pre-IE 11 compatibility
const basepath = rootNode.getAttribute('data-basepath') || ''
window.basepath = basepath
const browserHistory = useRouterHistory(createHistory)({
basename: basepath, // this is written in when available by the URL prefixer middleware

View File

@ -2,9 +2,9 @@ import React, {PropTypes} from 'react'
import buildInfluxQLQuery from 'utils/influxql'
import classnames from 'classnames'
import DatabaseList from '../../data_explorer/components/DatabaseList'
import MeasurementList from '../../data_explorer/components/MeasurementList'
import FieldList from '../../data_explorer/components/FieldList'
import DatabaseList from 'src/shared/components/DatabaseList'
import MeasurementList from 'src/shared/components/MeasurementList'
import FieldList from 'src/shared/components/FieldList'
import {defaultEveryFrequency} from 'src/kapacitor/constants'

View File

@ -44,7 +44,7 @@ class RuleMessageOptions extends Component {
<input
id="alert-input"
className="form-control input-sm form-malachite"
style={{flex: '1 0 0'}}
style={{flex: '1 0 0%'}}
type="text"
placeholder={args.placeholder}
onChange={e =>
@ -83,7 +83,7 @@ class RuleMessageOptions extends Component {
className="form-control input-sm form-malachite"
style={{
margin: '0 15px 0 5px',
flex: '1 0 0',
flex: '1 0 0%',
}}
type="text"
placeholder={placeholder}

View File

@ -215,7 +215,7 @@ const AutoRefresh = ComposedComponent => {
return (
<div className="graph-empty">
<p>No Results</p>
<p data-test="data-explorer-no-results">No Results</p>
</div>
)
},

View File

@ -83,6 +83,7 @@ const DatabaseList = React.createClass({
})}
key={`${database}..${retentionPolicy}`}
onClick={_.wrap(namespace, onChooseNamespace)}
data-test={`query-builder-list-item-database-${database}`}
>
{database}.{retentionPolicy}
</div>

View File

@ -0,0 +1,18 @@
import React, {PropTypes} from 'react'
const EmptyQueryState = ({onAddQuery}) =>
<div className="query-maker--empty">
<h5>This Graph has no Queries</h5>
<br />
<div className="btn btn-primary" onClick={onAddQuery}>
Add a Query
</div>
</div>
const {func} = PropTypes
EmptyQueryState.propTypes = {
onAddQuery: func.isRequired,
}
export default EmptyQueryState

View File

@ -74,6 +74,7 @@ class FunctionSelector extends Component {
<div
className="btn btn-xs btn-success"
onClick={this.handleApplyFunctions}
data-test="function-selector-apply"
>
Apply
</div>
@ -90,6 +91,7 @@ class FunctionSelector extends Component {
f,
singleSelect ? this.onSingleSelect : this.onSelect
)}
data-test={`function-selector-item-${f}`}
>
{f}
</div>

View File

@ -127,7 +127,7 @@ class LayoutRenderer extends Component {
}
return (
<div className="graph-empty">
<p>No Results</p>
<p data-test="data-explorer-no-results">No Results</p>
</div>
)
}

View File

@ -144,6 +144,7 @@ const MeasurementList = React.createClass({
className={classnames('query-builder--list-item', {
active: isActive,
})}
data-test={`query-builder-list-item-measurement-${measurement}`}
>
<span>
<div className="query-builder--caret icon caret-right" />

View File

@ -0,0 +1,53 @@
import React, {PropTypes} from 'react'
import LoadingDots from 'shared/components/LoadingDots'
import classnames from 'classnames'
const QueryStatus = ({status, children}) => {
if (!status) {
return <div className="query-editor--status" />
}
if (status.loading) {
return (
<div className="query-editor--status">
<LoadingDots />
{children}
</div>
)
}
return (
<div className="query-editor--status">
<span
className={classnames('query-status-output', {
'query-status-output--error': status.error,
'query-status-output--success': status.success,
'query-status-output--warning': status.warn,
})}
>
<span
className={classnames('icon', {
stop: status.error,
checkmark: status.success,
'alert-triangle': status.warn,
})}
/>
{status.error || status.warn || status.success}
</span>
{children}
</div>
)
}
const {node, shape, string} = PropTypes
QueryStatus.propTypes = {
status: shape({
error: string,
success: string,
warn: string,
}),
children: node,
}
export default QueryStatus

View File

@ -0,0 +1,49 @@
import React, {PropTypes} from 'react'
import QueryMakerTab from 'src/data_explorer/components/QueryMakerTab'
import buildInfluxQLQuery from 'utils/influxql'
const QueryTabList = ({
queries,
timeRange,
onAddQuery,
onDeleteQuery,
activeQueryIndex,
setActiveQueryIndex,
}) =>
<div className="query-maker--tabs">
{queries.map((q, i) =>
<QueryMakerTab
isActive={i === activeQueryIndex}
key={i}
queryIndex={i}
query={q}
onSelect={setActiveQueryIndex}
onDelete={onDeleteQuery}
queryTabText={
q.rawText || buildInfluxQLQuery(timeRange, q) || `Query ${i + 1}`
}
/>
)}
<div
className="query-maker--new btn btn-sm btn-primary"
onClick={onAddQuery}
>
<span className="icon plus" />
</div>
</div>
const {arrayOf, func, number, shape, string} = PropTypes
QueryTabList.propTypes = {
queries: arrayOf(shape({})).isRequired,
timeRange: shape({
upper: string,
lower: string,
}).isRequired,
onAddQuery: func.isRequired,
onDeleteQuery: func.isRequired,
activeQueryIndex: number.isRequired,
setActiveQueryIndex: func.isRequired,
}
export default QueryTabList

View File

@ -0,0 +1,62 @@
import React, {PropTypes} from 'react'
import DatabaseList from 'src/shared/components/DatabaseList'
import MeasurementList from 'src/shared/components/MeasurementList'
import FieldList from 'src/shared/components/FieldList'
const actionBinder = (id, action) => item => action(id, item)
const SchemaExplorer = ({
query,
query: {id},
actions: {
chooseTag,
groupByTag,
groupByTime,
chooseNamespace,
chooseMeasurement,
applyFuncsToField,
toggleTagAcceptance,
toggleFieldWithGroupByInterval,
},
}) =>
<div className="query-builder">
<DatabaseList
query={query}
onChooseNamespace={actionBinder(id, chooseNamespace)}
/>
<MeasurementList
query={query}
onChooseTag={actionBinder(id, chooseTag)}
onGroupByTag={actionBinder(id, groupByTag)}
onChooseMeasurement={actionBinder(id, chooseMeasurement)}
onToggleTagAcceptance={actionBinder(id, toggleTagAcceptance)}
/>
<FieldList
query={query}
onToggleField={actionBinder(id, toggleFieldWithGroupByInterval)}
onGroupByTime={actionBinder(id, groupByTime)}
applyFuncsToField={actionBinder(id, applyFuncsToField)}
/>
</div>
const {func, shape, string} = PropTypes
SchemaExplorer.propTypes = {
query: shape({
id: string,
}).isRequired,
actions: shape({
chooseNamespace: func.isRequired,
chooseMeasurement: func.isRequired,
applyFuncsToField: func.isRequired,
chooseTag: func.isRequired,
groupByTag: func.isRequired,
toggleField: func.isRequired,
groupByTime: func.isRequired,
toggleTagAcceptance: func.isRequired,
editRawTextAsync: func.isRequired,
}).isRequired,
}
export default SchemaExplorer

View File

@ -7,7 +7,10 @@ export default function resizeLayout() {
action.type === 'ENABLE_PRESENTATION_MODE' ||
action.type === 'DISABLE_PRESENTATION_MODE'
) {
window.dispatchEvent(new Event('resize'))
// Uses longer event object creation method due to IE compatibility.
const evt = document.createEvent('HTMLEvents')
evt.initEvent('resize', false, true)
window.dispatchEvent(evt)
}
}
}

View File

@ -161,10 +161,10 @@
line-height: 30px;
font-weight: 600;
color: $g13-mist;
flex: 1 0 0;
flex: 1 0 0%;
}
.dygraph-legend--filter {
flex: 1 0 0;
flex: 1 0 0%;
margin-top: 8px;
}
.dygraph-legend--divider {

View File

@ -5,7 +5,7 @@
*/
.query-builder {
width: 100%;
flex: 1 0 0;
flex: 1 0 0%;
display: flex;
align-items: stretch;
flex-wrap: nowrap;
@ -13,10 +13,10 @@
.query-builder--column {
display: flex;
flex-direction: column;
flex: 2 0 0;
flex: 2 0 0%;
}
.query-builder--column-db {
flex: 1 0 0;
flex: 1 0 0%;
}
.query-builder--heading {
@include no-user-select();
@ -39,7 +39,7 @@
}
.query-builder--list,
.query-builder--list-empty {
flex: 1 0 0;
flex: 1 0 0%;
}
.query-builder--list {
padding: 0;
@ -99,7 +99,7 @@
}
/* Filter Element */
.query-builder--filter {
flex: 1 0 0;
flex: 1 0 0%;
display: flex;
& > span {

View File

@ -64,7 +64,7 @@
}
}
.query-status-output {
flex: 1 0 0;
flex: 1 0 0%;
display: inline-block;
color: $query-editor--status-default;
white-space: nowrap;

View File

@ -94,7 +94,7 @@ $query-editor-tab-active: $g3-castle;
height: $query-maker--tabs-height;
margin: 0 2px 0 0;
max-width: $query-maker--tab-width;
flex: 1 0 0;
flex: 1 0 0%;
display: flex;
align-items: center;
justify-content: space-between;
@ -170,7 +170,7 @@ $query-editor-tab-active: $g3-castle;
}
.query-maker--tab-contents,
.query-maker--empty {
flex: 1 0 0;
flex: 1 0 0%;
margin: 0 0 $query-maker--gutter 0;
background-color: $query-maker--tab-contents-bg;
}

View File

@ -191,7 +191,7 @@ $table-tab-scrollbar-height: 6px;
flex-direction: column;
align-items: stretch;
> .panel-body {flex: 1 0 0;}
> .panel-body {flex: 1 0 0%;}
.generic-empty-state {height: 100%;}
}
}
@ -234,7 +234,7 @@ $table-tab-scrollbar-height: 6px;
color: $g17-whisper;
}
.alert-history-table--tbody {
flex: 1 0 0;
flex: 1 0 0%;
width: 100%;
}
.alert-history-table--tr {

View File

@ -42,7 +42,7 @@ button.btn.template-control--manage {
}
.template-control--controls {
display: flex;
flex: 1 0 0;
flex: 1 0 0%;
flex-wrap: wrap;
}
.template-control--empty {
@ -64,7 +64,7 @@ button.btn.template-control--manage {
.dropdown {
order: 2;
margin: 0;
flex: 1 0 0;
flex: 1 0 0%;
}
.dropdown-toggle {
border-radius: 0 0 $radius-small $radius-small;

View File

@ -166,7 +166,7 @@ $tvmp-table-gutter: 8px;
> *:last-child {margin-right: 0;}
.dropdown {
flex: 1 0 0;
flex: 1 0 0%;
& > .dropdown-toggle {width: 100%;}
}

View File

@ -103,7 +103,7 @@ table.table-highlight > tbody > tr.admin-table--edit-row:hover {
.form-control {
margin: 0 4px 0 0;
flex: 1 0 0;
flex: 1 0 0%;
}
}
pre.admin-table--query {
@ -118,7 +118,7 @@ pre.admin-table--query {
align-items: center;
> .form-control {
flex: 1 0 0;
flex: 1 0 0%;
margin-right: 4px;
}
}
@ -157,7 +157,7 @@ pre.admin-table--query {
.form-control {
margin: 0 8px 0 0;
flex: 1 0 0;
flex: 1 0 0%;
}
}

View File

@ -18,7 +18,7 @@ $config-endpoint-tab-bg-active: $g3-castle;
align-items: stretch;
}
.config-endpoint--tabs {
flex: 0 0 0;
flex: 0 0 0%;
display: flex;
.btn-group.tab-group {
@ -27,7 +27,7 @@ $config-endpoint-tab-bg-active: $g3-castle;
border-radius: $radius 0 0 $radius;
margin: 0;
display: flex;
flex: 1 0 0;
flex: 1 0 0%;
flex-direction: column;
align-items: stretch;
@ -58,8 +58,8 @@ $config-endpoint-tab-bg-active: $g3-castle;
}
}
.config-endpoint--tab-contents {
flex: 1 0 0;
flex: 1 0 0%;
background-color: $config-endpoint-tab-bg-active;
border-radius: 0 $radius $radius 0;
padding: 16px 42px;
}
}

View File

@ -86,7 +86,7 @@ $overlay-z: 100;
margin: 0 15%;
}
.overlay-technology .query-maker {
flex: 1 0 0;
flex: 1 0 0%;
padding: 0 18px;
margin: 0;
background-color: $g2-kevlar;

View File

@ -3887,7 +3887,7 @@ p .label {
text-overflow: ellipsis;
white-space: nowrap;
flex: 1 0 0;
flex: 1 0 0%;
}
.dropdown-toggle.btn-xs .caret {
right: 7px;
@ -4041,7 +4041,7 @@ p .label {
outline: none;
transition: color .25s ease;
flex: 1 0 0;
flex: 1 0 0%;
}
.dropdown-menu li.dropdown-item > a,
.dropdown-menu li.dropdown-item > a:hover,

View File

@ -0,0 +1,17 @@
import buildInfluxQLQuery from 'utils/influxql'
const buildQueries = (proxy, queryConfigs, timeRange) => {
const statements = queryConfigs.map(query => {
const text =
query.rawText || buildInfluxQLQuery(query.range || timeRange, query)
return {text, id: query.id, queryConfig: query}
})
const queries = statements.filter(s => s.text !== null).map(s => {
return {host: [proxy], text: s.text, id: s.id, queryConfig: s.queryConfig}
})
return queries
}
export default buildQueries

112
ui/tests/DataExplorer.js Normal file
View File

@ -0,0 +1,112 @@
const src = process.argv.find(s => s.includes('--src=')).replace('--src=', '')
const dataExplorerUrl = `http://localhost:8888/sources/${src}/chronograf/data-explorer`
const dataTest = s => `[data-test="${s}"]`
module.exports = {
'Data Explorer (functional) - SHOW DATABASES'(browser) {
browser
// Navigate to the Data Explorer
.url(dataExplorerUrl)
// Open a new query tab
.waitForElementVisible(dataTest('add-query-button'), 1000)
.click(dataTest('add-query-button'))
.waitForElementVisible(dataTest('query-editor-field'), 1000)
// Drop any existing testing database
.setValue(dataTest('query-editor-field'), 'DROP DATABASE "testing"\n')
.click(dataTest('query-editor-field'))
.pause(500)
// Create a new testing database
.clearValue(dataTest('query-editor-field'))
.setValue(dataTest('query-editor-field'), 'CREATE DATABASE "testing"\n')
.click(dataTest('query-editor-field'))
.pause(2000)
.refresh()
.waitForElementVisible(dataTest('query-editor-field'), 1000)
.clearValue(dataTest('query-editor-field'))
.setValue(dataTest('query-editor-field'), 'SHOW DATABASES\n')
.click(dataTest('query-editor-field'))
.pause(1000)
.waitForElementVisible(
dataTest('query-builder-list-item-database-testing'),
5000
)
.assert.containsText(
dataTest('query-builder-list-item-database-testing'),
'testing'
)
.end()
},
'Query Builder'(browser) {
browser
// Navigate to the Data Explorer
.url(dataExplorerUrl)
// Check to see that there are no results displayed
.waitForElementVisible(dataTest('data-explorer-no-results'), 5000)
.assert.containsText(dataTest('data-explorer-no-results'), 'No Results')
// Open a new query tab
.waitForElementVisible(dataTest('new-query-button'), 1000)
.click(dataTest('new-query-button'))
// Select the testing database
.waitForElementVisible(
dataTest('query-builder-list-item-database-testing'),
1000
)
.click(dataTest('query-builder-list-item-database-testing'))
// Open up the Write Data dialog
.click(dataTest('write-data-button'))
// Set the dialog to manual entry mode
.waitForElementVisible(dataTest('manual-entry-button'), 1000)
.click(dataTest('manual-entry-button'))
// Enter some time-series data
.setValue(
dataTest('manual-entry-field'),
'testing,test_measurement=1,test_measurement2=2 value=3,value2=4'
)
// Pause, then click the submit button
.pause(500)
.click(dataTest('write-data-submit-button'))
.pause(2000)
// Start building a query
// Select the testing measurement
.waitForElementVisible(
dataTest('query-builder-list-item-measurement-testing'),
2000
)
.click(dataTest('query-builder-list-item-measurement-testing'))
// Select both test measurements
.waitForElementVisible(
dataTest('query-builder-list-item-tag-test_measurement'),
1000
)
.click(dataTest('query-builder-list-item-tag-test_measurement'))
.click(dataTest('query-builder-list-item-tag-test_measurement2'))
.pause(500)
// Select both tag values
.waitForElementVisible(
dataTest('query-builder-list-item-tag-value-1'),
1000
)
.click(dataTest('query-builder-list-item-tag-value-1'))
.click(dataTest('query-builder-list-item-tag-value-2'))
.pause(500)
// Select both field values
.waitForElementVisible(
dataTest('query-builder-list-item-field-value'),
1000
)
.click(dataTest('query-builder-list-item-field-value'))
.click(dataTest('query-builder-list-item-field-value2'))
.pause(500)
// Assert the built query string
.assert.containsText(
dataTest('query-editor-field'),
'SELECT mean("value") AS "mean_value", mean("value2") AS "mean_value2" FROM "testing"."autogen"."testing" WHERE time > now() - 1h AND "test_measurement"=\'1\' AND "test_measurement2"=\'2\' GROUP BY time(10s)'
)
.click(dataTest('data-table'))
.click(dataTest('query-builder-list-item-function-value'))
.waitForElementVisible(dataTest('function-selector-item-mean'), 1000)
.click(dataTest('function-selector-item-mean'))
.click(dataTest('function-selector-apply'))
.end()
},
}

View File

@ -140,9 +140,9 @@ acorn@^3.0.0, acorn@^3.0.4, acorn@^3.1.0:
version "3.3.0"
resolved "https://registry.yarnpkg.com/acorn/-/acorn-3.3.0.tgz#45e37fb39e8da3f25baee3ff5369e2bb5f22017a"
acorn@^4.0.1:
version "4.0.4"
resolved "https://registry.yarnpkg.com/acorn/-/acorn-4.0.4.tgz#17a8d6a7a6c4ef538b814ec9abac2779293bf30a"
acorn@^5.1.1:
version "5.1.1"
resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.1.1.tgz#53fe161111f912ab999ee887a90a0bc52822fd75"
add-px-to-style@1.0.0:
version "1.0.0"
@ -200,10 +200,18 @@ ansi-html@0.0.6:
version "0.0.6"
resolved "https://registry.yarnpkg.com/ansi-html/-/ansi-html-0.0.6.tgz#bda8e33dd2ee1c20f54c08eb405713cbfc0ed80e"
ansi-regex@^0.2.0, ansi-regex@^0.2.1:
version "0.2.1"
resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-0.2.1.tgz#0d8e946967a3d8143f93e24e298525fc1b2235f9"
ansi-regex@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.0.0.tgz#c5061b6e0ef8a81775e50f5d66151bf6bf371107"
ansi-styles@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-1.1.0.tgz#eaecbf66cd706882760b2f4691582b8f55d7a7de"
ansi-styles@^2.2.1:
version "2.2.1"
resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-2.2.1.tgz#b432dd3358b634cf75e1e4664368240533c1ddbe"
@ -1105,7 +1113,7 @@ babel-plugin-transform-strict-mode@^6.18.0:
babel-runtime "^6.0.0"
babel-types "^6.18.0"
babel-polyfill@^6.13.0:
babel-polyfill@^6.13.0, babel-polyfill@^6.20.0:
version "6.20.0"
resolved "https://registry.yarnpkg.com/babel-polyfill/-/babel-polyfill-6.20.0.tgz#de4a371006139e20990aac0be367d398331204e7"
dependencies:
@ -1452,7 +1460,7 @@ block-stream@*:
dependencies:
inherits "~2.0.0"
bluebird@^3.1.1, bluebird@^3.3.0, bluebird@^3.4.6:
bluebird@^3.1.1, bluebird@^3.3.0, bluebird@^3.4.6, bluebird@^3.4.7:
version "3.4.7"
resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.4.7.tgz#f72d760be09b7f76d08ed8fae98b289a8d05fab3"
@ -1642,6 +1650,16 @@ chai@^3.5.0:
deep-eql "^0.1.3"
type-detect "^1.0.0"
chalk@0.5.1:
version "0.5.1"
resolved "https://registry.yarnpkg.com/chalk/-/chalk-0.5.1.tgz#663b3a648b68b55d04690d49167aa837858f2174"
dependencies:
ansi-styles "^1.1.0"
escape-string-regexp "^1.0.0"
has-ansi "^0.1.0"
strip-ansi "^0.3.0"
supports-color "^0.2.0"
chalk@^1.0.0, chalk@^1.1.0, chalk@^1.1.1, chalk@^1.1.3:
version "1.1.3"
resolved "https://registry.yarnpkg.com/chalk/-/chalk-1.1.3.tgz#a8115c55e4a702fe4d150abd3872822a7e09fc98"
@ -1679,7 +1697,7 @@ cheerio@^0.22.0:
lodash.reject "^4.4.0"
lodash.some "^4.4.0"
chokidar@^1.0.0, chokidar@^1.4.1:
chokidar@^1.0.0, chokidar@^1.4.1, chokidar@^1.4.3:
version "1.6.1"
resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-1.6.1.tgz#2f4447ab5e96e50fb3d789fd90d4c72e0e4c70c2"
dependencies:
@ -1832,6 +1850,10 @@ commander@2.3.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/commander/-/commander-2.3.0.tgz#fd430e889832ec353b9acd1de217c11cb3eef873"
commander@2.6.0:
version "2.6.0"
resolved "https://registry.yarnpkg.com/commander/-/commander-2.6.0.tgz#9df7e52fb2a0cb0fb89058ee80c3104225f37e1d"
commander@2.8.x:
version "2.8.1"
resolved "https://registry.yarnpkg.com/commander/-/commander-2.8.1.tgz#06be367febfda0c330aa1e2a072d3dc9762425d4"
@ -1917,7 +1939,7 @@ concat-stream@1.5.0:
readable-stream "~2.0.0"
typedarray "~0.0.5"
concat-stream@^1.4.6:
concat-stream@^1.5.2:
version "1.6.0"
resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.6.0.tgz#0aac662fd52be78964d5532f694784e70110acf7"
dependencies:
@ -1925,6 +1947,19 @@ concat-stream@^1.4.6:
readable-stream "^2.2.2"
typedarray "^0.0.6"
concurrently@^3.5.0:
version "3.5.0"
resolved "https://registry.yarnpkg.com/concurrently/-/concurrently-3.5.0.tgz#8cf1b7707a6916a78a4ff5b77bb04dec54b379b2"
dependencies:
chalk "0.5.1"
commander "2.6.0"
date-fns "^1.23.0"
lodash "^4.5.1"
rx "2.3.24"
spawn-command "^0.0.2-1"
supports-color "^3.2.3"
tree-kill "^1.1.0"
configstore@^2.0.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/configstore/-/configstore-2.1.0.tgz#737a3a7036e9886102aa6099e47bb33ab1aba1a1"
@ -2256,6 +2291,10 @@ dashdash@^1.12.0:
dependencies:
assert-plus "^1.0.0"
date-fns@^1.23.0:
version "1.28.5"
resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-1.28.5.tgz#257cfc45d322df45ef5658665967ee841cd73faf"
date-now@^0.1.4:
version "0.1.4"
resolved "https://registry.yarnpkg.com/date-now/-/date-now-0.1.4.tgz#eaf439fd4d4848ad74e5cc7dbef200672b9e345b"
@ -2276,11 +2315,11 @@ debug@2.3.3:
dependencies:
ms "0.7.2"
debug@^2.1.1, debug@^2.2.0:
version "2.5.2"
resolved "https://registry.yarnpkg.com/debug/-/debug-2.5.2.tgz#50c295a53dbf1657146e0c1b21307275e90d49cb"
debug@^2.1.1, debug@^2.2.0, debug@^2.6.3:
version "2.6.8"
resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.8.tgz#e731531ca2ede27d188222427da17821d68ff4fc"
dependencies:
ms "0.7.2"
ms "2.0.0"
decamelize@^1.0.0, decamelize@^1.1.1, decamelize@^1.1.2:
version "1.2.0"
@ -2706,7 +2745,7 @@ escape-string-regexp@1.0.2, escape-string-regexp@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.2.tgz#4dbc2fe674e71949caf3fb2695ce7f2dc1d9a8d1"
escape-string-regexp@^1.0.5:
escape-string-regexp@^1.0.0, escape-string-regexp@^1.0.5:
version "1.0.5"
resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4"
@ -2753,23 +2792,40 @@ eslint-plugin-react@6.6.0:
doctrine "^1.2.2"
jsx-ast-utils "^1.3.3"
eslint@3.9.1:
version "3.9.1"
resolved "https://registry.yarnpkg.com/eslint/-/eslint-3.9.1.tgz#5a8597706fc6048bc6061ac754d4a211d28f4f5b"
eslint-watch@^3.1.2:
version "3.1.2"
resolved "https://registry.yarnpkg.com/eslint-watch/-/eslint-watch-3.1.2.tgz#b93b3eca08915f113dc900994f880db1364de4b3"
dependencies:
babel-polyfill "^6.20.0"
bluebird "^3.4.7"
chalk "^1.1.3"
chokidar "^1.4.3"
debug "^2.6.3"
keypress "^0.2.1"
lodash "^4.17.4"
optionator "^0.8.2"
source-map-support "^0.4.14"
text-table "^0.2.0"
unicons "0.0.3"
eslint@^3.14.1:
version "3.19.0"
resolved "https://registry.yarnpkg.com/eslint/-/eslint-3.19.0.tgz#c8fc6201c7f40dd08941b87c085767386a679acc"
dependencies:
babel-code-frame "^6.16.0"
chalk "^1.1.3"
concat-stream "^1.4.6"
concat-stream "^1.5.2"
debug "^2.1.1"
doctrine "^1.2.2"
doctrine "^2.0.0"
escope "^3.6.0"
espree "^3.3.1"
espree "^3.4.0"
esquery "^1.0.0"
estraverse "^4.2.0"
esutils "^2.0.2"
file-entry-cache "^2.0.0"
glob "^7.0.3"
globals "^9.2.0"
ignore "^3.1.5"
globals "^9.14.0"
ignore "^3.2.0"
imurmurhash "^0.1.4"
inquirer "^0.12.0"
is-my-json-valid "^2.10.0"
@ -2787,16 +2843,16 @@ eslint@3.9.1:
require-uncached "^1.0.2"
shelljs "^0.7.5"
strip-bom "^3.0.0"
strip-json-comments "~1.0.1"
strip-json-comments "~2.0.1"
table "^3.7.8"
text-table "~0.2.0"
user-home "^2.0.0"
espree@^3.3.1:
version "3.3.2"
resolved "https://registry.yarnpkg.com/espree/-/espree-3.3.2.tgz#dbf3fadeb4ecb4d4778303e50103b3d36c88b89c"
espree@^3.4.0:
version "3.5.0"
resolved "https://registry.yarnpkg.com/espree/-/espree-3.5.0.tgz#98358625bdd055861ea27e2867ea729faf463d8d"
dependencies:
acorn "^4.0.1"
acorn "^5.1.1"
acorn-jsx "^3.0.0"
esprima-fb@^15001.1.0-dev-harmony-fb:
@ -2815,6 +2871,12 @@ esprima@~3.1.0:
version "3.1.3"
resolved "https://registry.yarnpkg.com/esprima/-/esprima-3.1.3.tgz#fdca51cee6133895e3c88d535ce49dbff62a4633"
esquery@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.0.0.tgz#cfba8b57d7fba93f17298a8a006a04cda13d80fa"
dependencies:
estraverse "^4.0.0"
esrecurse@^4.1.0:
version "4.1.0"
resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.1.0.tgz#4713b6536adf7f2ac4f327d559e7756bff648220"
@ -2826,7 +2888,7 @@ estraverse@^1.9.1:
version "1.9.3"
resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-1.9.3.tgz#af67f2dc922582415950926091a4005d29c9bb44"
estraverse@^4.1.1, estraverse@^4.2.0:
estraverse@^4.0.0, estraverse@^4.1.1, estraverse@^4.2.0:
version "4.2.0"
resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.2.0.tgz#0dee3fed31fcd469618ce7342099fc1afa0bdb13"
@ -3331,7 +3393,7 @@ glob@^7.0.0, glob@^7.0.3, glob@^7.0.4, glob@^7.0.5, glob@~7.1.1:
once "^1.3.0"
path-is-absolute "^1.0.0"
globals@^9.0.0, globals@^9.2.0:
globals@^9.0.0, globals@^9.14.0:
version "9.14.0"
resolved "https://registry.yarnpkg.com/globals/-/globals-9.14.0.tgz#8859936af0038741263053b39d0e76ca241e4034"
@ -3394,6 +3456,12 @@ har-validator@~2.0.6:
is-my-json-valid "^2.12.4"
pinkie-promise "^2.0.0"
has-ansi@^0.1.0:
version "0.1.0"
resolved "https://registry.yarnpkg.com/has-ansi/-/has-ansi-0.1.0.tgz#84f265aae8c0e6a88a12d7022894b7568894c62e"
dependencies:
ansi-regex "^0.2.0"
has-ansi@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/has-ansi/-/has-ansi-2.0.0.tgz#34f5049ce1ecdf2b0649af3ef24e45ed35416d91"
@ -3596,9 +3664,9 @@ ieee754@^1.1.4:
version "1.1.8"
resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.1.8.tgz#be33d40ac10ef1926701f6f08a2d86fbfd1ad3e4"
ignore@^3.1.5:
version "3.2.0"
resolved "https://registry.yarnpkg.com/ignore/-/ignore-3.2.0.tgz#8d88f03c3002a0ac52114db25d2c673b0bf1e435"
ignore@^3.2.0:
version "3.3.3"
resolved "https://registry.yarnpkg.com/ignore/-/ignore-3.3.3.tgz#432352e57accd87ab3110e82d3fea0e47812156d"
immutable@^3.8.1:
version "3.8.1"
@ -4147,6 +4215,10 @@ keycode@^2.1.1:
version "2.1.8"
resolved "https://registry.yarnpkg.com/keycode/-/keycode-2.1.8.tgz#94d2b7098215eff0e8f9a8931d5a59076c4532fb"
keypress@^0.2.1:
version "0.2.1"
resolved "https://registry.yarnpkg.com/keypress/-/keypress-0.2.1.tgz#1e80454250018dbad4c3fe94497d6e67b6269c77"
kind-of@^3.0.2:
version "3.1.0"
resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-3.1.0.tgz#475d698a5e49ff5e53d14e3e732429dc8bf4cf47"
@ -4428,7 +4500,7 @@ lodash.words@^3.0.0:
dependencies:
lodash._root "^3.0.0"
lodash@4.x.x, lodash@^4.0.0, lodash@^4.0.1, lodash@^4.1.0, lodash@^4.16.4, lodash@^4.17.2, lodash@^4.2.0, lodash@^4.2.1, lodash@^4.3.0, lodash@^4.5.0:
lodash@4.x.x, lodash@^4.0.0, lodash@^4.0.1, lodash@^4.1.0, lodash@^4.16.4, lodash@^4.17.2, lodash@^4.2.0, lodash@^4.2.1, lodash@^4.3.0, lodash@^4.5.0, lodash@^4.5.1:
version "4.17.3"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.3.tgz#557ed7d2a9438cac5fd5a43043ca60cb455e01f7"
@ -4436,6 +4508,10 @@ lodash@^3.8.0:
version "3.10.1"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-3.10.1.tgz#5bf45e8e49ba4189e17d482789dfd15bd140b7b6"
lodash@^4.17.4:
version "4.17.4"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.4.tgz#78203a4d1c328ae1d86dca6460e369b57f4055ae"
lodash@~4.16.4:
version "4.16.6"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.16.6.tgz#d22c9ac660288f3843e16ba7d2b5d06cca27d777"
@ -4697,6 +4773,10 @@ ms@0.7.2:
version "0.7.2"
resolved "https://registry.yarnpkg.com/ms/-/ms-0.7.2.tgz#ae25cf2512b3885a1d95d7f037868d8431124765"
ms@2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"
mustache@^2.2.1:
version "2.3.0"
resolved "https://registry.yarnpkg.com/mustache/-/mustache-2.3.0.tgz#4028f7778b17708a489930a6e52ac3bca0da41d0"
@ -6352,6 +6432,10 @@ rx-lite@^3.1.2:
version "3.1.2"
resolved "https://registry.yarnpkg.com/rx-lite/-/rx-lite-3.1.2.tgz#19ce502ca572665f3b647b10939f97fd1615f102"
rx@2.3.24:
version "2.3.24"
resolved "https://registry.yarnpkg.com/rx/-/rx-2.3.24.tgz#14f950a4217d7e35daa71bbcbe58eff68ea4b2b7"
samsam@1.1.2, samsam@~1.1:
version "1.1.2"
resolved "https://registry.yarnpkg.com/samsam/-/samsam-1.1.2.tgz#bec11fdc83a9fda063401210e40176c3024d1567"
@ -6668,11 +6752,11 @@ source-map-resolve@^0.3.0:
source-map-url "~0.3.0"
urix "~0.1.0"
source-map-support@^0.4.2:
version "0.4.8"
resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.4.8.tgz#4871918d8a3af07289182e974e32844327b2e98b"
source-map-support@^0.4.14, source-map-support@^0.4.2:
version "0.4.15"
resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.4.15.tgz#03202df65c06d2bd8c7ec2362a193056fef8d3b1"
dependencies:
source-map "^0.5.3"
source-map "^0.5.6"
source-map-url@~0.3.0:
version "0.3.0"
@ -6704,6 +6788,10 @@ spawn-args@^0.2.0:
version "0.2.0"
resolved "https://registry.yarnpkg.com/spawn-args/-/spawn-args-0.2.0.tgz#fb7d0bd1d70fd4316bd9e3dec389e65f9d6361bb"
spawn-command@^0.0.2-1:
version "0.0.2"
resolved "https://registry.yarnpkg.com/spawn-command/-/spawn-command-0.0.2.tgz#9544e1a43ca045f8531aac1a48cb29bdae62338e"
spdx-correct@~1.0.0:
version "1.0.2"
resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-1.0.2.tgz#4b3073d933ff51f3912f03ac5519498a4150db40"
@ -6823,6 +6911,12 @@ stringstream@~0.0.4:
version "0.0.5"
resolved "https://registry.yarnpkg.com/stringstream/-/stringstream-0.0.5.tgz#4e484cd4de5a0bbbee18e46307710a8a81621878"
strip-ansi@^0.3.0:
version "0.3.0"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-0.3.0.tgz#25f48ea22ca79187f3174a4db8759347bb126220"
dependencies:
ansi-regex "^0.2.1"
strip-ansi@^3.0.0, strip-ansi@^3.0.1:
version "3.0.1"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-3.0.1.tgz#6a385fb8853d952d5ff05d0e8aaf94278dc63dcf"
@ -6845,10 +6939,14 @@ strip-indent@^1.0.1:
dependencies:
get-stdin "^4.0.1"
strip-json-comments@~1.0.1, strip-json-comments@~1.0.4:
strip-json-comments@~1.0.4:
version "1.0.4"
resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-1.0.4.tgz#1e15fbcac97d3ee99bf2d73b4c656b082bbafb91"
strip-json-comments@~2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a"
style-loader@0.13.1, style-loader@^0.13.0:
version "0.13.1"
resolved "https://registry.yarnpkg.com/style-loader/-/style-loader-0.13.1.tgz#468280efbc0473023cd3a6cd56e33b5a1d7fc3a9"
@ -6877,9 +6975,9 @@ supports-color@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-2.0.0.tgz#535d045ce6b6363fa40117084629995e9df324c7"
supports-color@^3.1.0, supports-color@^3.1.1, supports-color@^3.1.2:
version "3.1.2"
resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-3.1.2.tgz#72a262894d9d408b956ca05ff37b2ed8a6e2a2d5"
supports-color@^3.1.0, supports-color@^3.1.1, supports-color@^3.1.2, supports-color@^3.2.3:
version "3.2.3"
resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-3.2.3.tgz#65ac0504b3954171d8a64946b2ae3cbb8a5f54f6"
dependencies:
has-flag "^1.0.0"
@ -6983,7 +7081,7 @@ testem@^1.2.1:
tap-parser "^3.0.2"
xmldom "^0.1.19"
text-table@~0.2.0:
text-table@^0.2.0, text-table@~0.2.0:
version "0.2.0"
resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4"
@ -7057,6 +7155,10 @@ tr46@~0.0.3:
version "0.0.3"
resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a"
tree-kill@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/tree-kill/-/tree-kill-1.1.0.tgz#c963dcf03722892ec59cba569e940b71954d1729"
trim-newlines@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/trim-newlines/-/trim-newlines-1.0.0.tgz#5887966bb582a4503a41eb524f7d35011815a613"
@ -7131,6 +7233,10 @@ underscore@>=1.8.3:
version "1.8.3"
resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.8.3.tgz#4f3fb53b106e6097fcf9cb4109f2a5e9bdfa5022"
unicons@0.0.3:
version "0.0.3"
resolved "https://registry.yarnpkg.com/unicons/-/unicons-0.0.3.tgz#6e6a7a1a6eaebb01ca3d8b12ad9687279eaba524"
uniq@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/uniq/-/uniq-1.0.1.tgz#b31c5ae8254844a3a8281541ce2b04b865a734ff"