Merge branch 'master' into nls/drag-fixes
commit
a5e8c5ba2e
|
@ -17,3 +17,4 @@ npm-debug.log
|
|||
.jssrc
|
||||
.dev-jssrc
|
||||
.bindata
|
||||
ui/reports
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -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 {
|
||||
|
|
|
@ -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{
|
||||
|
|
|
@ -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"`
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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",
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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>
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
},
|
||||
|
|
|
@ -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" />
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -24,6 +24,7 @@ const WriteDataBody = ({
|
|||
onKeyUp={handleKeyUp}
|
||||
onChange={handleEdit}
|
||||
autoFocus={true}
|
||||
data-test="manual-entry-field"
|
||||
/>
|
||||
: <div className="write-data-form--file">
|
||||
<input
|
||||
|
|
|
@ -37,6 +37,7 @@ const WriteDataFooter = ({
|
|||
(!uploadContent && !isManual) ||
|
||||
isUploading
|
||||
}
|
||||
data-test="write-data-submit-button"
|
||||
>
|
||||
Write
|
||||
</button>
|
||||
|
|
|
@ -27,6 +27,7 @@ const WriteDataHeader = ({
|
|||
<li
|
||||
onClick={() => toggleWriteView(true)}
|
||||
className={isManual ? 'active' : ''}
|
||||
data-test="manual-entry-button"
|
||||
>
|
||||
Manual Entry
|
||||
</li>
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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'
|
||||
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
},
|
||||
|
|
|
@ -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>
|
|
@ -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
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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" />
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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%;}
|
||||
}
|
||||
|
|
|
@ -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%;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
|
@ -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()
|
||||
},
|
||||
}
|
186
ui/yarn.lock
186
ui/yarn.lock
|
@ -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"
|
||||
|
|
Loading…
Reference in New Issue