Skip to content

Commit

Permalink
Use CM-powered QueryEditor in main components
Browse files Browse the repository at this point in the history
  • Loading branch information
ddelemeny committed Feb 5, 2024
1 parent 41f66b9 commit 32996df
Show file tree
Hide file tree
Showing 9 changed files with 162 additions and 140 deletions.
14 changes: 8 additions & 6 deletions src/LogContext/components/LogContextQueryBuilderSidebar.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import React, { useEffect, useMemo, useState } from "react";
// import { Field } from '@grafana/data';
import { useTheme2, CollapsableSection, Icon } from '@grafana/ui';
import { LogContextProps, useSearchableFields } from "./LogContextUI";
import { LogContextProps } from "./LogContextUI";
import { css, cx } from "@emotion/css";
import { LuceneQuery } from "utils/lucene";
import { useQueryBuilderContext } from 'LogContext/QueryBuilder';
import { useQueryBuilderContext } from '@/QueryBuilder/lucene';


// TODO : define sensible defaults here
Expand Down Expand Up @@ -98,23 +98,25 @@ const lcSidebarStyle = css`
`

type QueryBuilderProps = {
searchableFields: any[],
updateQuery: (query: LuceneQuery) => void
}

export function LogContextQueryBuilderSidebar(props: LogContextProps & QueryBuilderProps) {
const builder = useQueryBuilderContext();
const searchableFields = useSearchableFields();

const {row, updateQuery} = props;
const {row, updateQuery, searchableFields} = props;
const [fields, setFields] = useState<Field[]>([]);

const filteredFields = useMemo(() => {
return searchableFields
const searchableFieldsNames = searchableFields.map(f=>f.text);
return row.dataFrame.fields
.filter(f=>searchableFieldsNames.includes(f.name))
// exclude some low-filterability fields
.filter((f)=> !excludedFields.includes(f.name) && isPrimitive(f.type))
// sort fields by name
.sort((f1, f2)=> (f1.name>f2.name ? 1 : -1))
}, [searchableFields]);
}, [row, searchableFields]);

