Merge remote-tracking branch 'origin' into tempvars/select-default
commit
d9da2e3c6f
|
@ -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]
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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).
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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":[]}}]}
|
||||
`,
|
||||
},
|
||||
}
|
||||
|
|
|
@ -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": {
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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'
|
||||
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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'
|
||||
|
||||
|
|
|
@ -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 => {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -44,6 +44,7 @@ const RuleGraph = ({
|
|||
/>
|
||||
</div>
|
||||
<RefreshingLineGraph
|
||||
source={source}
|
||||
queries={queries}
|
||||
isGraphFilled={false}
|
||||
ruleValues={rule.values}
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -13,8 +13,6 @@ export const executeQueryAsync = async (
|
|||
db: namespace.database,
|
||||
rp: namespace.retentionPolicy,
|
||||
query,
|
||||
tempVars: [],
|
||||
resolution: null,
|
||||
})
|
||||
|
||||
return data
|
||||
|
|
|
@ -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" />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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})
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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>(
|
||||
|
|
|
@ -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 {
|
|
@ -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) {
|
||||
|
|
|
@ -31,5 +31,6 @@ export const getDatabasesWithRetentionPolicies = async (
|
|||
return sorted
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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}}) => ({
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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})
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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 => ({
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -11,6 +11,7 @@ export enum TemplateValueType {
|
|||
Points = 'points',
|
||||
Constant = 'constant',
|
||||
MetaQuery = 'influxql',
|
||||
TimeStamp = 'timeStamp',
|
||||
}
|
||||
|
||||
export interface TemplateValue {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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}
|
||||
})
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
|
|
|
@ -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', () => {
|
||||
|
|
|
@ -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'
|
||||
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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'
|
||||
|
||||
|
|
|
@ -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: [],
|
||||
|
|
|
@ -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> = {}) => {
|
||||
|
|
|
@ -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)
|
||||
})
|
||||
})
|
||||
|
|
|
@ -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', () => {
|
||||
|
|
|
@ -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>
|
||||
`;
|
||||
|
|
|
@ -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)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
|
@ -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"
|
||||
|
||||
|
|
Loading…
Reference in New Issue