Merge remote-tracking branch 'origin' into tempvars/select-default

pull/10616/head
Delmer Reed 2018-06-29 13:03:19 -04:00
commit d9da2e3c6f
94 changed files with 1410 additions and 1635 deletions

View File

@ -14,6 +14,8 @@
1. [#3671](https://github.com/influxdata/chronograf/pull/3671): Remove Snip functionality in hover legend
1. [#3659](https://github.com/influxdata/chronograf/pull/3659): Upgrade Data Explorer query text field with syntax highlighting and partial multi-line support
1. [#3663](https://github.com/influxdata/chronograf/pull/3663): Truncate message preview in Alert Rules table
1. [#3770](https://github.com/influxdata/chronograf/pull/3770): Improve performance of graph crosshairs
1. [#3790](https://github.com/influxdata/chronograf/pull/3790): Hide dashboard cell menu until mouse over cell
### Bug Fixes
@ -25,6 +27,7 @@
1. [#3733](https://github.com/influxdata/chronograf/pull/3733): Change arrows in table columns so that ascending sort points up and descending points down
1. [#3751](https://github.com/influxdata/chronograf/pull/3751): Fix crosshairs moving passed the edges of graphs
1. [#3759](https://github.com/influxdata/chronograf/pull/3759): Change y-axis options to have valid defaults
1. [#3793](https://github.com/influxdata/chronograf/pull/3793): Stop making requests for old sources after changing sources
## v1.5.0.0 [2018-05-15-RC]

View File

@ -184,16 +184,14 @@ type Template struct {
// Query retrieves a Response from a TimeSeries.
type Query struct {
Command string `json:"query"` // Command is the query itself
DB string `json:"db,omitempty"` // DB is optional and if empty will not be used.
RP string `json:"rp,omitempty"` // RP is a retention policy and optional; if empty will not be used.
Epoch string `json:"epoch,omitempty"` // Epoch is the time format for the return results
TemplateVars []TemplateVar `json:"tempVars,omitempty"` // TemplateVars are template variables to replace within an InfluxQL query
Wheres []string `json:"wheres,omitempty"` // Wheres restricts the query to certain attributes
GroupBys []string `json:"groupbys,omitempty"` // GroupBys collate the query by these tags
Resolution uint `json:"resolution,omitempty"` // Resolution is the available screen resolution to render query results
Label string `json:"label,omitempty"` // Label is the Y-Axis label for the data
Range *Range `json:"range,omitempty"` // Range is the default Y-Axis range for the data
Command string `json:"query"` // Command is the query itself
DB string `json:"db,omitempty"` // DB is optional and if empty will not be used.
RP string `json:"rp,omitempty"` // RP is a retention policy and optional; if empty will not be used.
Epoch string `json:"epoch,omitempty"` // Epoch is the time format for the return results
Wheres []string `json:"wheres,omitempty"` // Wheres restricts the query to certain attributes
GroupBys []string `json:"groupbys,omitempty"` // GroupBys collate the query by these tags
Label string `json:"label,omitempty"` // Label is the Y-Axis label for the data
Range *Range `json:"range,omitempty"` // Range is the default Y-Axis range for the data
}
// DashboardQuery includes state for the query builder. This is a transition
@ -212,7 +210,7 @@ type TemplateQuery struct {
Command string `json:"influxql"` // Command is the query itself
DB string `json:"db,omitempty"` // DB is optional and if empty will not be used.
RP string `json:"rp,omitempty"` // RP is a retention policy and optional; if empty will not be used.
Measurement string `json:"measurement"` // Measurement is the optinally selected measurement for the query
Measurement string `json:"measurement"` // Measurement is the optionally selected measurement for the query
TagKey string `json:"tagKey"` // TagKey is the optionally selected tag key for the query
FieldKey string `json:"fieldKey"` // FieldKey is the optionally selected field key for the query
}

View File

@ -10,7 +10,6 @@ import (
"net/http"
"net/url"
"strings"
"time"
"github.com/influxdata/chronograf"
)
@ -55,13 +54,6 @@ func (c *Client) query(u *url.URL, q chronograf.Query) (chronograf.Response, err
}
req.Header.Set("Content-Type", "application/json")
command := q.Command
// TODO(timraymond): move this upper Query() function
if len(q.TemplateVars) > 0 {
command, err = TemplateReplace(q.Command, q.TemplateVars, time.Now())
if err != nil {
return nil, err
}
}
logs := c.Logger.
WithField("component", "proxy").
WithField("host", req.Host).

View File

@ -277,18 +277,7 @@ func Test_Influx_HTTPS_InsecureSkipVerify(t *testing.T) {
called = false
q = ""
query = chronograf.Query{
Command: "select :field: from cpu",
TemplateVars: []chronograf.TemplateVar{
chronograf.TemplateVar{
Var: ":field:",
Values: []chronograf.TemplateValue{
{
Value: "usage_user",
Type: "fieldKey",
},
},
},
},
Command: `select "usage_user" from cpu`,
}
_, err = series.Query(ctx, query)
if err != nil {

View File

@ -1,128 +0,0 @@
package influx
import (
"regexp"
"sort"
"strconv"
"strings"
"time"
"github.com/influxdata/chronograf"
)
// SortTemplates the templates by size, then type, then value.
func SortTemplates(ts []chronograf.TemplateVar) []chronograf.TemplateVar {
sort.Slice(ts, func(i, j int) bool {
if ts[i].Var == ":interval:" {
return false
}
if len(ts[i].Values) != len(ts[j].Values) {
return len(ts[i].Values) < len(ts[j].Values)
}
if len(ts[i].Values) == 0 {
return i < j
}
for k := range ts[i].Values {
if ts[i].Values[k].Type != ts[j].Values[k].Type {
return ts[i].Values[k].Type < ts[j].Values[k].Type
}
if ts[i].Values[k].Value != ts[j].Values[k].Value {
return ts[i].Values[k].Value < ts[j].Values[k].Value
}
}
return i < j
})
return ts
}
// RenderTemplate converts the template variable into a correct InfluxQL string based
// on its type
func RenderTemplate(query string, t chronograf.TemplateVar, now time.Time) (string, error) {
if len(t.Values) == 0 {
return query, nil
}
// we only need to render the template if the template exists in the query
if !strings.Contains(query, t.Var) {
return query, nil
}
var q string
// First render template variable usages appearing within an InfluxQL regular expression (value should appear unquoted)
switch t.Values[0].Type {
case "tagKey", "fieldKey", "measurement", "tagValue":
r, err := regexp.Compile(`(/[.^/]*)(` + regexp.QuoteMeta(t.Var) + `)([.^/]*/)`)
if err != nil {
return "", err
}
q = r.ReplaceAllString(query, `${1}`+t.Values[0].Value+`${3}`)
default:
q = query
}
// Then render template variable usages not appearing in an InfluxQL regular expression (values may be quoted)
switch t.Values[0].Type {
case "tagKey", "fieldKey", "measurement", "database":
return strings.Replace(q, t.Var, `"`+t.Values[0].Value+`"`, -1), nil
case "tagValue", "timeStamp":
return strings.Replace(q, t.Var, `'`+t.Values[0].Value+`'`, -1), nil
case "csv", "constant", "influxql":
return strings.Replace(q, t.Var, t.Values[0].Value, -1), nil
}
tv := map[string]string{}
for i := range t.Values {
tv[t.Values[i].Type] = t.Values[i].Value
}
if pts, ok := tv["points"]; ok {
points, err := strconv.ParseInt(pts, 0, 64)
if err != nil {
return "", err
}
dur, err := ParseTime(query, now)
if err != nil {
return "", err
}
interval := AutoInterval(points, dur)
return strings.Replace(query, t.Var, interval, -1), nil
}
return query, nil
}
func AutoInterval(points int64, duration time.Duration) string {
// The function is: ((total_seconds * millisecond_converstion) / group_by) = pixels / 3
// Number of points given the pixels
pixels := float64(points)
msPerPixel := float64(duration/time.Millisecond) / pixels
secPerPixel := float64(duration/time.Second) / pixels
if secPerPixel < 1.0 {
if msPerPixel < 1.0 {
msPerPixel = 1.0
}
return strconv.FormatInt(int64(msPerPixel), 10) + "ms"
}
// If groupby is more than 1 second round to the second
return strconv.FormatInt(int64(secPerPixel), 10) + "s"
}
// TemplateReplace replaces templates with values within the query string
func TemplateReplace(query string, templates []chronograf.TemplateVar, now time.Time) (string, error) {
templates = SortTemplates(templates)
for i := range templates {
var err error
query, err = RenderTemplate(query, templates[i], now)
if err != nil {
return "", err
}
}
return query, nil
}

View File

@ -1,484 +0,0 @@
package influx
import (
"encoding/json"
"fmt"
"reflect"
"testing"
"time"
"github.com/influxdata/chronograf"
)
func TestTemplateReplace(t *testing.T) {
tests := []struct {
name string
query string
vars []chronograf.TemplateVar
want string
}{
{
name: "select with parameters",
query: ":method: field1, :field: FROM :measurement: WHERE temperature > :temperature:",
vars: []chronograf.TemplateVar{
chronograf.TemplateVar{
Var: ":temperature:",
Values: []chronograf.TemplateValue{
{
Type: "csv",
Value: "10",
},
},
},
chronograf.TemplateVar{
Var: ":field:",
Values: []chronograf.TemplateValue{
{
Type: "fieldKey",
Value: "field2",
},
},
},
chronograf.TemplateVar{
Var: ":method:",
Values: []chronograf.TemplateValue{
{
Type: "csv",
Value: "SELECT",
},
},
},
chronograf.TemplateVar{
Var: ":measurement:",
Values: []chronograf.TemplateValue{
{
Type: "csv",
Value: `"cpu"`,
},
},
},
},
want: `SELECT field1, "field2" FROM "cpu" WHERE temperature > 10`,
},
{
name: "select with parameters and aggregates",
query: `SELECT mean(:field:) FROM "cpu" WHERE :tag: = :value: GROUP BY :tag:`,
vars: []chronograf.TemplateVar{
chronograf.TemplateVar{
Var: ":value:",
Values: []chronograf.TemplateValue{
{
Type: "tagValue",
Value: "howdy.com",
},
},
},
chronograf.TemplateVar{
Var: ":tag:",
Values: []chronograf.TemplateValue{
{
Type: "tagKey",
Value: "host",
},
},
},
chronograf.TemplateVar{
Var: ":field:",
Values: []chronograf.TemplateValue{
{
Type: "fieldKey",
Value: "field",
},
},
},
},
want: `SELECT mean("field") FROM "cpu" WHERE "host" = 'howdy.com' GROUP BY "host"`,
},
{
name: "Non-existant parameters",
query: `SELECT :field: FROM "cpu"`,
want: `SELECT :field: FROM "cpu"`,
},
{
name: "var without a value",
query: `SELECT :field: FROM "cpu"`,
vars: []chronograf.TemplateVar{
chronograf.TemplateVar{
Var: ":field:",
},
},
want: `SELECT :field: FROM "cpu"`,
},
{
name: "var with unknown type",
query: `SELECT :field: FROM "cpu"`,
vars: []chronograf.TemplateVar{
chronograf.TemplateVar{
Var: ":field:",
Values: []chronograf.TemplateValue{
{
Type: "who knows?",
Value: "field",
},
},
},
},
want: `SELECT :field: FROM "cpu"`,
},
{
name: "auto interval",
query: `SELECT mean(usage_idle) from "cpu" where time > now() - 4320h group by time(:interval:)`,
vars: []chronograf.TemplateVar{
{
Var: ":interval:",
Values: []chronograf.TemplateValue{
{
Value: "333",
Type: "points",
},
},
},
},
want: `SELECT mean(usage_idle) from "cpu" where time > now() - 4320h group by time(46702s)`,
},
{
name: "auto interval",
query: `SELECT derivative(mean(usage_idle),:interval:) from "cpu" where time > now() - 4320h group by time(:interval:)`,
vars: []chronograf.TemplateVar{
{
Var: ":interval:",
Values: []chronograf.TemplateValue{
{
Value: "333",
Type: "points",
},
},
},
},
want: `SELECT derivative(mean(usage_idle),46702s) from "cpu" where time > now() - 4320h group by time(46702s)`,
},
{
name: "auto group by",
query: `SELECT mean(usage_idle) from "cpu" where time > now() - 4320h group by time(:interval:)`,
vars: []chronograf.TemplateVar{
{
Var: ":interval:",
Values: []chronograf.TemplateValue{
{
Value: "333",
Type: "points",
},
},
},
},
want: `SELECT mean(usage_idle) from "cpu" where time > now() - 4320h group by time(46702s)`,
},
{
name: "auto group by without duration",
query: `SELECT mean(usage_idle) from "cpu" WHERE time > now() - 4320h group by time(:interval:)`,
vars: []chronograf.TemplateVar{
{
Var: ":interval:",
Values: []chronograf.TemplateValue{
{
Value: "333",
Type: "points",
},
},
},
},
want: `SELECT mean(usage_idle) from "cpu" WHERE time > now() - 4320h group by time(46702s)`,
},
{
name: "auto group by with :dashboardTime:",
query: `SELECT mean(usage_idle) from "cpu" WHERE time > :dashboardTime: group by time(:interval:)`,
vars: []chronograf.TemplateVar{
{
Var: ":interval:",
Values: []chronograf.TemplateValue{
{
Value: "333",
Type: "points",
},
},
},
{
Var: ":dashboardTime:",
Values: []chronograf.TemplateValue{
{
Type: "constant",
Value: "now() - 4320h",
},
},
},
},
want: `SELECT mean(usage_idle) from "cpu" WHERE time > now() - 4320h group by time(46702s)`,
},
{
name: "auto group by failing condition",
query: `SELECT mean(usage_idle) FROM "cpu" WHERE time > :dashboardTime: GROUP BY time(:interval:)`,
vars: []chronograf.TemplateVar{
{
Var: ":interval:",
Values: []chronograf.TemplateValue{
{
Value: "38",
Type: "points",
},
},
},
{
Var: ":dashboardTime:",
Values: []chronograf.TemplateValue{
{
Value: "now() - 1h",
Type: "constant",
Selected: true,
},
},
},
},
want: `SELECT mean(usage_idle) FROM "cpu" WHERE time > now() - 1h GROUP BY time(94s)`,
},
{
name: "no template variables specified",
query: `SELECT mean(usage_idle) FROM "cpu" WHERE time > :dashboardTime: GROUP BY time(:interval:)`,
want: `SELECT mean(usage_idle) FROM "cpu" WHERE time > :dashboardTime: GROUP BY time(:interval:)`,
},
{
name: "auto group by failing condition",
query: `SELECT mean(usage_idle) FROM "cpu" WHERE time > :dashboardTime: GROUP BY time(:interval:)`,
vars: []chronograf.TemplateVar{
{
Var: ":interval:",
Values: []chronograf.TemplateValue{
{
Value: "38",
Type: "points",
},
},
},
{
Var: ":dashboardTime:",
Values: []chronograf.TemplateValue{
{
Value: "now() - 1h",
Type: "constant",
Selected: true,
},
},
},
},
want: `SELECT mean(usage_idle) FROM "cpu" WHERE time > now() - 1h GROUP BY time(94s)`,
},
{
name: "query with no template variables contained should return query",
query: `SHOW DATABASES`,
vars: []chronograf.TemplateVar{
{
Var: ":interval:",
Values: []chronograf.TemplateValue{
{
Value: "115",
Type: "points",
},
},
},
{
Var: ":dashboardTime:",
Values: []chronograf.TemplateValue{
{
Value: "now() - 1h",
Type: "constant",
Selected: true,
},
},
},
},
want: `SHOW DATABASES`,
},
{
name: "query with some tagValue template variables inside a regex",
query: `SELECT "usage_active" FROM "cpu" WHERE host =~ /:host:/ AND time > :dashboardTime: FILL(null)`,
vars: []chronograf.TemplateVar{
{
Var: ":host:",
Values: []chronograf.TemplateValue{
{
Value: "my-host.local",
Type: "tagValue",
},
},
},
{
Var: ":dashboardTime:",
Values: []chronograf.TemplateValue{
{
Value: "now() - 1h",
Type: "constant",
Selected: true,
},
},
},
},
want: `SELECT "usage_active" FROM "cpu" WHERE host =~ /my-host.local/ AND time > now() - 1h FILL(null)`,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
now, err := time.Parse(time.RFC3339, "1985-10-25T00:01:00Z")
if err != nil {
t.Fatal(err)
}
got, err := TemplateReplace(tt.query, tt.vars, now)
if err != nil {
t.Fatalf("TestParse unexpected TemplateReplace error: %v", err)
}
if got != tt.want {
t.Errorf("TestParse %s =\n%s\nwant\n%s", tt.name, got, tt.want)
}
})
}
}
func Test_TemplateVarsUnmarshalling(t *testing.T) {
req := `[
{
"tempVar": ":interval:",
"values": [
{
"value": "333",
"type": "points"
},
{
"value": "10",
"type": "reportingInterval"
}
]
},
{
"tempVar": ":cpu:",
"values": [
{
"type": "tagValue",
"value": "cpu-total",
"selected": false
}
]
}
]`
want := []chronograf.TemplateVar{
{
Var: ":interval:",
Values: []chronograf.TemplateValue{
{
Value: "333",
Type: "points",
},
{
Value: "10",
Type: "reportingInterval",
},
},
},
{
Var: ":cpu:",
Values: []chronograf.TemplateValue{
{
Value: "cpu-total",
Type: "tagValue",
Selected: false,
},
},
},
}
var got []chronograf.TemplateVar
err := json.Unmarshal([]byte(req), &got)
if err != nil {
t.Fatal("Err unmarshaling:", err)
}
if !reflect.DeepEqual(got, want) {
t.Errorf("UnmarshalJSON() = \n%#v\n want \n%#v\n", got, want)
}
}
func Test_RenderTemplate(t *testing.T) {
gbvTests := []struct {
name string
query string
want string
resolution uint // the screen resolution to render queries into
}{
{
name: "relative time only lower bound with one day of duration",
query: "SELECT mean(usage_idle) FROM cpu WHERE time > now() - 1d GROUP BY time(:interval:)",
resolution: 333,
want: "SELECT mean(usage_idle) FROM cpu WHERE time > now() - 1d GROUP BY time(259s)",
},
{
name: "relative time offset by week",
query: "SELECT mean(usage_idle) FROM cpu WHERE time > now() - 1d - 7d AND time < now() - 7d GROUP BY time(:interval:)",
resolution: 333,
want: "SELECT mean(usage_idle) FROM cpu WHERE time > now() - 1d - 7d AND time < now() - 7d GROUP BY time(259s)",
},
{
name: "relative time with relative upper bound with one minute of duration",
query: "SELECT mean(usage_idle) FROM cpu WHERE time > now() - 3m AND time < now() - 2m GROUP BY time(:interval:)",
resolution: 333,
want: "SELECT mean(usage_idle) FROM cpu WHERE time > now() - 3m AND time < now() - 2m GROUP BY time(180ms)",
},
{
name: "relative time with relative lower bound and now upper with one day of duration",
query: "SELECT mean(usage_idle) FROM cpu WHERE time > now() - 1d AND time < now() GROUP BY time(:interval:)",
resolution: 333,
want: "SELECT mean(usage_idle) FROM cpu WHERE time > now() - 1d AND time < now() GROUP BY time(259s)",
},
{
name: "absolute time with one minute of duration",
query: "SELECT mean(usage_idle) FROM cpu WHERE time > '1985-10-25T00:01:00Z' and time < '1985-10-25T00:02:00Z' GROUP BY time(:interval:)",
resolution: 333,
want: "SELECT mean(usage_idle) FROM cpu WHERE time > '1985-10-25T00:01:00Z' and time < '1985-10-25T00:02:00Z' GROUP BY time(180ms)",
},
{
name: "absolute time with nano seconds and zero duration",
query: "SELECT mean(usage_idle) FROM cpu WHERE time > '2017-07-24T15:33:42.994Z' and time < '2017-07-24T15:33:42.994Z' GROUP BY time(:interval:)",
resolution: 333,
want: "SELECT mean(usage_idle) FROM cpu WHERE time > '2017-07-24T15:33:42.994Z' and time < '2017-07-24T15:33:42.994Z' GROUP BY time(1ms)",
},
{
name: "query should be returned if there are no template variables",
query: "SHOW DATABASES",
want: "SHOW DATABASES",
},
}
for _, tt := range gbvTests {
t.Run(tt.name, func(t *testing.T) {
now, err := time.Parse(time.RFC3339, "1985-10-25T00:01:00Z")
if err != nil {
t.Fatal(err)
}
tvar := chronograf.TemplateVar{
Var: ":interval:",
Values: []chronograf.TemplateValue{
{
Value: fmt.Sprintf("%d", tt.resolution),
Type: "points",
},
},
}
got, err := RenderTemplate(tt.query, tvar, now)
if err != nil {
t.Fatalf("unexpected error rendering template %v", err)
}
if got != tt.want {
t.Fatalf("%q - durations not equal! Want: %s, Got: %s", tt.name, tt.want, got)
}
})
}
}

View File

@ -29,12 +29,12 @@ type QueriesRequest struct {
// QueryResponse is the return result of a QueryRequest including
// the raw query, the templated query, the queryConfig and the queryAST
type QueryResponse struct {
Duration int64 `json:"durationMs"`
ID string `json:"id"`
Query string `json:"query"`
QueryConfig chronograf.QueryConfig `json:"queryConfig"`
QueryAST *queries.SelectStatement `json:"queryAST,omitempty"`
QueryTemplated *string `json:"queryTemplated,omitempty"`
TemplateVars []chronograf.TemplateVar `json:"tempVars,omitempty"`
}
// QueriesResponse is the response for a QueriesRequest
@ -72,13 +72,7 @@ func (s *Service) Queries(w http.ResponseWriter, r *http.Request) {
Query: q.Query,
}
query, err := influx.TemplateReplace(q.Query, req.TemplateVars, time.Now())
if err != nil {
Error(w, http.StatusBadRequest, err.Error(), s.Logger)
return
}
qc := ToQueryConfig(query)
qc := ToQueryConfig(q.Query)
if err := s.DefaultRP(ctx, &qc, &src); err != nil {
Error(w, http.StatusBadRequest, err.Error(), s.Logger)
return
@ -86,14 +80,17 @@ func (s *Service) Queries(w http.ResponseWriter, r *http.Request) {
qc.Shifts = []chronograf.TimeShift{}
qr.QueryConfig = qc
if stmt, err := queries.ParseSelect(query); err == nil {
if stmt, err := queries.ParseSelect(q.Query); err == nil {
qr.QueryAST = stmt
}
if len(req.TemplateVars) > 0 {
qr.TemplateVars = req.TemplateVars
qr.QueryConfig.RawText = &qr.Query
qr.QueryTemplated = &query
if dur, err := influx.ParseTime(q.Query, time.Now()); err == nil {
ms := dur.Nanoseconds() / int64(time.Millisecond)
if ms == 0 {
ms = 1
}
qr.Duration = ms
}
qr.QueryConfig.ID = q.ID

View File

@ -60,7 +60,7 @@ func TestService_Queries(t *testing.T) {
"id": "82b60d37-251e-4afe-ac93-ca20a3642b11"
}
]}`))),
want: `{"queries":[{"id":"82b60d37-251e-4afe-ac93-ca20a3642b11","query":"SELECT \"pingReq\" FROM db.\"monitor\".\"httpd\" WHERE time \u003e now() - 1m","queryConfig":{"id":"82b60d37-251e-4afe-ac93-ca20a3642b11","database":"db","measurement":"httpd","retentionPolicy":"monitor","fields":[{"value":"pingReq","type":"field","alias":""}],"tags":{},"groupBy":{"time":"","tags":[]},"areTagsAccepted":false,"rawText":null,"range":{"upper":"","lower":"now() - 1m"},"shifts":[]},"queryAST":{"condition":{"expr":"binary","op":"\u003e","lhs":{"expr":"reference","val":"time"},"rhs":{"expr":"binary","op":"-","lhs":{"expr":"call","name":"now"},"rhs":{"expr":"literal","val":"1m","type":"duration"}}},"fields":[{"column":{"expr":"reference","val":"pingReq"}}],"sources":[{"database":"db","retentionPolicy":"monitor","name":"httpd","type":"measurement"}]}}]}
want: `{"queries":[{"durationMs":59999,"id":"82b60d37-251e-4afe-ac93-ca20a3642b11","query":"SELECT \"pingReq\" FROM db.\"monitor\".\"httpd\" WHERE time \u003e now() - 1m","queryConfig":{"id":"82b60d37-251e-4afe-ac93-ca20a3642b11","database":"db","measurement":"httpd","retentionPolicy":"monitor","fields":[{"value":"pingReq","type":"field","alias":""}],"tags":{},"groupBy":{"time":"","tags":[]},"areTagsAccepted":false,"rawText":null,"range":{"upper":"","lower":"now() - 1m"},"shifts":[]},"queryAST":{"condition":{"expr":"binary","op":"\u003e","lhs":{"expr":"reference","val":"time"},"rhs":{"expr":"binary","op":"-","lhs":{"expr":"call","name":"now"},"rhs":{"expr":"literal","val":"1m","type":"duration"}}},"fields":[{"column":{"expr":"reference","val":"pingReq"}}],"sources":[{"database":"db","retentionPolicy":"monitor","name":"httpd","type":"measurement"}]}}]}
`,
},
{
@ -81,88 +81,7 @@ func TestService_Queries(t *testing.T) {
"id": "82b60d37-251e-4afe-ac93-ca20a3642b11"
}
]}`))),
want: `{"queries":[{"id":"82b60d37-251e-4afe-ac93-ca20a3642b11","query":"SHOW DATABASES","queryConfig":{"id":"82b60d37-251e-4afe-ac93-ca20a3642b11","database":"","measurement":"","retentionPolicy":"","fields":[],"tags":{},"groupBy":{"time":"","tags":[]},"areTagsAccepted":false,"rawText":"SHOW DATABASES","range":null,"shifts":[]}}]}
`,
},
{
name: "query with template vars",
SourcesStore: &mocks.SourcesStore{
GetF: func(ctx context.Context, ID int) (chronograf.Source, error) {
return chronograf.Source{
ID: ID,
}, nil
},
},
ID: "1",
w: httptest.NewRecorder(),
r: httptest.NewRequest("POST", "/queries", bytes.NewReader([]byte(`{
"queries": [
{
"query": "SELECT \"pingReq\" FROM :dbs:.\"monitor\".\"httpd\" WHERE time > :dashboardTime: AND time < :upperDashboardTime: GROUP BY time(:interval:)",
"id": "82b60d37-251e-4afe-ac93-ca20a3642b11"
}
],
"tempVars": [
{
"tempVar": ":dbs:",
"values": [
{
"value": "_internal",
"type": "database",
"selected": true
}
],
"id": "792eda0d-2bb2-4de6-a86f-1f652889b044",
"type": "databases",
"label": "",
"query": {
"influxql": "SHOW DATABASES",
"measurement": "",
"tagKey": "",
"fieldKey": ""
},
"links": {
"self": "/chronograf/v1/dashboards/1/templates/792eda0d-2bb2-4de6-a86f-1f652889b044"
}
},
{
"id": "dashtime",
"tempVar": ":dashboardTime:",
"type": "constant",
"values": [
{
"value": "now() - 15m",
"type": "constant",
"selected": true
}
]
},
{
"id": "upperdashtime",
"tempVar": ":upperDashboardTime:",
"type": "constant",
"values": [
{
"value": "now()",
"type": "constant",
"selected": true
}
]
},
{
"id": "interval",
"type": "constant",
"tempVar": ":interval:",
"values": [
{
"value": "333",
"type": "points"
}
]
}
]
}`))),
want: `{"queries":[{"id":"82b60d37-251e-4afe-ac93-ca20a3642b11","query":"SELECT \"pingReq\" FROM :dbs:.\"monitor\".\"httpd\" WHERE time \u003e :dashboardTime: AND time \u003c :upperDashboardTime: GROUP BY time(:interval:)","queryConfig":{"id":"82b60d37-251e-4afe-ac93-ca20a3642b11","database":"","measurement":"","retentionPolicy":"","fields":[],"tags":{},"groupBy":{"time":"","tags":[]},"areTagsAccepted":false,"rawText":"SELECT \"pingReq\" FROM :dbs:.\"monitor\".\"httpd\" WHERE time \u003e :dashboardTime: AND time \u003c :upperDashboardTime: GROUP BY time(:interval:)","range":null,"shifts":[]},"queryTemplated":"SELECT \"pingReq\" FROM \"_internal\".\"monitor\".\"httpd\" WHERE time \u003e now() - 15m AND time \u003c now() GROUP BY time(2s)","tempVars":[{"tempVar":":upperDashboardTime:","values":[{"value":"now()","type":"constant","selected":true}]},{"tempVar":":dashboardTime:","values":[{"value":"now() - 15m","type":"constant","selected":true}]},{"tempVar":":dbs:","values":[{"value":"_internal","type":"database","selected":true}]},{"tempVar":":interval:","values":[{"value":"333","type":"points","selected":false}]}]}]}
want: `{"queries":[{"durationMs":0,"id":"82b60d37-251e-4afe-ac93-ca20a3642b11","query":"SHOW DATABASES","queryConfig":{"id":"82b60d37-251e-4afe-ac93-ca20a3642b11","database":"","measurement":"","retentionPolicy":"","fields":[],"tags":{},"groupBy":{"time":"","tags":[]},"areTagsAccepted":false,"rawText":"SHOW DATABASES","range":null,"shifts":[]}}]}
`,
},
}

View File

@ -2864,6 +2864,97 @@
}
}
}
},
"/chronograf/v1/config/:section": {
"get": {
"tags": ["config"],
"summary": "Returns the settings for a specific section of the app",
"description": "All settings for a specific section of the app",
"responses": {
"200": {
"description": "Returns an object with the settings for a section of the app",
"schema": {
"oneOf": [
{
"$ref": "#/definitions/LogViewerUIConfig"
},
{
"$ref": "#/definitions/AuthConfig"
}
]
}
},
"404": {
"description": "Could not find requested section",
"schema": {
"$ref": "#/definitions/Error"
}
},
"default": {
"description": "Unexpected internal server error",
"schema": {
"$ref": "#/definitions/Error"
}
}
}
},
"put": {
"tags": ["config"],
"summary": "Updates the settings for a specific section of the app",
"description": "Updates settings for a specific section of the app",
"parameters": [
{
"name": "section",
"in": "path",
"type": "string",
"description": "Section name for the target section settings",
"required": true
},
{
"name": {
"enum": ["logViewer", "auth"]
},
"in": "body",
"description":
"Section configuration update parameters",
"schema": {
"oneOf": [
{
"$ref": "#/definitions/LogViewerUIConfig"
},
{
"$ref": "#/definitions/AuthConfig"
}
]
},
"required": true
}
],
"responses": {
"200": {
"description": "Returns an object with the updated UI settings for a specific section",
"schema": {
"oneOf": [
{
"$ref": "#/definitions/LogViewerUIConfig"
}
]
}
},
"404": {
"description": "Could not find requested section",
"schema": {
"$ref": "#/definitions/Error"
}
},
"default": {
"description": "Unexpected internal server error",
"schema": {
"$ref": "#/definitions/Error"
}
}
}
}
}
},
"definitions": {
@ -5032,25 +5123,166 @@
},
"RenamableField": {
"description":
"renamableField describes a field that can be renamed and made visible or invisible",
"Describes a field that can be renamed and made visible or invisible",
"type": "object",
"properties": {
"internalName": {
"description": "internalName is the calculated name of a field",
"description": "This is the calculated name of a field",
"readOnly": true,
"type": "string"
},
"displayName": {
"description":
"displayName is the name that a field is renamed to by the user",
"This is the name that a field is renamed to by the user",
"type": "string"
},
"visible": {
"description":
"visible indicates whether this field should be visible on the table",
"Indicates whether this field should be visible on the table",
"type": "boolean"
}
}
},
"LogViewerUIColumn": {
"description": "Contains the settings for the log viewer page UI",
"type": "object",
"required": [
"name",
"encoding",
"position"
],
"properties": {
"name": {
"description": "Unique identifier name of the column",
"type": "string"
},
"position": {
"type": "integer",
"format": "int32"
},
"encoding": {
"description": "Composable encoding options for the column",
"type": "array",
"items": {
"description":"Type and value and optional name of an encoding",
"type": "object",
"required": ["type", "value"],
"properties": {
"type": {
"type": "string"
},
"value": {
"type": "string"
},
"name": {
"type": "string"
}
}
}
},
"example": {
"name": "severity",
"position": 0,
"encoding": [
{
"type": "label",
"value": "icon"
},
{
"type": "label",
"value": "text"
},
{
"type": "visibility",
"value": "visible"
},
{
"type": "color",
"name": "ruby",
"value": "emergency"
},
{
"type": "color",
"name": "rainforest",
"value": "info"
},
{
"type": "displayName",
"value": "Log Severity!"
}
]
}
},
"LogViewerUIConfig": {
"description": "Contains the settings for the log viewer page UI",
"type": "object",
"required": ["columns"],
"properties": {
"columns": {
"description": "Defines the order, names, and visibility of columns in the log viewer table",
"type": "array",
"items": {
"$ref": "#/definitions/LogViewerUIColumn"
}
}
},
"example": {
"columns": [
{
"name": "severity",
"position": 0,
"encoding": [
{
"type": "label",
"value": "icon"
},
{
"type": "label",
"value": "text"
},
{
"type": "visibility",
"value": "visible"
},
{
"type": "color",
"name": "ruby",
"value": "emergency"
},
{
"type": "color",
"name": "rainforest",
"value": "info"
},
{
"type": "displayName",
"value": "Log Severity!"
}
]
},
{
"name": "messages",
"position": 1,
"encoding": [
{
"type": "visibility",
"value": "hidden"
}
]
}
]
}
},
"AuthConfig": {
"type": "object",
"required": ["superAdminNewUsers"],
"properties": {
"superAdminNewUsers": {
"type": "boolean",
"default": true
}
}
},
"Routes": {
"type": "object",
"properties": {

View File

@ -37,6 +37,7 @@
"@types/chai": "^4.1.2",
"@types/chroma-js": "^1.3.4",
"@types/codemirror": "^0.0.56",
"@types/d3-color": "^1.2.1",
"@types/d3-scale": "^2.0.1",
"@types/dygraphs": "^1.1.6",
"@types/enzyme": "^3.1.9",
@ -129,6 +130,7 @@
"chroma-js": "^1.3.6",
"classnames": "^2.2.3",
"codemirror": "^5.36.0",
"d3-color": "^1.2.0",
"d3-scale": "^2.1.0",
"dygraphs": "2.1.0",
"enzyme-adapter-react-16": "^1.1.1",

View File

@ -29,7 +29,7 @@ import UsersTable from 'src/admin/components/UsersTable'
import RolesTable from 'src/admin/components/RolesTable'
import QueriesPage from 'src/admin/containers/QueriesPage'
import DatabaseManagerPage from 'src/admin/containers/DatabaseManagerPage'
import PageHeader from 'shared/components/PageHeader'
import PageHeader from 'src/reusable_ui/components/page_layout/PageHeader'
import FancyScrollbar from 'shared/components/FancyScrollbar'
import SubSections from 'shared/components/SubSections'
import {ErrorHandling} from 'src/shared/decorators/errors'

View File

@ -2,7 +2,7 @@ import React from 'react'
import PropTypes from 'prop-types'
import {connect} from 'react-redux'
import PageHeader from 'src/shared/components/PageHeader'
import PageHeader from 'src/reusable_ui/components/page_layout/PageHeader'
import SubSections from 'src/shared/components/SubSections'
import FancyScrollbar from 'shared/components/FancyScrollbar'

View File

@ -3,7 +3,7 @@ import React, {PureComponent} from 'react'
import AlertsTable from 'src/alerts/components/AlertsTable'
import NoKapacitorError from 'src/shared/components/NoKapacitorError'
import CustomTimeRangeDropdown from 'src/shared/components/CustomTimeRangeDropdown'
import PageHeader from 'src/shared/components/PageHeader'
import PageHeader from 'src/reusable_ui/components/page_layout/PageHeader'
import {ErrorHandling} from 'src/shared/decorators/errors'
import {getAlerts} from 'src/alerts/apis'

View File

@ -21,17 +21,19 @@ import * as queryTransitions from 'src/utils/queryTransitions'
import defaultQueryConfig from 'src/utils/defaultQueryConfig'
import {buildQuery} from 'src/utils/influxql'
import {nextSource} from 'src/dashboards/utils/sources'
import replaceTemplate, {replaceInterval} from 'src/tempVars/utils/replace'
// Constants
import {IS_STATIC_LEGEND} from 'src/shared/constants'
import {TYPE_QUERY_CONFIG} from 'src/dashboards/constants'
import {removeUnselectedTemplateValues} from 'src/tempVars/constants'
import {OVERLAY_TECHNOLOGY} from 'src/shared/constants/classNames'
import {MINIMUM_HEIGHTS, INITIAL_HEIGHTS} from 'src/data_explorer/constants'
import {
AUTO_GROUP_BY,
PREDEFINED_TEMP_VARS,
TEMP_VAR_DASHBOARD_TIME,
DEFAULT_DURATION_MS,
DEFAULT_PIXELS,
} from 'src/shared/constants'
import {getCellTypeColors} from 'src/dashboards/constants/cellEditor'
@ -42,6 +44,7 @@ import * as DashboardsActions from 'src/types/actions/dashboards'
import * as DashboardsModels from 'src/types/dashboards'
import * as QueriesModels from 'src/types/queries'
import * as SourcesModels from 'src/types/sources'
import {Template} from 'src/types/tempVars'
type QueryTransitions = typeof queryTransitions
type EditRawTextAsyncFunc = (
@ -62,10 +65,6 @@ const staticLegend: DashboardsModels.Legend = {
orientation: 'bottom',
}
interface Template {
tempVar: string
}
interface QueryStatus {
queryID: string
status: QueriesModels.Status
@ -397,6 +396,42 @@ class CellEditorOverlay extends Component<Props, State> {
})
}
private getConfig = async (
url,
id: string,
query: string,
templates: Template[]
): Promise<QueriesModels.QueryConfig> => {
// replace all templates but :interval:
query = replaceTemplate(query, templates)
let queries = []
let durationMs = DEFAULT_DURATION_MS
try {
// get durationMs to calculate interval
queries = await getQueryConfigAndStatus(url, [{query, id}])
durationMs = _.get(queries, '0.durationMs', DEFAULT_DURATION_MS)
// calc and replace :interval:
query = replaceInterval(query, DEFAULT_PIXELS, durationMs)
} catch (error) {
console.error(error)
throw error
}
try {
// fetch queryConfig for with all template variables replaced
queries = await getQueryConfigAndStatus(url, [{query, id}])
} catch (error) {
console.error(error)
throw error
}
const {queryConfig} = queries.find(q => q.id === id)
return queryConfig
}
// The schema explorer is not built to handle user defined template variables
// in the query in a clear manner. If they are being used, we indicate that in
// the query config in order to disable the fields column down stream because
@ -406,44 +441,34 @@ class CellEditorOverlay extends Component<Props, State> {
id: string,
text: string
): Promise<void> => {
const {templates} = this.props
const userDefinedTempVarsInQuery = this.findUserDefinedTempVarsInQuery(
text,
this.props.templates
templates
)
const isUsingUserDefinedTempVars: boolean = !!userDefinedTempVarsInQuery.length
try {
const selectedTempVars: Template[] = isUsingUserDefinedTempVars
? removeUnselectedTemplateValues(userDefinedTempVarsInQuery)
: []
const queryConfig = await this.getConfig(url, id, text, templates)
const nextQueries = this.state.queriesWorkingDraft.map(q => {
if (q.id === id) {
const isQuerySupportedByExplorer = !isUsingUserDefinedTempVars
const {data} = await getQueryConfigAndStatus(
url,
[{query: text, id}],
selectedTempVars
)
const config = data.queries.find(q => q.id === id)
const nextQueries: QueriesModels.QueryConfig[] = this.state.queriesWorkingDraft.map(
(q: QueriesModels.QueryConfig) => {
if (q.id === id) {
const isQuerySupportedByExplorer = !isUsingUserDefinedTempVars
if (isUsingUserDefinedTempVars) {
return {...q, rawText: text, isQuerySupportedByExplorer}
}
return {
...config.queryConfig,
source: q.source,
isQuerySupportedByExplorer,
}
if (isUsingUserDefinedTempVars) {
return {...q, rawText: text, isQuerySupportedByExplorer}
}
return q
return {
...queryConfig,
rawText: text,
source: q.source,
isQuerySupportedByExplorer,
}
}
)
return q
})
this.setState({queriesWorkingDraft: nextQueries})
} catch (error) {

View File

@ -3,8 +3,8 @@ import classnames from 'classnames'
import Authorized, {EDITOR_ROLE} from 'src/auth/Authorized'
import PageHeader from 'src/shared/components/PageHeader'
import PageHeaderTitle from 'src/shared/components/PageHeaderTitle'
import PageHeader from 'src/reusable_ui/components/page_layout/PageHeader'
import PageHeaderTitle from 'src/reusable_ui/components/page_layout/PageHeaderTitle'
import AutoRefreshDropdown from 'src/shared/components/AutoRefreshDropdown'
import TimeRangeDropdown from 'src/shared/components/TimeRangeDropdown'
import GraphTips from 'src/shared/components/GraphTips'

View File

@ -1,6 +1,5 @@
import React, {SFC} from 'react'
import {connect} from 'react-redux'
import _ from 'lodash'
import RefreshingGraph from 'src/shared/components/RefreshingGraph'
import buildQueries from 'src/utils/buildQueriesForGraphs'
@ -71,15 +70,12 @@ const DashVisualization: SFC<Props> = ({
<SourceContext.Consumer>
{(source: Source) => (
<RefreshingGraph
source={source}
colors={colors}
axes={axes}
type={type}
tableOptions={tableOptions}
queries={buildQueries(
_.get(source, 'links.proxy'),
queryConfigs,
timeRange
)}
queries={buildQueries(queryConfigs, timeRange)}
templates={templates}
autoRefresh={autoRefresh}
editQueryStatus={editQueryStatus}

View File

@ -157,6 +157,7 @@ class DashboardPage extends Component<Props, State> {
const {source, getAnnotationsAsync, timeRange} = this.props
if (this.props.autoRefresh !== nextProps.autoRefresh) {
clearInterval(this.intervalID)
this.intervalID = null
const annotationRange = millisecondTimeRange(timeRange)
if (nextProps.autoRefresh) {
this.intervalID = window.setInterval(() => {
@ -177,6 +178,7 @@ class DashboardPage extends Component<Props, State> {
public componentWillUnmount() {
clearInterval(this.intervalID)
this.intervalID = null
window.removeEventListener('resize', this.handleWindowResize, true)
this.props.handleDismissEditingAnnotation()
}

View File

@ -5,7 +5,7 @@ import download from 'src/external/download'
import _ from 'lodash'
import DashboardsContents from 'src/dashboards/components/DashboardsPageContents'
import PageHeader from 'src/shared/components/PageHeader'
import PageHeader from 'src/reusable_ui/components/page_layout/PageHeader'
import {createDashboard} from 'src/dashboards/apis'
import {

View File

@ -393,13 +393,13 @@ export const editRawTextAsync = (
text: string
) => async (dispatch): Promise<void> => {
try {
const {data} = await getQueryConfigAndStatus(url, [
const queries = await getQueryConfigAndStatus(url, [
{
query: text,
id,
},
])
const config = data.queries.find(q => q.id === id)
const config = queries.find(q => q.id === id)
dispatch(updateQueryConfig(config.queryConfig))
} catch (error) {
dispatch(errorThrown(error))

View File

@ -6,7 +6,6 @@ import download from 'src/external/download'
import {proxy} from 'src/utils/queryUrlGenerator'
import {timeSeriesToTableGraph} from 'src/utils/timeSeriesTransformers'
import {dataToCSV} from 'src/shared/parsing/dataToCSV'
import {TEMPLATES} from 'src/shared/constants'
import {Source, QueryConfig} from 'src/types'
export const writeLineProtocol = async (
@ -28,14 +27,14 @@ interface DeprecatedQuery {
}
export const getDataForCSV = (
source: Source,
query: DeprecatedQuery,
errorThrown
) => async () => {
try {
const response = await fetchTimeSeriesForCSV({
source: query.host,
source: source.links.proxy,
query: query.text,
tempVars: TEMPLATES,
})
const {data} = timeSeriesToTableGraph([{response}])
@ -47,9 +46,9 @@ export const getDataForCSV = (
}
}
const fetchTimeSeriesForCSV = async ({source, query, tempVars}) => {
const fetchTimeSeriesForCSV = async ({source, query}) => {
try {
const {data} = await proxy({source, query, tempVars})
const {data} = await proxy({source, query})
return data
} catch (error) {
console.error(error)

View File

@ -7,7 +7,6 @@ import {Table, Column, Cell} from 'fixed-data-table-2'
import Dropdown from 'src/shared/components/Dropdown'
import CustomCell from 'src/data_explorer/components/CustomCell'
import TabItem from 'src/data_explorer/components/TableTabItem'
import {TEMPLATES} from 'src/shared/constants'
import {fetchTimeSeriesAsync} from 'src/shared/actions/timeSeries'
import {ErrorHandling} from 'src/shared/decorators/errors'
@ -226,7 +225,6 @@ class ChronoTable extends PureComponent<Props, State> {
const {results} = await fetchTimeSeriesAsync({
source: this.source,
query,
tempVars: TEMPLATES,
})
this.setState({

View File

@ -2,8 +2,10 @@ import React, {PureComponent} from 'react'
import {getDataForCSV} from 'src/data_explorer/apis'
import VisHeaderTabs from 'src/data_explorer/components/VisHeaderTabs'
import {OnToggleView} from 'src/data_explorer/components/VisHeaderTab'
import {Source} from 'src/types'
interface Props {
source: Source
views: string[]
view: string
query: any
@ -13,7 +15,7 @@ interface Props {
class VisHeader extends PureComponent<Props> {
public render() {
const {views, view, onToggleView, query, errorThrown} = this.props
const {source, views, view, onToggleView, query, errorThrown} = this.props
return (
<div className="graph-heading">
@ -28,7 +30,7 @@ class VisHeader extends PureComponent<Props> {
{query && (
<div
className="btn btn-sm btn-default dlcsv"
onClick={getDataForCSV(query, errorThrown)}
onClick={getDataForCSV(source, query, errorThrown)}
>
<span className="icon download dlcsv" />
.csv

View File

@ -3,8 +3,9 @@ import React, {SFC} from 'react'
import Table from './Table'
import RefreshingGraph from 'src/shared/components/RefreshingGraph'
import {DEFAULT_LINE_COLORS} from 'src/shared/constants/graphColorPalettes'
import {SourceContext} from 'src/CheckSources'
import {Query, Template} from 'src/types'
import {Source, Query, Template} from 'src/types'
interface Props {
view: string
@ -38,15 +39,20 @@ const DataExplorerVisView: SFC<Props> = ({
}
return (
<RefreshingGraph
type="line-graph"
queries={queries}
templates={templates}
autoRefresh={autoRefresh}
colors={DEFAULT_LINE_COLORS}
manualRefresh={manualRefresh}
editQueryStatus={editQueryStatus}
/>
<SourceContext.Consumer>
{(source: Source) => (
<RefreshingGraph
type="line-graph"
source={source}
queries={queries}
templates={templates}
autoRefresh={autoRefresh}
colors={DEFAULT_LINE_COLORS}
manualRefresh={manualRefresh}
editQueryStatus={editQueryStatus}
/>
)}
</SourceContext.Consumer>
)
}

View File

@ -59,6 +59,7 @@ class DataExplorerVisualization extends PureComponent<Props, State> {
public render() {
const {
views,
source,
templates,
autoRefresh,
manualRefresh,
@ -73,6 +74,7 @@ class DataExplorerVisualization extends PureComponent<Props, State> {
<VisHeader
view={view}
views={views}
source={source}
query={this.query}
errorThrown={errorThrown}
onToggleView={this.handleToggleView}
@ -102,8 +104,8 @@ class DataExplorerVisualization extends PureComponent<Props, State> {
}
private get queries(): Query[] {
const {source, queryConfigs, timeRange} = this.props
return buildQueries(source.links.proxy, queryConfigs, timeRange)
const {queryConfigs, timeRange} = this.props
return buildQueries(queryConfigs, timeRange)
}
private get query(): Query {

View File

@ -18,7 +18,7 @@ import ManualRefresh from 'src/shared/components/ManualRefresh'
import AutoRefreshDropdown from 'src/shared/components/AutoRefreshDropdown'
import TimeRangeDropdown from 'src/shared/components/TimeRangeDropdown'
import GraphTips from 'src/shared/components/GraphTips'
import PageHeader from 'src/shared/components/PageHeader'
import PageHeader from 'src/reusable_ui/components/page_layout/PageHeader'
import {VIS_VIEWS, AUTO_GROUP_BY, TEMPLATES} from 'src/shared/constants'
import {MINIMUM_HEIGHTS, INITIAL_HEIGHTS} from 'src/data_explorer/constants'

View File

@ -1,6 +1,6 @@
import React, {SFC} from 'react'
import PageHeader from 'src/shared/components/PageHeader'
import PageHeader from 'src/reusable_ui/components/page_layout/PageHeader'
interface Props {
onShowOverlay: () => void

View File

@ -2,7 +2,7 @@ import React, {PureComponent} from 'react'
import FluxOverlay from 'src/flux/components/FluxOverlay'
import OverlayTechnology from 'src/reusable_ui/components/overlays/OverlayTechnology'
import PageHeader from 'src/shared/components/PageHeader'
import PageHeader from 'src/reusable_ui/components/page_layout/PageHeader'
import {Service} from 'src/types'

View File

@ -1,4 +1,5 @@
import {proxy} from 'utils/queryUrlGenerator'
import replaceTemplate from 'src/tempVars/utils/replace'
import AJAX from 'utils/ajax'
import _ from 'lodash'
@ -8,15 +9,20 @@ export const getCpuAndLoadForHosts = (
telegrafSystemInterval,
tempVars
) => {
return proxy({
source: proxyLink,
query: `SELECT mean("usage_user") FROM \":db:\".\":rp:\".\"cpu\" WHERE "cpu" = 'cpu-total' AND time > now() - 10m GROUP BY host;
const query = replaceTemplate(
`SELECT mean("usage_user") FROM \":db:\".\":rp:\".\"cpu\" WHERE "cpu" = 'cpu-total' AND time > now() - 10m GROUP BY host;
SELECT mean("load1") FROM \":db:\".\":rp:\".\"system\" WHERE time > now() - 10m GROUP BY host;
SELECT non_negative_derivative(mean(uptime)) AS deltaUptime FROM \":db:\".\":rp:\".\"system\" WHERE time > now() - ${telegrafSystemInterval} * 10 GROUP BY host, time(${telegrafSystemInterval}) fill(0);
SELECT mean("Percent_Processor_Time") FROM \":db:\".\":rp:\".\"win_cpu\" WHERE time > now() - 10m GROUP BY host;
SELECT mean("Processor_Queue_Length") FROM \":db:\".\":rp:\".\"win_system\" WHERE time > now() - 10s GROUP BY host;
SELECT non_negative_derivative(mean("System_Up_Time")) AS winDeltaUptime FROM \":db:\".\":rp:\".\"win_system\" WHERE time > now() - ${telegrafSystemInterval} * 10 GROUP BY host, time(${telegrafSystemInterval}) fill(0);
SHOW TAG VALUES WITH KEY = "host";`,
tempVars
)
return proxy({
source: proxyLink,
query,
db: telegrafDB,
tempVars,
}).then(resp => {

View File

@ -7,7 +7,7 @@ import _ from 'lodash'
import HostsTable from 'src/hosts/components/HostsTable'
import AutoRefreshDropdown from 'shared/components/AutoRefreshDropdown'
import ManualRefresh from 'src/shared/components/ManualRefresh'
import PageHeader from 'src/shared/components/PageHeader'
import PageHeader from 'src/reusable_ui/components/page_layout/PageHeader'
import {getCpuAndLoadForHosts, getLayouts, getAppsForHosts} from '../apis'
import {getEnv} from 'src/shared/apis/env'
@ -73,6 +73,7 @@ export class HostsPage extends Component {
async componentDidMount() {
const {notify, autoRefresh} = this.props
this.componentIsMounted = true
this.setState({hostsLoading: true}) // Only print this once
const results = await getLayouts()
@ -88,7 +89,7 @@ export class HostsPage extends Component {
return
}
await this.fetchHostsData()
if (autoRefresh) {
if (autoRefresh && this.componentIsMounted) {
this.intervalID = setInterval(() => this.fetchHostsData(), autoRefresh)
}
}
@ -151,6 +152,7 @@ export class HostsPage extends Component {
}
componentWillUnmount() {
this.componentIsMounted = false
clearInterval(this.intervalID)
this.intervalID = false
}

View File

@ -3,7 +3,7 @@ import React, {ChangeEvent, MouseEvent, PureComponent} from 'react'
import AlertOutputs from 'src/kapacitor/components/AlertOutputs'
import Input from 'src/kapacitor/components/KapacitorFormInput'
import FancyScrollbar from 'src/shared/components/FancyScrollbar'
import PageHeader from 'src/shared/components/PageHeader'
import PageHeader from 'src/reusable_ui/components/page_layout/PageHeader'
import KapacitorFormSkipVerify from 'src/kapacitor/components/KapacitorFormSkipVerify'
import {Kapacitor, Source, Notification, NotificationFunc} from 'src/types'

View File

@ -3,7 +3,7 @@ import {connect} from 'react-redux'
import {bindActionCreators} from 'redux'
import {InjectedRouter} from 'react-router'
import PageHeader from 'src/shared/components/PageHeader'
import PageHeader from 'src/reusable_ui/components/page_layout/PageHeader'
import NameSection from 'src/kapacitor/components/NameSection'
import ValuesSection from 'src/kapacitor/components/ValuesSection'
import RuleHeaderSave from 'src/kapacitor/components/RuleHeaderSave'

View File

@ -44,6 +44,7 @@ const RuleGraph = ({
/>
</div>
<RefreshingLineGraph
source={source}
queries={queries}
isGraphFilled={false}
ruleValues={rule.values}

View File

@ -1,6 +1,6 @@
import React, {Component} from 'react'
import PageHeader from 'src/shared/components/PageHeader'
import PageHeader from 'src/reusable_ui/components/page_layout/PageHeader'
import LogsToggle from 'src/kapacitor/components/LogsToggle'
import ConfirmButton from 'src/shared/components/ConfirmButton'
import TickscriptSave, {Task} from 'src/kapacitor/components/TickscriptSave'

View File

@ -9,7 +9,7 @@ import * as kapacitorActionCreators from '../actions/view'
import KapacitorRules from 'src/kapacitor/components/KapacitorRules'
import FancyScrollbar from 'src/shared/components/FancyScrollbar'
import QuestionMarkTooltip from 'src/shared/components/QuestionMarkTooltip'
import PageHeader from 'src/shared/components/PageHeader'
import PageHeader from 'src/reusable_ui/components/page_layout/PageHeader'
import {Source, Kapacitor, AlertRule} from 'src/types'
import {ErrorHandling} from 'src/shared/decorators/errors'

View File

@ -1,12 +1,6 @@
import moment from 'moment'
import _ from 'lodash'
import {
Source,
Namespace,
TimeRange,
QueryConfig,
RemoteDataState,
} from 'src/types'
import {Source, Namespace, TimeRange, QueryConfig} from 'src/types'
import {getSource} from 'src/shared/apis'
import {getDatabasesWithRetentionPolicies} from 'src/shared/apis/databases'
import {
@ -47,7 +41,6 @@ export enum ActionTypes {
SetNamespace = 'LOGS_SET_NAMESPACE',
SetHistogramQueryConfig = 'LOGS_SET_HISTOGRAM_QUERY_CONFIG',
SetHistogramData = 'LOGS_SET_HISTOGRAM_DATA',
SetHistogramDataStatus = 'LOGS_SET_HISTOGRAM_DATA_STATUS',
SetTableQueryConfig = 'LOGS_SET_TABLE_QUERY_CONFIG',
SetTableData = 'LOGS_SET_TABLE_DATA',
ChangeZoom = 'LOGS_CHANGE_ZOOM',
@ -139,11 +132,6 @@ interface SetHistogramData {
}
}
interface SetHistogramDataStatus {
type: ActionTypes.SetHistogramDataStatus
payload: RemoteDataState
}
interface SetTableQueryConfig {
type: ActionTypes.SetTableQueryConfig
payload: {
@ -179,7 +167,6 @@ export type Action =
| SetNamespaceAction
| SetHistogramQueryConfig
| SetHistogramData
| SetHistogramDataStatus
| ChangeZoomAction
| SetTableData
| SetTableQueryConfig
@ -237,13 +224,6 @@ const setHistogramData = (data): SetHistogramData => ({
payload: {data},
})
const setHistogramDataStatus = (
status: RemoteDataState
): SetHistogramDataStatus => ({
type: ActionTypes.SetHistogramDataStatus,
payload: status,
})
export const executeHistogramQueryAsync = () => async (
dispatch,
getState: GetState
@ -257,20 +237,20 @@ export const executeHistogramQueryAsync = () => async (
const searchTerm = getSearchTerm(state)
const filters = getFilters(state)
if (_.every([queryConfig, timeRange, namespace, proxyLink])) {
if (!_.every([queryConfig, timeRange, namespace, proxyLink])) {
return
}
try {
dispatch(incrementQueryCount())
const query = buildLogQuery(timeRange, queryConfig, filters, searchTerm)
const response = await executeQueryAsync(proxyLink, namespace, query)
const data = parseHistogramQueryResponse(response)
try {
dispatch(setHistogramDataStatus(RemoteDataState.Loading))
const response = await executeQueryAsync(proxyLink, namespace, query)
const data = parseHistogramQueryResponse(response)
dispatch(setHistogramData(data))
dispatch(setHistogramDataStatus(RemoteDataState.Done))
} catch {
dispatch(setHistogramDataStatus(RemoteDataState.Error))
}
dispatch(setHistogramData(data))
} finally {
dispatch(decrementQueryCount())
}
}
@ -292,7 +272,13 @@ export const executeTableQueryAsync = () => async (
const searchTerm = getSearchTerm(state)
const filters = getFilters(state)
if (_.every([queryConfig, timeRange, namespace, proxyLink])) {
if (!_.every([queryConfig, timeRange, namespace, proxyLink])) {
return
}
try {
dispatch(incrementQueryCount())
const query = buildLogQuery(timeRange, queryConfig, filters, searchTerm)
const response = await executeQueryAsync(
proxyLink,
@ -303,6 +289,8 @@ export const executeTableQueryAsync = () => async (
const series = getDeep(response, 'results.0.series.0', defaultTableData)
dispatch(setTableData(series))
} finally {
dispatch(decrementQueryCount())
}
}
@ -315,16 +303,13 @@ export const incrementQueryCount = () => ({
})
export const executeQueriesAsync = () => async dispatch => {
dispatch(incrementQueryCount())
try {
await Promise.all([
dispatch(executeHistogramQueryAsync()),
dispatch(executeTableQueryAsync()),
])
} catch (ex) {
} catch {
console.error('Could not make query requests')
} finally {
dispatch(decrementQueryCount())
}
}
@ -498,6 +483,5 @@ export const changeZoomAsync = (timeRange: TimeRange) => async (
if (namespace && proxyLink) {
await dispatch(setTimeRangeAsync(timeRange))
await dispatch(executeTableQueryAsync())
}
}

View File

@ -13,8 +13,6 @@ export const executeQueryAsync = async (
db: namespace.database,
rp: namespace.retentionPolicy,
query,
tempVars: [],
resolution: null,
})
return data

View File

@ -3,8 +3,8 @@ import React, {PureComponent} from 'react'
import {Source, Namespace} from 'src/types'
import classnames from 'classnames'
import Dropdown from 'src/shared/components/Dropdown'
import PageHeader from 'src/shared/components/PageHeader'
import PageHeaderTitle from 'src/shared/components/PageHeaderTitle'
import PageHeader from 'src/reusable_ui/components/page_layout/PageHeader'
import PageHeaderTitle from 'src/reusable_ui/components/page_layout/PageHeaderTitle'
import TimeRangeDropdown from 'src/logs/components/TimeRangeDropdown'
import {TimeRange} from 'src/types'
@ -42,7 +42,7 @@ class LogViewerHeader extends PureComponent<Props> {
return (
<>
{this.status}
<PageHeaderTitle title="Logs Viewer" />
<PageHeaderTitle title="Log Viewer" />
</>
)
}

View File

@ -3,9 +3,7 @@ import React, {PureComponent} from 'react'
class LogsGraphContainer extends PureComponent {
public render() {
return (
<div className="logs-viewer--graph-container">
<div className="logs-viewer--graph">{this.props.children}</div>
</div>
<div className="logs-viewer--graph-container">{this.props.children}</div>
)
}
}

View File

@ -1,11 +1,14 @@
import _ from 'lodash'
import moment from 'moment'
import classnames from 'classnames'
import React, {Component, MouseEvent} from 'react'
import React, {Component, MouseEvent, CSSProperties} from 'react'
import {Grid, AutoSizer, InfiniteLoader} from 'react-virtualized'
import {color} from 'd3-color'
import FancyScrollbar from 'src/shared/components/FancyScrollbar'
import {getDeep} from 'src/utils/wrappers'
import {colorForSeverity} from 'src/logs/utils/colors'
import {
getColumnFromData,
getValueFromData,
@ -173,16 +176,19 @@ class LogsTable extends Component<Props, State> {
width={width}
scrollLeft={this.state.scrollLeft}
scrollTop={this.state.scrollTop}
onScroll={this.handleGridScroll}
cellRenderer={this.cellRenderer}
onSectionRendered={this.handleRowRender(onRowsRendered)}
onScroll={this.handleGridScroll}
columnCount={columnCount}
columnWidth={this.getColumnWidth}
ref={(ref: Grid) => {
registerChild(ref)
this.grid = ref
}}
style={{height: this.calculateTotalHeight()}}
style={{
height: this.calculateTotalHeight(),
overflowY: 'hidden',
}}
/>
</FancyScrollbar>
)}
@ -194,14 +200,45 @@ class LogsTable extends Component<Props, State> {
}
private handleGridScroll = ({scrollLeft}) => {
this.handleScroll({scrollLeft, scrollTop: this.state.scrollTop})
this.handleScroll({scrollLeft})
}
private handleRowRender = onRowsRendered => ({
rowStartIndex,
rowStopIndex,
}) => {
onRowsRendered({startIndex: rowStartIndex, stopIndex: rowStopIndex})
private handleScrollbarScroll = (e: MouseEvent<JSX.Element>): void => {
e.stopPropagation()
e.preventDefault()
const {scrollTop, scrollLeft} = e.target as HTMLElement
this.handleScroll({
scrollTop,
scrollLeft,
})
}
private handleScroll = scrollInfo => {
if (_.has(scrollInfo, 'scrollTop')) {
const {scrollTop} = scrollInfo
const previousTop = this.state.scrollTop
this.setState({scrollTop})
if (scrollTop === 0) {
this.props.onScrolledToTop()
} else if (scrollTop !== previousTop) {
this.props.onScrollVertical()
}
}
if (_.has(scrollInfo, 'scrollLeft')) {
const {scrollLeft} = scrollInfo
this.setState({scrollLeft})
}
}
private handleRowRender = onRowsRendered => {
return ({rowStartIndex, rowStopIndex}) => {
onRowsRendered({startIndex: rowStartIndex, stopIndex: rowStopIndex})
}
}
private loadMoreRows = async () => {
@ -242,15 +279,6 @@ class LogsTable extends Component<Props, State> {
private handleHeaderScroll = ({scrollLeft}): void =>
this.setState({scrollLeft})
private handleScrollbarScroll = (e: MouseEvent<JSX.Element>): void => {
const target = e.target as HTMLElement
this.handleScroll({
scrollTop: target.scrollTop,
scrollLeft: this.state.scrollLeft,
})
}
private getColumnWidth = ({index}: {index: number}): number => {
const column = getColumnFromData(this.props.data, index + 1)
const {currentMessageWidth} = this.state
@ -270,6 +298,7 @@ class LogsTable extends Component<Props, State> {
private calculateTotalHeight = (): number => {
const data = getValuesFromData(this.props.data)
return _.reduce(
data,
(acc, __, index) => {
@ -284,32 +313,19 @@ class LogsTable extends Component<Props, State> {
const columnIndex = columns.indexOf('message')
const value = getValueFromData(this.props.data, index, columnIndex)
if (!value) {
if (_.isEmpty(value)) {
return ROW_HEIGHT
}
const lines = Math.round(value.length / this.rowCharLimit + 0.25)
const lines = Math.ceil(value.length / (this.rowCharLimit * 0.95))
return Math.max(lines, 1) * (ROW_HEIGHT - 14) + 14
return Math.max(lines, 1) * ROW_HEIGHT + 4
}
private calculateRowHeight = ({index}: {index: number}): number => {
return this.calculateMessageHeight(index)
}
private handleScroll = scrollInfo => {
const {scrollLeft, scrollTop} = scrollInfo
const previousScrolltop = this.state.scrollTop
this.setState({scrollLeft, scrollTop})
if (scrollTop === 0) {
this.props.onScrolledToTop()
} else if (scrollTop !== previousScrolltop) {
this.props.onScrollVertical()
}
}
private headerRenderer = ({key, style, columnIndex}) => {
const column = getColumnFromData(this.props.data, columnIndex + 1)
const classes = 'logs-viewer--cell logs-viewer--cell-header'
@ -333,6 +349,7 @@ class LogsTable extends Component<Props, State> {
title={value}
onMouseOver={this.handleMouseEnter}
data-index={rowIndex}
style={this.severityDotStyle(value)}
/>
)
} else {
@ -382,6 +399,17 @@ class LogsTable extends Component<Props, State> {
)
}
private severityDotStyle = (severity: string): CSSProperties => {
const severityColor = colorForSeverity(severity)
const brightSeverityColor = color(severityColor)
.brighter(0.5)
.hex()
return {
background: `linear-gradient(45deg, ${severityColor}, ${brightSeverityColor}`,
}
}
private handleMouseEnter = (e: MouseEvent<HTMLElement>): void => {
const target = e.target as HTMLElement
this.setState({currentRow: +target.dataset.index})

View File

@ -24,8 +24,9 @@ import SearchBar from 'src/logs/components/LogsSearchBar'
import FilterBar from 'src/logs/components/LogsFilterBar'
import LogsTable from 'src/logs/components/LogsTable'
import {getDeep} from 'src/utils/wrappers'
import {colorForSeverity} from 'src/logs/utils/colors'
import {Source, Namespace, TimeRange, RemoteDataState} from 'src/types'
import {Source, Namespace, TimeRange} from 'src/types'
import {Filter} from 'src/types/logs'
import {HistogramData, TimePeriod} from 'src/types/histogram'
@ -47,7 +48,6 @@ interface Props {
changeFilter: (id: string, operator: string, value: string) => void
timeRange: TimeRange
histogramData: HistogramData
histogramDataStatus: RemoteDataState
tableData: {
columns: string[]
values: string[]
@ -179,16 +179,16 @@ class LogsPage extends PureComponent<Props, State> {
}
private get chart(): JSX.Element {
const {histogramData, histogramDataStatus} = this.props
const {histogramData} = this.props
return (
<AutoSizer>
{({width, height}) => (
<HistogramChart
data={histogramData}
dataStatus={histogramDataStatus}
width={width}
height={height}
colorScale={colorForSeverity}
onZoom={this.handleChartZoom}
/>
)}
@ -292,7 +292,6 @@ const mapStateToProps = ({
timeRange,
currentNamespace,
histogramData,
histogramDataStatus,
tableData,
searchTerm,
filters,
@ -305,7 +304,6 @@ const mapStateToProps = ({
timeRange,
currentNamespace,
histogramData,
histogramDataStatus,
tableData,
searchTerm,
filters,

View File

@ -10,7 +10,6 @@ import {
ConcatMoreLogsAction,
} from 'src/logs/actions'
import {RemoteDataState} from 'src/types'
import {LogsState} from 'src/types/logs'
const defaultState: LogsState = {
@ -22,7 +21,6 @@ const defaultState: LogsState = {
tableQueryConfig: null,
tableData: {columns: [], values: []},
histogramData: [],
histogramDataStatus: RemoteDataState.NotStarted,
searchTerm: '',
filters: [],
queryCount: 0,
@ -111,8 +109,6 @@ export default (state: LogsState = defaultState, action: Action) => {
return {...state, histogramQueryConfig: action.payload.queryConfig}
case ActionTypes.SetHistogramData:
return {...state, histogramData: action.payload.data}
case ActionTypes.SetHistogramDataStatus:
return {...state, histogramDataStatus: action.payload}
case ActionTypes.SetTableQueryConfig:
return {...state, tableQueryConfig: action.payload.queryConfig}
case ActionTypes.SetTableData:

View File

@ -0,0 +1,16 @@
const SEVERITY_COLORS = {
emergency: '#BF3D5E',
alert: '#DC4E58',
critical: '#F95F53',
error: '#F48D38',
warning: '#FFB94A',
notice: '#4ED8A0',
info: '#7A65F2',
debug: '#8E91A1',
}
const DEFAULT_SEVERITY_COLOR = '#7A65F2'
export const colorForSeverity = (severity: string): string => {
return SEVERITY_COLORS[severity] || DEFAULT_SEVERITY_COLOR
}

View File

@ -32,18 +32,13 @@ export const formatColumnValue = (
case 'timestamp':
return moment(+value / 1000000).format('YYYY/MM/DD HH:mm:ss')
case 'message':
if (value) {
if (value.indexOf(' ') > charLimit - 5) {
return _.truncate(value, {length: charLimit - 5}).replace('\\n', '')
} else {
return value.replace('\\n', '')
}
value = (value || 'No Message Provided').replace('\\n', '')
if (value.indexOf(' ') > charLimit - 5) {
value = _.truncate(value, {length: charLimit - 5})
}
return ''
default:
return value
}
return value
}
export const header = (key: string): string => {
return getDeep<string>(

View File

@ -1,7 +1,7 @@
import React, {Component, ReactElement} from 'react'
import classnames from 'classnames'
import Title from 'src/shared/components/PageHeaderTitle'
import Title from 'src/reusable_ui/components/page_layout/PageHeaderTitle'
import SourceIndicator from 'src/shared/components/SourceIndicator'
interface Props {

View File

@ -7,7 +7,6 @@ import {Status} from 'src/types'
import {getDeep} from 'src/utils/wrappers'
interface Query {
host: string | string[]
text: string
id: string
database?: string
@ -18,10 +17,8 @@ interface Query {
interface Payload {
source: string
query: Query
tempVars: any[]
db?: string
rp?: string
resolution?: number
}
type EditQueryStatusFunction = (queryID: string, status: Status) => void
@ -81,7 +78,7 @@ const handleError = (
}
export const fetchTimeSeriesAsync = async (
{source, db, rp, query, tempVars, resolution}: Payload,
{source, db, rp, query}: Payload,
editQueryStatus: EditQueryStatusFunction = noop
): Promise<TimeSeriesResponse> => {
handleLoading(query, editQueryStatus)
@ -91,8 +88,6 @@ export const fetchTimeSeriesAsync = async (
db,
rp,
query: query.text,
tempVars,
resolution,
})
return handleSuccess(data, query, editQueryStatus)
} catch (error) {

View File

@ -31,5 +31,6 @@ export const getDatabasesWithRetentionPolicies = async (
return sorted
} catch (err) {
console.error(err)
return []
}
}

View File

@ -1,6 +1,6 @@
import AJAX from 'src/utils/ajax'
import {AlertTypes} from 'src/kapacitor/constants'
import {Kapacitor, Source, Service, NewService} from 'src/types'
import {Kapacitor, Source, Service, NewService, QueryConfig} from 'src/types'
export function getSources() {
return AJAX({
@ -319,12 +319,48 @@ export function kapacitorProxy(kapacitor, method, path, body?) {
})
}
export const getQueryConfigAndStatus = (url, queries, tempVars = []) =>
AJAX({
url,
method: 'POST',
data: {queries, tempVars},
})
export const getQueryConfigAndStatus = async (
url,
queries
): Promise<AnalyzeQueriesObject[]> => {
try {
const {data} = await AJAX({
url,
method: 'POST',
data: {queries},
})
return data.queries
} catch (error) {
console.error(error)
throw error
}
}
interface AnalyzeQueriesObject {
id: string
query: string
duration: string
queryConfig?: QueryConfig
}
export const analyzeQueries = async (
url: string,
queries: Array<{query: string}>
): Promise<AnalyzeQueriesObject[]> => {
try {
const {data} = await AJAX({
url,
method: 'POST',
data: {queries},
})
return data.queries
} catch (error) {
console.error(error)
throw error
}
}
export const getServices = async (url: string): Promise<Service[]> => {
try {

View File

@ -1,13 +1,14 @@
import _ from 'lodash'
import {getDeep} from 'src/utils/wrappers'
import {fetchTimeSeriesAsync} from 'src/shared/actions/timeSeries'
import {removeUnselectedTemplateValues} from 'src/tempVars/constants'
import {intervalValuesPoints} from 'src/shared/constants'
import {analyzeQueries} from 'src/shared/apis'
import {DEFAULT_DURATION_MS} from 'src/shared/constants'
import replaceTemplates, {replaceInterval} from 'src/tempVars/utils/replace'
import {Source} from 'src/types'
import {Template} from 'src/types'
interface Query {
host: string | string[]
text: string
database: string
db: string
@ -15,49 +16,59 @@ interface Query {
id: string
}
const parseSource = source => {
if (Array.isArray(source)) {
return _.get(source, '0', '')
}
return source
}
export const fetchTimeSeries = async (
source: Source,
queries: Query[],
resolution: number,
templates: Template[],
editQueryStatus: () => any
) => {
const timeSeriesPromises = queries.map(query => {
const {host, database, rp} = query
// the key `database` was used upstream in HostPage.js, and since as of this writing
// the codebase has not been fully converted to TypeScript, it's not clear where else
// it may be used, but this slight modification is intended to allow for the use of
// `database` while moving over to `db` for consistency over time
const timeSeriesPromises = queries.map(async query => {
const {database, rp} = query
const db = _.get(query, 'db', database)
const templatesWithIntervalVals = templates.map(temp => {
if (temp.tempVar === ':interval:') {
if (resolution) {
const values = temp.values.map(v => ({
...v,
value: `${_.toInteger(Number(resolution) / 3)}`,
}))
return {...temp, values}
}
try {
const text = await replace(query.text, source, templates, resolution)
return {...temp, values: intervalValuesPoints}
const payload = {
source: source.links.proxy,
db,
rp,
query: {...query, text},
}
return temp
})
const tempVars = removeUnselectedTemplateValues(templatesWithIntervalVals)
const source = parseSource(host)
const payload = {source, db, rp, query, tempVars, resolution}
return fetchTimeSeriesAsync(payload, editQueryStatus)
return fetchTimeSeriesAsync(payload, editQueryStatus)
} catch (error) {
console.error(error)
throw error
}
})
return Promise.all(timeSeriesPromises)
}
const replace = async (
query: string,
source: Source,
templates: Template[],
resolution: number
): Promise<string> => {
try {
query = replaceTemplates(query, templates)
const durationMs = await duration(query, source)
return replaceInterval(query, Math.floor(resolution / 3), durationMs)
} catch (error) {
console.error(error)
throw error
}
}
const duration = async (query: string, source: Source): Promise<number> => {
try {
const analysis = await analyzeQueries(source.links.queries, [{query}])
return getDeep<number>(analysis, '0.durationMs', DEFAULT_DURATION_MS)
} catch (error) {
console.error(error)
throw error
}
}

View File

@ -4,7 +4,7 @@ import _ from 'lodash'
import {fetchTimeSeries} from 'src/shared/apis/query'
import {DEFAULT_TIME_SERIES} from 'src/shared/constants/series'
import {TimeSeriesServerResponse, TimeSeriesResponse} from 'src/types/series'
import {Template} from 'src/types'
import {Template, Source} from 'src/types'
interface Axes {
bounds: {
@ -14,7 +14,6 @@ interface Axes {
}
interface Query {
host: string | string[]
text: string
database: string
db: string
@ -23,15 +22,16 @@ interface Query {
}
export interface Props {
type: string
autoRefresh: number
inView: boolean
templates: Template[]
queries: Query[]
source: Source
axes: Axes
type: string
inView: boolean
queries: Query[]
autoRefresh: number
templates: Template[]
editQueryStatus: () => void
grabDataForDownload: (timeSeries: TimeSeriesServerResponse[]) => void
onSetResolution?: (resolution: number) => void
grabDataForDownload: (timeSeries: TimeSeriesServerResponse[]) => void
}
interface State {
@ -80,7 +80,13 @@ const AutoRefresh = (
}
public executeQueries = async () => {
const {editQueryStatus, grabDataForDownload, inView, queries} = this.props
const {
source,
editQueryStatus,
grabDataForDownload,
inView,
queries,
} = this.props
const {resolution} = this.state
if (!inView) {
@ -97,6 +103,7 @@ const AutoRefresh = (
try {
const timeSeries = await fetchTimeSeries(
source,
queries,
resolution,
templates,
@ -204,7 +211,7 @@ const AutoRefresh = (
}
private queryDifference = (left, right) => {
const mapper = q => `${q.host}${q.text}`
const mapper = q => `${q.text}`
const leftStrs = left.map(mapper)
const rightStrs = right.map(mapper)
return _.difference(

View File

@ -16,7 +16,7 @@ interface Props {
class Crosshair extends PureComponent<Props> {
public render() {
if (!this.isVisible) {
return null
return <div className="crosshair-container" />
}
return (
@ -24,9 +24,8 @@ class Crosshair extends PureComponent<Props> {
<div
className="crosshair"
style={{
left: this.crosshairLeft,
transform: this.crosshairLeft,
height: this.crosshairHeight,
width: '1px',
}}
/>
</div>
@ -47,10 +46,10 @@ class Crosshair extends PureComponent<Props> {
return isValidHoverTime && hoverTime !== 0 && _.isFinite(hoverTime)
}
private get crosshairLeft(): number {
private get crosshairLeft(): string {
const {dygraph, hoverTime} = this.props
const cursorOffset = 16
return dygraph.toDomXCoord(hoverTime) + cursorOffset
return `translateX(${dygraph.toDomXCoord(hoverTime) + cursorOffset}px)`
}
private get crosshairHeight(): string {

View File

@ -52,7 +52,6 @@ class DatabaseList extends Component<DatabaseListProps, DatabaseListState> {
public componentDidUpdate({
querySource: prevSource,
query: prevQuery,
}: {
querySource?: Source
query: QueryConfig
@ -60,10 +59,6 @@ class DatabaseList extends Component<DatabaseListProps, DatabaseListState> {
const {querySource: nextSource, query: nextQuery} = this.props
const differentSource = !_.isEqual(prevSource, nextSource)
if (prevQuery.rawText === nextQuery.rawText) {
return
}
const newMetaQuery =
nextQuery.rawText && nextQuery.rawText.match(/^(create|drop)/i)

View File

@ -67,8 +67,9 @@ interface Props {
}
interface State {
staticLegendHeight: null | number
staticLegendHeight: number
xAxisRange: [number, number]
isMouseInLegend: boolean
}
@ErrorHandling
@ -102,8 +103,9 @@ class Dygraph extends Component<Props, State> {
constructor(props: Props) {
super(props)
this.state = {
staticLegendHeight: null,
staticLegendHeight: 0,
xAxisRange: [0, 0],
isMouseInLegend: false,
}
this.graphRef = React.createRef<HTMLDivElement>()
@ -148,7 +150,7 @@ class Dygraph extends Component<Props, State> {
zoomCallback: (lower: number, upper: number) =>
this.handleZoom(lower, upper),
drawCallback: () => this.handleDraw(),
highlightCircleSize: 0,
highlightCircleSize: 3,
}
if (isBarGraph) {
@ -242,7 +244,11 @@ class Dygraph extends Component<Props, State> {
const {staticLegend, cellID} = this.props
return (
<div className="dygraph-child">
<div
className="dygraph-child"
onMouseMove={this.handleShowLegend}
onMouseLeave={this.handleHideLegend}
>
{this.dygraph && (
<div className="dygraph-addons">
{this.areAnnotationsVisible && (
@ -258,25 +264,20 @@ class Dygraph extends Component<Props, State> {
dygraph={this.dygraph}
onHide={this.handleHideLegend}
onShow={this.handleShowLegend}
onMouseEnter={this.handleMouseEnterLegend}
/>
<Crosshair
dygraph={this.dygraph}
stasticLegendHeight={staticLegendHeight}
staticLegendHeight={staticLegendHeight}
/>
</div>
)}
<div
className="dygraph-child-container"
style={this.dygraphStyle}
onMouseEnter={this.handleShowLegend}
/>
{staticLegend && (
<StaticLegend
dygraphSeries={this.colorDygraphSeries}
dygraph={this.dygraph}
handleReceiveStaticLegendHeight={
this.handleReceiveStaticLegendHeight
}
height={staticLegendHeight}
onUpdateHeight={this.handleUpdateStaticLegendHeight}
/>
)}
{this.nestedGraph &&
@ -287,7 +288,6 @@ class Dygraph extends Component<Props, State> {
className="dygraph-child-container"
ref={this.graphRef}
style={this.dygraphStyle}
onMouseEnter={this.handleShowLegend}
/>
<ReactResizeDetector
handleWidth={true}
@ -390,10 +390,17 @@ class Dygraph extends Component<Props, State> {
}
private handleHideLegend = () => {
this.setState({isMouseInLegend: false})
this.props.handleSetHoverTime(NULL_HOVER_TIME)
}
private handleShowLegend = (e: MouseEvent<HTMLDivElement>): void => {
const {isMouseInLegend} = this.state
if (isMouseInLegend) {
return
}
const newTime = this.eventToTimestamp(e)
this.props.handleSetHoverTime(newTime)
}
@ -464,9 +471,13 @@ class Dygraph extends Component<Props, State> {
return nanoDate.toISOString()
}
private handleReceiveStaticLegendHeight = (staticLegendHeight: number) => {
private handleUpdateStaticLegendHeight = (staticLegendHeight: number) => {
this.setState({staticLegendHeight})
}
private handleMouseEnterLegend = () => {
this.setState({isMouseInLegend: true})
}
}
const mapStateToProps = ({annotations: {mode}}) => ({

View File

@ -15,12 +15,14 @@ import {NO_CELL} from 'src/shared/constants'
import {DygraphClass} from 'src/types'
interface Props {
hoverTime: number
dygraph: DygraphClass
cellID: string
onHide: () => void
onShow: (e: MouseEvent) => void
activeCellID: string
setActiveCell: (cellID: string) => void
onMouseEnter: () => void
}
interface LegendData {
@ -79,12 +81,14 @@ class DygraphLegend extends PureComponent<Props, State> {
}
public render() {
const {onMouseEnter} = this.props
const {legend, filterText, isAscending, isFilterVisible} = this.state
return (
<div
className={`dygraph-legend ${this.hidden}`}
ref={el => (this.legendRef = el)}
onMouseEnter={onMouseEnter}
onMouseLeave={this.handleHide}
style={this.styles}
>
@ -256,10 +260,14 @@ class DygraphLegend extends PureComponent<Props, State> {
private get styles() {
const {
dygraph,
dygraph: {graphDiv},
hoverTime,
} = this.props
const {pageX} = this.state
return makeLegendStyles(graphDiv, this.legendRef, pageX)
const cursorOffset = 16
const legendPosition = dygraph.toDomXCoord(hoverTime) + cursorOffset
return makeLegendStyles(graphDiv, this.legendRef, legendPosition)
}
}
@ -269,6 +277,7 @@ const mapDispatchToProps = {
const mapStateToProps = ({dashboardUI}) => ({
activeCellID: dashboardUI.activeCellID,
hoverTime: +dashboardUI.hoverTime,
})
export default connect(mapStateToProps, mapDispatchToProps)(DygraphLegend)

View File

@ -1,4 +1,4 @@
import React, {PureComponent, MouseEvent} from 'react'
import React, {PureComponent} from 'react'
import _ from 'lodash'
import {scaleLinear, scaleTime, ScaleLinear, ScaleTime} from 'd3-scale'
@ -9,20 +9,16 @@ import HistogramChartSkeleton from 'src/shared/components/HistogramChartSkeleton
import XBrush from 'src/shared/components/XBrush'
import extentBy from 'src/utils/extentBy'
import {getDeep} from 'src/utils/wrappers'
import {RemoteDataState} from 'src/types'
import {
TimePeriod,
HistogramData,
HistogramDatum,
Margins,
TooltipAnchor,
HoverData,
ColorScale,
} from 'src/types/histogram'
const PADDING_TOP = 0.2
const TOOLTIP_HORIZONTAL_MARGIN = 5
const TOOLTIP_REFLECT_DIST = 100
// Rather than use these magical constants, we could also render a digit and
// capture its measured width with as state before rendering anything else.
@ -32,28 +28,25 @@ const PERIOD_DIGIT_WIDTH = 4
interface Props {
data: HistogramData
dataStatus: RemoteDataState
width: number
height: number
colorScale: ColorScale
onZoom: (TimePeriod) => void
}
interface State {
hoverX: number
hoverY: number
hoverDatum?: HistogramDatum
hoverAnchor: TooltipAnchor
hoverData?: HoverData
}
class HistogramChart extends PureComponent<Props, State> {
constructor(props) {
super(props)
this.state = {hoverX: -1, hoverY: -1, hoverAnchor: 'left'}
this.state = {}
}
public render() {
const {width, height, data} = this.props
const {width, height, data, colorScale} = this.props
const {margins} = this
if (width === 0 || height === 0) {
@ -70,25 +63,12 @@ class HistogramChart extends PureComponent<Props, State> {
)
}
const {hoverDatum, hoverX, hoverY, hoverAnchor} = this.state
const {
xScale,
yScale,
adjustedWidth,
adjustedHeight,
bodyTransform,
loadingClass,
} = this
const {hoverData} = this.state
const {xScale, yScale, adjustedWidth, adjustedHeight, bodyTransform} = this
return (
<>
<svg
width={width}
height={height}
className={`histogram-chart ${loadingClass}`}
onMouseOver={this.handleMouseMove}
onMouseOut={this.handleMouseOut}
>
<svg width={width} height={height} className="histogram-chart">
<defs>
<clipPath id="histogram-chart--bars-clip">
<rect x="0" y="0" width={adjustedWidth} height={adjustedHeight} />
@ -122,15 +102,15 @@ class HistogramChart extends PureComponent<Props, State> {
data={data}
xScale={xScale}
yScale={yScale}
colorScale={colorScale}
hoverData={hoverData}
onHover={this.handleHover}
/>
</g>
</svg>
<HistogramChartTooltip
datum={hoverDatum}
x={hoverX}
y={hoverY}
anchor={hoverAnchor}
/>
{hoverData && (
<HistogramChartTooltip data={hoverData} colorScale={colorScale} />
)}
</>
)
}
@ -196,49 +176,13 @@ class HistogramChart extends PureComponent<Props, State> {
return Math.max(...counts)
}
private get loadingClass(): string {
const {dataStatus} = this.props
return dataStatus === RemoteDataState.Loading ? 'loading' : ''
}
private handleBrush = (t: TimePeriod): void => {
this.props.onZoom(t)
this.setState({hoverDatum: null})
this.setState({hoverData: null})
}
private handleMouseMove = (e: MouseEvent<SVGElement>): void => {
const key = getDeep<string>(e, 'target.dataset.key', '')
if (!key) {
return
}
const {data} = this.props
const hoverDatum = data.find(d => d.key === key)
if (!hoverDatum) {
return
}
const bar = e.target as SVGRectElement
const barRect = bar.getBoundingClientRect()
const barRectHeight = barRect.bottom - barRect.top
const hoverY = barRect.top + barRectHeight / 2
let hoverX = barRect.right + TOOLTIP_HORIZONTAL_MARGIN
let hoverAnchor: TooltipAnchor = 'left'
if (hoverX >= window.innerWidth - TOOLTIP_REFLECT_DIST) {
hoverX = window.innerWidth - barRect.left + TOOLTIP_HORIZONTAL_MARGIN
hoverAnchor = 'right'
}
this.setState({hoverDatum, hoverX, hoverY, hoverAnchor})
}
private handleMouseOut = (): void => {
this.setState({hoverDatum: null})
private handleHover = (hoverData: HoverData): void => {
this.setState({hoverData})
}
}

View File

@ -4,7 +4,7 @@ import {ScaleLinear, ScaleTime} from 'd3-scale'
import {Margins} from 'src/types/histogram'
const Y_TICK_COUNT = 5
const Y_TICK_PADDING_RIGHT = 5
const Y_TICK_PADDING_RIGHT = 7
const X_TICK_COUNT = 10
const X_TICK_PADDING_TOP = 8

View File

@ -1,11 +1,134 @@
import React, {PureComponent} from 'react'
import React, {PureComponent, MouseEvent} from 'react'
import _ from 'lodash'
import {ScaleLinear, ScaleTime} from 'd3-scale'
import {color} from 'd3-color'
import {HistogramData, HistogramDatum} from 'src/types/histogram'
import {getDeep} from 'src/utils/wrappers'
const BAR_BORDER_RADIUS = 4
import {
HistogramData,
HistogramDatum,
HoverData,
TooltipAnchor,
ColorScale,
} from 'src/types/histogram'
const BAR_BORDER_RADIUS = 3
const BAR_PADDING_SIDES = 4
const HOVER_BRIGTHEN_FACTOR = 0.4
const TOOLTIP_HORIZONTAL_MARGIN = 5
const TOOLTIP_REFLECT_DIST = 100
const getBarWidth = ({data, xScale, width}): number => {
const dataInView = data.filter(
d => xScale(d.time) >= 0 && xScale(d.time) <= width
)
const barCount = Object.values(_.groupBy(dataInView, 'time')).length
return Math.round(width / barCount - BAR_PADDING_SIDES)
}
type SortFn = (a: HistogramDatum, b: HistogramDatum) => number
const getSortFn = (data: HistogramData): SortFn => {
const counts = {}
for (const d of data) {
if (counts[d.group]) {
counts[d.group] += d.value
} else {
counts[d.group] = d.value
}
}
return (a, b) => counts[b.group] - counts[a.group]
}
interface BarGroup {
key: string
clip: {
x: number
y: number
width: number
height: number
}
bars: Array<{
key: string
group: string
x: number
y: number
width: number
height: number
fill: string
}>
data: HistogramData
}
const getBarGroups = ({
data,
width,
xScale,
yScale,
colorScale,
hoverData,
}): BarGroup[] => {
const barWidth = getBarWidth({data, xScale, width})
const sortFn = getSortFn(data)
const visibleData = data.filter(d => d.value !== 0)
const timeGroups = Object.values(_.groupBy(visibleData, 'time'))
for (const timeGroup of timeGroups) {
timeGroup.sort(sortFn)
}
let hoverDataKeys = []
if (!!hoverData) {
hoverDataKeys = hoverData.data.map(h => h.key)
}
return timeGroups.map(timeGroup => {
const time = timeGroup[0].time
const x = xScale(time) - barWidth / 2
const total = _.sumBy(timeGroup, 'value')
const barGroup = {
key: `${time}-${total}-${x}`,
clip: {
x,
y: yScale(total),
width: barWidth,
height: yScale(0) - yScale(total) + BAR_BORDER_RADIUS,
},
bars: [],
data: timeGroup,
}
let offset = 0
timeGroup.forEach((d: HistogramDatum) => {
const height = yScale(0) - yScale(d.value)
const k = hoverDataKeys.includes(d.key) ? HOVER_BRIGTHEN_FACTOR : 0
const fill = color(colorScale(d.group))
.brighter(k)
.hex()
barGroup.bars.push({
key: d.key,
group: d.group,
x,
y: yScale(d.value) - offset,
width: barWidth,
height,
fill,
})
offset += height
})
return barGroup
})
}
interface Props {
width: number
@ -13,15 +136,40 @@ interface Props {
data: HistogramData
xScale: ScaleTime<number, number>
yScale: ScaleLinear<number, number>
colorScale: ColorScale
hoverData?: HoverData
onHover: (h: HoverData) => void
}
class HistogramChartBars extends PureComponent<Props> {
interface State {
barGroups: BarGroup[]
}
class HistogramChartBars extends PureComponent<Props, State> {
public static getDerivedStateFromProps(props) {
return {barGroups: getBarGroups(props)}
}
constructor(props) {
super(props)
this.state = {barGroups: []}
}
public render() {
return this.renderData.map(group => {
const {barGroups} = this.state
return barGroups.map(group => {
const {key, clip, bars} = group
return (
<g key={key} className="histogram-chart-bars--bars">
<g
key={key}
className="histogram-chart-bars--bars"
data-key={key}
onMouseOver={this.handleMouseOver}
onMouseOut={this.handleMouseOut}
>
<defs>
<clipPath id={`histogram-chart-bars--clip-${key}`}>
<rect
@ -42,6 +190,7 @@ class HistogramChartBars extends PureComponent<Props> {
y={d.y}
width={d.width}
height={d.height}
fill={d.fill}
clipPath={`url(#histogram-chart-bars--clip-${key})`}
data-group={d.group}
data-key={d.key}
@ -52,79 +201,40 @@ class HistogramChartBars extends PureComponent<Props> {
})
}
private get renderData() {
const {data, xScale, yScale} = this.props
const {barWidth, sortFn} = this
private handleMouseOver = (e: MouseEvent<SVGGElement>): void => {
const groupKey = getDeep<string>(e, 'currentTarget.dataset.key', '')
const visibleData = data.filter(d => d.value !== 0)
const groups = Object.values(_.groupBy(visibleData, 'time'))
for (const group of groups) {
group.sort(sortFn)
if (!groupKey) {
return
}
return groups.map(group => {
const time = group[0].time
const x = xScale(time) - barWidth / 2
const groupTotal = _.sumBy(group, 'value')
const {barGroups} = this.state
const hoverGroup = barGroups.find(d => d.key === groupKey)
const renderData = {
key: `${time}-${groupTotal}-${x}`,
clip: {
x,
y: yScale(groupTotal),
width: barWidth,
height: yScale(0) - yScale(groupTotal) + BAR_BORDER_RADIUS,
},
bars: [],
}
let offset = 0
group.forEach((d: HistogramDatum) => {
const height = yScale(0) - yScale(d.value)
renderData.bars.push({
key: d.key,
group: d.group,
x,
y: yScale(d.value) - offset,
width: barWidth,
height,
})
offset += height
})
return renderData
})
}
private get sortFn() {
const {data} = this.props
const counts = {}
for (const d of data) {
if (counts[d.group]) {
counts[d.group] += d.value
} else {
counts[d.group] = d.value
}
if (!hoverGroup) {
return
}
return (a, b) => counts[b.group] - counts[a.group]
const {data} = hoverGroup
const barGroup = e.currentTarget as SVGGElement
const boundingRect = barGroup.getBoundingClientRect()
const boundingRectHeight = boundingRect.bottom - boundingRect.top
const y = boundingRect.top + boundingRectHeight / 2
let x = boundingRect.right + TOOLTIP_HORIZONTAL_MARGIN
let anchor: TooltipAnchor = 'left'
// This makes an assumption that the component is within the viewport
if (x >= window.innerWidth - TOOLTIP_REFLECT_DIST) {
x = window.innerWidth - boundingRect.left + TOOLTIP_HORIZONTAL_MARGIN
anchor = 'right'
}
this.props.onHover({data, x, y, anchor})
}
private get barWidth() {
const {data, xScale, width} = this.props
const dataInView = data.filter(
d => xScale(d.time) >= 0 && xScale(d.time) <= width
)
const barCount = Object.values(_.groupBy(dataInView, 'time')).length
return Math.round(width / barCount - BAR_PADDING_SIDES)
private handleMouseOut = (): void => {
this.props.onHover(null)
}
}

View File

@ -1,40 +1,43 @@
import React, {SFC, CSSProperties} from 'react'
import {HistogramDatum, TooltipAnchor} from 'src/types/histogram'
import {HoverData, ColorScale} from 'src/types/histogram'
interface Props {
datum: HistogramDatum
x: number
y: number
anchor?: TooltipAnchor
data: HoverData
colorScale: ColorScale
}
const HistogramChartTooltip: SFC<Props> = props => {
const {datum, x, y, anchor = 'left'} = props
const {colorScale} = props
const {data, x, y, anchor = 'left'} = props.data
if (!datum) {
return null
}
const style: CSSProperties = {
const tooltipStyle: CSSProperties = {
position: 'fixed',
top: y,
}
if (anchor === 'left') {
style.left = x
tooltipStyle.left = x
} else {
style.right = x
tooltipStyle.right = x
}
return (
<div
className="histogram-chart-tooltip"
style={style}
data-group={datum.group}
>
<div className="histogram-chart-tooltip--value">{datum.value}</div>
<div className="histogram-chart-tooltip--group">{datum.group}</div>
<div className="histogram-chart-tooltip" style={tooltipStyle}>
<div className="histogram-chart-tooltip--column">
{data.map(d => (
<div key={d.key} style={{color: colorScale(d.group)}}>
{d.value}
</div>
))}
</div>
<div className="histogram-chart-tooltip--column">
{data.map(d => (
<div key={d.key} style={{color: colorScale(d.group)}}>
{d.group}
</div>
))}
</div>
</div>
)
}

View File

@ -98,32 +98,28 @@ const Layout = (
<WidgetCell cell={cell} timeRange={timeRange} source={source} />
) : (
<RefreshingGraph
colors={colors}
inView={cell.inView}
axes={axes}
type={type}
isDragging={isDragging}
tableOptions={tableOptions}
fieldOptions={fieldOptions}
timeFormat={timeFormat}
decimalPlaces={decimalPlaces}
staticLegend={IS_STATIC_LEGEND(legend)}
cellHeight={h}
onZoom={onZoom}
colors={colors}
sources={sources}
inView={cell.inView}
timeRange={timeRange}
templates={templates}
isDragging={isDragging}
timeFormat={timeFormat}
autoRefresh={autoRefresh}
tableOptions={tableOptions}
fieldOptions={fieldOptions}
decimalPlaces={decimalPlaces}
manualRefresh={manualRefresh}
onSetResolution={onSetResolution}
staticLegend={IS_STATIC_LEGEND(legend)}
onStopAddAnnotation={onStopAddAnnotation}
grabDataForDownload={grabDataForDownload}
queries={buildQueriesForLayouts(
cell,
getSource(cell, source, sources, defaultSource),
timeRange,
host
)}
onSetResolution={onSetResolution}
queries={buildQueriesForLayouts(cell, timeRange, host)}
source={getSource(cell, source, sources, defaultSource)}
/>
)}
</LayoutCell>
@ -131,10 +127,6 @@ const Layout = (
const {arrayOf, bool, func, number, shape, string} = PropTypes
Layout.contextTypes = {
source: shape(),
}
const propTypes = {
isDragging: bool,
autoRefresh: number.isRequired,

View File

@ -18,6 +18,7 @@ const validateTimeSeries = timeseries => {
)
)
}
@ErrorHandlingWith(InvalidData)
class LineGraph extends Component {
constructor(props) {
@ -60,7 +61,6 @@ class LineGraph extends Component {
cellID,
onZoom,
queries,
hoverTime,
timeRange,
cellHeight,
ruleValues,
@ -120,7 +120,6 @@ class LineGraph extends Component {
labels={labels}
queries={queries}
options={options}
hoverTime={hoverTime}
timeRange={timeRange}
isBarGraph={isBarGraph}
timeSeries={timeSeries}
@ -182,7 +181,6 @@ LineGraph.propTypes = {
label: string,
}),
}),
hoverTime: string,
handleSetHoverTime: func,
title: string,
isFetchingInitially: bool,

View File

@ -31,7 +31,7 @@ const RefreshingGraph = ({
onZoom,
cellID,
queries,
hoverTime,
source,
tableOptions,
templates,
timeRange,
@ -62,6 +62,7 @@ const RefreshingGraph = ({
if (type === 'single-stat') {
return (
<RefreshingSingleStat
source={source}
type={type}
colors={colors}
key={manualRefresh}
@ -81,6 +82,7 @@ const RefreshingGraph = ({
if (type === 'gauge') {
return (
<RefreshingGaugeChart
source={source}
type={type}
colors={colors}
key={manualRefresh}
@ -103,10 +105,11 @@ const RefreshingGraph = ({
return (
<RefreshingTableGraph
type={type}
source={source}
cellID={cellID}
colors={colors}
inView={inView}
hoverTime={hoverTime}
isInCEO={isInCEO}
key={manualRefresh}
queries={queries}
templates={templates}
@ -120,7 +123,6 @@ const RefreshingGraph = ({
resizerTopHeight={resizerTopHeight}
grabDataForDownload={grabDataForDownload}
handleSetHoverTime={handleSetHoverTime}
isInCEO={isInCEO}
onSetResolution={onSetResolution}
/>
)
@ -135,6 +137,7 @@ const RefreshingGraph = ({
<RefreshingLineGraph
type={type}
axes={axes}
source={source}
cellID={cellID}
colors={colors}
onZoom={onZoom}
@ -199,10 +202,10 @@ RefreshingGraph.propTypes = {
isEnforced: bool.isRequired,
digits: number.isRequired,
}).isRequired,
hoverTime: string.isRequired,
handleSetHoverTime: func.isRequired,
isInCEO: bool,
onSetResolution: func,
source: shape().isRequired,
}
RefreshingGraph.defaultProps = {
@ -213,9 +216,8 @@ RefreshingGraph.defaultProps = {
decimalPlaces: DEFAULT_DECIMAL_PLACES,
}
const mapStateToProps = ({dashboardUI, annotations: {mode}}) => ({
const mapStateToProps = ({annotations: {mode}}) => ({
mode,
hoverTime: dashboardUI.hoverTime,
})
const mapDispatchToProps = dispatch => ({

View File

@ -29,16 +29,21 @@ class StaticLegend extends Component {
componentDidMount = () => {
const {height} = this.staticLegendRef.getBoundingClientRect()
this.props.handleReceiveStaticLegendHeight(height)
this.props.onUpdateHeight(height)
}
componentDidUpdate = () => {
componentDidUpdate = prevProps => {
const {height} = this.staticLegendRef.getBoundingClientRect()
this.props.handleReceiveStaticLegendHeight(height)
if (prevProps.height === height) {
return
}
this.props.onUpdateHeight(height)
}
componentWillUnmount = () => {
this.props.handleReceiveStaticLegendHeight(null)
this.props.onUpdateHeight(0)
}
handleClick = i => e => {
@ -96,12 +101,13 @@ class StaticLegend extends Component {
}
}
const {shape, func} = PropTypes
const {shape, func, number} = PropTypes
StaticLegend.propTypes = {
dygraphSeries: shape({}),
dygraph: shape({}),
handleReceiveStaticLegendHeight: func.isRequired,
height: number.isRequired,
onUpdateHeight: func.isRequired,
}
export default StaticLegend

View File

@ -589,8 +589,12 @@ class TableGraph extends Component<Props, State> {
}
}
const mstp = ({dashboardUI}) => ({
hoverTime: dashboardUI.hoverTime,
})
const mapDispatchToProps = dispatch => ({
handleUpdateFieldOptions: bindActionCreators(updateFieldOptions, dispatch),
})
export default connect(null, mapDispatchToProps)(TableGraph)
export default connect(mstp, mapDispatchToProps)(TableGraph)

View File

@ -3,6 +3,9 @@ import _ from 'lodash'
import {TemplateValueType, TemplateType} from 'src/types'
import {CellType} from 'src/types/dashboards'
export const DEFAULT_DURATION_MS = 1000
export const DEFAULT_PIXELS = 333
export const NO_CELL = 'none'
export const PERMISSIONS = {
@ -439,10 +442,9 @@ export const DEFAULT_SOURCE = {
metaUrl: '',
}
export const defaultIntervalValue = '333'
export const intervalValuesPoints = [
{
value: defaultIntervalValue,
value: `${DEFAULT_PIXELS}`,
type: TemplateValueType.Points,
selected: true,
picked: true,

View File

@ -114,8 +114,8 @@ export const barPlotter = e => {
}
}
export const makeLegendStyles = (graph, legend, pageX) => {
if (!graph || !legend || pageX === null) {
export const makeLegendStyles = (graph, legend, hoverTimeX) => {
if (!graph || !legend || hoverTimeX === null) {
return {}
}
@ -131,21 +131,20 @@ export const makeLegendStyles = (graph, legend, pageX) => {
const legendHeight = legendRect.height
const screenHeight = window.innerHeight
const legendMaxLeft = graphWidth - legendWidth / 2
const trueGraphX = pageX - graphRect.left
let legendLeft = trueGraphX
let legendLeft = hoverTimeX
// Enforcing max & min legend offsets
if (trueGraphX < legendWidth / 2) {
if (hoverTimeX < legendWidth / 2) {
legendLeft = legendWidth / 2
} else if (trueGraphX > legendMaxLeft) {
} else if (hoverTimeX > legendMaxLeft) {
legendLeft = legendMaxLeft
}
// Disallow screen overflow of legend
const isLegendBottomClipped = graphBottom + legendHeight > screenHeight
const isLegendTopClipped = legendHeight > graphRect.top - chronografChromeSize
const willLegendFitLeft = pageX - chronografChromeSize > legendWidth
const willLegendFitLeft = hoverTimeX - chronografChromeSize > legendWidth
let legendTop = graphHeight + 8
@ -159,10 +158,10 @@ export const makeLegendStyles = (graph, legend, pageX) => {
legendTop = 0
if (willLegendFitLeft) {
legendLeft = trueGraphX - legendWidth / 2
legendLeft = hoverTimeX - legendWidth / 2
legendLeft -= 8
} else {
legendLeft = trueGraphX + legendWidth / 2
legendLeft = hoverTimeX + legendWidth / 2
legendLeft += 32
}
}

View File

@ -6,7 +6,7 @@ import * as actions from 'src/shared/actions/sources'
import {notify as notifyAction} from 'src/shared/actions/notifications'
import FancyScrollbar from 'src/shared/components/FancyScrollbar'
import PageHeader from 'src/shared/components/PageHeader'
import PageHeader from 'src/reusable_ui/components/page_layout/PageHeader'
import InfluxTable from 'src/sources/components/InfluxTable'
import {

View File

@ -15,7 +15,7 @@ import {connect} from 'react-redux'
import Notifications from 'src/shared/components/Notifications'
import SourceForm from 'src/sources/components/SourceForm'
import FancyScrollbar from 'src/shared/components/FancyScrollbar'
import PageHeader from 'src/shared/components/PageHeader'
import PageHeader from 'src/reusable_ui/components/page_layout/PageHeader'
import {DEFAULT_SOURCE} from 'src/shared/constants'
const INITIAL_PATH = '/sources/new'

View File

@ -4,7 +4,7 @@ import FancyScrollbar from 'src/shared/components/FancyScrollbar'
import LayoutRenderer from 'src/shared/components/LayoutRenderer'
import {STATUS_PAGE_TIME_RANGE} from 'src/shared/data/timeRanges'
import {AUTOREFRESH_DEFAULT} from 'src/shared/constants'
import PageHeader from 'src/shared/components/PageHeader'
import PageHeader from 'src/reusable_ui/components/page_layout/PageHeader'
import {fixtureStatusPageCells} from 'src/status/fixtures'
import {ErrorHandling} from 'src/shared/decorators/errors'

View File

@ -3,17 +3,13 @@
------------------------------------------------------------------------------
*/
%crosshair-styles {
position: absolute;
cursor: pointer;
}
.crosshair {
@extend %crosshair-styles;
left: 0;
top: 0;
height: calc(100% - 20px);
width: 0.5px;
background-color: $g14-chromium;
pointer-events: none;
position: absolute;
width: 1px;
z-index: 3;
background: linear-gradient(to bottom, fade-out($g14-chromium, 1) 0%,$g14-chromium 7%,$g14-chromium 93%,fade-out($g14-chromium, 1) 100%);
pointer-events: none;
min-height: 20px;
}

View File

@ -48,13 +48,13 @@
}
.dygraph-axis-label-y {
padding: 0 9px 0 0 !important;
text-align: left !important;
text-align: right !important;
left: 0 !important;
user-select: none;
}
.dygraph-axis-label-y2 {
padding: 0 0 0 9px !important;
text-align: right !important;
text-align: left !important;
user-select: none;
}
@ -84,10 +84,10 @@
.graph--hasYLabel {
.dygraph-axis-label-y {
padding: 0 1px 0 16px !important;
padding: 0 4px 0 0 !important;
}
.dygraph-axis-label-y2 {
padding: 0 16px 0 1px !important;
padding: 0 0 0 4px !important;
}
}

View File

@ -1,61 +1,28 @@
@keyframes blur-in {
from {
filter: blur(0);
}
to {
filter: blur(2px);
}
}
@keyframes blur-out {
from {
filter: blur(2px);
}
to {
filter: blur(0);
}
}
.histogram-chart {
user-select: none;
&:not(.loading) {
animation-duration: 0.1s;
animation-name: blur-out;
}
&.loading {
animation-duration: 0.3s;
animation-name: blur-in;
animation-fill-mode: forwards;
}
}
.histogram-chart-bars--bar {
shape-rendering: crispEdges;
fill: $c-amethyst;
opacity: 1;
pointer: cursor;
shape-rendering: crispEdges;
cursor: pointer;
}
.histogram-chart--axes, .histogram-chart-skeleton {
.x-label, .y-label {
fill: $g13-mist;
font-size: 12px;
font-weight: bold;
fill: $g11-sidewalk;
font-size: 11px;
font-weight: 600;
}
.x-label {
text-anchor: middle;
alignment-baseline: hanging;
dominant-baseline: hanging;
}
.y-label {
text-anchor: end;
alignment-baseline: middle;
dominant-baseline: middle;
}
.y-tick {
@ -66,7 +33,7 @@
}
.histogram-chart-skeleton, .histogram-chart:not(.loading) .x-brush--area {
.histogram-chart-skeleton, .histogram-chart .x-brush--area {
cursor: crosshair;
}
@ -83,17 +50,17 @@
.histogram-chart-tooltip {
padding: 8px;
background-color: $g0-obsidian;
border-radius: 3px;
border-radius: $radius-small;
@extend %drop-shadow;
font-size: 12px;
font-weight: 600;
color: $g13-mist;
display: flex;
align-items: space-between;
transform: translate(0, -50%);
pointer-events: none;
.histogram-chart-tooltip--value {
margin-right: 10px;
}
display: flex;
justify-content: space-between;
}
.histogram-chart-tooltip--column:first-child {
margin-right: 12px;
}

View File

@ -189,131 +189,6 @@ $threesizer-shadow-stop: fade-out($g0-obsidian, 1);
margin-top: 10px;
}
.dash-graph-context--button {
width: 24px;
height: 24px;
border-radius: 3px;
font-size: 12px;
position: relative;
color: $g11-sidewalk;
margin-right: 2px;
transition: color 0.25s ease, background-color 0.25s ease;
&:hover,
&.active {
cursor: pointer;
color: $g20-white;
background-color: $g5-pepper;
}
&:last-child {
margin-right: 0;
}
>.icon {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
&.active {
position: relative;
z-index: 20;
}
}
.dash-graph-context--menu,
.dash-graph-context--menu.default {
z-index: 3;
position: absolute;
top: calc(100% + 8px);
left: 50%;
background-color: $g6-smoke;
transform: translateX(-50%);
border-radius: 3px;
display: flex;
flex-direction: column;
align-items: stretch;
justify-content: center;
&:before {
position: absolute;
content: '';
border: 6px solid transparent;
border-bottom-color: $g6-smoke;
left: 50%;
top: 0;
transform: translate(-50%, -100%);
transition: border-color 0.25s ease;
}
.dash-graph-context--menu-item {
@include no-user-select();
white-space: nowrap;
font-size: 12px;
font-weight: 700;
line-height: 26px;
height: 26px;
padding: 0 10px;
color: $g20-white;
transition: background-color 0.25s ease;
&:first-child {
border-top-left-radius: 3px;
border-top-right-radius: 3px;
}
&:last-child {
border-bottom-left-radius: 3px;
border-bottom-right-radius: 3px;
}
&:hover {
background-color: $g8-storm;
cursor: pointer;
}
&.disabled,
&.disabled:hover {
cursor: default;
background-color: transparent;
font-style: italic;
color: $g11-sidewalk;
}
}
}
.dash-graph-context--menu.primary {
background-color: $c-ocean;
&:before {
border-bottom-color: $c-ocean;
}
.dash-graph-context--menu-item:hover {
background-color: $c-pool;
}
}
.dash-graph-context--menu.warning {
background-color: $c-star;
&:before {
border-bottom-color: $c-star;
}
.dash-graph-context--menu-item:hover {
background-color: $c-comet;
}
}
.dash-graph-context--menu.success {
background-color: $c-rainforest;
&:before {
border-bottom-color: $c-rainforest;
}
.dash-graph-context--menu-item:hover {
background-color: $c-honeydew;
}
}
.dash-graph-context--menu.danger {
background-color: $c-curacao;
&:before {
border-bottom-color: $c-curacao;
}
.dash-graph-context--menu-item:hover {
background-color: $c-dreamsicle;
}
}
// Header Dropdown Menu
.threesizer--menu {
.dropdown-menu {

View File

@ -211,6 +211,12 @@ $dash-graph-options-arrow: 8px;
display: flex;
align-items: center;
flex-wrap: nowrap;
opacity: 0;
transition: opacity 0.25s ease;
}
.dash-graph:hover .dash-graph-context {
opacity: 1;
}
.dash-graph-context.dash-graph-context__open {

View File

@ -1,5 +1,5 @@
/*
Styles for Logs Viewer Page
Styles for Log Viewer Page
----------------------------------------------------------------------------
*/
@ -9,23 +9,6 @@ $logs-viewer-filter-height: 42px;
$logs-viewer-results-text-indent: 33px;
$logs-viewer-gutter: 60px;
$severity-emerg: $c-ruby;
$severity-alert: $c-fire;
$severity-crit: $c-curacao;
$severity-err: $c-tiger;
$severity-warning: $c-pineapple;
$severity-notice: $c-rainforest;
$severity-info: $c-star;
$severity-debug: $g9-mountain;
$severity-emerg-intense: $c-fire;
$severity-alert-intense: $c-curacao;
$severity-crit-intense: $c-tiger;
$severity-err-intense: $c-pineapple;
$severity-warning-intense: $c-thunder;
$severity-notice-intense: $c-honeydew;
$severity-info-intense: $c-comet;
$severity-debug-intense: $g10-wolf;
.logs-viewer {
display: flex;
flex-direction: column;
@ -34,7 +17,7 @@ $severity-debug-intense: $g10-wolf;
}
.logs-viewer--graph-container {
padding: 22px ($logs-viewer-gutter - 16px) 10px ($logs-viewer-gutter - 16px);
padding: 22px $logs-viewer-gutter 10px $logs-viewer-gutter;
height: $logs-viewer-graph-height;
@include gradient-v($g2-kevlar, $g0-obsidian);
display: flex;
@ -231,14 +214,7 @@ $severity-debug-intense: $g10-wolf;
width: 170px;
}
// Graph
.logs-viewer--graph {
position: relative;
width: 100%;
height: 100%;
padding: 8px 16px;
}
// Table Dots
.logs-viewer--dot {
width: 12px;
height: 12px;
@ -247,31 +223,6 @@ $severity-debug-intense: $g10-wolf;
background-color: $g0-obsidian;
border: 2px solid $g3-castle;
margin-left: 2px;
&.emerg-severity {
@include gradient-diag-up($severity-emerg, $severity-emerg-intense);
}
&.alert-severity {
@include gradient-diag-up($severity-alert, $severity-alert-intense);
}
&.crit-severity {
@include gradient-diag-up($severity-crit, $severity-crit-intense);
}
&.err-severity {
@include gradient-diag-up($severity-err, $severity-err-intense);
}
&.warning-severity {
@include gradient-diag-up($severity-warning, $severity-warning-intense);
}
&.notice-severity {
@include gradient-diag-up($severity-notice, $severity-notice-intense);
}
&.info-severity {
@include gradient-diag-up($severity-info, $severity-info-intense);
}
&.debug-severity {
@include gradient-diag-up($severity-debug, $severity-debug-intense);
}
}
// Play & Pause Toggle in Header
@ -327,79 +278,3 @@ $severity-debug-intense: $g10-wolf;
background-color: $c-laser;
}
}
.logs-viewer .histogram-chart-bars--bar, .logs-viewer .histogram-chart-tooltip {
&[data-group="emerg"] {
fill: $severity-emerg;
color: $severity-emerg;
}
&[data-group="alert"] {
fill: $severity-alert;
color: $severity-alert;
}
&[data-group="crit"] {
fill: $severity-crit;
color: $severity-crit;
}
&[data-group="err"] {
fill: $severity-err;
color: $severity-err;
}
&[data-group="warning"] {
fill: $severity-warning;
color: $severity-warning;
}
&[data-group="notice"] {
fill: $severity-notice;
color: $severity-notice;
}
&[data-group="info"] {
fill: $severity-info;
color: $severity-info;
}
&[data-group="debug"] {
fill: $severity-debug;
color: $severity-debug;
}
}
.logs-viewer .histogram-chart-bars--bar:hover {
&[data-group="emerg"] {
fill: $severity-emerg-intense;
}
&[data-group="alert"] {
fill: $severity-alert-intense;
}
&[data-group="crit"] {
fill: $severity-crit-intense;
}
&[data-group="err"] {
fill: $severity-err-intense;
}
&[data-group="warning"] {
fill: $severity-warning-intense;
}
&[data-group="notice"] {
fill: $severity-notice-intense;
}
&[data-group="info"] {
fill: $severity-info-intense;
}
&[data-group="debug"] {
fill: $severity-debug-intense;
}
}

View File

@ -179,13 +179,6 @@ export const insertTempVar = (query, tempVar) => {
export const unMask = query => {
return query.replace(/😸/g, ':')
}
export const removeUnselectedTemplateValues = templates => {
return templates.map(template => {
const selectedValues = template.values.filter(value => value.selected)
return {...template, values: selectedValues}
})
}
export const TEMPLATE_RANGE: TimeRange = {
upper: null,
lower: TEMP_VAR_DASHBOARD_TIME,

View File

@ -0,0 +1,118 @@
import {Template, TemplateValueType, TemplateValue} from 'src/types/tempVars'
import {
TEMP_VAR_INTERVAL,
DEFAULT_PIXELS,
DEFAULT_DURATION_MS,
} from 'src/shared/constants'
export const replaceInterval = (
query: string,
pixels: number,
durationMs: number
) => {
if (!query.includes(TEMP_VAR_INTERVAL)) {
return query
}
if (!pixels) {
pixels = DEFAULT_PIXELS
}
if (!durationMs) {
durationMs = DEFAULT_DURATION_MS
}
// duration / width of visualization in pixels
const msPerPixel = Math.floor(durationMs / pixels)
return replaceAll(query, TEMP_VAR_INTERVAL, `${msPerPixel}ms`)
}
const templateReplace = (query: string, tempVars: Template[]) => {
const replacedQuery = tempVars.reduce((acc, template) => {
return renderTemplate(acc, template)
}, query)
return replacedQuery
}
const renderTemplate = (query: string, template: Template): string => {
if (!template.values.length) {
return query
}
if (query && !query.includes(template.tempVar)) {
return query
}
const templateValue: TemplateValue = template.values.find(v => v.selected)
if (!templateValue) {
return query
}
const {tempVar} = template
const {value, type} = templateValue
let q = ''
// First replace all template variable types in regular expressions. Values should appear unquoted.
switch (type) {
case TemplateValueType.TagKey:
case TemplateValueType.FieldKey:
case TemplateValueType.Measurement:
case TemplateValueType.Database:
case TemplateValueType.TagValue:
case TemplateValueType.TimeStamp:
q = replaceAllRegex(query, tempVar, value)
break
default:
q = query
}
// Then render template variables not in regular expressions
switch (type) {
case TemplateValueType.TagKey:
case TemplateValueType.FieldKey:
case TemplateValueType.Measurement:
case TemplateValueType.Database:
return replaceAll(q, tempVar, `"${value}"`)
case TemplateValueType.TagValue:
case TemplateValueType.TimeStamp:
return replaceAll(q, tempVar, `'${value}'`)
case TemplateValueType.CSV:
case TemplateValueType.Constant:
case TemplateValueType.MetaQuery:
return replaceAll(q, tempVar, value)
default:
return query
}
}
const replaceAllRegex = (
query: string,
search: string,
replacement: string
) => {
// check for presence of anything between two forward slashes /[your stuff here]/
const matches = query.match(/\/([^\/]*)\//gm)
if (!matches) {
return query
}
return matches.reduce((acc, m) => {
if (m.includes(search)) {
const replaced = m.replace(search, replacement)
return acc.split(m).join(replaced)
}
return acc
}, query)
}
const replaceAll = (query: string, search: string, replacement: string) => {
return query.split(search).join(replacement)
}
export default templateReplace

View File

@ -22,3 +22,12 @@ export interface Margins {
bottom: number
left: number
}
export interface HoverData {
data: HistogramData
x: number
y: number
anchor: TooltipAnchor
}
export type ColorScale = (color: string) => string

View File

@ -3,7 +3,6 @@ import {
TimeRange,
Namespace,
Source,
RemoteDataState,
} from 'src/types'
export interface Filter {
@ -25,7 +24,6 @@ export interface LogsState {
timeRange: TimeRange
histogramQueryConfig: QueryConfig | null
histogramData: object[]
histogramDataStatus: RemoteDataState
tableQueryConfig: QueryConfig | null
tableData: TableData
searchTerm: string | null

View File

@ -1,7 +1,6 @@
import {Source} from 'src/types'
export interface Query {
host: string[] // doesn't come from server - is set in buildQueriesForGraphs
text: string
id: string
queryConfig: QueryConfig

View File

@ -11,6 +11,7 @@ export enum TemplateValueType {
Points = 'points',
Constant = 'constant',
MetaQuery = 'influxql',
TimeStamp = 'timeStamp',
}
export interface TemplateValue {

View File

@ -1,5 +1,4 @@
import _ from 'lodash'
import {getDeep} from 'src/utils/wrappers'
import {buildQuery} from 'src/utils/influxql'
import {TYPE_QUERY_CONFIG, TYPE_SHIFTED} from 'src/dashboards/constants'
@ -11,11 +10,7 @@ interface Statement {
text: string
}
const buildQueries = (
proxy: string,
queryConfigs: QueryConfig[],
tR: TimeRange
): Query[] => {
const buildQueries = (queryConfigs: QueryConfig[], tR: TimeRange): Query[] => {
const statements: Statement[] = queryConfigs.map((query: QueryConfig) => {
const {rawText, range, id, shifts, database, measurement, fields} = query
const timeRange: TimeRange = range || tR
@ -42,11 +37,7 @@ const buildQueries = (
const queries: Query[] = statements
.filter(s => s.text !== null)
.map(({queryConfig, text, id}) => {
const queryProxy = getDeep<string>(queryConfig, 'source.links.proxy', '')
const host: string[] = [queryProxy || proxy]
return {
host,
text,
id,
queryConfig,

View File

@ -8,7 +8,7 @@ import {
} from 'src/shared/constants'
import {timeRanges} from 'src/shared/data/timeRanges'
import {Cell, CellQuery, LayoutQuery, Source, TimeRange} from 'src/types'
import {Cell, CellQuery, LayoutQuery, TimeRange} from 'src/types'
const buildCannedDashboardQuery = (
query: LayoutQuery | CellQuery,
@ -84,7 +84,6 @@ const addTimeBoundsToRawText = (rawText: string): string => {
export const buildQueriesForLayouts = (
cell: Cell,
source: Source,
timeRange: TimeRange,
host: string
): CellQuery[] => {
@ -117,6 +116,6 @@ export const buildQueriesForLayouts = (
queryText = buildCannedDashboardQuery(query, timeRange, host)
}
return {...query, host: source.links.proxy, text: queryText}
return {...query, text: queryText}
})
}

View File

@ -1,30 +1,19 @@
import AJAX from 'src/utils/ajax'
interface ProxyQuery {
source: string | string[]
source: string
query: string
db?: string
rp?: string
tempVars?: any[]
resolution?: number
}
export async function proxy<T = any>({
source,
query,
db,
rp,
tempVars,
resolution,
}: ProxyQuery) {
export async function proxy<T = any>({source, query, db, rp}: ProxyQuery) {
try {
return await AJAX<T>({
method: 'POST',
url: source,
data: {
tempVars,
query,
resolution,
db,
rp,
},

View File

@ -2,8 +2,8 @@ import React from 'react'
import {shallow} from 'enzyme'
import {DisconnectedAdminInfluxDBPage} from 'src/admin/containers/AdminInfluxDBPage'
import PageHeader from 'src/shared/components/PageHeader'
import Title from 'src/shared/components/PageHeaderTitle'
import PageHeader from 'src/reusable_ui/components/page_layout/PageHeader'
import Title from 'src/reusable_ui/components/page_layout/PageHeaderTitle'
import {source} from 'test/resources'
describe('AdminInfluxDBPage', () => {

View File

@ -3,8 +3,8 @@ import {shallow} from 'enzyme'
import {HostsPage} from 'src/hosts/containers/HostsPage'
import HostsTable from 'src/hosts/components/HostsTable'
import PageHeader from 'src/shared/components/PageHeader'
import Title from 'src/shared/components/PageHeaderTitle'
import PageHeader from 'src/reusable_ui/components/page_layout/PageHeader'
import Title from 'src/reusable_ui/components/page_layout/PageHeaderTitle'
import {source} from 'test/resources'

View File

@ -1,7 +1,7 @@
import React from 'react'
import {mount} from 'enzyme'
import PageHeader from 'src/shared/components/PageHeader'
import PageHeader from 'src/reusable_ui/components/page_layout/PageHeader'
import TickscriptHeader from 'src/kapacitor/components/TickscriptHeader'
import TickscriptSave from 'src/kapacitor/components/TickscriptSave'
import {source} from 'test/resources'

View File

@ -2,7 +2,7 @@ import React from 'react'
import {shallow} from 'enzyme'
import {TickscriptPage} from 'src/kapacitor/containers/TickscriptPage'
import TickscriptHeader from 'src/kapacitor/components/TickscriptHeader'
import PageHeader from 'src/shared/components/PageHeader'
import PageHeader from 'src/reusable_ui/components/page_layout/PageHeader'
import TickscriptSave from 'src/kapacitor/components/TickscriptSave'
import {source, kapacitorRules} from 'test/resources'

View File

@ -623,6 +623,15 @@ export const template: Template = {
],
}
export const emptyTemplate: Template = {
id: '1',
type: TemplateType.CSV,
label: '',
tempVar: '',
query: {},
values: [],
}
export const dashboard: Dashboard = {
id: 1,
cells: [],

View File

@ -4,6 +4,7 @@ import AutoRefresh, {
} from 'src/shared/components/AutoRefresh'
import React, {Component} from 'react'
import {shallow} from 'enzyme'
import {source} from 'test/resources'
type ComponentProps = Props & OriginalProps
@ -33,6 +34,7 @@ const defaultProps = {
isFetchingInitially: false,
isRefreshing: false,
queryASTs: [],
source,
}
const setup = (overrides: Partial<ComponentProps> = {}) => {

View File

@ -1,5 +1,5 @@
import React from 'react'
import {mount, shallow} from 'enzyme'
import {mount} from 'enzyme'
import HistogramChart from 'src/shared/components/HistogramChart'
import HistogramChartTooltip from 'src/shared/components/HistogramChartTooltip'
@ -13,6 +13,7 @@ describe('HistogramChart', () => {
dataStatus: RemoteDataState.Done,
width: 600,
height: 400,
colorScale: () => 'blue',
onZoom: () => {},
}
@ -27,6 +28,7 @@ describe('HistogramChart', () => {
dataStatus: RemoteDataState.Done,
width: 0,
height: 0,
colorScale: () => 'blue',
onZoom: () => {},
}
@ -45,6 +47,7 @@ describe('HistogramChart', () => {
dataStatus: RemoteDataState.Done,
width: 600,
height: 400,
colorScale: () => 'blue',
onZoom: () => {},
}
@ -63,6 +66,7 @@ describe('HistogramChart', () => {
dataStatus: RemoteDataState.Done,
width: 600,
height: 400,
colorScale: () => 'blue',
onZoom: () => {},
}
@ -70,9 +74,6 @@ describe('HistogramChart', () => {
const fakeMouseOverEvent = {
target: {
dataset: {
key: '0',
},
getBoundingClientRect() {
return {top: 10, right: 10, bottom: 5, left: 5}
},
@ -80,7 +81,7 @@ describe('HistogramChart', () => {
}
wrapper
.find('.histogram-chart')
.find('.histogram-chart-bars--bars')
.first()
.simulate('mouseover', fakeMouseOverEvent)
@ -88,18 +89,4 @@ describe('HistogramChart', () => {
expect(tooltip).toMatchSnapshot()
})
test('has a "loading" class if data is reloading', () => {
const props = {
data: [{key: '', time: 0, value: 0, group: ''}],
dataStatus: RemoteDataState.Loading,
width: 600,
height: 400,
onZoom: () => {},
}
const wrapper = shallow(<HistogramChart {...props} />)
expect(wrapper.find('.histogram-chart').hasClass('loading')).toBe(true)
})
})

View File

@ -1,7 +1,7 @@
import React from 'react'
import {shallow} from 'enzyme'
import PageHeader from 'src/shared/components/PageHeader'
import PageHeader from 'src/reusable_ui/components/page_layout/PageHeader'
describe('PageHeader', () => {
it('should throw an error if neither titleText nor titleComponents is supplied', () => {

View File

@ -2,6 +2,7 @@
exports[`HistogramChart displays a HistogramChartSkeleton if empty data is passed 1`] = `
<HistogramChart
colorScale={[Function]}
data={Array []}
dataStatus="Done"
height={400}
@ -72,38 +73,60 @@ exports[`HistogramChart displays a HistogramChartSkeleton if empty data is passe
exports[`HistogramChart displays a HistogramChartTooltip when hovering over bars 1`] = `
<HistogramChartTooltip
anchor="left"
datum={
colorScale={[Function]}
data={
Object {
"group": "a",
"key": "0",
"time": 0,
"value": 0,
"anchor": "left",
"data": Array [
Object {
"group": "a",
"key": "1",
"time": 1,
"value": 1,
},
],
"x": 5,
"y": 0,
}
}
x={15}
y={7.5}
>
<div
className="histogram-chart-tooltip"
data-group="a"
style={
Object {
"left": 15,
"left": 5,
"position": "fixed",
"top": 7.5,
"top": 0,
}
}
>
<div
className="histogram-chart-tooltip--value"
className="histogram-chart-tooltip--column"
>
0
<div
key="1"
style={
Object {
"color": "blue",
}
}
>
1
</div>
</div>
<div
className="histogram-chart-tooltip--group"
className="histogram-chart-tooltip--column"
>
a
<div
key="1"
style={
Object {
"color": "blue",
}
}
>
a
</div>
</div>
</div>
</HistogramChartTooltip>
@ -111,6 +134,7 @@ exports[`HistogramChart displays a HistogramChartTooltip when hovering over bars
exports[`HistogramChart displays a nothing if passed width and height of 0 1`] = `
<HistogramChart
colorScale={[Function]}
data={Array []}
dataStatus="Done"
height={0}
@ -121,6 +145,7 @@ exports[`HistogramChart displays a nothing if passed width and height of 0 1`] =
exports[`HistogramChart displays the visualization with bars if nonempty data is passed 1`] = `
<HistogramChart
colorScale={[Function]}
data={
Array [
Object {
@ -149,10 +174,8 @@ exports[`HistogramChart displays the visualization with bars if nonempty data is
width={600}
>
<svg
className="histogram-chart "
className="histogram-chart"
height={400}
onMouseOut={[Function]}
onMouseOver={[Function]}
width={600}
>
<defs>
@ -227,7 +250,7 @@ exports[`HistogramChart displays the visualization with bars if nonempty data is
<text
className="y-label"
key="0-25-625-380"
x={20}
x={18}
y={380}
>
0
@ -235,7 +258,7 @@ exports[`HistogramChart displays the visualization with bars if nonempty data is
<text
className="y-label"
key="0.5-25-625-301.875"
x={20}
x={18}
y={301.875}
>
0.5
@ -243,7 +266,7 @@ exports[`HistogramChart displays the visualization with bars if nonempty data is
<text
className="y-label"
key="1-25-625-223.75"
x={20}
x={18}
y={223.75}
>
1
@ -251,7 +274,7 @@ exports[`HistogramChart displays the visualization with bars if nonempty data is
<text
className="y-label"
key="1.5-25-625-145.625"
x={20}
x={18}
y={145.625}
>
1.5
@ -259,7 +282,7 @@ exports[`HistogramChart displays the visualization with bars if nonempty data is
<text
className="y-label"
key="2-25-625-67.5"
x={20}
x={18}
y={67.5}
>
2
@ -307,6 +330,7 @@ exports[`HistogramChart displays the visualization with bars if nonempty data is
transform="translate(25, 5)"
>
<HistogramChartBars
colorScale={[Function]}
data={
Array [
Object {
@ -330,22 +354,26 @@ exports[`HistogramChart displays the visualization with bars if nonempty data is
]
}
height={375}
onHover={[Function]}
width={575}
xScale={[Function]}
yScale={[Function]}
>
<g
className="histogram-chart-bars--bars"
data-key="1-1-193.5"
key="1-1-193.5"
onMouseOut={[Function]}
onMouseOver={[Function]}
>
<defs>
<clipPath
id="histogram-chart-bars--clip-1-1-193.5"
>
<rect
height={160.25}
rx={4}
ry={4}
height={159.25}
rx={3}
ry={3}
width={188}
x={193.5}
y={218.75}
@ -357,6 +385,7 @@ exports[`HistogramChart displays the visualization with bars if nonempty data is
clipPath="url(#histogram-chart-bars--clip-1-1-193.5)"
data-group="a"
data-key="1"
fill="#0000ff"
height={156.25}
key="1"
width={188}
@ -366,16 +395,19 @@ exports[`HistogramChart displays the visualization with bars if nonempty data is
</g>
<g
className="histogram-chart-bars--bars"
data-key="2-2-481"
key="2-2-481"
onMouseOut={[Function]}
onMouseOver={[Function]}
>
<defs>
<clipPath
id="histogram-chart-bars--clip-2-2-481"
>
<rect
height={316.5}
rx={4}
ry={4}
height={315.5}
rx={3}
ry={3}
width={188}
x={481}
y={62.5}
@ -387,6 +419,7 @@ exports[`HistogramChart displays the visualization with bars if nonempty data is
clipPath="url(#histogram-chart-bars--clip-2-2-481)"
data-group="b"
data-key="2"
fill="#0000ff"
height={312.5}
key="2"
width={188}
@ -397,10 +430,5 @@ exports[`HistogramChart displays the visualization with bars if nonempty data is
</HistogramChartBars>
</g>
</svg>
<HistogramChartTooltip
anchor="left"
x={-1}
y={-1}
/>
</HistogramChart>
`;

View File

@ -0,0 +1,233 @@
import templateReplace, {replaceInterval} from 'src/tempVars/utils/replace'
import {TemplateValueType} from 'src/types/tempVars'
import {emptyTemplate} from 'test/resources'
describe('templates.utils.replace', () => {
it('can replace select with parameters', () => {
const vars = [
{
...emptyTemplate,
tempVar: ':temperature:',
values: [{type: TemplateValueType.CSV, value: '10', selected: true}],
},
{
...emptyTemplate,
tempVar: ':field:',
values: [
{type: TemplateValueType.FieldKey, value: 'field2', selected: true},
],
},
{
...emptyTemplate,
tempVar: ':method:',
values: [
{type: TemplateValueType.CSV, value: 'SELECT', selected: true},
],
},
{
...emptyTemplate,
tempVar: ':measurement:',
values: [{type: TemplateValueType.CSV, value: `"cpu"`, selected: true}],
},
]
const query =
':method: field1, :field: FROM :measurement: WHERE temperature > :temperature:'
const expected = `SELECT field1, "field2" FROM "cpu" WHERE temperature > 10`
const actual = templateReplace(query, vars)
expect(actual).toBe(expected)
})
it('can replace all in a select with parameters and aggregates', () => {
const vars = [
{
...emptyTemplate,
tempVar: ':value:',
values: [
{
type: TemplateValueType.TagValue,
value: 'howdy.com',
selected: true,
},
],
},
{
...emptyTemplate,
tempVar: ':tag:',
values: [
{type: TemplateValueType.TagKey, value: 'host', selected: true},
],
},
{
...emptyTemplate,
tempVar: ':field:',
values: [
{type: TemplateValueType.FieldKey, value: 'field', selected: true},
],
},
]
const query = `SELECT mean(:field:) FROM "cpu" WHERE :tag: = :value: GROUP BY :tag:`
const expected = `SELECT mean("field") FROM "cpu" WHERE "host" = 'howdy.com' GROUP BY "host"`
const actual = templateReplace(query, vars)
expect(actual).toBe(expected)
})
describe('queries with a regex', () => {
it('replaces properly', () => {
const vars = [
{
...emptyTemplate,
tempVar: ':host:',
values: [
{
type: TemplateValueType.TagValue,
value: 'my-host.local',
selected: true,
},
],
},
{
...emptyTemplate,
tempVar: ':region:',
values: [
{
type: TemplateValueType.TagValue,
value: 'north',
selected: true,
},
],
},
{
...emptyTemplate,
tempVar: ':dashboardTime:',
values: [
{
value: 'now() - 1h',
type: TemplateValueType.Constant,
selected: true,
},
],
},
]
const query = `SELECT "usage_active" FROM "cpu" WHERE host =~ /^:host:$/ AND host = :host: AND region =~ /:region:/ AND time > :dashboardTime: FILL(null)`
const expected = `SELECT "usage_active" FROM "cpu" WHERE host =~ /^my-host.local$/ AND host = 'my-host.local' AND region =~ /north/ AND time > now() - 1h FILL(null)`
const actual = templateReplace(query, vars)
expect(actual).toBe(expected)
})
})
describe('with no templates', () => {
it('does not do a replacement', () => {
const query = `SELECT :field: FROM "cpu"`
const expected = query
const actual = templateReplace(query, [])
expect(actual).toBe(expected)
})
})
describe('with no template values', () => {
it('does not do a replacement', () => {
const vars = [
{
...emptyTemplate,
tempVar: ':field:',
values: [],
},
]
const query = `SELECT :field: FROM "cpu"`
const expected = query
const actual = templateReplace(query, vars)
expect(actual).toBe(expected)
})
})
describe('replaceInterval', () => {
it('can replace :interval:', () => {
const query = `SELECT mean(usage_idle) from "cpu" where time > now() - 4320h group by time(:interval:)`
const expected = `SELECT mean(usage_idle) from "cpu" where time > now() - 4320h group by time(46702702ms)`
const pixels = 333
const durationMs = 15551999999
const actual = replaceInterval(query, pixels, durationMs)
expect(actual).toBe(expected)
})
it('can replace multiple intervals', () => {
const query = `SELECT NON_NEGATIVE_DERIVATIVE(mean(usage_idle), :interval:) from "cpu" where time > now() - 4320h group by time(:interval:)`
const expected = `SELECT NON_NEGATIVE_DERIVATIVE(mean(usage_idle), 46702702ms) from "cpu" where time > now() - 4320h group by time(46702702ms)`
const pixels = 333
const durationMs = 15551999999
const actual = replaceInterval(query, pixels, durationMs)
expect(actual).toBe(expected)
})
describe('when used with other template variables', () => {
it('can work with :dashboardTime:', () => {
const vars = [
{
...emptyTemplate,
tempVar: ':dashboardTime:',
values: [
{
type: TemplateValueType.Constant,
value: 'now() - 24h',
selected: true,
},
],
},
]
const pixels = 333
const durationMs = 86399999
const query = `SELECT mean(usage_idle) from "cpu" WHERE time > :dashboardTime: group by time(:interval:)`
let actual = templateReplace(query, vars)
actual = replaceInterval(actual, pixels, durationMs)
const expected = `SELECT mean(usage_idle) from "cpu" WHERE time > now() - 24h group by time(259459ms)`
expect(actual).toBe(expected)
})
it('can handle a failing condition', () => {
const vars = [
{
...emptyTemplate,
tempVar: ':dashboardTime:',
values: [
{
type: TemplateValueType.Constant,
value: 'now() - 1h',
selected: true,
},
],
},
]
const pixels = 38
const durationMs = 3599999
const query = `SELECT mean(usage_idle) from "cpu" WHERE time > :dashboardTime: group by time(:interval:)`
let actual = templateReplace(query, vars)
actual = replaceInterval(actual, pixels, durationMs)
const expected = `SELECT mean(usage_idle) from "cpu" WHERE time > now() - 1h group by time(94736ms)`
expect(actual).toBe(expected)
})
})
describe('with no :interval: present', () => {
it('returns the query', () => {
const expected = `SELECT mean(usage_idle) FROM "cpu" WHERE time > :dashboardTime: GROUP BY time(20ms)`
const actual = replaceInterval(expected, 10, 20000)
expect(actual).toBe(expected)
})
})
})
})

View File

@ -32,6 +32,10 @@
version "0.0.56"
resolved "https://registry.yarnpkg.com/@types/codemirror/-/codemirror-0.0.56.tgz#1fcf68df0d0a49791d843dadda7d94891ac88669"
"@types/d3-color@^1.2.1":
version "1.2.1"
resolved "https://registry.yarnpkg.com/@types/d3-color/-/d3-color-1.2.1.tgz#26141c3c554e320edd40726b793570a3ae57397e"
"@types/d3-scale@^2.0.1":
version "2.0.1"
resolved "https://registry.yarnpkg.com/@types/d3-scale/-/d3-scale-2.0.1.tgz#f94cd991c50422b2e68d8f43be3f9fffdb1ae7be"
@ -2624,7 +2628,7 @@ d3-collection@1:
version "1.0.4"
resolved "https://registry.yarnpkg.com/d3-collection/-/d3-collection-1.0.4.tgz#342dfd12837c90974f33f1cc0a785aea570dcdc2"
d3-color@1:
d3-color@1, d3-color@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/d3-color/-/d3-color-1.2.0.tgz#d1ea19db5859c86854586276ec892cf93148459a"