Skip to content

Commit 48e06a1

Browse files
committed
Improve LogContext feedback WIP
1 parent 1cd48d6 commit 48e06a1

File tree

4 files changed

+162
-72
lines changed

4 files changed

+162
-72
lines changed

src/LogContext/QueryBuilder.ts

+17-11
Original file line numberDiff line numberDiff line change
@@ -12,26 +12,29 @@ const splitBinaryAST = (ast: lucene.BinaryAST): lucene.LeftOnlyAST[] => {
1212
return result;
1313
};
1414

15+
export type ParseError = {
16+
name: string,
17+
message: string,
18+
location: {
19+
start: {line: number, column: number, offset: number},
20+
end: {line: number, column: number, offset: number},
21+
}
22+
}
23+
1524
export class QuickwitQuery {
1625
baseQuery: string;
17-
_parsedQuery: lucene.AST | null;
18-
_parseError?: any;
26+
parsedQuery: lucene.AST | null;
27+
_parseError?: ParseError;
1928

2029
constructor(query: string) {
2130
this.baseQuery = query;
22-
this._parsedQuery = null
23-
}
24-
25-
get parsedQuery() {
31+
this.parsedQuery = null;
2632
try {
27-
this._parsedQuery = lucene.parse(this.baseQuery);
33+
this.parsedQuery = lucene.parse(this.baseQuery);
2834
this._parseError = undefined;
29-
3035
} catch(_e: any) {
3136
this._parseError = _e
3237
}
33-
34-
return this._parsedQuery
3538
}
3639

3740
get isParsedSuccessfully() {
@@ -53,7 +56,10 @@ export class QuickwitQuery {
5356

5457
findFilter(filter: string): number {
5558
// Naive, slow, use AST
56-
return this.topLevelFilters.findIndex((astEl) => astEl === filter);
59+
return this.topLevelFilters.findIndex((astEl) => {
60+
const match = astEl.match(filter);
61+
return match && match.length > 0
62+
});
5763
}
5864

5965
addFilter(filter: string) {

src/LogContext/components/LogContextQueryBuilderSidebar.tsx

+62-36
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import React, { useEffect, useMemo, useState } from "react";
22
// import { Field } from '@grafana/data';
3-
import { CollapsableSection } from '@grafana/ui';
3+
import { useTheme2, CollapsableSection, Icon } from '@grafana/ui';
44
import { LogContextProps } from "./LogContextUI";
5-
import { css } from "@emotion/css";
5+
import { css, cx } from "@emotion/css";
66
import { QuickwitQuery } from 'LogContext/QueryBuilder';
77

88

@@ -21,24 +21,25 @@ const excludedFields = [
2121
type FieldContingency = { [value: string]: {
2222
count: number, pinned: boolean, active?: boolean
2323
}};
24+
type Field = {
25+
name: string,
26+
contingency: FieldContingency
27+
}
2428

2529

26-
const lcAttributeItemStyle = css`
27-
display: flex;
28-
justify-content: space-between;
29-
font-size: 0.8rem;
30-
padding-left: 10px;
31-
&[data-active=true] {
32-
background-color: rgba(127,127,255, 0.1);
33-
}
34-
&:hover {
35-
background-color: rgba(255,255,255,0.1);
36-
background-opacity: 0.2;
37-
}
38-
`;
3930

31+
function LogContextFieldSection(field: Field) {
32+
const theme = useTheme2()
33+
const hasActiveFilters = Object.entries(field.contingency).map(([_,entry])=>!!entry.active).reduce((a,b)=>a || b, false);
34+
return(
35+
<span className={css({fontSize:theme.typography.body.fontSize, display:"flex", alignItems: "baseline", gap:"0.5rem", width:"100%"})}>
36+
{hasActiveFilters && <Icon name={"filter"} className={css({ color:theme.colors.primary.text })}/>}
37+
<span>{field.name}</span>
38+
</span>
39+
)
40+
}
4041

41-
type AttrbuteItemsProps = {
42+
type FieldItemProps = {
4243
label: string,
4344
contingency: {
4445
count: number,
@@ -47,11 +48,25 @@ type AttrbuteItemsProps = {
4748
active?: boolean,
4849
onClick: () => void
4950
}
50-
function LogContextAttributeItem(props: AttrbuteItemsProps){
51+
function LogContextFieldItem(props: FieldItemProps){
52+
const theme = useTheme2()
53+
const lcAttributeItemStyle = css({
54+
display: "flex",
55+
justifyContent: "space-between",
56+
paddingLeft: "10px",
57+
fontSize: theme.typography.bodySmall.fontSize,
58+
"&[data-active=true]": {
59+
backgroundColor: theme.colors.primary.transparent,
60+
},
61+
"&:hover": {
62+
backgroundColor: theme.colors.secondary.shade,
63+
}
64+
}) ;
65+
5166
return (
5267
<a className={lcAttributeItemStyle} onClick={props.onClick} data-active={props.active}>
5368
<span className={css`text-overflow:ellipsis; min-width:0; flex:0 1`}>{props.label}</span>
54-
<span className={css`flex-grow:0`}>{props.contingency.pinned && "* "}{props.contingency.count}</span>
69+
<span className={css`flex-grow:0`}>{props.contingency.pinned && <Icon name={"crosshair"}/>}{props.contingency.count}</span>
5570
</a>
5671
)
5772
}
@@ -72,7 +87,7 @@ type QueryBuilderProps = {
7287
export function LogContextQueryBuilderSidebar(props: LogContextProps & QueryBuilderProps) {
7388

7489
const {row, parsedQuery, updateQuery} = props;
75-
const [fields, setFields] = useState<Array<{ name: string; contingency: FieldContingency; }>>([]);
90+
const [fields, setFields] = useState<Field[]>([]);
7691

7792

7893
const filteredFields = useMemo(() => (
@@ -88,7 +103,7 @@ export function LogContextQueryBuilderSidebar(props: LogContextProps & QueryBuil
88103
const fields = filteredFields
89104
.map((f) => {
90105
const contingency: FieldContingency = {};
91-
f.values.toArray().forEach((attrName, i) => {
106+
f.values.forEach((attrName, i) => {
92107
if (!contingency[attrName]) {
93108
contingency[attrName] = {
94109
count: 0,
@@ -115,23 +130,34 @@ export function LogContextQueryBuilderSidebar(props: LogContextProps & QueryBuil
115130
}
116131
}
117132

133+
const renderFieldSection = (field: Field)=>{
134+
return (
135+
<CollapsableSection
136+
label={LogContextFieldSection(field)}
137+
className={css`& > div { flex-grow:1; }` }
138+
isOpen={false} key="log-attribute-field-{field.name}"
139+
contentClassName={cx(css`margin:0; padding:0`)}>
140+
<div className={css`display:flex; flex-direction:column; gap:5px`}>
141+
142+
{field.contingency && Object.entries(field.contingency).map(([fieldValue, contingency], i) => (
143+
144+
<LogContextFieldItem
145+
label={fieldValue} contingency={contingency} key={`field-opt${i}`}
146+
onClick={() => {selectQueryFilter(field.name, fieldValue)}}
147+
active={contingency.active}
148+
/>
149+
))}
150+
</div>
151+
</CollapsableSection>
152+
)
153+
}
154+
155+
118156
return (
119157
<div className={lcSidebarStyle}>
120-
{fields && fields.map((field) => (
121-
<CollapsableSection label={(<h6>{field.name}</h6>)} isOpen={false} key="log-attribute-field-{field.name}" contentClassName={css`margin:0; padding:0`}>
122-
<div className={css`display:flex; flex-direction:column; gap:5px`}>
123-
124-
{field.contingency && Object.entries(field.contingency).map(([fieldValue, contingency], i) => (
125-
126-
<LogContextAttributeItem
127-
label={fieldValue} contingency={contingency} key={`field-opt${i}`}
128-
onClick={() => {selectQueryFilter(field.name, fieldValue)}}
129-
active={contingency.active}
130-
/>
131-
))}
132-
</div>
133-
</CollapsableSection>
134-
))}
135-
</div>
158+
{fields && fields.map((field) => {
159+
160+
return( renderFieldSection(field) );
161+
}) } </div>
136162
);
137163
}
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,92 @@
1-
import React from "react";
1+
import React, { useEffect, useRef, useState } from "react";
22
import Editor from "@monaco-editor/react";
3+
import { editor, MarkerSeverity } from "monaco-editor"
4+
import { QuickwitQuery } from "LogContext/QueryBuilder";
5+
import { css } from "@emotion/css";
6+
7+
8+
function useDomElement(id: string, root: HTMLElement) {
9+
const ref = useRef<HTMLElement|null>(null)
10+
11+
let domElement: HTMLElement| null = document.getElementById(id) as HTMLElement
12+
if (domElement === null){
13+
domElement = document.createElement('div')
14+
domElement.id= id
15+
root.appendChild(domElement)
16+
}
17+
18+
ref.current = domElement;
19+
return [ref]
20+
}
21+
22+
export function LogContextQueryEditor(props: {query: string, parsedQuery: QuickwitQuery|null, onChange?: (value?: string) => void}){
23+
const {query, parsedQuery, onChange} = props;
24+
const editorRef = useRef<editor.IStandaloneCodeEditor|null>(null);
25+
const [model, setModel] = useState<editor.ITextModel|null>(null);
26+
27+
// HACK : Monaco widgets may show up at a wrong position if in nested transforms
28+
// adding an overlay attempts to circumvent the issue
29+
// see https://github.com/microsoft/monaco-editor/issues/2793#issuecomment-999337740
30+
const [overflowWidgetsNode] = useDomElement('quickwit-monaco-overflow-widgets', document.body);
31+
if (overflowWidgetsNode.current){
32+
overflowWidgetsNode.current.className = css`
33+
position:fixed;
34+
z-index:1999;
35+
.monaco-hover {
36+
border: 1px solid rgba(255,255,255,0.2);
37+
}
38+
`;
39+
}
40+
41+
function handleEditorDidMount(editor: editor.IStandaloneCodeEditor){
42+
editorRef.current = editor
43+
setModel(editor.getModel())
44+
}
45+
46+
const editorOptions = {
47+
fontFamily: 'monospace',
48+
overviewRulerBorder: false,
49+
overviewRulerLanes: 0,
50+
minimap: { enabled: false },
51+
fontSize: 12,
52+
fixedOverflowWidgets: true,
53+
automaticLayout: true,
54+
overflowWidgetsDomNode: overflowWidgetsNode.current || undefined,
55+
}
56+
57+
useEffect(()=>{
58+
const markers: editor.IMarkerData[] = [];
59+
60+
const error = parsedQuery?._parseError
61+
if (error){
62+
markers.push({
63+
message: error.message,
64+
severity: MarkerSeverity.Error,
65+
startLineNumber: error.location.start.line,
66+
startColumn: error.location.start.column,
67+
endLineNumber: error.location.end.line,
68+
endColumn: error.location.end.column,
69+
});
70+
}
71+
if (model){
72+
editor.setModelMarkers(model, "owner", markers)
73+
}
74+
75+
}, [parsedQuery, model])
76+
77+
78+
function handleChange(value?: string) {
79+
if (onChange) { onChange(value)}
80+
}
381

4-
const editorOptions = {
5-
// readOnly: false,
6-
fontFamily: 'monospace',
7-
overviewRulerBorder: false,
8-
overviewRulerLanes: 0,
9-
minimap: { enabled: false },
10-
// scrollbar: {
11-
// alwaysConsumeMouseWheel: false,
12-
// },
13-
// renderLineHighlight: "gutter",
14-
fontSize: 12,
15-
// fixedOverflowWidgets: true,
16-
// scrollBeyondLastLine: false,
17-
automaticLayout: true,
18-
// wordWrap: 'on',
19-
// wrappingIndent: 'deepIndent',
20-
};
21-
22-
23-
export function LogContextQueryEditor(props: {query: string, onChange?: (value?: string) => void}){
2482
return (
2583
<Editor
2684
height="100%"
2785
theme='vs-dark'
28-
value={props.query}
29-
onChange={props.onChange}
86+
value={query}
87+
onChange={handleChange}
3088
options={editorOptions}
89+
onMount={handleEditorDidMount}
3190
/>
3291
);
3392
}

src/LogContext/components/LogContextUI.tsx

+1-2
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,6 @@ export function LogContextUI(props: LogContextUIProps ){
3737
}, [setQuery, origQuery])
3838

3939
const updateQueryFromSidebar = (query: QuickwitQuery)=>{
40-
console.log('update from sidebar', query.toString())
4140
setParsedQuery(query)
4241
}
4342
const updateQueryFromEditor = useCallback((content: string) => {
@@ -62,7 +61,7 @@ export function LogContextUI(props: LogContextUIProps ){
6261
<Button variant="secondary" onClick={resetQuery}>Reset</Button>
6362
<Button onClick={props.runContextQuery} {...canRunQuery ? {} : {disabled:true, tooltip:"Failed to parse query"}} >Run query</Button>
6463
</div>
65-
<LogContextQueryEditor {...props} query={query} onChange={(content) => updateQueryFromEditor(content || '')}/>
64+
<LogContextQueryEditor {...props} query={query} parsedQuery={parsedQuery} onChange={(content) => updateQueryFromEditor(content || '')}/>
6665
</div>
6766
</div>
6867
);

0 commit comments

Comments
 (0)