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:
parent
b57deb6eb0
commit
b8af4632be
|
@ -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}
|
||||
|
|
|
@ -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;
|
||||
|
|
Loading…
Reference in New Issue