Commit f1220fd2 by David Kaltschmidt

Explore WIP

parent 1dd4f031
......@@ -22,6 +22,7 @@
"axios": "^0.17.1",
"babel-core": "^6.26.0",
"babel-loader": "^7.1.2",
"babel-plugin-syntax-dynamic-import": "^6.18.0",
"babel-preset-es2015": "^6.24.1",
"clean-webpack-plugin": "^0.1.19",
"css-loader": "^0.28.7",
......@@ -150,6 +151,7 @@
"d3-scale-chromatic": "^1.1.1",
"eventemitter3": "^2.0.3",
"file-saver": "^1.3.3",
"immutable": "^3.8.2",
"jquery": "^3.2.1",
"lodash": "^4.17.4",
"mobx": "^3.4.1",
......@@ -158,6 +160,7 @@
"moment": "^2.18.1",
"mousetrap": "^1.6.0",
"mousetrap-global-bind": "^1.1.0",
"prismjs": "^1.6.0",
"prop-types": "^15.6.0",
"react": "^16.2.0",
"react-dom": "^16.2.0",
......@@ -170,6 +173,9 @@
"remarkable": "^1.7.1",
"rst2html": "github:thoward/rst2html#990cb89",
"rxjs": "^5.4.3",
"slate": "^0.33.4",
"slate-plain-serializer": "^0.5.10",
"slate-react": "^0.12.4",
"tether": "^1.4.0",
"tether-drop": "https://github.com/torkelo/drop/tarball/master",
"tinycolor2": "^1.4.1"
......
......@@ -117,6 +117,17 @@ func setIndexViewData(c *m.ReqContext) (*dtos.IndexViewData, error) {
Children: dashboardChildNavs,
})
// data.NavTree = append(data.NavTree, &dtos.NavLink{
// Text: "Explore",
// Id: "explore",
// SubTitle: "Explore your data",
// Icon: "fa fa-rocket",
// Url: setting.AppSubUrl + "/explore",
// Children: []*dtos.NavLink{
// {Text: "New tab", Icon: "gicon gicon-dashboard-new", Url: setting.AppSubUrl + "/explore/new"},
// },
// })
if c.IsSignedIn {
// Only set login if it's different from the name
var login string
......
import React, { PureComponent } from 'react';
const INTERVAL = 150;
export default class ElapsedTime extends PureComponent<any, any> {
offset: number;
timer: NodeJS.Timer;
state = {
elapsed: 0,
};
start() {
this.offset = Date.now();
this.timer = setInterval(this.tick, INTERVAL);
}
tick = () => {
const jetzt = Date.now();
const elapsed = jetzt - this.offset;
this.setState({ elapsed });
};
componentWillReceiveProps(nextProps) {
if (nextProps.time) {
clearInterval(this.timer);
} else if (this.props.time) {
this.start();
}
}
componentDidMount() {
this.start();
}
componentWillUnmount() {
clearInterval(this.timer);
}
render() {
const { elapsed } = this.state;
const { className, time } = this.props;
const value = (time || elapsed) / 1000;
return <span className={className}>{value.toFixed(1)}s</span>;
}
}
import React from 'react';
import { hot } from 'react-hot-loader';
import colors from 'app/core/utils/colors';
import TimeSeries from 'app/core/time_series2';
import ElapsedTime from './ElapsedTime';
import Legend from './Legend';
import QueryField from './QueryField';
import Graph from './Graph';
import Table from './Table';
import { DatasourceSrv } from 'app/features/plugins/datasource_srv';
function buildQueryOptions({ format, interval, instant, now, query }) {
const to = now;
const from = to - 1000 * 60 * 60 * 3;
return {
interval,
range: {
from,
to,
},
targets: [
{
expr: query,
format,
instant,
},
],
};
}
function makeTimeSeriesList(dataList, options) {
return dataList.map((seriesData, index) => {
const datapoints = seriesData.datapoints || [];
const alias = seriesData.target;
const colorIndex = index % colors.length;
const color = colors[colorIndex];
const series = new TimeSeries({
datapoints: datapoints,
alias: alias,
color: color,
unit: seriesData.unit,
});
if (datapoints && datapoints.length > 0) {
const last = datapoints[datapoints.length - 1][1];
const from = options.range.from;
if (last - from < -10000) {
series.isOutsideRange = true;
}
}
return series;
});
}
interface IExploreState {
datasource: any;
datasourceError: any;
datasourceLoading: any;
graphResult: any;
latency: number;
loading: any;
requestOptions: any;
showingGraph: boolean;
showingTable: boolean;
tableResult: any;
}
// @observer
export class Explore extends React.Component<any, IExploreState> {
datasourceSrv: DatasourceSrv;
query: string;
constructor(props) {
super(props);
this.state = {
datasource: null,
datasourceError: null,
datasourceLoading: true,
graphResult: null,
latency: 0,
loading: false,
requestOptions: null,
showingGraph: true,
showingTable: true,
tableResult: null,
};
}
async componentDidMount() {
const datasource = await this.props.datasourceSrv.get();
const testResult = await datasource.testDatasource();
if (testResult.status === 'success') {
this.setState({ datasource, datasourceError: null, datasourceLoading: false });
} else {
this.setState({ datasource: null, datasourceError: testResult.message, datasourceLoading: false });
}
}
handleClickGraphButton = () => {
this.setState(state => ({ showingGraph: !state.showingGraph }));
};
handleClickTableButton = () => {
this.setState(state => ({ showingTable: !state.showingTable }));
};
handleRequestError({ error }) {
console.error(error);
}
handleQueryChange = query => {
this.query = query;
};
handleSubmit = () => {
const { showingGraph, showingTable } = this.state;
if (showingTable) {
this.runTableQuery();
}
if (showingGraph) {
this.runGraphQuery();
}
};
async runGraphQuery() {
const { query } = this;
const { datasource } = this.state;
if (!query) {
return;
}
this.setState({ latency: 0, loading: true, graphResult: null });
const now = Date.now();
const options = buildQueryOptions({
format: 'time_series',
interval: datasource.interval,
instant: false,
now,
query,
});
try {
const res = await datasource.query(options);
const result = makeTimeSeriesList(res.data, options);
const latency = Date.now() - now;
this.setState({ latency, loading: false, graphResult: result, requestOptions: options });
} catch (error) {
console.error(error);
this.setState({ loading: false, graphResult: error });
}
}
async runTableQuery() {
const { query } = this;
const { datasource } = this.state;
if (!query) {
return;
}
this.setState({ latency: 0, loading: true, tableResult: null });
const now = Date.now();
const options = buildQueryOptions({ format: 'table', interval: datasource.interval, instant: true, now, query });
try {
const res = await datasource.query(options);
const tableModel = res.data[0];
const latency = Date.now() - now;
this.setState({ latency, loading: false, tableResult: tableModel, requestOptions: options });
} catch (error) {
console.error(error);
this.setState({ loading: false, tableResult: null });
}
}
request = url => {
const { datasource } = this.state;
return datasource.metadataRequest(url);
};
render() {
const {
datasource,
datasourceError,
datasourceLoading,
latency,
loading,
requestOptions,
graphResult,
showingGraph,
showingTable,
tableResult,
} = this.state;
const showingBoth = showingGraph && showingTable;
const graphHeight = showingBoth ? '200px' : null;
const graphButtonClassName = showingBoth || showingGraph ? 'btn m-r-1' : 'btn btn-inverse m-r-1';
const tableButtonClassName = showingBoth || showingTable ? 'btn m-r-1' : 'btn btn-inverse m-r-1';
return (
<div className="explore">
<div className="page-body page-full">
<h2 className="page-sub-heading">Explore</h2>
{datasourceLoading ? <div>Loading datasource...</div> : null}
{datasourceError ? <div title={datasourceError}>Error connecting to datasource.</div> : null}
{datasource ? (
<div className="m-r-3">
<div className="nav m-b-1">
<div className="pull-right" style={{ paddingRight: '6rem' }}>
<button type="submit" className="m-l-1 btn btn-primary" onClick={this.handleSubmit}>
<i className="fa fa-return" /> Run Query
</button>
</div>
<div>
<button className={graphButtonClassName} onClick={this.handleClickGraphButton}>
Graph
</button>
<button className={tableButtonClassName} onClick={this.handleClickTableButton}>
Table
</button>
</div>
</div>
<div className="query-field-wrapper">
<QueryField
request={this.request}
onPressEnter={this.handleSubmit}
onQueryChange={this.handleQueryChange}
onRequestError={this.handleRequestError}
/>
</div>
{loading || latency ? <ElapsedTime time={latency} className="m-l-1" /> : null}
<main className="m-t-2">
{showingGraph ? (
<Graph data={graphResult} id="explore-1" options={requestOptions} height={graphHeight} />
) : null}
{showingGraph ? <Legend data={graphResult} /> : null}
{showingTable ? <Table data={tableResult} className="m-t-3" /> : null}
</main>
</div>
) : null}
</div>
</div>
);
}
}
export default hot(module)(Explore);
import $ from 'jquery';
import React, { Component } from 'react';
import TimeSeries from 'app/core/time_series2';
import 'vendor/flot/jquery.flot';
import 'vendor/flot/jquery.flot.time';
// Copied from graph.ts
function time_format(ticks, min, max) {
if (min && max && ticks) {
var range = max - min;
var secPerTick = range / ticks / 1000;
var oneDay = 86400000;
var oneYear = 31536000000;
if (secPerTick <= 45) {
return '%H:%M:%S';
}
if (secPerTick <= 7200 || range <= oneDay) {
return '%H:%M';
}
if (secPerTick <= 80000) {
return '%m/%d %H:%M';
}
if (secPerTick <= 2419200 || range <= oneYear) {
return '%m/%d';
}
return '%Y-%m';
}
return '%H:%M';
}
const FLOT_OPTIONS = {
legend: {
show: false,
},
series: {
lines: {
linewidth: 1,
zero: false,
},
shadowSize: 0,
},
grid: {
minBorderMargin: 0,
markings: [],
backgroundColor: null,
borderWidth: 0,
// hoverable: true,
clickable: true,
color: '#a1a1a1',
margin: { left: 0, right: 0 },
labelMarginX: 0,
},
// selection: {
// mode: 'x',
// color: '#666',
// },
// crosshair: {
// mode: 'x',
// },
};
class Graph extends Component<any, any> {
componentDidMount() {
this.draw();
}
componentDidUpdate(prevProps) {
if (
prevProps.data !== this.props.data ||
prevProps.options !== this.props.options ||
prevProps.height !== this.props.height
) {
this.draw();
}
}
draw() {
const { data, options: userOptions } = this.props;
if (!data) {
return;
}
const series = data.map((ts: TimeSeries) => ({
label: ts.label,
data: ts.getFlotPairs('null'),
}));
const $el = $(`#${this.props.id}`);
const ticks = $el.width() / 100;
const min = userOptions.range.from.valueOf();
const max = userOptions.range.to.valueOf();
const dynamicOptions = {
xaxis: {
mode: 'time',
min: min,
max: max,
label: 'Datetime',
ticks: ticks,
timeformat: time_format(ticks, min, max),
},
};
const options = {
...FLOT_OPTIONS,
...dynamicOptions,
...userOptions,
};
$.plot($el, series, options);
}
render() {
const style = {
height: this.props.height || '400px',
width: this.props.width || '100%',
};
return <div id={this.props.id} style={style} />;
}
}
export default Graph;
import React, { PureComponent } from 'react';
const LegendItem = ({ series }) => (
<div className="graph-legend-series">
<div className="graph-legend-icon">
<i className="fa fa-minus pointer" style={{ color: series.color }} />
</div>
<a className="graph-legend-alias pointer">{series.alias}</a>
</div>
);
export default class Legend extends PureComponent<any, any> {
render() {
const { className = '', data } = this.props;
const items = data || [];
return (
<div className={`${className} graph-legend ps`}>
{items.map(series => <LegendItem key={series.id} series={series} />)}
</div>
);
}
}
import React, { PureComponent } from 'react';
// import TableModel from 'app/core/table_model';
const EMPTY_TABLE = {
columns: [],
rows: [],
};
export default class Table extends PureComponent<any, any> {
render() {
const { className = '', data } = this.props;
const tableModel = data || EMPTY_TABLE;
return (
<table className={`${className} filter-table`}>
<thead>
<tr>{tableModel.columns.map(col => <th key={col.text}>{col.text}</th>)}</tr>
</thead>
<tbody>
{tableModel.rows.map((row, i) => <tr key={i}>{row.map((content, j) => <td key={j}>{content}</td>)}</tr>)}
</tbody>
</table>
);
}
}
import React from 'react';
function scrollIntoView(el) {
if (!el || !el.offsetParent) {
return;
}
const container = el.offsetParent;
if (el.offsetTop > container.scrollTop + container.offsetHeight || el.offsetTop < container.scrollTop) {
container.scrollTop = el.offsetTop - container.offsetTop;
}
}
class TypeaheadItem extends React.PureComponent<any, any> {
el: any;
componentDidUpdate(prevProps) {
if (this.props.isSelected && !prevProps.isSelected) {
scrollIntoView(this.el);
}
}
getRef = el => {
this.el = el;
};
render() {
const { isSelected, label, onClickItem } = this.props;
const className = isSelected ? 'typeahead-item typeahead-item__selected' : 'typeahead-item';
const onClick = () => onClickItem(label);
return (
<li ref={this.getRef} className={className} onClick={onClick}>
{label}
</li>
);
}
}
class TypeaheadGroup extends React.PureComponent<any, any> {
render() {
const { items, label, selected, onClickItem } = this.props;
return (
<li className="typeahead-group">
<div className="typeahead-group__title">{label}</div>
<ul className="typeahead-group__list">
{items.map(item => (
<TypeaheadItem key={item} onClickItem={onClickItem} isSelected={selected.indexOf(item) > -1} label={item} />
))}
</ul>
</li>
);
}
}
class Typeahead extends React.PureComponent<any, any> {
render() {
const { groupedItems, menuRef, selectedItems, onClickItem } = this.props;
return (
<ul className="typeahead" ref={menuRef}>
{groupedItems.map(g => (
<TypeaheadGroup key={g.label} onClickItem={onClickItem} selected={selectedItems} {...g} />
))}
</ul>
);
}
}
export default Typeahead;
import Plain from 'slate-plain-serializer';
import BracesPlugin from './braces';
declare global {
interface Window {
KeyboardEvent: any;
}
}
describe('braces', () => {
const handler = BracesPlugin().onKeyDown;
it('adds closing braces around empty value', () => {
const change = Plain.deserialize('').change();
const event = new window.KeyboardEvent('keydown', { key: '(' });
handler(event, change);
expect(Plain.serialize(change.value)).toEqual('()');
});
it('adds closing braces around a value', () => {
const change = Plain.deserialize('foo').change();
const event = new window.KeyboardEvent('keydown', { key: '(' });
handler(event, change);
expect(Plain.serialize(change.value)).toEqual('(foo)');
});
it('adds closing braces around the following value only', () => {
const change = Plain.deserialize('foo bar ugh').change();
let event;
event = new window.KeyboardEvent('keydown', { key: '(' });
handler(event, change);
expect(Plain.serialize(change.value)).toEqual('(foo) bar ugh');
// Wrap bar
change.move(5);
event = new window.KeyboardEvent('keydown', { key: '(' });
handler(event, change);
expect(Plain.serialize(change.value)).toEqual('(foo) (bar) ugh');
// Create empty parens after (bar)
change.move(4);
event = new window.KeyboardEvent('keydown', { key: '(' });
handler(event, change);
expect(Plain.serialize(change.value)).toEqual('(foo) (bar)() ugh');
});
});
const BRACES = {
'[': ']',
'{': '}',
'(': ')',
};
export default function BracesPlugin() {
return {
onKeyDown(event, change) {
const { value } = change;
if (!value.isCollapsed) {
return undefined;
}
switch (event.key) {
case '{':
case '[': {
event.preventDefault();
// Insert matching braces
change
.insertText(`${event.key}${BRACES[event.key]}`)
.move(-1)
.focus();
return true;
}
case '(': {
event.preventDefault();
const text = value.anchorText.text;
const offset = value.anchorOffset;
const space = text.indexOf(' ', offset);
const length = space > 0 ? space : text.length;
const forward = length - offset;
// Insert matching braces
change
.insertText(event.key)
.move(forward)
.insertText(BRACES[event.key])
.move(-1 - forward)
.focus();
return true;
}
default: {
break;
}
}
return undefined;
},
};
}
import Plain from 'slate-plain-serializer';
import ClearPlugin from './clear';
describe('clear', () => {
const handler = ClearPlugin().onKeyDown;
it('does not change the empty value', () => {
const change = Plain.deserialize('').change();
const event = new window.KeyboardEvent('keydown', {
key: 'k',
ctrlKey: true,
});
handler(event, change);
expect(Plain.serialize(change.value)).toEqual('');
});
it('clears to the end of the line', () => {
const change = Plain.deserialize('foo').change();
const event = new window.KeyboardEvent('keydown', {
key: 'k',
ctrlKey: true,
});
handler(event, change);
expect(Plain.serialize(change.value)).toEqual('');
});
it('clears from the middle to the end of the line', () => {
const change = Plain.deserialize('foo bar').change();
change.move(4);
const event = new window.KeyboardEvent('keydown', {
key: 'k',
ctrlKey: true,
});
handler(event, change);
expect(Plain.serialize(change.value)).toEqual('foo ');
});
});
// Clears the rest of the line after the caret
export default function ClearPlugin() {
return {
onKeyDown(event, change) {
const { value } = change;
if (!value.isCollapsed) {
return undefined;
}
if (event.key === 'k' && event.ctrlKey) {
event.preventDefault();
const text = value.anchorText.text;
const offset = value.anchorOffset;
const length = text.length;
const forward = length - offset;
change.deleteForward(forward);
return true;
}
return undefined;
},
};
}
function getIndent(text) {
let offset = text.length - text.trimLeft().length;
if (offset) {
let indent = text[0];
while (--offset) {
indent += text[0];
}
return indent;
}
return '';
}
export default function NewlinePlugin() {
return {
onKeyDown(event, change) {
const { value } = change;
if (!value.isCollapsed) {
return undefined;
}
if (event.key === 'Enter' && event.shiftKey) {
event.preventDefault();
const { startBlock } = value;
const currentLineText = startBlock.text;
const indent = getIndent(currentLineText);
return change
.splitBlock()
.insertText(indent)
.focus();
}
},
};
}
import React from 'react';
import Prism from 'prismjs';
import Promql from './promql';
Prism.languages.promql = Promql;
const TOKEN_MARK = 'prism-token';
export function configurePrismMetricsTokens(metrics) {
Prism.languages.promql.metric = {
alias: 'variable',
pattern: new RegExp(`(?:^|\\s)(${metrics.join('|')})(?:$|\\s)`),
};
}
/**
* Code-highlighting plugin based on Prism and
* https://github.com/ianstormtaylor/slate/blob/master/examples/code-highlighting/index.js
*
* (Adapted to handle nested grammar definitions.)
*/
export default function PrismPlugin() {
return {
/**
* Render a Slate mark with appropiate CSS class names
*
* @param {Object} props
* @return {Element}
*/
renderMark(props) {
const { children, mark } = props;
// Only apply spans to marks identified by this plugin
if (mark.type !== TOKEN_MARK) {
return undefined;
}
const className = `token ${mark.data.get('types')}`;
return <span className={className}>{children}</span>;
},
/**
* Decorate code blocks with Prism.js highlighting.
*
* @param {Node} node
* @return {Array}
*/
decorateNode(node) {
if (node.type !== 'paragraph') {
return [];
}
const texts = node.getTexts().toArray();
const tstring = texts.map(t => t.text).join('\n');
const grammar = Prism.languages.promql;
const tokens = Prism.tokenize(tstring, grammar);
const decorations = [];
let startText = texts.shift();
let endText = startText;
let startOffset = 0;
let endOffset = 0;
let start = 0;
function processToken(token, acc?) {
// Accumulate token types down the tree
const types = `${acc || ''} ${token.type || ''} ${token.alias || ''}`;
// Add mark for token node
if (typeof token === 'string' || typeof token.content === 'string') {
startText = endText;
startOffset = endOffset;
const content = typeof token === 'string' ? token : token.content;
const newlines = content.split('\n').length - 1;
const length = content.length - newlines;
const end = start + length;
let available = startText.text.length - startOffset;
let remaining = length;
endOffset = startOffset + remaining;
while (available < remaining) {
endText = texts.shift();
remaining = length - available;
available = endText.text.length;
endOffset = remaining;
}
// Inject marks from up the tree (acc) as well
if (typeof token !== 'string' || acc) {
const range = {
anchorKey: startText.key,
anchorOffset: startOffset,
focusKey: endText.key,
focusOffset: endOffset,
marks: [{ type: TOKEN_MARK, data: { types } }],
};
decorations.push(range);
}
start = end;
} else if (token.content && token.content.length) {
// Tokens can be nested
for (const subToken of token.content) {
processToken(subToken, types);
}
}
}
// Process top-level tokens
for (const token of tokens) {
processToken(token);
}
return decorations;
},
};
}
export const OPERATORS = ['by', 'group_left', 'group_right', 'ignoring', 'on', 'offset', 'without'];
const AGGREGATION_OPERATORS = [
'sum',
'min',
'max',
'avg',
'stddev',
'stdvar',
'count',
'count_values',
'bottomk',
'topk',
'quantile',
];
export const FUNCTIONS = [
...AGGREGATION_OPERATORS,
'abs',
'absent',
'ceil',
'changes',
'clamp_max',
'clamp_min',
'count_scalar',
'day_of_month',
'day_of_week',
'days_in_month',
'delta',
'deriv',
'drop_common_labels',
'exp',
'floor',
'histogram_quantile',
'holt_winters',
'hour',
'idelta',
'increase',
'irate',
'label_replace',
'ln',
'log2',
'log10',
'minute',
'month',
'predict_linear',
'rate',
'resets',
'round',
'scalar',
'sort',
'sort_desc',
'sqrt',
'time',
'vector',
'year',
'avg_over_time',
'min_over_time',
'max_over_time',
'sum_over_time',
'count_over_time',
'quantile_over_time',
'stddev_over_time',
'stdvar_over_time',
];
const tokenizer = {
comment: {
pattern: /(^|[^\n])#.*/,
lookbehind: true,
},
'context-aggregation': {
pattern: /((by|without)\s*)\([^)]*\)/, // by ()
lookbehind: true,
inside: {
'label-key': {
pattern: /[^,\s][^,]*[^,\s]*/,
alias: 'attr-name',
},
},
},
'context-labels': {
pattern: /\{[^}]*(?=})/,
inside: {
'label-key': {
pattern: /[a-z_]\w*(?=\s*(=|!=|=~|!~))/,
alias: 'attr-name',
},
'label-value': {
pattern: /"(?:\\.|[^\\"])*"/,
greedy: true,
alias: 'attr-value',
},
},
},
function: new RegExp(`\\b(?:${FUNCTIONS.join('|')})(?=\\s*\\()`, 'i'),
'context-range': [
{
pattern: /\[[^\]]*(?=])/, // [1m]
inside: {
'range-duration': {
pattern: /\b\d+[smhdwy]\b/i,
alias: 'number',
},
},
},
{
pattern: /(offset\s+)\w+/, // offset 1m
lookbehind: true,
inside: {
'range-duration': {
pattern: /\b\d+[smhdwy]\b/i,
alias: 'number',
},
},
},
],
number: /\b-?\d+((\.\d*)?([eE][+-]?\d+)?)?\b/,
operator: new RegExp(`/[-+*/=%^~]|&&?|\\|?\\||!=?|<(?:=>?|<|>)?|>[>=]?|\\b(?:${OPERATORS.join('|')})\\b`, 'i'),
punctuation: /[{};()`,.]/,
};
export default tokenizer;
export default function RunnerPlugin({ handler }) {
return {
onKeyDown(event) {
// Handle enter
if (handler && event.key === 'Enter' && !event.shiftKey) {
// Submit on Enter
event.preventDefault();
handler(event);
return true;
}
return undefined;
},
};
}
// Based on underscore.js debounce()
export default function debounce(func, wait) {
let timeout;
return function() {
const context = this;
const args = arguments;
const later = function() {
timeout = null;
func.apply(context, args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
}
// Node.closest() polyfill
if ('Element' in window && !Element.prototype.closest) {
Element.prototype.closest = function(s) {
const matches = (this.document || this.ownerDocument).querySelectorAll(s);
let el = this;
let i;
// eslint-disable-next-line
do {
i = matches.length;
// eslint-disable-next-line
while (--i >= 0 && matches.item(i) !== el) {}
} while (i < 0 && (el = el.parentElement));
return el;
};
}
export function getPreviousCousin(node, selector) {
let sibling = node.parentElement.previousSibling;
let el;
while (sibling) {
el = sibling.querySelector(selector);
if (el) {
return el;
}
sibling = sibling.previousSibling;
}
return undefined;
}
export function getNextCharacter(global = window) {
const selection = global.getSelection();
if (!selection.anchorNode) {
return null;
}
const range = selection.getRangeAt(0);
const text = selection.anchorNode.textContent;
const offset = range.startOffset;
return text.substr(offset, 1);
}
export const RATE_RANGES = ['1m', '5m', '10m', '30m', '1h'];
export function processLabels(labels) {
const values = {};
labels.forEach(l => {
const { __name__, ...rest } = l;
Object.keys(rest).forEach(key => {
if (!values[key]) {
values[key] = [];
}
if (values[key].indexOf(rest[key]) === -1) {
values[key].push(rest[key]);
}
});
});
return { values, keys: Object.keys(values) };
}
// Strip syntax chars
export const cleanText = s => s.replace(/[{}[\]="(),!~+\-*/^%]/g, '').trim();
......@@ -8,11 +8,23 @@ import appEvents from 'app/core/app_events';
import Drop from 'tether-drop';
import { createStore } from 'app/stores/store';
import colors from 'app/core/utils/colors';
import { BackendSrv } from 'app/core/services/backend_srv';
import { DatasourceSrv } from 'app/features/plugins/datasource_srv';
export class GrafanaCtrl {
/** @ngInject */
constructor($scope, alertSrv, utilSrv, $rootScope, $controller, contextSrv, bridgeSrv, backendSrv) {
createStore(backendSrv);
constructor(
$scope,
alertSrv,
utilSrv,
$rootScope,
$controller,
contextSrv,
bridgeSrv,
backendSrv: BackendSrv,
datasourceSrv: DatasourceSrv
) {
createStore({ backendSrv, datasourceSrv });
$scope.init = function() {
$scope.contextSrv = contextSrv;
......
......@@ -15,7 +15,7 @@ export class DatasourceSrv {
this.datasources = {};
}
get(name) {
get(name?) {
if (!name) {
return this.get(config.defaultDatasource);
}
......
import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'mobx-react';
import coreModule from 'app/core/core_module';
import { store } from 'app/stores/store';
import { Provider } from 'mobx-react';
import { BackendSrv } from 'app/core/services/backend_srv';
import { DatasourceSrv } from 'app/features/plugins/datasource_srv';
function WrapInProvider(store, Component, props) {
return (
......@@ -13,14 +16,15 @@ function WrapInProvider(store, Component, props) {
}
/** @ngInject */
export function reactContainer($route, $location, backendSrv) {
export function reactContainer($route, $location, backendSrv: BackendSrv, datasourceSrv: DatasourceSrv) {
return {
restrict: 'E',
template: '',
link(scope, elem) {
let component = $route.current.locals.component;
let component = $route.current.locals.component.default;
let props = {
backendSrv: backendSrv,
datasourceSrv: datasourceSrv,
};
ReactDOM.render(WrapInProvider(store, component, props), elem[0]);
......
import './dashboard_loaders';
import './ReactContainer';
import ServerStats from 'app/containers/ServerStats/ServerStats';
import AlertRuleList from 'app/containers/AlertRuleList/AlertRuleList';
// import Explore from 'app/containers/Explore/Explore';
import FolderSettings from 'app/containers/ManageDashboards/FolderSettings';
import FolderPermissions from 'app/containers/ManageDashboards/FolderPermissions';
......@@ -109,6 +111,12 @@ export function setupAngularRoutes($routeProvider, $locationProvider) {
controller: 'FolderDashboardsCtrl',
controllerAs: 'ctrl',
})
.when('/explore', {
template: '<react-container />',
resolve: {
component: () => import(/* webpackChunkName: "explore" */ 'app/containers/Explore/Explore'),
},
})
.when('/org', {
templateUrl: 'public/app/features/org/partials/orgDetails.html',
controller: 'OrgDetailsCtrl',
......
......@@ -3,11 +3,11 @@ import config from 'app/core/config';
export let store: IRootStore;
export function createStore(backendSrv) {
export function createStore(services) {
store = RootStore.create(
{},
{
backendSrv: backendSrv,
...services,
navTree: config.bootData.navTree,
}
);
......
......@@ -104,5 +104,6 @@
@import 'pages/signup';
@import 'pages/styleguide';
@import 'pages/errorpage';
@import 'pages/explore';
@import 'old_responsive';
@import 'components/view_states.scss';
......@@ -23,6 +23,13 @@
@include clearfix();
}
.page-full {
margin-left: $page-sidebar-margin;
padding-left: $spacer;
padding-right: $spacer;
@include clearfix();
}
.scroll-canvas {
position: absolute;
width: 100%;
......
.explore {
.graph-legend {
flex-wrap: wrap;
}
}
.query-field {
font-size: 14px;
font-family: Consolas, Menlo, Courier, monospace;
height: auto;
}
.query-field-wrapper {
position: relative;
display: inline-block;
padding: 6px 7px 4px;
width: calc(100% - 6rem);
cursor: text;
line-height: 1.5;
color: rgba(0, 0, 0, 0.65);
background-color: #fff;
background-image: none;
border: 1px solid lightgray;
border-radius: 4px;
transition: all 0.3s;
}
.typeahead {
position: absolute;
z-index: auto;
top: -10000px;
left: -10000px;
opacity: 0;
border-radius: 4px;
transition: opacity 0.75s;
border: 1px solid #e4e4e4;
max-height: calc(66vh);
overflow-y: scroll;
max-width: calc(66%);
overflow-x: hidden;
outline: none;
list-style: none;
background: #fff;
color: rgba(0, 0, 0, 0.65);
transition: opacity 0.4s ease-out;
}
.typeahead-group__title {
color: rgba(0, 0, 0, 0.43);
font-size: 12px;
line-height: 1.5;
padding: 8px 16px;
}
.typeahead-item {
line-height: 200%;
height: auto;
font-family: Consolas, Menlo, Courier, monospace;
padding: 0 16px 0 28px;
font-size: 12px;
text-overflow: ellipsis;
overflow: hidden;
margin-left: -1px;
left: 1px;
position: relative;
z-index: 1;
display: block;
white-space: nowrap;
cursor: pointer;
transition: color 0.3s cubic-bezier(0.645, 0.045, 0.355, 1), border-color 0.3s cubic-bezier(0.645, 0.045, 0.355, 1),
background 0.3s cubic-bezier(0.645, 0.045, 0.355, 1), padding 0.15s cubic-bezier(0.645, 0.045, 0.355, 1);
}
.typeahead-item__selected {
background-color: #ecf6fd;
color: #108ee9;
}
/* SYNTAX */
/**
* prism.js Coy theme for JavaScript, CoffeeScript, CSS and HTML
* Based on https://github.com/tshedor/workshop-wp-theme (Example: http://workshop.kansan.com/category/sessions/basics or http://workshop.timshedor.com/category/sessions/basics);
* @author Tim Shedor
*/
code[class*='language-'],
pre[class*='language-'] {
color: black;
background: none;
font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace;
text-align: left;
white-space: pre;
word-spacing: normal;
word-break: normal;
word-wrap: normal;
line-height: 1.5;
-moz-tab-size: 4;
-o-tab-size: 4;
tab-size: 4;
-webkit-hyphens: none;
-moz-hyphens: none;
-ms-hyphens: none;
hyphens: none;
}
/* Code blocks */
pre[class*='language-'] {
position: relative;
margin: 0.5em 0;
overflow: visible;
padding: 0;
}
pre[class*='language-'] > code {
position: relative;
border-left: 10px solid #358ccb;
box-shadow: -1px 0px 0px 0px #358ccb, 0px 0px 0px 1px #dfdfdf;
background-color: #fdfdfd;
background-image: linear-gradient(transparent 50%, rgba(69, 142, 209, 0.04) 50%);
background-size: 3em 3em;
background-origin: content-box;
background-attachment: local;
}
code[class*='language'] {
max-height: inherit;
height: inherit;
padding: 0 1em;
display: block;
overflow: auto;
}
/* Margin bottom to accomodate shadow */
:not(pre) > code[class*='language-'],
pre[class*='language-'] {
background-color: #fdfdfd;
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
margin-bottom: 1em;
}
/* Inline code */
:not(pre) > code[class*='language-'] {
position: relative;
padding: 0.2em;
border-radius: 0.3em;
color: #c92c2c;
border: 1px solid rgba(0, 0, 0, 0.1);
display: inline;
white-space: normal;
}
pre[class*='language-']:before,
pre[class*='language-']:after {
content: '';
z-index: -2;
display: block;
position: absolute;
bottom: 0.75em;
left: 0.18em;
width: 40%;
height: 20%;
max-height: 13em;
box-shadow: 0px 13px 8px #979797;
-webkit-transform: rotate(-2deg);
-moz-transform: rotate(-2deg);
-ms-transform: rotate(-2deg);
-o-transform: rotate(-2deg);
transform: rotate(-2deg);
}
:not(pre) > code[class*='language-']:after,
pre[class*='language-']:after {
right: 0.75em;
left: auto;
-webkit-transform: rotate(2deg);
-moz-transform: rotate(2deg);
-ms-transform: rotate(2deg);
-o-transform: rotate(2deg);
transform: rotate(2deg);
}
.token.comment,
.token.block-comment,
.token.prolog,
.token.doctype,
.token.cdata {
color: #7d8b99;
}
.token.punctuation {
color: #5f6364;
}
.token.property,
.token.tag,
.token.boolean,
.token.number,
.token.function-name,
.token.constant,
.token.symbol,
.token.deleted {
color: #c92c2c;
}
.token.selector,
.token.attr-name,
.token.string,
.token.char,
.token.function,
.token.builtin,
.token.inserted {
color: #2f9c0a;
}
.token.operator,
.token.entity,
.token.url,
.token.variable {
color: #a67f59;
background: rgba(255, 255, 255, 0.5);
}
.token.atrule,
.token.attr-value,
.token.keyword,
.token.class-name {
color: #1990b8;
}
.token.regex,
.token.important {
color: #e90;
}
.language-css .token.string,
.style .token.string {
color: #a67f59;
background: rgba(255, 255, 255, 0.5);
}
.token.important {
font-weight: normal;
}
.token.bold {
font-weight: bold;
}
.token.italic {
font-style: italic;
}
.token.entity {
cursor: help;
}
.namespace {
opacity: 0.7;
}
@media screen and (max-width: 767px) {
pre[class*='language-']:before,
pre[class*='language-']:after {
bottom: 14px;
box-shadow: none;
}
}
/* Plugin styles */
.token.tab:not(:empty):before,
.token.cr:before,
.token.lf:before {
color: #e0d7d1;
}
/* Plugin styles: Line Numbers */
pre[class*='language-'].line-numbers {
padding-left: 0;
}
pre[class*='language-'].line-numbers code {
padding-left: 3.8em;
}
pre[class*='language-'].line-numbers .line-numbers-rows {
left: 0;
}
/* Plugin styles: Line Highlight */
pre[class*='language-'][data-line] {
padding-top: 0;
padding-bottom: 0;
padding-left: 0;
}
pre[data-line] code {
position: relative;
padding-left: 4em;
}
pre .line-highlight {
margin-top: 0;
}
......@@ -71,6 +71,7 @@ module.exports = merge(common, {
loader: 'babel-loader',
options: {
plugins: [
'syntax-dynamic-import',
'react-hot-loader/babel',
],
},
......
......@@ -36,7 +36,12 @@ module.exports = merge(common, {
test: /\.tsx?$/,
exclude: /node_modules/,
use: [
{ loader: "awesome-typescript-loader" }
{
loader: 'awesome-typescript-loader',
options: {
errorsAsWarnings: false,
},
},
]
},
require('./sass.rule.js')({
......
This source diff could not be displayed because it is too large. You can view the blob instead.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment