Skip to content

Commit

Permalink
feat: add query editor
Browse files Browse the repository at this point in the history
  • Loading branch information
Loori-R committed Feb 6, 2024
1 parent 06b14e5 commit 86694fc
Show file tree
Hide file tree
Showing 13 changed files with 150 additions and 666 deletions.
59 changes: 9 additions & 50 deletions src/components/QueryEditor/QueryCodeEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,40 +2,23 @@ import { css } from '@emotion/css';
import React from 'react';

import { GrafanaTheme2 } from '@grafana/data';
import { config } from '@grafana/runtime';
import { useStyles2, HorizontalGroup, IconButton, Tooltip, Icon } from '@grafana/ui';
import { getModKey } from 'app/core/utils/browser';
import { useStyles2 } from '@grafana/ui';

import { testIds } from '../../components/LokiQueryEditor';
import { LokiQueryField } from '../../components/LokiQueryField';
import { LokiQueryEditorProps } from '../../components/types';
import { formatLogqlQuery } from '../../queryUtils';
import { VictoriaLogsQueryEditorProps } from "../../types";

import { LokiQueryBuilderExplained } from './LokiQueryBuilderExplained';
import QueryField from "./QueryField";

type Props = LokiQueryEditorProps & {
type Props = VictoriaLogsQueryEditorProps & {
showExplain: boolean;
};

export function LokiQueryCodeEditor({
query,
datasource,
range,
onRunQuery,
onChange,
data,
app,
showExplain,
history,
}: Props) {
const QueryCodeEditor = (props: Props) => {
const { query, datasource, range, onRunQuery, onChange, data, app, history } = props;
const styles = useStyles2(getStyles);

const lokiFormatQuery = config.featureToggles.lokiFormatQuery;
const onClickFormatQueryButton = async () => onChange({ ...query, expr: formatLogqlQuery(query.expr, datasource) });

return (
<div className={styles.wrapper}>
<LokiQueryField
<QueryField
datasource={datasource}
query={query}
range={range}
Expand All @@ -44,30 +27,7 @@ export function LokiQueryCodeEditor({
history={history}
data={data}
app={app}
data-testid={testIds.editor}
ExtraFieldElement={
<>
{lokiFormatQuery && (
<div className={styles.buttonGroup}>
<div>
<HorizontalGroup spacing="sm">
<IconButton
onClick={onClickFormatQueryButton}
name="brackets-curly"
size="xs"
tooltip="Format query"
/>
<Tooltip content={`Use ${getModKey()}+z to undo`}>
<Icon className={styles.hint} name="keyboard" />
</Tooltip>
</HorizontalGroup>
</div>
</div>
)}
</>
}
/>
{showExplain && <LokiQueryBuilderExplained query={query.expr} />}
</div>
);
}
Expand All @@ -76,9 +36,6 @@ const getStyles = (theme: GrafanaTheme2) => {
return {
wrapper: css`
max-width: 100%;
.gf-form {
margin-bottom: 0.5;
}
`,
buttonGroup: css`
border: 1px solid ${theme.colors.border.medium};
Expand All @@ -97,3 +54,5 @@ const getStyles = (theme: GrafanaTheme2) => {
`,
};
};

export default QueryCodeEditor
200 changes: 16 additions & 184 deletions src/components/QueryEditor/QueryEditor.tsx
Original file line number Diff line number Diff line change
@@ -1,220 +1,52 @@
import { isEqual } from 'lodash';
import React, { SyntheticEvent, useCallback, useEffect, useId, useState } from 'react';
import { usePrevious } from 'react-use';
import React, { useEffect, useState } from 'react';

import { CoreApp, LoadingState } from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors';
import { EditorHeader, EditorRows, FlexItem, Space, Stack } from '@grafana/experimental';
import { config, reportInteraction } from '@grafana/runtime';
import { Button, ConfirmModal } from '@grafana/ui';
import { QueryEditorModeToggle } from 'app/plugins/datasource/prometheus/querybuilder/shared/QueryEditorModeToggle';
import { QueryHeaderSwitch } from 'app/plugins/datasource/prometheus/querybuilder/shared/QueryHeaderSwitch';
import { QueryEditorMode } from 'app/plugins/datasource/prometheus/querybuilder/shared/types';
import { Button } from '@grafana/ui';

import { LabelBrowserModal } from '../querybuilder/components/LabelBrowserModal';
import { LokiQueryBuilderContainer } from '../querybuilder/components/LokiQueryBuilderContainer';
import { LokiQueryBuilderOptions } from '../querybuilder/components/LokiQueryBuilderOptions';
import { LokiQueryCodeEditor } from '../querybuilder/components/LokiQueryCodeEditor';
import { QueryPatternsModal } from '../querybuilder/components/QueryPatternsModal';
import { buildVisualQueryFromString } from '../querybuilder/parsing';
import { changeEditorMode, getQueryWithDefaults } from '../querybuilder/state';
import { LokiQuery, QueryStats } from '../types';
import { Query, VictoriaLogsQueryEditorProps } from "../../types";

import { shouldUpdateStats } from './stats';
import { LokiQueryEditorProps } from './types';
import QueryCodeEditor from "./QueryCodeEditor";
import { getQueryWithDefaults } from "./state";

export const testIds = {
editor: 'loki-editor',
};

export const lokiQueryEditorExplainKey = 'LokiQueryEditorExplainDefault';

export const LokiQueryEditor = React.memo<LokiQueryEditorProps>((props) => {
const id = useId();
const { onChange, onRunQuery, onAddQuery, data, app, queries, datasource, range: timeRange } = props;
const [parseModalOpen, setParseModalOpen] = useState(false);
const [queryPatternsModalOpen, setQueryPatternsModalOpen] = useState(false);
const QueryEditor = React.memo<VictoriaLogsQueryEditorProps>((props) => {
const { onChange, onRunQuery, data, app, queries } = props;
const [dataIsStale, setDataIsStale] = useState(false);
const [labelBrowserVisible, setLabelBrowserVisible] = useState(false);
const [queryStats, setQueryStats] = useState<QueryStats | null>(null);
const [explain, setExplain] = useState(window.localStorage.getItem(lokiQueryEditorExplainKey) === 'true');

const predefinedOperations = datasource.predefinedOperations;
const previousTimeRange = usePrevious(timeRange);

const query = getQueryWithDefaults(props.query);
if (config.featureToggles.lokiPredefinedOperations && !query.expr && predefinedOperations) {
query.expr = `{} ${predefinedOperations}`;
}
const previousQueryExpr = usePrevious(query.expr);
const previousQueryType = usePrevious(query.queryType);

// This should be filled in from the defaults by now.
const editorMode = query.editorMode!;

const onExplainChange = (event: SyntheticEvent<HTMLInputElement>) => {
window.localStorage.setItem(lokiQueryEditorExplainKey, event.currentTarget.checked ? 'true' : 'false');
setExplain(event.currentTarget.checked);
};

const onEditorModeChange = useCallback(
(newEditorMode: QueryEditorMode) => {
reportInteraction('grafana_loki_editor_mode_clicked', {
newEditor: newEditorMode,
previousEditor: query.editorMode ?? '',
newQuery: !query.expr,
app: app ?? '',
});

if (newEditorMode === QueryEditorMode.Builder) {
const result = buildVisualQueryFromString(query.expr || '');
// If there are errors, give user a chance to decide if they want to go to builder as that can lose some data.
if (result.errors.length) {
setParseModalOpen(true);
return;
}
}
changeEditorMode(query, newEditorMode, onChange);
},
[onChange, query, app]
);

useEffect(() => {
setDataIsStale(false);
}, [data]);

const onChangeInternal = (query: LokiQuery) => {
const onChangeInternal = (query: Query) => {
if (!isEqual(query, props.query)) {
setDataIsStale(true);
}
onChange(query);
};

const onClickLabelBrowserButton = () => {
reportInteraction('grafana_loki_label_browser_opened', {
app: app,
});

setLabelBrowserVisible((visible) => !visible);
};

useEffect(() => {
const shouldUpdate = shouldUpdateStats(
query.expr,
previousQueryExpr,
timeRange,
previousTimeRange,
query.queryType,
previousQueryType
);
if (shouldUpdate && timeRange) {
const makeAsyncRequest = async () => {
// overwriting the refId that is later used to cancel inflight queries with the same ID.
const stats = await datasource.getStats({ ...query, refId: `${id}_${query.refId}` }, timeRange);
setQueryStats(stats);
};
makeAsyncRequest();
}
}, [datasource, timeRange, previousTimeRange, query, previousQueryExpr, previousQueryType, setQueryStats, id]);

return (
<>
<ConfirmModal
isOpen={parseModalOpen}
title="Query parsing"
body="There were errors while trying to parse the query. Continuing to visual builder may lose some parts of the query."
confirmText="Continue"
onConfirm={() => {
onChange({ ...query, editorMode: QueryEditorMode.Builder });
setParseModalOpen(false);
}}
onDismiss={() => setParseModalOpen(false)}
/>
<QueryPatternsModal
isOpen={queryPatternsModalOpen}
onClose={() => setQueryPatternsModalOpen(false)}
query={query}
queries={queries}
app={app}
onChange={onChange}
onAddQuery={onAddQuery}
/>
<LabelBrowserModal
isOpen={labelBrowserVisible}
datasource={datasource}
query={query}
app={app}
onClose={() => setLabelBrowserVisible(false)}
onChange={onChangeInternal}
onRunQuery={onRunQuery}
timeRange={timeRange}
/>
<EditorHeader>
<Stack gap={1}>
<Button
data-testid={selectors.components.QueryBuilder.queryPatterns}
variant="secondary"
size="sm"
onClick={() => {
setQueryPatternsModalOpen((prevValue) => !prevValue);

const visualQuery = buildVisualQueryFromString(query.expr || '');
reportInteraction('grafana_loki_query_patterns_opened', {
version: 'v2',
app: app ?? '',
editorMode: query.editorMode,
preSelectedOperationsCount: visualQuery.query.operations.length,
preSelectedLabelsCount: visualQuery.query.labels.length,
});
}}
>
Kick start your query
</Button>
<Button variant="secondary" size="sm" onClick={onClickLabelBrowserButton} data-testid="label-browser-button">
Label browser
</Button>
</Stack>
<QueryHeaderSwitch label="Explain query" value={explain} onChange={onExplainChange} />
<FlexItem grow={1} />
<div>
{app !== CoreApp.Explore && app !== CoreApp.Correlations && (
<Button
variant={dataIsStale ? 'primary' : 'secondary'}
size="sm"
onClick={onRunQuery}
icon={data?.state === LoadingState.Loading ? 'spinner' : undefined}
icon={data?.state === LoadingState.Loading ? 'fa fa-spinner' : undefined}
disabled={data?.state === LoadingState.Loading}
>
{queries && queries.length > 1 ? `Run queries` : `Run query`}
</Button>
)}
<QueryEditorModeToggle mode={editorMode!} onChange={onEditorModeChange} />
</EditorHeader>
<Space v={0.5} />
<EditorRows>
{editorMode === QueryEditorMode.Code && (
<LokiQueryCodeEditor {...props} query={query} onChange={onChangeInternal} showExplain={explain} />
)}
{editorMode === QueryEditorMode.Builder && (
<LokiQueryBuilderContainer
datasource={props.datasource}
query={query}
onChange={onChangeInternal}
onRunQuery={props.onRunQuery}
showExplain={explain}
timeRange={timeRange}
/>
)}
<LokiQueryBuilderOptions
query={query}
onChange={onChange}
onRunQuery={onRunQuery}
app={app}
maxLines={datasource.maxLines}
queryStats={queryStats}
/>
</EditorRows>
</div>
<div>
<QueryCodeEditor {...props} query={query} onChange={onChangeInternal} showExplain={true} />
</div>
</>
);
});

LokiQueryEditor.displayName = 'LokiQueryEditor';
QueryEditor.displayName = 'LokiQueryEditor';
export default QueryEditor
21 changes: 21 additions & 0 deletions src/components/QueryEditor/QueryEditorByApp.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import React, { memo } from 'react';

import { CoreApp } from '@grafana/data';

import { VictoriaLogsQueryEditorProps } from '../../types';

import QueryEditor from './QueryEditor';
import QueryEditorForAlerting from './QueryEditorForAlerting';

const QueryEditorByApp = (props: VictoriaLogsQueryEditorProps) => {
const { app } = props;

switch (app) {
case CoreApp.CloudAlerting:
return <QueryEditorForAlerting {...props} />;
default:
return <QueryEditor {...props} />;
}
}

export default memo(QueryEditorByApp);
21 changes: 4 additions & 17 deletions src/components/QueryEditor/QueryEditorForAlerting.tsx
Original file line number Diff line number Diff line change
@@ -1,25 +1,12 @@
import React from 'react';

import { LokiQueryField } from './LokiQueryField';
import { LokiQueryEditorProps } from './types';
import { VictoriaLogsQueryEditorProps } from "../../types";

export function LokiQueryEditorForAlerting(props: LokiQueryEditorProps) {
const { query, data, datasource, onChange, onRunQuery, history } = props;
const QueryEditorForAlerting = (props: VictoriaLogsQueryEditorProps) => {

return (
<LokiQueryField
datasource={datasource}
query={query}
onChange={onChange}
onRunQuery={onRunQuery}
history={history}
data={data}
placeholder="Enter a Loki query"
data-testid={testIds.editor}
/>
<div>QueryEditorForAlerting</div>
);
}

export const testIds = {
editor: 'loki-editor-cloud-alerting',
};
export default QueryEditorForAlerting
Loading

0 comments on commit 86694fc

Please sign in to comment.