Last active
January 15, 2021 17:34
-
-
Save ivanahuckova/2c305a56a2a14d35e8eecd51b8c87e59 to your computer and use it in GitHub Desktop.
Dependencies that needs to be installed: @monaco-editor and monaco-promql.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import React, { ReactNode } from 'react'; | |
import { css } from 'emotion'; | |
import _ from 'lodash'; | |
import { ButtonCascader, CascaderOption, withTheme } from '@grafana/ui'; | |
import { ExploreQueryFieldProps, QueryHint, GrafanaTheme } from '@grafana/data'; | |
//Monaco | |
import { ControlledEditor as Editor, monaco } from '@monaco-editor/react'; | |
import * as monacoEditor from 'monaco-editor/esm/vs/editor/editor.api'; | |
import { promLanguageDefinition } from 'monaco-promql'; | |
import { PromQuery, PromOptions, PromMetricsMetadata } from '../types'; | |
import { PrometheusDatasource } from '../datasource'; | |
import { CancelablePromise, makePromiseCancelable } from 'app/core/utils/CancelablePromise'; | |
//Variables | |
const HISTOGRAM_GROUP = '__histograms__'; | |
export const RECORDING_RULES_GROUP = '__recording_rules__'; | |
//Util functions | |
function initPromqlLanguage() { | |
const languageId = promLanguageDefinition.id; | |
monaco.init().then(monaco => { | |
monaco.languages.register(promLanguageDefinition); | |
monaco.languages.onLanguage(languageId, () => { | |
promLanguageDefinition.loader().then(mod => { | |
monaco.languages.setMonarchTokensProvider(languageId, mod.language); | |
monaco.languages.setLanguageConfiguration(languageId, mod.languageConfiguration); | |
monaco.languages.registerCompletionItemProvider(languageId, mod.completionItemProvider); | |
}); | |
}); | |
}); | |
} | |
function getChooserText(lookupDisabled: boolean, hasSyntax: boolean, metrics: string[]) { | |
if (lookupDisabled) { | |
return '(Disabled)'; | |
} | |
if (!hasSyntax) { | |
return 'Loading...'; | |
} | |
if (metrics && metrics.length === 0) { | |
return '(No metrics found)'; | |
} | |
return ' Metrics'; | |
} | |
function addMetricsMetadata(metric: string, metadata?: PromMetricsMetadata): CascaderOption { | |
const option: CascaderOption = { label: metric, value: metric }; | |
if (metadata && metadata[metric]) { | |
const { type = '', help } = metadata[metric][0]; | |
option.title = [metric, type.toUpperCase(), help].join('\n'); | |
} | |
return option; | |
} | |
export function groupMetricsByPrefix(metrics: string[], metadata?: PromMetricsMetadata): CascaderOption[] { | |
// Filter out recording rules and insert as first option | |
const ruleRegex = /:\w+:/; | |
const ruleNames = metrics.filter(metric => ruleRegex.test(metric)); | |
const rulesOption = { | |
label: 'Recording rules', | |
value: RECORDING_RULES_GROUP, | |
children: ruleNames | |
.slice() | |
.sort() | |
.map(name => ({ label: name, value: name })), | |
}; | |
const options = ruleNames.length > 0 ? [rulesOption] : []; | |
const delimiter = '_'; | |
const metricsOptions = _.chain(metrics) | |
.filter((metric: string) => !ruleRegex.test(metric)) | |
.groupBy((metric: string) => metric.split(delimiter)[0]) | |
.map( | |
(metricsForPrefix: string[], prefix: string): CascaderOption => { | |
const prefixIsMetric = metricsForPrefix.length === 1 && metricsForPrefix[0] === prefix; | |
const children = prefixIsMetric ? [] : metricsForPrefix.sort().map(m => addMetricsMetadata(m, metadata)); | |
return { | |
children, | |
label: prefix, | |
value: prefix, | |
}; | |
} | |
) | |
.sortBy('label') | |
.value(); | |
return [...options, ...metricsOptions]; | |
} | |
//Types | |
interface PromQueryFieldProps extends ExploreQueryFieldProps<PrometheusDatasource, PromQuery, PromOptions> { | |
ExtraFieldElement?: ReactNode; | |
theme: GrafanaTheme; | |
} | |
interface PromQueryFieldState { | |
metricsOptions: any[]; | |
syntaxLoaded: boolean; | |
hint: QueryHint | null; | |
} | |
//Component | |
class PromMonacoEditor extends React.PureComponent<PromQueryFieldProps, PromQueryFieldState> { | |
languageProviderInitializationPromise: CancelablePromise<any>; | |
constructor(props: PromQueryFieldProps, context: React.Context<any>) { | |
super(props, context); | |
this.state = { | |
metricsOptions: [], | |
syntaxLoaded: false, | |
hint: null, | |
}; | |
} | |
componentDidMount() { | |
initPromqlLanguage(); | |
this.refreshMetrics(); | |
} | |
refreshMetrics = async () => { | |
const { | |
datasource: { languageProvider }, | |
} = this.props; | |
this.languageProviderInitializationPromise = makePromiseCancelable(languageProvider.start()); | |
try { | |
const remainingTasks = await this.languageProviderInitializationPromise.promise; | |
await Promise.all(remainingTasks); | |
this.onUpdateLanguage(); | |
} catch (err) { | |
if (!err.isCanceled) { | |
throw err; | |
} | |
} | |
}; | |
onUpdateLanguage = () => { | |
const { | |
datasource: { languageProvider }, | |
} = this.props; | |
const { histogramMetrics, metrics, metricsMetadata } = languageProvider; | |
if (!metrics) { | |
return; | |
} | |
// Build metrics tree | |
const metricsByPrefix = groupMetricsByPrefix(metrics, metricsMetadata); | |
const histogramOptions = histogramMetrics.map((hm: any) => ({ label: hm, value: hm })); | |
const metricsOptions = | |
histogramMetrics.length > 0 | |
? [ | |
{ label: 'Histograms', value: HISTOGRAM_GROUP, children: histogramOptions, isLeaf: false }, | |
...metricsByPrefix, | |
] | |
: metricsByPrefix; | |
this.setState({ metricsOptions, syntaxLoaded: true }); | |
}; | |
onChangeMetrics = (values: string[], selectedOptions: CascaderOption[]) => { | |
let query; | |
if (selectedOptions.length === 1) { | |
const selectedOption = selectedOptions[0]; | |
if (!selectedOption.children || selectedOption.children.length === 0) { | |
query = selectedOption.value; | |
} else { | |
// Ignore click on group | |
return; | |
} | |
} else { | |
const prefix = selectedOptions[0].value; | |
const metric = selectedOptions[1].value; | |
if (prefix === HISTOGRAM_GROUP) { | |
query = `histogram_quantile(0.95, sum(rate(${metric}[5m])) by (le))`; | |
} else { | |
query = metric; | |
} | |
} | |
this.onChangeQuery(query, true); | |
}; | |
onChangeQuery = (value: string, override?: boolean) => { | |
// Send text change to parent | |
const { query, onChange, onRunQuery } = this.props; | |
if (onChange) { | |
const nextQuery: PromQuery = { ...query, expr: value }; | |
onChange(nextQuery); | |
if (override && onRunQuery) { | |
onRunQuery(); | |
} | |
} | |
}; | |
onClickHintFix = () => { | |
const { datasource, query, onChange, onRunQuery } = this.props; | |
const { hint } = this.state; | |
onChange(datasource.modifyQuery(query, hint!.fix!.action)); | |
onRunQuery(); | |
}; | |
render() { | |
const { datasource, query, ExtraFieldElement, theme } = this.props; | |
const { metricsOptions, syntaxLoaded, hint } = this.state; | |
const chooserText = getChooserText(datasource.lookupsDisabled, syntaxLoaded, metricsOptions); | |
const buttonDisabled = !(syntaxLoaded && metricsOptions && metricsOptions.length > 0); | |
return ( | |
<> | |
<div className="gf-form-inline gf-form-inline--xs-view-flex-column flex-grow-1"> | |
<div className="gf-form flex-shrink-0"> | |
<ButtonCascader | |
options={metricsOptions} | |
disabled={buttonDisabled} | |
onChange={this.onChangeMetrics} | |
className={css` | |
width: 97px; | |
`} | |
> | |
{chooserText} | |
</ButtonCascader> | |
</div> | |
<div className="gf-form gf-form--grow flex-shrink-1 min-width-15" style={{ maxWidth: '100%' }}> | |
<Editor | |
height="10vh" | |
width="98%" | |
language="promql" | |
theme={theme.type} | |
onChange={(e: monacoEditor.editor.IModelContentChangedEvent, v: string | undefined) => | |
this.onChangeQuery(v || '') | |
} | |
value={query.expr} | |
/> | |
</div> | |
</div> | |
{ExtraFieldElement} | |
{hint ? ( | |
<div className="query-row-break"> | |
<div className="prom-query-field-info text-warning"> | |
{hint.label}{' '} | |
{hint.fix ? ( | |
<a className="text-link muted" onClick={this.onClickHintFix}> | |
{hint.fix.label} | |
</a> | |
) : null} | |
</div> | |
</div> | |
) : null} | |
</> | |
); | |
} | |
} | |
export default withTheme(PromMonacoEditor); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment