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, "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.", "3"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "4"]
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "3"]
|
||||
],
|
||||
"public/app/core/utils/fetch.ts:5381": [
|
||||
[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, "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": [
|
||||
[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.", "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": [
|
||||
[0, 0, 0, "Do not use any type assertions.", "0"],
|
||||
[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": [
|
||||
[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": [
|
||||
[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": [
|
||||
[0, 0, 0, "Do not use any type assertions.", "0"],
|
||||
[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.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');
|
||||
canvases.should('have.length', 1);
|
||||
|
||||
// Both queries above should have been run and be shown in the query history
|
||||
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
|
||||
cy.get('button[title="Delete query"]').each((button) => {
|
||||
|
@ -1,15 +1,15 @@
|
||||
import { DataQuery } from '@grafana/schema';
|
||||
|
||||
import { PreferredVisualisationType } from './data';
|
||||
import { DataQuery } from './query';
|
||||
import { RawTimeRange, TimeRange } from './time';
|
||||
|
||||
type AnyQuery = DataQuery & Record<string, any>;
|
||||
|
||||
/** @internal */
|
||||
export interface ExploreUrlState<T extends DataQuery = AnyQuery> {
|
||||
datasource: string;
|
||||
datasource: string | null;
|
||||
queries: T[];
|
||||
range: RawTimeRange;
|
||||
context?: string;
|
||||
panelsState?: ExplorePanelsState;
|
||||
}
|
||||
|
||||
|
@ -2,15 +2,13 @@ import { dateTime, ExploreUrlState, LogsSortOrder } from '@grafana/data';
|
||||
import { serializeStateToUrlParam } from '@grafana/data/src/utils/url';
|
||||
import { RefreshPicker } from '@grafana/ui';
|
||||
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 {
|
||||
buildQueryTransaction,
|
||||
DEFAULT_RANGE,
|
||||
hasNonEmptyQuery,
|
||||
parseUrlState,
|
||||
refreshIntervalToSortOrder,
|
||||
updateHistory,
|
||||
getExploreUrl,
|
||||
@ -42,67 +40,6 @@ jest.mock('@grafana/runtime', () => ({
|
||||
}));
|
||||
|
||||
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', () => {
|
||||
it('returns url parameter value for a state object', () => {
|
||||
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', () => {
|
||||
@ -304,21 +186,21 @@ describe('when buildQueryTransaction', () => {
|
||||
const queries = [{ refId: 'A' }];
|
||||
const queryOptions = { maxDataPoints: 1000, minInterval: '15s' };
|
||||
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);
|
||||
});
|
||||
it('it should calculate interval taking minInterval into account', () => {
|
||||
const queries = [{ refId: 'A' }];
|
||||
const queryOptions = { maxDataPoints: 1000, minInterval: '15s' };
|
||||
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);
|
||||
});
|
||||
it('it should calculate interval taking maxDataPoints into account', () => {
|
||||
const queries = [{ refId: 'A' }];
|
||||
const queryOptions = { maxDataPoints: 10, minInterval: '15s' };
|
||||
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');
|
||||
});
|
||||
});
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { nanoid } from '@reduxjs/toolkit';
|
||||
import { omit } from 'lodash';
|
||||
import { Unsubscribable } from 'rxjs';
|
||||
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 { PanelModel } from 'app/features/dashboard/state';
|
||||
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 { getNextRefIdChar } from './query';
|
||||
|
||||
export const DEFAULT_RANGE = {
|
||||
from: 'now-1h',
|
||||
to: 'now',
|
||||
};
|
||||
|
||||
export const DEFAULT_UI_STATE = {
|
||||
dedupStrategy: LogsDedupStrategy.none,
|
||||
};
|
||||
@ -58,6 +54,10 @@ export interface GetExploreUrlArguments {
|
||||
timeSrv: TimeSrv;
|
||||
}
|
||||
|
||||
export function generateExploreId() {
|
||||
return nanoid(3);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 || {};
|
||||
state = {
|
||||
...state,
|
||||
datasource: exploreDatasource.name,
|
||||
context: 'explore',
|
||||
datasource: exploreDatasource.uid,
|
||||
queries: exploreDatasource.interpolateVariablesInQueries(exploreTargets, scopedVars),
|
||||
};
|
||||
} else {
|
||||
state = {
|
||||
...state,
|
||||
datasource: exploreDatasource.name,
|
||||
context: 'explore',
|
||||
datasource: exploreDatasource.uid,
|
||||
queries: exploreTargets,
|
||||
};
|
||||
}
|
||||
|
||||
const exploreState = JSON.stringify(state);
|
||||
url = urlUtil.renderUrl('/explore', { left: exploreState });
|
||||
const exploreState = JSON.stringify({ [generateExploreId()]: state });
|
||||
url = urlUtil.renderUrl('/explore', { panes: exploreState, schemaVersion: 1 });
|
||||
}
|
||||
|
||||
return url;
|
||||
}
|
||||
|
||||
export function buildQueryTransaction(
|
||||
exploreId: ExploreId,
|
||||
exploreId: string,
|
||||
queries: DataQuery[],
|
||||
queryOptions: QueryOptions,
|
||||
range: TimeRange,
|
||||
@ -172,16 +170,6 @@ export function buildQueryTransaction(
|
||||
|
||||
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 => {
|
||||
if (!text) {
|
||||
return;
|
||||
@ -208,40 +196,6 @@ export const safeStringifyValue = (value: unknown, space?: number) => {
|
||||
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 {
|
||||
return `Q-${uuidv4()}-${index}`;
|
||||
}
|
||||
|
@ -134,7 +134,6 @@ export const createUrlFromRichHistory = (query: RichHistoryQuery) => {
|
||||
range: { from: 'now-1h', to: 'now' },
|
||||
datasource: query.datasourceName,
|
||||
queries: query.queries,
|
||||
context: 'explore',
|
||||
};
|
||||
|
||||
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 { contextSrv } from 'app/core/services/context_srv';
|
||||
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';
|
||||
|
||||
@ -57,7 +57,7 @@ interface SubmissionError {
|
||||
|
||||
interface Props {
|
||||
onClose: () => void;
|
||||
exploreId: ExploreId;
|
||||
exploreId: string;
|
||||
}
|
||||
|
||||
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 { DashboardSearchItemType } from 'app/features/search/types';
|
||||
import { configureStore } from 'app/store/configureStore';
|
||||
import { ExploreId, ExploreState } from 'app/types';
|
||||
import { ExploreState } from 'app/types';
|
||||
|
||||
import { createEmptyQueryResponse } from '../state/utils';
|
||||
|
||||
@ -52,7 +52,7 @@ describe('AddToDashboardButton', () => {
|
||||
});
|
||||
|
||||
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 });
|
||||
expect(button).toBeDisabled();
|
||||
@ -81,7 +81,7 @@ describe('AddToDashboardButton', () => {
|
||||
});
|
||||
|
||||
it('Opens and closes the modal correctly', async () => {
|
||||
setup(<AddToDashboard exploreId={ExploreId.left} />);
|
||||
setup(<AddToDashboard exploreId={'left'} />);
|
||||
|
||||
await openModal();
|
||||
|
||||
@ -96,7 +96,7 @@ describe('AddToDashboardButton', () => {
|
||||
const openSpy = jest.spyOn(global, 'open').mockReturnValue(true);
|
||||
const pushSpy = jest.spyOn(locationService, 'push');
|
||||
|
||||
setup(<AddToDashboard exploreId={ExploreId.left} />);
|
||||
setup(<AddToDashboard exploreId={'left'} />);
|
||||
|
||||
await openModal();
|
||||
|
||||
@ -115,7 +115,7 @@ describe('AddToDashboardButton', () => {
|
||||
const openSpy = jest.spyOn(global, 'open').mockReturnValue(true);
|
||||
const pushSpy = jest.spyOn(locationService, 'push');
|
||||
|
||||
setup(<AddToDashboard exploreId={ExploreId.left} />);
|
||||
setup(<AddToDashboard exploreId={'left'} />);
|
||||
|
||||
await openModal();
|
||||
|
||||
@ -134,7 +134,7 @@ describe('AddToDashboardButton', () => {
|
||||
// @ts-expect-error global.open should return a Window, but is not implemented in js-dom.
|
||||
const openSpy = jest.spyOn(global, 'open').mockReturnValue(true);
|
||||
|
||||
setup(<AddToDashboard exploreId={ExploreId.left} />);
|
||||
setup(<AddToDashboard exploreId={'left'} />);
|
||||
|
||||
await openModal();
|
||||
|
||||
@ -148,7 +148,7 @@ describe('AddToDashboardButton', () => {
|
||||
it('Navigates to the new dashboard', async () => {
|
||||
const pushSpy = jest.spyOn(locationService, 'push');
|
||||
|
||||
setup(<AddToDashboard exploreId={ExploreId.left} />);
|
||||
setup(<AddToDashboard exploreId={'left'} />);
|
||||
|
||||
await openModal();
|
||||
|
||||
@ -165,7 +165,7 @@ describe('AddToDashboardButton', () => {
|
||||
|
||||
describe('Save to existing dashboard', () => {
|
||||
it('Renders the dashboard picker when switching to "Existing Dashboard"', async () => {
|
||||
setup(<AddToDashboard exploreId={ExploreId.left} />);
|
||||
setup(<AddToDashboard exploreId={'left'} />);
|
||||
|
||||
await openModal();
|
||||
|
||||
@ -178,7 +178,7 @@ describe('AddToDashboardButton', () => {
|
||||
it('Does not submit if no dashboard is selected', async () => {
|
||||
locationService.push = jest.fn();
|
||||
|
||||
setup(<AddToDashboard exploreId={ExploreId.left} />);
|
||||
setup(<AddToDashboard exploreId={'left'} />);
|
||||
|
||||
await openModal();
|
||||
|
||||
@ -211,7 +211,7 @@ describe('AddToDashboardButton', () => {
|
||||
},
|
||||
]);
|
||||
|
||||
setup(<AddToDashboard exploreId={ExploreId.left} />);
|
||||
setup(<AddToDashboard exploreId={'left'} />);
|
||||
|
||||
await openModal();
|
||||
|
||||
@ -252,7 +252,7 @@ describe('AddToDashboardButton', () => {
|
||||
},
|
||||
]);
|
||||
|
||||
setup(<AddToDashboard exploreId={ExploreId.left} />);
|
||||
setup(<AddToDashboard exploreId={'left'} />);
|
||||
|
||||
await openModal();
|
||||
|
||||
@ -290,7 +290,7 @@ describe('AddToDashboardButton', () => {
|
||||
return true;
|
||||
}
|
||||
});
|
||||
setup(<AddToDashboard exploreId={ExploreId.left} />);
|
||||
setup(<AddToDashboard exploreId={'left'} />);
|
||||
await openModal('Add panel to existing dashboard');
|
||||
expect(screen.queryByRole('radio')).not.toBeInTheDocument();
|
||||
});
|
||||
@ -303,7 +303,7 @@ describe('AddToDashboardButton', () => {
|
||||
return true;
|
||||
}
|
||||
});
|
||||
setup(<AddToDashboard exploreId={ExploreId.left} />);
|
||||
setup(<AddToDashboard exploreId={'left'} />);
|
||||
await openModal('Add panel to new dashboard');
|
||||
expect(screen.queryByRole('radio')).not.toBeInTheDocument();
|
||||
});
|
||||
@ -322,7 +322,7 @@ describe('AddToDashboardButton', () => {
|
||||
jest.spyOn(global, 'open').mockReturnValue(null);
|
||||
const removeDashboardSpy = jest.spyOn(initDashboard, 'removeDashboardToFetchFromLocalStorage');
|
||||
|
||||
setup(<AddToDashboard exploreId={ExploreId.left} />);
|
||||
setup(<AddToDashboard exploreId={'left'} />);
|
||||
|
||||
await openModal();
|
||||
expect(screen.queryByRole('alert')).not.toBeInTheDocument();
|
||||
@ -341,7 +341,7 @@ describe('AddToDashboardButton', () => {
|
||||
throw 'SOME ERROR';
|
||||
});
|
||||
|
||||
setup(<AddToDashboard exploreId={ExploreId.left} />);
|
||||
setup(<AddToDashboard exploreId={'left'} />);
|
||||
|
||||
await openModal();
|
||||
expect(screen.queryByRole('alert')).not.toBeInTheDocument();
|
||||
@ -367,7 +367,7 @@ describe('AddToDashboardButton', () => {
|
||||
},
|
||||
]);
|
||||
|
||||
setup(<AddToDashboard exploreId={ExploreId.left} />);
|
||||
setup(<AddToDashboard exploreId={'left'} />);
|
||||
|
||||
await openModal();
|
||||
expect(screen.queryByRole('alert')).not.toBeInTheDocument();
|
||||
@ -391,7 +391,7 @@ describe('AddToDashboardButton', () => {
|
||||
it('Shows an error if an unknown error happens', async () => {
|
||||
jest.spyOn(api, 'setDashboardInLocalStorage').mockRejectedValue('SOME ERROR');
|
||||
|
||||
setup(<AddToDashboard exploreId={ExploreId.left} />);
|
||||
setup(<AddToDashboard exploreId={'left'} />);
|
||||
|
||||
await openModal();
|
||||
expect(screen.queryByRole('alert')).not.toBeInTheDocument();
|
||||
|
@ -1,14 +1,14 @@
|
||||
import React, { useState } from 'react';
|
||||
|
||||
import { ToolbarButton } from '@grafana/ui';
|
||||
import { ExploreId, useSelector } from 'app/types';
|
||||
import { useSelector } from 'app/types';
|
||||
|
||||
import { getExploreItemSelector } from '../state/selectors';
|
||||
|
||||
import { AddToDashboardModal } from './AddToDashboardModal';
|
||||
|
||||
interface Props {
|
||||
exploreId: ExploreId;
|
||||
exploreId: string;
|
||||
}
|
||||
|
||||
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 { selectors } from '@grafana/e2e-selectors';
|
||||
import { configureStore } from 'app/store/configureStore';
|
||||
import { ExploreId } from 'app/types';
|
||||
|
||||
import { Explore, Props } from './Explore';
|
||||
import { initialExploreState } from './state/main';
|
||||
@ -61,7 +60,7 @@ const dummyProps: Props = {
|
||||
QueryEditorHelp: {},
|
||||
},
|
||||
} as DataSourceApi,
|
||||
exploreId: ExploreId.left,
|
||||
exploreId: 'left',
|
||||
loading: false,
|
||||
modifyQueries: 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 { StoreState } from 'app/types';
|
||||
import { AbsoluteTimeEvent } from 'app/types/events';
|
||||
import { ExploreId } from 'app/types/explore';
|
||||
|
||||
import { getTimeZone } from '../profile/state/selectors';
|
||||
|
||||
@ -98,7 +97,7 @@ const getStyles = (theme: GrafanaTheme2) => {
|
||||
};
|
||||
|
||||
export interface ExploreProps extends Themeable2 {
|
||||
exploreId: ExploreId;
|
||||
exploreId: string;
|
||||
theme: GrafanaTheme2;
|
||||
eventBus: EventBus;
|
||||
}
|
||||
|
@ -1,24 +1,22 @@
|
||||
import { useRegisterActions, useKBar, Action, Priority } from 'kbar';
|
||||
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 { runQueries } from './state/query';
|
||||
import { isSplit } from './state/selectors';
|
||||
import { isSplit, selectPanes } from './state/selectors';
|
||||
|
||||
interface Props {
|
||||
exploreIdLeft: ExploreId;
|
||||
exploreIdRight?: ExploreId;
|
||||
}
|
||||
|
||||
export const ExploreActions = ({ exploreIdLeft, exploreIdRight }: Props) => {
|
||||
// FIXME: this should use the new IDs
|
||||
export const ExploreActions = () => {
|
||||
const [actions, setActions] = useState<Action[]>([]);
|
||||
const { query } = useKBar();
|
||||
const dispatch = useDispatch();
|
||||
const panes = useSelector(selectPanes);
|
||||
const splitted = useSelector(isSplit);
|
||||
|
||||
useEffect(() => {
|
||||
const keys = Object.keys(panes);
|
||||
const exploreSection = {
|
||||
name: 'Explore',
|
||||
priority: Priority.HIGH + 1,
|
||||
@ -32,18 +30,18 @@ export const ExploreActions = ({ exploreIdLeft, exploreIdRight }: Props) => {
|
||||
name: 'Run query (left)',
|
||||
keywords: 'query left',
|
||||
perform: () => {
|
||||
dispatch(runQueries({ exploreId: exploreIdLeft }));
|
||||
dispatch(runQueries({ exploreId: keys[0] }));
|
||||
},
|
||||
section: exploreSection,
|
||||
});
|
||||
if (exploreIdRight) {
|
||||
if ([panes[1]]) {
|
||||
// we should always have the right exploreId if split
|
||||
actionsArr.push({
|
||||
id: 'explore/run-query-right',
|
||||
name: 'Run query (right)',
|
||||
keywords: 'query right',
|
||||
perform: () => {
|
||||
dispatch(runQueries({ exploreId: exploreIdRight }));
|
||||
dispatch(runQueries({ exploreId: keys[1] }));
|
||||
},
|
||||
section: exploreSection,
|
||||
});
|
||||
@ -52,7 +50,7 @@ export const ExploreActions = ({ exploreIdLeft, exploreIdRight }: Props) => {
|
||||
name: 'Close split view left',
|
||||
keywords: 'split',
|
||||
perform: () => {
|
||||
dispatch(splitClose(exploreIdLeft));
|
||||
dispatch(splitClose(keys[0]));
|
||||
},
|
||||
section: exploreSection,
|
||||
});
|
||||
@ -61,7 +59,7 @@ export const ExploreActions = ({ exploreIdLeft, exploreIdRight }: Props) => {
|
||||
name: 'Close split view right',
|
||||
keywords: 'split',
|
||||
perform: () => {
|
||||
dispatch(splitClose(exploreIdRight));
|
||||
dispatch(splitClose(keys[1]));
|
||||
},
|
||||
section: exploreSection,
|
||||
});
|
||||
@ -72,7 +70,7 @@ export const ExploreActions = ({ exploreIdLeft, exploreIdRight }: Props) => {
|
||||
name: 'Run query',
|
||||
keywords: 'query',
|
||||
perform: () => {
|
||||
dispatch(runQueries({ exploreId: exploreIdLeft }));
|
||||
dispatch(runQueries({ exploreId: keys[0] }));
|
||||
},
|
||||
section: exploreSection,
|
||||
});
|
||||
@ -87,7 +85,7 @@ export const ExploreActions = ({ exploreIdLeft, exploreIdRight }: Props) => {
|
||||
});
|
||||
}
|
||||
setActions(actionsArr);
|
||||
}, [exploreIdLeft, exploreIdRight, splitted, query, dispatch]);
|
||||
}, [panes, splitted, query, dispatch]);
|
||||
|
||||
useRegisterActions(!query ? [] : actions, [actions, query]);
|
||||
|
||||
|
@ -4,8 +4,6 @@ import React, { ComponentProps } from 'react';
|
||||
import AutoSizer from 'react-virtualized-auto-sizer';
|
||||
|
||||
import { serializeStateToUrlParam } from '@grafana/data';
|
||||
import { config } from '@grafana/runtime';
|
||||
import { ExploreId } from 'app/types';
|
||||
|
||||
import { makeLogsQueryResponse } from './spec/helper/query';
|
||||
import { setupExplore, tearDown, waitForExplore } from './spec/helper/setup';
|
||||
@ -101,7 +99,7 @@ describe('ExplorePage', () => {
|
||||
orgId: '1',
|
||||
};
|
||||
|
||||
const { datasources, location } = setupExplore({ urlParams });
|
||||
const { datasources } = setupExplore({ urlParams });
|
||||
jest.mocked(datasources.loki.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(`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
|
||||
expect(datasources.loki.query).toBeCalledTimes(1);
|
||||
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 () => {
|
||||
const urlParams = {
|
||||
left: JSON.stringify(['now-1h', 'now', 'loki', { expr: '{ label="value"}' }]),
|
||||
@ -207,7 +177,7 @@ describe('ExplorePage', () => {
|
||||
|
||||
const splitButton = await screen.findByText(/split/i);
|
||||
await userEvent.click(splitButton);
|
||||
await waitForExplore(ExploreId.left);
|
||||
await waitForExplore('left');
|
||||
|
||||
expect(await screen.findAllByLabelText('Widen pane')).toHaveLength(2);
|
||||
expect(screen.queryByLabelText('Narrow pane')).not.toBeInTheDocument();
|
||||
@ -226,276 +196,4 @@ describe('ExplorePage', () => {
|
||||
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 { GrafanaRouteComponentProps } from 'app/core/navigation/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 { ExplorePaneContainer } from './ExplorePaneContainer';
|
||||
import { useExploreCorrelations } from './hooks/useExploreCorrelations';
|
||||
import { useExplorePageTitle } from './hooks/useExplorePageTitle';
|
||||
import { useStateSync } from './hooks/useStateSync';
|
||||
import { useStopQueries } from './hooks/useStopQueries';
|
||||
import { useTimeSrvFix } from './hooks/useTimeSrvFix';
|
||||
import { splitSizeUpdateAction } from './state/main';
|
||||
import { selectOrderedExplorePanes } from './state/selectors';
|
||||
import { isSplit, selectPanesEntries } from './state/selectors';
|
||||
|
||||
const styles = {
|
||||
pageScrollbarWrapper: css`
|
||||
@ -32,10 +31,14 @@ const styles = {
|
||||
};
|
||||
|
||||
export default function ExplorePage(props: GrafanaRouteComponentProps<{}, ExploreQueryParams>) {
|
||||
useStopQueries();
|
||||
useTimeSrvFix();
|
||||
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();
|
||||
const dispatch = useDispatch();
|
||||
const { keybindings, chrome } = useGrafana();
|
||||
@ -45,7 +48,8 @@ export default function ExplorePage(props: GrafanaRouteComponentProps<{}, Explor
|
||||
const minWidth = 200;
|
||||
const exploreState = useSelector((state) => state.explore);
|
||||
|
||||
const panes = useSelector(selectOrderedExplorePanes);
|
||||
const panes = useSelector(selectPanesEntries);
|
||||
const hasSplit = useSelector(isSplit);
|
||||
|
||||
useEffect(() => {
|
||||
//This is needed for breadcrumbs and topnav.
|
||||
@ -65,7 +69,7 @@ export default function ExplorePage(props: GrafanaRouteComponentProps<{}, Explor
|
||||
} else {
|
||||
dispatch(
|
||||
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);
|
||||
};
|
||||
|
||||
const hasSplit = Object.entries(panes).length > 1;
|
||||
let widthCalc = 0;
|
||||
if (hasSplit) {
|
||||
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) {
|
||||
widthCalc = Math.floor(windowWidth / 2);
|
||||
} else if (rightPaneWidthRatio !== undefined) {
|
||||
@ -87,7 +90,7 @@ export default function ExplorePage(props: GrafanaRouteComponentProps<{}, Explor
|
||||
|
||||
return (
|
||||
<div className={styles.pageScrollbarWrapper}>
|
||||
<ExploreActions exploreIdLeft={ExploreId.left} exploreIdRight={ExploreId.right} />
|
||||
<ExploreActions />
|
||||
|
||||
<SplitPaneWrapper
|
||||
splitOrientation="vertical"
|
||||
@ -97,16 +100,12 @@ export default function ExplorePage(props: GrafanaRouteComponentProps<{}, Explor
|
||||
primary="second"
|
||||
splitVisible={hasSplit}
|
||||
paneStyle={{ overflow: 'auto', display: 'flex', flexDirection: 'column' }}
|
||||
onDragFinished={(size) => {
|
||||
if (size) {
|
||||
updateSplitSize(size);
|
||||
}
|
||||
}}
|
||||
onDragFinished={(size) => size && updateSplitSize(size)}
|
||||
>
|
||||
{Object.keys(panes).map((exploreId) => {
|
||||
{panes.map(([exploreId]) => {
|
||||
return (
|
||||
<ErrorBoundaryAlert key={exploreId} style="page">
|
||||
<ExplorePaneContainer exploreId={exploreId as ExploreId} />
|
||||
<ExplorePaneContainer exploreId={exploreId} />
|
||||
</ErrorBoundaryAlert>
|
||||
);
|
||||
})}
|
||||
|
@ -1,14 +1,15 @@
|
||||
import { css } from '@emotion/css';
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
import React, { useEffect, useMemo, useRef } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { EventBusSrv, GrafanaTheme2 } from '@grafana/data';
|
||||
import { selectors } from '@grafana/e2e-selectors';
|
||||
import { useStyles2 } from '@grafana/ui';
|
||||
import { StoreState } from 'app/types';
|
||||
import { ExploreId } from 'app/types/explore';
|
||||
import { stopQueryState } from 'app/core/utils/explore';
|
||||
import { StoreState, useSelector } from 'app/types';
|
||||
|
||||
import Explore from './Explore';
|
||||
import { getExploreItemSelector } from './state/selectors';
|
||||
|
||||
const getStyles = (theme: GrafanaTheme2) => {
|
||||
return {
|
||||
@ -26,7 +27,7 @@ const getStyles = (theme: GrafanaTheme2) => {
|
||||
};
|
||||
|
||||
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
|
||||
*/
|
||||
function ExplorePaneContainerUnconnected({ exploreId }: Props) {
|
||||
useStopQueries(exploreId);
|
||||
const styles = useStyles2(getStyles);
|
||||
const eventBus = useRef(new EventBusSrv());
|
||||
const ref = useRef(null);
|
||||
@ -64,3 +66,15 @@ function mapStateToProps(state: StoreState, props: Props) {
|
||||
const connector = connect(mapStateToProps);
|
||||
|
||||
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 { LoadingState, InternalTimeZones, getDefaultTimeRange } from '@grafana/data';
|
||||
import { ExploreId } from 'app/types';
|
||||
|
||||
import { ExploreQueryInspector } from './ExploreQueryInspector';
|
||||
|
||||
@ -38,7 +37,7 @@ const setup = (propOverrides = {}) => {
|
||||
const props: ExploreQueryInspectorProps = {
|
||||
loading: false,
|
||||
width: 100,
|
||||
exploreId: ExploreId.left,
|
||||
exploreId: 'left',
|
||||
onClose: jest.fn(),
|
||||
timeZone: InternalTimeZones.utc,
|
||||
queryResponse: {
|
||||
|
@ -10,13 +10,13 @@ import { InspectErrorTab } from 'app/features/inspector/InspectErrorTab';
|
||||
import { InspectJSONTab } from 'app/features/inspector/InspectJSONTab';
|
||||
import { InspectStatsTab } from 'app/features/inspector/InspectStatsTab';
|
||||
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';
|
||||
|
||||
interface DispatchProps {
|
||||
width: number;
|
||||
exploreId: ExploreId;
|
||||
exploreId: string;
|
||||
timeZone: TimeZone;
|
||||
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 item: ExploreItemState = explore.panes[exploreId]!;
|
||||
const { queryResponse } = item;
|
||||
|
@ -4,12 +4,11 @@ import { TimeRange, TimeZone, RawTimeRange, dateTimeForTimeZone, dateMath } from
|
||||
import { reportInteraction } from '@grafana/runtime';
|
||||
import { TimePickerWithHistory } from 'app/core/components/TimePicker/TimePickerWithHistory';
|
||||
import { getShiftedTimeRange, getZoomedTimeRange } from 'app/core/utils/timePicker';
|
||||
import { ExploreId } from 'app/types';
|
||||
|
||||
import { TimeSyncButton } from './TimeSyncButton';
|
||||
|
||||
export interface Props {
|
||||
exploreId: ExploreId;
|
||||
exploreId: string;
|
||||
hideText?: boolean;
|
||||
range: TimeRange;
|
||||
timeZone: TimeZone;
|
||||
|
@ -11,7 +11,6 @@ import { contextSrv } from 'app/core/core';
|
||||
import { createAndCopyShortLink } from 'app/core/utils/shortLinks';
|
||||
import { DataSourcePicker } from 'app/features/datasources/components/picker/DataSourcePicker';
|
||||
import { AccessControlAction } from 'app/types';
|
||||
import { ExploreId } from 'app/types/explore';
|
||||
import { StoreState, useDispatch, useSelector } from 'app/types/store';
|
||||
|
||||
import { DashNavButton } from '../dashboard/components/DashNav/DashNavButton';
|
||||
@ -24,7 +23,7 @@ import { LiveTailButton } from './LiveTailButton';
|
||||
import { changeDatasource } from './state/datasource';
|
||||
import { splitClose, splitOpen, maximizePaneAction, evenPaneResizeAction } from './state/main';
|
||||
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 { LiveTailControls } from './useLiveTailControls';
|
||||
|
||||
@ -39,7 +38,7 @@ const rotateIcon = css({
|
||||
});
|
||||
|
||||
interface Props {
|
||||
exploreId: ExploreId;
|
||||
exploreId: string;
|
||||
onChangeTime: (range: RawTimeRange, changedByScanner?: boolean) => void;
|
||||
topOfViewRef: RefObject<HTMLDivElement>;
|
||||
}
|
||||
@ -64,9 +63,11 @@ export function ExploreToolbar({ exploreId, topOfViewRef, onChangeTime }: Props)
|
||||
(state) => state.explore.panes[exploreId]!.containerWidth < (splitted ? 700 : 800)
|
||||
);
|
||||
|
||||
const panes = useSelector(selectPanesEntries);
|
||||
|
||||
const shouldRotateSplitIcon = useMemo(
|
||||
() => (exploreId === 'left' && isLargerPane) || (exploreId === 'right' && !isLargerPane),
|
||||
[isLargerPane, exploreId]
|
||||
() => (exploreId === panes[0][0] && isLargerPane) || (exploreId === panes[1]?.[0] && !isLargerPane),
|
||||
[isLargerPane, exploreId, panes]
|
||||
);
|
||||
|
||||
const onCopyShortLink = () => {
|
||||
|
@ -11,7 +11,6 @@ import {
|
||||
MutableDataFrame,
|
||||
toUtc,
|
||||
} from '@grafana/data';
|
||||
import { ExploreId } from 'app/types';
|
||||
|
||||
import { Logs } from './Logs';
|
||||
|
||||
@ -46,7 +45,7 @@ jest.mock('app/store/store', () => ({
|
||||
const changePanelState = jest.fn();
|
||||
jest.mock('../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);
|
||||
},
|
||||
}));
|
||||
@ -85,7 +84,7 @@ describe('Logs', () => {
|
||||
|
||||
return (
|
||||
<Logs
|
||||
exploreId={ExploreId.left}
|
||||
exploreId={'left'}
|
||||
splitOpen={() => undefined}
|
||||
logsVolumeEnabled={true}
|
||||
onSetLogsVolumeEnabled={() => null}
|
||||
@ -141,7 +140,7 @@ describe('Logs', () => {
|
||||
const scanningStarted = jest.fn();
|
||||
render(
|
||||
<Logs
|
||||
exploreId={ExploreId.left}
|
||||
exploreId={'left'}
|
||||
splitOpen={() => undefined}
|
||||
logsVolumeEnabled={true}
|
||||
onSetLogsVolumeEnabled={() => null}
|
||||
@ -178,7 +177,7 @@ describe('Logs', () => {
|
||||
it('should render a stop scanning button', () => {
|
||||
render(
|
||||
<Logs
|
||||
exploreId={ExploreId.left}
|
||||
exploreId={'left'}
|
||||
splitOpen={() => undefined}
|
||||
logsVolumeEnabled={true}
|
||||
onSetLogsVolumeEnabled={() => null}
|
||||
@ -218,7 +217,7 @@ describe('Logs', () => {
|
||||
|
||||
render(
|
||||
<Logs
|
||||
exploreId={ExploreId.left}
|
||||
exploreId={'left'}
|
||||
splitOpen={() => undefined}
|
||||
logsVolumeEnabled={true}
|
||||
onSetLogsVolumeEnabled={() => null}
|
||||
@ -270,10 +269,10 @@ describe('Logs', () => {
|
||||
const panelState = { logs: { id: '1' } };
|
||||
const { rerender } = setup({ loading: false, panelState });
|
||||
|
||||
rerender(getComponent({ loading: true, exploreId: ExploreId.right, panelState }));
|
||||
rerender(getComponent({ loading: false, exploreId: ExploreId.right, panelState }));
|
||||
rerender(getComponent({ loading: true, 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', () => {
|
||||
|
@ -46,7 +46,6 @@ import { dedupLogRows, filterLogLevels } from 'app/core/logsModel';
|
||||
import store from 'app/core/store';
|
||||
import { createAndCopyShortLink } from 'app/core/utils/shortLinks';
|
||||
import { getState, dispatch } from 'app/store/store';
|
||||
import { ExploreId } from 'app/types/explore';
|
||||
|
||||
import { LogRows } from '../../logs/components/LogRows';
|
||||
import { LogRowContextModal } from '../../logs/components/log-context/LogRowContextModal';
|
||||
@ -73,7 +72,7 @@ interface Props extends Themeable2 {
|
||||
timeZone: TimeZone;
|
||||
scanning?: boolean;
|
||||
scanRange?: RawTimeRange;
|
||||
exploreId: ExploreId;
|
||||
exploreId: string;
|
||||
datasourceType?: string;
|
||||
logsVolumeEnabled: boolean;
|
||||
logsVolumeData: DataQueryResponse | undefined;
|
||||
|
@ -21,7 +21,7 @@ import {
|
||||
import { DataQuery } from '@grafana/schema';
|
||||
import { Collapse } from '@grafana/ui';
|
||||
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 {
|
||||
@ -41,7 +41,7 @@ import { LogsCrossFadeTransition } from './utils/LogsCrossFadeTransition';
|
||||
|
||||
interface LogsContainerProps extends PropsFromRedux {
|
||||
width: number;
|
||||
exploreId: ExploreId;
|
||||
exploreId: string;
|
||||
scanRange?: RawTimeRange;
|
||||
syncedTimes: boolean;
|
||||
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 item: ExploreItemState = explore.panes[exploreId]!;
|
||||
const {
|
||||
|
@ -2,7 +2,6 @@ import { render, screen } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
|
||||
import { getDefaultTimeRange, MutableDataFrame } from '@grafana/data';
|
||||
import { ExploreId } from 'app/types';
|
||||
|
||||
import { UnconnectedNodeGraphContainer } from './NodeGraphContainer';
|
||||
|
||||
@ -11,7 +10,7 @@ describe('NodeGraphContainer', () => {
|
||||
const { container } = render(
|
||||
<UnconnectedNodeGraphContainer
|
||||
dataFrames={[emptyFrame]}
|
||||
exploreId={ExploreId.left}
|
||||
exploreId={'left'}
|
||||
range={getDefaultTimeRange()}
|
||||
splitOpenFn={() => {}}
|
||||
withTraceView={true}
|
||||
@ -27,7 +26,7 @@ describe('NodeGraphContainer', () => {
|
||||
const { container } = render(
|
||||
<UnconnectedNodeGraphContainer
|
||||
dataFrames={[nodes]}
|
||||
exploreId={ExploreId.left}
|
||||
exploreId={'left'}
|
||||
range={getDefaultTimeRange()}
|
||||
splitOpenFn={() => {}}
|
||||
datasourceType={''}
|
||||
|
@ -9,7 +9,7 @@ import { Collapse, useStyles2, useTheme2 } from '@grafana/ui';
|
||||
|
||||
import { NodeGraph } from '../../../plugins/panel/nodeGraph';
|
||||
import { useCategorizeFrames } from '../../../plugins/panel/nodeGraph/useCategorizeFrames';
|
||||
import { ExploreId, StoreState } from '../../../types';
|
||||
import { StoreState } from '../../../types';
|
||||
import { useLinks } from '../utils/links';
|
||||
|
||||
const getStyles = (theme: GrafanaTheme2) => ({
|
||||
@ -23,7 +23,7 @@ const getStyles = (theme: GrafanaTheme2) => ({
|
||||
interface OwnProps {
|
||||
// Edges and Nodes are separate frames
|
||||
dataFrames: DataFrame[];
|
||||
exploreId: ExploreId;
|
||||
exploreId: string;
|
||||
// When showing the node graph together with trace view we do some changes so it works better.
|
||||
withTraceView?: boolean;
|
||||
datasourceType: string;
|
||||
|
@ -5,7 +5,7 @@ import { Provider } from 'react-redux';
|
||||
import { DataSourceSrv, setDataSourceSrv } from '@grafana/runtime';
|
||||
import { DataQuery } from '@grafana/schema';
|
||||
import { configureStore } from 'app/store/configureStore';
|
||||
import { ExploreId, ExploreState } from 'app/types';
|
||||
import { ExploreState } from 'app/types';
|
||||
|
||||
import { UserState } from '../profile/state/reducers';
|
||||
|
||||
@ -77,7 +77,7 @@ describe('Explore QueryRows', () => {
|
||||
|
||||
render(
|
||||
<Provider store={store}>
|
||||
<QueryRows exploreId={ExploreId.left} />
|
||||
<QueryRows exploreId={'left'} />
|
||||
</Provider>
|
||||
);
|
||||
|
||||
|
@ -6,7 +6,6 @@ import { reportInteraction } from '@grafana/runtime';
|
||||
import { DataQuery } from '@grafana/schema';
|
||||
import { getNextRefIdChar } from 'app/core/utils/query';
|
||||
import { useDispatch, useSelector } from 'app/types';
|
||||
import { ExploreId } from 'app/types/explore';
|
||||
|
||||
import { getDatasourceSrv } from '../plugins/datasource_srv';
|
||||
import { QueryEditorRows } from '../query/components/QueryEditorRows';
|
||||
@ -15,10 +14,10 @@ import { changeQueries, runQueries } from './state/query';
|
||||
import { getExploreItemSelector } from './state/selectors';
|
||||
|
||||
interface Props {
|
||||
exploreId: ExploreId;
|
||||
exploreId: string;
|
||||
}
|
||||
|
||||
const makeSelectors = (exploreId: ExploreId) => {
|
||||
const makeSelectors = (exploreId: string) => {
|
||||
const exploreItemSelector = getExploreItemSelector(exploreId);
|
||||
return {
|
||||
getQueries: createSelector(exploreItemSelector, (s) => s!.queries),
|
||||
|
@ -2,7 +2,7 @@ import { fireEvent, render, screen, within } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
|
||||
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';
|
||||
|
||||
@ -52,7 +52,7 @@ const dataFrame = toDataFrame({
|
||||
});
|
||||
|
||||
const defaultProps = {
|
||||
exploreId: ExploreId.left,
|
||||
exploreId: 'left',
|
||||
loading: false,
|
||||
width: 800,
|
||||
onCellFilterAdded: jest.fn(),
|
||||
|
@ -8,7 +8,7 @@ import { Collapse, RadioButtonGroup, Table, AdHocFilterItem } from '@grafana/ui'
|
||||
import { config } from 'app/core/config';
|
||||
import { PANEL_BORDER } from 'app/core/constants';
|
||||
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 RawListContainer from '../PrometheusListView/RawListContainer';
|
||||
@ -17,7 +17,7 @@ import { getFieldLinksForExplore } from '../utils/links';
|
||||
|
||||
interface RawPrometheusContainerProps {
|
||||
ariaLabel?: string;
|
||||
exploreId: ExploreId;
|
||||
exploreId: string;
|
||||
width: number;
|
||||
timeZone: TimeZone;
|
||||
onCellFilterAdded?: (filter: AdHocFilterItem) => void;
|
||||
|
@ -4,7 +4,6 @@ import { TestProvider } from 'test/helpers/TestProvider';
|
||||
|
||||
import { DataQueryError, LoadingState } from '@grafana/data';
|
||||
import { selectors } from '@grafana/e2e-selectors';
|
||||
import { ExploreId } from 'app/types';
|
||||
|
||||
import { configureStore } from '../../store/configureStore';
|
||||
|
||||
@ -60,7 +59,7 @@ function setup(error: DataQueryError) {
|
||||
|
||||
render(
|
||||
<TestProvider store={store}>
|
||||
<ResponseErrorContainer exploreId={ExploreId.left} />
|
||||
<ResponseErrorContainer exploreId="left" />
|
||||
</TestProvider>
|
||||
);
|
||||
}
|
||||
|
@ -3,12 +3,10 @@ import React from 'react';
|
||||
import { LoadingState } from '@grafana/data';
|
||||
import { useSelector } from 'app/types';
|
||||
|
||||
import { ExploreId } from '../../types';
|
||||
|
||||
import { ErrorContainer } from './ErrorContainer';
|
||||
|
||||
interface Props {
|
||||
exploreId: ExploreId;
|
||||
exploreId: string;
|
||||
}
|
||||
export function ResponseErrorContainer(props: Props) {
|
||||
const queryResponse = useSelector((state) => state.explore.panes[props.exploreId]!.queryResponse);
|
||||
|
@ -3,7 +3,6 @@ import React from 'react';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { SortOrder } from 'app/core/utils/richHistory';
|
||||
import { ExploreId } from 'app/types';
|
||||
|
||||
import { RichHistory, RichHistoryProps, Tabs } from './RichHistory';
|
||||
|
||||
@ -23,7 +22,7 @@ jest.mock('@grafana/runtime', () => ({
|
||||
const setup = (propOverrides?: Partial<RichHistoryProps>) => {
|
||||
const props: RichHistoryProps = {
|
||||
theme: {} as GrafanaTheme2,
|
||||
exploreId: ExploreId.left,
|
||||
exploreId: 'left',
|
||||
height: 100,
|
||||
activeDatasourceInstance: 'Test datasource',
|
||||
richHistory: [],
|
||||
|
@ -4,7 +4,7 @@ import React, { PureComponent } from 'react';
|
||||
import { SelectableValue } from '@grafana/data';
|
||||
import { Themeable2, TabbedContainer, TabConfig, withTheme2 } from '@grafana/ui';
|
||||
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';
|
||||
|
||||
@ -32,14 +32,14 @@ export interface RichHistoryProps extends Themeable2 {
|
||||
richHistorySettings: RichHistorySettings;
|
||||
richHistorySearchFilters?: RichHistorySearchFilters;
|
||||
updateHistorySettings: (settings: RichHistorySettings) => void;
|
||||
updateHistorySearchFilters: (exploreId: ExploreId, filters: RichHistorySearchFilters) => void;
|
||||
loadRichHistory: (exploreId: ExploreId) => void;
|
||||
loadMoreRichHistory: (exploreId: ExploreId) => void;
|
||||
clearRichHistoryResults: (exploreId: ExploreId) => void;
|
||||
updateHistorySearchFilters: (exploreId: string, filters: RichHistorySearchFilters) => void;
|
||||
loadRichHistory: (exploreId: string) => void;
|
||||
loadMoreRichHistory: (exploreId: string) => void;
|
||||
clearRichHistoryResults: (exploreId: string) => void;
|
||||
deleteRichHistory: () => void;
|
||||
activeDatasourceInstance: string;
|
||||
firstTab: Tabs;
|
||||
exploreId: ExploreId;
|
||||
exploreId: string;
|
||||
height: number;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
@ -6,7 +6,7 @@ import { DataSourceApi, DataSourceInstanceSettings, DataSourcePluginMeta } from
|
||||
import { DataQuery, DataSourceRef } from '@grafana/schema';
|
||||
import appEvents from 'app/core/app_events';
|
||||
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 { RichHistoryCard, Props } from './RichHistoryCard';
|
||||
@ -120,7 +120,7 @@ const setup = (propOverrides?: Partial<Props<MockQuery>>) => {
|
||||
deleteHistoryItem: deleteRichHistoryMock,
|
||||
commentHistoryItem: jest.fn(),
|
||||
setQueries: jest.fn(),
|
||||
exploreId: ExploreId.left,
|
||||
exploreId: 'left',
|
||||
datasourceInstance: dsStore.loki,
|
||||
};
|
||||
|
||||
|
@ -19,9 +19,9 @@ import { setQueries } from 'app/features/explore/state/query';
|
||||
import { dispatch } from 'app/store/store';
|
||||
import { StoreState } from 'app/types';
|
||||
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 { datasourceInstance } = explore.panes[exploreId]!;
|
||||
return {
|
||||
|
@ -2,7 +2,6 @@ import { render } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
|
||||
import { SortOrder } from 'app/core/utils/richHistory';
|
||||
import { ExploreId } from 'app/types';
|
||||
|
||||
import { Tabs } from './RichHistory';
|
||||
import { RichHistoryContainer, Props } from './RichHistoryContainer';
|
||||
@ -22,7 +21,7 @@ jest.mock('@grafana/runtime', () => ({
|
||||
const setup = (propOverrides?: Partial<Props>) => {
|
||||
const props: Props = {
|
||||
width: 500,
|
||||
exploreId: ExploreId.left,
|
||||
exploreId: 'left',
|
||||
activeDatasourceInstance: 'Test datasource',
|
||||
richHistory: [],
|
||||
firstTab: Tabs.RichHistory,
|
||||
|
@ -6,7 +6,6 @@ import { config, reportInteraction } from '@grafana/runtime';
|
||||
import { useTheme2 } from '@grafana/ui';
|
||||
// Types
|
||||
import { ExploreItemState, StoreState } from 'app/types';
|
||||
import { ExploreId } from 'app/types/explore';
|
||||
|
||||
// Components, enums
|
||||
import { ExploreDrawer } from '../ExploreDrawer';
|
||||
@ -24,7 +23,7 @@ import { RichHistory, Tabs } from './RichHistory';
|
||||
|
||||
//Actions
|
||||
|
||||
function mapStateToProps(state: StoreState, { exploreId }: { exploreId: ExploreId }) {
|
||||
function mapStateToProps(state: StoreState, { exploreId }: { exploreId: string }) {
|
||||
const explore = state.explore;
|
||||
const item: ExploreItemState = explore.panes[exploreId]!;
|
||||
const richHistorySearchFilters = item.richHistorySearchFilters;
|
||||
@ -56,7 +55,7 @@ const connector = connect(mapStateToProps, mapDispatchToProps);
|
||||
|
||||
interface OwnProps {
|
||||
width: number;
|
||||
exploreId: ExploreId;
|
||||
exploreId: string;
|
||||
onClose: () => void;
|
||||
}
|
||||
export type Props = ConnectedProps<typeof connector> & OwnProps;
|
||||
|
@ -3,7 +3,6 @@ import React from 'react';
|
||||
|
||||
import { DataSourceSrv, setDataSourceSrv } from '@grafana/runtime';
|
||||
import { SortOrder } from 'app/core/utils/richHistoryTypes';
|
||||
import { ExploreId } from 'app/types';
|
||||
|
||||
import { RichHistoryQueriesTab, RichHistoryQueriesTabProps } from './RichHistoryQueriesTab';
|
||||
|
||||
@ -30,7 +29,7 @@ const setup = (propOverrides?: Partial<RichHistoryQueriesTabProps>) => {
|
||||
lastUsedDatasourceFilters: [],
|
||||
starredTabAsFirstTab: false,
|
||||
},
|
||||
exploreId: ExploreId.left,
|
||||
exploreId: 'left',
|
||||
height: 100,
|
||||
};
|
||||
|
||||
|
@ -12,7 +12,7 @@ import {
|
||||
RichHistorySearchFilters,
|
||||
RichHistorySettings,
|
||||
} from 'app/core/utils/richHistory';
|
||||
import { ExploreId, RichHistoryQuery } from 'app/types/explore';
|
||||
import { RichHistoryQuery } from 'app/types/explore';
|
||||
|
||||
import { getSortOrderOptions } from './RichHistory';
|
||||
import RichHistoryCard from './RichHistoryCard';
|
||||
@ -27,7 +27,7 @@ export interface RichHistoryQueriesTabProps {
|
||||
loadMoreRichHistory: () => void;
|
||||
richHistorySettings: RichHistorySettings;
|
||||
richHistorySearchFilters?: RichHistorySearchFilters;
|
||||
exploreId: ExploreId;
|
||||
exploreId: string;
|
||||
height: number;
|
||||
}
|
||||
|
||||
|
@ -2,7 +2,6 @@ import { fireEvent, render, screen } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
|
||||
import { SortOrder } from 'app/core/utils/richHistory';
|
||||
import { ExploreId } from 'app/types';
|
||||
|
||||
import { RichHistoryStarredTab, RichHistoryStarredTabProps } from './RichHistoryStarredTab';
|
||||
|
||||
@ -26,7 +25,7 @@ const setup = (propOverrides?: Partial<RichHistoryStarredTabProps>) => {
|
||||
updateFilters: jest.fn(),
|
||||
loadMoreRichHistory: jest.fn(),
|
||||
clearRichHistoryResults: jest.fn(),
|
||||
exploreId: ExploreId.left,
|
||||
exploreId: 'left',
|
||||
richHistorySettings: {
|
||||
retentionPeriod: 7,
|
||||
starredTabAsFirstTab: false,
|
||||
|
@ -10,7 +10,7 @@ import {
|
||||
RichHistorySearchFilters,
|
||||
RichHistorySettings,
|
||||
} from 'app/core/utils/richHistory';
|
||||
import { RichHistoryQuery, ExploreId } from 'app/types/explore';
|
||||
import { RichHistoryQuery } from 'app/types/explore';
|
||||
|
||||
import { getSortOrderOptions } from './RichHistory';
|
||||
import RichHistoryCard from './RichHistoryCard';
|
||||
@ -25,7 +25,7 @@ export interface RichHistoryStarredTabProps {
|
||||
loadMoreRichHistory: () => void;
|
||||
richHistorySearchFilters?: RichHistorySearchFilters;
|
||||
richHistorySettings: RichHistorySettings;
|
||||
exploreId: ExploreId;
|
||||
exploreId: string;
|
||||
}
|
||||
|
||||
const getStyles = (theme: GrafanaTheme2) => {
|
||||
|
@ -2,7 +2,6 @@ import { render, screen, within } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
|
||||
import { DataFrame, FieldType, getDefaultTimeRange, InternalTimeZones, toDataFrame } from '@grafana/data';
|
||||
import { ExploreId } from 'app/types';
|
||||
|
||||
import { TableContainer } from './TableContainer';
|
||||
|
||||
@ -48,7 +47,7 @@ const dataFrame = toDataFrame({
|
||||
});
|
||||
|
||||
const defaultProps = {
|
||||
exploreId: ExploreId.left,
|
||||
exploreId: 'left',
|
||||
loading: false,
|
||||
width: 800,
|
||||
onCellFilterAdded: jest.fn(),
|
||||
|
@ -5,7 +5,7 @@ import { ValueLinkConfig, applyFieldOverrides, TimeZone, SplitOpen, DataFrame, L
|
||||
import { Table, AdHocFilterItem, PanelChrome } from '@grafana/ui';
|
||||
import { config } from 'app/core/config';
|
||||
import { StoreState } from 'app/types';
|
||||
import { ExploreId, ExploreItemState } from 'app/types/explore';
|
||||
import { ExploreItemState } from 'app/types/explore';
|
||||
|
||||
import { MetaInfoText } from '../MetaInfoText';
|
||||
import { selectIsWaitingForData } from '../state/query';
|
||||
@ -13,7 +13,7 @@ import { getFieldLinksForExplore } from '../utils/links';
|
||||
|
||||
interface TableContainerProps {
|
||||
ariaLabel?: string;
|
||||
exploreId: ExploreId;
|
||||
exploreId: string;
|
||||
width: number;
|
||||
timeZone: TimeZone;
|
||||
onCellFilterAdded?: (filter: AdHocFilterItem) => void;
|
||||
|
@ -5,7 +5,6 @@ import { Provider } from 'react-redux';
|
||||
|
||||
import { DataFrame, MutableDataFrame, getDefaultTimeRange, LoadingState } from '@grafana/data';
|
||||
import { DataSourceSrv, setDataSourceSrv } from '@grafana/runtime';
|
||||
import { ExploreId } from 'app/types';
|
||||
|
||||
import { configureStore } from '../../../store/configureStore';
|
||||
|
||||
@ -26,7 +25,7 @@ function getTraceView(frames: DataFrame[]) {
|
||||
return (
|
||||
<Provider store={store}>
|
||||
<TraceView
|
||||
exploreId={ExploreId.left}
|
||||
exploreId="left"
|
||||
dataFrames={frames}
|
||||
splitOpenFn={() => {}}
|
||||
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 { TempoQuery } from 'app/plugins/datasource/tempo/types';
|
||||
import { useDispatch, useSelector } from 'app/types';
|
||||
import { ExploreId } from 'app/types/explore';
|
||||
|
||||
import { changePanelState } from '../state/explorePane';
|
||||
|
||||
@ -63,7 +62,7 @@ function noop(): {} {
|
||||
type Props = {
|
||||
dataFrames: DataFrame[];
|
||||
splitOpenFn?: SplitOpen;
|
||||
exploreId?: ExploreId;
|
||||
exploreId?: string;
|
||||
scrollElement?: Element;
|
||||
scrollElementClass?: string;
|
||||
traceProp: Trace;
|
||||
@ -251,7 +250,7 @@ export function TraceView(props: Props) {
|
||||
* @param options
|
||||
*/
|
||||
function useFocusSpanLink(options: {
|
||||
exploreId: ExploreId;
|
||||
exploreId: string;
|
||||
splitOpenFn: SplitOpen;
|
||||
refId?: string;
|
||||
datasource?: DataSourceApi;
|
||||
|
@ -5,7 +5,6 @@ import { Provider } from 'react-redux';
|
||||
|
||||
import { getDefaultTimeRange, LoadingState } from '@grafana/data';
|
||||
import { config } from '@grafana/runtime';
|
||||
import { ExploreId } from 'app/types';
|
||||
|
||||
import { configureStore } from '../../../store/configureStore';
|
||||
|
||||
@ -31,7 +30,7 @@ function renderTraceViewContainer(frames = [frameOld]) {
|
||||
const { container, baseElement } = render(
|
||||
<Provider store={store}>
|
||||
<TraceViewContainer
|
||||
exploreId={ExploreId.left}
|
||||
exploreId="left"
|
||||
dataFrames={frames}
|
||||
splitOpenFn={() => {}}
|
||||
queryResponse={mockPanelData}
|
||||
|
@ -5,7 +5,6 @@ import { DataFrame, SplitOpen, PanelData, GrafanaTheme2 } from '@grafana/data';
|
||||
import { config } from '@grafana/runtime';
|
||||
import { useStyles2 } from '@grafana/ui';
|
||||
import { StoreState, useSelector } from 'app/types';
|
||||
import { ExploreId } from 'app/types/explore';
|
||||
|
||||
import { TraceView } from './TraceView';
|
||||
import TracePageSearchBar from './components/TracePageHeader/TracePageSearchBar';
|
||||
@ -15,7 +14,7 @@ import { transformDataFrames } from './utils/transform';
|
||||
interface Props {
|
||||
dataFrames: DataFrame[];
|
||||
splitOpenFn: SplitOpen;
|
||||
exploreId: ExploreId;
|
||||
exploreId: string;
|
||||
scrollElement?: Element;
|
||||
queryResponse: PanelData;
|
||||
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 { 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() {
|
||||
const navModel = useNavModel('explore');
|
||||
export function useExplorePageTitle(params: ExploreQueryParams) {
|
||||
const navModel = useRef<NavModel>();
|
||||
navModel.current = useNavModel('explore');
|
||||
const dsService = useRef(getDatasourceSrv());
|
||||
|
||||
const datasourceNames = useSelector((state) =>
|
||||
Object.values(selectOrderedExplorePanes(state)).map((pane) => pane?.datasourceInstance?.name)
|
||||
).filter(isTruthy);
|
||||
useEffect(() => {
|
||||
if (!params.panes || typeof params.panes !== 'string') {
|
||||
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 { useEffect, useRef } from 'react';
|
||||
|
||||
import {
|
||||
CoreApp,
|
||||
serializeStateToUrlParam,
|
||||
ExploreUrlState,
|
||||
isDateTime,
|
||||
TimeRange,
|
||||
RawTimeRange,
|
||||
DataSourceApi,
|
||||
} from '@grafana/data';
|
||||
import { CoreApp, ExploreUrlState, isDateTime, TimeRange, RawTimeRange, DataSourceApi } from '@grafana/data';
|
||||
import { DataQuery, DataSourceRef } from '@grafana/schema';
|
||||
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 { 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 { initializeExplore } from '../state/explorePane';
|
||||
import { clearPanes, splitClose, splitOpen, syncTimesAction } from '../state/main';
|
||||
import { runQueries, setQueriesAction } from '../state/query';
|
||||
import { selectPanes } from '../state/selectors';
|
||||
import { changeRangeAction, updateTime } from '../state/time';
|
||||
import { DEFAULT_RANGE } from '../state/utils';
|
||||
import { withUniqueRefIds } from '../utils/queries';
|
||||
import { changeDatasource } from '../../state/datasource';
|
||||
import { initializeExplore } from '../../state/explorePane';
|
||||
import { clearPanes, splitClose, splitOpen, syncTimesAction } from '../../state/main';
|
||||
import { runQueries, setQueriesAction } from '../../state/query';
|
||||
import { selectPanes } from '../../state/selectors';
|
||||
import { changeRangeAction, updateTime } from '../../state/time';
|
||||
import { DEFAULT_RANGE } from '../../state/utils';
|
||||
import { withUniqueRefIds } from '../../utils/queries';
|
||||
import { isFulfilled } from '../utils';
|
||||
|
||||
import { parseURL } from './parseURL';
|
||||
|
||||
/**
|
||||
* Bi-directionally syncs URL changes with Explore's state.
|
||||
@ -37,15 +32,15 @@ export function useStateSync(params: ExploreQueryParams) {
|
||||
},
|
||||
} = useGrafana();
|
||||
const dispatch = useDispatch();
|
||||
const statePanes = useSelector(selectPanes);
|
||||
const panesState = useSelector(selectPanes);
|
||||
const orgId = useSelector((state) => state.user.orgId);
|
||||
const prevParams = useRef<ExploreQueryParams>(params);
|
||||
const prevParams = useRef(params);
|
||||
const initState = useRef<'notstarted' | 'pending' | 'done'>('notstarted');
|
||||
|
||||
useEffect(() => {
|
||||
// This happens when the user navigates to an explore "empty page" while within Explore.
|
||||
// ie. by clicking on the explore when explore is active.
|
||||
if (!params.left && !params.right) {
|
||||
if (!params.panes) {
|
||||
initState.current = 'notstarted';
|
||||
prevParams.current = params;
|
||||
}
|
||||
@ -68,22 +63,27 @@ export function useStateSync(params: ExploreQueryParams) {
|
||||
cancelActiveListeners();
|
||||
await delay(200);
|
||||
|
||||
const panesQueryParams = Object.entries(getState().explore.panes).reduce<Record<string, string>>(
|
||||
(acc, [id, paneState]) => ({ ...acc, [id]: serializeStateToUrlParam(getUrlStateFromPaneState(paneState)) }),
|
||||
{}
|
||||
);
|
||||
const panesQueryParams = Object.entries(getState().explore.panes).reduce((acc, [id, 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,
|
||||
// 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(
|
||||
{ left: panesQueryParams.left, right: panesQueryParams.right, orgId: getState().user.orgId },
|
||||
replace
|
||||
);
|
||||
location.partial({ panes: prevParams.current.panes }, replace);
|
||||
}
|
||||
},
|
||||
})
|
||||
@ -94,33 +94,29 @@ export function useStateSync(params: ExploreQueryParams) {
|
||||
}, [dispatch, location]);
|
||||
|
||||
useEffect(() => {
|
||||
const isURLOutOfSync = prevParams.current?.left !== params.left || prevParams.current?.right !== params.right;
|
||||
const isURLOutOfSync = prevParams.current?.panes !== params.panes;
|
||||
|
||||
const urlPanes = {
|
||||
left: parseUrlState(params.left),
|
||||
...(params.right && { right: parseUrlState(params.right) }),
|
||||
};
|
||||
const urlState = parseURL(params);
|
||||
|
||||
async function sync() {
|
||||
// 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.
|
||||
// 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 }));
|
||||
}
|
||||
|
||||
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 statePane = statePanes[exploreId];
|
||||
const paneState = panesState[exploreId];
|
||||
|
||||
if (statePane !== undefined) {
|
||||
// TODO: the diff contains panelState updates, but we are currently not handling them.
|
||||
const update = urlDiff(urlPane, getUrlStateFromPaneState(statePane));
|
||||
if (paneState !== undefined) {
|
||||
const update = urlDiff(urlPane, getUrlStateFromPaneState(paneState));
|
||||
|
||||
Promise.resolve()
|
||||
.then(async () => {
|
||||
if (update.datasource) {
|
||||
if (update.datasource && datasource) {
|
||||
await dispatch(changeDatasource(exploreId, datasource));
|
||||
}
|
||||
return;
|
||||
@ -146,20 +142,21 @@ export function useStateSync(params: ExploreQueryParams) {
|
||||
dispatch(
|
||||
initializeExplore({
|
||||
exploreId,
|
||||
datasource,
|
||||
datasource: datasource || '',
|
||||
queries: withUniqueRefIds(queries),
|
||||
range,
|
||||
panelsState,
|
||||
position: i,
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 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.
|
||||
Object.keys(statePanes)
|
||||
.filter((keyInStore) => !Object.keys(urlPanes).includes(keyInStore))
|
||||
.forEach((paneId) => dispatch(splitClose(paneId as ExploreId)));
|
||||
Object.keys(panesState)
|
||||
.filter((keyInStore) => !Object.keys(urlState.panes).includes(keyInStore))
|
||||
.forEach((paneId) => dispatch(splitClose(paneId)));
|
||||
}
|
||||
|
||||
// This happens when the user first navigates to explore.
|
||||
@ -172,11 +169,11 @@ export function useStateSync(params: ExploreQueryParams) {
|
||||
dispatch(clearPanes());
|
||||
|
||||
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(
|
||||
async (paneDatasource) => {
|
||||
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
|
||||
? queries.length
|
||||
? // if we have queries in the URL, we use them
|
||||
@ -212,7 +209,7 @@ export function useStateSync(params: ExploreQueryParams) {
|
||||
.then((queries) => {
|
||||
return dispatch(
|
||||
initializeExplore({
|
||||
exploreId: exploreId as ExploreId,
|
||||
exploreId,
|
||||
datasource: paneDatasource,
|
||||
queries,
|
||||
range,
|
||||
@ -224,25 +221,39 @@ export function useStateSync(params: ExploreQueryParams) {
|
||||
);
|
||||
})
|
||||
).then((panes) => {
|
||||
const urlState = panes.reduce<ExploreQueryParams>((acc, { exploreId, state }) => {
|
||||
return { ...acc, [exploreId]: serializeStateToUrlParam(getUrlStateFromPaneState(state)) };
|
||||
}, {});
|
||||
|
||||
location.partial({ ...urlState, orgId }, true);
|
||||
const newParams = panes.reduce(
|
||||
(acc, { exploreId, state }) => {
|
||||
return {
|
||||
...acc,
|
||||
panes: {
|
||||
...acc.panes,
|
||||
[exploreId]: getUrlStateFromPaneState(state),
|
||||
},
|
||||
};
|
||||
},
|
||||
{
|
||||
panes: {},
|
||||
}
|
||||
);
|
||||
|
||||
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 = {
|
||||
left: params.left,
|
||||
};
|
||||
if (params.right) {
|
||||
prevParams.current.right = params.right;
|
||||
}
|
||||
prevParams.current = params;
|
||||
|
||||
isURLOutOfSync && initState.current === 'done' && sync();
|
||||
}, [params.left, params.right, dispatch, statePanes, exploreMixedDatasource, orgId, location]);
|
||||
}, [dispatch, panesState, exploreMixedDatasource, orgId, location, params]);
|
||||
}
|
||||
|
||||
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
|
||||
* side effects needed.
|
||||
@ -396,7 +404,7 @@ function pruneObject(obj: object): object | undefined {
|
||||
return pruned;
|
||||
}
|
||||
|
||||
export const toRawTimeRange = (range: TimeRange): RawTimeRange => {
|
||||
const toRawTimeRange = (range: TimeRange): RawTimeRange => {
|
||||
let from = range.raw.from;
|
||||
if (isDateTime(from)) {
|
||||
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() {
|
||||
const { location } = useGrafana();
|
||||
|
||||
useEffect(() => {
|
||||
const searchParams = location.getSearchObject();
|
||||
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 { serializeStateToUrlParam } from '@grafana/data';
|
||||
|
||||
import { changeDatasource } from './helper/interactions';
|
||||
import { makeLogsQueryResponse } from './helper/query';
|
||||
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));
|
||||
});
|
||||
|
||||
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 { datasources, location } = setupExplore({ urlParams });
|
||||
const { datasources } = setupExplore({ urlParams });
|
||||
jest.mocked(datasources.loki.query).mockReturnValueOnce(makeLogsQueryResponse());
|
||||
await waitForExplore();
|
||||
await changeDatasource('elastic');
|
||||
|
||||
await screen.findByText('elastic Editor input:');
|
||||
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 { ExploreId } from '../../../../types';
|
||||
|
||||
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);
|
||||
|
||||
expect(await selector.findByText('1 queries')).toBeInTheDocument();
|
||||
@ -12,7 +10,7 @@ export const assertQueryHistoryExists = async (query: string, exploreId: Explore
|
||||
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);
|
||||
await waitFor(() => {
|
||||
expect(selector.getByText(new RegExp(`${expectedQueryTexts.length} queries`))).toBeInTheDocument();
|
||||
@ -23,10 +21,7 @@ export const assertQueryHistory = async (expectedQueryTexts: string[], exploreId
|
||||
});
|
||||
};
|
||||
|
||||
export const assertQueryHistoryComment = async (
|
||||
expectedQueryComments: string[],
|
||||
exploreId: ExploreId = ExploreId.left
|
||||
) => {
|
||||
export const assertQueryHistoryComment = async (expectedQueryComments: string[], exploreId = 'left') => {
|
||||
const selector = withinExplore(exploreId);
|
||||
await waitFor(() => {
|
||||
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 starButtons = selector.getAllByRole('button', { name: /Star query|Unstar query/ });
|
||||
await waitFor(() =>
|
||||
@ -49,12 +44,12 @@ export const assertQueryHistoryIsStarred = async (expectedStars: boolean[], expl
|
||||
|
||||
export const assertQueryHistoryTabIsSelected = (
|
||||
tabName: 'Query history' | 'Starred' | 'Settings',
|
||||
exploreId: ExploreId = ExploreId.left
|
||||
exploreId = 'left'
|
||||
) => {
|
||||
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)');
|
||||
if (visible) {
|
||||
expect(filterInput).toBeInTheDocument();
|
||||
@ -63,14 +58,10 @@ export const assertDataSourceFilterVisibility = (visible: boolean, exploreId: Ex
|
||||
}
|
||||
};
|
||||
|
||||
export const assertQueryHistoryElementsShown = (
|
||||
shown: number,
|
||||
total: number,
|
||||
exploreId: ExploreId = ExploreId.left
|
||||
) => {
|
||||
export const assertQueryHistoryElementsShown = (shown: number, total: number, exploreId = 'left') => {
|
||||
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();
|
||||
};
|
||||
|
@ -3,8 +3,6 @@ import userEvent from '@testing-library/user-event';
|
||||
|
||||
import { selectors } from '@grafana/e2e-selectors';
|
||||
|
||||
import { ExploreId } from '../../../../types';
|
||||
|
||||
import { withinExplore } from './setup';
|
||||
|
||||
export const changeDatasource = async (name: string) => {
|
||||
@ -14,59 +12,52 @@ export const changeDatasource = async (name: string) => {
|
||||
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' });
|
||||
await userEvent.clear(input);
|
||||
await userEvent.type(input, query);
|
||||
};
|
||||
|
||||
export const runQuery = async (exploreId: ExploreId = ExploreId.left) => {
|
||||
export const runQuery = async (exploreId = 'left') => {
|
||||
const explore = withinExplore(exploreId);
|
||||
const toolbar = within(explore.getByLabelText('Explore toolbar'));
|
||||
const button = toolbar.getByRole('button', { name: /run query/i });
|
||||
await userEvent.click(button);
|
||||
};
|
||||
|
||||
export const openQueryHistory = async (exploreId: ExploreId = ExploreId.left) => {
|
||||
export const openQueryHistory = async (exploreId = 'left') => {
|
||||
const selector = withinExplore(exploreId);
|
||||
const button = selector.getByRole('button', { name: 'Rich history button' });
|
||||
await userEvent.click(button);
|
||||
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' });
|
||||
await userEvent.click(closeButton);
|
||||
};
|
||||
|
||||
export const switchToQueryHistoryTab = async (
|
||||
name: 'Settings' | 'Query History',
|
||||
exploreId: ExploreId = ExploreId.left
|
||||
) => {
|
||||
export const switchToQueryHistoryTab = async (name: 'Settings' | 'Query History', exploreId = 'left') => {
|
||||
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', {
|
||||
name: /Change the default active tab from “Query history” to “Starred”/,
|
||||
});
|
||||
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.*/);
|
||||
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);
|
||||
};
|
||||
|
||||
export const commentQueryHistory = async (
|
||||
queryIndex: number,
|
||||
comment: string,
|
||||
exploreId: ExploreId = ExploreId.left
|
||||
) => {
|
||||
export const commentQueryHistory = async (queryIndex: number, comment: string, exploreId = 'left') => {
|
||||
await invokeAction(queryIndex, 'Add comment', exploreId);
|
||||
const input = withinExplore(exploreId).getByPlaceholderText('An optional description of what the query does.');
|
||||
await userEvent.clear(input);
|
||||
@ -74,16 +65,16 @@ export const commentQueryHistory = async (
|
||||
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);
|
||||
};
|
||||
|
||||
export const loadMoreQueryHistory = async (exploreId: ExploreId = ExploreId.left) => {
|
||||
export const loadMoreQueryHistory = async (exploreId = 'left') => {
|
||||
const button = withinExplore(exploreId).getByRole('button', { name: 'Load more' });
|
||||
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 buttons = selector.getAllByRole('button', { name: actionAccessibleName });
|
||||
await userEvent.click(buttons[queryIndex]);
|
||||
|
@ -33,7 +33,7 @@ import { configureStore } from 'app/store/configureStore';
|
||||
|
||||
import { LokiDatasource } from '../../../../plugins/datasource/loki/datasource';
|
||||
import { LokiQuery } from '../../../../plugins/datasource/loki/types';
|
||||
import { ExploreId, ExploreQueryParams } from '../../../../types';
|
||||
import { ExploreQueryParams } from '../../../../types';
|
||||
import { initialUserState } from '../../../profile/state/reducers';
|
||||
import ExplorePage from '../../ExplorePage';
|
||||
|
||||
@ -42,7 +42,7 @@ type DatasourceSetup = { settings: DataSourceInstanceSettings; api: DataSourceAp
|
||||
type SetupOptions = {
|
||||
clearLocalStorage?: boolean;
|
||||
datasources?: DatasourceSetup[];
|
||||
urlParams?: ExploreQueryParams & { [key: string]: string };
|
||||
urlParams?: ExploreQueryParams;
|
||||
prevUsedDatasource?: { orgId: number; datasource: string };
|
||||
mixedEnabled?: boolean;
|
||||
};
|
||||
@ -166,7 +166,7 @@ export function setupExplore(options?: SetupOptions): {
|
||||
};
|
||||
}
|
||||
|
||||
function makeDatasourceSetup({
|
||||
export function makeDatasourceSetup({
|
||||
name = 'loki',
|
||||
id = 1,
|
||||
uid: uidOverride,
|
||||
@ -232,10 +232,10 @@ function makeDatasourceSetup({
|
||||
};
|
||||
}
|
||||
|
||||
export const waitForExplore = (exploreId: ExploreId = ExploreId.left) => {
|
||||
export const waitForExplore = (exploreId = 'left') => {
|
||||
return waitFor(async () => {
|
||||
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();
|
||||
};
|
||||
|
||||
export const withinExplore = (exploreId: ExploreId) => {
|
||||
export const withinExplore = (exploreId: string) => {
|
||||
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 { locationService } from '@grafana/runtime';
|
||||
|
||||
import { makeLogsQueryResponse, makeMetricsQueryResponse } from './helper/query';
|
||||
import { makeLogsQueryResponse } from './helper/query';
|
||||
import { setupExplore, tearDown, waitForExplore } from './helper/setup';
|
||||
|
||||
describe('Explore: handle running/not running query', () => {
|
||||
afterEach(() => {
|
||||
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();
|
||||
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();
|
||||
});
|
||||
|
||||
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 = {
|
||||
left: serializeStateToUrlParam({
|
||||
datasource: 'loki-uid',
|
||||
@ -46,61 +36,10 @@ describe('Explore: handle running/not running query', () => {
|
||||
// And that the editor gets the expr from the url
|
||||
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
|
||||
expect(datasources.loki.query).toBeCalledTimes(1);
|
||||
expect(jest.mocked(datasources.loki.query).mock.calls[0][0]).toMatchObject({
|
||||
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 { config } from '@grafana/runtime';
|
||||
import { ExploreId } from 'app/types';
|
||||
|
||||
import { silenceConsoleOutput } from '../../../../test/core/utils/silenceConsoleOutput';
|
||||
|
||||
@ -155,26 +154,26 @@ describe('Explore: Query History', () => {
|
||||
const { datasources } = setupExplore({ urlParams });
|
||||
jest.mocked(datasources.loki.query).mockReturnValue(makeLogsQueryResponse());
|
||||
await waitForExplore();
|
||||
await waitForExplore(ExploreId.right);
|
||||
await waitForExplore('right');
|
||||
|
||||
// queries in history
|
||||
await openQueryHistory(ExploreId.left);
|
||||
await assertQueryHistory(['{"expr":"query #2"}', '{"expr":"query #1"}'], ExploreId.left);
|
||||
await openQueryHistory(ExploreId.right);
|
||||
await assertQueryHistory(['{"expr":"query #2"}', '{"expr":"query #1"}'], ExploreId.right);
|
||||
await openQueryHistory('left');
|
||||
await assertQueryHistory(['{"expr":"query #2"}', '{"expr":"query #1"}'], 'left');
|
||||
await openQueryHistory('right');
|
||||
await assertQueryHistory(['{"expr":"query #2"}', '{"expr":"query #1"}'], 'right');
|
||||
|
||||
// star one one query
|
||||
await starQueryHistory(1, ExploreId.left);
|
||||
await assertQueryHistoryIsStarred([false, true], ExploreId.left);
|
||||
await assertQueryHistoryIsStarred([false, true], ExploreId.right);
|
||||
await starQueryHistory(1, 'left');
|
||||
await assertQueryHistoryIsStarred([false, true], 'left');
|
||||
await assertQueryHistoryIsStarred([false, true], 'right');
|
||||
expect(reportInteractionMock).toBeCalledWith('grafana_explore_query_history_starred', {
|
||||
queryHistoryEnabled: false,
|
||||
newValue: true,
|
||||
});
|
||||
|
||||
await deleteQueryHistory(0, ExploreId.left);
|
||||
await assertQueryHistory(['{"expr":"query #1"}'], ExploreId.left);
|
||||
await assertQueryHistory(['{"expr":"query #1"}'], ExploreId.right);
|
||||
await deleteQueryHistory(0, 'left');
|
||||
await assertQueryHistory(['{"expr":"query #1"}'], 'left');
|
||||
await assertQueryHistory(['{"expr":"query #1"}'], 'right');
|
||||
expect(reportInteractionMock).toBeCalledWith('grafana_explore_query_history_deleted', {
|
||||
queryHistoryEnabled: false,
|
||||
});
|
||||
@ -193,10 +192,10 @@ describe('Explore: Query History', () => {
|
||||
jest.mocked(datasources.loki.query).mockReturnValueOnce(makeLogsQueryResponse());
|
||||
await waitForExplore();
|
||||
await openQueryHistory();
|
||||
await assertQueryHistory(['{"expr":"query #1"}'], ExploreId.left);
|
||||
await assertQueryHistory(['{"expr":"query #1"}'], 'left');
|
||||
|
||||
await commentQueryHistory(0, 'test comment');
|
||||
await assertQueryHistoryComment(['test comment'], ExploreId.left);
|
||||
await assertQueryHistoryComment(['test comment'], 'left');
|
||||
});
|
||||
|
||||
it('updates query history settings', async () => {
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { DataSourceApi } from '@grafana/data';
|
||||
import { DataQuery } from '@grafana/schema';
|
||||
import { ExploreId, ExploreItemState } from 'app/types';
|
||||
import { ExploreItemState } from 'app/types';
|
||||
|
||||
import { updateDatasourceInstanceAction, datasourceReducer } from './datasource';
|
||||
import { createEmptyQueryResponse } from './utils';
|
||||
@ -27,7 +27,7 @@ describe('Datasource reducer', () => {
|
||||
|
||||
const result = datasourceReducer(
|
||||
initialState,
|
||||
updateDatasourceInstanceAction({ exploreId: ExploreId.left, datasourceInstance, history: [] })
|
||||
updateDatasourceInstanceAction({ exploreId: 'left', datasourceInstance, history: [] })
|
||||
);
|
||||
|
||||
const expectedState: Partial<ExploreItemState> = {
|
||||
|
@ -7,7 +7,6 @@ import { DataSourceRef } from '@grafana/schema';
|
||||
import { RefreshPicker } from '@grafana/ui';
|
||||
import { stopQueryState } from 'app/core/utils/explore';
|
||||
import { ExploreItemState, ThunkResult } from 'app/types';
|
||||
import { ExploreId } from 'app/types/explore';
|
||||
|
||||
import { loadSupplementaryQueries } from '../utils/supplementaryQueries';
|
||||
|
||||
@ -23,7 +22,7 @@ import { createEmptyQueryResponse, loadAndInitDatasource } from './utils';
|
||||
* Updates datasource instance before datasource loading has started
|
||||
*/
|
||||
export interface UpdateDatasourceInstancePayload {
|
||||
exploreId: ExploreId;
|
||||
exploreId: string;
|
||||
datasourceInstance: DataSourceApi;
|
||||
history: HistoryItem[];
|
||||
}
|
||||
@ -39,7 +38,7 @@ export const updateDatasourceInstanceAction = createAction<UpdateDatasourceInsta
|
||||
* Loads a new datasource identified by the given name.
|
||||
*/
|
||||
export function changeDatasource(
|
||||
exploreId: ExploreId,
|
||||
exploreId: string,
|
||||
datasource: string | DataSourceRef,
|
||||
options?: { importQueries: boolean }
|
||||
): ThunkResult<Promise<void>> {
|
||||
|
@ -13,7 +13,7 @@ import { DataQuery, DataSourceRef } from '@grafana/schema';
|
||||
import { getQueryKeys } from 'app/core/utils/explore';
|
||||
import { getTimeZone } from 'app/features/profile/state/selectors';
|
||||
import { createAsyncThunk, ThunkResult } from 'app/types';
|
||||
import { ExploreId, ExploreItemState } from 'app/types/explore';
|
||||
import { ExploreItemState } from 'app/types/explore';
|
||||
|
||||
import { datasourceReducer } from './datasource';
|
||||
import { historyReducer } from './history';
|
||||
@ -32,7 +32,7 @@ import { makeExplorePaneState, loadAndInitDatasource, createEmptyQueryResponse,
|
||||
* The width will be used to calculate graph intervals (number of datapoints).
|
||||
*/
|
||||
export interface ChangeSizePayload {
|
||||
exploreId: ExploreId;
|
||||
exploreId: string;
|
||||
width: number;
|
||||
}
|
||||
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.
|
||||
*/
|
||||
interface ChangePanelsState {
|
||||
exploreId: ExploreId;
|
||||
exploreId: string;
|
||||
panelsState: ExplorePanelsState;
|
||||
}
|
||||
const changePanelsStateAction = createAction<ChangePanelsState>('explore/changePanels');
|
||||
export function changePanelState(
|
||||
exploreId: ExploreId,
|
||||
exploreId: string,
|
||||
panel: PreferredVisualisationType,
|
||||
panelState: ExplorePanelsState[PreferredVisualisationType]
|
||||
): ThunkResult<void> {
|
||||
@ -73,7 +73,7 @@ export function changePanelState(
|
||||
* Call this only on components for with the Explore state has not been initialized.
|
||||
*/
|
||||
interface InitializeExplorePayload {
|
||||
exploreId: ExploreId;
|
||||
exploreId: string;
|
||||
queries: DataQuery[];
|
||||
range: TimeRange;
|
||||
history: HistoryItem[];
|
||||
@ -82,7 +82,7 @@ interface InitializeExplorePayload {
|
||||
const initializeExploreAction = createAction<InitializeExplorePayload>('explore/initializeExploreAction');
|
||||
|
||||
export interface SetUrlReplacedPayload {
|
||||
exploreId: ExploreId;
|
||||
exploreId: string;
|
||||
}
|
||||
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.
|
||||
* 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 });
|
||||
}
|
||||
|
||||
interface InitializeExploreOptions {
|
||||
exploreId: ExploreId;
|
||||
exploreId: string;
|
||||
datasource: DataSourceRef | string | undefined;
|
||||
queries: DataQuery[];
|
||||
range: RawTimeRange;
|
||||
panelsState?: ExplorePanelsState;
|
||||
position?: number;
|
||||
}
|
||||
/**
|
||||
* Initialize Explore state with state from the URL and the React component.
|
||||
|
@ -12,7 +12,7 @@ import {
|
||||
updateRichHistorySettings,
|
||||
updateStarredInRichHistory,
|
||||
} 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 { RichHistorySearchFilters, RichHistorySettings } from '../../../core/utils/richHistoryTypes';
|
||||
@ -24,13 +24,14 @@ import {
|
||||
richHistoryStorageFullAction,
|
||||
richHistoryUpdatedAction,
|
||||
} from './main';
|
||||
import { selectPanesEntries } from './selectors';
|
||||
|
||||
//
|
||||
// Actions and Payloads
|
||||
//
|
||||
|
||||
export interface HistoryUpdatedPayload {
|
||||
exploreId: ExploreId;
|
||||
exploreId: string;
|
||||
history: HistoryItem[];
|
||||
}
|
||||
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]) => {
|
||||
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> => {
|
||||
return async (dispatch) => {
|
||||
return async (dispatch, getState) => {
|
||||
await deleteAllFromRichHistory();
|
||||
dispatch(
|
||||
richHistoryUpdatedAction({ richHistoryResults: { richHistory: [], total: 0 }, exploreId: ExploreId.left })
|
||||
);
|
||||
dispatch(
|
||||
richHistoryUpdatedAction({ richHistoryResults: { richHistory: [], total: 0 }, exploreId: ExploreId.right })
|
||||
);
|
||||
selectPanesEntries(getState()).forEach(([exploreId]) => {
|
||||
dispatch(richHistoryUpdatedAction({ richHistoryResults: { richHistory: [], total: 0 }, exploreId }));
|
||||
dispatch(richHistoryUpdatedAction({ richHistoryResults: { richHistory: [], total: 0 }, exploreId }));
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
export const loadRichHistory = (exploreId: ExploreId): ThunkResult<void> => {
|
||||
export const loadRichHistory = (exploreId: string): ThunkResult<void> => {
|
||||
return async (dispatch, getState) => {
|
||||
const filters = getState().explore.panes[exploreId]!.richHistorySearchFilters;
|
||||
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) => {
|
||||
const currentFilters = getState().explore.panes[exploreId]?.richHistorySearchFilters;
|
||||
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) => {
|
||||
dispatch(richHistorySearchFiltersUpdatedAction({ filters: undefined, 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
|
||||
*/
|
||||
export const updateHistorySearchFilters = (
|
||||
exploreId: ExploreId,
|
||||
filters: RichHistorySearchFilters
|
||||
): ThunkResult<void> => {
|
||||
export const updateHistorySearchFilters = (exploreId: string, filters: RichHistorySearchFilters): ThunkResult<void> => {
|
||||
return async (dispatch, getState) => {
|
||||
await dispatch(richHistorySearchFiltersUpdatedAction({ exploreId, filters: { ...filters } }));
|
||||
const currentSettings = getState().explore.richHistorySettings!;
|
||||
|
@ -7,7 +7,7 @@ import { PanelModel } from 'app/features/dashboard/state';
|
||||
|
||||
import { reducerTester } from '../../../../test/core/redux/reducerTester';
|
||||
import { MockDataSourceApi } from '../../../../test/mocks/datasource_srv';
|
||||
import { ExploreId, ExploreItemState, ExploreState } from '../../../types';
|
||||
import { ExploreItemState, ExploreState } from '../../../types';
|
||||
|
||||
import { exploreReducer, navigateToExplore, splitClose } from './main';
|
||||
|
||||
@ -117,36 +117,6 @@ describe('navigateToExplore', () => {
|
||||
describe('Explore reducer', () => {
|
||||
describe('split view', () => {
|
||||
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', () => {
|
||||
const leftItemMock = {
|
||||
containerWidth: 100,
|
||||
@ -166,7 +136,7 @@ describe('Explore reducer', () => {
|
||||
// closing left item
|
||||
reducerTester<ExploreState>()
|
||||
.givenReducer(exploreReducer, initialState)
|
||||
.whenActionIsDispatched(splitClose(ExploreId.right))
|
||||
.whenActionIsDispatched(splitClose('right'))
|
||||
.thenStateShouldEqual({
|
||||
evenSplitPanes: true,
|
||||
largerExploreId: undefined,
|
||||
@ -193,7 +163,7 @@ describe('Explore reducer', () => {
|
||||
|
||||
reducerTester<ExploreState>()
|
||||
.givenReducer(exploreReducer, initialState)
|
||||
.whenActionIsDispatched(splitClose(ExploreId.right))
|
||||
.whenActionIsDispatched(splitClose('right'))
|
||||
.thenStateShouldEqual({
|
||||
evenSplitPanes: true,
|
||||
panes: {
|
||||
|
@ -3,9 +3,9 @@ import { AnyAction } from 'redux';
|
||||
|
||||
import { SplitOpenOptions } from '@grafana/data';
|
||||
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 { ExploreId, ExploreItemState, ExploreState } from 'app/types/explore';
|
||||
import { ExploreItemState, ExploreState } from 'app/types/explore';
|
||||
|
||||
import { RichHistoryResults } from '../../../core/history/RichHistoryStorage';
|
||||
import { RichHistorySearchFilters, RichHistorySettings } from '../../../core/utils/richHistoryTypes';
|
||||
@ -26,7 +26,7 @@ export interface SyncTimesPayload {
|
||||
}
|
||||
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'
|
||||
);
|
||||
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 richHistorySearchFiltersUpdatedAction = createAction<{
|
||||
exploreId: ExploreId;
|
||||
exploreId: string;
|
||||
filters?: RichHistorySearchFilters;
|
||||
}>('explore/richHistorySearchFiltersUpdatedAction');
|
||||
|
||||
export const saveCorrelationsAction = createAction<CorrelationData[]>('explore/saveCorrelationsAction');
|
||||
|
||||
export const splitSizeUpdateAction = createAction<{
|
||||
largerExploreId?: ExploreId;
|
||||
largerExploreId?: string;
|
||||
}>('explore/splitSizeUpdateAction');
|
||||
|
||||
export const maximizePaneAction = createAction<{
|
||||
exploreId?: ExploreId;
|
||||
exploreId?: string;
|
||||
}>('explore/maximizePaneAction');
|
||||
|
||||
export const evenPaneResizeAction = createAction('explore/evenPaneResizeAction');
|
||||
@ -53,8 +53,7 @@ export const evenPaneResizeAction = createAction('explore/evenPaneResizeAction')
|
||||
/**
|
||||
* Close the pane with the given id.
|
||||
*/
|
||||
type SplitCloseActionPayload = ExploreId;
|
||||
export const splitClose = createAction<SplitCloseActionPayload>('explore/splitClose');
|
||||
export const splitClose = createAction<string>('explore/splitClose');
|
||||
|
||||
export interface SetPaneStateActionPayload {
|
||||
[itemId: string]: Partial<ExploreItemState>;
|
||||
@ -64,27 +63,31 @@ export const setPaneState = createAction<SetPaneStateActionPayload>('explore/set
|
||||
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.
|
||||
*
|
||||
* TODO: this can be improved by better inferring fallback values.
|
||||
*/
|
||||
export const splitOpen = createAsyncThunk(
|
||||
'explore/splitOpen',
|
||||
async (options: SplitOpenOptions | undefined, { getState, dispatch }) => {
|
||||
const leftState = getState().explore.panes.left;
|
||||
async (options: SplitOpenOptions | undefined, { getState, dispatch, requestId }) => {
|
||||
// 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(
|
||||
initializeExplore({
|
||||
exploreId: ExploreId.right,
|
||||
datasource: options?.datasourceUid || leftState?.datasourceInstance?.getRef(),
|
||||
exploreId: requestId,
|
||||
datasource: options?.datasourceUid || originState?.datasourceInstance?.getRef(),
|
||||
queries: withUniqueRefIds(queries),
|
||||
range: options?.range || leftState?.range.raw || DEFAULT_RANGE,
|
||||
panelsState: options?.panelsState || leftState?.panelsState,
|
||||
range: options?.range || originState?.range.raw || DEFAULT_RANGE,
|
||||
panelsState: options?.panelsState || originState?.panelsState,
|
||||
})
|
||||
);
|
||||
},
|
||||
{
|
||||
idGenerator: generateExploreId,
|
||||
}
|
||||
);
|
||||
|
||||
@ -138,9 +141,8 @@ export const initialExploreState: ExploreState = {
|
||||
*/
|
||||
export const exploreReducer = (state = initialExploreState, action: AnyAction): ExploreState => {
|
||||
if (splitClose.match(action)) {
|
||||
const panes = {
|
||||
left: action.payload === ExploreId.left ? state.panes.right : state.panes.left,
|
||||
};
|
||||
const { [action.payload]: _, ...panes } = { ...state.panes };
|
||||
|
||||
return {
|
||||
...state,
|
||||
panes,
|
||||
@ -218,18 +220,23 @@ export const exploreReducer = (state = initialExploreState, action: AnyAction):
|
||||
...state,
|
||||
panes: {
|
||||
...state.panes,
|
||||
right: initialExploreItemState,
|
||||
[action.meta.requestId]: initialExploreItemState,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
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 {
|
||||
...state,
|
||||
panes: {
|
||||
...state.panes,
|
||||
[action.meta.arg.exploreId]: initialExploreItemState,
|
||||
},
|
||||
panes,
|
||||
};
|
||||
}
|
||||
|
||||
@ -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') {
|
||||
return {
|
||||
...state,
|
||||
panes: Object.entries(state.panes).reduce<ExploreState['panes']>((acc, [id, pane]) => {
|
||||
if (id === exploreId) {
|
||||
acc[id] = paneReducer(pane, action);
|
||||
} else {
|
||||
acc[id as ExploreId] = pane;
|
||||
}
|
||||
return acc;
|
||||
panes: Object.entries(state.panes).reduce((acc, [id, pane]) => {
|
||||
return {
|
||||
...acc,
|
||||
[id]: id === exploreId ? paneReducer(pane, action) : pane,
|
||||
};
|
||||
}, {}),
|
||||
};
|
||||
}
|
||||
|
@ -16,7 +16,7 @@ import {
|
||||
} from '@grafana/data';
|
||||
import { config } from '@grafana/runtime';
|
||||
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 { configureStore } from '../../../store/configureStore';
|
||||
@ -51,7 +51,7 @@ import { makeExplorePaneState } from './utils';
|
||||
|
||||
const { testRange, defaultInitialState } = createDefaultInitialState();
|
||||
|
||||
const exploreId = ExploreId.left;
|
||||
const exploreId = 'left';
|
||||
const datasources: DataSourceApi[] = [
|
||||
{
|
||||
name: 'testDs',
|
||||
@ -158,7 +158,7 @@ describe('runQueries', () => {
|
||||
const { dispatch, getState } = setupTests();
|
||||
setupQueryResponse(getState());
|
||||
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!.graphResult).toBeDefined();
|
||||
});
|
||||
@ -167,7 +167,7 @@ describe('runQueries', () => {
|
||||
const { dispatch, getState } = setupTests();
|
||||
setupQueryResponse(getState());
|
||||
dispatch(saveCorrelationsAction([]));
|
||||
dispatch(runQueries({ exploreId: ExploreId.left }));
|
||||
dispatch(runQueries({ exploreId: 'left' }));
|
||||
|
||||
const state = getState().explore.panes.left!;
|
||||
expect(state.queryResponse.request?.requestId).toBe('explore_left');
|
||||
@ -187,7 +187,7 @@ describe('runQueries', () => {
|
||||
const leftDatasourceInstance = assertIsDefined(getState().explore.panes.left!.datasourceInstance);
|
||||
jest.mocked(leftDatasourceInstance.query).mockReturnValueOnce(EMPTY);
|
||||
await dispatch(saveCorrelationsAction([]));
|
||||
await dispatch(runQueries({ exploreId: ExploreId.left }));
|
||||
await dispatch(runQueries({ exploreId: 'left' }));
|
||||
await new Promise((resolve) => setTimeout(() => resolve(''), 500));
|
||||
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 () => {
|
||||
const { dispatch, getState } = setupTests();
|
||||
setupQueryResponse(getState());
|
||||
await dispatch(runQueries({ exploreId: ExploreId.left }));
|
||||
await dispatch(runQueries({ exploreId: 'left' }));
|
||||
expect(getState().explore.panes.left!.graphResult).not.toBeDefined();
|
||||
await dispatch(saveCorrelationsAction([]));
|
||||
expect(getState().explore.panes.left!.graphResult).toBeDefined();
|
||||
@ -206,7 +206,7 @@ describe('running queries', () => {
|
||||
it('should cancel running query when cancelQueries is dispatched', async () => {
|
||||
const unsubscribable = interval(1000);
|
||||
unsubscribable.subscribe();
|
||||
const exploreId = ExploreId.left;
|
||||
const exploreId = 'left';
|
||||
const initialState = {
|
||||
explore: {
|
||||
panes: {
|
||||
@ -274,13 +274,13 @@ describe('changeQueries', () => {
|
||||
await dispatch(
|
||||
changeQueries({
|
||||
queries: [{ refId: 'A', datasource: datasources[1].getRef() }],
|
||||
exploreId: ExploreId.left,
|
||||
exploreId: 'left',
|
||||
})
|
||||
);
|
||||
|
||||
expect(actions.changeQueriesAction).not.toHaveBeenCalled();
|
||||
expect(actions.importQueries).toHaveBeenCalledWith(
|
||||
ExploreId.left,
|
||||
'left',
|
||||
originalQueries,
|
||||
datasources[0],
|
||||
datasources[1],
|
||||
@ -308,7 +308,7 @@ describe('changeQueries', () => {
|
||||
await dispatch(
|
||||
changeQueries({
|
||||
queries: [{ refId: 'A', datasource: datasources[0].getRef(), queryType: 'someValue' }],
|
||||
exploreId: ExploreId.left,
|
||||
exploreId: 'left',
|
||||
})
|
||||
);
|
||||
|
||||
@ -337,7 +337,7 @@ describe('changeQueries', () => {
|
||||
await dispatch(
|
||||
changeQueries({
|
||||
queries: [{ refId: 'A', datasource: datasources[1].getRef() }],
|
||||
exploreId: ExploreId.left,
|
||||
exploreId: 'left',
|
||||
})
|
||||
);
|
||||
|
||||
@ -362,7 +362,7 @@ describe('changeQueries', () => {
|
||||
await dispatch(
|
||||
changeQueries({
|
||||
queries: [{ refId: 'A', datasource: datasources[0].getRef(), queryType: 'someValue' }],
|
||||
exploreId: ExploreId.left,
|
||||
exploreId: 'left',
|
||||
})
|
||||
);
|
||||
|
||||
@ -400,7 +400,7 @@ describe('changeQueries', () => {
|
||||
await dispatch(
|
||||
changeQueries({
|
||||
queries: [originalQueries[0]],
|
||||
exploreId: ExploreId.left,
|
||||
exploreId: 'left',
|
||||
})
|
||||
);
|
||||
|
||||
@ -425,7 +425,7 @@ describe('importing queries', () => {
|
||||
|
||||
await dispatch(
|
||||
importQueries(
|
||||
ExploreId.left,
|
||||
'left',
|
||||
[
|
||||
{ datasource: { type: 'postgresql', uid: 'ds1' }, refId: 'refId_A' },
|
||||
{ datasource: { type: 'postgresql', uid: 'ds1' }, refId: 'refId_B' },
|
||||
@ -606,7 +606,7 @@ describe('reducer', () => {
|
||||
|
||||
reducerTester<ExploreItemState>()
|
||||
.givenReducer(queryReducer, initialState)
|
||||
.whenActionIsDispatched(scanStartAction({ exploreId: ExploreId.left }))
|
||||
.whenActionIsDispatched(scanStartAction({ exploreId: 'left' }))
|
||||
.thenStateShouldEqual({
|
||||
...initialState,
|
||||
scanning: true,
|
||||
@ -621,7 +621,7 @@ describe('reducer', () => {
|
||||
|
||||
reducerTester<ExploreItemState>()
|
||||
.givenReducer(queryReducer, initialState)
|
||||
.whenActionIsDispatched(scanStopAction({ exploreId: ExploreId.left }))
|
||||
.whenActionIsDispatched(scanStopAction({ exploreId: 'left' }))
|
||||
.thenStateShouldEqual({
|
||||
...initialState,
|
||||
scanning: false,
|
||||
@ -638,7 +638,7 @@ describe('reducer', () => {
|
||||
} as unknown as ExploreItemState)
|
||||
.whenActionIsDispatched(
|
||||
addQueryRowAction({
|
||||
exploreId: ExploreId.left,
|
||||
exploreId: 'left',
|
||||
query: { refId: 'A', key: 'mockKey' },
|
||||
index: 0,
|
||||
})
|
||||
@ -655,7 +655,7 @@ describe('reducer', () => {
|
||||
} as unknown as ExploreItemState)
|
||||
.whenActionIsDispatched(
|
||||
addQueryRowAction({
|
||||
exploreId: ExploreId.left,
|
||||
exploreId: 'left',
|
||||
query: { refId: 'B', key: 'mockKey', datasource: { type: 'loki' } },
|
||||
index: 0,
|
||||
})
|
||||
@ -688,7 +688,7 @@ describe('reducer', () => {
|
||||
},
|
||||
} as unknown as Partial<StoreState>);
|
||||
|
||||
await dispatch(addResultsToCache(ExploreId.left));
|
||||
await dispatch(addResultsToCache('left'));
|
||||
|
||||
expect(getState().explore.panes.left!.cache).toEqual([
|
||||
{ key: 'from=1621348027000&to=1621348050000', value: { series: [{ name: 'test name' }], state: 'Done' } },
|
||||
@ -709,7 +709,7 @@ describe('reducer', () => {
|
||||
},
|
||||
} as unknown as Partial<StoreState>);
|
||||
|
||||
await dispatch(addResultsToCache(ExploreId.left));
|
||||
await dispatch(addResultsToCache('left'));
|
||||
|
||||
expect(getState().explore.panes.left!.cache).toEqual([]);
|
||||
});
|
||||
@ -737,7 +737,7 @@ describe('reducer', () => {
|
||||
},
|
||||
} 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).toEqual([
|
||||
@ -763,7 +763,7 @@ describe('reducer', () => {
|
||||
},
|
||||
} as unknown as Partial<StoreState>);
|
||||
|
||||
await dispatch(clearCache(ExploreId.left));
|
||||
await dispatch(clearCache('left'));
|
||||
|
||||
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 () => {
|
||||
dispatch(runQueries({ exploreId: ExploreId.left }));
|
||||
dispatch(runQueries({ exploreId: 'left' }));
|
||||
// first query is run automatically
|
||||
// loading in progress - subscriptions for both supplementary queries are created, not cleaned up yet
|
||||
expect(unsubscribes).toHaveLength(2);
|
||||
@ -830,7 +830,7 @@ describe('reducer', () => {
|
||||
expect(unsubscribes[1]).not.toBeCalled();
|
||||
|
||||
setupQueryResponse(getState());
|
||||
dispatch(runQueries({ exploreId: ExploreId.left }));
|
||||
dispatch(runQueries({ exploreId: 'left' }));
|
||||
// a new query is run while supplementary queries are not resolve yet...
|
||||
expect(unsubscribes[0]).toBeCalled();
|
||||
expect(unsubscribes[1]).toBeCalled();
|
||||
@ -841,12 +841,12 @@ describe('reducer', () => {
|
||||
});
|
||||
|
||||
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[0]).not.toBeCalled();
|
||||
expect(unsubscribes[1]).not.toBeCalled();
|
||||
|
||||
dispatch(cancelQueries(ExploreId.left));
|
||||
dispatch(cancelQueries('left'));
|
||||
expect(unsubscribes).toHaveLength(2);
|
||||
expect(unsubscribes[0]).toBeCalled();
|
||||
expect(unsubscribes[1]).toBeCalled();
|
||||
@ -858,7 +858,7 @@ describe('reducer', () => {
|
||||
});
|
||||
|
||||
it('should load supplementary queries after running the query', () => {
|
||||
dispatch(runQueries({ exploreId: ExploreId.left }));
|
||||
dispatch(runQueries({ exploreId: 'left' }));
|
||||
expect(unsubscribes).toHaveLength(2);
|
||||
});
|
||||
|
||||
@ -866,7 +866,7 @@ describe('reducer', () => {
|
||||
mockDataProvider = () => {
|
||||
return of({ state: LoadingState.Loading, error: undefined, data: [] });
|
||||
};
|
||||
dispatch(runQueries({ exploreId: ExploreId.left }));
|
||||
dispatch(runQueries({ exploreId: 'left' }));
|
||||
|
||||
for (const type of supplementaryQueryTypes) {
|
||||
expect(getState().explore.panes.left!.supplementaryQueries[type].data).toBeDefined();
|
||||
@ -879,7 +879,7 @@ describe('reducer', () => {
|
||||
expect(getState().explore.panes.left!.supplementaryQueries[type].dataProvider).toBeDefined();
|
||||
}
|
||||
|
||||
dispatch(cancelQueries(ExploreId.left));
|
||||
dispatch(cancelQueries('left'));
|
||||
for (const type of supplementaryQueryTypes) {
|
||||
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: [{}] }
|
||||
);
|
||||
};
|
||||
dispatch(runQueries({ exploreId: ExploreId.left }));
|
||||
dispatch(runQueries({ exploreId: 'left' }));
|
||||
|
||||
for (const types of supplementaryQueryTypes) {
|
||||
expect(getState().explore.panes.left!.supplementaryQueries[types].data).toBeDefined();
|
||||
@ -901,7 +901,7 @@ describe('reducer', () => {
|
||||
expect(getState().explore.panes.left!.supplementaryQueries[types].dataProvider).toBeDefined();
|
||||
}
|
||||
|
||||
dispatch(cancelQueries(ExploreId.left));
|
||||
dispatch(cancelQueries('left'));
|
||||
|
||||
for (const types of supplementaryQueryTypes) {
|
||||
expect(getState().explore.panes.left!.supplementaryQueries[types].data).toBeDefined();
|
||||
@ -915,14 +915,14 @@ describe('reducer', () => {
|
||||
return of({ state: LoadingState.Done, error: undefined, data: [{}] });
|
||||
};
|
||||
// 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(
|
||||
false
|
||||
);
|
||||
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
|
||||
dispatch(runQueries({ exploreId: ExploreId.left }));
|
||||
dispatch(runQueries({ exploreId: 'left' }));
|
||||
|
||||
expect(
|
||||
getState().explore.panes.left!.supplementaryQueries[SupplementaryQueryType.LogsVolume].data
|
||||
@ -945,11 +945,11 @@ describe('reducer', () => {
|
||||
|
||||
it('load data of supplementary query that gets enabled', async () => {
|
||||
// first we start with both supplementary queries disabled
|
||||
dispatch(setSupplementaryQueryEnabled(ExploreId.left, false, SupplementaryQueryType.LogsVolume));
|
||||
dispatch(setSupplementaryQueryEnabled(ExploreId.left, false, SupplementaryQueryType.LogsSample));
|
||||
dispatch(setSupplementaryQueryEnabled('left', false, SupplementaryQueryType.LogsVolume));
|
||||
dispatch(setSupplementaryQueryEnabled('left', false, SupplementaryQueryType.LogsSample));
|
||||
|
||||
// runQueries sets up providers, but does not run queries
|
||||
dispatch(runQueries({ exploreId: ExploreId.left }));
|
||||
dispatch(runQueries({ exploreId: 'left' }));
|
||||
expect(
|
||||
getState().explore.panes.left!.supplementaryQueries[SupplementaryQueryType.LogsVolume].dataProvider
|
||||
).toBeDefined();
|
||||
@ -958,7 +958,7 @@ describe('reducer', () => {
|
||||
).toBeDefined();
|
||||
|
||||
// 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
|
||||
expect(getState().explore.panes.left!.supplementaryQueries[SupplementaryQueryType.LogsVolume].enabled).toBe(true);
|
||||
@ -984,8 +984,8 @@ describe('reducer', () => {
|
||||
...defaultInitialState,
|
||||
explore: {
|
||||
panes: {
|
||||
[ExploreId.left]: {
|
||||
...defaultInitialState.explore.panes[ExploreId.left],
|
||||
['left']: {
|
||||
...defaultInitialState.explore.panes['left'],
|
||||
queryResponse: {
|
||||
state: LoadingState.Streaming,
|
||||
},
|
||||
@ -997,12 +997,12 @@ describe('reducer', () => {
|
||||
},
|
||||
},
|
||||
} 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[ExploreId.left]?.clearedAtIndex).toBe(logRows.length - 1);
|
||||
expect(getState().explore.panes['left']?.logsResult?.rows.length).toBe(0);
|
||||
expect(getState().explore.panes['left']?.clearedAtIndex).toBe(logRows.length - 1);
|
||||
});
|
||||
|
||||
it('should filter new log rows', async () => {
|
||||
@ -1014,8 +1014,8 @@ describe('reducer', () => {
|
||||
...defaultInitialState,
|
||||
explore: {
|
||||
panes: {
|
||||
[ExploreId.left]: {
|
||||
...defaultInitialState.explore.panes[ExploreId.left],
|
||||
['left']: {
|
||||
...defaultInitialState.explore.panes['left'],
|
||||
isLive: true,
|
||||
queryResponse: {
|
||||
state: LoadingState.Streaming,
|
||||
@ -1029,12 +1029,12 @@ describe('reducer', () => {
|
||||
},
|
||||
} 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(
|
||||
queryStreamUpdatedAction({
|
||||
exploreId: ExploreId.left,
|
||||
exploreId: 'left',
|
||||
response: {
|
||||
request: true,
|
||||
traceFrames: [],
|
||||
@ -1049,8 +1049,8 @@ describe('reducer', () => {
|
||||
} as unknown as QueryEndedPayload)
|
||||
);
|
||||
|
||||
expect(getState().explore.panes[ExploreId.left]?.logsResult?.rows.length).toBe(newLogRows.length);
|
||||
expect(getState().explore.panes[ExploreId.left]?.clearedAtIndex).toBe(oldLogRows.length - 1);
|
||||
expect(getState().explore.panes['left']?.logsResult?.rows.length).toBe(newLogRows.length);
|
||||
expect(getState().explore.panes['left']?.clearedAtIndex).toBe(oldLogRows.length - 1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -47,7 +47,7 @@ import {
|
||||
ThunkDispatch,
|
||||
ThunkResult,
|
||||
} 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 { 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
|
||||
*/
|
||||
export const selectIsWaitingForData = (exploreId: ExploreId) => {
|
||||
export const selectIsWaitingForData = (exploreId: string) => {
|
||||
return (state: StoreState) => {
|
||||
const panelState = state.explore.panes[exploreId];
|
||||
if (!panelState) {
|
||||
@ -83,7 +83,7 @@ export const selectIsWaitingForData = (exploreId: ExploreId) => {
|
||||
* Adds a query row after the row with the given index.
|
||||
*/
|
||||
export interface AddQueryRowPayload {
|
||||
exploreId: ExploreId;
|
||||
exploreId: string;
|
||||
index: number;
|
||||
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.
|
||||
*/
|
||||
export interface ChangeQueriesPayload {
|
||||
exploreId: ExploreId;
|
||||
exploreId: string;
|
||||
queries: DataQuery[];
|
||||
}
|
||||
export const changeQueriesAction = createAction<ChangeQueriesPayload>('explore/changeQueries');
|
||||
@ -103,18 +103,18 @@ export const changeQueriesAction = createAction<ChangeQueriesPayload>('explore/c
|
||||
* Cancel running queries.
|
||||
*/
|
||||
export interface CancelQueriesPayload {
|
||||
exploreId: ExploreId;
|
||||
exploreId: string;
|
||||
}
|
||||
export const cancelQueriesAction = createAction<CancelQueriesPayload>('explore/cancelQueries');
|
||||
|
||||
export interface QueriesImportedPayload {
|
||||
exploreId: ExploreId;
|
||||
exploreId: string;
|
||||
queries: DataQuery[];
|
||||
}
|
||||
export const queriesImportedAction = createAction<QueriesImportedPayload>('explore/queriesImported');
|
||||
|
||||
export interface QueryStoreSubscriptionPayload {
|
||||
exploreId: ExploreId;
|
||||
exploreId: string;
|
||||
querySubscription: Unsubscribable;
|
||||
}
|
||||
|
||||
@ -123,19 +123,19 @@ export const queryStoreSubscriptionAction = createAction<QueryStoreSubscriptionP
|
||||
);
|
||||
|
||||
const setSupplementaryQueryEnabledAction = createAction<{
|
||||
exploreId: ExploreId;
|
||||
exploreId: string;
|
||||
type: SupplementaryQueryType;
|
||||
enabled: boolean;
|
||||
}>('explore/setSupplementaryQueryEnabledAction');
|
||||
|
||||
export interface StoreSupplementaryQueryDataProvider {
|
||||
exploreId: ExploreId;
|
||||
exploreId: string;
|
||||
dataProvider?: Observable<DataQueryResponse>;
|
||||
type: SupplementaryQueryType;
|
||||
}
|
||||
|
||||
export interface CleanSupplementaryQueryDataProvider {
|
||||
exploreId: ExploreId;
|
||||
exploreId: string;
|
||||
type: SupplementaryQueryType;
|
||||
}
|
||||
|
||||
@ -150,12 +150,12 @@ export const cleanSupplementaryQueryDataProviderAction = createAction<CleanSuppl
|
||||
'explore/cleanSupplementaryQueryDataProviderAction'
|
||||
);
|
||||
|
||||
export const cleanSupplementaryQueryAction = createAction<{ exploreId: ExploreId; type: SupplementaryQueryType }>(
|
||||
export const cleanSupplementaryQueryAction = createAction<{ exploreId: string; type: SupplementaryQueryType }>(
|
||||
'explore/cleanSupplementaryQueryAction'
|
||||
);
|
||||
|
||||
export interface StoreSupplementaryQueryDataSubscriptionPayload {
|
||||
exploreId: ExploreId;
|
||||
exploreId: string;
|
||||
dataSubscription?: SubscriptionLike;
|
||||
type: SupplementaryQueryType;
|
||||
}
|
||||
@ -171,13 +171,13 @@ const storeSupplementaryQueryDataSubscriptionAction = createAction<StoreSuppleme
|
||||
* Stores data returned by the provider. Used internally by loadSupplementaryQueryData().
|
||||
*/
|
||||
const updateSupplementaryQueryDataAction = createAction<{
|
||||
exploreId: ExploreId;
|
||||
exploreId: string;
|
||||
type: SupplementaryQueryType;
|
||||
data: DataQueryResponse;
|
||||
}>('explore/updateSupplementaryQueryDataAction');
|
||||
|
||||
export interface QueryEndedPayload {
|
||||
exploreId: ExploreId;
|
||||
exploreId: string;
|
||||
response: ExplorePanelData;
|
||||
}
|
||||
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.
|
||||
*/
|
||||
export interface SetQueriesPayload {
|
||||
exploreId: ExploreId;
|
||||
exploreId: string;
|
||||
queries: DataQuery[];
|
||||
}
|
||||
export const setQueriesAction = createAction<SetQueriesPayload>('explore/setQueries');
|
||||
|
||||
export interface ChangeLoadingStatePayload {
|
||||
exploreId: ExploreId;
|
||||
exploreId: string;
|
||||
loadingState: LoadingState;
|
||||
}
|
||||
export const changeLoadingStateAction = createAction<ChangeLoadingStatePayload>('changeLoadingState');
|
||||
|
||||
export interface SetPausedStatePayload {
|
||||
exploreId: ExploreId;
|
||||
exploreId: string;
|
||||
isPaused: boolean;
|
||||
}
|
||||
export const setPausedStateAction = createAction<SetPausedStatePayload>('explore/setPausedState');
|
||||
|
||||
export interface ClearLogsPayload {
|
||||
exploreId: ExploreId;
|
||||
exploreId: string;
|
||||
}
|
||||
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
|
||||
*/
|
||||
export interface ScanStartPayload {
|
||||
exploreId: ExploreId;
|
||||
exploreId: string;
|
||||
}
|
||||
export const scanStartAction = createAction<ScanStartPayload>('explore/scanStart');
|
||||
|
||||
@ -221,7 +221,7 @@ export const scanStartAction = createAction<ScanStartPayload>('explore/scanStart
|
||||
* Stop any scanning for more results.
|
||||
*/
|
||||
export interface ScanStopPayload {
|
||||
exploreId: ExploreId;
|
||||
exploreId: string;
|
||||
}
|
||||
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).
|
||||
*/
|
||||
export interface AddResultsToCachePayload {
|
||||
exploreId: ExploreId;
|
||||
exploreId: string;
|
||||
cacheKey: string;
|
||||
queryResponse: ExplorePanelData;
|
||||
}
|
||||
@ -240,14 +240,14 @@ export const addResultsToCacheAction = createAction<AddResultsToCachePayload>('e
|
||||
* Clears cache.
|
||||
*/
|
||||
export interface ClearCachePayload {
|
||||
exploreId: ExploreId;
|
||||
exploreId: string;
|
||||
}
|
||||
export const clearCacheAction = createAction<ClearCachePayload>('explore/clearCache');
|
||||
|
||||
/**
|
||||
* 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) => {
|
||||
const queries = getState().explore.panes[exploreId]!.queries;
|
||||
let datasourceOverride = undefined;
|
||||
@ -270,7 +270,7 @@ export function addQueryRow(exploreId: ExploreId, index: number): ThunkResult<vo
|
||||
/**
|
||||
* Cancel running queries
|
||||
*/
|
||||
export function cancelQueries(exploreId: ExploreId): ThunkResult<void> {
|
||||
export function cancelQueries(exploreId: string): ThunkResult<void> {
|
||||
return (dispatch, getState) => {
|
||||
dispatch(scanStopAction({ exploreId }));
|
||||
dispatch(cancelQueriesAction({ exploreId }));
|
||||
@ -352,7 +352,7 @@ export const changeQueries = createAsyncThunk<void, ChangeQueriesPayload>(
|
||||
* @param targetDataSource
|
||||
*/
|
||||
export const importQueries = (
|
||||
exploreId: ExploreId,
|
||||
exploreId: string,
|
||||
queries: DataQuery[],
|
||||
sourceDataSource: DataSourceApi | undefined | null,
|
||||
targetDataSource: DataSourceApi,
|
||||
@ -425,7 +425,7 @@ export const importQueries = (
|
||||
* @param modifier Function that executes the modification, typically `datasourceInstance.modifyQueries`.
|
||||
*/
|
||||
export function modifyQueries(
|
||||
exploreId: ExploreId,
|
||||
exploreId: string,
|
||||
modification: QueryFixAction,
|
||||
modifier: (query: DataQuery, modification: QueryFixAction) => Promise<DataQuery>
|
||||
): ThunkResult<void> {
|
||||
@ -453,7 +453,7 @@ async function handleHistory(
|
||||
history: Array<HistoryItem<DataQuery>>,
|
||||
datasource: DataSourceApi,
|
||||
queries: DataQuery[],
|
||||
exploreId: ExploreId
|
||||
exploreId: string
|
||||
) {
|
||||
const datasourceId = datasource.meta.id;
|
||||
const nextHistory = updateHistory(history, datasourceId, queries);
|
||||
@ -465,12 +465,12 @@ async function handleHistory(
|
||||
// used filters. Instead, we refresh the query history list.
|
||||
// TODO: run only if Query History list is opened (#47252)
|
||||
for (const exploreId in state.panes) {
|
||||
await dispatch(loadRichHistory(exploreId as ExploreId));
|
||||
await dispatch(loadRichHistory(exploreId));
|
||||
}
|
||||
}
|
||||
|
||||
interface RunQueriesOptions {
|
||||
exploreId: ExploreId;
|
||||
exploreId: string;
|
||||
preserveCache?: boolean;
|
||||
}
|
||||
/**
|
||||
@ -653,7 +653,7 @@ const groupDataQueries = async (datasources: DataQuery[], scopedVars: ScopedVars
|
||||
};
|
||||
|
||||
type HandleSupplementaryQueriesOptions = {
|
||||
exploreId: ExploreId;
|
||||
exploreId: string;
|
||||
transaction: QueryTransaction;
|
||||
datasourceInstance: DataSourceApi;
|
||||
newQuerySource: Observable<ExplorePanelData>;
|
||||
@ -772,7 +772,7 @@ function canReuseSupplementaryQueryData(
|
||||
* Reset queries to the given queries. Any modifications will be discarded.
|
||||
* 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) => {
|
||||
// Inject react keys into query objects
|
||||
const queries = getState().explore.panes[exploreId]!.queries;
|
||||
@ -787,7 +787,7 @@ export function setQueries(exploreId: ExploreId, rawQueries: DataQuery[]): Thunk
|
||||
* @param exploreId Explore area
|
||||
* @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) => {
|
||||
// Register the scanner
|
||||
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) => {
|
||||
const queryResponse = getState().explore.panes[exploreId]!.queryResponse;
|
||||
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) => {
|
||||
dispatch(clearCacheAction({ exploreId }));
|
||||
};
|
||||
@ -821,7 +821,7 @@ export function clearCache(exploreId: ExploreId): ThunkResult<void> {
|
||||
/**
|
||||
* 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) => {
|
||||
const { supplementaryQueries } = getState().explore.panes[exploreId]!;
|
||||
const dataProvider = supplementaryQueries[type].dataProvider;
|
||||
@ -844,7 +844,7 @@ export function loadSupplementaryQueryData(exploreId: ExploreId, type: Supplemen
|
||||
}
|
||||
|
||||
export function setSupplementaryQueryEnabled(
|
||||
exploreId: ExploreId,
|
||||
exploreId: string,
|
||||
enabled: boolean,
|
||||
type: SupplementaryQueryType
|
||||
): 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 { ExploreId, ExploreState, StoreState } from 'app/types';
|
||||
import { ExploreItemState, StoreState } from 'app/types';
|
||||
|
||||
export const selectPanes = (state: Pick<StoreState, 'explore'>) => state.explore.panes;
|
||||
|
||||
/**
|
||||
* Explore renders panes by iterating over the panes object. This selector ensures that entries in the returned panes object
|
||||
* are in the correct order.
|
||||
*/
|
||||
export const selectOrderedExplorePanes = createSelector(selectPanes, (panes) => {
|
||||
const orderedPanes: ExploreState['panes'] = {};
|
||||
export const selectPanesEntries = createSelector<
|
||||
[(state: Pick<StoreState, 'explore'>) => Record<string, ExploreItemState | undefined>],
|
||||
Array<[string, ExploreItemState]>
|
||||
>(selectPanes, Object.entries);
|
||||
|
||||
if (panes.left) {
|
||||
orderedPanes.left = panes.left;
|
||||
}
|
||||
if (panes.right) {
|
||||
orderedPanes.right = panes.right;
|
||||
}
|
||||
return orderedPanes;
|
||||
});
|
||||
export const isSplit = createSelector(selectPanesEntries, (panes) => panes.length > 1);
|
||||
|
||||
export const isSplit = createSelector(selectPanes, (panes) => Object.keys(panes).length > 1);
|
||||
|
||||
export const getExploreItemSelector = (exploreId: ExploreId) =>
|
||||
createSelector(selectPanes, (panes) => panes[exploreId]);
|
||||
export const getExploreItemSelector = (exploreId: string) => createSelector(selectPanes, (panes) => panes[exploreId]);
|
||||
|
@ -2,7 +2,7 @@ import { reducerTester } from 'test/core/redux/reducerTester';
|
||||
|
||||
import { dateTime } from '@grafana/data';
|
||||
import { configureStore } from 'app/store/configureStore';
|
||||
import { ExploreId, ExploreItemState } from 'app/types';
|
||||
import { ExploreItemState } from 'app/types';
|
||||
|
||||
import { createDefaultInitialState } from './helpers';
|
||||
import { changeRangeAction, timeReducer, updateTime } from './time';
|
||||
@ -30,7 +30,7 @@ describe('Explore item reducer', () => {
|
||||
describe('When time is updated', () => {
|
||||
it('Time service is re-initialized and template service is updated with the new time range', async () => {
|
||||
const { dispatch } = configureStore(createDefaultInitialState().defaultInitialState as any);
|
||||
dispatch(updateTime({ exploreId: ExploreId.left }));
|
||||
dispatch(updateTime({ exploreId: 'left' }));
|
||||
expect(mockTimeSrv.init).toBeCalled();
|
||||
expect(mockTemplateSrv.updateTimeRange).toBeCalledWith(MOCK_TIME_RANGE);
|
||||
});
|
||||
@ -46,7 +46,7 @@ describe('Explore item reducer', () => {
|
||||
} as unknown as ExploreItemState)
|
||||
.whenActionIsDispatched(
|
||||
changeRangeAction({
|
||||
exploreId: ExploreId.left,
|
||||
exploreId: 'left',
|
||||
absoluteRange: { from: 1546297200000, to: 1546383600000 },
|
||||
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 { getFiscalYearStartMonth, getTimeZone } from 'app/features/profile/state/selectors';
|
||||
import { ExploreItemState, ThunkResult } from 'app/types';
|
||||
import { ExploreId } from 'app/types/explore';
|
||||
|
||||
import { getTimeSrv } from '../../dashboard/services/TimeSrv';
|
||||
import { TimeModel } from '../../dashboard/state/TimeModel';
|
||||
@ -20,7 +19,7 @@ import { runQueries } from './query';
|
||||
//
|
||||
|
||||
export interface ChangeRangePayload {
|
||||
exploreId: ExploreId;
|
||||
exploreId: string;
|
||||
range: TimeRange;
|
||||
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.
|
||||
*/
|
||||
export interface ChangeRefreshIntervalPayload {
|
||||
exploreId: ExploreId;
|
||||
exploreId: string;
|
||||
refreshInterval: string;
|
||||
}
|
||||
export const changeRefreshInterval = createAction<ChangeRefreshIntervalPayload>('explore/changeRefreshInterval');
|
||||
|
||||
export const updateTimeRange = (options: {
|
||||
exploreId: ExploreId;
|
||||
exploreId: string;
|
||||
rawRange?: RawTimeRange;
|
||||
absoluteRange?: AbsoluteTimeRange;
|
||||
}): ThunkResult<void> => {
|
||||
@ -44,8 +43,8 @@ export const updateTimeRange = (options: {
|
||||
const { syncedTimes } = getState().explore;
|
||||
if (syncedTimes) {
|
||||
Object.keys(getState().explore.panes).forEach((exploreId) => {
|
||||
dispatch(updateTime({ ...options, exploreId: exploreId as ExploreId }));
|
||||
dispatch(runQueries({ exploreId: exploreId as ExploreId, preserveCache: true }));
|
||||
dispatch(updateTime({ ...options, exploreId }));
|
||||
dispatch(runQueries({ exploreId: exploreId, preserveCache: true }));
|
||||
});
|
||||
} else {
|
||||
dispatch(updateTime({ ...options }));
|
||||
@ -55,7 +54,7 @@ export const updateTimeRange = (options: {
|
||||
};
|
||||
|
||||
export const updateTime = (config: {
|
||||
exploreId: ExploreId;
|
||||
exploreId: string;
|
||||
rawRange?: RawTimeRange;
|
||||
absoluteRange?: AbsoluteTimeRange;
|
||||
}): 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.
|
||||
* 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) => {
|
||||
const range = getState().explore.panes[exploreId]!.range.raw;
|
||||
|
||||
Object.keys(getState().explore.panes)
|
||||
.filter((key) => key !== exploreId)
|
||||
.forEach((exploreId) => {
|
||||
dispatch(updateTimeRange({ exploreId: exploreId as ExploreId, rawRange: range }));
|
||||
dispatch(updateTimeRange({ exploreId, rawRange: range }));
|
||||
});
|
||||
|
||||
const isTimeSynced = getState().explore.syncedTimes;
|
||||
@ -132,7 +131,7 @@ export function makeAbsoluteTime(): ThunkResult<void> {
|
||||
Object.entries(getState().explore.panes).forEach(([exploreId, exploreItemState]) => {
|
||||
const range = getTimeRange(timeZone, exploreItemState!.range.raw, fiscalYearStartMonth);
|
||||
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 { useDispatch } from 'app/types';
|
||||
|
||||
import { ExploreId } from '../../types';
|
||||
|
||||
import { setPausedStateAction, runQueries, clearLogs } from './state/query';
|
||||
import { changeRefreshInterval } from './state/time';
|
||||
|
||||
/**
|
||||
* 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 pause = useCallback(() => {
|
||||
@ -52,7 +50,7 @@ export function useLiveTailControls(exploreId: ExploreId) {
|
||||
}
|
||||
|
||||
type Props = {
|
||||
exploreId: ExploreId;
|
||||
exploreId: string;
|
||||
children: (controls: ReturnType<typeof useLiveTailControls>) => React.ReactElement;
|
||||
};
|
||||
|
||||
|
@ -13,7 +13,6 @@ import {
|
||||
TypeaheadOutput,
|
||||
withTheme2,
|
||||
} from '@grafana/ui';
|
||||
import { ExploreId } from 'app/types';
|
||||
|
||||
// Utils & Services
|
||||
// dom also includes Element polyfills
|
||||
@ -30,7 +29,7 @@ export interface CloudWatchLogsQueryFieldProps
|
||||
absoluteRange: AbsoluteTimeRange;
|
||||
onLabelsRefresh?: () => void;
|
||||
ExtraFieldElement?: ReactNode;
|
||||
exploreId: ExploreId;
|
||||
exploreId: string;
|
||||
query: CloudWatchLogsQuery;
|
||||
}
|
||||
const plugins: Array<Plugin<Editor>> = [
|
||||
|
@ -15,21 +15,13 @@ import {
|
||||
DataQueryResponse,
|
||||
ExplorePanelsState,
|
||||
SupplementaryQueryType,
|
||||
UrlQueryMap,
|
||||
} from '@grafana/data';
|
||||
import { RichHistorySearchFilters, RichHistorySettings } from 'app/core/utils/richHistoryTypes';
|
||||
|
||||
import { CorrelationData } from '../features/correlations/useCorrelations';
|
||||
|
||||
export enum ExploreId {
|
||||
left = 'left',
|
||||
right = 'right',
|
||||
}
|
||||
|
||||
export type ExploreQueryParams = {
|
||||
left?: string;
|
||||
right?: string;
|
||||
};
|
||||
|
||||
export type ExploreQueryParams = UrlQueryMap;
|
||||
/**
|
||||
* Global Explore state
|
||||
*/
|
||||
@ -39,13 +31,7 @@ export interface ExploreState {
|
||||
*/
|
||||
syncedTimes: boolean;
|
||||
|
||||
// This being optional wouldn't be needed with noUncheckedIndexedAccess set to true, but it cause more than 5k errors currently.
|
||||
// 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;
|
||||
};
|
||||
panes: Record<string, ExploreItemState | undefined>;
|
||||
|
||||
correlations?: CorrelationData[];
|
||||
|
||||
|
@ -20,12 +20,17 @@ export interface Props {
|
||||
* Wrapps component in redux store provider, Router and GrafanaContext
|
||||
*/
|
||||
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 (
|
||||
<Provider store={store}>
|
||||
<Router history={locationService.getHistory()}>
|
||||
<GrafanaContext.Provider value={grafanaContext}>{children}</GrafanaContext.Provider>
|
||||
<GrafanaContext.Provider value={context}>{children}</GrafanaContext.Provider>
|
||||
</Router>
|
||||
</Provider>
|
||||
);
|
||||
|
Loading…
Reference in New Issue
Block a user