From 812d7c0147646325a21318e992dee39b0c0e944c Mon Sep 17 00:00:00 2001 From: Hunter Trujillo Date: Mon, 10 Jul 2017 17:10:45 -0600 Subject: [PATCH 01/62] Initial IE 11 support. Add babel polyfill. Fix a small event error. --- ui/src/index.js | 2 ++ ui/src/shared/middleware/resizeLayout.js | 4 +++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/ui/src/index.js b/ui/src/index.js index 7b656caffc..cc292ca0ff 100644 --- a/ui/src/index.js +++ b/ui/src/index.js @@ -1,3 +1,5 @@ +import 'babel-polyfill' + import React from 'react' import {render} from 'react-dom' import {Provider} from 'react-redux' diff --git a/ui/src/shared/middleware/resizeLayout.js b/ui/src/shared/middleware/resizeLayout.js index df6d3c2bf3..0e682d2682 100644 --- a/ui/src/shared/middleware/resizeLayout.js +++ b/ui/src/shared/middleware/resizeLayout.js @@ -7,7 +7,9 @@ export default function resizeLayout() { action.type === 'ENABLE_PRESENTATION_MODE' || action.type === 'DISABLE_PRESENTATION_MODE' ) { - window.dispatchEvent(new Event('resize')) + const evt = document.createEvent('HTMLEvents') + evt.initEvent('resize', false, true) + window.dispatchEvent(evt) } } } From b81e1113bac653e9efba239dd06700b0161316c8 Mon Sep 17 00:00:00 2001 From: Hunter Trujillo Date: Mon, 24 Jul 2017 12:59:01 -0600 Subject: [PATCH 02/62] Use older data attribute accessor technique for pre IE 11 compat. --- ui/src/index.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ui/src/index.js b/ui/src/index.js index cc292ca0ff..0cc35d03a6 100644 --- a/ui/src/index.js +++ b/ui/src/index.js @@ -49,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 From c9cbe94de91516a8c536b938b3065f6b445162e0 Mon Sep 17 00:00:00 2001 From: Hunter Trujillo Date: Thu, 27 Jul 2017 13:34:10 -0600 Subject: [PATCH 03/62] Fix incorrect windows uptime query. Fix how the Windows value is used in the UI. --- ui/src/hosts/apis/index.js | 10 ++++++---- ui/src/hosts/components/HostsTable.js | 4 +++- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/ui/src/hosts/apis/index.js b/ui/src/hosts/apis/index.js index 225ac59a3e..1c0324dcfb 100644 --- a/ui/src/hosts/apis/index.js +++ b/ui/src/hosts/apis/index.js @@ -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 diff --git a/ui/src/hosts/components/HostsTable.js b/ui/src/hosts/components/HostsTable.js index 365eb6e721..bd04942725 100644 --- a/ui/src/hosts/components/HostsTable.js +++ b/ui/src/hosts/components/HostsTable.js @@ -209,7 +209,9 @@ const HostRow = React.createClass({
0 ? 'dot-success' : 'dot-critical' + Math.max(host.deltaUptime || 0, host.winDeltaUptime || 0) > 0 + ? 'dot-success' + : 'dot-critical' )} /> From b29ea3ad37ae88417c13df7683441164e0976a98 Mon Sep 17 00:00:00 2001 From: Hunter Trujillo Date: Thu, 27 Jul 2017 13:39:51 -0600 Subject: [PATCH 04/62] CHANGELOG. --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index bce5079bc6..25f3085fdc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,7 @@ ## v1.3.6.0 [unreleased] ### Bug Fixes +1. [#1795](https://github.com/influxdata/chronograf/pull/1795): Fix uptime status on Windows hosts running Telegraf + ### Features ### UI Improvements From 352f3640325157b665d0fb7a0bec430bc7b54712 Mon Sep 17 00:00:00 2001 From: Hunter Trujillo Date: Thu, 27 Jul 2017 17:28:55 -0600 Subject: [PATCH 05/62] We simply mustn't forget units on our flex-basis shorthand for IE11. --- ui/src/dashboards/components/DashboardHeaderEdit.js | 2 +- ui/src/kapacitor/components/RuleMessageOptions.js | 4 ++-- ui/src/style/components/dygraphs.scss | 4 ++-- ui/src/style/components/query-builder.scss | 10 +++++----- ui/src/style/components/query-editor.scss | 2 +- ui/src/style/components/query-maker.scss | 4 ++-- ui/src/style/components/tables.scss | 4 ++-- ui/src/style/components/template-control-bar.scss | 4 ++-- .../style/components/template-variables-manager.scss | 2 +- ui/src/style/pages/admin.scss | 6 +++--- ui/src/style/pages/config-endpoints.scss | 8 ++++---- ui/src/style/pages/overlay-technology.scss | 2 +- ui/src/style/theme/bootstrap-theme.scss | 4 ++-- 13 files changed, 28 insertions(+), 28 deletions(-) diff --git a/ui/src/dashboards/components/DashboardHeaderEdit.js b/ui/src/dashboards/components/DashboardHeaderEdit.js index a79273a39d..f1dd5bc8a4 100644 --- a/ui/src/dashboards/components/DashboardHeaderEdit.js +++ b/ui/src/dashboards/components/DashboardHeaderEdit.js @@ -38,7 +38,7 @@ class DashboardEditHeader extends Component {
@@ -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} diff --git a/ui/src/style/components/dygraphs.scss b/ui/src/style/components/dygraphs.scss index 654c309042..f38b0420a6 100644 --- a/ui/src/style/components/dygraphs.scss +++ b/ui/src/style/components/dygraphs.scss @@ -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 { diff --git a/ui/src/style/components/query-builder.scss b/ui/src/style/components/query-builder.scss index b9642a98e7..40ec78de1d 100644 --- a/ui/src/style/components/query-builder.scss +++ b/ui/src/style/components/query-builder.scss @@ -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 { diff --git a/ui/src/style/components/query-editor.scss b/ui/src/style/components/query-editor.scss index 0874272cca..f5045dc005 100644 --- a/ui/src/style/components/query-editor.scss +++ b/ui/src/style/components/query-editor.scss @@ -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; diff --git a/ui/src/style/components/query-maker.scss b/ui/src/style/components/query-maker.scss index 172f1832b2..d74c40dbdf 100644 --- a/ui/src/style/components/query-maker.scss +++ b/ui/src/style/components/query-maker.scss @@ -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; } diff --git a/ui/src/style/components/tables.scss b/ui/src/style/components/tables.scss index 632f17e193..9425cce4f8 100644 --- a/ui/src/style/components/tables.scss +++ b/ui/src/style/components/tables.scss @@ -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 { diff --git a/ui/src/style/components/template-control-bar.scss b/ui/src/style/components/template-control-bar.scss index 6ad2375e24..074e42508a 100644 --- a/ui/src/style/components/template-control-bar.scss +++ b/ui/src/style/components/template-control-bar.scss @@ -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; diff --git a/ui/src/style/components/template-variables-manager.scss b/ui/src/style/components/template-variables-manager.scss index 622e27c584..ad85d3c211 100644 --- a/ui/src/style/components/template-variables-manager.scss +++ b/ui/src/style/components/template-variables-manager.scss @@ -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%;} } diff --git a/ui/src/style/pages/admin.scss b/ui/src/style/pages/admin.scss index 8c1cce54a1..fc66cc6f34 100644 --- a/ui/src/style/pages/admin.scss +++ b/ui/src/style/pages/admin.scss @@ -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%; } } diff --git a/ui/src/style/pages/config-endpoints.scss b/ui/src/style/pages/config-endpoints.scss index 37aac2ca1f..44562000d3 100644 --- a/ui/src/style/pages/config-endpoints.scss +++ b/ui/src/style/pages/config-endpoints.scss @@ -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; -} \ No newline at end of file +} diff --git a/ui/src/style/pages/overlay-technology.scss b/ui/src/style/pages/overlay-technology.scss index 7062562499..59222a71e7 100644 --- a/ui/src/style/pages/overlay-technology.scss +++ b/ui/src/style/pages/overlay-technology.scss @@ -85,7 +85,7 @@ $overlay-z: 100; margin: 0 15%; } .overlay-technology .query-maker { - flex: 1 0 0; + flex: 1 0 0%; padding: 0 8px; border-radius: 0 0 $radius $radius; background-color: $g2-kevlar; diff --git a/ui/src/style/theme/bootstrap-theme.scss b/ui/src/style/theme/bootstrap-theme.scss index 8345c8ee16..dc9e8897fd 100755 --- a/ui/src/style/theme/bootstrap-theme.scss +++ b/ui/src/style/theme/bootstrap-theme.scss @@ -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, From f6b399d3a76b1a7124f48fa9d02063f2e7e49d6b Mon Sep 17 00:00:00 2001 From: Hunter Trujillo Date: Thu, 27 Jul 2017 17:31:24 -0600 Subject: [PATCH 06/62] CHAMGELOG --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3516dfe021..a34d7616b1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,7 @@ ## v1.3.6.0 [unreleased] ### Bug Fixes +1. [#1715](https://github.com/influxdata/chronograf/pull/1715): Chronograf now renders properly on IE11. + ### Features ### UI Improvements 1. [#1796](https://github.com/influxdata/chronograf/pull/1796): Add spinner to indicate data is being written From badc0a73749060194d5e5664bdc53b495931b615 Mon Sep 17 00:00:00 2001 From: Hunter Trujillo Date: Thu, 27 Jul 2017 17:43:45 -0600 Subject: [PATCH 07/62] Remember, kids... Always leave a note. --- ui/src/shared/middleware/resizeLayout.js | 1 + 1 file changed, 1 insertion(+) diff --git a/ui/src/shared/middleware/resizeLayout.js b/ui/src/shared/middleware/resizeLayout.js index 0e682d2682..d7a657d4ba 100644 --- a/ui/src/shared/middleware/resizeLayout.js +++ b/ui/src/shared/middleware/resizeLayout.js @@ -7,6 +7,7 @@ export default function resizeLayout() { action.type === 'ENABLE_PRESENTATION_MODE' || action.type === 'DISABLE_PRESENTATION_MODE' ) { + // Uses longer event object creation method due to IE compatibility. const evt = document.createEvent('HTMLEvents') evt.initEvent('resize', false, true) window.dispatchEvent(evt) From 228920fde4266887fad85f2437012ebad7ee7b3e Mon Sep 17 00:00:00 2001 From: Tim Raymond Date: Tue, 8 Aug 2017 11:05:50 -0700 Subject: [PATCH 08/62] Add absolute time range support to :interval: Previously, users of the :interval: macro were restricted to using a relative time range in their queries, or the :dashboardTime: macro. This permits users to also supply an absolute time range in the form of: time > '2017-01-01T00:00:00Z' and time < '2017-06-01T00:00:00Z' --- chronograf.go | 88 +++++++++++++++++++++++++++++++++++++++++----- chronograf_test.go | 49 ++++++++++++++++++++++++++ 2 files changed, 129 insertions(+), 8 deletions(-) create mode 100644 chronograf_test.go diff --git a/chronograf.go b/chronograf.go index 319065befc..aa9c6bc6c9 100644 --- a/chronograf.go +++ b/chronograf.go @@ -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 { diff --git a/chronograf_test.go b/chronograf_test.go new file mode 100644 index 0000000000..60164d5880 --- /dev/null +++ b/chronograf_test.go @@ -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) + } + }) + } +} From 5bcfda06658b265bd21ea86972b5df9e5b5a1291 Mon Sep 17 00:00:00 2001 From: Hunter Trujillo Date: Tue, 8 Aug 2017 22:43:16 -0700 Subject: [PATCH 09/62] Add SHOW DATABASES integration test with Nightwatch.js. --- .gitignore | 1 + ui/.eslintrc | 2 +- ui/nightwatch.json | 37 +++++++++++++++++++++++++++++++++++++ ui/package.json | 1 + ui/tests/DataExplorer.js | 34 ++++++++++++++++++++++++++++++++++ 5 files changed, 74 insertions(+), 1 deletion(-) create mode 100644 ui/nightwatch.json create mode 100644 ui/tests/DataExplorer.js diff --git a/.gitignore b/.gitignore index bbef07c3ad..b580642afd 100644 --- a/.gitignore +++ b/.gitignore @@ -17,3 +17,4 @@ npm-debug.log .jssrc .dev-jssrc .bindata +ui/reports diff --git a/ui/.eslintrc b/ui/.eslintrc index 2dda26b78f..a9ca0ef627 100644 --- a/ui/.eslintrc +++ b/ui/.eslintrc @@ -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, diff --git a/ui/nightwatch.json b/ui/nightwatch.json new file mode 100644 index 0000000000..9cf577fea3 --- /dev/null +++ b/ui/nightwatch.json @@ -0,0 +1,37 @@ +{ + "src_folders": ["tests"], + "output_folder": "reports", + "custom_commands_path": "", + "custom_assertions_path": "", + "page_objects_path": "", + "globals_path": "", + + "use_xpath": true, + + "selenium": { + "start_process": false, + "host": "hub-cloud.browserstack.com", + "port": 80 + }, + + "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": "1920x1080" + } + } + } +} diff --git a/ui/package.json b/ui/package.json index 19304204b7..f6ebfab52a 100644 --- a/ui/package.json +++ b/ui/package.json @@ -16,6 +16,7 @@ "test": "karma start", "test:lint": "npm run lint; npm run test", "test:dev": "nodemon --exec npm run test:lint", + "test:integration": "nightwatch tests --skip", "clean": "rm -rf build", "storybook": "node ./storybook", "prettier": "prettier --single-quote --trailing-comma es5 --bracket-spacing false --semi false --write \"{src,spec}/**/*.js\"; eslint src --fix" diff --git a/ui/tests/DataExplorer.js b/ui/tests/DataExplorer.js new file mode 100644 index 0000000000..5be92de2a2 --- /dev/null +++ b/ui/tests/DataExplorer.js @@ -0,0 +1,34 @@ +const src = process.argv.find(s => s.includes('--src=')).replace('--src=', '') +const url = `http://localhost:8888/sources/${src}/chronograf/data-explorer` + +module.exports = { + 'Data Explorer (functional) - SHOW DATABASES'(browser) { + browser + .url(url) + .useXpath() + .waitForElementVisible( + '//div[@class="btn btn-primary"][contains(.,"Add a Query")]', + 1000 + ) + .click('//div[@class="btn btn-primary"][contains(.,"Add a Query")]') + .pause(500) + .waitForElementVisible( + '//textarea[contains(@class,"query-editor--field")]', + 1000 + ) + .setValue( + '//textarea[contains(@class,"query-editor--field")]', + 'SHOW DATABASES\n' + ) + .pause(1000) + .waitForElementVisible( + '*//div[@class="fixedDataTableCellLayout_main public_fixedDataTableCell_main"]/span[contains(.,"_internal")]', + 5000 + ) + .assert.containsText( + '*//div[@class="fixedDataTableCellLayout_main public_fixedDataTableCell_main"]/span[contains(.,"_internal")]', + '_internal' + ) + .end() + }, +} From 47a4ff913079c605add9f1073807201c2196224d Mon Sep 17 00:00:00 2001 From: Hunter Trujillo Date: Wed, 9 Aug 2017 01:43:23 -0700 Subject: [PATCH 10/62] Refactor test selectors with data-test attributes. Excellent reduction in complexity. --- ui/nightwatch.json | 2 -- .../data_explorer/components/QueryEditor.js | 1 + ui/src/data_explorer/components/QueryMaker.js | 1 + ui/src/data_explorer/components/Table.js | 2 +- ui/tests/DataExplorer.js | 28 ++++--------------- 5 files changed, 9 insertions(+), 25 deletions(-) diff --git a/ui/nightwatch.json b/ui/nightwatch.json index 9cf577fea3..782749e185 100644 --- a/ui/nightwatch.json +++ b/ui/nightwatch.json @@ -6,8 +6,6 @@ "page_objects_path": "", "globals_path": "", - "use_xpath": true, - "selenium": { "start_process": false, "host": "hub-cloud.browserstack.com", diff --git a/ui/src/data_explorer/components/QueryEditor.js b/ui/src/data_explorer/components/QueryEditor.js index 4e6f49431d..c9b8e5a5eb 100644 --- a/ui/src/data_explorer/components/QueryEditor.js +++ b/ui/src/data_explorer/components/QueryEditor.js @@ -220,6 +220,7 @@ 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" />
Add a Query
diff --git a/ui/src/data_explorer/components/Table.js b/ui/src/data_explorer/components/Table.js index 4f13a53e53..c8ace2c54f 100644 --- a/ui/src/data_explorer/components/Table.js +++ b/ui/src/data_explorer/components/Table.js @@ -177,7 +177,7 @@ const ChronoTable = React.createClass({ selected={series[activeSeriesIndex].name} buttonSize="btn-xs" />} -
+
{(columns && !columns.length) || (values && !values.length) ?
This series is empty
: Date: Wed, 9 Aug 2017 15:16:15 -0700 Subject: [PATCH 11/62] Integration testing improvements and comments. WIP Query Builder test. --- ui/nightwatch.json | 2 +- .../data_explorer/components/DatabaseList.js | 1 + .../data_explorer/components/FieldListItem.js | 1 + .../components/MeasurementList.js | 1 + ui/src/data_explorer/components/QueryMaker.js | 1 + .../data_explorer/components/QueryMakerTab.js | 6 +- ui/src/data_explorer/components/Table.js | 2 +- .../data_explorer/components/TagListItem.js | 8 +- .../data_explorer/components/WriteDataBody.js | 1 + .../components/WriteDataFooter.js | 1 + .../components/WriteDataHeader.js | 1 + ui/src/data_explorer/containers/Header.js | 6 +- ui/src/shared/components/AutoRefresh.js | 2 +- ui/src/shared/components/LayoutRenderer.js | 2 +- ui/tests/DataExplorer.js | 90 +++++++++++++++++-- 15 files changed, 110 insertions(+), 15 deletions(-) diff --git a/ui/nightwatch.json b/ui/nightwatch.json index 782749e185..d47fca7b18 100644 --- a/ui/nightwatch.json +++ b/ui/nightwatch.json @@ -28,7 +28,7 @@ "browserstack.key": "${BROWSERSTACK_KEY}", "browserstack.debug": true, "browserstack.local": true, - "resolution": "1920x1080" + "resolution": "1024x768" } } } diff --git a/ui/src/data_explorer/components/DatabaseList.js b/ui/src/data_explorer/components/DatabaseList.js index 23ad6143a1..db1a34ab7c 100644 --- a/ui/src/data_explorer/components/DatabaseList.js +++ b/ui/src/data_explorer/components/DatabaseList.js @@ -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} diff --git a/ui/src/data_explorer/components/FieldListItem.js b/ui/src/data_explorer/components/FieldListItem.js index a91673de08..f1f463b5f6 100644 --- a/ui/src/data_explorer/components/FieldListItem.js +++ b/ui/src/data_explorer/components/FieldListItem.js @@ -64,6 +64,7 @@ const FieldListItem = React.createClass({ active: isSelected, })} onClick={_.wrap(fieldFunc, this.handleToggleField)} + data-test={`query-builder-list-item-field-${fieldText}`} >
diff --git a/ui/src/data_explorer/components/MeasurementList.js b/ui/src/data_explorer/components/MeasurementList.js index d69d751989..5b9b993a3a 100644 --- a/ui/src/data_explorer/components/MeasurementList.js +++ b/ui/src/data_explorer/components/MeasurementList.js @@ -144,6 +144,7 @@ const MeasurementList = React.createClass({ className={classnames('query-builder--list-item', { active: isActive, })} + data-test={`query-builder-list-item-measurement-${measurement}`} >
diff --git a/ui/src/data_explorer/components/QueryMaker.js b/ui/src/data_explorer/components/QueryMaker.js index 6e5dce0f58..c05b694dc9 100644 --- a/ui/src/data_explorer/components/QueryMaker.js +++ b/ui/src/data_explorer/components/QueryMaker.js @@ -168,6 +168,7 @@ const QueryMaker = React.createClass({
diff --git a/ui/src/data_explorer/components/QueryMakerTab.js b/ui/src/data_explorer/components/QueryMakerTab.js index 03207c4b46..68f0c51a13 100644 --- a/ui/src/data_explorer/components/QueryMakerTab.js +++ b/ui/src/data_explorer/components/QueryMakerTab.js @@ -33,7 +33,11 @@ const QueryMakerTab = React.createClass({ - +
) }, diff --git a/ui/src/data_explorer/components/Table.js b/ui/src/data_explorer/components/Table.js index c8ace2c54f..4f13a53e53 100644 --- a/ui/src/data_explorer/components/Table.js +++ b/ui/src/data_explorer/components/Table.js @@ -177,7 +177,7 @@ const ChronoTable = React.createClass({ selected={series[activeSeriesIndex].name} buttonSize="btn-xs" />} -
+
{(columns && !columns.length) || (values && !values.length) ?
This series is empty
:
-1, }) return ( -
+
{v} @@ -103,6 +108,7 @@ const TagListItem = React.createClass({
diff --git a/ui/src/data_explorer/components/WriteDataBody.js b/ui/src/data_explorer/components/WriteDataBody.js index 2cb59caab0..d94521a890 100644 --- a/ui/src/data_explorer/components/WriteDataBody.js +++ b/ui/src/data_explorer/components/WriteDataBody.js @@ -24,6 +24,7 @@ const WriteDataBody = ({ onKeyUp={handleKeyUp} onChange={handleEdit} autoFocus={true} + data-test="manual-entry-field" /> :
Write diff --git a/ui/src/data_explorer/components/WriteDataHeader.js b/ui/src/data_explorer/components/WriteDataHeader.js index beba8b7420..80c704f205 100644 --- a/ui/src/data_explorer/components/WriteDataHeader.js +++ b/ui/src/data_explorer/components/WriteDataHeader.js @@ -27,6 +27,7 @@ const WriteDataHeader = ({
  • toggleWriteView(true)} className={isManual ? 'active' : ''} + data-test="manual-entry-button" > Manual Entry
  • diff --git a/ui/src/data_explorer/containers/Header.js b/ui/src/data_explorer/containers/Header.js index 6913d778ae..32cd0e0bff 100644 --- a/ui/src/data_explorer/containers/Header.js +++ b/ui/src/data_explorer/containers/Header.js @@ -49,7 +49,11 @@ const Header = React.createClass({
    -
    +
    Write Data
    diff --git a/ui/src/shared/components/AutoRefresh.js b/ui/src/shared/components/AutoRefresh.js index 2574c19512..1997e1b6c9 100644 --- a/ui/src/shared/components/AutoRefresh.js +++ b/ui/src/shared/components/AutoRefresh.js @@ -215,7 +215,7 @@ const AutoRefresh = ComposedComponent => { return (
    -

    No Results

    +

    No Results

    ) }, diff --git a/ui/src/shared/components/LayoutRenderer.js b/ui/src/shared/components/LayoutRenderer.js index 61f1fc11ad..f43e6c7fba 100644 --- a/ui/src/shared/components/LayoutRenderer.js +++ b/ui/src/shared/components/LayoutRenderer.js @@ -127,7 +127,7 @@ class LayoutRenderer extends Component { } return (
    -

    No Results

    +

    No Results

    ) } diff --git a/ui/tests/DataExplorer.js b/ui/tests/DataExplorer.js index b46b159566..c5e546392b 100644 --- a/ui/tests/DataExplorer.js +++ b/ui/tests/DataExplorer.js @@ -1,18 +1,92 @@ const src = process.argv.find(s => s.includes('--src=')).replace('--src=', '') -const url = `http://localhost:8888/sources/${src}/chronograf/data-explorer` +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 - .url(url) - .waitForElementVisible('[data-test="add-query-button"]', 1000) - .click('[data-test="add-query-button"]') + // 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) - .waitForElementVisible('[data-test="query-editor-field"]', 1000) - .setValue('[data-test="query-editor-field"]', 'SHOW DATABASES\n') + // 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('[data-test="data-table"]', 5000) - .assert.containsText('[data-test="data-table"]', '_internal') + .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 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(5000) + // Open a new query tab + .waitForElementVisible(dataTest('new-query-button'), 1000) + .click(dataTest('new-query-button')) + // Start building a query + // Select the testing database + .waitForElementVisible( + dataTest('query-builder-list-item-database-testing'), + 1000 + ) + .click(dataTest('query-builder-list-item-database-testing')) + // Select both test measurements + .waitForElementVisible( + dataTest('query-builder-list-item-tag-test_measurement'), + 1000 + ) + .click('query-builder-list-item-tag-test_measurement') + .click('query-builder-list-item-tag-test_measurement2') + .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')) + .waitForElementVisible(dataTest('query-builder-list-item-field-value')) + .click(dataTest('query-builder-list-item-field-value')) + .click(dataTest('query-builder-list-item-field-value2')) + .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)' + ) .end() }, } From 0509c7bf5c9fabc9f17f60713b323d9fad21f8dd Mon Sep 17 00:00:00 2001 From: Hunter Trujillo Date: Wed, 9 Aug 2017 16:13:27 -0700 Subject: [PATCH 12/62] Finish the second Query Builder test. --- ui/tests/DataExplorer.js | 34 +++++++++++++++++++++++----------- 1 file changed, 23 insertions(+), 11 deletions(-) diff --git a/ui/tests/DataExplorer.js b/ui/tests/DataExplorer.js index c5e546392b..6eaa6629b7 100644 --- a/ui/tests/DataExplorer.js +++ b/ui/tests/DataExplorer.js @@ -43,6 +43,15 @@ module.exports = { // 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 @@ -56,33 +65,36 @@ module.exports = { // Pause, then click the submit button .pause(500) .click(dataTest('write-data-submit-button')) - .pause(5000) - // Open a new query tab - .waitForElementVisible(dataTest('new-query-button'), 1000) - .click(dataTest('new-query-button')) + .pause(2000) // Start building a query - // Select the testing database + // Select the testing measurement .waitForElementVisible( - dataTest('query-builder-list-item-database-testing'), - 1000 + dataTest('query-builder-list-item-measurement-testing'), + 2000 ) - .click(dataTest('query-builder-list-item-database-testing')) + .click(dataTest('query-builder-list-item-measurement-testing')) // Select both test measurements .waitForElementVisible( dataTest('query-builder-list-item-tag-test_measurement'), 1000 ) - .click('query-builder-list-item-tag-test_measurement') - .click('query-builder-list-item-tag-test_measurement2') + .click(dataTest('query-builder-list-item-tag-test_measurement')) + .click(dataTest('query-builder-list-item-tag-test_measurement2')) + // 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')) - .waitForElementVisible(dataTest('query-builder-list-item-field-value')) + // 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')) + // 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)' From 89a92a9cf3942cc165af72b1fc96a647dc53744e Mon Sep 17 00:00:00 2001 From: Hunter Trujillo Date: Wed, 9 Aug 2017 18:03:32 -0700 Subject: [PATCH 13/62] Add more integration test instrumentation for Query Builder test. Increase screen test resolution. --- ui/nightwatch.json | 4 +++- ui/src/data_explorer/components/FieldListItem.js | 1 + ui/src/data_explorer/components/VisHeader.js | 1 + ui/src/shared/components/FunctionSelector.js | 2 ++ ui/tests/DataExplorer.js | 8 ++++++++ 5 files changed, 15 insertions(+), 1 deletion(-) diff --git a/ui/nightwatch.json b/ui/nightwatch.json index d47fca7b18..367e9e2fe2 100644 --- a/ui/nightwatch.json +++ b/ui/nightwatch.json @@ -12,6 +12,8 @@ "port": 80 }, + "live_output" : true, + "test_settings": { "default": { "selenium_port": 80, @@ -28,7 +30,7 @@ "browserstack.key": "${BROWSERSTACK_KEY}", "browserstack.debug": true, "browserstack.local": true, - "resolution": "1024x768" + "resolution": "1280x1024" } } } diff --git a/ui/src/data_explorer/components/FieldListItem.js b/ui/src/data_explorer/components/FieldListItem.js index f1f463b5f6..9c730a47c9 100644 --- a/ui/src/data_explorer/components/FieldListItem.js +++ b/ui/src/data_explorer/components/FieldListItem.js @@ -78,6 +78,7 @@ const FieldListItem = React.createClass({ 'btn-primary': fieldFunc.funcs.length, })} onClick={this.toggleFunctionsMenu} + data-test={`query-builder-list-item-function-${fieldText}`} > {fieldFuncsLabel}
    diff --git a/ui/src/data_explorer/components/VisHeader.js b/ui/src/data_explorer/components/VisHeader.js index 8a822a52a5..4f118145ad 100644 --- a/ui/src/data_explorer/components/VisHeader.js +++ b/ui/src/data_explorer/components/VisHeader.js @@ -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)} diff --git a/ui/src/shared/components/FunctionSelector.js b/ui/src/shared/components/FunctionSelector.js index 75be5eb5ae..5abc26dd7d 100644 --- a/ui/src/shared/components/FunctionSelector.js +++ b/ui/src/shared/components/FunctionSelector.js @@ -74,6 +74,7 @@ class FunctionSelector extends Component {
    Apply
    @@ -90,6 +91,7 @@ class FunctionSelector extends Component { f, singleSelect ? this.onSingleSelect : this.onSelect )} + data-test={`function-selector-item-${f}`} > {f}
    diff --git a/ui/tests/DataExplorer.js b/ui/tests/DataExplorer.js index 6eaa6629b7..bcaeb34c71 100644 --- a/ui/tests/DataExplorer.js +++ b/ui/tests/DataExplorer.js @@ -80,6 +80,7 @@ module.exports = { ) .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'), @@ -87,6 +88,7 @@ module.exports = { ) .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'), @@ -94,11 +96,17 @@ module.exports = { ) .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() }, } From 436f5fdb345d70d8c41298d5ea051e015d160bb5 Mon Sep 17 00:00:00 2001 From: Nathan L Smith Date: Mon, 7 Aug 2017 19:28:23 +0000 Subject: [PATCH 14/62] Change some remaining npm mentions to yarn While `npm` is still installed as part of the build process, most of the scripts and documentation should be using yarn. Change `npm` to `yarn` where appropriate. Signed-off-by: Nathan L Smith --- CONTRIBUTING.md | 8 ++++---- Makefile | 8 ++++---- ui/README.md | 2 +- ui/karma.conf.js | 2 +- ui/package.json | 6 +++--- 5 files changed, 13 insertions(+), 13 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 27f84f45ce..f5090bb7c4 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -21,7 +21,7 @@ We really like to receive feature requests, as it helps us prioritize our work. Contributing to the source code ------------------------------- -Chronograf is built using Go for its API backend and serving the front-end assets. The front-end visualization is built with React and uses NPM for package management. The assumption is that all your Go development are done in `$GOPATH/src`. `GOPATH` can be any directory under which Chronograf and all its dependencies will be cloned. For full details on the project structure, follow along below. +Chronograf is built using Go for its API backend and serving the front-end assets. The front-end visualization is built with React and uses Yarn for package management. The assumption is that all your Go development are done in `$GOPATH/src`. `GOPATH` can be any directory under which Chronograf and all its dependencies will be cloned. For full details on the project structure, follow along below. Submitting a pull request ------------------------- @@ -43,9 +43,9 @@ Signing the CLA If you are going to be contributing back to Chronograf please take a second to sign our CLA, which can be found [on our website](https://influxdata.com/community/cla/). -Installing NPM +Installing Yarn -------------- -You'll need to install NPM to manage the JavaScript modules that the front-end uses. This varies depending on what platform you're developing on, but you should be able to find an installer on [the NPM downloads page](https://nodejs.org/en/download/). +You'll need to install Yarn to manage the JavaScript modules that the front-end uses. This varies depending on what platform you're developing on, but you should be able to find an installer on [the Yarn installation page](https://yarnpkg.com/en/docs/install). Installing Go ------------- @@ -105,7 +105,7 @@ Retaining the directory structure `$GOPATH/src/github.com/influxdata` is necessa Build and Test -------------- -Make sure you have `go` and `npm` installed and the project structure as shown above. We provide a `Makefile` to get up and running quickly, so all you'll need to do is run the following: +Make sure you have `go` and `yarn` installed and the project structure as shown above. We provide a `Makefile` to get up and running quickly, so all you'll need to do is run the following: ```bash cd $GOPATH/src/github.com/influxdata/chronograf diff --git a/Makefile b/Makefile index 25c9df79b0..a4de270cc2 100644 --- a/Makefile +++ b/Makefile @@ -60,11 +60,11 @@ canned/bin_gen.go: canned/*.json go generate -x ./canned .jssrc: $(UISOURCES) - cd ui && npm run build + cd ui && yarn run build @touch .jssrc .dev-jssrc: $(UISOURCES) - cd ui && npm run build:dev + cd ui && yarn run build:dev @touch .dev-jssrc dep: .jsdep .godep @@ -98,7 +98,7 @@ gotestrace: go test -race `go list ./... | grep -v /vendor/` jstest: - cd ui && npm test + cd ui && yarn test run: ${BINARY} ./chronograf @@ -108,7 +108,7 @@ run-dev: chronogiraffe clean: if [ -f ${BINARY} ] ; then rm ${BINARY} ; fi - cd ui && npm run clean + cd ui && yarn run clean cd ui && rm -rf node_modules rm -f dist/dist_gen.go canned/bin_gen.go server/swagger_gen.go @rm -f .godep .jsdep .jssrc .dev-jssrc .bindata diff --git a/ui/README.md b/ui/README.md index 997412b204..24f34aec5c 100644 --- a/ui/README.md +++ b/ui/README.md @@ -33,4 +33,4 @@ yarn upgrade packageName ``` ## Testing -Tests can be run via command line with `npm test`, from within the `/ui` directory. For more detailed reporting, use `npm test -- --reporters=verbose`. +Tests can be run via command line with `yarn test`, from within the `/ui` directory. For more detailed reporting, use `yarn test -- --reporters=verbose`. diff --git a/ui/karma.conf.js b/ui/karma.conf.js index a8a4f693f0..dad28ee4e2 100644 --- a/ui/karma.conf.js +++ b/ui/karma.conf.js @@ -16,7 +16,7 @@ module.exports = function(config) { 'spec/index.js': ['webpack', 'sourcemap'], }, // For more detailed reporting on tests, you can add 'verbose' and/or 'progress'. - // This can also be done via the command line with `npm test -- --reporters=verbose`. + // This can also be done via the command line with `yarn test -- --reporters=verbose`. reporters: ['dots'], webpack: { devtool: 'inline-source-map', diff --git a/ui/package.json b/ui/package.json index f6ebfab52a..963e71b7c8 100644 --- a/ui/package.json +++ b/ui/package.json @@ -9,14 +9,14 @@ "url": "github:influxdata/chronograf" }, "scripts": { - "build": "npm run clean && env NODE_ENV=production node_modules/webpack/bin/webpack.js -p --config ./webpack/prodConfig.js", + "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/", "test": "karma start", - "test:lint": "npm run lint; npm run test", - "test:dev": "nodemon --exec npm run test:lint", "test:integration": "nightwatch tests --skip", + "test:lint": "yarn run lint; yarn run test", + "test:dev": "nodemon --exec yarn run test:lint", "clean": "rm -rf build", "storybook": "node ./storybook", "prettier": "prettier --single-quote --trailing-comma es5 --bracket-spacing false --semi false --write \"{src,spec}/**/*.js\"; eslint src --fix" From 08a1fbcfc373db2ee67bd22a7c0d4b997daec3f4 Mon Sep 17 00:00:00 2001 From: Andrew Watkins Date: Tue, 8 Aug 2017 16:33:08 -0700 Subject: [PATCH 15/62] Change height of table dynamically according to resizer --- ui/src/data_explorer/components/Table.js | 11 +++++----- ui/src/data_explorer/components/VisView.js | 4 +++- .../data_explorer/components/Visualization.js | 3 +++ ui/src/shared/components/ResizeContainer.js | 22 ++++++++++++++----- 4 files changed, 28 insertions(+), 12 deletions(-) diff --git a/ui/src/data_explorer/components/Table.js b/ui/src/data_explorer/components/Table.js index 4f13a53e53..72538180f6 100644 --- a/ui/src/data_explorer/components/Table.js +++ b/ui/src/data_explorer/components/Table.js @@ -12,7 +12,6 @@ import {Table, Column, Cell} from 'fixed-data-table' const {arrayOf, bool, func, number, oneOfType, shape, string} = PropTypes -const defaultTableHeight = 1000 const emptySeries = {columns: [], values: []} const CustomCell = React.createClass({ @@ -64,7 +63,7 @@ const ChronoTable = React.createClass({ getDefaultProps() { return { - height: defaultTableHeight, + height: 500, } }, @@ -139,11 +138,11 @@ const ChronoTable = React.createClass({ const maximumTabsCount = 11 // adjust height to proper value by subtracting the heights of the UI around it // tab height, graph-container vertical padding, graph-heading height, multitable-header height - const stylePixelOffset = 136 - const rowHeight = 34 - const defaultColumnWidth = 200 - const headerHeight = 30 const minWidth = 70 + const rowHeight = 34 + const headerHeight = 30 + const stylePixelOffset = 130 + const defaultColumnWidth = 200 const styleAdjustedHeight = height - stylePixelOffset const width = columns && columns.length > 1 ? defaultColumnWidth : containerWidth diff --git a/ui/src/data_explorer/components/VisView.js b/ui/src/data_explorer/components/VisView.js index 9e717c6f14..1862405294 100644 --- a/ui/src/data_explorer/components/VisView.js +++ b/ui/src/data_explorer/components/VisView.js @@ -13,6 +13,7 @@ const VisView = ({ heightPixels, editQueryStatus, activeQueryIndex, + resizerBottomHeight, }) => { const activeQuery = queries[activeQueryIndex] const defaultQuery = queries[0] @@ -30,7 +31,7 @@ const VisView = ({ return (
    ) @@ -60,6 +61,7 @@ VisView.propTypes = { heightPixels: number, editQueryStatus: func.isRequired, activeQueryIndex: number, + resizerBottomHeight: number, } export default VisView diff --git a/ui/src/data_explorer/components/Visualization.js b/ui/src/data_explorer/components/Visualization.js index b33b99a7b3..e9cb86a4a0 100644 --- a/ui/src/data_explorer/components/Visualization.js +++ b/ui/src/data_explorer/components/Visualization.js @@ -31,6 +31,7 @@ const Visualization = React.createClass({ bounds: arrayOf(string), }), }), + resizerBottomHeight: number, }, contextTypes: { @@ -95,6 +96,7 @@ const Visualization = React.createClass({ editQueryStatus, activeQueryIndex, isInDataExplorer, + resizerBottomHeight, } = this.props const {source: {links: {proxy}}} = this.context const {view} = this.state @@ -134,6 +136,7 @@ const Visualization = React.createClass({ editQueryStatus={editQueryStatus} activeQueryIndex={activeQueryIndex} isInDataExplorer={isInDataExplorer} + resizerBottomHeight={resizerBottomHeight} /> diff --git a/ui/src/shared/components/ResizeContainer.js b/ui/src/shared/components/ResizeContainer.js index cb3fb19baf..a8baab69e7 100644 --- a/ui/src/shared/components/ResizeContainer.js +++ b/ui/src/shared/components/ResizeContainer.js @@ -31,6 +31,12 @@ class ResizeContainer extends Component { initialBottomHeight: defaultInitialBottomHeight, } + componentDidMount() { + this.setState({ + bottomHeightPixels: this.bottom.getBoundingClientRect().height, + }) + } + handleStartDrag() { this.setState({isDragging: true}) } @@ -51,7 +57,7 @@ class ResizeContainer extends Component { const {minTopHeight, minBottomHeight} = this.props const oneHundred = 100 const containerHeight = parseInt( - getComputedStyle(this.refs.resizeContainer).height, + getComputedStyle(this.resizeContainer).height, 10 ) // verticalOffset moves the resize handle as many pixels as the page-heading is taking up. @@ -85,11 +91,12 @@ class ResizeContainer extends Component { this.setState({ topHeight: `${newTopPanelPercent}%`, bottomHeight: `${newBottomPanelPercent}%`, + bottomHeightPixels, }) } render() { - const {topHeight, bottomHeight, isDragging} = this.state + const {bottomHeightPixels, topHeight, bottomHeight, isDragging} = this.state const {containerClass, children} = this.props if (React.Children.count(children) > maximumNumChildren) { @@ -107,10 +114,12 @@ class ResizeContainer extends Component { onMouseLeave={this.handleMouseLeave} onMouseUp={this.handleStopDrag} onMouseMove={this.handleDrag} - ref="resizeContainer" + ref={r => (this.resizeContainer = r)} >
    - {React.cloneElement(children[0])} + {React.cloneElement(children[0], { + resizerBottomHeight: bottomHeightPixels, + })}
    (this.bottom = r)} > - {React.cloneElement(children[1])} + {React.cloneElement(children[1], { + resizerBottomHeight: bottomHeightPixels, + })} ) From eae327f25da37075e5581771eb7f7da3be5bf58f Mon Sep 17 00:00:00 2001 From: Andrew Watkins Date: Wed, 9 Aug 2017 11:29:25 -0700 Subject: [PATCH 16/62] Pixel perfect --- ui/src/data_explorer/components/Table.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/src/data_explorer/components/Table.js b/ui/src/data_explorer/components/Table.js index 72538180f6..b2dbb5574a 100644 --- a/ui/src/data_explorer/components/Table.js +++ b/ui/src/data_explorer/components/Table.js @@ -141,7 +141,7 @@ const ChronoTable = React.createClass({ const minWidth = 70 const rowHeight = 34 const headerHeight = 30 - const stylePixelOffset = 130 + const stylePixelOffset = 125 const defaultColumnWidth = 200 const styleAdjustedHeight = height - stylePixelOffset const width = From 718c1085a9b2add1bc4b5e655a1f71387514f4aa Mon Sep 17 00:00:00 2001 From: Andrew Watkins Date: Wed, 9 Aug 2017 12:35:06 -0700 Subject: [PATCH 17/62] Update CHANGELOG --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9c266631d2..9e0e1cb1e5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,6 @@ ## v1.3.7.0 [unreleased] ### Bug Fixes +1. [#1845](https://github.com/influxdata/chronograf/pull/1845): Fix no-scroll bar appearing in the Data Explorer table ### Features From 27c9e0a4e4993de2740b7442b3cc1ecb072add20 Mon Sep 17 00:00:00 2001 From: Andrew Watkins Date: Wed, 9 Aug 2017 13:23:08 -0700 Subject: [PATCH 18/62] Pass editQueryStatus action creator --- ui/src/data_explorer/components/VisView.js | 1 + ui/src/shared/components/RefreshingGraph.js | 15 +++++++++------ 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/ui/src/data_explorer/components/VisView.js b/ui/src/data_explorer/components/VisView.js index 1862405294..1f9ddcdb12 100644 --- a/ui/src/data_explorer/components/VisView.js +++ b/ui/src/data_explorer/components/VisView.js @@ -45,6 +45,7 @@ const VisView = ({ templates={templates} cellHeight={heightPixels} autoRefresh={autoRefresh} + editQueryStatus={editQueryStatus} /> ) } diff --git a/ui/src/shared/components/RefreshingGraph.js b/ui/src/shared/components/RefreshingGraph.js index 56e43857ff..e70ff5b5f1 100644 --- a/ui/src/shared/components/RefreshingGraph.js +++ b/ui/src/shared/components/RefreshingGraph.js @@ -8,14 +8,15 @@ const RefreshingLineGraph = AutoRefresh(LineGraph) const RefreshingSingleStat = AutoRefresh(SingleStat) const RefreshingGraph = ({ - timeRange, - autoRefresh, - templates, - synchronizer, + axes, type, queries, + templates, + timeRange, cellHeight, - axes, + autoRefresh, + synchronizer, + editQueryStatus, }) => { if (type === 'single-stat') { return ( @@ -43,6 +44,7 @@ const RefreshingGraph = ({ isBarGraph={type === 'bar'} displayOptions={displayOptions} synchronizer={synchronizer} + editQueryStatus={editQueryStatus} axes={axes} /> ) @@ -58,9 +60,10 @@ RefreshingGraph.propTypes = { templates: arrayOf(shape()), synchronizer: func, type: string.isRequired, - queries: arrayOf(shape()).isRequired, cellHeight: number, axes: shape(), + queries: arrayOf(shape()).isRequired, + editQueryStatus: func, } export default RefreshingGraph From 1a8d2dd625c5c1c53abbc2bfb07b8eee83e544bd Mon Sep 17 00:00:00 2001 From: Andrew Watkins Date: Wed, 9 Aug 2017 16:25:07 -0700 Subject: [PATCH 19/62] Update DataExplorer to extend Component --- .../data_explorer/containers/DataExplorer.js | 113 +++++++++--------- 1 file changed, 58 insertions(+), 55 deletions(-) diff --git a/ui/src/data_explorer/containers/DataExplorer.js b/ui/src/data_explorer/containers/DataExplorer.js index 5ec4ee93f8..588384aa5e 100644 --- a/ui/src/data_explorer/containers/DataExplorer.js +++ b/ui/src/data_explorer/containers/DataExplorer.js @@ -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,29 @@ 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) + } render() { const { @@ -89,6 +54,7 @@ const DataExplorer = React.createClass({ source, writeLineProtocol, } = this.props + const {activeQueryIndex, showWriteForm} = this.state const selectedDatabase = _.get( queryConfigs, @@ -129,8 +95,8 @@ const DataExplorer = React.createClass({ autoRefresh={autoRefresh} timeRange={timeRange} isInDataExplorer={true} - setActiveQueryIndex={this.handleSetActiveQueryIndex} - onDeleteQuery={this.handleDeleteQuery} + setActiveQueryIndex={::this.handleSetActiveQueryIndex} + onDeleteQuery={::this.handleDeleteQuery} activeQueryIndex={activeQueryIndex} /> ) - }, -}) + } +} -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 +168,7 @@ function mapStateToProps(state) { } } -function mapDispatchToProps(dispatch) { +const mapDispatchToProps = dispatch => { return { handleChooseAutoRefresh: bindActionCreators(setAutoRefresh, dispatch), errorThrownAction: bindActionCreators(errorThrown, dispatch), From a61406b7799b726b3259505fdc519f7e10a43230 Mon Sep 17 00:00:00 2001 From: Andrew Watkins Date: Wed, 9 Aug 2017 16:36:59 -0700 Subject: [PATCH 20/62] Remove table logic from dashboard visualization --- .../components/CellEditorOverlay.js | 2 +- ui/src/dashboards/components/Visualization.js | 112 ++++++++++++++++++ 2 files changed, 113 insertions(+), 1 deletion(-) create mode 100644 ui/src/dashboards/components/Visualization.js diff --git a/ui/src/dashboards/components/CellEditorOverlay.js b/ui/src/dashboards/components/CellEditorOverlay.js index 57ce0d3492..4c2241360c 100644 --- a/ui/src/dashboards/components/CellEditorOverlay.js +++ b/ui/src/dashboards/components/CellEditorOverlay.js @@ -5,7 +5,7 @@ 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 Visualization from 'src/dashboards/components/Visualization' import OverlayControls from 'src/dashboards/components/OverlayControls' import DisplayOptions from 'src/dashboards/components/DisplayOptions' diff --git a/ui/src/dashboards/components/Visualization.js b/ui/src/dashboards/components/Visualization.js new file mode 100644 index 0000000000..267b8a5a9f --- /dev/null +++ b/ui/src/dashboards/components/Visualization.js @@ -0,0 +1,112 @@ +import React, {PropTypes} from 'react' +import buildInfluxQLQuery from 'utils/influxql' +import VisHeader from 'src/data_explorer/components/VisHeader' +import VisView from 'src/data_explorer/components/VisView' +import {GRAPH} from 'shared/constants' +import _ from 'lodash' + +const {arrayOf, bool, func, number, shape, string} = PropTypes + +const Visualization = React.createClass({ + propTypes: { + cellName: string, + cellType: string, + autoRefresh: number.isRequired, + templates: arrayOf(shape()), + isInDataExplorer: bool, + timeRange: shape({ + upper: string, + lower: string, + }).isRequired, + queryConfigs: arrayOf(shape({})).isRequired, + activeQueryIndex: number, + height: string, + heightPixels: number, + editQueryStatus: func.isRequired, + views: arrayOf(string).isRequired, + axes: shape({ + y: shape({ + bounds: arrayOf(string), + }), + }), + resizerBottomHeight: number, + }, + + contextTypes: { + source: PropTypes.shape({ + links: PropTypes.shape({ + proxy: PropTypes.string.isRequired, + }).isRequired, + }).isRequired, + }, + + getDefaultProps() { + return { + cellName: '', + cellType: '', + } + }, + + render() { + const { + axes, + views, + height, + cellType, + cellName, + timeRange, + templates, + autoRefresh, + heightPixels, + queryConfigs, + editQueryStatus, + activeQueryIndex, + isInDataExplorer, + resizerBottomHeight, + } = this.props + + const {source: {links: {proxy}}} = this.context + 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 ( +
    + +
    + +
    +
    + ) + }, + + getQueryText(queryConfigs, index) { + // rawText can be null + return _.get(queryConfigs, [`${index}`, 'rawText'], '') || '' + }, +}) + +export default Visualization From 538dda35410c4c0d3fd294f718a8eaf180a01506 Mon Sep 17 00:00:00 2001 From: Andrew Watkins Date: Wed, 9 Aug 2017 16:48:50 -0700 Subject: [PATCH 21/62] Move logic from render and remove VisView --- ui/src/dashboards/components/Visualization.js | 71 ++++++++----------- 1 file changed, 30 insertions(+), 41 deletions(-) diff --git a/ui/src/dashboards/components/Visualization.js b/ui/src/dashboards/components/Visualization.js index 267b8a5a9f..c6de4ab0ad 100644 --- a/ui/src/dashboards/components/Visualization.js +++ b/ui/src/dashboards/components/Visualization.js @@ -1,35 +1,29 @@ import React, {PropTypes} from 'react' +import RefreshingGraph from 'shared/components/RefreshingGraph' import buildInfluxQLQuery from 'utils/influxql' -import VisHeader from 'src/data_explorer/components/VisHeader' -import VisView from 'src/data_explorer/components/VisView' -import {GRAPH} from 'shared/constants' import _ from 'lodash' -const {arrayOf, bool, func, number, shape, string} = PropTypes +const {arrayOf, func, number, shape, string} = PropTypes -const Visualization = React.createClass({ +const DashVisualization = React.createClass({ propTypes: { cellName: string, cellType: string, autoRefresh: number.isRequired, templates: arrayOf(shape()), - isInDataExplorer: bool, timeRange: shape({ upper: string, lower: string, }).isRequired, queryConfigs: arrayOf(shape({})).isRequired, - activeQueryIndex: number, height: string, heightPixels: number, editQueryStatus: func.isRequired, - views: arrayOf(string).isRequired, axes: shape({ y: shape({ bounds: arrayOf(string), }), }), - resizerBottomHeight: number, }, contextTypes: { @@ -50,22 +44,41 @@ const Visualization = React.createClass({ render() { const { axes, - views, height, cellType, cellName, - timeRange, templates, autoRefresh, heightPixels, - queryConfigs, editQueryStatus, - activeQueryIndex, - isInDataExplorer, - resizerBottomHeight, } = this.props + return ( +
    +
    +
    + {cellName} +
    +
    +
    + +
    +
    + ) + }, + + buildQueries() { const {source: {links: {proxy}}} = this.context + const {queryConfigs, timeRange} = this.props + const statements = queryConfigs.map(query => { const text = query.rawText || buildInfluxQLQuery(query.range || timeRange, query) @@ -76,31 +89,7 @@ const Visualization = React.createClass({ return {host: [proxy], text: s.text, id: s.id, queryConfig: s.queryConfig} }) - return ( -
    - -
    - -
    -
    - ) + return queries }, getQueryText(queryConfigs, index) { @@ -109,4 +98,4 @@ const Visualization = React.createClass({ }, }) -export default Visualization +export default DashVisualization From abcf8976261364f849ae90903c8aaaae2ac7d73f Mon Sep 17 00:00:00 2001 From: Andrew Watkins Date: Wed, 9 Aug 2017 17:17:45 -0700 Subject: [PATCH 22/62] Turn DashVisualization stateless --- ui/src/dashboards/components/Visualization.js | 155 ++++++++---------- ui/src/utils/buildQueriesForGraphs.js | 17 ++ 2 files changed, 81 insertions(+), 91 deletions(-) create mode 100644 ui/src/utils/buildQueriesForGraphs.js diff --git a/ui/src/dashboards/components/Visualization.js b/ui/src/dashboards/components/Visualization.js index c6de4ab0ad..2ff3275c20 100644 --- a/ui/src/dashboards/components/Visualization.js +++ b/ui/src/dashboards/components/Visualization.js @@ -1,101 +1,74 @@ import React, {PropTypes} from 'react' import RefreshingGraph from 'shared/components/RefreshingGraph' -import buildInfluxQLQuery from 'utils/influxql' -import _ from 'lodash' +import buildQueries from 'utils/buildQueriesForGraphs' + +const DashVisualization = ( + { + axes, + height, + cellType, + cellName, + templates, + timeRange, + autoRefresh, + queryConfigs, + heightPixels, + editQueryStatus, + }, + {source: {links: {proxy}}} +) => +
    +
    +
    + {cellName} +
    +
    +
    + +
    +
    const {arrayOf, func, number, shape, string} = PropTypes -const DashVisualization = React.createClass({ - propTypes: { - cellName: string, - cellType: string, - autoRefresh: number.isRequired, - templates: arrayOf(shape()), - timeRange: shape({ - upper: string, - lower: string, - }).isRequired, - queryConfigs: arrayOf(shape({})).isRequired, - height: string, - heightPixels: number, - editQueryStatus: func.isRequired, - axes: shape({ - y: shape({ - bounds: arrayOf(string), - }), +DashVisualization.defaultProps = { + cellName: '', + cellType: '', +} + +DashVisualization.propTypes = { + cellName: string, + cellType: string, + autoRefresh: number.isRequired, + templates: arrayOf(shape()), + timeRange: shape({ + upper: string, + lower: string, + }).isRequired, + queryConfigs: arrayOf(shape({})).isRequired, + height: string, + heightPixels: number, + editQueryStatus: func.isRequired, + axes: shape({ + y: shape({ + bounds: arrayOf(string), }), - }, + }), +} - contextTypes: { - source: PropTypes.shape({ - links: PropTypes.shape({ - proxy: PropTypes.string.isRequired, - }).isRequired, +DashVisualization.contextTypes = { + source: PropTypes.shape({ + links: PropTypes.shape({ + proxy: PropTypes.string.isRequired, }).isRequired, - }, - - getDefaultProps() { - return { - cellName: '', - cellType: '', - } - }, - - render() { - const { - axes, - height, - cellType, - cellName, - templates, - autoRefresh, - heightPixels, - editQueryStatus, - } = this.props - - return ( -
    -
    -
    - {cellName} -
    -
    -
    - -
    -
    - ) - }, - - buildQueries() { - const {source: {links: {proxy}}} = this.context - const {queryConfigs, timeRange} = this.props - - 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 - }, - - getQueryText(queryConfigs, index) { - // rawText can be null - return _.get(queryConfigs, [`${index}`, 'rawText'], '') || '' - }, -}) + }).isRequired, +} export default DashVisualization diff --git a/ui/src/utils/buildQueriesForGraphs.js b/ui/src/utils/buildQueriesForGraphs.js new file mode 100644 index 0000000000..39ff302e5f --- /dev/null +++ b/ui/src/utils/buildQueriesForGraphs.js @@ -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 From 3dbdc5e0950d1c95b1f70440d8bf4540fb0ede01 Mon Sep 17 00:00:00 2001 From: Andrew Watkins Date: Thu, 10 Aug 2017 12:50:12 -0700 Subject: [PATCH 23/62] Remove unused props and simplify prop names --- .../components/CellEditorOverlay.js | 10 ++++---- ui/src/dashboards/components/Visualization.js | 23 ++++++++----------- 2 files changed, 13 insertions(+), 20 deletions(-) diff --git a/ui/src/dashboards/components/CellEditorOverlay.js b/ui/src/dashboards/components/CellEditorOverlay.js index 4c2241360c..d19bbaaef5 100644 --- a/ui/src/dashboards/components/CellEditorOverlay.js +++ b/ui/src/dashboards/components/CellEditorOverlay.js @@ -222,16 +222,14 @@ class CellEditorOverlay extends Component { initialBottomHeight={INITIAL_HEIGHTS.queryMaker} >
    -
    +
    - {cellName} + {name}
    @@ -39,13 +36,13 @@ const DashVisualization = ( const {arrayOf, func, number, shape, string} = PropTypes DashVisualization.defaultProps = { - cellName: '', - cellType: '', + name: '', + type: '', } DashVisualization.propTypes = { - cellName: string, - cellType: string, + name: string, + type: string, autoRefresh: number.isRequired, templates: arrayOf(shape()), timeRange: shape({ @@ -53,8 +50,6 @@ DashVisualization.propTypes = { lower: string, }).isRequired, queryConfigs: arrayOf(shape({})).isRequired, - height: string, - heightPixels: number, editQueryStatus: func.isRequired, axes: shape({ y: shape({ From 8dbd407107c9c1c4e59068d2b3dc413707199b2a Mon Sep 17 00:00:00 2001 From: Andrew Watkins Date: Thu, 10 Aug 2017 13:08:43 -0700 Subject: [PATCH 24/62] WIP moving query maker to sep component --- .../components/CellEditorOverlay.js | 2 +- ui/src/dashboards/components/QueryMaker.js | 181 ++++++++++++++++++ 2 files changed, 182 insertions(+), 1 deletion(-) create mode 100644 ui/src/dashboards/components/QueryMaker.js diff --git a/ui/src/dashboards/components/CellEditorOverlay.js b/ui/src/dashboards/components/CellEditorOverlay.js index d19bbaaef5..41999b23eb 100644 --- a/ui/src/dashboards/components/CellEditorOverlay.js +++ b/ui/src/dashboards/components/CellEditorOverlay.js @@ -4,7 +4,7 @@ import _ from 'lodash' import uuid from 'node-uuid' import ResizeContainer from 'shared/components/ResizeContainer' -import QueryMaker from 'src/data_explorer/components/QueryMaker' +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' diff --git a/ui/src/dashboards/components/QueryMaker.js b/ui/src/dashboards/components/QueryMaker.js new file mode 100644 index 0000000000..2ead74a064 --- /dev/null +++ b/ui/src/dashboards/components/QueryMaker.js @@ -0,0 +1,181 @@ +import React, {PropTypes} from 'react' + +import QueryBuilder from 'src/data_explorer/components/QueryBuilder' +import QueryMakerTab from 'src/data_explorer/components/QueryMakerTab' +import buildInfluxQLQuery from 'utils/influxql' +import classnames from 'classnames' + +const {arrayOf, bool, func, node, number, shape, string} = PropTypes + +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, + }, + + 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 ( +
    + {this.renderQueryTabList()} + {this.renderQueryBuilder()} +
    + ) + }, + + renderQueryBuilder() { + const { + timeRange, + actions, + source, + templates, + layout, + isInDataExplorer, + } = this.props + const query = this.getActiveQuery() + + if (!query) { + return ( +
    +
    This Graph has no Queries
    +
    +
    + Add a Query +
    +
    + ) + } + + // 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 + + return ( + + ) + }, + + renderQueryTabList() { + const { + queries, + activeQueryIndex, + onDeleteQuery, + timeRange, + setActiveQueryIndex, + } = this.props + + return ( +
    + {queries.map((q, i) => { + return ( + + ) + })} + {this.props.children} +
    + +
    +
    + ) + }, +}) + +QueryMaker.defaultProps = { + layout: 'default', +} +export default QueryMaker From 2276e9f7cb609db0aa229d0d2b1810edcb13b6a2 Mon Sep 17 00:00:00 2001 From: Andrew Watkins Date: Thu, 10 Aug 2017 14:19:33 -0700 Subject: [PATCH 25/62] Pull out tab list into separate component --- ui/src/dashboards/components/QueryMaker.js | 76 ++++++-------------- ui/src/dashboards/components/QueryTabList.js | 49 +++++++++++++ 2 files changed, 69 insertions(+), 56 deletions(-) create mode 100644 ui/src/dashboards/components/QueryTabList.js diff --git a/ui/src/dashboards/components/QueryMaker.js b/ui/src/dashboards/components/QueryMaker.js index 2ead74a064..5867b88633 100644 --- a/ui/src/dashboards/components/QueryMaker.js +++ b/ui/src/dashboards/components/QueryMaker.js @@ -1,8 +1,7 @@ import React, {PropTypes} from 'react' import QueryBuilder from 'src/data_explorer/components/QueryBuilder' -import QueryMakerTab from 'src/data_explorer/components/QueryMakerTab' -import buildInfluxQLQuery from 'utils/influxql' +import QueryTabList from 'src/dashboards/components/QueryTabList' import classnames from 'classnames' const {arrayOf, bool, func, node, number, shape, string} = PropTypes @@ -67,7 +66,17 @@ const QueryMaker = React.createClass({ }, render() { - const {height, top, layout} = this.props + const { + height, + top, + layout, + queries, + timeRange, + onDeleteQuery, + activeQueryIndex, + setActiveQueryIndex, + } = this.props + return (
    - {this.renderQueryTabList()} + {this.renderQueryBuilder()}
    ) @@ -108,19 +124,6 @@ const QueryMaker = React.createClass({ ) } - // 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 - return ( ) }, - - renderQueryTabList() { - const { - queries, - activeQueryIndex, - onDeleteQuery, - timeRange, - setActiveQueryIndex, - } = this.props - - return ( -
    - {queries.map((q, i) => { - return ( - - ) - })} - {this.props.children} -
    - -
    -
    - ) - }, }) QueryMaker.defaultProps = { diff --git a/ui/src/dashboards/components/QueryTabList.js b/ui/src/dashboards/components/QueryTabList.js new file mode 100644 index 0000000000..49d7225764 --- /dev/null +++ b/ui/src/dashboards/components/QueryTabList.js @@ -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, +}) => +
    + {queries.map((q, i) => + + )} +
    + +
    +
    + +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 From 160fa8663e4e6f68779187ef2db71fe65f53033d Mon Sep 17 00:00:00 2001 From: Andrew Watkins Date: Thu, 10 Aug 2017 14:26:43 -0700 Subject: [PATCH 26/62] Remove unused packages and props --- ui/src/dashboards/components/QueryMaker.js | 16 +++------------- 1 file changed, 3 insertions(+), 13 deletions(-) diff --git a/ui/src/dashboards/components/QueryMaker.js b/ui/src/dashboards/components/QueryMaker.js index 5867b88633..b77b762454 100644 --- a/ui/src/dashboards/components/QueryMaker.js +++ b/ui/src/dashboards/components/QueryMaker.js @@ -2,7 +2,6 @@ import React, {PropTypes} from 'react' import QueryBuilder from 'src/data_explorer/components/QueryBuilder' import QueryTabList from 'src/dashboards/components/QueryTabList' -import classnames from 'classnames' const {arrayOf, bool, func, node, number, shape, string} = PropTypes @@ -36,8 +35,6 @@ const QueryMaker = React.createClass({ applyFuncsToField: func.isRequired, editRawTextAsync: func.isRequired, }).isRequired, - height: string, - top: string, setActiveQueryIndex: func.isRequired, onDeleteQuery: func.isRequired, activeQueryIndex: number, @@ -67,9 +64,6 @@ const QueryMaker = React.createClass({ render() { const { - height, - top, - layout, queries, timeRange, onDeleteQuery, @@ -78,12 +72,7 @@ const QueryMaker = React.createClass({ } = this.props return ( -
    +
    Date: Thu, 10 Aug 2017 14:35:29 -0700 Subject: [PATCH 27/62] Move empty state to separate component --- ui/src/dashboards/components/EmptyQuery.js | 18 +++++++ ui/src/dashboards/components/QueryMaker.js | 60 +++++++--------------- 2 files changed, 36 insertions(+), 42 deletions(-) create mode 100644 ui/src/dashboards/components/EmptyQuery.js diff --git a/ui/src/dashboards/components/EmptyQuery.js b/ui/src/dashboards/components/EmptyQuery.js new file mode 100644 index 0000000000..e37b9cf72b --- /dev/null +++ b/ui/src/dashboards/components/EmptyQuery.js @@ -0,0 +1,18 @@ +import React, {PropTypes} from 'react' + +const EmptyQueryState = ({onAddQuery}) => +
    +
    This Graph has no Queries
    +
    +
    + Add a Query +
    +
    + +const {func} = PropTypes + +EmptyQueryState.propTypes = { + onAddQuery: func.isRequired, +} + +export default EmptyQueryState diff --git a/ui/src/dashboards/components/QueryMaker.js b/ui/src/dashboards/components/QueryMaker.js index b77b762454..efca3807dc 100644 --- a/ui/src/dashboards/components/QueryMaker.js +++ b/ui/src/dashboards/components/QueryMaker.js @@ -2,6 +2,7 @@ import React, {PropTypes} from 'react' import QueryBuilder from 'src/data_explorer/components/QueryBuilder' import QueryTabList from 'src/dashboards/components/QueryTabList' +import EmptyQuery from 'src/dashboards/components/EmptyQuery' const {arrayOf, bool, func, node, number, shape, string} = PropTypes @@ -64,13 +65,19 @@ const QueryMaker = React.createClass({ render() { const { + layout, + source, + actions, queries, timeRange, + templates, onDeleteQuery, activeQueryIndex, setActiveQueryIndex, } = this.props + const query = this.getActiveQuery() + return (
    - {this.renderQueryBuilder()} + {query + ? + : }
    ) }, - - renderQueryBuilder() { - const { - timeRange, - actions, - source, - templates, - isInDataExplorer, - layout, - } = this.props - const query = this.getActiveQuery() - - if (!query) { - return ( -
    -
    This Graph has no Queries
    -
    -
    - Add a Query -
    -
    - ) - } - - return ( - - ) - }, }) QueryMaker.defaultProps = { From dbf7702fb91a1ab97302bb49fdf76c79f234912d Mon Sep 17 00:00:00 2001 From: Andrew Watkins Date: Thu, 10 Aug 2017 14:42:52 -0700 Subject: [PATCH 28/62] Make QueryMaker an SFC --- ui/src/dashboards/components/QueryMaker.js | 171 ++++++++++----------- 1 file changed, 80 insertions(+), 91 deletions(-) diff --git a/ui/src/dashboards/components/QueryMaker.js b/ui/src/dashboards/components/QueryMaker.js index efca3807dc..d7660905e7 100644 --- a/ui/src/dashboards/components/QueryMaker.js +++ b/ui/src/dashboards/components/QueryMaker.js @@ -4,105 +4,94 @@ import QueryBuilder from 'src/data_explorer/components/QueryBuilder' import QueryTabList from 'src/dashboards/components/QueryTabList' import EmptyQuery from 'src/dashboards/components/EmptyQuery' -const {arrayOf, bool, func, node, number, shape, string} = PropTypes +const QueryMaker = ({ + layout, + source, + actions, + queries, + timeRange, + templates, + onDeleteQuery, + activeQueryIndex, + setActiveQueryIndex, +}) => { + const handleAddQuery = () => { + const newIndex = queries.length + actions.addQuery() + setActiveQueryIndex(newIndex) + } -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, - setActiveQueryIndex: func.isRequired, - onDeleteQuery: func.isRequired, - activeQueryIndex: number, - children: node, - layout: string, - }, - - 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 getActiveQuery = () => { const activeQuery = queries[activeQueryIndex] const defaultQuery = queries[0] return activeQuery || defaultQuery - }, + } - render() { - const { - layout, - source, - actions, - queries, - timeRange, - templates, - onDeleteQuery, - activeQueryIndex, - setActiveQueryIndex, - } = this.props + const query = getActiveQuery() - const query = this.getActiveQuery() + return ( +
    + + {query + ? + : } +
    + ) +} - return ( -
    - - {query - ? - : } -
    - ) - }, -}) +const {arrayOf, bool, func, node, 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, + 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, + setActiveQueryIndex: func.isRequired, + onDeleteQuery: func.isRequired, + activeQueryIndex: number, + children: node, + layout: string, +} QueryMaker.defaultProps = { layout: 'default', From fac077914b1cfe67a023d56f6cf83abad9c0fd12 Mon Sep 17 00:00:00 2001 From: Andrew Watkins Date: Thu, 10 Aug 2017 15:38:44 -0700 Subject: [PATCH 29/62] Move addQuery logic to container --- ui/src/dashboards/components/CellEditorOverlay.js | 13 ++++++++----- ui/src/dashboards/components/QueryMaker.js | 13 ++++--------- 2 files changed, 12 insertions(+), 14 deletions(-) diff --git a/ui/src/dashboards/components/CellEditorOverlay.js b/ui/src/dashboards/components/CellEditorOverlay.js index 41999b23eb..2104da4db6 100644 --- a/ui/src/dashboards/components/CellEditorOverlay.js +++ b/ui/src/dashboards/components/CellEditorOverlay.js @@ -111,10 +111,14 @@ class CellEditorOverlay extends Component { e.preventDefault() } - handleAddQuery(options) { - const newQuery = Object.assign({}, defaultQueryConfig(uuid.v4()), options) - const nextQueries = this.state.queriesWorkingDraft.concat(newQuery) + handleAddQuery() { + const {queriesWorkingDraft} = this.state + const newIndex = queriesWorkingDraft.length + const newQuery = {...defaultQueryConfig(uuid.v4())} + const nextQueries = queriesWorkingDraft.concat(newQuery) + this.setState({queriesWorkingDraft: nextQueries}) + this.handleSetActiveQueryIndex(newIndex) } handleDeleteQuery(index) { @@ -203,7 +207,6 @@ class CellEditorOverlay extends Component { } = this.state const queryActions = { - addQuery: this.handleAddQuery, editRawTextAsync: this.handleEditRawText, ..._.mapValues(queryModifiers, qm => this.queryStateReducer(qm)), } @@ -254,8 +257,8 @@ class CellEditorOverlay extends Component { actions={queryActions} autoRefresh={autoRefresh} timeRange={timeRange} - setActiveQueryIndex={this.handleSetActiveQueryIndex} onDeleteQuery={this.handleDeleteQuery} + onAddQuery={this.handleAddQuery} activeQueryIndex={activeQueryIndex} />}
    diff --git a/ui/src/dashboards/components/QueryMaker.js b/ui/src/dashboards/components/QueryMaker.js index d7660905e7..167fb5fe50 100644 --- a/ui/src/dashboards/components/QueryMaker.js +++ b/ui/src/dashboards/components/QueryMaker.js @@ -11,16 +11,11 @@ const QueryMaker = ({ queries, timeRange, templates, + onAddQuery, onDeleteQuery, activeQueryIndex, setActiveQueryIndex, }) => { - const handleAddQuery = () => { - const newIndex = queries.length - actions.addQuery() - setActiveQueryIndex(newIndex) - } - const getActiveQuery = () => { const activeQuery = queries[activeQueryIndex] const defaultQuery = queries[0] @@ -35,7 +30,7 @@ const QueryMaker = ({ - : } + : }
    ) } From 3a153ea1d6d572dbc1728616ccd163290af5d760 Mon Sep 17 00:00:00 2001 From: Andrew Watkins Date: Thu, 10 Aug 2017 15:48:07 -0700 Subject: [PATCH 30/62] Move active query logic to container --- .../components/CellEditorOverlay.js | 10 +++++++ ui/src/dashboards/components/QueryMaker.js | 28 +++++-------------- 2 files changed, 17 insertions(+), 21 deletions(-) diff --git a/ui/src/dashboards/components/CellEditorOverlay.js b/ui/src/dashboards/components/CellEditorOverlay.js index 2104da4db6..f15e95c91d 100644 --- a/ui/src/dashboards/components/CellEditorOverlay.js +++ b/ui/src/dashboards/components/CellEditorOverlay.js @@ -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 @@ -171,6 +172,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) @@ -260,6 +269,7 @@ class CellEditorOverlay extends Component { onDeleteQuery={this.handleDeleteQuery} onAddQuery={this.handleAddQuery} activeQueryIndex={activeQueryIndex} + activeQuery={this.getActiveQuery()} />}
    diff --git a/ui/src/dashboards/components/QueryMaker.js b/ui/src/dashboards/components/QueryMaker.js index 167fb5fe50..a4b89b7e11 100644 --- a/ui/src/dashboards/components/QueryMaker.js +++ b/ui/src/dashboards/components/QueryMaker.js @@ -5,26 +5,17 @@ import QueryTabList from 'src/dashboards/components/QueryTabList' import EmptyQuery from 'src/dashboards/components/EmptyQuery' const QueryMaker = ({ - layout, source, actions, queries, timeRange, templates, onAddQuery, + activeQuery, onDeleteQuery, activeQueryIndex, setActiveQueryIndex, }) => { - const getActiveQuery = () => { - const activeQuery = queries[activeQueryIndex] - const defaultQuery = queries[0] - - return activeQuery || defaultQuery - } - - const query = getActiveQuery() - return (
    - {query + {activeQuery ? Date: Thu, 10 Aug 2017 16:45:46 -0700 Subject: [PATCH 31/62] Make the QueryBuilder an SFC and rename it to SchemaExplorer --- ui/src/dashboards/components/QueryMaker.js | 8 +- .../dashboards/components/SchemaExplorer.js | 94 +++++++++++++++++++ 2 files changed, 97 insertions(+), 5 deletions(-) create mode 100644 ui/src/dashboards/components/SchemaExplorer.js diff --git a/ui/src/dashboards/components/QueryMaker.js b/ui/src/dashboards/components/QueryMaker.js index a4b89b7e11..8e7dda73c2 100644 --- a/ui/src/dashboards/components/QueryMaker.js +++ b/ui/src/dashboards/components/QueryMaker.js @@ -1,8 +1,8 @@ import React, {PropTypes} from 'react' -import QueryBuilder from 'src/data_explorer/components/QueryBuilder' -import QueryTabList from 'src/dashboards/components/QueryTabList' import EmptyQuery from 'src/dashboards/components/EmptyQuery' +import QueryTabList from 'src/dashboards/components/QueryTabList' +import SchemaExplorer from 'src/dashboards/components/SchemaExplorer' const QueryMaker = ({ source, @@ -27,12 +27,10 @@ const QueryMaker = ({ setActiveQueryIndex={setActiveQueryIndex} /> {activeQuery - ? diff --git a/ui/src/dashboards/components/SchemaExplorer.js b/ui/src/dashboards/components/SchemaExplorer.js new file mode 100644 index 0000000000..6ed4820757 --- /dev/null +++ b/ui/src/dashboards/components/SchemaExplorer.js @@ -0,0 +1,94 @@ +import React, {PropTypes} from 'react' + +import DatabaseList from 'src/data_explorer/components/DatabaseList' +import MeasurementList from 'src/data_explorer/components/MeasurementList' +import FieldList from 'src/data_explorer/components/FieldList' +import QueryEditor from 'src/data_explorer/components/QueryEditor' +import buildInfluxQLQuery from 'utils/influxql' + +const TEMPLATE_RANGE = {upper: null, lower: ':dashboardTime:'} + +const actionBinder = (id, action) => item => action(id, item) + +const rawTextBinder = (links, id, action) => text => + action(links.queries, id, text) + +const SchemaExplorer = ({ + source: {links}, + query, + query: {id, range}, + templates, + actions: { + chooseTag, + groupByTag, + groupByTime, + chooseNamespace, + editRawTextAsync, + chooseMeasurement, + applyFuncsToField, + toggleTagAcceptance, + toggleFieldWithGroupByInterval, + }, +}) => +
    + +
    + + + +
    +
    + +const {arrayOf, func, shape, string} = PropTypes + +SchemaExplorer.propTypes = { + source: shape({ + links: shape({ + queries: string.isRequired, + }).isRequired, + }).isRequired, + query: shape({ + id: string, + }).isRequired, + templates: arrayOf( + shape({ + tempVar: 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 From b9ee0dece60db9f3e372eac6a43f1f2973b43c62 Mon Sep 17 00:00:00 2001 From: Andrew Watkins Date: Thu, 10 Aug 2017 17:10:38 -0700 Subject: [PATCH 32/62] Separate QueryEditor from SchemaExplorer --- ui/src/dashboards/components/QueryMaker.js | 33 ++++++--- .../dashboards/components/SchemaExplorer.js | 70 +++++-------------- 2 files changed, 44 insertions(+), 59 deletions(-) diff --git a/ui/src/dashboards/components/QueryMaker.js b/ui/src/dashboards/components/QueryMaker.js index 8e7dda73c2..cfe04ecad3 100644 --- a/ui/src/dashboards/components/QueryMaker.js +++ b/ui/src/dashboards/components/QueryMaker.js @@ -2,16 +2,26 @@ import React, {PropTypes} from 'react' import EmptyQuery from 'src/dashboards/components/EmptyQuery' import QueryTabList from 'src/dashboards/components/QueryTabList' +import QueryEditor from 'src/data_explorer/components/QueryEditor' import SchemaExplorer from 'src/dashboards/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 = (rawText, range, q) => + rawText || buildInfluxQLQuery(range || TEMPLATE_RANGE, q) || '' const QueryMaker = ({ - source, + source: {links}, actions, queries, timeRange, templates, onAddQuery, activeQuery, + activeQuery: {id, range, rawText}, onDeleteQuery, activeQueryIndex, setActiveQueryIndex, @@ -27,13 +37,20 @@ const QueryMaker = ({ setActiveQueryIndex={setActiveQueryIndex} /> {activeQuery - ? + ?
    + + +
    : }
    ) diff --git a/ui/src/dashboards/components/SchemaExplorer.js b/ui/src/dashboards/components/SchemaExplorer.js index 6ed4820757..1b9b6087af 100644 --- a/ui/src/dashboards/components/SchemaExplorer.js +++ b/ui/src/dashboards/components/SchemaExplorer.js @@ -3,81 +3,49 @@ import React, {PropTypes} from 'react' import DatabaseList from 'src/data_explorer/components/DatabaseList' import MeasurementList from 'src/data_explorer/components/MeasurementList' import FieldList from 'src/data_explorer/components/FieldList' -import QueryEditor from 'src/data_explorer/components/QueryEditor' -import buildInfluxQLQuery from 'utils/influxql' - -const TEMPLATE_RANGE = {upper: null, lower: ':dashboardTime:'} const actionBinder = (id, action) => item => action(id, item) -const rawTextBinder = (links, id, action) => text => - action(links.queries, id, text) - const SchemaExplorer = ({ - source: {links}, query, - query: {id, range}, - templates, + query: {id}, actions: { chooseTag, groupByTag, groupByTime, chooseNamespace, - editRawTextAsync, chooseMeasurement, applyFuncsToField, toggleTagAcceptance, toggleFieldWithGroupByInterval, }, }) => -
    - + + + -
    - - - -
    -const {arrayOf, func, shape, string} = PropTypes +const {func, shape, string} = PropTypes SchemaExplorer.propTypes = { - source: shape({ - links: shape({ - queries: string.isRequired, - }).isRequired, - }).isRequired, query: shape({ id: string, }).isRequired, - templates: arrayOf( - shape({ - tempVar: string.isRequired, - }) - ), actions: shape({ chooseNamespace: func.isRequired, chooseMeasurement: func.isRequired, From 8eca27a20ffa8b13775596379f285eac4216df7c Mon Sep 17 00:00:00 2001 From: Andrew Watkins Date: Thu, 10 Aug 2017 17:19:48 -0700 Subject: [PATCH 33/62] Rename QueryEditor to QueryTextArea --- ui/src/dashboards/components/QueryMaker.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/ui/src/dashboards/components/QueryMaker.js b/ui/src/dashboards/components/QueryMaker.js index cfe04ecad3..7de7511e6a 100644 --- a/ui/src/dashboards/components/QueryMaker.js +++ b/ui/src/dashboards/components/QueryMaker.js @@ -2,14 +2,13 @@ import React, {PropTypes} from 'react' import EmptyQuery from 'src/dashboards/components/EmptyQuery' import QueryTabList from 'src/dashboards/components/QueryTabList' -import QueryEditor from 'src/data_explorer/components/QueryEditor' +import QueryTextArea from 'src/dashboards/components/QueryTextArea' import SchemaExplorer from 'src/dashboards/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 = (rawText, range, q) => rawText || buildInfluxQLQuery(range || TEMPLATE_RANGE, q) || '' @@ -38,7 +37,7 @@ const QueryMaker = ({ /> {activeQuery ?
    - Date: Thu, 10 Aug 2017 17:26:38 -0700 Subject: [PATCH 34/62] Make QueryTextArea and QueryStatus components --- ui/src/dashboards/components/QueryStatus.js | 50 ++++ ui/src/dashboards/components/QueryTextArea.js | 263 ++++++++++++++++++ 2 files changed, 313 insertions(+) create mode 100644 ui/src/dashboards/components/QueryStatus.js create mode 100644 ui/src/dashboards/components/QueryTextArea.js diff --git a/ui/src/dashboards/components/QueryStatus.js b/ui/src/dashboards/components/QueryStatus.js new file mode 100644 index 0000000000..95b8c2052e --- /dev/null +++ b/ui/src/dashboards/components/QueryStatus.js @@ -0,0 +1,50 @@ +import React, {PropTypes} from 'react' +import LoadingDots from 'shared/components/LoadingDots' +import classnames from 'classnames' + +const QueryStatus = ({status}) => { + if (!status) { + return
    + } + + if (status.loading) { + return ( +
    + +
    + ) + } + + return ( +
    + + + {status.error || status.warn || status.success} + +
    + ) +} + +const {shape, string} = PropTypes + +QueryStatus.propTypes = { + status: shape({ + error: string, + success: string, + warn: string, + }), +} + +export default QueryStatus diff --git a/ui/src/dashboards/components/QueryTextArea.js b/ui/src/dashboards/components/QueryTextArea.js new file mode 100644 index 0000000000..fc07ebef7d --- /dev/null +++ b/ui/src/dashboards/components/QueryTextArea.js @@ -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 'src/dashboards/components/QueryStatus' + +import { + MATCH_INCOMPLETE_TEMPLATES, + applyMasks, + insertTempVar, + unMask, +} from 'src/dashboards/constants' + +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 + } + + 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 ( +
    +