useEffect(() => {
const fields = filteredFields
Expand Down
89 changes: 0 additions & 89 deletions src/LogContext/components/LogContextQueryEditor.tsx

This file was deleted.

34 changes: 8 additions & 26 deletions src/LogContext/components/LogContextUI.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
import React, { useEffect, useState, useCallback, useMemo, createContext } from "react";
import { Field, LogRowModel, } from '@grafana/data';
import React, { useEffect, useState, useCallback, useMemo } from "react";
import { LogRowModel } from '@grafana/data';
import { ElasticsearchQuery as DataQuery } from '../../types';
import { LogContextQueryEditor } from "./LogContextQueryEditor";
import { LuceneQueryEditor } from "../../components/LuceneQueryEditor";

import { css } from "@emotion/css";
import { Button } from "@grafana/ui";
import { useQueryBuilder, QueryBuilderContext } from 'LogContext/QueryBuilder';
import { useQueryBuilder, QueryBuilderContext } from '@/QueryBuilder/lucene';
import { LogContextQueryBuilderSidebar } from "./LogContextQueryBuilderSidebar";
import { DatasourceContext } from "components/QueryEditor/ElasticsearchQueryContext";
import { QuickwitDataSource } from "datasource";
import { getHook } from "utils/context";
import { useDatasourceFields } from "datasource.utils";

const logContextUiStyle = css`
display: flex;
Expand All @@ -28,29 +28,13 @@ export interface LogContextUIProps extends LogContextProps {
datasource: QuickwitDataSource,
updateQuery: (query: string) => void
}
const SearchableFieldsContext = createContext<Field[]|undefined>(undefined)
export const useSearchableFields = getHook(SearchableFieldsContext)

export function LogContextUI(props: LogContextUIProps ){
const builder = useQueryBuilder();
const {query, parsedQuery, setQuery, setParsedQuery} = builder;
const [canRunQuery, setCanRunQuery] = useState<boolean>(false);
const { origQuery, updateQuery, runContextQuery } = props;
const datasource = props.datasource;
const [fields, setFields] = useState<typeof props.row.dataFrame.fields>([]);

useEffect(()=>{
const dfFields = props.row.dataFrame.fields
// Get datasource fields definitions
datasource.getTagKeys({searchable:true}).then((dsFields)=>{
const dsFieldsNames = dsFields.map(f=>f.text)
setFields( dfFields.filter(f=> dsFieldsNames.includes(f.name)))

});



}, [props.row, datasource, setFields])
const {fields, getSuggestions} = useDatasourceFields(props.datasource);

useEffect(()=>{
setQuery(origQuery?.query || '')
Expand Down Expand Up @@ -79,13 +63,11 @@ export function LogContextUI(props: LogContextUIProps ){
<div className={logContextUiStyle}>
<DatasourceContext.Provider value={props.datasource}>
<QueryBuilderContext.Provider value={builder}>
<SearchableFieldsContext.Provider value={fields}>
<LogContextQueryBuilderSidebar {...props} updateQuery={setParsedQuery}/>
<LogContextQueryBuilderSidebar {...props} updateQuery={setParsedQuery} searchableFields={fields}/>
<div className={css`width:100%; display:flex; flex-direction:column; gap:0.5rem; min-width:0;`}>
{ActionBar}
<LogContextQueryEditor />
<LuceneQueryEditor builder={builder} autocompleter={getSuggestions} onChange={builder.setQuery}/>
</div>
</SearchableFieldsContext.Provider>
</QueryBuilderContext.Provider>
</DatasourceContext.Provider>
</div>
Expand Down
2 changes: 1 addition & 1 deletion src/QueryBuilder.ts → src/QueryBuilder/elastic.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import {
TermsQuery,
} from './types';
} from '../types';

export class ElasticQueryBuilder {
timeField: string;
Expand Down
8 changes: 4 additions & 4 deletions src/LogContext/QueryBuilder.ts → src/QueryBuilder/lucene.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import * as lucene from "utils/lucene";
import { useState, useCallback, createContext } from 'react';
import { LuceneQuery as QueryBuilder } from 'utils/lucene';
import { getHook } from "utils/context";
import { getHook } from "@/utils/context";
import * as lucene from "@/utils/lucene";
import { LuceneQuery as QueryBuilder } from '@/utils/lucene';

type LuceneQueryBuilder = {
export type LuceneQueryBuilder = {
query: string,
parsedQuery: lucene.LuceneQuery,
setQuery: (query: string) => void
Expand Down
61 changes: 61 additions & 0 deletions src/components/LuceneQueryEditor.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import React, { useRef, useCallback } from "react";
import { css } from "@emotion/css";

import { LuceneQueryBuilder } from '@/QueryBuilder/lucene';

import CodeMirror, { ReactCodeMirrorRef } from '@uiw/react-codemirror';
import {linter, Diagnostic, lintGutter} from "@codemirror/lint"
import {autocompletion, CompletionContext} from "@codemirror/autocomplete"


export type LuceneQueryEditorProps = {
placeholder?: string,
builder: LuceneQueryBuilder,
autocompleter: (word: string) => any,
onChange: (query: string) => void
}

export function LuceneQueryEditor(props: LuceneQueryEditorProps){
const editorRef = useRef<ReactCodeMirrorRef|null>(null)

const queryLinter = linter( view => {
let diagnostics: Diagnostic[] = [];

const error = props.builder.parsedQuery?.parseError
if (error) {
diagnostics.push({
severity: "error",
message: error.message,
from: view.state.doc.line(error.location.start.line).from + error.location.start.column -1,
to: view.state.doc.line(error.location.end.line).from + error.location.end.column -1,
}) ;
}
return diagnostics
})


const {autocompleter} = props;
const datasourceCompletions = useCallback(async (context: CompletionContext)=>{
let word = context.matchBefore(/\S*/);
if (!word){ return null }
const suggestions = await autocompleter(word?.text);
return {
from: word.from + suggestions.from,
options: suggestions.options
}
},[autocompleter])


const autocomplete = autocompletion({ override: [datasourceCompletions] })

return (<CodeMirror
ref={editorRef}
className={css`height:100%`} // XXX : need to set height for both wrapper elements
height="100%"
theme={'dark'}
placeholder={props.placeholder}
value={props.builder.query}
onChange={props.onChange}
extensions={[queryLinter, lintGutter(), autocomplete]}
/>);
}
39 changes: 26 additions & 13 deletions src/components/QueryEditor/index.tsx
Original file line number Diff line number Diff line change
@@ -1,23 +1,28 @@
import { css } from '@emotion/css';

import React from 'react';
import React, { createContext, useCallback, useEffect } from 'react';

import { CoreApp, getDefaultTimeRange, GrafanaTheme2, QueryEditorProps } from '@grafana/data';
import { InlineLabel, QueryField, useStyles2 } from '@grafana/ui';
import { CoreApp, Field, getDefaultTimeRange, GrafanaTheme2, QueryEditorProps } from '@grafana/data';
import { InlineLabel, useStyles2 } from '@grafana/ui';

import { ElasticDatasource } from '@/datasource';
import { useNextId } from '@/hooks/useNextId';
import { useDispatch } from '@/hooks/useStatelessReducer';
import { ElasticsearchQuery } from '@/types';

import { BucketAggregationsEditor } from './BucketAggregationsEditor';
import { ElasticsearchProvider } from './ElasticsearchQueryContext';
import { ElasticsearchProvider, useDatasource } from './ElasticsearchQueryContext';
import { MetricAggregationsEditor } from './MetricAggregationsEditor';
import { metricAggregationConfig } from './MetricAggregationsEditor/utils';
import { changeQuery } from './state';
import { QuickwitOptions } from '../../quickwit';
import { QueryTypeSelector } from './QueryTypeSelector';

import { useQueryBuilder } from '@/QueryBuilder/lucene';
import { getHook } from 'utils/context';
import { LuceneQueryEditor } from '@/components/LuceneQueryEditor';
import { useDatasourceFields } from 'datasource.utils';

export type ElasticQueryEditorProps = QueryEditorProps<ElasticDatasource, ElasticsearchQuery, QuickwitOptions>;

export const QueryEditor = ({ query, onChange, onRunQuery, datasource, range, app }: ElasticQueryEditorProps) => {
Expand Down Expand Up @@ -45,24 +50,32 @@ const getStyles = (theme: GrafanaTheme2) => ({
`,
});

