Merge branch 'master' into http-tcp

# Conflicts:
#	CHANGELOG.md
pull/864/head
Hunter Trujillo 2017-02-10 13:31:01 -07:00
commit 57bf5f571a
51 changed files with 1227 additions and 1136 deletions

View File

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

@ -1,3 +1,4 @@
github.com/NYTimes/gziphandler 6710af535839f57c687b62c4c23d649f9545d885
github.com/Sirupsen/logrus 3ec0642a7fb6488f65b06f9040adc67e3990296a
github.com/boltdb/bolt 5cc10bbbc5c141029940133bb33c9e969512a698
github.com/bouk/httprouter ee8b3818a7f51fbc94cc709b5744b52c2c725e91

View File

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

22
dist/dist.go vendored
View File

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

View File

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

View File

@ -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 + "'"
}

50
kapacitor/vars_test.go Normal file
View File

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

View File

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

View File

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

View File

@ -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});
});
});

View File

@ -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);
});
});

View File

@ -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,
},
};
}

View File

@ -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>
);
},
});

View File

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

View File

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

View File

@ -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() {

View File

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

View File

@ -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>&nbsp;&nbsp;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);

View File

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

View File

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

View File

@ -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>
);
},
});

View File

@ -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>
);
},

View File

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

View File

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

View File

@ -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()}

View File

@ -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>
);
},

View File

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

View File

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

View File

@ -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,
};
}

View File

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

View File

@ -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,
};

View File

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

View File

@ -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;
}});

View File

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

View File

@ -79,7 +79,6 @@ export default React.createClass({
labels,
connectSeparatedPoints: true,
labelsKMB: true,
height: 300,
axisLineColor: '#383846',
gridLineColor: '#383846',
title,

View File

@ -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>
);
},

View File

@ -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}}
/>
);
},
});

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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