mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Loki: Redesign and improve query patterns (#55097)
* WIP * WIP * Query patterns: Redesign and improve feature * Remove duplicated pattern * Remove empty line * Refactor * Add tests * Update docs and e2e test * Update public/app/plugins/datasource/loki/querybuilder/LokiQueryModeller.ts Co-authored-by: Matias Chomicki <matyax@gmail.com> * Update public/app/plugins/datasource/loki/querybuilder/LokiQueryModeller.ts Co-authored-by: Matias Chomicki <matyax@gmail.com> * Update public/app/plugins/datasource/loki/querybuilder/LokiQueryModeller.ts Co-authored-by: Matias Chomicki <matyax@gmail.com> * Use capitalize * Refactor to use QueryPatternsCard component * Update public/app/plugins/datasource/loki/querybuilder/LokiQueryModeller.ts Co-authored-by: Matias Chomicki <matyax@gmail.com> * Update feature tracking for v2 * QueryPatternsCard: Remove unnecessary key * Update naming for card * Mock reportInteraction in tests Co-authored-by: Matias Chomicki <matyax@gmail.com>
This commit is contained in:
parent
231de5a32f
commit
c0ecdf6783
@ -107,8 +107,8 @@ With Loki log browser you can easily navigate through your list of labels and va
|
|||||||
In addition to `Run query` button and mode switcher, in builder mode additional elements are available:
|
In addition to `Run query` button and mode switcher, in builder mode additional elements are available:
|
||||||
|
|
||||||
| Name | Description |
|
| Name | Description |
|
||||||
| -------------- | --------------------------------------------------------------------------------------------------------------------------------- |
|
| --------------------- | --------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
| Query patterns | A list of useful operation patterns that can be used to quickly add multiple operations to your query to achieve a specific goal. |
|
| Kick start your query | A list of useful operation patterns that can be used to quickly add multiple operations to your query to achieve a specific goal. |
|
||||||
| Explain | Toggle to show a step by step explanation of all query parts and the operations. |
|
| Explain | Toggle to show a step by step explanation of all query parts and the operations. |
|
||||||
| Raw query | Toggle to show raw query generated by the builder that will be sent to Loki instance. |
|
| Raw query | Toggle to show raw query generated by the builder that will be sent to Loki instance. |
|
||||||
|
|
||||||
@ -293,6 +293,6 @@ datasources:
|
|||||||
type: jaeger
|
type: jaeger
|
||||||
url: http://jaeger-tracing-query:16686/
|
url: http://jaeger-tracing-query:16686/
|
||||||
access: proxy
|
access: proxy
|
||||||
# UID should match the datasourceUid in dervidedFields.
|
# UID should match the datasourceUid in derivedFields.
|
||||||
uid: my_jaeger_uid
|
uid: my_jaeger_uid
|
||||||
```
|
```
|
||||||
|
@ -45,7 +45,9 @@ describe('Loki query builder', () => {
|
|||||||
e2e().contains(dataSourceName).scrollIntoView().should('be.visible').click();
|
e2e().contains(dataSourceName).scrollIntoView().should('be.visible').click();
|
||||||
|
|
||||||
// Start in builder mode, click and choose query pattern
|
// Start in builder mode, click and choose query pattern
|
||||||
e2e.components.QueryBuilder.queryPatterns().click().type('Log query with parsing{enter}');
|
e2e.components.QueryBuilder.queryPatterns().click();
|
||||||
|
e2e().contains('Log query starters').click();
|
||||||
|
e2e().contains('Use this query').click();
|
||||||
e2e().contains('No pipeline errors').should('be.visible');
|
e2e().contains('No pipeline errors').should('be.visible');
|
||||||
e2e().contains('Logfmt').should('be.visible');
|
e2e().contains('Logfmt').should('be.visible');
|
||||||
e2e().contains('{} | logfmt | __error__=``').should('be.visible');
|
e2e().contains('{} | logfmt | __error__=``').should('be.visible');
|
||||||
|
@ -2,7 +2,7 @@ import { LokiAndPromQueryModellerBase } from '../../prometheus/querybuilder/shar
|
|||||||
import { QueryBuilderLabelFilter } from '../../prometheus/querybuilder/shared/types';
|
import { QueryBuilderLabelFilter } from '../../prometheus/querybuilder/shared/types';
|
||||||
|
|
||||||
import { getOperationDefinitions } from './operations';
|
import { getOperationDefinitions } from './operations';
|
||||||
import { LokiOperationId, LokiQueryPattern, LokiVisualQueryOperationCategory } from './types';
|
import { LokiOperationId, LokiQueryPattern, LokiQueryPatternType, LokiVisualQueryOperationCategory } from './types';
|
||||||
|
|
||||||
export class LokiQueryModeller extends LokiAndPromQueryModellerBase {
|
export class LokiQueryModeller extends LokiAndPromQueryModellerBase {
|
||||||
constructor() {
|
constructor() {
|
||||||
@ -29,7 +29,8 @@ export class LokiQueryModeller extends LokiAndPromQueryModellerBase {
|
|||||||
getQueryPatterns(): LokiQueryPattern[] {
|
getQueryPatterns(): LokiQueryPattern[] {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
name: 'Log query with parsing',
|
name: 'Parse log lines with logfmt parser',
|
||||||
|
type: LokiQueryPatternType.Log,
|
||||||
// {} | logfmt | __error__=``
|
// {} | logfmt | __error__=``
|
||||||
operations: [
|
operations: [
|
||||||
{ id: LokiOperationId.Logfmt, params: [] },
|
{ id: LokiOperationId.Logfmt, params: [] },
|
||||||
@ -37,7 +38,17 @@ export class LokiQueryModeller extends LokiAndPromQueryModellerBase {
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Log query with filtering and parsing',
|
name: 'Parse log lines with JSON parser',
|
||||||
|
type: LokiQueryPatternType.Log,
|
||||||
|
// {} | json | __error__=``
|
||||||
|
operations: [
|
||||||
|
{ id: LokiOperationId.Json, params: [] },
|
||||||
|
{ id: LokiOperationId.LabelFilterNoErrors, params: [] },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Filter log line and parse with logfmt parser',
|
||||||
|
type: LokiQueryPatternType.Log,
|
||||||
// {} |= `` | logfmt | __error__=``
|
// {} |= `` | logfmt | __error__=``
|
||||||
operations: [
|
operations: [
|
||||||
{ id: LokiOperationId.LineContains, params: [''] },
|
{ id: LokiOperationId.LineContains, params: [''] },
|
||||||
@ -46,7 +57,18 @@ export class LokiQueryModeller extends LokiAndPromQueryModellerBase {
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Log query with parsing and label filter',
|
name: 'Filter log lines and parse with json parser',
|
||||||
|
type: LokiQueryPatternType.Log,
|
||||||
|
// {} |= `` | json | __error__=``
|
||||||
|
operations: [
|
||||||
|
{ id: LokiOperationId.LineContains, params: [''] },
|
||||||
|
{ id: LokiOperationId.Json, params: [] },
|
||||||
|
{ id: LokiOperationId.LabelFilterNoErrors, params: [] },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Parse log line with logfmt parser and use label filter',
|
||||||
|
type: LokiQueryPatternType.Log,
|
||||||
// {} |= `` | logfmt | __error__=`` | label=`value`
|
// {} |= `` | logfmt | __error__=`` | label=`value`
|
||||||
operations: [
|
operations: [
|
||||||
{ id: LokiOperationId.LineContains, params: [''] },
|
{ id: LokiOperationId.LineContains, params: [''] },
|
||||||
@ -56,7 +78,8 @@ export class LokiQueryModeller extends LokiAndPromQueryModellerBase {
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Log query with parsing of nested json',
|
name: 'Parse log lines with nested json',
|
||||||
|
type: LokiQueryPatternType.Log,
|
||||||
// {} |= `` | json | line_format `{{ .message}}` | json
|
// {} |= `` | json | line_format `{{ .message}}` | json
|
||||||
operations: [
|
operations: [
|
||||||
{ id: LokiOperationId.LineContains, params: [''] },
|
{ id: LokiOperationId.LineContains, params: [''] },
|
||||||
@ -68,7 +91,8 @@ export class LokiQueryModeller extends LokiAndPromQueryModellerBase {
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Log query with reformatted log line',
|
name: 'Reformat log lines',
|
||||||
|
type: LokiQueryPatternType.Log,
|
||||||
// {} |= `` | logfmt | line_format `{{.message}}`
|
// {} |= `` | logfmt | line_format `{{.message}}`
|
||||||
operations: [
|
operations: [
|
||||||
{ id: LokiOperationId.LineContains, params: [''] },
|
{ id: LokiOperationId.LineContains, params: [''] },
|
||||||
@ -78,7 +102,8 @@ export class LokiQueryModeller extends LokiAndPromQueryModellerBase {
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Log query with mapped log level',
|
name: 'Rename lvl label to level',
|
||||||
|
type: LokiQueryPatternType.Log,
|
||||||
// {} |= `` | logfmt | label_format level=lvl
|
// {} |= `` | logfmt | label_format level=lvl
|
||||||
operations: [
|
operations: [
|
||||||
{ id: LokiOperationId.LineContains, params: [''] },
|
{ id: LokiOperationId.LineContains, params: [''] },
|
||||||
@ -88,7 +113,8 @@ export class LokiQueryModeller extends LokiAndPromQueryModellerBase {
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Metrics query on value inside log line',
|
name: 'Query on value inside a log line',
|
||||||
|
type: LokiQueryPatternType.Metric,
|
||||||
// sum(sum_over_time({ | logfmt | __error__=`` | unwrap | __error__=`` [$__interval]))
|
// sum(sum_over_time({ | logfmt | __error__=`` | unwrap | __error__=`` [$__interval]))
|
||||||
operations: [
|
operations: [
|
||||||
{ id: LokiOperationId.LineContains, params: [''] },
|
{ id: LokiOperationId.LineContains, params: [''] },
|
||||||
@ -101,7 +127,8 @@ export class LokiQueryModeller extends LokiAndPromQueryModellerBase {
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Metrics query for total requests per label of streams',
|
name: 'Total requests per label of streams',
|
||||||
|
type: LokiQueryPatternType.Metric,
|
||||||
// sum by() (count_over_time({}[$__interval)
|
// sum by() (count_over_time({}[$__interval)
|
||||||
operations: [
|
operations: [
|
||||||
{ id: LokiOperationId.LineContains, params: [''] },
|
{ id: LokiOperationId.LineContains, params: [''] },
|
||||||
@ -110,7 +137,8 @@ export class LokiQueryModeller extends LokiAndPromQueryModellerBase {
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Metrics query for total requests per parsed label or label of streams',
|
name: 'Total requests per parsed label or label of streams',
|
||||||
|
type: LokiQueryPatternType.Metric,
|
||||||
// sum by() (count_over_time({}| logfmt | __error__=`` [$__interval))
|
// sum by() (count_over_time({}| logfmt | __error__=`` [$__interval))
|
||||||
operations: [
|
operations: [
|
||||||
{ id: LokiOperationId.LineContains, params: [''] },
|
{ id: LokiOperationId.LineContains, params: [''] },
|
||||||
@ -121,7 +149,8 @@ export class LokiQueryModeller extends LokiAndPromQueryModellerBase {
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Metrics query for bytes used by log stream',
|
name: 'Bytes used by a log stream',
|
||||||
|
type: LokiQueryPatternType.Metric,
|
||||||
// bytes_over_time({}[$__interval])
|
// bytes_over_time({}[$__interval])
|
||||||
operations: [
|
operations: [
|
||||||
{ id: LokiOperationId.LineContains, params: [''] },
|
{ id: LokiOperationId.LineContains, params: [''] },
|
||||||
@ -129,7 +158,8 @@ export class LokiQueryModeller extends LokiAndPromQueryModellerBase {
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Metrics query for count of log lines per stream',
|
name: 'Count of log lines per stream',
|
||||||
|
type: LokiQueryPatternType.Metric,
|
||||||
// count_over_time({}[$__interval])
|
// count_over_time({}[$__interval])
|
||||||
operations: [
|
operations: [
|
||||||
{ id: LokiOperationId.LineContains, params: [''] },
|
{ id: LokiOperationId.LineContains, params: [''] },
|
||||||
@ -137,7 +167,8 @@ export class LokiQueryModeller extends LokiAndPromQueryModellerBase {
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Metrics query for top n results by label or parsed label',
|
name: 'Top N results by label or parsed label',
|
||||||
|
type: LokiQueryPatternType.Metric,
|
||||||
// topk(10, sum by () (count_over_time({} | logfmt | __error__=`` [$__interval])))
|
// topk(10, sum by () (count_over_time({} | logfmt | __error__=`` [$__interval])))
|
||||||
operations: [
|
operations: [
|
||||||
{ id: LokiOperationId.Logfmt, params: [] },
|
{ id: LokiOperationId.Logfmt, params: [] },
|
||||||
@ -148,7 +179,8 @@ export class LokiQueryModeller extends LokiAndPromQueryModellerBase {
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Metrics query for extracted quantile',
|
name: 'Extracted quantile',
|
||||||
|
type: LokiQueryPatternType.Metric,
|
||||||
// quantile_over_time(0.5,{} | logfmt | unwrap latency[$__interval]) by ()
|
// quantile_over_time(0.5,{} | logfmt | unwrap latency[$__interval]) by ()
|
||||||
operations: [
|
operations: [
|
||||||
{ id: LokiOperationId.Logfmt, params: [] },
|
{ id: LokiOperationId.Logfmt, params: [] },
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
import React, { SyntheticEvent, useCallback, useEffect, useState } from 'react';
|
import React, { SyntheticEvent, useCallback, useEffect, useState } from 'react';
|
||||||
|
|
||||||
import { CoreApp, LoadingState, SelectableValue } from '@grafana/data';
|
import { CoreApp, LoadingState } from '@grafana/data';
|
||||||
import { selectors } from '@grafana/e2e-selectors';
|
import { selectors } from '@grafana/e2e-selectors';
|
||||||
import { reportInteraction } from '@grafana/runtime';
|
import { reportInteraction } from '@grafana/runtime';
|
||||||
import { Button, ConfirmModal, EditorHeader, EditorRows, FlexItem, InlineSelect, Space } from '@grafana/ui';
|
import { Button, ConfirmModal, EditorHeader, EditorRows, FlexItem, Space } from '@grafana/ui';
|
||||||
import { QueryEditorModeToggle } from 'app/plugins/datasource/prometheus/querybuilder/shared/QueryEditorModeToggle';
|
import { QueryEditorModeToggle } from 'app/plugins/datasource/prometheus/querybuilder/shared/QueryEditorModeToggle';
|
||||||
import { QueryHeaderSwitch } from 'app/plugins/datasource/prometheus/querybuilder/shared/QueryHeaderSwitch';
|
import { QueryHeaderSwitch } from 'app/plugins/datasource/prometheus/querybuilder/shared/QueryHeaderSwitch';
|
||||||
import { QueryEditorMode } from 'app/plugins/datasource/prometheus/querybuilder/shared/types';
|
import { QueryEditorMode } from 'app/plugins/datasource/prometheus/querybuilder/shared/types';
|
||||||
@ -15,18 +15,18 @@ import {
|
|||||||
} from '../../../prometheus/querybuilder/shared/hooks/useFlag';
|
} from '../../../prometheus/querybuilder/shared/hooks/useFlag';
|
||||||
import { LokiQueryEditorProps } from '../../components/types';
|
import { LokiQueryEditorProps } from '../../components/types';
|
||||||
import { LokiQuery } from '../../types';
|
import { LokiQuery } from '../../types';
|
||||||
import { lokiQueryModeller } from '../LokiQueryModeller';
|
|
||||||
import { buildVisualQueryFromString } from '../parsing';
|
import { buildVisualQueryFromString } from '../parsing';
|
||||||
import { changeEditorMode, getQueryWithDefaults } from '../state';
|
import { changeEditorMode, getQueryWithDefaults } from '../state';
|
||||||
import { LokiQueryPattern } from '../types';
|
|
||||||
|
|
||||||
import { LokiQueryBuilderContainer } from './LokiQueryBuilderContainer';
|
import { LokiQueryBuilderContainer } from './LokiQueryBuilderContainer';
|
||||||
import { LokiQueryBuilderOptions } from './LokiQueryBuilderOptions';
|
import { LokiQueryBuilderOptions } from './LokiQueryBuilderOptions';
|
||||||
import { LokiQueryCodeEditor } from './LokiQueryCodeEditor';
|
import { LokiQueryCodeEditor } from './LokiQueryCodeEditor';
|
||||||
|
import { QueryPatternsModal } from './QueryPatternsModal';
|
||||||
|
|
||||||
export const LokiQueryEditorSelector = React.memo<LokiQueryEditorProps>((props) => {
|
export const LokiQueryEditorSelector = React.memo<LokiQueryEditorProps>((props) => {
|
||||||
const { onChange, onRunQuery, data, app } = props;
|
const { onChange, onRunQuery, onAddQuery, data, app, queries } = props;
|
||||||
const [parseModalOpen, setParseModalOpen] = useState(false);
|
const [parseModalOpen, setParseModalOpen] = useState(false);
|
||||||
|
const [queryPatternsModalOpen, setQueryPatternsModalOpen] = useState(false);
|
||||||
const [dataIsStale, setDataIsStale] = useState(false);
|
const [dataIsStale, setDataIsStale] = useState(false);
|
||||||
const { flag: explain, setFlag: setExplain } = useFlag(lokiQueryEditorExplainKey);
|
const { flag: explain, setFlag: setExplain } = useFlag(lokiQueryEditorExplainKey);
|
||||||
const { flag: rawQuery, setFlag: setRawQuery } = useFlag(lokiQueryEditorRawQueryKey, true);
|
const { flag: rawQuery, setFlag: setRawQuery } = useFlag(lokiQueryEditorRawQueryKey, true);
|
||||||
@ -88,42 +88,35 @@ export const LokiQueryEditorSelector = React.memo<LokiQueryEditorProps>((props)
|
|||||||
}}
|
}}
|
||||||
onDismiss={() => setParseModalOpen(false)}
|
onDismiss={() => setParseModalOpen(false)}
|
||||||
/>
|
/>
|
||||||
|
<QueryPatternsModal
|
||||||
|
isOpen={queryPatternsModalOpen}
|
||||||
|
onClose={() => setQueryPatternsModalOpen(false)}
|
||||||
|
query={query}
|
||||||
|
queries={queries}
|
||||||
|
app={app}
|
||||||
|
onChange={onChange}
|
||||||
|
onAddQuery={onAddQuery}
|
||||||
|
/>
|
||||||
<EditorHeader>
|
<EditorHeader>
|
||||||
<InlineSelect
|
<Button
|
||||||
value={null}
|
aria-label={selectors.components.QueryBuilder.queryPatterns}
|
||||||
onOpenMenu={() => {
|
variant="secondary"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => {
|
||||||
|
setQueryPatternsModalOpen((prevValue) => !prevValue);
|
||||||
|
|
||||||
const visualQuery = buildVisualQueryFromString(query.expr || '');
|
const visualQuery = buildVisualQueryFromString(query.expr || '');
|
||||||
reportInteraction('grafana_loki_query_patterns_opened', {
|
reportInteraction('grafana_loki_query_patterns_opened', {
|
||||||
version: 'v1',
|
version: 'v2',
|
||||||
app: app ?? '',
|
app: app ?? '',
|
||||||
editorMode: query.editorMode,
|
editorMode: query.editorMode,
|
||||||
preSelectedOperationsCount: visualQuery.query.operations.length,
|
preSelectedOperationsCount: visualQuery.query.operations.length,
|
||||||
preSelectedLabelsCount: visualQuery.query.labels.length,
|
preSelectedLabelsCount: visualQuery.query.labels.length,
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
placeholder="Query patterns"
|
>
|
||||||
aria-label={selectors.components.QueryBuilder.queryPatterns}
|
Kick start your query
|
||||||
allowCustomValue
|
</Button>
|
||||||
onChange={({ value }: SelectableValue<LokiQueryPattern>) => {
|
|
||||||
const visualQuery = buildVisualQueryFromString(query.expr || '');
|
|
||||||
reportInteraction('grafana_loki_query_patterns_selected', {
|
|
||||||
version: 'v1',
|
|
||||||
app: app ?? '',
|
|
||||||
editorMode: query.editorMode,
|
|
||||||
selectedPattern: value?.name,
|
|
||||||
preSelectedOperationsCount: visualQuery.query.operations.length,
|
|
||||||
preSelectedLabelsCount: visualQuery.query.labels.length,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Update operations
|
|
||||||
visualQuery.query.operations = value?.operations!;
|
|
||||||
onChange({
|
|
||||||
...query,
|
|
||||||
expr: lokiQueryModeller.renderQuery(visualQuery.query),
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
options={lokiQueryModeller.getQueryPatterns().map((x) => ({ label: x.name, value: x }))}
|
|
||||||
/>
|
|
||||||
<QueryHeaderSwitch label="Explain" value={explain} onChange={onExplainChange} />
|
<QueryHeaderSwitch label="Explain" value={explain} onChange={onExplainChange} />
|
||||||
{editorMode === QueryEditorMode.Builder && (
|
{editorMode === QueryEditorMode.Builder && (
|
||||||
<>
|
<>
|
||||||
|
@ -0,0 +1,109 @@
|
|||||||
|
import { css } from '@emotion/css';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import { GrafanaTheme2 } from '@grafana/data';
|
||||||
|
import { Button, Card, useStyles2 } from '@grafana/ui';
|
||||||
|
import { RawQuery } from 'app/plugins/datasource/prometheus/querybuilder/shared/RawQuery';
|
||||||
|
|
||||||
|
import logqlGrammar from '../../syntax';
|
||||||
|
import { lokiQueryModeller } from '../LokiQueryModeller';
|
||||||
|
import { LokiQueryPattern } from '../types';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
pattern: LokiQueryPattern;
|
||||||
|
hasNewQueryOption: boolean;
|
||||||
|
hasPreviousQuery: boolean;
|
||||||
|
selectedPatternName: string | null;
|
||||||
|
setSelectedPatternName: (name: string | null) => void;
|
||||||
|
onPatternSelect: (pattern: LokiQueryPattern, selectAsNewQuery?: boolean) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const QueryPattern = (props: Props) => {
|
||||||
|
const { pattern, onPatternSelect, hasNewQueryOption, hasPreviousQuery, selectedPatternName, setSelectedPatternName } =
|
||||||
|
props;
|
||||||
|
|
||||||
|
const styles = useStyles2(getStyles);
|
||||||
|
const lang = { grammar: logqlGrammar, name: 'logql' };
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card className={styles.card}>
|
||||||
|
<Card.Heading>{pattern.name}</Card.Heading>
|
||||||
|
<div className={styles.rawQueryContainer}>
|
||||||
|
<RawQuery
|
||||||
|
query={lokiQueryModeller.renderQuery({ labels: [], operations: pattern.operations })}
|
||||||
|
lang={lang}
|
||||||
|
className={styles.rawQuery}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Card.Actions>
|
||||||
|
{selectedPatternName !== pattern.name ? (
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
onClick={() => {
|
||||||
|
if (hasPreviousQuery) {
|
||||||
|
// If user has previous query, we need to confirm that they want to replace it
|
||||||
|
setSelectedPatternName(pattern.name);
|
||||||
|
} else {
|
||||||
|
onPatternSelect(pattern);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Use this query
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<div className={styles.spacing}>
|
||||||
|
{`If you would like to use this query, ${
|
||||||
|
hasNewQueryOption
|
||||||
|
? 'you can either replace your current query or create a new query'
|
||||||
|
: 'your current query will be replaced'
|
||||||
|
}.`}
|
||||||
|
</div>
|
||||||
|
<Button size="sm" fill="outline" onClick={() => setSelectedPatternName(null)}>
|
||||||
|
Back
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
onClick={() => {
|
||||||
|
onPatternSelect(pattern);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Replace query
|
||||||
|
</Button>
|
||||||
|
{hasNewQueryOption && (
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
onClick={() => {
|
||||||
|
onPatternSelect(pattern, true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Create new query
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Card.Actions>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getStyles = (theme: GrafanaTheme2) => {
|
||||||
|
return {
|
||||||
|
card: css`
|
||||||
|
width: 49.5%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
`,
|
||||||
|
rawQueryContainer: css`
|
||||||
|
flex-grow: 1;
|
||||||
|
`,
|
||||||
|
rawQuery: css`
|
||||||
|
background-color: ${theme.colors.background.primary};
|
||||||
|
padding: ${theme.spacing(1)};
|
||||||
|
margin-top: ${theme.spacing(1)};
|
||||||
|
`,
|
||||||
|
spacing: css`
|
||||||
|
margin-bottom: ${theme.spacing(1)};
|
||||||
|
`,
|
||||||
|
};
|
||||||
|
};
|
@ -0,0 +1,124 @@
|
|||||||
|
import { render, screen, waitFor } from '@testing-library/react';
|
||||||
|
import userEvent from '@testing-library/user-event';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import { lokiQueryModeller } from '../LokiQueryModeller';
|
||||||
|
import { LokiQueryPatternType } from '../types';
|
||||||
|
|
||||||
|
import { QueryPatternsModal } from './QueryPatternsModal';
|
||||||
|
|
||||||
|
// don't care about interaction tracking in our unit tests
|
||||||
|
jest.mock('@grafana/runtime', () => ({
|
||||||
|
...jest.requireActual('@grafana/runtime'),
|
||||||
|
reportInteraction: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const defaultProps = {
|
||||||
|
isOpen: true,
|
||||||
|
onClose: jest.fn(),
|
||||||
|
onChange: jest.fn(),
|
||||||
|
onAddQuery: jest.fn(),
|
||||||
|
query: {
|
||||||
|
refId: 'A',
|
||||||
|
expr: '{label1="foo", label2="bar"} |= "baz" |~ "qux"',
|
||||||
|
},
|
||||||
|
queries: [
|
||||||
|
{
|
||||||
|
refId: 'A',
|
||||||
|
expr: '{label1="foo", label2="bar"}',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const queryPatterns = {
|
||||||
|
logQueryPatterns: lokiQueryModeller.getQueryPatterns().filter((pattern) => pattern.type === LokiQueryPatternType.Log),
|
||||||
|
metricQueryPatterns: lokiQueryModeller
|
||||||
|
.getQueryPatterns()
|
||||||
|
.filter((pattern) => pattern.type === LokiQueryPatternType.Metric),
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('QueryPatternsModal', () => {
|
||||||
|
it('renders the modal', () => {
|
||||||
|
render(<QueryPatternsModal {...defaultProps} />);
|
||||||
|
expect(screen.getByText('Kick start your query')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
it('renders collapsible elements with all query pattern types', () => {
|
||||||
|
render(<QueryPatternsModal {...defaultProps} />);
|
||||||
|
Object.values(LokiQueryPatternType).forEach((pattern) => {
|
||||||
|
expect(screen.getByText(new RegExp(`${pattern} query starters`, 'i'))).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it('can open and close query patterns section', async () => {
|
||||||
|
render(<QueryPatternsModal {...defaultProps} />);
|
||||||
|
await userEvent.click(screen.getByText('Log query starters'));
|
||||||
|
expect(screen.getByText(queryPatterns.logQueryPatterns[0].name)).toBeInTheDocument();
|
||||||
|
|
||||||
|
await userEvent.click(screen.getByText('Log query starters'));
|
||||||
|
expect(screen.queryByText(queryPatterns.logQueryPatterns[0].name)).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('can open and close multiple query patterns section', async () => {
|
||||||
|
render(<QueryPatternsModal {...defaultProps} />);
|
||||||
|
await userEvent.click(screen.getByText('Log query starters'));
|
||||||
|
expect(screen.getByText(queryPatterns.logQueryPatterns[0].name)).toBeInTheDocument();
|
||||||
|
|
||||||
|
await userEvent.click(screen.getByText('Metric query starters'));
|
||||||
|
expect(screen.getByText(queryPatterns.metricQueryPatterns[0].name)).toBeInTheDocument();
|
||||||
|
|
||||||
|
await userEvent.click(screen.getByText('Log query starters'));
|
||||||
|
expect(screen.queryByText(queryPatterns.logQueryPatterns[0].name)).not.toBeInTheDocument();
|
||||||
|
|
||||||
|
// Metric patterns should still be open
|
||||||
|
expect(screen.getByText(queryPatterns.metricQueryPatterns[0].name)).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('uses pattern if there is no existing query', async () => {
|
||||||
|
render(<QueryPatternsModal {...defaultProps} query={{ expr: '{job="grafana"}', refId: 'A' }} />);
|
||||||
|
await userEvent.click(screen.getByText('Log query starters'));
|
||||||
|
expect(screen.getByText(queryPatterns.logQueryPatterns[0].name)).toBeInTheDocument();
|
||||||
|
const firstUseQueryButton = screen.getAllByRole('button', { name: 'Use this query' })[0];
|
||||||
|
await userEvent.click(firstUseQueryButton);
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(defaultProps.onChange).toHaveBeenCalledWith({
|
||||||
|
expr: '{job="grafana"} | logfmt | __error__=``',
|
||||||
|
refId: 'A',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('gives warning when selecting pattern if there is already existing query', async () => {
|
||||||
|
render(<QueryPatternsModal {...defaultProps} />);
|
||||||
|
await userEvent.click(screen.getByText('Log query starters'));
|
||||||
|
expect(screen.getByText(queryPatterns.logQueryPatterns[0].name)).toBeInTheDocument();
|
||||||
|
const firstUseQueryButton = screen.getAllByRole('button', { name: 'Use this query' })[0];
|
||||||
|
await userEvent.click(firstUseQueryButton);
|
||||||
|
expect(screen.getByText(/replace your current query or create a new query/)).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('can use create new query when selecting pattern if there is already existing query', async () => {
|
||||||
|
render(<QueryPatternsModal {...defaultProps} />);
|
||||||
|
await userEvent.click(screen.getByText('Log query starters'));
|
||||||
|
expect(screen.getByText(queryPatterns.logQueryPatterns[0].name)).toBeInTheDocument();
|
||||||
|
const firstUseQueryButton = screen.getAllByRole('button', { name: 'Use this query' })[0];
|
||||||
|
await userEvent.click(firstUseQueryButton);
|
||||||
|
const createNewQueryButton = screen.getByRole('button', { name: 'Create new query' });
|
||||||
|
expect(createNewQueryButton).toBeInTheDocument();
|
||||||
|
await userEvent.click(createNewQueryButton);
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(defaultProps.onAddQuery).toHaveBeenCalledWith({
|
||||||
|
expr: '{} | logfmt | __error__=``',
|
||||||
|
refId: 'B',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not show create new query option if onAddQuery function is not provided ', async () => {
|
||||||
|
render(<QueryPatternsModal {...defaultProps} onAddQuery={undefined} />);
|
||||||
|
await userEvent.click(screen.getByText('Log query starters'));
|
||||||
|
expect(screen.getByText(queryPatterns.logQueryPatterns[0].name)).toBeInTheDocument();
|
||||||
|
const useQueryButton = screen.getAllByRole('button', { name: 'Use this query' })[0];
|
||||||
|
await userEvent.click(useQueryButton);
|
||||||
|
expect(screen.queryByRole('button', { name: 'Create new query' })).not.toBeInTheDocument();
|
||||||
|
expect(screen.getByText(/your current query will be replaced/)).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,125 @@
|
|||||||
|
import { css } from '@emotion/css';
|
||||||
|
import { capitalize } from 'lodash';
|
||||||
|
import React, { useMemo, useState } from 'react';
|
||||||
|
|
||||||
|
import { CoreApp, DataQuery, GrafanaTheme2 } from '@grafana/data';
|
||||||
|
import { reportInteraction } from '@grafana/runtime';
|
||||||
|
import { Button, Collapse, Modal, useStyles2 } from '@grafana/ui';
|
||||||
|
import { getNextRefIdChar } from 'app/core/utils/query';
|
||||||
|
|
||||||
|
import { LokiQuery } from '../../types';
|
||||||
|
import { lokiQueryModeller } from '../LokiQueryModeller';
|
||||||
|
import { buildVisualQueryFromString } from '../parsing';
|
||||||
|
import { LokiQueryPattern, LokiQueryPatternType } from '../types';
|
||||||
|
|
||||||
|
import { QueryPattern } from './QueryPattern';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
isOpen: boolean;
|
||||||
|
query: LokiQuery;
|
||||||
|
queries: DataQuery[] | undefined;
|
||||||
|
app?: CoreApp;
|
||||||
|
onClose: () => void;
|
||||||
|
onChange: (query: LokiQuery) => void;
|
||||||
|
onAddQuery?: (query: LokiQuery) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const QueryPatternsModal = (props: Props) => {
|
||||||
|
const { isOpen, onClose, onChange, onAddQuery, query, queries, app } = props;
|
||||||
|
const [openTabs, setOpenTabs] = useState<string[]>([]);
|
||||||
|
const [selectedPatternName, setSelectedPatternName] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const styles = useStyles2(getStyles);
|
||||||
|
const hasNewQueryOption = !!onAddQuery;
|
||||||
|
const hasPreviousQuery = useMemo(
|
||||||
|
() => buildVisualQueryFromString(query.expr).query.operations.length > 0,
|
||||||
|
[query.expr]
|
||||||
|
);
|
||||||
|
|
||||||
|
const onPatternSelect = (pattern: LokiQueryPattern, selectAsNewQuery = false) => {
|
||||||
|
const visualQuery = buildVisualQueryFromString(selectAsNewQuery ? '' : query.expr);
|
||||||
|
reportInteraction('grafana_loki_query_patterns_selected', {
|
||||||
|
version: 'v2',
|
||||||
|
app: app ?? '',
|
||||||
|
editorMode: query.editorMode,
|
||||||
|
selectedPattern: pattern.name,
|
||||||
|
preSelectedOperationsCount: visualQuery.query.operations.length,
|
||||||
|
preSelectedLabelsCount: visualQuery.query.labels.length,
|
||||||
|
createNewQuery: hasNewQueryOption && selectAsNewQuery,
|
||||||
|
});
|
||||||
|
|
||||||
|
visualQuery.query.operations = pattern.operations;
|
||||||
|
if (hasNewQueryOption && selectAsNewQuery) {
|
||||||
|
onAddQuery({
|
||||||
|
...query,
|
||||||
|
refId: getNextRefIdChar(queries ?? [query]),
|
||||||
|
expr: lokiQueryModeller.renderQuery(visualQuery.query),
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
onChange({
|
||||||
|
...query,
|
||||||
|
expr: lokiQueryModeller.renderQuery(visualQuery.query),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
setSelectedPatternName(null);
|
||||||
|
onClose();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal isOpen={isOpen} title="Kick start your query" onDismiss={onClose}>
|
||||||
|
<div className={styles.spacing}>
|
||||||
|
Kick start your query by selecting one of these queries. You can then continue to complete your query.
|
||||||
|
</div>
|
||||||
|
{Object.values(LokiQueryPatternType).map((patternType) => {
|
||||||
|
return (
|
||||||
|
<Collapse
|
||||||
|
key={patternType}
|
||||||
|
label={`${capitalize(patternType)} query starters`}
|
||||||
|
isOpen={openTabs.includes(patternType)}
|
||||||
|
collapsible={true}
|
||||||
|
onToggle={() =>
|
||||||
|
setOpenTabs((tabs) =>
|
||||||
|
// close tab if it's already open, otherwise open it
|
||||||
|
tabs.includes(patternType) ? tabs.filter((t) => t !== patternType) : [...tabs, patternType]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div className={styles.cardsContainer}>
|
||||||
|
{lokiQueryModeller
|
||||||
|
.getQueryPatterns()
|
||||||
|
.filter((pattern) => pattern.type === patternType)
|
||||||
|
.map((pattern) => (
|
||||||
|
<QueryPattern
|
||||||
|
key={pattern.name}
|
||||||
|
pattern={pattern}
|
||||||
|
hasNewQueryOption={hasNewQueryOption}
|
||||||
|
hasPreviousQuery={hasPreviousQuery}
|
||||||
|
onPatternSelect={onPatternSelect}
|
||||||
|
selectedPatternName={selectedPatternName}
|
||||||
|
setSelectedPatternName={setSelectedPatternName}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</Collapse>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
<Button variant="secondary" onClick={onClose}>
|
||||||
|
Close
|
||||||
|
</Button>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getStyles = (theme: GrafanaTheme2) => {
|
||||||
|
return {
|
||||||
|
cardsContainer: css`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: space-between;
|
||||||
|
`,
|
||||||
|
spacing: css`
|
||||||
|
margin-bottom: ${theme.spacing(1)};
|
||||||
|
`,
|
||||||
|
};
|
||||||
|
};
|
@ -11,10 +11,15 @@ export interface LokiVisualQuery {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type LokiVisualQueryBinary = VisualQueryBinary<LokiVisualQuery>;
|
export type LokiVisualQueryBinary = VisualQueryBinary<LokiVisualQuery>;
|
||||||
|
export enum LokiQueryPatternType {
|
||||||
|
Log = 'log',
|
||||||
|
Metric = 'metric',
|
||||||
|
}
|
||||||
|
|
||||||
export interface LokiQueryPattern {
|
export interface LokiQueryPattern {
|
||||||
name: string;
|
name: string;
|
||||||
operations: QueryBuilderOperation[];
|
operations: QueryBuilderOperation[];
|
||||||
|
type: LokiQueryPatternType;
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum LokiVisualQueryOperationCategory {
|
export enum LokiVisualQueryOperationCategory {
|
||||||
|
@ -11,15 +11,16 @@ export interface Props {
|
|||||||
grammar: Grammar;
|
grammar: Grammar;
|
||||||
name: string;
|
name: string;
|
||||||
};
|
};
|
||||||
|
className?: string;
|
||||||
}
|
}
|
||||||
export function RawQuery({ query, lang }: Props) {
|
export function RawQuery({ query, lang, className }: Props) {
|
||||||
const theme = useTheme2();
|
const theme = useTheme2();
|
||||||
const styles = getStyles(theme);
|
const styles = getStyles(theme);
|
||||||
const highlighted = Prism.highlight(query, lang.grammar, lang.name);
|
const highlighted = Prism.highlight(query, lang.grammar, lang.name);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cx(styles.editorField, 'prism-syntax-highlight')}
|
className={cx(styles.editorField, 'prism-syntax-highlight', className)}
|
||||||
aria-label="selector"
|
aria-label="selector"
|
||||||
dangerouslySetInnerHTML={{ __html: highlighted }}
|
dangerouslySetInnerHTML={{ __html: highlighted }}
|
||||||
/>
|
/>
|
||||||
|
Loading…
Reference in New Issue
Block a user