Merge branch 'master' into feature/kapa_kafka_alert_node

pull/10616/head
Jared Scheib 2018-05-09 13:53:22 -07:00 committed by GitHub
commit fc87409adf
78 changed files with 4214 additions and 683 deletions

View File

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

View File

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

View File

@ -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 == "/" {

View File

@ -3678,7 +3678,9 @@
"http",
"hipchat",
"opsgenie",
"opsgenie2",
"pagerduty",
"pagerduty2",
"victorops",
"email",
"exec",

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -15,6 +15,7 @@ export const MINIMUM_HEIGHTS = {
queryMaker: 350,
visualization: 200,
}
export const INITIAL_HEIGHTS = {
queryMaker: '66.666%',
visualization: '33.334%',

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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: [],
},
}

View File

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

View File

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

View File

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

View File

@ -1,5 +1,5 @@
import PropTypes from 'prop-types'
import React, {Component} from 'react'
import PropTypes from 'prop-types'
import _ from 'lodash'

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,2 @@
import MultiGrid from './MultiGrid'
export {MultiGrid}

View File

@ -118,6 +118,7 @@ const RefreshingGraph = ({
decimalPlaces={decimalPlaces}
editQueryStatus={editQueryStatus}
resizerTopHeight={resizerTopHeight}
grabDataForDownload={grabDataForDownload}
handleSetHoverTime={handleSetHoverTime}
isInCEO={isInCEO}
/>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,3 +1,8 @@
/*
IFQL Code Mirror Editor
----------------------------------------------------------------------------
*/
.time-machine-container {
display: flex;
height: 90%;

View File

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

View File

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

View File

@ -23,8 +23,10 @@
<glyph unicode="&#xe90c;" 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="&#xe90d;" 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="&#xe90e;" 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="&#xe90f;" 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="&#xe910;" 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="&#xe911;" 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="&#xe912;" 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="&#xe916;" 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="&#xe917;" 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="&#xe918;" 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.

View File

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

View File

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

View File

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

View File

@ -51,7 +51,7 @@ export interface Func {
type Value = string | boolean
interface Arg {
export interface Arg {
key: string
value: Value
type: string

View File

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

View File

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

View File

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

View File

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