mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Explore: Adds support for new loki 'start' and 'end' params for labels endpoint (#17512)
* Explore: Adds support for new loki 'start' and 'end' params for labels endpoint Also initializes absoluteRange when explore is initialized Closes #16788 * Explore: Dispatches updateTimeRangeAction instead of passing absoluteRange when initializing Also removes dependency on sinon from loki language provider test * Loki: Refactors transformation of absolute time range to URL params into small utility function * Makes use of rangeToParams() util function in loki language provider test Also updates LanguageProvider.request() interface so that url should be type string, and adds optional params argument
This commit is contained in:
parent
8f5df80161
commit
246358344c
@ -13,7 +13,7 @@ import { changeQuery, modifyQueries, runQueries, addQueryRow } from './state/act
|
|||||||
|
|
||||||
// Types
|
// Types
|
||||||
import { StoreState } from 'app/types';
|
import { StoreState } from 'app/types';
|
||||||
import { TimeRange } from '@grafana/data';
|
import { TimeRange, AbsoluteTimeRange } from '@grafana/data';
|
||||||
import { DataQuery, DataSourceApi, QueryFixAction, DataSourceStatus, PanelData, DataQueryError } from '@grafana/ui';
|
import { DataQuery, DataSourceApi, QueryFixAction, DataSourceStatus, PanelData, DataQueryError } from '@grafana/ui';
|
||||||
import { HistoryItem, ExploreItemState, ExploreId, ExploreMode } from 'app/types/explore';
|
import { HistoryItem, ExploreItemState, ExploreId, ExploreMode } from 'app/types/explore';
|
||||||
import { Emitter } from 'app/core/utils/emitter';
|
import { Emitter } from 'app/core/utils/emitter';
|
||||||
@ -38,6 +38,7 @@ interface QueryRowProps extends PropsFromParent {
|
|||||||
query: DataQuery;
|
query: DataQuery;
|
||||||
modifyQueries: typeof modifyQueries;
|
modifyQueries: typeof modifyQueries;
|
||||||
range: TimeRange;
|
range: TimeRange;
|
||||||
|
absoluteRange: AbsoluteTimeRange;
|
||||||
removeQueryRowAction: typeof removeQueryRowAction;
|
removeQueryRowAction: typeof removeQueryRowAction;
|
||||||
runQueries: typeof runQueries;
|
runQueries: typeof runQueries;
|
||||||
queryResponse: PanelData;
|
queryResponse: PanelData;
|
||||||
@ -116,6 +117,7 @@ export class QueryRow extends PureComponent<QueryRowProps, QueryRowState> {
|
|||||||
query,
|
query,
|
||||||
exploreEvents,
|
exploreEvents,
|
||||||
range,
|
range,
|
||||||
|
absoluteRange,
|
||||||
datasourceStatus,
|
datasourceStatus,
|
||||||
queryResponse,
|
queryResponse,
|
||||||
latency,
|
latency,
|
||||||
@ -148,6 +150,7 @@ export class QueryRow extends PureComponent<QueryRowProps, QueryRowState> {
|
|||||||
onChange={this.onChange}
|
onChange={this.onChange}
|
||||||
panelData={null}
|
panelData={null}
|
||||||
queryResponse={queryResponse}
|
queryResponse={queryResponse}
|
||||||
|
absoluteRange={absoluteRange}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<QueryEditor
|
<QueryEditor
|
||||||
@ -202,6 +205,7 @@ function mapStateToProps(state: StoreState, { exploreId, index }: QueryRowProps)
|
|||||||
history,
|
history,
|
||||||
queries,
|
queries,
|
||||||
range,
|
range,
|
||||||
|
absoluteRange,
|
||||||
datasourceError,
|
datasourceError,
|
||||||
graphResult,
|
graphResult,
|
||||||
loadingState,
|
loadingState,
|
||||||
@ -224,6 +228,7 @@ function mapStateToProps(state: StoreState, { exploreId, index }: QueryRowProps)
|
|||||||
history,
|
history,
|
||||||
query,
|
query,
|
||||||
range,
|
range,
|
||||||
|
absoluteRange,
|
||||||
datasourceStatus,
|
datasourceStatus,
|
||||||
queryResponse,
|
queryResponse,
|
||||||
latency,
|
latency,
|
||||||
|
@ -250,6 +250,7 @@ export function initializeExplore(
|
|||||||
ui,
|
ui,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
dispatch(updateTimeRangeAction({ exploreId }));
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -9,7 +9,8 @@ const LokiQueryField: FunctionComponent<LokiQueryFieldFormProps> = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const { isSyntaxReady, setActiveOption, refreshLabels, ...syntaxProps } = useLokiSyntax(
|
const { isSyntaxReady, setActiveOption, refreshLabels, ...syntaxProps } = useLokiSyntax(
|
||||||
datasource.languageProvider,
|
datasource.languageProvider,
|
||||||
datasourceStatus
|
datasourceStatus,
|
||||||
|
otherProps.absoluteRange
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -17,6 +17,7 @@ import BracesPlugin from 'app/features/explore/slate-plugins/braces';
|
|||||||
import { LokiQuery } from '../types';
|
import { LokiQuery } from '../types';
|
||||||
import { TypeaheadOutput, HistoryItem } from 'app/types/explore';
|
import { TypeaheadOutput, HistoryItem } from 'app/types/explore';
|
||||||
import { DataSourceApi, ExploreQueryFieldProps, DataSourceStatus } from '@grafana/ui';
|
import { DataSourceApi, ExploreQueryFieldProps, DataSourceStatus } from '@grafana/ui';
|
||||||
|
import { AbsoluteTimeRange } from '@grafana/data';
|
||||||
|
|
||||||
function getChooserText(hasSyntax: boolean, hasLogLabels: boolean, datasourceStatus: DataSourceStatus) {
|
function getChooserText(hasSyntax: boolean, hasLogLabels: boolean, datasourceStatus: DataSourceStatus) {
|
||||||
if (datasourceStatus === DataSourceStatus.Disconnected) {
|
if (datasourceStatus === DataSourceStatus.Disconnected) {
|
||||||
@ -70,6 +71,7 @@ export interface LokiQueryFieldFormProps extends ExploreQueryFieldProps<DataSour
|
|||||||
syntax: any;
|
syntax: any;
|
||||||
logLabelOptions: any[];
|
logLabelOptions: any[];
|
||||||
syntaxLoaded: any;
|
syntaxLoaded: any;
|
||||||
|
absoluteRange: AbsoluteTimeRange;
|
||||||
onLoadOptions: (selectedOptions: CascaderOption[]) => void;
|
onLoadOptions: (selectedOptions: CascaderOption[]) => void;
|
||||||
onLabelsRefresh?: () => void;
|
onLabelsRefresh?: () => void;
|
||||||
}
|
}
|
||||||
@ -123,7 +125,7 @@ export class LokiQueryFieldForm extends React.PureComponent<LokiQueryFieldFormPr
|
|||||||
return { suggestions: [] };
|
return { suggestions: [] };
|
||||||
}
|
}
|
||||||
|
|
||||||
const { history } = this.props;
|
const { history, absoluteRange } = this.props;
|
||||||
const { prefix, text, value, wrapperNode } = typeahead;
|
const { prefix, text, value, wrapperNode } = typeahead;
|
||||||
|
|
||||||
// Get DOM-dependent context
|
// Get DOM-dependent context
|
||||||
@ -134,7 +136,7 @@ export class LokiQueryFieldForm extends React.PureComponent<LokiQueryFieldFormPr
|
|||||||
|
|
||||||
const result = datasource.languageProvider.provideCompletionItems(
|
const result = datasource.languageProvider.provideCompletionItems(
|
||||||
{ text, value, prefix, wrapperClasses, labelKey },
|
{ text, value, prefix, wrapperClasses, labelKey },
|
||||||
{ history }
|
{ history, absoluteRange }
|
||||||
);
|
);
|
||||||
|
|
||||||
console.log('handleTypeahead', wrapperClasses, text, prefix, nextChar, labelKey, result.context);
|
console.log('handleTypeahead', wrapperClasses, text, prefix, nextChar, labelKey, result.context);
|
||||||
|
@ -2,6 +2,7 @@ import { renderHook, act } from 'react-hooks-testing-library';
|
|||||||
import LanguageProvider from 'app/plugins/datasource/loki/language_provider';
|
import LanguageProvider from 'app/plugins/datasource/loki/language_provider';
|
||||||
import { useLokiLabels } from './useLokiLabels';
|
import { useLokiLabels } from './useLokiLabels';
|
||||||
import { DataSourceStatus } from '@grafana/ui/src/types/datasource';
|
import { DataSourceStatus } from '@grafana/ui/src/types/datasource';
|
||||||
|
import { AbsoluteTimeRange } from '@grafana/data';
|
||||||
|
|
||||||
describe('useLokiLabels hook', () => {
|
describe('useLokiLabels hook', () => {
|
||||||
it('should refresh labels', async () => {
|
it('should refresh labels', async () => {
|
||||||
@ -10,6 +11,10 @@ describe('useLokiLabels hook', () => {
|
|||||||
};
|
};
|
||||||
const languageProvider = new LanguageProvider(datasource);
|
const languageProvider = new LanguageProvider(datasource);
|
||||||
const logLabelOptionsMock = ['Holy mock!'];
|
const logLabelOptionsMock = ['Holy mock!'];
|
||||||
|
const rangeMock: AbsoluteTimeRange = {
|
||||||
|
from: 1560153109000,
|
||||||
|
to: 1560153109000,
|
||||||
|
};
|
||||||
|
|
||||||
languageProvider.refreshLogLabels = () => {
|
languageProvider.refreshLogLabels = () => {
|
||||||
languageProvider.logLabelOptions = logLabelOptionsMock;
|
languageProvider.logLabelOptions = logLabelOptionsMock;
|
||||||
@ -17,7 +22,7 @@ describe('useLokiLabels hook', () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const { result, waitForNextUpdate } = renderHook(() =>
|
const { result, waitForNextUpdate } = renderHook(() =>
|
||||||
useLokiLabels(languageProvider, true, [], DataSourceStatus.Connected, DataSourceStatus.Connected)
|
useLokiLabels(languageProvider, true, [], rangeMock, DataSourceStatus.Connected, DataSourceStatus.Connected)
|
||||||
);
|
);
|
||||||
act(() => result.current.refreshLabels());
|
act(() => result.current.refreshLabels());
|
||||||
expect(result.current.logLabelOptions).toEqual([]);
|
expect(result.current.logLabelOptions).toEqual([]);
|
||||||
@ -29,26 +34,38 @@ describe('useLokiLabels hook', () => {
|
|||||||
const datasource = {
|
const datasource = {
|
||||||
metadataRequest: () => ({ data: { data: [] as any[] } }),
|
metadataRequest: () => ({ data: { data: [] as any[] } }),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const rangeMock: AbsoluteTimeRange = {
|
||||||
|
from: 1560153109000,
|
||||||
|
to: 1560153109000,
|
||||||
|
};
|
||||||
|
|
||||||
const languageProvider = new LanguageProvider(datasource);
|
const languageProvider = new LanguageProvider(datasource);
|
||||||
languageProvider.refreshLogLabels = jest.fn();
|
languageProvider.refreshLogLabels = jest.fn();
|
||||||
|
|
||||||
renderHook(() =>
|
renderHook(() =>
|
||||||
useLokiLabels(languageProvider, true, [], DataSourceStatus.Connected, DataSourceStatus.Disconnected)
|
useLokiLabels(languageProvider, true, [], rangeMock, DataSourceStatus.Connected, DataSourceStatus.Disconnected)
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(languageProvider.refreshLogLabels).toBeCalledTimes(1);
|
expect(languageProvider.refreshLogLabels).toBeCalledTimes(1);
|
||||||
expect(languageProvider.refreshLogLabels).toBeCalledWith(true);
|
expect(languageProvider.refreshLogLabels).toBeCalledWith(rangeMock, true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not force refresh labels after a connect', () => {
|
it('should not force refresh labels after a connect', () => {
|
||||||
const datasource = {
|
const datasource = {
|
||||||
metadataRequest: () => ({ data: { data: [] as any[] } }),
|
metadataRequest: () => ({ data: { data: [] as any[] } }),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const rangeMock: AbsoluteTimeRange = {
|
||||||
|
from: 1560153109000,
|
||||||
|
to: 1560153109000,
|
||||||
|
};
|
||||||
|
|
||||||
const languageProvider = new LanguageProvider(datasource);
|
const languageProvider = new LanguageProvider(datasource);
|
||||||
languageProvider.refreshLogLabels = jest.fn();
|
languageProvider.refreshLogLabels = jest.fn();
|
||||||
|
|
||||||
renderHook(() =>
|
renderHook(() =>
|
||||||
useLokiLabels(languageProvider, true, [], DataSourceStatus.Disconnected, DataSourceStatus.Connected)
|
useLokiLabels(languageProvider, true, [], rangeMock, DataSourceStatus.Disconnected, DataSourceStatus.Connected)
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(languageProvider.refreshLogLabels).not.toBeCalled();
|
expect(languageProvider.refreshLogLabels).not.toBeCalled();
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { DataSourceStatus } from '@grafana/ui/src/types/datasource';
|
import { DataSourceStatus } from '@grafana/ui/src/types/datasource';
|
||||||
|
import { AbsoluteTimeRange } from '@grafana/data';
|
||||||
|
|
||||||
import LokiLanguageProvider from 'app/plugins/datasource/loki/language_provider';
|
import LokiLanguageProvider from 'app/plugins/datasource/loki/language_provider';
|
||||||
import { CascaderOption } from 'app/plugins/datasource/loki/components/LokiQueryFieldForm';
|
import { CascaderOption } from 'app/plugins/datasource/loki/components/LokiQueryFieldForm';
|
||||||
@ -17,6 +18,7 @@ export const useLokiLabels = (
|
|||||||
languageProvider: LokiLanguageProvider,
|
languageProvider: LokiLanguageProvider,
|
||||||
languageProviderInitialised: boolean,
|
languageProviderInitialised: boolean,
|
||||||
activeOption: CascaderOption[],
|
activeOption: CascaderOption[],
|
||||||
|
absoluteRange: AbsoluteTimeRange,
|
||||||
datasourceStatus: DataSourceStatus,
|
datasourceStatus: DataSourceStatus,
|
||||||
initialDatasourceStatus?: DataSourceStatus // used for test purposes
|
initialDatasourceStatus?: DataSourceStatus // used for test purposes
|
||||||
) => {
|
) => {
|
||||||
@ -32,14 +34,14 @@ export const useLokiLabels = (
|
|||||||
|
|
||||||
// Async
|
// Async
|
||||||
const fetchOptionValues = async (option: string) => {
|
const fetchOptionValues = async (option: string) => {
|
||||||
await languageProvider.fetchLabelValues(option);
|
await languageProvider.fetchLabelValues(option, absoluteRange);
|
||||||
if (mounted.current) {
|
if (mounted.current) {
|
||||||
setLogLabelOptions(languageProvider.logLabelOptions);
|
setLogLabelOptions(languageProvider.logLabelOptions);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const tryLabelsRefresh = async () => {
|
const tryLabelsRefresh = async () => {
|
||||||
await languageProvider.refreshLogLabels(shouldForceRefreshLabels);
|
await languageProvider.refreshLogLabels(absoluteRange, shouldForceRefreshLabels);
|
||||||
|
|
||||||
if (mounted.current) {
|
if (mounted.current) {
|
||||||
setRefreshLabels(false);
|
setRefreshLabels(false);
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { renderHook, act } from 'react-hooks-testing-library';
|
import { renderHook, act } from 'react-hooks-testing-library';
|
||||||
import { DataSourceStatus } from '@grafana/ui/src/types/datasource';
|
import { DataSourceStatus } from '@grafana/ui/src/types/datasource';
|
||||||
|
import { AbsoluteTimeRange } from '@grafana/data';
|
||||||
|
|
||||||
import LanguageProvider from 'app/plugins/datasource/loki/language_provider';
|
import LanguageProvider from 'app/plugins/datasource/loki/language_provider';
|
||||||
import { useLokiSyntax } from './useLokiSyntax';
|
import { useLokiSyntax } from './useLokiSyntax';
|
||||||
@ -14,6 +15,11 @@ describe('useLokiSyntax hook', () => {
|
|||||||
const logLabelOptionsMock2 = ['Mock the hell?!'];
|
const logLabelOptionsMock2 = ['Mock the hell?!'];
|
||||||
const logLabelOptionsMock3 = ['Oh my mock!'];
|
const logLabelOptionsMock3 = ['Oh my mock!'];
|
||||||
|
|
||||||
|
const rangeMock: AbsoluteTimeRange = {
|
||||||
|
from: 1560153109000,
|
||||||
|
to: 1560163909000,
|
||||||
|
};
|
||||||
|
|
||||||
languageProvider.refreshLogLabels = () => {
|
languageProvider.refreshLogLabels = () => {
|
||||||
languageProvider.logLabelOptions = logLabelOptionsMock;
|
languageProvider.logLabelOptions = logLabelOptionsMock;
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
@ -30,7 +36,9 @@ describe('useLokiSyntax hook', () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
it('should provide Loki syntax when used', async () => {
|
it('should provide Loki syntax when used', async () => {
|
||||||
const { result, waitForNextUpdate } = renderHook(() => useLokiSyntax(languageProvider, DataSourceStatus.Connected));
|
const { result, waitForNextUpdate } = renderHook(() =>
|
||||||
|
useLokiSyntax(languageProvider, DataSourceStatus.Connected, rangeMock)
|
||||||
|
);
|
||||||
expect(result.current.syntax).toEqual(null);
|
expect(result.current.syntax).toEqual(null);
|
||||||
|
|
||||||
await waitForNextUpdate();
|
await waitForNextUpdate();
|
||||||
@ -39,7 +47,9 @@ describe('useLokiSyntax hook', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should fetch labels on first call', async () => {
|
it('should fetch labels on first call', async () => {
|
||||||
const { result, waitForNextUpdate } = renderHook(() => useLokiSyntax(languageProvider, DataSourceStatus.Connected));
|
const { result, waitForNextUpdate } = renderHook(() =>
|
||||||
|
useLokiSyntax(languageProvider, DataSourceStatus.Connected, rangeMock)
|
||||||
|
);
|
||||||
expect(result.current.isSyntaxReady).toBeFalsy();
|
expect(result.current.isSyntaxReady).toBeFalsy();
|
||||||
expect(result.current.logLabelOptions).toEqual([]);
|
expect(result.current.logLabelOptions).toEqual([]);
|
||||||
|
|
||||||
@ -50,7 +60,9 @@ describe('useLokiSyntax hook', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should try to fetch missing options when active option changes', async () => {
|
it('should try to fetch missing options when active option changes', async () => {
|
||||||
const { result, waitForNextUpdate } = renderHook(() => useLokiSyntax(languageProvider, DataSourceStatus.Connected));
|
const { result, waitForNextUpdate } = renderHook(() =>
|
||||||
|
useLokiSyntax(languageProvider, DataSourceStatus.Connected, rangeMock)
|
||||||
|
);
|
||||||
await waitForNextUpdate();
|
await waitForNextUpdate();
|
||||||
expect(result.current.logLabelOptions).toEqual(logLabelOptionsMock2);
|
expect(result.current.logLabelOptions).toEqual(logLabelOptionsMock2);
|
||||||
|
|
||||||
|
@ -2,7 +2,7 @@ import { useState, useEffect } from 'react';
|
|||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
import Prism from 'prismjs';
|
import Prism from 'prismjs';
|
||||||
import { DataSourceStatus } from '@grafana/ui/src/types/datasource';
|
import { DataSourceStatus } from '@grafana/ui/src/types/datasource';
|
||||||
|
import { AbsoluteTimeRange } from '@grafana/data';
|
||||||
import LokiLanguageProvider from 'app/plugins/datasource/loki/language_provider';
|
import LokiLanguageProvider from 'app/plugins/datasource/loki/language_provider';
|
||||||
import { useLokiLabels } from 'app/plugins/datasource/loki/components/useLokiLabels';
|
import { useLokiLabels } from 'app/plugins/datasource/loki/components/useLokiLabels';
|
||||||
import { CascaderOption } from 'app/plugins/datasource/loki/components/LokiQueryFieldForm';
|
import { CascaderOption } from 'app/plugins/datasource/loki/components/LokiQueryFieldForm';
|
||||||
@ -15,7 +15,11 @@ const PRISM_SYNTAX = 'promql';
|
|||||||
* @param languageProvider
|
* @param languageProvider
|
||||||
* @description Initializes given language provider, exposes Loki syntax and enables loading label option values
|
* @description Initializes given language provider, exposes Loki syntax and enables loading label option values
|
||||||
*/
|
*/
|
||||||
export const useLokiSyntax = (languageProvider: LokiLanguageProvider, datasourceStatus: DataSourceStatus) => {
|
export const useLokiSyntax = (
|
||||||
|
languageProvider: LokiLanguageProvider,
|
||||||
|
datasourceStatus: DataSourceStatus,
|
||||||
|
absoluteRange: AbsoluteTimeRange
|
||||||
|
) => {
|
||||||
const mounted = useRefMounted();
|
const mounted = useRefMounted();
|
||||||
// State
|
// State
|
||||||
const [languageProviderInitialized, setLanguageProviderInitilized] = useState(false);
|
const [languageProviderInitialized, setLanguageProviderInitilized] = useState(false);
|
||||||
@ -32,11 +36,13 @@ export const useLokiSyntax = (languageProvider: LokiLanguageProvider, datasource
|
|||||||
languageProvider,
|
languageProvider,
|
||||||
languageProviderInitialized,
|
languageProviderInitialized,
|
||||||
activeOption,
|
activeOption,
|
||||||
|
absoluteRange,
|
||||||
datasourceStatus
|
datasourceStatus
|
||||||
);
|
);
|
||||||
|
|
||||||
// Async
|
// Async
|
||||||
const initializeLanguageProvider = async () => {
|
const initializeLanguageProvider = async () => {
|
||||||
|
languageProvider.initialRange = absoluteRange;
|
||||||
await languageProvider.start();
|
await languageProvider.start();
|
||||||
Prism.languages[PRISM_SYNTAX] = languageProvider.getSyntax();
|
Prism.languages[PRISM_SYNTAX] = languageProvider.getSyntax();
|
||||||
if (mounted.current) {
|
if (mounted.current) {
|
||||||
|
@ -78,6 +78,7 @@ export class LokiDatasource extends DataSourceApi<LokiQuery, LokiOptions> {
|
|||||||
...options,
|
...options,
|
||||||
url,
|
url,
|
||||||
};
|
};
|
||||||
|
|
||||||
return this.backendSrv.datasourceRequest(req);
|
return this.backendSrv.datasourceRequest(req);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -254,10 +255,10 @@ export class LokiDatasource extends DataSourceApi<LokiQuery, LokiOptions> {
|
|||||||
return this.languageProvider.importQueries(queries, originMeta.id);
|
return this.languageProvider.importQueries(queries, originMeta.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
metadataRequest(url: string) {
|
metadataRequest(url: string, params?: any) {
|
||||||
// HACK to get label values for {job=|}, will be replaced when implementing LokiQueryField
|
// HACK to get label values for {job=|}, will be replaced when implementing LokiQueryField
|
||||||
const apiUrl = url.replace('v1', 'prom');
|
const apiUrl = url.replace('v1', 'prom');
|
||||||
return this._request(apiUrl, { silent: true }).then((res: DataQueryResponse) => {
|
return this._request(apiUrl, params, { silent: true }).then((res: DataQueryResponse) => {
|
||||||
const data: any = { data: { data: res.data.values || [] } };
|
const data: any = { data: { data: res.data.values || [] } };
|
||||||
return data;
|
return data;
|
||||||
});
|
});
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
import Plain from 'slate-plain-serializer';
|
import Plain from 'slate-plain-serializer';
|
||||||
|
|
||||||
import LanguageProvider, { LABEL_REFRESH_INTERVAL } from './language_provider';
|
import LanguageProvider, { LABEL_REFRESH_INTERVAL, rangeToParams } from './language_provider';
|
||||||
|
import { AbsoluteTimeRange } from '@grafana/data';
|
||||||
import { advanceTo, clear, advanceBy } from 'jest-date-mock';
|
import { advanceTo, clear, advanceBy } from 'jest-date-mock';
|
||||||
import { beforeEach } from 'test/lib/common';
|
import { beforeEach } from 'test/lib/common';
|
||||||
import { DataQueryResponseData } from '@grafana/ui';
|
import { DataQueryResponseData } from '@grafana/ui';
|
||||||
@ -11,8 +12,13 @@ describe('Language completion provider', () => {
|
|||||||
metadataRequest: () => ({ data: { data: [] as DataQueryResponseData[] } }),
|
metadataRequest: () => ({ data: { data: [] as DataQueryResponseData[] } }),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const rangeMock: AbsoluteTimeRange = {
|
||||||
|
from: 1560153109000,
|
||||||
|
to: 1560163909000,
|
||||||
|
};
|
||||||
|
|
||||||
describe('empty query suggestions', () => {
|
describe('empty query suggestions', () => {
|
||||||
it('returns no suggestions on emtpty context', () => {
|
it('returns no suggestions on empty context', () => {
|
||||||
const instance = new LanguageProvider(datasource);
|
const instance = new LanguageProvider(datasource);
|
||||||
const value = Plain.deserialize('');
|
const value = Plain.deserialize('');
|
||||||
const result = instance.provideCompletionItems({ text: '', prefix: '', value, wrapperClasses: [] });
|
const result = instance.provideCompletionItems({ text: '', prefix: '', value, wrapperClasses: [] });
|
||||||
@ -21,7 +27,7 @@ describe('Language completion provider', () => {
|
|||||||
expect(result.suggestions.length).toEqual(0);
|
expect(result.suggestions.length).toEqual(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('returns default suggestions with history on emtpty context when history was provided', () => {
|
it('returns default suggestions with history on empty context when history was provided', () => {
|
||||||
const instance = new LanguageProvider(datasource);
|
const instance = new LanguageProvider(datasource);
|
||||||
const value = Plain.deserialize('');
|
const value = Plain.deserialize('');
|
||||||
const history = [
|
const history = [
|
||||||
@ -29,7 +35,10 @@ describe('Language completion provider', () => {
|
|||||||
query: { refId: '1', expr: '{app="foo"}' },
|
query: { refId: '1', expr: '{app="foo"}' },
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
const result = instance.provideCompletionItems({ text: '', prefix: '', value, wrapperClasses: [] }, { history });
|
const result = instance.provideCompletionItems(
|
||||||
|
{ text: '', prefix: '', value, wrapperClasses: [] },
|
||||||
|
{ history, absoluteRange: rangeMock }
|
||||||
|
);
|
||||||
expect(result.context).toBeUndefined();
|
expect(result.context).toBeUndefined();
|
||||||
expect(result.refresher).toBeUndefined();
|
expect(result.refresher).toBeUndefined();
|
||||||
expect(result.suggestions).toMatchObject([
|
expect(result.suggestions).toMatchObject([
|
||||||
@ -79,64 +88,102 @@ describe('Language completion provider', () => {
|
|||||||
anchorOffset: 1,
|
anchorOffset: 1,
|
||||||
});
|
});
|
||||||
const valueWithSelection = value.change().select(range).value;
|
const valueWithSelection = value.change().select(range).value;
|
||||||
const result = instance.provideCompletionItems({
|
const result = instance.provideCompletionItems(
|
||||||
text: '',
|
{
|
||||||
prefix: '',
|
text: '',
|
||||||
wrapperClasses: ['context-labels'],
|
prefix: '',
|
||||||
value: valueWithSelection,
|
wrapperClasses: ['context-labels'],
|
||||||
});
|
value: valueWithSelection,
|
||||||
|
},
|
||||||
|
{ absoluteRange: rangeMock }
|
||||||
|
);
|
||||||
expect(result.context).toBe('context-labels');
|
expect(result.context).toBe('context-labels');
|
||||||
expect(result.suggestions).toEqual([{ items: [{ label: 'job' }, { label: 'namespace' }], label: 'Labels' }]);
|
expect(result.suggestions).toEqual([{ items: [{ label: 'job' }, { label: 'namespace' }], label: 'Labels' }]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('Request URL', () => {
|
||||||
|
it('should contain range params', async () => {
|
||||||
|
const rangeMock: AbsoluteTimeRange = {
|
||||||
|
from: 1560153109000,
|
||||||
|
to: 1560163909000,
|
||||||
|
};
|
||||||
|
|
||||||
|
const datasourceWithLabels = {
|
||||||
|
metadataRequest: url => {
|
||||||
|
if (url.slice(0, 15) === '/api/prom/label') {
|
||||||
|
return { data: { data: ['other'] } };
|
||||||
|
} else {
|
||||||
|
return { data: { data: [] } };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const datasourceSpy = jest.spyOn(datasourceWithLabels, 'metadataRequest');
|
||||||
|
|
||||||
|
const instance = new LanguageProvider(datasourceWithLabels, { initialRange: rangeMock });
|
||||||
|
await instance.refreshLogLabels(rangeMock, true);
|
||||||
|
const expectedUrl = '/api/prom/label';
|
||||||
|
expect(datasourceSpy).toHaveBeenCalledWith(expectedUrl, rangeToParams(rangeMock));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('Query imports', () => {
|
describe('Query imports', () => {
|
||||||
const datasource = {
|
const datasource = {
|
||||||
metadataRequest: () => ({ data: { data: [] as DataQueryResponseData[] } }),
|
metadataRequest: () => ({ data: { data: [] as DataQueryResponseData[] } }),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const rangeMock: AbsoluteTimeRange = {
|
||||||
|
from: 1560153109000,
|
||||||
|
to: 1560163909000,
|
||||||
|
};
|
||||||
|
|
||||||
it('returns empty queries for unknown origin datasource', async () => {
|
it('returns empty queries for unknown origin datasource', async () => {
|
||||||
const instance = new LanguageProvider(datasource);
|
const instance = new LanguageProvider(datasource, { initialRange: rangeMock });
|
||||||
const result = await instance.importQueries([{ refId: 'bar', expr: 'foo' }], 'unknown');
|
const result = await instance.importQueries([{ refId: 'bar', expr: 'foo' }], 'unknown');
|
||||||
expect(result).toEqual([{ refId: 'bar', expr: '' }]);
|
expect(result).toEqual([{ refId: 'bar', expr: '' }]);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('prometheus query imports', () => {
|
describe('prometheus query imports', () => {
|
||||||
it('returns empty query from metric-only query', async () => {
|
it('returns empty query from metric-only query', async () => {
|
||||||
const instance = new LanguageProvider(datasource);
|
const instance = new LanguageProvider(datasource, { initialRange: rangeMock });
|
||||||
const result = await instance.importPrometheusQuery('foo');
|
const result = await instance.importPrometheusQuery('foo');
|
||||||
expect(result).toEqual('');
|
expect(result).toEqual('');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('returns empty query from selector query if label is not available', async () => {
|
it('returns empty query from selector query if label is not available', async () => {
|
||||||
const datasourceWithLabels = {
|
const datasourceWithLabels = {
|
||||||
metadataRequest: (url: string) =>
|
metadataRequest: url =>
|
||||||
url === '/api/prom/label' ? { data: { data: ['other'] } } : { data: { data: [] as DataQueryResponseData[] } },
|
url.slice(0, 15) === '/api/prom/label'
|
||||||
|
? { data: { data: ['other'] } }
|
||||||
|
: { data: { data: [] as DataQueryResponseData[] } },
|
||||||
};
|
};
|
||||||
const instance = new LanguageProvider(datasourceWithLabels);
|
const instance = new LanguageProvider(datasourceWithLabels, { initialRange: rangeMock });
|
||||||
const result = await instance.importPrometheusQuery('{foo="bar"}');
|
const result = await instance.importPrometheusQuery('{foo="bar"}');
|
||||||
expect(result).toEqual('{}');
|
expect(result).toEqual('{}');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('returns selector query from selector query with common labels', async () => {
|
it('returns selector query from selector query with common labels', async () => {
|
||||||
const datasourceWithLabels = {
|
const datasourceWithLabels = {
|
||||||
metadataRequest: (url: string) =>
|
metadataRequest: url =>
|
||||||
url === '/api/prom/label' ? { data: { data: ['foo'] } } : { data: { data: [] as DataQueryResponseData[] } },
|
url.slice(0, 15) === '/api/prom/label'
|
||||||
|
? { data: { data: ['foo'] } }
|
||||||
|
: { data: { data: [] as DataQueryResponseData[] } },
|
||||||
};
|
};
|
||||||
const instance = new LanguageProvider(datasourceWithLabels);
|
const instance = new LanguageProvider(datasourceWithLabels, { initialRange: rangeMock });
|
||||||
const result = await instance.importPrometheusQuery('metric{foo="bar",baz="42"}');
|
const result = await instance.importPrometheusQuery('metric{foo="bar",baz="42"}');
|
||||||
expect(result).toEqual('{foo="bar"}');
|
expect(result).toEqual('{foo="bar"}');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('returns selector query from selector query with all labels if logging label list is empty', async () => {
|
it('returns selector query from selector query with all labels if logging label list is empty', async () => {
|
||||||
const datasourceWithLabels = {
|
const datasourceWithLabels = {
|
||||||
metadataRequest: (url: string) =>
|
metadataRequest: url =>
|
||||||
url === '/api/prom/label'
|
url.slice(0, 15) === '/api/prom/label'
|
||||||
? { data: { data: [] as DataQueryResponseData[] } }
|
? { data: { data: [] as DataQueryResponseData[] } }
|
||||||
: { data: { data: [] as DataQueryResponseData[] } },
|
: { data: { data: [] as DataQueryResponseData[] } },
|
||||||
};
|
};
|
||||||
const instance = new LanguageProvider(datasourceWithLabels);
|
const instance = new LanguageProvider(datasourceWithLabels, { initialRange: rangeMock });
|
||||||
const result = await instance.importPrometheusQuery('metric{foo="bar",baz="42"}');
|
const result = await instance.importPrometheusQuery('metric{foo="bar",baz="42"}');
|
||||||
expect(result).toEqual('{baz="42",foo="bar"}');
|
expect(result).toEqual('{baz="42",foo="bar"}');
|
||||||
});
|
});
|
||||||
@ -149,6 +196,11 @@ describe('Labels refresh', () => {
|
|||||||
};
|
};
|
||||||
const instance = new LanguageProvider(datasource);
|
const instance = new LanguageProvider(datasource);
|
||||||
|
|
||||||
|
const rangeMock: AbsoluteTimeRange = {
|
||||||
|
from: 1560153109000,
|
||||||
|
to: 1560163909000,
|
||||||
|
};
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
instance.fetchLogLabels = jest.fn();
|
instance.fetchLogLabels = jest.fn();
|
||||||
});
|
});
|
||||||
@ -157,18 +209,20 @@ describe('Labels refresh', () => {
|
|||||||
jest.clearAllMocks();
|
jest.clearAllMocks();
|
||||||
clear();
|
clear();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should not refresh labels if refresh interval hasn't passed", () => {
|
it("should not refresh labels if refresh interval hasn't passed", () => {
|
||||||
advanceTo(new Date(2019, 1, 1, 0, 0, 0));
|
advanceTo(new Date(2019, 1, 1, 0, 0, 0));
|
||||||
instance.logLabelFetchTs = Date.now();
|
instance.logLabelFetchTs = Date.now();
|
||||||
advanceBy(LABEL_REFRESH_INTERVAL / 2);
|
advanceBy(LABEL_REFRESH_INTERVAL / 2);
|
||||||
instance.refreshLogLabels();
|
instance.refreshLogLabels(rangeMock);
|
||||||
expect(instance.fetchLogLabels).not.toBeCalled();
|
expect(instance.fetchLogLabels).not.toBeCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should refresh labels if refresh interval passed', () => {
|
it('should refresh labels if refresh interval passed', () => {
|
||||||
advanceTo(new Date(2019, 1, 1, 0, 0, 0));
|
advanceTo(new Date(2019, 1, 1, 0, 0, 0));
|
||||||
instance.logLabelFetchTs = Date.now();
|
instance.logLabelFetchTs = Date.now();
|
||||||
advanceBy(LABEL_REFRESH_INTERVAL + 1);
|
advanceBy(LABEL_REFRESH_INTERVAL + 1);
|
||||||
instance.refreshLogLabels();
|
instance.refreshLogLabels(rangeMock);
|
||||||
expect(instance.fetchLogLabels).toBeCalled();
|
expect(instance.fetchLogLabels).toBeCalled();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -15,16 +15,18 @@ import {
|
|||||||
HistoryItem,
|
HistoryItem,
|
||||||
} from 'app/types/explore';
|
} from 'app/types/explore';
|
||||||
import { LokiQuery } from './types';
|
import { LokiQuery } from './types';
|
||||||
import { dateTime } from '@grafana/data';
|
import { dateTime, AbsoluteTimeRange } from '@grafana/data';
|
||||||
import { PromQuery } from '../prometheus/types';
|
import { PromQuery } from '../prometheus/types';
|
||||||
|
|
||||||
const DEFAULT_KEYS = ['job', 'namespace'];
|
const DEFAULT_KEYS = ['job', 'namespace'];
|
||||||
const EMPTY_SELECTOR = '{}';
|
const EMPTY_SELECTOR = '{}';
|
||||||
const HISTORY_ITEM_COUNT = 10;
|
const HISTORY_ITEM_COUNT = 10;
|
||||||
const HISTORY_COUNT_CUTOFF = 1000 * 60 * 60 * 24; // 24h
|
const HISTORY_COUNT_CUTOFF = 1000 * 60 * 60 * 24; // 24h
|
||||||
|
const NS_IN_MS = 1_000_000;
|
||||||
export const LABEL_REFRESH_INTERVAL = 1000 * 30; // 30sec
|
export const LABEL_REFRESH_INTERVAL = 1000 * 30; // 30sec
|
||||||
|
|
||||||
const wrapLabel = (label: string) => ({ label });
|
const wrapLabel = (label: string) => ({ label });
|
||||||
|
export const rangeToParams = (range: AbsoluteTimeRange) => ({ start: range.from * NS_IN_MS, end: range.to * NS_IN_MS });
|
||||||
|
|
||||||
type LokiHistoryItem = HistoryItem<LokiQuery>;
|
type LokiHistoryItem = HistoryItem<LokiQuery>;
|
||||||
|
|
||||||
@ -50,6 +52,7 @@ export default class LokiLanguageProvider extends LanguageProvider {
|
|||||||
logLabelOptions: any[];
|
logLabelOptions: any[];
|
||||||
logLabelFetchTs?: number;
|
logLabelFetchTs?: number;
|
||||||
started: boolean;
|
started: boolean;
|
||||||
|
initialRange: AbsoluteTimeRange;
|
||||||
|
|
||||||
constructor(datasource: any, initialValues?: any) {
|
constructor(datasource: any, initialValues?: any) {
|
||||||
super();
|
super();
|
||||||
@ -67,13 +70,13 @@ export default class LokiLanguageProvider extends LanguageProvider {
|
|||||||
return syntax;
|
return syntax;
|
||||||
}
|
}
|
||||||
|
|
||||||
request = (url: string) => {
|
request = (url: string, params?: any) => {
|
||||||
return this.datasource.metadataRequest(url);
|
return this.datasource.metadataRequest(url, params);
|
||||||
};
|
};
|
||||||
|
|
||||||
start = () => {
|
start = () => {
|
||||||
if (!this.startTask) {
|
if (!this.startTask) {
|
||||||
this.startTask = this.fetchLogLabels();
|
this.startTask = this.fetchLogLabels(this.initialRange);
|
||||||
}
|
}
|
||||||
return this.startTask;
|
return this.startTask;
|
||||||
};
|
};
|
||||||
@ -120,7 +123,10 @@ export default class LokiLanguageProvider extends LanguageProvider {
|
|||||||
return { suggestions };
|
return { suggestions };
|
||||||
}
|
}
|
||||||
|
|
||||||
getLabelCompletionItems({ text, wrapperClasses, labelKey, value }: TypeaheadInput): TypeaheadOutput {
|
getLabelCompletionItems(
|
||||||
|
{ text, wrapperClasses, labelKey, value }: TypeaheadInput,
|
||||||
|
{ absoluteRange }: any
|
||||||
|
): TypeaheadOutput {
|
||||||
let context: string;
|
let context: string;
|
||||||
let refresher: Promise<any> = null;
|
let refresher: Promise<any> = null;
|
||||||
const suggestions: CompletionItemGroup[] = [];
|
const suggestions: CompletionItemGroup[] = [];
|
||||||
@ -146,7 +152,7 @@ export default class LokiLanguageProvider extends LanguageProvider {
|
|||||||
items: labelValues.map(wrapLabel),
|
items: labelValues.map(wrapLabel),
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
refresher = this.fetchLabelValues(labelKey);
|
refresher = this.fetchLabelValues(labelKey, absoluteRange);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@ -206,7 +212,7 @@ export default class LokiLanguageProvider extends LanguageProvider {
|
|||||||
if (existingKeys && existingKeys.length > 0) {
|
if (existingKeys && existingKeys.length > 0) {
|
||||||
// Check for common labels
|
// Check for common labels
|
||||||
for (const key in labels) {
|
for (const key in labels) {
|
||||||
if (existingKeys && existingKeys.indexOf(key) > -1) {
|
if (existingKeys && existingKeys.includes(key)) {
|
||||||
// Should we check for label value equality here?
|
// Should we check for label value equality here?
|
||||||
labelsToKeep[key] = labels[key];
|
labelsToKeep[key] = labels[key];
|
||||||
}
|
}
|
||||||
@ -227,11 +233,12 @@ export default class LokiLanguageProvider extends LanguageProvider {
|
|||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
async fetchLogLabels(): Promise<any> {
|
async fetchLogLabels(absoluteRange: AbsoluteTimeRange): Promise<any> {
|
||||||
const url = '/api/prom/label';
|
const url = '/api/prom/label';
|
||||||
try {
|
try {
|
||||||
this.logLabelFetchTs = Date.now();
|
this.logLabelFetchTs = Date.now();
|
||||||
const res = await this.request(url);
|
|
||||||
|
const res = await this.request(url, rangeToParams(absoluteRange));
|
||||||
const body = await (res.data || res.json());
|
const body = await (res.data || res.json());
|
||||||
const labelKeys = body.data.slice().sort();
|
const labelKeys = body.data.slice().sort();
|
||||||
this.labelKeys = {
|
this.labelKeys = {
|
||||||
@ -244,7 +251,7 @@ export default class LokiLanguageProvider extends LanguageProvider {
|
|||||||
return Promise.all(
|
return Promise.all(
|
||||||
labelKeys
|
labelKeys
|
||||||
.filter((key: string) => DEFAULT_KEYS.indexOf(key) > -1)
|
.filter((key: string) => DEFAULT_KEYS.indexOf(key) > -1)
|
||||||
.map((key: string) => this.fetchLabelValues(key))
|
.map((key: string) => this.fetchLabelValues(key, absoluteRange))
|
||||||
);
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
@ -252,16 +259,16 @@ export default class LokiLanguageProvider extends LanguageProvider {
|
|||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
async refreshLogLabels(forceRefresh?: boolean) {
|
async refreshLogLabels(absoluteRange: AbsoluteTimeRange, forceRefresh?: boolean) {
|
||||||
if ((this.labelKeys && Date.now() - this.logLabelFetchTs > LABEL_REFRESH_INTERVAL) || forceRefresh) {
|
if ((this.labelKeys && Date.now() - this.logLabelFetchTs > LABEL_REFRESH_INTERVAL) || forceRefresh) {
|
||||||
await this.fetchLogLabels();
|
await this.fetchLogLabels(absoluteRange);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fetchLabelValues(key: string) {
|
async fetchLabelValues(key: string, absoluteRange: AbsoluteTimeRange) {
|
||||||
const url = `/api/prom/label/${key}/values`;
|
const url = `/api/prom/label/${key}/values`;
|
||||||
try {
|
try {
|
||||||
const res = await this.request(url);
|
const res = await this.request(url, rangeToParams(absoluteRange));
|
||||||
const body = await (res.data || res.json());
|
const body = await (res.data || res.json());
|
||||||
const values = body.data.slice().sort();
|
const values = body.data.slice().sort();
|
||||||
|
|
||||||
|
@ -286,7 +286,7 @@ export interface HistoryItem<TQuery extends DataQuery = DataQuery> {
|
|||||||
|
|
||||||
export abstract class LanguageProvider {
|
export abstract class LanguageProvider {
|
||||||
datasource: any;
|
datasource: any;
|
||||||
request: (url: any) => Promise<any>;
|
request: (url: string, params?: any) => Promise<any>;
|
||||||
/**
|
/**
|
||||||
* Returns startTask that resolves with a task list when main syntax is loaded.
|
* Returns startTask that resolves with a task list when main syntax is loaded.
|
||||||
* Task list consists of secondary promises that load more detailed language features.
|
* Task list consists of secondary promises that load more detailed language features.
|
||||||
|
Loading…
Reference in New Issue
Block a user