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:
|
||||
|
||||
| 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. |
|
||||
| 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
|
||||
url: http://jaeger-tracing-query:16686/
|
||||
access: proxy
|
||||
# UID should match the datasourceUid in dervidedFields.
|
||||
# UID should match the datasourceUid in derivedFields.
|
||||
uid: my_jaeger_uid
|
||||
```
|
||||
|
@ -45,7 +45,9 @@ describe('Loki query builder', () => {
|
||||
e2e().contains(dataSourceName).scrollIntoView().should('be.visible').click();
|
||||
|
||||
// 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('Logfmt').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 { getOperationDefinitions } from './operations';
|
||||
import { LokiOperationId, LokiQueryPattern, LokiVisualQueryOperationCategory } from './types';
|
||||
import { LokiOperationId, LokiQueryPattern, LokiQueryPatternType, LokiVisualQueryOperationCategory } from './types';
|
||||
|
||||
export class LokiQueryModeller extends LokiAndPromQueryModellerBase {
|
||||
constructor() {
|
||||
@ -29,7 +29,8 @@ export class LokiQueryModeller extends LokiAndPromQueryModellerBase {
|
||||
getQueryPatterns(): LokiQueryPattern[] {
|
||||
return [
|
||||
{
|
||||
name: 'Log query with parsing',
|
||||
name: 'Parse log lines with logfmt parser',
|
||||
type: LokiQueryPatternType.Log,
|
||||
// {} | logfmt | __error__=``
|
||||
operations: [
|
||||
{ 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__=``
|
||||
operations: [
|
||||
{ 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`
|
||||
operations: [
|
||||
{ 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
|
||||
operations: [
|
||||
{ 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}}`
|
||||
operations: [
|
||||
{ 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
|
||||
operations: [
|
||||
{ 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]))
|
||||
operations: [
|
||||
{ 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)
|
||||
operations: [
|
||||
{ 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))
|
||||
operations: [
|
||||
{ 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])
|
||||
operations: [
|
||||
{ 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])
|
||||
operations: [
|
||||
{ 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])))
|
||||
operations: [
|
||||
{ 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 ()
|
||||
operations: [
|
||||
{ id: LokiOperationId.Logfmt, params: [] },
|
||||
|
@ -1,9 +1,9 @@
|
||||
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 { 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 { QueryHeaderSwitch } from 'app/plugins/datasource/prometheus/querybuilder/shared/QueryHeaderSwitch';
|
||||
import { QueryEditorMode } from 'app/plugins/datasource/prometheus/querybuilder/shared/types';
|
||||
@ -15,18 +15,18 @@ import {
|
||||
} from '../../../prometheus/querybuilder/shared/hooks/useFlag';
|
||||
import { LokiQueryEditorProps } from '../../components/types';
|
||||
import { LokiQuery } from '../../types';
|
||||
import { lokiQueryModeller } from '../LokiQueryModeller';
|
||||
import { buildVisualQueryFromString } from '../parsing';
|
||||
import { changeEditorMode, getQueryWithDefaults } from '../state';
|
||||
import { LokiQueryPattern } from '../types';
|
||||
|
||||
import { LokiQueryBuilderContainer } from './LokiQueryBuilderContainer';
|
||||
import { LokiQueryBuilderOptions } from './LokiQueryBuilderOptions';
|
||||
import { LokiQueryCodeEditor } from './LokiQueryCodeEditor';
|
||||
import { QueryPatternsModal } from './QueryPatternsModal';
|
||||
|
||||
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 [queryPatternsModalOpen, setQueryPatternsModalOpen] = useState(false);
|
||||
const [dataIsStale, setDataIsStale] = useState(false);
|
||||
const { flag: explain, setFlag: setExplain } = useFlag(lokiQueryEditorExplainKey);
|
||||
const { flag: rawQuery, setFlag: setRawQuery } = useFlag(lokiQueryEditorRawQueryKey, true);
|
||||
@ -88,42 +88,35 @@ export const LokiQueryEditorSelector = React.memo<LokiQueryEditorProps>((props)
|
||||
}}
|
||||
onDismiss={() => setParseModalOpen(false)}
|
||||
/>
|
||||
<QueryPatternsModal
|
||||
isOpen={queryPatternsModalOpen}
|
||||
onClose={() => setQueryPatternsModalOpen(false)}
|
||||
query={query}
|
||||
queries={queries}
|
||||
app={app}
|
||||
onChange={onChange}
|
||||
onAddQuery={onAddQuery}
|
||||
/>
|
||||
<EditorHeader>
|
||||
<InlineSelect
|
||||
value={null}
|
||||
onOpenMenu={() => {
|
||||
<Button
|
||||
aria-label={selectors.components.QueryBuilder.queryPatterns}
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
setQueryPatternsModalOpen((prevValue) => !prevValue);
|
||||
|
||||
const visualQuery = buildVisualQueryFromString(query.expr || '');
|
||||
reportInteraction('grafana_loki_query_patterns_opened', {
|
||||
version: 'v1',
|
||||
version: 'v2',
|
||||
app: app ?? '',
|
||||
editorMode: query.editorMode,
|
||||
preSelectedOperationsCount: visualQuery.query.operations.length,
|
||||
preSelectedLabelsCount: visualQuery.query.labels.length,
|
||||
});
|
||||
}}
|
||||
placeholder="Query patterns"
|
||||
aria-label={selectors.components.QueryBuilder.queryPatterns}
|
||||
allowCustomValue
|
||||
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 }))}
|
||||
/>
|
||||
>
|
||||
Kick start your query
|
||||
</Button>
|
||||
<QueryHeaderSwitch label="Explain" value={explain} onChange={onExplainChange} />
|
||||
{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 enum LokiQueryPatternType {
|
||||
Log = 'log',
|
||||
Metric = 'metric',
|
||||
}
|
||||
|
||||
export interface LokiQueryPattern {
|
||||
name: string;
|
||||
operations: QueryBuilderOperation[];
|
||||
type: LokiQueryPatternType;
|
||||
}
|
||||
|
||||
export enum LokiVisualQueryOperationCategory {
|
||||
|
@ -11,15 +11,16 @@ export interface Props {
|
||||
grammar: Grammar;
|
||||
name: string;
|
||||
};
|
||||
className?: string;
|
||||
}
|
||||
export function RawQuery({ query, lang }: Props) {
|
||||
export function RawQuery({ query, lang, className }: Props) {
|
||||
const theme = useTheme2();
|
||||
const styles = getStyles(theme);
|
||||
const highlighted = Prism.highlight(query, lang.grammar, lang.name);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cx(styles.editorField, 'prism-syntax-highlight')}
|
||||
className={cx(styles.editorField, 'prism-syntax-highlight', className)}
|
||||
aria-label="selector"
|
||||
dangerouslySetInnerHTML={{ __html: highlighted }}
|
||||
/>
|
||||
|
Loading…
Reference in New Issue
Block a user