Allow formatting PromQL expressions in the UI (#11039)

* Allow formatting PromQL expressions in the UI

Signed-off-by: Julius Volz <julius.volz@gmail.com>

* Improve error handling, also catch HTTP errors

Signed-off-by: Julius Volz <julius.volz@gmail.com>

* Remove now-unneeded async property

Signed-off-by: Julius Volz <julius.volz@gmail.com>

* Disable format button when already formatted

Signed-off-by: Julius Volz <julius.volz@gmail.com>

* Disable format button when there are linter errors

Signed-off-by: Julius Volz <julius.volz@gmail.com>

* Remove disabling of format button again for linter errors

Signed-off-by: Julius Volz <julius.volz@gmail.com>
This commit is contained in:
Julius Volz 2022-07-21 18:05:41 +02:00 committed by GitHub
parent b57deb6eb0
commit b8af4632be
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 71 additions and 6 deletions

View File

@ -1,5 +1,5 @@
import React, { FC, useState, useEffect, useRef } from 'react';
import { Button, InputGroup, InputGroupAddon, InputGroupText } from 'reactstrap';
import { Alert, Button, InputGroup, InputGroupAddon, InputGroupText } from 'reactstrap';
import { EditorView, highlightSpecialChars, keymap, ViewUpdate, placeholder } from '@codemirror/view';
import { EditorState, Prec, Compartment } from '@codemirror/state';
@ -18,12 +18,13 @@ import {
import { baseTheme, lightTheme, darkTheme, promqlHighlighter } from './CMTheme';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faSearch, faSpinner, faGlobeEurope } from '@fortawesome/free-solid-svg-icons';
import { faSearch, faSpinner, faGlobeEurope, faIndent, faCheck } from '@fortawesome/free-solid-svg-icons';
import MetricsExplorer from './MetricsExplorer';
import { usePathPrefix } from '../../contexts/PathPrefixContext';
import { useTheme } from '../../contexts/ThemeContext';
import { CompleteStrategy, PromQLExtension } from '@prometheus-io/codemirror-promql';
import { newCompleteStrategy } from '@prometheus-io/codemirror-promql/dist/esm/complete';
import { API_PATH } from '../../constants/constants';
const promqlExtension = new PromQLExtension();
@ -98,6 +99,10 @@ const ExpressionInput: FC<CMExpressionInputProps> = ({
const pathPrefix = usePathPrefix();
const { theme } = useTheme();
const [formatError, setFormatError] = useState<string | null>(null);
const [isFormatting, setIsFormatting] = useState<boolean>(false);
const [exprFormatted, setExprFormatted] = useState<boolean>(false);
// (Re)initialize editor based on settings / setting changes.
useEffect(() => {
// Build the dynamic part of the config.
@ -169,7 +174,10 @@ const ExpressionInput: FC<CMExpressionInputProps> = ({
])
),
EditorView.updateListener.of((update: ViewUpdate): void => {
onExpressionChange(update.state.doc.toString());
if (update.docChanged) {
onExpressionChange(update.state.doc.toString());
setExprFormatted(false);
}
}),
],
});
@ -209,6 +217,47 @@ const ExpressionInput: FC<CMExpressionInputProps> = ({
);
};
const formatExpression = () => {
setFormatError(null);
setIsFormatting(true);
fetch(
`${pathPrefix}/${API_PATH}/format_query?${new URLSearchParams({
query: value,
})}`,
{
cache: 'no-store',
credentials: 'same-origin',
}
)
.then((resp) => {
if (!resp.ok && resp.status !== 400) {
throw new Error(`format HTTP request failed: ${resp.statusText}`);
}
return resp.json();
})
.then((json) => {
if (json.status !== 'success') {
throw new Error(json.error || 'invalid response JSON');
}
const view = viewRef.current;
if (view === null) {
return;
}
view.dispatch(view.state.update({ changes: { from: 0, to: view.state.doc.length, insert: json.data } }));
setExprFormatted(true);
})
.catch((err) => {
setFormatError(err.message);
})
.finally(() => {
setIsFormatting(false);
});
};
return (
<>
<InputGroup className="expression-input">
@ -220,7 +269,21 @@ const ExpressionInput: FC<CMExpressionInputProps> = ({
<div ref={containerRef} className="cm-expression-input" />
<InputGroupAddon addonType="append">
<Button
className="metrics-explorer-btn"
className="expression-input-action-btn"
title={isFormatting ? 'Formatting expression' : exprFormatted ? 'Expression formatted' : 'Format expression'}
onClick={formatExpression}
disabled={isFormatting || exprFormatted}
>
{isFormatting ? (
<FontAwesomeIcon icon={faSpinner} spin />
) : exprFormatted ? (
<FontAwesomeIcon icon={faCheck} />
) : (
<FontAwesomeIcon icon={faIndent} />
)}
</Button>
<Button
className="expression-input-action-btn"
title="Open metrics explorer"
onClick={() => setShowMetricsExplorer(true)}
>
@ -232,6 +295,8 @@ const ExpressionInput: FC<CMExpressionInputProps> = ({
</InputGroupAddon>
</InputGroup>
{formatError && <Alert color="danger">Error formatting expression: {formatError}</Alert>}
<MetricsExplorer
show={showMetricsExplorer}
updateShow={setShowMetricsExplorer}

View File

@ -7,7 +7,7 @@
Example:
$foo = '#000';
2. Add a style rule in _shared.scss (this file) that uses new variable.
Example:
@ -58,7 +58,7 @@
.metrics-explorer .metric:hover {
background: $metrics-explorer-bg;
}
button.metrics-explorer-btn {
button.expression-input-action-btn {
color: $input-group-addon-color;
background-color: $input-group-addon-bg;
border: $input-border-width solid $input-group-addon-border-color;