Merge branch 'master' into logs-viewer-polish

pull/3797/head
Alex P 2018-06-28 15:29:26 -07:00
commit 7c44558958
52 changed files with 980 additions and 1343 deletions

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

@ -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

@ -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

@ -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

@ -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

@ -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

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

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

@ -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,
@ -346,6 +349,7 @@ class LogsTable extends Component<Props, State> {
title={value}
onMouseOver={this.handleMouseEnter}
data-index={rowIndex}
style={this.severityDotStyle(value)}
/>
)
} else {
@ -395,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

@ -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

@ -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

@ -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

@ -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

@ -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'
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

@ -31,6 +31,7 @@ const RefreshingGraph = ({
onZoom,
cellID,
queries,
source,
tableOptions,
templates,
timeRange,
@ -61,6 +62,7 @@ const RefreshingGraph = ({
if (type === 'single-stat') {
return (
<RefreshingSingleStat
source={source}
type={type}
colors={colors}
key={manualRefresh}
@ -80,6 +82,7 @@ const RefreshingGraph = ({
if (type === 'gauge') {
return (
<RefreshingGaugeChart
source={source}
type={type}
colors={colors}
key={manualRefresh}
@ -102,6 +105,7 @@ const RefreshingGraph = ({
return (
<RefreshingTableGraph
type={type}
source={source}
cellID={cellID}
colors={colors}
inView={inView}
@ -133,6 +137,7 @@ const RefreshingGraph = ({
<RefreshingLineGraph
type={type}
axes={axes}
source={source}
cellID={cellID}
colors={colors}
onZoom={onZoom}
@ -200,6 +205,7 @@ RefreshingGraph.propTypes = {
handleSetHoverTime: func.isRequired,
isInCEO: bool,
onSetResolution: func,
source: shape().isRequired,
}
RefreshingGraph.defaultProps = {

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,9 +442,8 @@ export const DEFAULT_SOURCE = {
metaUrl: '',
}
export const defaultIntervalValue = '333'
export const intervalValuesPoints = [
{value: defaultIntervalValue, type: TemplateValueType.Points, selected: true},
{value: `${DEFAULT_PIXELS}`, type: TemplateValueType.Points, selected: true},
]
export const interval = {

View File

@ -1,44 +1,11 @@
@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;
cursor: pointer;
shape-rendering: crispEdges;
}
.histogram-chart--axes, .histogram-chart-skeleton {
@ -50,12 +17,12 @@
.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;
}
@ -88,12 +55,12 @@
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

@ -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;
@ -240,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
@ -320,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

@ -178,13 +178,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

@ -10,6 +10,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

@ -608,6 +608,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

@ -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>
@ -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,13 +354,17 @@ 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
@ -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,7 +395,10 @@ 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
@ -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"