Commit 25bcdbca by David Committed by GitHub

Merge pull request #12284 from grafana/davkal/queryfield-refactor

Query field refactorings to support external plugins
parents 5a925461 a9e1e5f3
......@@ -9,7 +9,7 @@ import { getNextCharacter, getPreviousCousin } from './utils/dom';
import BracesPlugin from './slate-plugins/braces';
import ClearPlugin from './slate-plugins/clear';
import NewlinePlugin from './slate-plugins/newline';
import PluginPrism, { configurePrismMetricsTokens } from './slate-plugins/prism/index';
import PluginPrism, { setPrismTokens } from './slate-plugins/prism/index';
import RunnerPlugin from './slate-plugins/runner';
import debounce from './utils/debounce';
import { processLabels, RATE_RANGES, cleanText } from './utils/prometheus';
......@@ -17,13 +17,13 @@ import { processLabels, RATE_RANGES, cleanText } from './utils/prometheus';
import Typeahead from './Typeahead';
const EMPTY_METRIC = '';
const TYPEAHEAD_DEBOUNCE = 300;
export const TYPEAHEAD_DEBOUNCE = 300;
function flattenSuggestions(s) {
return s ? s.reduce((acc, g) => acc.concat(g.items), []) : [];
}
const getInitialValue = query =>
export const getInitialValue = query =>
Value.fromJSON({
document: {
nodes: [
......@@ -45,12 +45,14 @@ const getInitialValue = query =>
},
});
class Portal extends React.Component {
class Portal extends React.Component<any, any> {
node: any;
constructor(props) {
super(props);
const { index = 0, prefix = 'query' } = props;
this.node = document.createElement('div');
this.node.classList.add('explore-typeahead', `explore-typeahead-${props.index}`);
this.node.classList.add(`slate-typeahead`, `slate-typeahead-${prefix}-${index}`);
document.body.appendChild(this.node);
}
......@@ -71,12 +73,14 @@ class QueryField extends React.Component<any, any> {
constructor(props, context) {
super(props, context);
const { prismDefinition = {}, prismLanguage = 'promql' } = props;
this.plugins = [
BracesPlugin(),
ClearPlugin(),
RunnerPlugin({ handler: props.onPressEnter }),
NewlinePlugin(),
PluginPrism(),
PluginPrism({ definition: prismDefinition, language: prismLanguage }),
];
this.state = {
......@@ -131,7 +135,8 @@ class QueryField extends React.Component<any, any> {
if (!this.state.metrics) {
return;
}
configurePrismMetricsTokens(this.state.metrics);
setPrismTokens(this.props.prismLanguage, 'metrics', this.state.metrics);
// Trigger re-render
window.requestAnimationFrame(() => {
// Bogus edit to trigger highlighting
......@@ -162,7 +167,7 @@ class QueryField extends React.Component<any, any> {
const selection = window.getSelection();
if (selection.anchorNode) {
const wrapperNode = selection.anchorNode.parentElement;
const editorNode = wrapperNode.closest('.query-field');
const editorNode = wrapperNode.closest('.slate-query-field');
if (!editorNode || this.state.value.isBlurred) {
// Not inside this editor
return;
......@@ -330,20 +335,30 @@ class QueryField extends React.Component<any, any> {
}
onKeyDown = (event, change) => {
if (this.menuEl) {
const { typeaheadIndex, suggestions } = this.state;
switch (event.key) {
case 'Escape': {
if (this.menuEl) {
event.preventDefault();
this.resetTypeahead();
return true;
}
break;
const { typeaheadIndex, suggestions } = this.state;
switch (event.key) {
case 'Escape': {
if (this.menuEl) {
event.preventDefault();
event.stopPropagation();
this.resetTypeahead();
return true;
}
break;
}
case 'Tab': {
case ' ': {
if (event.ctrlKey) {
event.preventDefault();
this.handleTypeahead();
return true;
}
break;
}
case 'Tab': {
if (this.menuEl) {
// Dont blur input
event.preventDefault();
if (!suggestions || suggestions.length === 0) {
......@@ -359,25 +374,30 @@ class QueryField extends React.Component<any, any> {
this.applyTypeahead(change, suggestion);
return true;
}
break;
}
case 'ArrowDown': {
case 'ArrowDown': {
if (this.menuEl) {
// Select next suggestion
event.preventDefault();
this.setState({ typeaheadIndex: typeaheadIndex + 1 });
break;
}
break;
}
case 'ArrowUp': {
case 'ArrowUp': {
if (this.menuEl) {
// Select previous suggestion
event.preventDefault();
this.setState({ typeaheadIndex: Math.max(0, typeaheadIndex - 1) });
break;
}
break;
}
default: {
// console.log('default key', event.key, event.which, event.charCode, event.locale, data.key);
break;
}
default: {
// console.log('default key', event.key, event.which, event.charCode, event.locale, data.key);
break;
}
}
return undefined;
......@@ -502,10 +522,17 @@ class QueryField extends React.Component<any, any> {
// Align menu overlay to editor node
if (node) {
// Read from DOM
const rect = node.parentElement.getBoundingClientRect();
menu.style.opacity = 1;
menu.style.top = `${rect.top + window.scrollY + rect.height + 4}px`;
menu.style.left = `${rect.left + window.scrollX - 2}px`;
const scrollX = window.scrollX;
const scrollY = window.scrollY;
// Write DOM
requestAnimationFrame(() => {
menu.style.opacity = 1;
menu.style.top = `${rect.top + scrollY + rect.height + 4}px`;
menu.style.left = `${rect.left + scrollX - 2}px`;
});
}
};
......@@ -514,6 +541,7 @@ class QueryField extends React.Component<any, any> {
};
renderMenu = () => {
const { portalPrefix } = this.props;
const { suggestions } = this.state;
const hasSuggesstions = suggestions && suggestions.length > 0;
if (!hasSuggesstions) {
......@@ -524,11 +552,13 @@ class QueryField extends React.Component<any, any> {
let selectedIndex = Math.max(this.state.typeaheadIndex, 0);
const flattenedSuggestions = flattenSuggestions(suggestions);
selectedIndex = selectedIndex % flattenedSuggestions.length || 0;
const selectedKeys = flattenedSuggestions.length > 0 ? [flattenedSuggestions[selectedIndex]] : [];
const selectedKeys = (flattenedSuggestions.length > 0 ? [flattenedSuggestions[selectedIndex]] : []).map(
i => (typeof i === 'object' ? i.text : i)
);
// Create typeahead in DOM root so we can later position it absolutely
return (
<Portal>
<Portal prefix={portalPrefix}>
<Typeahead
menuRef={this.menuRef}
selectedItems={selectedKeys}
......@@ -541,7 +571,7 @@ class QueryField extends React.Component<any, any> {
render() {
return (
<div className="query-field">
<div className="slate-query-field">
{this.renderMenu()}
<Editor
autoCorrect={false}
......
import React, { PureComponent } from 'react';
import promql from './slate-plugins/prism/promql';
import QueryField from './QueryField';
class QueryRow extends PureComponent<any, any> {
......@@ -55,12 +56,15 @@ class QueryRow extends PureComponent<any, any> {
<i className="fa fa-minus" />
</button>
</div>
<div className="query-field-wrapper">
<div className="slate-query-field-wrapper">
<QueryField
initialQuery={edited ? null : query}
portalPrefix="explore"
onPressEnter={this.handlePressEnter}
onQueryChange={this.handleChangeQuery}
placeholder="Enter a PromQL query"
prismLanguage="promql"
prismDefinition={promql}
request={request}
/>
</div>
......
......@@ -23,12 +23,13 @@ class TypeaheadItem extends React.PureComponent<any, any> {
};
render() {
const { isSelected, label, onClickItem } = this.props;
const { hint, 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}
{hint && isSelected ? <div className="typeahead-item-hint">{hint}</div> : null}
</li>
);
}
......@@ -41,9 +42,19 @@ class TypeaheadGroup extends React.PureComponent<any, any> {
<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} />
))}
{items.map(item => {
const text = typeof item === 'object' ? item.text : item;
const label = typeof item === 'object' ? item.display || item.text : item;
return (
<TypeaheadItem
key={text}
onClickItem={onClickItem}
isSelected={selected.indexOf(text) > -1}
hint={item.hint}
label={label}
/>
);
})}
</ul>
</li>
);
......
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)`),
export function setPrismTokens(language, field, values, alias = 'variable') {
Prism.languages[language][field] = {
alias,
pattern: new RegExp(`(?:^|\\s)(${values.join('|')})(?:$|\\s)`),
};
}
......@@ -21,7 +17,12 @@ export function configurePrismMetricsTokens(metrics) {
* (Adapted to handle nested grammar definitions.)
*/
export default function PrismPlugin() {
export default function PrismPlugin({ definition, language }) {
if (definition) {
// Don't override exising modified definitions
Prism.languages[language] = Prism.languages[language] || definition;
}
return {
/**
* Render a Slate mark with appropiate CSS class names
......@@ -54,7 +55,7 @@ export default function PrismPlugin() {
const texts = node.getTexts().toArray();
const tstring = texts.map(t => t.text).join('\n');
const grammar = Prism.languages.promql;
const grammar = Prism.languages[language];
const tokens = Prism.tokenize(tstring, grammar);
const decorations = [];
let startText = texts.shift();
......
......@@ -67,6 +67,7 @@
@import 'components/filter-list';
@import 'components/filter-table';
@import 'components/old_stuff';
@import 'components/slate_editor';
@import 'components/typeahead';
@import 'components/modals';
@import 'components/dropdown';
......
.slate-query-field {
font-size: $font-size-root;
font-family: $font-family-monospace;
height: auto;
}
.slate-query-field-wrapper {
position: relative;
display: inline-block;
padding: 6px 7px 4px;
width: 100%;
cursor: text;
line-height: $line-height-base;
color: $text-color-weak;
background-color: $panel-bg;
background-image: none;
border: $panel-border;
border-radius: $border-radius;
transition: all 0.3s;
}
.slate-typeahead {
.typeahead {
position: absolute;
z-index: auto;
top: -10000px;
left: -10000px;
opacity: 0;
border-radius: $border-radius;
transition: opacity 0.75s;
border: $panel-border;
max-height: calc(66vh);
overflow-y: scroll;
max-width: calc(66%);
overflow-x: hidden;
outline: none;
list-style: none;
background: $panel-bg;
color: $text-color;
transition: opacity 0.4s ease-out;
box-shadow: $typeahead-shadow;
}
.typeahead-group__title {
color: $text-color-weak;
font-size: $font-size-sm;
line-height: $line-height-base;
padding: $input-padding-y $input-padding-x;
}
.typeahead-item {
height: auto;
font-family: $font-family-monospace;
padding: $input-padding-y $input-padding-x;
padding-left: $input-padding-x-lg;
font-size: $font-size-sm;
text-overflow: ellipsis;
overflow: hidden;
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: $typeahead-selected-bg;
color: $typeahead-selected-color;
.typeahead-item-hint {
font-size: $font-size-xs;
color: $text-color;
}
}
}
/* SYNTAX */
.slate-query-field {
.token.comment,
.token.block-comment,
.token.prolog,
.token.doctype,
.token.cdata {
color: $text-color-weak;
}
.token.punctuation {
color: $text-color-weak;
}
.token.property,
.token.tag,
.token.boolean,
.token.number,
.token.function-name,
.token.constant,
.token.symbol,
.token.deleted {
color: $query-red;
}
.token.selector,
.token.attr-name,
.token.string,
.token.char,
.token.function,
.token.builtin,
.token.inserted {
color: $query-green;
}
.token.operator,
.token.entity,
.token.url,
.token.variable {
color: $query-purple;
}
.token.atrule,
.token.attr-value,
.token.keyword,
.token.class-name {
color: $query-blue;
}
.token.regex,
.token.important {
color: $query-orange;
}
.token.important {
font-weight: normal;
}
.token.bold {
font-weight: bold;
}
.token.italic {
font-style: italic;
}
.token.entity {
cursor: help;
}
.namespace {
opacity: 0.7;
}
}
......@@ -93,150 +93,3 @@
.query-row-tools {
width: 4rem;
}
.query-field {
font-size: $font-size-root;
font-family: $font-family-monospace;
height: auto;
}
.query-field-wrapper {
position: relative;
display: inline-block;
padding: 6px 7px 4px;
width: 100%;
cursor: text;
line-height: $line-height-base;
color: $text-color-weak;
background-color: $panel-bg;
background-image: none;
border: $panel-border;
border-radius: $border-radius;
transition: all 0.3s;
}
.explore-typeahead {
.typeahead {
position: absolute;
z-index: auto;
top: -10000px;
left: -10000px;
opacity: 0;
border-radius: $border-radius;
transition: opacity 0.75s;
border: $panel-border;
max-height: calc(66vh);
overflow-y: scroll;
max-width: calc(66%);
overflow-x: hidden;
outline: none;
list-style: none;
background: $panel-bg;
color: $text-color;
transition: opacity 0.4s ease-out;
box-shadow: $typeahead-shadow;
}
.typeahead-group__title {
color: $text-color-weak;
font-size: $font-size-sm;
line-height: $line-height-base;
padding: $input-padding-y $input-padding-x;
}
.typeahead-item {
height: auto;
font-family: $font-family-monospace;
padding: $input-padding-y $input-padding-x;
padding-left: $input-padding-x-lg;
font-size: $font-size-sm;
text-overflow: ellipsis;
overflow: hidden;
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: $typeahead-selected-bg;
color: $typeahead-selected-color;
}
}
/* SYNTAX */
.explore {
.token.comment,
.token.block-comment,
.token.prolog,
.token.doctype,
.token.cdata {
color: $text-color-weak;
}
.token.punctuation {
color: $text-color-weak;
}
.token.property,
.token.tag,
.token.boolean,
.token.number,
.token.function-name,
.token.constant,
.token.symbol,
.token.deleted {
color: $query-red;
}
.token.selector,
.token.attr-name,
.token.string,
.token.char,
.token.function,
.token.builtin,
.token.inserted {
color: $query-green;
}
.token.operator,
.token.entity,
.token.url,
.token.variable {
color: $query-purple;
}
.token.atrule,
.token.attr-value,
.token.keyword,
.token.class-name {
color: $query-blue;
}
.token.regex,
.token.important {
color: $query-orange;
}
.token.important {
font-weight: normal;
}
.token.bold {
font-weight: bold;
}
.token.italic {
font-style: italic;
}
.token.entity {
cursor: help;
}
.namespace {
opacity: 0.7;
}
}
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