From 2be2deddb86d73fccb858d2ef379f60cac000b0c Mon Sep 17 00:00:00 2001 From: David Kaltschmidt Date: Thu, 10 Jan 2019 14:24:31 +0100 Subject: [PATCH 1/9] WIP Explore redux migration --- public/app/core/utils/explore.ts | 89 +- public/app/features/explore/Explore.tsx | 928 ++++-------------- public/app/features/explore/state/actions.ts | 694 +++++++++++++ public/app/features/explore/state/reducers.ts | 412 ++++++++ public/app/store/configureStore.ts | 2 + public/app/types/explore.ts | 23 +- public/app/types/index.ts | 2 + 7 files changed, 1413 insertions(+), 737 deletions(-) create mode 100644 public/app/features/explore/state/actions.ts create mode 100644 public/app/features/explore/state/reducers.ts diff --git a/public/app/core/utils/explore.ts b/public/app/core/utils/explore.ts index f3273ffa16d..871a020ccc2 100644 --- a/public/app/core/utils/explore.ts +++ b/public/app/core/utils/explore.ts @@ -1,6 +1,7 @@ import _ from 'lodash'; -import { colors } from '@grafana/ui'; +import { colors, RawTimeRange, IntervalValues } from '@grafana/ui'; +import * as dateMath from 'app/core/utils/datemath'; import { renderUrl } from 'app/core/utils/url'; import kbn from 'app/core/utils/kbn'; import store from 'app/core/store'; @@ -8,9 +9,15 @@ import { parse as parseDate } from 'app/core/utils/datemath'; import TimeSeries from 'app/core/time_series2'; import TableModel, { mergeTablesIntoModel } from 'app/core/table_model'; -import { ExploreState, ExploreUrlState, HistoryItem, QueryTransaction } from 'app/types/explore'; +import { + ExploreUrlState, + HistoryItem, + QueryTransaction, + ResultType, + QueryIntervals, + QueryOptions, +} from 'app/types/explore'; import { DataQuery, DataSourceApi } from 'app/types/series'; -import { RawTimeRange, IntervalValues } from '@grafana/ui'; export const DEFAULT_RANGE = { from: 'now-6h', @@ -19,6 +26,8 @@ export const DEFAULT_RANGE = { const MAX_HISTORY_ITEMS = 100; +export const LAST_USED_DATASOURCE_KEY = 'grafana.explore.datasource'; + /** * Returns an Explore-URL that contains a panel's queries and the dashboard time range. * @@ -77,6 +86,62 @@ export async function getExploreUrl( return url; } +export function buildQueryTransaction( + query: DataQuery, + rowIndex: number, + resultType: ResultType, + queryOptions: QueryOptions, + range: RawTimeRange, + queryIntervals: QueryIntervals, + scanning: boolean +): QueryTransaction { + const { interval, intervalMs } = queryIntervals; + + const configuredQueries = [ + { + ...query, + ...queryOptions, + }, + ]; + + // Clone range for query request + // const queryRange: RawTimeRange = { ...range }; + // const { from, to, raw } = this.timeSrv.timeRange(); + // Most datasource is using `panelId + query.refId` for cancellation logic. + // Using `format` here because it relates to the view panel that the request is for. + // However, some datasources don't use `panelId + query.refId`, but only `panelId`. + // Therefore panel id has to be unique. + const panelId = `${queryOptions.format}-${query.key}`; + + const options = { + interval, + intervalMs, + panelId, + targets: configuredQueries, // Datasources rely on DataQueries being passed under the targets key. + range: { + from: dateMath.parse(range.from, false), + to: dateMath.parse(range.to, true), + raw: range, + }, + rangeRaw: range, + scopedVars: { + __interval: { text: interval, value: interval }, + __interval_ms: { text: intervalMs, value: intervalMs }, + }, + }; + + return { + options, + query, + resultType, + rowIndex, + scanning, + id: generateKey(), // reusing for unique ID + done: false, + latency: 0, + }; +} + const clearQueryKeys: ((query: DataQuery) => object) = ({ key, refId, ...rest }) => rest; export function parseUrlState(initial: string | undefined): ExploreUrlState { @@ -103,12 +168,12 @@ export function parseUrlState(initial: string | undefined): ExploreUrlState { return { datasource: null, queries: [], range: DEFAULT_RANGE }; } -export function serializeStateToUrlParam(state: ExploreState, compact?: boolean): string { - const urlState: ExploreUrlState = { - datasource: state.initialDatasource, - queries: state.initialQueries.map(clearQueryKeys), - range: state.range, - }; +export function serializeStateToUrlParam(urlState: ExploreUrlState, compact?: boolean): string { + // const urlState: ExploreUrlState = { + // datasource: state.initialDatasource, + // queries: state.initialQueries.map(clearQueryKeys), + // range: state.range, + // }; if (compact) { return JSON.stringify([urlState.range.from, urlState.range.to, urlState.datasource, ...urlState.queries]); } @@ -123,7 +188,7 @@ export function generateRefId(index = 0): string { return `${index + 1}`; } -export function generateQueryKeys(index = 0): { refId: string; key: string } { +export function generateEmptyQuery(index = 0): { refId: string; key: string } { return { refId: generateRefId(index), key: generateKey(index) }; } @@ -132,9 +197,9 @@ export function generateQueryKeys(index = 0): { refId: string; key: string } { */ export function ensureQueries(queries?: DataQuery[]): DataQuery[] { if (queries && typeof queries === 'object' && queries.length > 0) { - return queries.map((query, i) => ({ ...query, ...generateQueryKeys(i) })); + return queries.map((query, i) => ({ ...query, ...generateEmptyQuery(i) })); } - return [{ ...generateQueryKeys() }]; + return [{ ...generateEmptyQuery() }]; } /** diff --git a/public/app/features/explore/Explore.tsx b/public/app/features/explore/Explore.tsx index d4d645950c1..64e9c66ece5 100644 --- a/public/app/features/explore/Explore.tsx +++ b/public/app/features/explore/Explore.tsx @@ -1,35 +1,38 @@ import React from 'react'; import { hot } from 'react-hot-loader'; +import { connect } from 'react-redux'; import _ from 'lodash'; +import { withSize } from 'react-sizeme'; +import { RawTimeRange, TimeRange } from '@grafana/ui'; -import { DataSource } from 'app/types/datasources'; -import { - ExploreState, - ExploreUrlState, - QueryTransaction, - ResultType, - QueryHintGetter, - QueryHint, -} from 'app/types/explore'; -import { TimeRange } from '@grafana/ui'; +import { DataSourceSelectItem } from 'app/types/datasources'; +import { ExploreUrlState, HistoryItem, QueryTransaction, RangeScanner } from 'app/types/explore'; import { DataQuery } from 'app/types/series'; import store from 'app/core/store'; -import { - DEFAULT_RANGE, - calculateResultsFromQueryTransactions, - ensureQueries, - getIntervals, - generateKey, - generateQueryKeys, - hasNonEmptyQuery, - makeTimeSeriesList, - updateHistory, -} from 'app/core/utils/explore'; +import { LAST_USED_DATASOURCE_KEY, ensureQueries } from 'app/core/utils/explore'; import { DataSourcePicker } from 'app/core/components/Select/DataSourcePicker'; -import TableModel from 'app/core/table_model'; -import { DatasourceSrv } from 'app/features/plugins/datasource_srv'; import { Emitter } from 'app/core/utils/emitter'; -import * as dateMath from 'app/core/utils/datemath'; + +import { + addQueryRow, + changeDatasource, + changeQuery, + changeSize, + changeTime, + clickClear, + clickExample, + clickGraphButton, + clickLogsButton, + clickTableButton, + highlightLogsExpression, + initializeExplore, + modifyQueries, + removeQueryRow, + runQueries, + scanStart, + scanStop, +} from './state/actions'; +import { ExploreState } from './state/reducers'; import Panel from './Panel'; import QueryRows from './QueryRows'; @@ -39,17 +42,57 @@ import Table from './Table'; import ErrorBoundary from './ErrorBoundary'; import { Alert } from './Error'; import TimePicker, { parseTime } from './TimePicker'; - -const LAST_USED_DATASOURCE_KEY = 'grafana.explore.datasource'; +import { LogsModel } from 'app/core/logs_model'; +import TableModel from 'app/core/table_model'; interface ExploreProps { - datasourceSrv: DatasourceSrv; + StartPage?: any; + addQueryRow: typeof addQueryRow; + changeDatasource: typeof changeDatasource; + changeQuery: typeof changeQuery; + changeTime: typeof changeTime; + clickClear: typeof clickClear; + clickExample: typeof clickExample; + clickGraphButton: typeof clickGraphButton; + clickLogsButton: typeof clickLogsButton; + clickTableButton: typeof clickTableButton; + datasourceError: string; + datasourceInstance: any; + datasourceLoading: boolean | null; + datasourceMissing: boolean; + exploreDatasources: DataSourceSelectItem[]; + graphResult?: any[]; + highlightLogsExpression: typeof highlightLogsExpression; + history: HistoryItem[]; + initialDatasource?: string; + initialQueries: DataQuery[]; + initializeExplore: typeof initializeExplore; + logsHighlighterExpressions?: string[]; + logsResult?: LogsModel; + modifyQueries: typeof modifyQueries; onChangeSplit: (split: boolean, state?: ExploreState) => void; onSaveState: (key: string, state: ExploreState) => void; position: string; + queryTransactions: QueryTransaction[]; + removeQueryRow: typeof removeQueryRow; + range: RawTimeRange; + runQueries: typeof runQueries; + scanner?: RangeScanner; + scanning?: boolean; + scanRange?: RawTimeRange; + scanStart: typeof scanStart; + scanStop: typeof scanStop; split: boolean; splitState?: ExploreState; stateKey: string; + showingGraph: boolean; + showingLogs: boolean; + showingStartPage?: boolean; + showingTable: boolean; + supportsGraph: boolean | null; + supportsLogs: boolean | null; + supportsTable: boolean | null; + tableResult?: TableModel; urlState: ExploreUrlState; } @@ -89,23 +132,9 @@ interface ExploreProps { * The result viewers determine some of the query options sent to the datasource, e.g., * `format`, to indicate eventual transformations by the datasources' result transformers. */ -export class Explore extends React.PureComponent { +export class Explore extends React.PureComponent { el: any; exploreEvents: Emitter; - /** - * Set via URL or local storage - */ - initialDatasource: string; - /** - * Current query expressions of the rows including their modifications, used for running queries. - * Not kept in component state to prevent edit-render roundtrips. - */ - modifiedQueries: DataQuery[]; - /** - * Local ID cache to compare requested vs selected datasource - */ - requestedDatasourceId: string; - scanTimer: NodeJS.Timer; /** * Timepicker to control scanning */ @@ -113,166 +142,22 @@ export class Explore extends React.PureComponent { constructor(props) { super(props); - const splitState: ExploreState = props.splitState; - let initialQueries: DataQuery[]; - if (splitState) { - // Split state overrides everything - this.state = splitState; - initialQueries = splitState.initialQueries; - } else { - const { datasource, queries, range } = props.urlState as ExploreUrlState; - const initialDatasource = datasource || store.get(LAST_USED_DATASOURCE_KEY); - initialQueries = ensureQueries(queries); - const initialRange = { from: parseTime(range.from), to: parseTime(range.to) } || { ...DEFAULT_RANGE }; - // Millies step for helper bar charts - const initialGraphInterval = 15 * 1000; - this.state = { - datasource: null, - datasourceError: null, - datasourceLoading: null, - datasourceMissing: false, - exploreDatasources: [], - graphInterval: initialGraphInterval, - graphResult: [], - initialDatasource, - initialQueries, - history: [], - logsResult: null, - queryTransactions: [], - range: initialRange, - scanning: false, - showingGraph: true, - showingLogs: true, - showingStartPage: false, - showingTable: true, - supportsGraph: null, - supportsLogs: null, - supportsTable: null, - tableResult: new TableModel(), - }; - } - this.modifiedQueries = initialQueries.slice(); this.exploreEvents = new Emitter(); this.timepickerRef = React.createRef(); } async componentDidMount() { - const { datasourceSrv } = this.props; - const { initialDatasource } = this.state; - if (!datasourceSrv) { - throw new Error('No datasource service passed as props.'); - } - - const datasources = datasourceSrv.getExternal(); - const exploreDatasources = datasources.map(ds => ({ - value: ds.name, - name: ds.name, - meta: ds.meta, - })); - - if (datasources.length > 0) { - this.setState({ datasourceLoading: true, exploreDatasources }); - // Priority for datasource preselection: URL, localstorage, default datasource - let datasource; - if (initialDatasource) { - datasource = await datasourceSrv.get(initialDatasource); - } else { - datasource = await datasourceSrv.get(); - } - await this.setDatasource(datasource); - } else { - this.setState({ datasourceMissing: true }); - } + // Load URL state and parse range + const { datasource, queries, range } = this.props.urlState as ExploreUrlState; + const initialDatasource = datasource || store.get(LAST_USED_DATASOURCE_KEY); + const initialQueries: DataQuery[] = ensureQueries(queries); + const initialRange = { from: parseTime(range.from), to: parseTime(range.to) }; + const width = this.el ? this.el.offsetWidth : 0; + this.props.initializeExplore(initialDatasource, initialQueries, initialRange, width, this.exploreEvents); } componentWillUnmount() { this.exploreEvents.removeAllListeners(); - clearTimeout(this.scanTimer); - } - - async setDatasource(datasource: any, origin?: DataSource) { - const { initialQueries, range } = this.state; - - const supportsGraph = datasource.meta.metrics; - const supportsLogs = datasource.meta.logs; - const supportsTable = datasource.meta.tables; - const datasourceId = datasource.meta.id; - let datasourceError = null; - - // Keep ID to track selection - this.requestedDatasourceId = datasourceId; - - try { - const testResult = await datasource.testDatasource(); - datasourceError = testResult.status === 'success' ? null : testResult.message; - } catch (error) { - datasourceError = (error && error.statusText) || 'Network error'; - } - - if (datasourceId !== this.requestedDatasourceId) { - // User already changed datasource again, discard results - return; - } - - const historyKey = `grafana.explore.history.${datasourceId}`; - const history = store.getObject(historyKey, []); - - if (datasource.init) { - datasource.init(); - } - - // Check if queries can be imported from previously selected datasource - let modifiedQueries = this.modifiedQueries; - if (origin) { - if (origin.meta.id === datasource.meta.id) { - // Keep same queries if same type of datasource - modifiedQueries = [...this.modifiedQueries]; - } else if (datasource.importQueries) { - // Datasource-specific importers - modifiedQueries = await datasource.importQueries(this.modifiedQueries, origin.meta); - } else { - // Default is blank queries - modifiedQueries = ensureQueries(); - } - } - - // Reset edit state with new queries - const nextQueries = initialQueries.map((q, i) => ({ - ...modifiedQueries[i], - ...generateQueryKeys(i), - })); - this.modifiedQueries = modifiedQueries; - - // Custom components - const StartPage = datasource.pluginExports.ExploreStartPage; - - // Calculate graph bucketing interval - const graphInterval = getIntervals(range, datasource, this.el ? this.el.offsetWidth : 0).intervalMs; - - this.setState( - { - StartPage, - datasource, - datasourceError, - graphInterval, - history, - supportsGraph, - supportsLogs, - supportsTable, - datasourceLoading: false, - initialDatasource: datasource.name, - initialQueries: nextQueries, - logsHighlighterExpressions: undefined, - showingStartPage: Boolean(StartPage), - }, - () => { - if (datasourceError === null) { - // Save last-used datasource - store.set(LAST_USED_DATASOURCE_KEY, datasource.name); - this.onSubmit(); - } - } - ); } getRef = el => { @@ -280,106 +165,32 @@ export class Explore extends React.PureComponent { }; onAddQueryRow = index => { - // Local cache - this.modifiedQueries[index + 1] = { ...generateQueryKeys(index + 1) }; - - this.setState(state => { - const { initialQueries, queryTransactions } = state; - - const nextQueries = [ - ...initialQueries.slice(0, index + 1), - { ...this.modifiedQueries[index + 1] }, - ...initialQueries.slice(index + 1), - ]; - - // Ongoing transactions need to update their row indices - const nextQueryTransactions = queryTransactions.map(qt => { - if (qt.rowIndex > index) { - return { - ...qt, - rowIndex: qt.rowIndex + 1, - }; - } - return qt; - }); - - return { - initialQueries: nextQueries, - logsHighlighterExpressions: undefined, - queryTransactions: nextQueryTransactions, - }; - }); + this.props.addQueryRow(index); }; onChangeDatasource = async option => { - const origin = this.state.datasource; - this.setState({ - datasource: null, - datasourceError: null, - datasourceLoading: true, - queryTransactions: [], - }); - const datasourceName = option.value; - const datasource = await this.props.datasourceSrv.get(datasourceName); - this.setDatasource(datasource as any, origin); + this.props.changeDatasource(option.value); }; - onChangeQuery = (value: DataQuery, index: number, override?: boolean) => { - // Null value means reset - if (value === null) { - value = { ...generateQueryKeys(index) }; - } + onChangeQuery = (query: DataQuery, index: number, override?: boolean) => { + const { changeQuery, datasourceInstance } = this.props; - // Keep current value in local cache - this.modifiedQueries[index] = value; - - if (override) { - this.setState(state => { - // Replace query row by injecting new key - const { initialQueries, queryTransactions } = state; - const query: DataQuery = { - ...value, - ...generateQueryKeys(index), - }; - const nextQueries = [...initialQueries]; - nextQueries[index] = query; - this.modifiedQueries = [...nextQueries]; - - // Discard ongoing transaction related to row query - const nextQueryTransactions = queryTransactions.filter(qt => qt.rowIndex !== index); - - return { - initialQueries: nextQueries, - queryTransactions: nextQueryTransactions, - }; - }, this.onSubmit); - } else if (this.state.datasource.getHighlighterExpression && this.modifiedQueries.length === 1) { - // Live preview of log search matches. Can only work on single row query for now - this.updateLogsHighlights(value); + changeQuery(query, index, override); + if (query && !override && datasourceInstance.getHighlighterExpression && index === 0) { + // Live preview of log search matches. Only use on first row for now + this.updateLogsHighlights(query); } }; - onChangeTime = (nextRange: TimeRange, scanning?: boolean) => { - const range: TimeRange = { - ...nextRange, - }; - if (this.state.scanning && !scanning) { + onChangeTime = (range: TimeRange, changedByScanner?: boolean) => { + if (this.props.scanning && !changedByScanner) { this.onStopScanning(); } - this.setState({ range, scanning }, () => this.onSubmit()); + this.props.changeTime(range); }; onClickClear = () => { - this.onStopScanning(); - this.modifiedQueries = ensureQueries(); - this.setState( - prevState => ({ - initialQueries: [...this.modifiedQueries], - queryTransactions: [], - showingStartPage: Boolean(prevState.StartPage), - }), - this.saveState - ); + this.props.clickClear(); }; onClickCloseSplit = () => { @@ -390,82 +201,28 @@ export class Explore extends React.PureComponent { }; onClickGraphButton = () => { - this.setState( - state => { - const showingGraph = !state.showingGraph; - let nextQueryTransactions = state.queryTransactions; - if (!showingGraph) { - // Discard transactions related to Graph query - nextQueryTransactions = state.queryTransactions.filter(qt => qt.resultType !== 'Graph'); - } - return { queryTransactions: nextQueryTransactions, showingGraph }; - }, - () => { - if (this.state.showingGraph) { - this.onSubmit(); - } - } - ); + this.props.clickGraphButton(); }; onClickLogsButton = () => { - this.setState( - state => { - const showingLogs = !state.showingLogs; - let nextQueryTransactions = state.queryTransactions; - if (!showingLogs) { - // Discard transactions related to Logs query - nextQueryTransactions = state.queryTransactions.filter(qt => qt.resultType !== 'Logs'); - } - return { queryTransactions: nextQueryTransactions, showingLogs }; - }, - () => { - if (this.state.showingLogs) { - this.onSubmit(); - } - } - ); + this.props.clickLogsButton(); }; // Use this in help pages to set page to a single query onClickExample = (query: DataQuery) => { - const nextQueries = [{ ...query, ...generateQueryKeys() }]; - this.modifiedQueries = [...nextQueries]; - this.setState({ initialQueries: nextQueries }, this.onSubmit); + this.props.clickExample(query); }; onClickSplit = () => { const { onChangeSplit } = this.props; if (onChangeSplit) { - const state = this.cloneState(); - onChangeSplit(true, state); + // const state = this.cloneState(); + // onChangeSplit(true, state); } }; onClickTableButton = () => { - this.setState( - state => { - const showingTable = !state.showingTable; - if (showingTable) { - return { showingTable, queryTransactions: state.queryTransactions }; - } - - // Toggle off needs discarding of table queries - const nextQueryTransactions = state.queryTransactions.filter(qt => qt.resultType !== 'Table'); - const results = calculateResultsFromQueryTransactions( - nextQueryTransactions, - state.datasource, - state.graphInterval - ); - - return { ...results, queryTransactions: nextQueryTransactions, showingTable }; - }, - () => { - if (this.state.showingTable) { - this.onSubmit(); - } - } - ); + this.props.clickTableButton(); }; onClickLabel = (key: string, value: string) => { @@ -473,404 +230,62 @@ export class Explore extends React.PureComponent { }; onModifyQueries = (action, index?: number) => { - const { datasource } = this.state; - if (datasource && datasource.modifyQuery) { - const preventSubmit = action.preventSubmit; - this.setState( - state => { - const { initialQueries, queryTransactions } = state; - let nextQueries: DataQuery[]; - let nextQueryTransactions; - if (index === undefined) { - // Modify all queries - nextQueries = initialQueries.map((query, i) => ({ - ...datasource.modifyQuery(this.modifiedQueries[i], action), - ...generateQueryKeys(i), - })); - // Discard all ongoing transactions - nextQueryTransactions = []; - } else { - // Modify query only at index - nextQueries = initialQueries.map((query, i) => { - // Synchronize all queries with local query cache to ensure consistency - // TODO still needed? - return i === index - ? { - ...datasource.modifyQuery(this.modifiedQueries[i], action), - ...generateQueryKeys(i), - } - : query; - }); - nextQueryTransactions = queryTransactions - // Consume the hint corresponding to the action - .map(qt => { - if (qt.hints != null && qt.rowIndex === index) { - qt.hints = qt.hints.filter(hint => hint.fix.action !== action); - } - return qt; - }) - // Preserve previous row query transaction to keep results visible if next query is incomplete - .filter(qt => preventSubmit || qt.rowIndex !== index); - } - this.modifiedQueries = [...nextQueries]; - return { - initialQueries: nextQueries, - queryTransactions: nextQueryTransactions, - }; - }, - // Accepting certain fixes do not result in a well-formed query which should not be submitted - !preventSubmit ? () => this.onSubmit() : null - ); + const { datasourceInstance } = this.props; + if (datasourceInstance && datasourceInstance.modifyQuery) { + const modifier = (queries: DataQuery, action: any) => datasourceInstance.modifyQuery(queries, action); + this.props.modifyQueries(action, index, modifier); } }; onRemoveQueryRow = index => { - // Remove from local cache - this.modifiedQueries = [...this.modifiedQueries.slice(0, index), ...this.modifiedQueries.slice(index + 1)]; - - this.setState( - state => { - const { initialQueries, queryTransactions } = state; - if (initialQueries.length <= 1) { - return null; - } - // Remove row from react state - const nextQueries = [...initialQueries.slice(0, index), ...initialQueries.slice(index + 1)]; - - // Discard transactions related to row query - const nextQueryTransactions = queryTransactions.filter(qt => qt.rowIndex !== index); - const results = calculateResultsFromQueryTransactions( - nextQueryTransactions, - state.datasource, - state.graphInterval - ); - - return { - ...results, - initialQueries: nextQueries, - logsHighlighterExpressions: undefined, - queryTransactions: nextQueryTransactions, - }; - }, - () => this.onSubmit() - ); + this.props.removeQueryRow(index); }; onStartScanning = () => { - this.setState({ scanning: true }, this.scanPreviousRange); + // Scanner will trigger a query + const scanner = this.scanPreviousRange; + this.props.scanStart(scanner); }; - scanPreviousRange = () => { - const scanRange = this.timepickerRef.current.move(-1, true); - this.setState({ scanRange }); + scanPreviousRange = (): RawTimeRange => { + // Calling move() on the timepicker will trigger this.onChangeTime() + return this.timepickerRef.current.move(-1, true); }; onStopScanning = () => { - clearTimeout(this.scanTimer); - this.setState(state => { - const { queryTransactions } = state; - const nextQueryTransactions = queryTransactions.filter(qt => qt.scanning && !qt.done); - return { queryTransactions: nextQueryTransactions, scanning: false, scanRange: undefined }; - }); + this.props.scanStop(); }; onSubmit = () => { - const { showingLogs, showingGraph, showingTable, supportsGraph, supportsLogs, supportsTable } = this.state; - // Keep table queries first since they need to return quickly - if (showingTable && supportsTable) { - this.runQueries( - 'Table', - { - format: 'table', - instant: true, - valueWithRefId: true, - }, - data => data[0] - ); - } - if (showingGraph && supportsGraph) { - this.runQueries( - 'Graph', - { - format: 'time_series', - instant: false, - }, - makeTimeSeriesList - ); - } - if (showingLogs && supportsLogs) { - this.runQueries('Logs', { format: 'logs' }); - } - this.saveState(); + this.props.runQueries(); }; - buildQueryOptions(query: DataQuery, queryOptions: { format: string; hinting?: boolean; instant?: boolean }) { - const { datasource, range } = this.state; - const { interval, intervalMs } = getIntervals(range, datasource, this.el.offsetWidth); - - const configuredQueries = [ - { - ...query, - ...queryOptions, - }, - ]; - - // Clone range for query request - // const queryRange: RawTimeRange = { ...range }; - // const { from, to, raw } = this.timeSrv.timeRange(); - // Most datasource is using `panelId + query.refId` for cancellation logic. - // Using `format` here because it relates to the view panel that the request is for. - // However, some datasources don't use `panelId + query.refId`, but only `panelId`. - // Therefore panel id has to be unique. - const panelId = `${queryOptions.format}-${query.key}`; - - return { - interval, - intervalMs, - panelId, - targets: configuredQueries, // Datasources rely on DataQueries being passed under the targets key. - range: { - from: dateMath.parse(range.from, false), - to: dateMath.parse(range.to, true), - raw: range, - }, - rangeRaw: range, - scopedVars: { - __interval: { text: interval, value: interval }, - __interval_ms: { text: intervalMs, value: intervalMs }, - }, - }; - } - - startQueryTransaction(query: DataQuery, rowIndex: number, resultType: ResultType, options: any): QueryTransaction { - const queryOptions = this.buildQueryOptions(query, options); - const transaction: QueryTransaction = { - query, - resultType, - rowIndex, - id: generateKey(), // reusing for unique ID - done: false, - latency: 0, - options: queryOptions, - scanning: this.state.scanning, - }; - - // Using updater style because we might be modifying queryTransactions in quick succession - this.setState(state => { - const { queryTransactions } = state; - // Discarding existing transactions of same type - const remainingTransactions = queryTransactions.filter( - qt => !(qt.resultType === resultType && qt.rowIndex === rowIndex) - ); - - // Append new transaction - const nextQueryTransactions = [...remainingTransactions, transaction]; - - const results = calculateResultsFromQueryTransactions( - nextQueryTransactions, - state.datasource, - state.graphInterval - ); - - return { - ...results, - queryTransactions: nextQueryTransactions, - showingStartPage: false, - graphInterval: queryOptions.intervalMs, - }; - }); - - return transaction; - } - - completeQueryTransaction( - transactionId: string, - result: any, - latency: number, - queries: DataQuery[], - datasourceId: string - ) { - const { datasource } = this.state; - if (datasource.meta.id !== datasourceId) { - // Navigated away, queries did not matter - return; + updateLogsHighlights = _.debounce((value: DataQuery) => { + const { datasourceInstance } = this.props; + if (datasourceInstance.getHighlighterExpression) { + const expressions = [datasourceInstance.getHighlighterExpression(value)]; + this.props.highlightLogsExpression(expressions); } - - this.setState(state => { - const { history, queryTransactions } = state; - let { scanning } = state; - - // Transaction might have been discarded - const transaction = queryTransactions.find(qt => qt.id === transactionId); - if (!transaction) { - return null; - } - - // Get query hints - let hints: QueryHint[]; - if (datasource.getQueryHints as QueryHintGetter) { - hints = datasource.getQueryHints(transaction.query, result); - } - - // Mark transactions as complete - const nextQueryTransactions = queryTransactions.map(qt => { - if (qt.id === transactionId) { - return { - ...qt, - hints, - latency, - result, - done: true, - }; - } - return qt; - }); - - const results = calculateResultsFromQueryTransactions( - nextQueryTransactions, - state.datasource, - state.graphInterval - ); - - const nextHistory = updateHistory(history, datasourceId, queries); - - // Keep scanning for results if this was the last scanning transaction - if (scanning) { - if (_.size(result) === 0) { - const other = nextQueryTransactions.find(qt => qt.scanning && !qt.done); - if (!other) { - this.scanTimer = setTimeout(this.scanPreviousRange, 1000); - } - } else { - // We can stop scanning if we have a result - scanning = false; - } - } - - return { - ...results, - scanning, - history: nextHistory, - queryTransactions: nextQueryTransactions, - }; - }); - } - - failQueryTransaction(transactionId: string, response: any, datasourceId: string) { - const { datasource } = this.state; - if (datasource.meta.id !== datasourceId || response.cancelled) { - // Navigated away, queries did not matter - return; - } - - console.error(response); - - let error: string | JSX.Element; - if (response.data) { - if (typeof response.data === 'string') { - error = response.data; - } else if (response.data.error) { - error = response.data.error; - if (response.data.response) { - error = ( - <> - {response.data.error} -
{response.data.response}
- - ); - } - } else { - throw new Error('Could not handle error response'); - } - } else if (response.message) { - error = response.message; - } else if (typeof response === 'string') { - error = response; - } else { - error = 'Unknown error during query transaction. Please check JS console logs.'; - } - - this.setState(state => { - // Transaction might have been discarded - if (!state.queryTransactions.find(qt => qt.id === transactionId)) { - return null; - } - - // Mark transactions as complete - const nextQueryTransactions = state.queryTransactions.map(qt => { - if (qt.id === transactionId) { - return { - ...qt, - error, - done: true, - }; - } - return qt; - }); - - return { - queryTransactions: nextQueryTransactions, - }; - }); - } - - async runQueries(resultType: ResultType, queryOptions: any, resultGetter?: any) { - const queries = [...this.modifiedQueries]; - if (!hasNonEmptyQuery(queries)) { - this.setState({ - queryTransactions: [], - }); - return; - } - const { datasource } = this.state; - const datasourceId = datasource.meta.id; - // Run all queries concurrentlyso - queries.forEach(async (query, rowIndex) => { - const transaction = this.startQueryTransaction(query, rowIndex, resultType, queryOptions); - try { - const now = Date.now(); - const res = await datasource.query(transaction.options); - this.exploreEvents.emit('data-received', res.data || []); - const latency = Date.now() - now; - const results = resultGetter ? resultGetter(res.data) : res.data; - this.completeQueryTransaction(transaction.id, results, latency, queries, datasourceId); - } catch (response) { - this.exploreEvents.emit('data-error', response); - this.failQueryTransaction(transaction.id, response, datasourceId); - } - }); - } - - updateLogsHighlights = _.debounce((value: DataQuery, index: number) => { - this.setState(state => { - const { datasource } = state; - if (datasource.getHighlighterExpression) { - const logsHighlighterExpressions = [state.datasource.getHighlighterExpression(value)]; - return { logsHighlighterExpressions }; - } - return null; - }); }, 500); - cloneState(): ExploreState { - // Copy state, but copy queries including modifications - return { - ...this.state, - queryTransactions: [], - initialQueries: [...this.modifiedQueries], - }; - } + // cloneState(): ExploreState { + // // Copy state, but copy queries including modifications + // return { + // ...this.state, + // queryTransactions: [], + // initialQueries: [...this.modifiedQueries], + // }; + // } - saveState = () => { - const { stateKey, onSaveState } = this.props; - onSaveState(stateKey, this.cloneState()); - }; + // saveState = () => { + // const { stateKey, onSaveState } = this.props; + // onSaveState(stateKey, this.cloneState()); + // }; render() { - const { position, split } = this.props; const { StartPage, - datasource, + datasourceInstance, datasourceError, datasourceLoading, datasourceMissing, @@ -881,6 +296,7 @@ export class Explore extends React.PureComponent { logsHighlighterExpressions, logsResult, queryTransactions, + position, range, scanning, scanRange, @@ -888,14 +304,17 @@ export class Explore extends React.PureComponent { showingLogs, showingStartPage, showingTable, + split, supportsGraph, supportsLogs, supportsTable, tableResult, - } = this.state; + } = this.props; const graphHeight = showingGraph && showingTable ? '200px' : '400px'; const exploreClass = split ? 'explore explore-split' : 'explore'; - const selectedDatasource = datasource ? exploreDatasources.find(d => d.name === datasource.name) : undefined; + const selectedDatasource = datasourceInstance + ? exploreDatasources.find(d => d.name === datasourceInstance.name) + : undefined; const graphLoading = queryTransactions.some(qt => qt.resultType === 'Graph' && !qt.done); const tableLoading = queryTransactions.some(qt => qt.resultType === 'Table' && !qt.done); const logsLoading = queryTransactions.some(qt => qt.resultType === 'Logs' && !qt.done); @@ -959,10 +378,10 @@ export class Explore extends React.PureComponent { )} - {datasource && !datasourceError ? ( + {datasourceInstance && !datasourceError ? (
{ } } -export default hot(module)(Explore); +function mapStateToProps({ explore }) { + const { + StartPage, + datasourceError, + datasourceInstance, + datasourceLoading, + datasourceMissing, + exploreDatasources, + graphResult, + initialDatasource, + initialQueries, + history, + logsHighlighterExpressions, + logsResult, + queryTransactions, + range, + scanning, + scanRange, + showingGraph, + showingLogs, + showingStartPage, + showingTable, + supportsGraph, + supportsLogs, + supportsTable, + tableResult, + } = explore as ExploreState; + return { + StartPage, + datasourceError, + datasourceInstance, + datasourceLoading, + datasourceMissing, + exploreDatasources, + graphResult, + initialDatasource, + initialQueries, + history, + logsHighlighterExpressions, + logsResult, + queryTransactions, + range, + scanning, + scanRange, + showingGraph, + showingLogs, + showingStartPage, + showingTable, + supportsGraph, + supportsLogs, + supportsTable, + tableResult, + }; +} + +const mapDispatchToProps = { + addQueryRow, + changeDatasource, + changeQuery, + changeTime, + clickClear, + clickExample, + clickGraphButton, + clickLogsButton, + clickTableButton, + highlightLogsExpression, + initializeExplore, + modifyQueries, + onSize: changeSize, // used by withSize HOC + removeQueryRow, + runQueries, + scanStart, + scanStop, +}; + +export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(withSize()(Explore))); diff --git a/public/app/features/explore/state/actions.ts b/public/app/features/explore/state/actions.ts new file mode 100644 index 00000000000..d70a458059e --- /dev/null +++ b/public/app/features/explore/state/actions.ts @@ -0,0 +1,694 @@ +import _ from 'lodash'; +import { ThunkAction } from 'redux-thunk'; +import { RawTimeRange, TimeRange } from '@grafana/ui'; + +import { + LAST_USED_DATASOURCE_KEY, + ensureQueries, + generateEmptyQuery, + hasNonEmptyQuery, + makeTimeSeriesList, + updateHistory, + buildQueryTransaction, +} from 'app/core/utils/explore'; + +import store from 'app/core/store'; +import { DataSourceSelectItem } from 'app/types/datasources'; +import { DataQuery, StoreState } from 'app/types'; +import { getDatasourceSrv } from 'app/features/plugins/datasource_srv'; +import { + HistoryItem, + RangeScanner, + ResultType, + QueryOptions, + QueryTransaction, + QueryHint, + QueryHintGetter, +} from 'app/types/explore'; +import { Emitter } from 'app/core/core'; +import { dispatch } from 'rxjs/internal/observable/pairs'; + +export enum ActionTypes { + AddQueryRow = 'ADD_QUERY_ROW', + ChangeDatasource = 'CHANGE_DATASOURCE', + ChangeQuery = 'CHANGE_QUERY', + ChangeSize = 'CHANGE_SIZE', + ChangeTime = 'CHANGE_TIME', + ClickClear = 'CLICK_CLEAR', + ClickExample = 'CLICK_EXAMPLE', + ClickGraphButton = 'CLICK_GRAPH_BUTTON', + ClickLogsButton = 'CLICK_LOGS_BUTTON', + ClickTableButton = 'CLICK_TABLE_BUTTON', + HighlightLogsExpression = 'HIGHLIGHT_LOGS_EXPRESSION', + InitializeExplore = 'INITIALIZE_EXPLORE', + LoadDatasourceFailure = 'LOAD_DATASOURCE_FAILURE', + LoadDatasourceMissing = 'LOAD_DATASOURCE_MISSING', + LoadDatasourcePending = 'LOAD_DATASOURCE_PENDING', + LoadDatasourceSuccess = 'LOAD_DATASOURCE_SUCCESS', + ModifyQueries = 'MODIFY_QUERIES', + QueryTransactionFailure = 'QUERY_TRANSACTION_FAILURE', + QueryTransactionStart = 'QUERY_TRANSACTION_START', + QueryTransactionSuccess = 'QUERY_TRANSACTION_SUCCESS', + RemoveQueryRow = 'REMOVE_QUERY_ROW', + RunQueries = 'RUN_QUERIES', + RunQueriesEmpty = 'RUN_QUERIES', + ScanRange = 'SCAN_RANGE', + ScanStart = 'SCAN_START', + ScanStop = 'SCAN_STOP', +} + +export interface AddQueryRowAction { + type: ActionTypes.AddQueryRow; + index: number; + query: DataQuery; +} + +export interface ChangeQueryAction { + type: ActionTypes.ChangeQuery; + query: DataQuery; + index: number; + override: boolean; +} + +export interface ChangeSizeAction { + type: ActionTypes.ChangeSize; + width: number; + height: number; +} + +export interface ChangeTimeAction { + type: ActionTypes.ChangeTime; + range: TimeRange; +} + +export interface ClickClearAction { + type: ActionTypes.ClickClear; +} + +export interface ClickExampleAction { + type: ActionTypes.ClickExample; + query: DataQuery; +} + +export interface ClickGraphButtonAction { + type: ActionTypes.ClickGraphButton; +} + +export interface ClickLogsButtonAction { + type: ActionTypes.ClickLogsButton; +} + +export interface ClickTableButtonAction { + type: ActionTypes.ClickTableButton; +} + +export interface InitializeExploreAction { + type: ActionTypes.InitializeExplore; + containerWidth: number; + datasource: string; + eventBridge: Emitter; + exploreDatasources: DataSourceSelectItem[]; + queries: DataQuery[]; + range: RawTimeRange; +} + +export interface HighlightLogsExpressionAction { + type: ActionTypes.HighlightLogsExpression; + expressions: string[]; +} + +export interface LoadDatasourceFailureAction { + type: ActionTypes.LoadDatasourceFailure; + error: string; +} + +export interface LoadDatasourcePendingAction { + type: ActionTypes.LoadDatasourcePending; + datasourceId: number; +} + +export interface LoadDatasourceMissingAction { + type: ActionTypes.LoadDatasourceMissing; +} + +export interface LoadDatasourceSuccessAction { + type: ActionTypes.LoadDatasourceSuccess; + StartPage?: any; + datasourceInstance: any; + history: HistoryItem[]; + initialDatasource: string; + initialQueries: DataQuery[]; + logsHighlighterExpressions?: any[]; + showingStartPage: boolean; + supportsGraph: boolean; + supportsLogs: boolean; + supportsTable: boolean; +} + +export interface ModifyQueriesAction { + type: ActionTypes.ModifyQueries; + modification: any; + index: number; + modifier: (queries: DataQuery[], modification: any) => DataQuery[]; +} + +export interface QueryTransactionFailureAction { + type: ActionTypes.QueryTransactionFailure; + queryTransactions: QueryTransaction[]; +} + +export interface QueryTransactionStartAction { + type: ActionTypes.QueryTransactionStart; + resultType: ResultType; + rowIndex: number; + transaction: QueryTransaction; +} + +export interface QueryTransactionSuccessAction { + type: ActionTypes.QueryTransactionSuccess; + history: HistoryItem[]; + queryTransactions: QueryTransaction[]; +} + +export interface RemoveQueryRowAction { + type: ActionTypes.RemoveQueryRow; + index: number; +} + +export interface ScanStartAction { + type: ActionTypes.ScanStart; + scanner: RangeScanner; +} + +export interface ScanRangeAction { + type: ActionTypes.ScanRange; + range: RawTimeRange; +} + +export interface ScanStopAction { + type: ActionTypes.ScanStop; +} + +export type Action = + | AddQueryRowAction + | ChangeQueryAction + | ChangeSizeAction + | ChangeTimeAction + | ClickClearAction + | ClickExampleAction + | ClickGraphButtonAction + | ClickLogsButtonAction + | ClickTableButtonAction + | HighlightLogsExpressionAction + | InitializeExploreAction + | LoadDatasourceFailureAction + | LoadDatasourceMissingAction + | LoadDatasourcePendingAction + | LoadDatasourceSuccessAction + | ModifyQueriesAction + | QueryTransactionFailureAction + | QueryTransactionStartAction + | QueryTransactionSuccessAction + | RemoveQueryRowAction + | ScanRangeAction + | ScanStartAction + | ScanStopAction; +type ThunkResult = ThunkAction; + +export function addQueryRow(index: number): AddQueryRowAction { + const query = generateEmptyQuery(index + 1); + return { type: ActionTypes.AddQueryRow, index, query }; +} + +export function changeDatasource(datasource: string): ThunkResult { + return async dispatch => { + const instance = await getDatasourceSrv().get(datasource); + dispatch(loadDatasource(instance)); + }; +} + +export function changeQuery(query: DataQuery, index: number, override: boolean): ThunkResult { + return dispatch => { + // Null query means reset + if (query === null) { + query = { ...generateEmptyQuery(index) }; + } + + dispatch({ type: ActionTypes.ChangeQuery, query, index, override }); + if (override) { + dispatch(runQueries()); + } + }; +} + +export function changeSize({ height, width }: { height: number; width: number }): ChangeSizeAction { + return { type: ActionTypes.ChangeSize, height, width }; +} + +export function changeTime(range: TimeRange): ThunkResult { + return dispatch => { + dispatch({ type: ActionTypes.ChangeTime, range }); + dispatch(runQueries()); + }; +} + +export function clickExample(rawQuery: DataQuery): ThunkResult { + return dispatch => { + const query = { ...rawQuery, ...generateEmptyQuery() }; + dispatch({ + type: ActionTypes.ClickExample, + query, + }); + dispatch(runQueries()); + }; +} + +export function clickClear(): ThunkResult { + return dispatch => { + dispatch(scanStop()); + dispatch({ type: ActionTypes.ClickClear }); + // TODO save state + }; +} + +export function clickGraphButton(): ThunkResult { + return (dispatch, getState) => { + dispatch({ type: ActionTypes.ClickGraphButton }); + if (getState().explore.showingGraph) { + dispatch(runQueries()); + } + }; +} + +export function clickLogsButton(): ThunkResult { + return (dispatch, getState) => { + dispatch({ type: ActionTypes.ClickLogsButton }); + if (getState().explore.showingLogs) { + dispatch(runQueries()); + } + }; +} + +export function clickTableButton(): ThunkResult { + return (dispatch, getState) => { + dispatch({ type: ActionTypes.ClickTableButton }); + if (getState().explore.showingTable) { + dispatch(runQueries()); + } + }; +} + +export function highlightLogsExpression(expressions: string[]): HighlightLogsExpressionAction { + return { type: ActionTypes.HighlightLogsExpression, expressions }; +} + +export function initializeExplore( + datasource: string, + queries: DataQuery[], + range: RawTimeRange, + containerWidth: number, + eventBridge: Emitter +): ThunkResult { + return async dispatch => { + const exploreDatasources: DataSourceSelectItem[] = getDatasourceSrv() + .getExternal() + .map(ds => ({ + value: ds.name, + name: ds.name, + meta: ds.meta, + })); + + dispatch({ + type: ActionTypes.InitializeExplore, + containerWidth, + datasource, + eventBridge, + exploreDatasources, + queries, + range, + }); + + if (exploreDatasources.length > 1) { + let instance; + if (datasource) { + instance = await getDatasourceSrv().get(datasource); + } else { + instance = await getDatasourceSrv().get(); + } + dispatch(loadDatasource(instance)); + } else { + dispatch(loadDatasourceMissing); + } + }; +} + +export const loadDatasourceFailure = (error: string): LoadDatasourceFailureAction => ({ + type: ActionTypes.LoadDatasourceFailure, + error, +}); + +export const loadDatasourceMissing: LoadDatasourceMissingAction = { type: ActionTypes.LoadDatasourceMissing }; + +export const loadDatasourcePending = (datasourceId: number): LoadDatasourcePendingAction => ({ + type: ActionTypes.LoadDatasourcePending, + datasourceId, +}); + +export const loadDatasourceSuccess = (instance: any, queries: DataQuery[]): LoadDatasourceSuccessAction => { + // Capabilities + const supportsGraph = instance.meta.metrics; + const supportsLogs = instance.meta.logs; + const supportsTable = instance.meta.tables; + // Custom components + const StartPage = instance.pluginExports.ExploreStartPage; + + const historyKey = `grafana.explore.history.${instance.meta.id}`; + const history = store.getObject(historyKey, []); + // Save last-used datasource + store.set(LAST_USED_DATASOURCE_KEY, instance.name); + + return { + type: ActionTypes.LoadDatasourceSuccess, + StartPage, + datasourceInstance: instance, + history, + initialDatasource: instance.name, + initialQueries: queries, + showingStartPage: Boolean(StartPage), + supportsGraph, + supportsLogs, + supportsTable, + }; +}; + +export function loadDatasource(instance: any): ThunkResult { + return async (dispatch, getState) => { + const datasourceId = instance.meta.id; + + // Keep ID to track selection + dispatch(loadDatasourcePending(datasourceId)); + + let datasourceError = null; + try { + const testResult = await instance.testDatasource(); + datasourceError = testResult.status === 'success' ? null : testResult.message; + } catch (error) { + datasourceError = (error && error.statusText) || 'Network error'; + } + if (datasourceError) { + dispatch(loadDatasourceFailure(datasourceError)); + return; + } + + if (datasourceId !== getState().explore.requestedDatasourceId) { + // User already changed datasource again, discard results + return; + } + + if (instance.init) { + instance.init(); + } + + // Check if queries can be imported from previously selected datasource + const queries = getState().explore.modifiedQueries; + let importedQueries = queries; + const origin = getState().explore.datasourceInstance; + if (origin) { + if (origin.meta.id === instance.meta.id) { + // Keep same queries if same type of datasource + importedQueries = [...queries]; + } else if (instance.importQueries) { + // Datasource-specific importers + importedQueries = await instance.importQueries(queries, origin.meta); + } else { + // Default is blank queries + importedQueries = ensureQueries(); + } + } + + if (datasourceId !== getState().explore.requestedDatasourceId) { + // User already changed datasource again, discard results + return; + } + + // Reset edit state with new queries + const nextQueries = importedQueries.map((q, i) => ({ + ...importedQueries[i], + ...generateEmptyQuery(i), + })); + + dispatch(loadDatasourceSuccess(instance, nextQueries)); + dispatch(runQueries()); + }; +} + +export function modifyQueries(modification: any, index: number, modifier: any): ThunkResult { + return dispatch => { + dispatch({ type: ActionTypes.ModifyQueries, modification, index, modifier }); + if (!modification.preventSubmit) { + dispatch(runQueries()); + } + }; +} + +export function queryTransactionFailure(transactionId: string, response: any, datasourceId: string): ThunkResult { + return (dispatch, getState) => { + const { datasourceInstance, queryTransactions } = getState().explore; + if (datasourceInstance.meta.id !== datasourceId || response.cancelled) { + // Navigated away, queries did not matter + return; + } + + // Transaction might have been discarded + if (!queryTransactions.find(qt => qt.id === transactionId)) { + return null; + } + + console.error(response); + + let error: string; + let errorDetails: string; + if (response.data) { + if (typeof response.data === 'string') { + error = response.data; + } else if (response.data.error) { + error = response.data.error; + if (response.data.response) { + errorDetails = response.data.response; + } + } else { + throw new Error('Could not handle error response'); + } + } else if (response.message) { + error = response.message; + } else if (typeof response === 'string') { + error = response; + } else { + error = 'Unknown error during query transaction. Please check JS console logs.'; + } + + // Mark transactions as complete + const nextQueryTransactions = queryTransactions.map(qt => { + if (qt.id === transactionId) { + return { + ...qt, + error, + errorDetails, + done: true, + }; + } + return qt; + }); + + dispatch({ type: ActionTypes.QueryTransactionFailure, queryTransactions: nextQueryTransactions }); + }; +} + +export function queryTransactionStart( + transaction: QueryTransaction, + resultType: ResultType, + rowIndex: number +): QueryTransactionStartAction { + return { type: ActionTypes.QueryTransactionStart, resultType, rowIndex, transaction }; +} + +export function queryTransactionSuccess( + transactionId: string, + result: any, + latency: number, + queries: DataQuery[], + datasourceId: string +): ThunkResult { + return (dispatch, getState) => { + const { datasourceInstance, history, queryTransactions, scanner, scanning } = getState().explore; + + // If datasource already changed, results do not matter + if (datasourceInstance.meta.id !== datasourceId) { + return; + } + + // Transaction might have been discarded + const transaction = queryTransactions.find(qt => qt.id === transactionId); + if (!transaction) { + return; + } + + // Get query hints + let hints: QueryHint[]; + if (datasourceInstance.getQueryHints as QueryHintGetter) { + hints = datasourceInstance.getQueryHints(transaction.query, result); + } + + // Mark transactions as complete and attach result + const nextQueryTransactions = queryTransactions.map(qt => { + if (qt.id === transactionId) { + return { + ...qt, + hints, + latency, + result, + done: true, + }; + } + return qt; + }); + + // Side-effect: Saving history in localstorage + const nextHistory = updateHistory(history, datasourceId, queries); + + dispatch({ + type: ActionTypes.QueryTransactionSuccess, + history: nextHistory, + queryTransactions: nextQueryTransactions, + }); + + // Keep scanning for results if this was the last scanning transaction + if (scanning) { + if (_.size(result) === 0) { + const other = nextQueryTransactions.find(qt => qt.scanning && !qt.done); + if (!other) { + const range = scanner(); + dispatch({ type: ActionTypes.ScanRange, range }); + } + } else { + // We can stop scanning if we have a result + dispatch(scanStop()); + } + } + }; +} + +export function removeQueryRow(index: number): ThunkResult { + return dispatch => { + dispatch({ type: ActionTypes.RemoveQueryRow, index }); + dispatch(runQueries()); + }; +} + +export function runQueries() { + return (dispatch, getState) => { + const { + datasourceInstance, + modifiedQueries, + showingLogs, + showingGraph, + showingTable, + supportsGraph, + supportsLogs, + supportsTable, + } = getState().explore; + + if (!hasNonEmptyQuery(modifiedQueries)) { + dispatch({ type: ActionTypes.RunQueriesEmpty }); + return; + } + + // Some datasource's query builders allow per-query interval limits, + // but we're using the datasource interval limit for now + const interval = datasourceInstance.interval; + + // Keep table queries first since they need to return quickly + if (showingTable && supportsTable) { + dispatch( + runQueriesForType( + 'Table', + { + interval, + format: 'table', + instant: true, + valueWithRefId: true, + }, + data => data[0] + ) + ); + } + if (showingGraph && supportsGraph) { + dispatch( + runQueriesForType( + 'Graph', + { + interval, + format: 'time_series', + instant: false, + }, + makeTimeSeriesList + ) + ); + } + if (showingLogs && supportsLogs) { + dispatch(runQueriesForType('Logs', { interval, format: 'logs' })); + } + // TODO save state + }; +} + +function runQueriesForType(resultType: ResultType, queryOptions: QueryOptions, resultGetter?: any) { + return async (dispatch, getState) => { + const { + datasourceInstance, + eventBridge, + modifiedQueries: queries, + queryIntervals, + range, + scanning, + } = getState().explore; + const datasourceId = datasourceInstance.meta.id; + + // Run all queries concurrently + queries.forEach(async (query, rowIndex) => { + const transaction = buildQueryTransaction( + query, + rowIndex, + resultType, + queryOptions, + range, + queryIntervals, + scanning + ); + dispatch(queryTransactionStart(transaction, resultType, rowIndex)); + try { + const now = Date.now(); + const res = await datasourceInstance.query(transaction.options); + eventBridge.emit('data-received', res.data || []); + const latency = Date.now() - now; + const results = resultGetter ? resultGetter(res.data) : res.data; + dispatch(queryTransactionSuccess(transaction.id, results, latency, queries, datasourceId)); + } catch (response) { + eventBridge.emit('data-error', response); + dispatch(queryTransactionFailure(transaction.id, response, datasourceId)); + } + }); + }; +} + +export function scanStart(scanner: RangeScanner): ThunkResult { + return dispatch => { + dispatch({ type: ActionTypes.ScanStart, scanner }); + const range = scanner(); + dispatch({ type: ActionTypes.ScanRange, range }); + }; +} + +export function scanStop(): ScanStopAction { + return { type: ActionTypes.ScanStop }; +} diff --git a/public/app/features/explore/state/reducers.ts b/public/app/features/explore/state/reducers.ts new file mode 100644 index 00000000000..b49d54405d1 --- /dev/null +++ b/public/app/features/explore/state/reducers.ts @@ -0,0 +1,412 @@ +import { RawTimeRange, TimeRange } from '@grafana/ui'; + +import { + calculateResultsFromQueryTransactions, + generateEmptyQuery, + getIntervals, + ensureQueries, +} from 'app/core/utils/explore'; +import { DataSourceSelectItem } from 'app/types/datasources'; +import { HistoryItem, QueryTransaction, QueryIntervals, RangeScanner } from 'app/types/explore'; +import { DataQuery } from 'app/types/series'; + +import { Action, ActionTypes } from './actions'; +import { Emitter } from 'app/core/core'; +import { LogsModel } from 'app/core/logs_model'; +import TableModel from 'app/core/table_model'; + +// TODO move to types +export interface ExploreState { + StartPage?: any; + containerWidth: number; + datasourceInstance: any; + datasourceError: string; + datasourceLoading: boolean | null; + datasourceMissing: boolean; + eventBridge?: Emitter; + exploreDatasources: DataSourceSelectItem[]; + graphResult?: any[]; + history: HistoryItem[]; + initialDatasource?: string; + initialQueries: DataQuery[]; + logsHighlighterExpressions?: string[]; + logsResult?: LogsModel; + modifiedQueries: DataQuery[]; + queryIntervals: QueryIntervals; + queryTransactions: QueryTransaction[]; + requestedDatasourceId?: number; + range: TimeRange | RawTimeRange; + scanner?: RangeScanner; + scanning?: boolean; + scanRange?: RawTimeRange; + showingGraph: boolean; + showingLogs: boolean; + showingStartPage?: boolean; + showingTable: boolean; + supportsGraph: boolean | null; + supportsLogs: boolean | null; + supportsTable: boolean | null; + tableResult?: TableModel; +} + +export const DEFAULT_RANGE = { + from: 'now-6h', + to: 'now', +}; + +// Millies step for helper bar charts +const DEFAULT_GRAPH_INTERVAL = 15 * 1000; + +const initialExploreState: ExploreState = { + StartPage: undefined, + containerWidth: 0, + datasourceInstance: null, + datasourceError: null, + datasourceLoading: null, + datasourceMissing: false, + exploreDatasources: [], + history: [], + initialQueries: [], + modifiedQueries: [], + queryTransactions: [], + queryIntervals: { interval: '15s', intervalMs: DEFAULT_GRAPH_INTERVAL }, + range: DEFAULT_RANGE, + scanning: false, + scanRange: null, + showingGraph: true, + showingLogs: true, + showingTable: true, + supportsGraph: null, + supportsLogs: null, + supportsTable: null, +}; + +export const exploreReducer = (state = initialExploreState, action: Action): ExploreState => { + switch (action.type) { + case ActionTypes.AddQueryRow: { + const { initialQueries, modifiedQueries, queryTransactions } = state; + const { index, query } = action; + modifiedQueries[index + 1] = query; + + const nextQueries = [ + ...initialQueries.slice(0, index + 1), + { ...modifiedQueries[index + 1] }, + ...initialQueries.slice(index + 1), + ]; + + // Ongoing transactions need to update their row indices + const nextQueryTransactions = queryTransactions.map(qt => { + if (qt.rowIndex > index) { + return { + ...qt, + rowIndex: qt.rowIndex + 1, + }; + } + return qt; + }); + + return { + ...state, + modifiedQueries, + initialQueries: nextQueries, + logsHighlighterExpressions: undefined, + queryTransactions: nextQueryTransactions, + }; + } + + case ActionTypes.ChangeQuery: { + const { initialQueries, queryTransactions } = state; + let { modifiedQueries } = state; + const { query, index, override } = action; + modifiedQueries[index] = query; + if (override) { + const nextQuery: DataQuery = { + ...query, + ...generateEmptyQuery(index), + }; + const nextQueries = [...initialQueries]; + nextQueries[index] = nextQuery; + modifiedQueries = [...nextQueries]; + + // Discard ongoing transaction related to row query + const nextQueryTransactions = queryTransactions.filter(qt => qt.rowIndex !== index); + + return { + ...state, + initialQueries: nextQueries, + modifiedQueries: nextQueries.slice(), + queryTransactions: nextQueryTransactions, + }; + } + return { + ...state, + modifiedQueries, + }; + } + + case ActionTypes.ChangeSize: { + const { range, datasourceInstance } = state; + if (!datasourceInstance) { + return state; + } + const containerWidth = action.width; + const queryIntervals = getIntervals(range, datasourceInstance.interval, containerWidth); + return { ...state, containerWidth, queryIntervals }; + } + + case ActionTypes.ChangeTime: { + return { + ...state, + range: action.range, + }; + } + + case ActionTypes.ClickClear: { + const queries = ensureQueries(); + return { + ...state, + initialQueries: queries.slice(), + modifiedQueries: queries.slice(), + showingStartPage: Boolean(state.StartPage), + }; + } + + case ActionTypes.ClickExample: { + const modifiedQueries = [action.query]; + return { ...state, initialQueries: modifiedQueries.slice(), modifiedQueries }; + } + + case ActionTypes.ClickGraphButton: { + const showingGraph = !state.showingGraph; + let nextQueryTransactions = state.queryTransactions; + if (!showingGraph) { + // Discard transactions related to Graph query + nextQueryTransactions = state.queryTransactions.filter(qt => qt.resultType !== 'Graph'); + } + return { ...state, queryTransactions: nextQueryTransactions, showingGraph }; + } + + case ActionTypes.ClickLogsButton: { + const showingLogs = !state.showingLogs; + let nextQueryTransactions = state.queryTransactions; + if (!showingLogs) { + // Discard transactions related to Logs query + nextQueryTransactions = state.queryTransactions.filter(qt => qt.resultType !== 'Logs'); + } + return { ...state, queryTransactions: nextQueryTransactions, showingLogs }; + } + + case ActionTypes.ClickTableButton: { + const showingTable = !state.showingTable; + if (showingTable) { + return { ...state, showingTable, queryTransactions: state.queryTransactions }; + } + + // Toggle off needs discarding of table queries and results + const nextQueryTransactions = state.queryTransactions.filter(qt => qt.resultType !== 'Table'); + const results = calculateResultsFromQueryTransactions( + nextQueryTransactions, + state.datasourceInstance, + state.queryIntervals.intervalMs + ); + + return { ...state, ...results, queryTransactions: nextQueryTransactions, showingTable }; + } + + case ActionTypes.InitializeExplore: { + const { containerWidth, eventBridge, exploreDatasources, range } = action; + return { + ...state, + containerWidth, + eventBridge, + exploreDatasources, + range, + initialDatasource: action.datasource, + initialQueries: action.queries, + modifiedQueries: action.queries.slice(), + }; + } + + case ActionTypes.LoadDatasourceFailure: { + return { ...state, datasourceError: action.error, datasourceLoading: false }; + } + + case ActionTypes.LoadDatasourceMissing: { + return { ...state, datasourceMissing: true, datasourceLoading: false }; + } + + case ActionTypes.LoadDatasourcePending: { + return { ...state, datasourceLoading: true, requestedDatasourceId: action.datasourceId }; + } + + case ActionTypes.LoadDatasourceSuccess: { + const { containerWidth, range } = state; + const queryIntervals = getIntervals(range, action.datasourceInstance.interval, containerWidth); + + return { + ...state, + queryIntervals, + StartPage: action.StartPage, + datasourceInstance: action.datasourceInstance, + datasourceLoading: false, + datasourceMissing: false, + history: action.history, + initialDatasource: action.initialDatasource, + initialQueries: action.initialQueries, + logsHighlighterExpressions: undefined, + modifiedQueries: action.initialQueries.slice(), + showingStartPage: action.showingStartPage, + supportsGraph: action.supportsGraph, + supportsLogs: action.supportsLogs, + supportsTable: action.supportsTable, + }; + } + + case ActionTypes.ModifyQueries: { + const { initialQueries, modifiedQueries, queryTransactions } = state; + const { action: modification, index, modifier } = action as any; + let nextQueries: DataQuery[]; + let nextQueryTransactions; + if (index === undefined) { + // Modify all queries + nextQueries = initialQueries.map((query, i) => ({ + ...modifier(modifiedQueries[i], modification), + ...generateEmptyQuery(i), + })); + // Discard all ongoing transactions + nextQueryTransactions = []; + } else { + // Modify query only at index + nextQueries = initialQueries.map((query, i) => { + // Synchronize all queries with local query cache to ensure consistency + // TODO still needed? + return i === index + ? { + ...modifier(modifiedQueries[i], modification), + ...generateEmptyQuery(i), + } + : query; + }); + nextQueryTransactions = queryTransactions + // Consume the hint corresponding to the action + .map(qt => { + if (qt.hints != null && qt.rowIndex === index) { + qt.hints = qt.hints.filter(hint => hint.fix.action !== modification); + } + return qt; + }) + // Preserve previous row query transaction to keep results visible if next query is incomplete + .filter(qt => modification.preventSubmit || qt.rowIndex !== index); + } + return { + ...state, + initialQueries: nextQueries, + modifiedQueries: nextQueries.slice(), + queryTransactions: nextQueryTransactions, + }; + } + + case ActionTypes.RemoveQueryRow: { + const { datasourceInstance, initialQueries, queryIntervals, queryTransactions } = state; + let { modifiedQueries } = state; + const { index } = action; + + modifiedQueries = [...modifiedQueries.slice(0, index), ...modifiedQueries.slice(index + 1)]; + + if (initialQueries.length <= 1) { + return state; + } + + const nextQueries = [...initialQueries.slice(0, index), ...initialQueries.slice(index + 1)]; + + // Discard transactions related to row query + const nextQueryTransactions = queryTransactions.filter(qt => qt.rowIndex !== index); + const results = calculateResultsFromQueryTransactions( + nextQueryTransactions, + datasourceInstance, + queryIntervals.intervalMs + ); + + return { + ...state, + ...results, + initialQueries: nextQueries, + logsHighlighterExpressions: undefined, + modifiedQueries: nextQueries.slice(), + queryTransactions: nextQueryTransactions, + }; + } + + case ActionTypes.QueryTransactionFailure: { + const { queryTransactions } = action; + return { + ...state, + queryTransactions, + showingStartPage: false, + }; + } + + case ActionTypes.QueryTransactionStart: { + const { datasourceInstance, queryIntervals, queryTransactions } = state; + const { resultType, rowIndex, transaction } = action; + // Discarding existing transactions of same type + const remainingTransactions = queryTransactions.filter( + qt => !(qt.resultType === resultType && qt.rowIndex === rowIndex) + ); + + // Append new transaction + const nextQueryTransactions: QueryTransaction[] = [...remainingTransactions, transaction]; + + const results = calculateResultsFromQueryTransactions( + nextQueryTransactions, + datasourceInstance, + queryIntervals.intervalMs + ); + + return { + ...state, + ...results, + queryTransactions: nextQueryTransactions, + showingStartPage: false, + }; + } + + case ActionTypes.QueryTransactionSuccess: { + const { datasourceInstance, queryIntervals } = state; + const { history, queryTransactions } = action; + const results = calculateResultsFromQueryTransactions( + queryTransactions, + datasourceInstance, + queryIntervals.intervalMs + ); + + return { + ...state, + ...results, + history, + queryTransactions, + showingStartPage: false, + }; + } + + case ActionTypes.ScanRange: { + return { ...state, scanRange: action.range }; + } + + case ActionTypes.ScanStart: { + return { ...state, scanning: true }; + } + + case ActionTypes.ScanStop: { + const { queryTransactions } = state; + const nextQueryTransactions = queryTransactions.filter(qt => qt.scanning && !qt.done); + return { ...state, queryTransactions: nextQueryTransactions, scanning: false, scanRange: undefined }; + } + } + + return state; +}; + +export default { + explore: exploreReducer, +}; diff --git a/public/app/store/configureStore.ts b/public/app/store/configureStore.ts index 943aff80a70..570a387cd74 100644 --- a/public/app/store/configureStore.ts +++ b/public/app/store/configureStore.ts @@ -7,6 +7,7 @@ import teamsReducers from 'app/features/teams/state/reducers'; import apiKeysReducers from 'app/features/api-keys/state/reducers'; import foldersReducers from 'app/features/folders/state/reducers'; import dashboardReducers from 'app/features/dashboard/state/reducers'; +import exploreReducers from 'app/features/explore/state/reducers'; import pluginReducers from 'app/features/plugins/state/reducers'; import dataSourcesReducers from 'app/features/datasources/state/reducers'; import usersReducers from 'app/features/users/state/reducers'; @@ -20,6 +21,7 @@ const rootReducers = { ...apiKeysReducers, ...foldersReducers, ...dashboardReducers, + ...exploreReducers, ...pluginReducers, ...dataSourcesReducers, ...usersReducers, diff --git a/public/app/types/explore.ts b/public/app/types/explore.ts index c2c59d35f5b..c64ce6133cf 100644 --- a/public/app/types/explore.ts +++ b/public/app/types/explore.ts @@ -4,7 +4,6 @@ import { DataQuery } from './series'; import { RawTimeRange } from '@grafana/ui'; import TableModel from 'app/core/table_model'; import { LogsModel } from 'app/core/logs_model'; -import { DataSourceSelectItem } from 'app/types/datasources'; export interface CompletionItem { /** @@ -128,6 +127,19 @@ export interface QueryHintGetter { (query: DataQuery, results: any[], ...rest: any): QueryHint[]; } +export interface QueryIntervals { + interval: string; + intervalMs: number; +} + +export interface QueryOptions { + interval: string; + format: string; + hinting?: boolean; + instant?: boolean; + valueWithRefId?: boolean; +} + export interface QueryTransaction { id: string; done: boolean; @@ -142,6 +154,8 @@ export interface QueryTransaction { scanning?: boolean; } +export type RangeScanner = () => RawTimeRange; + export interface TextMatch { text: string; start: number; @@ -153,18 +167,11 @@ export interface ExploreState { StartPage?: any; datasource: any; datasourceError: any; - datasourceLoading: boolean | null; - datasourceMissing: boolean; - exploreDatasources: DataSourceSelectItem[]; - graphInterval: number; // in ms graphResult?: any[]; history: HistoryItem[]; - initialDatasource?: string; - initialQueries: DataQuery[]; logsHighlighterExpressions?: string[]; logsResult?: LogsModel; queryTransactions: QueryTransaction[]; - range: RawTimeRange; scanning?: boolean; scanRange?: RawTimeRange; showingGraph: boolean; diff --git a/public/app/types/index.ts b/public/app/types/index.ts index 72da1c76ea8..018c4c51d3d 100644 --- a/public/app/types/index.ts +++ b/public/app/types/index.ts @@ -19,6 +19,7 @@ import { } from './appNotifications'; import { DashboardSearchHit } from './search'; import { ValidationEvents, ValidationRule } from './form'; +import { ExploreState } from 'app/features/explore/state/reducers'; export { Team, TeamsState, @@ -81,6 +82,7 @@ export interface StoreState { folder: FolderState; dashboard: DashboardState; dataSources: DataSourcesState; + explore: ExploreState; users: UsersState; organization: OrganizationState; appNotifications: AppNotificationsState; From 68c039b28901908aa91d9a9fea83f5e0d2655237 Mon Sep 17 00:00:00 2001 From: David Kaltschmidt Date: Fri, 11 Jan 2019 18:26:56 +0100 Subject: [PATCH 2/9] Allow multiple Explore items for split --- public/app/core/utils/explore.ts | 13 +- public/app/features/explore/Explore.tsx | 272 +++++++++--------- public/app/features/explore/Logs.tsx | 6 +- public/app/features/explore/Wrapper.tsx | 86 ++---- public/app/features/explore/state/actions.ts | 266 +++++++++++------ public/app/features/explore/state/reducers.ts | 47 ++- public/app/types/explore.ts | 5 + public/sass/pages/_explore.scss | 2 +- 8 files changed, 413 insertions(+), 284 deletions(-) diff --git a/public/app/core/utils/explore.ts b/public/app/core/utils/explore.ts index 871a020ccc2..026d1ba324c 100644 --- a/public/app/core/utils/explore.ts +++ b/public/app/core/utils/explore.ts @@ -206,11 +206,14 @@ export function ensureQueries(queries?: DataQuery[]): DataQuery[] { * A target is non-empty when it has keys (with non-empty values) other than refId and key. */ export function hasNonEmptyQuery(queries: DataQuery[]): boolean { - return queries.some( - query => - Object.keys(query) - .map(k => query[k]) - .filter(v => v).length > 2 + return ( + queries && + queries.some( + query => + Object.keys(query) + .map(k => query[k]) + .filter(v => v).length > 2 + ) ); } diff --git a/public/app/features/explore/Explore.tsx b/public/app/features/explore/Explore.tsx index 64e9c66ece5..a3177bebe79 100644 --- a/public/app/features/explore/Explore.tsx +++ b/public/app/features/explore/Explore.tsx @@ -2,14 +2,15 @@ import React from 'react'; import { hot } from 'react-hot-loader'; import { connect } from 'react-redux'; import _ from 'lodash'; -import { withSize } from 'react-sizeme'; +import { AutoSizer } from 'react-virtualized'; import { RawTimeRange, TimeRange } from '@grafana/ui'; import { DataSourceSelectItem } from 'app/types/datasources'; -import { ExploreUrlState, HistoryItem, QueryTransaction, RangeScanner } from 'app/types/explore'; +import { ExploreUrlState, HistoryItem, QueryTransaction, RangeScanner, ExploreId } from 'app/types/explore'; import { DataQuery } from 'app/types/series'; +import { StoreState } from 'app/types'; import store from 'app/core/store'; -import { LAST_USED_DATASOURCE_KEY, ensureQueries } from 'app/core/utils/explore'; +import { LAST_USED_DATASOURCE_KEY, ensureQueries, DEFAULT_RANGE } from 'app/core/utils/explore'; import { DataSourcePicker } from 'app/core/components/Select/DataSourcePicker'; import { Emitter } from 'app/core/utils/emitter'; @@ -20,9 +21,11 @@ import { changeSize, changeTime, clickClear, + clickCloseSplit, clickExample, clickGraphButton, clickLogsButton, + clickSplit, clickTableButton, highlightLogsExpression, initializeExplore, @@ -32,7 +35,7 @@ import { scanStart, scanStop, } from './state/actions'; -import { ExploreState } from './state/reducers'; +import { ExploreItemState } from './state/reducers'; import Panel from './Panel'; import QueryRows from './QueryRows'; @@ -50,17 +53,21 @@ interface ExploreProps { addQueryRow: typeof addQueryRow; changeDatasource: typeof changeDatasource; changeQuery: typeof changeQuery; + changeSize: typeof changeSize; changeTime: typeof changeTime; clickClear: typeof clickClear; + clickCloseSplit: typeof clickCloseSplit; clickExample: typeof clickExample; clickGraphButton: typeof clickGraphButton; clickLogsButton: typeof clickLogsButton; + clickSplit: typeof clickSplit; clickTableButton: typeof clickTableButton; datasourceError: string; datasourceInstance: any; datasourceLoading: boolean | null; datasourceMissing: boolean; exploreDatasources: DataSourceSelectItem[]; + exploreId: ExploreId; graphResult?: any[]; highlightLogsExpression: typeof highlightLogsExpression; history: HistoryItem[]; @@ -70,9 +77,6 @@ interface ExploreProps { logsHighlighterExpressions?: string[]; logsResult?: LogsModel; modifyQueries: typeof modifyQueries; - onChangeSplit: (split: boolean, state?: ExploreState) => void; - onSaveState: (key: string, state: ExploreState) => void; - position: string; queryTransactions: QueryTransaction[]; removeQueryRow: typeof removeQueryRow; range: RawTimeRange; @@ -83,8 +87,6 @@ interface ExploreProps { scanStart: typeof scanStart; scanStop: typeof scanStop; split: boolean; - splitState?: ExploreState; - stateKey: string; showingGraph: boolean; showingLogs: boolean; showingStartPage?: boolean; @@ -132,7 +134,7 @@ interface ExploreProps { * The result viewers determine some of the query options sent to the datasource, e.g., * `format`, to indicate eventual transformations by the datasources' result transformers. */ -export class Explore extends React.PureComponent { +export class Explore extends React.PureComponent { el: any; exploreEvents: Emitter; /** @@ -147,13 +149,23 @@ export class Explore extends React.PureComponent { } async componentDidMount() { - // Load URL state and parse range - const { datasource, queries, range } = this.props.urlState as ExploreUrlState; - const initialDatasource = datasource || store.get(LAST_USED_DATASOURCE_KEY); - const initialQueries: DataQuery[] = ensureQueries(queries); - const initialRange = { from: parseTime(range.from), to: parseTime(range.to) }; - const width = this.el ? this.el.offsetWidth : 0; - this.props.initializeExplore(initialDatasource, initialQueries, initialRange, width, this.exploreEvents); + const { exploreId, split, urlState } = this.props; + if (!split) { + // Load URL state and parse range + const { datasource, queries, range = DEFAULT_RANGE } = (urlState || {}) as ExploreUrlState; + const initialDatasource = datasource || store.get(LAST_USED_DATASOURCE_KEY); + const initialQueries: DataQuery[] = ensureQueries(queries); + const initialRange = { from: parseTime(range.from), to: parseTime(range.to) }; + const width = this.el ? this.el.offsetWidth : 0; + this.props.initializeExplore( + exploreId, + initialDatasource, + initialQueries, + initialRange, + width, + this.exploreEvents + ); + } } componentWillUnmount() { @@ -165,17 +177,17 @@ export class Explore extends React.PureComponent { }; onAddQueryRow = index => { - this.props.addQueryRow(index); + this.props.addQueryRow(this.props.exploreId, index); }; onChangeDatasource = async option => { - this.props.changeDatasource(option.value); + this.props.changeDatasource(this.props.exploreId, option.value); }; onChangeQuery = (query: DataQuery, index: number, override?: boolean) => { - const { changeQuery, datasourceInstance } = this.props; + const { changeQuery, datasourceInstance, exploreId } = this.props; - changeQuery(query, index, override); + changeQuery(exploreId, query, index, override); if (query && !override && datasourceInstance.getHighlighterExpression && index === 0) { // Live preview of log search matches. Only use on first row for now this.updateLogsHighlights(query); @@ -186,43 +198,36 @@ export class Explore extends React.PureComponent { if (this.props.scanning && !changedByScanner) { this.onStopScanning(); } - this.props.changeTime(range); + this.props.changeTime(this.props.exploreId, range); }; onClickClear = () => { - this.props.clickClear(); + this.props.clickClear(this.props.exploreId); }; onClickCloseSplit = () => { - const { onChangeSplit } = this.props; - if (onChangeSplit) { - onChangeSplit(false); - } + this.props.clickCloseSplit(); }; onClickGraphButton = () => { - this.props.clickGraphButton(); + this.props.clickGraphButton(this.props.exploreId); }; onClickLogsButton = () => { - this.props.clickLogsButton(); + this.props.clickLogsButton(this.props.exploreId); }; // Use this in help pages to set page to a single query onClickExample = (query: DataQuery) => { - this.props.clickExample(query); + this.props.clickExample(this.props.exploreId, query); }; onClickSplit = () => { - const { onChangeSplit } = this.props; - if (onChangeSplit) { - // const state = this.cloneState(); - // onChangeSplit(true, state); - } + this.props.clickSplit(); }; onClickTableButton = () => { - this.props.clickTableButton(); + this.props.clickTableButton(this.props.exploreId); }; onClickLabel = (key: string, value: string) => { @@ -233,18 +238,22 @@ export class Explore extends React.PureComponent { const { datasourceInstance } = this.props; if (datasourceInstance && datasourceInstance.modifyQuery) { const modifier = (queries: DataQuery, action: any) => datasourceInstance.modifyQuery(queries, action); - this.props.modifyQueries(action, index, modifier); + this.props.modifyQueries(this.props.exploreId, action, index, modifier); } }; onRemoveQueryRow = index => { - this.props.removeQueryRow(index); + this.props.removeQueryRow(this.props.exploreId, index); + }; + + onResize = (size: { height: number; width: number }) => { + this.props.changeSize(this.props.exploreId, size); }; onStartScanning = () => { // Scanner will trigger a query const scanner = this.scanPreviousRange; - this.props.scanStart(scanner); + this.props.scanStart(this.props.exploreId, scanner); }; scanPreviousRange = (): RawTimeRange => { @@ -253,30 +262,21 @@ export class Explore extends React.PureComponent { }; onStopScanning = () => { - this.props.scanStop(); + this.props.scanStop(this.props.exploreId); }; onSubmit = () => { - this.props.runQueries(); + this.props.runQueries(this.props.exploreId); }; updateLogsHighlights = _.debounce((value: DataQuery) => { const { datasourceInstance } = this.props; if (datasourceInstance.getHighlighterExpression) { const expressions = [datasourceInstance.getHighlighterExpression(value)]; - this.props.highlightLogsExpression(expressions); + this.props.highlightLogsExpression(this.props.exploreId, expressions); } }, 500); - // cloneState(): ExploreState { - // // Copy state, but copy queries including modifications - // return { - // ...this.state, - // queryTransactions: [], - // initialQueries: [...this.modifiedQueries], - // }; - // } - // saveState = () => { // const { stateKey, onSaveState } = this.props; // onSaveState(stateKey, this.cloneState()); @@ -290,13 +290,13 @@ export class Explore extends React.PureComponent { datasourceLoading, datasourceMissing, exploreDatasources, + exploreId, graphResult, history, initialQueries, logsHighlighterExpressions, logsResult, queryTransactions, - position, range, scanning, scanRange, @@ -323,7 +323,7 @@ export class Explore extends React.PureComponent { return (
- {position === 'left' ? ( + {exploreId === 'left' ? ( ) : null}
- {position === 'left' && !split ? ( + {exploreId === 'left' && !split ? (
)} - {datasourceInstance && !datasourceError ? ( -
- -
- - {showingStartPage && } - {!showingStartPage && ( - <> - {supportsGraph && ( - - - - )} - {supportsTable && ( - - - - )} - {supportsLogs && ( - - - - )} - + {datasourceInstance && + !datasourceError && ( +
+ + + {({ width }) => ( +
+ + {showingStartPage && } + {!showingStartPage && ( + <> + {supportsGraph && ( + + + + )} + {supportsTable && ( + +
+ + )} + {supportsLogs && ( + + + + )} + + )} + + )} - - - - ) : null} + + + )} ); } } -function mapStateToProps({ explore }) { +function mapStateToProps(state: StoreState, { exploreId }) { + const explore = state.explore; + const { split } = explore; + const item: ExploreItemState = explore[exploreId]; const { StartPage, datasourceError, @@ -480,7 +493,7 @@ function mapStateToProps({ explore }) { supportsLogs, supportsTable, tableResult, - } = explore as ExploreState; + } = item; return { StartPage, datasourceError, @@ -502,6 +515,7 @@ function mapStateToProps({ explore }) { showingLogs, showingStartPage, showingTable, + split, supportsGraph, supportsLogs, supportsTable, @@ -513,20 +527,22 @@ const mapDispatchToProps = { addQueryRow, changeDatasource, changeQuery, + changeSize, changeTime, clickClear, + clickCloseSplit, clickExample, clickGraphButton, clickLogsButton, + clickSplit, clickTableButton, highlightLogsExpression, initializeExplore, modifyQueries, - onSize: changeSize, // used by withSize HOC removeQueryRow, runQueries, scanStart, scanStop, }; -export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(withSize()(Explore))); +export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(Explore)); diff --git a/public/app/features/explore/Logs.tsx b/public/app/features/explore/Logs.tsx index 1a384cf011d..d07b31e2ff1 100644 --- a/public/app/features/explore/Logs.tsx +++ b/public/app/features/explore/Logs.tsx @@ -241,9 +241,9 @@ function renderMetaItem(value: any, kind: LogsMetaKind) { interface LogsProps { data: LogsModel; + exploreId: string; highlighterExpressions: string[]; loading: boolean; - position: string; range?: RawTimeRange; scanning?: boolean; scanRange?: RawTimeRange; @@ -348,10 +348,10 @@ export default class Logs extends PureComponent { render() { const { data, + exploreId, highlighterExpressions, loading = false, onClickLabel, - position, range, scanning, scanRange, @@ -400,7 +400,7 @@ export default class Logs extends PureComponent { data={data.series} height="100px" range={range} - id={`explore-logs-graph-${position}`} + id={`explore-logs-graph-${exploreId}`} onChangeTime={this.props.onChangeTime} onToggleSeries={this.onToggleLogLevel} userOptions={graphOptions} diff --git a/public/app/features/explore/Wrapper.tsx b/public/app/features/explore/Wrapper.tsx index de1eee4c662..04f189749bf 100644 --- a/public/app/features/explore/Wrapper.tsx +++ b/public/app/features/explore/Wrapper.tsx @@ -3,9 +3,9 @@ import { hot } from 'react-hot-loader'; import { connect } from 'react-redux'; import { updateLocation } from 'app/core/actions'; -import { serializeStateToUrlParam, parseUrlState } from 'app/core/utils/explore'; +// import { serializeStateToUrlParam, parseUrlState } from 'app/core/utils/explore'; import { StoreState } from 'app/types'; -import { ExploreState } from 'app/types/explore'; +import { ExploreId } from 'app/types/explore'; import ErrorBoundary from './ErrorBoundary'; import Explore from './Explore'; @@ -13,81 +13,41 @@ import Explore from './Explore'; interface WrapperProps { backendSrv?: any; datasourceSrv?: any; - updateLocation: typeof updateLocation; - urlStates: { [key: string]: string }; -} - -interface WrapperState { split: boolean; - splitState: ExploreState; + updateLocation: typeof updateLocation; + // urlStates: { [key: string]: string }; } -const STATE_KEY_LEFT = 'state'; -const STATE_KEY_RIGHT = 'stateRight'; - -export class Wrapper extends Component { - urlStates: { [key: string]: string }; +export class Wrapper extends Component { + // urlStates: { [key: string]: string }; constructor(props: WrapperProps) { super(props); - this.urlStates = props.urlStates; - this.state = { - split: Boolean(props.urlStates[STATE_KEY_RIGHT]), - splitState: undefined, - }; + // this.urlStates = props.urlStates; } - onChangeSplit = (split: boolean, splitState: ExploreState) => { - this.setState({ split, splitState }); - // When closing split, remove URL state for split part - if (!split) { - delete this.urlStates[STATE_KEY_RIGHT]; - this.props.updateLocation({ - query: this.urlStates, - }); - } - }; - - onSaveState = (key: string, state: ExploreState) => { - const urlState = serializeStateToUrlParam(state, true); - this.urlStates[key] = urlState; - this.props.updateLocation({ - query: this.urlStates, - }); - }; + // onSaveState = (key: string, state: ExploreState) => { + // const urlState = serializeStateToUrlParam(state, true); + // this.urlStates[key] = urlState; + // this.props.updateLocation({ + // query: this.urlStates, + // }); + // }; render() { - const { datasourceSrv } = this.props; + const { split } = this.props; // State overrides for props from first Explore - const { split, splitState } = this.state; - const urlStateLeft = parseUrlState(this.urlStates[STATE_KEY_LEFT]); - const urlStateRight = parseUrlState(this.urlStates[STATE_KEY_RIGHT]); + // const urlStateLeft = parseUrlState(this.urlStates[STATE_KEY_LEFT]); + // const urlStateRight = parseUrlState(this.urlStates[STATE_KEY_RIGHT]); return (
- + {split && ( - + )}
@@ -95,9 +55,11 @@ export class Wrapper extends Component { } } -const mapStateToProps = (state: StoreState) => ({ - urlStates: state.location.query, -}); +const mapStateToProps = (state: StoreState) => { + // urlStates: state.location.query, + const { split } = state.explore; + return { split }; +}; const mapDispatchToProps = { updateLocation, diff --git a/public/app/features/explore/state/actions.ts b/public/app/features/explore/state/actions.ts index d70a458059e..145b7506e79 100644 --- a/public/app/features/explore/state/actions.ts +++ b/public/app/features/explore/state/actions.ts @@ -17,6 +17,7 @@ import { DataSourceSelectItem } from 'app/types/datasources'; import { DataQuery, StoreState } from 'app/types'; import { getDatasourceSrv } from 'app/features/plugins/datasource_srv'; import { + ExploreId, HistoryItem, RangeScanner, ResultType, @@ -26,7 +27,7 @@ import { QueryHintGetter, } from 'app/types/explore'; import { Emitter } from 'app/core/core'; -import { dispatch } from 'rxjs/internal/observable/pairs'; +import { ExploreItemState } from './reducers'; export enum ActionTypes { AddQueryRow = 'ADD_QUERY_ROW', @@ -35,9 +36,11 @@ export enum ActionTypes { ChangeSize = 'CHANGE_SIZE', ChangeTime = 'CHANGE_TIME', ClickClear = 'CLICK_CLEAR', + ClickCloseSplit = 'CLICK_CLOSE_SPLIT', ClickExample = 'CLICK_EXAMPLE', ClickGraphButton = 'CLICK_GRAPH_BUTTON', ClickLogsButton = 'CLICK_LOGS_BUTTON', + ClickSplit = 'CLICK_SPLIT', ClickTableButton = 'CLICK_TABLE_BUTTON', HighlightLogsExpression = 'HIGHLIGHT_LOGS_EXPRESSION', InitializeExplore = 'INITIALIZE_EXPLORE', @@ -59,12 +62,14 @@ export enum ActionTypes { export interface AddQueryRowAction { type: ActionTypes.AddQueryRow; + exploreId: ExploreId; index: number; query: DataQuery; } export interface ChangeQueryAction { type: ActionTypes.ChangeQuery; + exploreId: ExploreId; query: DataQuery; index: number; override: boolean; @@ -72,38 +77,55 @@ export interface ChangeQueryAction { export interface ChangeSizeAction { type: ActionTypes.ChangeSize; + exploreId: ExploreId; width: number; height: number; } export interface ChangeTimeAction { type: ActionTypes.ChangeTime; + exploreId: ExploreId; range: TimeRange; } export interface ClickClearAction { type: ActionTypes.ClickClear; + exploreId: ExploreId; +} + +export interface ClickCloseSplitAction { + type: ActionTypes.ClickCloseSplit; } export interface ClickExampleAction { type: ActionTypes.ClickExample; + exploreId: ExploreId; query: DataQuery; } export interface ClickGraphButtonAction { type: ActionTypes.ClickGraphButton; + exploreId: ExploreId; } export interface ClickLogsButtonAction { type: ActionTypes.ClickLogsButton; + exploreId: ExploreId; +} + +export interface ClickSplitAction { + type: ActionTypes.ClickSplit; + itemState: ExploreItemState; } export interface ClickTableButtonAction { type: ActionTypes.ClickTableButton; + exploreId: ExploreId; } export interface InitializeExploreAction { type: ActionTypes.InitializeExplore; + exploreId: ExploreId; containerWidth: number; datasource: string; eventBridge: Emitter; @@ -114,25 +136,30 @@ export interface InitializeExploreAction { export interface HighlightLogsExpressionAction { type: ActionTypes.HighlightLogsExpression; + exploreId: ExploreId; expressions: string[]; } export interface LoadDatasourceFailureAction { type: ActionTypes.LoadDatasourceFailure; + exploreId: ExploreId; error: string; } export interface LoadDatasourcePendingAction { type: ActionTypes.LoadDatasourcePending; + exploreId: ExploreId; datasourceId: number; } export interface LoadDatasourceMissingAction { type: ActionTypes.LoadDatasourceMissing; + exploreId: ExploreId; } export interface LoadDatasourceSuccessAction { type: ActionTypes.LoadDatasourceSuccess; + exploreId: ExploreId; StartPage?: any; datasourceInstance: any; history: HistoryItem[]; @@ -147,6 +174,7 @@ export interface LoadDatasourceSuccessAction { export interface ModifyQueriesAction { type: ActionTypes.ModifyQueries; + exploreId: ExploreId; modification: any; index: number; modifier: (queries: DataQuery[], modification: any) => DataQuery[]; @@ -154,11 +182,13 @@ export interface ModifyQueriesAction { export interface QueryTransactionFailureAction { type: ActionTypes.QueryTransactionFailure; + exploreId: ExploreId; queryTransactions: QueryTransaction[]; } export interface QueryTransactionStartAction { type: ActionTypes.QueryTransactionStart; + exploreId: ExploreId; resultType: ResultType; rowIndex: number; transaction: QueryTransaction; @@ -166,27 +196,32 @@ export interface QueryTransactionStartAction { export interface QueryTransactionSuccessAction { type: ActionTypes.QueryTransactionSuccess; + exploreId: ExploreId; history: HistoryItem[]; queryTransactions: QueryTransaction[]; } export interface RemoveQueryRowAction { type: ActionTypes.RemoveQueryRow; + exploreId: ExploreId; index: number; } export interface ScanStartAction { type: ActionTypes.ScanStart; + exploreId: ExploreId; scanner: RangeScanner; } export interface ScanRangeAction { type: ActionTypes.ScanRange; + exploreId: ExploreId; range: RawTimeRange; } export interface ScanStopAction { type: ActionTypes.ScanStop; + exploreId: ExploreId; } export type Action = @@ -195,9 +230,11 @@ export type Action = | ChangeSizeAction | ChangeTimeAction | ClickClearAction + | ClickCloseSplitAction | ClickExampleAction | ClickGraphButtonAction | ClickLogsButtonAction + | ClickSplitAction | ClickTableButtonAction | HighlightLogsExpressionAction | InitializeExploreAction @@ -215,94 +252,126 @@ export type Action = | ScanStopAction; type ThunkResult = ThunkAction; -export function addQueryRow(index: number): AddQueryRowAction { +export function addQueryRow(exploreId: ExploreId, index: number): AddQueryRowAction { const query = generateEmptyQuery(index + 1); - return { type: ActionTypes.AddQueryRow, index, query }; + return { type: ActionTypes.AddQueryRow, exploreId, index, query }; } -export function changeDatasource(datasource: string): ThunkResult { +export function changeDatasource(exploreId: ExploreId, datasource: string): ThunkResult { return async dispatch => { const instance = await getDatasourceSrv().get(datasource); - dispatch(loadDatasource(instance)); + dispatch(loadDatasource(exploreId, instance)); }; } -export function changeQuery(query: DataQuery, index: number, override: boolean): ThunkResult { +export function changeQuery( + exploreId: ExploreId, + query: DataQuery, + index: number, + override: boolean +): ThunkResult { return dispatch => { // Null query means reset if (query === null) { query = { ...generateEmptyQuery(index) }; } - dispatch({ type: ActionTypes.ChangeQuery, query, index, override }); + dispatch({ type: ActionTypes.ChangeQuery, exploreId, query, index, override }); if (override) { - dispatch(runQueries()); + dispatch(runQueries(exploreId)); } }; } -export function changeSize({ height, width }: { height: number; width: number }): ChangeSizeAction { - return { type: ActionTypes.ChangeSize, height, width }; +export function changeSize( + exploreId: ExploreId, + { height, width }: { height: number; width: number } +): ChangeSizeAction { + return { type: ActionTypes.ChangeSize, exploreId, height, width }; } -export function changeTime(range: TimeRange): ThunkResult { +export function changeTime(exploreId: ExploreId, range: TimeRange): ThunkResult { return dispatch => { - dispatch({ type: ActionTypes.ChangeTime, range }); - dispatch(runQueries()); + dispatch({ type: ActionTypes.ChangeTime, exploreId, range }); + dispatch(runQueries(exploreId)); }; } -export function clickExample(rawQuery: DataQuery): ThunkResult { +export function clickClear(exploreId: ExploreId): ThunkResult { return dispatch => { - const query = { ...rawQuery, ...generateEmptyQuery() }; - dispatch({ - type: ActionTypes.ClickExample, - query, - }); - dispatch(runQueries()); - }; -} - -export function clickClear(): ThunkResult { - return dispatch => { - dispatch(scanStop()); - dispatch({ type: ActionTypes.ClickClear }); + dispatch(scanStop(exploreId)); + dispatch({ type: ActionTypes.ClickClear, exploreId }); // TODO save state }; } -export function clickGraphButton(): ThunkResult { +export function clickCloseSplit(): ThunkResult { + return dispatch => { + dispatch({ type: ActionTypes.ClickCloseSplit }); + // When closing split, remove URL state for split part + // TODO save state + }; +} + +export function clickExample(exploreId: ExploreId, rawQuery: DataQuery): ThunkResult { + return dispatch => { + const query = { ...rawQuery, ...generateEmptyQuery() }; + dispatch({ + type: ActionTypes.ClickExample, + exploreId, + query, + }); + dispatch(runQueries(exploreId)); + }; +} + +export function clickGraphButton(exploreId: ExploreId): ThunkResult { return (dispatch, getState) => { - dispatch({ type: ActionTypes.ClickGraphButton }); - if (getState().explore.showingGraph) { - dispatch(runQueries()); + dispatch({ type: ActionTypes.ClickGraphButton, exploreId }); + if (getState().explore[exploreId].showingGraph) { + dispatch(runQueries(exploreId)); } }; } -export function clickLogsButton(): ThunkResult { +export function clickLogsButton(exploreId: ExploreId): ThunkResult { return (dispatch, getState) => { - dispatch({ type: ActionTypes.ClickLogsButton }); - if (getState().explore.showingLogs) { - dispatch(runQueries()); + dispatch({ type: ActionTypes.ClickLogsButton, exploreId }); + if (getState().explore[exploreId].showingLogs) { + dispatch(runQueries(exploreId)); } }; } -export function clickTableButton(): ThunkResult { +export function clickSplit(): ThunkResult { return (dispatch, getState) => { - dispatch({ type: ActionTypes.ClickTableButton }); - if (getState().explore.showingTable) { - dispatch(runQueries()); + // Clone left state to become the right state + const leftState = getState().explore.left; + const itemState = { + ...leftState, + queryTransactions: [], + initialQueries: leftState.modifiedQueries.slice(), + }; + dispatch({ type: ActionTypes.ClickSplit, itemState }); + // TODO save state + }; +} + +export function clickTableButton(exploreId: ExploreId): ThunkResult { + return (dispatch, getState) => { + dispatch({ type: ActionTypes.ClickTableButton, exploreId }); + if (getState().explore[exploreId].showingTable) { + dispatch(runQueries(exploreId)); } }; } -export function highlightLogsExpression(expressions: string[]): HighlightLogsExpressionAction { - return { type: ActionTypes.HighlightLogsExpression, expressions }; +export function highlightLogsExpression(exploreId: ExploreId, expressions: string[]): HighlightLogsExpressionAction { + return { type: ActionTypes.HighlightLogsExpression, exploreId, expressions }; } export function initializeExplore( + exploreId: ExploreId, datasource: string, queries: DataQuery[], range: RawTimeRange, @@ -320,6 +389,7 @@ export function initializeExplore( dispatch({ type: ActionTypes.InitializeExplore, + exploreId, containerWidth, datasource, eventBridge, @@ -335,26 +405,35 @@ export function initializeExplore( } else { instance = await getDatasourceSrv().get(); } - dispatch(loadDatasource(instance)); + dispatch(loadDatasource(exploreId, instance)); } else { - dispatch(loadDatasourceMissing); + dispatch(loadDatasourceMissing(exploreId)); } }; } -export const loadDatasourceFailure = (error: string): LoadDatasourceFailureAction => ({ +export const loadDatasourceFailure = (exploreId: ExploreId, error: string): LoadDatasourceFailureAction => ({ type: ActionTypes.LoadDatasourceFailure, + exploreId, error, }); -export const loadDatasourceMissing: LoadDatasourceMissingAction = { type: ActionTypes.LoadDatasourceMissing }; +export const loadDatasourceMissing = (exploreId: ExploreId): LoadDatasourceMissingAction => ({ + type: ActionTypes.LoadDatasourceMissing, + exploreId, +}); -export const loadDatasourcePending = (datasourceId: number): LoadDatasourcePendingAction => ({ +export const loadDatasourcePending = (exploreId: ExploreId, datasourceId: number): LoadDatasourcePendingAction => ({ type: ActionTypes.LoadDatasourcePending, + exploreId, datasourceId, }); -export const loadDatasourceSuccess = (instance: any, queries: DataQuery[]): LoadDatasourceSuccessAction => { +export const loadDatasourceSuccess = ( + exploreId: ExploreId, + instance: any, + queries: DataQuery[] +): LoadDatasourceSuccessAction => { // Capabilities const supportsGraph = instance.meta.metrics; const supportsLogs = instance.meta.logs; @@ -369,6 +448,7 @@ export const loadDatasourceSuccess = (instance: any, queries: DataQuery[]): Load return { type: ActionTypes.LoadDatasourceSuccess, + exploreId, StartPage, datasourceInstance: instance, history, @@ -381,12 +461,12 @@ export const loadDatasourceSuccess = (instance: any, queries: DataQuery[]): Load }; }; -export function loadDatasource(instance: any): ThunkResult { +export function loadDatasource(exploreId: ExploreId, instance: any): ThunkResult { return async (dispatch, getState) => { const datasourceId = instance.meta.id; // Keep ID to track selection - dispatch(loadDatasourcePending(datasourceId)); + dispatch(loadDatasourcePending(exploreId, datasourceId)); let datasourceError = null; try { @@ -396,11 +476,11 @@ export function loadDatasource(instance: any): ThunkResult { datasourceError = (error && error.statusText) || 'Network error'; } if (datasourceError) { - dispatch(loadDatasourceFailure(datasourceError)); + dispatch(loadDatasourceFailure(exploreId, datasourceError)); return; } - if (datasourceId !== getState().explore.requestedDatasourceId) { + if (datasourceId !== getState().explore[exploreId].requestedDatasourceId) { // User already changed datasource again, discard results return; } @@ -410,9 +490,9 @@ export function loadDatasource(instance: any): ThunkResult { } // Check if queries can be imported from previously selected datasource - const queries = getState().explore.modifiedQueries; + const queries = getState().explore[exploreId].modifiedQueries; let importedQueries = queries; - const origin = getState().explore.datasourceInstance; + const origin = getState().explore[exploreId].datasourceInstance; if (origin) { if (origin.meta.id === instance.meta.id) { // Keep same queries if same type of datasource @@ -426,7 +506,7 @@ export function loadDatasource(instance: any): ThunkResult { } } - if (datasourceId !== getState().explore.requestedDatasourceId) { + if (datasourceId !== getState().explore[exploreId].requestedDatasourceId) { // User already changed datasource again, discard results return; } @@ -437,23 +517,33 @@ export function loadDatasource(instance: any): ThunkResult { ...generateEmptyQuery(i), })); - dispatch(loadDatasourceSuccess(instance, nextQueries)); - dispatch(runQueries()); + dispatch(loadDatasourceSuccess(exploreId, instance, nextQueries)); + dispatch(runQueries(exploreId)); }; } -export function modifyQueries(modification: any, index: number, modifier: any): ThunkResult { +export function modifyQueries( + exploreId: ExploreId, + modification: any, + index: number, + modifier: any +): ThunkResult { return dispatch => { - dispatch({ type: ActionTypes.ModifyQueries, modification, index, modifier }); + dispatch({ type: ActionTypes.ModifyQueries, exploreId, modification, index, modifier }); if (!modification.preventSubmit) { - dispatch(runQueries()); + dispatch(runQueries(exploreId)); } }; } -export function queryTransactionFailure(transactionId: string, response: any, datasourceId: string): ThunkResult { +export function queryTransactionFailure( + exploreId: ExploreId, + transactionId: string, + response: any, + datasourceId: string +): ThunkResult { return (dispatch, getState) => { - const { datasourceInstance, queryTransactions } = getState().explore; + const { datasourceInstance, queryTransactions } = getState().explore[exploreId]; if (datasourceInstance.meta.id !== datasourceId || response.cancelled) { // Navigated away, queries did not matter return; @@ -500,19 +590,21 @@ export function queryTransactionFailure(transactionId: string, response: any, da return qt; }); - dispatch({ type: ActionTypes.QueryTransactionFailure, queryTransactions: nextQueryTransactions }); + dispatch({ type: ActionTypes.QueryTransactionFailure, exploreId, queryTransactions: nextQueryTransactions }); }; } export function queryTransactionStart( + exploreId: ExploreId, transaction: QueryTransaction, resultType: ResultType, rowIndex: number ): QueryTransactionStartAction { - return { type: ActionTypes.QueryTransactionStart, resultType, rowIndex, transaction }; + return { type: ActionTypes.QueryTransactionStart, exploreId, resultType, rowIndex, transaction }; } export function queryTransactionSuccess( + exploreId: ExploreId, transactionId: string, result: any, latency: number, @@ -520,7 +612,7 @@ export function queryTransactionSuccess( datasourceId: string ): ThunkResult { return (dispatch, getState) => { - const { datasourceInstance, history, queryTransactions, scanner, scanning } = getState().explore; + const { datasourceInstance, history, queryTransactions, scanner, scanning } = getState().explore[exploreId]; // If datasource already changed, results do not matter if (datasourceInstance.meta.id !== datasourceId) { @@ -558,6 +650,7 @@ export function queryTransactionSuccess( dispatch({ type: ActionTypes.QueryTransactionSuccess, + exploreId, history: nextHistory, queryTransactions: nextQueryTransactions, }); @@ -568,24 +661,24 @@ export function queryTransactionSuccess( const other = nextQueryTransactions.find(qt => qt.scanning && !qt.done); if (!other) { const range = scanner(); - dispatch({ type: ActionTypes.ScanRange, range }); + dispatch({ type: ActionTypes.ScanRange, exploreId, range }); } } else { // We can stop scanning if we have a result - dispatch(scanStop()); + dispatch(scanStop(exploreId)); } } }; } -export function removeQueryRow(index: number): ThunkResult { +export function removeQueryRow(exploreId: ExploreId, index: number): ThunkResult { return dispatch => { - dispatch({ type: ActionTypes.RemoveQueryRow, index }); - dispatch(runQueries()); + dispatch({ type: ActionTypes.RemoveQueryRow, exploreId, index }); + dispatch(runQueries(exploreId)); }; } -export function runQueries() { +export function runQueries(exploreId: ExploreId) { return (dispatch, getState) => { const { datasourceInstance, @@ -596,10 +689,10 @@ export function runQueries() { supportsGraph, supportsLogs, supportsTable, - } = getState().explore; + } = getState().explore[exploreId]; if (!hasNonEmptyQuery(modifiedQueries)) { - dispatch({ type: ActionTypes.RunQueriesEmpty }); + dispatch({ type: ActionTypes.RunQueriesEmpty, exploreId }); return; } @@ -611,6 +704,7 @@ export function runQueries() { if (showingTable && supportsTable) { dispatch( runQueriesForType( + exploreId, 'Table', { interval, @@ -625,6 +719,7 @@ export function runQueries() { if (showingGraph && supportsGraph) { dispatch( runQueriesForType( + exploreId, 'Graph', { interval, @@ -636,13 +731,18 @@ export function runQueries() { ); } if (showingLogs && supportsLogs) { - dispatch(runQueriesForType('Logs', { interval, format: 'logs' })); + dispatch(runQueriesForType(exploreId, 'Logs', { interval, format: 'logs' })); } // TODO save state }; } -function runQueriesForType(resultType: ResultType, queryOptions: QueryOptions, resultGetter?: any) { +function runQueriesForType( + exploreId: ExploreId, + resultType: ResultType, + queryOptions: QueryOptions, + resultGetter?: any +) { return async (dispatch, getState) => { const { datasourceInstance, @@ -651,7 +751,7 @@ function runQueriesForType(resultType: ResultType, queryOptions: QueryOptions, r queryIntervals, range, scanning, - } = getState().explore; + } = getState().explore[exploreId]; const datasourceId = datasourceInstance.meta.id; // Run all queries concurrently @@ -665,30 +765,30 @@ function runQueriesForType(resultType: ResultType, queryOptions: QueryOptions, r queryIntervals, scanning ); - dispatch(queryTransactionStart(transaction, resultType, rowIndex)); + dispatch(queryTransactionStart(exploreId, transaction, resultType, rowIndex)); try { const now = Date.now(); const res = await datasourceInstance.query(transaction.options); eventBridge.emit('data-received', res.data || []); const latency = Date.now() - now; const results = resultGetter ? resultGetter(res.data) : res.data; - dispatch(queryTransactionSuccess(transaction.id, results, latency, queries, datasourceId)); + dispatch(queryTransactionSuccess(exploreId, transaction.id, results, latency, queries, datasourceId)); } catch (response) { eventBridge.emit('data-error', response); - dispatch(queryTransactionFailure(transaction.id, response, datasourceId)); + dispatch(queryTransactionFailure(exploreId, transaction.id, response, datasourceId)); } }); }; } -export function scanStart(scanner: RangeScanner): ThunkResult { +export function scanStart(exploreId: ExploreId, scanner: RangeScanner): ThunkResult { return dispatch => { - dispatch({ type: ActionTypes.ScanStart, scanner }); + dispatch({ type: ActionTypes.ScanStart, exploreId, scanner }); const range = scanner(); - dispatch({ type: ActionTypes.ScanRange, range }); + dispatch({ type: ActionTypes.ScanRange, exploreId, range }); }; } -export function scanStop(): ScanStopAction { - return { type: ActionTypes.ScanStop }; +export function scanStop(exploreId: ExploreId): ScanStopAction { + return { type: ActionTypes.ScanStop, exploreId }; } diff --git a/public/app/features/explore/state/reducers.ts b/public/app/features/explore/state/reducers.ts index b49d54405d1..cea4d766c58 100644 --- a/public/app/features/explore/state/reducers.ts +++ b/public/app/features/explore/state/reducers.ts @@ -16,7 +16,14 @@ import { LogsModel } from 'app/core/logs_model'; import TableModel from 'app/core/table_model'; // TODO move to types + export interface ExploreState { + split: boolean; + left: ExploreItemState; + right: ExploreItemState; +} + +export interface ExploreItemState { StartPage?: any; containerWidth: number; datasourceInstance: any; @@ -57,7 +64,7 @@ export const DEFAULT_RANGE = { // Millies step for helper bar charts const DEFAULT_GRAPH_INTERVAL = 15 * 1000; -const initialExploreState: ExploreState = { +const makeExploreItemState = (): ExploreItemState => ({ StartPage: undefined, containerWidth: 0, datasourceInstance: null, @@ -79,9 +86,15 @@ const initialExploreState: ExploreState = { supportsGraph: null, supportsLogs: null, supportsTable: null, +}); + +const initialExploreState: ExploreState = { + split: false, + left: makeExploreItemState(), + right: makeExploreItemState(), }; -export const exploreReducer = (state = initialExploreState, action: Action): ExploreState => { +const itemReducer = (state, action: Action): ExploreItemState => { switch (action.type) { case ActionTypes.AddQueryRow: { const { initialQueries, modifiedQueries, queryTransactions } = state; @@ -407,6 +420,36 @@ export const exploreReducer = (state = initialExploreState, action: Action): Exp return state; }; +export const exploreReducer = (state = initialExploreState, action: Action): ExploreState => { + switch (action.type) { + case ActionTypes.ClickCloseSplit: { + return { + ...state, + split: false, + }; + } + + case ActionTypes.ClickSplit: { + return { + ...state, + split: true, + right: action.itemState, + }; + } + } + + const { exploreId } = action as any; + if (exploreId !== undefined) { + const exploreItemState = state[exploreId]; + return { + ...state, + [exploreId]: itemReducer(exploreItemState, action), + }; + } + + return state; +}; + export default { explore: exploreReducer, }; diff --git a/public/app/types/explore.ts b/public/app/types/explore.ts index c64ce6133cf..525c8f74c81 100644 --- a/public/app/types/explore.ts +++ b/public/app/types/explore.ts @@ -75,6 +75,11 @@ export interface CompletionItemGroup { skipSort?: boolean; } +export enum ExploreId { + left = 'left', + right = 'right', +} + export interface HistoryItem { ts: number; query: DataQuery; diff --git a/public/sass/pages/_explore.scss b/public/sass/pages/_explore.scss index 098dae1a4a2..abd13a10368 100644 --- a/public/sass/pages/_explore.scss +++ b/public/sass/pages/_explore.scss @@ -1,5 +1,5 @@ .explore { - width: 100%; + flex: 1 1 auto; &-container { padding: $dashboard-padding; From be172d3e4a4c22e6f389a85aaa0fec02fe9ff75a Mon Sep 17 00:00:00 2001 From: David Kaltschmidt Date: Sat, 12 Jan 2019 23:22:28 +0100 Subject: [PATCH 3/9] Save state in URL and fix tests --- public/app/core/utils/explore.test.ts | 90 +++++++------------ public/app/core/utils/explore.ts | 7 +- public/app/features/explore/Explore.tsx | 17 ++-- public/app/features/explore/Wrapper.tsx | 48 +++++----- public/app/features/explore/state/actions.ts | 60 +++++++++++-- public/app/features/explore/state/reducers.ts | 14 ++- 6 files changed, 131 insertions(+), 105 deletions(-) diff --git a/public/app/core/utils/explore.test.ts b/public/app/core/utils/explore.test.ts index a3b08516d16..32135eab90a 100644 --- a/public/app/core/utils/explore.test.ts +++ b/public/app/core/utils/explore.test.ts @@ -6,26 +6,13 @@ import { clearHistory, hasNonEmptyQuery, } from './explore'; -import { ExploreState } from 'app/types/explore'; +import { ExploreUrlState } from 'app/types/explore'; import store from 'app/core/store'; -const DEFAULT_EXPLORE_STATE: ExploreState = { +const DEFAULT_EXPLORE_STATE: ExploreUrlState = { datasource: null, - datasourceError: null, - datasourceLoading: null, - datasourceMissing: false, - exploreDatasources: [], - graphInterval: 1000, - history: [], - initialQueries: [], - queryTransactions: [], + queries: [], range: DEFAULT_RANGE, - showingGraph: true, - showingLogs: true, - showingTable: true, - supportsGraph: null, - supportsLogs: null, - supportsTable: null, }; describe('state functions', () => { @@ -68,21 +55,19 @@ describe('state functions', () => { it('returns url parameter value for a state object', () => { const state = { ...DEFAULT_EXPLORE_STATE, - initialDatasource: 'foo', + datasource: 'foo', + queries: [ + { + expr: 'metric{test="a/b"}', + }, + { + expr: 'super{foo="x/z"}', + }, + ], range: { from: 'now-5h', to: 'now', }, - initialQueries: [ - { - refId: '1', - expr: 'metric{test="a/b"}', - }, - { - refId: '2', - expr: 'super{foo="x/z"}', - }, - ], }; expect(serializeStateToUrlParam(state)).toBe( '{"datasource":"foo","queries":[{"expr":"metric{test=\\"a/b\\"}"},' + @@ -93,21 +78,19 @@ describe('state functions', () => { it('returns url parameter value for a state object', () => { const state = { ...DEFAULT_EXPLORE_STATE, - initialDatasource: 'foo', + datasource: 'foo', + queries: [ + { + expr: 'metric{test="a/b"}', + }, + { + expr: 'super{foo="x/z"}', + }, + ], range: { from: 'now-5h', to: 'now', }, - initialQueries: [ - { - refId: '1', - expr: 'metric{test="a/b"}', - }, - { - refId: '2', - expr: 'super{foo="x/z"}', - }, - ], }; expect(serializeStateToUrlParam(state, true)).toBe( '["now-5h","now","foo",{"expr":"metric{test=\\"a/b\\"}"},{"expr":"super{foo=\\"x/z\\"}"}]' @@ -119,35 +102,24 @@ describe('state functions', () => { it('can parse the serialized state into the original state', () => { const state = { ...DEFAULT_EXPLORE_STATE, - initialDatasource: 'foo', + datasource: 'foo', + queries: [ + { + expr: 'metric{test="a/b"}', + }, + { + expr: 'super{foo="x/z"}', + }, + ], range: { from: 'now - 5h', to: 'now', }, - initialQueries: [ - { - refId: '1', - expr: 'metric{test="a/b"}', - }, - { - refId: '2', - expr: 'super{foo="x/z"}', - }, - ], }; const serialized = serializeStateToUrlParam(state); const parsed = parseUrlState(serialized); - // Account for datasource vs datasourceName - const { datasource, queries, ...rest } = parsed; - const resultState = { - ...rest, - datasource: DEFAULT_EXPLORE_STATE.datasource, - initialDatasource: datasource, - initialQueries: queries, - }; - - expect(state).toMatchObject(resultState); + expect(state).toMatchObject(parsed); }); }); }); diff --git a/public/app/core/utils/explore.ts b/public/app/core/utils/explore.ts index 026d1ba324c..b0dcf2117d0 100644 --- a/public/app/core/utils/explore.ts +++ b/public/app/core/utils/explore.ts @@ -142,7 +142,7 @@ export function buildQueryTransaction( }; } -const clearQueryKeys: ((query: DataQuery) => object) = ({ key, refId, ...rest }) => rest; +export const clearQueryKeys: ((query: DataQuery) => object) = ({ key, refId, ...rest }) => rest; export function parseUrlState(initial: string | undefined): ExploreUrlState { if (initial) { @@ -169,11 +169,6 @@ export function parseUrlState(initial: string | undefined): ExploreUrlState { } export function serializeStateToUrlParam(urlState: ExploreUrlState, compact?: boolean): string { - // const urlState: ExploreUrlState = { - // datasource: state.initialDatasource, - // queries: state.initialQueries.map(clearQueryKeys), - // range: state.range, - // }; if (compact) { return JSON.stringify([urlState.range.from, urlState.range.to, urlState.datasource, ...urlState.queries]); } diff --git a/public/app/features/explore/Explore.tsx b/public/app/features/explore/Explore.tsx index a3177bebe79..b0a17884fc3 100644 --- a/public/app/features/explore/Explore.tsx +++ b/public/app/features/explore/Explore.tsx @@ -13,6 +13,8 @@ import store from 'app/core/store'; import { LAST_USED_DATASOURCE_KEY, ensureQueries, DEFAULT_RANGE } from 'app/core/utils/explore'; import { DataSourcePicker } from 'app/core/components/Select/DataSourcePicker'; import { Emitter } from 'app/core/utils/emitter'; +import { LogsModel } from 'app/core/logs_model'; +import TableModel from 'app/core/table_model'; import { addQueryRow, @@ -45,8 +47,6 @@ import Table from './Table'; import ErrorBoundary from './ErrorBoundary'; import { Alert } from './Error'; import TimePicker, { parseTime } from './TimePicker'; -import { LogsModel } from 'app/core/logs_model'; -import TableModel from 'app/core/table_model'; interface ExploreProps { StartPage?: any; @@ -74,6 +74,7 @@ interface ExploreProps { initialDatasource?: string; initialQueries: DataQuery[]; initializeExplore: typeof initializeExplore; + initialized: boolean; logsHighlighterExpressions?: string[]; logsResult?: LogsModel; modifyQueries: typeof modifyQueries; @@ -149,8 +150,9 @@ export class Explore extends React.PureComponent { } async componentDidMount() { - const { exploreId, split, urlState } = this.props; - if (!split) { + const { exploreId, initialized, urlState } = this.props; + // Don't initialize on split, but need to initialize urlparameters when present + if (!initialized) { // Load URL state and parse range const { datasource, queries, range = DEFAULT_RANGE } = (urlState || {}) as ExploreUrlState; const initialDatasource = datasource || store.get(LAST_USED_DATASOURCE_KEY); @@ -277,11 +279,6 @@ export class Explore extends React.PureComponent { } }, 500); - // saveState = () => { - // const { stateKey, onSaveState } = this.props; - // onSaveState(stateKey, this.cloneState()); - // }; - render() { const { StartPage, @@ -478,6 +475,7 @@ function mapStateToProps(state: StoreState, { exploreId }) { graphResult, initialDatasource, initialQueries, + initialized, history, logsHighlighterExpressions, logsResult, @@ -504,6 +502,7 @@ function mapStateToProps(state: StoreState, { exploreId }) { graphResult, initialDatasource, initialQueries, + initialized, history, logsHighlighterExpressions, logsResult, diff --git a/public/app/features/explore/Wrapper.tsx b/public/app/features/explore/Wrapper.tsx index 04f189749bf..7ea8f228af8 100644 --- a/public/app/features/explore/Wrapper.tsx +++ b/public/app/features/explore/Wrapper.tsx @@ -3,51 +3,56 @@ import { hot } from 'react-hot-loader'; import { connect } from 'react-redux'; import { updateLocation } from 'app/core/actions'; -// import { serializeStateToUrlParam, parseUrlState } from 'app/core/utils/explore'; import { StoreState } from 'app/types'; -import { ExploreId } from 'app/types/explore'; +import { ExploreId, ExploreUrlState } from 'app/types/explore'; +import { parseUrlState } from 'app/core/utils/explore'; +import { initializeExploreSplit } from './state/actions'; import ErrorBoundary from './ErrorBoundary'; import Explore from './Explore'; interface WrapperProps { - backendSrv?: any; - datasourceSrv?: any; + initializeExploreSplit: typeof initializeExploreSplit; split: boolean; updateLocation: typeof updateLocation; - // urlStates: { [key: string]: string }; + urlStates: { [key: string]: string }; } export class Wrapper extends Component { - // urlStates: { [key: string]: string }; + initialSplit: boolean; + urlStates: { [key: string]: ExploreUrlState }; constructor(props: WrapperProps) { super(props); - // this.urlStates = props.urlStates; + this.urlStates = {}; + const { left, right } = props.urlStates; + if (props.urlStates.left) { + this.urlStates.leftState = parseUrlState(left); + } + if (props.urlStates.right) { + this.urlStates.rightState = parseUrlState(right); + this.initialSplit = true; + } } - // onSaveState = (key: string, state: ExploreState) => { - // const urlState = serializeStateToUrlParam(state, true); - // this.urlStates[key] = urlState; - // this.props.updateLocation({ - // query: this.urlStates, - // }); - // }; + componentDidMount() { + if (this.initialSplit) { + this.props.initializeExploreSplit(); + } + } render() { const { split } = this.props; - // State overrides for props from first Explore - // const urlStateLeft = parseUrlState(this.urlStates[STATE_KEY_LEFT]); - // const urlStateRight = parseUrlState(this.urlStates[STATE_KEY_RIGHT]); + const { leftState, rightState } = this.urlStates; return (
- + {split && ( - + )}
@@ -56,12 +61,13 @@ export class Wrapper extends Component { } const mapStateToProps = (state: StoreState) => { - // urlStates: state.location.query, + const urlStates = state.location.query; const { split } = state.explore; - return { split }; + return { split, urlStates }; }; const mapDispatchToProps = { + initializeExploreSplit, updateLocation, }; diff --git a/public/app/features/explore/state/actions.ts b/public/app/features/explore/state/actions.ts index 145b7506e79..979c80395c3 100644 --- a/public/app/features/explore/state/actions.ts +++ b/public/app/features/explore/state/actions.ts @@ -4,14 +4,17 @@ import { RawTimeRange, TimeRange } from '@grafana/ui'; import { LAST_USED_DATASOURCE_KEY, + clearQueryKeys, ensureQueries, generateEmptyQuery, hasNonEmptyQuery, makeTimeSeriesList, updateHistory, buildQueryTransaction, + serializeStateToUrlParam, } from 'app/core/utils/explore'; +import { updateLocation } from 'app/core/actions'; import store from 'app/core/store'; import { DataSourceSelectItem } from 'app/types/datasources'; import { DataQuery, StoreState } from 'app/types'; @@ -25,6 +28,7 @@ import { QueryTransaction, QueryHint, QueryHintGetter, + ExploreUrlState, } from 'app/types/explore'; import { Emitter } from 'app/core/core'; import { ExploreItemState } from './reducers'; @@ -44,6 +48,7 @@ export enum ActionTypes { ClickTableButton = 'CLICK_TABLE_BUTTON', HighlightLogsExpression = 'HIGHLIGHT_LOGS_EXPRESSION', InitializeExplore = 'INITIALIZE_EXPLORE', + InitializeExploreSplit = 'INITIALIZE_EXPLORE_SPLIT', LoadDatasourceFailure = 'LOAD_DATASOURCE_FAILURE', LoadDatasourceMissing = 'LOAD_DATASOURCE_MISSING', LoadDatasourcePending = 'LOAD_DATASOURCE_PENDING', @@ -58,6 +63,7 @@ export enum ActionTypes { ScanRange = 'SCAN_RANGE', ScanStart = 'SCAN_START', ScanStop = 'SCAN_STOP', + StateSave = 'STATE_SAVE', } export interface AddQueryRowAction { @@ -123,6 +129,12 @@ export interface ClickTableButtonAction { exploreId: ExploreId; } +export interface HighlightLogsExpressionAction { + type: ActionTypes.HighlightLogsExpression; + exploreId: ExploreId; + expressions: string[]; +} + export interface InitializeExploreAction { type: ActionTypes.InitializeExplore; exploreId: ExploreId; @@ -134,10 +146,8 @@ export interface InitializeExploreAction { range: RawTimeRange; } -export interface HighlightLogsExpressionAction { - type: ActionTypes.HighlightLogsExpression; - exploreId: ExploreId; - expressions: string[]; +export interface InitializeExploreSplitAction { + type: ActionTypes.InitializeExploreSplit; } export interface LoadDatasourceFailureAction { @@ -224,6 +234,10 @@ export interface ScanStopAction { exploreId: ExploreId; } +export interface StateSaveAction { + type: ActionTypes.StateSave; +} + export type Action = | AddQueryRowAction | ChangeQueryAction @@ -238,6 +252,7 @@ export type Action = | ClickTableButtonAction | HighlightLogsExpressionAction | InitializeExploreAction + | InitializeExploreSplitAction | LoadDatasourceFailureAction | LoadDatasourceMissingAction | LoadDatasourcePendingAction @@ -301,15 +316,14 @@ export function clickClear(exploreId: ExploreId): ThunkResult { return dispatch => { dispatch(scanStop(exploreId)); dispatch({ type: ActionTypes.ClickClear, exploreId }); - // TODO save state + dispatch(stateSave()); }; } export function clickCloseSplit(): ThunkResult { return dispatch => { dispatch({ type: ActionTypes.ClickCloseSplit }); - // When closing split, remove URL state for split part - // TODO save state + dispatch(stateSave()); }; } @@ -353,7 +367,7 @@ export function clickSplit(): ThunkResult { initialQueries: leftState.modifiedQueries.slice(), }; dispatch({ type: ActionTypes.ClickSplit, itemState }); - // TODO save state + dispatch(stateSave()); }; } @@ -412,6 +426,12 @@ export function initializeExplore( }; } +export function initializeExploreSplit() { + return async dispatch => { + dispatch({ type: ActionTypes.InitializeExploreSplit }); + }; +} + export const loadDatasourceFailure = (exploreId: ExploreId, error: string): LoadDatasourceFailureAction => ({ type: ActionTypes.LoadDatasourceFailure, exploreId, @@ -733,7 +753,7 @@ export function runQueries(exploreId: ExploreId) { if (showingLogs && supportsLogs) { dispatch(runQueriesForType(exploreId, 'Logs', { interval, format: 'logs' })); } - // TODO save state + dispatch(stateSave()); }; } @@ -792,3 +812,25 @@ export function scanStart(exploreId: ExploreId, scanner: RangeScanner): ThunkRes export function scanStop(exploreId: ExploreId): ScanStopAction { return { type: ActionTypes.ScanStop, exploreId }; } + +export function stateSave() { + return (dispatch, getState) => { + const { left, right, split } = getState().explore; + const urlStates: { [index: string]: string } = {}; + const leftUrlState: ExploreUrlState = { + datasource: left.datasourceInstance.name, + queries: left.modifiedQueries.map(clearQueryKeys), + range: left.range, + }; + urlStates.left = serializeStateToUrlParam(leftUrlState, true); + if (split) { + const rightUrlState: ExploreUrlState = { + datasource: right.datasourceInstance.name, + queries: right.modifiedQueries.map(clearQueryKeys), + range: right.range, + }; + urlStates.right = serializeStateToUrlParam(rightUrlState, true); + } + dispatch(updateLocation({ query: urlStates })); + }; +} diff --git a/public/app/features/explore/state/reducers.ts b/public/app/features/explore/state/reducers.ts index cea4d766c58..dda3b37fdae 100644 --- a/public/app/features/explore/state/reducers.ts +++ b/public/app/features/explore/state/reducers.ts @@ -36,6 +36,7 @@ export interface ExploreItemState { history: HistoryItem[]; initialDatasource?: string; initialQueries: DataQuery[]; + initialized: boolean; logsHighlighterExpressions?: string[]; logsResult?: LogsModel; modifiedQueries: DataQuery[]; @@ -74,6 +75,7 @@ const makeExploreItemState = (): ExploreItemState => ({ exploreDatasources: [], history: [], initialQueries: [], + initialized: false, modifiedQueries: [], queryTransactions: [], queryIntervals: { interval: '15s', intervalMs: DEFAULT_GRAPH_INTERVAL }, @@ -89,7 +91,7 @@ const makeExploreItemState = (): ExploreItemState => ({ }); const initialExploreState: ExploreState = { - split: false, + split: null, left: makeExploreItemState(), right: makeExploreItemState(), }; @@ -236,6 +238,7 @@ const itemReducer = (state, action: Action): ExploreItemState => { range, initialDatasource: action.datasource, initialQueries: action.queries, + initialized: true, modifiedQueries: action.queries.slice(), }; } @@ -436,6 +439,13 @@ export const exploreReducer = (state = initialExploreState, action: Action): Exp right: action.itemState, }; } + + case ActionTypes.InitializeExploreSplit: { + return { + ...state, + split: true, + }; + } } const { exploreId } = action as any; @@ -447,6 +457,8 @@ export const exploreReducer = (state = initialExploreState, action: Action): Exp }; } + console.error('Unhandled action', action.type); + return state; }; From f02f41c9b0d753e00397fb3d26a5e81a3cbb8aad Mon Sep 17 00:00:00 2001 From: David Kaltschmidt Date: Sat, 12 Jan 2019 23:28:51 +0100 Subject: [PATCH 4/9] Move types to types/explore --- public/app/features/explore/Explore.tsx | 10 ++- public/app/features/explore/state/actions.ts | 4 +- public/app/features/explore/state/reducers.ts | 50 +---------- public/app/types/explore.ts | 82 ++++++++++++------- public/app/types/index.ts | 2 +- 5 files changed, 64 insertions(+), 84 deletions(-) diff --git a/public/app/features/explore/Explore.tsx b/public/app/features/explore/Explore.tsx index b0a17884fc3..b34986f81ef 100644 --- a/public/app/features/explore/Explore.tsx +++ b/public/app/features/explore/Explore.tsx @@ -6,7 +6,14 @@ import { AutoSizer } from 'react-virtualized'; import { RawTimeRange, TimeRange } from '@grafana/ui'; import { DataSourceSelectItem } from 'app/types/datasources'; -import { ExploreUrlState, HistoryItem, QueryTransaction, RangeScanner, ExploreId } from 'app/types/explore'; +import { + ExploreItemState, + ExploreUrlState, + HistoryItem, + QueryTransaction, + RangeScanner, + ExploreId, +} from 'app/types/explore'; import { DataQuery } from 'app/types/series'; import { StoreState } from 'app/types'; import store from 'app/core/store'; @@ -37,7 +44,6 @@ import { scanStart, scanStop, } from './state/actions'; -import { ExploreItemState } from './state/reducers'; import Panel from './Panel'; import QueryRows from './QueryRows'; diff --git a/public/app/features/explore/state/actions.ts b/public/app/features/explore/state/actions.ts index 979c80395c3..26811606bd9 100644 --- a/public/app/features/explore/state/actions.ts +++ b/public/app/features/explore/state/actions.ts @@ -21,6 +21,8 @@ import { DataQuery, StoreState } from 'app/types'; import { getDatasourceSrv } from 'app/features/plugins/datasource_srv'; import { ExploreId, + ExploreItemState, + ExploreUrlState, HistoryItem, RangeScanner, ResultType, @@ -28,10 +30,8 @@ import { QueryTransaction, QueryHint, QueryHintGetter, - ExploreUrlState, } from 'app/types/explore'; import { Emitter } from 'app/core/core'; -import { ExploreItemState } from './reducers'; export enum ActionTypes { AddQueryRow = 'ADD_QUERY_ROW', diff --git a/public/app/features/explore/state/reducers.ts b/public/app/features/explore/state/reducers.ts index dda3b37fdae..9474bc717e0 100644 --- a/public/app/features/explore/state/reducers.ts +++ b/public/app/features/explore/state/reducers.ts @@ -1,61 +1,13 @@ -import { RawTimeRange, TimeRange } from '@grafana/ui'; - import { calculateResultsFromQueryTransactions, generateEmptyQuery, getIntervals, ensureQueries, } from 'app/core/utils/explore'; -import { DataSourceSelectItem } from 'app/types/datasources'; -import { HistoryItem, QueryTransaction, QueryIntervals, RangeScanner } from 'app/types/explore'; +import { ExploreItemState, ExploreState, QueryTransaction } from 'app/types/explore'; import { DataQuery } from 'app/types/series'; import { Action, ActionTypes } from './actions'; -import { Emitter } from 'app/core/core'; -import { LogsModel } from 'app/core/logs_model'; -import TableModel from 'app/core/table_model'; - -// TODO move to types - -export interface ExploreState { - split: boolean; - left: ExploreItemState; - right: ExploreItemState; -} - -export interface ExploreItemState { - StartPage?: any; - containerWidth: number; - datasourceInstance: any; - datasourceError: string; - datasourceLoading: boolean | null; - datasourceMissing: boolean; - eventBridge?: Emitter; - exploreDatasources: DataSourceSelectItem[]; - graphResult?: any[]; - history: HistoryItem[]; - initialDatasource?: string; - initialQueries: DataQuery[]; - initialized: boolean; - logsHighlighterExpressions?: string[]; - logsResult?: LogsModel; - modifiedQueries: DataQuery[]; - queryIntervals: QueryIntervals; - queryTransactions: QueryTransaction[]; - requestedDatasourceId?: number; - range: TimeRange | RawTimeRange; - scanner?: RangeScanner; - scanning?: boolean; - scanRange?: RawTimeRange; - showingGraph: boolean; - showingLogs: boolean; - showingStartPage?: boolean; - showingTable: boolean; - supportsGraph: boolean | null; - supportsLogs: boolean | null; - supportsTable: boolean | null; - tableResult?: TableModel; -} export const DEFAULT_RANGE = { from: 'now-6h', diff --git a/public/app/types/explore.ts b/public/app/types/explore.ts index 525c8f74c81..3cef4124ee4 100644 --- a/public/app/types/explore.ts +++ b/public/app/types/explore.ts @@ -1,9 +1,12 @@ import { Value } from 'slate'; +import { RawTimeRange, TimeRange } from '@grafana/ui'; + +import { Emitter } from 'app/core/core'; +import { LogsModel } from 'app/core/logs_model'; +import TableModel from 'app/core/table_model'; +import { DataSourceSelectItem } from 'app/types/datasources'; import { DataQuery } from './series'; -import { RawTimeRange } from '@grafana/ui'; -import TableModel from 'app/core/table_model'; -import { LogsModel } from 'app/core/logs_model'; export interface CompletionItem { /** @@ -80,6 +83,52 @@ export enum ExploreId { right = 'right', } +export interface ExploreState { + split: boolean; + left: ExploreItemState; + right: ExploreItemState; +} + +export interface ExploreItemState { + StartPage?: any; + containerWidth: number; + datasourceInstance: any; + datasourceError: string; + datasourceLoading: boolean | null; + datasourceMissing: boolean; + eventBridge?: Emitter; + exploreDatasources: DataSourceSelectItem[]; + graphResult?: any[]; + history: HistoryItem[]; + initialDatasource?: string; + initialQueries: DataQuery[]; + initialized: boolean; + logsHighlighterExpressions?: string[]; + logsResult?: LogsModel; + modifiedQueries: DataQuery[]; + queryIntervals: QueryIntervals; + queryTransactions: QueryTransaction[]; + requestedDatasourceId?: number; + range: TimeRange | RawTimeRange; + scanner?: RangeScanner; + scanning?: boolean; + scanRange?: RawTimeRange; + showingGraph: boolean; + showingLogs: boolean; + showingStartPage?: boolean; + showingTable: boolean; + supportsGraph: boolean | null; + supportsLogs: boolean | null; + supportsTable: boolean | null; + tableResult?: TableModel; +} + +export interface ExploreUrlState { + datasource: string; + queries: any[]; // Should be a DataQuery, but we're going to strip refIds, so typing makes less sense + range: RawTimeRange; +} + export interface HistoryItem { ts: number; query: DataQuery; @@ -168,31 +217,4 @@ export interface TextMatch { end: number; } -export interface ExploreState { - StartPage?: any; - datasource: any; - datasourceError: any; - graphResult?: any[]; - history: HistoryItem[]; - logsHighlighterExpressions?: string[]; - logsResult?: LogsModel; - queryTransactions: QueryTransaction[]; - scanning?: boolean; - scanRange?: RawTimeRange; - showingGraph: boolean; - showingLogs: boolean; - showingStartPage?: boolean; - showingTable: boolean; - supportsGraph: boolean | null; - supportsLogs: boolean | null; - supportsTable: boolean | null; - tableResult?: TableModel; -} - -export interface ExploreUrlState { - datasource: string; - queries: any[]; // Should be a DataQuery, but we're going to strip refIds, so typing makes less sense - range: RawTimeRange; -} - export type ResultType = 'Graph' | 'Logs' | 'Table'; diff --git a/public/app/types/index.ts b/public/app/types/index.ts index 018c4c51d3d..ad9f19e2c9f 100644 --- a/public/app/types/index.ts +++ b/public/app/types/index.ts @@ -19,7 +19,7 @@ import { } from './appNotifications'; import { DashboardSearchHit } from './search'; import { ValidationEvents, ValidationRule } from './form'; -import { ExploreState } from 'app/features/explore/state/reducers'; +import { ExploreState } from './explore'; export { Team, TeamsState, From 607f7c25de670ba608ea5ba281d6af8a9e5aea6d Mon Sep 17 00:00:00 2001 From: David Kaltschmidt Date: Sat, 12 Jan 2019 23:44:24 +0100 Subject: [PATCH 5/9] Update comments --- public/app/features/explore/Explore.tsx | 24 +++++-------------- public/app/features/explore/state/reducers.ts | 15 ++++++++++-- 2 files changed, 19 insertions(+), 20 deletions(-) diff --git a/public/app/features/explore/Explore.tsx b/public/app/features/explore/Explore.tsx index b34986f81ef..2e9c71e5517 100644 --- a/public/app/features/explore/Explore.tsx +++ b/public/app/features/explore/Explore.tsx @@ -110,26 +110,14 @@ interface ExploreProps { * Once a datasource is selected it populates the query section at the top. * When queries are run, their results are being displayed in the main section. * The datasource determines what kind of query editor it brings, and what kind - * of results viewers it supports. + * of results viewers it supports. The state is managed entirely in Redux. * - * QUERY HANDLING + * SPLIT VIEW * - * TLDR: to not re-render Explore during edits, query editing is not "controlled" - * in a React sense: values need to be pushed down via `initialQueries`, while - * edits travel up via `this.modifiedQueries`. - * - * By default the query rows start without prior state: `initialQueries` will - * contain one empty DataQuery. While the user modifies the DataQuery, the - * modifications are being tracked in `this.modifiedQueries`, which need to be - * used whenever a query is sent to the datasource to reflect what the user sees - * on the screen. Query"react-popper": "^0.7.5", rows can be initialized or reset using `initialQueries`, - * by giving the respec"react-popper": "^0.7.5",tive row a new key. This wipes the old row and its state. - * This property is als"react-popper": "^0.7.5",o used to govern how many query rows there are (minimum 1). - * - * This flow makes sure that a query row can be arbitrarily complex without the - * fear of being wiped or re-initialized via props. The query row is free to keep - * its own state while the user edits or builds a query. Valid queries can be sent - * up to Explore via the `onChangeQuery` prop. + * Explore can have two Explore areas side-by-side. This is handled in `Wrapper.tsx`. + * Since there can be multiple Explores (e.g., left and right) each action needs + * the `exploreId` as first parameter so that the reducer knows which Explore state + * is affected. * * DATASOURCE REQUESTS * diff --git a/public/app/features/explore/state/reducers.ts b/public/app/features/explore/state/reducers.ts index 9474bc717e0..97a02c33e67 100644 --- a/public/app/features/explore/state/reducers.ts +++ b/public/app/features/explore/state/reducers.ts @@ -17,6 +17,9 @@ export const DEFAULT_RANGE = { // Millies step for helper bar charts const DEFAULT_GRAPH_INTERVAL = 15 * 1000; +/** + * Returns a fresh Explore area state + */ const makeExploreItemState = (): ExploreItemState => ({ StartPage: undefined, containerWidth: 0, @@ -42,12 +45,18 @@ const makeExploreItemState = (): ExploreItemState => ({ supportsTable: null, }); +/** + * Global Explore state that handles multiple Explore areas and the split state + */ const initialExploreState: ExploreState = { split: null, left: makeExploreItemState(), right: makeExploreItemState(), }; +/** + * Reducer for an Explore area, to be used by the global Explore reducer. + */ const itemReducer = (state, action: Action): ExploreItemState => { switch (action.type) { case ActionTypes.AddQueryRow: { @@ -375,6 +384,10 @@ const itemReducer = (state, action: Action): ExploreItemState => { return state; }; +/** + * Global Explore reducer that handles multiple Explore areas (left and right). + * Actions that have an `exploreId` get routed to the ExploreItemReducer. + */ export const exploreReducer = (state = initialExploreState, action: Action): ExploreState => { switch (action.type) { case ActionTypes.ClickCloseSplit: { @@ -409,8 +422,6 @@ export const exploreReducer = (state = initialExploreState, action: Action): Exp }; } - console.error('Unhandled action', action.type); - return state; }; From 546a3a9d983c001240dbd6f76c81575cb0b972fc Mon Sep 17 00:00:00 2001 From: David Kaltschmidt Date: Sun, 13 Jan 2019 23:10:23 +0100 Subject: [PATCH 6/9] Connect Explore child components to store --- public/app/features/explore/Explore.tsx | 201 ++---------------- .../app/features/explore/GraphContainer.tsx | 61 ++++++ public/app/features/explore/LogsContainer.tsx | 91 ++++++++ public/app/features/explore/QueryRow.tsx | 163 ++++++++++++++ public/app/features/explore/QueryRows.tsx | 152 +------------ .../app/features/explore/TableContainer.tsx | 49 +++++ public/app/features/explore/state/reducers.ts | 1 + 7 files changed, 394 insertions(+), 324 deletions(-) create mode 100644 public/app/features/explore/GraphContainer.tsx create mode 100644 public/app/features/explore/LogsContainer.tsx create mode 100644 public/app/features/explore/QueryRow.tsx create mode 100644 public/app/features/explore/TableContainer.tsx diff --git a/public/app/features/explore/Explore.tsx b/public/app/features/explore/Explore.tsx index 2e9c71e5517..a70135c00ad 100644 --- a/public/app/features/explore/Explore.tsx +++ b/public/app/features/explore/Explore.tsx @@ -6,86 +6,58 @@ import { AutoSizer } from 'react-virtualized'; import { RawTimeRange, TimeRange } from '@grafana/ui'; import { DataSourceSelectItem } from 'app/types/datasources'; -import { - ExploreItemState, - ExploreUrlState, - HistoryItem, - QueryTransaction, - RangeScanner, - ExploreId, -} from 'app/types/explore'; +import { ExploreItemState, ExploreUrlState, RangeScanner, ExploreId } from 'app/types/explore'; import { DataQuery } from 'app/types/series'; import { StoreState } from 'app/types'; import store from 'app/core/store'; import { LAST_USED_DATASOURCE_KEY, ensureQueries, DEFAULT_RANGE } from 'app/core/utils/explore'; import { DataSourcePicker } from 'app/core/components/Select/DataSourcePicker'; import { Emitter } from 'app/core/utils/emitter'; -import { LogsModel } from 'app/core/logs_model'; -import TableModel from 'app/core/table_model'; import { - addQueryRow, changeDatasource, - changeQuery, changeSize, changeTime, clickClear, clickCloseSplit, clickExample, - clickGraphButton, - clickLogsButton, clickSplit, - clickTableButton, - highlightLogsExpression, initializeExplore, modifyQueries, - removeQueryRow, runQueries, scanStart, scanStop, } from './state/actions'; -import Panel from './Panel'; -import QueryRows from './QueryRows'; -import Graph from './Graph'; -import Logs from './Logs'; -import Table from './Table'; -import ErrorBoundary from './ErrorBoundary'; import { Alert } from './Error'; +import ErrorBoundary from './ErrorBoundary'; +import GraphContainer from './GraphContainer'; +import LogsContainer from './LogsContainer'; +import QueryRows from './QueryRows'; +import TableContainer from './TableContainer'; import TimePicker, { parseTime } from './TimePicker'; interface ExploreProps { StartPage?: any; - addQueryRow: typeof addQueryRow; changeDatasource: typeof changeDatasource; - changeQuery: typeof changeQuery; changeSize: typeof changeSize; changeTime: typeof changeTime; clickClear: typeof clickClear; clickCloseSplit: typeof clickCloseSplit; clickExample: typeof clickExample; - clickGraphButton: typeof clickGraphButton; - clickLogsButton: typeof clickLogsButton; clickSplit: typeof clickSplit; - clickTableButton: typeof clickTableButton; datasourceError: string; datasourceInstance: any; datasourceLoading: boolean | null; datasourceMissing: boolean; exploreDatasources: DataSourceSelectItem[]; exploreId: ExploreId; - graphResult?: any[]; - highlightLogsExpression: typeof highlightLogsExpression; - history: HistoryItem[]; initialDatasource?: string; initialQueries: DataQuery[]; initializeExplore: typeof initializeExplore; initialized: boolean; - logsHighlighterExpressions?: string[]; - logsResult?: LogsModel; + loading: boolean; modifyQueries: typeof modifyQueries; - queryTransactions: QueryTransaction[]; - removeQueryRow: typeof removeQueryRow; range: RawTimeRange; runQueries: typeof runQueries; scanner?: RangeScanner; @@ -94,14 +66,10 @@ interface ExploreProps { scanStart: typeof scanStart; scanStop: typeof scanStop; split: boolean; - showingGraph: boolean; - showingLogs: boolean; showingStartPage?: boolean; - showingTable: boolean; supportsGraph: boolean | null; supportsLogs: boolean | null; supportsTable: boolean | null; - tableResult?: TableModel; urlState: ExploreUrlState; } @@ -172,24 +140,10 @@ export class Explore extends React.PureComponent { this.el = el; }; - onAddQueryRow = index => { - this.props.addQueryRow(this.props.exploreId, index); - }; - onChangeDatasource = async option => { this.props.changeDatasource(this.props.exploreId, option.value); }; - onChangeQuery = (query: DataQuery, index: number, override?: boolean) => { - const { changeQuery, datasourceInstance, exploreId } = this.props; - - changeQuery(exploreId, query, index, override); - if (query && !override && datasourceInstance.getHighlighterExpression && index === 0) { - // Live preview of log search matches. Only use on first row for now - this.updateLogsHighlights(query); - } - }; - onChangeTime = (range: TimeRange, changedByScanner?: boolean) => { if (this.props.scanning && !changedByScanner) { this.onStopScanning(); @@ -205,14 +159,6 @@ export class Explore extends React.PureComponent { this.props.clickCloseSplit(); }; - onClickGraphButton = () => { - this.props.clickGraphButton(this.props.exploreId); - }; - - onClickLogsButton = () => { - this.props.clickLogsButton(this.props.exploreId); - }; - // Use this in help pages to set page to a single query onClickExample = (query: DataQuery) => { this.props.clickExample(this.props.exploreId, query); @@ -222,10 +168,6 @@ export class Explore extends React.PureComponent { this.props.clickSplit(); }; - onClickTableButton = () => { - this.props.clickTableButton(this.props.exploreId); - }; - onClickLabel = (key: string, value: string) => { this.onModifyQueries({ type: 'ADD_FILTER', key, value }); }; @@ -238,10 +180,6 @@ export class Explore extends React.PureComponent { } }; - onRemoveQueryRow = index => { - this.props.removeQueryRow(this.props.exploreId, index); - }; - onResize = (size: { height: number; width: number }) => { this.props.changeSize(this.props.exploreId, size); }; @@ -265,14 +203,6 @@ export class Explore extends React.PureComponent { this.props.runQueries(this.props.exploreId); }; - updateLogsHighlights = _.debounce((value: DataQuery) => { - const { datasourceInstance } = this.props; - if (datasourceInstance.getHighlighterExpression) { - const expressions = [datasourceInstance.getHighlighterExpression(value)]; - this.props.highlightLogsExpression(this.props.exploreId, expressions); - } - }, 500); - render() { const { StartPage, @@ -282,34 +212,19 @@ export class Explore extends React.PureComponent { datasourceMissing, exploreDatasources, exploreId, - graphResult, - history, + loading, initialQueries, - logsHighlighterExpressions, - logsResult, - queryTransactions, range, - scanning, - scanRange, - showingGraph, - showingLogs, showingStartPage, - showingTable, split, supportsGraph, supportsLogs, supportsTable, - tableResult, } = this.props; - const graphHeight = showingGraph && showingTable ? '200px' : '400px'; const exploreClass = split ? 'explore explore-split' : 'explore'; const selectedDatasource = datasourceInstance ? exploreDatasources.find(d => d.name === datasourceInstance.name) : undefined; - const graphLoading = queryTransactions.some(qt => qt.resultType === 'Graph' && !qt.done); - const tableLoading = queryTransactions.some(qt => qt.resultType === 'Table' && !qt.done); - const logsLoading = queryTransactions.some(qt => qt.resultType === 'Logs' && !qt.done); - const loading = queryTransactions.some(qt => !qt.done); return (
@@ -372,19 +287,7 @@ export class Explore extends React.PureComponent { {datasourceInstance && !datasourceError && (
- + {({ width }) => (
@@ -392,55 +295,16 @@ export class Explore extends React.PureComponent { {showingStartPage && } {!showingStartPage && ( <> - {supportsGraph && ( - - - - )} - {supportsTable && ( - -
- - )} + {supportsGraph && } + {supportsTable && } {supportsLogs && ( - - - + )} )} @@ -466,26 +330,17 @@ function mapStateToProps(state: StoreState, { exploreId }) { datasourceLoading, datasourceMissing, exploreDatasources, - graphResult, initialDatasource, initialQueries, initialized, - history, - logsHighlighterExpressions, - logsResult, queryTransactions, range, - scanning, - scanRange, - showingGraph, - showingLogs, showingStartPage, - showingTable, supportsGraph, supportsLogs, supportsTable, - tableResult, } = item; + const loading = queryTransactions.some(qt => !qt.done); return { StartPage, datasourceError, @@ -493,46 +348,30 @@ function mapStateToProps(state: StoreState, { exploreId }) { datasourceLoading, datasourceMissing, exploreDatasources, - graphResult, initialDatasource, initialQueries, initialized, - history, - logsHighlighterExpressions, - logsResult, + loading, queryTransactions, range, - scanning, - scanRange, - showingGraph, - showingLogs, showingStartPage, - showingTable, split, supportsGraph, supportsLogs, supportsTable, - tableResult, }; } const mapDispatchToProps = { - addQueryRow, changeDatasource, - changeQuery, changeSize, changeTime, clickClear, clickCloseSplit, clickExample, - clickGraphButton, - clickLogsButton, clickSplit, - clickTableButton, - highlightLogsExpression, initializeExplore, modifyQueries, - removeQueryRow, runQueries, scanStart, scanStop, diff --git a/public/app/features/explore/GraphContainer.tsx b/public/app/features/explore/GraphContainer.tsx new file mode 100644 index 00000000000..da098f0c92d --- /dev/null +++ b/public/app/features/explore/GraphContainer.tsx @@ -0,0 +1,61 @@ +import React, { PureComponent } from 'react'; +import { hot } from 'react-hot-loader'; +import { connect } from 'react-redux'; +import { RawTimeRange, TimeRange } from '@grafana/ui'; + +import { ExploreId, ExploreItemState } from 'app/types/explore'; +import { StoreState } from 'app/types'; + +import { clickGraphButton } from './state/actions'; +import Graph from './Graph'; +import Panel from './Panel'; + +interface GraphContainerProps { + onChangeTime: (range: TimeRange) => void; + clickGraphButton: typeof clickGraphButton; + exploreId: ExploreId; + graphResult?: any[]; + loading: boolean; + range: RawTimeRange; + showingGraph: boolean; + showingTable: boolean; + split: boolean; +} + +export class GraphContainer extends PureComponent { + onClickGraphButton = () => { + this.props.clickGraphButton(this.props.exploreId); + }; + + render() { + const { exploreId, graphResult, loading, onChangeTime, showingGraph, showingTable, range, split } = this.props; + const graphHeight = showingGraph && showingTable ? '200px' : '400px'; + return ( + + + + ); + } +} + +function mapStateToProps(state: StoreState, { exploreId }) { + const explore = state.explore; + const { split } = explore; + const item: ExploreItemState = explore[exploreId]; + const { graphResult, queryTransactions, range, showingGraph, showingTable } = item; + const loading = queryTransactions.some(qt => qt.resultType === 'Graph' && !qt.done); + return { graphResult, loading, range, showingGraph, showingTable, split }; +} + +const mapDispatchToProps = { + clickGraphButton, +}; + +export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(GraphContainer)); diff --git a/public/app/features/explore/LogsContainer.tsx b/public/app/features/explore/LogsContainer.tsx new file mode 100644 index 00000000000..db2f681a9c5 --- /dev/null +++ b/public/app/features/explore/LogsContainer.tsx @@ -0,0 +1,91 @@ +import React, { PureComponent } from 'react'; +import { hot } from 'react-hot-loader'; +import { connect } from 'react-redux'; +import { RawTimeRange, TimeRange } from '@grafana/ui'; + +import { ExploreId, ExploreItemState } from 'app/types/explore'; +import { LogsModel } from 'app/core/logs_model'; +import { StoreState } from 'app/types'; + +import { clickLogsButton } from './state/actions'; +import Logs from './Logs'; +import Panel from './Panel'; + +interface LogsContainerProps { + clickLogsButton: typeof clickLogsButton; + exploreId: ExploreId; + loading: boolean; + logsHighlighterExpressions?: string[]; + logsResult?: LogsModel; + onChangeTime: (range: TimeRange) => void; + onClickLabel: (key: string, value: string) => void; + onStartScanning: () => void; + onStopScanning: () => void; + range: RawTimeRange; + scanning?: boolean; + scanRange?: RawTimeRange; + showingLogs: boolean; +} + +export class LogsContainer extends PureComponent { + onClickLogsButton = () => { + this.props.clickLogsButton(this.props.exploreId); + }; + + render() { + const { + exploreId, + loading, + logsHighlighterExpressions, + logsResult, + onChangeTime, + onClickLabel, + onStartScanning, + onStopScanning, + range, + showingLogs, + scanning, + scanRange, + } = this.props; + return ( + + + + ); + } +} + +function mapStateToProps(state: StoreState, { exploreId }) { + const explore = state.explore; + const item: ExploreItemState = explore[exploreId]; + const { logsHighlighterExpressions, logsResult, queryTransactions, scanning, scanRange, showingLogs, range } = item; + const loading = queryTransactions.some(qt => qt.resultType === 'Logs' && !qt.done); + return { + loading, + logsHighlighterExpressions, + logsResult, + scanning, + scanRange, + showingLogs, + range, + }; +} + +const mapDispatchToProps = { + clickLogsButton, +}; + +export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(LogsContainer)); diff --git a/public/app/features/explore/QueryRow.tsx b/public/app/features/explore/QueryRow.tsx new file mode 100644 index 00000000000..b5b150b3ba8 --- /dev/null +++ b/public/app/features/explore/QueryRow.tsx @@ -0,0 +1,163 @@ +import React, { PureComponent } from 'react'; +import { hot } from 'react-hot-loader'; +import { connect } from 'react-redux'; +import { RawTimeRange } from '@grafana/ui'; +import _ from 'lodash'; + +import { QueryTransaction, HistoryItem, QueryHint, ExploreItemState, ExploreId } from 'app/types/explore'; +import { Emitter } from 'app/core/utils/emitter'; +import { DataQuery, StoreState } from 'app/types'; + +// import DefaultQueryField from './QueryField'; +import QueryEditor from './QueryEditor'; +import QueryTransactionStatus from './QueryTransactionStatus'; +import { + addQueryRow, + changeQuery, + highlightLogsExpression, + modifyQueries, + removeQueryRow, + runQueries, +} from './state/actions'; + +function getFirstHintFromTransactions(transactions: QueryTransaction[]): QueryHint { + const transaction = transactions.find(qt => qt.hints && qt.hints.length > 0); + if (transaction) { + return transaction.hints[0]; + } + return undefined; +} + +interface QueryRowProps { + addQueryRow: typeof addQueryRow; + changeQuery: typeof changeQuery; + className?: string; + exploreId: ExploreId; + datasourceInstance: any; + highlightLogsExpression: typeof highlightLogsExpression; + history: HistoryItem[]; + index: number; + initialQuery: DataQuery; + modifyQueries: typeof modifyQueries; + queryTransactions: QueryTransaction[]; + exploreEvents: Emitter; + range: RawTimeRange; + removeQueryRow: typeof removeQueryRow; + runQueries: typeof runQueries; +} + +export class QueryRow extends PureComponent { + onExecuteQuery = () => { + const { exploreId } = this.props; + this.props.runQueries(exploreId); + }; + + onChangeQuery = (query: DataQuery, override?: boolean) => { + const { datasourceInstance, exploreId, index } = this.props; + this.props.changeQuery(exploreId, query, index, override); + if (query && !override && datasourceInstance.getHighlighterExpression && index === 0) { + // Live preview of log search matches. Only use on first row for now + this.updateLogsHighlights(query); + } + }; + + onClickAddButton = () => { + const { exploreId, index } = this.props; + this.props.addQueryRow(exploreId, index); + }; + + onClickClearButton = () => { + this.onChangeQuery(null, true); + }; + + onClickHintFix = action => { + const { datasourceInstance, exploreId, index } = this.props; + if (datasourceInstance && datasourceInstance.modifyQuery) { + const modifier = (queries: DataQuery, action: any) => datasourceInstance.modifyQuery(queries, action); + this.props.modifyQueries(exploreId, action, index, modifier); + } + }; + + onClickRemoveButton = () => { + const { exploreId, index } = this.props; + this.props.removeQueryRow(exploreId, index); + }; + + updateLogsHighlights = _.debounce((value: DataQuery) => { + const { datasourceInstance } = this.props; + if (datasourceInstance.getHighlighterExpression) { + const expressions = [datasourceInstance.getHighlighterExpression(value)]; + this.props.highlightLogsExpression(this.props.exploreId, expressions); + } + }, 500); + + render() { + const { datasourceInstance, history, index, initialQuery, queryTransactions, exploreEvents, range } = this.props; + const transactions = queryTransactions.filter(t => t.rowIndex === index); + const transactionWithError = transactions.find(t => t.error !== undefined); + const hint = getFirstHintFromTransactions(transactions); + const queryError = transactionWithError ? transactionWithError.error : null; + const QueryField = datasourceInstance.pluginExports.ExploreQueryField; + return ( +
+
+ +
+
+ {QueryField ? ( + + ) : ( + + )} +
+
+ + + +
+
+ ); + } +} + +function mapStateToProps(state: StoreState, { exploreId, index }) { + const explore = state.explore; + const item: ExploreItemState = explore[exploreId]; + const { datasourceInstance, history, initialQueries, queryTransactions, range } = item; + const initialQuery = initialQueries[index]; + return { datasourceInstance, history, initialQuery, queryTransactions, range }; +} + +const mapDispatchToProps = { + addQueryRow, + changeQuery, + highlightLogsExpression, + modifyQueries, + removeQueryRow, + runQueries, +}; + +export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(QueryRow)); diff --git a/public/app/features/explore/QueryRows.tsx b/public/app/features/explore/QueryRows.tsx index 4101475092b..01bd409f444 100644 --- a/public/app/features/explore/QueryRows.tsx +++ b/public/app/features/explore/QueryRows.tsx @@ -1,159 +1,25 @@ import React, { PureComponent } from 'react'; -import { QueryTransaction, HistoryItem, QueryHint } from 'app/types/explore'; import { Emitter } from 'app/core/utils/emitter'; +import { DataQuery } from 'app/types'; +import { ExploreId } from 'app/types/explore'; -// import DefaultQueryField from './QueryField'; -import QueryEditor from './QueryEditor'; -import QueryTransactionStatus from './QueryTransactionStatus'; -import { DataSource, DataQuery } from 'app/types'; -import { RawTimeRange } from '@grafana/ui'; +import QueryRow from './QueryRow'; -function getFirstHintFromTransactions(transactions: QueryTransaction[]): QueryHint { - const transaction = transactions.find(qt => qt.hints && qt.hints.length > 0); - if (transaction) { - return transaction.hints[0]; - } - return undefined; -} - -interface QueryRowEventHandlers { - onAddQueryRow: (index: number) => void; - onChangeQuery: (value: DataQuery, index: number, override?: boolean) => void; - onClickHintFix: (action: object, index?: number) => void; - onExecuteQuery: () => void; - onRemoveQueryRow: (index: number) => void; -} - -interface QueryRowCommonProps { +interface QueryRowsProps { className?: string; - datasource: DataSource; - history: HistoryItem[]; - transactions: QueryTransaction[]; exploreEvents: Emitter; - range: RawTimeRange; + exploreId: ExploreId; + initialQueries: DataQuery[]; } - -type QueryRowProps = QueryRowCommonProps & - QueryRowEventHandlers & { - index: number; - initialQuery: DataQuery; - }; - -class QueryRow extends PureComponent { - onExecuteQuery = () => { - const { onExecuteQuery } = this.props; - onExecuteQuery(); - }; - - onChangeQuery = (value: DataQuery, override?: boolean) => { - const { index, onChangeQuery } = this.props; - if (onChangeQuery) { - onChangeQuery(value, index, override); - } - }; - - onClickAddButton = () => { - const { index, onAddQueryRow } = this.props; - if (onAddQueryRow) { - onAddQueryRow(index); - } - }; - - onClickClearButton = () => { - this.onChangeQuery(null, true); - }; - - onClickHintFix = action => { - const { index, onClickHintFix } = this.props; - if (onClickHintFix) { - onClickHintFix(action, index); - } - }; - - onClickRemoveButton = () => { - const { index, onRemoveQueryRow } = this.props; - if (onRemoveQueryRow) { - onRemoveQueryRow(index); - } - }; - - onPressEnter = () => { - const { onExecuteQuery } = this.props; - if (onExecuteQuery) { - onExecuteQuery(); - } - }; - - render() { - const { datasource, history, initialQuery, transactions, exploreEvents, range } = this.props; - const transactionWithError = transactions.find(t => t.error !== undefined); - const hint = getFirstHintFromTransactions(transactions); - const queryError = transactionWithError ? transactionWithError.error : null; - const QueryField = datasource.pluginExports.ExploreQueryField; - return ( -
-
- -
-
- {QueryField ? ( - - ) : ( - - )} -
-
- - - -
-
- ); - } -} - -type QueryRowsProps = QueryRowCommonProps & - QueryRowEventHandlers & { - initialQueries: DataQuery[]; - }; - export default class QueryRows extends PureComponent { render() { - const { className = '', initialQueries, transactions, ...handlers } = this.props; + const { className = '', exploreEvents, exploreId, initialQueries } = this.props; return (
{initialQueries.map((query, index) => ( - t.rowIndex === index)} - {...handlers} - /> + // TODO instead of relying on initialQueries, move to react key list in redux + ))}
); diff --git a/public/app/features/explore/TableContainer.tsx b/public/app/features/explore/TableContainer.tsx new file mode 100644 index 00000000000..e510a77c97f --- /dev/null +++ b/public/app/features/explore/TableContainer.tsx @@ -0,0 +1,49 @@ +import React, { PureComponent } from 'react'; +import { hot } from 'react-hot-loader'; +import { connect } from 'react-redux'; + +import { ExploreId, ExploreItemState } from 'app/types/explore'; +import { StoreState } from 'app/types'; + +import { clickTableButton } from './state/actions'; +import Table from './Table'; +import Panel from './Panel'; +import TableModel from 'app/core/table_model'; + +interface TableContainerProps { + clickTableButton: typeof clickTableButton; + exploreId: ExploreId; + loading: boolean; + onClickLabel: (key: string, value: string) => void; + showingTable: boolean; + tableResult?: TableModel; +} + +export class TableContainer extends PureComponent { + onClickTableButton = () => { + this.props.clickTableButton(this.props.exploreId); + }; + + render() { + const { loading, onClickLabel, showingTable, tableResult } = this.props; + return ( + +
+ + ); + } +} + +function mapStateToProps(state: StoreState, { exploreId }) { + const explore = state.explore; + const item: ExploreItemState = explore[exploreId]; + const { queryTransactions, showingTable, tableResult } = item; + const loading = queryTransactions.some(qt => qt.resultType === 'Table' && !qt.done); + return { loading, showingTable, tableResult }; +} + +const mapDispatchToProps = { + clickTableButton, +}; + +export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(TableContainer)); diff --git a/public/app/features/explore/state/reducers.ts b/public/app/features/explore/state/reducers.ts index 97a02c33e67..b8273051ffe 100644 --- a/public/app/features/explore/state/reducers.ts +++ b/public/app/features/explore/state/reducers.ts @@ -232,6 +232,7 @@ const itemReducer = (state, action: Action): ExploreItemState => { initialQueries: action.initialQueries, logsHighlighterExpressions: undefined, modifiedQueries: action.initialQueries.slice(), + queryTransactions: [], showingStartPage: action.showingStartPage, supportsGraph: action.supportsGraph, supportsLogs: action.supportsLogs, From 9aede9e6368947543d6488a0c6f99e4367ba1790 Mon Sep 17 00:00:00 2001 From: David Kaltschmidt Date: Sun, 13 Jan 2019 23:26:04 +0100 Subject: [PATCH 7/9] Fix reducer issues --- public/app/core/utils/explore.ts | 8 ++++---- public/app/features/explore/QueryEditor.tsx | 2 +- public/app/features/explore/state/reducers.ts | 12 +++++++++--- 3 files changed, 14 insertions(+), 8 deletions(-) diff --git a/public/app/core/utils/explore.ts b/public/app/core/utils/explore.ts index b0dcf2117d0..b05e38a4b33 100644 --- a/public/app/core/utils/explore.ts +++ b/public/app/core/utils/explore.ts @@ -17,7 +17,7 @@ import { QueryIntervals, QueryOptions, } from 'app/types/explore'; -import { DataQuery, DataSourceApi } from 'app/types/series'; +import { DataQuery } from 'app/types/series'; export const DEFAULT_RANGE = { from: 'now-6h', @@ -243,8 +243,8 @@ export function calculateResultsFromQueryTransactions( }; } -export function getIntervals(range: RawTimeRange, datasource: DataSourceApi, resolution: number): IntervalValues { - if (!datasource || !resolution) { +export function getIntervals(range: RawTimeRange, lowLimit: string, resolution: number): IntervalValues { + if (!resolution) { return { interval: '1s', intervalMs: 1000 }; } @@ -253,7 +253,7 @@ export function getIntervals(range: RawTimeRange, datasource: DataSourceApi, res to: parseDate(range.to, true), }; - return kbn.calculateInterval(absoluteRange, resolution, datasource.interval); + return kbn.calculateInterval(absoluteRange, resolution, lowLimit); } export function makeTimeSeriesList(dataList) { diff --git a/public/app/features/explore/QueryEditor.tsx b/public/app/features/explore/QueryEditor.tsx index ce0a8a6e03e..dde674d3fcd 100644 --- a/public/app/features/explore/QueryEditor.tsx +++ b/public/app/features/explore/QueryEditor.tsx @@ -48,7 +48,7 @@ export default class QueryEditor extends PureComponent { getNextQueryLetter: x => '', }, hideEditorRowActions: true, - ...getIntervals(range, datasource, null), // Possible to get resolution? + ...getIntervals(range, (datasource || {}).interval, null), // Possible to get resolution? }, }; diff --git a/public/app/features/explore/state/reducers.ts b/public/app/features/explore/state/reducers.ts index b8273051ffe..91d5c4cf925 100644 --- a/public/app/features/explore/state/reducers.ts +++ b/public/app/features/explore/state/reducers.ts @@ -122,11 +122,12 @@ const itemReducer = (state, action: Action): ExploreItemState => { case ActionTypes.ChangeSize: { const { range, datasourceInstance } = state; - if (!datasourceInstance) { - return state; + let interval = '1s'; + if (datasourceInstance && datasourceInstance.interval) { + interval = datasourceInstance.interval; } const containerWidth = action.width; - const queryIntervals = getIntervals(range, datasourceInstance.interval, containerWidth); + const queryIntervals = getIntervals(range, interval, containerWidth); return { ...state, containerWidth, queryIntervals }; } @@ -189,6 +190,11 @@ const itemReducer = (state, action: Action): ExploreItemState => { return { ...state, ...results, queryTransactions: nextQueryTransactions, showingTable }; } + case ActionTypes.HighlightLogsExpression: { + const { expressions } = action; + return { ...state, logsHighlighterExpressions: expressions }; + } + case ActionTypes.InitializeExplore: { const { containerWidth, eventBridge, exploreDatasources, range } = action; return { From 6ff15039a9bcb6fa79f98ad8fb0224f633aa4936 Mon Sep 17 00:00:00 2001 From: David Kaltschmidt Date: Tue, 15 Jan 2019 19:52:53 +0100 Subject: [PATCH 8/9] File organization, action naming, comments - moved ActionTypes to `./state/actionTypes` - renamed click-related actions - added comments to actions and state types - prefixed Explore actions with `explore/` - fixed query override issue when row was added --- public/app/features/explore/Explore.tsx | 34 +- .../app/features/explore/GraphContainer.tsx | 8 +- public/app/features/explore/LogsContainer.tsx | 8 +- .../app/features/explore/TableContainer.tsx | 14 +- .../app/features/explore/state/actionTypes.ts | 252 +++++++++ public/app/features/explore/state/actions.ts | 498 +++++++----------- public/app/features/explore/state/reducers.ts | 210 ++++---- public/app/types/explore.ts | 117 ++++ 8 files changed, 714 insertions(+), 427 deletions(-) create mode 100644 public/app/features/explore/state/actionTypes.ts diff --git a/public/app/features/explore/Explore.tsx b/public/app/features/explore/Explore.tsx index a70135c00ad..a8acab50137 100644 --- a/public/app/features/explore/Explore.tsx +++ b/public/app/features/explore/Explore.tsx @@ -18,15 +18,15 @@ import { changeDatasource, changeSize, changeTime, - clickClear, - clickCloseSplit, - clickExample, - clickSplit, + clearQueries, initializeExplore, modifyQueries, runQueries, scanStart, scanStop, + setQueries, + splitClose, + splitOpen, } from './state/actions'; import { Alert } from './Error'; @@ -42,10 +42,7 @@ interface ExploreProps { changeDatasource: typeof changeDatasource; changeSize: typeof changeSize; changeTime: typeof changeTime; - clickClear: typeof clickClear; - clickCloseSplit: typeof clickCloseSplit; - clickExample: typeof clickExample; - clickSplit: typeof clickSplit; + clearQueries: typeof clearQueries; datasourceError: string; datasourceInstance: any; datasourceLoading: boolean | null; @@ -65,7 +62,10 @@ interface ExploreProps { scanRange?: RawTimeRange; scanStart: typeof scanStart; scanStop: typeof scanStop; + setQueries: typeof setQueries; split: boolean; + splitClose: typeof splitClose; + splitOpen: typeof splitOpen; showingStartPage?: boolean; supportsGraph: boolean | null; supportsLogs: boolean | null; @@ -152,20 +152,20 @@ export class Explore extends React.PureComponent { }; onClickClear = () => { - this.props.clickClear(this.props.exploreId); + this.props.clearQueries(this.props.exploreId); }; onClickCloseSplit = () => { - this.props.clickCloseSplit(); + this.props.splitClose(); }; // Use this in help pages to set page to a single query onClickExample = (query: DataQuery) => { - this.props.clickExample(this.props.exploreId, query); + this.props.setQueries(this.props.exploreId, [query]); }; onClickSplit = () => { - this.props.clickSplit(); + this.props.splitOpen(); }; onClickLabel = (key: string, value: string) => { @@ -175,7 +175,7 @@ export class Explore extends React.PureComponent { onModifyQueries = (action, index?: number) => { const { datasourceInstance } = this.props; if (datasourceInstance && datasourceInstance.modifyQuery) { - const modifier = (queries: DataQuery, action: any) => datasourceInstance.modifyQuery(queries, action); + const modifier = (queries: DataQuery, modification: any) => datasourceInstance.modifyQuery(queries, modification); this.props.modifyQueries(this.props.exploreId, action, index, modifier); } }; @@ -366,15 +366,15 @@ const mapDispatchToProps = { changeDatasource, changeSize, changeTime, - clickClear, - clickCloseSplit, - clickExample, - clickSplit, + clearQueries, initializeExplore, modifyQueries, runQueries, scanStart, scanStop, + setQueries, + splitClose, + splitOpen, }; export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(Explore)); diff --git a/public/app/features/explore/GraphContainer.tsx b/public/app/features/explore/GraphContainer.tsx index da098f0c92d..e2610bcc781 100644 --- a/public/app/features/explore/GraphContainer.tsx +++ b/public/app/features/explore/GraphContainer.tsx @@ -6,13 +6,12 @@ import { RawTimeRange, TimeRange } from '@grafana/ui'; import { ExploreId, ExploreItemState } from 'app/types/explore'; import { StoreState } from 'app/types'; -import { clickGraphButton } from './state/actions'; +import { toggleGraph } from './state/actions'; import Graph from './Graph'; import Panel from './Panel'; interface GraphContainerProps { onChangeTime: (range: TimeRange) => void; - clickGraphButton: typeof clickGraphButton; exploreId: ExploreId; graphResult?: any[]; loading: boolean; @@ -20,11 +19,12 @@ interface GraphContainerProps { showingGraph: boolean; showingTable: boolean; split: boolean; + toggleGraph: typeof toggleGraph; } export class GraphContainer extends PureComponent { onClickGraphButton = () => { - this.props.clickGraphButton(this.props.exploreId); + this.props.toggleGraph(this.props.exploreId); }; render() { @@ -55,7 +55,7 @@ function mapStateToProps(state: StoreState, { exploreId }) { } const mapDispatchToProps = { - clickGraphButton, + toggleGraph, }; export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(GraphContainer)); diff --git a/public/app/features/explore/LogsContainer.tsx b/public/app/features/explore/LogsContainer.tsx index db2f681a9c5..e58cd2b5e95 100644 --- a/public/app/features/explore/LogsContainer.tsx +++ b/public/app/features/explore/LogsContainer.tsx @@ -7,12 +7,11 @@ import { ExploreId, ExploreItemState } from 'app/types/explore'; import { LogsModel } from 'app/core/logs_model'; import { StoreState } from 'app/types'; -import { clickLogsButton } from './state/actions'; +import { toggleLogs } from './state/actions'; import Logs from './Logs'; import Panel from './Panel'; interface LogsContainerProps { - clickLogsButton: typeof clickLogsButton; exploreId: ExploreId; loading: boolean; logsHighlighterExpressions?: string[]; @@ -25,11 +24,12 @@ interface LogsContainerProps { scanning?: boolean; scanRange?: RawTimeRange; showingLogs: boolean; + toggleLogs: typeof toggleLogs; } export class LogsContainer extends PureComponent { onClickLogsButton = () => { - this.props.clickLogsButton(this.props.exploreId); + this.props.toggleLogs(this.props.exploreId); }; render() { @@ -85,7 +85,7 @@ function mapStateToProps(state: StoreState, { exploreId }) { } const mapDispatchToProps = { - clickLogsButton, + toggleLogs, }; export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(LogsContainer)); diff --git a/public/app/features/explore/TableContainer.tsx b/public/app/features/explore/TableContainer.tsx index e510a77c97f..1d00a441e14 100644 --- a/public/app/features/explore/TableContainer.tsx +++ b/public/app/features/explore/TableContainer.tsx @@ -5,30 +5,30 @@ import { connect } from 'react-redux'; import { ExploreId, ExploreItemState } from 'app/types/explore'; import { StoreState } from 'app/types'; -import { clickTableButton } from './state/actions'; +import { toggleGraph } from './state/actions'; import Table from './Table'; import Panel from './Panel'; import TableModel from 'app/core/table_model'; interface TableContainerProps { - clickTableButton: typeof clickTableButton; exploreId: ExploreId; loading: boolean; - onClickLabel: (key: string, value: string) => void; + onClickCell: (key: string, value: string) => void; showingTable: boolean; tableResult?: TableModel; + toggleGraph: typeof toggleGraph; } export class TableContainer extends PureComponent { onClickTableButton = () => { - this.props.clickTableButton(this.props.exploreId); + this.props.toggleGraph(this.props.exploreId); }; render() { - const { loading, onClickLabel, showingTable, tableResult } = this.props; + const { loading, onClickCell, showingTable, tableResult } = this.props; return ( -
+
); } @@ -43,7 +43,7 @@ function mapStateToProps(state: StoreState, { exploreId }) { } const mapDispatchToProps = { - clickTableButton, + toggleGraph, }; export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(TableContainer)); diff --git a/public/app/features/explore/state/actionTypes.ts b/public/app/features/explore/state/actionTypes.ts new file mode 100644 index 00000000000..ed0995cff17 --- /dev/null +++ b/public/app/features/explore/state/actionTypes.ts @@ -0,0 +1,252 @@ +import { RawTimeRange, TimeRange } from '@grafana/ui'; + +import { Emitter } from 'app/core/core'; +import { + ExploreId, + ExploreItemState, + HistoryItem, + RangeScanner, + ResultType, + QueryTransaction, +} from 'app/types/explore'; +import { DataSourceSelectItem } from 'app/types/datasources'; +import { DataQuery } from 'app/types'; + +export enum ActionTypes { + AddQueryRow = 'explore/ADD_QUERY_ROW', + ChangeDatasource = 'explore/CHANGE_DATASOURCE', + ChangeQuery = 'explore/CHANGE_QUERY', + ChangeSize = 'explore/CHANGE_SIZE', + ChangeTime = 'explore/CHANGE_TIME', + ClearQueries = 'explore/CLEAR_QUERIES', + HighlightLogsExpression = 'explore/HIGHLIGHT_LOGS_EXPRESSION', + InitializeExplore = 'explore/INITIALIZE_EXPLORE', + InitializeExploreSplit = 'explore/INITIALIZE_EXPLORE_SPLIT', + LoadDatasourceFailure = 'explore/LOAD_DATASOURCE_FAILURE', + LoadDatasourceMissing = 'explore/LOAD_DATASOURCE_MISSING', + LoadDatasourcePending = 'explore/LOAD_DATASOURCE_PENDING', + LoadDatasourceSuccess = 'explore/LOAD_DATASOURCE_SUCCESS', + ModifyQueries = 'explore/MODIFY_QUERIES', + QueryTransactionFailure = 'explore/QUERY_TRANSACTION_FAILURE', + QueryTransactionStart = 'explore/QUERY_TRANSACTION_START', + QueryTransactionSuccess = 'explore/QUERY_TRANSACTION_SUCCESS', + RemoveQueryRow = 'explore/REMOVE_QUERY_ROW', + RunQueries = 'explore/RUN_QUERIES', + RunQueriesEmpty = 'explore/RUN_QUERIES_EMPTY', + ScanRange = 'explore/SCAN_RANGE', + ScanStart = 'explore/SCAN_START', + ScanStop = 'explore/SCAN_STOP', + SetQueries = 'explore/SET_QUERIES', + SplitClose = 'explore/SPLIT_CLOSE', + SplitOpen = 'explore/SPLIT_OPEN', + StateSave = 'explore/STATE_SAVE', + ToggleGraph = 'explore/TOGGLE_GRAPH', + ToggleLogs = 'explore/TOGGLE_LOGS', + ToggleTable = 'explore/TOGGLE_TABLE', +} + +export interface AddQueryRowAction { + type: ActionTypes.AddQueryRow; + exploreId: ExploreId; + index: number; + query: DataQuery; +} + +export interface ChangeQueryAction { + type: ActionTypes.ChangeQuery; + exploreId: ExploreId; + query: DataQuery; + index: number; + override: boolean; +} + +export interface ChangeSizeAction { + type: ActionTypes.ChangeSize; + exploreId: ExploreId; + width: number; + height: number; +} + +export interface ChangeTimeAction { + type: ActionTypes.ChangeTime; + exploreId: ExploreId; + range: TimeRange; +} + +export interface ClearQueriesAction { + type: ActionTypes.ClearQueries; + exploreId: ExploreId; +} + +export interface HighlightLogsExpressionAction { + type: ActionTypes.HighlightLogsExpression; + exploreId: ExploreId; + expressions: string[]; +} + +export interface InitializeExploreAction { + type: ActionTypes.InitializeExplore; + exploreId: ExploreId; + containerWidth: number; + datasource: string; + eventBridge: Emitter; + exploreDatasources: DataSourceSelectItem[]; + queries: DataQuery[]; + range: RawTimeRange; +} + +export interface InitializeExploreSplitAction { + type: ActionTypes.InitializeExploreSplit; +} + +export interface LoadDatasourceFailureAction { + type: ActionTypes.LoadDatasourceFailure; + exploreId: ExploreId; + error: string; +} + +export interface LoadDatasourcePendingAction { + type: ActionTypes.LoadDatasourcePending; + exploreId: ExploreId; + datasourceId: number; +} + +export interface LoadDatasourceMissingAction { + type: ActionTypes.LoadDatasourceMissing; + exploreId: ExploreId; +} + +export interface LoadDatasourceSuccessAction { + type: ActionTypes.LoadDatasourceSuccess; + exploreId: ExploreId; + StartPage?: any; + datasourceInstance: any; + history: HistoryItem[]; + initialDatasource: string; + initialQueries: DataQuery[]; + logsHighlighterExpressions?: any[]; + showingStartPage: boolean; + supportsGraph: boolean; + supportsLogs: boolean; + supportsTable: boolean; +} + +export interface ModifyQueriesAction { + type: ActionTypes.ModifyQueries; + exploreId: ExploreId; + modification: any; + index: number; + modifier: (queries: DataQuery[], modification: any) => DataQuery[]; +} + +export interface QueryTransactionFailureAction { + type: ActionTypes.QueryTransactionFailure; + exploreId: ExploreId; + queryTransactions: QueryTransaction[]; +} + +export interface QueryTransactionStartAction { + type: ActionTypes.QueryTransactionStart; + exploreId: ExploreId; + resultType: ResultType; + rowIndex: number; + transaction: QueryTransaction; +} + +export interface QueryTransactionSuccessAction { + type: ActionTypes.QueryTransactionSuccess; + exploreId: ExploreId; + history: HistoryItem[]; + queryTransactions: QueryTransaction[]; +} + +export interface RemoveQueryRowAction { + type: ActionTypes.RemoveQueryRow; + exploreId: ExploreId; + index: number; +} + +export interface RunQueriesEmptyAction { + type: ActionTypes.RunQueriesEmpty; + exploreId: ExploreId; +} + +export interface ScanStartAction { + type: ActionTypes.ScanStart; + exploreId: ExploreId; + scanner: RangeScanner; +} + +export interface ScanRangeAction { + type: ActionTypes.ScanRange; + exploreId: ExploreId; + range: RawTimeRange; +} + +export interface ScanStopAction { + type: ActionTypes.ScanStop; + exploreId: ExploreId; +} + +export interface SetQueriesAction { + type: ActionTypes.SetQueries; + exploreId: ExploreId; + queries: DataQuery[]; +} + +export interface SplitCloseAction { + type: ActionTypes.SplitClose; +} + +export interface SplitOpenAction { + type: ActionTypes.SplitOpen; + itemState: ExploreItemState; +} + +export interface StateSaveAction { + type: ActionTypes.StateSave; +} + +export interface ToggleTableAction { + type: ActionTypes.ToggleTable; + exploreId: ExploreId; +} + +export interface ToggleGraphAction { + type: ActionTypes.ToggleGraph; + exploreId: ExploreId; +} + +export interface ToggleLogsAction { + type: ActionTypes.ToggleLogs; + exploreId: ExploreId; +} + +export type Action = + | AddQueryRowAction + | ChangeQueryAction + | ChangeSizeAction + | ChangeTimeAction + | ClearQueriesAction + | HighlightLogsExpressionAction + | InitializeExploreAction + | InitializeExploreSplitAction + | LoadDatasourceFailureAction + | LoadDatasourceMissingAction + | LoadDatasourcePendingAction + | LoadDatasourceSuccessAction + | ModifyQueriesAction + | QueryTransactionFailureAction + | QueryTransactionStartAction + | QueryTransactionSuccessAction + | RemoveQueryRowAction + | RunQueriesEmptyAction + | ScanRangeAction + | ScanStartAction + | ScanStopAction + | SetQueriesAction + | SplitCloseAction + | SplitOpenAction + | ToggleGraphAction + | ToggleLogsAction + | ToggleTableAction; diff --git a/public/app/features/explore/state/actions.ts b/public/app/features/explore/state/actions.ts index 26811606bd9..ecfb35c8c2f 100644 --- a/public/app/features/explore/state/actions.ts +++ b/public/app/features/explore/state/actions.ts @@ -21,9 +21,7 @@ import { DataQuery, StoreState } from 'app/types'; import { getDatasourceSrv } from 'app/features/plugins/datasource_srv'; import { ExploreId, - ExploreItemState, ExploreUrlState, - HistoryItem, RangeScanner, ResultType, QueryOptions, @@ -33,245 +31,33 @@ import { } from 'app/types/explore'; import { Emitter } from 'app/core/core'; -export enum ActionTypes { - AddQueryRow = 'ADD_QUERY_ROW', - ChangeDatasource = 'CHANGE_DATASOURCE', - ChangeQuery = 'CHANGE_QUERY', - ChangeSize = 'CHANGE_SIZE', - ChangeTime = 'CHANGE_TIME', - ClickClear = 'CLICK_CLEAR', - ClickCloseSplit = 'CLICK_CLOSE_SPLIT', - ClickExample = 'CLICK_EXAMPLE', - ClickGraphButton = 'CLICK_GRAPH_BUTTON', - ClickLogsButton = 'CLICK_LOGS_BUTTON', - ClickSplit = 'CLICK_SPLIT', - ClickTableButton = 'CLICK_TABLE_BUTTON', - HighlightLogsExpression = 'HIGHLIGHT_LOGS_EXPRESSION', - InitializeExplore = 'INITIALIZE_EXPLORE', - InitializeExploreSplit = 'INITIALIZE_EXPLORE_SPLIT', - LoadDatasourceFailure = 'LOAD_DATASOURCE_FAILURE', - LoadDatasourceMissing = 'LOAD_DATASOURCE_MISSING', - LoadDatasourcePending = 'LOAD_DATASOURCE_PENDING', - LoadDatasourceSuccess = 'LOAD_DATASOURCE_SUCCESS', - ModifyQueries = 'MODIFY_QUERIES', - QueryTransactionFailure = 'QUERY_TRANSACTION_FAILURE', - QueryTransactionStart = 'QUERY_TRANSACTION_START', - QueryTransactionSuccess = 'QUERY_TRANSACTION_SUCCESS', - RemoveQueryRow = 'REMOVE_QUERY_ROW', - RunQueries = 'RUN_QUERIES', - RunQueriesEmpty = 'RUN_QUERIES', - ScanRange = 'SCAN_RANGE', - ScanStart = 'SCAN_START', - ScanStop = 'SCAN_STOP', - StateSave = 'STATE_SAVE', -} +import { + Action as ThunkableAction, + ActionTypes, + AddQueryRowAction, + ChangeSizeAction, + HighlightLogsExpressionAction, + LoadDatasourceFailureAction, + LoadDatasourceMissingAction, + LoadDatasourcePendingAction, + LoadDatasourceSuccessAction, + QueryTransactionStartAction, + ScanStopAction, +} from './actionTypes'; -export interface AddQueryRowAction { - type: ActionTypes.AddQueryRow; - exploreId: ExploreId; - index: number; - query: DataQuery; -} - -export interface ChangeQueryAction { - type: ActionTypes.ChangeQuery; - exploreId: ExploreId; - query: DataQuery; - index: number; - override: boolean; -} - -export interface ChangeSizeAction { - type: ActionTypes.ChangeSize; - exploreId: ExploreId; - width: number; - height: number; -} - -export interface ChangeTimeAction { - type: ActionTypes.ChangeTime; - exploreId: ExploreId; - range: TimeRange; -} - -export interface ClickClearAction { - type: ActionTypes.ClickClear; - exploreId: ExploreId; -} - -export interface ClickCloseSplitAction { - type: ActionTypes.ClickCloseSplit; -} - -export interface ClickExampleAction { - type: ActionTypes.ClickExample; - exploreId: ExploreId; - query: DataQuery; -} - -export interface ClickGraphButtonAction { - type: ActionTypes.ClickGraphButton; - exploreId: ExploreId; -} - -export interface ClickLogsButtonAction { - type: ActionTypes.ClickLogsButton; - exploreId: ExploreId; -} - -export interface ClickSplitAction { - type: ActionTypes.ClickSplit; - itemState: ExploreItemState; -} - -export interface ClickTableButtonAction { - type: ActionTypes.ClickTableButton; - exploreId: ExploreId; -} - -export interface HighlightLogsExpressionAction { - type: ActionTypes.HighlightLogsExpression; - exploreId: ExploreId; - expressions: string[]; -} - -export interface InitializeExploreAction { - type: ActionTypes.InitializeExplore; - exploreId: ExploreId; - containerWidth: number; - datasource: string; - eventBridge: Emitter; - exploreDatasources: DataSourceSelectItem[]; - queries: DataQuery[]; - range: RawTimeRange; -} - -export interface InitializeExploreSplitAction { - type: ActionTypes.InitializeExploreSplit; -} - -export interface LoadDatasourceFailureAction { - type: ActionTypes.LoadDatasourceFailure; - exploreId: ExploreId; - error: string; -} - -export interface LoadDatasourcePendingAction { - type: ActionTypes.LoadDatasourcePending; - exploreId: ExploreId; - datasourceId: number; -} - -export interface LoadDatasourceMissingAction { - type: ActionTypes.LoadDatasourceMissing; - exploreId: ExploreId; -} - -export interface LoadDatasourceSuccessAction { - type: ActionTypes.LoadDatasourceSuccess; - exploreId: ExploreId; - StartPage?: any; - datasourceInstance: any; - history: HistoryItem[]; - initialDatasource: string; - initialQueries: DataQuery[]; - logsHighlighterExpressions?: any[]; - showingStartPage: boolean; - supportsGraph: boolean; - supportsLogs: boolean; - supportsTable: boolean; -} - -export interface ModifyQueriesAction { - type: ActionTypes.ModifyQueries; - exploreId: ExploreId; - modification: any; - index: number; - modifier: (queries: DataQuery[], modification: any) => DataQuery[]; -} - -export interface QueryTransactionFailureAction { - type: ActionTypes.QueryTransactionFailure; - exploreId: ExploreId; - queryTransactions: QueryTransaction[]; -} - -export interface QueryTransactionStartAction { - type: ActionTypes.QueryTransactionStart; - exploreId: ExploreId; - resultType: ResultType; - rowIndex: number; - transaction: QueryTransaction; -} - -export interface QueryTransactionSuccessAction { - type: ActionTypes.QueryTransactionSuccess; - exploreId: ExploreId; - history: HistoryItem[]; - queryTransactions: QueryTransaction[]; -} - -export interface RemoveQueryRowAction { - type: ActionTypes.RemoveQueryRow; - exploreId: ExploreId; - index: number; -} - -export interface ScanStartAction { - type: ActionTypes.ScanStart; - exploreId: ExploreId; - scanner: RangeScanner; -} - -export interface ScanRangeAction { - type: ActionTypes.ScanRange; - exploreId: ExploreId; - range: RawTimeRange; -} - -export interface ScanStopAction { - type: ActionTypes.ScanStop; - exploreId: ExploreId; -} - -export interface StateSaveAction { - type: ActionTypes.StateSave; -} - -export type Action = - | AddQueryRowAction - | ChangeQueryAction - | ChangeSizeAction - | ChangeTimeAction - | ClickClearAction - | ClickCloseSplitAction - | ClickExampleAction - | ClickGraphButtonAction - | ClickLogsButtonAction - | ClickSplitAction - | ClickTableButtonAction - | HighlightLogsExpressionAction - | InitializeExploreAction - | InitializeExploreSplitAction - | LoadDatasourceFailureAction - | LoadDatasourceMissingAction - | LoadDatasourcePendingAction - | LoadDatasourceSuccessAction - | ModifyQueriesAction - | QueryTransactionFailureAction - | QueryTransactionStartAction - | QueryTransactionSuccessAction - | RemoveQueryRowAction - | ScanRangeAction - | ScanStartAction - | ScanStopAction; -type ThunkResult = ThunkAction; +type ThunkResult = ThunkAction; +/** + * Adds a query row after the row with the given index. + */ export function addQueryRow(exploreId: ExploreId, index: number): AddQueryRowAction { const query = generateEmptyQuery(index + 1); return { type: ActionTypes.AddQueryRow, exploreId, index, query }; } +/** + * Loads a new datasource identified by the given name. + */ export function changeDatasource(exploreId: ExploreId, datasource: string): ThunkResult { return async dispatch => { const instance = await getDatasourceSrv().get(datasource); @@ -279,6 +65,10 @@ export function changeDatasource(exploreId: ExploreId, datasource: string): Thun }; } +/** + * Query change handler for the query row with the given index. + * If `override` is reset the query modifications and run the queries. Use this to set queries via a link. + */ export function changeQuery( exploreId: ExploreId, query: DataQuery, @@ -298,6 +88,10 @@ export function changeQuery( }; } +/** + * 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, { height, width }: { height: number; width: number } @@ -305,6 +99,9 @@ export function changeSize( return { type: ActionTypes.ChangeSize, exploreId, height, width }; } +/** + * Change the time range of Explore. Usually called from the Timepicker or a graph interaction. + */ export function changeTime(exploreId: ExploreId, range: TimeRange): ThunkResult { return dispatch => { dispatch({ type: ActionTypes.ChangeTime, exploreId, range }); @@ -312,78 +109,28 @@ export function changeTime(exploreId: ExploreId, range: TimeRange): ThunkResult< }; } -export function clickClear(exploreId: ExploreId): ThunkResult { +/** + * Clear all queries and results. + */ +export function clearQueries(exploreId: ExploreId): ThunkResult { return dispatch => { dispatch(scanStop(exploreId)); - dispatch({ type: ActionTypes.ClickClear, exploreId }); + dispatch({ type: ActionTypes.ClearQueries, exploreId }); dispatch(stateSave()); }; } -export function clickCloseSplit(): ThunkResult { - return dispatch => { - dispatch({ type: ActionTypes.ClickCloseSplit }); - dispatch(stateSave()); - }; -} - -export function clickExample(exploreId: ExploreId, rawQuery: DataQuery): ThunkResult { - return dispatch => { - const query = { ...rawQuery, ...generateEmptyQuery() }; - dispatch({ - type: ActionTypes.ClickExample, - exploreId, - query, - }); - dispatch(runQueries(exploreId)); - }; -} - -export function clickGraphButton(exploreId: ExploreId): ThunkResult { - return (dispatch, getState) => { - dispatch({ type: ActionTypes.ClickGraphButton, exploreId }); - if (getState().explore[exploreId].showingGraph) { - dispatch(runQueries(exploreId)); - } - }; -} - -export function clickLogsButton(exploreId: ExploreId): ThunkResult { - return (dispatch, getState) => { - dispatch({ type: ActionTypes.ClickLogsButton, exploreId }); - if (getState().explore[exploreId].showingLogs) { - dispatch(runQueries(exploreId)); - } - }; -} - -export function clickSplit(): ThunkResult { - return (dispatch, getState) => { - // Clone left state to become the right state - const leftState = getState().explore.left; - const itemState = { - ...leftState, - queryTransactions: [], - initialQueries: leftState.modifiedQueries.slice(), - }; - dispatch({ type: ActionTypes.ClickSplit, itemState }); - dispatch(stateSave()); - }; -} - -export function clickTableButton(exploreId: ExploreId): ThunkResult { - return (dispatch, getState) => { - dispatch({ type: ActionTypes.ClickTableButton, exploreId }); - if (getState().explore[exploreId].showingTable) { - dispatch(runQueries(exploreId)); - } - }; -} - +/** + * Highlight expressions in the log results + */ export function highlightLogsExpression(exploreId: ExploreId, expressions: string[]): HighlightLogsExpressionAction { return { type: ActionTypes.HighlightLogsExpression, exploreId, expressions }; } +/** + * Initialize Explore state with state from the URL and the React component. + * Call this only on components for with the Explore state has not been initialized. + */ export function initializeExplore( exploreId: ExploreId, datasource: string, @@ -426,29 +173,46 @@ export function initializeExplore( }; } +/** + * Initialize the wrapper split state + */ export function initializeExploreSplit() { return async dispatch => { dispatch({ type: ActionTypes.InitializeExploreSplit }); }; } +/** + * Display an error that happened during the selection of a datasource + */ export const loadDatasourceFailure = (exploreId: ExploreId, error: string): LoadDatasourceFailureAction => ({ type: ActionTypes.LoadDatasourceFailure, exploreId, error, }); +/** + * Display an error when no datasources have been configured + */ export const loadDatasourceMissing = (exploreId: ExploreId): LoadDatasourceMissingAction => ({ type: ActionTypes.LoadDatasourceMissing, exploreId, }); +/** + * Start the async process of loading a datasource to display a loading indicator + */ export const loadDatasourcePending = (exploreId: ExploreId, datasourceId: number): LoadDatasourcePendingAction => ({ type: ActionTypes.LoadDatasourcePending, exploreId, datasourceId, }); +/** + * Datasource loading was successfully completed. The instance is stored in the state as well in case we need to + * run datasource-specific code. Existing queries are imported to the new datasource if an importer exists, + * e.g., Prometheus -> Loki queries. + */ export const loadDatasourceSuccess = ( exploreId: ExploreId, instance: any, @@ -481,6 +245,9 @@ export const loadDatasourceSuccess = ( }; }; +/** + * Main action to asynchronously load a datasource. Dispatches lots of smaller actions for feedback. + */ export function loadDatasource(exploreId: ExploreId, instance: any): ThunkResult { return async (dispatch, getState) => { const datasourceId = instance.meta.id; @@ -542,6 +309,13 @@ export function loadDatasource(exploreId: ExploreId, instance: any): ThunkResult }; } +/** + * Action to modify a query given a datasource-specific modifier action. + * @param exploreId Explore area + * @param modification Action object with a type, e.g., ADD_FILTER + * @param index Optional query row index. If omitted, the modification is applied to all query rows. + * @param modifier Function that executes the modification, typically `datasourceInstance.modifyQueries`. + */ export function modifyQueries( exploreId: ExploreId, modification: any, @@ -556,6 +330,10 @@ export function modifyQueries( }; } +/** + * Mark a query transaction as failed with an error extracted from the query response. + * The transaction will be marked as `done`. + */ export function queryTransactionFailure( exploreId: ExploreId, transactionId: string, @@ -614,6 +392,13 @@ export function queryTransactionFailure( }; } +/** + * Start a query transaction for the given result type. + * @param exploreId Explore area + * @param transaction Query options and `done` status. + * @param resultType Associate the transaction with a result viewer, e.g., Graph + * @param rowIndex Index is used to associate latency for this transaction with a query row + */ export function queryTransactionStart( exploreId: ExploreId, transaction: QueryTransaction, @@ -623,6 +408,17 @@ export function queryTransactionStart( return { type: ActionTypes.QueryTransactionStart, exploreId, resultType, rowIndex, transaction }; } +/** + * Complete a query transaction, mark the transaction as `done` and store query state in URL. + * If the transaction was started by a scanner, it keeps on scanning for more results. + * Side-effect: the query is stored in localStorage. + * @param exploreId Explore area + * @param transactionId ID + * @param result Response from `datasourceInstance.query()` + * @param latency Duration between request and response + * @param queries Queries from all query rows + * @param datasourceId Origin datasource instance, used to discard results if current datasource is different + */ export function queryTransactionSuccess( exploreId: ExploreId, transactionId: string, @@ -691,6 +487,9 @@ export function queryTransactionSuccess( }; } +/** + * Remove query row of the given index, as well as associated query results. + */ export function removeQueryRow(exploreId: ExploreId, index: number): ThunkResult { return dispatch => { dispatch({ type: ActionTypes.RemoveQueryRow, exploreId, index }); @@ -698,6 +497,9 @@ export function removeQueryRow(exploreId: ExploreId, index: number): ThunkResult }; } +/** + * Main action to run queries and dispatches sub-actions based on which result viewers are active + */ export function runQueries(exploreId: ExploreId) { return (dispatch, getState) => { const { @@ -757,6 +559,13 @@ export function runQueries(exploreId: ExploreId) { }; } +/** + * Helper action to build a query transaction object and handing the query to the datasource. + * @param exploreId Explore area + * @param resultType Result viewer that will be associated with this query result + * @param queryOptions Query options as required by the datasource's `query()` function. + * @param resultGetter Optional result extractor, e.g., if the result is a list and you only need the first element. + */ function runQueriesForType( exploreId: ExploreId, resultType: ResultType, @@ -801,18 +610,79 @@ function runQueriesForType( }; } +/** + * Start a scan for more results using the given scanner. + * @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, scanner: RangeScanner): ThunkResult { return dispatch => { + // Register the scanner dispatch({ type: ActionTypes.ScanStart, exploreId, scanner }); + // Scanning must trigger query run, and return the new range const range = scanner(); + // Set the new range to be displayed dispatch({ type: ActionTypes.ScanRange, exploreId, range }); }; } +/** + * Stop any scanning for more results. + */ export function scanStop(exploreId: ExploreId): ScanStopAction { return { type: ActionTypes.ScanStop, exploreId }; } +/** + * 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 { + return dispatch => { + // Inject react keys into query objects + const queries = rawQueries.map(q => ({ ...q, ...generateEmptyQuery() })); + dispatch({ + type: ActionTypes.SetQueries, + exploreId, + queries, + }); + dispatch(runQueries(exploreId)); + }; +} + +/** + * Close the split view and save URL state. + */ +export function splitClose(): ThunkResult { + return dispatch => { + dispatch({ type: ActionTypes.SplitClose }); + dispatch(stateSave()); + }; +} + +/** + * Open the split view and copy the left state to be the right state. + * The right state is automatically initialized. + * The copy keeps all query modifications but wipes the query results. + */ +export function splitOpen(): ThunkResult { + return (dispatch, getState) => { + // Clone left state to become the right state + const leftState = getState().explore.left; + const itemState = { + ...leftState, + queryTransactions: [], + initialQueries: leftState.modifiedQueries.slice(), + }; + dispatch({ type: ActionTypes.SplitOpen, itemState }); + dispatch(stateSave()); + }; +} + +/** + * Saves Explore state to URL using the `left` and `right` parameters. + * If split view is not active, `right` will not be set. + */ export function stateSave() { return (dispatch, getState) => { const { left, right, split } = getState().explore; @@ -834,3 +704,39 @@ export function stateSave() { dispatch(updateLocation({ query: urlStates })); }; } + +/** + * Expand/collapse the graph result viewer. When collapsed, graph queries won't be run. + */ +export function toggleGraph(exploreId: ExploreId): ThunkResult { + return (dispatch, getState) => { + dispatch({ type: ActionTypes.ToggleGraph, exploreId }); + if (getState().explore[exploreId].showingGraph) { + dispatch(runQueries(exploreId)); + } + }; +} + +/** + * Expand/collapse the logs result viewer. When collapsed, log queries won't be run. + */ +export function toggleLogs(exploreId: ExploreId): ThunkResult { + return (dispatch, getState) => { + dispatch({ type: ActionTypes.ToggleLogs, exploreId }); + if (getState().explore[exploreId].showingLogs) { + dispatch(runQueries(exploreId)); + } + }; +} + +/** + * Expand/collapse the table result viewer. When collapsed, table queries won't be run. + */ +export function toggleTable(exploreId: ExploreId): ThunkResult { + return (dispatch, getState) => { + dispatch({ type: ActionTypes.ToggleTable, exploreId }); + if (getState().explore[exploreId].showingTable) { + dispatch(runQueries(exploreId)); + } + }; +} diff --git a/public/app/features/explore/state/reducers.ts b/public/app/features/explore/state/reducers.ts index 91d5c4cf925..73790ba14bc 100644 --- a/public/app/features/explore/state/reducers.ts +++ b/public/app/features/explore/state/reducers.ts @@ -7,7 +7,7 @@ import { import { ExploreItemState, ExploreState, QueryTransaction } from 'app/types/explore'; import { DataQuery } from 'app/types/series'; -import { Action, ActionTypes } from './actions'; +import { Action, ActionTypes } from './actionTypes'; export const DEFAULT_RANGE = { from: 'now-6h', @@ -62,14 +62,17 @@ const itemReducer = (state, action: Action): ExploreItemState => { case ActionTypes.AddQueryRow: { const { initialQueries, modifiedQueries, queryTransactions } = state; const { index, query } = action; - modifiedQueries[index + 1] = query; - const nextQueries = [ - ...initialQueries.slice(0, index + 1), - { ...modifiedQueries[index + 1] }, + // Add new query row after given index, keep modifications of existing rows + const nextModifiedQueries = [ + ...modifiedQueries.slice(0, index + 1), + { ...query }, ...initialQueries.slice(index + 1), ]; + // Add to initialQueries, which will cause a new row to be rendered + const nextQueries = [...initialQueries.slice(0, index + 1), { ...query }, ...initialQueries.slice(index + 1)]; + // Ongoing transactions need to update their row indices const nextQueryTransactions = queryTransactions.map(qt => { if (qt.rowIndex > index) { @@ -83,9 +86,9 @@ const itemReducer = (state, action: Action): ExploreItemState => { return { ...state, - modifiedQueries, initialQueries: nextQueries, logsHighlighterExpressions: undefined, + modifiedQueries: nextModifiedQueries, queryTransactions: nextQueryTransactions, }; } @@ -94,29 +97,33 @@ const itemReducer = (state, action: Action): ExploreItemState => { const { initialQueries, queryTransactions } = state; let { modifiedQueries } = state; const { query, index, override } = action; + + // Fast path: only change modifiedQueries to not trigger an update modifiedQueries[index] = query; - if (override) { - const nextQuery: DataQuery = { - ...query, - ...generateEmptyQuery(index), - }; - const nextQueries = [...initialQueries]; - nextQueries[index] = nextQuery; - modifiedQueries = [...nextQueries]; - - // Discard ongoing transaction related to row query - const nextQueryTransactions = queryTransactions.filter(qt => qt.rowIndex !== index); - + if (!override) { return { ...state, - initialQueries: nextQueries, - modifiedQueries: nextQueries.slice(), - queryTransactions: nextQueryTransactions, + modifiedQueries, }; } + + // Override path: queries are completely reset + const nextQuery: DataQuery = { + ...query, + ...generateEmptyQuery(index), + }; + const nextQueries = [...initialQueries]; + nextQueries[index] = nextQuery; + modifiedQueries = [...nextQueries]; + + // Discard ongoing transaction related to row query + const nextQueryTransactions = queryTransactions.filter(qt => qt.rowIndex !== index); + return { ...state, - modifiedQueries, + initialQueries: nextQueries, + modifiedQueries: nextQueries.slice(), + queryTransactions: nextQueryTransactions, }; } @@ -138,58 +145,17 @@ const itemReducer = (state, action: Action): ExploreItemState => { }; } - case ActionTypes.ClickClear: { + case ActionTypes.ClearQueries: { const queries = ensureQueries(); return { ...state, initialQueries: queries.slice(), modifiedQueries: queries.slice(), + queryTransactions: [], showingStartPage: Boolean(state.StartPage), }; } - case ActionTypes.ClickExample: { - const modifiedQueries = [action.query]; - return { ...state, initialQueries: modifiedQueries.slice(), modifiedQueries }; - } - - case ActionTypes.ClickGraphButton: { - const showingGraph = !state.showingGraph; - let nextQueryTransactions = state.queryTransactions; - if (!showingGraph) { - // Discard transactions related to Graph query - nextQueryTransactions = state.queryTransactions.filter(qt => qt.resultType !== 'Graph'); - } - return { ...state, queryTransactions: nextQueryTransactions, showingGraph }; - } - - case ActionTypes.ClickLogsButton: { - const showingLogs = !state.showingLogs; - let nextQueryTransactions = state.queryTransactions; - if (!showingLogs) { - // Discard transactions related to Logs query - nextQueryTransactions = state.queryTransactions.filter(qt => qt.resultType !== 'Logs'); - } - return { ...state, queryTransactions: nextQueryTransactions, showingLogs }; - } - - case ActionTypes.ClickTableButton: { - const showingTable = !state.showingTable; - if (showingTable) { - return { ...state, showingTable, queryTransactions: state.queryTransactions }; - } - - // Toggle off needs discarding of table queries and results - const nextQueryTransactions = state.queryTransactions.filter(qt => qt.resultType !== 'Table'); - const results = calculateResultsFromQueryTransactions( - nextQueryTransactions, - state.datasourceInstance, - state.queryIntervals.intervalMs - ); - - return { ...state, ...results, queryTransactions: nextQueryTransactions, showingTable }; - } - case ActionTypes.HighlightLogsExpression: { const { expressions } = action; return { ...state, logsHighlighterExpressions: expressions }; @@ -248,7 +214,7 @@ const itemReducer = (state, action: Action): ExploreItemState => { case ActionTypes.ModifyQueries: { const { initialQueries, modifiedQueries, queryTransactions } = state; - const { action: modification, index, modifier } = action as any; + const { modification, index, modifier } = action as any; let nextQueries: DataQuery[]; let nextQueryTransactions; if (index === undefined) { @@ -290,37 +256,6 @@ const itemReducer = (state, action: Action): ExploreItemState => { }; } - case ActionTypes.RemoveQueryRow: { - const { datasourceInstance, initialQueries, queryIntervals, queryTransactions } = state; - let { modifiedQueries } = state; - const { index } = action; - - modifiedQueries = [...modifiedQueries.slice(0, index), ...modifiedQueries.slice(index + 1)]; - - if (initialQueries.length <= 1) { - return state; - } - - const nextQueries = [...initialQueries.slice(0, index), ...initialQueries.slice(index + 1)]; - - // Discard transactions related to row query - const nextQueryTransactions = queryTransactions.filter(qt => qt.rowIndex !== index); - const results = calculateResultsFromQueryTransactions( - nextQueryTransactions, - datasourceInstance, - queryIntervals.intervalMs - ); - - return { - ...state, - ...results, - initialQueries: nextQueries, - logsHighlighterExpressions: undefined, - modifiedQueries: nextQueries.slice(), - queryTransactions: nextQueryTransactions, - }; - } - case ActionTypes.QueryTransactionFailure: { const { queryTransactions } = action; return { @@ -373,6 +308,41 @@ const itemReducer = (state, action: Action): ExploreItemState => { }; } + case ActionTypes.RemoveQueryRow: { + const { datasourceInstance, initialQueries, queryIntervals, queryTransactions } = state; + let { modifiedQueries } = state; + const { index } = action; + + modifiedQueries = [...modifiedQueries.slice(0, index), ...modifiedQueries.slice(index + 1)]; + + if (initialQueries.length <= 1) { + return state; + } + + const nextQueries = [...initialQueries.slice(0, index), ...initialQueries.slice(index + 1)]; + + // Discard transactions related to row query + const nextQueryTransactions = queryTransactions.filter(qt => qt.rowIndex !== index); + const results = calculateResultsFromQueryTransactions( + nextQueryTransactions, + datasourceInstance, + queryIntervals.intervalMs + ); + + return { + ...state, + ...results, + initialQueries: nextQueries, + logsHighlighterExpressions: undefined, + modifiedQueries: nextQueries.slice(), + queryTransactions: nextQueryTransactions, + }; + } + + case ActionTypes.RunQueriesEmpty: { + return { ...state, queryTransactions: [] }; + } + case ActionTypes.ScanRange: { return { ...state, scanRange: action.range }; } @@ -386,6 +356,48 @@ const itemReducer = (state, action: Action): ExploreItemState => { const nextQueryTransactions = queryTransactions.filter(qt => qt.scanning && !qt.done); return { ...state, queryTransactions: nextQueryTransactions, scanning: false, scanRange: undefined }; } + + case ActionTypes.SetQueries: { + const { queries } = action; + return { ...state, initialQueries: queries.slice(), modifiedQueries: queries.slice() }; + } + + case ActionTypes.ToggleGraph: { + const showingGraph = !state.showingGraph; + let nextQueryTransactions = state.queryTransactions; + if (!showingGraph) { + // Discard transactions related to Graph query + nextQueryTransactions = state.queryTransactions.filter(qt => qt.resultType !== 'Graph'); + } + return { ...state, queryTransactions: nextQueryTransactions, showingGraph }; + } + + case ActionTypes.ToggleLogs: { + const showingLogs = !state.showingLogs; + let nextQueryTransactions = state.queryTransactions; + if (!showingLogs) { + // Discard transactions related to Logs query + nextQueryTransactions = state.queryTransactions.filter(qt => qt.resultType !== 'Logs'); + } + return { ...state, queryTransactions: nextQueryTransactions, showingLogs }; + } + + case ActionTypes.ToggleTable: { + const showingTable = !state.showingTable; + if (showingTable) { + return { ...state, showingTable, queryTransactions: state.queryTransactions }; + } + + // Toggle off needs discarding of table queries and results + const nextQueryTransactions = state.queryTransactions.filter(qt => qt.resultType !== 'Table'); + const results = calculateResultsFromQueryTransactions( + nextQueryTransactions, + state.datasourceInstance, + state.queryIntervals.intervalMs + ); + + return { ...state, ...results, queryTransactions: nextQueryTransactions, showingTable }; + } } return state; @@ -397,14 +409,14 @@ const itemReducer = (state, action: Action): ExploreItemState => { */ export const exploreReducer = (state = initialExploreState, action: Action): ExploreState => { switch (action.type) { - case ActionTypes.ClickCloseSplit: { + case ActionTypes.SplitClose: { return { ...state, split: false, }; } - case ActionTypes.ClickSplit: { + case ActionTypes.SplitOpen: { return { ...state, split: true, diff --git a/public/app/types/explore.ts b/public/app/types/explore.ts index 3cef4124ee4..5636bb3acdb 100644 --- a/public/app/types/explore.ts +++ b/public/app/types/explore.ts @@ -83,43 +83,160 @@ export enum ExploreId { right = 'right', } +/** + * Global Explore state + */ export interface ExploreState { + /** + * True if split view is active. + */ split: boolean; + /** + * Explore state of the left split (left is default in non-split view). + */ left: ExploreItemState; + /** + * Explore state of the right area in split view. + */ right: ExploreItemState; } export interface ExploreItemState { + /** + * React component to be shown when no queries have been run yet, e.g., for a query language cheat sheet. + */ StartPage?: any; + /** + * Width used for calculating the graph interval (can't have more datapoints than pixels) + */ containerWidth: number; + /** + * Datasource instance that has been selected. Datasource-specific logic can be run on this object. + */ datasourceInstance: any; + /** + * Error to be shown when datasource loading or testing failed. + */ datasourceError: string; + /** + * True if the datasource is loading. `null` if the loading has not started yet. + */ datasourceLoading: boolean | null; + /** + * True if there is no datasource to be selected. + */ datasourceMissing: boolean; + /** + * Emitter to send events to the rest of Grafana. + */ eventBridge?: Emitter; + /** + * List of datasources to be shown in the datasource selector. + */ exploreDatasources: DataSourceSelectItem[]; + /** + * List of timeseries to be shown in the Explore graph result viewer. + */ graphResult?: any[]; + /** + * History of recent queries. Datasource-specific and initialized via localStorage. + */ history: HistoryItem[]; + /** + * Initial datasource for this Explore, e.g., set via URL. + */ initialDatasource?: string; + /** + * Initial queries for this Explore, e.g., set via URL. Each query will be + * converted to a query row. Query edits should be tracked in `modifiedQueries` though. + */ initialQueries: DataQuery[]; + /** + * True if this Explore area has been initialized. + * Used to distinguish URL state injection versus split view state injection. + */ initialized: boolean; + /** + * Log line substrings to be highlighted as you type in a query field. + * Currently supports only the first query row. + */ logsHighlighterExpressions?: string[]; + /** + * Log query result to be displayed in the logs result viewer. + */ logsResult?: LogsModel; + /** + * Copy of `initialQueries` that tracks user edits. + * Don't connect this property to a react component as it is updated on every query change. + * Used when running queries. Needs to be reset to `initialQueries` when those are reset as well. + */ modifiedQueries: DataQuery[]; + /** + * Query intervals for graph queries to determine how many datapoints to return. + * Needs to be updated when `datasourceInstance` or `containerWidth` is changed. + */ queryIntervals: QueryIntervals; + /** + * List of query transaction to track query duration and query result. + * Graph/Logs/Table results are calculated on the fly from the transaction, + * based on the transaction's result types. Transaction also holds the row index + * so that results can be dropped and re-computed without running queries again + * when query rows are removed. + */ queryTransactions: QueryTransaction[]; + /** + * Tracks datasource when selected in the datasource selector. + * Allows the selection to be discarded if something went wrong during the asynchronous + * loading of the datasource. + */ requestedDatasourceId?: number; + /** + * Time range for this Explore. Managed by the time picker and used by all query runs. + */ range: TimeRange | RawTimeRange; + /** + * Scanner function that calculates a new range, triggers a query run, and returns the new range. + */ scanner?: RangeScanner; + /** + * True if scanning for more results is active. + */ scanning?: boolean; + /** + * Current scanning range to be shown to the user while scanning is active. + */ scanRange?: RawTimeRange; + /** + * True if graph result viewer is expanded. Query runs will contain graph queries. + */ showingGraph: boolean; + /** + * True if logs result viewer is expanded. Query runs will contain logs queries. + */ showingLogs: boolean; + /** + * True StartPage needs to be shown. Typically set to `false` once queries have been run. + */ showingStartPage?: boolean; + /** + * True if table result viewer is expanded. Query runs will contain table queries. + */ showingTable: boolean; + /** + * True if `datasourceInstance` supports graph queries. + */ supportsGraph: boolean | null; + /** + * True if `datasourceInstance` supports logs queries. + */ supportsLogs: boolean | null; + /** + * True if `datasourceInstance` supports table queries. + */ supportsTable: boolean | null; + /** + * Table model that combines all query table results into a single table. + */ tableResult?: TableModel; } From 9575a4a2c0c8ba1b249859b70fc425bf86695c71 Mon Sep 17 00:00:00 2001 From: David Kaltschmidt Date: Wed, 16 Jan 2019 10:21:11 +0100 Subject: [PATCH 9/9] Move action properties to payload --- .../app/features/explore/state/actionTypes.ts | 180 +++++++++++------- public/app/features/explore/state/actions.ts | 109 ++++++----- public/app/features/explore/state/reducers.ts | 87 +++++---- 3 files changed, 227 insertions(+), 149 deletions(-) diff --git a/public/app/features/explore/state/actionTypes.ts b/public/app/features/explore/state/actionTypes.ts index ed0995cff17..b267da4f2c1 100644 --- a/public/app/features/explore/state/actionTypes.ts +++ b/public/app/features/explore/state/actionTypes.ts @@ -47,52 +47,66 @@ export enum ActionTypes { export interface AddQueryRowAction { type: ActionTypes.AddQueryRow; - exploreId: ExploreId; - index: number; - query: DataQuery; + payload: { + exploreId: ExploreId; + index: number; + query: DataQuery; + }; } export interface ChangeQueryAction { type: ActionTypes.ChangeQuery; - exploreId: ExploreId; - query: DataQuery; - index: number; - override: boolean; + payload: { + exploreId: ExploreId; + query: DataQuery; + index: number; + override: boolean; + }; } export interface ChangeSizeAction { type: ActionTypes.ChangeSize; - exploreId: ExploreId; - width: number; - height: number; + payload: { + exploreId: ExploreId; + width: number; + height: number; + }; } export interface ChangeTimeAction { type: ActionTypes.ChangeTime; - exploreId: ExploreId; - range: TimeRange; + payload: { + exploreId: ExploreId; + range: TimeRange; + }; } export interface ClearQueriesAction { type: ActionTypes.ClearQueries; - exploreId: ExploreId; + payload: { + exploreId: ExploreId; + }; } export interface HighlightLogsExpressionAction { type: ActionTypes.HighlightLogsExpression; - exploreId: ExploreId; - expressions: string[]; + payload: { + exploreId: ExploreId; + expressions: string[]; + }; } export interface InitializeExploreAction { type: ActionTypes.InitializeExplore; - exploreId: ExploreId; - containerWidth: number; - datasource: string; - eventBridge: Emitter; - exploreDatasources: DataSourceSelectItem[]; - queries: DataQuery[]; - range: RawTimeRange; + payload: { + exploreId: ExploreId; + containerWidth: number; + datasource: string; + eventBridge: Emitter; + exploreDatasources: DataSourceSelectItem[]; + queries: DataQuery[]; + range: RawTimeRange; + }; } export interface InitializeExploreSplitAction { @@ -101,97 +115,125 @@ export interface InitializeExploreSplitAction { export interface LoadDatasourceFailureAction { type: ActionTypes.LoadDatasourceFailure; - exploreId: ExploreId; - error: string; + payload: { + exploreId: ExploreId; + error: string; + }; } export interface LoadDatasourcePendingAction { type: ActionTypes.LoadDatasourcePending; - exploreId: ExploreId; - datasourceId: number; + payload: { + exploreId: ExploreId; + datasourceId: number; + }; } export interface LoadDatasourceMissingAction { type: ActionTypes.LoadDatasourceMissing; - exploreId: ExploreId; + payload: { + exploreId: ExploreId; + }; } export interface LoadDatasourceSuccessAction { type: ActionTypes.LoadDatasourceSuccess; - exploreId: ExploreId; - StartPage?: any; - datasourceInstance: any; - history: HistoryItem[]; - initialDatasource: string; - initialQueries: DataQuery[]; - logsHighlighterExpressions?: any[]; - showingStartPage: boolean; - supportsGraph: boolean; - supportsLogs: boolean; - supportsTable: boolean; + payload: { + exploreId: ExploreId; + StartPage?: any; + datasourceInstance: any; + history: HistoryItem[]; + initialDatasource: string; + initialQueries: DataQuery[]; + logsHighlighterExpressions?: any[]; + showingStartPage: boolean; + supportsGraph: boolean; + supportsLogs: boolean; + supportsTable: boolean; + }; } export interface ModifyQueriesAction { type: ActionTypes.ModifyQueries; - exploreId: ExploreId; - modification: any; - index: number; - modifier: (queries: DataQuery[], modification: any) => DataQuery[]; + payload: { + exploreId: ExploreId; + modification: any; + index: number; + modifier: (queries: DataQuery[], modification: any) => DataQuery[]; + }; } export interface QueryTransactionFailureAction { type: ActionTypes.QueryTransactionFailure; - exploreId: ExploreId; - queryTransactions: QueryTransaction[]; + payload: { + exploreId: ExploreId; + queryTransactions: QueryTransaction[]; + }; } export interface QueryTransactionStartAction { type: ActionTypes.QueryTransactionStart; - exploreId: ExploreId; - resultType: ResultType; - rowIndex: number; - transaction: QueryTransaction; + payload: { + exploreId: ExploreId; + resultType: ResultType; + rowIndex: number; + transaction: QueryTransaction; + }; } export interface QueryTransactionSuccessAction { type: ActionTypes.QueryTransactionSuccess; - exploreId: ExploreId; - history: HistoryItem[]; - queryTransactions: QueryTransaction[]; + payload: { + exploreId: ExploreId; + history: HistoryItem[]; + queryTransactions: QueryTransaction[]; + }; } export interface RemoveQueryRowAction { type: ActionTypes.RemoveQueryRow; - exploreId: ExploreId; - index: number; + payload: { + exploreId: ExploreId; + index: number; + }; } export interface RunQueriesEmptyAction { type: ActionTypes.RunQueriesEmpty; - exploreId: ExploreId; + payload: { + exploreId: ExploreId; + }; } export interface ScanStartAction { type: ActionTypes.ScanStart; - exploreId: ExploreId; - scanner: RangeScanner; + payload: { + exploreId: ExploreId; + scanner: RangeScanner; + }; } export interface ScanRangeAction { type: ActionTypes.ScanRange; - exploreId: ExploreId; - range: RawTimeRange; + payload: { + exploreId: ExploreId; + range: RawTimeRange; + }; } export interface ScanStopAction { type: ActionTypes.ScanStop; - exploreId: ExploreId; + payload: { + exploreId: ExploreId; + }; } export interface SetQueriesAction { type: ActionTypes.SetQueries; - exploreId: ExploreId; - queries: DataQuery[]; + payload: { + exploreId: ExploreId; + queries: DataQuery[]; + }; } export interface SplitCloseAction { @@ -200,7 +242,9 @@ export interface SplitCloseAction { export interface SplitOpenAction { type: ActionTypes.SplitOpen; - itemState: ExploreItemState; + payload: { + itemState: ExploreItemState; + }; } export interface StateSaveAction { @@ -209,17 +253,23 @@ export interface StateSaveAction { export interface ToggleTableAction { type: ActionTypes.ToggleTable; - exploreId: ExploreId; + payload: { + exploreId: ExploreId; + }; } export interface ToggleGraphAction { type: ActionTypes.ToggleGraph; - exploreId: ExploreId; + payload: { + exploreId: ExploreId; + }; } export interface ToggleLogsAction { type: ActionTypes.ToggleLogs; - exploreId: ExploreId; + payload: { + exploreId: ExploreId; + }; } export type Action = diff --git a/public/app/features/explore/state/actions.ts b/public/app/features/explore/state/actions.ts index ecfb35c8c2f..ae0bce6a019 100644 --- a/public/app/features/explore/state/actions.ts +++ b/public/app/features/explore/state/actions.ts @@ -52,7 +52,7 @@ type ThunkResult = ThunkAction; */ export function addQueryRow(exploreId: ExploreId, index: number): AddQueryRowAction { const query = generateEmptyQuery(index + 1); - return { type: ActionTypes.AddQueryRow, exploreId, index, query }; + return { type: ActionTypes.AddQueryRow, payload: { exploreId, index, query } }; } /** @@ -81,7 +81,7 @@ export function changeQuery( query = { ...generateEmptyQuery(index) }; } - dispatch({ type: ActionTypes.ChangeQuery, exploreId, query, index, override }); + dispatch({ type: ActionTypes.ChangeQuery, payload: { exploreId, query, index, override } }); if (override) { dispatch(runQueries(exploreId)); } @@ -96,7 +96,7 @@ export function changeSize( exploreId: ExploreId, { height, width }: { height: number; width: number } ): ChangeSizeAction { - return { type: ActionTypes.ChangeSize, exploreId, height, width }; + return { type: ActionTypes.ChangeSize, payload: { exploreId, height, width } }; } /** @@ -104,7 +104,7 @@ export function changeSize( */ export function changeTime(exploreId: ExploreId, range: TimeRange): ThunkResult { return dispatch => { - dispatch({ type: ActionTypes.ChangeTime, exploreId, range }); + dispatch({ type: ActionTypes.ChangeTime, payload: { exploreId, range } }); dispatch(runQueries(exploreId)); }; } @@ -115,7 +115,7 @@ export function changeTime(exploreId: ExploreId, range: TimeRange): ThunkResult< export function clearQueries(exploreId: ExploreId): ThunkResult { return dispatch => { dispatch(scanStop(exploreId)); - dispatch({ type: ActionTypes.ClearQueries, exploreId }); + dispatch({ type: ActionTypes.ClearQueries, payload: { exploreId } }); dispatch(stateSave()); }; } @@ -124,7 +124,7 @@ export function clearQueries(exploreId: ExploreId): ThunkResult { * Highlight expressions in the log results */ export function highlightLogsExpression(exploreId: ExploreId, expressions: string[]): HighlightLogsExpressionAction { - return { type: ActionTypes.HighlightLogsExpression, exploreId, expressions }; + return { type: ActionTypes.HighlightLogsExpression, payload: { exploreId, expressions } }; } /** @@ -150,13 +150,15 @@ export function initializeExplore( dispatch({ type: ActionTypes.InitializeExplore, - exploreId, - containerWidth, - datasource, - eventBridge, - exploreDatasources, - queries, - range, + payload: { + exploreId, + containerWidth, + datasource, + eventBridge, + exploreDatasources, + queries, + range, + }, }); if (exploreDatasources.length > 1) { @@ -187,8 +189,10 @@ export function initializeExploreSplit() { */ export const loadDatasourceFailure = (exploreId: ExploreId, error: string): LoadDatasourceFailureAction => ({ type: ActionTypes.LoadDatasourceFailure, - exploreId, - error, + payload: { + exploreId, + error, + }, }); /** @@ -196,7 +200,7 @@ export const loadDatasourceFailure = (exploreId: ExploreId, error: string): Load */ export const loadDatasourceMissing = (exploreId: ExploreId): LoadDatasourceMissingAction => ({ type: ActionTypes.LoadDatasourceMissing, - exploreId, + payload: { exploreId }, }); /** @@ -204,8 +208,10 @@ export const loadDatasourceMissing = (exploreId: ExploreId): LoadDatasourceMissi */ export const loadDatasourcePending = (exploreId: ExploreId, datasourceId: number): LoadDatasourcePendingAction => ({ type: ActionTypes.LoadDatasourcePending, - exploreId, - datasourceId, + payload: { + exploreId, + datasourceId, + }, }); /** @@ -232,16 +238,18 @@ export const loadDatasourceSuccess = ( return { type: ActionTypes.LoadDatasourceSuccess, - exploreId, - StartPage, - datasourceInstance: instance, - history, - initialDatasource: instance.name, - initialQueries: queries, - showingStartPage: Boolean(StartPage), - supportsGraph, - supportsLogs, - supportsTable, + payload: { + exploreId, + StartPage, + datasourceInstance: instance, + history, + initialDatasource: instance.name, + initialQueries: queries, + showingStartPage: Boolean(StartPage), + supportsGraph, + supportsLogs, + supportsTable, + }, }; }; @@ -323,7 +331,7 @@ export function modifyQueries( modifier: any ): ThunkResult { return dispatch => { - dispatch({ type: ActionTypes.ModifyQueries, exploreId, modification, index, modifier }); + dispatch({ type: ActionTypes.ModifyQueries, payload: { exploreId, modification, index, modifier } }); if (!modification.preventSubmit) { dispatch(runQueries(exploreId)); } @@ -349,7 +357,7 @@ export function queryTransactionFailure( // Transaction might have been discarded if (!queryTransactions.find(qt => qt.id === transactionId)) { - return null; + return; } console.error(response); @@ -388,7 +396,10 @@ export function queryTransactionFailure( return qt; }); - dispatch({ type: ActionTypes.QueryTransactionFailure, exploreId, queryTransactions: nextQueryTransactions }); + dispatch({ + type: ActionTypes.QueryTransactionFailure, + payload: { exploreId, queryTransactions: nextQueryTransactions }, + }); }; } @@ -405,7 +416,7 @@ export function queryTransactionStart( resultType: ResultType, rowIndex: number ): QueryTransactionStartAction { - return { type: ActionTypes.QueryTransactionStart, exploreId, resultType, rowIndex, transaction }; + return { type: ActionTypes.QueryTransactionStart, payload: { exploreId, resultType, rowIndex, transaction } }; } /** @@ -466,9 +477,11 @@ export function queryTransactionSuccess( dispatch({ type: ActionTypes.QueryTransactionSuccess, - exploreId, - history: nextHistory, - queryTransactions: nextQueryTransactions, + payload: { + exploreId, + history: nextHistory, + queryTransactions: nextQueryTransactions, + }, }); // Keep scanning for results if this was the last scanning transaction @@ -477,7 +490,7 @@ export function queryTransactionSuccess( const other = nextQueryTransactions.find(qt => qt.scanning && !qt.done); if (!other) { const range = scanner(); - dispatch({ type: ActionTypes.ScanRange, exploreId, range }); + dispatch({ type: ActionTypes.ScanRange, payload: { exploreId, range } }); } } else { // We can stop scanning if we have a result @@ -492,7 +505,7 @@ export function queryTransactionSuccess( */ export function removeQueryRow(exploreId: ExploreId, index: number): ThunkResult { return dispatch => { - dispatch({ type: ActionTypes.RemoveQueryRow, exploreId, index }); + dispatch({ type: ActionTypes.RemoveQueryRow, payload: { exploreId, index } }); dispatch(runQueries(exploreId)); }; } @@ -514,7 +527,7 @@ export function runQueries(exploreId: ExploreId) { } = getState().explore[exploreId]; if (!hasNonEmptyQuery(modifiedQueries)) { - dispatch({ type: ActionTypes.RunQueriesEmpty, exploreId }); + dispatch({ type: ActionTypes.RunQueriesEmpty, payload: { exploreId } }); return; } @@ -618,11 +631,11 @@ function runQueriesForType( export function scanStart(exploreId: ExploreId, scanner: RangeScanner): ThunkResult { return dispatch => { // Register the scanner - dispatch({ type: ActionTypes.ScanStart, exploreId, scanner }); + dispatch({ type: ActionTypes.ScanStart, payload: { exploreId, scanner } }); // Scanning must trigger query run, and return the new range const range = scanner(); // Set the new range to be displayed - dispatch({ type: ActionTypes.ScanRange, exploreId, range }); + dispatch({ type: ActionTypes.ScanRange, payload: { exploreId, range } }); }; } @@ -630,7 +643,7 @@ export function scanStart(exploreId: ExploreId, scanner: RangeScanner): ThunkRes * Stop any scanning for more results. */ export function scanStop(exploreId: ExploreId): ScanStopAction { - return { type: ActionTypes.ScanStop, exploreId }; + return { type: ActionTypes.ScanStop, payload: { exploreId } }; } /** @@ -643,8 +656,10 @@ export function setQueries(exploreId: ExploreId, rawQueries: DataQuery[]): Thunk const queries = rawQueries.map(q => ({ ...q, ...generateEmptyQuery() })); dispatch({ type: ActionTypes.SetQueries, - exploreId, - queries, + payload: { + exploreId, + queries, + }, }); dispatch(runQueries(exploreId)); }; @@ -674,7 +689,7 @@ export function splitOpen(): ThunkResult { queryTransactions: [], initialQueries: leftState.modifiedQueries.slice(), }; - dispatch({ type: ActionTypes.SplitOpen, itemState }); + dispatch({ type: ActionTypes.SplitOpen, payload: { itemState } }); dispatch(stateSave()); }; } @@ -710,7 +725,7 @@ export function stateSave() { */ export function toggleGraph(exploreId: ExploreId): ThunkResult { return (dispatch, getState) => { - dispatch({ type: ActionTypes.ToggleGraph, exploreId }); + dispatch({ type: ActionTypes.ToggleGraph, payload: { exploreId } }); if (getState().explore[exploreId].showingGraph) { dispatch(runQueries(exploreId)); } @@ -722,7 +737,7 @@ export function toggleGraph(exploreId: ExploreId): ThunkResult { */ export function toggleLogs(exploreId: ExploreId): ThunkResult { return (dispatch, getState) => { - dispatch({ type: ActionTypes.ToggleLogs, exploreId }); + dispatch({ type: ActionTypes.ToggleLogs, payload: { exploreId } }); if (getState().explore[exploreId].showingLogs) { dispatch(runQueries(exploreId)); } @@ -734,7 +749,7 @@ export function toggleLogs(exploreId: ExploreId): ThunkResult { */ export function toggleTable(exploreId: ExploreId): ThunkResult { return (dispatch, getState) => { - dispatch({ type: ActionTypes.ToggleTable, exploreId }); + dispatch({ type: ActionTypes.ToggleTable, payload: { exploreId } }); if (getState().explore[exploreId].showingTable) { dispatch(runQueries(exploreId)); } diff --git a/public/app/features/explore/state/reducers.ts b/public/app/features/explore/state/reducers.ts index 73790ba14bc..b112a5370e3 100644 --- a/public/app/features/explore/state/reducers.ts +++ b/public/app/features/explore/state/reducers.ts @@ -61,7 +61,7 @@ const itemReducer = (state, action: Action): ExploreItemState => { switch (action.type) { case ActionTypes.AddQueryRow: { const { initialQueries, modifiedQueries, queryTransactions } = state; - const { index, query } = action; + const { index, query } = action.payload; // Add new query row after given index, keep modifications of existing rows const nextModifiedQueries = [ @@ -96,7 +96,7 @@ const itemReducer = (state, action: Action): ExploreItemState => { case ActionTypes.ChangeQuery: { const { initialQueries, queryTransactions } = state; let { modifiedQueries } = state; - const { query, index, override } = action; + const { query, index, override } = action.payload; // Fast path: only change modifiedQueries to not trigger an update modifiedQueries[index] = query; @@ -133,7 +133,7 @@ const itemReducer = (state, action: Action): ExploreItemState => { if (datasourceInstance && datasourceInstance.interval) { interval = datasourceInstance.interval; } - const containerWidth = action.width; + const containerWidth = action.payload.width; const queryIntervals = getIntervals(range, interval, containerWidth); return { ...state, containerWidth, queryIntervals }; } @@ -141,7 +141,7 @@ const itemReducer = (state, action: Action): ExploreItemState => { case ActionTypes.ChangeTime: { return { ...state, - range: action.range, + range: action.payload.range, }; } @@ -157,27 +157,27 @@ const itemReducer = (state, action: Action): ExploreItemState => { } case ActionTypes.HighlightLogsExpression: { - const { expressions } = action; + const { expressions } = action.payload; return { ...state, logsHighlighterExpressions: expressions }; } case ActionTypes.InitializeExplore: { - const { containerWidth, eventBridge, exploreDatasources, range } = action; + const { containerWidth, datasource, eventBridge, exploreDatasources, queries, range } = action.payload; return { ...state, containerWidth, eventBridge, exploreDatasources, range, - initialDatasource: action.datasource, - initialQueries: action.queries, + initialDatasource: datasource, + initialQueries: queries, initialized: true, - modifiedQueries: action.queries.slice(), + modifiedQueries: queries.slice(), }; } case ActionTypes.LoadDatasourceFailure: { - return { ...state, datasourceError: action.error, datasourceLoading: false }; + return { ...state, datasourceError: action.payload.error, datasourceLoading: false }; } case ActionTypes.LoadDatasourceMissing: { @@ -185,36 +185,47 @@ const itemReducer = (state, action: Action): ExploreItemState => { } case ActionTypes.LoadDatasourcePending: { - return { ...state, datasourceLoading: true, requestedDatasourceId: action.datasourceId }; + return { ...state, datasourceLoading: true, requestedDatasourceId: action.payload.datasourceId }; } case ActionTypes.LoadDatasourceSuccess: { const { containerWidth, range } = state; - const queryIntervals = getIntervals(range, action.datasourceInstance.interval, containerWidth); + const { + StartPage, + datasourceInstance, + history, + initialDatasource, + initialQueries, + showingStartPage, + supportsGraph, + supportsLogs, + supportsTable, + } = action.payload; + const queryIntervals = getIntervals(range, datasourceInstance.interval, containerWidth); return { ...state, queryIntervals, - StartPage: action.StartPage, - datasourceInstance: action.datasourceInstance, + StartPage, + datasourceInstance, + history, + initialDatasource, + initialQueries, + showingStartPage, + supportsGraph, + supportsLogs, + supportsTable, datasourceLoading: false, datasourceMissing: false, - history: action.history, - initialDatasource: action.initialDatasource, - initialQueries: action.initialQueries, logsHighlighterExpressions: undefined, - modifiedQueries: action.initialQueries.slice(), + modifiedQueries: initialQueries.slice(), queryTransactions: [], - showingStartPage: action.showingStartPage, - supportsGraph: action.supportsGraph, - supportsLogs: action.supportsLogs, - supportsTable: action.supportsTable, }; } case ActionTypes.ModifyQueries: { const { initialQueries, modifiedQueries, queryTransactions } = state; - const { modification, index, modifier } = action as any; + const { modification, index, modifier } = action.payload as any; let nextQueries: DataQuery[]; let nextQueryTransactions; if (index === undefined) { @@ -257,7 +268,7 @@ const itemReducer = (state, action: Action): ExploreItemState => { } case ActionTypes.QueryTransactionFailure: { - const { queryTransactions } = action; + const { queryTransactions } = action.payload; return { ...state, queryTransactions, @@ -267,7 +278,7 @@ const itemReducer = (state, action: Action): ExploreItemState => { case ActionTypes.QueryTransactionStart: { const { datasourceInstance, queryIntervals, queryTransactions } = state; - const { resultType, rowIndex, transaction } = action; + const { resultType, rowIndex, transaction } = action.payload; // Discarding existing transactions of same type const remainingTransactions = queryTransactions.filter( qt => !(qt.resultType === resultType && qt.rowIndex === rowIndex) @@ -292,7 +303,7 @@ const itemReducer = (state, action: Action): ExploreItemState => { case ActionTypes.QueryTransactionSuccess: { const { datasourceInstance, queryIntervals } = state; - const { history, queryTransactions } = action; + const { history, queryTransactions } = action.payload; const results = calculateResultsFromQueryTransactions( queryTransactions, datasourceInstance, @@ -311,7 +322,7 @@ const itemReducer = (state, action: Action): ExploreItemState => { case ActionTypes.RemoveQueryRow: { const { datasourceInstance, initialQueries, queryIntervals, queryTransactions } = state; let { modifiedQueries } = state; - const { index } = action; + const { index } = action.payload; modifiedQueries = [...modifiedQueries.slice(0, index), ...modifiedQueries.slice(index + 1)]; @@ -344,7 +355,7 @@ const itemReducer = (state, action: Action): ExploreItemState => { } case ActionTypes.ScanRange: { - return { ...state, scanRange: action.range }; + return { ...state, scanRange: action.payload.range }; } case ActionTypes.ScanStart: { @@ -358,7 +369,7 @@ const itemReducer = (state, action: Action): ExploreItemState => { } case ActionTypes.SetQueries: { - const { queries } = action; + const { queries } = action.payload; return { ...state, initialQueries: queries.slice(), modifiedQueries: queries.slice() }; } @@ -420,7 +431,7 @@ export const exploreReducer = (state = initialExploreState, action: Action): Exp return { ...state, split: true, - right: action.itemState, + right: action.payload.itemState, }; } @@ -432,13 +443,15 @@ export const exploreReducer = (state = initialExploreState, action: Action): Exp } } - const { exploreId } = action as any; - if (exploreId !== undefined) { - const exploreItemState = state[exploreId]; - return { - ...state, - [exploreId]: itemReducer(exploreItemState, action), - }; + if (action.payload) { + const { exploreId } = action.payload as any; + if (exploreId !== undefined) { + const exploreItemState = state[exploreId]; + return { + ...state, + [exploreId]: itemReducer(exploreItemState, action), + }; + } } return state;