const SearchableFieldsContext = createContext<Field[]|undefined>(undefined)
export const useSearchableFields = getHook(SearchableFieldsContext)

interface Props {
value: ElasticsearchQuery;
}

export const ElasticSearchQueryField = ({ value, onChange }: { value?: string; onChange: (v: string) => void }) => {
const styles = useStyles2(getStyles);
const builder = useQueryBuilder();
const {setQuery} = builder;
const datasource = useDatasource()
const { getSuggestions } = useDatasourceFields(datasource);

useEffect(()=>{
setQuery(value || '')
}, [setQuery, value])

const onEditorChange = useCallback((query: string)=>{
setQuery(query);
onChange(query)
},[setQuery, onChange])

return (
<div className={styles.queryItem}>
<QueryField
query={value}
// By default QueryField calls onChange if onBlur is not defined, this will trigger a rerender
// And slate will claim the focus, making it impossible to leave the field.
onBlur={() => {}}
onChange={onChange}
placeholder="Enter a lucene query"
portalOrigin="elasticsearch"
/>
<LuceneQueryEditor placeholder="Enter a lucene query" builder={builder} autocompleter={getSuggestions} onChange={onEditorChange}/>
</div>
);
};
Expand Down
2 changes: 1 addition & 1 deletion src/datasource.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ import {
TemplateSrv,
getDataSourceSrv } from '@grafana/runtime';
import { QuickwitOptions } from 'quickwit';
import { ElasticQueryBuilder } from 'QueryBuilder';
import { ElasticQueryBuilder } from '@/QueryBuilder/elastic';
import { colors } from '@grafana/ui';

import { BarAlignment, DataQuery, GraphDrawStyle, StackingMode } from '@grafana/schema';
Expand Down
Loading

0 comments on commit 32996df

Please sign in to comment.