Merge branch 'master' into feature/kapa_kafka_alert_node
commit
fc87409adf
|
@ -17,6 +17,9 @@
|
|||
1. [#3245](https://github.com/influxdata/chronograf/pull/3245): Display 'no results' on cells without results
|
||||
1. [#3354](https://github.com/influxdata/chronograf/pull/3354): Disable template variables for non editing users
|
||||
1. [#3353](https://github.com/influxdata/chronograf/pull/3353): YAxisLabels in Dashboard Graph Builder not showing until graph is redrawn
|
||||
1. [#3378](https://github.com/influxdata/chronograf/pull/3378): Ensure table graphs have a consistent ux between chrome and firefox
|
||||
1. [#3401](https://github.com/influxdata/chronograf/pull/3401): Change AutoRefresh interval to paused.
|
||||
1. [#3404](https://github.com/influxdata/chronograf/pull/3404): Get cloned cell name for notification from cloned cell generator function
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
|
@ -29,6 +32,8 @@
|
|||
1. [#3353](https://github.com/influxdata/chronograf/pull/3353): Display y-axis label on initial graph load
|
||||
1. [#3352](https://github.com/influxdata/chronograf/pull/3352): Fix not being able to change the source in the CEO display
|
||||
1. [#3357](https://github.com/influxdata/chronograf/pull/3357): Fix only the selected template variable value getting loaded
|
||||
1. [#3389](https://github.com/influxdata/chronograf/pull/3389): Fix Generic OAuth bug for GitHub Enterprise where the principal was incorrectly being checked for email being Primary and Verified
|
||||
1. [#3402](https://github.com/influxdata/chronograf/pull/3402): Fix missing icons when using basepath
|
||||
|
||||
## v1.4.4.1 [2018-04-16]
|
||||
|
||||
|
|
|
@ -165,28 +165,6 @@ type UserEmail struct {
|
|||
Verified *bool `json:"verified,omitempty"`
|
||||
}
|
||||
|
||||
// GetPrimary returns if the email is the primary email.
|
||||
// If primary is not present, all emails are considered the primary.
|
||||
func (u *UserEmail) GetPrimary() bool {
|
||||
if u == nil {
|
||||
return false
|
||||
} else if u.Primary == nil {
|
||||
return true
|
||||
}
|
||||
return *u.Primary
|
||||
}
|
||||
|
||||
// GetVerified returns if the email has been verified.
|
||||
// If verified is not present, all emails are considered verified.
|
||||
func (u *UserEmail) GetVerified() bool {
|
||||
if u == nil {
|
||||
return false
|
||||
} else if u.Verified == nil {
|
||||
return true
|
||||
}
|
||||
return *u.Verified
|
||||
}
|
||||
|
||||
// getPrimaryEmail gets the private email account for the authenticated user.
|
||||
func (g *Generic) getPrimaryEmail(client *http.Client) (string, error) {
|
||||
emailsEndpoint := g.APIURL + "/emails"
|
||||
|
@ -211,7 +189,7 @@ func (g *Generic) getPrimaryEmail(client *http.Client) (string, error) {
|
|||
|
||||
func (g *Generic) primaryEmail(emails []*UserEmail) (string, error) {
|
||||
for _, m := range emails {
|
||||
if m != nil && m.GetPrimary() && m.GetVerified() && m.Email != nil {
|
||||
if m != nil && m.Primary != nil && m.Verified != nil && m.Email != nil {
|
||||
return *m.Email, nil
|
||||
}
|
||||
}
|
||||
|
|
|
@ -155,9 +155,7 @@ func TestGenericPrincipalIDDomain(t *testing.T) {
|
|||
Primary bool `json:"primary"`
|
||||
Verified bool `json:"verified"`
|
||||
}{
|
||||
{"mcfly@example.com", false, true},
|
||||
{"martymcspelledwrong@example.com", false, false},
|
||||
{"martymcfly@pinheads.rok", true, true},
|
||||
{"martymcfly@pinheads.rok", true, false},
|
||||
}
|
||||
mockAPI := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path == "/" {
|
||||
|
|
|
@ -3678,7 +3678,9 @@
|
|||
"http",
|
||||
"hipchat",
|
||||
"opsgenie",
|
||||
"opsgenie2",
|
||||
"pagerduty",
|
||||
"pagerduty2",
|
||||
"victorops",
|
||||
"email",
|
||||
"exec",
|
||||
|
|
|
@ -5,6 +5,7 @@ import (
|
|||
"bytes"
|
||||
"io"
|
||||
"net/http"
|
||||
"regexp"
|
||||
|
||||
"github.com/influxdata/chronograf"
|
||||
)
|
||||
|
@ -83,6 +84,12 @@ func (up *URLPrefixer) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
|
|||
return
|
||||
}
|
||||
|
||||
isSVG, _ := regexp.Match(".svg$", []byte(r.URL.String()))
|
||||
if isSVG {
|
||||
up.Next.ServeHTTP(rw, r)
|
||||
return
|
||||
}
|
||||
|
||||
// chunked transfer because we're modifying the response on the fly, so we
|
||||
// won't know the final content-length
|
||||
rw.Header().Set("Connection", "Keep-Alive")
|
||||
|
|
|
@ -38,6 +38,7 @@
|
|||
},
|
||||
"devDependencies": {
|
||||
"@types/chai": "^4.1.2",
|
||||
"@types/codemirror": "^0.0.56",
|
||||
"@types/dygraphs": "^1.1.6",
|
||||
"@types/enzyme": "^3.1.9",
|
||||
"@types/jest": "^22.1.4",
|
||||
|
@ -49,7 +50,6 @@
|
|||
"@types/react-dnd-html5-backend": "^2.1.9",
|
||||
"@types/react-router": "^3.0.15",
|
||||
"@types/text-encoding": "^0.0.32",
|
||||
"@types/codemirror": "^0.0.56",
|
||||
"autoprefixer": "^6.3.1",
|
||||
"babel-core": "^6.5.1",
|
||||
"babel-eslint": "6.1.2",
|
||||
|
|
|
@ -61,6 +61,7 @@ export default class AllUsersTableRow extends PureComponent<Props> {
|
|||
<td style={{width: colOrganizations}}>
|
||||
<Tags
|
||||
tags={this.userOrganizationTags}
|
||||
confirmText="Remove user from organization?"
|
||||
onDeleteTag={onRemoveFromOrganization(user)}
|
||||
addMenuItems={this.dropdownOrganizationsItems}
|
||||
addMenuChoose={onAddToOrganization(user)}
|
||||
|
|
|
@ -19,7 +19,6 @@ import {
|
|||
notifyDashboardDeleted,
|
||||
notifyDashboardDeleteFailed,
|
||||
notifyCellAdded,
|
||||
notifyCellCloned,
|
||||
notifyCellDeleted,
|
||||
} from 'shared/copy/notifications'
|
||||
|
||||
|
@ -319,12 +318,10 @@ export const addDashboardCellAsync = (
|
|||
|
||||
export const cloneDashboardCellAsync = (dashboard, cell) => async dispatch => {
|
||||
try {
|
||||
const {data} = await addDashboardCellAJAX(
|
||||
dashboard,
|
||||
getClonedDashboardCell(dashboard, cell)
|
||||
)
|
||||
const clonedCell = getClonedDashboardCell(dashboard, cell)
|
||||
const {data} = await addDashboardCellAJAX(dashboard, clonedCell)
|
||||
dispatch(addDashboardCell(dashboard, data))
|
||||
dispatch(notify(notifyCellCloned(cell.name)))
|
||||
dispatch(notify(notifyCellAdded(clonedCell.name)))
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
dispatch(errorThrown(error))
|
||||
|
|
|
@ -89,7 +89,7 @@ export const getNewDashboardCell = (dashboard, cellType) => {
|
|||
export const getClonedDashboardCell = (dashboard, cloneCell) => {
|
||||
const {x, y} = getNextAvailablePosition(dashboard, cloneCell)
|
||||
|
||||
const name = `${cloneCell.name} (Clone)`
|
||||
const name = `${cloneCell.name} (clone)`
|
||||
|
||||
return {...cloneCell, x, y, name}
|
||||
}
|
||||
|
|
|
@ -2,25 +2,29 @@ import React from 'react'
|
|||
import PropTypes from 'prop-types'
|
||||
import classnames from 'classnames'
|
||||
import _ from 'lodash'
|
||||
import moment from 'moment'
|
||||
|
||||
import {fetchTimeSeriesAsync} from 'shared/actions/timeSeries'
|
||||
import {resultsToCSV} from 'src/shared/parsing/resultsToCSV.js'
|
||||
import {timeSeriesToTableGraph} from 'src/utils/timeSeriesTransformers'
|
||||
import {dataToCSV} from 'src/shared/parsing/dataToCSV'
|
||||
import download from 'src/external/download.js'
|
||||
import {TEMPLATES} from 'src/shared/constants'
|
||||
|
||||
const getCSV = (query, errorThrown) => async () => {
|
||||
const getDataForCSV = (query, errorThrown) => async () => {
|
||||
try {
|
||||
const {results} = await fetchTimeSeriesAsync({
|
||||
const response = await fetchTimeSeriesAsync({
|
||||
source: query.host,
|
||||
query,
|
||||
tempVars: TEMPLATES,
|
||||
})
|
||||
const {flag, name, CSVString} = resultsToCSV(results)
|
||||
if (flag === 'no_data') {
|
||||
errorThrown('no data', 'There are no data to download.')
|
||||
return
|
||||
}
|
||||
download(CSVString, `${name}.csv`, 'text/plain')
|
||||
const {data} = timeSeriesToTableGraph([{response}])
|
||||
const db = _.get(query, ['queryConfig', 'database'], '')
|
||||
const rp = _.get(query, ['queryConfig', 'retentionPolicy'], '')
|
||||
const measurement = _.get(query, ['queryConfig', 'measurement'], '')
|
||||
|
||||
const timestring = moment().format('YYYY-MM-DD-HH-mm')
|
||||
const name = `${db}.${rp}.${measurement}.${timestring}`
|
||||
download(dataToCSV(data), `${name}.csv`, 'text/plain')
|
||||
} catch (error) {
|
||||
errorThrown(error, 'Unable to download .csv file')
|
||||
console.error(error)
|
||||
|
@ -46,7 +50,7 @@ const VisHeader = ({views, view, onToggleView, query, errorThrown}) => (
|
|||
{query ? (
|
||||
<div
|
||||
className="btn btn-sm btn-default dlcsv"
|
||||
onClick={getCSV(query, errorThrown)}
|
||||
onClick={getDataForCSV(query, errorThrown)}
|
||||
>
|
||||
<span className="icon download dlcsv" />
|
||||
.csv
|
||||
|
|
|
@ -15,6 +15,7 @@ export const MINIMUM_HEIGHTS = {
|
|||
queryMaker: 350,
|
||||
visualization: 200,
|
||||
}
|
||||
|
||||
export const INITIAL_HEIGHTS = {
|
||||
queryMaker: '66.666%',
|
||||
visualization: '33.334%',
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
/* eslint-disable */
|
||||
const CodeMirror = require('codemirror')
|
||||
|
||||
CodeMirror.defineSimpleMode = function(name, states) {
|
||||
CodeMirror.defineMode(name, function(config) {
|
||||
CodeMirror.defineSimpleMode = function (name, states) {
|
||||
CodeMirror.defineMode(name, function (config) {
|
||||
return CodeMirror.simpleMode(config, states)
|
||||
})
|
||||
}
|
||||
|
||||
CodeMirror.simpleMode = function(config, states) {
|
||||
CodeMirror.simpleMode = function (config, states) {
|
||||
ensureState(states, 'start')
|
||||
const states_ = {},
|
||||
meta = states.meta || {}
|
||||
|
@ -53,10 +53,8 @@ CodeMirror.simpleMode = function(config, states) {
|
|||
s.persistentStates = {
|
||||
mode: pers.mode,
|
||||
spec: pers.spec,
|
||||
state:
|
||||
pers.state === state.localState
|
||||
? s.localState
|
||||
: CodeMirror.copyState(pers.mode, pers.state),
|
||||
state: pers.state === state.localState ?
|
||||
s.localState : CodeMirror.copyState(pers.mode, pers.state),
|
||||
next: s.persistentStates,
|
||||
}
|
||||
}
|
||||
|
@ -64,7 +62,10 @@ CodeMirror.simpleMode = function(config, states) {
|
|||
},
|
||||
token: tokenFunction(states_, config),
|
||||
innerMode(state) {
|
||||
return state.local && {mode: state.local.mode, state: state.localState}
|
||||
return state.local && {
|
||||
mode: state.local.mode,
|
||||
state: state.localState
|
||||
}
|
||||
},
|
||||
indent: indentFunction(states_, meta),
|
||||
}
|
||||
|
@ -127,7 +128,7 @@ function Rule(data, states) {
|
|||
}
|
||||
|
||||
function tokenFunction(states, config) {
|
||||
return function(stream, state) {
|
||||
return function (stream, state) {
|
||||
if (state.pending) {
|
||||
const pend = state.pending.shift()
|
||||
if (state.pending.length === 0) {
|
||||
|
@ -163,8 +164,8 @@ function tokenFunction(states, config) {
|
|||
if (matches) {
|
||||
if (rule.data.next) {
|
||||
state.state = rule.data.next
|
||||
} else if (rule.data.push) {
|
||||
;(state.stack || (state.stack = [])).push(state.state)
|
||||
} else if (rule.data.push) {;
|
||||
(state.stack || (state.stack = [])).push(state.state)
|
||||
state.state = rule.data.push
|
||||
} else if (rule.data.pop && state.stack && state.stack.length) {
|
||||
state.state = state.stack.pop()
|
||||
|
@ -187,7 +188,10 @@ function tokenFunction(states, config) {
|
|||
state.pending = []
|
||||
for (let j = 2; j < matches.length; j++) {
|
||||
if (matches[j]) {
|
||||
state.pending.push({text: matches[j], token: rule.token[j - 1]})
|
||||
state.pending.push({
|
||||
text: matches[j],
|
||||
token: rule.token[j - 1]
|
||||
})
|
||||
}
|
||||
}
|
||||
stream.backUp(
|
||||
|
@ -238,9 +242,9 @@ function enterLocalMode(config, state, spec, token) {
|
|||
}
|
||||
}
|
||||
}
|
||||
const mode = pers
|
||||
? pers.mode
|
||||
: spec.mode || CodeMirror.getMode(config, spec.spec)
|
||||
const mode = pers ?
|
||||
pers.mode :
|
||||
spec.mode || CodeMirror.getMode(config, spec.spec)
|
||||
const lState = pers ? pers.state : CodeMirror.startState(mode)
|
||||
if (spec.persistent && !pers) {
|
||||
state.persistentStates = {
|
||||
|
@ -269,7 +273,7 @@ function indexOf(val, arr) {
|
|||
}
|
||||
|
||||
function indentFunction(states, meta) {
|
||||
return function(state, textAfter, line) {
|
||||
return function (state, textAfter, line) {
|
||||
if (state.local && state.local.mode.indent) {
|
||||
return state.local.mode.indent(state.localState, textAfter, line)
|
||||
}
|
||||
|
@ -309,8 +313,14 @@ CodeMirror.defineSimpleMode('tickscript', {
|
|||
// The start state contains the rules that are intially used
|
||||
start: [
|
||||
// The regex matches the token, the token property contains the type
|
||||
{regex: /"(?:[^\\]|\\.)*?(?:"|$)/, token: 'string.double'},
|
||||
{regex: /'(?:[^\\]|\\.)*?(?:'|$)/, token: 'string.single'},
|
||||
{
|
||||
regex: /"(?:[^\\]|\\.)*?(?:"|$)/,
|
||||
token: 'string.double'
|
||||
},
|
||||
{
|
||||
regex: /'(?:[^\\]|\\.)*?(?:'|$)/,
|
||||
token: 'string.single'
|
||||
},
|
||||
{
|
||||
regex: /(function)(\s+)([a-z$][\w$]*)/,
|
||||
token: ['keyword', null, 'variable-2'],
|
||||
|
@ -321,22 +331,47 @@ CodeMirror.defineSimpleMode('tickscript', {
|
|||
regex: /(?:var|return|if|for|while|else|do|this|stream|batch|influxql|lambda)/,
|
||||
token: 'keyword',
|
||||
},
|
||||
{regex: /true|false|null|undefined|TRUE|FALSE/, token: 'atom'},
|
||||
{
|
||||
regex: /true|false|null|undefined|TRUE|FALSE/,
|
||||
token: 'atom'
|
||||
},
|
||||
{
|
||||
regex: /0x[a-f\d]+|[-+]?(?:\.\d+|\d+\.?\d*)(?:e[-+]?\d+)?/i,
|
||||
token: 'number',
|
||||
},
|
||||
{regex: /\/\/.*/, token: 'comment'},
|
||||
{regex: /\/(?:[^\\]|\\.)*?\//, token: 'variable-3'},
|
||||
{
|
||||
regex: /\/\/.*/,
|
||||
token: 'comment'
|
||||
},
|
||||
{
|
||||
regex: /\/(?:[^\\]|\\.)*?\//,
|
||||
token: 'variable-3'
|
||||
},
|
||||
// A next property will cause the mode to move to a different state
|
||||
{regex: /\/\*/, token: 'comment', next: 'comment'},
|
||||
{regex: /[-+\/*=<>!]+/, token: 'operator'},
|
||||
{regex: /[a-z$][\w$]*/, token: 'variable'},
|
||||
{
|
||||
regex: /\/\*/,
|
||||
token: 'comment',
|
||||
next: 'comment'
|
||||
},
|
||||
{
|
||||
regex: /[-+\/*=<>!]+/,
|
||||
token: 'operator'
|
||||
},
|
||||
{
|
||||
regex: /[a-z$][\w$]*/,
|
||||
token: 'variable'
|
||||
},
|
||||
],
|
||||
// The multi-line comment state.
|
||||
comment: [
|
||||
{regex: /.*?\*\//, token: 'comment', next: 'start'},
|
||||
{regex: /.*/, token: 'comment'},
|
||||
comment: [{
|
||||
regex: /.*?\*\//,
|
||||
token: 'comment',
|
||||
next: 'start'
|
||||
},
|
||||
{
|
||||
regex: /.*/,
|
||||
token: 'comment'
|
||||
},
|
||||
],
|
||||
// The meta property contains global information about the mode. It
|
||||
// can contain properties like lineComment, which are supported by
|
||||
|
@ -347,3 +382,536 @@ CodeMirror.defineSimpleMode('tickscript', {
|
|||
lineComment: '//',
|
||||
},
|
||||
})
|
||||
|
||||
// CodeMirror Hints
|
||||
|
||||
var HINT_ELEMENT_CLASS = "CodeMirror-hint";
|
||||
var ACTIVE_HINT_ELEMENT_CLASS = "CodeMirror-hint-active";
|
||||
|
||||
// This is the old interface, kept around for now to stay backwards-compatible.
|
||||
CodeMirror.showHint = function (cm, getHints, options) {
|
||||
if (!getHints) return cm.showHint(options);
|
||||
if (options && options.async) getHints.async = true;
|
||||
var newOpts = {
|
||||
hint: getHints
|
||||
};
|
||||
if (options)
|
||||
for (var prop in options) newOpts[prop] = options[prop];
|
||||
return cm.showHint(newOpts);
|
||||
};
|
||||
|
||||
CodeMirror.defineExtension("showHint", function (options) {
|
||||
options = parseOptions(this, this.getCursor("start"), options);
|
||||
var selections = this.listSelections()
|
||||
if (selections.length > 1) return;
|
||||
// By default, don't allow completion when something is selected.
|
||||
// A hint function can have a `supportsSelection` property to
|
||||
// indicate that it can handle selections.
|
||||
if (this.somethingSelected()) {
|
||||
if (!options.hint.supportsSelection) return;
|
||||
// Don't try with cross-line selections
|
||||
for (var i = 0; i < selections.length; i++)
|
||||
if (selections[i].head.line != selections[i].anchor.line) return;
|
||||
}
|
||||
|
||||
if (this.state.completionActive) this.state.completionActive.close();
|
||||
var completion = this.state.completionActive = new Completion(this, options);
|
||||
if (!completion.options.hint) return;
|
||||
|
||||
CodeMirror.signal(this, "startCompletion", this);
|
||||
completion.update(true);
|
||||
});
|
||||
|
||||
function Completion(cm, options) {
|
||||
this.cm = cm;
|
||||
this.options = options;
|
||||
this.widget = null;
|
||||
this.debounce = 0;
|
||||
this.tick = 0;
|
||||
this.startPos = this.cm.getCursor("start");
|
||||
this.startLen = this.cm.getLine(this.startPos.line).length - this.cm.getSelection().length;
|
||||
|
||||
var self = this;
|
||||
cm.on("cursorActivity", this.activityFunc = function () {
|
||||
self.cursorActivity();
|
||||
});
|
||||
}
|
||||
|
||||
var requestAnimationFrame = window.requestAnimationFrame || function (fn) {
|
||||
return setTimeout(fn, 1000 / 60);
|
||||
};
|
||||
var cancelAnimationFrame = window.cancelAnimationFrame || clearTimeout;
|
||||
|
||||
Completion.prototype = {
|
||||
close: function () {
|
||||
if (!this.active()) return;
|
||||
this.cm.state.completionActive = null;
|
||||
this.tick = null;
|
||||
this.cm.off("cursorActivity", this.activityFunc);
|
||||
|
||||
if (this.widget && this.data) CodeMirror.signal(this.data, "close");
|
||||
if (this.widget) this.widget.close();
|
||||
CodeMirror.signal(this.cm, "endCompletion", this.cm);
|
||||
},
|
||||
|
||||
active: function () {
|
||||
return this.cm.state.completionActive == this;
|
||||
},
|
||||
|
||||
pick: function (data, i) {
|
||||
var completion = data.list[i];
|
||||
if (completion.hint) completion.hint(this.cm, data, completion);
|
||||
else this.cm.replaceRange(getText(completion), completion.from || data.from,
|
||||
completion.to || data.to, "complete");
|
||||
CodeMirror.signal(data, "pick", completion);
|
||||
this.close();
|
||||
},
|
||||
|
||||
cursorActivity: function () {
|
||||
if (this.debounce) {
|
||||
cancelAnimationFrame(this.debounce);
|
||||
this.debounce = 0;
|
||||
}
|
||||
|
||||
var pos = this.cm.getCursor(),
|
||||
line = this.cm.getLine(pos.line);
|
||||
if (pos.line != this.startPos.line || line.length - pos.ch != this.startLen - this.startPos.ch ||
|
||||
pos.ch < this.startPos.ch || this.cm.somethingSelected() ||
|
||||
(pos.ch && this.options.closeCharacters.test(line.charAt(pos.ch - 1)))) {
|
||||
this.close();
|
||||
} else {
|
||||
var self = this;
|
||||
this.debounce = requestAnimationFrame(function () {
|
||||
self.update();
|
||||
});
|
||||
if (this.widget) this.widget.disable();
|
||||
}
|
||||
},
|
||||
|
||||
update: function (first) {
|
||||
if (this.tick == null) return
|
||||
var self = this,
|
||||
myTick = ++this.tick
|
||||
fetchHints(this.options.hint, this.cm, this.options, function (data) {
|
||||
if (self.tick == myTick) self.finishUpdate(data, first)
|
||||
})
|
||||
},
|
||||
|
||||
finishUpdate: function (data, first) {
|
||||
if (this.data) CodeMirror.signal(this.data, "update");
|
||||
|
||||
var picked = (this.widget && this.widget.picked) || (first && this.options.completeSingle);
|
||||
if (this.widget) this.widget.close();
|
||||
|
||||
this.data = data;
|
||||
|
||||
if (data && data.list.length) {
|
||||
if (picked && data.list.length == 1) {
|
||||
this.pick(data, 0);
|
||||
} else {
|
||||
this.widget = new Widget(this, data);
|
||||
CodeMirror.signal(data, "shown");
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
function parseOptions(cm, pos, options) {
|
||||
var editor = cm.options.hintOptions;
|
||||
var out = {};
|
||||
for (var prop in defaultOptions) out[prop] = defaultOptions[prop];
|
||||
if (editor)
|
||||
for (var prop in editor)
|
||||
if (editor[prop] !== undefined) out[prop] = editor[prop];
|
||||
if (options)
|
||||
for (var prop in options)
|
||||
if (options[prop] !== undefined) out[prop] = options[prop];
|
||||
if (out.hint.resolve) out.hint = out.hint.resolve(cm, pos)
|
||||
return out;
|
||||
}
|
||||
|
||||
function getText(completion) {
|
||||
if (typeof completion == "string") return completion;
|
||||
else return completion.text;
|
||||
}
|
||||
|
||||
function buildKeyMap(completion, handle) {
|
||||
var baseMap = {
|
||||
Up: function () {
|
||||
handle.moveFocus(-1);
|
||||
},
|
||||
Down: function () {
|
||||
handle.moveFocus(1);
|
||||
},
|
||||
PageUp: function () {
|
||||
handle.moveFocus(-handle.menuSize() + 1, true);
|
||||
},
|
||||
PageDown: function () {
|
||||
handle.moveFocus(handle.menuSize() - 1, true);
|
||||
},
|
||||
Home: function () {
|
||||
handle.setFocus(0);
|
||||
},
|
||||
End: function () {
|
||||
handle.setFocus(handle.length - 1);
|
||||
},
|
||||
Enter: handle.pick,
|
||||
Tab: handle.pick,
|
||||
Esc: handle.close
|
||||
};
|
||||
var custom = completion.options.customKeys;
|
||||
var ourMap = custom ? {} : baseMap;
|
||||
|
||||
function addBinding(key, val) {
|
||||
var bound;
|
||||
if (typeof val != "string")
|
||||
bound = function (cm) {
|
||||
return val(cm, handle);
|
||||
};
|
||||
// This mechanism is deprecated
|
||||
else if (baseMap.hasOwnProperty(val))
|
||||
bound = baseMap[val];
|
||||
else
|
||||
bound = val;
|
||||
ourMap[key] = bound;
|
||||
}
|
||||
if (custom)
|
||||
for (var key in custom)
|
||||
if (custom.hasOwnProperty(key))
|
||||
addBinding(key, custom[key]);
|
||||
var extra = completion.options.extraKeys;
|
||||
if (extra)
|
||||
for (var key in extra)
|
||||
if (extra.hasOwnProperty(key))
|
||||
addBinding(key, extra[key]);
|
||||
return ourMap;
|
||||
}
|
||||
|
||||
function getHintElement(hintsElement, el) {
|
||||
while (el && el != hintsElement) {
|
||||
if (el.nodeName.toUpperCase() === "LI" && el.parentNode == hintsElement) return el;
|
||||
el = el.parentNode;
|
||||
}
|
||||
}
|
||||
|
||||
function Widget(completion, data) {
|
||||
this.completion = completion;
|
||||
this.data = data;
|
||||
this.picked = false;
|
||||
var widget = this,
|
||||
cm = completion.cm;
|
||||
|
||||
var hints = this.hints = document.createElement("ul");
|
||||
hints.className = "CodeMirror-hints";
|
||||
this.selectedHint = data.selectedHint || 0;
|
||||
|
||||
var completions = data.list;
|
||||
for (var i = 0; i < completions.length; ++i) {
|
||||
var elt = hints.appendChild(document.createElement("li")),
|
||||
cur = completions[i];
|
||||
var className = HINT_ELEMENT_CLASS + (i != this.selectedHint ? "" : " " + ACTIVE_HINT_ELEMENT_CLASS);
|
||||
if (cur.className != null) className = cur.className + " " + className;
|
||||
elt.className = className;
|
||||
if (cur.render) cur.render(elt, data, cur);
|
||||
else elt.appendChild(document.createTextNode(cur.displayText || getText(cur)));
|
||||
elt.hintId = i;
|
||||
}
|
||||
|
||||
var pos = cm.cursorCoords(completion.options.alignWithWord ? data.from : null);
|
||||
var left = pos.left,
|
||||
top = pos.bottom,
|
||||
below = true;
|
||||
hints.style.left = left + "px";
|
||||
hints.style.top = top + "px";
|
||||
// If we're at the edge of the screen, then we want the menu to appear on the left of the cursor.
|
||||
var winW = window.innerWidth || Math.max(document.body.offsetWidth, document.documentElement.offsetWidth);
|
||||
var winH = window.innerHeight || Math.max(document.body.offsetHeight, document.documentElement.offsetHeight);
|
||||
(completion.options.container || document.body).appendChild(hints);
|
||||
var box = hints.getBoundingClientRect(),
|
||||
overlapY = box.bottom - winH;
|
||||
var scrolls = hints.scrollHeight > hints.clientHeight + 1
|
||||
var startScroll = cm.getScrollInfo();
|
||||
|
||||
if (overlapY > 0) {
|
||||
var height = box.bottom - box.top,
|
||||
curTop = pos.top - (pos.bottom - box.top);
|
||||
if (curTop - height > 0) { // Fits above cursor
|
||||
hints.style.top = (top = pos.top - height) + "px";
|
||||
below = false;
|
||||
} else if (height > winH) {
|
||||
hints.style.height = (winH - 5) + "px";
|
||||
hints.style.top = (top = pos.bottom - box.top) + "px";
|
||||
var cursor = cm.getCursor();
|
||||
if (data.from.ch != cursor.ch) {
|
||||
pos = cm.cursorCoords(cursor);
|
||||
hints.style.left = (left = pos.left) + "px";
|
||||
box = hints.getBoundingClientRect();
|
||||
}
|
||||
}
|
||||
}
|
||||
var overlapX = box.right - winW;
|
||||
if (overlapX > 0) {
|
||||
if (box.right - box.left > winW) {
|
||||
hints.style.width = (winW - 5) + "px";
|
||||
overlapX -= (box.right - box.left) - winW;
|
||||
}
|
||||
hints.style.left = (left = pos.left - overlapX) + "px";
|
||||
}
|
||||
if (scrolls)
|
||||
for (var node = hints.firstChild; node; node = node.nextSibling)
|
||||
node.style.paddingRight = cm.display.nativeBarWidth + "px"
|
||||
|
||||
cm.addKeyMap(this.keyMap = buildKeyMap(completion, {
|
||||
moveFocus: function (n, avoidWrap) {
|
||||
widget.changeActive(widget.selectedHint + n, avoidWrap);
|
||||
},
|
||||
setFocus: function (n) {
|
||||
widget.changeActive(n);
|
||||
},
|
||||
menuSize: function () {
|
||||
return widget.screenAmount();
|
||||
},
|
||||
length: completions.length,
|
||||
close: function () {
|
||||
completion.close();
|
||||
},
|
||||
pick: function () {
|
||||
widget.pick();
|
||||
},
|
||||
data: data
|
||||
}));
|
||||
|
||||
if (completion.options.closeOnUnfocus) {
|
||||
var closingOnBlur;
|
||||
cm.on("blur", this.onBlur = function () {
|
||||
closingOnBlur = setTimeout(function () {
|
||||
completion.close();
|
||||
}, 100);
|
||||
});
|
||||
cm.on("focus", this.onFocus = function () {
|
||||
clearTimeout(closingOnBlur);
|
||||
});
|
||||
}
|
||||
|
||||
cm.on("scroll", this.onScroll = function () {
|
||||
var curScroll = cm.getScrollInfo(),
|
||||
editor = cm.getWrapperElement().getBoundingClientRect();
|
||||
var newTop = top + startScroll.top - curScroll.top;
|
||||
var point = newTop - (window.pageYOffset || (document.documentElement || document.body).scrollTop);
|
||||
if (!below) point += hints.offsetHeight;
|
||||
if (point <= editor.top || point >= editor.bottom) return completion.close();
|
||||
hints.style.top = newTop + "px";
|
||||
hints.style.left = (left + startScroll.left - curScroll.left) + "px";
|
||||
});
|
||||
|
||||
CodeMirror.on(hints, "dblclick", function (e) {
|
||||
var t = getHintElement(hints, e.target || e.srcElement);
|
||||
if (t && t.hintId != null) {
|
||||
widget.changeActive(t.hintId);
|
||||
widget.pick();
|
||||
}
|
||||
});
|
||||
|
||||
CodeMirror.on(hints, "click", function (e) {
|
||||
var t = getHintElement(hints, e.target || e.srcElement);
|
||||
if (t && t.hintId != null) {
|
||||
widget.changeActive(t.hintId);
|
||||
if (completion.options.completeOnSingleClick) widget.pick();
|
||||
}
|
||||
});
|
||||
|
||||
CodeMirror.on(hints, "mousedown", function () {
|
||||
setTimeout(function () {
|
||||
cm.focus();
|
||||
}, 20);
|
||||
});
|
||||
|
||||
CodeMirror.signal(data, "select", completions[this.selectedHint], hints.childNodes[this.selectedHint]);
|
||||
return true;
|
||||
}
|
||||
|
||||
Widget.prototype = {
|
||||
close: function () {
|
||||
if (this.completion.widget != this) return;
|
||||
this.completion.widget = null;
|
||||
this.hints.parentNode.removeChild(this.hints);
|
||||
this.completion.cm.removeKeyMap(this.keyMap);
|
||||
|
||||
var cm = this.completion.cm;
|
||||
if (this.completion.options.closeOnUnfocus) {
|
||||
cm.off("blur", this.onBlur);
|
||||
cm.off("focus", this.onFocus);
|
||||
}
|
||||
cm.off("scroll", this.onScroll);
|
||||
},
|
||||
|
||||
disable: function () {
|
||||
this.completion.cm.removeKeyMap(this.keyMap);
|
||||
var widget = this;
|
||||
this.keyMap = {
|
||||
Enter: function () {
|
||||
widget.picked = true;
|
||||
}
|
||||
};
|
||||
this.completion.cm.addKeyMap(this.keyMap);
|
||||
},
|
||||
|
||||
pick: function () {
|
||||
this.completion.pick(this.data, this.selectedHint);
|
||||
},
|
||||
|
||||
changeActive: function (i, avoidWrap) {
|
||||
if (i >= this.data.list.length)
|
||||
i = avoidWrap ? this.data.list.length - 1 : 0;
|
||||
else if (i < 0)
|
||||
i = avoidWrap ? 0 : this.data.list.length - 1;
|
||||
if (this.selectedHint == i) return;
|
||||
var node = this.hints.childNodes[this.selectedHint];
|
||||
node.className = node.className.replace(" " + ACTIVE_HINT_ELEMENT_CLASS, "");
|
||||
node = this.hints.childNodes[this.selectedHint = i];
|
||||
node.className += " " + ACTIVE_HINT_ELEMENT_CLASS;
|
||||
if (node.offsetTop < this.hints.scrollTop)
|
||||
this.hints.scrollTop = node.offsetTop - 3;
|
||||
else if (node.offsetTop + node.offsetHeight > this.hints.scrollTop + this.hints.clientHeight)
|
||||
this.hints.scrollTop = node.offsetTop + node.offsetHeight - this.hints.clientHeight + 3;
|
||||
CodeMirror.signal(this.data, "select", this.data.list[this.selectedHint], node);
|
||||
},
|
||||
|
||||
screenAmount: function () {
|
||||
return Math.floor(this.hints.clientHeight / this.hints.firstChild.offsetHeight) || 1;
|
||||
}
|
||||
};
|
||||
|
||||
function applicableHelpers(cm, helpers) {
|
||||
if (!cm.somethingSelected()) return helpers
|
||||
var result = []
|
||||
for (var i = 0; i < helpers.length; i++)
|
||||
if (helpers[i].supportsSelection) result.push(helpers[i])
|
||||
return result
|
||||
}
|
||||
|
||||
function fetchHints(hint, cm, options, callback) {
|
||||
if (hint.async) {
|
||||
hint(cm, callback, options)
|
||||
} else {
|
||||
var result = hint(cm, options)
|
||||
if (result && result.then) result.then(callback)
|
||||
else callback(result)
|
||||
}
|
||||
}
|
||||
|
||||
function resolveAutoHints(cm, pos) {
|
||||
var helpers = cm.getHelpers(pos, "hint"),
|
||||
words
|
||||
if (helpers.length) {
|
||||
var resolved = function (cm, callback, options) {
|
||||
var app = applicableHelpers(cm, helpers);
|
||||
|
||||
function run(i) {
|
||||
if (i == app.length) return callback(null)
|
||||
fetchHints(app[i], cm, options, function (result) {
|
||||
if (result && result.list.length > 0) callback(result)
|
||||
else run(i + 1)
|
||||
})
|
||||
}
|
||||
run(0)
|
||||
}
|
||||
resolved.async = true
|
||||
resolved.supportsSelection = true
|
||||
return resolved
|
||||
} else if (words = cm.getHelper(cm.getCursor(), "hintWords")) {
|
||||
return function (cm) {
|
||||
return CodeMirror.hint.fromList(cm, {
|
||||
words: words
|
||||
})
|
||||
}
|
||||
} else if (CodeMirror.hint.anyword) {
|
||||
return function (cm, options) {
|
||||
return CodeMirror.hint.anyword(cm, options)
|
||||
}
|
||||
} else {
|
||||
return function () {}
|
||||
}
|
||||
}
|
||||
|
||||
CodeMirror.registerHelper("hint", "auto", {
|
||||
resolve: resolveAutoHints
|
||||
});
|
||||
|
||||
CodeMirror.registerHelper("hint", "fromList", function (cm, options) {
|
||||
var cur = cm.getCursor(),
|
||||
token = cm.getTokenAt(cur)
|
||||
var term, from = CodeMirror.Pos(cur.line, token.start),
|
||||
to = cur
|
||||
if (token.start < cur.ch && /\w/.test(token.string.charAt(cur.ch - token.start - 1))) {
|
||||
term = token.string.substr(0, cur.ch - token.start)
|
||||
} else {
|
||||
term = ""
|
||||
from = cur
|
||||
}
|
||||
var found = [];
|
||||
for (var i = 0; i < options.words.length; i++) {
|
||||
var word = options.words[i];
|
||||
if (word.slice(0, term.length) == term)
|
||||
found.push(word);
|
||||
}
|
||||
|
||||
if (found.length) return {
|
||||
list: found,
|
||||
from: from,
|
||||
to: to
|
||||
};
|
||||
});
|
||||
|
||||
CodeMirror.commands.autocomplete = CodeMirror.showHint;
|
||||
|
||||
var defaultOptions = {
|
||||
hint: CodeMirror.hint.auto,
|
||||
completeSingle: true,
|
||||
alignWithWord: true,
|
||||
closeCharacters: /[\s()\[\]{};:>,]/,
|
||||
closeOnUnfocus: true,
|
||||
completeOnSingleClick: true,
|
||||
container: null,
|
||||
customKeys: null,
|
||||
extraKeys: null
|
||||
};
|
||||
|
||||
CodeMirror.defineOption("hintOptions", null);
|
||||
var WORD = /[\w$]+/,
|
||||
RANGE = 500;
|
||||
|
||||
CodeMirror.registerHelper("hint", "anyword", function (editor, options) {
|
||||
var word = options && options.word || WORD;
|
||||
var range = options && options.range || RANGE;
|
||||
var cur = editor.getCursor(),
|
||||
curLine = editor.getLine(cur.line);
|
||||
var end = cur.ch,
|
||||
start = end;
|
||||
while (start && word.test(curLine.charAt(start - 1))) --start;
|
||||
var curWord = start != end && curLine.slice(start, end);
|
||||
|
||||
var list = options && options.list || [],
|
||||
seen = {};
|
||||
var re = new RegExp(word.source, "g");
|
||||
for (var dir = -1; dir <= 1; dir += 2) {
|
||||
var line = cur.line,
|
||||
endLine = Math.min(Math.max(line + dir * range, editor.firstLine()), editor.lastLine()) + dir;
|
||||
for (; line != endLine; line += dir) {
|
||||
var text = editor.getLine(line),
|
||||
m;
|
||||
while (m = re.exec(text)) {
|
||||
if (line == cur.line && m[0] === curWord) continue;
|
||||
if ((!curWord || m[0].lastIndexOf(curWord, 0) == 0) && !Object.prototype.hasOwnProperty.call(seen, m[0])) {
|
||||
seen[m[0]] = true;
|
||||
list.push(m[0]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return {
|
||||
list: list,
|
||||
from: CodeMirror.Pos(cur.line, start),
|
||||
to: CodeMirror.Pos(cur.line, end)
|
||||
};
|
||||
});
|
|
@ -46,3 +46,25 @@ export const getDatabases = async () => {
|
|||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
export const getTags = async () => {
|
||||
try {
|
||||
const response = {data: {tags: ['tk1', 'tk2', 'tk3']}}
|
||||
const {data} = await Promise.resolve(response)
|
||||
return data.tags
|
||||
} catch (error) {
|
||||
console.error('Could not get tags', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
export const getTagValues = async () => {
|
||||
try {
|
||||
const response = {data: {values: ['tv1', 'tv2', 'tv3']}}
|
||||
const {data} = await Promise.resolve(response)
|
||||
return data.values
|
||||
} catch (error) {
|
||||
console.error('Could not get tag values', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,6 +2,8 @@ import React, {PureComponent} from 'react'
|
|||
import _ from 'lodash'
|
||||
|
||||
import ExpressionNode from 'src/ifql/components/ExpressionNode'
|
||||
import VariableName from 'src/ifql/components/VariableName'
|
||||
import FuncSelector from 'src/ifql/components/FuncSelector'
|
||||
|
||||
import {FlatBody, Suggestion} from 'src/types/ifql'
|
||||
|
||||
|
@ -21,8 +23,8 @@ class BodyBuilder extends PureComponent<Props> {
|
|||
return b.declarations.map(d => {
|
||||
if (d.funcs) {
|
||||
return (
|
||||
<div key={b.id}>
|
||||
<div className="func-node--name">{d.name} =</div>
|
||||
<div className="declaration" key={b.id}>
|
||||
<VariableName name={d.name} />
|
||||
<ExpressionNode
|
||||
key={b.id}
|
||||
bodyID={b.id}
|
||||
|
@ -35,24 +37,51 @@ class BodyBuilder extends PureComponent<Props> {
|
|||
}
|
||||
|
||||
return (
|
||||
<div className="func-node--name" key={b.id}>
|
||||
{b.source}
|
||||
<div className="declaration" key={b.id}>
|
||||
<VariableName name={b.source} />
|
||||
</div>
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="declaration" key={b.id}>
|
||||
<VariableName />
|
||||
<ExpressionNode
|
||||
key={b.id}
|
||||
bodyID={b.id}
|
||||
funcs={b.funcs}
|
||||
funcNames={this.funcNames}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
|
||||
return _.flatten(bodybuilder)
|
||||
return (
|
||||
<div className="body-builder">
|
||||
{_.flatten(bodybuilder)}
|
||||
<div className="declaration">
|
||||
<FuncSelector
|
||||
bodyID="fake-body-id"
|
||||
declarationID="fake-declaration-id"
|
||||
onAddNode={this.createNewDeclaration}
|
||||
funcs={this.newDeclarationFuncs}
|
||||
connectorVisible={false}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
private get newDeclarationFuncs(): string[] {
|
||||
// 'JOIN' only available if there are at least 2 named declarations
|
||||
return ['from', 'join', 'variable']
|
||||
}
|
||||
|
||||
private createNewDeclaration = (bodyID, name, declarationID) => {
|
||||
// Returning a string here so linter stops yelling
|
||||
// TODO: write a real function
|
||||
|
||||
return `${bodyID} / ${name} / ${declarationID}`
|
||||
}
|
||||
|
||||
private get funcNames() {
|
||||
|
|
|
@ -0,0 +1,61 @@
|
|||
import React, {PureComponent} from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
import DatabaseListItem from 'src/ifql/components/DatabaseListItem'
|
||||
|
||||
import {showDatabases} from 'src/shared/apis/metaQuery'
|
||||
import showDatabasesParser from 'src/shared/parsing/showDatabases'
|
||||
|
||||
import {ErrorHandling} from 'src/shared/decorators/errors'
|
||||
|
||||
interface DatabaseListState {
|
||||
databases: string[]
|
||||
measurement: string
|
||||
db: string
|
||||
}
|
||||
|
||||
const {shape} = PropTypes
|
||||
|
||||
@ErrorHandling
|
||||
class DatabaseList extends PureComponent<{}, DatabaseListState> {
|
||||
public static contextTypes = {
|
||||
source: shape({
|
||||
links: shape({}).isRequired,
|
||||
}).isRequired,
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
super(props)
|
||||
this.state = {
|
||||
databases: [],
|
||||
measurement: '',
|
||||
db: '',
|
||||
}
|
||||
}
|
||||
|
||||
public componentDidMount() {
|
||||
this.getDatabases()
|
||||
}
|
||||
|
||||
public async getDatabases() {
|
||||
const {source} = this.context
|
||||
|
||||
try {
|
||||
const {data} = await showDatabases(source.links.proxy)
|
||||
const {databases} = showDatabasesParser(data)
|
||||
const sorted = databases.sort()
|
||||
|
||||
this.setState({databases: sorted})
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
}
|
||||
}
|
||||
|
||||
public render() {
|
||||
return this.state.databases.map(db => {
|
||||
return <DatabaseListItem db={db} key={db} />
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export default DatabaseList
|
|
@ -0,0 +1,49 @@
|
|||
import React, {PureComponent} from 'react'
|
||||
|
||||
import classnames from 'classnames'
|
||||
|
||||
import TagList from 'src/ifql/components/TagList'
|
||||
|
||||
interface Props {
|
||||
db: string
|
||||
}
|
||||
|
||||
interface State {
|
||||
isOpen: boolean
|
||||
}
|
||||
|
||||
class DatabaseListItem extends PureComponent<Props, State> {
|
||||
constructor(props) {
|
||||
super(props)
|
||||
this.state = {
|
||||
isOpen: false,
|
||||
}
|
||||
}
|
||||
|
||||
public render() {
|
||||
const {db} = this.props
|
||||
|
||||
return (
|
||||
<div className={this.className} onClick={this.handleChooseDatabase}>
|
||||
<div className="ifql-schema-item">
|
||||
<div className="ifql-schema-item-toggle" />
|
||||
{db}
|
||||
<span className="ifql-schema-type">Bucket</span>
|
||||
</div>
|
||||
{this.state.isOpen && <TagList db={db} />}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
private get className(): string {
|
||||
return classnames('ifql-schema-tree', {
|
||||
expanded: this.state.isOpen,
|
||||
})
|
||||
}
|
||||
|
||||
private handleChooseDatabase = () => {
|
||||
this.setState({isOpen: !this.state.isOpen})
|
||||
}
|
||||
}
|
||||
|
||||
export default DatabaseListItem
|
|
@ -21,7 +21,7 @@ class ExpressionNode extends PureComponent<Props> {
|
|||
<IFQLContext.Consumer>
|
||||
{({onDeleteFuncNode, onAddNode, onChangeArg, onGenerateScript}) => {
|
||||
return (
|
||||
<div className="func-nodes-container">
|
||||
<>
|
||||
{funcs.map(func => (
|
||||
<FuncNode
|
||||
key={func.id}
|
||||
|
@ -39,7 +39,7 @@ class ExpressionNode extends PureComponent<Props> {
|
|||
onAddNode={onAddNode}
|
||||
declarationID={declarationID}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}}
|
||||
</IFQLContext.Consumer>
|
||||
|
|
|
@ -41,12 +41,13 @@ class From extends PureComponent<Props, State> {
|
|||
|
||||
public render() {
|
||||
const {value, argKey} = this.props
|
||||
|
||||
return (
|
||||
<div className="from">
|
||||
<label className="from--label">{argKey}: </label>
|
||||
<div className="func-arg">
|
||||
<label className="func-arg--label">{argKey}</label>
|
||||
<Dropdown
|
||||
selected={value}
|
||||
className="from--dropdown dropdown-160"
|
||||
className="from--dropdown dropdown-160 func-arg--value"
|
||||
menuClass="dropdown-astronaut"
|
||||
buttonColor="btn-default"
|
||||
items={this.items}
|
||||
|
|
|
@ -88,7 +88,8 @@ class FuncArg extends PureComponent<Props> {
|
|||
// TODO: make separate function component
|
||||
return (
|
||||
<div className="func-arg">
|
||||
{argKey} : {value}
|
||||
<label className="func-arg--label">{argKey}</label>
|
||||
<div className="func-arg--value">{value}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
@ -96,14 +97,16 @@ class FuncArg extends PureComponent<Props> {
|
|||
// TODO: handle nil type
|
||||
return (
|
||||
<div className="func-arg">
|
||||
{argKey} : {value}
|
||||
<label className="func-arg--label">{argKey}</label>
|
||||
<div className="func-arg--value">{value}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
default: {
|
||||
return (
|
||||
<div className="func-arg">
|
||||
{argKey} : {value}
|
||||
<label className="func-arg--label">{argKey}</label>
|
||||
<div className="func-arg--value">{value}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -16,10 +16,12 @@ interface Props {
|
|||
class FuncArgBool extends PureComponent<Props> {
|
||||
public render() {
|
||||
return (
|
||||
<div>
|
||||
{this.props.argKey}:
|
||||
<div className="func-arg">
|
||||
<label className="func-arg--label">{this.props.argKey}</label>
|
||||
<div className="func-arg--value">
|
||||
<SlideToggle active={this.props.value} onToggle={this.handleToggle} />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -17,9 +17,13 @@ interface Props {
|
|||
class FuncArgInput extends PureComponent<Props> {
|
||||
public render() {
|
||||
const {argKey, value, type} = this.props
|
||||
|
||||
return (
|
||||
<div>
|
||||
<label htmlFor={argKey}>{argKey}: </label>
|
||||
<div className="func-arg">
|
||||
<label className="func-arg--label" htmlFor={argKey}>
|
||||
{argKey}
|
||||
</label>
|
||||
<div className="func-arg--value">
|
||||
<input
|
||||
name={argKey}
|
||||
value={value}
|
||||
|
@ -27,11 +31,12 @@ class FuncArgInput extends PureComponent<Props> {
|
|||
onChange={this.handleChange}
|
||||
onKeyDown={this.handleKeyDown}
|
||||
type="text"
|
||||
className="form-control input-xs"
|
||||
className="form-control input-sm"
|
||||
spellCheck={false}
|
||||
autoComplete="off"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -10,6 +10,7 @@ interface Props {
|
|||
onChangeArg: OnChangeArg
|
||||
declarationID: string
|
||||
onGenerateScript: () => void
|
||||
onDeleteFunc: () => void
|
||||
}
|
||||
|
||||
@ErrorHandling
|
||||
|
@ -19,12 +20,13 @@ export default class FuncArgs extends PureComponent<Props> {
|
|||
func,
|
||||
bodyID,
|
||||
onChangeArg,
|
||||
onDeleteFunc,
|
||||
declarationID,
|
||||
onGenerateScript,
|
||||
} = this.props
|
||||
|
||||
return (
|
||||
<div className="func-args">
|
||||
<div className="func-node--tooltip">
|
||||
{func.args.map(({key, value, type}) => {
|
||||
return (
|
||||
<FuncArg
|
||||
|
@ -41,6 +43,12 @@ export default class FuncArgs extends PureComponent<Props> {
|
|||
/>
|
||||
)
|
||||
})}
|
||||
<div
|
||||
className="btn btn-sm btn-danger func-node--delete"
|
||||
onClick={onDeleteFunc}
|
||||
>
|
||||
Delete
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,66 @@
|
|||
import React, {PureComponent} from 'react'
|
||||
import {Arg} from 'src/types/ifql'
|
||||
import uuid from 'uuid'
|
||||
|
||||
interface Props {
|
||||
args: Arg[]
|
||||
}
|
||||
|
||||
export default class FuncArgsPreview extends PureComponent<Props> {
|
||||
public render() {
|
||||
return <div className="func-node--preview">{this.summarizeArguments}</div>
|
||||
}
|
||||
|
||||
private get summarizeArguments(): JSX.Element | JSX.Element[] {
|
||||
const {args} = this.props
|
||||
|
||||
if (!args) {
|
||||
return
|
||||
}
|
||||
|
||||
return this.colorizedArguments
|
||||
}
|
||||
|
||||
private get colorizedArguments(): JSX.Element | JSX.Element[] {
|
||||
const {args} = this.props
|
||||
|
||||
return args.map((arg, i): JSX.Element => {
|
||||
if (!arg.value) {
|
||||
return
|
||||
}
|
||||
|
||||
const separator = i === 0 ? null : ', '
|
||||
|
||||
return (
|
||||
<React.Fragment key={uuid.v4()}>
|
||||
{separator}
|
||||
{arg.key}: {this.colorArgType(`${arg.value}`, arg.type)}
|
||||
</React.Fragment>
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
private colorArgType = (argument: string, type: string): JSX.Element => {
|
||||
switch (type) {
|
||||
case 'time':
|
||||
case 'number':
|
||||
case 'period':
|
||||
case 'duration':
|
||||
case 'array': {
|
||||
return <span className="variable-value--number">{argument}</span>
|
||||
}
|
||||
case 'bool': {
|
||||
return <span className="variable-value--boolean">{argument}</span>
|
||||
}
|
||||
case 'string': {
|
||||
return <span className="variable-value--string">"{argument}"</span>
|
||||
}
|
||||
case 'invalid': {
|
||||
return <span className="variable-value--invalid">{argument}</span>
|
||||
}
|
||||
default: {
|
||||
return <span>{argument}</span>
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -48,7 +48,7 @@ const FuncList: SFC<Props> = ({
|
|||
/>
|
||||
))
|
||||
) : (
|
||||
<div className="ifql-func--item empty">No results</div>
|
||||
<div className="ifql-func--item empty">No matches</div>
|
||||
)}
|
||||
</FancyScrollbar>
|
||||
</ul>
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
import React, {PureComponent, MouseEvent} from 'react'
|
||||
|
||||
import FuncArgs from 'src/ifql/components/FuncArgs'
|
||||
import FuncArgsPreview from 'src/ifql/components/FuncArgsPreview'
|
||||
import {OnDeleteFuncNode, OnChangeArg, Func} from 'src/types/ifql'
|
||||
import {ErrorHandling} from 'src/shared/decorators/errors'
|
||||
|
||||
|
@ -13,7 +15,7 @@ interface Props {
|
|||
}
|
||||
|
||||
interface State {
|
||||
isOpen: boolean
|
||||
isExpanded: boolean
|
||||
}
|
||||
|
||||
@ErrorHandling
|
||||
|
@ -25,37 +27,39 @@ export default class FuncNode extends PureComponent<Props, State> {
|
|||
constructor(props) {
|
||||
super(props)
|
||||
this.state = {
|
||||
isOpen: true,
|
||||
isExpanded: false,
|
||||
}
|
||||
}
|
||||
|
||||
public render() {
|
||||
const {
|
||||
func,
|
||||
func: {args},
|
||||
bodyID,
|
||||
onChangeArg,
|
||||
declarationID,
|
||||
onGenerateScript,
|
||||
} = this.props
|
||||
const {isOpen} = this.state
|
||||
const {isExpanded} = this.state
|
||||
|
||||
return (
|
||||
<div className="func-node">
|
||||
<div className="func-node--name" onClick={this.handleClick}>
|
||||
<div>{func.name}</div>
|
||||
</div>
|
||||
{isOpen && (
|
||||
<div
|
||||
className="func-node"
|
||||
onMouseEnter={this.handleMouseEnter}
|
||||
onMouseLeave={this.handleMouseLeave}
|
||||
>
|
||||
<div className="func-node--name">{func.name}</div>
|
||||
<FuncArgsPreview args={args} />
|
||||
{isExpanded && (
|
||||
<FuncArgs
|
||||
func={func}
|
||||
bodyID={bodyID}
|
||||
onChangeArg={onChangeArg}
|
||||
declarationID={declarationID}
|
||||
onGenerateScript={onGenerateScript}
|
||||
onDeleteFunc={this.handleDelete}
|
||||
/>
|
||||
)}
|
||||
<div className="btn btn-danger btn-square" onClick={this.handleDelete}>
|
||||
<span className="icon-trash" />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
@ -66,10 +70,15 @@ export default class FuncNode extends PureComponent<Props, State> {
|
|||
this.props.onDelete({funcID: func.id, bodyID, declarationID})
|
||||
}
|
||||
|
||||
private handleClick = (e: MouseEvent<HTMLElement>): void => {
|
||||
private handleMouseEnter = (e: MouseEvent<HTMLElement>): void => {
|
||||
e.stopPropagation()
|
||||
|
||||
const {isOpen} = this.state
|
||||
this.setState({isOpen: !isOpen})
|
||||
this.setState({isExpanded: true})
|
||||
}
|
||||
|
||||
private handleMouseLeave = (e: MouseEvent<HTMLElement>): void => {
|
||||
e.stopPropagation()
|
||||
|
||||
this.setState({isExpanded: false})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import React, {PureComponent, ChangeEvent, KeyboardEvent} from 'react'
|
||||
import _ from 'lodash'
|
||||
import classnames from 'classnames'
|
||||
|
||||
import {ClickOutside} from 'src/shared/components/ClickOutside'
|
||||
import FuncList from 'src/ifql/components/FuncList'
|
||||
|
@ -17,10 +18,15 @@ interface Props {
|
|||
bodyID: string
|
||||
declarationID: string
|
||||
onAddNode: OnAddNode
|
||||
connectorVisible?: boolean
|
||||
}
|
||||
|
||||
@ErrorHandling
|
||||
export class FuncSelector extends PureComponent<Props, State> {
|
||||
public static defaultProps: Partial<Props> = {
|
||||
connectorVisible: true,
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
super(props)
|
||||
|
||||
|
@ -33,10 +39,12 @@ export class FuncSelector extends PureComponent<Props, State> {
|
|||
|
||||
public render() {
|
||||
const {isOpen, inputText, selectedFunc} = this.state
|
||||
const {connectorVisible} = this.props
|
||||
|
||||
return (
|
||||
<ClickOutside onClickOutside={this.handleClickOutside}>
|
||||
<div className="ifql-func--selector">
|
||||
<div className={this.className}>
|
||||
{connectorVisible && <div className="func-selector--connector" />}
|
||||
{isOpen ? (
|
||||
<FuncList
|
||||
inputText={inputText}
|
||||
|
@ -53,7 +61,7 @@ export class FuncSelector extends PureComponent<Props, State> {
|
|||
onClick={this.handleOpenList}
|
||||
tabIndex={0}
|
||||
>
|
||||
𝑓⟮𝑥⟯
|
||||
<span className="icon plus" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
@ -61,6 +69,12 @@ export class FuncSelector extends PureComponent<Props, State> {
|
|||
)
|
||||
}
|
||||
|
||||
private get className(): string {
|
||||
const {isOpen} = this.state
|
||||
|
||||
return classnames('ifql-func--selector', {open: isOpen})
|
||||
}
|
||||
|
||||
private handleCloseList = () => {
|
||||
this.setState({isOpen: false, selectedFunc: ''})
|
||||
}
|
||||
|
|
|
@ -0,0 +1,32 @@
|
|||
import React, {PureComponent} from 'react'
|
||||
import DatabaseList from 'src/ifql/components/DatabaseList'
|
||||
|
||||
class SchemaExplorer extends PureComponent {
|
||||
public render() {
|
||||
return (
|
||||
<div className="ifql-schema-explorer">
|
||||
<div className="ifql-schema--controls">
|
||||
<div className="ifql-schema--filter">
|
||||
<input
|
||||
className="form-control input-sm"
|
||||
placeholder="Filter YO schema dawg..."
|
||||
type="text"
|
||||
spellCheck={false}
|
||||
autoComplete="off"
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
className="btn btn-sm btn-default btn-square"
|
||||
disabled={true}
|
||||
title="Collapse YO tree"
|
||||
>
|
||||
<span className="icon collapse" />
|
||||
</button>
|
||||
</div>
|
||||
<DatabaseList />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default SchemaExplorer
|
|
@ -0,0 +1,65 @@
|
|||
import PropTypes from 'prop-types'
|
||||
import React, {PureComponent} from 'react'
|
||||
|
||||
import _ from 'lodash'
|
||||
|
||||
import TagListItem from 'src/ifql/components/TagListItem'
|
||||
|
||||
import {getTags, getTagValues} from 'src/ifql/apis'
|
||||
import {ErrorHandling} from 'src/shared/decorators/errors'
|
||||
|
||||
const {shape} = PropTypes
|
||||
|
||||
interface Props {
|
||||
db: string
|
||||
}
|
||||
|
||||
interface State {
|
||||
tags: {}
|
||||
selectedTag: string
|
||||
}
|
||||
|
||||
@ErrorHandling
|
||||
class TagList extends PureComponent<Props, State> {
|
||||
public static contextTypes = {
|
||||
source: shape({
|
||||
links: shape({}).isRequired,
|
||||
}).isRequired,
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
super(props)
|
||||
this.state = {
|
||||
tags: {},
|
||||
selectedTag: '',
|
||||
}
|
||||
}
|
||||
|
||||
public componentDidMount() {
|
||||
const {db} = this.props
|
||||
if (!db) {
|
||||
return
|
||||
}
|
||||
|
||||
this.getTags()
|
||||
}
|
||||
|
||||
public async getTags() {
|
||||
const keys = await getTags()
|
||||
const values = await getTagValues()
|
||||
|
||||
const tags = keys.reduce((acc, k) => {
|
||||
return {...acc, [k]: values}
|
||||
}, {})
|
||||
|
||||
this.setState({tags})
|
||||
}
|
||||
|
||||
public render() {
|
||||
return _.map(this.state.tags, (tagValues: string[], tagKey: string) => (
|
||||
<TagListItem key={tagKey} tagKey={tagKey} tagValues={tagValues} />
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
export default TagList
|
|
@ -0,0 +1,69 @@
|
|||
import classnames from 'classnames'
|
||||
import React, {PureComponent, MouseEvent} from 'react'
|
||||
import {ErrorHandling} from 'src/shared/decorators/errors'
|
||||
|
||||
interface Props {
|
||||
tagKey: string
|
||||
tagValues: string[]
|
||||
}
|
||||
|
||||
interface State {
|
||||
isOpen: boolean
|
||||
}
|
||||
|
||||
@ErrorHandling
|
||||
class TagListItem extends PureComponent<Props, State> {
|
||||
constructor(props) {
|
||||
super(props)
|
||||
this.state = {
|
||||
isOpen: false,
|
||||
}
|
||||
}
|
||||
|
||||
public render() {
|
||||
const {isOpen} = this.state
|
||||
|
||||
return (
|
||||
<div className={this.className}>
|
||||
<div className="ifql-schema-item" onClick={this.handleClick}>
|
||||
<div className="ifql-schema-item-toggle" />
|
||||
{this.tagItemLabel}
|
||||
<span className="ifql-schema-type">Tag Key</span>
|
||||
</div>
|
||||
{isOpen && this.renderTagValues}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
private handleClick = (e: MouseEvent<HTMLElement>): void => {
|
||||
e.stopPropagation()
|
||||
this.setState({isOpen: !this.state.isOpen})
|
||||
}
|
||||
|
||||
private get tagItemLabel(): string {
|
||||
const {tagKey} = this.props
|
||||
return `${tagKey}`
|
||||
}
|
||||
|
||||
private get renderTagValues(): JSX.Element[] | JSX.Element {
|
||||
const {tagValues} = this.props
|
||||
if (!tagValues || !tagValues.length) {
|
||||
return <div className="ifql-schema-tree__empty">No tag values</div>
|
||||
}
|
||||
|
||||
return tagValues.map(v => {
|
||||
return (
|
||||
<div key={v} className="ifql-schema-item readonly ifql-tree-node">
|
||||
{v}
|
||||
</div>
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
private get className(): string {
|
||||
const {isOpen} = this.state
|
||||
return classnames('ifql-schema-tree ifql-tree-node', {expanded: isOpen})
|
||||
}
|
||||
}
|
||||
|
||||
export default TagListItem
|
|
@ -1,20 +1,17 @@
|
|||
import React, {PureComponent} from 'react'
|
||||
import SchemaExplorer from 'src/ifql/components/SchemaExplorer'
|
||||
import BodyBuilder from 'src/ifql/components/BodyBuilder'
|
||||
import TimeMachineEditor from 'src/ifql/components/TimeMachineEditor'
|
||||
|
||||
import {
|
||||
Suggestion,
|
||||
OnChangeScript,
|
||||
OnSubmitScript,
|
||||
FlatBody,
|
||||
} from 'src/types/ifql'
|
||||
import TimeMachineVis from 'src/ifql/components/TimeMachineVis'
|
||||
import Threesizer from 'src/shared/components/Threesizer'
|
||||
import {Suggestion, OnChangeScript, FlatBody} from 'src/types/ifql'
|
||||
import {ErrorHandling} from 'src/shared/decorators/errors'
|
||||
import {HANDLE_VERTICAL, HANDLE_HORIZONTAL} from 'src/shared/constants/index'
|
||||
|
||||
interface Props {
|
||||
script: string
|
||||
suggestions: Suggestion[]
|
||||
body: Body[]
|
||||
onSubmitScript: OnSubmitScript
|
||||
onChangeScript: OnChangeScript
|
||||
}
|
||||
|
||||
|
@ -25,27 +22,52 @@ interface Body extends FlatBody {
|
|||
@ErrorHandling
|
||||
class TimeMachine extends PureComponent<Props> {
|
||||
public render() {
|
||||
const {
|
||||
body,
|
||||
script,
|
||||
onChangeScript,
|
||||
onSubmitScript,
|
||||
suggestions,
|
||||
} = this.props
|
||||
|
||||
return (
|
||||
<div className="time-machine-container">
|
||||
<TimeMachineEditor
|
||||
script={script}
|
||||
onChangeScript={onChangeScript}
|
||||
onSubmitScript={onSubmitScript}
|
||||
<Threesizer
|
||||
orientation={HANDLE_HORIZONTAL}
|
||||
divisions={this.mainSplit}
|
||||
containerClass="page-contents"
|
||||
/>
|
||||
<div className="expression-container">
|
||||
<BodyBuilder body={body} suggestions={suggestions} />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
private get mainSplit() {
|
||||
return [
|
||||
{
|
||||
handleDisplay: 'none',
|
||||
render: () => (
|
||||
<Threesizer
|
||||
divisions={this.divisions}
|
||||
orientation={HANDLE_VERTICAL}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
handlePixels: 8,
|
||||
render: () => <TimeMachineVis blob="Visualizer" />,
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
private get divisions() {
|
||||
const {body, suggestions, script, onChangeScript} = this.props
|
||||
return [
|
||||
{
|
||||
name: 'Script',
|
||||
render: () => (
|
||||
<TimeMachineEditor script={script} onChangeScript={onChangeScript} />
|
||||
),
|
||||
},
|
||||
{
|
||||
name: 'Build',
|
||||
render: () => <BodyBuilder body={body} suggestions={suggestions} />,
|
||||
},
|
||||
{
|
||||
name: 'Explore',
|
||||
render: () => <SchemaExplorer />,
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
export default TimeMachine
|
||||
|
|
|
@ -3,12 +3,16 @@ import {Controlled as CodeMirror, IInstance} from 'react-codemirror2'
|
|||
import {EditorChange} from 'codemirror'
|
||||
import 'src/external/codemirror'
|
||||
import {ErrorHandling} from 'src/shared/decorators/errors'
|
||||
import {OnSubmitScript, OnChangeScript} from 'src/types/ifql'
|
||||
import {OnChangeScript} from 'src/types/ifql'
|
||||
import {editor} from 'src/ifql/constants'
|
||||
|
||||
interface Props {
|
||||
script: string
|
||||
onChangeScript: OnChangeScript
|
||||
onSubmitScript: OnSubmitScript
|
||||
}
|
||||
|
||||
interface EditorInstance extends IInstance {
|
||||
showHint: (options?: any) => void
|
||||
}
|
||||
|
||||
@ErrorHandling
|
||||
|
@ -25,10 +29,11 @@ class TimeMachineEditor extends PureComponent<Props> {
|
|||
theme: 'material',
|
||||
tabIndex: 1,
|
||||
readonly: false,
|
||||
extraKeys: {'Ctrl-Space': 'autocomplete'},
|
||||
completeSingle: false,
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="time-machine-editor-container">
|
||||
<div className="time-machine-editor">
|
||||
<CodeMirror
|
||||
autoFocus={true}
|
||||
|
@ -37,18 +42,22 @@ class TimeMachineEditor extends PureComponent<Props> {
|
|||
options={options}
|
||||
onBeforeChange={this.updateCode}
|
||||
onTouchStart={this.onTouchStart}
|
||||
onKeyUp={this.handleKeyUp}
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
className="btn btn-lg btn-primary"
|
||||
onClick={this.props.onSubmitScript}
|
||||
>
|
||||
Submit Script
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
private handleKeyUp = (instance: EditorInstance, e: KeyboardEvent) => {
|
||||
const {key} = e
|
||||
|
||||
if (editor.EXCLUDED_KEYS.includes(key)) {
|
||||
return
|
||||
}
|
||||
|
||||
instance.showHint({completeSingle: false})
|
||||
}
|
||||
|
||||
private onTouchStart = () => {}
|
||||
|
||||
private updateCode = (
|
||||
|
|
|
@ -0,0 +1,14 @@
|
|||
import React, {SFC} from 'react'
|
||||
|
||||
interface Props {
|
||||
blob: string
|
||||
}
|
||||
const TimeMachineVis: SFC<Props> = ({blob}) => (
|
||||
<div className="time-machine-visualization">
|
||||
<div className="time-machine--graph">
|
||||
<div className="time-machine--graph-body">{blob}</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
export default TimeMachineVis
|
|
@ -0,0 +1,126 @@
|
|||
import React, {PureComponent, MouseEvent} from 'react'
|
||||
|
||||
interface Props {
|
||||
name?: string
|
||||
}
|
||||
|
||||
interface State {
|
||||
isExpanded: boolean
|
||||
}
|
||||
|
||||
export default class VariableName extends PureComponent<Props, State> {
|
||||
public static defaultProps: Partial<Props> = {
|
||||
name: '',
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
super(props)
|
||||
|
||||
this.state = {
|
||||
isExpanded: false,
|
||||
}
|
||||
}
|
||||
|
||||
public render() {
|
||||
const {isExpanded} = this.state
|
||||
|
||||
return (
|
||||
<div
|
||||
className="variable-string"
|
||||
onMouseEnter={this.handleMouseEnter}
|
||||
onMouseLeave={this.handleMouseLeave}
|
||||
>
|
||||
{this.nameElement}
|
||||
{isExpanded && this.renderTooltip}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
private get renderTooltip(): JSX.Element {
|
||||
const {name} = this.props
|
||||
|
||||
if (name.includes('=')) {
|
||||
const split = name.split('=')
|
||||
const varName = split[0].substring(0, split[0].length - 1)
|
||||
const varValue = split[1].substring(1)
|
||||
|
||||
return (
|
||||
<div className="variable-name--tooltip">
|
||||
<input
|
||||
type="text"
|
||||
className="form-control form-plutonium input-sm variable-name--input"
|
||||
defaultValue={varName}
|
||||
placeholder="Name"
|
||||
/>
|
||||
<span className="variable-name--operator">=</span>
|
||||
<input
|
||||
type="text"
|
||||
className="form-control input-sm variable-name--input"
|
||||
defaultValue={varValue}
|
||||
placeholder="Value"
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="variable-name--tooltip">
|
||||
<input
|
||||
type="text"
|
||||
className="form-control form-plutonium input-sm variable-name--input"
|
||||
defaultValue={name}
|
||||
placeholder="Name this query..."
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
private handleMouseEnter = (e: MouseEvent<HTMLElement>): void => {
|
||||
e.stopPropagation()
|
||||
|
||||
this.setState({isExpanded: true})
|
||||
}
|
||||
|
||||
private handleMouseLeave = (e: MouseEvent<HTMLElement>): void => {
|
||||
e.stopPropagation()
|
||||
|
||||
this.setState({isExpanded: false})
|
||||
}
|
||||
|
||||
private get nameElement(): JSX.Element {
|
||||
const {name} = this.props
|
||||
|
||||
if (!name) {
|
||||
return <span className="variable-blank">Untitled</span>
|
||||
}
|
||||
|
||||
if (name.includes('=')) {
|
||||
return this.colorizeSyntax
|
||||
}
|
||||
|
||||
return <span className="variable-name">{name}</span>
|
||||
}
|
||||
|
||||
private get colorizeSyntax(): JSX.Element {
|
||||
const {name} = this.props
|
||||
const split = name.split('=')
|
||||
const varName = split[0].substring(0, split[0].length - 1)
|
||||
const varValue = split[1].substring(1)
|
||||
|
||||
const valueIsString = varValue.endsWith('"')
|
||||
|
||||
return (
|
||||
<>
|
||||
<span className="variable-name">{varName}</span>
|
||||
{' = '}
|
||||
<span
|
||||
className={
|
||||
valueIsString ? 'variable-value--string' : 'variable-value--number'
|
||||
}
|
||||
>
|
||||
{varValue}
|
||||
</span>
|
||||
</>
|
||||
)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,155 @@
|
|||
export const ast = {
|
||||
type: 'File',
|
||||
start: 0,
|
||||
end: 22,
|
||||
loc: {
|
||||
start: {
|
||||
line: 1,
|
||||
column: 0,
|
||||
},
|
||||
end: {
|
||||
line: 1,
|
||||
column: 22,
|
||||
},
|
||||
},
|
||||
program: {
|
||||
type: 'Program',
|
||||
start: 0,
|
||||
end: 22,
|
||||
loc: {
|
||||
start: {
|
||||
line: 1,
|
||||
column: 0,
|
||||
},
|
||||
end: {
|
||||
line: 1,
|
||||
column: 22,
|
||||
},
|
||||
},
|
||||
sourceType: 'module',
|
||||
body: [
|
||||
{
|
||||
type: 'ExpressionStatement',
|
||||
start: 0,
|
||||
end: 22,
|
||||
loc: {
|
||||
start: {
|
||||
line: 1,
|
||||
column: 0,
|
||||
},
|
||||
end: {
|
||||
line: 1,
|
||||
column: 22,
|
||||
},
|
||||
},
|
||||
expression: {
|
||||
type: 'CallExpression',
|
||||
start: 0,
|
||||
end: 22,
|
||||
loc: {
|
||||
start: {
|
||||
line: 1,
|
||||
column: 0,
|
||||
},
|
||||
end: {
|
||||
line: 1,
|
||||
column: 22,
|
||||
},
|
||||
},
|
||||
callee: {
|
||||
type: 'Identifier',
|
||||
start: 0,
|
||||
end: 4,
|
||||
loc: {
|
||||
start: {
|
||||
line: 1,
|
||||
column: 0,
|
||||
},
|
||||
end: {
|
||||
line: 1,
|
||||
column: 4,
|
||||
},
|
||||
identifierName: 'from',
|
||||
},
|
||||
name: 'from',
|
||||
},
|
||||
arguments: [
|
||||
{
|
||||
type: 'ObjectExpression',
|
||||
start: 5,
|
||||
end: 21,
|
||||
loc: {
|
||||
start: {
|
||||
line: 1,
|
||||
column: 5,
|
||||
},
|
||||
end: {
|
||||
line: 1,
|
||||
column: 21,
|
||||
},
|
||||
},
|
||||
properties: [
|
||||
{
|
||||
type: 'ObjectProperty',
|
||||
start: 6,
|
||||
end: 20,
|
||||
loc: {
|
||||
start: {
|
||||
line: 1,
|
||||
column: 6,
|
||||
},
|
||||
end: {
|
||||
line: 1,
|
||||
column: 20,
|
||||
},
|
||||
},
|
||||
method: false,
|
||||
shorthand: false,
|
||||
computed: false,
|
||||
key: {
|
||||
type: 'Identifier',
|
||||
start: 6,
|
||||
end: 8,
|
||||
loc: {
|
||||
start: {
|
||||
line: 1,
|
||||
column: 6,
|
||||
},
|
||||
end: {
|
||||
line: 1,
|
||||
column: 8,
|
||||
},
|
||||
identifierName: 'db',
|
||||
},
|
||||
name: 'db',
|
||||
},
|
||||
value: {
|
||||
type: 'StringLiteral',
|
||||
start: 10,
|
||||
end: 20,
|
||||
loc: {
|
||||
start: {
|
||||
line: 1,
|
||||
column: 10,
|
||||
},
|
||||
end: {
|
||||
line: 1,
|
||||
column: 20,
|
||||
},
|
||||
},
|
||||
extra: {
|
||||
rawValue: 'telegraf',
|
||||
raw: 'telegraf',
|
||||
},
|
||||
value: 'telegraf',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
directives: [],
|
||||
},
|
||||
}
|
|
@ -0,0 +1,57 @@
|
|||
export const EXCLUDED_KEYS = [
|
||||
'ArrowRight',
|
||||
'ArrowLeft',
|
||||
'ArrowDown',
|
||||
'ArrowUp',
|
||||
'Backspace',
|
||||
'Tab',
|
||||
'Enter',
|
||||
'Shift',
|
||||
'Ctrl',
|
||||
'Control',
|
||||
'Alt',
|
||||
'Pause',
|
||||
'Capslock',
|
||||
'Escape',
|
||||
'Pageup',
|
||||
'Pagedown',
|
||||
'End',
|
||||
'Home',
|
||||
'Left',
|
||||
'Up',
|
||||
'Right',
|
||||
'Down',
|
||||
'Insert',
|
||||
'Delete',
|
||||
'Left window key',
|
||||
'Right window key',
|
||||
'Select',
|
||||
'Add',
|
||||
'Subtract',
|
||||
'Decimal point',
|
||||
'Divide',
|
||||
'F1',
|
||||
'F2',
|
||||
'F3',
|
||||
'F4',
|
||||
'F5',
|
||||
'F6',
|
||||
'F7',
|
||||
'F8',
|
||||
'F9',
|
||||
'F10',
|
||||
'F11',
|
||||
'F12',
|
||||
'Numlock',
|
||||
'Scrolllock',
|
||||
'Semicolon',
|
||||
'Equalsign',
|
||||
'Comma',
|
||||
'Dash',
|
||||
'Slash',
|
||||
'Graveaccent',
|
||||
'Backslash',
|
||||
'Quote',
|
||||
'Meta',
|
||||
' ',
|
||||
]
|
|
@ -1,160 +1,6 @@
|
|||
import * as funcNames from 'src/ifql/constants/funcNames'
|
||||
import * as argTypes from 'src/ifql/constants/argumentTypes'
|
||||
import {ast} from 'src/ifql/constants/ast'
|
||||
import * as editor from 'src/ifql/constants/editor'
|
||||
|
||||
const ast = {
|
||||
type: 'File',
|
||||
start: 0,
|
||||
end: 22,
|
||||
loc: {
|
||||
start: {
|
||||
line: 1,
|
||||
column: 0,
|
||||
},
|
||||
end: {
|
||||
line: 1,
|
||||
column: 22,
|
||||
},
|
||||
},
|
||||
program: {
|
||||
type: 'Program',
|
||||
start: 0,
|
||||
end: 22,
|
||||
loc: {
|
||||
start: {
|
||||
line: 1,
|
||||
column: 0,
|
||||
},
|
||||
end: {
|
||||
line: 1,
|
||||
column: 22,
|
||||
},
|
||||
},
|
||||
sourceType: 'module',
|
||||
body: [
|
||||
{
|
||||
type: 'ExpressionStatement',
|
||||
start: 0,
|
||||
end: 22,
|
||||
loc: {
|
||||
start: {
|
||||
line: 1,
|
||||
column: 0,
|
||||
},
|
||||
end: {
|
||||
line: 1,
|
||||
column: 22,
|
||||
},
|
||||
},
|
||||
expression: {
|
||||
type: 'CallExpression',
|
||||
start: 0,
|
||||
end: 22,
|
||||
loc: {
|
||||
start: {
|
||||
line: 1,
|
||||
column: 0,
|
||||
},
|
||||
end: {
|
||||
line: 1,
|
||||
column: 22,
|
||||
},
|
||||
},
|
||||
callee: {
|
||||
type: 'Identifier',
|
||||
start: 0,
|
||||
end: 4,
|
||||
loc: {
|
||||
start: {
|
||||
line: 1,
|
||||
column: 0,
|
||||
},
|
||||
end: {
|
||||
line: 1,
|
||||
column: 4,
|
||||
},
|
||||
identifierName: 'from',
|
||||
},
|
||||
name: 'from',
|
||||
},
|
||||
arguments: [
|
||||
{
|
||||
type: 'ObjectExpression',
|
||||
start: 5,
|
||||
end: 21,
|
||||
loc: {
|
||||
start: {
|
||||
line: 1,
|
||||
column: 5,
|
||||
},
|
||||
end: {
|
||||
line: 1,
|
||||
column: 21,
|
||||
},
|
||||
},
|
||||
properties: [
|
||||
{
|
||||
type: 'ObjectProperty',
|
||||
start: 6,
|
||||
end: 20,
|
||||
loc: {
|
||||
start: {
|
||||
line: 1,
|
||||
column: 6,
|
||||
},
|
||||
end: {
|
||||
line: 1,
|
||||
column: 20,
|
||||
},
|
||||
},
|
||||
method: false,
|
||||
shorthand: false,
|
||||
computed: false,
|
||||
key: {
|
||||
type: 'Identifier',
|
||||
start: 6,
|
||||
end: 8,
|
||||
loc: {
|
||||
start: {
|
||||
line: 1,
|
||||
column: 6,
|
||||
},
|
||||
end: {
|
||||
line: 1,
|
||||
column: 8,
|
||||
},
|
||||
identifierName: 'db',
|
||||
},
|
||||
name: 'db',
|
||||
},
|
||||
value: {
|
||||
type: 'StringLiteral',
|
||||
start: 10,
|
||||
end: 20,
|
||||
loc: {
|
||||
start: {
|
||||
line: 1,
|
||||
column: 10,
|
||||
},
|
||||
end: {
|
||||
line: 1,
|
||||
column: 20,
|
||||
},
|
||||
},
|
||||
extra: {
|
||||
rawValue: 'telegraf',
|
||||
raw: 'telegraf',
|
||||
},
|
||||
value: 'telegraf',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
directives: [],
|
||||
},
|
||||
}
|
||||
|
||||
export {ast, funcNames, argTypes}
|
||||
export {ast, funcNames, argTypes, editor}
|
||||
|
|
|
@ -69,25 +69,28 @@ export class IFQLPage extends PureComponent<Props, State> {
|
|||
<IFQLContext.Provider value={this.handlers}>
|
||||
<KeyboardShortcuts onControlEnter={this.handleSubmitScript}>
|
||||
<div className="page hosts-list-page">
|
||||
<div className="page-header">
|
||||
<div className="page-header full-width">
|
||||
<div className="page-header__container">
|
||||
<div className="page-header__left">
|
||||
<h1 className="page-header__title">Time Machine</h1>
|
||||
</div>
|
||||
<div className="page-header__right">
|
||||
<button
|
||||
className="btn btn-sm btn-primary"
|
||||
onClick={this.handleSubmitScript}
|
||||
>
|
||||
Submit Script
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="page-contents">
|
||||
<div className="container-fluid">
|
||||
<TimeMachine
|
||||
script={script}
|
||||
body={this.state.body}
|
||||
suggestions={suggestions}
|
||||
onSubmitScript={this.handleSubmitScript}
|
||||
onChangeScript={this.handleChangeScript}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</KeyboardShortcuts>
|
||||
</IFQLContext.Provider>
|
||||
)
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import PropTypes from 'prop-types'
|
||||
import React, {Component} from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
import _ from 'lodash'
|
||||
|
||||
|
|
|
@ -1,26 +1,64 @@
|
|||
import _ from 'lodash'
|
||||
import React, {Component} from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import classnames from 'classnames'
|
||||
import {Scrollbars} from 'react-custom-scrollbars'
|
||||
import {ErrorHandling} from 'src/shared/decorators/errors'
|
||||
|
||||
@ErrorHandling
|
||||
class FancyScrollbar extends Component {
|
||||
constructor(props) {
|
||||
super(props)
|
||||
}
|
||||
interface DefaultProps {
|
||||
autoHide: boolean
|
||||
autoHeight: boolean
|
||||
maxHeight: number
|
||||
setScrollTop: (value: React.MouseEvent<JSX.Element>) => void
|
||||
style: React.CSSProperties
|
||||
}
|
||||
|
||||
static defaultProps = {
|
||||
interface Props {
|
||||
className?: string
|
||||
scrollTop?: number
|
||||
scrollLeft?: number
|
||||
}
|
||||
|
||||
@ErrorHandling
|
||||
class FancyScrollbar extends Component<Props & Partial<DefaultProps>> {
|
||||
public static defaultProps = {
|
||||
autoHide: true,
|
||||
autoHeight: false,
|
||||
maxHeight: null,
|
||||
style: {},
|
||||
setScrollTop: () => {},
|
||||
}
|
||||
|
||||
handleMakeDiv = className => props => {
|
||||
private ref: React.RefObject<Scrollbars>
|
||||
|
||||
constructor(props) {
|
||||
super(props)
|
||||
this.ref = React.createRef<Scrollbars>()
|
||||
}
|
||||
|
||||
public updateScroll() {
|
||||
const ref = this.ref.current
|
||||
if (ref && !_.isNil(this.props.scrollTop)) {
|
||||
ref.scrollTop(this.props.scrollTop)
|
||||
}
|
||||
|
||||
if (ref && !_.isNil(this.props.scrollLeft)) {
|
||||
ref.scrollLeft(this.props.scrollLeft)
|
||||
}
|
||||
}
|
||||
|
||||
public componentDidMount() {
|
||||
this.updateScroll()
|
||||
}
|
||||
|
||||
public componentDidUpdate() {
|
||||
this.updateScroll()
|
||||
}
|
||||
|
||||
public handleMakeDiv = (className: string) => (props): JSX.Element => {
|
||||
return <div {...props} className={`fancy-scroll--${className}`} />
|
||||
}
|
||||
|
||||
render() {
|
||||
public render() {
|
||||
const {
|
||||
autoHide,
|
||||
autoHeight,
|
||||
|
@ -28,6 +66,7 @@ class FancyScrollbar extends Component {
|
|||
className,
|
||||
maxHeight,
|
||||
setScrollTop,
|
||||
style,
|
||||
} = this.props
|
||||
|
||||
return (
|
||||
|
@ -35,6 +74,8 @@ class FancyScrollbar extends Component {
|
|||
className={classnames('fancy-scroll--container', {
|
||||
[className]: className,
|
||||
})}
|
||||
ref={this.ref}
|
||||
style={style}
|
||||
onScroll={setScrollTop}
|
||||
autoHide={autoHide}
|
||||
autoHideTimeout={1000}
|
||||
|
@ -53,15 +94,4 @@ class FancyScrollbar extends Component {
|
|||
}
|
||||
}
|
||||
|
||||
const {bool, func, node, number, string} = PropTypes
|
||||
|
||||
FancyScrollbar.propTypes = {
|
||||
children: node.isRequired,
|
||||
className: string,
|
||||
autoHide: bool,
|
||||
autoHeight: bool,
|
||||
maxHeight: number,
|
||||
setScrollTop: func,
|
||||
}
|
||||
|
||||
export default FancyScrollbar
|
|
@ -18,7 +18,7 @@ const FuncSelectorInput: SFC<Props> = ({
|
|||
className="form-control input-sm ifql-func--input"
|
||||
type="text"
|
||||
autoFocus={true}
|
||||
placeholder="Add Function..."
|
||||
placeholder="Add a Function..."
|
||||
spellCheck={false}
|
||||
onChange={onFilterChange}
|
||||
onKeyDown={onFilterKeyPress}
|
||||
|
|
|
@ -23,11 +23,11 @@ const getSource = (cell, source, sources, defaultSource) => {
|
|||
@ErrorHandling
|
||||
class LayoutState extends Component {
|
||||
state = {
|
||||
celldata: [],
|
||||
cellData: [],
|
||||
}
|
||||
|
||||
grabDataForDownload = celldata => {
|
||||
this.setState({celldata})
|
||||
grabDataForDownload = cellData => {
|
||||
this.setState({cellData})
|
||||
}
|
||||
|
||||
render() {
|
||||
|
@ -59,7 +59,7 @@ const Layout = (
|
|||
source,
|
||||
sources,
|
||||
onZoom,
|
||||
celldata,
|
||||
cellData,
|
||||
templates,
|
||||
timeRange,
|
||||
isEditable,
|
||||
|
@ -79,7 +79,7 @@ const Layout = (
|
|||
) => (
|
||||
<LayoutCell
|
||||
cell={cell}
|
||||
celldata={celldata}
|
||||
cellData={cellData}
|
||||
isEditable={isEditable}
|
||||
onEditCell={onEditCell}
|
||||
onCloneCell={onCloneCell}
|
||||
|
@ -200,7 +200,7 @@ LayoutState.propTypes = {...propTypes}
|
|||
Layout.propTypes = {
|
||||
...propTypes,
|
||||
grabDataForDownload: func,
|
||||
celldata: arrayOf(shape()),
|
||||
cellData: arrayOf(shape({})),
|
||||
}
|
||||
|
||||
export default LayoutState
|
||||
|
|
|
@ -8,9 +8,10 @@ import LayoutCellMenu from 'shared/components/LayoutCellMenu'
|
|||
import LayoutCellHeader from 'shared/components/LayoutCellHeader'
|
||||
import {notify} from 'src/shared/actions/notifications'
|
||||
import {notifyCSVDownloadFailed} from 'src/shared/copy/notifications'
|
||||
import {dashboardtoCSV} from 'shared/parsing/resultsToCSV'
|
||||
import download from 'src/external/download.js'
|
||||
import {ErrorHandling} from 'src/shared/decorators/errors'
|
||||
import {dataToCSV} from 'src/shared/parsing/dataToCSV'
|
||||
import {timeSeriesToTableGraph} from 'src/utils/timeSeriesTransformers'
|
||||
|
||||
@ErrorHandling
|
||||
class LayoutCell extends Component {
|
||||
|
@ -24,9 +25,11 @@ class LayoutCell extends Component {
|
|||
|
||||
handleCSVDownload = cell => () => {
|
||||
const joinedName = cell.name.split(' ').join('_')
|
||||
const {celldata} = this.props
|
||||
const {cellData} = this.props
|
||||
const {data} = timeSeriesToTableGraph(cellData)
|
||||
|
||||
try {
|
||||
download(dashboardtoCSV(celldata), `${joinedName}.csv`, 'text/plain')
|
||||
download(dataToCSV(data), `${joinedName}.csv`, 'text/plain')
|
||||
} catch (error) {
|
||||
notify(notifyCSVDownloadFailed())
|
||||
console.error(error)
|
||||
|
@ -34,7 +37,7 @@ class LayoutCell extends Component {
|
|||
}
|
||||
|
||||
render() {
|
||||
const {cell, children, isEditable, celldata, onCloneCell} = this.props
|
||||
const {cell, children, isEditable, cellData, onCloneCell} = this.props
|
||||
|
||||
const queries = _.get(cell, ['queries'], [])
|
||||
|
||||
|
@ -49,7 +52,7 @@ class LayoutCell extends Component {
|
|||
<LayoutCellMenu
|
||||
cell={cell}
|
||||
queries={queries}
|
||||
dataExists={!!celldata.length}
|
||||
dataExists={!!cellData.length}
|
||||
isEditable={isEditable}
|
||||
onDelete={this.handleDeleteCell}
|
||||
onEdit={this.handleSummonOverlay}
|
||||
|
@ -96,7 +99,7 @@ LayoutCell.propTypes = {
|
|||
onSummonOverlayTechnologies: func,
|
||||
isEditable: bool,
|
||||
onCancelEditCell: func,
|
||||
celldata: arrayOf(shape()),
|
||||
cellData: arrayOf(shape({})),
|
||||
}
|
||||
|
||||
export default LayoutCell
|
||||
|
|
|
@ -0,0 +1,105 @@
|
|||
import {CellMeasurerCache} from 'react-virtualized'
|
||||
|
||||
interface CellMeasurerCacheDecoratorParams {
|
||||
cellMeasurerCache: CellMeasurerCache
|
||||
columnIndexOffset: number
|
||||
rowIndexOffset: number
|
||||
}
|
||||
|
||||
interface IndexParam {
|
||||
index: number
|
||||
}
|
||||
|
||||
class CellMeasurerCacheDecorator {
|
||||
private cellMeasurerCache: CellMeasurerCache
|
||||
private columnIndexOffset: number
|
||||
private rowIndexOffset: number
|
||||
|
||||
constructor(params: Partial<CellMeasurerCacheDecoratorParams> = {}) {
|
||||
const {
|
||||
cellMeasurerCache,
|
||||
columnIndexOffset = 0,
|
||||
rowIndexOffset = 0,
|
||||
} = params
|
||||
|
||||
this.cellMeasurerCache = cellMeasurerCache
|
||||
this.columnIndexOffset = columnIndexOffset
|
||||
this.rowIndexOffset = rowIndexOffset
|
||||
}
|
||||
|
||||
public clear(rowIndex: number, columnIndex: number): void {
|
||||
this.cellMeasurerCache.clear(
|
||||
rowIndex + this.rowIndexOffset,
|
||||
columnIndex + this.columnIndexOffset
|
||||
)
|
||||
}
|
||||
|
||||
public clearAll(): void {
|
||||
this.cellMeasurerCache.clearAll()
|
||||
}
|
||||
|
||||
public columnWidth = ({index}: IndexParam) => {
|
||||
this.cellMeasurerCache.columnWidth({
|
||||
index: index + this.columnIndexOffset,
|
||||
})
|
||||
}
|
||||
|
||||
get defaultHeight(): number {
|
||||
return this.cellMeasurerCache.defaultHeight
|
||||
}
|
||||
|
||||
get defaultWidth(): number {
|
||||
return this.cellMeasurerCache.defaultWidth
|
||||
}
|
||||
|
||||
public hasFixedHeight(): boolean {
|
||||
return this.cellMeasurerCache.hasFixedHeight()
|
||||
}
|
||||
|
||||
public hasFixedWidth(): boolean {
|
||||
return this.cellMeasurerCache.hasFixedWidth()
|
||||
}
|
||||
|
||||
public getHeight(rowIndex: number, columnIndex: number = 0): number | null {
|
||||
return this.cellMeasurerCache.getHeight(
|
||||
rowIndex + this.rowIndexOffset,
|
||||
columnIndex + this.columnIndexOffset
|
||||
)
|
||||
}
|
||||
|
||||
public getWidth(rowIndex: number, columnIndex: number = 0): number | null {
|
||||
return this.cellMeasurerCache.getWidth(
|
||||
rowIndex + this.rowIndexOffset,
|
||||
columnIndex + this.columnIndexOffset
|
||||
)
|
||||
}
|
||||
|
||||
public has(rowIndex: number, columnIndex: number = 0): boolean {
|
||||
return this.cellMeasurerCache.has(
|
||||
rowIndex + this.rowIndexOffset,
|
||||
columnIndex + this.columnIndexOffset
|
||||
)
|
||||
}
|
||||
|
||||
public rowHeight = ({index}: IndexParam) => {
|
||||
this.cellMeasurerCache.rowHeight({
|
||||
index: index + this.rowIndexOffset,
|
||||
})
|
||||
}
|
||||
|
||||
public set(
|
||||
rowIndex: number,
|
||||
columnIndex: number,
|
||||
width: number,
|
||||
height: number
|
||||
): void {
|
||||
this.cellMeasurerCache.set(
|
||||
rowIndex + this.rowIndexOffset,
|
||||
columnIndex + this.columnIndexOffset,
|
||||
width,
|
||||
height
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default CellMeasurerCacheDecorator
|
|
@ -0,0 +1,813 @@
|
|||
import * as React from 'react'
|
||||
import CellMeasurerCacheDecorator from './CellMeasurerCacheDecorator'
|
||||
import FancyScrollbar from 'src/shared/components/FancyScrollbar'
|
||||
import {Grid} from 'react-virtualized'
|
||||
|
||||
const SCROLLBAR_SIZE_BUFFER = 20
|
||||
|
||||
interface Props {
|
||||
columnCount?: number
|
||||
classNameBottomLeftGrid?: string
|
||||
classNameBottomRightGrid?: string
|
||||
classNameTopLeftGrid?: string
|
||||
classNameTopRightGrid?: string
|
||||
enableFixedColumnScroll?: boolean
|
||||
enableFixedRowScroll?: boolean
|
||||
fixedColumnCount?: number
|
||||
fixedRowCount?: number
|
||||
style?: object
|
||||
styleBottomLeftGrid?: object
|
||||
styleBottomRightGrid?: object
|
||||
styleTopLeftGrid?: object
|
||||
styleTopRightGrid?: object
|
||||
scrollTop?: number
|
||||
scrollLeft?: number
|
||||
rowCount?: number
|
||||
rowHeight?: (arg: {index: number}) => {} | number
|
||||
columnWidth?: (arg: object) => {} | number
|
||||
onScroll?: (arg: object) => {}
|
||||
width: number
|
||||
height: number
|
||||
scrollToRow?: () => {}
|
||||
onSectionRendered?: () => {}
|
||||
scrollToColumn?: () => {}
|
||||
cellRenderer?: (arg: object) => JSX.Element
|
||||
}
|
||||
|
||||
interface State {
|
||||
scrollLeft: number
|
||||
scrollTop: number
|
||||
scrollbarSize: number
|
||||
showHorizontalScrollbar: boolean
|
||||
showVerticalScrollbar: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders 1, 2, or 4 Grids depending on configuration.
|
||||
* A main (body) Grid will always be rendered.
|
||||
* Optionally, 1-2 Grids for sticky header rows will also be rendered.
|
||||
* If no sticky columns, only 1 sticky header Grid will be rendered.
|
||||
* If sticky columns, 2 sticky header Grids will be rendered.
|
||||
*/
|
||||
class MultiGrid extends React.PureComponent<Props, State> {
|
||||
public static defaultProps = {
|
||||
classNameBottomLeftGrid: '',
|
||||
classNameBottomRightGrid: '',
|
||||
classNameTopLeftGrid: '',
|
||||
classNameTopRightGrid: '',
|
||||
enableFixedColumnScroll: false,
|
||||
enableFixedRowScroll: false,
|
||||
fixedColumnCount: 0,
|
||||
fixedRowCount: 0,
|
||||
scrollToColumn: -1,
|
||||
scrollToRow: -1,
|
||||
style: {},
|
||||
styleBottomLeftGrid: {},
|
||||
styleBottomRightGrid: {},
|
||||
styleTopLeftGrid: {},
|
||||
styleTopRightGrid: {},
|
||||
}
|
||||
|
||||
public static getDerivedStateFromProps(nextProps, prevState) {
|
||||
if (
|
||||
nextProps.scrollLeft !== prevState.scrollLeft ||
|
||||
nextProps.scrollTop !== prevState.scrollTop
|
||||
) {
|
||||
return {
|
||||
scrollLeft:
|
||||
nextProps.scrollLeft != null && nextProps.scrollLeft >= 0
|
||||
? nextProps.scrollLeft
|
||||
: prevState.scrollLeft,
|
||||
scrollTop:
|
||||
nextProps.scrollTop != null && nextProps.scrollTop >= 0
|
||||
? nextProps.scrollTop
|
||||
: prevState.scrollTop,
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
private deferredInvalidateColumnIndex: number = 0
|
||||
private deferredInvalidateRowIndex: number = 0
|
||||
private bottomLeftGrid: Grid
|
||||
private bottomRightGrid: Grid
|
||||
private topLeftGrid: Grid
|
||||
private topRightGrid: Grid
|
||||
private deferredMeasurementCacheBottomLeftGrid: CellMeasurerCacheDecorator
|
||||
private deferredMeasurementCacheBottomRightGrid: CellMeasurerCacheDecorator
|
||||
private deferredMeasurementCacheTopRightGrid: CellMeasurerCacheDecorator
|
||||
private leftGridWidth: number | null = 0
|
||||
private topGridHeight: number | null = 0
|
||||
private lastRenderedColumnWidth: (arg: object) => {} | number
|
||||
private lastRenderedFixedColumnCount: number = 0
|
||||
private lastRenderedFixedRowCount: number = 0
|
||||
private lastRenderedRowHeight: (arg: {index: number}) => {} | number
|
||||
private bottomRightGridStyle: object | null
|
||||
private topRightGridStyle: object | null
|
||||
private lastRenderedStyle: object | null
|
||||
private lastRenderedHeight: number = 0
|
||||
private lastRenderedWidth: number = 0
|
||||
private containerTopStyle: object | null
|
||||
private containerBottomStyle: object | null
|
||||
private containerOuterStyle: object | null
|
||||
private lastRenderedStyleBottomLeftGrid: object | null
|
||||
private lastRenderedStyleBottomRightGrid: object | null
|
||||
private lastRenderedStyleTopLeftGrid: object | null
|
||||
private lastRenderedStyleTopRightGrid: object | null
|
||||
private bottomLeftGridStyle: object | null
|
||||
private topLeftGridStyle: object | null
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context)
|
||||
|
||||
this.state = {
|
||||
scrollLeft: 0,
|
||||
scrollTop: 0,
|
||||
scrollbarSize: 0,
|
||||
showHorizontalScrollbar: false,
|
||||
showVerticalScrollbar: false,
|
||||
}
|
||||
|
||||
const {deferredMeasurementCache, fixedColumnCount, fixedRowCount} = props
|
||||
|
||||
this.maybeCalculateCachedStyles(true)
|
||||
|
||||
if (deferredMeasurementCache) {
|
||||
this.deferredMeasurementCacheBottomLeftGrid =
|
||||
fixedRowCount > 0
|
||||
? new CellMeasurerCacheDecorator({
|
||||
cellMeasurerCache: deferredMeasurementCache,
|
||||
columnIndexOffset: 0,
|
||||
rowIndexOffset: fixedRowCount,
|
||||
})
|
||||
: deferredMeasurementCache
|
||||
|
||||
this.deferredMeasurementCacheBottomRightGrid =
|
||||
fixedColumnCount > 0 || fixedRowCount > 0
|
||||
? new CellMeasurerCacheDecorator({
|
||||
cellMeasurerCache: deferredMeasurementCache,
|
||||
columnIndexOffset: fixedColumnCount,
|
||||
rowIndexOffset: fixedRowCount,
|
||||
})
|
||||
: deferredMeasurementCache
|
||||
|
||||
this.deferredMeasurementCacheTopRightGrid =
|
||||
fixedColumnCount > 0
|
||||
? new CellMeasurerCacheDecorator({
|
||||
cellMeasurerCache: deferredMeasurementCache,
|
||||
columnIndexOffset: fixedColumnCount,
|
||||
rowIndexOffset: 0,
|
||||
})
|
||||
: deferredMeasurementCache
|
||||
}
|
||||
}
|
||||
|
||||
public forceUpdateGrids() {
|
||||
if (this.bottomLeftGrid) {
|
||||
this.bottomLeftGrid.forceUpdate()
|
||||
}
|
||||
if (this.bottomRightGrid) {
|
||||
this.bottomRightGrid.forceUpdate()
|
||||
}
|
||||
if (this.topLeftGrid) {
|
||||
this.topLeftGrid.forceUpdate()
|
||||
}
|
||||
if (this.topRightGrid) {
|
||||
this.topRightGrid.forceUpdate()
|
||||
}
|
||||
}
|
||||
|
||||
/** See Grid#invalidateCellSizeAfterRender */
|
||||
public invalidateCellSizeAfterRender({columnIndex = 0, rowIndex = 0} = {}) {
|
||||
this.deferredInvalidateColumnIndex =
|
||||
typeof this.deferredInvalidateColumnIndex === 'number'
|
||||
? Math.min(this.deferredInvalidateColumnIndex, columnIndex)
|
||||
: columnIndex
|
||||
this.deferredInvalidateRowIndex =
|
||||
typeof this.deferredInvalidateRowIndex === 'number'
|
||||
? Math.min(this.deferredInvalidateRowIndex, rowIndex)
|
||||
: rowIndex
|
||||
}
|
||||
|
||||
/** See Grid#measureAllCells */
|
||||
public measureAllCells() {
|
||||
if (this.bottomLeftGrid) {
|
||||
this.bottomLeftGrid.measureAllCells()
|
||||
}
|
||||
if (this.bottomRightGrid) {
|
||||
this.bottomRightGrid.measureAllCells()
|
||||
}
|
||||
if (this.topLeftGrid) {
|
||||
this.topLeftGrid.measureAllCells()
|
||||
}
|
||||
if (this.topRightGrid) {
|
||||
this.topRightGrid.measureAllCells()
|
||||
}
|
||||
}
|
||||
|
||||
public recomputeGridSize({columnIndex = 0, rowIndex = 0} = {}) {
|
||||
const {fixedColumnCount, fixedRowCount} = this.props
|
||||
|
||||
const adjustedColumnIndex = Math.max(0, columnIndex - fixedColumnCount)
|
||||
const adjustedRowIndex = Math.max(0, rowIndex - fixedRowCount)
|
||||
|
||||
if (this.bottomLeftGrid) {
|
||||
this.bottomLeftGrid.recomputeGridSize({
|
||||
columnIndex,
|
||||
rowIndex: adjustedRowIndex,
|
||||
})
|
||||
}
|
||||
if (this.bottomRightGrid) {
|
||||
this.bottomRightGrid.recomputeGridSize({
|
||||
columnIndex: adjustedColumnIndex,
|
||||
rowIndex: adjustedRowIndex,
|
||||
})
|
||||
}
|
||||
|
||||
if (this.topLeftGrid) {
|
||||
this.topLeftGrid.recomputeGridSize({
|
||||
columnIndex,
|
||||
rowIndex,
|
||||
})
|
||||
}
|
||||
|
||||
if (this.topRightGrid) {
|
||||
this.topRightGrid.recomputeGridSize({
|
||||
columnIndex: adjustedColumnIndex,
|
||||
rowIndex,
|
||||
})
|
||||
}
|
||||
|
||||
this.leftGridWidth = null
|
||||
this.topGridHeight = null
|
||||
this.maybeCalculateCachedStyles(true)
|
||||
}
|
||||
|
||||
public componentDidMount() {
|
||||
const {scrollLeft, scrollTop} = this.props
|
||||
|
||||
if (scrollLeft > 0 || scrollTop > 0) {
|
||||
const newState: Partial<State> = {}
|
||||
|
||||
if (scrollLeft > 0) {
|
||||
newState.scrollLeft = scrollLeft
|
||||
}
|
||||
|
||||
if (scrollTop > 0) {
|
||||
newState.scrollTop = scrollTop
|
||||
}
|
||||
|
||||
this.setState({...this.state, ...newState})
|
||||
}
|
||||
this.handleInvalidatedGridSize()
|
||||
}
|
||||
|
||||
public componentDidUpdate() {
|
||||
this.handleInvalidatedGridSize()
|
||||
}
|
||||
|
||||
public render() {
|
||||
const {
|
||||
onScroll,
|
||||
scrollLeft: scrollLeftProp, // eslint-disable-line no-unused-vars
|
||||
onSectionRendered,
|
||||
scrollToRow,
|
||||
scrollToColumn,
|
||||
scrollTop: scrollTopProp, // eslint-disable-line no-unused-vars
|
||||
...rest
|
||||
} = this.props
|
||||
|
||||
this.prepareForRender()
|
||||
|
||||
// Don't render any of our Grids if there are no cells.
|
||||
// This mirrors what Grid does,
|
||||
// And prevents us from recording inaccurage measurements when used with CellMeasurer.
|
||||
if (this.props.width === 0 || this.props.height === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
// scrollTop and scrollLeft props are explicitly filtered out and ignored
|
||||
|
||||
const {scrollLeft, scrollTop} = this.state
|
||||
|
||||
return (
|
||||
<div style={this.containerOuterStyle}>
|
||||
<div style={this.containerTopStyle}>
|
||||
{this.renderTopLeftGrid(rest)}
|
||||
{this.renderTopRightGrid({
|
||||
...rest,
|
||||
...onScroll,
|
||||
scrollLeft,
|
||||
})}
|
||||
</div>
|
||||
<div style={this.containerBottomStyle}>
|
||||
{this.renderBottomLeftGrid({
|
||||
...rest,
|
||||
onScroll,
|
||||
scrollTop,
|
||||
})}
|
||||
{this.renderBottomRightGrid({
|
||||
...rest,
|
||||
onScroll,
|
||||
onSectionRendered,
|
||||
scrollLeft,
|
||||
scrollToColumn,
|
||||
scrollToRow,
|
||||
scrollTop,
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
public cellRendererBottomLeftGrid = ({
|
||||
rowIndex,
|
||||
...rest
|
||||
}: Partial<Props> & {rowIndex: number; key: string}): JSX.Element => {
|
||||
const {cellRenderer, fixedRowCount, rowCount} = this.props
|
||||
|
||||
if (rowIndex === rowCount - fixedRowCount) {
|
||||
return (
|
||||
<div
|
||||
key={rest.key}
|
||||
style={{
|
||||
...rest.style,
|
||||
height: SCROLLBAR_SIZE_BUFFER,
|
||||
}}
|
||||
/>
|
||||
)
|
||||
} else {
|
||||
return cellRenderer({
|
||||
...rest,
|
||||
parent: this,
|
||||
rowIndex: rowIndex + fixedRowCount,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
private getBottomGridHeight(props) {
|
||||
const {height} = props
|
||||
|
||||
const topGridHeight = this.getTopGridHeight(props)
|
||||
|
||||
return height - topGridHeight
|
||||
}
|
||||
|
||||
private getLeftGridWidth(props) {
|
||||
const {fixedColumnCount, columnWidth} = props
|
||||
|
||||
if (this.leftGridWidth == null) {
|
||||
if (typeof columnWidth === 'function') {
|
||||
let leftGridWidth = 0
|
||||
|
||||
for (let index = 0; index < fixedColumnCount; index++) {
|
||||
leftGridWidth += columnWidth({index})
|
||||
}
|
||||
|
||||
this.leftGridWidth = leftGridWidth
|
||||
} else {
|
||||
this.leftGridWidth = columnWidth * fixedColumnCount
|
||||
}
|
||||
}
|
||||
|
||||
return this.leftGridWidth
|
||||
}
|
||||
|
||||
private getRightGridWidth(props) {
|
||||
const {width} = props
|
||||
|
||||
const leftGridWidth = this.getLeftGridWidth(props)
|
||||
const result = width - leftGridWidth
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
private getTopGridHeight(props) {
|
||||
const {fixedRowCount, rowHeight} = props
|
||||
|
||||
if (this.topGridHeight == null) {
|
||||
if (typeof rowHeight === 'function') {
|
||||
let topGridHeight = 0
|
||||
|
||||
for (let index = 0; index < fixedRowCount; index++) {
|
||||
topGridHeight += rowHeight({index})
|
||||
}
|
||||
|
||||
this.topGridHeight = topGridHeight
|
||||
} else {
|
||||
this.topGridHeight = rowHeight * fixedRowCount
|
||||
}
|
||||
}
|
||||
|
||||
return this.topGridHeight
|
||||
}
|
||||
|
||||
private onScrollbarsScroll = (e: React.MouseEvent<JSX.Element>) => {
|
||||
const {target} = e
|
||||
this.onScroll(target)
|
||||
}
|
||||
|
||||
private onScroll = scrollInfo => {
|
||||
const {scrollLeft, scrollTop} = scrollInfo
|
||||
this.setState({
|
||||
scrollLeft,
|
||||
scrollTop,
|
||||
})
|
||||
|
||||
const {onScroll} = this.props
|
||||
if (onScroll) {
|
||||
onScroll(scrollInfo)
|
||||
}
|
||||
}
|
||||
|
||||
private onScrollLeft = scrollInfo => {
|
||||
const {scrollLeft} = scrollInfo
|
||||
this.onScroll({
|
||||
scrollLeft,
|
||||
scrollTop: this.state.scrollTop,
|
||||
})
|
||||
}
|
||||
|
||||
private renderBottomLeftGrid(props) {
|
||||
const {fixedColumnCount, fixedRowCount, rowCount} = props
|
||||
|
||||
if (!fixedColumnCount) {
|
||||
return null
|
||||
}
|
||||
|
||||
const width = this.getLeftGridWidth(props)
|
||||
const height = this.getBottomGridHeight(props)
|
||||
|
||||
return (
|
||||
<Grid
|
||||
{...props}
|
||||
cellRenderer={this.cellRendererBottomLeftGrid}
|
||||
className={this.props.classNameBottomLeftGrid}
|
||||
columnCount={fixedColumnCount}
|
||||
deferredMeasurementCache={this.deferredMeasurementCacheBottomLeftGrid}
|
||||
onScroll={this.onScroll}
|
||||
height={height}
|
||||
ref={this.bottomLeftGridRef}
|
||||
rowCount={Math.max(0, rowCount - fixedRowCount)}
|
||||
rowHeight={this.rowHeightBottomGrid}
|
||||
style={{
|
||||
...this.bottomLeftGridStyle,
|
||||
}}
|
||||
tabIndex={null}
|
||||
width={width}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
private renderBottomRightGrid(props) {
|
||||
const {
|
||||
columnCount,
|
||||
fixedColumnCount,
|
||||
fixedRowCount,
|
||||
rowCount,
|
||||
scrollToColumn,
|
||||
scrollToRow,
|
||||
} = props
|
||||
|
||||
const width = this.getRightGridWidth(props)
|
||||
const height = this.getBottomGridHeight(props)
|
||||
|
||||
return (
|
||||
<FancyScrollbar
|
||||
style={{...this.bottomRightGridStyle, width, height}}
|
||||
autoHide={true}
|
||||
scrollTop={this.state.scrollTop}
|
||||
scrollLeft={this.state.scrollLeft}
|
||||
setScrollTop={this.onScrollbarsScroll}
|
||||
>
|
||||
<Grid
|
||||
{...props}
|
||||
cellRenderer={this.cellRendererBottomRightGrid}
|
||||
className={this.props.classNameBottomRightGrid}
|
||||
columnCount={Math.max(0, columnCount - fixedColumnCount)}
|
||||
columnWidth={this.columnWidthRightGrid}
|
||||
deferredMeasurementCache={
|
||||
this.deferredMeasurementCacheBottomRightGrid
|
||||
}
|
||||
height={height}
|
||||
ref={this.bottomRightGridRef}
|
||||
rowCount={Math.max(0, rowCount - fixedRowCount)}
|
||||
rowHeight={this.rowHeightBottomGrid}
|
||||
onScroll={this.onScroll}
|
||||
scrollToColumn={scrollToColumn - fixedColumnCount}
|
||||
scrollToRow={scrollToRow - fixedRowCount}
|
||||
style={{
|
||||
...this.bottomRightGridStyle,
|
||||
overflowX: false,
|
||||
overflowY: true,
|
||||
left: 0,
|
||||
}}
|
||||
width={width}
|
||||
/>
|
||||
</FancyScrollbar>
|
||||
)
|
||||
}
|
||||
|
||||
private renderTopLeftGrid(props) {
|
||||
const {fixedColumnCount, fixedRowCount} = props
|
||||
|
||||
if (!fixedColumnCount || !fixedRowCount) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<Grid
|
||||
{...props}
|
||||
className={this.props.classNameTopLeftGrid}
|
||||
columnCount={fixedColumnCount}
|
||||
height={this.getTopGridHeight(props)}
|
||||
ref={this.topLeftGridRef}
|
||||
rowCount={fixedRowCount}
|
||||
style={this.topLeftGridStyle}
|
||||
tabIndex={null}
|
||||
width={this.getLeftGridWidth(props)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
private renderTopRightGrid(props) {
|
||||
const {
|
||||
columnCount,
|
||||
enableFixedRowScroll,
|
||||
fixedColumnCount,
|
||||
fixedRowCount,
|
||||
scrollLeft,
|
||||
} = props
|
||||
|
||||
if (!fixedRowCount) {
|
||||
return null
|
||||
}
|
||||
|
||||
const width = this.getRightGridWidth(props)
|
||||
const height = this.getTopGridHeight(props)
|
||||
|
||||
return (
|
||||
<Grid
|
||||
{...props}
|
||||
cellRenderer={this.cellRendererTopRightGrid}
|
||||
className={this.props.classNameTopRightGrid}
|
||||
columnCount={Math.max(0, columnCount - fixedColumnCount)}
|
||||
columnWidth={this.columnWidthRightGrid}
|
||||
deferredMeasurementCache={this.deferredMeasurementCacheTopRightGrid}
|
||||
height={height}
|
||||
onScroll={enableFixedRowScroll ? this.onScrollLeft : undefined}
|
||||
ref={this.topRightGridRef}
|
||||
rowCount={fixedRowCount}
|
||||
scrollLeft={scrollLeft}
|
||||
style={this.topRightGridStyle}
|
||||
tabIndex={null}
|
||||
width={width}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
private rowHeightBottomGrid = ({index}) => {
|
||||
const {fixedRowCount, rowCount, rowHeight} = this.props
|
||||
const {scrollbarSize, showVerticalScrollbar} = this.state
|
||||
|
||||
// An extra cell is added to the count
|
||||
// This gives the smaller Grid extra room for offset,
|
||||
// In case the main (bottom right) Grid has a scrollbar
|
||||
// If no scrollbar, the extra space is overflow:hidden anyway
|
||||
if (showVerticalScrollbar && index === rowCount - fixedRowCount) {
|
||||
return scrollbarSize
|
||||
}
|
||||
|
||||
return typeof rowHeight === 'function'
|
||||
? rowHeight({index: index + fixedRowCount})
|
||||
: rowHeight
|
||||
}
|
||||
|
||||
private topLeftGridRef = ref => {
|
||||
this.topLeftGrid = ref
|
||||
}
|
||||
|
||||
private topRightGridRef = ref => {
|
||||
this.topRightGrid = ref
|
||||
}
|
||||
|
||||
/**
|
||||
* Avoid recreating inline styles each render; this bypasses Grid's shallowCompare.
|
||||
* This method recalculates styles only when specific props change.
|
||||
*/
|
||||
private maybeCalculateCachedStyles(resetAll) {
|
||||
const {
|
||||
columnWidth,
|
||||
height,
|
||||
fixedColumnCount,
|
||||
fixedRowCount,
|
||||
rowHeight,
|
||||
style,
|
||||
styleBottomLeftGrid,
|
||||
styleBottomRightGrid,
|
||||
styleTopLeftGrid,
|
||||
styleTopRightGrid,
|
||||
width,
|
||||
} = this.props
|
||||
|
||||
const sizeChange =
|
||||
resetAll ||
|
||||
height !== this.lastRenderedHeight ||
|
||||
width !== this.lastRenderedWidth
|
||||
const leftSizeChange =
|
||||
resetAll ||
|
||||
columnWidth !== this.lastRenderedColumnWidth ||
|
||||
fixedColumnCount !== this.lastRenderedFixedColumnCount
|
||||
const topSizeChange =
|
||||
resetAll ||
|
||||
fixedRowCount !== this.lastRenderedFixedRowCount ||
|
||||
rowHeight !== this.lastRenderedRowHeight
|
||||
|
||||
if (resetAll || sizeChange || style !== this.lastRenderedStyle) {
|
||||
this.containerOuterStyle = {
|
||||
height,
|
||||
overflow: 'visible', // Let :focus outline show through
|
||||
width,
|
||||
...style,
|
||||
}
|
||||
}
|
||||
|
||||
if (resetAll || sizeChange || topSizeChange) {
|
||||
this.containerTopStyle = {
|
||||
height: this.getTopGridHeight(this.props),
|
||||
position: 'relative',
|
||||
width,
|
||||
}
|
||||
|
||||
this.containerBottomStyle = {
|
||||
height: height - this.getTopGridHeight(this.props),
|
||||
overflow: 'visible', // Let :focus outline show through
|
||||
position: 'relative',
|
||||
width,
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
resetAll ||
|
||||
styleBottomLeftGrid !== this.lastRenderedStyleBottomLeftGrid
|
||||
) {
|
||||
this.bottomLeftGridStyle = {
|
||||
left: 0,
|
||||
overflowY: 'hidden',
|
||||
overflowX: 'hidden',
|
||||
position: 'absolute',
|
||||
...styleBottomLeftGrid,
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
resetAll ||
|
||||
leftSizeChange ||
|
||||
styleBottomRightGrid !== this.lastRenderedStyleBottomRightGrid
|
||||
) {
|
||||
this.bottomRightGridStyle = {
|
||||
left: this.getLeftGridWidth(this.props),
|
||||
position: 'absolute',
|
||||
...styleBottomRightGrid,
|
||||
}
|
||||
}
|
||||
|
||||
if (resetAll || styleTopLeftGrid !== this.lastRenderedStyleTopLeftGrid) {
|
||||
this.topLeftGridStyle = {
|
||||
left: 0,
|
||||
overflowX: 'hidden',
|
||||
overflowY: 'hidden',
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
...styleTopLeftGrid,
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
resetAll ||
|
||||
leftSizeChange ||
|
||||
styleTopRightGrid !== this.lastRenderedStyleTopRightGrid
|
||||
) {
|
||||
this.topRightGridStyle = {
|
||||
left: this.getLeftGridWidth(this.props),
|
||||
overflowX: 'hidden',
|
||||
overflowY: 'hidden',
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
...styleTopRightGrid,
|
||||
}
|
||||
}
|
||||
|
||||
this.lastRenderedColumnWidth = columnWidth
|
||||
this.lastRenderedFixedColumnCount = fixedColumnCount
|
||||
this.lastRenderedFixedRowCount = fixedRowCount
|
||||
this.lastRenderedHeight = height
|
||||
this.lastRenderedRowHeight = rowHeight
|
||||
this.lastRenderedStyle = style
|
||||
this.lastRenderedStyleBottomLeftGrid = styleBottomLeftGrid
|
||||
this.lastRenderedStyleBottomRightGrid = styleBottomRightGrid
|
||||
this.lastRenderedStyleTopLeftGrid = styleTopLeftGrid
|
||||
this.lastRenderedStyleTopRightGrid = styleTopRightGrid
|
||||
this.lastRenderedWidth = width
|
||||
}
|
||||
|
||||
private bottomLeftGridRef = ref => {
|
||||
this.bottomLeftGrid = ref
|
||||
}
|
||||
|
||||
private bottomRightGridRef = ref => {
|
||||
this.bottomRightGrid = ref
|
||||
}
|
||||
|
||||
private cellRendererBottomRightGrid = ({columnIndex, rowIndex, ...rest}) => {
|
||||
const {cellRenderer, fixedColumnCount, fixedRowCount} = this.props
|
||||
|
||||
return cellRenderer({
|
||||
...rest,
|
||||
columnIndex: columnIndex + fixedColumnCount,
|
||||
parent: this,
|
||||
rowIndex: rowIndex + fixedRowCount,
|
||||
})
|
||||
}
|
||||
|
||||
private cellRendererTopRightGrid = ({columnIndex, ...rest}) => {
|
||||
const {cellRenderer, columnCount, fixedColumnCount} = this.props
|
||||
|
||||
if (columnIndex === columnCount - fixedColumnCount) {
|
||||
return (
|
||||
<div
|
||||
key={rest.key}
|
||||
style={{
|
||||
...rest.style,
|
||||
width: SCROLLBAR_SIZE_BUFFER,
|
||||
}}
|
||||
/>
|
||||
)
|
||||
} else {
|
||||
return cellRenderer({
|
||||
...rest,
|
||||
columnIndex: columnIndex + fixedColumnCount,
|
||||
parent: this,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
private columnWidthRightGrid = ({index}) => {
|
||||
const {columnCount, fixedColumnCount, columnWidth} = this.props
|
||||
const {scrollbarSize, showHorizontalScrollbar} = this.state
|
||||
|
||||
// An extra cell is added to the count
|
||||
// This gives the smaller Grid extra room for offset,
|
||||
// In case the main (bottom right) Grid has a scrollbar
|
||||
// If no scrollbar, the extra space is overflow:hidden anyway
|
||||
if (showHorizontalScrollbar && index === columnCount - fixedColumnCount) {
|
||||
return scrollbarSize
|
||||
}
|
||||
|
||||
return typeof columnWidth === 'function'
|
||||
? columnWidth({index: index + fixedColumnCount})
|
||||
: columnWidth
|
||||
}
|
||||
|
||||
private handleInvalidatedGridSize() {
|
||||
if (typeof this.deferredInvalidateColumnIndex === 'number') {
|
||||
const columnIndex = this.deferredInvalidateColumnIndex
|
||||
const rowIndex = this.deferredInvalidateRowIndex
|
||||
|
||||
this.deferredInvalidateColumnIndex = null
|
||||
this.deferredInvalidateRowIndex = null
|
||||
|
||||
this.recomputeGridSize({
|
||||
columnIndex,
|
||||
rowIndex,
|
||||
})
|
||||
this.forceUpdate()
|
||||
}
|
||||
}
|
||||
|
||||
private prepareForRender() {
|
||||
if (
|
||||
this.lastRenderedColumnWidth !== this.props.columnWidth ||
|
||||
this.lastRenderedFixedColumnCount !== this.props.fixedColumnCount
|
||||
) {
|
||||
this.leftGridWidth = null
|
||||
}
|
||||
|
||||
if (
|
||||
this.lastRenderedFixedRowCount !== this.props.fixedRowCount ||
|
||||
this.lastRenderedRowHeight !== this.props.rowHeight
|
||||
) {
|
||||
this.topGridHeight = null
|
||||
}
|
||||
|
||||
this.maybeCalculateCachedStyles(false)
|
||||
|
||||
this.lastRenderedColumnWidth = this.props.columnWidth
|
||||
this.lastRenderedFixedColumnCount = this.props.fixedColumnCount
|
||||
this.lastRenderedFixedRowCount = this.props.fixedRowCount
|
||||
this.lastRenderedRowHeight = this.props.rowHeight
|
||||
}
|
||||
}
|
||||
|
||||
export default MultiGrid
|
|
@ -0,0 +1,2 @@
|
|||
import MultiGrid from './MultiGrid'
|
||||
export {MultiGrid}
|
|
@ -118,6 +118,7 @@ const RefreshingGraph = ({
|
|||
decimalPlaces={decimalPlaces}
|
||||
editQueryStatus={editQueryStatus}
|
||||
resizerTopHeight={resizerTopHeight}
|
||||
grabDataForDownload={grabDataForDownload}
|
||||
handleSetHoverTime={handleSetHoverTime}
|
||||
isInCEO={isInCEO}
|
||||
/>
|
||||
|
|
|
@ -0,0 +1,209 @@
|
|||
import React, {PureComponent, ReactElement, MouseEvent} from 'react'
|
||||
import classnames from 'classnames'
|
||||
import calculateSize from 'calculate-size'
|
||||
|
||||
import {HANDLE_VERTICAL, HANDLE_HORIZONTAL} from 'src/shared/constants/index'
|
||||
|
||||
const NOOP = () => {}
|
||||
|
||||
interface Props {
|
||||
name?: string
|
||||
handleDisplay?: string
|
||||
handlePixels: number
|
||||
id: string
|
||||
size: number
|
||||
offset: number
|
||||
draggable: boolean
|
||||
orientation: string
|
||||
activeHandleID: string
|
||||
render: () => ReactElement<any>
|
||||
onHandleStartDrag: (id: string, e: MouseEvent<HTMLElement>) => void
|
||||
onDoubleClick: (id: string) => void
|
||||
}
|
||||
|
||||
class Division extends PureComponent<Props> {
|
||||
public static defaultProps: Partial<Props> = {
|
||||
name: '',
|
||||
handleDisplay: 'visible',
|
||||
}
|
||||
|
||||
private collapseThreshold: number = 0
|
||||
private containerRef: HTMLElement
|
||||
|
||||
public componentDidMount() {
|
||||
const {name} = this.props
|
||||
|
||||
if (!name) {
|
||||
return 0
|
||||
}
|
||||
|
||||
const {width} = calculateSize(name, {
|
||||
font: '"Roboto", Helvetica, Arial, Tahoma, Verdana, sans-serif',
|
||||
fontSize: '16px',
|
||||
fontWeight: '500',
|
||||
})
|
||||
const NAME_OFFSET = 66
|
||||
|
||||
this.collapseThreshold = width + NAME_OFFSET
|
||||
}
|
||||
|
||||
public render() {
|
||||
const {name, render, draggable} = this.props
|
||||
return (
|
||||
<div
|
||||
className={this.containerClass}
|
||||
style={this.containerStyle}
|
||||
ref={r => (this.containerRef = r)}
|
||||
>
|
||||
<div
|
||||
style={this.handleStyle}
|
||||
title={this.title}
|
||||
draggable={draggable}
|
||||
onDragStart={this.drag}
|
||||
className={this.handleClass}
|
||||
onDoubleClick={this.handleDoubleClick}
|
||||
>
|
||||
<div className={this.titleClass}>{name}</div>
|
||||
</div>
|
||||
<div className={this.contentsClass} style={this.contentStyle}>
|
||||
{name && <div className="threesizer--header" />}
|
||||
<div className="threesizer--body">{render()}</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
private get title() {
|
||||
return 'Drag to resize.\nDouble click to expand.'
|
||||
}
|
||||
|
||||
private get contentStyle() {
|
||||
if (this.props.orientation === HANDLE_HORIZONTAL) {
|
||||
return {
|
||||
height: `calc(100% - ${this.handlePixels}px)`,
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
width: `calc(100% - ${this.handlePixels}px)`,
|
||||
}
|
||||
}
|
||||
|
||||
private get handleStyle() {
|
||||
const {handleDisplay: display, orientation, handlePixels} = this.props
|
||||
|
||||
if (orientation === HANDLE_HORIZONTAL) {
|
||||
return {
|
||||
display,
|
||||
height: `${handlePixels}px`,
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
display,
|
||||
width: `${handlePixels}px`,
|
||||
}
|
||||
}
|
||||
|
||||
private get containerStyle() {
|
||||
if (this.props.orientation === HANDLE_HORIZONTAL) {
|
||||
return {
|
||||
height: this.size,
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
width: this.size,
|
||||
}
|
||||
}
|
||||
|
||||
private get size(): string {
|
||||
const {size, offset} = this.props
|
||||
return `calc((100% - ${offset}px) * ${size} + ${this.handlePixels}px)`
|
||||
}
|
||||
|
||||
private get handlePixels(): number {
|
||||
if (this.props.handleDisplay === 'none') {
|
||||
return 0
|
||||
}
|
||||
|
||||
return this.props.handlePixels
|
||||
}
|
||||
|
||||
private get containerClass(): string {
|
||||
const {orientation} = this.props
|
||||
const isAnyHandleBeingDragged = !!this.props.activeHandleID
|
||||
return classnames('threesizer--division', {
|
||||
dragging: isAnyHandleBeingDragged,
|
||||
vertical: orientation === HANDLE_VERTICAL,
|
||||
horizontal: orientation === HANDLE_HORIZONTAL,
|
||||
})
|
||||
}
|
||||
|
||||
private get handleClass(): string {
|
||||
const {draggable, orientation} = this.props
|
||||
|
||||
return classnames('threesizer--handle', {
|
||||
disabled: !draggable,
|
||||
dragging: this.isDragging,
|
||||
vertical: orientation === HANDLE_VERTICAL,
|
||||
horizontal: orientation === HANDLE_HORIZONTAL,
|
||||
})
|
||||
}
|
||||
|
||||
private get contentsClass(): string {
|
||||
const {orientation, size} = this.props
|
||||
return classnames(`threesizer--contents ${orientation}`, {
|
||||
'no-shadows': !size,
|
||||
})
|
||||
}
|
||||
|
||||
private get titleClass(): string {
|
||||
const {orientation} = this.props
|
||||
|
||||
const collapsed = orientation === HANDLE_VERTICAL && this.isTitleObscured
|
||||
|
||||
return classnames('threesizer--title', {
|
||||
'threesizer--collapsed': collapsed,
|
||||
vertical: orientation === HANDLE_VERTICAL,
|
||||
horizontal: orientation === HANDLE_HORIZONTAL,
|
||||
})
|
||||
}
|
||||
|
||||
private get isTitleObscured(): boolean {
|
||||
if (this.props.size === 0) {
|
||||
return true
|
||||
}
|
||||
|
||||
if (!this.containerRef || this.props.size >= 0.33) {
|
||||
return false
|
||||
}
|
||||
|
||||
const {width} = this.containerRef.getBoundingClientRect()
|
||||
|
||||
return width <= this.collapseThreshold
|
||||
}
|
||||
|
||||
private get isDragging(): boolean {
|
||||
const {id, activeHandleID} = this.props
|
||||
return id === activeHandleID
|
||||
}
|
||||
|
||||
private drag = e => {
|
||||
const {draggable, id} = this.props
|
||||
|
||||
if (!draggable) {
|
||||
return NOOP
|
||||
}
|
||||
|
||||
this.props.onHandleStartDrag(id, e)
|
||||
}
|
||||
|
||||
private handleDoubleClick = () => {
|
||||
const {onDoubleClick, id} = this.props
|
||||
|
||||
onDoubleClick(id)
|
||||
}
|
||||
}
|
||||
|
||||
export default Division
|
|
@ -4,7 +4,8 @@ import _ from 'lodash'
|
|||
import classnames from 'classnames'
|
||||
import {connect} from 'react-redux'
|
||||
|
||||
import {MultiGrid, ColumnSizer} from 'react-virtualized'
|
||||
import {ColumnSizer} from 'react-virtualized'
|
||||
import {MultiGrid} from 'src/shared/components/MultiGrid'
|
||||
import {bindActionCreators} from 'redux'
|
||||
import moment from 'moment'
|
||||
import {reduce} from 'fast.js'
|
||||
|
|
|
@ -11,6 +11,7 @@ interface Item {
|
|||
|
||||
interface TagsProps {
|
||||
tags: Item[]
|
||||
confirmText?: string
|
||||
onDeleteTag?: (item: Item) => void
|
||||
addMenuItems?: Item[]
|
||||
addMenuChoose?: (item: Item) => void
|
||||
|
@ -21,11 +22,19 @@ const Tags: SFC<TagsProps> = ({
|
|||
onDeleteTag,
|
||||
addMenuItems,
|
||||
addMenuChoose,
|
||||
confirmText,
|
||||
}) => {
|
||||
return (
|
||||
<div className="input-tag-list">
|
||||
{tags.map(item => {
|
||||
return <Tag key={uuid.v4()} item={item} onDelete={onDeleteTag} />
|
||||
return (
|
||||
<Tag
|
||||
key={uuid.v4()}
|
||||
item={item}
|
||||
onDelete={onDeleteTag}
|
||||
confirmText={confirmText}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
{addMenuItems && addMenuItems.length && addMenuChoose ? (
|
||||
<TagsAddButton items={addMenuItems} onChoose={addMenuChoose} />
|
||||
|
@ -35,23 +44,28 @@ const Tags: SFC<TagsProps> = ({
|
|||
}
|
||||
|
||||
interface TagProps {
|
||||
confirmText?: string
|
||||
item: Item
|
||||
onDelete: (item: Item) => void
|
||||
}
|
||||
|
||||
@ErrorHandling
|
||||
class Tag extends PureComponent<TagProps> {
|
||||
public static defaultProps: Partial<TagProps> = {
|
||||
confirmText: 'Delete',
|
||||
}
|
||||
|
||||
public render() {
|
||||
const {item} = this.props
|
||||
const {item, confirmText} = this.props
|
||||
return (
|
||||
<span key={uuid.v4()} className="input-tag--item">
|
||||
<span>{item.text || item.name || item}</span>
|
||||
<ConfirmButton
|
||||
icon="remove"
|
||||
size="btn-xs"
|
||||
customClass="input-tag--remove"
|
||||
square={true}
|
||||
confirmText="Remove user from organization?"
|
||||
confirmText={confirmText}
|
||||
customClass="input-tag--remove"
|
||||
confirmAction={this.handleClickDelete(item)}
|
||||
/>
|
||||
</span>
|
||||
|
|
|
@ -0,0 +1,451 @@
|
|||
import React, {Component, ReactElement, MouseEvent} from 'react'
|
||||
import classnames from 'classnames'
|
||||
import uuid from 'uuid'
|
||||
import _ from 'lodash'
|
||||
|
||||
import ResizeDivision from 'src/shared/components/ResizeDivision'
|
||||
import {ErrorHandling} from 'src/shared/decorators/errors'
|
||||
import {
|
||||
HANDLE_NONE,
|
||||
HANDLE_PIXELS,
|
||||
HANDLE_HORIZONTAL,
|
||||
HANDLE_VERTICAL,
|
||||
MIN_SIZE,
|
||||
MAX_SIZE,
|
||||
} from 'src/shared/constants/'
|
||||
|
||||
const initialDragEvent = {
|
||||
percentX: 0,
|
||||
percentY: 0,
|
||||
mouseX: null,
|
||||
mouseY: null,
|
||||
}
|
||||
|
||||
interface State {
|
||||
activeHandleID: string
|
||||
divisions: DivisionState[]
|
||||
dragDirection: string
|
||||
dragEvent: any
|
||||
}
|
||||
|
||||
interface Division {
|
||||
name?: string
|
||||
handleDisplay?: string
|
||||
handlePixels?: number
|
||||
render: () => ReactElement<any>
|
||||
}
|
||||
|
||||
interface DivisionState extends Division {
|
||||
id: string
|
||||
size: number
|
||||
}
|
||||
|
||||
interface Props {
|
||||
divisions: Division[]
|
||||
orientation: string
|
||||
containerClass?: string
|
||||
}
|
||||
|
||||
@ErrorHandling
|
||||
class Threesizer extends Component<Props, State> {
|
||||
public static defaultProps: Partial<Props> = {
|
||||
orientation: HANDLE_HORIZONTAL,
|
||||
containerClass: '',
|
||||
}
|
||||
|
||||
private containerRef: HTMLElement
|
||||
private percentChangeX: number = 0
|
||||
private percentChangeY: number = 0
|
||||
|
||||
constructor(props) {
|
||||
super(props)
|
||||
this.state = {
|
||||
activeHandleID: null,
|
||||
divisions: this.initialDivisions,
|
||||
dragEvent: initialDragEvent,
|
||||
dragDirection: '',
|
||||
}
|
||||
}
|
||||
|
||||
public componentDidMount() {
|
||||
document.addEventListener('mouseup', this.handleStopDrag)
|
||||
document.addEventListener('mouseleave', this.handleStopDrag)
|
||||
}
|
||||
|
||||
public componentWillUnmount() {
|
||||
document.removeEventListener('mouseup', this.handleStopDrag)
|
||||
document.removeEventListener('mouseleave', this.handleStopDrag)
|
||||
}
|
||||
|
||||
public componentDidUpdate(__, prevState) {
|
||||
const {dragEvent} = this.state
|
||||
const {orientation} = this.props
|
||||
|
||||
if (_.isEqual(dragEvent, prevState.dragEvent)) {
|
||||
return
|
||||
}
|
||||
|
||||
this.percentChangeX = this.pixelsToPercentX(
|
||||
prevState.dragEvent.mouseX,
|
||||
dragEvent.mouseX
|
||||
)
|
||||
|
||||
this.percentChangeY = this.pixelsToPercentY(
|
||||
prevState.dragEvent.mouseY,
|
||||
dragEvent.mouseY
|
||||
)
|
||||
|
||||
const {percentX, percentY} = dragEvent
|
||||
const {dragEvent: prevDrag} = prevState
|
||||
|
||||
if (orientation === HANDLE_VERTICAL) {
|
||||
const left = percentX < prevDrag.percentX
|
||||
|
||||
if (left) {
|
||||
return this.move.left()
|
||||
}
|
||||
|
||||
return this.move.right()
|
||||
}
|
||||
|
||||
const up = percentY < prevDrag.percentY
|
||||
|
||||
if (up) {
|
||||
return this.move.up()
|
||||
}
|
||||
|
||||
return this.move.down()
|
||||
}
|
||||
|
||||
public render() {
|
||||
const {activeHandleID, divisions} = this.state
|
||||
const {orientation} = this.props
|
||||
|
||||
return (
|
||||
<div
|
||||
className={this.className}
|
||||
onMouseUp={this.handleStopDrag}
|
||||
onMouseMove={this.handleDrag}
|
||||
ref={r => (this.containerRef = r)}
|
||||
>
|
||||
{divisions.map((d, i) => (
|
||||
<ResizeDivision
|
||||
key={d.id}
|
||||
id={d.id}
|
||||
name={d.name}
|
||||
size={d.size}
|
||||
offset={this.offset}
|
||||
draggable={i > 0}
|
||||
orientation={orientation}
|
||||
handlePixels={d.handlePixels}
|
||||
handleDisplay={d.handleDisplay}
|
||||
activeHandleID={activeHandleID}
|
||||
onDoubleClick={this.handleDoubleClick}
|
||||
onHandleStartDrag={this.handleStartDrag}
|
||||
render={this.props.divisions[i].render}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
private get offset(): number {
|
||||
const handlesPixelCount = this.state.divisions.reduce((acc, d) => {
|
||||
if (d.handleDisplay === HANDLE_NONE) {
|
||||
return acc
|
||||
}
|
||||
|
||||
return acc + d.handlePixels
|
||||
}, 0)
|
||||
|
||||
return handlesPixelCount
|
||||
}
|
||||
|
||||
private get className(): string {
|
||||
const {orientation, containerClass} = this.props
|
||||
const {activeHandleID} = this.state
|
||||
|
||||
return classnames(`threesizer ${containerClass}`, {
|
||||
dragging: activeHandleID,
|
||||
horizontal: orientation === HANDLE_HORIZONTAL,
|
||||
vertical: orientation === HANDLE_VERTICAL,
|
||||
})
|
||||
}
|
||||
|
||||
private get initialDivisions() {
|
||||
const {divisions} = this.props
|
||||
|
||||
const size = 1 / divisions.length
|
||||
|
||||
return divisions.map(d => ({
|
||||
...d,
|
||||
id: uuid.v4(),
|
||||
size,
|
||||
handlePixels: d.handlePixels || HANDLE_PIXELS,
|
||||
}))
|
||||
}
|
||||
|
||||
private handleDoubleClick = (id: string): void => {
|
||||
const clickedDiv = this.state.divisions.find(d => d.id === id)
|
||||
|
||||
if (!clickedDiv) {
|
||||
return
|
||||
}
|
||||
|
||||
const isMaxed = clickedDiv.size === 1
|
||||
|
||||
if (isMaxed) {
|
||||
return this.equalize()
|
||||
}
|
||||
|
||||
const divisions = this.state.divisions.map(d => {
|
||||
if (d.id !== id) {
|
||||
return {...d, size: 0}
|
||||
}
|
||||
|
||||
return {...d, size: 1}
|
||||
})
|
||||
|
||||
this.setState({divisions})
|
||||
}
|
||||
|
||||
private equalize = () => {
|
||||
const denominator = this.state.divisions.length
|
||||
const divisions = this.state.divisions.map(d => {
|
||||
return {...d, size: 1 / denominator}
|
||||
})
|
||||
|
||||
this.setState({divisions})
|
||||
}
|
||||
|
||||
private handleStartDrag = (activeHandleID, e: MouseEvent<HTMLElement>) => {
|
||||
const dragEvent = this.mousePosWithinContainer(e)
|
||||
this.setState({activeHandleID, dragEvent})
|
||||
}
|
||||
|
||||
private handleStopDrag = () => {
|
||||
this.setState({activeHandleID: '', dragEvent: initialDragEvent})
|
||||
}
|
||||
|
||||
private mousePosWithinContainer = (e: MouseEvent<HTMLElement>) => {
|
||||
const {pageY, pageX} = e
|
||||
const {top, left, width, height} = this.containerRef.getBoundingClientRect()
|
||||
|
||||
const mouseX = pageX - left
|
||||
const mouseY = pageY - top
|
||||
|
||||
const percentX = mouseX / width
|
||||
const percentY = mouseY / height
|
||||
|
||||
return {
|
||||
mouseX,
|
||||
mouseY,
|
||||
percentX,
|
||||
percentY,
|
||||
}
|
||||
}
|
||||
|
||||
private pixelsToPercentX = (startValue, endValue) => {
|
||||
if (!startValue || !endValue) {
|
||||
return 0
|
||||
}
|
||||
|
||||
const delta = Math.abs(startValue - endValue)
|
||||
const {width} = this.containerRef.getBoundingClientRect()
|
||||
|
||||
return delta / width
|
||||
}
|
||||
|
||||
private pixelsToPercentY = (startValue, endValue) => {
|
||||
if (!startValue || !endValue) {
|
||||
return 0
|
||||
}
|
||||
|
||||
const delta = startValue - endValue
|
||||
const {height} = this.containerRef.getBoundingClientRect()
|
||||
|
||||
return Math.abs(delta / height)
|
||||
}
|
||||
|
||||
private handleDrag = (e: MouseEvent<HTMLElement>) => {
|
||||
const {activeHandleID} = this.state
|
||||
if (!activeHandleID) {
|
||||
return
|
||||
}
|
||||
|
||||
const dragEvent = this.mousePosWithinContainer(e)
|
||||
this.setState({dragEvent})
|
||||
}
|
||||
|
||||
private get move() {
|
||||
const {activeHandleID} = this.state
|
||||
|
||||
const activePosition = _.findIndex(
|
||||
this.state.divisions,
|
||||
d => d.id === activeHandleID
|
||||
)
|
||||
|
||||
return {
|
||||
up: this.up(activePosition),
|
||||
down: this.down(activePosition),
|
||||
left: this.left(activePosition),
|
||||
right: this.right(activePosition),
|
||||
}
|
||||
}
|
||||
|
||||
private up = activePosition => () => {
|
||||
const divisions = this.state.divisions.map((d, i) => {
|
||||
if (!activePosition) {
|
||||
return d
|
||||
}
|
||||
|
||||
const first = i === 0
|
||||
const before = i === activePosition - 1
|
||||
const current = i === activePosition
|
||||
|
||||
if (first && !before) {
|
||||
const second = this.state.divisions[1]
|
||||
if (second.size === 0) {
|
||||
return {...d, size: this.shorter(d.size)}
|
||||
}
|
||||
|
||||
return {...d}
|
||||
}
|
||||
|
||||
if (before) {
|
||||
return {...d, size: this.shorter(d.size)}
|
||||
}
|
||||
|
||||
if (current) {
|
||||
return {...d, size: this.taller(d.size)}
|
||||
}
|
||||
|
||||
return {...d}
|
||||
})
|
||||
|
||||
this.setState({divisions})
|
||||
}
|
||||
|
||||
private left = activePosition => () => {
|
||||
const divisions = this.state.divisions.map((d, i) => {
|
||||
if (!activePosition) {
|
||||
return d
|
||||
}
|
||||
|
||||
const first = i === 0
|
||||
const before = i === activePosition - 1
|
||||
const active = i === activePosition
|
||||
|
||||
if (first && !before) {
|
||||
const second = this.state.divisions[1]
|
||||
if (second.size === 0) {
|
||||
return {...d, size: this.thinner(d.size)}
|
||||
}
|
||||
|
||||
return {...d}
|
||||
}
|
||||
|
||||
if (before) {
|
||||
return {...d, size: this.thinner(d.size)}
|
||||
}
|
||||
|
||||
if (active) {
|
||||
return {...d, size: this.fatter(d.size)}
|
||||
}
|
||||
|
||||
return {...d}
|
||||
})
|
||||
|
||||
this.setState({divisions})
|
||||
}
|
||||
|
||||
private right = activePosition => () => {
|
||||
const divisions = this.state.divisions.map((d, i, divs) => {
|
||||
const before = i === activePosition - 1
|
||||
const active = i === activePosition
|
||||
const after = i === activePosition + 1
|
||||
|
||||
if (before) {
|
||||
return {...d, size: this.fatter(d.size)}
|
||||
}
|
||||
|
||||
if (active) {
|
||||
return {...d, size: this.thinner(d.size)}
|
||||
}
|
||||
|
||||
if (after) {
|
||||
const leftIndex = i - 1
|
||||
const left = _.get(divs, leftIndex, {size: 'none'})
|
||||
|
||||
if (left.size === 0) {
|
||||
return {...d, size: this.thinner(d.size)}
|
||||
}
|
||||
|
||||
return {...d}
|
||||
}
|
||||
|
||||
return {...d}
|
||||
})
|
||||
|
||||
this.setState({divisions})
|
||||
}
|
||||
|
||||
private down = activePosition => () => {
|
||||
const divisions = this.state.divisions.map((d, i, divs) => {
|
||||
const before = i === activePosition - 1
|
||||
const current = i === activePosition
|
||||
const after = i === activePosition + 1
|
||||
|
||||
if (before) {
|
||||
return {...d, size: this.taller(d.size)}
|
||||
}
|
||||
|
||||
if (current) {
|
||||
return {...d, size: this.shorter(d.size)}
|
||||
}
|
||||
|
||||
if (after) {
|
||||
const above = divs[i - 1]
|
||||
if (above.size === 0) {
|
||||
return {...d, size: this.shorter(d.size)}
|
||||
}
|
||||
|
||||
return {...d}
|
||||
}
|
||||
|
||||
return {...d}
|
||||
})
|
||||
|
||||
this.setState({divisions})
|
||||
}
|
||||
|
||||
private taller = (size: number): number => {
|
||||
const newSize = size + this.percentChangeY
|
||||
return this.enforceMax(newSize)
|
||||
}
|
||||
|
||||
private fatter = (size: number): number => {
|
||||
const newSize = size + this.percentChangeX
|
||||
return this.enforceMax(newSize)
|
||||
}
|
||||
|
||||
private shorter = (size: number): number => {
|
||||
const newSize = size - this.percentChangeY
|
||||
return this.enforceMin(newSize)
|
||||
}
|
||||
|
||||
private thinner = (size: number): number => {
|
||||
const newSize = size - this.percentChangeX
|
||||
return this.enforceMin(newSize)
|
||||
}
|
||||
|
||||
private enforceMax = (size: number): number => {
|
||||
return size > MAX_SIZE ? MAX_SIZE : size
|
||||
}
|
||||
|
||||
private enforceMin = (size: number): number => {
|
||||
return size < MIN_SIZE ? MIN_SIZE : size
|
||||
}
|
||||
}
|
||||
|
||||
export default Threesizer
|
|
@ -402,7 +402,7 @@ export const HTTP_UNAUTHORIZED = 401
|
|||
export const HTTP_FORBIDDEN = 403
|
||||
export const HTTP_NOT_FOUND = 404
|
||||
|
||||
export const AUTOREFRESH_DEFAULT = 15000 // in milliseconds
|
||||
export const AUTOREFRESH_DEFAULT = 0 // in milliseconds
|
||||
|
||||
export const GRAPH = 'graph'
|
||||
export const TABLE = 'table'
|
||||
|
@ -477,3 +477,13 @@ export const NOTIFICATION_TRANSITION = 250
|
|||
export const FIVE_SECONDS = 5000
|
||||
export const TEN_SECONDS = 10000
|
||||
export const INFINITE = -1
|
||||
|
||||
// Resizer && Threesizer
|
||||
export const HUNDRED = 100
|
||||
export const REQUIRED_HALVES = 2
|
||||
export const HANDLE_VERTICAL = 'vertical'
|
||||
export const HANDLE_HORIZONTAL = 'horizontal'
|
||||
export const HANDLE_NONE = 'none'
|
||||
export const HANDLE_PIXELS = 30
|
||||
export const MAX_SIZE = 1
|
||||
export const MIN_SIZE = 0
|
||||
|
|
|
@ -424,13 +424,6 @@ export const notifyCellAdded = name => ({
|
|||
message: `Added "${name}" to dashboard.`,
|
||||
})
|
||||
|
||||
export const notifyCellCloned = name => ({
|
||||
...defaultSuccessNotification,
|
||||
icon: 'duplicate',
|
||||
duration: 1900,
|
||||
message: `Added "${name}" to dashboard.`,
|
||||
})
|
||||
|
||||
export const notifyCellDeleted = name => ({
|
||||
...defaultDeletionNotification,
|
||||
icon: 'dash-h',
|
||||
|
|
|
@ -0,0 +1,26 @@
|
|||
import _ from 'lodash'
|
||||
import moment from 'moment'
|
||||
import {map} from 'fast.js'
|
||||
|
||||
export const formatDate = timestamp =>
|
||||
moment(timestamp).format('M/D/YYYY h:mm:ss.SSSSSSSSS A')
|
||||
|
||||
export const dataToCSV = ([titleRow, ...valueRows]) => {
|
||||
if (_.isEmpty(titleRow)) {
|
||||
return ''
|
||||
}
|
||||
if (_.isEmpty(valueRows)) {
|
||||
return ['date', titleRow.slice(1)].join(',')
|
||||
}
|
||||
if (titleRow[0] === 'time') {
|
||||
const titlesString = ['date', titleRow.slice(1)].join(',')
|
||||
|
||||
const valuesString = map(valueRows, ([timestamp, ...values]) => [
|
||||
[formatDate(timestamp), ...values].join(','),
|
||||
]).join('\n')
|
||||
return `${titlesString}\n${valuesString}`
|
||||
}
|
||||
const allRows = [titleRow, ...valueRows]
|
||||
const allRowsStringArray = map(allRows, r => r.join(','))
|
||||
return allRowsStringArray.join('\n')
|
||||
}
|
|
@ -1,53 +0,0 @@
|
|||
import _ from 'lodash'
|
||||
import moment from 'moment'
|
||||
|
||||
export const formatDate = timestamp =>
|
||||
moment(timestamp).format('M/D/YYYY h:mm:ss.SSSSSSSSS A')
|
||||
|
||||
export const resultsToCSV = results => {
|
||||
if (!_.get(results, ['0', 'series', '0'])) {
|
||||
return {flag: 'no_data', name: '', CSVString: ''}
|
||||
}
|
||||
|
||||
const {name, columns, values} = _.get(results, ['0', 'series', '0'])
|
||||
|
||||
if (columns[0] === 'time') {
|
||||
const [, ...cols] = columns
|
||||
const CSVString = [['date', ...cols].join(',')]
|
||||
.concat(
|
||||
values.map(([timestamp, ...measurements]) =>
|
||||
// MS Excel format
|
||||
[formatDate(timestamp), ...measurements].join(',')
|
||||
)
|
||||
)
|
||||
.join('\n')
|
||||
return {flag: 'ok', name, CSVString}
|
||||
}
|
||||
|
||||
const CSVString = [columns.join(',')]
|
||||
.concat(values.map(row => row.join(',')))
|
||||
.join('\n')
|
||||
return {flag: 'ok', name, CSVString}
|
||||
}
|
||||
|
||||
export const dashboardtoCSV = data => {
|
||||
const columnNames = _.flatten(
|
||||
data.map(r => _.get(r, 'results[0].series[0].columns', []))
|
||||
)
|
||||
const timeIndices = columnNames
|
||||
.map((e, i) => (e === 'time' ? i : -1))
|
||||
.filter(e => e >= 0)
|
||||
|
||||
let values = data.map(r => _.get(r, 'results[0].series[0].values', []))
|
||||
values = _.unzip(values).map(v => _.flatten(v))
|
||||
if (timeIndices) {
|
||||
values.map(v => {
|
||||
timeIndices.forEach(i => (v[i] = formatDate(v[i])))
|
||||
return v
|
||||
})
|
||||
}
|
||||
const CSVString = [columnNames.join(',')]
|
||||
.concat(values.map(v => v.join(',')))
|
||||
.join('\n')
|
||||
return CSVString
|
||||
}
|
|
@ -68,9 +68,9 @@
|
|||
@import 'components/source-selector';
|
||||
@import 'components/tables';
|
||||
@import 'components/table-graph';
|
||||
@import 'components/threesizer';
|
||||
@import 'components/threshold-controls';
|
||||
@import 'components/kapacitor-logs-table';
|
||||
@import 'components/func-node.scss';
|
||||
|
||||
// Pages
|
||||
@import 'pages/config-endpoints';
|
||||
|
@ -81,11 +81,8 @@
|
|||
@import 'pages/admin';
|
||||
@import 'pages/users';
|
||||
@import 'pages/tickscript-editor';
|
||||
@import 'pages/time-machine';
|
||||
@import 'pages/manage-providers';
|
||||
|
||||
// TODO
|
||||
@import 'unsorted';
|
||||
|
||||
// IFQL - Time Machine
|
||||
@import 'components/funcs-button';
|
||||
@import 'components/time-machine';
|
||||
|
|
|
@ -20,73 +20,190 @@
|
|||
font-weight: 600;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.CodeMirror-vscrollbar {
|
||||
@include custom-scrollbar-round($g2-kevlar,$g6-smoke);
|
||||
@include custom-scrollbar-round($g2-kevlar, $g6-smoke);
|
||||
}
|
||||
|
||||
.CodeMirror-hscrollbar {
|
||||
@include custom-scrollbar-round($g0-obsidian,$g6-smoke);
|
||||
@include custom-scrollbar-round($g0-obsidian, $g6-smoke);
|
||||
}
|
||||
|
||||
.cm-s-material .CodeMirror-gutters {
|
||||
@include gradient-v($g2-kevlar, $g0-obsidian)
|
||||
border: none;
|
||||
@include gradient-v($g2-kevlar, $g0-obsidian) border: none;
|
||||
}
|
||||
|
||||
.cm-s-material .CodeMirror-gutters .CodeMirror-gutter {
|
||||
background-color: fade-out($g4-onyx, 0.75);
|
||||
height: calc(100% + 30px);
|
||||
}
|
||||
|
||||
.CodeMirror-gutter.CodeMirror-linenumbers {
|
||||
width: 60px;
|
||||
}
|
||||
|
||||
.cm-s-material.CodeMirror .CodeMirror-sizer {
|
||||
margin-left: 60px;
|
||||
}
|
||||
|
||||
.cm-s-material.CodeMirror .CodeMirror-linenumber.CodeMirror-gutter-elt {
|
||||
padding-right: 9px;
|
||||
width: 46px;
|
||||
color: $g8-storm;
|
||||
}
|
||||
.cm-s-material .CodeMirror-guttermarker, .cm-s-material .CodeMirror-guttermarker-subtle, .cm-s-material .CodeMirror-linenumber { color: rgb(83,127,126); }
|
||||
|
||||
.cm-s-material .CodeMirror-guttermarker,
|
||||
.cm-s-material .CodeMirror-guttermarker-subtle,
|
||||
.cm-s-material .CodeMirror-linenumber {
|
||||
color: rgb(83, 127, 126);
|
||||
}
|
||||
|
||||
.cm-s-material .CodeMirror-cursor {
|
||||
width: 2px;
|
||||
border: 0;
|
||||
background-color: $g20-white;
|
||||
box-shadow:
|
||||
0 0 3px $c-laser,
|
||||
0 0 6px $c-ocean,
|
||||
0 0 11px $c-amethyst;
|
||||
box-shadow: 0 0 3px $c-laser, 0 0 6px $c-ocean, 0 0 11px $c-amethyst;
|
||||
}
|
||||
|
||||
.cm-s-material div.CodeMirror-selected,
|
||||
.cm-s-material.CodeMirror-focused div.CodeMirror-selected {
|
||||
background-color: fade-out($g8-storm,0.7);
|
||||
background-color: fade-out($g8-storm, 0.7);
|
||||
}
|
||||
|
||||
.cm-s-material .CodeMirror-line::selection,
|
||||
.cm-s-material .CodeMirror-line>span::selection,
|
||||
.cm-s-material .CodeMirror-line>span>span::selection {
|
||||
background: rgba(255, 255, 255, 0.10);
|
||||
}
|
||||
|
||||
.cm-s-material .CodeMirror-line::-moz-selection,
|
||||
.cm-s-material .CodeMirror-line>span::-moz-selection,
|
||||
.cm-s-material .CodeMirror-line>span>span::-moz-selection {
|
||||
background: rgba(255, 255, 255, 0.10);
|
||||
}
|
||||
|
||||
.cm-s-material .CodeMirror-activeline-background {
|
||||
background: rgba(0, 0, 0, 0);
|
||||
}
|
||||
|
||||
.cm-s-material .cm-keyword {
|
||||
color: $c-comet;
|
||||
}
|
||||
|
||||
.cm-s-material .cm-operator {
|
||||
color: $c-dreamsicle;
|
||||
}
|
||||
|
||||
.cm-s-material .cm-variable-2 {
|
||||
color: #80CBC4;
|
||||
}
|
||||
|
||||
.cm-s-material .cm-variable-3,
|
||||
.cm-s-material .cm-type {
|
||||
color: $c-laser;
|
||||
}
|
||||
|
||||
.cm-s-material .cm-builtin {
|
||||
color: #DECB6B;
|
||||
}
|
||||
|
||||
.cm-s-material .cm-atom {
|
||||
color: $c-viridian;
|
||||
}
|
||||
|
||||
.cm-s-material .cm-number {
|
||||
color: $c-daisy;
|
||||
}
|
||||
|
||||
.cm-s-material .cm-def {
|
||||
color: rgba(233, 237, 237, 1);
|
||||
}
|
||||
|
||||
.cm-s-material .cm-string {
|
||||
color: $c-krypton;
|
||||
}
|
||||
|
||||
.cm-s-material .cm-string-2 {
|
||||
color: #80CBC4;
|
||||
}
|
||||
|
||||
.cm-s-material .cm-comment {
|
||||
color: $g10-wolf;
|
||||
}
|
||||
|
||||
.cm-s-material .cm-variable {
|
||||
color: $c-laser;
|
||||
}
|
||||
|
||||
.cm-s-material .cm-tag {
|
||||
color: #80CBC4;
|
||||
}
|
||||
|
||||
.cm-s-material .cm-meta {
|
||||
color: #80CBC4;
|
||||
}
|
||||
|
||||
.cm-s-material .cm-attribute {
|
||||
color: #FFCB6B;
|
||||
}
|
||||
|
||||
.cm-s-material .cm-property {
|
||||
color: #80CBAE;
|
||||
}
|
||||
|
||||
.cm-s-material .cm-qualifier {
|
||||
color: #DECB6B;
|
||||
}
|
||||
|
||||
.cm-s-material .cm-variable-3,
|
||||
.cm-s-material .cm-type {
|
||||
color: #DECB6B;
|
||||
}
|
||||
|
||||
.cm-s-material .cm-tag {
|
||||
color: rgba(255, 83, 112, 1);
|
||||
}
|
||||
.cm-s-material .CodeMirror-line::selection, .cm-s-material .CodeMirror-line > span::selection, .cm-s-material .CodeMirror-line > span > span::selection { background: rgba(255, 255, 255, 0.10); }
|
||||
.cm-s-material .CodeMirror-line::-moz-selection, .cm-s-material .CodeMirror-line > span::-moz-selection, .cm-s-material .CodeMirror-line > span > span::-moz-selection { background: rgba(255, 255, 255, 0.10); }
|
||||
|
||||
.cm-s-material .CodeMirror-activeline-background { background: rgba(0, 0, 0, 0); }
|
||||
.cm-s-material .cm-keyword { color: $c-comet; }
|
||||
.cm-s-material .cm-operator { color: $c-dreamsicle; }
|
||||
.cm-s-material .cm-variable-2 { color: #80CBC4; }
|
||||
.cm-s-material .cm-variable-3, .cm-s-material .cm-type { color: $c-laser; }
|
||||
.cm-s-material .cm-builtin { color: #DECB6B; }
|
||||
.cm-s-material .cm-atom { color: $c-viridian; }
|
||||
.cm-s-material .cm-number { color: $c-daisy; }
|
||||
.cm-s-material .cm-def { color: rgba(233, 237, 237, 1); }
|
||||
.cm-s-material .cm-string { color: $c-krypton; }
|
||||
.cm-s-material .cm-string-2 { color: #80CBC4; }
|
||||
.cm-s-material .cm-comment { color: $g10-wolf; }
|
||||
.cm-s-material .cm-variable { color: $c-laser; }
|
||||
.cm-s-material .cm-tag { color: #80CBC4; }
|
||||
.cm-s-material .cm-meta { color: #80CBC4; }
|
||||
.cm-s-material .cm-attribute { color: #FFCB6B; }
|
||||
.cm-s-material .cm-property { color: #80CBAE; }
|
||||
.cm-s-material .cm-qualifier { color: #DECB6B; }
|
||||
.cm-s-material .cm-variable-3, .cm-s-material .cm-type { color: #DECB6B; }
|
||||
.cm-s-material .cm-tag { color: rgba(255, 83, 112, 1); }
|
||||
.cm-s-material .cm-error {
|
||||
color: rgba(255, 255, 255, 1.0);
|
||||
background-color: #EC5F67;
|
||||
}
|
||||
|
||||
.cm-s-material .CodeMirror-matchingbracket {
|
||||
text-decoration: underline;
|
||||
color: white !important;
|
||||
}
|
||||
|
||||
// CodeMirror hints
|
||||
.CodeMirror-hints {
|
||||
position: absolute;
|
||||
z-index: 10;
|
||||
overflow: hidden;
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 2px;
|
||||
-webkit-box-shadow: 2px 3px 5px rgba(0, 0, 0, .2);
|
||||
-moz-box-shadow: 2px 3px 5px rgba(0, 0, 0, .2);
|
||||
box-shadow: 2px 3px 5px rgba(0, 0, 0, .2);
|
||||
border-radius: 3px;
|
||||
border: 1px solid silver;
|
||||
background: white;
|
||||
font-size: 90%;
|
||||
font-family: monospace;
|
||||
max-height: 20em;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.CodeMirror-hint {
|
||||
margin: 0;
|
||||
padding: 0 4px;
|
||||
border-radius: 2px;
|
||||
white-space: pre;
|
||||
color: black;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
li.CodeMirror-hint-active {
|
||||
background: #08f;
|
||||
color: white;
|
||||
}
|
|
@ -1,39 +0,0 @@
|
|||
.func-nodes-container {
|
||||
display: inline-flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.func-node {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.func-node--name {
|
||||
background: #252b35;
|
||||
border-radius: $radius-small;
|
||||
padding: 10px;
|
||||
width: auto;
|
||||
display: flex;
|
||||
color: $ix-text-default;
|
||||
margin-bottom: $ix-marg-a;
|
||||
font-family: $ix-text-font;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.func-args {
|
||||
background: #252b35;
|
||||
border-radius: $radius-small;
|
||||
padding: 10px;
|
||||
margin-bottom: $ix-marg-a;
|
||||
width: auto;
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
flex-direction: column;
|
||||
color: $ix-text-default;
|
||||
font-family: $ix-text-font;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.func-arg {
|
||||
display: flex;
|
||||
}
|
|
@ -14,14 +14,13 @@ $resizer-color: $g5-pepper;
|
|||
$resizer-color-hover: $g8-storm;
|
||||
$resizer-color-active: $c-pool;
|
||||
$resizer-color-kapacitor: $c-rainforest;
|
||||
|
||||
.resize--container {
|
||||
overflow: hidden !important;
|
||||
|
||||
&.resize--dragging * {
|
||||
@include no-user-select();
|
||||
}
|
||||
}
|
||||
|
||||
.resize--top,
|
||||
.resize--bottom {
|
||||
position: absolute;
|
||||
|
@ -41,6 +40,7 @@ $resizer-color-kapacitor: $c-rainforest;
|
|||
Resizable Container Handle
|
||||
----------------------------------------------
|
||||
*/
|
||||
|
||||
.resizer--handle {
|
||||
top: 60%;
|
||||
left: 0;
|
||||
|
@ -51,9 +51,7 @@ $resizer-color-kapacitor: $c-rainforest;
|
|||
z-index: 1;
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
position: absolute;
|
||||
|
||||
// Psuedo element for handle
|
||||
position: absolute; // Psuedo element for handle
|
||||
&:before {
|
||||
z-index: $resizer-handle-z;
|
||||
color: $resizer-dots;
|
||||
|
@ -63,7 +61,7 @@ $resizer-color-kapacitor: $c-rainforest;
|
|||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%,-50%);
|
||||
transform: translate(-50%, -50%);
|
||||
width: 160px;
|
||||
height: $resizer-handle-width;
|
||||
line-height: $resizer-handle-width;
|
||||
|
@ -71,10 +69,8 @@ $resizer-color-kapacitor: $c-rainforest;
|
|||
border-radius: 3px;
|
||||
white-space: nowrap;
|
||||
text-align: center;
|
||||
transition:
|
||||
background-color 0.25s ease;
|
||||
}
|
||||
// Psuedo element for line
|
||||
transition: background-color 0.25s ease;
|
||||
} // Psuedo element for line
|
||||
&:after {
|
||||
z-index: $resizer-line-z;
|
||||
content: '';
|
||||
|
@ -87,12 +83,10 @@ $resizer-color-kapacitor: $c-rainforest;
|
|||
height: $resizer-line-width;
|
||||
background-color: $resizer-color;
|
||||
box-shadow: 0 0 0 transparent;
|
||||
transition:
|
||||
background-color 0.19s ease;
|
||||
transition: background-color 0.19s ease;
|
||||
}
|
||||
&:hover {
|
||||
cursor: ns-resize;
|
||||
|
||||
&:before {
|
||||
background-color: $resizer-color-hover;
|
||||
}
|
||||
|
@ -103,9 +97,7 @@ $resizer-color-kapacitor: $c-rainforest;
|
|||
&.dragging {
|
||||
&:before,
|
||||
&:after {
|
||||
transition:
|
||||
box-shadow 0.3s ease,
|
||||
background-color 0.3s ease;
|
||||
transition: box-shadow 0.3s ease, background-color 0.3s ease;
|
||||
background-color: $resizer-color-active;
|
||||
box-shadow: 0 0 $resizer-glow $resizer-color-active;
|
||||
}
|
||||
|
@ -113,6 +105,7 @@ $resizer-color-kapacitor: $c-rainforest;
|
|||
}
|
||||
|
||||
/* Kapacitor Theme */
|
||||
|
||||
.resizer--handle.resizer--malachite.dragging {
|
||||
&:before,
|
||||
&:after {
|
||||
|
|
|
@ -31,7 +31,7 @@
|
|||
|
||||
// Highlight
|
||||
&:after {
|
||||
content: '';
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
|
@ -80,8 +80,8 @@
|
|||
padding-right: 17px;
|
||||
|
||||
&:before {
|
||||
font-family: 'icomoon';
|
||||
content: '\e902';
|
||||
font-family: "icomoon";
|
||||
content: "\e902";
|
||||
font-size: 17px;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
|
|
|
@ -0,0 +1,183 @@
|
|||
/*
|
||||
Resizable Container with 3 divisions
|
||||
------------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
$threesizer-handle: 30px;
|
||||
|
||||
.threesizer {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
|
||||
&.dragging .threesizer--division {
|
||||
@include no-user-select();
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
&.vertical {
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
&.horizontal {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
||||
.threesizer--division {
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
|
||||
transition: height 0.25s ease-in-out, width 0.25s ease-in-out;
|
||||
|
||||
&.dragging {
|
||||
transition: none;
|
||||
}
|
||||
|
||||
&.vertical {
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
&.horizontal {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
||||
/* Draggable Handle With Title */
|
||||
.threesizer--handle {
|
||||
@include no-user-select();
|
||||
background-color: $g4-onyx;
|
||||
transition: background-color 0.25s ease, color 0.25s ease;
|
||||
|
||||
&.vertical {
|
||||
border-right: solid 2px $g3-castle;
|
||||
|
||||
&:hover,
|
||||
&.dragging {
|
||||
cursor: col-resize;
|
||||
}
|
||||
}
|
||||
|
||||
&.horizontal {
|
||||
border-bottom: solid 2px $g3-castle;
|
||||
|
||||
&:hover,
|
||||
&.dragging {
|
||||
cursor: row-resize;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
&.disabled {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
color: $g16-pearl;
|
||||
background-color: $g5-pepper;
|
||||
}
|
||||
|
||||
&.dragging {
|
||||
color: $c-laser;
|
||||
background-color: $g5-pepper;
|
||||
}
|
||||
}
|
||||
|
||||
.threesizer--title {
|
||||
padding-left: 14px;
|
||||
position: relative;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
white-space: nowrap;
|
||||
color: $g11-sidewalk;
|
||||
z-index: 1;
|
||||
transition: transform 0.25s ease;
|
||||
|
||||
&.vertical {
|
||||
transform: translate(28px, 14px);
|
||||
|
||||
&.threesizer--collapsed {
|
||||
transform: translate(0, 3px) rotate(90deg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$threesizer-shadow-size: 9px;
|
||||
$threesizer-z-index: 2;
|
||||
$threesizer-shadow-start: fade-out($g0-obsidian, 0.82);
|
||||
$threesizer-shadow-stop: fade-out($g0-obsidian, 1);
|
||||
|
||||
.threesizer--contents {
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
flex-wrap: nowrap;
|
||||
position: relative;
|
||||
|
||||
&.horizontal {
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
&.vertical {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
// Bottom Shadow
|
||||
&.horizontal:after,
|
||||
&.vertical:after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
z-index: $threesizer-z-index;
|
||||
}
|
||||
|
||||
&.horizontal:after {
|
||||
width: 100%;
|
||||
height: $threesizer-shadow-size;
|
||||
@include gradient-v($threesizer-shadow-stop, $threesizer-shadow-start);
|
||||
}
|
||||
|
||||
&.vertical:after {
|
||||
height: 100%;
|
||||
width: $threesizer-shadow-size;
|
||||
@include gradient-h($threesizer-shadow-stop, $threesizer-shadow-start);
|
||||
}
|
||||
}
|
||||
|
||||
// Hide bottom shadow on last division
|
||||
.threesizer--contents.no-shadows:before,
|
||||
.threesizer--contents.no-shadows:after,
|
||||
.threesizer--division:last-child .threesizer--contents:after {
|
||||
content: none;
|
||||
display: none;
|
||||
}
|
||||
|
||||
// Header
|
||||
.threesizer--header {
|
||||
background-color: $g2-kevlar;
|
||||
|
||||
.horizontal > & {
|
||||
width: 50px;
|
||||
border-right: 2px solid $g4-onyx;
|
||||
}
|
||||
|
||||
.vertical > & {
|
||||
height: 50px;
|
||||
border-bottom: 2px solid $g4-onyx;
|
||||
}
|
||||
}
|
||||
|
||||
.threesizer--body {
|
||||
.horizontal > &:only-child {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.vertical > &:only-child {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.threesizer--header + & {
|
||||
flex: 1 0 0;
|
||||
}
|
||||
}
|
|
@ -3,11 +3,38 @@
|
|||
----------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
$ifql-func-selector--gap: 10px;
|
||||
$ifql-func-selector--height: 30px;
|
||||
|
||||
.ifql-func--selector {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
|
||||
&.open {
|
||||
z-index: 9999;
|
||||
}
|
||||
}
|
||||
|
||||
.ifql-func--button {
|
||||
.func-selector--connector {
|
||||
width: $ifql-func-selector--gap;
|
||||
height: $ifql-func-selector--height;
|
||||
position: relative;
|
||||
|
||||
&:after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
width: 100%;
|
||||
height: 4px;
|
||||
transform: translateY(-50%);
|
||||
@include gradient-h($g4-onyx, $c-pool);
|
||||
}
|
||||
}
|
||||
|
||||
.btn.btn-sm.ifql-func--button {
|
||||
border-radius: 50%;
|
||||
float: left;
|
||||
&:focus {
|
||||
box-shadow: 0 0 8px 3px $c-amethyst;
|
||||
}
|
||||
|
@ -16,26 +43,28 @@
|
|||
.ifql-func--autocomplete,
|
||||
.ifql-func--list {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
width: 166px;
|
||||
}
|
||||
|
||||
.ifql-func--autocomplete {
|
||||
left: 0;
|
||||
top: 0;
|
||||
|
||||
.func-selector--connector + & {
|
||||
left: $ifql-func-selector--gap;
|
||||
}
|
||||
}
|
||||
|
||||
.ifql-func--list {
|
||||
border-radius: 4px;
|
||||
top: 30px;
|
||||
left: 0;
|
||||
border-radius: $radius;
|
||||
top: $ifql-func-selector--height;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
@extend %no-user-select;
|
||||
@include gradient-h($c-star, $c-pool);
|
||||
}
|
||||
|
||||
.ifql-func--input {
|
||||
}
|
||||
|
||||
.ifql-func--item {
|
||||
height: 28px;
|
||||
line-height: 28px;
|
|
@ -0,0 +1,207 @@
|
|||
$ifql-node-height: 30px;
|
||||
$ifql-node-tooltip-gap: $ifql-node-height + 4px;
|
||||
$ifql-node-gap: 5px;
|
||||
$ifql-node-padding: 10px;
|
||||
$ifql-arg-min-width: 120px;
|
||||
|
||||
/*
|
||||
Shared Node styles
|
||||
------------------
|
||||
*/
|
||||
%ifql-node {
|
||||
height: $ifql-node-height;
|
||||
border-radius: $radius;
|
||||
padding: 0 $ifql-node-padding;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
position: relative;
|
||||
background-color: $g4-onyx;
|
||||
transition: background-color 0.25s ease;
|
||||
|
||||
&:hover {
|
||||
background-color: $g6-smoke;
|
||||
}
|
||||
}
|
||||
|
||||
.body-builder {
|
||||
padding: 30px;
|
||||
min-width: 440px;
|
||||
overflow: hidden;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
background-color: $g1-raven;
|
||||
}
|
||||
|
||||
.declaration {
|
||||
width: 100%;
|
||||
margin-bottom: 24px;
|
||||
display: flex;
|
||||
flex-wrap: nowrap;
|
||||
|
||||
&:last-of-type {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.variable-string {
|
||||
@extend %ifql-node;
|
||||
color: $g11-sidewalk;
|
||||
line-height: $ifql-node-height;
|
||||
white-space: nowrap;
|
||||
@include no-user-select();
|
||||
}
|
||||
.variable-blank {
|
||||
font-style: italic;
|
||||
}
|
||||
.variable-name {
|
||||
color: $c-pool;
|
||||
}
|
||||
.variable-value--string {
|
||||
color: $c-honeydew
|
||||
}
|
||||
.variable-value--boolean {
|
||||
color: $c-viridian
|
||||
}
|
||||
.variable-value--number {
|
||||
color: $c-neutrino;
|
||||
}
|
||||
.variable-value--invalid {
|
||||
color: $c-dreamsicle;
|
||||
}
|
||||
|
||||
.func-node {
|
||||
@extend %ifql-node;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-left: $ifql-node-gap;
|
||||
|
||||
// Connection Line
|
||||
&:after {
|
||||
content: '';
|
||||
height: 4px;
|
||||
width: $ifql-node-gap;
|
||||
background-color: $g4-onyx;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 0;
|
||||
transform: translate(-100%, -50%);
|
||||
}
|
||||
|
||||
&:first-child:after {
|
||||
content: none;
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
.func-node--name,
|
||||
.func-node--preview {
|
||||
font-size: 13px;
|
||||
@include no-user-select();
|
||||
white-space: nowrap;
|
||||
transition: color 0.25s ease;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.func-node--name {
|
||||
color: $c-comet;
|
||||
|
||||
.func-node:hover & {
|
||||
color: $c-potassium;
|
||||
}
|
||||
}
|
||||
|
||||
.func-node--preview {
|
||||
color: $g11-sidewalk;
|
||||
margin-left: 4px;
|
||||
|
||||
.func-node:hover & {
|
||||
color: $g17-whisper;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.func-node--tooltip,
|
||||
.variable-name--tooltip {
|
||||
background-color: $g3-castle;
|
||||
border-radius: $radius;
|
||||
padding: 10px;
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
flex-direction: column;
|
||||
position: absolute;
|
||||
top: $ifql-node-tooltip-gap;
|
||||
left: 0;
|
||||
z-index: 9999;
|
||||
box-shadow: 0 0 10px 2px $g2-kevlar;
|
||||
|
||||
// Caret
|
||||
&:before {
|
||||
content: '';
|
||||
border-width: 9px;
|
||||
border-style: solid;
|
||||
border-color: transparent;
|
||||
border-bottom-color: $g3-castle;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: $ifql-node-padding + 3px;
|
||||
transform: translate(-50%, -100%);
|
||||
}
|
||||
|
||||
// Invisible block to continue hovering
|
||||
&:after {
|
||||
content: '';
|
||||
width: 80%;
|
||||
height: 7px;
|
||||
position: absolute;
|
||||
top: -7px;
|
||||
left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.func-node--delete {
|
||||
margin-top: 12px;
|
||||
width: 60px;
|
||||
}
|
||||
|
||||
.func-arg {
|
||||
min-width: $ifql-arg-min-width;
|
||||
display: flex;
|
||||
flex-wrap: nowrap;
|
||||
align-items: center;
|
||||
margin-bottom: 4px;
|
||||
|
||||
&:last-of-type {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
.func-arg--label {
|
||||
white-space: nowrap;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: $g10-wolf;
|
||||
padding-right: 8px;
|
||||
@include no-user-select();
|
||||
}
|
||||
.func-arg--value {
|
||||
flex: 1 0 0;
|
||||
}
|
||||
|
||||
|
||||
.variable-name--tooltip {
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
|
||||
.variable-name--input {
|
||||
width: 140px;
|
||||
}
|
||||
|
||||
.variable-name--operator {
|
||||
width: 20px;
|
||||
height: 30px;
|
||||
text-align: center;
|
||||
line-height: 30px;
|
||||
font-weight: 600;
|
||||
@include no-user-select();
|
||||
}
|
|
@ -1,3 +1,8 @@
|
|||
/*
|
||||
IFQL Code Mirror Editor
|
||||
----------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
.time-machine-container {
|
||||
display: flex;
|
||||
height: 90%;
|
|
@ -0,0 +1,185 @@
|
|||
/*
|
||||
IFQL Schema Explorer -- Tree View
|
||||
----------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
$ifql-tree-indent: 26px;
|
||||
$ifql-tree-line: 2px;
|
||||
|
||||
.ifql-schema-explorer {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: $g2-kevlar;
|
||||
min-width: 200px;
|
||||
}
|
||||
|
||||
.ifql-schema-tree {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
padding-left: 0;
|
||||
|
||||
> .ifql-schema-tree {
|
||||
padding-left: $ifql-tree-indent;
|
||||
}
|
||||
}
|
||||
|
||||
.ifql-schema-tree__empty {
|
||||
height: $ifql-tree-indent;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 11px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: $g8-storm;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.ifql-schema-item-toggle {
|
||||
width: $ifql-tree-indent;
|
||||
height: $ifql-tree-indent;
|
||||
position: relative;
|
||||
|
||||
// Plus Sign
|
||||
&:before,
|
||||
&:after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
background-color: $g11-sidewalk;
|
||||
width: $ifql-tree-indent / 3;
|
||||
height: $ifql-tree-line;
|
||||
transition: transform 0.25s ease, background-color 0.25s ease;
|
||||
}
|
||||
// Vertical Line
|
||||
&:after {
|
||||
transform: translate(-50%, -50%) rotate(90deg);
|
||||
}
|
||||
}
|
||||
|
||||
.ifql-schema-item {
|
||||
@include no-user-select();
|
||||
position: relative;
|
||||
height: $ifql-tree-indent;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 11px;
|
||||
padding-left: 0;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: $g11-sidewalk;
|
||||
white-space: nowrap;
|
||||
transition: color 0.25s ease, background-color 0.25s ease;
|
||||
|
||||
> span.icon {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: $ifql-tree-indent / 2;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: $g17-whisper;
|
||||
cursor: pointer;
|
||||
background-color: $g4-onyx;
|
||||
|
||||
.ifql-schema-item-toggle:before,
|
||||
.ifql-schema-item-toggle:after {
|
||||
background-color: $g17-whisper;
|
||||
}
|
||||
}
|
||||
|
||||
.expanded > & {
|
||||
color: $c-pool;
|
||||
|
||||
.ifql-schema-item-toggle:before,
|
||||
.ifql-schema-item-toggle:after {
|
||||
background-color: $c-pool;
|
||||
}
|
||||
.ifql-schema-item-toggle:before {
|
||||
transform: translate(-50%, -50%) rotate(-90deg);
|
||||
width: $ifql-tree-line;
|
||||
}
|
||||
.ifql-schema-item-toggle:after {
|
||||
transform: translate(-50%, -50%) rotate(0deg);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: $c-laser;
|
||||
|
||||
.ifql-schema-item-toggle:before,
|
||||
.ifql-schema-item-toggle:after {
|
||||
background-color: $c-laser;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.readonly,
|
||||
&.readonly:hover {
|
||||
padding-left: $ifql-tree-indent + 8px;
|
||||
background-color: transparent;
|
||||
color: $g11-sidewalk;
|
||||
cursor: default;
|
||||
}
|
||||
}
|
||||
|
||||
/* Tree Node Lines */
|
||||
.ifql-tree-node:before,
|
||||
.ifql-tree-node:after {
|
||||
content: '';
|
||||
background-color: $g4-onyx;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
// Vertical Line
|
||||
.ifql-tree-node:before {
|
||||
top: 0;
|
||||
left: $ifql-tree-indent / 2;
|
||||
width: $ifql-tree-line;
|
||||
height: 100%;
|
||||
}
|
||||
.ifql-tree-node:last-child:before {
|
||||
height: $ifql-tree-indent / 2;
|
||||
}
|
||||
|
||||
// Horizontal Line
|
||||
.ifql-tree-node:after {
|
||||
top: $ifql-tree-indent / 2;
|
||||
left: $ifql-tree-indent / 2;
|
||||
width: $ifql-tree-indent / 2;
|
||||
height: $ifql-tree-line;
|
||||
}
|
||||
|
||||
/*
|
||||
Controls
|
||||
----------------------------------------------------------------------------
|
||||
*/
|
||||
.ifql-schema--controls {
|
||||
padding: 11px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.ifql-schema--filter {
|
||||
flex: 1 0 0;
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
|
||||
|
||||
// Hints
|
||||
.ifql-schema-type {
|
||||
color: $g11-sidewalk;
|
||||
display: inline-block;
|
||||
margin-left: 8px;
|
||||
opacity: 0;
|
||||
transition: opacity 0.25s ease;
|
||||
|
||||
.ifql-schema-item:hover & {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,48 @@
|
|||
/*
|
||||
Time Machine Visualization
|
||||
----------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
.time-machine-visualization {
|
||||
display: flex;
|
||||
align-content: center;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
@include gradient-v($g2-kevlar, $g0-obsidian);
|
||||
}
|
||||
|
||||
.time-machine--graph {
|
||||
width: calc(100% - 60px);
|
||||
height: calc(100% - 60px);
|
||||
background-color: $g3-castle;
|
||||
border-radius: $radius;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
|
||||
.time-machine--graph-header {
|
||||
height: 56px;
|
||||
padding: 0 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center
|
||||
}
|
||||
|
||||
.time-machine--graph-header .nav.nav-tablist {
|
||||
width: 180px;
|
||||
|
||||
li {
|
||||
justify-content: center;
|
||||
flex: 1 0 0;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
.time-machine--graph-body {
|
||||
padding: 0 16px 8px 16px;
|
||||
flex: 1 0 0;
|
||||
}
|
Binary file not shown.
|
@ -23,8 +23,10 @@
|
|||
<glyph unicode="" glyph-name="arrow-left" d="M678.4 678.4c7.68 0 17.92-2.56 25.6-10.24 15.36-12.8 17.92-38.4 5.12-53.76l-312.32-358.4 309.76-358.4c12.8-15.36 12.8-40.96-5.12-53.76-15.36-12.8-40.96-12.8-53.76 5.12l-332.8 384c-12.8 15.36-12.8 35.84 0 51.2l332.8 384c10.24 5.12 20.48 10.24 30.72 10.24z" />
|
||||
<glyph unicode="" glyph-name="user" d="M588.8 289.28c58.88 28.16 99.84 89.6 99.84 161.28 0 99.84-79.36 179.2-179.2 179.2s-179.2-79.36-179.2-179.2c0-71.68 40.96-130.56 99.84-161.28-84.48-20.48-176.64-120.32-176.64-232.96v-145.92c0-12.8 7.68-25.6 17.92-25.6h476.16c10.24 0 17.92 10.24 17.92 25.6v145.92c0 112.64-89.6 209.92-176.64 232.96z" />
|
||||
<glyph unicode="" glyph-name="graphline" d="M888.32 465.92c-17.92 0-33.28-5.12-48.64-12.8l-76.8 79.36c10.24 15.36 15.36 33.28 15.36 53.76 0 56.32-46.080 104.96-104.96 104.96s-104.96-46.080-104.96-104.96c0-33.28 15.36-64 40.96-81.92l-192-476.16c-2.56 0-7.68 0-10.24 0-20.48 0-40.96-7.68-58.88-17.92l-117.76 99.84c5.12 12.8 10.24 28.16 10.24 43.52 0 56.32-46.080 104.96-104.96 104.96s-104.96-46.080-104.96-104.96 46.080-104.96 104.96-104.96c20.48 0 40.96 7.68 58.88 17.92l117.76-99.84c-5.12-12.8-10.24-28.16-10.24-43.52 0-56.32 46.080-104.96 104.96-104.96s104.96 46.080 104.96 104.96c0 33.28-15.36 64-40.96 81.92l192 476.16c2.56 0 7.68 0 10.24 0 17.92 0 33.28 5.12 48.64 12.8l76.8-79.36c-10.24-15.36-15.36-33.28-15.36-53.76 0-56.32 46.080-104.96 104.96-104.96s104.96 46.080 104.96 104.96-48.64 104.96-104.96 104.96z" />
|
||||
<glyph unicode="" glyph-name="collapse" d="M560.64 189.44c-2.56 2.56-5.12 5.12-7.68 7.68 0 0-2.56 2.56-2.56 2.56-2.56 0-2.56 2.56-5.12 2.56s-2.56 0-5.12 2.56c-2.56 0-2.56 0-5.12 2.56-2.56 0-7.68 0-10.24 0 0 0 0 0 0 0s0 0 0 0c-2.56 0-7.68 0-10.24 0s-2.56 0-5.12 0-2.56 0-5.12-2.56c-2.56 0-2.56-2.56-5.12-2.56s-2.56-2.56-5.12-2.56-5.12-5.12-7.68-7.68l-204.8-204.8c-20.48-20.48-20.48-51.2 0-71.68s51.2-20.48 71.68 0l117.76 117.76v-238.080c0-28.16 23.040-51.2 51.2-51.2s51.2 23.040 51.2 51.2v235.52l117.76-117.76c10.24-10.24 23.040-15.36 35.84-15.36s25.6 5.12 35.84 15.36c20.48 20.48 20.48 51.2 0 71.68l-202.24 204.8zM488.96 322.56c2.56-2.56 5.12-5.12 7.68-7.68 0 0 2.56-2.56 5.12-2.56s2.56-2.56 5.12-2.56c2.56 0 2.56 0 5.12-2.56 2.56 0 2.56 0 5.12-2.56 2.56 0 7.68 0 10.24 0s7.68 0 10.24 0c2.56 0 2.56 0 5.12 2.56 2.56 0 2.56 0 5.12 2.56 2.56 0 2.56 2.56 5.12 2.56 0 0 2.56 0 2.56 2.56 2.56 2.56 5.12 5.12 7.68 7.68l204.8 204.8c20.48 20.48 20.48 51.2 0 71.68s-51.2 20.48-71.68 0l-117.76-117.76v235.52c0 28.16-23.040 51.2-51.2 51.2s-51.2-23.040-51.2-51.2v-235.52l-117.76 117.76c-20.48 20.48-51.2 20.48-71.68 0s-20.48-51.2 0-71.68l202.24-204.8z" />
|
||||
<glyph unicode="" glyph-name="arrow-down" d="M89.6 422.4c0 7.68 2.56 17.92 10.24 25.6 12.8 15.36 38.4 17.92 53.76 5.12l358.4-309.76 358.4 309.76c15.36 12.8 40.96 12.8 53.76-5.12 12.8-15.36 12.8-40.96-5.12-53.76l-384-332.8c-15.36-12.8-35.84-12.8-51.2 0l-384 332.8c-5.12 7.68-10.24 17.92-10.24 28.16z" />
|
||||
<glyph unicode="" glyph-name="arrow-right" d="M345.6-166.4c-7.68 0-17.92 2.56-25.6 10.24-15.36 12.8-17.92 38.4-5.12 53.76l309.76 358.4-307.2 358.4c-12.8 15.36-12.8 40.96 5.12 53.76 15.36 12.8 40.96 12.8 53.76-5.12l332.8-384c12.8-15.36 12.8-35.84 0-51.2l-332.8-384c-10.24-5.12-20.48-10.24-30.72-10.24z" />
|
||||
<glyph unicode="" glyph-name="okta" d="M816.64 642.56v-286.72c69.12 10.24 125.44 71.68 125.44 143.36s-56.32 135.68-125.44 143.36zM650.24 499.2c0-71.68 53.76-133.12 125.44-143.36v286.72c-71.68-7.68-125.44-69.12-125.44-143.36zM1024 499.2c0 125.44-102.4 227.84-227.84 227.84-48.64 0-94.72-15.36-130.56-40.96-58.88 25.6-125.44 40.96-194.56 40.96-258.56 0-471.040-209.92-471.040-471.040s212.48-471.040 471.040-471.040c261.12 0 471.040 212.48 471.040 471.040 0 23.040-2.56 46.080-5.12 66.56 53.76 43.52 87.040 107.52 87.040 176.64zM471.040-133.12c-215.040 0-389.12 174.080-389.12 389.12s176.64 389.12 389.12 389.12c46.080 0 89.6-7.68 130.56-23.040-15.36-23.040-25.6-48.64-30.72-76.8-30.72 10.24-64 17.92-99.84 17.92-168.96 0-307.2-138.24-307.2-307.2s138.24-307.2 307.2-307.2c168.96 0 307.2 138.24 307.2 307.2 0 5.12 0 12.8 0 17.92 5.12 0 12.8 0 17.92 0 23.040 0 43.52 2.56 64 10.24 0-7.68 2.56-17.92 2.56-25.6 0-217.6-176.64-391.68-391.68-391.68zM796.16 314.88c-102.4 0-186.88 84.48-186.88 186.88s84.48 186.88 186.88 186.88 186.88-84.48 186.88-186.88-84.48-186.88-186.88-186.88z" />
|
||||
<glyph unicode="" glyph-name="search" d="M993.28-99.84l-202.24 202.24c40.96 64 64 135.68 64 217.6 0 225.28-181.76 407.040-407.040 407.040s-404.48-184.32-404.48-407.040 181.76-407.040 407.040-407.040c79.36 0 153.6 23.040 217.6 64l202.24-202.24c17.92-17.92 40.96-25.6 64-25.6s46.080 7.68 64 25.6c30.72 33.28 30.72 92.16-5.12 125.44zM145.92 320c0 166.4 135.68 302.080 302.080 302.080s304.64-135.68 304.64-304.64c0-79.36-30.72-151.040-79.36-204.8-2.56-2.56-7.68-5.12-10.24-7.68s-5.12-7.68-7.68-10.24c-53.76-48.64-125.44-79.36-204.8-79.36-168.96 0-304.64 135.68-304.64 304.64z" />
|
||||
<glyph unicode="" glyph-name="duplicate" d="M921.6 768h-563.2c-56.32 0-102.4-46.080-102.4-102.4v-153.6h-153.6c-56.32 0-102.4-46.080-102.4-102.4v-563.2c0-56.32 46.080-102.4 102.4-102.4h563.2c56.32 0 102.4 46.080 102.4 102.4v153.6h153.6c56.32 0 102.4 46.080 102.4 102.4v563.2c0 56.32-46.080 102.4-102.4 102.4zM691.2-153.6c0-12.8-12.8-25.6-25.6-25.6h-563.2c-12.8 0-25.6 12.8-25.6 25.6v563.2c0 12.8 12.8 25.6 25.6 25.6h153.6v-332.8c0-56.32 46.080-102.4 102.4-102.4h332.8v-153.6z" />
|
||||
<glyph unicode="" glyph-name="checkmark" d="M345.6-87.040l-271.36 271.36c-33.28 33.28-38.4 89.6-7.68 128 35.84 38.4 94.72 38.4 130.56 2.56l168.96-168.96c5.12-5.12 12.8-5.12 15.36 0l442.88 442.88c35.84 35.84 94.72 35.84 130.56-2.56 33.28-35.84 28.16-92.16-7.68-128l-542.72-542.72c-15.36-20.48-43.52-20.48-58.88-2.56z" />
|
||||
|
|
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 38 KiB |
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -30,6 +30,8 @@
|
|||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
|
||||
&.collapse:before {content: "\e90f";}
|
||||
&.okta:before {content: "\e912";}
|
||||
&.user-remove:before {content: "\e904";}
|
||||
&.user-add:before {content: "\e907";}
|
||||
&.group:before {content: "\e908";}
|
||||
|
|
|
@ -2,14 +2,15 @@
|
|||
Variables
|
||||
------------------------------------------------------
|
||||
*/
|
||||
|
||||
$dash-graph-heading: 30px;
|
||||
$dash-graph-heading-context: $dash-graph-heading - 8px;
|
||||
$dash-graph-options-arrow: 8px;
|
||||
|
||||
/*
|
||||
Animations
|
||||
------------------------------------------------------
|
||||
*/
|
||||
|
||||
@keyframes refreshingSpinnerA {
|
||||
0% {
|
||||
transform: translate(-50%, -50%) scale(1.75);
|
||||
|
@ -25,6 +26,7 @@ $dash-graph-options-arrow: 8px;
|
|||
transform: translate(-50%, -50%) scale(1, 1);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes refreshingSpinnerB {
|
||||
0% {
|
||||
transform: translate(-50%, -50%) scale(1, 1);
|
||||
|
@ -40,6 +42,7 @@ $dash-graph-options-arrow: 8px;
|
|||
transform: translate(-50%, -50%) scale(1, 1);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes refreshingSpinnerC {
|
||||
0% {
|
||||
transform: translate(-50%, -50%) scale(1, 1);
|
||||
|
@ -60,6 +63,7 @@ $dash-graph-options-arrow: 8px;
|
|||
Dashboard Index Page
|
||||
------------------------------------------------------
|
||||
*/
|
||||
|
||||
.dashboards-page--actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
@ -69,6 +73,7 @@ $dash-graph-options-arrow: 8px;
|
|||
Default Dashboard Mode
|
||||
------------------------------------------------------
|
||||
*/
|
||||
|
||||
.cell-shell {
|
||||
background-color: $g3-castle;
|
||||
border-radius: $radius;
|
||||
|
@ -89,6 +94,7 @@ $dash-graph-options-arrow: 8px;
|
|||
left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.dash-graph {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
|
@ -96,6 +102,7 @@ $dash-graph-options-arrow: 8px;
|
|||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.dash-graph--container {
|
||||
user-select: none !important;
|
||||
-o-user-select: none !important;
|
||||
|
@ -108,7 +115,6 @@ $dash-graph-options-arrow: 8px;
|
|||
top: $dash-graph-heading;
|
||||
left: 0;
|
||||
padding: 0;
|
||||
|
||||
.dygraph {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
|
@ -126,6 +132,7 @@ $dash-graph-options-arrow: 8px;
|
|||
top: (-$dash-graph-heading + 5px) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.dash-graph--heading {
|
||||
user-select: none !important;
|
||||
-o-user-select: none !important;
|
||||
|
@ -151,6 +158,7 @@ $dash-graph-options-arrow: 8px;
|
|||
background-color: $g5-pepper;
|
||||
}
|
||||
}
|
||||
|
||||
.dash-graph--name {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
|
@ -165,23 +173,24 @@ $dash-graph-options-arrow: 8px;
|
|||
padding-left: 10px;
|
||||
transition: color 0.25s ease, background-color 0.25s ease,
|
||||
border-color 0.25s ease;
|
||||
|
||||
&:only-child {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.dash-graph--name.dash-graph--name__default {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.dash-graph--draggable {
|
||||
cursor: move !important;
|
||||
}
|
||||
|
||||
.dash-graph--custom-indicators {
|
||||
height: 24px;
|
||||
border-radius: 3px;
|
||||
display: flex;
|
||||
cursor: default;
|
||||
|
||||
> .custom-indicator,
|
||||
> .source-indicator {
|
||||
font-size: 10px;
|
||||
|
@ -196,13 +205,13 @@ $dash-graph-options-arrow: 8px;
|
|||
}
|
||||
> .source-indicator {
|
||||
height: 24px;
|
||||
|
||||
> .icon {
|
||||
font-size: 12px;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.dash-graph-context {
|
||||
z-index: 2;
|
||||
position: absolute;
|
||||
|
@ -213,12 +222,15 @@ $dash-graph-options-arrow: 8px;
|
|||
align-items: center;
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
|
||||
.dash-graph-context.dash-graph-context__open {
|
||||
z-index: 20;
|
||||
}
|
||||
|
||||
.dash-graph-context--buttons {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.dash-graph-context--button {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
|
@ -228,7 +240,6 @@ $dash-graph-options-arrow: 8px;
|
|||
color: $g11-sidewalk;
|
||||
margin-right: 2px;
|
||||
transition: color 0.25s ease, background-color 0.25s ease;
|
||||
|
||||
&:hover,
|
||||
&.active {
|
||||
cursor: pointer;
|
||||
|
@ -238,7 +249,6 @@ $dash-graph-options-arrow: 8px;
|
|||
&:last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
> .icon {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
|
@ -250,6 +260,7 @@ $dash-graph-options-arrow: 8px;
|
|||
z-index: 20;
|
||||
}
|
||||
}
|
||||
|
||||
.dash-graph-context--menu,
|
||||
.dash-graph-context--menu.default {
|
||||
z-index: 3;
|
||||
|
@ -263,7 +274,6 @@ $dash-graph-options-arrow: 8px;
|
|||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
justify-content: center;
|
||||
|
||||
&:before {
|
||||
position: absolute;
|
||||
content: '';
|
||||
|
@ -274,7 +284,6 @@ $dash-graph-options-arrow: 8px;
|
|||
transform: translate(-50%, -100%);
|
||||
transition: border-color 0.25s ease;
|
||||
}
|
||||
|
||||
.dash-graph-context--menu-item {
|
||||
@include no-user-select();
|
||||
white-space: nowrap;
|
||||
|
@ -285,7 +294,6 @@ $dash-graph-options-arrow: 8px;
|
|||
padding: 0 10px;
|
||||
color: $g20-white;
|
||||
transition: background-color 0.25s ease;
|
||||
|
||||
&:first-child {
|
||||
border-top-left-radius: 3px;
|
||||
border-top-right-radius: 3px;
|
||||
|
@ -298,7 +306,6 @@ $dash-graph-options-arrow: 8px;
|
|||
background-color: $g8-storm;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&.disabled,
|
||||
&.disabled:hover {
|
||||
cursor: default;
|
||||
|
@ -318,6 +325,7 @@ $dash-graph-options-arrow: 8px;
|
|||
background-color: $c-pool;
|
||||
}
|
||||
}
|
||||
|
||||
.dash-graph-context--menu.warning {
|
||||
background-color: $c-star;
|
||||
&:before {
|
||||
|
@ -327,6 +335,7 @@ $dash-graph-options-arrow: 8px;
|
|||
background-color: $c-comet;
|
||||
}
|
||||
}
|
||||
|
||||
.dash-graph-context--menu.success {
|
||||
background-color: $c-rainforest;
|
||||
&:before {
|
||||
|
@ -336,6 +345,7 @@ $dash-graph-options-arrow: 8px;
|
|||
background-color: $c-honeydew;
|
||||
}
|
||||
}
|
||||
|
||||
.dash-graph-context--menu.danger {
|
||||
background-color: $c-curacao;
|
||||
&:before {
|
||||
|
@ -347,6 +357,7 @@ $dash-graph-options-arrow: 8px;
|
|||
}
|
||||
|
||||
/* Presentation Mode */
|
||||
|
||||
.presentation-mode {
|
||||
.dash-graph-context {
|
||||
display: none;
|
||||
|
@ -364,7 +375,6 @@ $dash-graph-options-arrow: 8px;
|
|||
transform: translateX(50%);
|
||||
width: 16px;
|
||||
height: 18px;
|
||||
|
||||
> div {
|
||||
width: 4px;
|
||||
height: 4px;
|
||||
|
@ -374,7 +384,6 @@ $dash-graph-options-arrow: 8px;
|
|||
top: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
|
||||
div:nth-child(1) {
|
||||
left: 0;
|
||||
animation: refreshingSpinnerA 0.8s cubic-bezier(0.645, 0.045, 0.355, 1)
|
||||
|
@ -396,13 +405,15 @@ $dash-graph-options-arrow: 8px;
|
|||
Dashboard Edit Mode
|
||||
------------------------------------------------------
|
||||
*/
|
||||
|
||||
.react-grid-placeholder {
|
||||
@include gradient-diag-down($c-pool,$c-comet);
|
||||
@include gradient-diag-down($c-pool, $c-comet);
|
||||
border: 0 !important;
|
||||
opacity: 0.3;
|
||||
z-index: 2;
|
||||
border-radius: $radius !important;
|
||||
}
|
||||
|
||||
.react-grid-item {
|
||||
&.resizing {
|
||||
background-color: fade-out($g3-castle, 0.09);
|
||||
|
@ -413,7 +424,6 @@ $dash-graph-options-arrow: 8px;
|
|||
border-image-width: 2px;
|
||||
border-image-source: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGQAAABkCAYAAABw4pVUAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyhpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuNi1jMDY3IDc5LjE1Nzc0NywgMjAxNS8wMy8zMC0yMzo0MDo0MiAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENDIDIwMTUgKE1hY2ludG9zaCkiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6MTg0NjVDRkVGMEVFMTFFNkE0QjVFRTJGNEI1ODc0RDMiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6MTg0NjVDRkZGMEVFMTFFNkE0QjVFRTJGNEI1ODc0RDMiPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDoxODQ2NUNGQ0YwRUUxMUU2QTRCNUVFMkY0QjU4NzREMyIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDoxODQ2NUNGREYwRUUxMUU2QTRCNUVFMkY0QjU4NzREMyIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/PpeetfIAAAMnSURBVHja7N1PatwwFMfxJ5NlKT1DIfQKWZfSA/Q0hexDL9McoOQAPUKglwhp6dZ9Ho/HfyTZs6l+b/E1GDm27IH5oH9Pyji9//7XfLtNZt88/eT722TzlvrFseXHaXFmypuO8vd5nmW6nyeNefrKfZv7i9f75blU/NzafXvns2dV7tl8zqsnT55+9f3Xjf/xwQ9+evou+xLB+N8Ydi4AX3z/6PnvOj94AEOGMV/rB4P00J2rKTC0GNOTPne0GWEwhv1NB0YYjNPWgREHI00gYMTAOIGAEQdjuKcDIw7GXGWBEQJjrLLACIORrFBlgaHDsG2VBYYWY1VlgaHHSH3WqIOhxLB1ow6GGmPRqIMRAeMMAkYUDFuGTsDQYwxP6MCIg1Hp9oKhwih0e8FQYthuLAuM5hj1WBYYEoxUjGWBIcOwrFEHQ4qxLiFgyDFOvSww4mCM8yFghMEoDgzB0GGk2owhGBoMq5UQMDQYxRIChg4ja0PA0GLYMrgIhh7jUkLAiIExV1lghMA4GBiC0RrjNIULRhyMysAQDBVGYWAIhhJjM6cOhhpjUULAiIAxr1wEIwTGPDAEIwTGWGWBEQajHu0FQ4JRjvaCIcPIo71gSDHW0V4w5Bj5SB0MKUZxoRwYOoxsPgQMLcZqPgQMPUaxUQdDh2HVcQgYEoxUHIeAIcPIqywwpBjrKgsMOcb8f+pghMDIwu9gaDFWI3Uw9Bg2N+pgRMA497LAiIJRXf0OhgajuPodDB3G1dFeMNpgXBXtBaMdxmG0F4y2GLvRXjDaY2wGhmCoMawU7QVDh5G20V4wtBjzwBCMEBiXVx6BEQPjsJcFRluM3V4WGO0xqr0sMDQYVuplgaHDWL1YEgw9hi17WWDoMVJ1ChcMCYYVp3DBkGFUl5KCocGw6deAwIiBYUfBRTDaYmTdXjC0GFYLLoKhwSj+cAAYOgzbBhfB0GKsgotg6DGuWrkIRjuMudsLRgiMsQ0BIwzG5ZVHYMTAmKqsVzBiYPj2Z+j2PoERAmM4/2MoIfe+v4Ahx3jx5H4AefYLd37q0Y9/g9EcY/jOHz11A3v+J8AA9wisahRCWTQAAAAASUVORK5CYII=);
|
||||
z-index: 3;
|
||||
|
||||
& > .react-resizable-handle {
|
||||
&:before,
|
||||
&:after {
|
||||
|
@ -433,7 +443,6 @@ $dash-graph-options-arrow: 8px;
|
|||
&:hover {
|
||||
cursor: move;
|
||||
}
|
||||
|
||||
.dash-graph--heading {
|
||||
background-color: $g5-pepper;
|
||||
cursor: move;
|
||||
|
@ -442,7 +451,6 @@ $dash-graph-options-arrow: 8px;
|
|||
& > .react-resizable-handle {
|
||||
background-image: none;
|
||||
cursor: nwse-resize;
|
||||
|
||||
&:before,
|
||||
&:after {
|
||||
content: '';
|
||||
|
@ -477,28 +485,29 @@ $dash-graph-options-arrow: 8px;
|
|||
Dashboard Empty State
|
||||
------------------------------------------------------
|
||||
*/
|
||||
@import '../components/dashboard-empty';
|
||||
|
||||
@import '../components/dashboard-empty';
|
||||
/*
|
||||
Template Control Bar
|
||||
------------------------------------------------------
|
||||
*/
|
||||
@import '../components/template-control-bar';
|
||||
|
||||
@import '../components/template-control-bar';
|
||||
/*
|
||||
Cell Editor Overlay
|
||||
------------------------------------------------------
|
||||
*/
|
||||
@import 'cell-editor-overlay';
|
||||
|
||||
@import 'cell-editor-overlay';
|
||||
/*
|
||||
Template Variables Manager
|
||||
------------------------------------------------------
|
||||
*/
|
||||
@import '../components/template-variables-manager';
|
||||
|
||||
@import '../components/template-variables-manager';
|
||||
/*
|
||||
Write Data Form
|
||||
------------------------------------------------------
|
||||
*/
|
||||
|
||||
@import '../components/write-data-form';
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
/*
|
||||
Styles for IFQL Builder aka TIME MACHINE aka DELOREAN
|
||||
----------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
@import '../components/time-machine/ifql-editor';
|
||||
@import '../components/time-machine/ifql-builder';
|
||||
@import '../components/time-machine/ifql-explorer';
|
||||
@import '../components/time-machine/visualization';
|
||||
@import '../components/time-machine/add-func-button';
|
|
@ -51,7 +51,7 @@ export interface Func {
|
|||
|
||||
type Value = string | boolean
|
||||
|
||||
interface Arg {
|
||||
export interface Arg {
|
||||
key: string
|
||||
value: Value
|
||||
type: string
|
||||
|
|
|
@ -166,21 +166,31 @@ const insertGroupByValues = (
|
|||
sortedLabels
|
||||
) => {
|
||||
const dashArray = Array(sortedLabels.length).fill('-')
|
||||
let timeSeries = []
|
||||
forEach(serieses, (s, sind) => {
|
||||
if (s.isGroupBy) {
|
||||
forEach(s.values, vs => {
|
||||
const timeSeries = []
|
||||
|
||||
for (let x = 0; x < serieses.length; x++) {
|
||||
const s = serieses[x]
|
||||
if (!s.isGroupBy) {
|
||||
continue
|
||||
}
|
||||
|
||||
for (let i = 0; i < s.values.length; i++) {
|
||||
const vs = s.values[i]
|
||||
const tsRow = {time: vs[0], values: clone(dashArray)}
|
||||
forEach(vs.slice(1), (v, i) => {
|
||||
const label = seriesLabels[sind][i].label
|
||||
|
||||
const vss = vs.slice(1)
|
||||
for (let j = 0; j < vss.length; j++) {
|
||||
const v = vss[j]
|
||||
const label = seriesLabels[x][j].label
|
||||
|
||||
tsRow.values[
|
||||
labelsToValueIndex[label + s.responseIndex + s.seriesIndex]
|
||||
] = v
|
||||
})
|
||||
timeSeries = [...timeSeries, tsRow]
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
timeSeries.push(tsRow)
|
||||
}
|
||||
}
|
||||
|
||||
return timeSeries
|
||||
}
|
||||
|
@ -245,7 +255,6 @@ const constructTimeSeries = (serieses, cells, sortedLabels, seriesLabels) => {
|
|||
export const groupByTimeSeriesTransform = (raw, isTable) => {
|
||||
const results = constructResults(raw, isTable)
|
||||
const serieses = constructSerieses(results)
|
||||
|
||||
const {cells, sortedLabels, seriesLabels} = constructCells(serieses)
|
||||
|
||||
const sortedTimeSeries = constructTimeSeries(
|
||||
|
|
|
@ -0,0 +1,46 @@
|
|||
import {dataToCSV, formatDate} from 'shared/parsing/dataToCSV'
|
||||
import moment from 'moment'
|
||||
|
||||
describe('formatDate', () => {
|
||||
it('converts timestamp to an excel compatible date string', () => {
|
||||
const timestamp = 1000000000000
|
||||
const result = formatDate(timestamp)
|
||||
expect(moment(result, 'M/D/YYYY h:mm:ss.SSSSSSSSS A').valueOf()).toBe(
|
||||
timestamp
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('dataToCSV', () => {
|
||||
it('parses data, an array of arrays, to a csv string', () => {
|
||||
const data = [[1, 2], [3, 4], [5, 6], [7, 8]]
|
||||
const returned = dataToCSV(data)
|
||||
const expected = `1,2\n3,4\n5,6\n7,8`
|
||||
|
||||
expect(returned).toEqual(expected)
|
||||
})
|
||||
|
||||
it('converts values to dates if title of first column is time.', () => {
|
||||
const data = [
|
||||
['time', 'something'],
|
||||
[1505262600000, 0.06163066773148772],
|
||||
[1505264400000, 2.616484718180463],
|
||||
[1505266200000, 1.6174323943535571],
|
||||
]
|
||||
const returned = dataToCSV(data)
|
||||
const expected = `date,something\n${formatDate(
|
||||
1505262600000
|
||||
)},0.06163066773148772\n${formatDate(
|
||||
1505264400000
|
||||
)},2.616484718180463\n${formatDate(1505266200000)},1.6174323943535571`
|
||||
|
||||
expect(returned).toEqual(expected)
|
||||
})
|
||||
|
||||
it('returns an empty string if data is empty', () => {
|
||||
const data = [[]]
|
||||
const returned = dataToCSV(data)
|
||||
const expected = ''
|
||||
expect(returned).toEqual(expected)
|
||||
})
|
||||
})
|
|
@ -1,105 +0,0 @@
|
|||
import {
|
||||
resultsToCSV,
|
||||
formatDate,
|
||||
dashboardtoCSV,
|
||||
} from 'shared/parsing/resultsToCSV'
|
||||
import moment from 'moment'
|
||||
|
||||
describe('formatDate', () => {
|
||||
it('converts timestamp to an excel compatible date string', () => {
|
||||
const timestamp = 1000000000000
|
||||
const result = formatDate(timestamp)
|
||||
expect(moment(result, 'M/D/YYYY h:mm:ss.SSSSSSSSS A').valueOf()).toBe(
|
||||
timestamp
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('resultsToCSV', () => {
|
||||
it('parses results, a time series data structure, to an object with name and CSVString keys', () => {
|
||||
const results = [
|
||||
{
|
||||
statement_id: 0,
|
||||
series: [
|
||||
{
|
||||
name: 'procstat',
|
||||
columns: ['time', 'mean_cpu_usage'],
|
||||
values: [
|
||||
[1505262600000, 0.06163066773148772],
|
||||
[1505264400000, 2.616484718180463],
|
||||
[1505266200000, 1.6174323943535571],
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
const response = resultsToCSV(results)
|
||||
const expected = {
|
||||
flag: 'ok',
|
||||
name: 'procstat',
|
||||
CSVString: `date,mean_cpu_usage\n${formatDate(
|
||||
1505262600000
|
||||
)},0.06163066773148772\n${formatDate(
|
||||
1505264400000
|
||||
)},2.616484718180463\n${formatDate(1505266200000)},1.6174323943535571`,
|
||||
}
|
||||
expect(Object.keys(response).sort()).toEqual(
|
||||
['flag', 'name', 'CSVString'].sort()
|
||||
)
|
||||
expect(response.flag).toBe(expected.flag)
|
||||
expect(response.name).toBe(expected.name)
|
||||
expect(response.CSVString).toBe(expected.CSVString)
|
||||
})
|
||||
})
|
||||
|
||||
describe('dashboardtoCSV', () => {
|
||||
it('parses the array of timeseries data displayed by the dashboard cell to a CSVstring for download', () => {
|
||||
const data = [
|
||||
{
|
||||
results: [
|
||||
{
|
||||
statement_id: 0,
|
||||
series: [
|
||||
{
|
||||
name: 'procstat',
|
||||
columns: ['time', 'mean_cpu_usage'],
|
||||
values: [
|
||||
[1505262600000, 0.06163066773148772],
|
||||
[1505264400000, 2.616484718180463],
|
||||
[1505266200000, 1.6174323943535571],
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
results: [
|
||||
{
|
||||
statement_id: 0,
|
||||
series: [
|
||||
{
|
||||
name: 'procstat',
|
||||
columns: ['not-time', 'mean_cpu_usage'],
|
||||
values: [
|
||||
[1505262600000, 0.06163066773148772],
|
||||
[1505264400000, 2.616484718180463],
|
||||
[1505266200000, 1.6174323943535571],
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
const result = dashboardtoCSV(data)
|
||||
const expected = `time,mean_cpu_usage,not-time,mean_cpu_usage\n${formatDate(
|
||||
1505262600000
|
||||
)},0.06163066773148772,1505262600000,0.06163066773148772\n${formatDate(
|
||||
1505264400000
|
||||
)},2.616484718180463,1505264400000,2.616484718180463\n${formatDate(
|
||||
1505266200000
|
||||
)},1.6174323943535571,1505266200000,1.6174323943535571`
|
||||
expect(result).toBe(expected)
|
||||
})
|
||||
})
|
|
@ -36,7 +36,7 @@ module.exports = {
|
|||
},
|
||||
watch: true,
|
||||
cache: true,
|
||||
devtool: 'source-map',
|
||||
devtool: 'cheap-eval-source-map',
|
||||
entry: {
|
||||
app: path.resolve(__dirname, '..', 'src', 'index.tsx'),
|
||||
},
|
||||
|
|
Loading…
Reference in New Issue