Skip to content

Commit

Permalink
Enable log context query edition, refactor
Browse files Browse the repository at this point in the history
  • Loading branch information
ddelemeny committed Jan 18, 2024
1 parent 7060dac commit 2637672
Show file tree
Hide file tree
Showing 12 changed files with 271 additions and 152 deletions.
83 changes: 41 additions & 42 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -65,10 +65,10 @@
"@grafana/experimental": "^1.1.0",
"@grafana/runtime": "9.5.14",
"@grafana/ui": "9.5.14",
"@monaco-editor/react": "^4.6.0",
"@reduxjs/toolkit": "^1.9.5",
"react": "17.0.2",
"react-dom": "17.0.2",
"react-monaco-editor": "^0.55.0",
"tslib": "2.5.3"
},
"packageManager": "[email protected]"
Expand Down
20 changes: 12 additions & 8 deletions src/LogContextProvider.ts → src/LogContext/LogContextProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,9 @@ import {
rangeUtil,
} from '@grafana/data';

import { ElasticsearchQuery, Logs} from './types';
import { ElasticsearchQuery, Logs} from '../types';

import { LogContextUI } from 'components/LogContext/LogContextUI';
import { LogContextUI } from 'LogContext/components/LogContextUI';

export interface LogRowContextOptions {
direction?: LogRowContextQueryDirection;
Expand Down Expand Up @@ -44,9 +44,11 @@ function createContextTimeRange(rowTimeEpochMs: number, direction: string) {

export class LogContextProvider {
datasource: QuickwitDataSource;
contextQuery: string[];

constructor(datasource: QuickwitDataSource) {
this.datasource = datasource;
this.contextQuery = [];
}

private makeLogContextDataRequest = (
Expand All @@ -72,7 +74,7 @@ export class LogContextProvider {
const query: ElasticsearchQuery = {
refId: `log-context-${row.dataFrame.refId}-${direction}`,
metrics: [logQuery],
query: origQuery?.query,
query: this.contextQuery.join(' ')
};

const timeRange = createContextTimeRange(row.timeEpochMs, direction);
Expand Down Expand Up @@ -119,11 +121,13 @@ export class LogContextProvider {
)
);
};

getLogRowContextUi(
row: LogRowModel,
runContextQuery?: (() => void),
origQuery?: ElasticsearchQuery
): ReactNode {
return ( LogContextUI({row, runContextQuery, origQuery}))
row: LogRowModel,
runContextQuery?: (() => void),
origQuery?: ElasticsearchQuery
): ReactNode {
const updateQuery = (query:string) => { this.contextQuery = [query]; }

Check failure on line 130 in src/LogContext/LogContextProvider.ts

View workflow job for this annotation

GitHub Actions / build

Expected a space after the ':'
return ( LogContextUI({row, runContextQuery, origQuery, updateQuery}))
}
}
24 changes: 24 additions & 0 deletions src/LogContext/QueryBuilder.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { useEffect, useState } from "react";
import { ContextFilter } from "./types";

export function useQueryBuilder(initialContextFilters: ContextFilter[]){
const [contextFilters, setContextFilters] = useState<ContextFilter[]>(initialContextFilters)
const [query, setQuery] = useState<string>('')

const updateFilters = (contextFilters: ContextFilter[]) => {
setContextFilters(contextFilters);
}

useEffect(()=>{
const preparedQuery = contextFilters
.map(filter => (filter.value ? filter.value : ''))
.join(' ')
setQuery(preparedQuery);
}, [contextFilters])

return {
contextFilters,
updateFilters,
query,
}
}
103 changes: 103 additions & 0 deletions src/LogContext/components/LogContextQueryBuilderSidebar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import React, { useEffect, useMemo, useState } from "react";
import { Field } from '@grafana/data';
import { CollapsableSection } from '@grafana/ui';
import { LogContextUIProps } from "./LogContextUI";
import { css } from "@emotion/css";

const filterFields = [
'attributes.appname',
'attributes.proc_id',
'attributes.hostname',
'attributes.priority',
'severity_number',
'severity_text'
];

type FieldContingency = {[value: string]: number};


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 LogContextAttributeItem(props: {label: string, count: number, active?: boolean, onClick: () => void}){
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>{props.count}</span>
</a>
)
}

const lcSidebarStyle = css`
width: 300px;
min-width: 300px;
flex-shrink: 0;
overflow-y: scroll;
padding-right: 1rem;
`

type QueryBuilderProps = {
activeFilters?: string[],
onUpdateFilters?: (filters: string[]) => void
}

export function LogContextQueryBuilderSidebar(props: LogContextUIProps & QueryBuilderProps) {
const [fields, setFields] = useState<Array<{ name: string; contingency: FieldContingency; }>>([]);
const filteredFields = useMemo(() => (
props.row.dataFrame.fields.filter((f: Field) => (filterFields.includes(f.name)))
), [props.row.dataFrame.fields]);

useEffect(() => {
setFields(filteredFields.map((f) => {
const contingency: FieldContingency = {};
f.values.toArray().forEach(attrName => {
contingency[attrName] = (contingency[attrName] || 0) + 1;
});
return { name: f.name, contingency };
}));
}, [filteredFields]);

const selectQueryFilter = (attrName: string, attrValue: string): (() => void) => {
return () => {
if (!(props.onUpdateFilters && props.activeFilters)) {return;}

const newActiveFilters = [...props.activeFilters];
const filter = `${attrName}:${attrValue}`;
if (newActiveFilters.includes(filter)){
newActiveFilters.splice(newActiveFilters.findIndex(v => (v===filter)), 1);
} else {
newActiveFilters.push(filter)
}
props.onUpdateFilters(newActiveFilters)
};
}

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, count]: [string, number], i) => (
<LogContextAttributeItem
label={fieldValue} count={count} key={`field-opt${i}`}
onClick={selectQueryFilter(field.name, fieldValue)}
active={props.activeFilters?.includes(`${field.name}:${fieldValue}`)}
/>
))}
</div>
</CollapsableSection>
))}
</div>
);
}
33 changes: 33 additions & 0 deletions src/LogContext/components/LogContextQueryEditor.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import React from "react";
import Editor from "@monaco-editor/react";

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}
options={editorOptions}
/>
);
}
Loading

0 comments on commit 2637672

Please sign in to comment.