Skip to content

Commit

Permalink
Improve LogContext feedback WIP
Browse files Browse the repository at this point in the history
  • Loading branch information
ddelemeny committed Jan 25, 2024
1 parent 1cd48d6 commit 48e06a1
Show file tree
Hide file tree
Showing 4 changed files with 162 additions and 72 deletions.
28 changes: 17 additions & 11 deletions src/LogContext/QueryBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,26 +12,29 @@ const splitBinaryAST = (ast: lucene.BinaryAST): lucene.LeftOnlyAST[] => {
return result;
};

export type ParseError = {
name: string,
message: string,
location: {
start: {line: number, column: number, offset: number},
end: {line: number, column: number, offset: number},
}
}

export class QuickwitQuery {
baseQuery: string;
_parsedQuery: lucene.AST | null;
_parseError?: any;
parsedQuery: lucene.AST | null;
_parseError?: ParseError;

constructor(query: string) {
this.baseQuery = query;
this._parsedQuery = null
}

get parsedQuery() {
this.parsedQuery = null;
try {
this._parsedQuery = lucene.parse(this.baseQuery);
this.parsedQuery = lucene.parse(this.baseQuery);
this._parseError = undefined;

} catch(_e: any) {
this._parseError = _e
}

return this._parsedQuery
}

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

findFilter(filter: string): number {
// Naive, slow, use AST
return this.topLevelFilters.findIndex((astEl) => astEl === filter);
return this.topLevelFilters.findIndex((astEl) => {
const match = astEl.match(filter);
return match && match.length > 0
});
}

addFilter(filter: string) {
Expand Down
98 changes: 62 additions & 36 deletions src/LogContext/components/LogContextQueryBuilderSidebar.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import React, { useEffect, useMemo, useState } from "react";
// import { Field } from '@grafana/data';
import { CollapsableSection } from '@grafana/ui';
import { useTheme2, CollapsableSection, Icon } from '@grafana/ui';
import { LogContextProps } from "./LogContextUI";
import { css } from "@emotion/css";
import { css, cx } from "@emotion/css";
import { QuickwitQuery } from 'LogContext/QueryBuilder';


Expand All @@ -21,24 +21,25 @@ const excludedFields = [
type FieldContingency = { [value: string]: {
count: number, pinned: boolean, active?: boolean
}};
type Field = {
name: string,
contingency: FieldContingency
}


const lcAttributeItemStyle = css`
display: flex;
justify-content: space-between;
font-size: 0.8rem;
padding-left: 10px;
&[data-active=true] {
background-color: rgba(127,127,255, 0.1);
}
&:hover {
background-color: rgba(255,255,255,0.1);
background-opacity: 0.2;
}
`;

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

type AttrbuteItemsProps = {
type FieldItemProps = {
label: string,
contingency: {
count: number,
Expand All @@ -47,11 +48,25 @@ type AttrbuteItemsProps = {
active?: boolean,
onClick: () => void
}
function LogContextAttributeItem(props: AttrbuteItemsProps){
function LogContextFieldItem(props: FieldItemProps){
const theme = useTheme2()
const lcAttributeItemStyle = css({
display: "flex",
justifyContent: "space-between",
paddingLeft: "10px",
fontSize: theme.typography.bodySmall.fontSize,
"&[data-active=true]": {
backgroundColor: theme.colors.primary.transparent,
},
"&:hover": {
backgroundColor: theme.colors.secondary.shade,
}
}) ;

return (
<a className={lcAttributeItemStyle} onClick={props.onClick} data-active={props.active}>
<span className={css`text-overflow:ellipsis; min-width:0; flex:0 1`}>{props.label}</span>
<span className={css`flex-grow:0`}>{props.contingency.pinned && "* "}{props.contingency.count}</span>
<span className={css`flex-grow:0`}>{props.contingency.pinned && <Icon name={"crosshair"}/>}{props.contingency.count}</span>
</a>
)
}
Expand All @@ -72,7 +87,7 @@ type QueryBuilderProps = {
export function LogContextQueryBuilderSidebar(props: LogContextProps & QueryBuilderProps) {

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


const filteredFields = useMemo(() => (
Expand All @@ -88,7 +103,7 @@ export function LogContextQueryBuilderSidebar(props: LogContextProps & QueryBuil
const fields = filteredFields
.map((f) => {
const contingency: FieldContingency = {};
f.values.toArray().forEach((attrName, i) => {
f.values.forEach((attrName, i) => {
if (!contingency[attrName]) {
contingency[attrName] = {
count: 0,
Expand All @@ -115,23 +130,34 @@ export function LogContextQueryBuilderSidebar(props: LogContextProps & QueryBuil
}
}

const renderFieldSection = (field: Field)=>{
return (
<CollapsableSection
label={LogContextFieldSection(field)}
className={css`& > div { flex-grow:1; }` }
isOpen={false} key="log-attribute-field-{field.name}"
contentClassName={cx(css`margin:0; padding:0`)}>
<div className={css`display:flex; flex-direction:column; gap:5px`}>

{field.contingency && Object.entries(field.contingency).map(([fieldValue, contingency], i) => (

<LogContextFieldItem
label={fieldValue} contingency={contingency} key={`field-opt${i}`}
onClick={() => {selectQueryFilter(field.name, fieldValue)}}
active={contingency.active}
/>
))}
</div>
</CollapsableSection>
)
}


return (
<div className={lcSidebarStyle}>
{fields && fields.map((field) => (
<CollapsableSection label={(<h6>{field.name}</h6>)} isOpen={false} key="log-attribute-field-{field.name}" contentClassName={css`margin:0; padding:0`}>
<div className={css`display:flex; flex-direction:column; gap:5px`}>

{field.contingency && Object.entries(field.contingency).map(([fieldValue, contingency], i) => (

<LogContextAttributeItem
label={fieldValue} contingency={contingency} key={`field-opt${i}`}
onClick={() => {selectQueryFilter(field.name, fieldValue)}}
active={contingency.active}
/>
))}
</div>
</CollapsableSection>
))}
</div>
{fields && fields.map((field) => {

return( renderFieldSection(field) );
}) } </div>
);
}
105 changes: 82 additions & 23 deletions src/LogContext/components/LogContextQueryEditor.tsx
Original file line number Diff line number Diff line change
@@ -1,33 +1,92 @@
import React from "react";
import React, { useEffect, useRef, useState } from "react";
import Editor from "@monaco-editor/react";
import { editor, MarkerSeverity } from "monaco-editor"
import { QuickwitQuery } from "LogContext/QueryBuilder";
import { css } from "@emotion/css";


function useDomElement(id: string, root: HTMLElement) {
const ref = useRef<HTMLElement|null>(null)

let domElement: HTMLElement| null = document.getElementById(id) as HTMLElement
if (domElement === null){
domElement = document.createElement('div')
domElement.id= id
root.appendChild(domElement)
}

ref.current = domElement;
return [ref]
}

export function LogContextQueryEditor(props: {query: string, parsedQuery: QuickwitQuery|null, onChange?: (value?: string) => void}){
const {query, parsedQuery, onChange} = props;
const editorRef = useRef<editor.IStandaloneCodeEditor|null>(null);
const [model, setModel] = useState<editor.ITextModel|null>(null);

// HACK : Monaco widgets may show up at a wrong position if in nested transforms
// adding an overlay attempts to circumvent the issue
// see https://github.com/microsoft/monaco-editor/issues/2793#issuecomment-999337740
const [overflowWidgetsNode] = useDomElement('quickwit-monaco-overflow-widgets', document.body);
if (overflowWidgetsNode.current){
overflowWidgetsNode.current.className = css`
position:fixed;
z-index:1999;
.monaco-hover {
border: 1px solid rgba(255,255,255,0.2);
}
`;
}

function handleEditorDidMount(editor: editor.IStandaloneCodeEditor){
editorRef.current = editor
setModel(editor.getModel())
}

const editorOptions = {
fontFamily: 'monospace',
overviewRulerBorder: false,
overviewRulerLanes: 0,
minimap: { enabled: false },
fontSize: 12,
fixedOverflowWidgets: true,
automaticLayout: true,
overflowWidgetsDomNode: overflowWidgetsNode.current || undefined,
}

useEffect(()=>{
const markers: editor.IMarkerData[] = [];

const error = parsedQuery?._parseError
if (error){
markers.push({
message: error.message,
severity: MarkerSeverity.Error,
startLineNumber: error.location.start.line,
startColumn: error.location.start.column,
endLineNumber: error.location.end.line,
endColumn: error.location.end.column,
});
}
if (model){
editor.setModelMarkers(model, "owner", markers)
}

}, [parsedQuery, model])


function handleChange(value?: string) {
if (onChange) { onChange(value)}
}

const editorOptions = {
// readOnly: false,
fontFamily: 'monospace',
overviewRulerBorder: false,
overviewRulerLanes: 0,
minimap: { enabled: false },
// scrollbar: {
// alwaysConsumeMouseWheel: false,
// },
// renderLineHighlight: "gutter",
fontSize: 12,
// fixedOverflowWidgets: true,
// scrollBeyondLastLine: false,
automaticLayout: true,
// wordWrap: 'on',
// wrappingIndent: 'deepIndent',
};


export function LogContextQueryEditor(props: {query: string, onChange?: (value?: string) => void}){
return (
<Editor
height="100%"
theme='vs-dark'
value={props.query}
onChange={props.onChange}
value={query}
onChange={handleChange}
options={editorOptions}
onMount={handleEditorDidMount}
/>
);
}
3 changes: 1 addition & 2 deletions src/LogContext/components/LogContextUI.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,6 @@ export function LogContextUI(props: LogContextUIProps ){
}, [setQuery, origQuery])

const updateQueryFromSidebar = (query: QuickwitQuery)=>{
console.log('update from sidebar', query.toString())
setParsedQuery(query)
}
const updateQueryFromEditor = useCallback((content: string) => {
Expand All @@ -62,7 +61,7 @@ export function LogContextUI(props: LogContextUIProps ){
<Button variant="secondary" onClick={resetQuery}>Reset</Button>
<Button onClick={props.runContextQuery} {...canRunQuery ? {} : {disabled:true, tooltip:"Failed to parse query"}} >Run query</Button>
</div>
<LogContextQueryEditor {...props} query={query} onChange={(content) => updateQueryFromEditor(content || '')}/>
<LogContextQueryEditor {...props} query={query} parsedQuery={parsedQuery} onChange={(content) => updateQueryFromEditor(content || '')}/>
</div>
</div>
);
Expand Down

0 comments on commit 48e06a1

Please sign in to comment.