commit
57bf5f571a
|
@ -1,11 +1,13 @@
|
|||
## v1.2.0 [unreleased]
|
||||
|
||||
### Upcoming Bug Fixes
|
||||
1. [#865](https://github.com/influxdata/chronograf/issues/865): Support for String fields compare kapacitor rules in Chronograf UI
|
||||
|
||||
### Upcoming Features
|
||||
1. [#838](https://github.com/influxdata/chronograf/issues/838): Add detail node to kapacitor alerts
|
||||
2. [#853](https://github.com/influxdata/chronograf/issues/853): Updated builds to use yarn over npm install
|
||||
3. [#864](https://github.com/influxdata/chronograf/issues/864): Add support to kapacitor rule alert config for:
|
||||
3. [#860](https://github.com/influxdata/chronograf/issues/860): Add gzip encoding and caching of static assets to server
|
||||
4. [#864](https://github.com/influxdata/chronograf/issues/864): Add support to kapacitor rule alert config for:
|
||||
- HTTP
|
||||
- TCP
|
||||
- Exec
|
||||
|
@ -13,6 +15,11 @@
|
|||
- Alerta
|
||||
|
||||
### Upcoming UI Improvements
|
||||
1. [#822](https://github.com/influxdata/chronograf/issues/822): Simplify and improve layout of the Data Explorer
|
||||
- The Data Explorer's intention and purpose has always been the ad hoc and ephemeral exploration of your schema and data.
|
||||
The concept of `Exploration` sessions and `Panels` betrayed this initial intention. The DE turned into a "poor man's"
|
||||
dashboarding tool. In turn, this introduced complexity in the code and the UI. In the future if I want to save, manipulate,
|
||||
and view multiple visualizations this will be done more efficiently and effectively in our dashboarding solution.
|
||||
|
||||
## v1.2.0-beta1 [2017-01-27]
|
||||
|
||||
|
|
1
Godeps
1
Godeps
|
@ -1,3 +1,4 @@
|
|||
github.com/NYTimes/gziphandler 6710af535839f57c687b62c4c23d649f9545d885
|
||||
github.com/Sirupsen/logrus 3ec0642a7fb6488f65b06f9040adc67e3990296a
|
||||
github.com/boltdb/bolt 5cc10bbbc5c141029940133bb33c9e969512a698
|
||||
github.com/bouk/httprouter ee8b3818a7f51fbc94cc709b5744b52c2c725e91
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
### Go
|
||||
* github.com/NYTimes/gziphandler [APACHE-2.0](https://github.com/NYTimes/gziphandler/blob/master/LICENSE.md)
|
||||
* github.com/Sirupsen/logrus [MIT](https://github.com/Sirupsen/logrus/blob/master/LICENSE)
|
||||
* github.com/boltdb/bolt [MIT](https://github.com/boltdb/bolt/blob/master/LICENSE)
|
||||
* github.com/bouk/httprouter [BSD](https://github.com/bouk/httprouter/blob/master/LICENSE)
|
||||
|
|
|
@ -3,6 +3,7 @@ package dist
|
|||
//go:generate go-bindata -o dist_gen.go -ignore 'map|go' -pkg dist ../ui/build/...
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/elazarl/go-bindata-assetfs"
|
||||
|
@ -32,6 +33,21 @@ func (b *BindataAssets) Handler() http.Handler {
|
|||
return b
|
||||
}
|
||||
|
||||
// addCacheHeaders requests an hour of Cache-Control and sets an ETag based on file size and modtime
|
||||
func (b *BindataAssets) addCacheHeaders(filename string, w http.ResponseWriter) error {
|
||||
w.Header().Add("Cache-Control", "public, max-age=3600")
|
||||
fi, err := AssetInfo(filename)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
hour, minute, second := fi.ModTime().Clock()
|
||||
etag := fmt.Sprintf(`"%d%d%d%d%d"`, fi.Size(), fi.ModTime().Day(), hour, minute, second)
|
||||
|
||||
w.Header().Set("ETag", etag)
|
||||
return nil
|
||||
}
|
||||
|
||||
// ServeHTTP wraps http.FileServer by returning a default asset if the asset
|
||||
// doesn't exist. This supports single-page react-apps with its own
|
||||
// built-in router. Additionally, we override the content-type if the
|
||||
|
@ -52,8 +68,14 @@ func (b *BindataAssets) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||
// Additionally, because we know we are returning the default asset,
|
||||
// we need to set the default asset's content-type.
|
||||
w.Header().Set("Content-Type", b.DefaultContentType)
|
||||
if err := b.addCacheHeaders(b.Default, w); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return Asset(b.Default)
|
||||
}
|
||||
if err := b.addCacheHeaders(name, w); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return octets, nil
|
||||
}
|
||||
var dir http.FileSystem = &assetfs.AssetFS{
|
||||
|
|
|
@ -199,6 +199,280 @@ trigger
|
|||
}
|
||||
}
|
||||
|
||||
func TestThresholdStringCrit(t *testing.T) {
|
||||
alert := chronograf.AlertRule{
|
||||
Name: "haproxy",
|
||||
Trigger: "threshold",
|
||||
Alerts: []string{"email"},
|
||||
TriggerValues: chronograf.TriggerValues{
|
||||
Operator: "equal to",
|
||||
Value: "DOWN",
|
||||
},
|
||||
Every: "10s",
|
||||
Message: `Haproxy monitor : {{.ID}} : {{ index .Tags "server" }} : {{ index .Tags "pxname" }} is {{ .Level }} `,
|
||||
Details: "Email template",
|
||||
Query: chronograf.QueryConfig{
|
||||
Database: "influxdb",
|
||||
RetentionPolicy: "autogen",
|
||||
Measurement: "haproxy",
|
||||
Fields: []chronograf.Field{
|
||||
{
|
||||
Field: "status",
|
||||
Funcs: []string{"last"},
|
||||
},
|
||||
},
|
||||
GroupBy: chronograf.GroupBy{
|
||||
Time: "10s",
|
||||
Tags: []string{"pxname"},
|
||||
},
|
||||
AreTagsAccepted: true,
|
||||
},
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
alert chronograf.AlertRule
|
||||
want chronograf.TICKScript
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "Test valid template alert",
|
||||
alert: alert,
|
||||
want: `var db = 'influxdb'
|
||||
|
||||
var rp = 'autogen'
|
||||
|
||||
var measurement = 'haproxy'
|
||||
|
||||
var groupBy = ['pxname']
|
||||
|
||||
var whereFilter = lambda: TRUE
|
||||
|
||||
var period = 10s
|
||||
|
||||
var every = 10s
|
||||
|
||||
var name = 'haproxy'
|
||||
|
||||
var idVar = name + ':{{.Group}}'
|
||||
|
||||
var message = 'Haproxy monitor : {{.ID}} : {{ index .Tags "server" }} : {{ index .Tags "pxname" }} is {{ .Level }} '
|
||||
|
||||
var idTag = 'alertID'
|
||||
|
||||
var levelTag = 'level'
|
||||
|
||||
var messageField = 'message'
|
||||
|
||||
var durationField = 'duration'
|
||||
|
||||
var outputDB = 'chronograf'
|
||||
|
||||
var outputRP = 'autogen'
|
||||
|
||||
var outputMeasurement = 'alerts'
|
||||
|
||||
var triggerType = 'threshold'
|
||||
|
||||
var details = 'Email template'
|
||||
|
||||
var crit = 'DOWN'
|
||||
|
||||
var data = stream
|
||||
|from()
|
||||
.database(db)
|
||||
.retentionPolicy(rp)
|
||||
.measurement(measurement)
|
||||
.groupBy(groupBy)
|
||||
.where(whereFilter)
|
||||
|window()
|
||||
.period(period)
|
||||
.every(every)
|
||||
.align()
|
||||
|last('status')
|
||||
.as('value')
|
||||
|
||||
var trigger = data
|
||||
|alert()
|
||||
.crit(lambda: "value" == crit)
|
||||
.stateChangesOnly()
|
||||
.message(message)
|
||||
.id(idVar)
|
||||
.idTag(idTag)
|
||||
.levelTag(levelTag)
|
||||
.messageField(messageField)
|
||||
.durationField(durationField)
|
||||
.details(details)
|
||||
.email()
|
||||
|
||||
trigger
|
||||
|influxDBOut()
|
||||
.create()
|
||||
.database(outputDB)
|
||||
.retentionPolicy(outputRP)
|
||||
.measurement(outputMeasurement)
|
||||
.tag('alertName', name)
|
||||
.tag('triggerType', triggerType)
|
||||
|
||||
trigger
|
||||
|httpOut('output')
|
||||
`,
|
||||
wantErr: false,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
gen := Alert{}
|
||||
got, err := gen.Generate(tt.alert)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("%q. Threshold() error = %v, wantErr %v", tt.name, err, tt.wantErr)
|
||||
continue
|
||||
}
|
||||
if got != tt.want {
|
||||
diff := diffmatchpatch.New()
|
||||
delta := diff.DiffMain(string(tt.want), string(got), true)
|
||||
t.Errorf("%q\n%s", tt.name, diff.DiffPrettyText(delta))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Check with Nathaniel if kapacitor can do inequalities on strings
|
||||
// If it cannot, I think we should add operator checks.
|
||||
func TestThresholdStringCritGreater(t *testing.T) {
|
||||
alert := chronograf.AlertRule{
|
||||
Name: "haproxy",
|
||||
Trigger: "threshold",
|
||||
Alerts: []string{"email"},
|
||||
TriggerValues: chronograf.TriggerValues{
|
||||
Operator: "greater than",
|
||||
Value: "DOWN",
|
||||
},
|
||||
Every: "10s",
|
||||
Message: `Haproxy monitor : {{.ID}} : {{ index .Tags "server" }} : {{ index .Tags "pxname" }} is {{ .Level }} `,
|
||||
Details: "Email template",
|
||||
Query: chronograf.QueryConfig{
|
||||
Database: "influxdb",
|
||||
RetentionPolicy: "autogen",
|
||||
Measurement: "haproxy",
|
||||
Fields: []chronograf.Field{
|
||||
{
|
||||
Field: "status",
|
||||
Funcs: []string{"last"},
|
||||
},
|
||||
},
|
||||
GroupBy: chronograf.GroupBy{
|
||||
Time: "10s",
|
||||
Tags: []string{"pxname"},
|
||||
},
|
||||
AreTagsAccepted: true,
|
||||
},
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
alert chronograf.AlertRule
|
||||
want chronograf.TICKScript
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "Test valid template alert",
|
||||
alert: alert,
|
||||
want: `var db = 'influxdb'
|
||||
|
||||
var rp = 'autogen'
|
||||
|
||||
var measurement = 'haproxy'
|
||||
|
||||
var groupBy = ['pxname']
|
||||
|
||||
var whereFilter = lambda: TRUE
|
||||
|
||||
var period = 10s
|
||||
|
||||
var every = 10s
|
||||
|
||||
var name = 'haproxy'
|
||||
|
||||
var idVar = name + ':{{.Group}}'
|
||||
|
||||
var message = 'Haproxy monitor : {{.ID}} : {{ index .Tags "server" }} : {{ index .Tags "pxname" }} is {{ .Level }} '
|
||||
|
||||
var idTag = 'alertID'
|
||||
|
||||
var levelTag = 'level'
|
||||
|
||||
var messageField = 'message'
|
||||
|
||||
var durationField = 'duration'
|
||||
|
||||
var outputDB = 'chronograf'
|
||||
|
||||
var outputRP = 'autogen'
|
||||
|
||||
var outputMeasurement = 'alerts'
|
||||
|
||||
var triggerType = 'threshold'
|
||||
|
||||
var details = 'Email template'
|
||||
|
||||
var crit = 'DOWN'
|
||||
|
||||
var data = stream
|
||||
|from()
|
||||
.database(db)
|
||||
.retentionPolicy(rp)
|
||||
.measurement(measurement)
|
||||
.groupBy(groupBy)
|
||||
.where(whereFilter)
|
||||
|window()
|
||||
.period(period)
|
||||
.every(every)
|
||||
.align()
|
||||
|last('status')
|
||||
.as('value')
|
||||
|
||||
var trigger = data
|
||||
|alert()
|
||||
.crit(lambda: "value" > crit)
|
||||
.stateChangesOnly()
|
||||
.message(message)
|
||||
.id(idVar)
|
||||
.idTag(idTag)
|
||||
.levelTag(levelTag)
|
||||
.messageField(messageField)
|
||||
.durationField(durationField)
|
||||
.details(details)
|
||||
.email()
|
||||
|
||||
trigger
|
||||
|influxDBOut()
|
||||
.create()
|
||||
.database(outputDB)
|
||||
.retentionPolicy(outputRP)
|
||||
.measurement(outputMeasurement)
|
||||
.tag('alertName', name)
|
||||
.tag('triggerType', triggerType)
|
||||
|
||||
trigger
|
||||
|httpOut('output')
|
||||
`,
|
||||
wantErr: false,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
gen := Alert{}
|
||||
got, err := gen.Generate(tt.alert)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("%q. Threshold() error = %v, wantErr %v", tt.name, err, tt.wantErr)
|
||||
continue
|
||||
}
|
||||
if got != tt.want {
|
||||
diff := diffmatchpatch.New()
|
||||
delta := diff.DiffMain(string(tt.want), string(got), true)
|
||||
t.Errorf("%q\n%s", tt.name, diff.DiffPrettyText(delta))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestThresholdDetail(t *testing.T) {
|
||||
alert := chronograf.AlertRule{
|
||||
Name: "name",
|
||||
|
|
|
@ -3,6 +3,7 @@ package kapacitor
|
|||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/influxdata/chronograf"
|
||||
|
@ -39,15 +40,13 @@ func Vars(rule chronograf.AlertRule) (string, error) {
|
|||
%s
|
||||
var crit = %s
|
||||
`
|
||||
return fmt.Sprintf(vars,
|
||||
common,
|
||||
rule.TriggerValues.Value), nil
|
||||
return fmt.Sprintf(vars, common, formatValue(rule.TriggerValues.Value)), nil
|
||||
} else {
|
||||
vars := `
|
||||
%s
|
||||
var lower = %s
|
||||
var upper = %s
|
||||
`
|
||||
`
|
||||
return fmt.Sprintf(vars,
|
||||
common,
|
||||
rule.TriggerValues.Value,
|
||||
|
@ -178,3 +177,13 @@ func whereFilter(q chronograf.QueryConfig) string {
|
|||
|
||||
return "lambda: TRUE"
|
||||
}
|
||||
|
||||
// formatValue return the same string if a numeric type or if it is a string
|
||||
// will return it as a kapacitor formatted single-quoted string
|
||||
func formatValue(value string) string {
|
||||
// Test if numeric if it can be converted to a float
|
||||
if _, err := strconv.ParseFloat(value, 64); err == nil {
|
||||
return value
|
||||
}
|
||||
return "'" + value + "'"
|
||||
}
|
||||
|
|
|
@ -0,0 +1,50 @@
|
|||
package kapacitor
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/influxdata/chronograf"
|
||||
)
|
||||
|
||||
func TestVarsCritStringEqual(t *testing.T) {
|
||||
alert := chronograf.AlertRule{
|
||||
Name: "name",
|
||||
Trigger: "threshold",
|
||||
TriggerValues: chronograf.TriggerValues{
|
||||
Operator: "equal to",
|
||||
Value: "DOWN",
|
||||
},
|
||||
Every: "30s",
|
||||
Query: chronograf.QueryConfig{
|
||||
Database: "telegraf",
|
||||
Measurement: "haproxy",
|
||||
RetentionPolicy: "autogen",
|
||||
Fields: []chronograf.Field{
|
||||
{
|
||||
Field: "status",
|
||||
},
|
||||
},
|
||||
GroupBy: chronograf.GroupBy{
|
||||
Time: "10m",
|
||||
Tags: []string{"pxname"},
|
||||
},
|
||||
AreTagsAccepted: true,
|
||||
},
|
||||
}
|
||||
|
||||
raw, err := Vars(alert)
|
||||
if err != nil {
|
||||
fmt.Printf("%s", raw)
|
||||
t.Fatalf("Error generating alert: %v %s", err, raw)
|
||||
}
|
||||
|
||||
tick, err := formatTick(raw)
|
||||
if err != nil {
|
||||
t.Errorf("Error formatting alert: %v %s", err, raw)
|
||||
}
|
||||
|
||||
if err := validateTick(tick); err != nil {
|
||||
t.Errorf("Error validating alert: %v %s", err, tick)
|
||||
}
|
||||
}
|
|
@ -7,6 +7,7 @@ import (
|
|||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/NYTimes/gziphandler"
|
||||
"github.com/bouk/httprouter"
|
||||
"github.com/influxdata/chronograf" // When julienschmidt/httprouter v2 w/ context is out, switch
|
||||
"github.com/influxdata/chronograf/jwt"
|
||||
|
@ -119,7 +120,10 @@ func NewMux(opts MuxOpts, service Service) http.Handler {
|
|||
auth := AuthAPI(opts, router)
|
||||
return Logger(opts.Logger, auth)
|
||||
}
|
||||
return Logger(opts.Logger, router)
|
||||
|
||||
compressed := gziphandler.GzipHandler(router)
|
||||
logged := Logger(opts.Logger, compressed)
|
||||
return logged
|
||||
}
|
||||
|
||||
// AuthAPI adds the OAuth routes if auth is enabled.
|
||||
|
|
|
@ -22,16 +22,27 @@ type wrapResponseWriter struct {
|
|||
Substitute *io.PipeWriter
|
||||
|
||||
headerWritten bool
|
||||
dupHeader http.Header
|
||||
dupHeader *http.Header
|
||||
}
|
||||
|
||||
func (wrw wrapResponseWriter) Write(p []byte) (int, error) {
|
||||
func (wrw *wrapResponseWriter) Write(p []byte) (int, error) {
|
||||
return wrw.Substitute.Write(p)
|
||||
}
|
||||
|
||||
func (wrw wrapResponseWriter) WriteHeader(code int) {
|
||||
func (wrw *wrapResponseWriter) WriteHeader(code int) {
|
||||
if !wrw.headerWritten {
|
||||
wrw.ResponseWriter.Header().Set("Content-Type", wrw.Header().Get("Content-Type"))
|
||||
wrw.ResponseWriter.Header().Set("Content-Type", wrw.dupHeader.Get("Content-Type"))
|
||||
header := wrw.ResponseWriter.Header()
|
||||
// Filter out content length header to prevent stopping writing
|
||||
if wrw.dupHeader != nil {
|
||||
for k, v := range *wrw.dupHeader {
|
||||
if k == "Content-Length" {
|
||||
continue
|
||||
}
|
||||
header[k] = v
|
||||
}
|
||||
}
|
||||
|
||||
wrw.headerWritten = true
|
||||
}
|
||||
wrw.ResponseWriter.WriteHeader(code)
|
||||
|
@ -39,13 +50,16 @@ func (wrw wrapResponseWriter) WriteHeader(code int) {
|
|||
|
||||
// Header() copies the Header map from the underlying ResponseWriter to prevent
|
||||
// modifications to it by callers
|
||||
func (wrw wrapResponseWriter) Header() http.Header {
|
||||
wrw.dupHeader = http.Header{}
|
||||
origHeader := wrw.ResponseWriter.Header()
|
||||
for k, v := range origHeader {
|
||||
wrw.dupHeader[k] = v
|
||||
func (wrw *wrapResponseWriter) Header() http.Header {
|
||||
if wrw.dupHeader == nil {
|
||||
h := http.Header{}
|
||||
origHeader := wrw.ResponseWriter.Header()
|
||||
for k, v := range origHeader {
|
||||
h[k] = v
|
||||
}
|
||||
wrw.dupHeader = &h
|
||||
}
|
||||
return wrw.dupHeader
|
||||
return *wrw.dupHeader
|
||||
}
|
||||
|
||||
const CHUNK_SIZE int = 512
|
||||
|
@ -73,7 +87,7 @@ func (up *URLPrefixer) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
|
|||
nextRead, nextWrite := io.Pipe()
|
||||
go func() {
|
||||
defer nextWrite.Close()
|
||||
up.Next.ServeHTTP(wrapResponseWriter{ResponseWriter: rw, Substitute: nextWrite}, r)
|
||||
up.Next.ServeHTTP(&wrapResponseWriter{ResponseWriter: rw, Substitute: nextWrite}, r)
|
||||
}()
|
||||
|
||||
// setup a buffer which is the max length of our target attrs
|
||||
|
|
|
@ -1,11 +0,0 @@
|
|||
import reducer from 'src/data_explorer/reducers/dataExplorerUI';
|
||||
import {activatePanel} from 'src/data_explorer/actions/view';
|
||||
|
||||
describe('DataExplorer.Reducers.UI', () => {
|
||||
it('can set the active panel', () => {
|
||||
const activePanel = 123;
|
||||
const actual = reducer({}, activatePanel(activePanel));
|
||||
|
||||
expect(actual).to.deep.equal({activePanel});
|
||||
});
|
||||
});
|
|
@ -1,34 +0,0 @@
|
|||
import reducer from 'src/data_explorer/reducers/panels';
|
||||
import {deletePanel} from 'src/data_explorer/actions/view';
|
||||
|
||||
const fakeAddPanelAction = (panelID, queryID) => {
|
||||
return {
|
||||
type: 'CREATE_PANEL',
|
||||
payload: {panelID, queryID},
|
||||
};
|
||||
};
|
||||
|
||||
describe('Chronograf.Reducers.Panel', () => {
|
||||
let state;
|
||||
const panelID = 123;
|
||||
const queryID = 456;
|
||||
|
||||
beforeEach(() => {
|
||||
state = reducer({}, fakeAddPanelAction(panelID, queryID));
|
||||
});
|
||||
|
||||
it('can add a panel', () => {
|
||||
const actual = state[panelID];
|
||||
expect(actual).to.deep.equal({
|
||||
id: panelID,
|
||||
queryIds: [queryID],
|
||||
});
|
||||
});
|
||||
|
||||
it('can delete a panel', () => {
|
||||
const nextState = reducer(state, deletePanel(panelID));
|
||||
|
||||
const actual = nextState[panelID];
|
||||
expect(actual).to.equal(undefined);
|
||||
});
|
||||
});
|
|
@ -1,51 +1,20 @@
|
|||
import uuid from 'node-uuid';
|
||||
|
||||
export function createPanel() {
|
||||
return {
|
||||
type: 'CREATE_PANEL',
|
||||
payload: {
|
||||
panelID: uuid.v4(), // for the default Panel
|
||||
queryID: uuid.v4(), // for the default Query
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function renamePanel(panelId, name) {
|
||||
return {
|
||||
type: 'RENAME_PANEL',
|
||||
payload: {
|
||||
panelId,
|
||||
name,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function deletePanel(panelId) {
|
||||
return {
|
||||
type: 'DELETE_PANEL',
|
||||
payload: {
|
||||
panelId,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function addQuery(panelId, options) {
|
||||
export function addQuery(options = {}) {
|
||||
return {
|
||||
type: 'ADD_QUERY',
|
||||
payload: {
|
||||
panelId,
|
||||
queryId: uuid.v4(),
|
||||
queryID: uuid.v4(),
|
||||
options,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function deleteQuery(panelId, queryId) {
|
||||
export function deleteQuery(queryID) {
|
||||
return {
|
||||
type: 'DELETE_QUERY',
|
||||
payload: {
|
||||
queryId,
|
||||
panelId,
|
||||
queryID,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
@ -159,12 +128,3 @@ export function updateRawQuery(queryID, text) {
|
|||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function activatePanel(panelID) {
|
||||
return {
|
||||
type: 'ACTIVATE_PANEL',
|
||||
payload: {
|
||||
panelID,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
@ -60,18 +60,21 @@ const DatabaseList = React.createClass({
|
|||
const {onChooseNamespace, query} = this.props;
|
||||
|
||||
return (
|
||||
<ul className="qeditor--list">
|
||||
{this.state.namespaces.map((namespace) => {
|
||||
const {database, retentionPolicy} = namespace;
|
||||
const isActive = database === query.database && retentionPolicy === query.retentionPolicy;
|
||||
<div className="query-builder--column">
|
||||
<div className="query-builder--column-heading">Databases</div>
|
||||
<ul className="qeditor--list">
|
||||
{this.state.namespaces.map((namespace) => {
|
||||
const {database, retentionPolicy} = namespace;
|
||||
const isActive = database === query.database && retentionPolicy === query.retentionPolicy;
|
||||
|
||||
return (
|
||||
<li className={classNames('qeditor--list-item qeditor--list-radio', {active: isActive})} key={`${database}..${retentionPolicy}`} onClick={_.wrap(namespace, onChooseNamespace)}>
|
||||
{database}.{retentionPolicy}
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
return (
|
||||
<li className={classNames('qeditor--list-item qeditor--list-radio', {active: isActive})} key={`${database}..${retentionPolicy}`} onClick={_.wrap(namespace, onChooseNamespace)}>
|
||||
{database}.{retentionPolicy}
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
});
|
||||
|
|
|
@ -41,25 +41,26 @@ const FieldList = React.createClass({
|
|||
},
|
||||
|
||||
componentDidMount() {
|
||||
const {database, measurement, retentionPolicy} = this.props.query;
|
||||
const {database, measurement} = this.props.query;
|
||||
if (!database || !measurement) {
|
||||
return;
|
||||
}
|
||||
|
||||
const {source} = this.context;
|
||||
const proxySource = source.links.proxy;
|
||||
showFieldKeys(proxySource, database, measurement, retentionPolicy).then((resp) => {
|
||||
const {errors, fieldSets} = showFieldKeysParser(resp.data);
|
||||
if (errors.length) {
|
||||
// TODO: do something
|
||||
}
|
||||
this._getFields();
|
||||
},
|
||||
|
||||
this.setState({
|
||||
fields: fieldSets[measurement].map((f) => {
|
||||
return {field: f, funcs: []};
|
||||
}),
|
||||
});
|
||||
});
|
||||
componentDidUpdate(prevProps) {
|
||||
const {database, measurement, retentionPolicy} = this.props.query;
|
||||
const {database: prevDB, measurement: prevMeas, retentionPolicy: prevRP} = prevProps.query;
|
||||
if (!database || !measurement) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (database === prevDB && measurement === prevMeas && retentionPolicy === prevRP) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._getFields();
|
||||
},
|
||||
|
||||
handleGroupByTime(groupBy) {
|
||||
|
@ -72,7 +73,8 @@ const FieldList = React.createClass({
|
|||
const hasGroupByTime = query.groupBy.time;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="query-builder--column">
|
||||
<div className="query-builder--column-heading">Fields</div>
|
||||
{
|
||||
hasAggregates ?
|
||||
<div className="qeditor--list-header">
|
||||
|
@ -94,23 +96,43 @@ const FieldList = React.createClass({
|
|||
return <div className="qeditor--empty">No <strong>Measurement</strong> selected</div>;
|
||||
}
|
||||
|
||||
return (<ul className="qeditor--list">
|
||||
{this.state.fields.map((fieldFunc) => {
|
||||
const selectedField = this.props.query.fields.find((f) => f.field === fieldFunc.field);
|
||||
return (
|
||||
<FieldListItem
|
||||
key={fieldFunc.field}
|
||||
onToggleField={this.props.onToggleField}
|
||||
onApplyFuncsToField={this.props.applyFuncsToField}
|
||||
isSelected={!!selectedField}
|
||||
fieldFunc={selectedField || fieldFunc}
|
||||
isKapacitorRule={this.props.isKapacitorRule}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
return (
|
||||
<ul className="qeditor--list">
|
||||
{this.state.fields.map((fieldFunc) => {
|
||||
const selectedField = this.props.query.fields.find((f) => f.field === fieldFunc.field);
|
||||
return (
|
||||
<FieldListItem
|
||||
key={fieldFunc.field}
|
||||
onToggleField={this.props.onToggleField}
|
||||
onApplyFuncsToField={this.props.applyFuncsToField}
|
||||
isSelected={!!selectedField}
|
||||
fieldFunc={selectedField || fieldFunc}
|
||||
isKapacitorRule={this.props.isKapacitorRule}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
);
|
||||
},
|
||||
|
||||
_getFields() {
|
||||
const {database, measurement, retentionPolicy} = this.props.query;
|
||||
const {source} = this.context;
|
||||
const proxySource = source.links.proxy;
|
||||
|
||||
showFieldKeys(proxySource, database, measurement, retentionPolicy).then((resp) => {
|
||||
const {errors, fieldSets} = showFieldKeysParser(resp.data);
|
||||
if (errors.length) {
|
||||
// TODO: do something
|
||||
}
|
||||
|
||||
this.setState({
|
||||
fields: fieldSets[measurement].map((f) => {
|
||||
return {field: f, funcs: []};
|
||||
}),
|
||||
});
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
export default FieldList;
|
||||
|
|
|
@ -34,19 +34,21 @@ const MeasurementList = React.createClass({
|
|||
return;
|
||||
}
|
||||
|
||||
const {source} = this.context;
|
||||
const proxy = source.links.proxy;
|
||||
showMeasurements(proxy, this.props.query.database).then((resp) => {
|
||||
const {errors, measurementSets} = showMeasurementsParser(resp.data);
|
||||
if (errors.length) {
|
||||
// TODO: display errors in the UI.
|
||||
return console.error('InfluxDB returned error(s): ', errors); // eslint-disable-line no-console
|
||||
}
|
||||
this._getMeasurements();
|
||||
},
|
||||
|
||||
this.setState({
|
||||
measurements: measurementSets[0].measurements,
|
||||
});
|
||||
});
|
||||
componentDidUpdate(prevProps) {
|
||||
const {query} = this.props;
|
||||
|
||||
if (!query.database) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (prevProps.query.database === query.database) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._getMeasurements();
|
||||
},
|
||||
|
||||
handleFilterText(e) {
|
||||
|
@ -69,9 +71,10 @@ const MeasurementList = React.createClass({
|
|||
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
<div className="query-builder--column">
|
||||
<div className="query-builder--column-heading">Measurements</div>
|
||||
{this.props.query.database ? <div className="qeditor--list-header">
|
||||
<input className="qeditor--filter" ref="filterText" placeholder="Filter Measurements..." type="text" value={this.state.filterText} onChange={this.handleFilterText} onKeyUp={this.handleEscape} />
|
||||
<input className="qeditor--filter" ref="filterText" placeholder="Filter" type="text" value={this.state.filterText} onChange={this.handleFilterText} onKeyUp={this.handleEscape} />
|
||||
<span className="icon search"></span>
|
||||
</div> : null }
|
||||
{this.renderList()}
|
||||
|
@ -97,6 +100,23 @@ const MeasurementList = React.createClass({
|
|||
</ul>
|
||||
);
|
||||
},
|
||||
|
||||
_getMeasurements() {
|
||||
const {source} = this.context;
|
||||
const proxy = source.links.proxy;
|
||||
showMeasurements(proxy, this.props.query.database).then((resp) => {
|
||||
const {errors, measurementSets} = showMeasurementsParser(resp.data);
|
||||
if (errors.length) {
|
||||
// TODO: display errors in the UI.
|
||||
return console.error('InfluxDB returned error(s): ', errors); // eslint-disable-line no-console
|
||||
}
|
||||
|
||||
this.setState({
|
||||
measurements: measurementSets[0].measurements,
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
});
|
||||
|
||||
export default MeasurementList;
|
||||
|
|
|
@ -2,7 +2,14 @@ import React, {PropTypes} from 'react';
|
|||
import Table from './Table';
|
||||
import classNames from 'classnames';
|
||||
|
||||
const {bool, string, shape, arrayOf, func} = PropTypes;
|
||||
const {
|
||||
arrayOf,
|
||||
bool,
|
||||
func,
|
||||
number,
|
||||
shape,
|
||||
string,
|
||||
} = PropTypes;
|
||||
|
||||
const MultiTable = React.createClass({
|
||||
propTypes: {
|
||||
|
@ -10,6 +17,7 @@ const MultiTable = React.createClass({
|
|||
host: arrayOf(string.isRequired).isRequired,
|
||||
text: string.isRequired,
|
||||
})),
|
||||
height: number,
|
||||
},
|
||||
|
||||
getInitialState() {
|
||||
|
@ -40,13 +48,14 @@ const MultiTable = React.createClass({
|
|||
},
|
||||
|
||||
renderTable() {
|
||||
const {height} = this.props;
|
||||
const query = this.getActiveQuery();
|
||||
const noQuery = !query || !query.text;
|
||||
if (noQuery) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <Table key={query.text} query={query} />;
|
||||
return <Table key={query.text} query={query} height={height} />;
|
||||
},
|
||||
|
||||
renderTabs() {
|
||||
|
|
|
@ -1,183 +0,0 @@
|
|||
import React, {PropTypes} from 'react';
|
||||
import classNames from 'classnames';
|
||||
import QueryEditor from './QueryEditor';
|
||||
import QueryTabItem from './QueryTabItem';
|
||||
import RenamePanelModal from './RenamePanelModal';
|
||||
import SimpleDropdown from 'src/shared/components/SimpleDropdown';
|
||||
|
||||
const Panel = React.createClass({
|
||||
propTypes: {
|
||||
panel: PropTypes.shape({
|
||||
id: PropTypes.string.isRequired,
|
||||
}).isRequired,
|
||||
queries: PropTypes.arrayOf(PropTypes.shape({})).isRequired,
|
||||
timeRange: PropTypes.shape({
|
||||
upper: PropTypes.string,
|
||||
lower: PropTypes.string,
|
||||
}).isRequired,
|
||||
isExpanded: PropTypes.bool.isRequired,
|
||||
onTogglePanel: PropTypes.func.isRequired,
|
||||
actions: PropTypes.shape({
|
||||
chooseNamespace: PropTypes.func.isRequired,
|
||||
chooseMeasurement: PropTypes.func.isRequired,
|
||||
chooseTag: PropTypes.func.isRequired,
|
||||
groupByTag: PropTypes.func.isRequired,
|
||||
addQuery: PropTypes.func.isRequired,
|
||||
deleteQuery: PropTypes.func.isRequired,
|
||||
toggleField: PropTypes.func.isRequired,
|
||||
groupByTime: PropTypes.func.isRequired,
|
||||
toggleTagAcceptance: PropTypes.func.isRequired,
|
||||
applyFuncsToField: PropTypes.func.isRequired,
|
||||
deletePanel: PropTypes.func.isRequired,
|
||||
renamePanel: PropTypes.func.isRequired,
|
||||
}).isRequired,
|
||||
setActiveQuery: PropTypes.func.isRequired,
|
||||
activeQueryID: PropTypes.string,
|
||||
},
|
||||
|
||||
handleSetActiveQuery(query) {
|
||||
this.props.setActiveQuery(query.id);
|
||||
},
|
||||
|
||||
handleAddQuery() {
|
||||
this.props.actions.addQuery();
|
||||
},
|
||||
|
||||
handleAddRawQuery() {
|
||||
this.props.actions.addQuery({rawText: `SELECT "fields" from "db"."rp"."measurement"`});
|
||||
},
|
||||
|
||||
handleDeleteQuery(query) {
|
||||
this.props.actions.deleteQuery(query.id);
|
||||
},
|
||||
|
||||
handleSelectPanel() {
|
||||
this.props.onTogglePanel(this.props.panel);
|
||||
},
|
||||
|
||||
handleDeletePanel(e) {
|
||||
e.stopPropagation();
|
||||
this.props.actions.deletePanel(this.props.panel.id);
|
||||
},
|
||||
|
||||
getActiveQuery() {
|
||||
const {queries, activeQueryID} = this.props;
|
||||
const activeQuery = queries.find((query) => query.id === activeQueryID);
|
||||
const defaultQuery = queries[0];
|
||||
|
||||
return activeQuery || defaultQuery;
|
||||
},
|
||||
|
||||
openRenamePanelModal(e) {
|
||||
e.stopPropagation();
|
||||
$(`#renamePanelModal-${this.props.panel.id}`).modal('show'); // eslint-disable-line no-undef
|
||||
},
|
||||
|
||||
handleRename(newName) {
|
||||
this.props.actions.renamePanel(this.props.panel.id, newName);
|
||||
},
|
||||
|
||||
|
||||
render() {
|
||||
const {panel, isExpanded} = this.props;
|
||||
|
||||
return (
|
||||
<div className={classNames('panel', {active: isExpanded})}>
|
||||
<div className="panel--header" onClick={this.handleSelectPanel}>
|
||||
<div className="panel--name">
|
||||
<span className="icon caret-right"></span>
|
||||
{panel.name || "Graph"}
|
||||
</div>
|
||||
<div className="panel--actions">
|
||||
{/* <div title="Export Queries to Dashboard" className="panel--action"><span className="icon export"></span></div> */}
|
||||
<div title="Rename Graph" className="panel--action" onClick={this.openRenamePanelModal}><span className="icon pencil"></span></div>
|
||||
<div title="Delete Graph" className="panel--action" onClick={this.handleDeletePanel}><span className="icon trash"></span></div>
|
||||
</div>
|
||||
</div>
|
||||
{this.renderQueryTabList()}
|
||||
{this.renderQueryEditor()}
|
||||
<RenamePanelModal panel={panel} onConfirm={this.handleRename} />
|
||||
</div>
|
||||
);
|
||||
},
|
||||
|
||||
renderQueryEditor() {
|
||||
if (!this.props.isExpanded) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const {timeRange, actions} = this.props;
|
||||
const query = this.getActiveQuery();
|
||||
|
||||
if (!query) {
|
||||
return (
|
||||
<div className="qeditor--empty">
|
||||
<h5>This Graph has no Queries</h5>
|
||||
<br/>
|
||||
<div className="btn btn-primary" role="button" onClick={this.handleAddQuery}>Add a Query</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<QueryEditor
|
||||
timeRange={timeRange}
|
||||
query={this.getActiveQuery()}
|
||||
actions={actions}
|
||||
onAddQuery={this.handleAddQuery}
|
||||
/>
|
||||
);
|
||||
},
|
||||
|
||||
renderQueryTabList() {
|
||||
const {isExpanded, queries} = this.props;
|
||||
if (!isExpanded) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<div className="panel--tabs">
|
||||
{queries.map((q) => {
|
||||
let queryTabText;
|
||||
if (q.rawText) {
|
||||
queryTabText = 'InfluxQL';
|
||||
} else {
|
||||
queryTabText = (q.measurement && q.fields.length !== 0) ? `${q.measurement}.${q.fields[0].field}` : 'Query';
|
||||
}
|
||||
return (
|
||||
<QueryTabItem
|
||||
isActive={this.getActiveQuery().id === q.id}
|
||||
key={q.id}
|
||||
query={q}
|
||||
onSelect={this.handleSetActiveQuery}
|
||||
onDelete={this.handleDeleteQuery}
|
||||
queryTabText={queryTabText}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
||||
{this.renderAddQuery()}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
|
||||
onChoose(item) {
|
||||
switch (item.text) {
|
||||
case 'Query Builder':
|
||||
this.handleAddQuery();
|
||||
break;
|
||||
case 'InfluxQL':
|
||||
this.handleAddRawQuery();
|
||||
break;
|
||||
}
|
||||
},
|
||||
|
||||
renderAddQuery() {
|
||||
return (
|
||||
<SimpleDropdown onChoose={this.onChoose} items={[{text: 'Query Builder'}, {text: 'InfluxQL'}]} className="panel--tab-new">
|
||||
<span className="icon plus"></span>
|
||||
</SimpleDropdown>
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
export default Panel;
|
|
@ -1,63 +0,0 @@
|
|||
import React, {PropTypes} from 'react';
|
||||
import {connect} from 'react-redux';
|
||||
import {bindActionCreators} from 'redux';
|
||||
import PanelList from './PanelList';
|
||||
import * as viewActions from '../actions/view';
|
||||
|
||||
const {string, func} = PropTypes;
|
||||
const PanelBuilder = React.createClass({
|
||||
propTypes: {
|
||||
width: string,
|
||||
actions: PropTypes.shape({
|
||||
activatePanel: func.isRequired,
|
||||
createPanel: func.isRequired,
|
||||
deleteQuery: func.isRequired,
|
||||
addQuery: func.isRequired,
|
||||
editRawText: func.isRequired,
|
||||
chooseNamespace: func.isRequired,
|
||||
chooseMeasurement: func.isRequired,
|
||||
toggleField: func.isRequired,
|
||||
groupByTime: func.isRequired,
|
||||
applyFuncsToField: func.isRequired,
|
||||
chooseTag: func.isRequired,
|
||||
groupByTag: func.isRequired,
|
||||
toggleTagAcceptance: func.isRequired,
|
||||
deletePanel: func.isRequired,
|
||||
}).isRequired,
|
||||
setActiveQuery: func.isRequired,
|
||||
activePanelID: string,
|
||||
activeQueryID: string,
|
||||
},
|
||||
|
||||
handleCreateExplorer() {
|
||||
this.props.actions.createPanel();
|
||||
},
|
||||
|
||||
render() {
|
||||
const {width, actions, setActiveQuery, activePanelID, activeQueryID} = this.props;
|
||||
|
||||
return (
|
||||
<div className="panel-builder" style={{width}}>
|
||||
<div className="btn btn-block btn-primary" onClick={this.handleCreateExplorer}><span className="icon graphline"></span> Create Graph</div>
|
||||
<PanelList
|
||||
actions={actions}
|
||||
setActiveQuery={setActiveQuery}
|
||||
activePanelID={activePanelID}
|
||||
activeQueryID={activeQueryID}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
function mapStateToProps() {
|
||||
return {};
|
||||
}
|
||||
|
||||
function mapDispatchToProps(dispatch) {
|
||||
return {
|
||||
actions: bindActionCreators(viewActions, dispatch),
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(PanelBuilder);
|
|
@ -1,76 +0,0 @@
|
|||
import React, {PropTypes} from 'react';
|
||||
import {connect} from 'react-redux';
|
||||
import _ from 'lodash';
|
||||
|
||||
import Panel from './Panel';
|
||||
|
||||
const {func, string, shape} = PropTypes;
|
||||
const PanelList = React.createClass({
|
||||
propTypes: {
|
||||
timeRange: shape({
|
||||
upper: string,
|
||||
lower: string,
|
||||
}).isRequired,
|
||||
panels: shape({}).isRequired,
|
||||
queryConfigs: PropTypes.shape({}),
|
||||
actions: shape({
|
||||
activatePanel: func.isRequired,
|
||||
deleteQuery: func.isRequired,
|
||||
addQuery: func.isRequired,
|
||||
}).isRequired,
|
||||
setActiveQuery: func.isRequired,
|
||||
activePanelID: string,
|
||||
activeQueryID: string,
|
||||
},
|
||||
|
||||
handleTogglePanel(panel) {
|
||||
const panelID = panel.id === this.props.activePanelID ? null : panel.id;
|
||||
this.props.actions.activatePanel(panelID);
|
||||
|
||||
// Reset the activeQueryID when toggling Exporations
|
||||
this.props.setActiveQuery(null);
|
||||
},
|
||||
|
||||
render() {
|
||||
const {actions, panels, timeRange, queryConfigs, setActiveQuery, activeQueryID, activePanelID} = this.props;
|
||||
|
||||
return (
|
||||
<div>
|
||||
{Object.keys(panels).map((panelID) => {
|
||||
const panel = panels[panelID];
|
||||
const queries = panel.queryIds.map((configId) => queryConfigs[configId]);
|
||||
const deleteQueryFromPanel = _.partial(actions.deleteQuery, panelID);
|
||||
const addQueryToPanel = _.partial(actions.addQuery, panelID);
|
||||
const allActions = Object.assign({}, actions, {
|
||||
addQuery: addQueryToPanel,
|
||||
deleteQuery: deleteQueryFromPanel,
|
||||
});
|
||||
|
||||
return (
|
||||
<Panel
|
||||
key={panelID}
|
||||
panel={panel}
|
||||
queries={queries}
|
||||
timeRange={timeRange}
|
||||
onTogglePanel={this.handleTogglePanel}
|
||||
setActiveQuery={setActiveQuery}
|
||||
isExpanded={panelID === activePanelID}
|
||||
actions={allActions}
|
||||
activeQueryID={activeQueryID}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
function mapStateToProps(state) {
|
||||
return {
|
||||
timeRange: state.timeRange,
|
||||
panels: state.panels,
|
||||
queryConfigs: state.queryConfigs,
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps)(PanelList);
|
|
@ -0,0 +1,160 @@
|
|||
import React, {PropTypes} from 'react';
|
||||
import {bindActionCreators} from 'redux';
|
||||
import {connect} from 'react-redux';
|
||||
|
||||
import QueryEditor from './QueryEditor';
|
||||
import QueryTabItem from './QueryTabItem';
|
||||
import SimpleDropdown from 'src/shared/components/SimpleDropdown';
|
||||
|
||||
import * as viewActions from '../actions/view';
|
||||
const {
|
||||
arrayOf,
|
||||
func,
|
||||
shape,
|
||||
string,
|
||||
} = PropTypes;
|
||||
|
||||
const QueryBuilder = React.createClass({
|
||||
propTypes: {
|
||||
queries: arrayOf(shape({})).isRequired,
|
||||
timeRange: shape({
|
||||
upper: string,
|
||||
lower: string,
|
||||
}).isRequired,
|
||||
actions: shape({
|
||||
chooseNamespace: func.isRequired,
|
||||
chooseMeasurement: func.isRequired,
|
||||
chooseTag: func.isRequired,
|
||||
groupByTag: func.isRequired,
|
||||
addQuery: func.isRequired,
|
||||
deleteQuery: func.isRequired,
|
||||
toggleField: func.isRequired,
|
||||
groupByTime: func.isRequired,
|
||||
toggleTagAcceptance: func.isRequired,
|
||||
applyFuncsToField: func.isRequired,
|
||||
}).isRequired,
|
||||
height: string,
|
||||
top: string,
|
||||
setActiveQuery: func.isRequired,
|
||||
activeQueryID: string,
|
||||
},
|
||||
|
||||
handleSetActiveQuery(query) {
|
||||
this.props.setActiveQuery(query.id);
|
||||
},
|
||||
|
||||
handleAddQuery() {
|
||||
this.props.actions.addQuery();
|
||||
},
|
||||
|
||||
handleAddRawQuery() {
|
||||
this.props.actions.addQuery({rawText: `SELECT "fields" from "db"."rp"."measurement"`});
|
||||
},
|
||||
|
||||
handleDeleteQuery(query) {
|
||||
this.props.actions.deleteQuery(query.id);
|
||||
},
|
||||
|
||||
getActiveQuery() {
|
||||
const {queries, activeQueryID} = this.props;
|
||||
const activeQuery = queries.find((query) => query.id === activeQueryID);
|
||||
const defaultQuery = queries[0];
|
||||
|
||||
return activeQuery || defaultQuery;
|
||||
},
|
||||
|
||||
render() {
|
||||
const {height, top} = this.props;
|
||||
return (
|
||||
<div className="query-builder" style={{height, top}}>
|
||||
{this.renderQueryTabList()}
|
||||
{this.renderQueryEditor()}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
|
||||
renderQueryEditor() {
|
||||
const {timeRange, actions} = this.props;
|
||||
const query = this.getActiveQuery();
|
||||
|
||||
if (!query) {
|
||||
return (
|
||||
<div className="qeditor--empty">
|
||||
<h5>This Graph has no Queries</h5>
|
||||
<br/>
|
||||
<div className="btn btn-primary" role="button" onClick={this.handleAddQuery}>Add a Query</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<QueryEditor
|
||||
timeRange={timeRange}
|
||||
query={this.getActiveQuery()}
|
||||
actions={actions}
|
||||
onAddQuery={this.handleAddQuery}
|
||||
/>
|
||||
);
|
||||
},
|
||||
|
||||
renderQueryTabList() {
|
||||
const {queries} = this.props;
|
||||
return (
|
||||
<div className="query-builder--tabs">
|
||||
<div className="query-builder--tabs-heading">
|
||||
<h1>Queries</h1>
|
||||
{this.renderAddQuery()}
|
||||
</div>
|
||||
{queries.map((q, i) => {
|
||||
let queryTabText;
|
||||
if (q.rawText) {
|
||||
queryTabText = 'InfluxQL';
|
||||
} else {
|
||||
queryTabText = (q.measurement && q.fields.length !== 0) ? `${q.measurement}.${q.fields[0].field}` : 'Query';
|
||||
}
|
||||
return (
|
||||
<QueryTabItem
|
||||
isActive={this.getActiveQuery().id === q.id}
|
||||
key={q.id + i}
|
||||
query={q}
|
||||
onSelect={this.handleSetActiveQuery}
|
||||
onDelete={this.handleDeleteQuery}
|
||||
queryTabText={queryTabText}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
|
||||
onChoose(item) {
|
||||
switch (item.text) {
|
||||
case 'Query Builder':
|
||||
this.handleAddQuery();
|
||||
break;
|
||||
case 'InfluxQL':
|
||||
this.handleAddRawQuery();
|
||||
break;
|
||||
}
|
||||
},
|
||||
|
||||
renderAddQuery() {
|
||||
return (
|
||||
<SimpleDropdown onChoose={this.onChoose} items={[{text: 'Query Builder'}, {text: 'InfluxQL'}]} className="panel--tab-new">
|
||||
<span className="icon plus"></span>
|
||||
</SimpleDropdown>
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
function mapStateToProps() {
|
||||
return {};
|
||||
}
|
||||
|
||||
function mapDispatchToProps(dispatch) {
|
||||
return {
|
||||
actions: bindActionCreators(viewActions, dispatch),
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(QueryBuilder);
|
|
@ -1,6 +1,4 @@
|
|||
import React, {PropTypes} from 'react';
|
||||
import classNames from 'classnames';
|
||||
import _ from 'lodash';
|
||||
import selectStatement from '../utils/influxql/select';
|
||||
|
||||
import DatabaseList from './DatabaseList';
|
||||
|
@ -9,12 +7,11 @@ import FieldList from './FieldList';
|
|||
import TagList from './TagList';
|
||||
import RawQueryEditor from './RawQueryEditor';
|
||||
|
||||
const DB_TAB = 'databases';
|
||||
const MEASUREMENTS_TAB = 'measurments';
|
||||
const FIELDS_TAB = 'fields';
|
||||
const TAGS_TAB = 'tags';
|
||||
|
||||
const {string, shape, func} = PropTypes;
|
||||
const {
|
||||
string,
|
||||
shape,
|
||||
func,
|
||||
} = PropTypes;
|
||||
const QueryEditor = React.createClass({
|
||||
propTypes: {
|
||||
query: shape({
|
||||
|
@ -38,29 +35,17 @@ const QueryEditor = React.createClass({
|
|||
|
||||
getInitialState() {
|
||||
return {
|
||||
activeTab: DB_TAB,
|
||||
database: null,
|
||||
measurement: null,
|
||||
};
|
||||
},
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
const changingQueries = this.props.query.id !== nextProps.query.id;
|
||||
if (changingQueries) {
|
||||
this.setState({activeTab: DB_TAB});
|
||||
}
|
||||
},
|
||||
|
||||
handleChooseNamespace(namespace) {
|
||||
this.props.actions.chooseNamespace(this.props.query.id, namespace);
|
||||
|
||||
this.setState({activeTab: MEASUREMENTS_TAB});
|
||||
},
|
||||
|
||||
handleChooseMeasurement(measurement) {
|
||||
this.props.actions.chooseMeasurement(this.props.query.id, measurement);
|
||||
|
||||
this.setState({activeTab: FIELDS_TAB});
|
||||
},
|
||||
|
||||
handleToggleField(field) {
|
||||
|
@ -91,15 +76,13 @@ const QueryEditor = React.createClass({
|
|||
this.props.actions.editRawText(this.props.query.id, text);
|
||||
},
|
||||
|
||||
handleClickTab(tab) {
|
||||
this.setState({activeTab: tab});
|
||||
},
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className="panel--tab-contents">
|
||||
{this.renderQuery()}
|
||||
{this.renderLists()}
|
||||
<div className="query-builder--tab-contents">
|
||||
<div>
|
||||
{this.renderQuery()}
|
||||
{this.renderLists()}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
|
@ -110,7 +93,7 @@ const QueryEditor = React.createClass({
|
|||
|
||||
if (!query.rawText) {
|
||||
return (
|
||||
<div className="qeditor--query-preview">
|
||||
<div className="query-builder--query-preview">
|
||||
<pre><code>{statement}</code></pre>
|
||||
</div>
|
||||
);
|
||||
|
@ -120,60 +103,32 @@ const QueryEditor = React.createClass({
|
|||
},
|
||||
|
||||
renderLists() {
|
||||
const {activeTab} = this.state;
|
||||
return (
|
||||
<div>
|
||||
<div className="qeditor--tabs">
|
||||
<div className="qeditor--tabs-heading">Schema Explorer</div>
|
||||
<div onClick={_.wrap(DB_TAB, this.handleClickTab)} className={classNames("qeditor--tab", {active: activeTab === DB_TAB})}>Databases</div>
|
||||
<div onClick={_.wrap(MEASUREMENTS_TAB, this.handleClickTab)} className={classNames("qeditor--tab", {active: activeTab === MEASUREMENTS_TAB})}>Measurements</div>
|
||||
<div onClick={_.wrap(FIELDS_TAB, this.handleClickTab)} className={classNames("qeditor--tab", {active: activeTab === FIELDS_TAB})}>Fields</div>
|
||||
<div onClick={_.wrap(TAGS_TAB, this.handleClickTab)} className={classNames("qeditor--tab", {active: activeTab === TAGS_TAB})}>Tags</div>
|
||||
</div>
|
||||
{this.renderList()}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
|
||||
renderList() {
|
||||
const {query} = this.props;
|
||||
|
||||
switch (this.state.activeTab) {
|
||||
case DB_TAB:
|
||||
return (
|
||||
<DatabaseList
|
||||
query={query}
|
||||
onChooseNamespace={this.handleChooseNamespace}
|
||||
/>
|
||||
);
|
||||
case MEASUREMENTS_TAB:
|
||||
return (
|
||||
<MeasurementList
|
||||
query={query}
|
||||
onChooseMeasurement={this.handleChooseMeasurement}
|
||||
/>
|
||||
);
|
||||
case FIELDS_TAB:
|
||||
return (
|
||||
<FieldList
|
||||
query={query}
|
||||
onToggleField={this.handleToggleField}
|
||||
onGroupByTime={this.handleGroupByTime}
|
||||
applyFuncsToField={this.handleApplyFuncsToField}
|
||||
/>
|
||||
);
|
||||
case TAGS_TAB:
|
||||
return (
|
||||
<TagList
|
||||
query={query}
|
||||
onChooseTag={this.handleChooseTag}
|
||||
onGroupByTag={this.handleGroupByTag}
|
||||
onToggleTagAcceptance={this.handleToggleTagAcceptance}
|
||||
/>
|
||||
);
|
||||
default:
|
||||
return <ul className="qeditor--list"></ul>;
|
||||
}
|
||||
return (
|
||||
<div className="query-builder--columns">
|
||||
<DatabaseList
|
||||
query={query}
|
||||
onChooseNamespace={this.handleChooseNamespace}
|
||||
/>
|
||||
<MeasurementList
|
||||
query={query}
|
||||
onChooseMeasurement={this.handleChooseMeasurement}
|
||||
/>
|
||||
<FieldList
|
||||
query={query}
|
||||
onToggleField={this.handleToggleField}
|
||||
onGroupByTime={this.handleGroupByTime}
|
||||
applyFuncsToField={this.handleApplyFuncsToField}
|
||||
/>
|
||||
<TagList
|
||||
query={query}
|
||||
onChooseTag={this.handleChooseTag}
|
||||
onGroupByTag={this.handleGroupByTag}
|
||||
onToggleTagAcceptance={this.handleToggleTagAcceptance}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
@ -23,9 +23,9 @@ const QueryTabItem = React.createClass({
|
|||
|
||||
render() {
|
||||
return (
|
||||
<div className={classNames('panel--tab', {active: this.props.isActive})} onClick={this.handleSelect}>
|
||||
<span className="panel--tab-label">{this.props.queryTabText}</span>
|
||||
<span className="panel--tab-delete" onClick={this.handleDelete}></span>
|
||||
<div className={classNames('query-builder--tab', {active: this.props.isActive})} onClick={this.handleSelect}>
|
||||
<span className="query-builder--tab-label">{this.props.queryTabText}</span>
|
||||
<span className="query-builder--tab-delete" onClick={this.handleDelete}></span>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
|
|
|
@ -1,67 +0,0 @@
|
|||
import React, {PropTypes} from 'react';
|
||||
|
||||
const RenamePanelModal = React.createClass({
|
||||
propTypes: {
|
||||
onConfirm: PropTypes.func.isRequired,
|
||||
panel: PropTypes.shape({
|
||||
id: PropTypes.string.isRequired,
|
||||
}),
|
||||
},
|
||||
|
||||
getInitialState() {
|
||||
return {error: null};
|
||||
},
|
||||
|
||||
componentDidMount() {
|
||||
this.refs.name.focus();
|
||||
},
|
||||
|
||||
render() {
|
||||
const {panel} = this.props;
|
||||
|
||||
return (
|
||||
<div className="modal fade in" id={`renamePanelModal-${panel.id}`} tabIndex="-1" role="dialog">
|
||||
<div className="modal-dialog">
|
||||
<div className="modal-content">
|
||||
<div className="modal-header">
|
||||
<button type="button" className="close" data-dismiss="modal" aria-label="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
<h4 className="modal-title">Rename Panel</h4>
|
||||
</div>
|
||||
<div className="modal-body">
|
||||
{this.state.error ?
|
||||
<div className="alert alert-danger" role="alert">{this.state.error}</div>
|
||||
: null}
|
||||
<div className="form-grid padding-top">
|
||||
<div className="form-group col-md-8 col-md-offset-2">
|
||||
<input ref="name" name="renameExplorer" type="text" placeholder={panel.name} className="form-control input-lg" id="renameExplorer" required={true} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="modal-footer">
|
||||
<button className="btn btn-info" data-dismiss="modal">Cancel</button>
|
||||
<button onClick={this.handleConfirm} className="btn btn-success">Rename</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
|
||||
handleConfirm() {
|
||||
const name = this.refs.name.value;
|
||||
|
||||
if (name === '') {
|
||||
this.setState({error: "Name can't be blank"});
|
||||
return;
|
||||
}
|
||||
|
||||
$(`#renamePanelModal-${this.props.panel.id}`).modal('hide'); // eslint-disable-line no-undef
|
||||
this.refs.name.value = '';
|
||||
this.setState({error: null});
|
||||
this.props.onConfirm(name);
|
||||
},
|
||||
});
|
||||
|
||||
export default RenamePanelModal;
|
|
@ -33,6 +33,7 @@ const ChronoTable = React.createClass({
|
|||
text: string.isRequired,
|
||||
}),
|
||||
containerWidth: number.isRequired,
|
||||
height: number,
|
||||
},
|
||||
|
||||
getInitialState() {
|
||||
|
@ -45,6 +46,12 @@ const ChronoTable = React.createClass({
|
|||
};
|
||||
},
|
||||
|
||||
getDefaultProps() {
|
||||
return {
|
||||
height: 600,
|
||||
};
|
||||
},
|
||||
|
||||
fetchCellData(query) {
|
||||
this.setState({isLoading: true});
|
||||
// second param is db, we want to leave this blank
|
||||
|
@ -81,30 +88,33 @@ const ChronoTable = React.createClass({
|
|||
|
||||
// Table data as a list of array.
|
||||
render() {
|
||||
const {containerWidth} = this.props;
|
||||
const {containerWidth, height} = this.props;
|
||||
const {cellData, columnWidths, isLoading} = this.state;
|
||||
const {columns, values} = cellData;
|
||||
|
||||
const ownerHeight = 300;
|
||||
// adjust height to proper value by subtracting the heights of the UI around it
|
||||
// tab height, graph-container vertical padding, graph-heading height, multitable-header height
|
||||
const stylePixelOffset = 136;
|
||||
|
||||
const rowHeight = 34;
|
||||
const height = 300;
|
||||
const width = 200;
|
||||
const headerHeight = 40;
|
||||
const headerHeight = 30;
|
||||
const minWidth = 70;
|
||||
const styleAdjustedHeight = height - stylePixelOffset;
|
||||
|
||||
if (!isLoading && !values.length) {
|
||||
return <div>Your query returned no data</div>;
|
||||
return <div className="generic-empty-state">Your query returned no data</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<Table
|
||||
onColumnResizeEndCallback={this.handleColumnResize}
|
||||
isColumnResizing={false}
|
||||
ownerHeight={ownerHeight}
|
||||
rowHeight={rowHeight}
|
||||
rowsCount={values.length}
|
||||
width={containerWidth}
|
||||
height={height}
|
||||
ownerHeight={styleAdjustedHeight}
|
||||
height={styleAdjustedHeight}
|
||||
headerHeight={headerHeight}>
|
||||
{columns.map((columnName, colIndex) => {
|
||||
return (
|
||||
|
|
|
@ -36,14 +36,11 @@ const TagList = React.createClass({
|
|||
};
|
||||
},
|
||||
|
||||
componentDidMount() {
|
||||
_getTags() {
|
||||
const {database, measurement, retentionPolicy} = this.props.query;
|
||||
const {source} = this.context;
|
||||
if (!database || !measurement || !retentionPolicy) {
|
||||
return;
|
||||
}
|
||||
|
||||
const sourceProxy = source.links.proxy;
|
||||
|
||||
showTagKeys({source: sourceProxy, database, retentionPolicy, measurement}).then((resp) => {
|
||||
const {errors, tagKeys} = showTagKeysParser(resp.data);
|
||||
if (errors.length) {
|
||||
|
@ -61,6 +58,29 @@ const TagList = React.createClass({
|
|||
});
|
||||
},
|
||||
|
||||
componentDidMount() {
|
||||
const {database, measurement, retentionPolicy} = this.props.query;
|
||||
if (!database || !measurement || !retentionPolicy) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._getTags();
|
||||
},
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
const {database, measurement, retentionPolicy} = this.props.query;
|
||||
const {database: prevDB, measurement: prevMeas, retentionPolicy: prevRP} = prevProps.query;
|
||||
if (!database || !measurement || !retentionPolicy) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (database === prevDB && measurement === prevMeas && retentionPolicy === prevRP) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._getTags();
|
||||
},
|
||||
|
||||
handleAcceptReject(e) {
|
||||
e.stopPropagation();
|
||||
this.props.onToggleTagAcceptance();
|
||||
|
@ -70,11 +90,12 @@ const TagList = React.createClass({
|
|||
const {query} = this.props;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="query-builder--column">
|
||||
<div className="query-builder--column-heading">Tags</div>
|
||||
{(!query.database || !query.measurement || !query.retentionPolicy) ? null : <div className="qeditor--list-header">
|
||||
<div className="toggle toggle-sm">
|
||||
<div onClick={this.handleAcceptReject} className={cx("toggle-btn", {active: query.areTagsAccepted})}>Accept</div>
|
||||
<div onClick={this.handleAcceptReject} className={cx("toggle-btn", {active: !query.areTagsAccepted})}>Reject</div>
|
||||
<div onClick={this.handleAcceptReject} className={cx("toggle-btn", {active: query.areTagsAccepted})}>=</div>
|
||||
<div onClick={this.handleAcceptReject} className={cx("toggle-btn", {active: !query.areTagsAccepted})}>!=</div>
|
||||
</div>
|
||||
</div>}
|
||||
{this.renderList()}
|
||||
|
|
|
@ -81,7 +81,10 @@ const TagListItem = React.createClass({
|
|||
},
|
||||
|
||||
render() {
|
||||
const itemClasses = classNames("qeditor--list-item tag-list__item", {open: this.state.isOpen});
|
||||
const {tagKey, tagValues} = this.props;
|
||||
const {isOpen} = this.state;
|
||||
const itemClasses = classNames("qeditor--list-item tag-list__item", {open: isOpen});
|
||||
|
||||
return (
|
||||
<div>
|
||||
<li className={itemClasses} onClick={this.handleClickKey}>
|
||||
|
@ -89,12 +92,15 @@ const TagListItem = React.createClass({
|
|||
<div className="tag-list__caret">
|
||||
<div className="icon caret-right"></div>
|
||||
</div>
|
||||
{this.props.tagKey}
|
||||
<span className="badge">{this.props.tagValues.length}</span>
|
||||
{tagKey}
|
||||
<span className="badge">{tagValues.length}</span>
|
||||
</div>
|
||||
<div
|
||||
className={classNames('btn btn-info btn-xs tag-list__group-by', {active: this.props.isUsingGroupBy})}
|
||||
onClick={this.handleGroupBy}>Group By {tagKey}
|
||||
</div>
|
||||
<div className={classNames('btn btn-info btn-xs tag-list__group-by', {active: this.props.isUsingGroupBy})} onClick={this.handleGroupBy}>Group By</div>
|
||||
</li>
|
||||
{this.state.isOpen ? this.renderTagValues() : null}
|
||||
{isOpen ? this.renderTagValues() : null}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
|
|
|
@ -6,16 +6,24 @@ import LineGraph from 'shared/components/LineGraph';
|
|||
import MultiTable from './MultiTable';
|
||||
const RefreshingLineGraph = AutoRefresh(LineGraph);
|
||||
|
||||
const {
|
||||
arrayOf,
|
||||
number,
|
||||
shape,
|
||||
string,
|
||||
} = PropTypes;
|
||||
|
||||
const Visualization = React.createClass({
|
||||
propTypes: {
|
||||
timeRange: PropTypes.shape({
|
||||
upper: PropTypes.string,
|
||||
lower: PropTypes.string,
|
||||
timeRange: shape({
|
||||
upper: string,
|
||||
lower: string,
|
||||
}).isRequired,
|
||||
queryConfigs: PropTypes.arrayOf(PropTypes.shape({})).isRequired,
|
||||
isActive: PropTypes.bool.isRequired,
|
||||
name: PropTypes.string,
|
||||
activeQueryIndex: PropTypes.number,
|
||||
queryConfigs: arrayOf(shape({})).isRequired,
|
||||
name: string,
|
||||
activeQueryIndex: number,
|
||||
height: string,
|
||||
heightPixels: number,
|
||||
},
|
||||
|
||||
contextTypes: {
|
||||
|
@ -32,20 +40,12 @@ const Visualization = React.createClass({
|
|||
};
|
||||
},
|
||||
|
||||
componentDidUpdate() {
|
||||
if (this.props.isActive) {
|
||||
this.panel.scrollIntoView();
|
||||
// scrollIntoView scrolls slightly *too* far, so this adds some top offset.
|
||||
this.panel.parentNode.scrollTop -= 10;
|
||||
}
|
||||
},
|
||||
|
||||
handleToggleView() {
|
||||
this.setState({isGraphInView: !this.state.isGraphInView});
|
||||
},
|
||||
|
||||
render() {
|
||||
const {queryConfigs, timeRange, isActive, name, activeQueryIndex} = this.props;
|
||||
const {queryConfigs, timeRange, activeQueryIndex, height, heightPixels} = this.props;
|
||||
const {source} = this.context;
|
||||
const proxyLink = source.links.proxy;
|
||||
|
||||
|
@ -61,7 +61,7 @@ const Visualization = React.createClass({
|
|||
const isInDataExplorer = true;
|
||||
|
||||
return (
|
||||
<div ref={(p) => this.panel = p} className={classNames("graph", {active: isActive})}>
|
||||
<div className={classNames("graph", {active: true})} style={{height}}>
|
||||
<div className="graph-heading">
|
||||
<div className="graph-title">
|
||||
{name || "Graph"}
|
||||
|
@ -73,7 +73,7 @@ const Visualization = React.createClass({
|
|||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div className="graph-container">
|
||||
<div className={classNames("", {"graph-container": isGraphInView, "table-container": !isGraphInView})}>
|
||||
{isGraphInView ? (
|
||||
<RefreshingLineGraph
|
||||
queries={queries}
|
||||
|
@ -81,7 +81,7 @@ const Visualization = React.createClass({
|
|||
activeQueryIndex={activeQueryIndex}
|
||||
isInDataExplorer={isInDataExplorer}
|
||||
/>
|
||||
) : <MultiTable queries={queries} />}
|
||||
) : <MultiTable queries={queries} height={heightPixels} />}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -1,61 +0,0 @@
|
|||
import React, {PropTypes} from 'react';
|
||||
import {connect} from 'react-redux';
|
||||
import Visualization from './Visualization';
|
||||
|
||||
const {shape, string} = PropTypes;
|
||||
|
||||
const Visualizations = React.createClass({
|
||||
propTypes: {
|
||||
timeRange: shape({
|
||||
upper: string,
|
||||
lower: string,
|
||||
}).isRequired,
|
||||
panels: shape({}).isRequired,
|
||||
queryConfigs: shape({}).isRequired,
|
||||
width: string,
|
||||
activePanelID: string,
|
||||
activeQueryID: string,
|
||||
},
|
||||
|
||||
render() {
|
||||
const {panels, queryConfigs, timeRange, width, activePanelID} = this.props;
|
||||
|
||||
const visualizations = Object.keys(panels).map((panelID) => {
|
||||
const panel = panels[panelID];
|
||||
const queries = panel.queryIds.map((id) => queryConfigs[id]);
|
||||
const isActive = panelID === activePanelID;
|
||||
|
||||
return <Visualization activeQueryIndex={this.getActiveQueryIndex(panelID)} name={panel.name} key={panelID} queryConfigs={queries} timeRange={timeRange} isActive={isActive} />;
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="panels" style={{width}}>
|
||||
{visualizations}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
|
||||
getActiveQueryIndex(panelID) {
|
||||
const {activeQueryID, activePanelID, panels} = this.props;
|
||||
const isPanelActive = panelID === activePanelID;
|
||||
|
||||
if (!isPanelActive) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (activeQueryID === null) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return panels[panelID].queryIds.indexOf(activeQueryID);
|
||||
},
|
||||
});
|
||||
|
||||
function mapStateToProps(state) {
|
||||
return {
|
||||
panels: state.panels,
|
||||
queryConfigs: state.queryConfigs,
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps)(Visualizations);
|
|
@ -1,9 +1,9 @@
|
|||
import React, {PropTypes} from 'react';
|
||||
import {connect} from 'react-redux';
|
||||
import PanelBuilder from '../components/PanelBuilder';
|
||||
import Visualizations from '../components/Visualizations';
|
||||
import QueryBuilder from '../components/QueryBuilder';
|
||||
import Visualization from '../components/Visualization';
|
||||
import Header from '../containers/Header';
|
||||
import ResizeContainer from 'shared/components/ResizeContainer';
|
||||
import ResizeContainer from 'src/shared/components/ResizeContainer';
|
||||
|
||||
import {
|
||||
setTimeRange as setTimeRangeAction,
|
||||
|
@ -23,11 +23,11 @@ const DataExplorer = React.createClass({
|
|||
self: string.isRequired,
|
||||
}).isRequired,
|
||||
}).isRequired,
|
||||
queryConfigs: PropTypes.shape({}),
|
||||
timeRange: shape({
|
||||
upper: string,
|
||||
lower: string,
|
||||
}).isRequired,
|
||||
activePanel: string,
|
||||
setTimeRange: func.isRequired,
|
||||
},
|
||||
|
||||
|
@ -55,7 +55,9 @@ const DataExplorer = React.createClass({
|
|||
},
|
||||
|
||||
render() {
|
||||
const {timeRange, setTimeRange, activePanel} = this.props;
|
||||
const {timeRange, setTimeRange, queryConfigs} = this.props;
|
||||
const {activeQueryID} = this.state;
|
||||
const queries = Object.keys(queryConfigs).map((q) => queryConfigs[q]);
|
||||
|
||||
return (
|
||||
<div className="data-explorer">
|
||||
|
@ -64,16 +66,17 @@ const DataExplorer = React.createClass({
|
|||
timeRange={timeRange}
|
||||
/>
|
||||
<ResizeContainer>
|
||||
<PanelBuilder
|
||||
<Visualization
|
||||
timeRange={timeRange}
|
||||
activePanelID={activePanel}
|
||||
queryConfigs={queries}
|
||||
activeQueryID={this.state.activeQueryID}
|
||||
setActiveQuery={this.handleSetActiveQuery}
|
||||
activeQueryIndex={0}
|
||||
/>
|
||||
<Visualizations
|
||||
<QueryBuilder
|
||||
queries={queries}
|
||||
timeRange={timeRange}
|
||||
activePanelID={activePanel}
|
||||
activeQueryID={this.state.activeQueryID}
|
||||
setActiveQuery={this.handleSetActiveQuery}
|
||||
activeQueryID={activeQueryID}
|
||||
/>
|
||||
</ResizeContainer>
|
||||
</div>
|
||||
|
@ -82,11 +85,11 @@ const DataExplorer = React.createClass({
|
|||
});
|
||||
|
||||
function mapStateToProps(state) {
|
||||
const {timeRange, dataExplorerUI} = state;
|
||||
const {timeRange, queryConfigs} = state;
|
||||
|
||||
return {
|
||||
timeRange,
|
||||
activePanel: dataExplorerUI.activePanel,
|
||||
queryConfigs,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -1,11 +0,0 @@
|
|||
export default function dataExplorerUI(state = {}, action) {
|
||||
switch (action.type) {
|
||||
case 'ACTIVATE_PANEL':
|
||||
case 'CREATE_PANEL': {
|
||||
const {panelID} = action.payload;
|
||||
return {...state, activePanel: panelID};
|
||||
}
|
||||
}
|
||||
|
||||
return state;
|
||||
}
|
|
@ -1,11 +1,7 @@
|
|||
import queryConfigs from './queryConfigs';
|
||||
import panels from './panels';
|
||||
import timeRange from './timeRange';
|
||||
import dataExplorerUI from './dataExplorerUI';
|
||||
|
||||
export {
|
||||
queryConfigs,
|
||||
panels,
|
||||
timeRange,
|
||||
dataExplorerUI,
|
||||
};
|
||||
|
|
|
@ -1,51 +0,0 @@
|
|||
import update from 'react-addons-update';
|
||||
|
||||
export default function panels(state = {}, action) {
|
||||
switch (action.type) {
|
||||
case 'CREATE_PANEL': {
|
||||
const {panelID, queryID} = action.payload;
|
||||
return {
|
||||
...state,
|
||||
[panelID]: {id: panelID, queryIds: [queryID]},
|
||||
};
|
||||
}
|
||||
|
||||
case 'RENAME_PANEL': {
|
||||
const {panelId, name} = action.payload;
|
||||
return update(state, {
|
||||
[panelId]: {
|
||||
name: {$set: name},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
case 'DELETE_PANEL': {
|
||||
const {panelId} = action.payload;
|
||||
return update(state, {$apply: (p) => {
|
||||
const panelsCopy = Object.assign({}, p);
|
||||
delete panelsCopy[panelId];
|
||||
return panelsCopy;
|
||||
}});
|
||||
}
|
||||
|
||||
case 'ADD_QUERY': {
|
||||
const {panelId, queryId} = action.payload;
|
||||
return update(state, {
|
||||
[panelId]: {
|
||||
queryIds: {$push: [queryId]},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
case 'DELETE_QUERY': {
|
||||
const {panelId, queryId} = action.payload;
|
||||
return update(state, {
|
||||
[panelId]: {
|
||||
queryIds: {$set: state[panelId].queryIds.filter((id) => id !== queryId)},
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return state;
|
||||
}
|
|
@ -46,7 +46,6 @@ export default function queryConfigs(state = {}, action) {
|
|||
return nextState;
|
||||
}
|
||||
|
||||
case 'CREATE_PANEL':
|
||||
case 'ADD_KAPACITOR_QUERY':
|
||||
case 'ADD_QUERY': {
|
||||
const {queryID, options} = action.payload;
|
||||
|
@ -94,9 +93,9 @@ export default function queryConfigs(state = {}, action) {
|
|||
}
|
||||
|
||||
case 'DELETE_QUERY': {
|
||||
const {queryId} = action.payload;
|
||||
const {queryID} = action.payload;
|
||||
const nextState = update(state, {$apply: (configs) => {
|
||||
delete configs[queryId];
|
||||
delete configs[queryID];
|
||||
return configs;
|
||||
}});
|
||||
|
||||
|
|
|
@ -19,13 +19,11 @@ export const loadLocalStorage = () => {
|
|||
}
|
||||
};
|
||||
|
||||
export const saveToLocalStorage = ({panels, queryConfigs, timeRange, dataExplorerUI}) => {
|
||||
export const saveToLocalStorage = ({queryConfigs, timeRange}) => {
|
||||
try {
|
||||
window.localStorage.setItem('state', JSON.stringify({
|
||||
panels,
|
||||
queryConfigs,
|
||||
timeRange,
|
||||
dataExplorerUI,
|
||||
}));
|
||||
} catch (err) {
|
||||
console.error('Unable to save data explorer: ', JSON.parse(err)); // eslint-disable-line no-console
|
||||
|
|
|
@ -79,7 +79,6 @@ export default React.createClass({
|
|||
labels,
|
||||
connectSeparatedPoints: true,
|
||||
labelsKMB: true,
|
||||
height: 300,
|
||||
axisLineColor: '#383846',
|
||||
gridLineColor: '#383846',
|
||||
title,
|
||||
|
|
|
@ -9,8 +9,8 @@ const ResizeContainer = React.createClass({
|
|||
|
||||
getInitialState() {
|
||||
return {
|
||||
leftWidth: '34%',
|
||||
rightWidth: '66%',
|
||||
topHeight: '60%',
|
||||
bottomHeight: '40%',
|
||||
isDragging: false,
|
||||
};
|
||||
},
|
||||
|
@ -32,40 +32,43 @@ const ResizeContainer = React.createClass({
|
|||
return;
|
||||
}
|
||||
|
||||
const appWidth = parseInt(getComputedStyle(this.refs.resizeContainer).width, 10);
|
||||
// handleOffSet moves the resize handle as many pixels as the side bar is taking up.
|
||||
const handleOffSet = window.innerWidth - appWidth;
|
||||
const appHeight = parseInt(getComputedStyle(this.refs.resizeContainer).height, 10);
|
||||
// headingOffset moves the resize handle as many pixels as the page-heading is taking up.
|
||||
const headingOffset = window.innerHeight - appHeight;
|
||||
const turnToPercent = 100;
|
||||
const newLeftPanelPercent = Math.ceil(((e.pageX - handleOffSet) / (appWidth)) * turnToPercent);
|
||||
const newRightPanelPercent = (turnToPercent - newLeftPanelPercent);
|
||||
const newTopPanelPercent = Math.ceil(((e.pageY - headingOffset) / (appHeight)) * turnToPercent);
|
||||
const newBottomPanelPercent = (turnToPercent - newTopPanelPercent);
|
||||
|
||||
// Don't trigger a resize unless the change in size is greater than minResizePercentage
|
||||
const minResizePercentage = 0.5;
|
||||
if (Math.abs(newLeftPanelPercent - parseFloat(this.state.leftWidth)) < minResizePercentage) {
|
||||
if (Math.abs(newTopPanelPercent - parseFloat(this.state.topHeight)) < minResizePercentage) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Don't trigger a resize if the new sizes are too small
|
||||
const minLeftPanelWidth = 371;
|
||||
const minRightPanelWidth = 389;
|
||||
if (((newLeftPanelPercent / turnToPercent) * appWidth) < minLeftPanelWidth || ((newRightPanelPercent / turnToPercent) * appWidth) < minRightPanelWidth) {
|
||||
const minTopPanelHeight = 200;
|
||||
const minBottomPanelHeight = 100;
|
||||
const topHeightPixels = ((newTopPanelPercent / turnToPercent) * appHeight);
|
||||
const bottomHeightPixels = ((newBottomPanelPercent / turnToPercent) * appHeight);
|
||||
|
||||
if (topHeightPixels < minTopPanelHeight || bottomHeightPixels < minBottomPanelHeight) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState({leftWidth: `${(newLeftPanelPercent)}%`, rightWidth: `${(newRightPanelPercent)}%`});
|
||||
this.setState({topHeight: `${(newTopPanelPercent)}%`, bottomHeight: `${(newBottomPanelPercent)}%`, topHeightPixels});
|
||||
},
|
||||
|
||||
render() {
|
||||
const {leftWidth, rightWidth, isDragging} = this.state;
|
||||
const left = React.cloneElement(this.props.children[0], {width: leftWidth});
|
||||
const right = React.cloneElement(this.props.children[1], {width: rightWidth});
|
||||
const handle = <ResizeHandle isDragging={isDragging} onHandleStartDrag={this.handleStartDrag} />;
|
||||
const {topHeight, bottomHeight, isDragging, topHeightPixels} = this.state;
|
||||
const top = React.cloneElement(this.props.children[0], {height: topHeight, heightPixels: topHeightPixels});
|
||||
const bottom = React.cloneElement(this.props.children[1], {height: bottomHeight, top: topHeight});
|
||||
const handle = <ResizeHandle isDragging={isDragging} onHandleStartDrag={this.handleStartDrag} top={topHeight} />;
|
||||
|
||||
return (
|
||||
<div className="resize-container page-contents" onMouseLeave={this.handleMouseLeave} onMouseUp={this.handleStopDrag} onMouseMove={this.handleDrag} ref="resizeContainer" >
|
||||
{left}
|
||||
{top}
|
||||
{handle}
|
||||
{right}
|
||||
{bottom}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
|
|
|
@ -1,15 +1,24 @@
|
|||
import React from 'react';
|
||||
import cx from 'classnames';
|
||||
|
||||
const {func, bool} = React.PropTypes;
|
||||
const {func, bool, string} = React.PropTypes;
|
||||
const ResizeHandle = React.createClass({
|
||||
propTypes: {
|
||||
onHandleStartDrag: func.isRequired,
|
||||
isDragging: bool.isRequired,
|
||||
top: string,
|
||||
},
|
||||
|
||||
render() {
|
||||
return <div className={cx("resizer__handle", {dragging: this.props.isDragging})} ref="resizer" onMouseDown={this.props.onHandleStartDrag} />;
|
||||
const {isDragging, onHandleStartDrag, top} = this.props;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cx("resizer__handle", {dragging: isDragging})}
|
||||
onMouseDown={onHandleStartDrag}
|
||||
style={{top}}
|
||||
/>
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
@ -82,21 +82,25 @@
|
|||
.dygraph-axis-label {
|
||||
color: $g11-sidewalk !important;
|
||||
font-weight: 500 !important;
|
||||
user-select: none;
|
||||
}
|
||||
.dygraph-axis-label-y {
|
||||
padding: 0 9px 0 0 !important;
|
||||
text-align: left !important;
|
||||
left: 0 !important;
|
||||
user-select: none;
|
||||
}
|
||||
.dygraph-axis-label-y2 {
|
||||
padding: 0 0 0 9px !important;
|
||||
text-align: right !important;
|
||||
user-select: none;
|
||||
}
|
||||
.graph-container > div > div > div > div {}
|
||||
|
||||
/* Vertical Axis Labels */
|
||||
.dygraph-ylabel,
|
||||
.dygraph-y2label {
|
||||
user-select: none;
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
.group-by-time-dropdown .dropdown-toggle {
|
||||
.group-by-time-dropdown .dropdown-toggle {
|
||||
width: 70px;
|
||||
}
|
||||
.group-by-time {
|
||||
|
@ -17,6 +17,7 @@
|
|||
border-style: solid;
|
||||
border-color: $g5-pepper;
|
||||
border-width: 2px;
|
||||
background-color: $g3-castle;
|
||||
}
|
||||
.dropdown-toggle {
|
||||
border-radius: 0px 3px 3px 0;
|
||||
|
|
|
@ -10,12 +10,12 @@ $resizer-color-hover: $g8-storm;
|
|||
$resizer-color-active: $c-pool;
|
||||
|
||||
.resizer__handle {
|
||||
top: 0;
|
||||
top: 60%;
|
||||
left: 0;
|
||||
width: $resizer-click-area;
|
||||
margin-left: -$resizer-click-area/2;
|
||||
margin-right: -$resizer-click-area/2;
|
||||
height: 100%;
|
||||
height: $resizer-click-area;
|
||||
margin-top: -$resizer-click-area/2;
|
||||
margin-bottom: -$resizer-click-area/2;
|
||||
width: 100%;
|
||||
z-index: 2;
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
|
@ -31,7 +31,7 @@ $resizer-color-active: $c-pool;
|
|||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%,-50%) rotate(90deg);
|
||||
transform: translate(-50%,-50%);
|
||||
width: 130px;
|
||||
height: $resizer-handle-width;
|
||||
line-height: $resizer-handle-width;
|
||||
|
@ -48,18 +48,18 @@ $resizer-color-active: $c-pool;
|
|||
content: '';
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
height: 100%;
|
||||
width: $resizer-line-width;
|
||||
top: 50%;
|
||||
left: 0;
|
||||
transform: translateY(-50%);
|
||||
width: 100%;
|
||||
height: $resizer-line-width;
|
||||
background-color: $resizer-color;
|
||||
box-shadow: 0 0 0 transparent;
|
||||
transition:
|
||||
background-color 0.19s ease;
|
||||
}
|
||||
&:hover {
|
||||
cursor: ew-resize;
|
||||
cursor: ns-resize;
|
||||
|
||||
&:before {
|
||||
background-color: $resizer-color-hover;
|
||||
|
@ -85,4 +85,4 @@ $resizer-color-active: $c-pool;
|
|||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: stretch;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,4 +11,4 @@ $chronograf-page-header-height: 60px;
|
|||
$sidebar-tier1-height: 56px;
|
||||
|
||||
// Data Explorer
|
||||
$explorer-page-padding: 18px;
|
||||
$explorer-page-padding: $page-wrapper-padding;
|
||||
|
|
|
@ -15,18 +15,30 @@
|
|||
|
||||
.page-header {
|
||||
padding-left: $explorer-page-padding;
|
||||
padding-right: ($explorer-page-padding + $scrollbar-width);
|
||||
padding-right: $explorer-page-padding;
|
||||
}
|
||||
.page-header__container {
|
||||
max-width: 100%;
|
||||
}
|
||||
.page-contents {
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
$query-editor-gutter: 16px;
|
||||
$query-editor-tab-inactive: $g2-kevlar;
|
||||
$query-editor-tab-active: $g3-castle;
|
||||
$query-editor-height: 250px;
|
||||
$graph-bg-color: $g3-castle;
|
||||
$graph-active-color: $g4-onyx;
|
||||
$graph-radius: 4px;
|
||||
$de-vertical-margin: 16px;
|
||||
$dygraphs-legend-offset: 32px;
|
||||
$de-graph-heading-height: 44px;
|
||||
|
||||
// DE Specific components
|
||||
@import 'data-explorer/query-builder';
|
||||
@import 'data-explorer/page-header';
|
||||
@import 'data-explorer/panel-builder';
|
||||
@import 'data-explorer/panel';
|
||||
@import 'data-explorer/query-editor';
|
||||
@import 'data-explorer/raw-text';
|
||||
@import 'data-explorer/tag-list';
|
||||
|
|
|
@ -1,15 +0,0 @@
|
|||
.panel-builder {
|
||||
width: 399px;
|
||||
overflow-x: hidden;
|
||||
background: $g1-raven;
|
||||
padding: $explorer-page-padding;
|
||||
@include gradient-v($g2-kevlar,$g0-obsidian);
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
> .btn {
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
}
|
|
@ -1,218 +0,0 @@
|
|||
.panels {
|
||||
padding: $explorer-page-padding;
|
||||
overflow: auto;
|
||||
user-select: none;
|
||||
-webkit-touch-callout: none;
|
||||
-webkit-user-select: none;
|
||||
-khtml-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
width: 100%;
|
||||
@include gradient-v($g2-kevlar,$g0-obsidian);
|
||||
@include custom-scrollbar($g2-kevlar,$c-pool);
|
||||
}
|
||||
|
||||
.panel {
|
||||
display: block;
|
||||
background-color: $g3-castle;
|
||||
border-radius: $radius;
|
||||
margin-bottom: 6px;
|
||||
transition: background-color 0.25s ease;
|
||||
border: 0;
|
||||
|
||||
&:hover {
|
||||
background-color: $g4-onyx;
|
||||
}
|
||||
|
||||
// For when an panel item is open
|
||||
&.active {
|
||||
background-color: $g4-onyx;
|
||||
|
||||
.panel--name {
|
||||
color: $g20-white;
|
||||
|
||||
.icon {
|
||||
transform: translateY(-50%) rotate(90deg);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// panel Header Bar
|
||||
.panel--header {
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
display: flex;
|
||||
height: 36px;
|
||||
padding: 0 11px;
|
||||
cursor: pointer;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
border-radius: $radius;
|
||||
}
|
||||
.panel--name {
|
||||
color: $g13-mist;
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
position: relative;
|
||||
padding-left: 16px;
|
||||
transition: color 0.25s ease;
|
||||
|
||||
.icon.caret-right {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 50%;
|
||||
transform: translateY(-50%) rotate(0deg);
|
||||
font-size: 0.75em;
|
||||
transition: transform 0.25s ease;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: $g17-whisper;
|
||||
}
|
||||
}
|
||||
.panel--actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.panel--action {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border: 0;
|
||||
background-color: transparent;
|
||||
color: $g9-mountain;
|
||||
margin-left: 2px;
|
||||
transition: color 0.25s ease;
|
||||
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
color: $g18-cloud;
|
||||
}
|
||||
}
|
||||
|
||||
// Tabs
|
||||
.panel--tabs {
|
||||
display: flex;
|
||||
background-color: $g4-onyx;
|
||||
padding: 0 11px;
|
||||
}
|
||||
.panel--tab {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: $g11-sidewalk;
|
||||
background: $g5-pepper;
|
||||
height: 28px;
|
||||
margin-right: 2px;
|
||||
border-radius: $radius $radius 0 0;
|
||||
cursor: pointer;
|
||||
padding: 0 8px 0 8px;
|
||||
transition:
|
||||
color 0.25s ease,
|
||||
background-color 0.25s ease;
|
||||
|
||||
&:hover {
|
||||
color: $g18-cloud;
|
||||
background-color: $g6-smoke;
|
||||
}
|
||||
&.active {
|
||||
background: $g6-smoke;
|
||||
color: $g15-platinum;
|
||||
}
|
||||
|
||||
&-delete {
|
||||
margin: 0 -4px 0 1px;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
background-color: transparent;
|
||||
display: inline-block;
|
||||
vertical-align: text-top;
|
||||
position: relative;
|
||||
|
||||
&:before,
|
||||
&:after {
|
||||
display: block;
|
||||
content: '';
|
||||
width: 8px;
|
||||
height: 2px;
|
||||
background-color: $g8-storm;
|
||||
transition:
|
||||
background-color 0.25s ease;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
}
|
||||
&:before {
|
||||
transform: translate(-50%,-50%) rotate(45deg);
|
||||
}
|
||||
&:after {
|
||||
transform: translate(-50%,-50%) rotate(-45deg);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
&:before,
|
||||
&:after {
|
||||
background-color: $c-dreamsicle;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.panel--tab-new {
|
||||
> .dropdown-toggle {
|
||||
height: 28px !important;
|
||||
border-radius: $radius $radius 0 0;
|
||||
|
||||
> .icon {
|
||||
margin: 0;
|
||||
font-size: 12px;
|
||||
position: relative;
|
||||
top: -1px;
|
||||
}
|
||||
}
|
||||
> .dropdown-menu {
|
||||
width: 108px !important;
|
||||
min-width: 108px !important;
|
||||
max-width: 108px !important;
|
||||
}
|
||||
}
|
||||
.panel--tab-label {
|
||||
display: inline-block;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
max-width: 177px;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
/*
|
||||
Tab Contents
|
||||
-------------------------------------------
|
||||
*/
|
||||
.panel--tab-contents {
|
||||
padding: 6px;
|
||||
background-color: $g6-smoke;
|
||||
border-radius: 0 0 $radius $radius;
|
||||
}
|
||||
|
||||
|
||||
/* Time Range Selector */
|
||||
.time-range-dropdown {
|
||||
display: inline-block;
|
||||
|
||||
.dropdown-toggle {
|
||||
width: 160px;
|
||||
}
|
||||
}
|
||||
|
||||
.panel__header-actions {
|
||||
display: flex;
|
||||
|
||||
* {
|
||||
margin-left: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.alert.alert-rawquery {
|
||||
border-color: $g5-pepper;
|
||||
border-color: $g6-smoke;
|
||||
color: $g12-forge;
|
||||
}
|
|
@ -0,0 +1,218 @@
|
|||
.query-builder {
|
||||
position: absolute;
|
||||
width: calc(100% - #{($explorer-page-padding * 2)});
|
||||
left: $explorer-page-padding;
|
||||
height: 40%;
|
||||
top: 60%;
|
||||
border: 0;
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
// Tabs
|
||||
.query-builder--tabs {
|
||||
display: flex;
|
||||
width: 250px;
|
||||
margin-top: $de-vertical-margin;
|
||||
height: calc(100% - #{($de-vertical-margin * 2)});
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
@include gradient-v($g3-castle,$g1-raven);
|
||||
border-radius: $radius 0 0 $radius;
|
||||
}
|
||||
.query-builder--tabs-heading {
|
||||
height: 60px;
|
||||
padding: 0 9px 0 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
h1 {
|
||||
font-size: 17px;
|
||||
font-weight: 400;
|
||||
text-transform: uppercase;
|
||||
color: $g18-cloud;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
.query-builder--tab {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
color: $g11-sidewalk;
|
||||
background: transparent;
|
||||
height: 30px;
|
||||
cursor: pointer;
|
||||
padding: 0 8px 0 16px;
|
||||
transition:
|
||||
color 0.25s ease,
|
||||
background-color 0.25s ease;
|
||||
|
||||
&:hover {
|
||||
color: $g15-platinum;
|
||||
background-color: $g4-onyx;
|
||||
}
|
||||
&.active {
|
||||
color: $g18-cloud;
|
||||
background: $g5-pepper;
|
||||
}
|
||||
|
||||
&-delete {
|
||||
margin: 0;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
background-color: transparent;
|
||||
display: inline-block;
|
||||
vertical-align: text-top;
|
||||
position: relative;
|
||||
|
||||
&:before,
|
||||
&:after {
|
||||
display: block;
|
||||
content: '';
|
||||
width: 8px;
|
||||
height: 2px;
|
||||
background-color: $g8-storm;
|
||||
transition:
|
||||
background-color 0.25s ease;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
}
|
||||
&:before {
|
||||
transform: translate(-50%,-50%) rotate(45deg);
|
||||
}
|
||||
&:after {
|
||||
transform: translate(-50%,-50%) rotate(-45deg);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
&:before,
|
||||
&:after {
|
||||
background-color: $c-dreamsicle;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.panel--tab-new {
|
||||
> .dropdown-toggle {
|
||||
height: 30px !important;
|
||||
border-radius: $radius;
|
||||
background-color: $c-pool;
|
||||
color: $g20-white !important;
|
||||
padding: 0;
|
||||
|
||||
> .icon {
|
||||
margin: 0;
|
||||
font-size: 12px;
|
||||
position: relative;
|
||||
top: -1px;
|
||||
}
|
||||
&:hover {
|
||||
background-color: $c-laser;
|
||||
}
|
||||
}
|
||||
> .dropdown-menu {
|
||||
width: 108px !important;
|
||||
min-width: 108px !important;
|
||||
max-width: 108px !important;
|
||||
}
|
||||
}
|
||||
.panel--tab-new.open {
|
||||
> .dropdown-toggle,
|
||||
> .dropdown-toggle:hover {
|
||||
background-color: $c-laser !important;
|
||||
color: $g20-white !important;
|
||||
}
|
||||
}
|
||||
.query-builder--tab-label {
|
||||
display: inline-block;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
max-width: 177px;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
/*
|
||||
Tab Contents
|
||||
-------------------------------------------
|
||||
*/
|
||||
$query-builder--column-heading-height: 60px;
|
||||
.query-builder--tab-contents {
|
||||
width: 100%;
|
||||
margin-top: $de-vertical-margin;
|
||||
height: calc(100% - #{($de-vertical-margin * 2)});
|
||||
background-color: $g4-onyx;
|
||||
border-radius: 0 $radius $radius 0;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
.query-builder--tab-contents > div {
|
||||
position: absolute;
|
||||
top: 4px;
|
||||
left: 4px;
|
||||
width: calc(100% - 8px);
|
||||
height: calc(100% - 8px);
|
||||
}
|
||||
.query-builder--columns {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: calc(100% - 60px);
|
||||
top: 60px;
|
||||
}
|
||||
.query-builder--column-heading {
|
||||
width: 100%;
|
||||
height: $query-builder--column-heading-height;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: $g13-mist;
|
||||
padding: 0 9px;
|
||||
line-height: $query-builder--column-heading-height;
|
||||
border-bottom: 2px solid $g5-pepper;
|
||||
}
|
||||
.query-builder--column {
|
||||
position: absolute;
|
||||
width: 25%;
|
||||
height: 100%;
|
||||
top: 0;
|
||||
|
||||
.qeditor--list {
|
||||
position: absolute;
|
||||
top: $query-builder--column-heading-height;
|
||||
height: calc(100% - #{$query-builder--column-heading-height});
|
||||
width: 100%;
|
||||
left: 0;
|
||||
padding: 0;
|
||||
overflow: auto;
|
||||
overflow-x: hidden;
|
||||
overflow-y: scroll;
|
||||
@include custom-scrollbar($g4-onyx,$c-pool);
|
||||
background-color: $g4-onyx;
|
||||
}
|
||||
}
|
||||
.query-builder--column:nth-of-type(1) { left: 0; }
|
||||
.query-builder--column:nth-of-type(2) { left: 25%; }
|
||||
.query-builder--column:nth-of-type(3) { left: 50%; }
|
||||
.query-builder--column:nth-of-type(4) { left: 75%; }
|
||||
|
||||
/* Time Range Selector */
|
||||
.time-range-dropdown {
|
||||
display: inline-block;
|
||||
|
||||
.dropdown-toggle {
|
||||
width: 160px;
|
||||
}
|
||||
}
|
||||
|
||||
.alert.alert-rawquery {
|
||||
border-color: $g5-pepper;
|
||||
border-color: $g6-smoke;
|
||||
color: $g12-forge;
|
||||
}
|
|
@ -5,12 +5,8 @@
|
|||
Abbreviated as "qeditor"
|
||||
|
||||
*/
|
||||
$query-editor-gutter: 16px;
|
||||
$query-editor-tab-inactive: $g2-kevlar;
|
||||
$query-editor-tab-active: $g3-castle;
|
||||
$query-editor-height: 250px;
|
||||
|
||||
.qeditor--query-preview {
|
||||
.query-builder--query-preview {
|
||||
position: relative;
|
||||
|
||||
pre {
|
||||
|
@ -18,11 +14,10 @@ $query-editor-height: 250px;
|
|||
border: 0;
|
||||
background-color: $query-editor-tab-inactive;
|
||||
color: $c-pool;
|
||||
border-radius: $radius-small $radius-small 0 0;
|
||||
border-bottom: 2px solid $query-editor-tab-active;
|
||||
border-radius: $radius;
|
||||
margin-bottom: 0;
|
||||
overflow: auto;
|
||||
min-height: 3.25em;
|
||||
height: 56px;
|
||||
@include custom-scrollbar($query-editor-tab-inactive, $c-pool);
|
||||
|
||||
code {
|
||||
|
@ -90,22 +85,23 @@ $query-editor-height: 250px;
|
|||
list-style-type: none;
|
||||
margin: 0;
|
||||
font-size: 12px;
|
||||
padding: 4px 9px 4px 18px;
|
||||
font-weight: 500;
|
||||
padding: 4px 9px;
|
||||
transition:
|
||||
color 0.25s ease,
|
||||
background-color 0.25s ease;
|
||||
|
||||
&:hover {
|
||||
background-color: $g4-onyx;
|
||||
color: $g17-whisper;
|
||||
background-color: $g5-pepper;
|
||||
color: $g15-platinum;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
&-radio {
|
||||
&.active {
|
||||
color: $g20-white;
|
||||
background-color: $g4-onyx;
|
||||
font-weight: 600;
|
||||
background-color: $g5-pepper;
|
||||
font-weight: 700;
|
||||
}
|
||||
}
|
||||
&-checkbox {
|
||||
|
@ -169,11 +165,18 @@ $query-editor-height: 250px;
|
|||
}
|
||||
}
|
||||
}
|
||||
&-header {
|
||||
position: relative;
|
||||
background-color: $query-editor-tab-active;
|
||||
padding: 8px 18px 0px 18px;
|
||||
}
|
||||
}
|
||||
.qeditor--list-header {
|
||||
position: absolute;
|
||||
top: 15px;
|
||||
right: 16px;
|
||||
width: calc(60% - 16px);
|
||||
height: 30px;
|
||||
padding: 0;
|
||||
z-index: 10;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
// List empty state
|
||||
|
@ -184,8 +187,7 @@ $query-editor-height: 250px;
|
|||
width: 100%;
|
||||
padding: 18px 0;
|
||||
height: $query-editor-height;
|
||||
background-color: $query-editor-tab-active;
|
||||
border-radius: 0 0 $radius $radius;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
// Hidden dropdowns
|
||||
|
@ -210,7 +212,7 @@ $query-editor-height: 250px;
|
|||
height: 30px;
|
||||
border-radius: 15px;
|
||||
font-size: 13px;
|
||||
padding-left: 38px;
|
||||
padding-left: 28px;
|
||||
outline: none;
|
||||
color: $g20-white;
|
||||
font-weight: 700;
|
||||
|
@ -244,12 +246,12 @@ $query-editor-height: 250px;
|
|||
}
|
||||
+ .icon {
|
||||
position: absolute;
|
||||
top: calc(50% + 5px);
|
||||
left: calc(19px * 2);
|
||||
top: 50%;
|
||||
left: 11px;
|
||||
transform: translateY(-50%);
|
||||
color: $g10-wolf;
|
||||
transition: color 0.25s ease;
|
||||
font-size: 12px;
|
||||
z-index: 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -26,12 +26,12 @@ $raw-text-color: $c-comet;
|
|||
@include custom-scrollbar($g2-kevlar, $raw-text-color);
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100px;
|
||||
height: 56px;
|
||||
background-color: $g2-kevlar;
|
||||
border: 2px solid $g2-kevlar;
|
||||
color: $raw-text-color;
|
||||
padding: (9px - 2px);
|
||||
border-radius: 3px 3px 0 0;
|
||||
border-radius: $radius;
|
||||
line-height: 1.5em;
|
||||
-webkit-appearance: none;
|
||||
-moz-appearance: none;
|
||||
|
|
|
@ -1,10 +1,15 @@
|
|||
.tag-list {
|
||||
|
||||
&__item {
|
||||
height: 30px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
&:hover .tag-list__group-by {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
&.open {
|
||||
font-weight: 600;
|
||||
color: $g20-white;
|
||||
|
@ -63,7 +68,7 @@
|
|||
opacity: 0;
|
||||
background-color: $c-pool;
|
||||
border-radius: 50%;
|
||||
transition:
|
||||
transition:
|
||||
transform 0.25s ease,
|
||||
opacity 0.25s ease;
|
||||
}
|
||||
|
@ -124,7 +129,7 @@
|
|||
color: $g10-wolf;
|
||||
font-weight: 500;
|
||||
}
|
||||
&:-ms-input-placeholder {
|
||||
&:-ms-input-placeholder {
|
||||
color: $g10-wolf;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
@ -148,6 +153,7 @@
|
|||
}
|
||||
|
||||
.tag-list__group-by {
|
||||
display: none;
|
||||
background-color: $g5-pepper;
|
||||
border-color: $g5-pepper;
|
||||
color: $g13-mist !important;
|
||||
|
@ -165,6 +171,7 @@
|
|||
border-color: $g6-smoke;
|
||||
}
|
||||
&.active {
|
||||
display: flex;
|
||||
background: $c-pool;
|
||||
border-color: $c-pool;
|
||||
|
||||
|
@ -173,4 +180,4 @@
|
|||
border-color: $c-laser;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,24 +1,19 @@
|
|||
$graph-bg-color: $g3-castle;
|
||||
$graph-active-color: $g4-onyx;
|
||||
$graph-radius: 4px;
|
||||
|
||||
$dygraphs-legend-offset: 32px;
|
||||
|
||||
.graph {
|
||||
position: relative;
|
||||
margin-bottom: 18px;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 100%;
|
||||
}
|
||||
position: absolute;
|
||||
width: calc(100% - #{($explorer-page-padding * 2)});
|
||||
left: $explorer-page-padding;
|
||||
top: 0;
|
||||
height: 60%;
|
||||
}
|
||||
.graph-heading {
|
||||
position: relative;
|
||||
top: $de-vertical-margin;
|
||||
background-color: $graph-bg-color;
|
||||
border-radius: $graph-radius $graph-radius 0 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
height: 44px;
|
||||
height: $de-graph-heading-height;
|
||||
padding: 0 16px;
|
||||
transition:
|
||||
background-color 0.25s ease;
|
||||
|
@ -39,6 +34,41 @@ $dygraphs-legend-offset: 32px;
|
|||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.table-container {
|
||||
background-color: $graph-bg-color;
|
||||
border-radius: 0 0 $graph-radius $graph-radius;
|
||||
padding: 8px 16px;
|
||||
position: relative;
|
||||
top: $de-vertical-margin;
|
||||
height: calc(100% - #{$de-graph-heading-height} - #{($de-vertical-margin * 2)});
|
||||
|
||||
& > div {
|
||||
position: absolute;
|
||||
width: calc(100% - #{($de-vertical-margin * 2)});
|
||||
height: calc(100% - #{$de-vertical-margin});
|
||||
top: ($de-vertical-margin/2);
|
||||
left: $de-vertical-margin;;
|
||||
}
|
||||
& > div .multi-table__tabs {
|
||||
position: absolute;
|
||||
height: 30px;
|
||||
width: 100%;
|
||||
}
|
||||
& > div > div:last-child {
|
||||
position: absolute;
|
||||
top: 30px;
|
||||
height: calc(100% - 30px) !important;
|
||||
width: 100%;
|
||||
}
|
||||
.fixedDataTableLayout_main {
|
||||
height: 100% !important;
|
||||
}
|
||||
.generic-empty-state {
|
||||
background-color: $g6-smoke;
|
||||
padding: 50px 0;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
.graph-container {
|
||||
background-color: $graph-bg-color;
|
||||
border-radius: 0 0 $graph-radius $graph-radius;
|
||||
|
@ -48,6 +78,30 @@ $dygraphs-legend-offset: 32px;
|
|||
transition:
|
||||
background-color 0.25s ease;
|
||||
}
|
||||
.data-explorer .graph-container {
|
||||
top: $de-vertical-margin;
|
||||
height: calc(100% - #{$de-graph-heading-height} - #{($de-vertical-margin * 2)});
|
||||
padding: 0;
|
||||
|
||||
& > div {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
& > div > div {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 8px 16px;
|
||||
// width: calc(100% - #{($de-vertical-margin * 2)});
|
||||
// height: calc(100% - #{$de-vertical-margin});
|
||||
// top: ($de-vertical-margin / 2);
|
||||
// left: $de-vertical-margin;
|
||||
}
|
||||
& > div > div > div:first-child {
|
||||
height: 100% !important;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* Active State */
|
||||
|
@ -144,4 +198,4 @@ $dygraphs-legend-offset: 32px;
|
|||
background-color: $g6-smoke;
|
||||
color: $g14-chromium;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -219,6 +219,34 @@ div.qeditor.kapacitor-metric-selector {
|
|||
.kapacitor-tab-list {
|
||||
background-color: $kapacitor-graphic-color;
|
||||
border-radius: 0 0 $kap-radius-lg $kap-radius-lg;
|
||||
|
||||
.query-builder--column {
|
||||
position: relative;
|
||||
top: initial;
|
||||
left: initial;
|
||||
width: 100%;
|
||||
height: 190px;
|
||||
|
||||
.qeditor--list-header {
|
||||
width: 50%;
|
||||
top: -34px;
|
||||
right: 0;
|
||||
z-index: 5;
|
||||
}
|
||||
.qeditor--list {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
.group-by-time-dropdown {
|
||||
width: 70px;
|
||||
}
|
||||
}
|
||||
.query-builder--column-heading {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
.qeditor--list {
|
||||
overflow: auto;
|
||||
|
|
|
@ -443,7 +443,7 @@ $toggle-border: 2px;
|
|||
.toggle-btn {
|
||||
height: ($toggle-height-sm - ($toggle-border * 2));
|
||||
line-height: ($toggle-height-sm - ($toggle-border * 2));
|
||||
padding: 0 $toggle-padding-sm;
|
||||
padding: 0 16px;
|
||||
font-size: $toggle-font-sm;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue