diff --git a/public/app/containers/Explore/QueryField.tsx b/public/app/containers/Explore/QueryField.tsx index 53354584fea..bedb955b9b9 100644 --- a/public/app/containers/Explore/QueryField.tsx +++ b/public/app/containers/Explore/QueryField.tsx @@ -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 { 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 { 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 { 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 { 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 { } onKeyDown = (event, change) => { - if (this.menuEl) { - const { typeaheadIndex, suggestions } = this.state; + const { typeaheadIndex, suggestions } = this.state; - switch (event.key) { - case 'Escape': { - if (this.menuEl) { - event.preventDefault(); - this.resetTypeahead(); - return true; - } - break; + 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 { 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 { // 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 { }; 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 { 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 ( - + { render() { return ( -
+
{this.renderMenu()} { @@ -55,12 +56,15 @@ class QueryRow extends PureComponent {
-
+
diff --git a/public/app/containers/Explore/Typeahead.tsx b/public/app/containers/Explore/Typeahead.tsx index 4943622fe4e..44fce7f8c7e 100644 --- a/public/app/containers/Explore/Typeahead.tsx +++ b/public/app/containers/Explore/Typeahead.tsx @@ -23,12 +23,13 @@ class TypeaheadItem extends React.PureComponent { }; 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 (
  • {label} + {hint && isSelected ?
    {hint}
    : null}
  • ); } @@ -41,9 +42,19 @@ class TypeaheadGroup extends React.PureComponent {
  • {label}
      - {items.map(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 ( + -1} + hint={item.hint} + label={label} + /> + ); + })}
  • ); diff --git a/public/app/containers/Explore/slate-plugins/prism/index.tsx b/public/app/containers/Explore/slate-plugins/prism/index.tsx index 7c3fa296d8e..d185518790f 100644 --- a/public/app/containers/Explore/slate-plugins/prism/index.tsx +++ b/public/app/containers/Explore/slate-plugins/prism/index.tsx @@ -1,16 +1,12 @@ 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(); diff --git a/public/sass/_grafana.scss b/public/sass/_grafana.scss index afc869f8b15..9e3bec267ed 100644 --- a/public/sass/_grafana.scss +++ b/public/sass/_grafana.scss @@ -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'; diff --git a/public/sass/components/_slate_editor.scss b/public/sass/components/_slate_editor.scss new file mode 100644 index 00000000000..de8a6e6d721 --- /dev/null +++ b/public/sass/components/_slate_editor.scss @@ -0,0 +1,151 @@ +.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; + } +} diff --git a/public/sass/pages/_explore.scss b/public/sass/pages/_explore.scss index 7dacccf6a87..876260c4f76 100644 --- a/public/sass/pages/_explore.scss +++ b/public/sass/pages/_explore.scss @@ -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; - } -}