prometheus/web/ui/mantine-ui/src/pages/query/ExpressionInput.tsx
Julius Volz 87a22500e1
Some checks failed
CI / Go tests (push) Has been cancelled
CI / More Go tests (push) Has been cancelled
CI / Go tests with previous Go version (push) Has been cancelled
CI / UI tests (push) Has been cancelled
CI / Go tests on Windows (push) Has been cancelled
CI / Mixins tests (push) Has been cancelled
CI / Build Prometheus for common architectures (0) (push) Has been cancelled
CI / Build Prometheus for common architectures (1) (push) Has been cancelled
CI / Build Prometheus for common architectures (2) (push) Has been cancelled
CI / Build Prometheus for all architectures (0) (push) Has been cancelled
CI / Build Prometheus for all architectures (1) (push) Has been cancelled
CI / Build Prometheus for all architectures (10) (push) Has been cancelled
CI / Build Prometheus for all architectures (11) (push) Has been cancelled
CI / Build Prometheus for all architectures (2) (push) Has been cancelled
CI / Build Prometheus for all architectures (3) (push) Has been cancelled
CI / Build Prometheus for all architectures (4) (push) Has been cancelled
CI / Build Prometheus for all architectures (5) (push) Has been cancelled
CI / Build Prometheus for all architectures (6) (push) Has been cancelled
CI / Build Prometheus for all architectures (7) (push) Has been cancelled
CI / Build Prometheus for all architectures (8) (push) Has been cancelled
CI / Build Prometheus for all architectures (9) (push) Has been cancelled
CI / Check generated parser (push) Has been cancelled
CI / golangci-lint (push) Has been cancelled
CI / fuzzing (push) Has been cancelled
CI / codeql (push) Has been cancelled
CI / Report status of build Prometheus for all architectures (push) Has been cancelled
CI / Publish main branch artifacts (push) Has been cancelled
CI / Publish release artefacts (push) Has been cancelled
CI / Publish UI on npm Registry (push) Has been cancelled
Add PromQL logic code and labels explorer from PromLens, add testing deps
Signed-off-by: Julius Volz <julius.volz@gmail.com>
2024-09-02 13:45:36 +02:00

367 lines
10 KiB
TypeScript

