2024-03-07 04:16:54 -08:00
import {
ActionIcon ,
Button ,
Group ,
InputBase ,
2024-03-07 08:45:14 -08:00
Loader ,
2024-03-07 04:16:54 -08:00
Menu ,
rem ,
useComputedColorScheme ,
} from "@mantine/core" ;
import {
CompleteStrategy ,
PromQLExtension ,
newCompleteStrategy ,
} from "@prometheus-io/codemirror-promql" ;
import { FC , useEffect , useState } from "react" ;
import CodeMirror , {
EditorState ,
EditorView ,
Prec ,
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" ;
2024-03-07 08:45:14 -08:00
import { useAPIQuery } from "../../api/api" ;
import { notifications } from "@mantine/notifications" ;
2024-03-14 04:20:19 -07:00
import { useAppSelector } from "../../state/hooks" ;
2024-03-07 04:16:54 -08:00
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 ;
2024-03-08 06:15:49 -08:00
metricNames : string [ ] ;
2024-03-07 04:16:54 -08:00
executeQuery : ( expr : string ) = > void ;
2024-03-07 07:59:47 -08:00
removePanel : ( ) = > void ;
2024-03-07 04:16:54 -08:00
}
const ExpressionInput : FC < ExpressionInputProps > = ( {
initialExpr ,
2024-03-08 06:15:49 -08:00
metricNames ,
2024-03-07 04:16:54 -08:00
executeQuery ,
2024-03-07 07:59:47 -08:00
removePanel ,
2024-03-07 04:16:54 -08:00
} ) = > {
const theme = useComputedColorScheme ( ) ;
2024-03-14 04:20:19 -07:00
const {
pathPrefix ,
enableAutocomplete ,
enableSyntaxHighlighting ,
enableLinter ,
} = useAppSelector ( ( state ) = > state . settings ) ;
2024-03-07 04:16:54 -08:00
const [ expr , setExpr ] = useState ( initialExpr ) ;
useEffect ( ( ) = > {
setExpr ( initialExpr ) ;
} , [ initialExpr ] ) ;
2024-03-07 08:45:14 -08:00
const {
data : formatResult ,
error : formatError ,
isFetching : isFormatting ,
refetch : formatQuery ,
} = useAPIQuery < string > ( {
key : expr ,
2024-03-08 06:19:43 -08:00
path : "/format_query" ,
2024-03-07 08:45:14 -08:00
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 ( {
color : "green" ,
title : "Expression formatted" ,
message : "Expression formatted successfully!" ,
} ) ;
}
} , [ formatResult , formatError ] ) ;
2024-03-07 04:16:54 -08:00
const queryHistory = [ ] as 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 ,
2024-03-08 06:15:49 -08:00
cache : { initialMetricList : metricNames } ,
2024-03-07 04:16:54 -08:00
} ,
} ) ,
queryHistory
) ,
} ) ;
2024-03-14 04:20:19 -07:00
} , [ pathPrefix , metricNames , enableAutocomplete , enableLinter , queryHistory ] ) ; // TODO: Make this depend on external settings changes, maybe use dynamic config compartment again.
2024-03-07 04:16:54 -08:00
return (
< Group align = "flex-start" wrap = "nowrap" gap = "xs" >
2024-04-03 05:45:35 -07:00
{ / * T O D O : F o r w r a p p e d l o n g l i n e s , t h e i n p u t g r o w s i n w i d t h m o r e a n d m o r e , t h e
longer the line is . Figure out why and fix it . * / }
2024-03-07 04:16:54 -08:00
< InputBase < any >
2024-03-07 08:45:14 -08:00
leftSection = {
isFormatting ? < Loader size = "xs" color = "gray.5" / > : < IconTerminal / >
}
2024-03-07 04:16:54 -08:00
rightSection = {
< Menu shadow = "md" width = { 200 } >
< Menu.Target >
< ActionIcon
size = "lg"
variant = "transparent"
color = "gray"
aria - label = "Decrease range"
>
< 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 ) } } / >
}
>
Explore metrics
< / Menu.Item >
< Menu.Item
leftSection = {
< IconAlignJustified
style = { { width : rem ( 14 ) , height : rem ( 14 ) } }
/ >
}
2024-03-07 08:45:14 -08:00
onClick = { ( ) = > formatQuery ( ) }
disabled = {
2024-03-07 12:00:43 -08:00
isFormatting || expr === "" || expr === formatResult ? . data
2024-03-07 08:45:14 -08:00
}
2024-03-07 04:16:54 -08:00
>
Format expression
< / Menu.Item >
< Menu.Item
color = "red"
leftSection = {
< IconTrash style = { { width : rem ( 14 ) , height : rem ( 14 ) } } / >
}
2024-03-07 07:59:47 -08:00
onClick = { removePanel }
2024-03-07 04:16:54 -08:00
>
Remove query
< / Menu.Item >
< / Menu.Dropdown >
< / Menu >
}
component = { CodeMirror }
className = { classes . input }
basicSetup = { false }
value = { expr }
onChange = { setExpr }
autoFocus
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)" ) ,
2024-03-14 04:20:19 -07:00
enableSyntaxHighlighting
? syntaxHighlighting (
theme === "light" ? promqlHighlighter : darkPromqlHighlighter
)
: [ ] ,
2024-03-07 04:16:54 -08:00
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 ) } >
Execute
< / Button >
< / Group >
) ;
} ;
export default ExpressionInput ;