mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Explore: URL migrations & improved state management (#69692)
Co-authored-by: Piotr Jamróz <pm.jamroz@gmail.com>
This commit is contained in:
parent
a489135825
commit
d3450d75a4
@ -1675,8 +1675,7 @@ exports[`better eslint`] = {
|
|||||||
[0, 0, 0, "Do not use any type assertions.", "0"],
|
[0, 0, 0, "Do not use any type assertions.", "0"],
|
||||||
[0, 0, 0, "Unexpected any. Specify a different type.", "1"],
|
[0, 0, 0, "Unexpected any. Specify a different type.", "1"],
|
||||||
[0, 0, 0, "Unexpected any. Specify a different type.", "2"],
|
[0, 0, 0, "Unexpected any. Specify a different type.", "2"],
|
||||||
[0, 0, 0, "Unexpected any. Specify a different type.", "3"],
|
[0, 0, 0, "Unexpected any. Specify a different type.", "3"]
|
||||||
[0, 0, 0, "Unexpected any. Specify a different type.", "4"]
|
|
||||||
],
|
],
|
||||||
"public/app/core/utils/fetch.ts:5381": [
|
"public/app/core/utils/fetch.ts:5381": [
|
||||||
[0, 0, 0, "Do not use any type assertions.", "0"],
|
[0, 0, 0, "Do not use any type assertions.", "0"],
|
||||||
@ -2504,9 +2503,6 @@ exports[`better eslint`] = {
|
|||||||
[0, 0, 0, "Do not use any type assertions.", "0"],
|
[0, 0, 0, "Do not use any type assertions.", "0"],
|
||||||
[0, 0, 0, "Unexpected any. Specify a different type.", "1"]
|
[0, 0, 0, "Unexpected any. Specify a different type.", "1"]
|
||||||
],
|
],
|
||||||
"public/app/features/explore/ExplorePage.tsx:5381": [
|
|
||||||
[0, 0, 0, "Do not use any type assertions.", "0"]
|
|
||||||
],
|
|
||||||
"public/app/features/explore/ExploreQueryInspector.tsx:5381": [
|
"public/app/features/explore/ExploreQueryInspector.tsx:5381": [
|
||||||
[0, 0, 0, "Do not use any type assertions.", "0"]
|
[0, 0, 0, "Do not use any type assertions.", "0"]
|
||||||
],
|
],
|
||||||
@ -2567,11 +2563,6 @@ exports[`better eslint`] = {
|
|||||||
[0, 0, 0, "Do not use any type assertions.", "0"],
|
[0, 0, 0, "Do not use any type assertions.", "0"],
|
||||||
[0, 0, 0, "Do not use any type assertions.", "1"]
|
[0, 0, 0, "Do not use any type assertions.", "1"]
|
||||||
],
|
],
|
||||||
"public/app/features/explore/hooks/useStateSync.ts:5381": [
|
|
||||||
[0, 0, 0, "Do not use any type assertions.", "0"],
|
|
||||||
[0, 0, 0, "Do not use any type assertions.", "1"],
|
|
||||||
[0, 0, 0, "Do not use any type assertions.", "2"]
|
|
||||||
],
|
|
||||||
"public/app/features/explore/spec/helper/setup.tsx:5381": [
|
"public/app/features/explore/spec/helper/setup.tsx:5381": [
|
||||||
[0, 0, 0, "Do not use any type assertions.", "0"],
|
[0, 0, 0, "Do not use any type assertions.", "0"],
|
||||||
[0, 0, 0, "Unexpected any. Specify a different type.", "1"]
|
[0, 0, 0, "Unexpected any. Specify a different type.", "1"]
|
||||||
@ -2582,24 +2573,9 @@ exports[`better eslint`] = {
|
|||||||
"public/app/features/explore/spec/queryHistory.test.tsx:5381": [
|
"public/app/features/explore/spec/queryHistory.test.tsx:5381": [
|
||||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
|
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
|
||||||
],
|
],
|
||||||
"public/app/features/explore/state/history.ts:5381": [
|
|
||||||
[0, 0, 0, "Do not use any type assertions.", "0"]
|
|
||||||
],
|
|
||||||
"public/app/features/explore/state/main.ts:5381": [
|
|
||||||
[0, 0, 0, "Do not use any type assertions.", "0"]
|
|
||||||
],
|
|
||||||
"public/app/features/explore/state/query.ts:5381": [
|
|
||||||
[0, 0, 0, "Do not use any type assertions.", "0"]
|
|
||||||
],
|
|
||||||
"public/app/features/explore/state/time.test.ts:5381": [
|
"public/app/features/explore/state/time.test.ts:5381": [
|
||||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
|
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
|
||||||
],
|
],
|
||||||
"public/app/features/explore/state/time.ts:5381": [
|
|
||||||
[0, 0, 0, "Do not use any type assertions.", "0"],
|
|
||||||
[0, 0, 0, "Do not use any type assertions.", "1"],
|
|
||||||
[0, 0, 0, "Do not use any type assertions.", "2"],
|
|
||||||
[0, 0, 0, "Do not use any type assertions.", "3"]
|
|
||||||
],
|
|
||||||
"public/app/features/explore/state/utils.ts:5381": [
|
"public/app/features/explore/state/utils.ts:5381": [
|
||||||
[0, 0, 0, "Do not use any type assertions.", "0"],
|
[0, 0, 0, "Do not use any type assertions.", "0"],
|
||||||
[0, 0, 0, "Unexpected any. Specify a different type.", "1"],
|
[0, 0, 0, "Unexpected any. Specify a different type.", "1"],
|
||||||
|
@ -28,34 +28,12 @@ e2e.scenario({
|
|||||||
|
|
||||||
cy.contains('CSV Metric Values').scrollIntoView().should('be.visible').click();
|
cy.contains('CSV Metric Values').scrollIntoView().should('be.visible').click();
|
||||||
|
|
||||||
cy.location().then((loc) => {
|
|
||||||
const params = new URLSearchParams(loc.search);
|
|
||||||
const leftJSON = JSON.parse(params.get('left'));
|
|
||||||
expect(leftJSON.range.to).to.equal('now');
|
|
||||||
expect(leftJSON.range.from).to.equal('now-1h');
|
|
||||||
|
|
||||||
cy.get('body').click();
|
|
||||||
cy.get('body').type('t{leftarrow}');
|
|
||||||
|
|
||||||
cy.location().should((locPostKeypress) => {
|
|
||||||
const params = new URLSearchParams(locPostKeypress.search);
|
|
||||||
const leftJSON = JSON.parse(params.get('left'));
|
|
||||||
// be sure the keypress affected the time window
|
|
||||||
expect(leftJSON.range.to).to.not.equal('now');
|
|
||||||
expect(leftJSON.range.from).to.not.equal('now-1h');
|
|
||||||
// be sure the url does not contain dashboard range values
|
|
||||||
// eslint wants this to be a function, so we use this instead of to.be.false
|
|
||||||
expect(params.has('to')).to.equal(false);
|
|
||||||
expect(params.has('from')).to.equal(false);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
const canvases = e2e().get('canvas');
|
const canvases = e2e().get('canvas');
|
||||||
canvases.should('have.length', 1);
|
canvases.should('have.length', 1);
|
||||||
|
|
||||||
// Both queries above should have been run and be shown in the query history
|
// Both queries above should have been run and be shown in the query history
|
||||||
e2e.components.QueryTab.queryHistoryButton().should('be.visible').click();
|
e2e.components.QueryTab.queryHistoryButton().should('be.visible').click();
|
||||||
e2e.components.QueryHistory.queryText().should('have.length', 2).should('contain', 'csv_metric_values');
|
e2e.components.QueryHistory.queryText().should('have.length', 1).should('contain', 'csv_metric_values');
|
||||||
|
|
||||||
// delete all queries
|
// delete all queries
|
||||||
cy.get('button[title="Delete query"]').each((button) => {
|
cy.get('button[title="Delete query"]').each((button) => {
|
||||||
|
@ -1,15 +1,15 @@
|
|||||||
|
import { DataQuery } from '@grafana/schema';
|
||||||
|
|
||||||
import { PreferredVisualisationType } from './data';
|
import { PreferredVisualisationType } from './data';
|
||||||
import { DataQuery } from './query';
|
|
||||||
import { RawTimeRange, TimeRange } from './time';
|
import { RawTimeRange, TimeRange } from './time';
|
||||||
|
|
||||||
type AnyQuery = DataQuery & Record<string, any>;
|
type AnyQuery = DataQuery & Record<string, any>;
|
||||||
|
|
||||||
/** @internal */
|
/** @internal */
|
||||||
export interface ExploreUrlState<T extends DataQuery = AnyQuery> {
|
export interface ExploreUrlState<T extends DataQuery = AnyQuery> {
|
||||||
datasource: string;
|
datasource: string | null;
|
||||||
queries: T[];
|
queries: T[];
|
||||||
range: RawTimeRange;
|
range: RawTimeRange;
|
||||||
context?: string;
|
|
||||||
panelsState?: ExplorePanelsState;
|
panelsState?: ExplorePanelsState;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2,15 +2,13 @@ import { dateTime, ExploreUrlState, LogsSortOrder } from '@grafana/data';
|
|||||||
import { serializeStateToUrlParam } from '@grafana/data/src/utils/url';
|
import { serializeStateToUrlParam } from '@grafana/data/src/utils/url';
|
||||||
import { RefreshPicker } from '@grafana/ui';
|
import { RefreshPicker } from '@grafana/ui';
|
||||||
import store from 'app/core/store';
|
import store from 'app/core/store';
|
||||||
import { ExploreId } from 'app/types';
|
import { DEFAULT_RANGE } from 'app/features/explore/state/utils';
|
||||||
|
|
||||||
import { DatasourceSrvMock, MockDataSourceApi } from '../../../test/mocks/datasource_srv';
|
import { DatasourceSrvMock, MockDataSourceApi } from '../../../test/mocks/datasource_srv';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
buildQueryTransaction,
|
buildQueryTransaction,
|
||||||
DEFAULT_RANGE,
|
|
||||||
hasNonEmptyQuery,
|
hasNonEmptyQuery,
|
||||||
parseUrlState,
|
|
||||||
refreshIntervalToSortOrder,
|
refreshIntervalToSortOrder,
|
||||||
updateHistory,
|
updateHistory,
|
||||||
getExploreUrl,
|
getExploreUrl,
|
||||||
@ -42,67 +40,6 @@ jest.mock('@grafana/runtime', () => ({
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
describe('state functions', () => {
|
describe('state functions', () => {
|
||||||
describe('parseUrlState', () => {
|
|
||||||
it('returns default state on empty string', () => {
|
|
||||||
expect(parseUrlState('')).toMatchObject({
|
|
||||||
datasource: null,
|
|
||||||
queries: [],
|
|
||||||
range: DEFAULT_RANGE,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('returns a valid Explore state from URL parameter', () => {
|
|
||||||
const paramValue = '{"datasource":"Local","queries":[{"expr":"metric"}],"range":{"from":"now-1h","to":"now"}}';
|
|
||||||
expect(parseUrlState(paramValue)).toMatchObject({
|
|
||||||
datasource: 'Local',
|
|
||||||
queries: [{ expr: 'metric' }],
|
|
||||||
range: {
|
|
||||||
from: 'now-1h',
|
|
||||||
to: 'now',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('returns a valid Explore state from a compact URL parameter', () => {
|
|
||||||
const paramValue = '["now-1h","now","Local",{"expr":"metric"},{"ui":[true,true,true,"none"]}]';
|
|
||||||
expect(parseUrlState(paramValue)).toMatchObject({
|
|
||||||
datasource: 'Local',
|
|
||||||
queries: [{ expr: 'metric' }],
|
|
||||||
range: {
|
|
||||||
from: 'now-1h',
|
|
||||||
to: 'now',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should not return a query for mode in the url', () => {
|
|
||||||
// Previous versions of Grafana included "Explore mode" in the URL; this should not be treated as a query.
|
|
||||||
const paramValue =
|
|
||||||
'["now-1h","now","x-ray-datasource",{"queryType":"getTraceSummaries"},{"mode":"Metrics"},{"ui":[true,true,true,"none"]}]';
|
|
||||||
expect(parseUrlState(paramValue)).toMatchObject({
|
|
||||||
datasource: 'x-ray-datasource',
|
|
||||||
queries: [{ queryType: 'getTraceSummaries' }],
|
|
||||||
range: {
|
|
||||||
from: 'now-1h',
|
|
||||||
to: 'now',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return queries if queryType is present in the url', () => {
|
|
||||||
const paramValue =
|
|
||||||
'["now-1h","now","x-ray-datasource",{"queryType":"getTraceSummaries"},{"ui":[true,true,true,"none"]}]';
|
|
||||||
expect(parseUrlState(paramValue)).toMatchObject({
|
|
||||||
datasource: 'x-ray-datasource',
|
|
||||||
queries: [{ queryType: 'getTraceSummaries' }],
|
|
||||||
range: {
|
|
||||||
from: 'now-1h',
|
|
||||||
to: 'now',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('serializeStateToUrlParam', () => {
|
describe('serializeStateToUrlParam', () => {
|
||||||
it('returns url parameter value for a state object', () => {
|
it('returns url parameter value for a state object', () => {
|
||||||
const state = {
|
const state = {
|
||||||
@ -130,61 +67,6 @@ describe('state functions', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('interplay', () => {
|
|
||||||
it('can parse the serialized state into the original state', () => {
|
|
||||||
const state = {
|
|
||||||
...DEFAULT_EXPLORE_STATE,
|
|
||||||
datasource: 'foo',
|
|
||||||
queries: [
|
|
||||||
{
|
|
||||||
expr: 'metric{test="a/b"}',
|
|
||||||
refId: 'A',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
expr: 'super{foo="x/z"}',
|
|
||||||
refId: 'B',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
range: {
|
|
||||||
from: 'now - 5h',
|
|
||||||
to: 'now',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
const serialized = serializeStateToUrlParam(state);
|
|
||||||
const parsed = parseUrlState(serialized);
|
|
||||||
expect(state).toMatchObject(parsed);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('can parse serialized panelsState into the original state', () => {
|
|
||||||
const state = {
|
|
||||||
...DEFAULT_EXPLORE_STATE,
|
|
||||||
datasource: 'foo',
|
|
||||||
queries: [
|
|
||||||
{
|
|
||||||
expr: 'metric{test="a/b"}',
|
|
||||||
refId: 'A',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
expr: 'super{foo="x/z"}',
|
|
||||||
refId: 'B',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
range: {
|
|
||||||
from: 'now - 5h',
|
|
||||||
to: 'now',
|
|
||||||
},
|
|
||||||
panelsState: {
|
|
||||||
trace: {
|
|
||||||
spanId: 'abcdef',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
const serialized = serializeStateToUrlParam(state);
|
|
||||||
const parsed = parseUrlState(serialized);
|
|
||||||
expect(state).toMatchObject(parsed);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('getExploreUrl', () => {
|
describe('getExploreUrl', () => {
|
||||||
@ -304,21 +186,21 @@ describe('when buildQueryTransaction', () => {
|
|||||||
const queries = [{ refId: 'A' }];
|
const queries = [{ refId: 'A' }];
|
||||||
const queryOptions = { maxDataPoints: 1000, minInterval: '15s' };
|
const queryOptions = { maxDataPoints: 1000, minInterval: '15s' };
|
||||||
const range = { from: dateTime().subtract(1, 'd'), to: dateTime(), raw: { from: '1h', to: '1h' } };
|
const range = { from: dateTime().subtract(1, 'd'), to: dateTime(), raw: { from: '1h', to: '1h' } };
|
||||||
const transaction = buildQueryTransaction(ExploreId.left, queries, queryOptions, range, false);
|
const transaction = buildQueryTransaction('left', queries, queryOptions, range, false);
|
||||||
expect(transaction.request.intervalMs).toEqual(60000);
|
expect(transaction.request.intervalMs).toEqual(60000);
|
||||||
});
|
});
|
||||||
it('it should calculate interval taking minInterval into account', () => {
|
it('it should calculate interval taking minInterval into account', () => {
|
||||||
const queries = [{ refId: 'A' }];
|
const queries = [{ refId: 'A' }];
|
||||||
const queryOptions = { maxDataPoints: 1000, minInterval: '15s' };
|
const queryOptions = { maxDataPoints: 1000, minInterval: '15s' };
|
||||||
const range = { from: dateTime().subtract(1, 'm'), to: dateTime(), raw: { from: '1h', to: '1h' } };
|
const range = { from: dateTime().subtract(1, 'm'), to: dateTime(), raw: { from: '1h', to: '1h' } };
|
||||||
const transaction = buildQueryTransaction(ExploreId.left, queries, queryOptions, range, false);
|
const transaction = buildQueryTransaction('left', queries, queryOptions, range, false);
|
||||||
expect(transaction.request.intervalMs).toEqual(15000);
|
expect(transaction.request.intervalMs).toEqual(15000);
|
||||||
});
|
});
|
||||||
it('it should calculate interval taking maxDataPoints into account', () => {
|
it('it should calculate interval taking maxDataPoints into account', () => {
|
||||||
const queries = [{ refId: 'A' }];
|
const queries = [{ refId: 'A' }];
|
||||||
const queryOptions = { maxDataPoints: 10, minInterval: '15s' };
|
const queryOptions = { maxDataPoints: 10, minInterval: '15s' };
|
||||||
const range = { from: dateTime().subtract(1, 'd'), to: dateTime(), raw: { from: '1h', to: '1h' } };
|
const range = { from: dateTime().subtract(1, 'd'), to: dateTime(), raw: { from: '1h', to: '1h' } };
|
||||||
const transaction = buildQueryTransaction(ExploreId.left, queries, queryOptions, range, false);
|
const transaction = buildQueryTransaction('left', queries, queryOptions, range, false);
|
||||||
expect(transaction.request.interval).toEqual('2h');
|
expect(transaction.request.interval).toEqual('2h');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import { nanoid } from '@reduxjs/toolkit';
|
||||||
import { omit } from 'lodash';
|
import { omit } from 'lodash';
|
||||||
import { Unsubscribable } from 'rxjs';
|
import { Unsubscribable } from 'rxjs';
|
||||||
import { v4 as uuidv4 } from 'uuid';
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
@ -26,17 +27,12 @@ import store from 'app/core/store';
|
|||||||
import { TimeSrv } from 'app/features/dashboard/services/TimeSrv';
|
import { TimeSrv } from 'app/features/dashboard/services/TimeSrv';
|
||||||
import { PanelModel } from 'app/features/dashboard/state';
|
import { PanelModel } from 'app/features/dashboard/state';
|
||||||
import { ExpressionDatasourceUID } from 'app/features/expressions/types';
|
import { ExpressionDatasourceUID } from 'app/features/expressions/types';
|
||||||
import { ExploreId, QueryOptions, QueryTransaction } from 'app/types/explore';
|
import { QueryOptions, QueryTransaction } from 'app/types/explore';
|
||||||
|
|
||||||
import { config } from '../config';
|
import { config } from '../config';
|
||||||
|
|
||||||
import { getNextRefIdChar } from './query';
|
import { getNextRefIdChar } from './query';
|
||||||
|
|
||||||
export const DEFAULT_RANGE = {
|
|
||||||
from: 'now-1h',
|
|
||||||
to: 'now',
|
|
||||||
};
|
|
||||||
|
|
||||||
export const DEFAULT_UI_STATE = {
|
export const DEFAULT_UI_STATE = {
|
||||||
dedupStrategy: LogsDedupStrategy.none,
|
dedupStrategy: LogsDedupStrategy.none,
|
||||||
};
|
};
|
||||||
@ -58,6 +54,10 @@ export interface GetExploreUrlArguments {
|
|||||||
timeSrv: TimeSrv;
|
timeSrv: TimeSrv;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function generateExploreId() {
|
||||||
|
return nanoid(3);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns an Explore-URL that contains a panel's queries and the dashboard time range.
|
* Returns an Explore-URL that contains a panel's queries and the dashboard time range.
|
||||||
*/
|
*/
|
||||||
@ -98,28 +98,26 @@ export async function getExploreUrl(args: GetExploreUrlArguments): Promise<strin
|
|||||||
const scopedVars = panel.scopedVars || {};
|
const scopedVars = panel.scopedVars || {};
|
||||||
state = {
|
state = {
|
||||||
...state,
|
...state,
|
||||||
datasource: exploreDatasource.name,
|
datasource: exploreDatasource.uid,
|
||||||
context: 'explore',
|
|
||||||
queries: exploreDatasource.interpolateVariablesInQueries(exploreTargets, scopedVars),
|
queries: exploreDatasource.interpolateVariablesInQueries(exploreTargets, scopedVars),
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
state = {
|
state = {
|
||||||
...state,
|
...state,
|
||||||
datasource: exploreDatasource.name,
|
datasource: exploreDatasource.uid,
|
||||||
context: 'explore',
|
|
||||||
queries: exploreTargets,
|
queries: exploreTargets,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const exploreState = JSON.stringify(state);
|
const exploreState = JSON.stringify({ [generateExploreId()]: state });
|
||||||
url = urlUtil.renderUrl('/explore', { left: exploreState });
|
url = urlUtil.renderUrl('/explore', { panes: exploreState, schemaVersion: 1 });
|
||||||
}
|
}
|
||||||
|
|
||||||
return url;
|
return url;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function buildQueryTransaction(
|
export function buildQueryTransaction(
|
||||||
exploreId: ExploreId,
|
exploreId: string,
|
||||||
queries: DataQuery[],
|
queries: DataQuery[],
|
||||||
queryOptions: QueryOptions,
|
queryOptions: QueryOptions,
|
||||||
range: TimeRange,
|
range: TimeRange,
|
||||||
@ -172,16 +170,6 @@ export function buildQueryTransaction(
|
|||||||
|
|
||||||
export const clearQueryKeys: (query: DataQuery) => DataQuery = ({ key, ...rest }) => rest;
|
export const clearQueryKeys: (query: DataQuery) => DataQuery = ({ key, ...rest }) => rest;
|
||||||
|
|
||||||
const isSegment = (segment: { [key: string]: string }, ...props: string[]) =>
|
|
||||||
props.some((prop) => segment.hasOwnProperty(prop));
|
|
||||||
|
|
||||||
enum ParseUrlStateIndex {
|
|
||||||
RangeFrom = 0,
|
|
||||||
RangeTo = 1,
|
|
||||||
Datasource = 2,
|
|
||||||
SegmentsStart = 3,
|
|
||||||
}
|
|
||||||
|
|
||||||
export const safeParseJson = (text?: string): any | undefined => {
|
export const safeParseJson = (text?: string): any | undefined => {
|
||||||
if (!text) {
|
if (!text) {
|
||||||
return;
|
return;
|
||||||
@ -208,40 +196,6 @@ export const safeStringifyValue = (value: unknown, space?: number) => {
|
|||||||
return '';
|
return '';
|
||||||
};
|
};
|
||||||
|
|
||||||
export function parseUrlState(initial: string | undefined): ExploreUrlState {
|
|
||||||
const parsed = safeParseJson(initial);
|
|
||||||
const errorResult: any = {
|
|
||||||
datasource: null,
|
|
||||||
queries: [],
|
|
||||||
range: DEFAULT_RANGE,
|
|
||||||
mode: null,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!parsed) {
|
|
||||||
return errorResult;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!Array.isArray(parsed)) {
|
|
||||||
return { queries: [], range: DEFAULT_RANGE, ...parsed };
|
|
||||||
}
|
|
||||||
|
|
||||||
if (parsed.length <= ParseUrlStateIndex.SegmentsStart) {
|
|
||||||
console.error('Error parsing compact URL state for Explore.');
|
|
||||||
return errorResult;
|
|
||||||
}
|
|
||||||
|
|
||||||
const range = {
|
|
||||||
from: parsed[ParseUrlStateIndex.RangeFrom],
|
|
||||||
to: parsed[ParseUrlStateIndex.RangeTo],
|
|
||||||
};
|
|
||||||
const datasource = parsed[ParseUrlStateIndex.Datasource];
|
|
||||||
const parsedSegments = parsed.slice(ParseUrlStateIndex.SegmentsStart);
|
|
||||||
const queries = parsedSegments.filter((segment) => !isSegment(segment, 'ui', 'mode', '__panelsState'));
|
|
||||||
|
|
||||||
const panelsState = parsedSegments.find((segment) => isSegment(segment, '__panelsState'))?.__panelsState;
|
|
||||||
return { datasource, queries, range, panelsState };
|
|
||||||
}
|
|
||||||
|
|
||||||
export function generateKey(index = 0): string {
|
export function generateKey(index = 0): string {
|
||||||
return `Q-${uuidv4()}-${index}`;
|
return `Q-${uuidv4()}-${index}`;
|
||||||
}
|
}
|
||||||
|
@ -134,7 +134,6 @@ export const createUrlFromRichHistory = (query: RichHistoryQuery) => {
|
|||||||
range: { from: 'now-1h', to: 'now' },
|
range: { from: 'now-1h', to: 'now' },
|
||||||
datasource: query.datasourceName,
|
datasource: query.datasourceName,
|
||||||
queries: query.queries,
|
queries: query.queries,
|
||||||
context: 'explore',
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const serializedState = serializeStateToUrlParam(exploreState);
|
const serializedState = serializeStateToUrlParam(exploreState);
|
||||||
|
@ -8,7 +8,7 @@ import { Alert, Button, Field, InputControl, Modal, RadioButtonGroup } from '@gr
|
|||||||
import { DashboardPicker } from 'app/core/components/Select/DashboardPicker';
|
import { DashboardPicker } from 'app/core/components/Select/DashboardPicker';
|
||||||
import { contextSrv } from 'app/core/services/context_srv';
|
import { contextSrv } from 'app/core/services/context_srv';
|
||||||
import { removeDashboardToFetchFromLocalStorage } from 'app/features/dashboard/state/initDashboard';
|
import { removeDashboardToFetchFromLocalStorage } from 'app/features/dashboard/state/initDashboard';
|
||||||
import { ExploreId, AccessControlAction, useSelector } from 'app/types';
|
import { AccessControlAction, useSelector } from 'app/types';
|
||||||
|
|
||||||
import { getExploreItemSelector } from '../state/selectors';
|
import { getExploreItemSelector } from '../state/selectors';
|
||||||
|
|
||||||
@ -57,7 +57,7 @@ interface SubmissionError {
|
|||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
exploreId: ExploreId;
|
exploreId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const AddToDashboardModal = ({ onClose, exploreId }: Props) => {
|
export const AddToDashboardModal = ({ onClose, exploreId }: Props) => {
|
||||||
|
@ -11,7 +11,7 @@ import { Echo } from 'app/core/services/echo/Echo';
|
|||||||
import * as initDashboard from 'app/features/dashboard/state/initDashboard';
|
import * as initDashboard from 'app/features/dashboard/state/initDashboard';
|
||||||
import { DashboardSearchItemType } from 'app/features/search/types';
|
import { DashboardSearchItemType } from 'app/features/search/types';
|
||||||
import { configureStore } from 'app/store/configureStore';
|
import { configureStore } from 'app/store/configureStore';
|
||||||
import { ExploreId, ExploreState } from 'app/types';
|
import { ExploreState } from 'app/types';
|
||||||
|
|
||||||
import { createEmptyQueryResponse } from '../state/utils';
|
import { createEmptyQueryResponse } from '../state/utils';
|
||||||
|
|
||||||
@ -52,7 +52,7 @@ describe('AddToDashboardButton', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('Is disabled if explore pane has no queries', async () => {
|
it('Is disabled if explore pane has no queries', async () => {
|
||||||
setup(<AddToDashboard exploreId={ExploreId.left} />, []);
|
setup(<AddToDashboard exploreId={'left'} />, []);
|
||||||
|
|
||||||
const button = await screen.findByRole('button', { name: /add to dashboard/i });
|
const button = await screen.findByRole('button', { name: /add to dashboard/i });
|
||||||
expect(button).toBeDisabled();
|
expect(button).toBeDisabled();
|
||||||
@ -81,7 +81,7 @@ describe('AddToDashboardButton', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('Opens and closes the modal correctly', async () => {
|
it('Opens and closes the modal correctly', async () => {
|
||||||
setup(<AddToDashboard exploreId={ExploreId.left} />);
|
setup(<AddToDashboard exploreId={'left'} />);
|
||||||
|
|
||||||
await openModal();
|
await openModal();
|
||||||
|
|
||||||
@ -96,7 +96,7 @@ describe('AddToDashboardButton', () => {
|
|||||||
const openSpy = jest.spyOn(global, 'open').mockReturnValue(true);
|
const openSpy = jest.spyOn(global, 'open').mockReturnValue(true);
|
||||||
const pushSpy = jest.spyOn(locationService, 'push');
|
const pushSpy = jest.spyOn(locationService, 'push');
|
||||||
|
|
||||||
setup(<AddToDashboard exploreId={ExploreId.left} />);
|
setup(<AddToDashboard exploreId={'left'} />);
|
||||||
|
|
||||||
await openModal();
|
await openModal();
|
||||||
|
|
||||||
@ -115,7 +115,7 @@ describe('AddToDashboardButton', () => {
|
|||||||
const openSpy = jest.spyOn(global, 'open').mockReturnValue(true);
|
const openSpy = jest.spyOn(global, 'open').mockReturnValue(true);
|
||||||
const pushSpy = jest.spyOn(locationService, 'push');
|
const pushSpy = jest.spyOn(locationService, 'push');
|
||||||
|
|
||||||
setup(<AddToDashboard exploreId={ExploreId.left} />);
|
setup(<AddToDashboard exploreId={'left'} />);
|
||||||
|
|
||||||
await openModal();
|
await openModal();
|
||||||
|
|
||||||
@ -134,7 +134,7 @@ describe('AddToDashboardButton', () => {
|
|||||||
// @ts-expect-error global.open should return a Window, but is not implemented in js-dom.
|
// @ts-expect-error global.open should return a Window, but is not implemented in js-dom.
|
||||||
const openSpy = jest.spyOn(global, 'open').mockReturnValue(true);
|
const openSpy = jest.spyOn(global, 'open').mockReturnValue(true);
|
||||||
|
|
||||||
setup(<AddToDashboard exploreId={ExploreId.left} />);
|
setup(<AddToDashboard exploreId={'left'} />);
|
||||||
|
|
||||||
await openModal();
|
await openModal();
|
||||||
|
|
||||||
@ -148,7 +148,7 @@ describe('AddToDashboardButton', () => {
|
|||||||
it('Navigates to the new dashboard', async () => {
|
it('Navigates to the new dashboard', async () => {
|
||||||
const pushSpy = jest.spyOn(locationService, 'push');
|
const pushSpy = jest.spyOn(locationService, 'push');
|
||||||
|
|
||||||
setup(<AddToDashboard exploreId={ExploreId.left} />);
|
setup(<AddToDashboard exploreId={'left'} />);
|
||||||
|
|
||||||
await openModal();
|
await openModal();
|
||||||
|
|
||||||
@ -165,7 +165,7 @@ describe('AddToDashboardButton', () => {
|
|||||||
|
|
||||||
describe('Save to existing dashboard', () => {
|
describe('Save to existing dashboard', () => {
|
||||||
it('Renders the dashboard picker when switching to "Existing Dashboard"', async () => {
|
it('Renders the dashboard picker when switching to "Existing Dashboard"', async () => {
|
||||||
setup(<AddToDashboard exploreId={ExploreId.left} />);
|
setup(<AddToDashboard exploreId={'left'} />);
|
||||||
|
|
||||||
await openModal();
|
await openModal();
|
||||||
|
|
||||||
@ -178,7 +178,7 @@ describe('AddToDashboardButton', () => {
|
|||||||
it('Does not submit if no dashboard is selected', async () => {
|
it('Does not submit if no dashboard is selected', async () => {
|
||||||
locationService.push = jest.fn();
|
locationService.push = jest.fn();
|
||||||
|
|
||||||
setup(<AddToDashboard exploreId={ExploreId.left} />);
|
setup(<AddToDashboard exploreId={'left'} />);
|
||||||
|
|
||||||
await openModal();
|
await openModal();
|
||||||
|
|
||||||
@ -211,7 +211,7 @@ describe('AddToDashboardButton', () => {
|
|||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
setup(<AddToDashboard exploreId={ExploreId.left} />);
|
setup(<AddToDashboard exploreId={'left'} />);
|
||||||
|
|
||||||
await openModal();
|
await openModal();
|
||||||
|
|
||||||
@ -252,7 +252,7 @@ describe('AddToDashboardButton', () => {
|
|||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
setup(<AddToDashboard exploreId={ExploreId.left} />);
|
setup(<AddToDashboard exploreId={'left'} />);
|
||||||
|
|
||||||
await openModal();
|
await openModal();
|
||||||
|
|
||||||
@ -290,7 +290,7 @@ describe('AddToDashboardButton', () => {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
setup(<AddToDashboard exploreId={ExploreId.left} />);
|
setup(<AddToDashboard exploreId={'left'} />);
|
||||||
await openModal('Add panel to existing dashboard');
|
await openModal('Add panel to existing dashboard');
|
||||||
expect(screen.queryByRole('radio')).not.toBeInTheDocument();
|
expect(screen.queryByRole('radio')).not.toBeInTheDocument();
|
||||||
});
|
});
|
||||||
@ -303,7 +303,7 @@ describe('AddToDashboardButton', () => {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
setup(<AddToDashboard exploreId={ExploreId.left} />);
|
setup(<AddToDashboard exploreId={'left'} />);
|
||||||
await openModal('Add panel to new dashboard');
|
await openModal('Add panel to new dashboard');
|
||||||
expect(screen.queryByRole('radio')).not.toBeInTheDocument();
|
expect(screen.queryByRole('radio')).not.toBeInTheDocument();
|
||||||
});
|
});
|
||||||
@ -322,7 +322,7 @@ describe('AddToDashboardButton', () => {
|
|||||||
jest.spyOn(global, 'open').mockReturnValue(null);
|
jest.spyOn(global, 'open').mockReturnValue(null);
|
||||||
const removeDashboardSpy = jest.spyOn(initDashboard, 'removeDashboardToFetchFromLocalStorage');
|
const removeDashboardSpy = jest.spyOn(initDashboard, 'removeDashboardToFetchFromLocalStorage');
|
||||||
|
|
||||||
setup(<AddToDashboard exploreId={ExploreId.left} />);
|
setup(<AddToDashboard exploreId={'left'} />);
|
||||||
|
|
||||||
await openModal();
|
await openModal();
|
||||||
expect(screen.queryByRole('alert')).not.toBeInTheDocument();
|
expect(screen.queryByRole('alert')).not.toBeInTheDocument();
|
||||||
@ -341,7 +341,7 @@ describe('AddToDashboardButton', () => {
|
|||||||
throw 'SOME ERROR';
|
throw 'SOME ERROR';
|
||||||
});
|
});
|
||||||
|
|
||||||
setup(<AddToDashboard exploreId={ExploreId.left} />);
|
setup(<AddToDashboard exploreId={'left'} />);
|
||||||
|
|
||||||
await openModal();
|
await openModal();
|
||||||
expect(screen.queryByRole('alert')).not.toBeInTheDocument();
|
expect(screen.queryByRole('alert')).not.toBeInTheDocument();
|
||||||
@ -367,7 +367,7 @@ describe('AddToDashboardButton', () => {
|
|||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
setup(<AddToDashboard exploreId={ExploreId.left} />);
|
setup(<AddToDashboard exploreId={'left'} />);
|
||||||
|
|
||||||
await openModal();
|
await openModal();
|
||||||
expect(screen.queryByRole('alert')).not.toBeInTheDocument();
|
expect(screen.queryByRole('alert')).not.toBeInTheDocument();
|
||||||
@ -391,7 +391,7 @@ describe('AddToDashboardButton', () => {
|
|||||||
it('Shows an error if an unknown error happens', async () => {
|
it('Shows an error if an unknown error happens', async () => {
|
||||||
jest.spyOn(api, 'setDashboardInLocalStorage').mockRejectedValue('SOME ERROR');
|
jest.spyOn(api, 'setDashboardInLocalStorage').mockRejectedValue('SOME ERROR');
|
||||||
|
|
||||||
setup(<AddToDashboard exploreId={ExploreId.left} />);
|
setup(<AddToDashboard exploreId={'left'} />);
|
||||||
|
|
||||||
await openModal();
|
await openModal();
|
||||||
expect(screen.queryByRole('alert')).not.toBeInTheDocument();
|
expect(screen.queryByRole('alert')).not.toBeInTheDocument();
|
||||||
|
@ -1,14 +1,14 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
|
|
||||||
import { ToolbarButton } from '@grafana/ui';
|
import { ToolbarButton } from '@grafana/ui';
|
||||||
import { ExploreId, useSelector } from 'app/types';
|
import { useSelector } from 'app/types';
|
||||||
|
|
||||||
import { getExploreItemSelector } from '../state/selectors';
|
import { getExploreItemSelector } from '../state/selectors';
|
||||||
|
|
||||||
import { AddToDashboardModal } from './AddToDashboardModal';
|
import { AddToDashboardModal } from './AddToDashboardModal';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
exploreId: ExploreId;
|
exploreId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const AddToDashboard = ({ exploreId }: Props) => {
|
export const AddToDashboard = ({ exploreId }: Props) => {
|
||||||
|
@ -6,7 +6,6 @@ import { TestProvider } from 'test/helpers/TestProvider';
|
|||||||
import { DataSourceApi, LoadingState, CoreApp, createTheme, EventBusSrv } from '@grafana/data';
|
import { DataSourceApi, LoadingState, CoreApp, createTheme, EventBusSrv } from '@grafana/data';
|
||||||
import { selectors } from '@grafana/e2e-selectors';
|
import { selectors } from '@grafana/e2e-selectors';
|
||||||
import { configureStore } from 'app/store/configureStore';
|
import { configureStore } from 'app/store/configureStore';
|
||||||
import { ExploreId } from 'app/types';
|
|
||||||
|
|
||||||
import { Explore, Props } from './Explore';
|
import { Explore, Props } from './Explore';
|
||||||
import { initialExploreState } from './state/main';
|
import { initialExploreState } from './state/main';
|
||||||
@ -61,7 +60,7 @@ const dummyProps: Props = {
|
|||||||
QueryEditorHelp: {},
|
QueryEditorHelp: {},
|
||||||
},
|
},
|
||||||
} as DataSourceApi,
|
} as DataSourceApi,
|
||||||
exploreId: ExploreId.left,
|
exploreId: 'left',
|
||||||
loading: false,
|
loading: false,
|
||||||
modifyQueries: jest.fn(),
|
modifyQueries: jest.fn(),
|
||||||
scanStart: jest.fn(),
|
scanStart: jest.fn(),
|
||||||
|
@ -34,7 +34,6 @@ import { MIXED_DATASOURCE_NAME } from 'app/plugins/datasource/mixed/MixedDataSou
|
|||||||
import { getNodeGraphDataFrames } from 'app/plugins/panel/nodeGraph/utils';
|
import { getNodeGraphDataFrames } from 'app/plugins/panel/nodeGraph/utils';
|
||||||
import { StoreState } from 'app/types';
|
import { StoreState } from 'app/types';
|
||||||
import { AbsoluteTimeEvent } from 'app/types/events';
|
import { AbsoluteTimeEvent } from 'app/types/events';
|
||||||
import { ExploreId } from 'app/types/explore';
|
|
||||||
|
|
||||||
import { getTimeZone } from '../profile/state/selectors';
|
import { getTimeZone } from '../profile/state/selectors';
|
||||||
|
|
||||||
@ -98,7 +97,7 @@ const getStyles = (theme: GrafanaTheme2) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export interface ExploreProps extends Themeable2 {
|
export interface ExploreProps extends Themeable2 {
|
||||||
exploreId: ExploreId;
|
exploreId: string;
|
||||||
theme: GrafanaTheme2;
|
theme: GrafanaTheme2;
|
||||||
eventBus: EventBus;
|
eventBus: EventBus;
|
||||||
}
|
}
|
||||||
|
@ -1,24 +1,22 @@
|
|||||||
import { useRegisterActions, useKBar, Action, Priority } from 'kbar';
|
import { useRegisterActions, useKBar, Action, Priority } from 'kbar';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
import { ExploreId, useDispatch, useSelector } from 'app/types';
|
import { useDispatch, useSelector } from 'app/types';
|
||||||
|
|
||||||
import { splitOpen, splitClose } from './state/main';
|
import { splitOpen, splitClose } from './state/main';
|
||||||
import { runQueries } from './state/query';
|
import { runQueries } from './state/query';
|
||||||
import { isSplit } from './state/selectors';
|
import { isSplit, selectPanes } from './state/selectors';
|
||||||
|
|
||||||
interface Props {
|
// FIXME: this should use the new IDs
|
||||||
exploreIdLeft: ExploreId;
|
export const ExploreActions = () => {
|
||||||
exploreIdRight?: ExploreId;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const ExploreActions = ({ exploreIdLeft, exploreIdRight }: Props) => {
|
|
||||||
const [actions, setActions] = useState<Action[]>([]);
|
const [actions, setActions] = useState<Action[]>([]);
|
||||||
const { query } = useKBar();
|
const { query } = useKBar();
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
|
const panes = useSelector(selectPanes);
|
||||||
const splitted = useSelector(isSplit);
|
const splitted = useSelector(isSplit);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
const keys = Object.keys(panes);
|
||||||
const exploreSection = {
|
const exploreSection = {
|
||||||
name: 'Explore',
|
name: 'Explore',
|
||||||
priority: Priority.HIGH + 1,
|
priority: Priority.HIGH + 1,
|
||||||
@ -32,18 +30,18 @@ export const ExploreActions = ({ exploreIdLeft, exploreIdRight }: Props) => {
|
|||||||
name: 'Run query (left)',
|
name: 'Run query (left)',
|
||||||
keywords: 'query left',
|
keywords: 'query left',
|
||||||
perform: () => {
|
perform: () => {
|
||||||
dispatch(runQueries({ exploreId: exploreIdLeft }));
|
dispatch(runQueries({ exploreId: keys[0] }));
|
||||||
},
|
},
|
||||||
section: exploreSection,
|
section: exploreSection,
|
||||||
});
|
});
|
||||||
if (exploreIdRight) {
|
if ([panes[1]]) {
|
||||||
// we should always have the right exploreId if split
|
// we should always have the right exploreId if split
|
||||||
actionsArr.push({
|
actionsArr.push({
|
||||||
id: 'explore/run-query-right',
|
id: 'explore/run-query-right',
|
||||||
name: 'Run query (right)',
|
name: 'Run query (right)',
|
||||||
keywords: 'query right',
|
keywords: 'query right',
|
||||||
perform: () => {
|
perform: () => {
|
||||||
dispatch(runQueries({ exploreId: exploreIdRight }));
|
dispatch(runQueries({ exploreId: keys[1] }));
|
||||||
},
|
},
|
||||||
section: exploreSection,
|
section: exploreSection,
|
||||||
});
|
});
|
||||||
@ -52,7 +50,7 @@ export const ExploreActions = ({ exploreIdLeft, exploreIdRight }: Props) => {
|
|||||||
name: 'Close split view left',
|
name: 'Close split view left',
|
||||||
keywords: 'split',
|
keywords: 'split',
|
||||||
perform: () => {
|
perform: () => {
|
||||||
dispatch(splitClose(exploreIdLeft));
|
dispatch(splitClose(keys[0]));
|
||||||
},
|
},
|
||||||
section: exploreSection,
|
section: exploreSection,
|
||||||
});
|
});
|
||||||
@ -61,7 +59,7 @@ export const ExploreActions = ({ exploreIdLeft, exploreIdRight }: Props) => {
|
|||||||
name: 'Close split view right',
|
name: 'Close split view right',
|
||||||
keywords: 'split',
|
keywords: 'split',
|
||||||
perform: () => {
|
perform: () => {
|
||||||
dispatch(splitClose(exploreIdRight));
|
dispatch(splitClose(keys[1]));
|
||||||
},
|
},
|
||||||
section: exploreSection,
|
section: exploreSection,
|
||||||
});
|
});
|
||||||
@ -72,7 +70,7 @@ export const ExploreActions = ({ exploreIdLeft, exploreIdRight }: Props) => {
|
|||||||
name: 'Run query',
|
name: 'Run query',
|
||||||
keywords: 'query',
|
keywords: 'query',
|
||||||
perform: () => {
|
perform: () => {
|
||||||
dispatch(runQueries({ exploreId: exploreIdLeft }));
|
dispatch(runQueries({ exploreId: keys[0] }));
|
||||||
},
|
},
|
||||||
section: exploreSection,
|
section: exploreSection,
|
||||||
});
|
});
|
||||||
@ -87,7 +85,7 @@ export const ExploreActions = ({ exploreIdLeft, exploreIdRight }: Props) => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
setActions(actionsArr);
|
setActions(actionsArr);
|
||||||
}, [exploreIdLeft, exploreIdRight, splitted, query, dispatch]);
|
}, [panes, splitted, query, dispatch]);
|
||||||
|
|
||||||
useRegisterActions(!query ? [] : actions, [actions, query]);
|
useRegisterActions(!query ? [] : actions, [actions, query]);
|
||||||
|
|
||||||
|
@ -4,8 +4,6 @@ import React, { ComponentProps } from 'react';
|
|||||||
import AutoSizer from 'react-virtualized-auto-sizer';
|
import AutoSizer from 'react-virtualized-auto-sizer';
|
||||||
|
|
||||||
import { serializeStateToUrlParam } from '@grafana/data';
|
import { serializeStateToUrlParam } from '@grafana/data';
|
||||||
import { config } from '@grafana/runtime';
|
|
||||||
import { ExploreId } from 'app/types';
|
|
||||||
|
|
||||||
import { makeLogsQueryResponse } from './spec/helper/query';
|
import { makeLogsQueryResponse } from './spec/helper/query';
|
||||||
import { setupExplore, tearDown, waitForExplore } from './spec/helper/setup';
|
import { setupExplore, tearDown, waitForExplore } from './spec/helper/setup';
|
||||||
@ -101,7 +99,7 @@ describe('ExplorePage', () => {
|
|||||||
orgId: '1',
|
orgId: '1',
|
||||||
};
|
};
|
||||||
|
|
||||||
const { datasources, location } = setupExplore({ urlParams });
|
const { datasources } = setupExplore({ urlParams });
|
||||||
jest.mocked(datasources.loki.query).mockReturnValueOnce(makeLogsQueryResponse());
|
jest.mocked(datasources.loki.query).mockReturnValueOnce(makeLogsQueryResponse());
|
||||||
jest.mocked(datasources.elastic.query).mockReturnValueOnce(makeLogsQueryResponse());
|
jest.mocked(datasources.elastic.query).mockReturnValueOnce(makeLogsQueryResponse());
|
||||||
|
|
||||||
@ -119,9 +117,6 @@ describe('ExplorePage', () => {
|
|||||||
expect(screen.getByText(`loki Editor input: { label="value"}`)).toBeInTheDocument();
|
expect(screen.getByText(`loki Editor input: { label="value"}`)).toBeInTheDocument();
|
||||||
expect(screen.getByText(`elastic Editor input: error`)).toBeInTheDocument();
|
expect(screen.getByText(`elastic Editor input: error`)).toBeInTheDocument();
|
||||||
|
|
||||||
// We did not change the url
|
|
||||||
expect(location.getSearchObject()).toEqual(expect.objectContaining(urlParams));
|
|
||||||
|
|
||||||
// We called the data source query method once
|
// We called the data source query method once
|
||||||
expect(datasources.loki.query).toBeCalledTimes(1);
|
expect(datasources.loki.query).toBeCalledTimes(1);
|
||||||
expect(jest.mocked(datasources.loki.query).mock.calls[0][0]).toMatchObject({
|
expect(jest.mocked(datasources.loki.query).mock.calls[0][0]).toMatchObject({
|
||||||
@ -155,31 +150,6 @@ describe('ExplorePage', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Reacts to URL changes and opens a pane if an entry is pushed to history', async () => {
|
|
||||||
const urlParams = {
|
|
||||||
left: JSON.stringify(['now-1h', 'now', 'loki', { expr: '{ label="value"}' }]),
|
|
||||||
};
|
|
||||||
const { datasources, location } = setupExplore({ urlParams });
|
|
||||||
jest.mocked(datasources.loki.query).mockReturnValue(makeLogsQueryResponse());
|
|
||||||
jest.mocked(datasources.elastic.query).mockReturnValue(makeLogsQueryResponse());
|
|
||||||
|
|
||||||
await waitFor(() => {
|
|
||||||
expect(screen.getByText(`loki Editor input: { label="value"}`)).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
act(() => {
|
|
||||||
location.partial({
|
|
||||||
left: JSON.stringify(['now-1h', 'now', 'loki', { expr: '{ label="value"}' }]),
|
|
||||||
right: JSON.stringify(['now-1h', 'now', 'elastic', { expr: 'error' }]),
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
await waitFor(() => {
|
|
||||||
expect(screen.getByText(`loki Editor input: { label="value"}`)).toBeInTheDocument();
|
|
||||||
expect(screen.getByText(`elastic Editor input: error`)).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('handles opening split with split open func', async () => {
|
it('handles opening split with split open func', async () => {
|
||||||
const urlParams = {
|
const urlParams = {
|
||||||
left: JSON.stringify(['now-1h', 'now', 'loki', { expr: '{ label="value"}' }]),
|
left: JSON.stringify(['now-1h', 'now', 'loki', { expr: '{ label="value"}' }]),
|
||||||
@ -207,7 +177,7 @@ describe('ExplorePage', () => {
|
|||||||
|
|
||||||
const splitButton = await screen.findByText(/split/i);
|
const splitButton = await screen.findByText(/split/i);
|
||||||
await userEvent.click(splitButton);
|
await userEvent.click(splitButton);
|
||||||
await waitForExplore(ExploreId.left);
|
await waitForExplore('left');
|
||||||
|
|
||||||
expect(await screen.findAllByLabelText('Widen pane')).toHaveLength(2);
|
expect(await screen.findAllByLabelText('Widen pane')).toHaveLength(2);
|
||||||
expect(screen.queryByLabelText('Narrow pane')).not.toBeInTheDocument();
|
expect(screen.queryByLabelText('Narrow pane')).not.toBeInTheDocument();
|
||||||
@ -226,276 +196,4 @@ describe('ExplorePage', () => {
|
|||||||
expect(await screen.findAllByLabelText('Narrow pane')).toHaveLength(1);
|
expect(await screen.findAllByLabelText('Narrow pane')).toHaveLength(1);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Handles document title changes', () => {
|
|
||||||
it('changes the document title of the explore page to include the datasource in use', async () => {
|
|
||||||
const urlParams = {
|
|
||||||
left: JSON.stringify(['now-1h', 'now', 'loki', { expr: '{ label="value"}' }]),
|
|
||||||
};
|
|
||||||
const { datasources } = setupExplore({ urlParams });
|
|
||||||
jest.mocked(datasources.loki.query).mockReturnValue(makeLogsQueryResponse());
|
|
||||||
// This is mainly to wait for render so that the left pane state is initialized as that is needed for the title
|
|
||||||
// to include the datasource
|
|
||||||
await screen.findByText(`loki Editor input: { label="value"}`);
|
|
||||||
|
|
||||||
await waitFor(() => expect(document.title).toEqual('Explore - loki - Grafana'));
|
|
||||||
});
|
|
||||||
|
|
||||||
it('changes the document title to include the two datasources in use in split view mode', async () => {
|
|
||||||
const urlParams = {
|
|
||||||
left: JSON.stringify(['now-1h', 'now', 'loki', { expr: '{ label="value"}' }]),
|
|
||||||
};
|
|
||||||
const { datasources, store } = setupExplore({ urlParams });
|
|
||||||
jest.mocked(datasources.loki.query).mockReturnValue(makeLogsQueryResponse());
|
|
||||||
jest.mocked(datasources.elastic.query).mockReturnValue(makeLogsQueryResponse());
|
|
||||||
|
|
||||||
// This is mainly to wait for render so that the left pane state is initialized as that is needed for splitOpen
|
|
||||||
// to work
|
|
||||||
await screen.findByText(`loki Editor input: { label="value"}`);
|
|
||||||
|
|
||||||
act(() => {
|
|
||||||
store.dispatch(mainState.splitOpen({ datasourceUid: 'elastic', query: { expr: 'error', refId: 'A' } }));
|
|
||||||
});
|
|
||||||
await waitFor(() => expect(document.title).toEqual('Explore - loki | elastic - Grafana'));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('Handles different URL datasource redirects', () => {
|
|
||||||
describe('exploreMixedDatasource on', () => {
|
|
||||||
beforeAll(() => {
|
|
||||||
config.featureToggles.exploreMixedDatasource = true;
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('When root datasource is not specified in the URL', () => {
|
|
||||||
it('Redirects to default datasource', async () => {
|
|
||||||
const { location } = setupExplore({ mixedEnabled: true });
|
|
||||||
await waitForExplore();
|
|
||||||
|
|
||||||
await waitFor(() => {
|
|
||||||
const urlParams = decodeURIComponent(location.getSearch().toString());
|
|
||||||
|
|
||||||
expect(urlParams).toBe(
|
|
||||||
'left={"datasource":"loki-uid","queries":[{"refId":"A","datasource":{"type":"logs","uid":"loki-uid"}}],"range":{"from":"now-1h","to":"now"}}&orgId=1'
|
|
||||||
);
|
|
||||||
});
|
|
||||||
expect(location.getHistory()).toHaveLength(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('Redirects to last used datasource when available', async () => {
|
|
||||||
const { location } = setupExplore({
|
|
||||||
prevUsedDatasource: { orgId: 1, datasource: 'elastic-uid' },
|
|
||||||
mixedEnabled: true,
|
|
||||||
});
|
|
||||||
await waitForExplore();
|
|
||||||
|
|
||||||
await waitFor(() => {
|
|
||||||
const urlParams = decodeURIComponent(location.getSearch().toString());
|
|
||||||
expect(urlParams).toBe(
|
|
||||||
'left={"datasource":"elastic-uid","queries":[{"refId":"A","datasource":{"type":"logs","uid":"elastic-uid"}}],"range":{"from":"now-1h","to":"now"}}&orgId=1'
|
|
||||||
);
|
|
||||||
});
|
|
||||||
expect(location.getHistory()).toHaveLength(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Redirects to first query's datasource", async () => {
|
|
||||||
const { location } = setupExplore({
|
|
||||||
urlParams: {
|
|
||||||
left: '{"queries":[{"refId":"A","datasource":{"type":"logs","uid":"loki-uid"}}],"range":{"from":"now-1h","to":"now"}}',
|
|
||||||
},
|
|
||||||
prevUsedDatasource: { orgId: 1, datasource: 'elastic' },
|
|
||||||
mixedEnabled: true,
|
|
||||||
});
|
|
||||||
await waitForExplore();
|
|
||||||
|
|
||||||
await waitFor(() => {
|
|
||||||
const urlParams = decodeURIComponent(location.getSearch().toString());
|
|
||||||
expect(urlParams).toBe(
|
|
||||||
'left={"datasource":"loki-uid","queries":[{"refId":"A","datasource":{"type":"logs","uid":"loki-uid"}}],"range":{"from":"now-1h","to":"now"}}&orgId=1'
|
|
||||||
);
|
|
||||||
});
|
|
||||||
expect(location.getHistory()).toHaveLength(1);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('When root datasource is specified in the URL', () => {
|
|
||||||
it('Uses the datasource in the URL', async () => {
|
|
||||||
const { location } = setupExplore({
|
|
||||||
urlParams: {
|
|
||||||
left: '{"datasource":"elastic-uid","queries":[{"refId":"A"}],"range":{"from":"now-1h","to":"now"}}',
|
|
||||||
},
|
|
||||||
prevUsedDatasource: { orgId: 1, datasource: 'elastic' },
|
|
||||||
mixedEnabled: true,
|
|
||||||
});
|
|
||||||
await waitForExplore();
|
|
||||||
|
|
||||||
await waitFor(() => {
|
|
||||||
const urlParams = decodeURIComponent(location.getSearch().toString());
|
|
||||||
expect(urlParams).toBe(
|
|
||||||
'left={"datasource":"elastic-uid","queries":[{"refId":"A","datasource":{"type":"logs","uid":"elastic-uid"}}],"range":{"from":"now-1h","to":"now"}}&orgId=1'
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(location.getHistory()).toHaveLength(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('Filters out queries not using the root datasource', async () => {
|
|
||||||
const { location } = setupExplore({
|
|
||||||
urlParams: {
|
|
||||||
left: '{"datasource":"elastic-uid","queries":[{"refId":"A","datasource":{"type":"logs","uid":"loki-uid"}},{"refId":"B","datasource":{"type":"logs","uid":"elastic-uid"}}],"range":{"from":"now-1h","to":"now"}}',
|
|
||||||
},
|
|
||||||
prevUsedDatasource: { orgId: 1, datasource: 'elastic' },
|
|
||||||
mixedEnabled: true,
|
|
||||||
});
|
|
||||||
await waitForExplore();
|
|
||||||
|
|
||||||
await waitFor(() => {
|
|
||||||
const urlParams = decodeURIComponent(location.getSearch().toString());
|
|
||||||
expect(urlParams).toBe(
|
|
||||||
'left={"datasource":"elastic-uid","queries":[{"refId":"B","datasource":{"type":"logs","uid":"elastic-uid"}}],"range":{"from":"now-1h","to":"now"}}&orgId=1'
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('Fallbacks to last used datasource if root datasource does not exist', async () => {
|
|
||||||
const { location } = setupExplore({
|
|
||||||
urlParams: { left: '{"datasource":"NON-EXISTENT","range":{"from":"now-1h","to":"now"}}' },
|
|
||||||
prevUsedDatasource: { orgId: 1, datasource: 'elastic' },
|
|
||||||
mixedEnabled: true,
|
|
||||||
});
|
|
||||||
await waitForExplore();
|
|
||||||
|
|
||||||
await waitFor(() => {
|
|
||||||
const urlParams = decodeURIComponent(location.getSearch().toString());
|
|
||||||
expect(urlParams).toBe(
|
|
||||||
'left={"datasource":"elastic-uid","queries":[{"refId":"A","datasource":{"type":"logs","uid":"elastic-uid"}}],"range":{"from":"now-1h","to":"now"}}&orgId=1'
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('Fallbacks to default datasource if root datasource does not exist and last used datasource does not exist', async () => {
|
|
||||||
const { location } = setupExplore({
|
|
||||||
urlParams: { left: '{"datasource":"NON-EXISTENT","range":{"from":"now-1h","to":"now"}}' },
|
|
||||||
prevUsedDatasource: { orgId: 1, datasource: 'I DO NOT EXIST' },
|
|
||||||
mixedEnabled: true,
|
|
||||||
});
|
|
||||||
await waitForExplore();
|
|
||||||
|
|
||||||
await waitFor(() => {
|
|
||||||
const urlParams = decodeURIComponent(location.getSearch().toString());
|
|
||||||
expect(urlParams).toBe(
|
|
||||||
'left={"datasource":"loki-uid","queries":[{"refId":"A","datasource":{"type":"logs","uid":"loki-uid"}}],"range":{"from":"now-1h","to":"now"}}&orgId=1'
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('Fallbacks to default datasource if root datasource does not exist there is no last used datasource', async () => {
|
|
||||||
const { location } = setupExplore({
|
|
||||||
urlParams: { left: '{"datasource":"NON-EXISTENT","range":{"from":"now-1h","to":"now"}}' },
|
|
||||||
mixedEnabled: true,
|
|
||||||
});
|
|
||||||
await waitForExplore();
|
|
||||||
|
|
||||||
await waitFor(() => {
|
|
||||||
const urlParams = decodeURIComponent(location.getSearch().toString());
|
|
||||||
expect(urlParams).toBe(
|
|
||||||
'left={"datasource":"loki-uid","queries":[{"refId":"A","datasource":{"type":"logs","uid":"loki-uid"}}],"range":{"from":"now-1h","to":"now"}}&orgId=1'
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('Queries using nonexisting datasources gets removed', async () => {
|
|
||||||
const { location } = setupExplore({
|
|
||||||
urlParams: {
|
|
||||||
left: '{"datasource":"-- Mixed --","queries":[{"refId":"A","datasource":{"type":"NON-EXISTENT","uid":"NON-EXISTENT"}},{"refId":"B","datasource":{"type":"logs","uid":"elastic-uid"}}],"range":{"from":"now-1h","to":"now"}}',
|
|
||||||
},
|
|
||||||
prevUsedDatasource: { orgId: 1, datasource: 'elastic' },
|
|
||||||
mixedEnabled: true,
|
|
||||||
});
|
|
||||||
await waitForExplore();
|
|
||||||
|
|
||||||
await waitFor(() => {
|
|
||||||
const urlParams = decodeURIComponent(location.getSearch().toString());
|
|
||||||
expect(urlParams).toBe(
|
|
||||||
'left={"datasource":"--+Mixed+--","queries":[{"refId":"B","datasource":{"type":"logs","uid":"elastic-uid"}}],"range":{"from":"now-1h","to":"now"}}&orgId=1'
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('Only keeps queries using root datasource', async () => {
|
|
||||||
const { location } = setupExplore({
|
|
||||||
urlParams: {
|
|
||||||
left: '{"datasource":"elastic-uid","queries":[{"refId":"A","datasource":{"type":"logs","uid":"loki-uid"}},{"refId":"B","datasource":{"type":"logs","uid":"elastic-uid"}}],"range":{"from":"now-1h","to":"now"}}',
|
|
||||||
},
|
|
||||||
prevUsedDatasource: { orgId: 1, datasource: 'elastic' },
|
|
||||||
mixedEnabled: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
await waitForExplore();
|
|
||||||
|
|
||||||
await waitFor(() => {
|
|
||||||
const urlParams = decodeURIComponent(location.getSearch().toString());
|
|
||||||
// because there are no import/export queries in our mock datasources, only the first one remains
|
|
||||||
|
|
||||||
expect(urlParams).toBe(
|
|
||||||
'left={"datasource":"elastic-uid","queries":[{"refId":"B","datasource":{"type":"logs","uid":"elastic-uid"}}],"range":{"from":"now-1h","to":"now"}}&orgId=1'
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('exploreMixedDatasource off', () => {
|
|
||||||
beforeAll(() => {
|
|
||||||
config.featureToggles.exploreMixedDatasource = false;
|
|
||||||
});
|
|
||||||
|
|
||||||
it('Redirects to the first query datasource if the root is mixed', async () => {
|
|
||||||
const { location } = setupExplore({
|
|
||||||
urlParams: {
|
|
||||||
left: '{"datasource":"-- Mixed --","queries":[{"refId":"A","datasource":{"type":"logs","uid":"elastic-uid"}},{"refId":"B","datasource":{"type":"logs","uid":"loki-uid"}}],"range":{"from":"now-1h","to":"now"}}',
|
|
||||||
},
|
|
||||||
mixedEnabled: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
await waitForExplore();
|
|
||||||
|
|
||||||
await waitFor(() => {
|
|
||||||
const urlParams = decodeURIComponent(location.getSearch().toString());
|
|
||||||
|
|
||||||
expect(urlParams).toBe(
|
|
||||||
'left={"datasource":"elastic-uid","queries":[{"refId":"A","datasource":{"type":"logs","uid":"elastic-uid"}}],"range":{"from":"now-1h","to":"now"}}&orgId=1'
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('Redirects to the default datasource if the root is mixed and there are no queries', async () => {
|
|
||||||
const { location } = setupExplore({
|
|
||||||
urlParams: {
|
|
||||||
left: '{"datasource":"-- Mixed --","range":{"from":"now-1h","to":"now"}}',
|
|
||||||
},
|
|
||||||
mixedEnabled: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
await waitForExplore();
|
|
||||||
|
|
||||||
await waitFor(() => {
|
|
||||||
const urlParams = decodeURIComponent(location.getSearch().toString());
|
|
||||||
|
|
||||||
expect(urlParams).toBe(
|
|
||||||
'left={"datasource":"loki-uid","queries":[{"refId":"A","datasource":{"type":"logs","uid":"loki-uid"}}],"range":{"from":"now-1h","to":"now"}}&orgId=1'
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('removes `from` and `to` parameters from url when first mounted', async () => {
|
|
||||||
const { location } = setupExplore({ urlParams: { from: '1', to: '2' } });
|
|
||||||
await waitForExplore();
|
|
||||||
|
|
||||||
await waitFor(() => {
|
|
||||||
expect(location.getSearchObject()).toEqual(expect.not.objectContaining({ from: '1', to: '2' }));
|
|
||||||
expect(location.getSearchObject()).toEqual(expect.objectContaining({ orgId: '1' }));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
@ -9,17 +9,16 @@ import { useGrafana } from 'app/core/context/GrafanaContext';
|
|||||||
import { useNavModel } from 'app/core/hooks/useNavModel';
|
import { useNavModel } from 'app/core/hooks/useNavModel';
|
||||||
import { GrafanaRouteComponentProps } from 'app/core/navigation/types';
|
import { GrafanaRouteComponentProps } from 'app/core/navigation/types';
|
||||||
import { useDispatch, useSelector } from 'app/types';
|
import { useDispatch, useSelector } from 'app/types';
|
||||||
import { ExploreId, ExploreQueryParams } from 'app/types/explore';
|
import { ExploreQueryParams } from 'app/types/explore';
|
||||||
|
|
||||||
import { ExploreActions } from './ExploreActions';
|
import { ExploreActions } from './ExploreActions';
|
||||||
import { ExplorePaneContainer } from './ExplorePaneContainer';
|
import { ExplorePaneContainer } from './ExplorePaneContainer';
|
||||||
import { useExploreCorrelations } from './hooks/useExploreCorrelations';
|
import { useExploreCorrelations } from './hooks/useExploreCorrelations';
|
||||||
import { useExplorePageTitle } from './hooks/useExplorePageTitle';
|
import { useExplorePageTitle } from './hooks/useExplorePageTitle';
|
||||||
import { useStateSync } from './hooks/useStateSync';
|
import { useStateSync } from './hooks/useStateSync';
|
||||||
import { useStopQueries } from './hooks/useStopQueries';
|
|
||||||
import { useTimeSrvFix } from './hooks/useTimeSrvFix';
|
import { useTimeSrvFix } from './hooks/useTimeSrvFix';
|
||||||
import { splitSizeUpdateAction } from './state/main';
|
import { splitSizeUpdateAction } from './state/main';
|
||||||
import { selectOrderedExplorePanes } from './state/selectors';
|
import { isSplit, selectPanesEntries } from './state/selectors';
|
||||||
|
|
||||||
const styles = {
|
const styles = {
|
||||||
pageScrollbarWrapper: css`
|
pageScrollbarWrapper: css`
|
||||||
@ -32,10 +31,14 @@ const styles = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default function ExplorePage(props: GrafanaRouteComponentProps<{}, ExploreQueryParams>) {
|
export default function ExplorePage(props: GrafanaRouteComponentProps<{}, ExploreQueryParams>) {
|
||||||
useStopQueries();
|
|
||||||
useTimeSrvFix();
|
useTimeSrvFix();
|
||||||
useStateSync(props.queryParams);
|
useStateSync(props.queryParams);
|
||||||
useExplorePageTitle();
|
// We want to set the title according to the URL and not to the state because the URL itself may lag
|
||||||
|
// (due to how useStateSync above works) by a few milliseconds.
|
||||||
|
// When a URL is pushed to the history, the browser also saves the title of the page and
|
||||||
|
// if we were to update the URL on state change, the title would not match the URL.
|
||||||
|
// Ultimately the URL is the single source of truth from which state is derived, the page title is not different
|
||||||
|
useExplorePageTitle(props.queryParams);
|
||||||
useExploreCorrelations();
|
useExploreCorrelations();
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const { keybindings, chrome } = useGrafana();
|
const { keybindings, chrome } = useGrafana();
|
||||||
@ -45,7 +48,8 @@ export default function ExplorePage(props: GrafanaRouteComponentProps<{}, Explor
|
|||||||
const minWidth = 200;
|
const minWidth = 200;
|
||||||
const exploreState = useSelector((state) => state.explore);
|
const exploreState = useSelector((state) => state.explore);
|
||||||
|
|
||||||
const panes = useSelector(selectOrderedExplorePanes);
|
const panes = useSelector(selectPanesEntries);
|
||||||
|
const hasSplit = useSelector(isSplit);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
//This is needed for breadcrumbs and topnav.
|
//This is needed for breadcrumbs and topnav.
|
||||||
@ -65,7 +69,7 @@ export default function ExplorePage(props: GrafanaRouteComponentProps<{}, Explor
|
|||||||
} else {
|
} else {
|
||||||
dispatch(
|
dispatch(
|
||||||
splitSizeUpdateAction({
|
splitSizeUpdateAction({
|
||||||
largerExploreId: size > evenSplitWidth ? ExploreId.right : ExploreId.left,
|
largerExploreId: size > evenSplitWidth ? panes[1][0] : panes[0][0],
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -73,11 +77,10 @@ export default function ExplorePage(props: GrafanaRouteComponentProps<{}, Explor
|
|||||||
setRightPaneWidthRatio(size / windowWidth);
|
setRightPaneWidthRatio(size / windowWidth);
|
||||||
};
|
};
|
||||||
|
|
||||||
const hasSplit = Object.entries(panes).length > 1;
|
|
||||||
let widthCalc = 0;
|
let widthCalc = 0;
|
||||||
if (hasSplit) {
|
if (hasSplit) {
|
||||||
if (!exploreState.evenSplitPanes && exploreState.maxedExploreId) {
|
if (!exploreState.evenSplitPanes && exploreState.maxedExploreId) {
|
||||||
widthCalc = exploreState.maxedExploreId === ExploreId.right ? windowWidth - minWidth : minWidth;
|
widthCalc = exploreState.maxedExploreId === panes[1][0] ? windowWidth - minWidth : minWidth;
|
||||||
} else if (exploreState.evenSplitPanes) {
|
} else if (exploreState.evenSplitPanes) {
|
||||||
widthCalc = Math.floor(windowWidth / 2);
|
widthCalc = Math.floor(windowWidth / 2);
|
||||||
} else if (rightPaneWidthRatio !== undefined) {
|
} else if (rightPaneWidthRatio !== undefined) {
|
||||||
@ -87,7 +90,7 @@ export default function ExplorePage(props: GrafanaRouteComponentProps<{}, Explor
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.pageScrollbarWrapper}>
|
<div className={styles.pageScrollbarWrapper}>
|
||||||
<ExploreActions exploreIdLeft={ExploreId.left} exploreIdRight={ExploreId.right} />
|
<ExploreActions />
|
||||||
|
|
||||||
<SplitPaneWrapper
|
<SplitPaneWrapper
|
||||||
splitOrientation="vertical"
|
splitOrientation="vertical"
|
||||||
@ -97,16 +100,12 @@ export default function ExplorePage(props: GrafanaRouteComponentProps<{}, Explor
|
|||||||
primary="second"
|
primary="second"
|
||||||
splitVisible={hasSplit}
|
splitVisible={hasSplit}
|
||||||
paneStyle={{ overflow: 'auto', display: 'flex', flexDirection: 'column' }}
|
paneStyle={{ overflow: 'auto', display: 'flex', flexDirection: 'column' }}
|
||||||
onDragFinished={(size) => {
|
onDragFinished={(size) => size && updateSplitSize(size)}
|
||||||
if (size) {
|
|
||||||
updateSplitSize(size);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
{Object.keys(panes).map((exploreId) => {
|
{panes.map(([exploreId]) => {
|
||||||
return (
|
return (
|
||||||
<ErrorBoundaryAlert key={exploreId} style="page">
|
<ErrorBoundaryAlert key={exploreId} style="page">
|
||||||
<ExplorePaneContainer exploreId={exploreId as ExploreId} />
|
<ExplorePaneContainer exploreId={exploreId} />
|
||||||
</ErrorBoundaryAlert>
|
</ErrorBoundaryAlert>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
@ -1,14 +1,15 @@
|
|||||||
import { css } from '@emotion/css';
|
import { css } from '@emotion/css';
|
||||||
import React, { useEffect, useRef } from 'react';
|
import React, { useEffect, useMemo, useRef } from 'react';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
|
|
||||||
import { EventBusSrv, GrafanaTheme2 } from '@grafana/data';
|
import { EventBusSrv, GrafanaTheme2 } from '@grafana/data';
|
||||||
import { selectors } from '@grafana/e2e-selectors';
|
import { selectors } from '@grafana/e2e-selectors';
|
||||||
import { useStyles2 } from '@grafana/ui';
|
import { useStyles2 } from '@grafana/ui';
|
||||||
import { StoreState } from 'app/types';
|
import { stopQueryState } from 'app/core/utils/explore';
|
||||||
import { ExploreId } from 'app/types/explore';
|
import { StoreState, useSelector } from 'app/types';
|
||||||
|
|
||||||
import Explore from './Explore';
|
import Explore from './Explore';
|
||||||
|
import { getExploreItemSelector } from './state/selectors';
|
||||||
|
|
||||||
const getStyles = (theme: GrafanaTheme2) => {
|
const getStyles = (theme: GrafanaTheme2) => {
|
||||||
return {
|
return {
|
||||||
@ -26,7 +27,7 @@ const getStyles = (theme: GrafanaTheme2) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
exploreId: ExploreId;
|
exploreId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@ -39,6 +40,7 @@ interface Props {
|
|||||||
You can read more about this issue here: https://react-redux.js.org/api/hooks#stale-props-and-zombie-children
|
You can read more about this issue here: https://react-redux.js.org/api/hooks#stale-props-and-zombie-children
|
||||||
*/
|
*/
|
||||||
function ExplorePaneContainerUnconnected({ exploreId }: Props) {
|
function ExplorePaneContainerUnconnected({ exploreId }: Props) {
|
||||||
|
useStopQueries(exploreId);
|
||||||
const styles = useStyles2(getStyles);
|
const styles = useStyles2(getStyles);
|
||||||
const eventBus = useRef(new EventBusSrv());
|
const eventBus = useRef(new EventBusSrv());
|
||||||
const ref = useRef(null);
|
const ref = useRef(null);
|
||||||
@ -64,3 +66,15 @@ function mapStateToProps(state: StoreState, props: Props) {
|
|||||||
const connector = connect(mapStateToProps);
|
const connector = connect(mapStateToProps);
|
||||||
|
|
||||||
export const ExplorePaneContainer = connector(ExplorePaneContainerUnconnected);
|
export const ExplorePaneContainer = connector(ExplorePaneContainerUnconnected);
|
||||||
|
|
||||||
|
function useStopQueries(exploreId: string) {
|
||||||
|
const paneSelector = useMemo(() => getExploreItemSelector(exploreId), [exploreId]);
|
||||||
|
const paneRef = useRef<ReturnType<typeof paneSelector>>();
|
||||||
|
paneRef.current = useSelector(paneSelector);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
stopQueryState(paneRef.current?.querySubscription);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
}
|
||||||
|
@ -3,7 +3,6 @@ import React, { ComponentProps } from 'react';
|
|||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
|
|
||||||
import { LoadingState, InternalTimeZones, getDefaultTimeRange } from '@grafana/data';
|
import { LoadingState, InternalTimeZones, getDefaultTimeRange } from '@grafana/data';
|
||||||
import { ExploreId } from 'app/types';
|
|
||||||
|
|
||||||
import { ExploreQueryInspector } from './ExploreQueryInspector';
|
import { ExploreQueryInspector } from './ExploreQueryInspector';
|
||||||
|
|
||||||
@ -38,7 +37,7 @@ const setup = (propOverrides = {}) => {
|
|||||||
const props: ExploreQueryInspectorProps = {
|
const props: ExploreQueryInspectorProps = {
|
||||||
loading: false,
|
loading: false,
|
||||||
width: 100,
|
width: 100,
|
||||||
exploreId: ExploreId.left,
|
exploreId: 'left',
|
||||||
onClose: jest.fn(),
|
onClose: jest.fn(),
|
||||||
timeZone: InternalTimeZones.utc,
|
timeZone: InternalTimeZones.utc,
|
||||||
queryResponse: {
|
queryResponse: {
|
||||||
|
@ -10,13 +10,13 @@ import { InspectErrorTab } from 'app/features/inspector/InspectErrorTab';
|
|||||||
import { InspectJSONTab } from 'app/features/inspector/InspectJSONTab';
|
import { InspectJSONTab } from 'app/features/inspector/InspectJSONTab';
|
||||||
import { InspectStatsTab } from 'app/features/inspector/InspectStatsTab';
|
import { InspectStatsTab } from 'app/features/inspector/InspectStatsTab';
|
||||||
import { QueryInspector } from 'app/features/inspector/QueryInspector';
|
import { QueryInspector } from 'app/features/inspector/QueryInspector';
|
||||||
import { StoreState, ExploreItemState, ExploreId } from 'app/types';
|
import { StoreState, ExploreItemState } from 'app/types';
|
||||||
|
|
||||||
import { runQueries, selectIsWaitingForData } from './state/query';
|
import { runQueries, selectIsWaitingForData } from './state/query';
|
||||||
|
|
||||||
interface DispatchProps {
|
interface DispatchProps {
|
||||||
width: number;
|
width: number;
|
||||||
exploreId: ExploreId;
|
exploreId: string;
|
||||||
timeZone: TimeZone;
|
timeZone: TimeZone;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
}
|
}
|
||||||
@ -90,7 +90,7 @@ export function ExploreQueryInspector(props: Props) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function mapStateToProps(state: StoreState, { exploreId }: { exploreId: ExploreId }) {
|
function mapStateToProps(state: StoreState, { exploreId }: { exploreId: string }) {
|
||||||
const explore = state.explore;
|
const explore = state.explore;
|
||||||
const item: ExploreItemState = explore.panes[exploreId]!;
|
const item: ExploreItemState = explore.panes[exploreId]!;
|
||||||
const { queryResponse } = item;
|
const { queryResponse } = item;
|
||||||
|
@ -4,12 +4,11 @@ import { TimeRange, TimeZone, RawTimeRange, dateTimeForTimeZone, dateMath } from
|
|||||||
import { reportInteraction } from '@grafana/runtime';
|
import { reportInteraction } from '@grafana/runtime';
|
||||||
import { TimePickerWithHistory } from 'app/core/components/TimePicker/TimePickerWithHistory';
|
import { TimePickerWithHistory } from 'app/core/components/TimePicker/TimePickerWithHistory';
|
||||||
import { getShiftedTimeRange, getZoomedTimeRange } from 'app/core/utils/timePicker';
|
import { getShiftedTimeRange, getZoomedTimeRange } from 'app/core/utils/timePicker';
|
||||||
import { ExploreId } from 'app/types';
|
|
||||||
|
|
||||||
import { TimeSyncButton } from './TimeSyncButton';
|
import { TimeSyncButton } from './TimeSyncButton';
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
exploreId: ExploreId;
|
exploreId: string;
|
||||||
hideText?: boolean;
|
hideText?: boolean;
|
||||||
range: TimeRange;
|
range: TimeRange;
|
||||||
timeZone: TimeZone;
|
timeZone: TimeZone;
|
||||||
|
@ -11,7 +11,6 @@ import { contextSrv } from 'app/core/core';
|
|||||||
import { createAndCopyShortLink } from 'app/core/utils/shortLinks';
|
import { createAndCopyShortLink } from 'app/core/utils/shortLinks';
|
||||||
import { DataSourcePicker } from 'app/features/datasources/components/picker/DataSourcePicker';
|
import { DataSourcePicker } from 'app/features/datasources/components/picker/DataSourcePicker';
|
||||||
import { AccessControlAction } from 'app/types';
|
import { AccessControlAction } from 'app/types';
|
||||||
import { ExploreId } from 'app/types/explore';
|
|
||||||
import { StoreState, useDispatch, useSelector } from 'app/types/store';
|
import { StoreState, useDispatch, useSelector } from 'app/types/store';
|
||||||
|
|
||||||
import { DashNavButton } from '../dashboard/components/DashNav/DashNavButton';
|
import { DashNavButton } from '../dashboard/components/DashNav/DashNavButton';
|
||||||
@ -24,7 +23,7 @@ import { LiveTailButton } from './LiveTailButton';
|
|||||||
import { changeDatasource } from './state/datasource';
|
import { changeDatasource } from './state/datasource';
|
||||||
import { splitClose, splitOpen, maximizePaneAction, evenPaneResizeAction } from './state/main';
|
import { splitClose, splitOpen, maximizePaneAction, evenPaneResizeAction } from './state/main';
|
||||||
import { cancelQueries, runQueries, selectIsWaitingForData } from './state/query';
|
import { cancelQueries, runQueries, selectIsWaitingForData } from './state/query';
|
||||||
import { isSplit } from './state/selectors';
|
import { isSplit, selectPanesEntries } from './state/selectors';
|
||||||
import { syncTimes, changeRefreshInterval } from './state/time';
|
import { syncTimes, changeRefreshInterval } from './state/time';
|
||||||
import { LiveTailControls } from './useLiveTailControls';
|
import { LiveTailControls } from './useLiveTailControls';
|
||||||
|
|
||||||
@ -39,7 +38,7 @@ const rotateIcon = css({
|
|||||||
});
|
});
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
exploreId: ExploreId;
|
exploreId: string;
|
||||||
onChangeTime: (range: RawTimeRange, changedByScanner?: boolean) => void;
|
onChangeTime: (range: RawTimeRange, changedByScanner?: boolean) => void;
|
||||||
topOfViewRef: RefObject<HTMLDivElement>;
|
topOfViewRef: RefObject<HTMLDivElement>;
|
||||||
}
|
}
|
||||||
@ -64,9 +63,11 @@ export function ExploreToolbar({ exploreId, topOfViewRef, onChangeTime }: Props)
|
|||||||
(state) => state.explore.panes[exploreId]!.containerWidth < (splitted ? 700 : 800)
|
(state) => state.explore.panes[exploreId]!.containerWidth < (splitted ? 700 : 800)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const panes = useSelector(selectPanesEntries);
|
||||||
|
|
||||||
const shouldRotateSplitIcon = useMemo(
|
const shouldRotateSplitIcon = useMemo(
|
||||||
() => (exploreId === 'left' && isLargerPane) || (exploreId === 'right' && !isLargerPane),
|
() => (exploreId === panes[0][0] && isLargerPane) || (exploreId === panes[1]?.[0] && !isLargerPane),
|
||||||
[isLargerPane, exploreId]
|
[isLargerPane, exploreId, panes]
|
||||||
);
|
);
|
||||||
|
|
||||||
const onCopyShortLink = () => {
|
const onCopyShortLink = () => {
|
||||||
|
@ -11,7 +11,6 @@ import {
|
|||||||
MutableDataFrame,
|
MutableDataFrame,
|
||||||
toUtc,
|
toUtc,
|
||||||
} from '@grafana/data';
|
} from '@grafana/data';
|
||||||
import { ExploreId } from 'app/types';
|
|
||||||
|
|
||||||
import { Logs } from './Logs';
|
import { Logs } from './Logs';
|
||||||
|
|
||||||
@ -46,7 +45,7 @@ jest.mock('app/store/store', () => ({
|
|||||||
const changePanelState = jest.fn();
|
const changePanelState = jest.fn();
|
||||||
jest.mock('../state/explorePane', () => ({
|
jest.mock('../state/explorePane', () => ({
|
||||||
...jest.requireActual('../state/explorePane'),
|
...jest.requireActual('../state/explorePane'),
|
||||||
changePanelState: (exploreId: ExploreId, panel: 'logs', panelState: {} | ExploreLogsPanelState) => {
|
changePanelState: (exploreId: string, panel: 'logs', panelState: {} | ExploreLogsPanelState) => {
|
||||||
return changePanelState(exploreId, panel, panelState);
|
return changePanelState(exploreId, panel, panelState);
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
@ -85,7 +84,7 @@ describe('Logs', () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Logs
|
<Logs
|
||||||
exploreId={ExploreId.left}
|
exploreId={'left'}
|
||||||
splitOpen={() => undefined}
|
splitOpen={() => undefined}
|
||||||
logsVolumeEnabled={true}
|
logsVolumeEnabled={true}
|
||||||
onSetLogsVolumeEnabled={() => null}
|
onSetLogsVolumeEnabled={() => null}
|
||||||
@ -141,7 +140,7 @@ describe('Logs', () => {
|
|||||||
const scanningStarted = jest.fn();
|
const scanningStarted = jest.fn();
|
||||||
render(
|
render(
|
||||||
<Logs
|
<Logs
|
||||||
exploreId={ExploreId.left}
|
exploreId={'left'}
|
||||||
splitOpen={() => undefined}
|
splitOpen={() => undefined}
|
||||||
logsVolumeEnabled={true}
|
logsVolumeEnabled={true}
|
||||||
onSetLogsVolumeEnabled={() => null}
|
onSetLogsVolumeEnabled={() => null}
|
||||||
@ -178,7 +177,7 @@ describe('Logs', () => {
|
|||||||
it('should render a stop scanning button', () => {
|
it('should render a stop scanning button', () => {
|
||||||
render(
|
render(
|
||||||
<Logs
|
<Logs
|
||||||
exploreId={ExploreId.left}
|
exploreId={'left'}
|
||||||
splitOpen={() => undefined}
|
splitOpen={() => undefined}
|
||||||
logsVolumeEnabled={true}
|
logsVolumeEnabled={true}
|
||||||
onSetLogsVolumeEnabled={() => null}
|
onSetLogsVolumeEnabled={() => null}
|
||||||
@ -218,7 +217,7 @@ describe('Logs', () => {
|
|||||||
|
|
||||||
render(
|
render(
|
||||||
<Logs
|
<Logs
|
||||||
exploreId={ExploreId.left}
|
exploreId={'left'}
|
||||||
splitOpen={() => undefined}
|
splitOpen={() => undefined}
|
||||||
logsVolumeEnabled={true}
|
logsVolumeEnabled={true}
|
||||||
onSetLogsVolumeEnabled={() => null}
|
onSetLogsVolumeEnabled={() => null}
|
||||||
@ -270,10 +269,10 @@ describe('Logs', () => {
|
|||||||
const panelState = { logs: { id: '1' } };
|
const panelState = { logs: { id: '1' } };
|
||||||
const { rerender } = setup({ loading: false, panelState });
|
const { rerender } = setup({ loading: false, panelState });
|
||||||
|
|
||||||
rerender(getComponent({ loading: true, exploreId: ExploreId.right, panelState }));
|
rerender(getComponent({ loading: true, exploreId: 'right', panelState }));
|
||||||
rerender(getComponent({ loading: false, exploreId: ExploreId.right, panelState }));
|
rerender(getComponent({ loading: false, exploreId: 'right', panelState }));
|
||||||
|
|
||||||
expect(changePanelState).toHaveBeenCalledWith(ExploreId.right, 'logs', { logs: {} });
|
expect(changePanelState).toHaveBeenCalledWith('right', 'logs', { logs: {} });
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should scroll the scrollElement into view if rows contain id', () => {
|
it('should scroll the scrollElement into view if rows contain id', () => {
|
||||||
|
@ -46,7 +46,6 @@ import { dedupLogRows, filterLogLevels } from 'app/core/logsModel';
|
|||||||
import store from 'app/core/store';
|
import store from 'app/core/store';
|
||||||
import { createAndCopyShortLink } from 'app/core/utils/shortLinks';
|
import { createAndCopyShortLink } from 'app/core/utils/shortLinks';
|
||||||
import { getState, dispatch } from 'app/store/store';
|
import { getState, dispatch } from 'app/store/store';
|
||||||
import { ExploreId } from 'app/types/explore';
|
|
||||||
|
|
||||||
import { LogRows } from '../../logs/components/LogRows';
|
import { LogRows } from '../../logs/components/LogRows';
|
||||||
import { LogRowContextModal } from '../../logs/components/log-context/LogRowContextModal';
|
import { LogRowContextModal } from '../../logs/components/log-context/LogRowContextModal';
|
||||||
@ -73,7 +72,7 @@ interface Props extends Themeable2 {
|
|||||||
timeZone: TimeZone;
|
timeZone: TimeZone;
|
||||||
scanning?: boolean;
|
scanning?: boolean;
|
||||||
scanRange?: RawTimeRange;
|
scanRange?: RawTimeRange;
|
||||||
exploreId: ExploreId;
|
exploreId: string;
|
||||||
datasourceType?: string;
|
datasourceType?: string;
|
||||||
logsVolumeEnabled: boolean;
|
logsVolumeEnabled: boolean;
|
||||||
logsVolumeData: DataQueryResponse | undefined;
|
logsVolumeData: DataQueryResponse | undefined;
|
||||||
|
@ -21,7 +21,7 @@ import {
|
|||||||
import { DataQuery } from '@grafana/schema';
|
import { DataQuery } from '@grafana/schema';
|
||||||
import { Collapse } from '@grafana/ui';
|
import { Collapse } from '@grafana/ui';
|
||||||
import { StoreState } from 'app/types';
|
import { StoreState } from 'app/types';
|
||||||
import { ExploreId, ExploreItemState } from 'app/types/explore';
|
import { ExploreItemState } from 'app/types/explore';
|
||||||
|
|
||||||
import { getTimeZone } from '../../profile/state/selectors';
|
import { getTimeZone } from '../../profile/state/selectors';
|
||||||
import {
|
import {
|
||||||
@ -41,7 +41,7 @@ import { LogsCrossFadeTransition } from './utils/LogsCrossFadeTransition';
|
|||||||
|
|
||||||
interface LogsContainerProps extends PropsFromRedux {
|
interface LogsContainerProps extends PropsFromRedux {
|
||||||
width: number;
|
width: number;
|
||||||
exploreId: ExploreId;
|
exploreId: string;
|
||||||
scanRange?: RawTimeRange;
|
scanRange?: RawTimeRange;
|
||||||
syncedTimes: boolean;
|
syncedTimes: boolean;
|
||||||
loadingState: LoadingState;
|
loadingState: LoadingState;
|
||||||
@ -218,7 +218,7 @@ class LogsContainer extends PureComponent<LogsContainerProps> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function mapStateToProps(state: StoreState, { exploreId }: { exploreId: ExploreId }) {
|
function mapStateToProps(state: StoreState, { exploreId }: { exploreId: string }) {
|
||||||
const explore = state.explore;
|
const explore = state.explore;
|
||||||
const item: ExploreItemState = explore.panes[exploreId]!;
|
const item: ExploreItemState = explore.panes[exploreId]!;
|
||||||
const {
|
const {
|
||||||
|
@ -2,7 +2,6 @@ import { render, screen } from '@testing-library/react';
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import { getDefaultTimeRange, MutableDataFrame } from '@grafana/data';
|
import { getDefaultTimeRange, MutableDataFrame } from '@grafana/data';
|
||||||
import { ExploreId } from 'app/types';
|
|
||||||
|
|
||||||
import { UnconnectedNodeGraphContainer } from './NodeGraphContainer';
|
import { UnconnectedNodeGraphContainer } from './NodeGraphContainer';
|
||||||
|
|
||||||
@ -11,7 +10,7 @@ describe('NodeGraphContainer', () => {
|
|||||||
const { container } = render(
|
const { container } = render(
|
||||||
<UnconnectedNodeGraphContainer
|
<UnconnectedNodeGraphContainer
|
||||||
dataFrames={[emptyFrame]}
|
dataFrames={[emptyFrame]}
|
||||||
exploreId={ExploreId.left}
|
exploreId={'left'}
|
||||||
range={getDefaultTimeRange()}
|
range={getDefaultTimeRange()}
|
||||||
splitOpenFn={() => {}}
|
splitOpenFn={() => {}}
|
||||||
withTraceView={true}
|
withTraceView={true}
|
||||||
@ -27,7 +26,7 @@ describe('NodeGraphContainer', () => {
|
|||||||
const { container } = render(
|
const { container } = render(
|
||||||
<UnconnectedNodeGraphContainer
|
<UnconnectedNodeGraphContainer
|
||||||
dataFrames={[nodes]}
|
dataFrames={[nodes]}
|
||||||
exploreId={ExploreId.left}
|
exploreId={'left'}
|
||||||
range={getDefaultTimeRange()}
|
range={getDefaultTimeRange()}
|
||||||
splitOpenFn={() => {}}
|
splitOpenFn={() => {}}
|
||||||
datasourceType={''}
|
datasourceType={''}
|
||||||
|
@ -9,7 +9,7 @@ import { Collapse, useStyles2, useTheme2 } from '@grafana/ui';
|
|||||||
|
|
||||||
import { NodeGraph } from '../../../plugins/panel/nodeGraph';
|
import { NodeGraph } from '../../../plugins/panel/nodeGraph';
|
||||||
import { useCategorizeFrames } from '../../../plugins/panel/nodeGraph/useCategorizeFrames';
|
import { useCategorizeFrames } from '../../../plugins/panel/nodeGraph/useCategorizeFrames';
|
||||||
import { ExploreId, StoreState } from '../../../types';
|
import { StoreState } from '../../../types';
|
||||||
import { useLinks } from '../utils/links';
|
import { useLinks } from '../utils/links';
|
||||||
|
|
||||||
const getStyles = (theme: GrafanaTheme2) => ({
|
const getStyles = (theme: GrafanaTheme2) => ({
|
||||||
@ -23,7 +23,7 @@ const getStyles = (theme: GrafanaTheme2) => ({
|
|||||||
interface OwnProps {
|
interface OwnProps {
|
||||||
// Edges and Nodes are separate frames
|
// Edges and Nodes are separate frames
|
||||||
dataFrames: DataFrame[];
|
dataFrames: DataFrame[];
|
||||||
exploreId: ExploreId;
|
exploreId: string;
|
||||||
// When showing the node graph together with trace view we do some changes so it works better.
|
// When showing the node graph together with trace view we do some changes so it works better.
|
||||||
withTraceView?: boolean;
|
withTraceView?: boolean;
|
||||||
datasourceType: string;
|
datasourceType: string;
|
||||||
|
@ -5,7 +5,7 @@ import { Provider } from 'react-redux';
|
|||||||
import { DataSourceSrv, setDataSourceSrv } from '@grafana/runtime';
|
import { DataSourceSrv, setDataSourceSrv } from '@grafana/runtime';
|
||||||
import { DataQuery } from '@grafana/schema';
|
import { DataQuery } from '@grafana/schema';
|
||||||
import { configureStore } from 'app/store/configureStore';
|
import { configureStore } from 'app/store/configureStore';
|
||||||
import { ExploreId, ExploreState } from 'app/types';
|
import { ExploreState } from 'app/types';
|
||||||
|
|
||||||
import { UserState } from '../profile/state/reducers';
|
import { UserState } from '../profile/state/reducers';
|
||||||
|
|
||||||
@ -77,7 +77,7 @@ describe('Explore QueryRows', () => {
|
|||||||
|
|
||||||
render(
|
render(
|
||||||
<Provider store={store}>
|
<Provider store={store}>
|
||||||
<QueryRows exploreId={ExploreId.left} />
|
<QueryRows exploreId={'left'} />
|
||||||
</Provider>
|
</Provider>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -6,7 +6,6 @@ import { reportInteraction } from '@grafana/runtime';
|
|||||||
import { DataQuery } from '@grafana/schema';
|
import { DataQuery } from '@grafana/schema';
|
||||||
import { getNextRefIdChar } from 'app/core/utils/query';
|
import { getNextRefIdChar } from 'app/core/utils/query';
|
||||||
import { useDispatch, useSelector } from 'app/types';
|
import { useDispatch, useSelector } from 'app/types';
|
||||||
import { ExploreId } from 'app/types/explore';
|
|
||||||
|
|
||||||
import { getDatasourceSrv } from '../plugins/datasource_srv';
|
import { getDatasourceSrv } from '../plugins/datasource_srv';
|
||||||
import { QueryEditorRows } from '../query/components/QueryEditorRows';
|
import { QueryEditorRows } from '../query/components/QueryEditorRows';
|
||||||
@ -15,10 +14,10 @@ import { changeQueries, runQueries } from './state/query';
|
|||||||
import { getExploreItemSelector } from './state/selectors';
|
import { getExploreItemSelector } from './state/selectors';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
exploreId: ExploreId;
|
exploreId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const makeSelectors = (exploreId: ExploreId) => {
|
const makeSelectors = (exploreId: string) => {
|
||||||
const exploreItemSelector = getExploreItemSelector(exploreId);
|
const exploreItemSelector = getExploreItemSelector(exploreId);
|
||||||
return {
|
return {
|
||||||
getQueries: createSelector(exploreItemSelector, (s) => s!.queries),
|
getQueries: createSelector(exploreItemSelector, (s) => s!.queries),
|
||||||
|
@ -2,7 +2,7 @@ import { fireEvent, render, screen, within } from '@testing-library/react';
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import { FieldType, getDefaultTimeRange, InternalTimeZones, toDataFrame } from '@grafana/data';
|
import { FieldType, getDefaultTimeRange, InternalTimeZones, toDataFrame } from '@grafana/data';
|
||||||
import { ExploreId, TABLE_RESULTS_STYLE } from 'app/types';
|
import { TABLE_RESULTS_STYLE } from 'app/types';
|
||||||
|
|
||||||
import { RawPrometheusContainer } from './RawPrometheusContainer';
|
import { RawPrometheusContainer } from './RawPrometheusContainer';
|
||||||
|
|
||||||
@ -52,7 +52,7 @@ const dataFrame = toDataFrame({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const defaultProps = {
|
const defaultProps = {
|
||||||
exploreId: ExploreId.left,
|
exploreId: 'left',
|
||||||
loading: false,
|
loading: false,
|
||||||
width: 800,
|
width: 800,
|
||||||
onCellFilterAdded: jest.fn(),
|
onCellFilterAdded: jest.fn(),
|
||||||
|
@ -8,7 +8,7 @@ import { Collapse, RadioButtonGroup, Table, AdHocFilterItem } from '@grafana/ui'
|
|||||||
import { config } from 'app/core/config';
|
import { config } from 'app/core/config';
|
||||||
import { PANEL_BORDER } from 'app/core/constants';
|
import { PANEL_BORDER } from 'app/core/constants';
|
||||||
import { StoreState, TABLE_RESULTS_STYLE } from 'app/types';
|
import { StoreState, TABLE_RESULTS_STYLE } from 'app/types';
|
||||||
import { ExploreId, ExploreItemState, TABLE_RESULTS_STYLES, TableResultsStyle } from 'app/types/explore';
|
import { ExploreItemState, TABLE_RESULTS_STYLES, TableResultsStyle } from 'app/types/explore';
|
||||||
|
|
||||||
import { MetaInfoText } from '../MetaInfoText';
|
import { MetaInfoText } from '../MetaInfoText';
|
||||||
import RawListContainer from '../PrometheusListView/RawListContainer';
|
import RawListContainer from '../PrometheusListView/RawListContainer';
|
||||||
@ -17,7 +17,7 @@ import { getFieldLinksForExplore } from '../utils/links';
|
|||||||
|
|
||||||
interface RawPrometheusContainerProps {
|
interface RawPrometheusContainerProps {
|
||||||
ariaLabel?: string;
|
ariaLabel?: string;
|
||||||
exploreId: ExploreId;
|
exploreId: string;
|
||||||
width: number;
|
width: number;
|
||||||
timeZone: TimeZone;
|
timeZone: TimeZone;
|
||||||
onCellFilterAdded?: (filter: AdHocFilterItem) => void;
|
onCellFilterAdded?: (filter: AdHocFilterItem) => void;
|
||||||
|
@ -4,7 +4,6 @@ import { TestProvider } from 'test/helpers/TestProvider';
|
|||||||
|
|
||||||
import { DataQueryError, LoadingState } from '@grafana/data';
|
import { DataQueryError, LoadingState } from '@grafana/data';
|
||||||
import { selectors } from '@grafana/e2e-selectors';
|
import { selectors } from '@grafana/e2e-selectors';
|
||||||
import { ExploreId } from 'app/types';
|
|
||||||
|
|
||||||
import { configureStore } from '../../store/configureStore';
|
import { configureStore } from '../../store/configureStore';
|
||||||
|
|
||||||
@ -60,7 +59,7 @@ function setup(error: DataQueryError) {
|
|||||||
|
|
||||||
render(
|
render(
|
||||||
<TestProvider store={store}>
|
<TestProvider store={store}>
|
||||||
<ResponseErrorContainer exploreId={ExploreId.left} />
|
<ResponseErrorContainer exploreId="left" />
|
||||||
</TestProvider>
|
</TestProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -3,12 +3,10 @@ import React from 'react';
|
|||||||
import { LoadingState } from '@grafana/data';
|
import { LoadingState } from '@grafana/data';
|
||||||
import { useSelector } from 'app/types';
|
import { useSelector } from 'app/types';
|
||||||
|
|
||||||
import { ExploreId } from '../../types';
|
|
||||||
|
|
||||||
import { ErrorContainer } from './ErrorContainer';
|
import { ErrorContainer } from './ErrorContainer';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
exploreId: ExploreId;
|
exploreId: string;
|
||||||
}
|
}
|
||||||
export function ResponseErrorContainer(props: Props) {
|
export function ResponseErrorContainer(props: Props) {
|
||||||
const queryResponse = useSelector((state) => state.explore.panes[props.exploreId]!.queryResponse);
|
const queryResponse = useSelector((state) => state.explore.panes[props.exploreId]!.queryResponse);
|
||||||
|
@ -3,7 +3,6 @@ import React from 'react';
|
|||||||
|
|
||||||
import { GrafanaTheme2 } from '@grafana/data';
|
import { GrafanaTheme2 } from '@grafana/data';
|
||||||
import { SortOrder } from 'app/core/utils/richHistory';
|
import { SortOrder } from 'app/core/utils/richHistory';
|
||||||
import { ExploreId } from 'app/types';
|
|
||||||
|
|
||||||
import { RichHistory, RichHistoryProps, Tabs } from './RichHistory';
|
import { RichHistory, RichHistoryProps, Tabs } from './RichHistory';
|
||||||
|
|
||||||
@ -23,7 +22,7 @@ jest.mock('@grafana/runtime', () => ({
|
|||||||
const setup = (propOverrides?: Partial<RichHistoryProps>) => {
|
const setup = (propOverrides?: Partial<RichHistoryProps>) => {
|
||||||
const props: RichHistoryProps = {
|
const props: RichHistoryProps = {
|
||||||
theme: {} as GrafanaTheme2,
|
theme: {} as GrafanaTheme2,
|
||||||
exploreId: ExploreId.left,
|
exploreId: 'left',
|
||||||
height: 100,
|
height: 100,
|
||||||
activeDatasourceInstance: 'Test datasource',
|
activeDatasourceInstance: 'Test datasource',
|
||||||
richHistory: [],
|
richHistory: [],
|
||||||
|
@ -4,7 +4,7 @@ import React, { PureComponent } from 'react';
|
|||||||
import { SelectableValue } from '@grafana/data';
|
import { SelectableValue } from '@grafana/data';
|
||||||
import { Themeable2, TabbedContainer, TabConfig, withTheme2 } from '@grafana/ui';
|
import { Themeable2, TabbedContainer, TabConfig, withTheme2 } from '@grafana/ui';
|
||||||
import { SortOrder, RichHistorySearchFilters, RichHistorySettings } from 'app/core/utils/richHistory';
|
import { SortOrder, RichHistorySearchFilters, RichHistorySettings } from 'app/core/utils/richHistory';
|
||||||
import { RichHistoryQuery, ExploreId } from 'app/types/explore';
|
import { RichHistoryQuery } from 'app/types/explore';
|
||||||
|
|
||||||
import { supportedFeatures } from '../../../core/history/richHistoryStorageProvider';
|
import { supportedFeatures } from '../../../core/history/richHistoryStorageProvider';
|
||||||
|
|
||||||
@ -32,14 +32,14 @@ export interface RichHistoryProps extends Themeable2 {
|
|||||||
richHistorySettings: RichHistorySettings;
|
richHistorySettings: RichHistorySettings;
|
||||||
richHistorySearchFilters?: RichHistorySearchFilters;
|
richHistorySearchFilters?: RichHistorySearchFilters;
|
||||||
updateHistorySettings: (settings: RichHistorySettings) => void;
|
updateHistorySettings: (settings: RichHistorySettings) => void;
|
||||||
updateHistorySearchFilters: (exploreId: ExploreId, filters: RichHistorySearchFilters) => void;
|
updateHistorySearchFilters: (exploreId: string, filters: RichHistorySearchFilters) => void;
|
||||||
loadRichHistory: (exploreId: ExploreId) => void;
|
loadRichHistory: (exploreId: string) => void;
|
||||||
loadMoreRichHistory: (exploreId: ExploreId) => void;
|
loadMoreRichHistory: (exploreId: string) => void;
|
||||||
clearRichHistoryResults: (exploreId: ExploreId) => void;
|
clearRichHistoryResults: (exploreId: string) => void;
|
||||||
deleteRichHistory: () => void;
|
deleteRichHistory: () => void;
|
||||||
activeDatasourceInstance: string;
|
activeDatasourceInstance: string;
|
||||||
firstTab: Tabs;
|
firstTab: Tabs;
|
||||||
exploreId: ExploreId;
|
exploreId: string;
|
||||||
height: number;
|
height: number;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
}
|
}
|
||||||
|
@ -6,7 +6,7 @@ import { DataSourceApi, DataSourceInstanceSettings, DataSourcePluginMeta } from
|
|||||||
import { DataQuery, DataSourceRef } from '@grafana/schema';
|
import { DataQuery, DataSourceRef } from '@grafana/schema';
|
||||||
import appEvents from 'app/core/app_events';
|
import appEvents from 'app/core/app_events';
|
||||||
import { MixedDatasource } from 'app/plugins/datasource/mixed/MixedDataSource';
|
import { MixedDatasource } from 'app/plugins/datasource/mixed/MixedDataSource';
|
||||||
import { ExploreId, RichHistoryQuery } from 'app/types';
|
import { RichHistoryQuery } from 'app/types';
|
||||||
import { ShowConfirmModalEvent } from 'app/types/events';
|
import { ShowConfirmModalEvent } from 'app/types/events';
|
||||||
|
|
||||||
import { RichHistoryCard, Props } from './RichHistoryCard';
|
import { RichHistoryCard, Props } from './RichHistoryCard';
|
||||||
@ -120,7 +120,7 @@ const setup = (propOverrides?: Partial<Props<MockQuery>>) => {
|
|||||||
deleteHistoryItem: deleteRichHistoryMock,
|
deleteHistoryItem: deleteRichHistoryMock,
|
||||||
commentHistoryItem: jest.fn(),
|
commentHistoryItem: jest.fn(),
|
||||||
setQueries: jest.fn(),
|
setQueries: jest.fn(),
|
||||||
exploreId: ExploreId.left,
|
exploreId: 'left',
|
||||||
datasourceInstance: dsStore.loki,
|
datasourceInstance: dsStore.loki,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -19,9 +19,9 @@ import { setQueries } from 'app/features/explore/state/query';
|
|||||||
import { dispatch } from 'app/store/store';
|
import { dispatch } from 'app/store/store';
|
||||||
import { StoreState } from 'app/types';
|
import { StoreState } from 'app/types';
|
||||||
import { ShowConfirmModalEvent } from 'app/types/events';
|
import { ShowConfirmModalEvent } from 'app/types/events';
|
||||||
import { RichHistoryQuery, ExploreId } from 'app/types/explore';
|
import { RichHistoryQuery } from 'app/types/explore';
|
||||||
|
|
||||||
function mapStateToProps(state: StoreState, { exploreId }: { exploreId: ExploreId }) {
|
function mapStateToProps(state: StoreState, { exploreId }: { exploreId: string }) {
|
||||||
const explore = state.explore;
|
const explore = state.explore;
|
||||||
const { datasourceInstance } = explore.panes[exploreId]!;
|
const { datasourceInstance } = explore.panes[exploreId]!;
|
||||||
return {
|
return {
|
||||||
|
@ -2,7 +2,6 @@ import { render } from '@testing-library/react';
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import { SortOrder } from 'app/core/utils/richHistory';
|
import { SortOrder } from 'app/core/utils/richHistory';
|
||||||
import { ExploreId } from 'app/types';
|
|
||||||
|
|
||||||
import { Tabs } from './RichHistory';
|
import { Tabs } from './RichHistory';
|
||||||
import { RichHistoryContainer, Props } from './RichHistoryContainer';
|
import { RichHistoryContainer, Props } from './RichHistoryContainer';
|
||||||
@ -22,7 +21,7 @@ jest.mock('@grafana/runtime', () => ({
|
|||||||
const setup = (propOverrides?: Partial<Props>) => {
|
const setup = (propOverrides?: Partial<Props>) => {
|
||||||
const props: Props = {
|
const props: Props = {
|
||||||
width: 500,
|
width: 500,
|
||||||
exploreId: ExploreId.left,
|
exploreId: 'left',
|
||||||
activeDatasourceInstance: 'Test datasource',
|
activeDatasourceInstance: 'Test datasource',
|
||||||
richHistory: [],
|
richHistory: [],
|
||||||
firstTab: Tabs.RichHistory,
|
firstTab: Tabs.RichHistory,
|
||||||
|
@ -6,7 +6,6 @@ import { config, reportInteraction } from '@grafana/runtime';
|
|||||||
import { useTheme2 } from '@grafana/ui';
|
import { useTheme2 } from '@grafana/ui';
|
||||||
// Types
|
// Types
|
||||||
import { ExploreItemState, StoreState } from 'app/types';
|
import { ExploreItemState, StoreState } from 'app/types';
|
||||||
import { ExploreId } from 'app/types/explore';
|
|
||||||
|
|
||||||
// Components, enums
|
// Components, enums
|
||||||
import { ExploreDrawer } from '../ExploreDrawer';
|
import { ExploreDrawer } from '../ExploreDrawer';
|
||||||
@ -24,7 +23,7 @@ import { RichHistory, Tabs } from './RichHistory';
|
|||||||
|
|
||||||
//Actions
|
//Actions
|
||||||
|
|
||||||
function mapStateToProps(state: StoreState, { exploreId }: { exploreId: ExploreId }) {
|
function mapStateToProps(state: StoreState, { exploreId }: { exploreId: string }) {
|
||||||
const explore = state.explore;
|
const explore = state.explore;
|
||||||
const item: ExploreItemState = explore.panes[exploreId]!;
|
const item: ExploreItemState = explore.panes[exploreId]!;
|
||||||
const richHistorySearchFilters = item.richHistorySearchFilters;
|
const richHistorySearchFilters = item.richHistorySearchFilters;
|
||||||
@ -56,7 +55,7 @@ const connector = connect(mapStateToProps, mapDispatchToProps);
|
|||||||
|
|
||||||
interface OwnProps {
|
interface OwnProps {
|
||||||
width: number;
|
width: number;
|
||||||
exploreId: ExploreId;
|
exploreId: string;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
}
|
}
|
||||||
export type Props = ConnectedProps<typeof connector> & OwnProps;
|
export type Props = ConnectedProps<typeof connector> & OwnProps;
|
||||||
|
@ -3,7 +3,6 @@ import React from 'react';
|
|||||||
|
|
||||||
import { DataSourceSrv, setDataSourceSrv } from '@grafana/runtime';
|
import { DataSourceSrv, setDataSourceSrv } from '@grafana/runtime';
|
||||||
import { SortOrder } from 'app/core/utils/richHistoryTypes';
|
import { SortOrder } from 'app/core/utils/richHistoryTypes';
|
||||||
import { ExploreId } from 'app/types';
|
|
||||||
|
|
||||||
import { RichHistoryQueriesTab, RichHistoryQueriesTabProps } from './RichHistoryQueriesTab';
|
import { RichHistoryQueriesTab, RichHistoryQueriesTabProps } from './RichHistoryQueriesTab';
|
||||||
|
|
||||||
@ -30,7 +29,7 @@ const setup = (propOverrides?: Partial<RichHistoryQueriesTabProps>) => {
|
|||||||
lastUsedDatasourceFilters: [],
|
lastUsedDatasourceFilters: [],
|
||||||
starredTabAsFirstTab: false,
|
starredTabAsFirstTab: false,
|
||||||
},
|
},
|
||||||
exploreId: ExploreId.left,
|
exploreId: 'left',
|
||||||
height: 100,
|
height: 100,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -12,7 +12,7 @@ import {
|
|||||||
RichHistorySearchFilters,
|
RichHistorySearchFilters,
|
||||||
RichHistorySettings,
|
RichHistorySettings,
|
||||||
} from 'app/core/utils/richHistory';
|
} from 'app/core/utils/richHistory';
|
||||||
import { ExploreId, RichHistoryQuery } from 'app/types/explore';
|
import { RichHistoryQuery } from 'app/types/explore';
|
||||||
|
|
||||||
import { getSortOrderOptions } from './RichHistory';
|
import { getSortOrderOptions } from './RichHistory';
|
||||||
import RichHistoryCard from './RichHistoryCard';
|
import RichHistoryCard from './RichHistoryCard';
|
||||||
@ -27,7 +27,7 @@ export interface RichHistoryQueriesTabProps {
|
|||||||
loadMoreRichHistory: () => void;
|
loadMoreRichHistory: () => void;
|
||||||
richHistorySettings: RichHistorySettings;
|
richHistorySettings: RichHistorySettings;
|
||||||
richHistorySearchFilters?: RichHistorySearchFilters;
|
richHistorySearchFilters?: RichHistorySearchFilters;
|
||||||
exploreId: ExploreId;
|
exploreId: string;
|
||||||
height: number;
|
height: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2,7 +2,6 @@ import { fireEvent, render, screen } from '@testing-library/react';
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import { SortOrder } from 'app/core/utils/richHistory';
|
import { SortOrder } from 'app/core/utils/richHistory';
|
||||||
import { ExploreId } from 'app/types';
|
|
||||||
|
|
||||||
import { RichHistoryStarredTab, RichHistoryStarredTabProps } from './RichHistoryStarredTab';
|
import { RichHistoryStarredTab, RichHistoryStarredTabProps } from './RichHistoryStarredTab';
|
||||||
|
|
||||||
@ -26,7 +25,7 @@ const setup = (propOverrides?: Partial<RichHistoryStarredTabProps>) => {
|
|||||||
updateFilters: jest.fn(),
|
updateFilters: jest.fn(),
|
||||||
loadMoreRichHistory: jest.fn(),
|
loadMoreRichHistory: jest.fn(),
|
||||||
clearRichHistoryResults: jest.fn(),
|
clearRichHistoryResults: jest.fn(),
|
||||||
exploreId: ExploreId.left,
|
exploreId: 'left',
|
||||||
richHistorySettings: {
|
richHistorySettings: {
|
||||||
retentionPeriod: 7,
|
retentionPeriod: 7,
|
||||||
starredTabAsFirstTab: false,
|
starredTabAsFirstTab: false,
|
||||||
|
@ -10,7 +10,7 @@ import {
|
|||||||
RichHistorySearchFilters,
|
RichHistorySearchFilters,
|
||||||
RichHistorySettings,
|
RichHistorySettings,
|
||||||
} from 'app/core/utils/richHistory';
|
} from 'app/core/utils/richHistory';
|
||||||
import { RichHistoryQuery, ExploreId } from 'app/types/explore';
|
import { RichHistoryQuery } from 'app/types/explore';
|
||||||
|
|
||||||
import { getSortOrderOptions } from './RichHistory';
|
import { getSortOrderOptions } from './RichHistory';
|
||||||
import RichHistoryCard from './RichHistoryCard';
|
import RichHistoryCard from './RichHistoryCard';
|
||||||
@ -25,7 +25,7 @@ export interface RichHistoryStarredTabProps {
|
|||||||
loadMoreRichHistory: () => void;
|
loadMoreRichHistory: () => void;
|
||||||
richHistorySearchFilters?: RichHistorySearchFilters;
|
richHistorySearchFilters?: RichHistorySearchFilters;
|
||||||
richHistorySettings: RichHistorySettings;
|
richHistorySettings: RichHistorySettings;
|
||||||
exploreId: ExploreId;
|
exploreId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const getStyles = (theme: GrafanaTheme2) => {
|
const getStyles = (theme: GrafanaTheme2) => {
|
||||||
|
@ -2,7 +2,6 @@ import { render, screen, within } from '@testing-library/react';
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import { DataFrame, FieldType, getDefaultTimeRange, InternalTimeZones, toDataFrame } from '@grafana/data';
|
import { DataFrame, FieldType, getDefaultTimeRange, InternalTimeZones, toDataFrame } from '@grafana/data';
|
||||||
import { ExploreId } from 'app/types';
|
|
||||||
|
|
||||||
import { TableContainer } from './TableContainer';
|
import { TableContainer } from './TableContainer';
|
||||||
|
|
||||||
@ -48,7 +47,7 @@ const dataFrame = toDataFrame({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const defaultProps = {
|
const defaultProps = {
|
||||||
exploreId: ExploreId.left,
|
exploreId: 'left',
|
||||||
loading: false,
|
loading: false,
|
||||||
width: 800,
|
width: 800,
|
||||||
onCellFilterAdded: jest.fn(),
|
onCellFilterAdded: jest.fn(),
|
||||||
|
@ -5,7 +5,7 @@ import { ValueLinkConfig, applyFieldOverrides, TimeZone, SplitOpen, DataFrame, L
|
|||||||
import { Table, AdHocFilterItem, PanelChrome } from '@grafana/ui';
|
import { Table, AdHocFilterItem, PanelChrome } from '@grafana/ui';
|
||||||
import { config } from 'app/core/config';
|
import { config } from 'app/core/config';
|
||||||
import { StoreState } from 'app/types';
|
import { StoreState } from 'app/types';
|
||||||
import { ExploreId, ExploreItemState } from 'app/types/explore';
|
import { ExploreItemState } from 'app/types/explore';
|
||||||
|
|
||||||
import { MetaInfoText } from '../MetaInfoText';
|
import { MetaInfoText } from '../MetaInfoText';
|
||||||
import { selectIsWaitingForData } from '../state/query';
|
import { selectIsWaitingForData } from '../state/query';
|
||||||
@ -13,7 +13,7 @@ import { getFieldLinksForExplore } from '../utils/links';
|
|||||||
|
|
||||||
interface TableContainerProps {
|
interface TableContainerProps {
|
||||||
ariaLabel?: string;
|
ariaLabel?: string;
|
||||||
exploreId: ExploreId;
|
exploreId: string;
|
||||||
width: number;
|
width: number;
|
||||||
timeZone: TimeZone;
|
timeZone: TimeZone;
|
||||||
onCellFilterAdded?: (filter: AdHocFilterItem) => void;
|
onCellFilterAdded?: (filter: AdHocFilterItem) => void;
|
||||||
|
@ -5,7 +5,6 @@ import { Provider } from 'react-redux';
|
|||||||
|
|
||||||
import { DataFrame, MutableDataFrame, getDefaultTimeRange, LoadingState } from '@grafana/data';
|
import { DataFrame, MutableDataFrame, getDefaultTimeRange, LoadingState } from '@grafana/data';
|
||||||
import { DataSourceSrv, setDataSourceSrv } from '@grafana/runtime';
|
import { DataSourceSrv, setDataSourceSrv } from '@grafana/runtime';
|
||||||
import { ExploreId } from 'app/types';
|
|
||||||
|
|
||||||
import { configureStore } from '../../../store/configureStore';
|
import { configureStore } from '../../../store/configureStore';
|
||||||
|
|
||||||
@ -26,7 +25,7 @@ function getTraceView(frames: DataFrame[]) {
|
|||||||
return (
|
return (
|
||||||
<Provider store={store}>
|
<Provider store={store}>
|
||||||
<TraceView
|
<TraceView
|
||||||
exploreId={ExploreId.left}
|
exploreId="left"
|
||||||
dataFrames={frames}
|
dataFrames={frames}
|
||||||
splitOpenFn={() => {}}
|
splitOpenFn={() => {}}
|
||||||
traceProp={transformDataFrames(frames[0])!}
|
traceProp={transformDataFrames(frames[0])!}
|
||||||
|
@ -24,7 +24,6 @@ import { getDatasourceSrv } from 'app/features/plugins/datasource_srv';
|
|||||||
import { getTimeZone } from 'app/features/profile/state/selectors';
|
import { getTimeZone } from 'app/features/profile/state/selectors';
|
||||||
import { TempoQuery } from 'app/plugins/datasource/tempo/types';
|
import { TempoQuery } from 'app/plugins/datasource/tempo/types';
|
||||||
import { useDispatch, useSelector } from 'app/types';
|
import { useDispatch, useSelector } from 'app/types';
|
||||||
import { ExploreId } from 'app/types/explore';
|
|
||||||
|
|
||||||
import { changePanelState } from '../state/explorePane';
|
import { changePanelState } from '../state/explorePane';
|
||||||
|
|
||||||
@ -63,7 +62,7 @@ function noop(): {} {
|
|||||||
type Props = {
|
type Props = {
|
||||||
dataFrames: DataFrame[];
|
dataFrames: DataFrame[];
|
||||||
splitOpenFn?: SplitOpen;
|
splitOpenFn?: SplitOpen;
|
||||||
exploreId?: ExploreId;
|
exploreId?: string;
|
||||||
scrollElement?: Element;
|
scrollElement?: Element;
|
||||||
scrollElementClass?: string;
|
scrollElementClass?: string;
|
||||||
traceProp: Trace;
|
traceProp: Trace;
|
||||||
@ -251,7 +250,7 @@ export function TraceView(props: Props) {
|
|||||||
* @param options
|
* @param options
|
||||||
*/
|
*/
|
||||||
function useFocusSpanLink(options: {
|
function useFocusSpanLink(options: {
|
||||||
exploreId: ExploreId;
|
exploreId: string;
|
||||||
splitOpenFn: SplitOpen;
|
splitOpenFn: SplitOpen;
|
||||||
refId?: string;
|
refId?: string;
|
||||||
datasource?: DataSourceApi;
|
datasource?: DataSourceApi;
|
||||||
|
@ -5,7 +5,6 @@ import { Provider } from 'react-redux';
|
|||||||
|
|
||||||
import { getDefaultTimeRange, LoadingState } from '@grafana/data';
|
import { getDefaultTimeRange, LoadingState } from '@grafana/data';
|
||||||
import { config } from '@grafana/runtime';
|
import { config } from '@grafana/runtime';
|
||||||
import { ExploreId } from 'app/types';
|
|
||||||
|
|
||||||
import { configureStore } from '../../../store/configureStore';
|
import { configureStore } from '../../../store/configureStore';
|
||||||
|
|
||||||
@ -31,7 +30,7 @@ function renderTraceViewContainer(frames = [frameOld]) {
|
|||||||
const { container, baseElement } = render(
|
const { container, baseElement } = render(
|
||||||
<Provider store={store}>
|
<Provider store={store}>
|
||||||
<TraceViewContainer
|
<TraceViewContainer
|
||||||
exploreId={ExploreId.left}
|
exploreId="left"
|
||||||
dataFrames={frames}
|
dataFrames={frames}
|
||||||
splitOpenFn={() => {}}
|
splitOpenFn={() => {}}
|
||||||
queryResponse={mockPanelData}
|
queryResponse={mockPanelData}
|
||||||
|
@ -5,7 +5,6 @@ import { DataFrame, SplitOpen, PanelData, GrafanaTheme2 } from '@grafana/data';
|
|||||||
import { config } from '@grafana/runtime';
|
import { config } from '@grafana/runtime';
|
||||||
import { useStyles2 } from '@grafana/ui';
|
import { useStyles2 } from '@grafana/ui';
|
||||||
import { StoreState, useSelector } from 'app/types';
|
import { StoreState, useSelector } from 'app/types';
|
||||||
import { ExploreId } from 'app/types/explore';
|
|
||||||
|
|
||||||
import { TraceView } from './TraceView';
|
import { TraceView } from './TraceView';
|
||||||
import TracePageSearchBar from './components/TracePageHeader/TracePageSearchBar';
|
import TracePageSearchBar from './components/TracePageHeader/TracePageSearchBar';
|
||||||
@ -15,7 +14,7 @@ import { transformDataFrames } from './utils/transform';
|
|||||||
interface Props {
|
interface Props {
|
||||||
dataFrames: DataFrame[];
|
dataFrames: DataFrame[];
|
||||||
splitOpenFn: SplitOpen;
|
splitOpenFn: SplitOpen;
|
||||||
exploreId: ExploreId;
|
exploreId: string;
|
||||||
scrollElement?: Element;
|
scrollElement?: Element;
|
||||||
queryResponse: PanelData;
|
queryResponse: PanelData;
|
||||||
topOfViewRef: RefObject<HTMLDivElement>;
|
topOfViewRef: RefObject<HTMLDivElement>;
|
||||||
|
@ -0,0 +1,96 @@
|
|||||||
|
import { renderHook, waitFor } from '@testing-library/react';
|
||||||
|
import { TestProvider } from 'test/helpers/TestProvider';
|
||||||
|
|
||||||
|
import { setDataSourceSrv } from '@grafana/runtime';
|
||||||
|
import { DataSourceRef } from '@grafana/schema';
|
||||||
|
|
||||||
|
import { makeDatasourceSetup } from '../spec/helper/setup';
|
||||||
|
|
||||||
|
import { useExplorePageTitle } from './useExplorePageTitle';
|
||||||
|
|
||||||
|
describe('useExplorePageTitle', () => {
|
||||||
|
it('changes the document title of the explore page to include the datasource in use', async () => {
|
||||||
|
const datasources = [
|
||||||
|
makeDatasourceSetup({ name: 'loki', uid: 'loki-uid' }),
|
||||||
|
makeDatasourceSetup({ name: 'elastic', uid: 'elastic-uid' }),
|
||||||
|
];
|
||||||
|
|
||||||
|
setDataSourceSrv({
|
||||||
|
get(datasource?: string | DataSourceRef | null) {
|
||||||
|
let ds;
|
||||||
|
if (!datasource) {
|
||||||
|
ds = datasources[0]?.api;
|
||||||
|
} else {
|
||||||
|
ds = datasources.find((ds) =>
|
||||||
|
typeof datasource === 'string'
|
||||||
|
? ds.api.name === datasource || ds.api.uid === datasource
|
||||||
|
: ds.api.uid === datasource?.uid
|
||||||
|
)?.api;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ds) {
|
||||||
|
return Promise.resolve(ds);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.reject();
|
||||||
|
},
|
||||||
|
getInstanceSettings: jest.fn(),
|
||||||
|
getList: jest.fn(),
|
||||||
|
reload: jest.fn(),
|
||||||
|
});
|
||||||
|
|
||||||
|
renderHook(() => useExplorePageTitle({ panes: JSON.stringify({ a: { datasource: 'loki-uid' } }) }), {
|
||||||
|
wrapper: TestProvider,
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(global.document.title).toEqual(expect.stringContaining('loki'));
|
||||||
|
expect(global.document.title).toEqual(expect.not.stringContaining('elastic'));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('changes the document title to include the two datasources in use in split view mode', async () => {
|
||||||
|
const datasources = [
|
||||||
|
makeDatasourceSetup({ name: 'loki', uid: 'loki-uid' }),
|
||||||
|
makeDatasourceSetup({ name: 'elastic', uid: 'elastic-uid' }),
|
||||||
|
];
|
||||||
|
|
||||||
|
setDataSourceSrv({
|
||||||
|
get(datasource?: string | DataSourceRef | null) {
|
||||||
|
let ds;
|
||||||
|
if (!datasource) {
|
||||||
|
ds = datasources[0]?.api;
|
||||||
|
} else {
|
||||||
|
ds = datasources.find((ds) =>
|
||||||
|
typeof datasource === 'string'
|
||||||
|
? ds.api.name === datasource || ds.api.uid === datasource
|
||||||
|
: ds.api.uid === datasource?.uid
|
||||||
|
)?.api;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ds) {
|
||||||
|
return Promise.resolve(ds);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.reject();
|
||||||
|
},
|
||||||
|
getInstanceSettings: jest.fn(),
|
||||||
|
getList: jest.fn(),
|
||||||
|
reload: jest.fn(),
|
||||||
|
});
|
||||||
|
|
||||||
|
renderHook(
|
||||||
|
() =>
|
||||||
|
useExplorePageTitle({
|
||||||
|
panes: JSON.stringify({ a: { datasource: 'loki-uid' }, b: { datasource: 'elastic-uid' } }),
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
wrapper: TestProvider,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(global.document.title).toEqual(expect.stringContaining('loki | elastic'));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@ -1,16 +1,53 @@
|
|||||||
import { isTruthy } from '@grafana/data';
|
import { useEffect, useRef } from 'react';
|
||||||
|
|
||||||
|
import { NavModel } from '@grafana/data';
|
||||||
import { Branding } from 'app/core/components/Branding/Branding';
|
import { Branding } from 'app/core/components/Branding/Branding';
|
||||||
import { useNavModel } from 'app/core/hooks/useNavModel';
|
import { useNavModel } from 'app/core/hooks/useNavModel';
|
||||||
import { useSelector } from 'app/types';
|
import { safeParseJson } from 'app/core/utils/explore';
|
||||||
|
import { getDatasourceSrv } from 'app/features/plugins/datasource_srv';
|
||||||
|
import { ExploreQueryParams } from 'app/types';
|
||||||
|
|
||||||
import { selectOrderedExplorePanes } from '../state/selectors';
|
import { isFulfilled, hasKey } from './utils';
|
||||||
|
|
||||||
export function useExplorePageTitle() {
|
export function useExplorePageTitle(params: ExploreQueryParams) {
|
||||||
const navModel = useNavModel('explore');
|
const navModel = useRef<NavModel>();
|
||||||
|
navModel.current = useNavModel('explore');
|
||||||
|
const dsService = useRef(getDatasourceSrv());
|
||||||
|
|
||||||
const datasourceNames = useSelector((state) =>
|
useEffect(() => {
|
||||||
Object.values(selectOrderedExplorePanes(state)).map((pane) => pane?.datasourceInstance?.name)
|
if (!params.panes || typeof params.panes !== 'string') {
|
||||||
).filter(isTruthy);
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
document.title = `${navModel.main.text} - ${datasourceNames.join(' | ')} - ${Branding.AppTitle}`;
|
Promise.allSettled(
|
||||||
|
Object.values(safeParseJson(params.panes)).map((pane) => {
|
||||||
|
if (
|
||||||
|
!pane ||
|
||||||
|
typeof pane !== 'object' ||
|
||||||
|
!hasKey('datasource', pane) ||
|
||||||
|
!pane.datasource ||
|
||||||
|
typeof pane.datasource !== 'string'
|
||||||
|
) {
|
||||||
|
return Promise.reject();
|
||||||
|
}
|
||||||
|
|
||||||
|
return dsService.current.get(pane.datasource);
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.then((results) => results.filter(isFulfilled).map((result) => result.value))
|
||||||
|
.then((datasources) => {
|
||||||
|
if (!navModel.current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const names = datasources.map((ds) => ds.name);
|
||||||
|
|
||||||
|
if (names.length === 0) {
|
||||||
|
global.document.title = `${navModel.current.main.text} - ${Branding.AppTitle}`;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
global.document.title = `${navModel.current.main.text} - ${names.join(' | ')} - ${Branding.AppTitle}`;
|
||||||
|
});
|
||||||
|
}, [params.panes]);
|
||||||
}
|
}
|
||||||
|
443
public/app/features/explore/hooks/useStateSync/index.test.tsx
Normal file
443
public/app/features/explore/hooks/useStateSync/index.test.tsx
Normal file
@ -0,0 +1,443 @@
|
|||||||
|
import { act, waitFor } from '@testing-library/react';
|
||||||
|
import { renderHook } from '@testing-library/react-hooks';
|
||||||
|
import { createMemoryHistory } from 'history';
|
||||||
|
import { stringify } from 'querystring';
|
||||||
|
import React, { ReactNode } from 'react';
|
||||||
|
import { TestProvider } from 'test/helpers/TestProvider';
|
||||||
|
import { getGrafanaContextMock } from 'test/mocks/getGrafanaContextMock';
|
||||||
|
|
||||||
|
import { UrlQueryMap } from '@grafana/data';
|
||||||
|
import { HistoryWrapper, setDataSourceSrv } from '@grafana/runtime';
|
||||||
|
import { setLastUsedDatasourceUID } from 'app/core/utils/explore';
|
||||||
|
import { MIXED_DATASOURCE_NAME } from 'app/plugins/datasource/mixed/MixedDataSource';
|
||||||
|
import { configureStore } from 'app/store/configureStore';
|
||||||
|
|
||||||
|
import { makeDatasourceSetup } from '../../spec/helper/setup';
|
||||||
|
import { splitClose, splitOpen } from '../../state/main';
|
||||||
|
|
||||||
|
import { useStateSync } from './';
|
||||||
|
|
||||||
|
interface SetupParams {
|
||||||
|
queryParams?: UrlQueryMap;
|
||||||
|
exploreMixedDatasource?: boolean;
|
||||||
|
}
|
||||||
|
function setup({ queryParams = {}, exploreMixedDatasource = false }: SetupParams) {
|
||||||
|
const history = createMemoryHistory({
|
||||||
|
initialEntries: [{ pathname: '/explore', search: stringify(queryParams) }],
|
||||||
|
});
|
||||||
|
|
||||||
|
const location = new HistoryWrapper(history);
|
||||||
|
|
||||||
|
const datasources = [
|
||||||
|
makeDatasourceSetup({ name: 'loki', uid: 'loki-uid' }),
|
||||||
|
makeDatasourceSetup({ name: 'elastic', uid: 'elastic-uid' }),
|
||||||
|
];
|
||||||
|
|
||||||
|
if (exploreMixedDatasource) {
|
||||||
|
datasources.push(makeDatasourceSetup({ name: MIXED_DATASOURCE_NAME, uid: MIXED_DATASOURCE_NAME, id: 999 }));
|
||||||
|
}
|
||||||
|
|
||||||
|
setDataSourceSrv({
|
||||||
|
get(datasource) {
|
||||||
|
let ds;
|
||||||
|
if (!datasource) {
|
||||||
|
ds = datasources[0]?.api;
|
||||||
|
} else {
|
||||||
|
ds = datasources.find((ds) =>
|
||||||
|
typeof datasource === 'string'
|
||||||
|
? ds.api.name === datasource || ds.api.uid === datasource
|
||||||
|
: ds.api.uid === datasource?.uid
|
||||||
|
)?.api;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ds) {
|
||||||
|
return Promise.resolve(ds);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.reject();
|
||||||
|
},
|
||||||
|
getInstanceSettings: jest.fn(),
|
||||||
|
getList: jest.fn(),
|
||||||
|
reload: jest.fn(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const store = configureStore({
|
||||||
|
user: {
|
||||||
|
orgId: 1,
|
||||||
|
fiscalYearStartMonth: 0,
|
||||||
|
isUpdating: false,
|
||||||
|
orgs: [],
|
||||||
|
orgsAreLoading: false,
|
||||||
|
sessions: [],
|
||||||
|
sessionsAreLoading: false,
|
||||||
|
teams: [],
|
||||||
|
teamsAreLoading: false,
|
||||||
|
timeZone: 'utc',
|
||||||
|
user: null,
|
||||||
|
weekStart: 'monday',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const context = getGrafanaContextMock();
|
||||||
|
|
||||||
|
const wrapper = ({ children }: { children: ReactNode }) => (
|
||||||
|
<TestProvider
|
||||||
|
grafanaContext={{
|
||||||
|
...context,
|
||||||
|
location,
|
||||||
|
config: {
|
||||||
|
...context.config,
|
||||||
|
featureToggles: {
|
||||||
|
exploreMixedDatasource,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
store={store}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</TestProvider>
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...renderHook<{ params: UrlQueryMap; children: ReactNode }, void>(({ params }) => useStateSync(params), {
|
||||||
|
wrapper,
|
||||||
|
initialProps: {
|
||||||
|
children: null,
|
||||||
|
params: queryParams,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
location,
|
||||||
|
store,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('useStateSync', () => {
|
||||||
|
it('does not push a new entry to history on first render', async () => {
|
||||||
|
const { location, waitForNextUpdate } = setup({});
|
||||||
|
|
||||||
|
const initialHistoryLength = location.getHistory().length;
|
||||||
|
|
||||||
|
await waitForNextUpdate();
|
||||||
|
|
||||||
|
expect(location.getHistory().length).toBe(initialHistoryLength);
|
||||||
|
|
||||||
|
const search = location.getSearchObject();
|
||||||
|
expect(search.panes).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('inits an explore pane for each key in the panes search object', async () => {
|
||||||
|
const { location, waitForNextUpdate, store } = setup({
|
||||||
|
queryParams: {
|
||||||
|
panes: JSON.stringify({
|
||||||
|
one: { datasource: 'loki-uid', queries: [{ datasource: { name: 'loki', uid: 'loki-uid' } }] },
|
||||||
|
two: { datasource: 'elastic-uid', queries: [{ datasource: { name: 'elastic', uid: 'elastic-uid' } }] },
|
||||||
|
}),
|
||||||
|
schemaVersion: 1,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const initialHistoryLength = location.getHistory().length;
|
||||||
|
|
||||||
|
await waitForNextUpdate();
|
||||||
|
|
||||||
|
expect(location.getHistory().length).toBe(initialHistoryLength);
|
||||||
|
|
||||||
|
const search = location.getSearchObject();
|
||||||
|
expect(search.panes).toBeDefined();
|
||||||
|
expect(Object.keys(store.getState().explore.panes)).toHaveLength(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('inits with a default query from the root level datasource when there are no valid queries in the URL', async () => {
|
||||||
|
const { location, waitForNextUpdate, store } = setup({
|
||||||
|
queryParams: {
|
||||||
|
panes: JSON.stringify({
|
||||||
|
one: { datasource: 'loki-uid', queries: [{ datasource: { name: 'UNKNOWN', uid: 'UNKNOWN-DS' } }] },
|
||||||
|
}),
|
||||||
|
schemaVersion: 1,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const initialHistoryLength = location.getHistory().length;
|
||||||
|
|
||||||
|
await waitForNextUpdate();
|
||||||
|
|
||||||
|
expect(location.getHistory().length).toBe(initialHistoryLength);
|
||||||
|
|
||||||
|
const search = location.getSearchObject();
|
||||||
|
expect(search.panes).toBeDefined();
|
||||||
|
|
||||||
|
const queries = store.getState().explore.panes['one']?.queries;
|
||||||
|
expect(queries).toHaveLength(1);
|
||||||
|
|
||||||
|
expect(queries?.[0].datasource?.uid).toBe('loki-uid');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('inits with the last used datasource from localStorage', async () => {
|
||||||
|
setLastUsedDatasourceUID(1, 'elastic-uid');
|
||||||
|
const { waitForNextUpdate, store } = setup({
|
||||||
|
queryParams: {},
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitForNextUpdate();
|
||||||
|
|
||||||
|
expect(Object.values(store.getState().explore.panes)[0]?.datasourceInstance?.uid).toBe('elastic-uid');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('inits with the default datasource if the last used in localStorage does not exits', async () => {
|
||||||
|
setLastUsedDatasourceUID(1, 'unknown-ds-uid');
|
||||||
|
const { waitForNextUpdate, store } = setup({
|
||||||
|
queryParams: {},
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitForNextUpdate();
|
||||||
|
|
||||||
|
expect(Object.values(store.getState().explore.panes)[0]?.datasourceInstance?.uid).toBe('loki-uid');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('updates the state with correct queries from URL', async () => {
|
||||||
|
const { waitForNextUpdate, rerender, store } = setup({
|
||||||
|
queryParams: {
|
||||||
|
panes: JSON.stringify({
|
||||||
|
one: { datasource: 'loki-uid', queries: [{ expr: 'a' }] },
|
||||||
|
}),
|
||||||
|
schemaVersion: 1,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitForNextUpdate();
|
||||||
|
|
||||||
|
let queries = store.getState().explore.panes['one']?.queries;
|
||||||
|
expect(queries).toHaveLength(1);
|
||||||
|
expect(queries?.[0]).toMatchObject({ expr: 'a' });
|
||||||
|
|
||||||
|
rerender({
|
||||||
|
children: null,
|
||||||
|
params: {
|
||||||
|
panes: JSON.stringify({
|
||||||
|
one: { datasource: 'loki-uid', queries: [{ expr: 'a' }, { expr: 'b' }] },
|
||||||
|
}),
|
||||||
|
schemaVersion: 1,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitForNextUpdate();
|
||||||
|
|
||||||
|
queries = store.getState().explore.panes['one']?.queries;
|
||||||
|
expect(queries).toHaveLength(2);
|
||||||
|
expect(queries?.[0]).toMatchObject({ expr: 'a' });
|
||||||
|
expect(queries?.[1]).toMatchObject({ expr: 'b' });
|
||||||
|
|
||||||
|
rerender({
|
||||||
|
children: null,
|
||||||
|
params: {
|
||||||
|
panes: JSON.stringify({
|
||||||
|
one: { datasource: 'loki-uid', queries: [{ expr: 'a' }] },
|
||||||
|
}),
|
||||||
|
schemaVersion: 1,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitForNextUpdate();
|
||||||
|
|
||||||
|
queries = store.getState().explore.panes['one']?.queries;
|
||||||
|
expect(queries).toHaveLength(1);
|
||||||
|
expect(queries?.[0]).toMatchObject({ expr: 'a' });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Opens and closes the split pane if an a new pane is added or removed in the URL', async () => {
|
||||||
|
const { waitForNextUpdate, rerender, store } = setup({
|
||||||
|
queryParams: {
|
||||||
|
panes: JSON.stringify({
|
||||||
|
one: { datasource: 'loki-uid', queries: [{ expr: 'a' }] },
|
||||||
|
}),
|
||||||
|
schemaVersion: 1,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitForNextUpdate();
|
||||||
|
|
||||||
|
let panes = Object.keys(store.getState().explore.panes);
|
||||||
|
expect(panes).toHaveLength(1);
|
||||||
|
|
||||||
|
rerender({
|
||||||
|
children: null,
|
||||||
|
params: {
|
||||||
|
panes: JSON.stringify({
|
||||||
|
one: { datasource: 'loki-uid', queries: [{ expr: 'a' }, { expr: 'b' }] },
|
||||||
|
two: { datasource: 'loki-uid', queries: [{ expr: 'a' }, { expr: 'b' }] },
|
||||||
|
}),
|
||||||
|
schemaVersion: 1,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitForNextUpdate();
|
||||||
|
|
||||||
|
expect(Object.keys(store.getState().explore.panes)).toHaveLength(2);
|
||||||
|
|
||||||
|
rerender({
|
||||||
|
children: null,
|
||||||
|
params: {
|
||||||
|
panes: JSON.stringify({
|
||||||
|
one: { datasource: 'loki-uid', queries: [{ expr: 'a' }] },
|
||||||
|
}),
|
||||||
|
schemaVersion: 1,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitForNextUpdate();
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(Object.keys(store.getState().explore.panes)).toHaveLength(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Changes datasource when the datasource in the URL is updated', async () => {
|
||||||
|
const { waitForNextUpdate, rerender, store } = setup({
|
||||||
|
queryParams: {
|
||||||
|
panes: JSON.stringify({
|
||||||
|
one: { datasource: 'loki-uid', queries: [{ expr: 'a' }] },
|
||||||
|
}),
|
||||||
|
schemaVersion: 1,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitForNextUpdate();
|
||||||
|
|
||||||
|
expect(store.getState().explore.panes['one']?.datasourceInstance?.getRef()).toMatchObject({
|
||||||
|
type: 'logs',
|
||||||
|
uid: 'loki-uid',
|
||||||
|
});
|
||||||
|
|
||||||
|
rerender({
|
||||||
|
children: null,
|
||||||
|
params: {
|
||||||
|
panes: JSON.stringify({
|
||||||
|
one: { datasource: 'elastic-uid', queries: [{ expr: 'a' }, { expr: 'b' }] },
|
||||||
|
}),
|
||||||
|
schemaVersion: 1,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitForNextUpdate();
|
||||||
|
|
||||||
|
expect(store.getState().explore.panes['one']?.datasourceInstance?.getRef()).toMatchObject({
|
||||||
|
type: 'logs',
|
||||||
|
uid: 'elastic-uid',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Changes time rage when the range in the URL is updated', async () => {
|
||||||
|
const { waitForNextUpdate, rerender, store } = setup({
|
||||||
|
queryParams: {
|
||||||
|
panes: JSON.stringify({
|
||||||
|
one: { datasource: 'loki-uid', queries: [{ expr: 'a' }], range: { from: 'now-1h', to: 'now' } },
|
||||||
|
}),
|
||||||
|
schemaVersion: 1,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitForNextUpdate();
|
||||||
|
|
||||||
|
expect(store.getState().explore.panes['one']?.range.raw).toMatchObject({ from: 'now-1h', to: 'now' });
|
||||||
|
|
||||||
|
rerender({
|
||||||
|
children: null,
|
||||||
|
params: {
|
||||||
|
panes: JSON.stringify({
|
||||||
|
one: { datasource: 'loki-uid', queries: [{ expr: 'a' }], range: { from: 'now-6h', to: 'now' } },
|
||||||
|
}),
|
||||||
|
schemaVersion: 1,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitForNextUpdate();
|
||||||
|
|
||||||
|
expect(store.getState().explore.panes['one']?.range.raw).toMatchObject({ from: 'now-6h', to: 'now' });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('uses the first query datasource if no root datasource is specified in the URL', async () => {
|
||||||
|
const { waitForNextUpdate, store } = setup({
|
||||||
|
exploreMixedDatasource: true,
|
||||||
|
queryParams: {
|
||||||
|
panes: JSON.stringify({
|
||||||
|
one: {
|
||||||
|
queries: [{ expr: 'b', datasource: { uid: 'loki-uid', type: 'logs' }, refId: 'B' }],
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
schemaVersion: 1,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitForNextUpdate();
|
||||||
|
|
||||||
|
expect(store.getState().explore.panes['one']?.datasourceInstance?.getRef()).toMatchObject({
|
||||||
|
uid: 'loki-uid',
|
||||||
|
type: 'logs',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('updates the URL opening and closing a pane datasource changes', async () => {
|
||||||
|
const { waitForNextUpdate, store, location } = setup({
|
||||||
|
exploreMixedDatasource: true,
|
||||||
|
queryParams: {
|
||||||
|
panes: JSON.stringify({
|
||||||
|
one: {
|
||||||
|
datasource: 'loki-uid',
|
||||||
|
queries: [{ expr: 'a', datasource: { uid: 'loki-uid', type: 'logs' }, refId: 'A' }],
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
schemaVersion: 1,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitForNextUpdate();
|
||||||
|
|
||||||
|
expect(location.getHistory().length).toBe(1);
|
||||||
|
|
||||||
|
expect(store.getState().explore.panes['one']?.datasourceInstance?.uid).toBe('loki-uid');
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
store.dispatch(splitOpen());
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitForNextUpdate();
|
||||||
|
|
||||||
|
await waitFor(async () => {
|
||||||
|
expect(location.getHistory().length).toBe(2);
|
||||||
|
});
|
||||||
|
expect(Object.keys(store.getState().explore.panes)).toHaveLength(2);
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
store.dispatch(splitClose('one'));
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitFor(async () => {
|
||||||
|
expect(location.getHistory()).toHaveLength(3);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('with exploreMixedDatasource enabled', () => {
|
||||||
|
it('filters out queries from the URL that do not have a datasource', async () => {
|
||||||
|
const { waitForNextUpdate, store } = setup({
|
||||||
|
exploreMixedDatasource: true,
|
||||||
|
queryParams: {
|
||||||
|
panes: JSON.stringify({
|
||||||
|
one: {
|
||||||
|
datasource: MIXED_DATASOURCE_NAME,
|
||||||
|
queries: [
|
||||||
|
{ expr: 'a', refId: 'A' },
|
||||||
|
{ expr: 'b', datasource: { uid: 'loki-uid', type: 'logs' }, refId: 'B' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
schemaVersion: 1,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitForNextUpdate();
|
||||||
|
|
||||||
|
expect(store.getState().explore.panes['one']?.queries.length).toBe(1);
|
||||||
|
expect(store.getState().explore.panes['one']?.queries[0]).toMatchObject({ expr: 'b', refId: 'B' });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@ -1,30 +1,25 @@
|
|||||||
import { identity, isEmpty, isEqual, isObject, mapValues, omitBy } from 'lodash';
|
import { identity, isEmpty, isEqual, isObject, mapValues, omitBy } from 'lodash';
|
||||||
import { useEffect, useRef } from 'react';
|
import { useEffect, useRef } from 'react';
|
||||||
|
|
||||||
import {
|
import { CoreApp, ExploreUrlState, isDateTime, TimeRange, RawTimeRange, DataSourceApi } from '@grafana/data';
|
||||||
CoreApp,
|
|
||||||
serializeStateToUrlParam,
|
|
||||||
ExploreUrlState,
|
|
||||||
isDateTime,
|
|
||||||
TimeRange,
|
|
||||||
RawTimeRange,
|
|
||||||
DataSourceApi,
|
|
||||||
} from '@grafana/data';
|
|
||||||
import { DataQuery, DataSourceRef } from '@grafana/schema';
|
import { DataQuery, DataSourceRef } from '@grafana/schema';
|
||||||
import { useGrafana } from 'app/core/context/GrafanaContext';
|
import { useGrafana } from 'app/core/context/GrafanaContext';
|
||||||
import { clearQueryKeys, getLastUsedDatasourceUID, parseUrlState } from 'app/core/utils/explore';
|
import { clearQueryKeys, getLastUsedDatasourceUID } from 'app/core/utils/explore';
|
||||||
import { getDatasourceSrv } from 'app/features/plugins/datasource_srv';
|
import { getDatasourceSrv } from 'app/features/plugins/datasource_srv';
|
||||||
import { MIXED_DATASOURCE_NAME } from 'app/plugins/datasource/mixed/MixedDataSource';
|
import { MIXED_DATASOURCE_NAME } from 'app/plugins/datasource/mixed/MixedDataSource';
|
||||||
import { addListener, ExploreId, ExploreItemState, ExploreQueryParams, useDispatch, useSelector } from 'app/types';
|
import { addListener, ExploreItemState, ExploreQueryParams, useDispatch, useSelector } from 'app/types';
|
||||||
|
|
||||||
import { changeDatasource } from '../state/datasource';
|
import { changeDatasource } from '../../state/datasource';
|
||||||
import { initializeExplore } from '../state/explorePane';
|
import { initializeExplore } from '../../state/explorePane';
|
||||||
import { clearPanes, splitClose, splitOpen, syncTimesAction } from '../state/main';
|
import { clearPanes, splitClose, splitOpen, syncTimesAction } from '../../state/main';
|
||||||
import { runQueries, setQueriesAction } from '../state/query';
|
import { runQueries, setQueriesAction } from '../../state/query';
|
||||||
import { selectPanes } from '../state/selectors';
|
import { selectPanes } from '../../state/selectors';
|
||||||
import { changeRangeAction, updateTime } from '../state/time';
|
import { changeRangeAction, updateTime } from '../../state/time';
|
||||||
import { DEFAULT_RANGE } from '../state/utils';
|
import { DEFAULT_RANGE } from '../../state/utils';
|
||||||
import { withUniqueRefIds } from '../utils/queries';
|
import { withUniqueRefIds } from '../../utils/queries';
|
||||||
|
import { isFulfilled } from '../utils';
|
||||||
|
|
||||||
|
import { parseURL } from './parseURL';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Bi-directionally syncs URL changes with Explore's state.
|
* Bi-directionally syncs URL changes with Explore's state.
|
||||||
@ -37,15 +32,15 @@ export function useStateSync(params: ExploreQueryParams) {
|
|||||||
},
|
},
|
||||||
} = useGrafana();
|
} = useGrafana();
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const statePanes = useSelector(selectPanes);
|
const panesState = useSelector(selectPanes);
|
||||||
const orgId = useSelector((state) => state.user.orgId);
|
const orgId = useSelector((state) => state.user.orgId);
|
||||||
const prevParams = useRef<ExploreQueryParams>(params);
|
const prevParams = useRef(params);
|
||||||
const initState = useRef<'notstarted' | 'pending' | 'done'>('notstarted');
|
const initState = useRef<'notstarted' | 'pending' | 'done'>('notstarted');
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// This happens when the user navigates to an explore "empty page" while within Explore.
|
// This happens when the user navigates to an explore "empty page" while within Explore.
|
||||||
// ie. by clicking on the explore when explore is active.
|
// ie. by clicking on the explore when explore is active.
|
||||||
if (!params.left && !params.right) {
|
if (!params.panes) {
|
||||||
initState.current = 'notstarted';
|
initState.current = 'notstarted';
|
||||||
prevParams.current = params;
|
prevParams.current = params;
|
||||||
}
|
}
|
||||||
@ -68,22 +63,27 @@ export function useStateSync(params: ExploreQueryParams) {
|
|||||||
cancelActiveListeners();
|
cancelActiveListeners();
|
||||||
await delay(200);
|
await delay(200);
|
||||||
|
|
||||||
const panesQueryParams = Object.entries(getState().explore.panes).reduce<Record<string, string>>(
|
const panesQueryParams = Object.entries(getState().explore.panes).reduce((acc, [id, paneState]) => {
|
||||||
(acc, [id, paneState]) => ({ ...acc, [id]: serializeStateToUrlParam(getUrlStateFromPaneState(paneState)) }),
|
if (!paneState) {
|
||||||
{}
|
return acc;
|
||||||
);
|
}
|
||||||
|
return {
|
||||||
|
...acc,
|
||||||
|
[id]: getUrlStateFromPaneState(paneState),
|
||||||
|
};
|
||||||
|
}, {});
|
||||||
|
|
||||||
if (!isEqual(prevParams.current, panesQueryParams)) {
|
if (!isEqual(prevParams.current.panes, JSON.stringify(panesQueryParams))) {
|
||||||
// If there's no previous state it means we are mounting explore for the first time,
|
// If there's no previous state it means we are mounting explore for the first time,
|
||||||
// in this case we want to replace the URL instead of pushing a new entry to the history.
|
// in this case we want to replace the URL instead of pushing a new entry to the history.
|
||||||
const replace = Object.values(prevParams.current).filter(Boolean).length === 0;
|
const replace =
|
||||||
|
!!prevParams.current.panes && Object.values(prevParams.current.panes).filter(Boolean).length === 0;
|
||||||
|
|
||||||
prevParams.current = panesQueryParams;
|
prevParams.current = {
|
||||||
|
panes: JSON.stringify(panesQueryParams),
|
||||||
|
};
|
||||||
|
|
||||||
location.partial(
|
location.partial({ panes: prevParams.current.panes }, replace);
|
||||||
{ left: panesQueryParams.left, right: panesQueryParams.right, orgId: getState().user.orgId },
|
|
||||||
replace
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
@ -94,33 +94,29 @@ export function useStateSync(params: ExploreQueryParams) {
|
|||||||
}, [dispatch, location]);
|
}, [dispatch, location]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const isURLOutOfSync = prevParams.current?.left !== params.left || prevParams.current?.right !== params.right;
|
const isURLOutOfSync = prevParams.current?.panes !== params.panes;
|
||||||
|
|
||||||
const urlPanes = {
|
const urlState = parseURL(params);
|
||||||
left: parseUrlState(params.left),
|
|
||||||
...(params.right && { right: parseUrlState(params.right) }),
|
|
||||||
};
|
|
||||||
|
|
||||||
async function sync() {
|
async function sync() {
|
||||||
// if navigating the history causes one of the time range to not being equal to all the other ones,
|
// if navigating the history causes one of the time range to not being equal to all the other ones,
|
||||||
// we set syncedTimes to false to avoid inconsistent UI state.
|
// we set syncedTimes to false to avoid inconsistent UI state.
|
||||||
// Ideally `syncedTimes` should be saved in the URL.
|
// Ideally `syncedTimes` should be saved in the URL.
|
||||||
if (Object.values(urlPanes).some(({ range }, _, [{ range: firstRange }]) => !isEqual(range, firstRange))) {
|
if (Object.values(urlState.panes).some(({ range }, _, [{ range: firstRange }]) => !isEqual(range, firstRange))) {
|
||||||
dispatch(syncTimesAction({ syncedTimes: false }));
|
dispatch(syncTimesAction({ syncedTimes: false }));
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const [exploreId, urlPane] of Object.entries(urlPanes) as Array<[ExploreId, ExploreUrlState]>) {
|
Object.entries(urlState.panes).forEach(([exploreId, urlPane], i) => {
|
||||||
const { datasource, queries, range, panelsState } = urlPane;
|
const { datasource, queries, range, panelsState } = urlPane;
|
||||||
|
|
||||||
const statePane = statePanes[exploreId];
|
const paneState = panesState[exploreId];
|
||||||
|
|
||||||
if (statePane !== undefined) {
|
if (paneState !== undefined) {
|
||||||
// TODO: the diff contains panelState updates, but we are currently not handling them.
|
const update = urlDiff(urlPane, getUrlStateFromPaneState(paneState));
|
||||||
const update = urlDiff(urlPane, getUrlStateFromPaneState(statePane));
|
|
||||||
|
|
||||||
Promise.resolve()
|
Promise.resolve()
|
||||||
.then(async () => {
|
.then(async () => {
|
||||||
if (update.datasource) {
|
if (update.datasource && datasource) {
|
||||||
await dispatch(changeDatasource(exploreId, datasource));
|
await dispatch(changeDatasource(exploreId, datasource));
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
@ -146,20 +142,21 @@ export function useStateSync(params: ExploreQueryParams) {
|
|||||||
dispatch(
|
dispatch(
|
||||||
initializeExplore({
|
initializeExplore({
|
||||||
exploreId,
|
exploreId,
|
||||||
datasource,
|
datasource: datasource || '',
|
||||||
queries: withUniqueRefIds(queries),
|
queries: withUniqueRefIds(queries),
|
||||||
range,
|
range,
|
||||||
panelsState,
|
panelsState,
|
||||||
|
position: i,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
|
|
||||||
// Close all the panes that are not in the URL but are still in the store
|
// Close all the panes that are not in the URL but are still in the store
|
||||||
// ie. because the user has navigated back after opening the split view.
|
// ie. because the user has navigated back after opening the split view.
|
||||||
Object.keys(statePanes)
|
Object.keys(panesState)
|
||||||
.filter((keyInStore) => !Object.keys(urlPanes).includes(keyInStore))
|
.filter((keyInStore) => !Object.keys(urlState.panes).includes(keyInStore))
|
||||||
.forEach((paneId) => dispatch(splitClose(paneId as ExploreId)));
|
.forEach((paneId) => dispatch(splitClose(paneId)));
|
||||||
}
|
}
|
||||||
|
|
||||||
// This happens when the user first navigates to explore.
|
// This happens when the user first navigates to explore.
|
||||||
@ -172,11 +169,11 @@ export function useStateSync(params: ExploreQueryParams) {
|
|||||||
dispatch(clearPanes());
|
dispatch(clearPanes());
|
||||||
|
|
||||||
Promise.all(
|
Promise.all(
|
||||||
Object.entries(urlPanes).map(([exploreId, { datasource, queries, range, panelsState }]) => {
|
Object.entries(urlState.panes).map(([exploreId, { datasource, queries, range, panelsState }]) => {
|
||||||
return getPaneDatasource(datasource, queries, orgId, !!exploreMixedDatasource).then(
|
return getPaneDatasource(datasource, queries, orgId, !!exploreMixedDatasource).then(
|
||||||
async (paneDatasource) => {
|
async (paneDatasource) => {
|
||||||
return Promise.resolve(
|
return Promise.resolve(
|
||||||
// In theory, given the Grafana datasource will always be present, this should always be defined.
|
// FIXME: In theory, given the Grafana datasource will always be present, this should always be defined.
|
||||||
paneDatasource
|
paneDatasource
|
||||||
? queries.length
|
? queries.length
|
||||||
? // if we have queries in the URL, we use them
|
? // if we have queries in the URL, we use them
|
||||||
@ -212,7 +209,7 @@ export function useStateSync(params: ExploreQueryParams) {
|
|||||||
.then((queries) => {
|
.then((queries) => {
|
||||||
return dispatch(
|
return dispatch(
|
||||||
initializeExplore({
|
initializeExplore({
|
||||||
exploreId: exploreId as ExploreId,
|
exploreId,
|
||||||
datasource: paneDatasource,
|
datasource: paneDatasource,
|
||||||
queries,
|
queries,
|
||||||
range,
|
range,
|
||||||
@ -224,25 +221,39 @@ export function useStateSync(params: ExploreQueryParams) {
|
|||||||
);
|
);
|
||||||
})
|
})
|
||||||
).then((panes) => {
|
).then((panes) => {
|
||||||
const urlState = panes.reduce<ExploreQueryParams>((acc, { exploreId, state }) => {
|
const newParams = panes.reduce(
|
||||||
return { ...acc, [exploreId]: serializeStateToUrlParam(getUrlStateFromPaneState(state)) };
|
(acc, { exploreId, state }) => {
|
||||||
}, {});
|
return {
|
||||||
|
...acc,
|
||||||
location.partial({ ...urlState, orgId }, true);
|
panes: {
|
||||||
|
...acc.panes,
|
||||||
|
[exploreId]: getUrlStateFromPaneState(state),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
{
|
||||||
|
panes: {},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
initState.current = 'done';
|
initState.current = 'done';
|
||||||
|
|
||||||
|
location.replace({
|
||||||
|
search: Object.entries({
|
||||||
|
panes: JSON.stringify(newParams.panes),
|
||||||
|
schemaVersion: urlState.schemaVersion,
|
||||||
|
orgId,
|
||||||
|
})
|
||||||
|
.map(([key, value]) => `${key}=${value}`)
|
||||||
|
.join('&'),
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
prevParams.current = {
|
prevParams.current = params;
|
||||||
left: params.left,
|
|
||||||
};
|
|
||||||
if (params.right) {
|
|
||||||
prevParams.current.right = params.right;
|
|
||||||
}
|
|
||||||
|
|
||||||
isURLOutOfSync && initState.current === 'done' && sync();
|
isURLOutOfSync && initState.current === 'done' && sync();
|
||||||
}, [params.left, params.right, dispatch, statePanes, exploreMixedDatasource, orgId, location]);
|
}, [dispatch, panesState, exploreMixedDatasource, orgId, location, params]);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getDefaultQuery(ds: DataSourceApi) {
|
function getDefaultQuery(ds: DataSourceApi) {
|
||||||
@ -342,9 +353,6 @@ async function getPaneDatasource(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const isFulfilled = <T>(promise: PromiseSettledResult<T>): promise is PromiseFulfilledResult<T> =>
|
|
||||||
promise.status === 'fulfilled';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Compare 2 explore urls and return a map of what changed. Used to update the local state with all the
|
* Compare 2 explore urls and return a map of what changed. Used to update the local state with all the
|
||||||
* side effects needed.
|
* side effects needed.
|
||||||
@ -396,7 +404,7 @@ function pruneObject(obj: object): object | undefined {
|
|||||||
return pruned;
|
return pruned;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const toRawTimeRange = (range: TimeRange): RawTimeRange => {
|
const toRawTimeRange = (range: TimeRange): RawTimeRange => {
|
||||||
let from = range.raw.from;
|
let from = range.raw.from;
|
||||||
if (isDateTime(from)) {
|
if (isDateTime(from)) {
|
||||||
from = from.valueOf().toString(10);
|
from = from.valueOf().toString(10);
|
@ -0,0 +1,16 @@
|
|||||||
|
import { UrlQueryMap } from '@grafana/data';
|
||||||
|
|
||||||
|
export interface MigrationHandler<From extends BaseExploreURL | never, To> {
|
||||||
|
/**
|
||||||
|
* The parse function is used to parse the URL parameters into the state object.
|
||||||
|
*/
|
||||||
|
parse: (params: UrlQueryMap) => To;
|
||||||
|
/**
|
||||||
|
* the migrate function takes a state object from the previous schema version and returns a new state object
|
||||||
|
*/
|
||||||
|
migrate?: From extends never ? never : (from: From) => To;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BaseExploreURL {
|
||||||
|
schemaVersion: number;
|
||||||
|
}
|
@ -0,0 +1,125 @@
|
|||||||
|
import { DEFAULT_RANGE } from 'app/features/explore/state/utils';
|
||||||
|
|
||||||
|
import { v0Migrator } from './v0';
|
||||||
|
|
||||||
|
describe('v0 migrator', () => {
|
||||||
|
describe('parse', () => {
|
||||||
|
beforeEach(function () {
|
||||||
|
jest.spyOn(console, 'error').mockImplementation(() => void 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
jest.restoreAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns default state on empty string', () => {
|
||||||
|
expect(v0Migrator.parse({})).toMatchObject({
|
||||||
|
left: {
|
||||||
|
datasource: null,
|
||||||
|
queries: [],
|
||||||
|
range: DEFAULT_RANGE,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns a valid Explore state from URL parameter', () => {
|
||||||
|
const paramValue = '{"datasource":"Local","queries":[{"expr":"metric"}],"range":{"from":"now-1h","to":"now"}}';
|
||||||
|
expect(v0Migrator.parse({ left: paramValue })).toMatchObject({
|
||||||
|
left: {
|
||||||
|
datasource: 'Local',
|
||||||
|
queries: [{ expr: 'metric' }],
|
||||||
|
range: {
|
||||||
|
from: 'now-1h',
|
||||||
|
to: 'now',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns a valid Explore state from right URL parameter', () => {
|
||||||
|
const paramValue = '{"datasource":"Local","queries":[{"expr":"metric"}],"range":{"from":"now-1h","to":"now"}}';
|
||||||
|
expect(v0Migrator.parse({ right: paramValue })).toMatchObject({
|
||||||
|
right: {
|
||||||
|
datasource: 'Local',
|
||||||
|
queries: [{ expr: 'metric' }],
|
||||||
|
range: {
|
||||||
|
from: 'now-1h',
|
||||||
|
to: 'now',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns a default state from invalid right URL parameter', () => {
|
||||||
|
const paramValue = 10;
|
||||||
|
expect(v0Migrator.parse({ right: paramValue })).toMatchObject({
|
||||||
|
right: {
|
||||||
|
datasource: null,
|
||||||
|
queries: [],
|
||||||
|
range: DEFAULT_RANGE,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns a valid Explore state from a compact URL parameter', () => {
|
||||||
|
const paramValue =
|
||||||
|
'["now-1h","now","Local",{"expr":"metric"},{"ui":[true,true,true,"none"],"__panelsState":{"logs":"1"}}]';
|
||||||
|
expect(v0Migrator.parse({ left: paramValue })).toMatchObject({
|
||||||
|
left: {
|
||||||
|
datasource: 'Local',
|
||||||
|
queries: [{ expr: 'metric' }],
|
||||||
|
range: {
|
||||||
|
from: 'now-1h',
|
||||||
|
to: 'now',
|
||||||
|
},
|
||||||
|
panelsState: {
|
||||||
|
logs: '1',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns default state on compact URLs with too few segments ', () => {
|
||||||
|
const paramValue = '["now-1h",{"expr":"metric"},{"ui":[true,true,true,"none"]}]';
|
||||||
|
expect(v0Migrator.parse({ left: paramValue })).toMatchObject({
|
||||||
|
left: {
|
||||||
|
datasource: null,
|
||||||
|
queries: [],
|
||||||
|
range: DEFAULT_RANGE,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(console.error).toHaveBeenCalledWith('Error parsing compact URL state for Explore.');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not return a query for mode in the url', () => {
|
||||||
|
// Previous versions of Grafana included "Explore mode" in the URL; this should not be treated as a query.
|
||||||
|
const paramValue =
|
||||||
|
'["now-1h","now","x-ray-datasource",{"queryType":"getTraceSummaries"},{"mode":"Metrics"},{"ui":[true,true,true,"none"]}]';
|
||||||
|
expect(v0Migrator.parse({ left: paramValue })).toMatchObject({
|
||||||
|
left: {
|
||||||
|
datasource: 'x-ray-datasource',
|
||||||
|
queries: [{ queryType: 'getTraceSummaries' }],
|
||||||
|
range: {
|
||||||
|
from: 'now-1h',
|
||||||
|
to: 'now',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return queries if queryType is present in the url', () => {
|
||||||
|
const paramValue =
|
||||||
|
'["now-1h","now","x-ray-datasource",{"queryType":"getTraceSummaries"},{"ui":[true,true,true,"none"]}]';
|
||||||
|
expect(v0Migrator.parse({ left: paramValue })).toMatchObject({
|
||||||
|
left: {
|
||||||
|
datasource: 'x-ray-datasource',
|
||||||
|
queries: [{ queryType: 'getTraceSummaries' }],
|
||||||
|
range: {
|
||||||
|
from: 'now-1h',
|
||||||
|
to: 'now',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,66 @@
|
|||||||
|
import { ExploreUrlState } from '@grafana/data';
|
||||||
|
import { safeParseJson } from 'app/core/utils/explore';
|
||||||
|
import { DEFAULT_RANGE } from 'app/features/explore/state/utils';
|
||||||
|
|
||||||
|
import { BaseExploreURL, MigrationHandler } from './types';
|
||||||
|
|
||||||
|
export interface ExploreURLV0 extends BaseExploreURL {
|
||||||
|
schemaVersion: 0;
|
||||||
|
left: ExploreUrlState;
|
||||||
|
right?: ExploreUrlState;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const v0Migrator: MigrationHandler<never, ExploreURLV0> = {
|
||||||
|
parse: (params) => {
|
||||||
|
return {
|
||||||
|
schemaVersion: 0,
|
||||||
|
left: parseUrlState(typeof params.left === 'string' ? params.left : undefined),
|
||||||
|
...(params.right && {
|
||||||
|
right: parseUrlState(typeof params.right === 'string' ? params.right : undefined),
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const isSegment = (segment: { [key: string]: string }, ...props: string[]) =>
|
||||||
|
props.some((prop) => segment.hasOwnProperty(prop));
|
||||||
|
|
||||||
|
enum ParseUrlStateIndex {
|
||||||
|
RangeFrom = 0,
|
||||||
|
RangeTo = 1,
|
||||||
|
Datasource = 2,
|
||||||
|
SegmentsStart = 3,
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseUrlState(initial: string | undefined): ExploreUrlState {
|
||||||
|
const parsed = safeParseJson(initial);
|
||||||
|
const errorResult = {
|
||||||
|
datasource: null,
|
||||||
|
queries: [],
|
||||||
|
range: DEFAULT_RANGE,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!parsed) {
|
||||||
|
return errorResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Array.isArray(parsed)) {
|
||||||
|
return { queries: [], range: DEFAULT_RANGE, ...parsed };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parsed.length <= ParseUrlStateIndex.SegmentsStart) {
|
||||||
|
console.error('Error parsing compact URL state for Explore.');
|
||||||
|
return errorResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
const range = {
|
||||||
|
from: parsed[ParseUrlStateIndex.RangeFrom],
|
||||||
|
to: parsed[ParseUrlStateIndex.RangeTo],
|
||||||
|
};
|
||||||
|
const datasource = parsed[ParseUrlStateIndex.Datasource];
|
||||||
|
const parsedSegments = parsed.slice(ParseUrlStateIndex.SegmentsStart);
|
||||||
|
const queries = parsedSegments.filter((segment) => !isSegment(segment, 'ui', 'mode', '__panelsState'));
|
||||||
|
|
||||||
|
const panelsState = parsedSegments.find((segment) => isSegment(segment, '__panelsState'))?.__panelsState;
|
||||||
|
return { datasource, queries, range, panelsState };
|
||||||
|
}
|
@ -0,0 +1,118 @@
|
|||||||
|
import { DEFAULT_RANGE } from 'app/features/explore/state/utils';
|
||||||
|
|
||||||
|
import { v1Migrator } from './v1';
|
||||||
|
|
||||||
|
jest.mock('app/core/utils/explore', () => ({
|
||||||
|
...jest.requireActual('app/core/utils/explore'),
|
||||||
|
generateExploreId: () => 'ID',
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe('v1 migrator', () => {
|
||||||
|
describe('parse', () => {
|
||||||
|
beforeEach(function () {
|
||||||
|
jest.spyOn(console, 'error').mockImplementation(() => void 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
jest.restoreAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('correctly returns default state when no params are provided', () => {
|
||||||
|
expect(v1Migrator.parse({})).toMatchObject({
|
||||||
|
panes: {
|
||||||
|
ID: {
|
||||||
|
datasource: null,
|
||||||
|
queries: [],
|
||||||
|
range: DEFAULT_RANGE,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('correctly returns default state when panes param is an empty object', () => {
|
||||||
|
expect(v1Migrator.parse({ panes: '{}' })).toMatchObject({
|
||||||
|
panes: {
|
||||||
|
ID: {
|
||||||
|
datasource: null,
|
||||||
|
queries: [],
|
||||||
|
range: DEFAULT_RANGE,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('correctly returns default state when panes param is not a valid JSON object', () => {
|
||||||
|
expect(v1Migrator.parse({ panes: '{a malformed json}' })).toMatchObject({
|
||||||
|
panes: {
|
||||||
|
ID: {
|
||||||
|
datasource: null,
|
||||||
|
queries: [],
|
||||||
|
range: DEFAULT_RANGE,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(console.error).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('correctly returns default state when a pane in panes params is an empty object', () => {
|
||||||
|
expect(v1Migrator.parse({ panes: '{"aaa": {}}' })).toMatchObject({
|
||||||
|
panes: {
|
||||||
|
aaa: {
|
||||||
|
datasource: null,
|
||||||
|
queries: [],
|
||||||
|
range: DEFAULT_RANGE,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('correctly returns default state when a pane in panes params is not a valid JSON object', () => {
|
||||||
|
expect(v1Migrator.parse({ panes: '{"aaa": "NOT A VALID URL STATE"}' })).toMatchObject({
|
||||||
|
panes: {
|
||||||
|
aaa: {
|
||||||
|
datasource: null,
|
||||||
|
queries: [],
|
||||||
|
range: DEFAULT_RANGE,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('correctly parses state', () => {
|
||||||
|
expect(
|
||||||
|
v1Migrator.parse({
|
||||||
|
panes: `{
|
||||||
|
"aaa": {
|
||||||
|
"datasource": "my-ds",
|
||||||
|
"queries": [
|
||||||
|
{
|
||||||
|
"refId": "A"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"range": {
|
||||||
|
"from": "now",
|
||||||
|
"to": "now-5m"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}`,
|
||||||
|
})
|
||||||
|
).toMatchObject({
|
||||||
|
panes: {
|
||||||
|
aaa: {
|
||||||
|
datasource: 'my-ds',
|
||||||
|
queries: [{ refId: 'A' }],
|
||||||
|
range: {
|
||||||
|
from: 'now',
|
||||||
|
to: 'now-5m',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('migrate', () => {
|
||||||
|
// TODO: implement
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,89 @@
|
|||||||
|
import { ExploreUrlState } from '@grafana/data';
|
||||||
|
import { generateExploreId, safeParseJson } from 'app/core/utils/explore';
|
||||||
|
import { DEFAULT_RANGE } from 'app/features/explore/state/utils';
|
||||||
|
|
||||||
|
import { hasKey } from '../../utils';
|
||||||
|
|
||||||
|
import { BaseExploreURL, MigrationHandler } from './types';
|
||||||
|
import { ExploreURLV0 } from './v0';
|
||||||
|
|
||||||
|
export interface ExploreURLV1 extends BaseExploreURL {
|
||||||
|
schemaVersion: 1;
|
||||||
|
panes: {
|
||||||
|
[id: string]: ExploreUrlState;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const v1Migrator: MigrationHandler<ExploreURLV0, ExploreURLV1> = {
|
||||||
|
parse: (params) => {
|
||||||
|
if (!params || !params.panes || typeof params.panes !== 'string') {
|
||||||
|
return {
|
||||||
|
schemaVersion: 1,
|
||||||
|
panes: {
|
||||||
|
[generateExploreId()]: DEFAULT_STATE,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const rawPanes: Record<string, unknown> = safeParseJson(params.panes) || {};
|
||||||
|
|
||||||
|
const panes = Object.entries(rawPanes)
|
||||||
|
.map(([key, value]) => [key, applyDefaults(value)] as const)
|
||||||
|
.reduce<Record<string, ExploreUrlState>>((acc, [key, value]) => {
|
||||||
|
return {
|
||||||
|
...acc,
|
||||||
|
[key]: value,
|
||||||
|
};
|
||||||
|
}, {});
|
||||||
|
|
||||||
|
if (!Object.keys(panes).length) {
|
||||||
|
panes[generateExploreId()] = DEFAULT_STATE;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
schemaVersion: 1,
|
||||||
|
panes,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
migrate: (params) => {
|
||||||
|
return {
|
||||||
|
schemaVersion: 1,
|
||||||
|
panes: {
|
||||||
|
[generateExploreId()]: params.left,
|
||||||
|
...(params.right && { [generateExploreId()]: params.right }),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const DEFAULT_STATE: ExploreUrlState = {
|
||||||
|
datasource: null,
|
||||||
|
queries: [],
|
||||||
|
range: DEFAULT_RANGE,
|
||||||
|
};
|
||||||
|
|
||||||
|
function applyDefaults(input: unknown): ExploreUrlState {
|
||||||
|
if (!input || typeof input !== 'object') {
|
||||||
|
return DEFAULT_STATE;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...DEFAULT_STATE,
|
||||||
|
// queries
|
||||||
|
...(hasKey('queries', input) && Array.isArray(input.queries) && { queries: input.queries }),
|
||||||
|
// datasource
|
||||||
|
...(hasKey('datasource', input) && typeof input.datasource === 'string' && { datasource: input.datasource }),
|
||||||
|
// panelsState
|
||||||
|
...(hasKey('panelsState', input) &&
|
||||||
|
!!input.panelsState &&
|
||||||
|
typeof input.panelsState === 'object' && { panelsState: input.panelsState }),
|
||||||
|
// range
|
||||||
|
...(hasKey('range', input) &&
|
||||||
|
!!input.range &&
|
||||||
|
typeof input.range === 'object' &&
|
||||||
|
hasKey('from', input.range) &&
|
||||||
|
hasKey('to', input.range) &&
|
||||||
|
typeof input.range.from === 'string' &&
|
||||||
|
typeof input.range.to === 'string' && { range: { from: input.range.from, to: input.range.to } }),
|
||||||
|
};
|
||||||
|
}
|
44
public/app/features/explore/hooks/useStateSync/parseURL.ts
Normal file
44
public/app/features/explore/hooks/useStateSync/parseURL.ts
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
import { ExploreQueryParams } from 'app/types';
|
||||||
|
|
||||||
|
import { v0Migrator } from './migrators/v0';
|
||||||
|
import { ExploreURLV1, v1Migrator } from './migrators/v1';
|
||||||
|
|
||||||
|
type ExploreURL = ExploreURLV1;
|
||||||
|
|
||||||
|
export const parseURL = (params: ExploreQueryParams) => {
|
||||||
|
return migrate(params);
|
||||||
|
};
|
||||||
|
|
||||||
|
const migrators = [v0Migrator, v1Migrator] as const;
|
||||||
|
|
||||||
|
const migrate = (params: ExploreQueryParams): ExploreURL => {
|
||||||
|
const schemaVersion = getSchemaVersion(params);
|
||||||
|
|
||||||
|
const [parser, ...migratorsToRun] = migrators.slice(schemaVersion);
|
||||||
|
|
||||||
|
const parsedUrl = parser.parse(params);
|
||||||
|
|
||||||
|
// @ts-expect-error
|
||||||
|
const final: ExploreURL = migratorsToRun.reduce((acc, migrator) => {
|
||||||
|
// @ts-expect-error
|
||||||
|
return migrator.migrate ? migrator.migrate(acc) : acc;
|
||||||
|
}, parsedUrl);
|
||||||
|
|
||||||
|
return final;
|
||||||
|
};
|
||||||
|
|
||||||
|
function getSchemaVersion(params: ExploreQueryParams): number {
|
||||||
|
if (!params || !('schemaVersion' in params) || !params.schemaVersion) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof params.schemaVersion === 'number') {
|
||||||
|
return params.schemaVersion;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof params.schemaVersion === 'string') {
|
||||||
|
return Number.parseInt(params.schemaVersion, 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
@ -1,23 +0,0 @@
|
|||||||
import { useEffect, useRef } from 'react';
|
|
||||||
|
|
||||||
import { stopQueryState } from 'app/core/utils/explore';
|
|
||||||
import { useSelector } from 'app/types';
|
|
||||||
|
|
||||||
import { selectPanes } from '../state/selectors';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Unsubscribe from queries when unmounting.
|
|
||||||
* This avoids unnecessary state changes when navigating away from Explore.
|
|
||||||
*/
|
|
||||||
export function useStopQueries() {
|
|
||||||
const panesRef = useRef<ReturnType<typeof selectPanes>>({});
|
|
||||||
panesRef.current = useSelector(selectPanes);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
return () => {
|
|
||||||
for (const [, pane] of Object.entries(panesRef.current)) {
|
|
||||||
stopQueryState(pane.querySubscription);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
}
|
|
45
public/app/features/explore/hooks/useTimeSrvFix.test.tsx
Normal file
45
public/app/features/explore/hooks/useTimeSrvFix.test.tsx
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
import { renderHook, waitFor } from '@testing-library/react';
|
||||||
|
import { createMemoryHistory } from 'history';
|
||||||
|
import { stringify } from 'querystring';
|
||||||
|
import React from 'react';
|
||||||
|
import { TestProvider } from 'test/helpers/TestProvider';
|
||||||
|
import { getGrafanaContextMock } from 'test/mocks/getGrafanaContextMock';
|
||||||
|
|
||||||
|
import { HistoryWrapper } from '@grafana/runtime';
|
||||||
|
|
||||||
|
import { useTimeSrvFix } from './useTimeSrvFix';
|
||||||
|
|
||||||
|
describe('useTimeSrvFix', () => {
|
||||||
|
it('removes `from` and `to` parameters from url when first mounted', async () => {
|
||||||
|
const history = createMemoryHistory({
|
||||||
|
initialEntries: [{ pathname: '/explore', search: stringify({ from: '1', to: '2' }) }],
|
||||||
|
});
|
||||||
|
|
||||||
|
const location = new HistoryWrapper(history);
|
||||||
|
|
||||||
|
const context = getGrafanaContextMock();
|
||||||
|
|
||||||
|
renderHook(() => useTimeSrvFix(), {
|
||||||
|
wrapper: ({ children }) => (
|
||||||
|
<TestProvider
|
||||||
|
grafanaContext={{
|
||||||
|
...context,
|
||||||
|
location,
|
||||||
|
config: {
|
||||||
|
...context.config,
|
||||||
|
featureToggles: {
|
||||||
|
exploreMixedDatasource: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</TestProvider>
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(location.getSearchObject()).toEqual(expect.not.objectContaining({ from: '1', to: '2' }));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@ -15,6 +15,7 @@ import { useGrafana } from 'app/core/context/GrafanaContext';
|
|||||||
*/
|
*/
|
||||||
export function useTimeSrvFix() {
|
export function useTimeSrvFix() {
|
||||||
const { location } = useGrafana();
|
const { location } = useGrafana();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const searchParams = location.getSearchObject();
|
const searchParams = location.getSearchObject();
|
||||||
if (searchParams.from || searchParams.to) {
|
if (searchParams.from || searchParams.to) {
|
||||||
|
7
public/app/features/explore/hooks/utils.ts
Normal file
7
public/app/features/explore/hooks/utils.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
export const isFulfilled = <T>(promise: PromiseSettledResult<T>): promise is PromiseFulfilledResult<T> =>
|
||||||
|
promise.status === 'fulfilled';
|
||||||
|
|
||||||
|
// TS<5 does not support `in` operator for type narrowing. once we upgrade to TS5, we can remove this function and just use the in operator instead.
|
||||||
|
export function hasKey<K extends string, T extends object>(k: K, o: T): o is T & Record<K, unknown> {
|
||||||
|
return k in o;
|
||||||
|
}
|
@ -1,7 +1,5 @@
|
|||||||
import { screen, waitFor } from '@testing-library/react';
|
import { screen, waitFor } from '@testing-library/react';
|
||||||
|
|
||||||
import { serializeStateToUrlParam } from '@grafana/data';
|
|
||||||
|
|
||||||
import { changeDatasource } from './helper/interactions';
|
import { changeDatasource } from './helper/interactions';
|
||||||
import { makeLogsQueryResponse } from './helper/query';
|
import { makeLogsQueryResponse } from './helper/query';
|
||||||
import { setupExplore, tearDown, waitForExplore } from './helper/setup';
|
import { setupExplore, tearDown, waitForExplore } from './helper/setup';
|
||||||
@ -15,25 +13,14 @@ describe('Explore: handle datasource states', () => {
|
|||||||
await waitFor(() => screen.getByText(/Explore requires at least one data source/i));
|
await waitFor(() => screen.getByText(/Explore requires at least one data source/i));
|
||||||
});
|
});
|
||||||
|
|
||||||
it('handles changing the datasource manually', async () => {
|
it('handles datasource changes', async () => {
|
||||||
const urlParams = { left: JSON.stringify(['now-1h', 'now', 'loki', { expr: '{ label="value"}', refId: 'A' }]) };
|
const urlParams = { left: JSON.stringify(['now-1h', 'now', 'loki', { expr: '{ label="value"}', refId: 'A' }]) };
|
||||||
const { datasources, location } = setupExplore({ urlParams });
|
const { datasources } = setupExplore({ urlParams });
|
||||||
jest.mocked(datasources.loki.query).mockReturnValueOnce(makeLogsQueryResponse());
|
jest.mocked(datasources.loki.query).mockReturnValueOnce(makeLogsQueryResponse());
|
||||||
await waitForExplore();
|
await waitForExplore();
|
||||||
await changeDatasource('elastic');
|
await changeDatasource('elastic');
|
||||||
|
|
||||||
await screen.findByText('elastic Editor input:');
|
await screen.findByText('elastic Editor input:');
|
||||||
expect(datasources.elastic.query).not.toBeCalled();
|
expect(datasources.elastic.query).not.toBeCalled();
|
||||||
|
|
||||||
await waitFor(async () => {
|
|
||||||
expect(location.getSearchObject()).toEqual({
|
|
||||||
orgId: '1',
|
|
||||||
left: serializeStateToUrlParam({
|
|
||||||
datasource: 'elastic-uid',
|
|
||||||
queries: [{ refId: 'A', datasource: { type: 'logs', uid: 'elastic-uid' } }],
|
|
||||||
range: { from: 'now-1h', to: 'now' },
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -1,10 +1,8 @@
|
|||||||
import { waitFor } from '@testing-library/react';
|
import { waitFor } from '@testing-library/react';
|
||||||
|
|
||||||
import { ExploreId } from '../../../../types';
|
|
||||||
|
|
||||||
import { withinExplore } from './setup';
|
import { withinExplore } from './setup';
|
||||||
|
|
||||||
export const assertQueryHistoryExists = async (query: string, exploreId: ExploreId = ExploreId.left) => {
|
export const assertQueryHistoryExists = async (query: string, exploreId = 'left') => {
|
||||||
const selector = withinExplore(exploreId);
|
const selector = withinExplore(exploreId);
|
||||||
|
|
||||||
expect(await selector.findByText('1 queries')).toBeInTheDocument();
|
expect(await selector.findByText('1 queries')).toBeInTheDocument();
|
||||||
@ -12,7 +10,7 @@ export const assertQueryHistoryExists = async (query: string, exploreId: Explore
|
|||||||
expect(queryItem).toHaveTextContent(query);
|
expect(queryItem).toHaveTextContent(query);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const assertQueryHistory = async (expectedQueryTexts: string[], exploreId: ExploreId = ExploreId.left) => {
|
export const assertQueryHistory = async (expectedQueryTexts: string[], exploreId = 'left') => {
|
||||||
const selector = withinExplore(exploreId);
|
const selector = withinExplore(exploreId);
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(selector.getByText(new RegExp(`${expectedQueryTexts.length} queries`))).toBeInTheDocument();
|
expect(selector.getByText(new RegExp(`${expectedQueryTexts.length} queries`))).toBeInTheDocument();
|
||||||
@ -23,10 +21,7 @@ export const assertQueryHistory = async (expectedQueryTexts: string[], exploreId
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const assertQueryHistoryComment = async (
|
export const assertQueryHistoryComment = async (expectedQueryComments: string[], exploreId = 'left') => {
|
||||||
expectedQueryComments: string[],
|
|
||||||
exploreId: ExploreId = ExploreId.left
|
|
||||||
) => {
|
|
||||||
const selector = withinExplore(exploreId);
|
const selector = withinExplore(exploreId);
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(selector.getByText(new RegExp(`${expectedQueryComments.length} queries`))).toBeInTheDocument();
|
expect(selector.getByText(new RegExp(`${expectedQueryComments.length} queries`))).toBeInTheDocument();
|
||||||
@ -37,7 +32,7 @@ export const assertQueryHistoryComment = async (
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const assertQueryHistoryIsStarred = async (expectedStars: boolean[], exploreId: ExploreId = ExploreId.left) => {
|
export const assertQueryHistoryIsStarred = async (expectedStars: boolean[], exploreId = 'left') => {
|
||||||
const selector = withinExplore(exploreId);
|
const selector = withinExplore(exploreId);
|
||||||
const starButtons = selector.getAllByRole('button', { name: /Star query|Unstar query/ });
|
const starButtons = selector.getAllByRole('button', { name: /Star query|Unstar query/ });
|
||||||
await waitFor(() =>
|
await waitFor(() =>
|
||||||
@ -49,12 +44,12 @@ export const assertQueryHistoryIsStarred = async (expectedStars: boolean[], expl
|
|||||||
|
|
||||||
export const assertQueryHistoryTabIsSelected = (
|
export const assertQueryHistoryTabIsSelected = (
|
||||||
tabName: 'Query history' | 'Starred' | 'Settings',
|
tabName: 'Query history' | 'Starred' | 'Settings',
|
||||||
exploreId: ExploreId = ExploreId.left
|
exploreId = 'left'
|
||||||
) => {
|
) => {
|
||||||
expect(withinExplore(exploreId).getByRole('tab', { name: `Tab ${tabName}`, selected: true })).toBeInTheDocument();
|
expect(withinExplore(exploreId).getByRole('tab', { name: `Tab ${tabName}`, selected: true })).toBeInTheDocument();
|
||||||
};
|
};
|
||||||
|
|
||||||
export const assertDataSourceFilterVisibility = (visible: boolean, exploreId: ExploreId = ExploreId.left) => {
|
export const assertDataSourceFilterVisibility = (visible: boolean, exploreId = 'left') => {
|
||||||
const filterInput = withinExplore(exploreId).queryByLabelText('Filter queries for data sources(s)');
|
const filterInput = withinExplore(exploreId).queryByLabelText('Filter queries for data sources(s)');
|
||||||
if (visible) {
|
if (visible) {
|
||||||
expect(filterInput).toBeInTheDocument();
|
expect(filterInput).toBeInTheDocument();
|
||||||
@ -63,14 +58,10 @@ export const assertDataSourceFilterVisibility = (visible: boolean, exploreId: Ex
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const assertQueryHistoryElementsShown = (
|
export const assertQueryHistoryElementsShown = (shown: number, total: number, exploreId = 'left') => {
|
||||||
shown: number,
|
|
||||||
total: number,
|
|
||||||
exploreId: ExploreId = ExploreId.left
|
|
||||||
) => {
|
|
||||||
expect(withinExplore(exploreId).queryByText(`Showing ${shown} of ${total}`)).toBeInTheDocument();
|
expect(withinExplore(exploreId).queryByText(`Showing ${shown} of ${total}`)).toBeInTheDocument();
|
||||||
};
|
};
|
||||||
|
|
||||||
export const assertLoadMoreQueryHistoryNotVisible = (exploreId: ExploreId = ExploreId.left) => {
|
export const assertLoadMoreQueryHistoryNotVisible = (exploreId = 'left') => {
|
||||||
expect(withinExplore(exploreId).queryByRole('button', { name: 'Load more' })).not.toBeInTheDocument();
|
expect(withinExplore(exploreId).queryByRole('button', { name: 'Load more' })).not.toBeInTheDocument();
|
||||||
};
|
};
|
||||||
|
@ -3,8 +3,6 @@ import userEvent from '@testing-library/user-event';
|
|||||||
|
|
||||||
import { selectors } from '@grafana/e2e-selectors';
|
import { selectors } from '@grafana/e2e-selectors';
|
||||||
|
|
||||||
import { ExploreId } from '../../../../types';
|
|
||||||
|
|
||||||
import { withinExplore } from './setup';
|
import { withinExplore } from './setup';
|
||||||
|
|
||||||
export const changeDatasource = async (name: string) => {
|
export const changeDatasource = async (name: string) => {
|
||||||
@ -14,59 +12,52 @@ export const changeDatasource = async (name: string) => {
|
|||||||
fireEvent.click(option);
|
fireEvent.click(option);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const inputQuery = async (query: string, exploreId: ExploreId = ExploreId.left) => {
|
export const inputQuery = async (query: string, exploreId = 'left') => {
|
||||||
const input = withinExplore(exploreId).getByRole('textbox', { name: 'query' });
|
const input = withinExplore(exploreId).getByRole('textbox', { name: 'query' });
|
||||||
await userEvent.clear(input);
|
await userEvent.clear(input);
|
||||||
await userEvent.type(input, query);
|
await userEvent.type(input, query);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const runQuery = async (exploreId: ExploreId = ExploreId.left) => {
|
export const runQuery = async (exploreId = 'left') => {
|
||||||
const explore = withinExplore(exploreId);
|
const explore = withinExplore(exploreId);
|
||||||
const toolbar = within(explore.getByLabelText('Explore toolbar'));
|
const toolbar = within(explore.getByLabelText('Explore toolbar'));
|
||||||
const button = toolbar.getByRole('button', { name: /run query/i });
|
const button = toolbar.getByRole('button', { name: /run query/i });
|
||||||
await userEvent.click(button);
|
await userEvent.click(button);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const openQueryHistory = async (exploreId: ExploreId = ExploreId.left) => {
|
export const openQueryHistory = async (exploreId = 'left') => {
|
||||||
const selector = withinExplore(exploreId);
|
const selector = withinExplore(exploreId);
|
||||||
const button = selector.getByRole('button', { name: 'Rich history button' });
|
const button = selector.getByRole('button', { name: 'Rich history button' });
|
||||||
await userEvent.click(button);
|
await userEvent.click(button);
|
||||||
expect(await selector.findByPlaceholderText('Search queries')).toBeInTheDocument();
|
expect(await selector.findByPlaceholderText('Search queries')).toBeInTheDocument();
|
||||||
};
|
};
|
||||||
|
|
||||||
export const closeQueryHistory = async (exploreId: ExploreId = ExploreId.left) => {
|
export const closeQueryHistory = async (exploreId = 'left') => {
|
||||||
const closeButton = withinExplore(exploreId).getByRole('button', { name: 'Close query history' });
|
const closeButton = withinExplore(exploreId).getByRole('button', { name: 'Close query history' });
|
||||||
await userEvent.click(closeButton);
|
await userEvent.click(closeButton);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const switchToQueryHistoryTab = async (
|
export const switchToQueryHistoryTab = async (name: 'Settings' | 'Query History', exploreId = 'left') => {
|
||||||
name: 'Settings' | 'Query History',
|
|
||||||
exploreId: ExploreId = ExploreId.left
|
|
||||||
) => {
|
|
||||||
await userEvent.click(withinExplore(exploreId).getByRole('tab', { name: `Tab ${name}` }));
|
await userEvent.click(withinExplore(exploreId).getByRole('tab', { name: `Tab ${name}` }));
|
||||||
};
|
};
|
||||||
|
|
||||||
export const selectStarredTabFirst = async (exploreId: ExploreId = ExploreId.left) => {
|
export const selectStarredTabFirst = async (exploreId = 'left') => {
|
||||||
const checkbox = withinExplore(exploreId).getByRole('checkbox', {
|
const checkbox = withinExplore(exploreId).getByRole('checkbox', {
|
||||||
name: /Change the default active tab from “Query history” to “Starred”/,
|
name: /Change the default active tab from “Query history” to “Starred”/,
|
||||||
});
|
});
|
||||||
await userEvent.click(checkbox);
|
await userEvent.click(checkbox);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const selectOnlyActiveDataSource = async (exploreId: ExploreId = ExploreId.left) => {
|
export const selectOnlyActiveDataSource = async (exploreId = 'left') => {
|
||||||
const checkbox = withinExplore(exploreId).getByLabelText(/Only show queries for data source currently active.*/);
|
const checkbox = withinExplore(exploreId).getByLabelText(/Only show queries for data source currently active.*/);
|
||||||
await userEvent.click(checkbox);
|
await userEvent.click(checkbox);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const starQueryHistory = async (queryIndex: number, exploreId: ExploreId = ExploreId.left) => {
|
export const starQueryHistory = async (queryIndex: number, exploreId = 'left') => {
|
||||||
await invokeAction(queryIndex, 'Star query', exploreId);
|
await invokeAction(queryIndex, 'Star query', exploreId);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const commentQueryHistory = async (
|
export const commentQueryHistory = async (queryIndex: number, comment: string, exploreId = 'left') => {
|
||||||
queryIndex: number,
|
|
||||||
comment: string,
|
|
||||||
exploreId: ExploreId = ExploreId.left
|
|
||||||
) => {
|
|
||||||
await invokeAction(queryIndex, 'Add comment', exploreId);
|
await invokeAction(queryIndex, 'Add comment', exploreId);
|
||||||
const input = withinExplore(exploreId).getByPlaceholderText('An optional description of what the query does.');
|
const input = withinExplore(exploreId).getByPlaceholderText('An optional description of what the query does.');
|
||||||
await userEvent.clear(input);
|
await userEvent.clear(input);
|
||||||
@ -74,16 +65,16 @@ export const commentQueryHistory = async (
|
|||||||
await invokeAction(queryIndex, 'Save comment', exploreId);
|
await invokeAction(queryIndex, 'Save comment', exploreId);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const deleteQueryHistory = async (queryIndex: number, exploreId: ExploreId = ExploreId.left) => {
|
export const deleteQueryHistory = async (queryIndex: number, exploreId = 'left') => {
|
||||||
await invokeAction(queryIndex, 'Delete query', exploreId);
|
await invokeAction(queryIndex, 'Delete query', exploreId);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const loadMoreQueryHistory = async (exploreId: ExploreId = ExploreId.left) => {
|
export const loadMoreQueryHistory = async (exploreId = 'left') => {
|
||||||
const button = withinExplore(exploreId).getByRole('button', { name: 'Load more' });
|
const button = withinExplore(exploreId).getByRole('button', { name: 'Load more' });
|
||||||
await userEvent.click(button);
|
await userEvent.click(button);
|
||||||
};
|
};
|
||||||
|
|
||||||
const invokeAction = async (queryIndex: number, actionAccessibleName: string, exploreId: ExploreId) => {
|
const invokeAction = async (queryIndex: number, actionAccessibleName: string, exploreId: string) => {
|
||||||
const selector = withinExplore(exploreId);
|
const selector = withinExplore(exploreId);
|
||||||
const buttons = selector.getAllByRole('button', { name: actionAccessibleName });
|
const buttons = selector.getAllByRole('button', { name: actionAccessibleName });
|
||||||
await userEvent.click(buttons[queryIndex]);
|
await userEvent.click(buttons[queryIndex]);
|
||||||
|
@ -33,7 +33,7 @@ import { configureStore } from 'app/store/configureStore';
|
|||||||
|
|
||||||
import { LokiDatasource } from '../../../../plugins/datasource/loki/datasource';
|
import { LokiDatasource } from '../../../../plugins/datasource/loki/datasource';
|
||||||
import { LokiQuery } from '../../../../plugins/datasource/loki/types';
|
import { LokiQuery } from '../../../../plugins/datasource/loki/types';
|
||||||
import { ExploreId, ExploreQueryParams } from '../../../../types';
|
import { ExploreQueryParams } from '../../../../types';
|
||||||
import { initialUserState } from '../../../profile/state/reducers';
|
import { initialUserState } from '../../../profile/state/reducers';
|
||||||
import ExplorePage from '../../ExplorePage';
|
import ExplorePage from '../../ExplorePage';
|
||||||
|
|
||||||
@ -42,7 +42,7 @@ type DatasourceSetup = { settings: DataSourceInstanceSettings; api: DataSourceAp
|
|||||||
type SetupOptions = {
|
type SetupOptions = {
|
||||||
clearLocalStorage?: boolean;
|
clearLocalStorage?: boolean;
|
||||||
datasources?: DatasourceSetup[];
|
datasources?: DatasourceSetup[];
|
||||||
urlParams?: ExploreQueryParams & { [key: string]: string };
|
urlParams?: ExploreQueryParams;
|
||||||
prevUsedDatasource?: { orgId: number; datasource: string };
|
prevUsedDatasource?: { orgId: number; datasource: string };
|
||||||
mixedEnabled?: boolean;
|
mixedEnabled?: boolean;
|
||||||
};
|
};
|
||||||
@ -166,7 +166,7 @@ export function setupExplore(options?: SetupOptions): {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function makeDatasourceSetup({
|
export function makeDatasourceSetup({
|
||||||
name = 'loki',
|
name = 'loki',
|
||||||
id = 1,
|
id = 1,
|
||||||
uid: uidOverride,
|
uid: uidOverride,
|
||||||
@ -232,10 +232,10 @@ function makeDatasourceSetup({
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export const waitForExplore = (exploreId: ExploreId = ExploreId.left) => {
|
export const waitForExplore = (exploreId = 'left') => {
|
||||||
return waitFor(async () => {
|
return waitFor(async () => {
|
||||||
const container = screen.getAllByTestId('data-testid Explore');
|
const container = screen.getAllByTestId('data-testid Explore');
|
||||||
return within(container[exploreId === ExploreId.left ? 0 : 1]);
|
return within(container[exploreId === 'left' ? 0 : 1]);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -243,7 +243,7 @@ export const tearDown = () => {
|
|||||||
window.localStorage.clear();
|
window.localStorage.clear();
|
||||||
};
|
};
|
||||||
|
|
||||||
export const withinExplore = (exploreId: ExploreId) => {
|
export const withinExplore = (exploreId: string) => {
|
||||||
const container = screen.getAllByTestId('data-testid Explore');
|
const container = screen.getAllByTestId('data-testid Explore');
|
||||||
return within(container[exploreId === ExploreId.left ? 0 : 1]);
|
return within(container[exploreId === 'left' ? 0 : 1]);
|
||||||
};
|
};
|
||||||
|
@ -1,32 +1,22 @@
|
|||||||
import { act, screen } from '@testing-library/react';
|
import { screen } from '@testing-library/react';
|
||||||
|
|
||||||
import { serializeStateToUrlParam } from '@grafana/data';
|
import { serializeStateToUrlParam } from '@grafana/data';
|
||||||
import { locationService } from '@grafana/runtime';
|
|
||||||
|
|
||||||
import { makeLogsQueryResponse, makeMetricsQueryResponse } from './helper/query';
|
import { makeLogsQueryResponse } from './helper/query';
|
||||||
import { setupExplore, tearDown, waitForExplore } from './helper/setup';
|
import { setupExplore, tearDown, waitForExplore } from './helper/setup';
|
||||||
|
|
||||||
describe('Explore: handle running/not running query', () => {
|
describe('Explore: handle running/not running query', () => {
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
tearDown();
|
tearDown();
|
||||||
});
|
});
|
||||||
it('inits url and renders editor but does not call query on empty url', async () => {
|
it('inits and renders editor but does not call query on empty initial state', async () => {
|
||||||
const { datasources } = setupExplore();
|
const { datasources } = setupExplore();
|
||||||
await waitForExplore();
|
await waitForExplore();
|
||||||
|
|
||||||
// At this point url should be initialised to some defaults
|
|
||||||
expect(locationService.getSearchObject()).toEqual({
|
|
||||||
orgId: '1',
|
|
||||||
left: serializeStateToUrlParam({
|
|
||||||
datasource: 'loki-uid',
|
|
||||||
queries: [{ refId: 'A', datasource: { type: 'logs', uid: 'loki-uid' } }],
|
|
||||||
range: { from: 'now-1h', to: 'now' },
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
expect(datasources.loki.query).not.toBeCalled();
|
expect(datasources.loki.query).not.toBeCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('runs query when url contains query and renders results', async () => {
|
it('runs query when initial state contains query and renders results', async () => {
|
||||||
const urlParams = {
|
const urlParams = {
|
||||||
left: serializeStateToUrlParam({
|
left: serializeStateToUrlParam({
|
||||||
datasource: 'loki-uid',
|
datasource: 'loki-uid',
|
||||||
@ -46,61 +36,10 @@ describe('Explore: handle running/not running query', () => {
|
|||||||
// And that the editor gets the expr from the url
|
// And that the editor gets the expr from the url
|
||||||
await screen.findByText(`loki Editor input: { label="value"}`);
|
await screen.findByText(`loki Editor input: { label="value"}`);
|
||||||
|
|
||||||
// We did not change the url
|
|
||||||
expect(locationService.getSearchObject()).toEqual({
|
|
||||||
orgId: '1',
|
|
||||||
...urlParams,
|
|
||||||
});
|
|
||||||
|
|
||||||
// We called the data source query method once
|
// We called the data source query method once
|
||||||
expect(datasources.loki.query).toBeCalledTimes(1);
|
expect(datasources.loki.query).toBeCalledTimes(1);
|
||||||
expect(jest.mocked(datasources.loki.query).mock.calls[0][0]).toMatchObject({
|
expect(jest.mocked(datasources.loki.query).mock.calls[0][0]).toMatchObject({
|
||||||
targets: [{ expr: '{ label="value"}' }],
|
targets: [{ expr: '{ label="value"}' }],
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('handles url change', () => {
|
|
||||||
const urlParams = { left: JSON.stringify(['now-1h', 'now', 'loki', { expr: '{ label="value"}' }]) };
|
|
||||||
|
|
||||||
it('and runs the new query', async () => {
|
|
||||||
const { datasources } = setupExplore({ urlParams });
|
|
||||||
jest.mocked(datasources.loki.query).mockReturnValueOnce(makeLogsQueryResponse());
|
|
||||||
// Wait for rendering the logs
|
|
||||||
await screen.findByText(/custom log line/i);
|
|
||||||
|
|
||||||
jest.mocked(datasources.loki.query).mockReturnValueOnce(makeLogsQueryResponse('different log'));
|
|
||||||
|
|
||||||
act(() => {
|
|
||||||
locationService.partial({
|
|
||||||
left: JSON.stringify(['now-1h', 'now', 'loki', { expr: '{ label="different"}' }]),
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Editor renders the new query
|
|
||||||
await screen.findByText(`loki Editor input: { label="different"}`);
|
|
||||||
// Renders new response
|
|
||||||
await screen.findByText(/different log/i);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('and runs the new query with different datasource', async () => {
|
|
||||||
const { datasources } = setupExplore({ urlParams });
|
|
||||||
jest.mocked(datasources.loki.query).mockReturnValueOnce(makeLogsQueryResponse());
|
|
||||||
// Wait for rendering the logs
|
|
||||||
await screen.findByText(/custom log line/i);
|
|
||||||
await screen.findByText(`loki Editor input: { label="value"}`);
|
|
||||||
|
|
||||||
jest.mocked(datasources.elastic.query).mockReturnValueOnce(makeMetricsQueryResponse());
|
|
||||||
|
|
||||||
act(() => {
|
|
||||||
locationService.partial({
|
|
||||||
left: JSON.stringify(['now-1h', 'now', 'elastic', { expr: 'other query' }]),
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Editor renders the new query
|
|
||||||
await screen.findByText(`elastic Editor input: other query`);
|
|
||||||
// Renders graph
|
|
||||||
await screen.findByText(/Graph/i);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
@ -3,7 +3,6 @@ import { of } from 'rxjs';
|
|||||||
|
|
||||||
import { serializeStateToUrlParam } from '@grafana/data';
|
import { serializeStateToUrlParam } from '@grafana/data';
|
||||||
import { config } from '@grafana/runtime';
|
import { config } from '@grafana/runtime';
|
||||||
import { ExploreId } from 'app/types';
|
|
||||||
|
|
||||||
import { silenceConsoleOutput } from '../../../../test/core/utils/silenceConsoleOutput';
|
import { silenceConsoleOutput } from '../../../../test/core/utils/silenceConsoleOutput';
|
||||||
|
|
||||||
@ -155,26 +154,26 @@ describe('Explore: Query History', () => {
|
|||||||
const { datasources } = setupExplore({ urlParams });
|
const { datasources } = setupExplore({ urlParams });
|
||||||
jest.mocked(datasources.loki.query).mockReturnValue(makeLogsQueryResponse());
|
jest.mocked(datasources.loki.query).mockReturnValue(makeLogsQueryResponse());
|
||||||
await waitForExplore();
|
await waitForExplore();
|
||||||
await waitForExplore(ExploreId.right);
|
await waitForExplore('right');
|
||||||
|
|
||||||
// queries in history
|
// queries in history
|
||||||
await openQueryHistory(ExploreId.left);
|
await openQueryHistory('left');
|
||||||
await assertQueryHistory(['{"expr":"query #2"}', '{"expr":"query #1"}'], ExploreId.left);
|
await assertQueryHistory(['{"expr":"query #2"}', '{"expr":"query #1"}'], 'left');
|
||||||
await openQueryHistory(ExploreId.right);
|
await openQueryHistory('right');
|
||||||
await assertQueryHistory(['{"expr":"query #2"}', '{"expr":"query #1"}'], ExploreId.right);
|
await assertQueryHistory(['{"expr":"query #2"}', '{"expr":"query #1"}'], 'right');
|
||||||
|
|
||||||
// star one one query
|
// star one one query
|
||||||
await starQueryHistory(1, ExploreId.left);
|
await starQueryHistory(1, 'left');
|
||||||
await assertQueryHistoryIsStarred([false, true], ExploreId.left);
|
await assertQueryHistoryIsStarred([false, true], 'left');
|
||||||
await assertQueryHistoryIsStarred([false, true], ExploreId.right);
|
await assertQueryHistoryIsStarred([false, true], 'right');
|
||||||
expect(reportInteractionMock).toBeCalledWith('grafana_explore_query_history_starred', {
|
expect(reportInteractionMock).toBeCalledWith('grafana_explore_query_history_starred', {
|
||||||
queryHistoryEnabled: false,
|
queryHistoryEnabled: false,
|
||||||
newValue: true,
|
newValue: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
await deleteQueryHistory(0, ExploreId.left);
|
await deleteQueryHistory(0, 'left');
|
||||||
await assertQueryHistory(['{"expr":"query #1"}'], ExploreId.left);
|
await assertQueryHistory(['{"expr":"query #1"}'], 'left');
|
||||||
await assertQueryHistory(['{"expr":"query #1"}'], ExploreId.right);
|
await assertQueryHistory(['{"expr":"query #1"}'], 'right');
|
||||||
expect(reportInteractionMock).toBeCalledWith('grafana_explore_query_history_deleted', {
|
expect(reportInteractionMock).toBeCalledWith('grafana_explore_query_history_deleted', {
|
||||||
queryHistoryEnabled: false,
|
queryHistoryEnabled: false,
|
||||||
});
|
});
|
||||||
@ -193,10 +192,10 @@ describe('Explore: Query History', () => {
|
|||||||
jest.mocked(datasources.loki.query).mockReturnValueOnce(makeLogsQueryResponse());
|
jest.mocked(datasources.loki.query).mockReturnValueOnce(makeLogsQueryResponse());
|
||||||
await waitForExplore();
|
await waitForExplore();
|
||||||
await openQueryHistory();
|
await openQueryHistory();
|
||||||
await assertQueryHistory(['{"expr":"query #1"}'], ExploreId.left);
|
await assertQueryHistory(['{"expr":"query #1"}'], 'left');
|
||||||
|
|
||||||
await commentQueryHistory(0, 'test comment');
|
await commentQueryHistory(0, 'test comment');
|
||||||
await assertQueryHistoryComment(['test comment'], ExploreId.left);
|
await assertQueryHistoryComment(['test comment'], 'left');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('updates query history settings', async () => {
|
it('updates query history settings', async () => {
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { DataSourceApi } from '@grafana/data';
|
import { DataSourceApi } from '@grafana/data';
|
||||||
import { DataQuery } from '@grafana/schema';
|
import { DataQuery } from '@grafana/schema';
|
||||||
import { ExploreId, ExploreItemState } from 'app/types';
|
import { ExploreItemState } from 'app/types';
|
||||||
|
|
||||||
import { updateDatasourceInstanceAction, datasourceReducer } from './datasource';
|
import { updateDatasourceInstanceAction, datasourceReducer } from './datasource';
|
||||||
import { createEmptyQueryResponse } from './utils';
|
import { createEmptyQueryResponse } from './utils';
|
||||||
@ -27,7 +27,7 @@ describe('Datasource reducer', () => {
|
|||||||
|
|
||||||
const result = datasourceReducer(
|
const result = datasourceReducer(
|
||||||
initialState,
|
initialState,
|
||||||
updateDatasourceInstanceAction({ exploreId: ExploreId.left, datasourceInstance, history: [] })
|
updateDatasourceInstanceAction({ exploreId: 'left', datasourceInstance, history: [] })
|
||||||
);
|
);
|
||||||
|
|
||||||
const expectedState: Partial<ExploreItemState> = {
|
const expectedState: Partial<ExploreItemState> = {
|
||||||
|
@ -7,7 +7,6 @@ import { DataSourceRef } from '@grafana/schema';
|
|||||||
import { RefreshPicker } from '@grafana/ui';
|
import { RefreshPicker } from '@grafana/ui';
|
||||||
import { stopQueryState } from 'app/core/utils/explore';
|
import { stopQueryState } from 'app/core/utils/explore';
|
||||||
import { ExploreItemState, ThunkResult } from 'app/types';
|
import { ExploreItemState, ThunkResult } from 'app/types';
|
||||||
import { ExploreId } from 'app/types/explore';
|
|
||||||
|
|
||||||
import { loadSupplementaryQueries } from '../utils/supplementaryQueries';
|
import { loadSupplementaryQueries } from '../utils/supplementaryQueries';
|
||||||
|
|
||||||
@ -23,7 +22,7 @@ import { createEmptyQueryResponse, loadAndInitDatasource } from './utils';
|
|||||||
* Updates datasource instance before datasource loading has started
|
* Updates datasource instance before datasource loading has started
|
||||||
*/
|
*/
|
||||||
export interface UpdateDatasourceInstancePayload {
|
export interface UpdateDatasourceInstancePayload {
|
||||||
exploreId: ExploreId;
|
exploreId: string;
|
||||||
datasourceInstance: DataSourceApi;
|
datasourceInstance: DataSourceApi;
|
||||||
history: HistoryItem[];
|
history: HistoryItem[];
|
||||||
}
|
}
|
||||||
@ -39,7 +38,7 @@ export const updateDatasourceInstanceAction = createAction<UpdateDatasourceInsta
|
|||||||
* Loads a new datasource identified by the given name.
|
* Loads a new datasource identified by the given name.
|
||||||
*/
|
*/
|
||||||
export function changeDatasource(
|
export function changeDatasource(
|
||||||
exploreId: ExploreId,
|
exploreId: string,
|
||||||
datasource: string | DataSourceRef,
|
datasource: string | DataSourceRef,
|
||||||
options?: { importQueries: boolean }
|
options?: { importQueries: boolean }
|
||||||
): ThunkResult<Promise<void>> {
|
): ThunkResult<Promise<void>> {
|
||||||
|
@ -13,7 +13,7 @@ import { DataQuery, DataSourceRef } from '@grafana/schema';
|
|||||||
import { getQueryKeys } from 'app/core/utils/explore';
|
import { getQueryKeys } from 'app/core/utils/explore';
|
||||||
import { getTimeZone } from 'app/features/profile/state/selectors';
|
import { getTimeZone } from 'app/features/profile/state/selectors';
|
||||||
import { createAsyncThunk, ThunkResult } from 'app/types';
|
import { createAsyncThunk, ThunkResult } from 'app/types';
|
||||||
import { ExploreId, ExploreItemState } from 'app/types/explore';
|
import { ExploreItemState } from 'app/types/explore';
|
||||||
|
|
||||||
import { datasourceReducer } from './datasource';
|
import { datasourceReducer } from './datasource';
|
||||||
import { historyReducer } from './history';
|
import { historyReducer } from './history';
|
||||||
@ -32,7 +32,7 @@ import { makeExplorePaneState, loadAndInitDatasource, createEmptyQueryResponse,
|
|||||||
* The width will be used to calculate graph intervals (number of datapoints).
|
* The width will be used to calculate graph intervals (number of datapoints).
|
||||||
*/
|
*/
|
||||||
export interface ChangeSizePayload {
|
export interface ChangeSizePayload {
|
||||||
exploreId: ExploreId;
|
exploreId: string;
|
||||||
width: number;
|
width: number;
|
||||||
}
|
}
|
||||||
export const changeSizeAction = createAction<ChangeSizePayload>('explore/changeSize');
|
export const changeSizeAction = createAction<ChangeSizePayload>('explore/changeSize');
|
||||||
@ -41,12 +41,12 @@ export const changeSizeAction = createAction<ChangeSizePayload>('explore/changeS
|
|||||||
* Tracks the state of explore panels that gets synced with the url.
|
* Tracks the state of explore panels that gets synced with the url.
|
||||||
*/
|
*/
|
||||||
interface ChangePanelsState {
|
interface ChangePanelsState {
|
||||||
exploreId: ExploreId;
|
exploreId: string;
|
||||||
panelsState: ExplorePanelsState;
|
panelsState: ExplorePanelsState;
|
||||||
}
|
}
|
||||||
const changePanelsStateAction = createAction<ChangePanelsState>('explore/changePanels');
|
const changePanelsStateAction = createAction<ChangePanelsState>('explore/changePanels');
|
||||||
export function changePanelState(
|
export function changePanelState(
|
||||||
exploreId: ExploreId,
|
exploreId: string,
|
||||||
panel: PreferredVisualisationType,
|
panel: PreferredVisualisationType,
|
||||||
panelState: ExplorePanelsState[PreferredVisualisationType]
|
panelState: ExplorePanelsState[PreferredVisualisationType]
|
||||||
): ThunkResult<void> {
|
): ThunkResult<void> {
|
||||||
@ -73,7 +73,7 @@ export function changePanelState(
|
|||||||
* Call this only on components for with the Explore state has not been initialized.
|
* Call this only on components for with the Explore state has not been initialized.
|
||||||
*/
|
*/
|
||||||
interface InitializeExplorePayload {
|
interface InitializeExplorePayload {
|
||||||
exploreId: ExploreId;
|
exploreId: string;
|
||||||
queries: DataQuery[];
|
queries: DataQuery[];
|
||||||
range: TimeRange;
|
range: TimeRange;
|
||||||
history: HistoryItem[];
|
history: HistoryItem[];
|
||||||
@ -82,7 +82,7 @@ interface InitializeExplorePayload {
|
|||||||
const initializeExploreAction = createAction<InitializeExplorePayload>('explore/initializeExploreAction');
|
const initializeExploreAction = createAction<InitializeExplorePayload>('explore/initializeExploreAction');
|
||||||
|
|
||||||
export interface SetUrlReplacedPayload {
|
export interface SetUrlReplacedPayload {
|
||||||
exploreId: ExploreId;
|
exploreId: string;
|
||||||
}
|
}
|
||||||
export const setUrlReplacedAction = createAction<SetUrlReplacedPayload>('explore/setUrlReplaced');
|
export const setUrlReplacedAction = createAction<SetUrlReplacedPayload>('explore/setUrlReplaced');
|
||||||
|
|
||||||
@ -90,16 +90,17 @@ export const setUrlReplacedAction = createAction<SetUrlReplacedPayload>('explore
|
|||||||
* Keep track of the Explore container size, in particular the width.
|
* Keep track of the Explore container size, in particular the width.
|
||||||
* The width will be used to calculate graph intervals (number of datapoints).
|
* The width will be used to calculate graph intervals (number of datapoints).
|
||||||
*/
|
*/
|
||||||
export function changeSize(exploreId: ExploreId, { width }: { width: number }): PayloadAction<ChangeSizePayload> {
|
export function changeSize(exploreId: string, { width }: { width: number }): PayloadAction<ChangeSizePayload> {
|
||||||
return changeSizeAction({ exploreId, width });
|
return changeSizeAction({ exploreId, width });
|
||||||
}
|
}
|
||||||
|
|
||||||
interface InitializeExploreOptions {
|
interface InitializeExploreOptions {
|
||||||
exploreId: ExploreId;
|
exploreId: string;
|
||||||
datasource: DataSourceRef | string | undefined;
|
datasource: DataSourceRef | string | undefined;
|
||||||
queries: DataQuery[];
|
queries: DataQuery[];
|
||||||
range: RawTimeRange;
|
range: RawTimeRange;
|
||||||
panelsState?: ExplorePanelsState;
|
panelsState?: ExplorePanelsState;
|
||||||
|
position?: number;
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* Initialize Explore state with state from the URL and the React component.
|
* Initialize Explore state with state from the URL and the React component.
|
||||||
|
@ -12,7 +12,7 @@ import {
|
|||||||
updateRichHistorySettings,
|
updateRichHistorySettings,
|
||||||
updateStarredInRichHistory,
|
updateStarredInRichHistory,
|
||||||
} from 'app/core/utils/richHistory';
|
} from 'app/core/utils/richHistory';
|
||||||
import { ExploreId, ExploreItemState, ExploreState, RichHistoryQuery, ThunkResult } from 'app/types';
|
import { ExploreItemState, ExploreState, RichHistoryQuery, ThunkResult } from 'app/types';
|
||||||
|
|
||||||
import { supportedFeatures } from '../../../core/history/richHistoryStorageProvider';
|
import { supportedFeatures } from '../../../core/history/richHistoryStorageProvider';
|
||||||
import { RichHistorySearchFilters, RichHistorySettings } from '../../../core/utils/richHistoryTypes';
|
import { RichHistorySearchFilters, RichHistorySettings } from '../../../core/utils/richHistoryTypes';
|
||||||
@ -24,13 +24,14 @@ import {
|
|||||||
richHistoryStorageFullAction,
|
richHistoryStorageFullAction,
|
||||||
richHistoryUpdatedAction,
|
richHistoryUpdatedAction,
|
||||||
} from './main';
|
} from './main';
|
||||||
|
import { selectPanesEntries } from './selectors';
|
||||||
|
|
||||||
//
|
//
|
||||||
// Actions and Payloads
|
// Actions and Payloads
|
||||||
//
|
//
|
||||||
|
|
||||||
export interface HistoryUpdatedPayload {
|
export interface HistoryUpdatedPayload {
|
||||||
exploreId: ExploreId;
|
exploreId: string;
|
||||||
history: HistoryItem[];
|
history: HistoryItem[];
|
||||||
}
|
}
|
||||||
export const historyUpdatedAction = createAction<HistoryUpdatedPayload>('explore/historyUpdated');
|
export const historyUpdatedAction = createAction<HistoryUpdatedPayload>('explore/historyUpdated');
|
||||||
@ -66,9 +67,9 @@ const updateRichHistoryState = ({ updatedQuery, deletedId }: SyncHistoryUpdatesO
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const forEachExplorePane = (state: ExploreState, callback: (item: ExploreItemState, exploreId: ExploreId) => void) => {
|
const forEachExplorePane = (state: ExploreState, callback: (item: ExploreItemState, exploreId: string) => void) => {
|
||||||
Object.entries(state.panes).forEach(([exploreId, item]) => {
|
Object.entries(state.panes).forEach(([exploreId, item]) => {
|
||||||
callback(item!, exploreId as ExploreId);
|
item && callback(item, exploreId);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -118,18 +119,16 @@ export const deleteHistoryItem = (id: string): ThunkResult<void> => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const deleteRichHistory = (): ThunkResult<void> => {
|
export const deleteRichHistory = (): ThunkResult<void> => {
|
||||||
return async (dispatch) => {
|
return async (dispatch, getState) => {
|
||||||
await deleteAllFromRichHistory();
|
await deleteAllFromRichHistory();
|
||||||
dispatch(
|
selectPanesEntries(getState()).forEach(([exploreId]) => {
|
||||||
richHistoryUpdatedAction({ richHistoryResults: { richHistory: [], total: 0 }, exploreId: ExploreId.left })
|
dispatch(richHistoryUpdatedAction({ richHistoryResults: { richHistory: [], total: 0 }, exploreId }));
|
||||||
);
|
dispatch(richHistoryUpdatedAction({ richHistoryResults: { richHistory: [], total: 0 }, exploreId }));
|
||||||
dispatch(
|
});
|
||||||
richHistoryUpdatedAction({ richHistoryResults: { richHistory: [], total: 0 }, exploreId: ExploreId.right })
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export const loadRichHistory = (exploreId: ExploreId): ThunkResult<void> => {
|
export const loadRichHistory = (exploreId: string): ThunkResult<void> => {
|
||||||
return async (dispatch, getState) => {
|
return async (dispatch, getState) => {
|
||||||
const filters = getState().explore.panes[exploreId]!.richHistorySearchFilters;
|
const filters = getState().explore.panes[exploreId]!.richHistorySearchFilters;
|
||||||
if (filters) {
|
if (filters) {
|
||||||
@ -139,7 +138,7 @@ export const loadRichHistory = (exploreId: ExploreId): ThunkResult<void> => {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export const loadMoreRichHistory = (exploreId: ExploreId): ThunkResult<void> => {
|
export const loadMoreRichHistory = (exploreId: string): ThunkResult<void> => {
|
||||||
return async (dispatch, getState) => {
|
return async (dispatch, getState) => {
|
||||||
const currentFilters = getState().explore.panes[exploreId]?.richHistorySearchFilters;
|
const currentFilters = getState().explore.panes[exploreId]?.richHistorySearchFilters;
|
||||||
const currentRichHistory = getState().explore.panes[exploreId]?.richHistory;
|
const currentRichHistory = getState().explore.panes[exploreId]?.richHistory;
|
||||||
@ -155,7 +154,7 @@ export const loadMoreRichHistory = (exploreId: ExploreId): ThunkResult<void> =>
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export const clearRichHistoryResults = (exploreId: ExploreId): ThunkResult<void> => {
|
export const clearRichHistoryResults = (exploreId: string): ThunkResult<void> => {
|
||||||
return async (dispatch) => {
|
return async (dispatch) => {
|
||||||
dispatch(richHistorySearchFiltersUpdatedAction({ filters: undefined, exploreId }));
|
dispatch(richHistorySearchFiltersUpdatedAction({ filters: undefined, exploreId }));
|
||||||
dispatch(richHistoryUpdatedAction({ richHistoryResults: { richHistory: [], total: 0 }, exploreId }));
|
dispatch(richHistoryUpdatedAction({ richHistoryResults: { richHistory: [], total: 0 }, exploreId }));
|
||||||
@ -186,10 +185,7 @@ export const updateHistorySettings = (settings: RichHistorySettings): ThunkResul
|
|||||||
/**
|
/**
|
||||||
* Assumed this can be called only when settings and filters are initialised
|
* Assumed this can be called only when settings and filters are initialised
|
||||||
*/
|
*/
|
||||||
export const updateHistorySearchFilters = (
|
export const updateHistorySearchFilters = (exploreId: string, filters: RichHistorySearchFilters): ThunkResult<void> => {
|
||||||
exploreId: ExploreId,
|
|
||||||
filters: RichHistorySearchFilters
|
|
||||||
): ThunkResult<void> => {
|
|
||||||
return async (dispatch, getState) => {
|
return async (dispatch, getState) => {
|
||||||
await dispatch(richHistorySearchFiltersUpdatedAction({ exploreId, filters: { ...filters } }));
|
await dispatch(richHistorySearchFiltersUpdatedAction({ exploreId, filters: { ...filters } }));
|
||||||
const currentSettings = getState().explore.richHistorySettings!;
|
const currentSettings = getState().explore.richHistorySettings!;
|
||||||
|
@ -7,7 +7,7 @@ import { PanelModel } from 'app/features/dashboard/state';
|
|||||||
|
|
||||||
import { reducerTester } from '../../../../test/core/redux/reducerTester';
|
import { reducerTester } from '../../../../test/core/redux/reducerTester';
|
||||||
import { MockDataSourceApi } from '../../../../test/mocks/datasource_srv';
|
import { MockDataSourceApi } from '../../../../test/mocks/datasource_srv';
|
||||||
import { ExploreId, ExploreItemState, ExploreState } from '../../../types';
|
import { ExploreItemState, ExploreState } from '../../../types';
|
||||||
|
|
||||||
import { exploreReducer, navigateToExplore, splitClose } from './main';
|
import { exploreReducer, navigateToExplore, splitClose } from './main';
|
||||||
|
|
||||||
@ -117,36 +117,6 @@ describe('navigateToExplore', () => {
|
|||||||
describe('Explore reducer', () => {
|
describe('Explore reducer', () => {
|
||||||
describe('split view', () => {
|
describe('split view', () => {
|
||||||
describe('split close', () => {
|
describe('split close', () => {
|
||||||
it('should move right pane to left when left is closed', () => {
|
|
||||||
const leftItemMock = {
|
|
||||||
containerWidth: 100,
|
|
||||||
} as unknown as ExploreItemState;
|
|
||||||
|
|
||||||
const rightItemMock = {
|
|
||||||
containerWidth: 200,
|
|
||||||
} as unknown as ExploreItemState;
|
|
||||||
|
|
||||||
const initialState = {
|
|
||||||
panes: {
|
|
||||||
left: leftItemMock,
|
|
||||||
right: rightItemMock,
|
|
||||||
},
|
|
||||||
} as unknown as ExploreState;
|
|
||||||
|
|
||||||
// closing left item
|
|
||||||
reducerTester<ExploreState>()
|
|
||||||
.givenReducer(exploreReducer, initialState)
|
|
||||||
.whenActionIsDispatched(splitClose(ExploreId.left))
|
|
||||||
.thenStateShouldEqual({
|
|
||||||
evenSplitPanes: true,
|
|
||||||
largerExploreId: undefined,
|
|
||||||
panes: {
|
|
||||||
left: rightItemMock,
|
|
||||||
},
|
|
||||||
maxedExploreId: undefined,
|
|
||||||
syncedTimes: false,
|
|
||||||
} as unknown as ExploreState);
|
|
||||||
});
|
|
||||||
it('should reset right pane when it is closed', () => {
|
it('should reset right pane when it is closed', () => {
|
||||||
const leftItemMock = {
|
const leftItemMock = {
|
||||||
containerWidth: 100,
|
containerWidth: 100,
|
||||||
@ -166,7 +136,7 @@ describe('Explore reducer', () => {
|
|||||||
// closing left item
|
// closing left item
|
||||||
reducerTester<ExploreState>()
|
reducerTester<ExploreState>()
|
||||||
.givenReducer(exploreReducer, initialState)
|
.givenReducer(exploreReducer, initialState)
|
||||||
.whenActionIsDispatched(splitClose(ExploreId.right))
|
.whenActionIsDispatched(splitClose('right'))
|
||||||
.thenStateShouldEqual({
|
.thenStateShouldEqual({
|
||||||
evenSplitPanes: true,
|
evenSplitPanes: true,
|
||||||
largerExploreId: undefined,
|
largerExploreId: undefined,
|
||||||
@ -193,7 +163,7 @@ describe('Explore reducer', () => {
|
|||||||
|
|
||||||
reducerTester<ExploreState>()
|
reducerTester<ExploreState>()
|
||||||
.givenReducer(exploreReducer, initialState)
|
.givenReducer(exploreReducer, initialState)
|
||||||
.whenActionIsDispatched(splitClose(ExploreId.right))
|
.whenActionIsDispatched(splitClose('right'))
|
||||||
.thenStateShouldEqual({
|
.thenStateShouldEqual({
|
||||||
evenSplitPanes: true,
|
evenSplitPanes: true,
|
||||||
panes: {
|
panes: {
|
||||||
|
@ -3,9 +3,9 @@ import { AnyAction } from 'redux';
|
|||||||
|
|
||||||
import { SplitOpenOptions } from '@grafana/data';
|
import { SplitOpenOptions } from '@grafana/data';
|
||||||
import { DataSourceSrv, locationService } from '@grafana/runtime';
|
import { DataSourceSrv, locationService } from '@grafana/runtime';
|
||||||
import { GetExploreUrlArguments } from 'app/core/utils/explore';
|
import { generateExploreId, GetExploreUrlArguments } from 'app/core/utils/explore';
|
||||||
import { PanelModel } from 'app/features/dashboard/state';
|
import { PanelModel } from 'app/features/dashboard/state';
|
||||||
import { ExploreId, ExploreItemState, ExploreState } from 'app/types/explore';
|
import { ExploreItemState, ExploreState } from 'app/types/explore';
|
||||||
|
|
||||||
import { RichHistoryResults } from '../../../core/history/RichHistoryStorage';
|
import { RichHistoryResults } from '../../../core/history/RichHistoryStorage';
|
||||||
import { RichHistorySearchFilters, RichHistorySettings } from '../../../core/utils/richHistoryTypes';
|
import { RichHistorySearchFilters, RichHistorySettings } from '../../../core/utils/richHistoryTypes';
|
||||||
@ -26,7 +26,7 @@ export interface SyncTimesPayload {
|
|||||||
}
|
}
|
||||||
export const syncTimesAction = createAction<SyncTimesPayload>('explore/syncTimes');
|
export const syncTimesAction = createAction<SyncTimesPayload>('explore/syncTimes');
|
||||||
|
|
||||||
export const richHistoryUpdatedAction = createAction<{ richHistoryResults: RichHistoryResults; exploreId: ExploreId }>(
|
export const richHistoryUpdatedAction = createAction<{ richHistoryResults: RichHistoryResults; exploreId: string }>(
|
||||||
'explore/richHistoryUpdated'
|
'explore/richHistoryUpdated'
|
||||||
);
|
);
|
||||||
export const richHistoryStorageFullAction = createAction('explore/richHistoryStorageFullAction');
|
export const richHistoryStorageFullAction = createAction('explore/richHistoryStorageFullAction');
|
||||||
@ -34,18 +34,18 @@ export const richHistoryLimitExceededAction = createAction('explore/richHistoryL
|
|||||||
|
|
||||||
export const richHistorySettingsUpdatedAction = createAction<RichHistorySettings>('explore/richHistorySettingsUpdated');
|
export const richHistorySettingsUpdatedAction = createAction<RichHistorySettings>('explore/richHistorySettingsUpdated');
|
||||||
export const richHistorySearchFiltersUpdatedAction = createAction<{
|
export const richHistorySearchFiltersUpdatedAction = createAction<{
|
||||||
exploreId: ExploreId;
|
exploreId: string;
|
||||||
filters?: RichHistorySearchFilters;
|
filters?: RichHistorySearchFilters;
|
||||||
}>('explore/richHistorySearchFiltersUpdatedAction');
|
}>('explore/richHistorySearchFiltersUpdatedAction');
|
||||||
|
|
||||||
export const saveCorrelationsAction = createAction<CorrelationData[]>('explore/saveCorrelationsAction');
|
export const saveCorrelationsAction = createAction<CorrelationData[]>('explore/saveCorrelationsAction');
|
||||||
|
|
||||||
export const splitSizeUpdateAction = createAction<{
|
export const splitSizeUpdateAction = createAction<{
|
||||||
largerExploreId?: ExploreId;
|
largerExploreId?: string;
|
||||||
}>('explore/splitSizeUpdateAction');
|
}>('explore/splitSizeUpdateAction');
|
||||||
|
|
||||||
export const maximizePaneAction = createAction<{
|
export const maximizePaneAction = createAction<{
|
||||||
exploreId?: ExploreId;
|
exploreId?: string;
|
||||||
}>('explore/maximizePaneAction');
|
}>('explore/maximizePaneAction');
|
||||||
|
|
||||||
export const evenPaneResizeAction = createAction('explore/evenPaneResizeAction');
|
export const evenPaneResizeAction = createAction('explore/evenPaneResizeAction');
|
||||||
@ -53,8 +53,7 @@ export const evenPaneResizeAction = createAction('explore/evenPaneResizeAction')
|
|||||||
/**
|
/**
|
||||||
* Close the pane with the given id.
|
* Close the pane with the given id.
|
||||||
*/
|
*/
|
||||||
type SplitCloseActionPayload = ExploreId;
|
export const splitClose = createAction<string>('explore/splitClose');
|
||||||
export const splitClose = createAction<SplitCloseActionPayload>('explore/splitClose');
|
|
||||||
|
|
||||||
export interface SetPaneStateActionPayload {
|
export interface SetPaneStateActionPayload {
|
||||||
[itemId: string]: Partial<ExploreItemState>;
|
[itemId: string]: Partial<ExploreItemState>;
|
||||||
@ -64,27 +63,31 @@ export const setPaneState = createAction<SetPaneStateActionPayload>('explore/set
|
|||||||
export const clearPanes = createAction('explore/clearPanes');
|
export const clearPanes = createAction('explore/clearPanes');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Opens a new split pane. It either copies existing state of the left pane
|
* Opens a new split pane. It either copies existing state of an already present pane
|
||||||
* or uses values from options arg.
|
* or uses values from options arg.
|
||||||
*
|
*
|
||||||
* TODO: this can be improved by better inferring fallback values.
|
* TODO: this can be improved by better inferring fallback values.
|
||||||
*/
|
*/
|
||||||
export const splitOpen = createAsyncThunk(
|
export const splitOpen = createAsyncThunk(
|
||||||
'explore/splitOpen',
|
'explore/splitOpen',
|
||||||
async (options: SplitOpenOptions | undefined, { getState, dispatch }) => {
|
async (options: SplitOpenOptions | undefined, { getState, dispatch, requestId }) => {
|
||||||
const leftState = getState().explore.panes.left;
|
// we currently support showing only 2 panes in explore, so if this action is dispatched we know it has been dispatched from the "first" pane.
|
||||||
|
const originState = Object.values(getState().explore.panes)[0];
|
||||||
|
|
||||||
const queries = options?.queries ?? (options?.query ? [options?.query] : leftState?.queries || []);
|
const queries = options?.queries ?? (options?.query ? [options?.query] : originState?.queries || []);
|
||||||
|
|
||||||
await dispatch(
|
await dispatch(
|
||||||
initializeExplore({
|
initializeExplore({
|
||||||
exploreId: ExploreId.right,
|
exploreId: requestId,
|
||||||
datasource: options?.datasourceUid || leftState?.datasourceInstance?.getRef(),
|
datasource: options?.datasourceUid || originState?.datasourceInstance?.getRef(),
|
||||||
queries: withUniqueRefIds(queries),
|
queries: withUniqueRefIds(queries),
|
||||||
range: options?.range || leftState?.range.raw || DEFAULT_RANGE,
|
range: options?.range || originState?.range.raw || DEFAULT_RANGE,
|
||||||
panelsState: options?.panelsState || leftState?.panelsState,
|
panelsState: options?.panelsState || originState?.panelsState,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
},
|
||||||
|
{
|
||||||
|
idGenerator: generateExploreId,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -138,9 +141,8 @@ export const initialExploreState: ExploreState = {
|
|||||||
*/
|
*/
|
||||||
export const exploreReducer = (state = initialExploreState, action: AnyAction): ExploreState => {
|
export const exploreReducer = (state = initialExploreState, action: AnyAction): ExploreState => {
|
||||||
if (splitClose.match(action)) {
|
if (splitClose.match(action)) {
|
||||||
const panes = {
|
const { [action.payload]: _, ...panes } = { ...state.panes };
|
||||||
left: action.payload === ExploreId.left ? state.panes.right : state.panes.left,
|
|
||||||
};
|
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
panes,
|
panes,
|
||||||
@ -218,18 +220,23 @@ export const exploreReducer = (state = initialExploreState, action: AnyAction):
|
|||||||
...state,
|
...state,
|
||||||
panes: {
|
panes: {
|
||||||
...state.panes,
|
...state.panes,
|
||||||
right: initialExploreItemState,
|
[action.meta.requestId]: initialExploreItemState,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (initializeExplore.pending.match(action)) {
|
if (initializeExplore.pending.match(action)) {
|
||||||
|
const initialPanes = Object.entries(state.panes);
|
||||||
|
const before = initialPanes.slice(0, action.meta.arg.position);
|
||||||
|
const after = initialPanes.slice(before.length);
|
||||||
|
const panes = [...before, [action.meta.arg.exploreId, initialExploreItemState] as const, ...after].reduce(
|
||||||
|
(acc, [id, pane]) => ({ ...acc, [id]: pane }),
|
||||||
|
{}
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
panes: {
|
panes,
|
||||||
...state.panes,
|
|
||||||
[action.meta.arg.exploreId]: initialExploreItemState,
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -240,17 +247,15 @@ export const exploreReducer = (state = initialExploreState, action: AnyAction):
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const exploreId: ExploreId | undefined = action.payload?.exploreId;
|
const exploreId: string | undefined = action.payload?.exploreId;
|
||||||
if (typeof exploreId === 'string') {
|
if (typeof exploreId === 'string') {
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
panes: Object.entries(state.panes).reduce<ExploreState['panes']>((acc, [id, pane]) => {
|
panes: Object.entries(state.panes).reduce((acc, [id, pane]) => {
|
||||||
if (id === exploreId) {
|
return {
|
||||||
acc[id] = paneReducer(pane, action);
|
...acc,
|
||||||
} else {
|
[id]: id === exploreId ? paneReducer(pane, action) : pane,
|
||||||
acc[id as ExploreId] = pane;
|
};
|
||||||
}
|
|
||||||
return acc;
|
|
||||||
}, {}),
|
}, {}),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -16,7 +16,7 @@ import {
|
|||||||
} from '@grafana/data';
|
} from '@grafana/data';
|
||||||
import { config } from '@grafana/runtime';
|
import { config } from '@grafana/runtime';
|
||||||
import { DataQuery, DataSourceRef } from '@grafana/schema';
|
import { DataQuery, DataSourceRef } from '@grafana/schema';
|
||||||
import { createAsyncThunk, ExploreId, ExploreItemState, StoreState, ThunkDispatch } from 'app/types';
|
import { createAsyncThunk, ExploreItemState, StoreState, ThunkDispatch } from 'app/types';
|
||||||
|
|
||||||
import { reducerTester } from '../../../../test/core/redux/reducerTester';
|
import { reducerTester } from '../../../../test/core/redux/reducerTester';
|
||||||
import { configureStore } from '../../../store/configureStore';
|
import { configureStore } from '../../../store/configureStore';
|
||||||
@ -51,7 +51,7 @@ import { makeExplorePaneState } from './utils';
|
|||||||
|
|
||||||
const { testRange, defaultInitialState } = createDefaultInitialState();
|
const { testRange, defaultInitialState } = createDefaultInitialState();
|
||||||
|
|
||||||
const exploreId = ExploreId.left;
|
const exploreId = 'left';
|
||||||
const datasources: DataSourceApi[] = [
|
const datasources: DataSourceApi[] = [
|
||||||
{
|
{
|
||||||
name: 'testDs',
|
name: 'testDs',
|
||||||
@ -158,7 +158,7 @@ describe('runQueries', () => {
|
|||||||
const { dispatch, getState } = setupTests();
|
const { dispatch, getState } = setupTests();
|
||||||
setupQueryResponse(getState());
|
setupQueryResponse(getState());
|
||||||
await dispatch(saveCorrelationsAction([]));
|
await dispatch(saveCorrelationsAction([]));
|
||||||
await dispatch(runQueries({ exploreId: ExploreId.left }));
|
await dispatch(runQueries({ exploreId: 'left' }));
|
||||||
expect(getState().explore.panes.left!.showMetrics).toBeTruthy();
|
expect(getState().explore.panes.left!.showMetrics).toBeTruthy();
|
||||||
expect(getState().explore.panes.left!.graphResult).toBeDefined();
|
expect(getState().explore.panes.left!.graphResult).toBeDefined();
|
||||||
});
|
});
|
||||||
@ -167,7 +167,7 @@ describe('runQueries', () => {
|
|||||||
const { dispatch, getState } = setupTests();
|
const { dispatch, getState } = setupTests();
|
||||||
setupQueryResponse(getState());
|
setupQueryResponse(getState());
|
||||||
dispatch(saveCorrelationsAction([]));
|
dispatch(saveCorrelationsAction([]));
|
||||||
dispatch(runQueries({ exploreId: ExploreId.left }));
|
dispatch(runQueries({ exploreId: 'left' }));
|
||||||
|
|
||||||
const state = getState().explore.panes.left!;
|
const state = getState().explore.panes.left!;
|
||||||
expect(state.queryResponse.request?.requestId).toBe('explore_left');
|
expect(state.queryResponse.request?.requestId).toBe('explore_left');
|
||||||
@ -187,7 +187,7 @@ describe('runQueries', () => {
|
|||||||
const leftDatasourceInstance = assertIsDefined(getState().explore.panes.left!.datasourceInstance);
|
const leftDatasourceInstance = assertIsDefined(getState().explore.panes.left!.datasourceInstance);
|
||||||
jest.mocked(leftDatasourceInstance.query).mockReturnValueOnce(EMPTY);
|
jest.mocked(leftDatasourceInstance.query).mockReturnValueOnce(EMPTY);
|
||||||
await dispatch(saveCorrelationsAction([]));
|
await dispatch(saveCorrelationsAction([]));
|
||||||
await dispatch(runQueries({ exploreId: ExploreId.left }));
|
await dispatch(runQueries({ exploreId: 'left' }));
|
||||||
await new Promise((resolve) => setTimeout(() => resolve(''), 500));
|
await new Promise((resolve) => setTimeout(() => resolve(''), 500));
|
||||||
expect(getState().explore.panes.left!.queryResponse.state).toBe(LoadingState.Done);
|
expect(getState().explore.panes.left!.queryResponse.state).toBe(LoadingState.Done);
|
||||||
});
|
});
|
||||||
@ -195,7 +195,7 @@ describe('runQueries', () => {
|
|||||||
it('shows results only after correlations are loaded', async () => {
|
it('shows results only after correlations are loaded', async () => {
|
||||||
const { dispatch, getState } = setupTests();
|
const { dispatch, getState } = setupTests();
|
||||||
setupQueryResponse(getState());
|
setupQueryResponse(getState());
|
||||||
await dispatch(runQueries({ exploreId: ExploreId.left }));
|
await dispatch(runQueries({ exploreId: 'left' }));
|
||||||
expect(getState().explore.panes.left!.graphResult).not.toBeDefined();
|
expect(getState().explore.panes.left!.graphResult).not.toBeDefined();
|
||||||
await dispatch(saveCorrelationsAction([]));
|
await dispatch(saveCorrelationsAction([]));
|
||||||
expect(getState().explore.panes.left!.graphResult).toBeDefined();
|
expect(getState().explore.panes.left!.graphResult).toBeDefined();
|
||||||
@ -206,7 +206,7 @@ describe('running queries', () => {
|
|||||||
it('should cancel running query when cancelQueries is dispatched', async () => {
|
it('should cancel running query when cancelQueries is dispatched', async () => {
|
||||||
const unsubscribable = interval(1000);
|
const unsubscribable = interval(1000);
|
||||||
unsubscribable.subscribe();
|
unsubscribable.subscribe();
|
||||||
const exploreId = ExploreId.left;
|
const exploreId = 'left';
|
||||||
const initialState = {
|
const initialState = {
|
||||||
explore: {
|
explore: {
|
||||||
panes: {
|
panes: {
|
||||||
@ -274,13 +274,13 @@ describe('changeQueries', () => {
|
|||||||
await dispatch(
|
await dispatch(
|
||||||
changeQueries({
|
changeQueries({
|
||||||
queries: [{ refId: 'A', datasource: datasources[1].getRef() }],
|
queries: [{ refId: 'A', datasource: datasources[1].getRef() }],
|
||||||
exploreId: ExploreId.left,
|
exploreId: 'left',
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(actions.changeQueriesAction).not.toHaveBeenCalled();
|
expect(actions.changeQueriesAction).not.toHaveBeenCalled();
|
||||||
expect(actions.importQueries).toHaveBeenCalledWith(
|
expect(actions.importQueries).toHaveBeenCalledWith(
|
||||||
ExploreId.left,
|
'left',
|
||||||
originalQueries,
|
originalQueries,
|
||||||
datasources[0],
|
datasources[0],
|
||||||
datasources[1],
|
datasources[1],
|
||||||
@ -308,7 +308,7 @@ describe('changeQueries', () => {
|
|||||||
await dispatch(
|
await dispatch(
|
||||||
changeQueries({
|
changeQueries({
|
||||||
queries: [{ refId: 'A', datasource: datasources[0].getRef(), queryType: 'someValue' }],
|
queries: [{ refId: 'A', datasource: datasources[0].getRef(), queryType: 'someValue' }],
|
||||||
exploreId: ExploreId.left,
|
exploreId: 'left',
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -337,7 +337,7 @@ describe('changeQueries', () => {
|
|||||||
await dispatch(
|
await dispatch(
|
||||||
changeQueries({
|
changeQueries({
|
||||||
queries: [{ refId: 'A', datasource: datasources[1].getRef() }],
|
queries: [{ refId: 'A', datasource: datasources[1].getRef() }],
|
||||||
exploreId: ExploreId.left,
|
exploreId: 'left',
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -362,7 +362,7 @@ describe('changeQueries', () => {
|
|||||||
await dispatch(
|
await dispatch(
|
||||||
changeQueries({
|
changeQueries({
|
||||||
queries: [{ refId: 'A', datasource: datasources[0].getRef(), queryType: 'someValue' }],
|
queries: [{ refId: 'A', datasource: datasources[0].getRef(), queryType: 'someValue' }],
|
||||||
exploreId: ExploreId.left,
|
exploreId: 'left',
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -400,7 +400,7 @@ describe('changeQueries', () => {
|
|||||||
await dispatch(
|
await dispatch(
|
||||||
changeQueries({
|
changeQueries({
|
||||||
queries: [originalQueries[0]],
|
queries: [originalQueries[0]],
|
||||||
exploreId: ExploreId.left,
|
exploreId: 'left',
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -425,7 +425,7 @@ describe('importing queries', () => {
|
|||||||
|
|
||||||
await dispatch(
|
await dispatch(
|
||||||
importQueries(
|
importQueries(
|
||||||
ExploreId.left,
|
'left',
|
||||||
[
|
[
|
||||||
{ datasource: { type: 'postgresql', uid: 'ds1' }, refId: 'refId_A' },
|
{ datasource: { type: 'postgresql', uid: 'ds1' }, refId: 'refId_A' },
|
||||||
{ datasource: { type: 'postgresql', uid: 'ds1' }, refId: 'refId_B' },
|
{ datasource: { type: 'postgresql', uid: 'ds1' }, refId: 'refId_B' },
|
||||||
@ -606,7 +606,7 @@ describe('reducer', () => {
|
|||||||
|
|
||||||
reducerTester<ExploreItemState>()
|
reducerTester<ExploreItemState>()
|
||||||
.givenReducer(queryReducer, initialState)
|
.givenReducer(queryReducer, initialState)
|
||||||
.whenActionIsDispatched(scanStartAction({ exploreId: ExploreId.left }))
|
.whenActionIsDispatched(scanStartAction({ exploreId: 'left' }))
|
||||||
.thenStateShouldEqual({
|
.thenStateShouldEqual({
|
||||||
...initialState,
|
...initialState,
|
||||||
scanning: true,
|
scanning: true,
|
||||||
@ -621,7 +621,7 @@ describe('reducer', () => {
|
|||||||
|
|
||||||
reducerTester<ExploreItemState>()
|
reducerTester<ExploreItemState>()
|
||||||
.givenReducer(queryReducer, initialState)
|
.givenReducer(queryReducer, initialState)
|
||||||
.whenActionIsDispatched(scanStopAction({ exploreId: ExploreId.left }))
|
.whenActionIsDispatched(scanStopAction({ exploreId: 'left' }))
|
||||||
.thenStateShouldEqual({
|
.thenStateShouldEqual({
|
||||||
...initialState,
|
...initialState,
|
||||||
scanning: false,
|
scanning: false,
|
||||||
@ -638,7 +638,7 @@ describe('reducer', () => {
|
|||||||
} as unknown as ExploreItemState)
|
} as unknown as ExploreItemState)
|
||||||
.whenActionIsDispatched(
|
.whenActionIsDispatched(
|
||||||
addQueryRowAction({
|
addQueryRowAction({
|
||||||
exploreId: ExploreId.left,
|
exploreId: 'left',
|
||||||
query: { refId: 'A', key: 'mockKey' },
|
query: { refId: 'A', key: 'mockKey' },
|
||||||
index: 0,
|
index: 0,
|
||||||
})
|
})
|
||||||
@ -655,7 +655,7 @@ describe('reducer', () => {
|
|||||||
} as unknown as ExploreItemState)
|
} as unknown as ExploreItemState)
|
||||||
.whenActionIsDispatched(
|
.whenActionIsDispatched(
|
||||||
addQueryRowAction({
|
addQueryRowAction({
|
||||||
exploreId: ExploreId.left,
|
exploreId: 'left',
|
||||||
query: { refId: 'B', key: 'mockKey', datasource: { type: 'loki' } },
|
query: { refId: 'B', key: 'mockKey', datasource: { type: 'loki' } },
|
||||||
index: 0,
|
index: 0,
|
||||||
})
|
})
|
||||||
@ -688,7 +688,7 @@ describe('reducer', () => {
|
|||||||
},
|
},
|
||||||
} as unknown as Partial<StoreState>);
|
} as unknown as Partial<StoreState>);
|
||||||
|
|
||||||
await dispatch(addResultsToCache(ExploreId.left));
|
await dispatch(addResultsToCache('left'));
|
||||||
|
|
||||||
expect(getState().explore.panes.left!.cache).toEqual([
|
expect(getState().explore.panes.left!.cache).toEqual([
|
||||||
{ key: 'from=1621348027000&to=1621348050000', value: { series: [{ name: 'test name' }], state: 'Done' } },
|
{ key: 'from=1621348027000&to=1621348050000', value: { series: [{ name: 'test name' }], state: 'Done' } },
|
||||||
@ -709,7 +709,7 @@ describe('reducer', () => {
|
|||||||
},
|
},
|
||||||
} as unknown as Partial<StoreState>);
|
} as unknown as Partial<StoreState>);
|
||||||
|
|
||||||
await dispatch(addResultsToCache(ExploreId.left));
|
await dispatch(addResultsToCache('left'));
|
||||||
|
|
||||||
expect(getState().explore.panes.left!.cache).toEqual([]);
|
expect(getState().explore.panes.left!.cache).toEqual([]);
|
||||||
});
|
});
|
||||||
@ -737,7 +737,7 @@ describe('reducer', () => {
|
|||||||
},
|
},
|
||||||
} as unknown as Partial<StoreState>);
|
} as unknown as Partial<StoreState>);
|
||||||
|
|
||||||
await dispatch(addResultsToCache(ExploreId.left));
|
await dispatch(addResultsToCache('left'));
|
||||||
|
|
||||||
expect(getState().explore.panes.left!.cache).toHaveLength(1);
|
expect(getState().explore.panes.left!.cache).toHaveLength(1);
|
||||||
expect(getState().explore.panes.left!.cache).toEqual([
|
expect(getState().explore.panes.left!.cache).toEqual([
|
||||||
@ -763,7 +763,7 @@ describe('reducer', () => {
|
|||||||
},
|
},
|
||||||
} as unknown as Partial<StoreState>);
|
} as unknown as Partial<StoreState>);
|
||||||
|
|
||||||
await dispatch(clearCache(ExploreId.left));
|
await dispatch(clearCache('left'));
|
||||||
|
|
||||||
expect(getState().explore.panes.left!.cache).toEqual([]);
|
expect(getState().explore.panes.left!.cache).toEqual([]);
|
||||||
});
|
});
|
||||||
@ -822,7 +822,7 @@ describe('reducer', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should cancel any unfinished supplementary queries when a new query is run', async () => {
|
it('should cancel any unfinished supplementary queries when a new query is run', async () => {
|
||||||
dispatch(runQueries({ exploreId: ExploreId.left }));
|
dispatch(runQueries({ exploreId: 'left' }));
|
||||||
// first query is run automatically
|
// first query is run automatically
|
||||||
// loading in progress - subscriptions for both supplementary queries are created, not cleaned up yet
|
// loading in progress - subscriptions for both supplementary queries are created, not cleaned up yet
|
||||||
expect(unsubscribes).toHaveLength(2);
|
expect(unsubscribes).toHaveLength(2);
|
||||||
@ -830,7 +830,7 @@ describe('reducer', () => {
|
|||||||
expect(unsubscribes[1]).not.toBeCalled();
|
expect(unsubscribes[1]).not.toBeCalled();
|
||||||
|
|
||||||
setupQueryResponse(getState());
|
setupQueryResponse(getState());
|
||||||
dispatch(runQueries({ exploreId: ExploreId.left }));
|
dispatch(runQueries({ exploreId: 'left' }));
|
||||||
// a new query is run while supplementary queries are not resolve yet...
|
// a new query is run while supplementary queries are not resolve yet...
|
||||||
expect(unsubscribes[0]).toBeCalled();
|
expect(unsubscribes[0]).toBeCalled();
|
||||||
expect(unsubscribes[1]).toBeCalled();
|
expect(unsubscribes[1]).toBeCalled();
|
||||||
@ -841,12 +841,12 @@ describe('reducer', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should cancel all supported supplementary queries when the main query is canceled', () => {
|
it('should cancel all supported supplementary queries when the main query is canceled', () => {
|
||||||
dispatch(runQueries({ exploreId: ExploreId.left }));
|
dispatch(runQueries({ exploreId: 'left' }));
|
||||||
expect(unsubscribes).toHaveLength(2);
|
expect(unsubscribes).toHaveLength(2);
|
||||||
expect(unsubscribes[0]).not.toBeCalled();
|
expect(unsubscribes[0]).not.toBeCalled();
|
||||||
expect(unsubscribes[1]).not.toBeCalled();
|
expect(unsubscribes[1]).not.toBeCalled();
|
||||||
|
|
||||||
dispatch(cancelQueries(ExploreId.left));
|
dispatch(cancelQueries('left'));
|
||||||
expect(unsubscribes).toHaveLength(2);
|
expect(unsubscribes).toHaveLength(2);
|
||||||
expect(unsubscribes[0]).toBeCalled();
|
expect(unsubscribes[0]).toBeCalled();
|
||||||
expect(unsubscribes[1]).toBeCalled();
|
expect(unsubscribes[1]).toBeCalled();
|
||||||
@ -858,7 +858,7 @@ describe('reducer', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should load supplementary queries after running the query', () => {
|
it('should load supplementary queries after running the query', () => {
|
||||||
dispatch(runQueries({ exploreId: ExploreId.left }));
|
dispatch(runQueries({ exploreId: 'left' }));
|
||||||
expect(unsubscribes).toHaveLength(2);
|
expect(unsubscribes).toHaveLength(2);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -866,7 +866,7 @@ describe('reducer', () => {
|
|||||||
mockDataProvider = () => {
|
mockDataProvider = () => {
|
||||||
return of({ state: LoadingState.Loading, error: undefined, data: [] });
|
return of({ state: LoadingState.Loading, error: undefined, data: [] });
|
||||||
};
|
};
|
||||||
dispatch(runQueries({ exploreId: ExploreId.left }));
|
dispatch(runQueries({ exploreId: 'left' }));
|
||||||
|
|
||||||
for (const type of supplementaryQueryTypes) {
|
for (const type of supplementaryQueryTypes) {
|
||||||
expect(getState().explore.panes.left!.supplementaryQueries[type].data).toBeDefined();
|
expect(getState().explore.panes.left!.supplementaryQueries[type].data).toBeDefined();
|
||||||
@ -879,7 +879,7 @@ describe('reducer', () => {
|
|||||||
expect(getState().explore.panes.left!.supplementaryQueries[type].dataProvider).toBeDefined();
|
expect(getState().explore.panes.left!.supplementaryQueries[type].dataProvider).toBeDefined();
|
||||||
}
|
}
|
||||||
|
|
||||||
dispatch(cancelQueries(ExploreId.left));
|
dispatch(cancelQueries('left'));
|
||||||
for (const type of supplementaryQueryTypes) {
|
for (const type of supplementaryQueryTypes) {
|
||||||
expect(getState().explore.panes.left!.supplementaryQueries[type].data).toBeUndefined();
|
expect(getState().explore.panes.left!.supplementaryQueries[type].data).toBeUndefined();
|
||||||
expect(getState().explore.panes.left!.supplementaryQueries[type].data).toBeUndefined();
|
expect(getState().explore.panes.left!.supplementaryQueries[type].data).toBeUndefined();
|
||||||
@ -893,7 +893,7 @@ describe('reducer', () => {
|
|||||||
{ state: LoadingState.Done, error: undefined, data: [{}] }
|
{ state: LoadingState.Done, error: undefined, data: [{}] }
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
dispatch(runQueries({ exploreId: ExploreId.left }));
|
dispatch(runQueries({ exploreId: 'left' }));
|
||||||
|
|
||||||
for (const types of supplementaryQueryTypes) {
|
for (const types of supplementaryQueryTypes) {
|
||||||
expect(getState().explore.panes.left!.supplementaryQueries[types].data).toBeDefined();
|
expect(getState().explore.panes.left!.supplementaryQueries[types].data).toBeDefined();
|
||||||
@ -901,7 +901,7 @@ describe('reducer', () => {
|
|||||||
expect(getState().explore.panes.left!.supplementaryQueries[types].dataProvider).toBeDefined();
|
expect(getState().explore.panes.left!.supplementaryQueries[types].dataProvider).toBeDefined();
|
||||||
}
|
}
|
||||||
|
|
||||||
dispatch(cancelQueries(ExploreId.left));
|
dispatch(cancelQueries('left'));
|
||||||
|
|
||||||
for (const types of supplementaryQueryTypes) {
|
for (const types of supplementaryQueryTypes) {
|
||||||
expect(getState().explore.panes.left!.supplementaryQueries[types].data).toBeDefined();
|
expect(getState().explore.panes.left!.supplementaryQueries[types].data).toBeDefined();
|
||||||
@ -915,14 +915,14 @@ describe('reducer', () => {
|
|||||||
return of({ state: LoadingState.Done, error: undefined, data: [{}] });
|
return of({ state: LoadingState.Done, error: undefined, data: [{}] });
|
||||||
};
|
};
|
||||||
// turn logs volume off (but keep logs sample on)
|
// turn logs volume off (but keep logs sample on)
|
||||||
dispatch(setSupplementaryQueryEnabled(ExploreId.left, false, SupplementaryQueryType.LogsVolume));
|
dispatch(setSupplementaryQueryEnabled('left', false, SupplementaryQueryType.LogsVolume));
|
||||||
expect(getState().explore.panes.left!.supplementaryQueries[SupplementaryQueryType.LogsVolume].enabled).toBe(
|
expect(getState().explore.panes.left!.supplementaryQueries[SupplementaryQueryType.LogsVolume].enabled).toBe(
|
||||||
false
|
false
|
||||||
);
|
);
|
||||||
expect(getState().explore.panes.left!.supplementaryQueries[SupplementaryQueryType.LogsSample].enabled).toBe(true);
|
expect(getState().explore.panes.left!.supplementaryQueries[SupplementaryQueryType.LogsSample].enabled).toBe(true);
|
||||||
|
|
||||||
// verify that if we run a query, it will: 1) not do logs volume, 2) do logs sample 3) provider will still be set for both
|
// verify that if we run a query, it will: 1) not do logs volume, 2) do logs sample 3) provider will still be set for both
|
||||||
dispatch(runQueries({ exploreId: ExploreId.left }));
|
dispatch(runQueries({ exploreId: 'left' }));
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
getState().explore.panes.left!.supplementaryQueries[SupplementaryQueryType.LogsVolume].data
|
getState().explore.panes.left!.supplementaryQueries[SupplementaryQueryType.LogsVolume].data
|
||||||
@ -945,11 +945,11 @@ describe('reducer', () => {
|
|||||||
|
|
||||||
it('load data of supplementary query that gets enabled', async () => {
|
it('load data of supplementary query that gets enabled', async () => {
|
||||||
// first we start with both supplementary queries disabled
|
// first we start with both supplementary queries disabled
|
||||||
dispatch(setSupplementaryQueryEnabled(ExploreId.left, false, SupplementaryQueryType.LogsVolume));
|
dispatch(setSupplementaryQueryEnabled('left', false, SupplementaryQueryType.LogsVolume));
|
||||||
dispatch(setSupplementaryQueryEnabled(ExploreId.left, false, SupplementaryQueryType.LogsSample));
|
dispatch(setSupplementaryQueryEnabled('left', false, SupplementaryQueryType.LogsSample));
|
||||||
|
|
||||||
// runQueries sets up providers, but does not run queries
|
// runQueries sets up providers, but does not run queries
|
||||||
dispatch(runQueries({ exploreId: ExploreId.left }));
|
dispatch(runQueries({ exploreId: 'left' }));
|
||||||
expect(
|
expect(
|
||||||
getState().explore.panes.left!.supplementaryQueries[SupplementaryQueryType.LogsVolume].dataProvider
|
getState().explore.panes.left!.supplementaryQueries[SupplementaryQueryType.LogsVolume].dataProvider
|
||||||
).toBeDefined();
|
).toBeDefined();
|
||||||
@ -958,7 +958,7 @@ describe('reducer', () => {
|
|||||||
).toBeDefined();
|
).toBeDefined();
|
||||||
|
|
||||||
// we turn 1 supplementary query (logs volume) on
|
// we turn 1 supplementary query (logs volume) on
|
||||||
dispatch(setSupplementaryQueryEnabled(ExploreId.left, true, SupplementaryQueryType.LogsVolume));
|
dispatch(setSupplementaryQueryEnabled('left', true, SupplementaryQueryType.LogsVolume));
|
||||||
|
|
||||||
// verify it was turned on
|
// verify it was turned on
|
||||||
expect(getState().explore.panes.left!.supplementaryQueries[SupplementaryQueryType.LogsVolume].enabled).toBe(true);
|
expect(getState().explore.panes.left!.supplementaryQueries[SupplementaryQueryType.LogsVolume].enabled).toBe(true);
|
||||||
@ -984,8 +984,8 @@ describe('reducer', () => {
|
|||||||
...defaultInitialState,
|
...defaultInitialState,
|
||||||
explore: {
|
explore: {
|
||||||
panes: {
|
panes: {
|
||||||
[ExploreId.left]: {
|
['left']: {
|
||||||
...defaultInitialState.explore.panes[ExploreId.left],
|
...defaultInitialState.explore.panes['left'],
|
||||||
queryResponse: {
|
queryResponse: {
|
||||||
state: LoadingState.Streaming,
|
state: LoadingState.Streaming,
|
||||||
},
|
},
|
||||||
@ -997,12 +997,12 @@ describe('reducer', () => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
} as unknown as Partial<StoreState>);
|
} as unknown as Partial<StoreState>);
|
||||||
expect(getState().explore.panes[ExploreId.left]?.logsResult?.rows.length).toBe(logRows.length);
|
expect(getState().explore.panes['left']?.logsResult?.rows.length).toBe(logRows.length);
|
||||||
|
|
||||||
await dispatch(clearLogs({ exploreId: ExploreId.left }));
|
await dispatch(clearLogs({ exploreId: 'left' }));
|
||||||
|
|
||||||
expect(getState().explore.panes[ExploreId.left]?.logsResult?.rows.length).toBe(0);
|
expect(getState().explore.panes['left']?.logsResult?.rows.length).toBe(0);
|
||||||
expect(getState().explore.panes[ExploreId.left]?.clearedAtIndex).toBe(logRows.length - 1);
|
expect(getState().explore.panes['left']?.clearedAtIndex).toBe(logRows.length - 1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should filter new log rows', async () => {
|
it('should filter new log rows', async () => {
|
||||||
@ -1014,8 +1014,8 @@ describe('reducer', () => {
|
|||||||
...defaultInitialState,
|
...defaultInitialState,
|
||||||
explore: {
|
explore: {
|
||||||
panes: {
|
panes: {
|
||||||
[ExploreId.left]: {
|
['left']: {
|
||||||
...defaultInitialState.explore.panes[ExploreId.left],
|
...defaultInitialState.explore.panes['left'],
|
||||||
isLive: true,
|
isLive: true,
|
||||||
queryResponse: {
|
queryResponse: {
|
||||||
state: LoadingState.Streaming,
|
state: LoadingState.Streaming,
|
||||||
@ -1029,12 +1029,12 @@ describe('reducer', () => {
|
|||||||
},
|
},
|
||||||
} as unknown as Partial<StoreState>);
|
} as unknown as Partial<StoreState>);
|
||||||
|
|
||||||
expect(getState().explore.panes[ExploreId.left]?.logsResult?.rows.length).toBe(oldLogRows.length);
|
expect(getState().explore.panes['left']?.logsResult?.rows.length).toBe(oldLogRows.length);
|
||||||
|
|
||||||
await dispatch(clearLogs({ exploreId: ExploreId.left }));
|
await dispatch(clearLogs({ exploreId: 'left' }));
|
||||||
await dispatch(
|
await dispatch(
|
||||||
queryStreamUpdatedAction({
|
queryStreamUpdatedAction({
|
||||||
exploreId: ExploreId.left,
|
exploreId: 'left',
|
||||||
response: {
|
response: {
|
||||||
request: true,
|
request: true,
|
||||||
traceFrames: [],
|
traceFrames: [],
|
||||||
@ -1049,8 +1049,8 @@ describe('reducer', () => {
|
|||||||
} as unknown as QueryEndedPayload)
|
} as unknown as QueryEndedPayload)
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(getState().explore.panes[ExploreId.left]?.logsResult?.rows.length).toBe(newLogRows.length);
|
expect(getState().explore.panes['left']?.logsResult?.rows.length).toBe(newLogRows.length);
|
||||||
expect(getState().explore.panes[ExploreId.left]?.clearedAtIndex).toBe(oldLogRows.length - 1);
|
expect(getState().explore.panes['left']?.clearedAtIndex).toBe(oldLogRows.length - 1);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -47,7 +47,7 @@ import {
|
|||||||
ThunkDispatch,
|
ThunkDispatch,
|
||||||
ThunkResult,
|
ThunkResult,
|
||||||
} from 'app/types';
|
} from 'app/types';
|
||||||
import { ExploreId, ExploreState, QueryOptions, SupplementaryQueries } from 'app/types/explore';
|
import { ExploreState, QueryOptions, SupplementaryQueries } from 'app/types/explore';
|
||||||
|
|
||||||
import { notifyApp } from '../../../core/actions';
|
import { notifyApp } from '../../../core/actions';
|
||||||
import { createErrorNotification } from '../../../core/copy/appNotification';
|
import { createErrorNotification } from '../../../core/copy/appNotification';
|
||||||
@ -66,7 +66,7 @@ import { createCacheKey, filterLogRowsByIndex, getResultsFromCache } from './uti
|
|||||||
/**
|
/**
|
||||||
* Derives from explore state if a given Explore pane is waiting for more data to be received
|
* Derives from explore state if a given Explore pane is waiting for more data to be received
|
||||||
*/
|
*/
|
||||||
export const selectIsWaitingForData = (exploreId: ExploreId) => {
|
export const selectIsWaitingForData = (exploreId: string) => {
|
||||||
return (state: StoreState) => {
|
return (state: StoreState) => {
|
||||||
const panelState = state.explore.panes[exploreId];
|
const panelState = state.explore.panes[exploreId];
|
||||||
if (!panelState) {
|
if (!panelState) {
|
||||||
@ -83,7 +83,7 @@ export const selectIsWaitingForData = (exploreId: ExploreId) => {
|
|||||||
* Adds a query row after the row with the given index.
|
* Adds a query row after the row with the given index.
|
||||||
*/
|
*/
|
||||||
export interface AddQueryRowPayload {
|
export interface AddQueryRowPayload {
|
||||||
exploreId: ExploreId;
|
exploreId: string;
|
||||||
index: number;
|
index: number;
|
||||||
query: DataQuery;
|
query: DataQuery;
|
||||||
}
|
}
|
||||||
@ -94,7 +94,7 @@ export const addQueryRowAction = createAction<AddQueryRowPayload>('explore/addQu
|
|||||||
* If `override` is reset the query modifications and run the queries. Use this to set queries via a link.
|
* If `override` is reset the query modifications and run the queries. Use this to set queries via a link.
|
||||||
*/
|
*/
|
||||||
export interface ChangeQueriesPayload {
|
export interface ChangeQueriesPayload {
|
||||||
exploreId: ExploreId;
|
exploreId: string;
|
||||||
queries: DataQuery[];
|
queries: DataQuery[];
|
||||||
}
|
}
|
||||||
export const changeQueriesAction = createAction<ChangeQueriesPayload>('explore/changeQueries');
|
export const changeQueriesAction = createAction<ChangeQueriesPayload>('explore/changeQueries');
|
||||||
@ -103,18 +103,18 @@ export const changeQueriesAction = createAction<ChangeQueriesPayload>('explore/c
|
|||||||
* Cancel running queries.
|
* Cancel running queries.
|
||||||
*/
|
*/
|
||||||
export interface CancelQueriesPayload {
|
export interface CancelQueriesPayload {
|
||||||
exploreId: ExploreId;
|
exploreId: string;
|
||||||
}
|
}
|
||||||
export const cancelQueriesAction = createAction<CancelQueriesPayload>('explore/cancelQueries');
|
export const cancelQueriesAction = createAction<CancelQueriesPayload>('explore/cancelQueries');
|
||||||
|
|
||||||
export interface QueriesImportedPayload {
|
export interface QueriesImportedPayload {
|
||||||
exploreId: ExploreId;
|
exploreId: string;
|
||||||
queries: DataQuery[];
|
queries: DataQuery[];
|
||||||
}
|
}
|
||||||
export const queriesImportedAction = createAction<QueriesImportedPayload>('explore/queriesImported');
|
export const queriesImportedAction = createAction<QueriesImportedPayload>('explore/queriesImported');
|
||||||
|
|
||||||
export interface QueryStoreSubscriptionPayload {
|
export interface QueryStoreSubscriptionPayload {
|
||||||
exploreId: ExploreId;
|
exploreId: string;
|
||||||
querySubscription: Unsubscribable;
|
querySubscription: Unsubscribable;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -123,19 +123,19 @@ export const queryStoreSubscriptionAction = createAction<QueryStoreSubscriptionP
|
|||||||
);
|
);
|
||||||
|
|
||||||
const setSupplementaryQueryEnabledAction = createAction<{
|
const setSupplementaryQueryEnabledAction = createAction<{
|
||||||
exploreId: ExploreId;
|
exploreId: string;
|
||||||
type: SupplementaryQueryType;
|
type: SupplementaryQueryType;
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
}>('explore/setSupplementaryQueryEnabledAction');
|
}>('explore/setSupplementaryQueryEnabledAction');
|
||||||
|
|
||||||
export interface StoreSupplementaryQueryDataProvider {
|
export interface StoreSupplementaryQueryDataProvider {
|
||||||
exploreId: ExploreId;
|
exploreId: string;
|
||||||
dataProvider?: Observable<DataQueryResponse>;
|
dataProvider?: Observable<DataQueryResponse>;
|
||||||
type: SupplementaryQueryType;
|
type: SupplementaryQueryType;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CleanSupplementaryQueryDataProvider {
|
export interface CleanSupplementaryQueryDataProvider {
|
||||||
exploreId: ExploreId;
|
exploreId: string;
|
||||||
type: SupplementaryQueryType;
|
type: SupplementaryQueryType;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -150,12 +150,12 @@ export const cleanSupplementaryQueryDataProviderAction = createAction<CleanSuppl
|
|||||||
'explore/cleanSupplementaryQueryDataProviderAction'
|
'explore/cleanSupplementaryQueryDataProviderAction'
|
||||||
);
|
);
|
||||||
|
|
||||||
export const cleanSupplementaryQueryAction = createAction<{ exploreId: ExploreId; type: SupplementaryQueryType }>(
|
export const cleanSupplementaryQueryAction = createAction<{ exploreId: string; type: SupplementaryQueryType }>(
|
||||||
'explore/cleanSupplementaryQueryAction'
|
'explore/cleanSupplementaryQueryAction'
|
||||||
);
|
);
|
||||||
|
|
||||||
export interface StoreSupplementaryQueryDataSubscriptionPayload {
|
export interface StoreSupplementaryQueryDataSubscriptionPayload {
|
||||||
exploreId: ExploreId;
|
exploreId: string;
|
||||||
dataSubscription?: SubscriptionLike;
|
dataSubscription?: SubscriptionLike;
|
||||||
type: SupplementaryQueryType;
|
type: SupplementaryQueryType;
|
||||||
}
|
}
|
||||||
@ -171,13 +171,13 @@ const storeSupplementaryQueryDataSubscriptionAction = createAction<StoreSuppleme
|
|||||||
* Stores data returned by the provider. Used internally by loadSupplementaryQueryData().
|
* Stores data returned by the provider. Used internally by loadSupplementaryQueryData().
|
||||||
*/
|
*/
|
||||||
const updateSupplementaryQueryDataAction = createAction<{
|
const updateSupplementaryQueryDataAction = createAction<{
|
||||||
exploreId: ExploreId;
|
exploreId: string;
|
||||||
type: SupplementaryQueryType;
|
type: SupplementaryQueryType;
|
||||||
data: DataQueryResponse;
|
data: DataQueryResponse;
|
||||||
}>('explore/updateSupplementaryQueryDataAction');
|
}>('explore/updateSupplementaryQueryDataAction');
|
||||||
|
|
||||||
export interface QueryEndedPayload {
|
export interface QueryEndedPayload {
|
||||||
exploreId: ExploreId;
|
exploreId: string;
|
||||||
response: ExplorePanelData;
|
response: ExplorePanelData;
|
||||||
}
|
}
|
||||||
export const queryStreamUpdatedAction = createAction<QueryEndedPayload>('explore/queryStreamUpdated');
|
export const queryStreamUpdatedAction = createAction<QueryEndedPayload>('explore/queryStreamUpdated');
|
||||||
@ -186,25 +186,25 @@ export const queryStreamUpdatedAction = createAction<QueryEndedPayload>('explore
|
|||||||
* Reset queries to the given queries. Any modifications will be discarded.
|
* Reset queries to the given queries. Any modifications will be discarded.
|
||||||
*/
|
*/
|
||||||
export interface SetQueriesPayload {
|
export interface SetQueriesPayload {
|
||||||
exploreId: ExploreId;
|
exploreId: string;
|
||||||
queries: DataQuery[];
|
queries: DataQuery[];
|
||||||
}
|
}
|
||||||
export const setQueriesAction = createAction<SetQueriesPayload>('explore/setQueries');
|
export const setQueriesAction = createAction<SetQueriesPayload>('explore/setQueries');
|
||||||
|
|
||||||
export interface ChangeLoadingStatePayload {
|
export interface ChangeLoadingStatePayload {
|
||||||
exploreId: ExploreId;
|
exploreId: string;
|
||||||
loadingState: LoadingState;
|
loadingState: LoadingState;
|
||||||
}
|
}
|
||||||
export const changeLoadingStateAction = createAction<ChangeLoadingStatePayload>('changeLoadingState');
|
export const changeLoadingStateAction = createAction<ChangeLoadingStatePayload>('changeLoadingState');
|
||||||
|
|
||||||
export interface SetPausedStatePayload {
|
export interface SetPausedStatePayload {
|
||||||
exploreId: ExploreId;
|
exploreId: string;
|
||||||
isPaused: boolean;
|
isPaused: boolean;
|
||||||
}
|
}
|
||||||
export const setPausedStateAction = createAction<SetPausedStatePayload>('explore/setPausedState');
|
export const setPausedStateAction = createAction<SetPausedStatePayload>('explore/setPausedState');
|
||||||
|
|
||||||
export interface ClearLogsPayload {
|
export interface ClearLogsPayload {
|
||||||
exploreId: ExploreId;
|
exploreId: string;
|
||||||
}
|
}
|
||||||
export const clearLogs = createAction<ClearLogsPayload>('explore/clearLogs');
|
export const clearLogs = createAction<ClearLogsPayload>('explore/clearLogs');
|
||||||
/**
|
/**
|
||||||
@ -213,7 +213,7 @@ export const clearLogs = createAction<ClearLogsPayload>('explore/clearLogs');
|
|||||||
* @param scanner Function that a) returns a new time range and b) triggers a query run for the new range
|
* @param scanner Function that a) returns a new time range and b) triggers a query run for the new range
|
||||||
*/
|
*/
|
||||||
export interface ScanStartPayload {
|
export interface ScanStartPayload {
|
||||||
exploreId: ExploreId;
|
exploreId: string;
|
||||||
}
|
}
|
||||||
export const scanStartAction = createAction<ScanStartPayload>('explore/scanStart');
|
export const scanStartAction = createAction<ScanStartPayload>('explore/scanStart');
|
||||||
|
|
||||||
@ -221,7 +221,7 @@ export const scanStartAction = createAction<ScanStartPayload>('explore/scanStart
|
|||||||
* Stop any scanning for more results.
|
* Stop any scanning for more results.
|
||||||
*/
|
*/
|
||||||
export interface ScanStopPayload {
|
export interface ScanStopPayload {
|
||||||
exploreId: ExploreId;
|
exploreId: string;
|
||||||
}
|
}
|
||||||
export const scanStopAction = createAction<ScanStopPayload>('explore/scanStop');
|
export const scanStopAction = createAction<ScanStopPayload>('explore/scanStop');
|
||||||
|
|
||||||
@ -230,7 +230,7 @@ export const scanStopAction = createAction<ScanStopPayload>('explore/scanStop');
|
|||||||
* This is currently used to cache last 5 query results for log queries run from logs navigation (pagination).
|
* This is currently used to cache last 5 query results for log queries run from logs navigation (pagination).
|
||||||
*/
|
*/
|
||||||
export interface AddResultsToCachePayload {
|
export interface AddResultsToCachePayload {
|
||||||
exploreId: ExploreId;
|
exploreId: string;
|
||||||
cacheKey: string;
|
cacheKey: string;
|
||||||
queryResponse: ExplorePanelData;
|
queryResponse: ExplorePanelData;
|
||||||
}
|
}
|
||||||
@ -240,14 +240,14 @@ export const addResultsToCacheAction = createAction<AddResultsToCachePayload>('e
|
|||||||
* Clears cache.
|
* Clears cache.
|
||||||
*/
|
*/
|
||||||
export interface ClearCachePayload {
|
export interface ClearCachePayload {
|
||||||
exploreId: ExploreId;
|
exploreId: string;
|
||||||
}
|
}
|
||||||
export const clearCacheAction = createAction<ClearCachePayload>('explore/clearCache');
|
export const clearCacheAction = createAction<ClearCachePayload>('explore/clearCache');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Adds a query row after the row with the given index.
|
* Adds a query row after the row with the given index.
|
||||||
*/
|
*/
|
||||||
export function addQueryRow(exploreId: ExploreId, index: number): ThunkResult<void> {
|
export function addQueryRow(exploreId: string, index: number): ThunkResult<void> {
|
||||||
return async (dispatch, getState) => {
|
return async (dispatch, getState) => {
|
||||||
const queries = getState().explore.panes[exploreId]!.queries;
|
const queries = getState().explore.panes[exploreId]!.queries;
|
||||||
let datasourceOverride = undefined;
|
let datasourceOverride = undefined;
|
||||||
@ -270,7 +270,7 @@ export function addQueryRow(exploreId: ExploreId, index: number): ThunkResult<vo
|
|||||||
/**
|
/**
|
||||||
* Cancel running queries
|
* Cancel running queries
|
||||||
*/
|
*/
|
||||||
export function cancelQueries(exploreId: ExploreId): ThunkResult<void> {
|
export function cancelQueries(exploreId: string): ThunkResult<void> {
|
||||||
return (dispatch, getState) => {
|
return (dispatch, getState) => {
|
||||||
dispatch(scanStopAction({ exploreId }));
|
dispatch(scanStopAction({ exploreId }));
|
||||||
dispatch(cancelQueriesAction({ exploreId }));
|
dispatch(cancelQueriesAction({ exploreId }));
|
||||||
@ -352,7 +352,7 @@ export const changeQueries = createAsyncThunk<void, ChangeQueriesPayload>(
|
|||||||
* @param targetDataSource
|
* @param targetDataSource
|
||||||
*/
|
*/
|
||||||
export const importQueries = (
|
export const importQueries = (
|
||||||
exploreId: ExploreId,
|
exploreId: string,
|
||||||
queries: DataQuery[],
|
queries: DataQuery[],
|
||||||
sourceDataSource: DataSourceApi | undefined | null,
|
sourceDataSource: DataSourceApi | undefined | null,
|
||||||
targetDataSource: DataSourceApi,
|
targetDataSource: DataSourceApi,
|
||||||
@ -425,7 +425,7 @@ export const importQueries = (
|
|||||||
* @param modifier Function that executes the modification, typically `datasourceInstance.modifyQueries`.
|
* @param modifier Function that executes the modification, typically `datasourceInstance.modifyQueries`.
|
||||||
*/
|
*/
|
||||||
export function modifyQueries(
|
export function modifyQueries(
|
||||||
exploreId: ExploreId,
|
exploreId: string,
|
||||||
modification: QueryFixAction,
|
modification: QueryFixAction,
|
||||||
modifier: (query: DataQuery, modification: QueryFixAction) => Promise<DataQuery>
|
modifier: (query: DataQuery, modification: QueryFixAction) => Promise<DataQuery>
|
||||||
): ThunkResult<void> {
|
): ThunkResult<void> {
|
||||||
@ -453,7 +453,7 @@ async function handleHistory(
|
|||||||
history: Array<HistoryItem<DataQuery>>,
|
history: Array<HistoryItem<DataQuery>>,
|
||||||
datasource: DataSourceApi,
|
datasource: DataSourceApi,
|
||||||
queries: DataQuery[],
|
queries: DataQuery[],
|
||||||
exploreId: ExploreId
|
exploreId: string
|
||||||
) {
|
) {
|
||||||
const datasourceId = datasource.meta.id;
|
const datasourceId = datasource.meta.id;
|
||||||
const nextHistory = updateHistory(history, datasourceId, queries);
|
const nextHistory = updateHistory(history, datasourceId, queries);
|
||||||
@ -465,12 +465,12 @@ async function handleHistory(
|
|||||||
// used filters. Instead, we refresh the query history list.
|
// used filters. Instead, we refresh the query history list.
|
||||||
// TODO: run only if Query History list is opened (#47252)
|
// TODO: run only if Query History list is opened (#47252)
|
||||||
for (const exploreId in state.panes) {
|
for (const exploreId in state.panes) {
|
||||||
await dispatch(loadRichHistory(exploreId as ExploreId));
|
await dispatch(loadRichHistory(exploreId));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
interface RunQueriesOptions {
|
interface RunQueriesOptions {
|
||||||
exploreId: ExploreId;
|
exploreId: string;
|
||||||
preserveCache?: boolean;
|
preserveCache?: boolean;
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
@ -653,7 +653,7 @@ const groupDataQueries = async (datasources: DataQuery[], scopedVars: ScopedVars
|
|||||||
};
|
};
|
||||||
|
|
||||||
type HandleSupplementaryQueriesOptions = {
|
type HandleSupplementaryQueriesOptions = {
|
||||||
exploreId: ExploreId;
|
exploreId: string;
|
||||||
transaction: QueryTransaction;
|
transaction: QueryTransaction;
|
||||||
datasourceInstance: DataSourceApi;
|
datasourceInstance: DataSourceApi;
|
||||||
newQuerySource: Observable<ExplorePanelData>;
|
newQuerySource: Observable<ExplorePanelData>;
|
||||||
@ -772,7 +772,7 @@ function canReuseSupplementaryQueryData(
|
|||||||
* Reset queries to the given queries. Any modifications will be discarded.
|
* Reset queries to the given queries. Any modifications will be discarded.
|
||||||
* Use this action for clicks on query examples. Triggers a query run.
|
* Use this action for clicks on query examples. Triggers a query run.
|
||||||
*/
|
*/
|
||||||
export function setQueries(exploreId: ExploreId, rawQueries: DataQuery[]): ThunkResult<void> {
|
export function setQueries(exploreId: string, rawQueries: DataQuery[]): ThunkResult<void> {
|
||||||
return (dispatch, getState) => {
|
return (dispatch, getState) => {
|
||||||
// Inject react keys into query objects
|
// Inject react keys into query objects
|
||||||
const queries = getState().explore.panes[exploreId]!.queries;
|
const queries = getState().explore.panes[exploreId]!.queries;
|
||||||
@ -787,7 +787,7 @@ export function setQueries(exploreId: ExploreId, rawQueries: DataQuery[]): Thunk
|
|||||||
* @param exploreId Explore area
|
* @param exploreId Explore area
|
||||||
* @param scanner Function that a) returns a new time range and b) triggers a query run for the new range
|
* @param scanner Function that a) returns a new time range and b) triggers a query run for the new range
|
||||||
*/
|
*/
|
||||||
export function scanStart(exploreId: ExploreId): ThunkResult<void> {
|
export function scanStart(exploreId: string): ThunkResult<void> {
|
||||||
return (dispatch, getState) => {
|
return (dispatch, getState) => {
|
||||||
// Register the scanner
|
// Register the scanner
|
||||||
dispatch(scanStartAction({ exploreId }));
|
dispatch(scanStartAction({ exploreId }));
|
||||||
@ -799,7 +799,7 @@ export function scanStart(exploreId: ExploreId): ThunkResult<void> {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function addResultsToCache(exploreId: ExploreId): ThunkResult<void> {
|
export function addResultsToCache(exploreId: string): ThunkResult<void> {
|
||||||
return (dispatch, getState) => {
|
return (dispatch, getState) => {
|
||||||
const queryResponse = getState().explore.panes[exploreId]!.queryResponse;
|
const queryResponse = getState().explore.panes[exploreId]!.queryResponse;
|
||||||
const absoluteRange = getState().explore.panes[exploreId]!.absoluteRange;
|
const absoluteRange = getState().explore.panes[exploreId]!.absoluteRange;
|
||||||
@ -812,7 +812,7 @@ export function addResultsToCache(exploreId: ExploreId): ThunkResult<void> {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function clearCache(exploreId: ExploreId): ThunkResult<void> {
|
export function clearCache(exploreId: string): ThunkResult<void> {
|
||||||
return (dispatch, getState) => {
|
return (dispatch, getState) => {
|
||||||
dispatch(clearCacheAction({ exploreId }));
|
dispatch(clearCacheAction({ exploreId }));
|
||||||
};
|
};
|
||||||
@ -821,7 +821,7 @@ export function clearCache(exploreId: ExploreId): ThunkResult<void> {
|
|||||||
/**
|
/**
|
||||||
* Initializes loading logs volume data and stores emitted value.
|
* Initializes loading logs volume data and stores emitted value.
|
||||||
*/
|
*/
|
||||||
export function loadSupplementaryQueryData(exploreId: ExploreId, type: SupplementaryQueryType): ThunkResult<void> {
|
export function loadSupplementaryQueryData(exploreId: string, type: SupplementaryQueryType): ThunkResult<void> {
|
||||||
return (dispatch, getState) => {
|
return (dispatch, getState) => {
|
||||||
const { supplementaryQueries } = getState().explore.panes[exploreId]!;
|
const { supplementaryQueries } = getState().explore.panes[exploreId]!;
|
||||||
const dataProvider = supplementaryQueries[type].dataProvider;
|
const dataProvider = supplementaryQueries[type].dataProvider;
|
||||||
@ -844,7 +844,7 @@ export function loadSupplementaryQueryData(exploreId: ExploreId, type: Supplemen
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function setSupplementaryQueryEnabled(
|
export function setSupplementaryQueryEnabled(
|
||||||
exploreId: ExploreId,
|
exploreId: string,
|
||||||
enabled: boolean,
|
enabled: boolean,
|
||||||
type: SupplementaryQueryType
|
type: SupplementaryQueryType
|
||||||
): ThunkResult<void> {
|
): ThunkResult<void> {
|
||||||
|
@ -1,19 +0,0 @@
|
|||||||
import { initialExploreState } from './main';
|
|
||||||
import { selectOrderedExplorePanes } from './selectors';
|
|
||||||
import { makeExplorePaneState } from './utils';
|
|
||||||
|
|
||||||
describe('getOrderedExplorePanes', () => {
|
|
||||||
it('returns a panes object with entries in the correct order', () => {
|
|
||||||
const selectorResult = selectOrderedExplorePanes({
|
|
||||||
explore: {
|
|
||||||
...initialExploreState,
|
|
||||||
panes: {
|
|
||||||
right: makeExplorePaneState(),
|
|
||||||
left: makeExplorePaneState(),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(Object.keys(selectorResult)).toEqual(['left', 'right']);
|
|
||||||
});
|
|
||||||
});
|
|
@ -1,26 +1,14 @@
|
|||||||
import { createSelector } from '@reduxjs/toolkit';
|
import { createSelector } from '@reduxjs/toolkit';
|
||||||
|
|
||||||
import { ExploreId, ExploreState, StoreState } from 'app/types';
|
import { ExploreItemState, StoreState } from 'app/types';
|
||||||
|
|
||||||
export const selectPanes = (state: Pick<StoreState, 'explore'>) => state.explore.panes;
|
export const selectPanes = (state: Pick<StoreState, 'explore'>) => state.explore.panes;
|
||||||
|
|
||||||
/**
|
export const selectPanesEntries = createSelector<
|
||||||
* Explore renders panes by iterating over the panes object. This selector ensures that entries in the returned panes object
|
[(state: Pick<StoreState, 'explore'>) => Record<string, ExploreItemState | undefined>],
|
||||||
* are in the correct order.
|
Array<[string, ExploreItemState]>
|
||||||
*/
|
>(selectPanes, Object.entries);
|
||||||
export const selectOrderedExplorePanes = createSelector(selectPanes, (panes) => {
|
|
||||||
const orderedPanes: ExploreState['panes'] = {};
|
|
||||||
|
|
||||||
if (panes.left) {
|
export const isSplit = createSelector(selectPanesEntries, (panes) => panes.length > 1);
|
||||||
orderedPanes.left = panes.left;
|
|
||||||
}
|
|
||||||
if (panes.right) {
|
|
||||||
orderedPanes.right = panes.right;
|
|
||||||
}
|
|
||||||
return orderedPanes;
|
|
||||||
});
|
|
||||||
|
|
||||||
export const isSplit = createSelector(selectPanes, (panes) => Object.keys(panes).length > 1);
|
export const getExploreItemSelector = (exploreId: string) => createSelector(selectPanes, (panes) => panes[exploreId]);
|
||||||
|
|
||||||
export const getExploreItemSelector = (exploreId: ExploreId) =>
|
|
||||||
createSelector(selectPanes, (panes) => panes[exploreId]);
|
|
||||||
|
@ -2,7 +2,7 @@ import { reducerTester } from 'test/core/redux/reducerTester';
|
|||||||
|
|
||||||
import { dateTime } from '@grafana/data';
|
import { dateTime } from '@grafana/data';
|
||||||
import { configureStore } from 'app/store/configureStore';
|
import { configureStore } from 'app/store/configureStore';
|
||||||
import { ExploreId, ExploreItemState } from 'app/types';
|
import { ExploreItemState } from 'app/types';
|
||||||
|
|
||||||
import { createDefaultInitialState } from './helpers';
|
import { createDefaultInitialState } from './helpers';
|
||||||
import { changeRangeAction, timeReducer, updateTime } from './time';
|
import { changeRangeAction, timeReducer, updateTime } from './time';
|
||||||
@ -30,7 +30,7 @@ describe('Explore item reducer', () => {
|
|||||||
describe('When time is updated', () => {
|
describe('When time is updated', () => {
|
||||||
it('Time service is re-initialized and template service is updated with the new time range', async () => {
|
it('Time service is re-initialized and template service is updated with the new time range', async () => {
|
||||||
const { dispatch } = configureStore(createDefaultInitialState().defaultInitialState as any);
|
const { dispatch } = configureStore(createDefaultInitialState().defaultInitialState as any);
|
||||||
dispatch(updateTime({ exploreId: ExploreId.left }));
|
dispatch(updateTime({ exploreId: 'left' }));
|
||||||
expect(mockTimeSrv.init).toBeCalled();
|
expect(mockTimeSrv.init).toBeCalled();
|
||||||
expect(mockTemplateSrv.updateTimeRange).toBeCalledWith(MOCK_TIME_RANGE);
|
expect(mockTemplateSrv.updateTimeRange).toBeCalledWith(MOCK_TIME_RANGE);
|
||||||
});
|
});
|
||||||
@ -46,7 +46,7 @@ describe('Explore item reducer', () => {
|
|||||||
} as unknown as ExploreItemState)
|
} as unknown as ExploreItemState)
|
||||||
.whenActionIsDispatched(
|
.whenActionIsDispatched(
|
||||||
changeRangeAction({
|
changeRangeAction({
|
||||||
exploreId: ExploreId.left,
|
exploreId: 'left',
|
||||||
absoluteRange: { from: 1546297200000, to: 1546383600000 },
|
absoluteRange: { from: 1546297200000, to: 1546383600000 },
|
||||||
range: { from: dateTime('2019-01-01'), to: dateTime('2019-01-02'), raw: { from: 'now-1d', to: 'now' } },
|
range: { from: dateTime('2019-01-01'), to: dateTime('2019-01-02'), raw: { from: 'now-1d', to: 'now' } },
|
||||||
})
|
})
|
||||||
|
@ -7,7 +7,6 @@ import { getTimeRange, refreshIntervalToSortOrder, stopQueryState } from 'app/co
|
|||||||
import { sortLogsResult } from 'app/features/logs/utils';
|
import { sortLogsResult } from 'app/features/logs/utils';
|
||||||
import { getFiscalYearStartMonth, getTimeZone } from 'app/features/profile/state/selectors';
|
import { getFiscalYearStartMonth, getTimeZone } from 'app/features/profile/state/selectors';
|
||||||
import { ExploreItemState, ThunkResult } from 'app/types';
|
import { ExploreItemState, ThunkResult } from 'app/types';
|
||||||
import { ExploreId } from 'app/types/explore';
|
|
||||||
|
|
||||||
import { getTimeSrv } from '../../dashboard/services/TimeSrv';
|
import { getTimeSrv } from '../../dashboard/services/TimeSrv';
|
||||||
import { TimeModel } from '../../dashboard/state/TimeModel';
|
import { TimeModel } from '../../dashboard/state/TimeModel';
|
||||||
@ -20,7 +19,7 @@ import { runQueries } from './query';
|
|||||||
//
|
//
|
||||||
|
|
||||||
export interface ChangeRangePayload {
|
export interface ChangeRangePayload {
|
||||||
exploreId: ExploreId;
|
exploreId: string;
|
||||||
range: TimeRange;
|
range: TimeRange;
|
||||||
absoluteRange: AbsoluteTimeRange;
|
absoluteRange: AbsoluteTimeRange;
|
||||||
}
|
}
|
||||||
@ -30,13 +29,13 @@ export const changeRangeAction = createAction<ChangeRangePayload>('explore/chang
|
|||||||
* Change the time range of Explore. Usually called from the Timepicker or a graph interaction.
|
* Change the time range of Explore. Usually called from the Timepicker or a graph interaction.
|
||||||
*/
|
*/
|
||||||
export interface ChangeRefreshIntervalPayload {
|
export interface ChangeRefreshIntervalPayload {
|
||||||
exploreId: ExploreId;
|
exploreId: string;
|
||||||
refreshInterval: string;
|
refreshInterval: string;
|
||||||
}
|
}
|
||||||
export const changeRefreshInterval = createAction<ChangeRefreshIntervalPayload>('explore/changeRefreshInterval');
|
export const changeRefreshInterval = createAction<ChangeRefreshIntervalPayload>('explore/changeRefreshInterval');
|
||||||
|
|
||||||
export const updateTimeRange = (options: {
|
export const updateTimeRange = (options: {
|
||||||
exploreId: ExploreId;
|
exploreId: string;
|
||||||
rawRange?: RawTimeRange;
|
rawRange?: RawTimeRange;
|
||||||
absoluteRange?: AbsoluteTimeRange;
|
absoluteRange?: AbsoluteTimeRange;
|
||||||
}): ThunkResult<void> => {
|
}): ThunkResult<void> => {
|
||||||
@ -44,8 +43,8 @@ export const updateTimeRange = (options: {
|
|||||||
const { syncedTimes } = getState().explore;
|
const { syncedTimes } = getState().explore;
|
||||||
if (syncedTimes) {
|
if (syncedTimes) {
|
||||||
Object.keys(getState().explore.panes).forEach((exploreId) => {
|
Object.keys(getState().explore.panes).forEach((exploreId) => {
|
||||||
dispatch(updateTime({ ...options, exploreId: exploreId as ExploreId }));
|
dispatch(updateTime({ ...options, exploreId }));
|
||||||
dispatch(runQueries({ exploreId: exploreId as ExploreId, preserveCache: true }));
|
dispatch(runQueries({ exploreId: exploreId, preserveCache: true }));
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
dispatch(updateTime({ ...options }));
|
dispatch(updateTime({ ...options }));
|
||||||
@ -55,7 +54,7 @@ export const updateTimeRange = (options: {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const updateTime = (config: {
|
export const updateTime = (config: {
|
||||||
exploreId: ExploreId;
|
exploreId: string;
|
||||||
rawRange?: RawTimeRange;
|
rawRange?: RawTimeRange;
|
||||||
absoluteRange?: AbsoluteTimeRange;
|
absoluteRange?: AbsoluteTimeRange;
|
||||||
}): ThunkResult<void> => {
|
}): ThunkResult<void> => {
|
||||||
@ -104,14 +103,14 @@ export const updateTime = (config: {
|
|||||||
* Syncs time interval, if they are not synced on both panels in a split mode.
|
* Syncs time interval, if they are not synced on both panels in a split mode.
|
||||||
* Unsyncs time interval, if they are synced on both panels in a split mode.
|
* Unsyncs time interval, if they are synced on both panels in a split mode.
|
||||||
*/
|
*/
|
||||||
export function syncTimes(exploreId: ExploreId): ThunkResult<void> {
|
export function syncTimes(exploreId: string): ThunkResult<void> {
|
||||||
return (dispatch, getState) => {
|
return (dispatch, getState) => {
|
||||||
const range = getState().explore.panes[exploreId]!.range.raw;
|
const range = getState().explore.panes[exploreId]!.range.raw;
|
||||||
|
|
||||||
Object.keys(getState().explore.panes)
|
Object.keys(getState().explore.panes)
|
||||||
.filter((key) => key !== exploreId)
|
.filter((key) => key !== exploreId)
|
||||||
.forEach((exploreId) => {
|
.forEach((exploreId) => {
|
||||||
dispatch(updateTimeRange({ exploreId: exploreId as ExploreId, rawRange: range }));
|
dispatch(updateTimeRange({ exploreId, rawRange: range }));
|
||||||
});
|
});
|
||||||
|
|
||||||
const isTimeSynced = getState().explore.syncedTimes;
|
const isTimeSynced = getState().explore.syncedTimes;
|
||||||
@ -132,7 +131,7 @@ export function makeAbsoluteTime(): ThunkResult<void> {
|
|||||||
Object.entries(getState().explore.panes).forEach(([exploreId, exploreItemState]) => {
|
Object.entries(getState().explore.panes).forEach(([exploreId, exploreItemState]) => {
|
||||||
const range = getTimeRange(timeZone, exploreItemState!.range.raw, fiscalYearStartMonth);
|
const range = getTimeRange(timeZone, exploreItemState!.range.raw, fiscalYearStartMonth);
|
||||||
const absoluteRange: AbsoluteTimeRange = { from: range.from.valueOf(), to: range.to.valueOf() };
|
const absoluteRange: AbsoluteTimeRange = { from: range.from.valueOf(), to: range.to.valueOf() };
|
||||||
dispatch(updateTime({ exploreId: exploreId as ExploreId, absoluteRange }));
|
dispatch(updateTime({ exploreId, absoluteRange }));
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -3,15 +3,13 @@ import React, { useCallback } from 'react';
|
|||||||
import { RefreshPicker } from '@grafana/ui';
|
import { RefreshPicker } from '@grafana/ui';
|
||||||
import { useDispatch } from 'app/types';
|
import { useDispatch } from 'app/types';
|
||||||
|
|
||||||
import { ExploreId } from '../../types';
|
|
||||||
|
|
||||||
import { setPausedStateAction, runQueries, clearLogs } from './state/query';
|
import { setPausedStateAction, runQueries, clearLogs } from './state/query';
|
||||||
import { changeRefreshInterval } from './state/time';
|
import { changeRefreshInterval } from './state/time';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Hook that gives you all the functions needed to control the live tailing.
|
* Hook that gives you all the functions needed to control the live tailing.
|
||||||
*/
|
*/
|
||||||
export function useLiveTailControls(exploreId: ExploreId) {
|
export function useLiveTailControls(exploreId: string) {
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
const pause = useCallback(() => {
|
const pause = useCallback(() => {
|
||||||
@ -52,7 +50,7 @@ export function useLiveTailControls(exploreId: ExploreId) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
exploreId: ExploreId;
|
exploreId: string;
|
||||||
children: (controls: ReturnType<typeof useLiveTailControls>) => React.ReactElement;
|
children: (controls: ReturnType<typeof useLiveTailControls>) => React.ReactElement;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -13,7 +13,6 @@ import {
|
|||||||
TypeaheadOutput,
|
TypeaheadOutput,
|
||||||
withTheme2,
|
withTheme2,
|
||||||
} from '@grafana/ui';
|
} from '@grafana/ui';
|
||||||
import { ExploreId } from 'app/types';
|
|
||||||
|
|
||||||
// Utils & Services
|
// Utils & Services
|
||||||
// dom also includes Element polyfills
|
// dom also includes Element polyfills
|
||||||
@ -30,7 +29,7 @@ export interface CloudWatchLogsQueryFieldProps
|
|||||||
absoluteRange: AbsoluteTimeRange;
|
absoluteRange: AbsoluteTimeRange;
|
||||||
onLabelsRefresh?: () => void;
|
onLabelsRefresh?: () => void;
|
||||||
ExtraFieldElement?: ReactNode;
|
ExtraFieldElement?: ReactNode;
|
||||||
exploreId: ExploreId;
|
exploreId: string;
|
||||||
query: CloudWatchLogsQuery;
|
query: CloudWatchLogsQuery;
|
||||||
}
|
}
|
||||||
const plugins: Array<Plugin<Editor>> = [
|
const plugins: Array<Plugin<Editor>> = [
|
||||||
|
@ -15,21 +15,13 @@ import {
|
|||||||
DataQueryResponse,
|
DataQueryResponse,
|
||||||
ExplorePanelsState,
|
ExplorePanelsState,
|
||||||
SupplementaryQueryType,
|
SupplementaryQueryType,
|
||||||
|
UrlQueryMap,
|
||||||
} from '@grafana/data';
|
} from '@grafana/data';
|
||||||
import { RichHistorySearchFilters, RichHistorySettings } from 'app/core/utils/richHistoryTypes';
|
import { RichHistorySearchFilters, RichHistorySettings } from 'app/core/utils/richHistoryTypes';
|
||||||
|
|
||||||
import { CorrelationData } from '../features/correlations/useCorrelations';
|
import { CorrelationData } from '../features/correlations/useCorrelations';
|
||||||
|
|
||||||
export enum ExploreId {
|
export type ExploreQueryParams = UrlQueryMap;
|
||||||
left = 'left',
|
|
||||||
right = 'right',
|
|
||||||
}
|
|
||||||
|
|
||||||
export type ExploreQueryParams = {
|
|
||||||
left?: string;
|
|
||||||
right?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Global Explore state
|
* Global Explore state
|
||||||
*/
|
*/
|
||||||
@ -39,13 +31,7 @@ export interface ExploreState {
|
|||||||
*/
|
*/
|
||||||
syncedTimes: boolean;
|
syncedTimes: boolean;
|
||||||
|
|
||||||
// This being optional wouldn't be needed with noUncheckedIndexedAccess set to true, but it cause more than 5k errors currently.
|
panes: Record<string, ExploreItemState | undefined>;
|
||||||
// In order to be safe, we declare each item as pssobly undefined to force existence checks.
|
|
||||||
// This will have the side effect of also forcing undefined checks when iterating over this object entries, but
|
|
||||||
// it's better to error on the safer side.
|
|
||||||
panes: {
|
|
||||||
[paneId in ExploreId]?: ExploreItemState;
|
|
||||||
};
|
|
||||||
|
|
||||||
correlations?: CorrelationData[];
|
correlations?: CorrelationData[];
|
||||||
|
|
||||||
|
@ -20,12 +20,17 @@ export interface Props {
|
|||||||
* Wrapps component in redux store provider, Router and GrafanaContext
|
* Wrapps component in redux store provider, Router and GrafanaContext
|
||||||
*/
|
*/
|
||||||
export function TestProvider(props: Props) {
|
export function TestProvider(props: Props) {
|
||||||
const { store = configureStore(props.storeState), grafanaContext = getGrafanaContextMock(), children } = props;
|
const { store = configureStore(props.storeState), children } = props;
|
||||||
|
|
||||||
|
const context = {
|
||||||
|
...getGrafanaContextMock(),
|
||||||
|
...props.grafanaContext,
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Provider store={store}>
|
<Provider store={store}>
|
||||||
<Router history={locationService.getHistory()}>
|
<Router history={locationService.getHistory()}>
|
||||||
<GrafanaContext.Provider value={grafanaContext}>{children}</GrafanaContext.Provider>
|
<GrafanaContext.Provider value={context}>{children}</GrafanaContext.Provider>
|
||||||
</Router>
|
</Router>
|
||||||
</Provider>
|
</Provider>
|
||||||
);
|
);
|
||||||
|
Loading…
Reference in New Issue
Block a user