mirror of
https://github.com/prometheus/prometheus.git
synced 2025-03-05 20:59:13 -08:00
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 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 { EditorView, highlightSpecialChars, keymap, ViewUpdate, placeholder } from '@codemirror/view';
|
||||||
import { EditorState, Prec, Compartment } from '@codemirror/state';
|
import { EditorState, Prec, Compartment } from '@codemirror/state';
|
||||||
|
@ -18,12 +18,13 @@ import {
|
||||||
import { baseTheme, lightTheme, darkTheme, promqlHighlighter } from './CMTheme';
|
import { baseTheme, lightTheme, darkTheme, promqlHighlighter } from './CMTheme';
|
||||||
|
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
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 MetricsExplorer from './MetricsExplorer';
|
||||||
import { usePathPrefix } from '../../contexts/PathPrefixContext';
|
import { usePathPrefix } from '../../contexts/PathPrefixContext';
|
||||||
import { useTheme } from '../../contexts/ThemeContext';
|
import { useTheme } from '../../contexts/ThemeContext';
|
||||||
import { CompleteStrategy, PromQLExtension } from '@prometheus-io/codemirror-promql';
|
import { CompleteStrategy, PromQLExtension } from '@prometheus-io/codemirror-promql';
|
||||||
import { newCompleteStrategy } from '@prometheus-io/codemirror-promql/dist/esm/complete';
|
import { newCompleteStrategy } from '@prometheus-io/codemirror-promql/dist/esm/complete';
|
||||||
|
import { API_PATH } from '../../constants/constants';
|
||||||
|
|
||||||
const promqlExtension = new PromQLExtension();
|
const promqlExtension = new PromQLExtension();
|
||||||
|
|
||||||
|
@ -98,6 +99,10 @@ const ExpressionInput: FC<CMExpressionInputProps> = ({
|
||||||
const pathPrefix = usePathPrefix();
|
const pathPrefix = usePathPrefix();
|
||||||
const { theme } = useTheme();
|
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.
|
// (Re)initialize editor based on settings / setting changes.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Build the dynamic part of the config.
|
// Build the dynamic part of the config.
|
||||||
|
@ -169,7 +174,10 @@ const ExpressionInput: FC<CMExpressionInputProps> = ({
|
||||||
])
|
])
|
||||||
),
|
),
|
||||||
EditorView.updateListener.of((update: ViewUpdate): void => {
|
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
<InputGroup className="expression-input">
|
<InputGroup className="expression-input">
|
||||||
|
@ -220,7 +269,21 @@ const ExpressionInput: FC<CMExpressionInputProps> = ({
|
||||||
<div ref={containerRef} className="cm-expression-input" />
|
<div ref={containerRef} className="cm-expression-input" />
|
||||||
<InputGroupAddon addonType="append">
|
<InputGroupAddon addonType="append">
|
||||||
<Button
|
<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"
|
title="Open metrics explorer"
|
||||||
onClick={() => setShowMetricsExplorer(true)}
|
onClick={() => setShowMetricsExplorer(true)}
|
||||||
>
|
>
|
||||||
|
@ -232,6 +295,8 @@ const ExpressionInput: FC<CMExpressionInputProps> = ({
|
||||||
</InputGroupAddon>
|
</InputGroupAddon>
|
||||||
</InputGroup>
|
</InputGroup>
|
||||||
|
|
||||||
|
{formatError && <Alert color="danger">Error formatting expression: {formatError}</Alert>}
|
||||||
|
|
||||||
<MetricsExplorer
|
<MetricsExplorer
|
||||||
show={showMetricsExplorer}
|
show={showMetricsExplorer}
|
||||||
updateShow={setShowMetricsExplorer}
|
updateShow={setShowMetricsExplorer}
|
||||||
|
|
|
@ -7,7 +7,7 @@
|
||||||
Example:
|
Example:
|
||||||
|
|
||||||
$foo = '#000';
|
$foo = '#000';
|
||||||
|
|
||||||
2. Add a style rule in _shared.scss (this file) that uses new variable.
|
2. Add a style rule in _shared.scss (this file) that uses new variable.
|
||||||
Example:
|
Example:
|
||||||
|
|
||||||
|
@ -58,7 +58,7 @@
|
||||||
.metrics-explorer .metric:hover {
|
.metrics-explorer .metric:hover {
|
||||||
background: $metrics-explorer-bg;
|
background: $metrics-explorer-bg;
|
||||||
}
|
}
|
||||||
button.metrics-explorer-btn {
|
button.expression-input-action-btn {
|
||||||
color: $input-group-addon-color;
|
color: $input-group-addon-color;
|
||||||
background-color: $input-group-addon-bg;
|
background-color: $input-group-addon-bg;
|
||||||
border: $input-border-width solid $input-group-addon-border-color;
|
border: $input-border-width solid $input-group-addon-border-color;
|
||||||
|
|
Loading…
Reference in a new issue