import {
ActionIcon,
Box,
Button,
Group,
InputBase,
Loader,
Menu,
Modal,
rem,
Skeleton,
useComputedColorScheme,
} from "@mantine/core";
import {
CompleteStrategy,
PromQLExtension,
newCompleteStrategy,
} from "@prometheus-io/codemirror-promql";
import { FC, Suspense, useEffect, useMemo, useRef, useState } from "react";
import CodeMirror, {
EditorState,
EditorView,
Prec,
ReactCodeMirrorRef,
highlightSpecialChars,
keymap,
placeholder,
} from "@uiw/react-codemirror";
import {
baseTheme,
darkPromqlHighlighter,
darkTheme,
lightTheme,
promqlHighlighter,
} from "../../codemirror/theme";
import {
bracketMatching,
indentOnInput,
syntaxHighlighting,
syntaxTree,
} from "@codemirror/language";
import classes from "./ExpressionInput.module.css";
import {
CompletionContext,
CompletionResult,
autocompletion,
closeBrackets,
closeBracketsKeymap,
completionKeymap,
} from "@codemirror/autocomplete";
import {
defaultKeymap,
history,
historyKeymap,
insertNewlineAndIndent,
} from "@codemirror/commands";
import { highlightSelectionMatches } from "@codemirror/search";
import { lintKeymap } from "@codemirror/lint";
import {
IconAlignJustified,
IconDotsVertical,
IconSearch,
IconTerminal,
IconTrash,
} from "@tabler/icons-react";
import { useAPIQuery } from "../../api/api";
import { notifications } from "@mantine/notifications";
import { useSettings } from "../../state/settingsSlice";
import MetricsExplorer from "./MetricsExplorer/MetricsExplorer";
import ErrorBoundary from "../../components/ErrorBoundary";
const promqlExtension = new PromQLExtension();
// Autocompletion strategy that wraps the main one and enriches
// it with past query items.
export class HistoryCompleteStrategy implements CompleteStrategy {
private complete: CompleteStrategy;
private queryHistory: string[];
constructor(complete: CompleteStrategy, queryHistory: string[]) {
this.complete = complete;
this.queryHistory = queryHistory;
}
promQL(
context: CompletionContext
): Promise<CompletionResult | null> | CompletionResult | null {
return Promise.resolve(this.complete.promQL(context)).then((res) => {
const { state, pos } = context;
const tree = syntaxTree(state).resolve(pos, -1);
const start = res != null ? res.from : tree.from;
if (start !== 0) {
return res;
}
const historyItems: CompletionResult = {
from: start,
to: pos,
options: this.queryHistory.map((q) => ({
label: q.length < 80 ? q : q.slice(0, 76).concat("..."),
detail: "past query",
apply: q,
info: q.length < 80 ? undefined : q,
})),
validFor: /^[a-zA-Z0-9_:]+$/,
};
if (res !== null) {
historyItems.options = historyItems.options.concat(res.options);
}
return historyItems;
});
}
}
interface ExpressionInputProps {
initialExpr: string;
metricNames: string[];
executeQuery: (expr: string) => void;
removePanel: () => void;
}
const ExpressionInput: FC<ExpressionInputProps> = ({
initialExpr,
metricNames,
executeQuery,
removePanel,
}) => {
const theme = useComputedColorScheme();
const {
pathPrefix,
enableAutocomplete,
enableSyntaxHighlighting,
enableLinter,
} = useSettings();
const [expr, setExpr] = useState(initialExpr);
useEffect(() => {
setExpr(initialExpr);
}, [initialExpr]);
const {
data: formatResult,
error: formatError,
isFetching: isFormatting,
refetch: formatQuery,
} = useAPIQuery<string>({
key: expr,
path: "/format_query",
params: {
query: expr,
},
enabled: false,
});
useEffect(() => {
if (formatError) {
notifications.show({
color: "red",
title: "Error formatting query",
message: formatError.message,
});
return;
}
if (formatResult) {
setExpr(formatResult.data);
notifications.show({
title: "Expression formatted",
message: "Expression formatted successfully!",
});
}
}, [formatResult, formatError]);
const cmRef = useRef<ReactCodeMirrorRef>(null);
const [showMetricsExplorer, setShowMetricsExplorer] = useState(false);
// TODO: Implement query history.
// This is just a placeholder until query history is implemented, so disable the linter warning.
const queryHistory = useMemo<string[]>(() => [], []);
// (Re)initialize editor based on settings / setting changes.
useEffect(() => {
// Build the dynamic part of the config.
promqlExtension
.activateCompletion(enableAutocomplete)
.activateLinter(enableLinter)
.setComplete({
completeStrategy: new HistoryCompleteStrategy(
newCompleteStrategy({
remote: {
url: pathPrefix,
cache: { initialMetricList: metricNames },
},
}),
queryHistory
),
});
}, [pathPrefix, metricNames, enableAutocomplete, enableLinter, queryHistory]); // TODO: Make this depend on external settings changes, maybe use dynamic config compartment again.
return (
<Group align="flex-start" wrap="nowrap" gap="xs">
{/* eslint-disable-next-line @typescript-eslint/no-explicit-any */}
<InputBase<any>
leftSection={
isFormatting ? <Loader size="xs" color="gray.5" /> : <IconTerminal />
}
rightSection={
<Menu shadow="md" width={200}>
<Menu.Target>
<ActionIcon
size="lg"
variant="transparent"
color="gray"
aria-label="Show query options"
>
<IconDotsVertical style={{ width: "1rem", height: "1rem" }} />
</ActionIcon>
</Menu.Target>
<Menu.Dropdown>
<Menu.Label>Query options</Menu.Label>
<Menu.Item
leftSection={
<IconSearch style={{ width: rem(14), height: rem(14) }} />
}
onClick={() => setShowMetricsExplorer(true)}
>
Explore metrics
</Menu.Item>
<Menu.Item
leftSection={
<IconAlignJustified
style={{ width: rem(14), height: rem(14) }}
/>
}
onClick={() => formatQuery()}
disabled={
isFormatting || expr === "" || expr === formatResult?.data
}
>
Format expression
</Menu.Item>
<Menu.Item
color="red"
leftSection={
<IconTrash style={{ width: rem(14), height: rem(14) }} />
}
onClick={removePanel}
>
Remove query
</Menu.Item>
</Menu.Dropdown>
</Menu>
}
component={CodeMirror}
className={classes.input}
basicSetup={false}
value={expr}
onChange={setExpr}
autoFocus
ref={cmRef}
extensions={[
baseTheme,
highlightSpecialChars(),
history(),
EditorState.allowMultipleSelections.of(true),
indentOnInput(),
bracketMatching(),
closeBrackets(),
autocompletion(),
highlightSelectionMatches(),
EditorView.lineWrapping,
keymap.of([
...closeBracketsKeymap,
...defaultKeymap,
...historyKeymap,
...completionKeymap,
...lintKeymap,
]),
placeholder("Enter expression (press Shift+Enter for newlines)"),
enableSyntaxHighlighting
? syntaxHighlighting(
theme === "light" ? promqlHighlighter : darkPromqlHighlighter
)
: [],
promqlExtension.asExtension(),
theme === "light" ? lightTheme : darkTheme,
keymap.of([
{
key: "Escape",
run: (v: EditorView): boolean => {
v.contentDOM.blur();
return false;
},
},
]),
Prec.highest(
keymap.of([
{
key: "Enter",
run: (): boolean => {
executeQuery(expr);
return true;
},
},
{
key: "Shift-Enter",
run: insertNewlineAndIndent,
},
])
),
]}
multiline
/>
<Button
variant="primary"
onClick={() => executeQuery(expr)}
// Without this, the button can be squeezed to a width
// that doesn't fit its text when the window is too narrow.
style={{ flexShrink: 0 }}
>
Execute
</Button>
<Modal
size="95%"
opened={showMetricsExplorer}
onClose={() => setShowMetricsExplorer(false)}
title="Explore metrics"
>
<ErrorBoundary key={location.pathname} title="Error showing metrics">
<Suspense
fallback={
<Box mt="lg">
{Array.from(Array(20), (_, i) => (
<Skeleton key={i} height={30} mb={15} width="100%" />
))}
</Box>
}
>
<MetricsExplorer
metricNames={metricNames}
insertText={(text: string) => {
if (cmRef.current && cmRef.current.view) {
const view = cmRef.current.view;
view.dispatch(
view.state.update({
changes: {
from: view.state.selection.ranges[0].from,
to: view.state.selection.ranges[0].to,
insert: text,
},
})
);
}
}}
close={() => setShowMetricsExplorer(false)}
/>
</Suspense>
</ErrorBoundary>
</Modal>
</Group>
);
};
export default ExpressionInput;