mirror of
https://github.com/grafana/grafana.git
synced 2025-02-13 00:55:47 -06:00
* Explore: updates grafana-data explore query field props with explore mode * Explore: updates query row to pass down explore mode to query fields * Explore: adds LokiExploreQueryEditor * Explore: updates loki query field form to render children * Explore: adds loki explore extra field component * Explore: adds extra field element to loki query field form * Explore: updates loki explore query editor to use extra field element * Explore: moves ExploreMode to grafana-data * Explore: updates query row limit string * Explore: adds maxLines to DataQuery * Explore: adds maxLines to loki datasource runRangeQueryWithFallback * Explore: adds onChangeQueryLimit to LokiExploreQueryEditor * Explore: updates loki explore query editor to render extra field only in logs mode * Explore: fixes query limits for live and legacy queries * Explore: fixes result processor max lines limit in get logs result * Explore: fixes Loki datasource limit test * Explore: removes unnecessary ExploreMode from Loki language provider * Explore: fixes formatting * Explore: updates grafana-data datasource types - replaces strings with explore mode enum * Explore: updates loki explore query field props to take ReactNode * Explore: updates the way we calculate loki query lines limit to fall back to 0 lines on negative or invalid input instead of datasource maxLines * Explore: updates result processor get logs result method to avoid counting invalid/negative line limits * Explore: updates loki result transformer to process only an appropriate slice of a result instead of an entire one * Explore: adds a method for query limit preprocessing/mapping * Explore: updates loki datasource run range query with fallback method to use options.maxDataPoints in dashboards * Explore: removes unnecessary maxlineslimt from getLogsResult in resultProcessor * Explore: moves line limit to metadata * Explore: adds an ability to specify input type of extra field * Explore: updates LokiExploreQueryEditor - adds an input type * Explore: updates LokiExploreQueryEditor to run queries when maxLines is positive * Explore: fixes failing import of ExploreMode * Explore: fixes reducers test imports formatting * Explore: updates Loki extra field with min value set to 0 * Explore: exports LokiExploreExtraFieldProps * Explore: adds render test of LokiExploreQueryEditor * Explore: adds LokiExploreQueryEditor snapshot * Explore: updates LokiExploreQueryEditor onChangeQueryLimit method to prevent it from running when query input is empty - fixes cheatsheet display issue * Explore: updates Loki editor snapshots * Explore: fixes typo in test set name in LokiExploreQueryEditor * Explore: adds a render test of LokiExploreExtraField * Explore: fixes typo in LokiExploreQueryEditor * Explore: updates LokiExploreQueryEditor snapshot due to timezone issues * Explore: updates LokiExploreExtraField to export both functional component and a version using memo * Explore: updates LokiExploreQueryEditor to export both functional component and memoized function * Explore: updates LokiExploreQueryEditor - removes unnecessary react fragment * Explore: updates LokiExploreQueryEditor snapshot * Explore: adds LokiExploreQueryEditor tests for different explore mode cases * Explore: fixes Loki datasource and result transformer * Explore: updates LokiExploreQueryEditor snapshot * Explore: updates LokiExploreQueryEditor tests and test setup * Explore: updates LokiExploreQueryEditor - refactors component * Explore: updates LokiExploreQueryEditor to use default import from LokiExploreExtraField * Explore: updates LokiExploreQueryEditor snapshot * Explore: fixes formatting * Explore: updates LokiExploreQueryEditor max lines change * Explore: updates LokiExploreQueryEditor tests checking ExtraFieldElement * Explore: adds mock loki datasource to LokiExploreQueryEditor * Explore: updates LokiExploreQueryEditor test mock - adds language provider * Explore: updates LokiExploreQueryEditor snapshot * Explore: updates Loki ResultTransformer to filter out rows on limit - logic to be moved into a component with new form styles * Explore: updates LokiExploreQueryEditor tests
417 lines
14 KiB
TypeScript
417 lines
14 KiB
TypeScript
import { PayloadAction } from '@reduxjs/toolkit';
|
|
import { DataQuery, DefaultTimeZone, LogsDedupStrategy, RawTimeRange, toUtc, ExploreMode } from '@grafana/data';
|
|
|
|
import * as Actions from './actions';
|
|
import { changeDatasource, loadDatasource, navigateToExplore, refreshExplore } from './actions';
|
|
import { ExploreId, ExploreUpdateState, ExploreUrlState } from 'app/types';
|
|
import { thunkTester } from 'test/core/thunk/thunkTester';
|
|
import {
|
|
initializeExploreAction,
|
|
InitializeExplorePayload,
|
|
loadDatasourcePendingAction,
|
|
loadDatasourceReadyAction,
|
|
setQueriesAction,
|
|
updateDatasourceInstanceAction,
|
|
updateUIStateAction,
|
|
} from './actionTypes';
|
|
import { Emitter } from 'app/core/core';
|
|
import { makeInitialUpdateState } from './reducers';
|
|
import { PanelModel } from 'app/features/dashboard/state';
|
|
import { updateLocation } from '../../../core/actions';
|
|
import { MockDataSourceApi } from '../../../../test/mocks/datasource_srv';
|
|
import * as DatasourceSrv from 'app/features/plugins/datasource_srv';
|
|
|
|
jest.mock('app/features/plugins/datasource_srv');
|
|
const getDatasourceSrvMock = (DatasourceSrv.getDatasourceSrv as any) as jest.Mock<DatasourceSrv.DatasourceSrv>;
|
|
|
|
beforeEach(() => {
|
|
getDatasourceSrvMock.mockClear();
|
|
getDatasourceSrvMock.mockImplementation(
|
|
() =>
|
|
({
|
|
getExternal: jest.fn().mockReturnValue([]),
|
|
get: jest.fn().mockReturnValue({
|
|
testDatasource: jest.fn(),
|
|
init: jest.fn(),
|
|
}),
|
|
} as any)
|
|
);
|
|
});
|
|
|
|
jest.mock('../../dashboard/services/TimeSrv', () => ({
|
|
getTimeSrv: jest.fn().mockReturnValue({
|
|
init: jest.fn(),
|
|
}),
|
|
}));
|
|
|
|
const t = toUtc();
|
|
const testRange = {
|
|
from: t,
|
|
to: t,
|
|
raw: {
|
|
from: t,
|
|
to: t,
|
|
},
|
|
};
|
|
jest.mock('app/core/utils/explore', () => ({
|
|
...jest.requireActual('app/core/utils/explore'),
|
|
getTimeRangeFromUrl: (range: RawTimeRange) => testRange,
|
|
}));
|
|
|
|
const setup = (updateOverides?: Partial<ExploreUpdateState>) => {
|
|
const exploreId = ExploreId.left;
|
|
const containerWidth = 1920;
|
|
const eventBridge = {} as Emitter;
|
|
const ui = { dedupStrategy: LogsDedupStrategy.none, showingGraph: false, showingLogs: false, showingTable: false };
|
|
const timeZone = DefaultTimeZone;
|
|
const range = testRange;
|
|
const urlState: ExploreUrlState = {
|
|
datasource: 'some-datasource',
|
|
queries: [],
|
|
range: range.raw,
|
|
mode: ExploreMode.Metrics,
|
|
ui,
|
|
};
|
|
const updateDefaults = makeInitialUpdateState();
|
|
const update = { ...updateDefaults, ...updateOverides };
|
|
const initialState = {
|
|
user: {
|
|
orgId: '1',
|
|
timeZone,
|
|
},
|
|
explore: {
|
|
[exploreId]: {
|
|
initialized: true,
|
|
urlState,
|
|
containerWidth,
|
|
eventBridge,
|
|
update,
|
|
datasourceInstance: { name: 'some-datasource' },
|
|
queries: [] as DataQuery[],
|
|
range,
|
|
ui,
|
|
refreshInterval: {
|
|
label: 'Off',
|
|
value: 0,
|
|
},
|
|
},
|
|
},
|
|
};
|
|
|
|
return {
|
|
initialState,
|
|
exploreId,
|
|
range,
|
|
ui,
|
|
containerWidth,
|
|
eventBridge,
|
|
};
|
|
};
|
|
|
|
describe('refreshExplore', () => {
|
|
describe('when explore is initialized', () => {
|
|
describe('and update datasource is set', () => {
|
|
it('then it should dispatch initializeExplore', async () => {
|
|
const { exploreId, ui, initialState, containerWidth, eventBridge } = setup({ datasource: true });
|
|
|
|
const dispatchedActions = await thunkTester(initialState)
|
|
.givenThunk(refreshExplore)
|
|
.whenThunkIsDispatched(exploreId);
|
|
|
|
const initializeExplore = dispatchedActions[1] as PayloadAction<InitializeExplorePayload>;
|
|
const { type, payload } = initializeExplore;
|
|
|
|
expect(type).toEqual(initializeExploreAction.type);
|
|
expect(payload.containerWidth).toEqual(containerWidth);
|
|
expect(payload.eventBridge).toEqual(eventBridge);
|
|
expect(payload.queries.length).toBe(1); // Queries have generated keys hard to expect on
|
|
expect(payload.range.from).toEqual(testRange.from);
|
|
expect(payload.range.to).toEqual(testRange.to);
|
|
expect(payload.range.raw.from).toEqual(testRange.raw.from);
|
|
expect(payload.range.raw.to).toEqual(testRange.raw.to);
|
|
expect(payload.ui).toEqual(ui);
|
|
});
|
|
});
|
|
|
|
describe('and update ui is set', () => {
|
|
it('then it should dispatch updateUIStateAction', async () => {
|
|
const { exploreId, initialState, ui } = setup({ ui: true });
|
|
|
|
const dispatchedActions = await thunkTester(initialState)
|
|
.givenThunk(refreshExplore)
|
|
.whenThunkIsDispatched(exploreId);
|
|
|
|
expect(dispatchedActions[0].type).toEqual(updateUIStateAction.type);
|
|
expect(dispatchedActions[0].payload).toEqual({ ...ui, exploreId });
|
|
});
|
|
});
|
|
|
|
describe('and update queries is set', () => {
|
|
it('then it should dispatch setQueriesAction', async () => {
|
|
const { exploreId, initialState } = setup({ queries: true });
|
|
|
|
const dispatchedActions = await thunkTester(initialState)
|
|
.givenThunk(refreshExplore)
|
|
.whenThunkIsDispatched(exploreId);
|
|
|
|
expect(dispatchedActions[0].type).toEqual(setQueriesAction.type);
|
|
expect(dispatchedActions[0].payload).toEqual({ exploreId, queries: [] });
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('when update is not initialized', () => {
|
|
it('then it should not dispatch any actions', async () => {
|
|
const exploreId = ExploreId.left;
|
|
const initialState = { explore: { [exploreId]: { initialized: false } } };
|
|
|
|
const dispatchedActions = await thunkTester(initialState)
|
|
.givenThunk(refreshExplore)
|
|
.whenThunkIsDispatched(exploreId);
|
|
|
|
expect(dispatchedActions).toEqual([]);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('changing datasource', () => {
|
|
it('should switch to logs mode when changing from prometheus to loki', async () => {
|
|
const lokiMock = {
|
|
testDatasource: () => Promise.resolve({ status: 'success' }),
|
|
name: 'Loki',
|
|
init: jest.fn(),
|
|
meta: { id: 'some id', name: 'Loki' },
|
|
};
|
|
|
|
getDatasourceSrvMock.mockImplementation(
|
|
() =>
|
|
({
|
|
getExternal: jest.fn().mockReturnValue([]),
|
|
get: jest.fn().mockReturnValue(lokiMock),
|
|
} as any)
|
|
);
|
|
|
|
const exploreId = ExploreId.left;
|
|
const name = 'Prometheus';
|
|
const mockPromDatasourceInstance = {
|
|
testDatasource: () => Promise.resolve({ status: 'success' }),
|
|
name,
|
|
init: jest.fn(),
|
|
meta: { id: 'some id', name },
|
|
};
|
|
|
|
const initialState = {
|
|
explore: {
|
|
[exploreId]: {
|
|
requestedDatasourceName: 'Loki',
|
|
datasourceInstance: mockPromDatasourceInstance,
|
|
},
|
|
},
|
|
user: {
|
|
orgId: 1,
|
|
},
|
|
};
|
|
|
|
jest.spyOn(Actions, 'importQueries').mockImplementationOnce(() => jest.fn);
|
|
jest.spyOn(Actions, 'loadDatasource').mockImplementationOnce(() => jest.fn);
|
|
jest.spyOn(Actions, 'runQueries').mockImplementationOnce(() => jest.fn);
|
|
const dispatchedActions = await thunkTester(initialState)
|
|
.givenThunk(changeDatasource)
|
|
.whenThunkIsDispatched(exploreId, name);
|
|
|
|
expect(dispatchedActions).toEqual([
|
|
updateDatasourceInstanceAction({
|
|
exploreId,
|
|
datasourceInstance: lokiMock as any,
|
|
version: undefined,
|
|
mode: ExploreMode.Logs,
|
|
}),
|
|
]);
|
|
});
|
|
});
|
|
|
|
describe('loading datasource', () => {
|
|
describe('when loadDatasource thunk is dispatched', () => {
|
|
describe('and all goes fine', () => {
|
|
it('then it should dispatch correct actions', async () => {
|
|
const exploreId = ExploreId.left;
|
|
const name = 'some-datasource';
|
|
const initialState = { explore: { [exploreId]: { requestedDatasourceName: name } } };
|
|
const mockDatasourceInstance = {
|
|
testDatasource: () => {
|
|
return Promise.resolve({ status: 'success' });
|
|
},
|
|
name,
|
|
init: jest.fn(),
|
|
meta: { id: 'some id' },
|
|
};
|
|
|
|
const dispatchedActions = await thunkTester(initialState)
|
|
.givenThunk(loadDatasource)
|
|
.whenThunkIsDispatched(exploreId, mockDatasourceInstance);
|
|
|
|
expect(dispatchedActions).toEqual([
|
|
loadDatasourcePendingAction({
|
|
exploreId,
|
|
requestedDatasourceName: mockDatasourceInstance.name,
|
|
}),
|
|
loadDatasourceReadyAction({ exploreId, history: [] }),
|
|
]);
|
|
});
|
|
});
|
|
|
|
describe('and user changes datasource during load', () => {
|
|
it('then it should dispatch correct actions', async () => {
|
|
const exploreId = ExploreId.left;
|
|
const name = 'some-datasource';
|
|
const initialState = { explore: { [exploreId]: { requestedDatasourceName: 'some-other-datasource' } } };
|
|
const mockDatasourceInstance = {
|
|
testDatasource: () => {
|
|
return Promise.resolve({ status: 'success' });
|
|
},
|
|
name,
|
|
init: jest.fn(),
|
|
meta: { id: 'some id' },
|
|
};
|
|
|
|
const dispatchedActions = await thunkTester(initialState)
|
|
.givenThunk(loadDatasource)
|
|
.whenThunkIsDispatched(exploreId, mockDatasourceInstance);
|
|
|
|
expect(dispatchedActions).toEqual([
|
|
loadDatasourcePendingAction({
|
|
exploreId,
|
|
requestedDatasourceName: mockDatasourceInstance.name,
|
|
}),
|
|
]);
|
|
});
|
|
});
|
|
});
|
|
});
|
|
|
|
const getNavigateToExploreContext = async (openInNewWindow: (url: string) => void = undefined) => {
|
|
const url = 'http://www.someurl.com';
|
|
const panel: Partial<PanelModel> = {
|
|
datasource: 'mocked datasource',
|
|
targets: [{ refId: 'A' }],
|
|
};
|
|
const datasource = new MockDataSourceApi(panel.datasource);
|
|
const get = jest.fn().mockResolvedValue(datasource);
|
|
const getDataSourceSrv = jest.fn().mockReturnValue({ get });
|
|
const getTimeSrv = jest.fn();
|
|
const getExploreUrl = jest.fn().mockResolvedValue(url);
|
|
|
|
const dispatchedActions = await thunkTester({})
|
|
.givenThunk(navigateToExplore)
|
|
.whenThunkIsDispatched(panel, { getDataSourceSrv, getTimeSrv, getExploreUrl, openInNewWindow });
|
|
|
|
return {
|
|
url,
|
|
panel,
|
|
datasource,
|
|
get,
|
|
getDataSourceSrv,
|
|
getTimeSrv,
|
|
getExploreUrl,
|
|
dispatchedActions,
|
|
};
|
|
};
|
|
|
|
describe('navigateToExplore', () => {
|
|
describe('when navigateToExplore thunk is dispatched', () => {
|
|
describe('and openInNewWindow is undefined', () => {
|
|
const openInNewWindow: (url: string) => void = undefined;
|
|
it('then it should dispatch correct actions', async () => {
|
|
const { dispatchedActions, url } = await getNavigateToExploreContext(openInNewWindow);
|
|
|
|
expect(dispatchedActions).toEqual([updateLocation({ path: url, query: {} })]);
|
|
});
|
|
|
|
it('then getDataSourceSrv should have been once', async () => {
|
|
const { getDataSourceSrv } = await getNavigateToExploreContext(openInNewWindow);
|
|
|
|
expect(getDataSourceSrv).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it('then getDataSourceSrv.get should have been called with correct arguments', async () => {
|
|
const { get, panel } = await getNavigateToExploreContext(openInNewWindow);
|
|
|
|
expect(get).toHaveBeenCalledTimes(1);
|
|
expect(get).toHaveBeenCalledWith(panel.datasource);
|
|
});
|
|
|
|
it('then getTimeSrv should have been called once', async () => {
|
|
const { getTimeSrv } = await getNavigateToExploreContext(openInNewWindow);
|
|
|
|
expect(getTimeSrv).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it('then getExploreUrl should have been called with correct arguments', async () => {
|
|
const { getExploreUrl, panel, datasource, getDataSourceSrv, getTimeSrv } = await getNavigateToExploreContext(
|
|
openInNewWindow
|
|
);
|
|
|
|
expect(getExploreUrl).toHaveBeenCalledTimes(1);
|
|
expect(getExploreUrl).toHaveBeenCalledWith({
|
|
panel,
|
|
panelTargets: panel.targets,
|
|
panelDatasource: datasource,
|
|
datasourceSrv: getDataSourceSrv(),
|
|
timeSrv: getTimeSrv(),
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('and openInNewWindow is defined', () => {
|
|
const openInNewWindow: (url: string) => void = jest.fn();
|
|
it('then it should dispatch no actions', async () => {
|
|
const { dispatchedActions } = await getNavigateToExploreContext(openInNewWindow);
|
|
|
|
expect(dispatchedActions).toEqual([]);
|
|
});
|
|
|
|
it('then getDataSourceSrv should have been once', async () => {
|
|
const { getDataSourceSrv } = await getNavigateToExploreContext(openInNewWindow);
|
|
|
|
expect(getDataSourceSrv).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it('then getDataSourceSrv.get should have been called with correct arguments', async () => {
|
|
const { get, panel } = await getNavigateToExploreContext(openInNewWindow);
|
|
|
|
expect(get).toHaveBeenCalledTimes(1);
|
|
expect(get).toHaveBeenCalledWith(panel.datasource);
|
|
});
|
|
|
|
it('then getTimeSrv should have been called once', async () => {
|
|
const { getTimeSrv } = await getNavigateToExploreContext(openInNewWindow);
|
|
|
|
expect(getTimeSrv).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it('then getExploreUrl should have been called with correct arguments', async () => {
|
|
const { getExploreUrl, panel, datasource, getDataSourceSrv, getTimeSrv } = await getNavigateToExploreContext(
|
|
openInNewWindow
|
|
);
|
|
|
|
expect(getExploreUrl).toHaveBeenCalledTimes(1);
|
|
expect(getExploreUrl).toHaveBeenCalledWith({
|
|
panel,
|
|
panelTargets: panel.targets,
|
|
panelDatasource: datasource,
|
|
datasourceSrv: getDataSourceSrv(),
|
|
timeSrv: getTimeSrv(),
|
|
});
|
|
});
|
|
|
|
it('then openInNewWindow should have been called with correct arguments', async () => {
|
|
const openInNewWindowFunc = jest.fn();
|
|
const { url } = await getNavigateToExploreContext(openInNewWindowFunc);
|
|
|
|
expect(openInNewWindowFunc).toHaveBeenCalledTimes(1);
|
|
expect(openInNewWindowFunc).toHaveBeenCalledWith(url);
|
|
});
|
|
});
|
|
});
|
|
});
|