From a098bdef589547c1ffcea537df82446ed16fb543 Mon Sep 17 00:00:00 2001 From: Gareth Dawson Date: Wed, 23 Nov 2022 16:48:41 +0000 Subject: [PATCH] Loki: Make label browser accessible in query builder (#58525) * add label browser button to query editor header * add dynamic button label text * add LabelBrowserModal.tsx * toggle label browser modal on click * pass required props to LabelBrowserModal * add placeholder to text input * render label browser inside of the modal * change button based on label status * remove label browser button from code mode * fix element overlap in label browser * fix undefined app in feature tracking * remove all any types * add tests for label browser button * update modal component width * update label loading function * add tests to LabelBrowserModal * fix broken mock datasource * update test names * use stack component for button spacing * revert modal width * update label search placeholder * remove unused import * add test assertion for closed modal * remove redundant if statement * remove unnecessary code * update error message and fix position * fix input placeholder text --- .../loki/components/LokiLabelBrowser.tsx | 22 ++--- .../loki/components/LokiQueryEditor.test.tsx | 5 ++ .../loki/components/LokiQueryEditor.tsx | 87 ++++++++++++++----- .../loki/components/LokiQueryField.tsx | 38 -------- .../components/LabelBrowserModal.test.tsx | 42 +++++++++ .../components/LabelBrowserModal.tsx | 57 ++++++++++++ 6 files changed, 182 insertions(+), 69 deletions(-) create mode 100644 public/app/plugins/datasource/loki/querybuilder/components/LabelBrowserModal.test.tsx create mode 100644 public/app/plugins/datasource/loki/querybuilder/components/LabelBrowserModal.tsx diff --git a/public/app/plugins/datasource/loki/components/LokiLabelBrowser.tsx b/public/app/plugins/datasource/loki/components/LokiLabelBrowser.tsx index a9903e08db1..2fd67e7169a 100644 --- a/public/app/plugins/datasource/loki/components/LokiLabelBrowser.tsx +++ b/public/app/plugins/datasource/loki/components/LokiLabelBrowser.tsx @@ -131,16 +131,11 @@ const getStyles = (theme: GrafanaTheme2) => ({ margin-bottom: ${theme.spacing(1)}; `, status: css` - padding: ${theme.spacing(0.5)}; + margin-bottom: ${theme.spacing(1)}; color: ${theme.colors.text.secondary}; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; - /* using absolute positioning because flex interferes with ellipsis */ - position: absolute; - width: 50%; - right: 0; - text-align: right; transition: opacity 100ms linear; opacity: 0; `, @@ -367,7 +362,7 @@ export class UnthemedLokiLabelBrowser extends React.Component
- +
{selectedLabels.map((label) => ( @@ -520,6 +520,9 @@ export class UnthemedLokiLabelBrowser extends React.Component {validationStatus &&
{validationStatus}
} +
+ {error || status} +
-
- {error || status} -
diff --git a/public/app/plugins/datasource/loki/components/LokiQueryEditor.test.tsx b/public/app/plugins/datasource/loki/components/LokiQueryEditor.test.tsx index 3e943ee6b88..69df9eae73a 100644 --- a/public/app/plugins/datasource/loki/components/LokiQueryEditor.test.tsx +++ b/public/app/plugins/datasource/loki/components/LokiQueryEditor.test.tsx @@ -149,6 +149,11 @@ describe('LokiQueryEditorSelector', () => { expect(screen.getByText('Rate')).toBeInTheDocument(); expect(screen.getByText('$__interval')).toBeInTheDocument(); }); + + it('renders the label browser button', async () => { + renderWithMode(QueryEditorMode.Code); + expect(await screen.findByTestId('label-browser-button')).toBeInTheDocument(); + }); }); function renderWithMode(mode: QueryEditorMode) { diff --git a/public/app/plugins/datasource/loki/components/LokiQueryEditor.tsx b/public/app/plugins/datasource/loki/components/LokiQueryEditor.tsx index 5e65d770ebe..e33a4c27359 100644 --- a/public/app/plugins/datasource/loki/components/LokiQueryEditor.tsx +++ b/public/app/plugins/datasource/loki/components/LokiQueryEditor.tsx @@ -2,7 +2,7 @@ import React, { SyntheticEvent, useCallback, useEffect, useState } from 'react'; import { CoreApp, LoadingState } from '@grafana/data'; import { selectors } from '@grafana/e2e-selectors'; -import { EditorHeader, EditorRows, FlexItem, Space } from '@grafana/experimental'; +import { EditorHeader, EditorRows, FlexItem, Space, Stack } from '@grafana/experimental'; import { reportInteraction } from '@grafana/runtime'; import { Button, ConfirmModal } from '@grafana/ui'; import { QueryEditorModeToggle } from 'app/plugins/datasource/prometheus/querybuilder/shared/QueryEditorModeToggle'; @@ -10,6 +10,7 @@ import { QueryHeaderSwitch } from 'app/plugins/datasource/prometheus/querybuilde import { QueryEditorMode } from 'app/plugins/datasource/prometheus/querybuilder/shared/types'; import { lokiQueryEditorExplainKey, useFlag } from '../../prometheus/querybuilder/shared/hooks/useFlag'; +import { LabelBrowserModal } from '../querybuilder/components/LabelBrowserModal'; import { LokiQueryBuilderContainer } from '../querybuilder/components/LokiQueryBuilderContainer'; import { LokiQueryBuilderOptions } from '../querybuilder/components/LokiQueryBuilderOptions'; import { LokiQueryCodeEditor } from '../querybuilder/components/LokiQueryCodeEditor'; @@ -25,10 +26,12 @@ export const testIds = { }; export const LokiQueryEditor = React.memo((props) => { - const { onChange, onRunQuery, onAddQuery, data, app, queries } = props; + const { onChange, onRunQuery, onAddQuery, data, app, queries, datasource } = props; const [parseModalOpen, setParseModalOpen] = useState(false); const [queryPatternsModalOpen, setQueryPatternsModalOpen] = useState(false); const [dataIsStale, setDataIsStale] = useState(false); + const [labelBrowserVisible, setLabelBrowserVisible] = useState(false); + const [labelsLoaded, setLabelsLoaded] = useState(false); const { flag: explain, setFlag: setExplain } = useFlag(lokiQueryEditorExplainKey); const query = getQueryWithDefaults(props.query); @@ -70,6 +73,30 @@ export const LokiQueryEditor = React.memo((props) => { onChange(query); }; + const onClickChooserButton = () => { + setLabelBrowserVisible((visible) => !visible); + }; + + const getChooserText = (logLabelsLoaded: boolean, hasLogLabels: boolean) => { + if (!logLabelsLoaded) { + return 'Loading labels...'; + } + if (!hasLogLabels) { + return '(No labels found)'; + } + return 'Label browser'; + }; + + useEffect(() => { + datasource.languageProvider.start().then(() => { + setLabelsLoaded(true); + }); + }, [datasource]); + + const hasLogLabels = datasource.languageProvider.getLabelKeys().length > 0; + const labelBrowserText = getChooserText(labelsLoaded, hasLogLabels); + const buttonDisabled = !(labelsLoaded && hasLogLabels); + return ( <> ((props) => { onAddQuery={onAddQuery} /> - + 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 + + + {app !== CoreApp.Explore && ( diff --git a/public/app/plugins/datasource/loki/components/LokiQueryField.tsx b/public/app/plugins/datasource/loki/components/LokiQueryField.tsx index b695b2d4c08..9ec7354f88d 100644 --- a/public/app/plugins/datasource/loki/components/LokiQueryField.tsx +++ b/public/app/plugins/datasource/loki/components/LokiQueryField.tsx @@ -13,7 +13,6 @@ import { TypeaheadInput, BracesPlugin, DOMUtil, - Icon, } from '@grafana/ui'; import { LocalStorageValueProvider } from 'app/core/components/LocalStorageValueProvider'; @@ -22,21 +21,10 @@ import { LokiDatasource } from '../datasource'; import { escapeLabelValueInSelector, shouldRefreshLabels } from '../languageUtils'; import { LokiQuery, LokiOptions } from '../types'; -import { LokiLabelBrowser } from './LokiLabelBrowser'; import { MonacoQueryFieldWrapper } from './monaco-query-field/MonacoQueryFieldWrapper'; const LAST_USED_LABELS_KEY = 'grafana.datasources.loki.browser.labels'; -function getChooserText(hasSyntax: boolean, hasLogLabels: boolean) { - if (!hasSyntax) { - return 'Loading labels...'; - } - if (!hasLogLabels) { - return '(No labels found)'; - } - return 'Label browser'; -} - function willApplySuggestion(suggestion: string, { typeaheadContext, typeaheadText }: SuggestionsState): string { // Modify suggestion based on context switch (typeaheadContext) { @@ -191,11 +179,6 @@ export class LokiQueryField extends React.PureComponent 0; - const chooserText = getChooserText(labelsLoaded, hasLogLabels); - const buttonDisabled = !(labelsLoaded && hasLogLabels); - return ( storageKey={LAST_USED_LABELS_KEY} defaultValue={[]}> {(lastUsedLabels, onLastUsedLabelsSave, onLastUsedLabelsDelete) => { @@ -205,14 +188,6 @@ export class LokiQueryField extends React.PureComponent -
{config.featureToggles.lokiMonacoEditor ? (
- {labelBrowserVisible && ( -
- -
- )} - {ExtraFieldElement} ); diff --git a/public/app/plugins/datasource/loki/querybuilder/components/LabelBrowserModal.test.tsx b/public/app/plugins/datasource/loki/querybuilder/components/LabelBrowserModal.test.tsx new file mode 100644 index 00000000000..3f95c0b1bc6 --- /dev/null +++ b/public/app/plugins/datasource/loki/querybuilder/components/LabelBrowserModal.test.tsx @@ -0,0 +1,42 @@ +import { render, screen } from '@testing-library/react'; +import React from 'react'; + +import { LokiDatasource } from '../../datasource'; +import { createLokiDatasource } from '../../mocks'; +import { LokiQuery } from '../../types'; + +import { LabelBrowserModal, Props } from './LabelBrowserModal'; + +jest.mock('@grafana/runtime', () => ({ + ...jest.requireActual('@grafana/runtime'), + reportInteraction: jest.fn(), +})); + +describe('LabelBrowserModal', () => { + let datasource: LokiDatasource, props: Props; + + beforeEach(() => { + datasource = createLokiDatasource(); + + props = { + isOpen: true, + languageProvider: datasource.languageProvider, + query: {} as LokiQuery, + onClose: jest.fn(), + onChange: jest.fn(), + onRunQuery: jest.fn(), + }; + + jest.spyOn(datasource, 'metadataRequest').mockResolvedValue({}); + }); + + it('renders the label browser modal when open', () => { + render(); + expect(screen.getByRole('heading', { name: /label browser/i })).toBeInTheDocument(); + }); + + it("doesn't render the label browser modal when closed", () => { + render(); + expect(screen.queryByRole('heading', { name: /label browser/i })).toBeNull(); + }); +}); diff --git a/public/app/plugins/datasource/loki/querybuilder/components/LabelBrowserModal.tsx b/public/app/plugins/datasource/loki/querybuilder/components/LabelBrowserModal.tsx new file mode 100644 index 00000000000..4a0b0ba6bdd --- /dev/null +++ b/public/app/plugins/datasource/loki/querybuilder/components/LabelBrowserModal.tsx @@ -0,0 +1,57 @@ +import React from 'react'; + +import { CoreApp } from '@grafana/data'; +import { Modal } from '@grafana/ui'; +import { LocalStorageValueProvider } from 'app/core/components/LocalStorageValueProvider'; + +import LanguageProvider from '../../LanguageProvider'; +import { LokiLabelBrowser } from '../../components/LokiLabelBrowser'; +import { LokiQuery } from '../../types'; + +export interface Props { + isOpen: boolean; + languageProvider: LanguageProvider; + query: LokiQuery; + app?: CoreApp; + onClose: () => void; + onChange: (query: LokiQuery) => void; + onRunQuery: () => void; +} + +export const LabelBrowserModal = (props: Props) => { + const { isOpen, onClose, languageProvider, app } = props; + + const LAST_USED_LABELS_KEY = 'grafana.datasources.loki.browser.labels'; + + const changeQuery = (value: string) => { + const { query, onChange, onRunQuery } = props; + + const nextQuery = { ...query, expr: value }; + onChange(nextQuery); + onRunQuery(); + }; + + const onChange = (selector: string) => { + changeQuery(selector); + onClose(); + }; + + return ( + + storageKey={LAST_USED_LABELS_KEY} defaultValue={[]}> + {(lastUsedLabels, onLastUsedLabelsSave, onLastUsedLabelsDelete) => { + return ( + + ); + }} + + + ); +};