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();
});
});