diff --git a/e2e/various-suite/loki-editor.spec.ts b/e2e/various-suite/loki-editor.spec.ts new file mode 100644 index 00000000000..ac3fd035b35 --- /dev/null +++ b/e2e/various-suite/loki-editor.spec.ts @@ -0,0 +1,56 @@ +import { e2e } from '@grafana/e2e'; + +const dataSourceName = 'LokiEditor'; +const addDataSource = () => { + e2e.flows.addDataSource({ + type: 'Loki', + expectedAlertMessage: + 'Unable to fetch labels from Loki (Failed to call resource), please check the server logs for more details', + name: dataSourceName, + form: () => { + e2e.components.DataSource.DataSourceHttpSettings.urlInput().type('http://loki-url:3100'); + }, + }); +}; + +e2e.scenario({ + describeName: 'Loki Query Editor', + itName: 'Autocomplete features should work as expected.', + addScenarioDataSource: false, + addScenarioDashBoard: false, + skipScenario: false, + scenario: () => { + addDataSource(); + + e2e().intercept(/labels?/, (req) => { + req.reply({ status: 'success', data: ['instance', 'job', 'source'] }); + }); + + e2e().intercept(/series?/, (req) => { + req.reply({ status: 'success', data: [{ instance: 'instance1' }] }); + }); + + // Go to Explore and choose Loki data source + e2e.pages.Explore.visit(); + e2e.components.DataSourcePicker.container().should('be.visible').click(); + e2e().contains(dataSourceName).scrollIntoView().should('be.visible').click(); + + cy.contains('label', 'Code').click(); + + // we need to wait for the query-field being lazy-loaded, in two steps: + // it is a two-step process: + // 1. first we wait for the text 'Loading...' to appear + // 1. then we wait for the text 'Loading...' to disappear + const monacoLoadingText = 'Loading...'; + const queryText = `rate(http_requests_total{job="grafana"}[5m])`; + e2e.components.QueryField.container().should('be.visible').should('have.text', monacoLoadingText); + e2e.components.QueryField.container().should('be.visible').should('not.have.text', monacoLoadingText); + e2e.components.QueryField.container().type(queryText, { parseSpecialCharSequences: false }).type('{backspace}'); + + cy.contains(queryText.slice(0, -1)).should('be.visible'); + + e2e.components.QueryField.container().type(e2e.typings.undo()); + + cy.contains(queryText).should('be.visible'); + }, +}); diff --git a/e2e/various-suite/slate.spec.ts b/e2e/various-suite/slate.spec.ts deleted file mode 100644 index 26f8087fbfa..00000000000 --- a/e2e/various-suite/slate.spec.ts +++ /dev/null @@ -1,113 +0,0 @@ -import { e2e } from '@grafana/e2e'; - -const dataSourceName = 'LokiSlate'; -const addDataSource = () => { - e2e.flows.addDataSource({ - type: 'Loki', - expectedAlertMessage: - 'Unable to fetch labels from Loki (Failed to call resource), please check the server logs for more details', - name: dataSourceName, - form: () => { - e2e.components.DataSource.DataSourceHttpSettings.urlInput().type('http://loki-url:3100'); - }, - }); -}; - -describe('Loki slate editor', () => { - beforeEach(() => { - e2e.flows.login('admin', 'admin'); - - e2e() - .request({ url: `${e2e.env('BASE_URL')}/api/datasources/name/${dataSourceName}`, failOnStatusCode: false }) - .then((response) => { - if (response.isOkStatusCode) { - return; - } - addDataSource(); - }); - }); - - it('Braces plugin should insert closing brace', () => { - e2e().intercept(/labels?/, (req) => { - req.reply({ status: 'success', data: ['instance', 'job', 'source'] }); - }); - - e2e().intercept(/series?/, (req) => { - req.reply({ status: 'success', data: [{ instance: 'instance1' }] }); - }); - - // Go to Explore and choose Loki data source - e2e.pages.Explore.visit(); - e2e.components.DataSourcePicker.container().should('be.visible').click(); - e2e().contains(dataSourceName).scrollIntoView().should('be.visible').click(); - - // adds closing braces around empty value - e2e().contains('Code').click(); - const queryField = e2e().get('.slate-query-field'); - queryField.type('time('); - queryField.should(($el) => { - expect($el.text().replace(/\uFEFF/g, '')).to.eq('time()'); - }); - - // removes closing brace when opening brace is removed - queryField.type('{backspace}'); - queryField.should(($el) => { - expect($el.text().replace(/\uFEFF/g, '')).to.eq('time'); - }); - - // keeps closing brace when opening brace is removed and inner values exist - queryField.clear(); - queryField.type('time(test{leftArrow}{leftArrow}{leftArrow}{leftArrow}{backspace}'); - queryField.should(($el) => { - expect($el.text().replace(/\uFEFF/g, '')).to.eq('timetest)'); - }); - - // overrides an automatically inserted brace - queryField.clear(); - queryField.type('time()'); - queryField.should(($el) => { - expect($el.text().replace(/\uFEFF/g, '')).to.eq('time()'); - }); - - // does not override manually inserted braces - queryField.clear(); - queryField.type('))'); - queryField.should(($el) => { - expect($el.text().replace(/\uFEFF/g, '')).to.eq('))'); - }); - - /** Clear Plugin */ - - //does not change the empty value - queryField.clear(); - queryField.type('{ctrl+k}'); - queryField.should(($el) => { - expect($el.text().replace(/\uFEFF/g, '')).to.match(/Enter a Loki query/); - }); - - // clears to the end of the line - queryField.clear(); - queryField.type('foo{leftArrow}{leftArrow}{leftArrow}{ctrl+k}'); - queryField.should(($el) => { - expect($el.text().replace(/\uFEFF/g, '')).to.match(/Enter a Loki query/); - }); - - // clears from the middle to the end of the line - queryField.clear(); - queryField.type('foo bar{leftArrow}{leftArrow}{leftArrow}{leftArrow}{ctrl+k}'); - queryField.should(($el) => { - expect($el.text().replace(/\uFEFF/g, '')).to.eq('foo'); - }); - - /** Runner plugin */ - - //should execute query when enter with shift is pressed - queryField.clear(); - queryField.type('{shift+enter}'); - e2e().get('[data-testid="explore-no-data"]').should('be.visible'); - - /** Suggestions plugin */ - e2e().get('.slate-query-field').type(`{selectall}av`); - e2e().get('.slate-typeahead').should('be.visible').contains('avg_over_time'); - }); -}); diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go index 1ba42512802..230fd34f893 100644 --- a/pkg/services/featuremgmt/registry.go +++ b/pkg/services/featuremgmt/registry.go @@ -113,9 +113,11 @@ var ( State: FeatureStateAlpha, }, { - Name: "lokiMonacoEditor", - Description: "Access to Monaco query editor for Loki", - State: FeatureStateAlpha, + Name: "lokiMonacoEditor", + Description: "Access to Monaco query editor for Loki", + State: FeatureStateAlpha, + Expression: "true", + FrontendOnly: true, }, { Name: "swaggerUi", diff --git a/public/app/plugins/datasource/loki/components/LokiQueryEditor.test.tsx b/public/app/plugins/datasource/loki/components/LokiQueryEditor.test.tsx index 6ffe2241290..be95a86110c 100644 --- a/public/app/plugins/datasource/loki/components/LokiQueryEditor.test.tsx +++ b/public/app/plugins/datasource/loki/components/LokiQueryEditor.test.tsx @@ -3,10 +3,10 @@ import userEvent from '@testing-library/user-event'; import { cloneDeep, defaultsDeep } from 'lodash'; import React from 'react'; -import { DataSourcePluginMeta } from '@grafana/data'; +import { config } from '@grafana/runtime'; import { QueryEditorMode } from 'app/plugins/datasource/prometheus/querybuilder/shared/types'; -import { LokiDatasource } from '../datasource'; +import { createLokiDatasource } from '../mocks'; import { EXPLAIN_LABEL_FILTER_CONTENT } from '../querybuilder/components/LokiQueryBuilderExplained'; import { LokiQuery, LokiQueryType } from '../types'; @@ -36,24 +36,10 @@ const defaultQuery = { expr: '{label1="foo", label2="bar"}', }; -const datasource = new LokiDatasource( - { - id: 1, - uid: '', - type: 'loki', - name: 'loki-test', - access: 'proxy', - url: '', - jsonData: {}, - meta: {} as DataSourcePluginMeta, - readOnly: false, - }, - undefined, - undefined -); +const datasource = createLokiDatasource(); -datasource.languageProvider.fetchLabels = jest.fn().mockResolvedValue([]); -datasource.getDataSamples = jest.fn().mockResolvedValue([]); +jest.spyOn(datasource.languageProvider, 'fetchLabels').mockResolvedValue([]); +jest.spyOn(datasource, 'getDataSamples').mockResolvedValue([]); const defaultProps = { datasource, @@ -62,11 +48,15 @@ const defaultProps = { onChange: () => {}, }; +beforeAll(() => { + config.featureToggles.lokiMonacoEditor = true; +}); + describe('LokiQueryEditorSelector', () => { it('shows code editor if expr and nothing else', async () => { // We opt for showing code editor for queries created before this feature was added render(); - expectCodeEditor(); + await expectCodeEditor(); }); it('shows builder if new query', async () => { @@ -84,7 +74,7 @@ describe('LokiQueryEditorSelector', () => { it('shows code editor when code mode is set', async () => { renderWithMode(QueryEditorMode.Code); - expectCodeEditor(); + await expectCodeEditor(); }); it('shows builder when builder mode is set', async () => { @@ -94,6 +84,7 @@ describe('LokiQueryEditorSelector', () => { it('changes to builder mode', async () => { const { onChange } = renderWithMode(QueryEditorMode.Code); + await expectCodeEditor(); await switchToMode(QueryEditorMode.Builder); expect(onChange).toBeCalledWith({ refId: 'A', @@ -129,7 +120,11 @@ describe('LokiQueryEditorSelector', () => { it('changes to code mode', async () => { const { onChange } = renderWithMode(QueryEditorMode.Builder); + + await expectBuilder(); + await switchToMode(QueryEditorMode.Code); + expect(onChange).toBeCalledWith({ refId: 'A', expr: defaultQuery.expr, @@ -144,6 +139,7 @@ describe('LokiQueryEditorSelector', () => { expr: 'rate({instance="host.docker.internal:3000"}[$__interval])', editorMode: QueryEditorMode.Code, }); + await expectCodeEditor(); await switchToMode(QueryEditorMode.Builder); rerender( ) { return { onChange, ...stuff }; } -function expectCodeEditor() { +async function expectCodeEditor() { // Log browser shows this until log labels are loaded. - expect(screen.getByText('Loading labels...')).toBeInTheDocument(); + expect(await screen.findByText('Loading...')).toBeInTheDocument(); } async function expectBuilder() { diff --git a/public/app/plugins/datasource/loki/components/LokiQueryField.test.tsx b/public/app/plugins/datasource/loki/components/LokiQueryField.test.tsx index 32429c7bcbc..4fb1361ef29 100644 --- a/public/app/plugins/datasource/loki/components/LokiQueryField.test.tsx +++ b/public/app/plugins/datasource/loki/components/LokiQueryField.test.tsx @@ -1,49 +1,43 @@ -import { render } from '@testing-library/react'; +import { render, screen } from '@testing-library/react'; import React, { ComponentProps } from 'react'; import { dateTime } from '@grafana/data'; +import { config } from '@grafana/runtime'; -import LokiLanguageProvider from '../LanguageProvider'; -import { LokiDatasource } from '../datasource'; -import syntax from '../syntax'; +import { createLokiDatasource } from '../mocks'; import { LokiQueryField } from './LokiQueryField'; type Props = ComponentProps; - -const defaultProps: Props = { - datasource: { - languageProvider: { - start: () => Promise.resolve(['label1']), - fetchLabels: Promise.resolve(['label1']), - getSyntax: () => syntax, - getLabelKeys: () => ['label1'], - getLabelValues: () => Promise.resolve(['value1']), - } as unknown as LokiLanguageProvider, - } as LokiDatasource, - range: { - from: dateTime([2021, 1, 11, 12, 0, 0]), - to: dateTime([2021, 1, 11, 18, 0, 0]), - raw: { - from: 'now-1h', - to: 'now', - }, - }, - query: { expr: '', refId: '' }, - onRunQuery: () => {}, - onChange: () => {}, - history: [], -}; - describe('LokiQueryField', () => { - it('refreshes metrics when time range changes over 1 minute', async () => { - const fetchLabelsMock = jest.fn(); - const props = defaultProps; - props.datasource.languageProvider.fetchLabels = fetchLabelsMock; + let props: Props; + beforeEach(() => { + props = { + datasource: createLokiDatasource(), + range: { + from: dateTime([2021, 1, 11, 12, 0, 0]), + to: dateTime([2021, 1, 11, 18, 0, 0]), + raw: { + from: 'now-1h', + to: 'now', + }, + }, + query: { expr: '', refId: '' }, + onRunQuery: () => {}, + onChange: () => {}, + history: [], + }; + jest.spyOn(props.datasource.languageProvider, 'start').mockResolvedValue([]); + jest.spyOn(props.datasource.languageProvider, 'fetchLabels').mockResolvedValue(['label1']); + config.featureToggles.lokiMonacoEditor = true; + }); + it('refreshes metrics when time range changes over 1 minute', async () => { const { rerender } = render(); - expect(fetchLabelsMock).not.toHaveBeenCalled(); + expect(await screen.findByText('Loading...')).toBeInTheDocument(); + + expect(props.datasource.languageProvider.fetchLabels).not.toHaveBeenCalled(); // 2 minutes difference over the initial time const newRange = { @@ -56,17 +50,15 @@ describe('LokiQueryField', () => { }; rerender(); - expect(fetchLabelsMock).toHaveBeenCalledTimes(1); + expect(props.datasource.languageProvider.fetchLabels).toHaveBeenCalledTimes(1); }); it('does not refreshes metrics when time range change by less than 1 minute', async () => { - const fetchLabelsMock = jest.fn(); - const props = defaultProps; - props.datasource.languageProvider.fetchLabels = fetchLabelsMock; - const { rerender } = render(); - expect(fetchLabelsMock).not.toHaveBeenCalled(); + expect(await screen.findByText('Loading...')).toBeInTheDocument(); + + expect(props.datasource.languageProvider.fetchLabels).not.toHaveBeenCalled(); // 20 seconds difference over the initial time const newRange = { @@ -79,6 +71,14 @@ describe('LokiQueryField', () => { }; rerender(); - expect(fetchLabelsMock).not.toHaveBeenCalled(); + expect(props.datasource.languageProvider.fetchLabels).not.toHaveBeenCalled(); + }); + + it('can fall back to the legacy editor', async () => { + config.featureToggles.lokiMonacoEditor = false; + render(); + + expect(await screen.findByText('Enter a Loki query (run with Shift+Enter)')).toBeInTheDocument(); + expect(screen.queryByText('Loading...')).not.toBeInTheDocument(); }); }); diff --git a/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryCodeEditor.test.tsx b/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryCodeEditor.test.tsx index e5332aed02d..23b6c2e65c3 100644 --- a/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryCodeEditor.test.tsx +++ b/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryCodeEditor.test.tsx @@ -1,9 +1,9 @@ import { render, screen } from '@testing-library/react'; import React from 'react'; -import { DataSourceInstanceSettings, DataSourcePluginMeta } from '@grafana/data'; +import { config } from '@grafana/runtime'; -import { LokiDatasource } from '../../datasource'; +import { createLokiDatasource } from '../../mocks'; import { LokiQuery } from '../../types'; import { EXPLAIN_LABEL_FILTER_CONTENT } from './LokiQueryBuilderExplained'; @@ -15,15 +15,7 @@ const defaultQuery: LokiQuery = { }; const createDefaultProps = () => { - const datasource = new LokiDatasource( - { - url: '', - jsonData: {}, - meta: {} as DataSourcePluginMeta, - } as DataSourceInstanceSettings, - undefined, - undefined - ); + const datasource = createLokiDatasource(); const props = { datasource, @@ -35,19 +27,25 @@ const createDefaultProps = () => { return props; }; +beforeAll(() => { + config.featureToggles.lokiMonacoEditor = true; +}); + describe('LokiQueryCodeEditor', () => { - it('shows explain section when showExplain is true', () => { + it('shows explain section when showExplain is true', async () => { const props = createDefaultProps(); props.showExplain = true; props.datasource.metadataRequest = jest.fn().mockResolvedValue([]); render(); + expect(await screen.findByText('Loading...')).toBeInTheDocument(); expect(screen.getByText(EXPLAIN_LABEL_FILTER_CONTENT)).toBeInTheDocument(); }); - it('does not show explain section when showExplain is false', () => { + it('does not show explain section when showExplain is false', async () => { const props = createDefaultProps(); props.datasource.metadataRequest = jest.fn().mockResolvedValue([]); render(); + expect(await screen.findByText('Loading...')).toBeInTheDocument(); expect(screen.queryByText(EXPLAIN_LABEL_FILTER_CONTENT)).not.toBeInTheDocument(); }); });