Skip to content

Instantly share code, notes, and snippets.

@ivanahuckova
Last active January 15, 2021 17:34
Show Gist options
  • Save ivanahuckova/2c305a56a2a14d35e8eecd51b8c87e59 to your computer and use it in GitHub Desktop.
Save ivanahuckova/2c305a56a2a14d35e8eecd51b8c87e59 to your computer and use it in GitHub Desktop.
Dependencies that needs to be installed: @monaco-editor and monaco-promql.
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