diff --git a/web/ui/react-app/src/pages/graph/ExpressionInput.tsx b/web/ui/react-app/src/pages/graph/ExpressionInput.tsx index d93014ef2e..7a628652e4 100644 --- a/web/ui/react-app/src/pages/graph/ExpressionInput.tsx +++ b/web/ui/react-app/src/pages/graph/ExpressionInput.tsx @@ -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 = ({ const pathPrefix = usePathPrefix(); const { theme } = useTheme(); + const [formatError, setFormatError] = useState(null); + const [isFormatting, setIsFormatting] = useState(false); + const [exprFormatted, setExprFormatted] = useState(false); + // (Re)initialize editor based on settings / setting changes. useEffect(() => { // Build the dynamic part of the config. @@ -169,7 +174,10 @@ const ExpressionInput: FC = ({ ]) ), 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 = ({ ); }; + 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 ( <> @@ -220,7 +269,21 @@ const ExpressionInput: FC = ({
+