grafana/public/app/features/explore/state/actions.ts
Johannes Schill 406ef962fc Explore & Dashboard: New Refresh picker (#16505)
* Added RefreshButton

* Added RefreshSelect

* Added RefreshSelectButton

* Added RefreshPicker

* Removed the magic string Paused

* Minor style changes and using Off instead of Pause

* Added HeadlessSelect

* Added HeadlessSelect story

* Added SelectButton

* Removed RefreshSelectButton

* Added TimePicker and moved ClickOutsideWrapper to ui/components

* Added TimePickerPopOver

* Added react-calendar

* Missed yarn lock file

* Added inputs to popover

* Added TimePicker and RefreshPicker to DashNav

* Moved TimePicker and RefreshPicker to app/core

* Added react-calendar to app and removed from ui/components

* Fixed PopOver onClick

* Moved everything back to ui components because of typings problems

* Exporing RefreshPicker and TimePicker

* Added Apply and inputs

* Added typings

* Added TimePickerInput and logic

* Fixed parsing of string to Moments

* Fixed range string

* Styling and connecting the calendars and inputs

* Changed Calendar styling

* Added backward forward and zoom

* Fixed responsive styles

* Moved TimePicker and RefreshPicker into app core

* Renamed menuIsOpen to isOpen

* Changed from className={} to className=""

* Moved Popover to TimePickerOptionGroup

* Renamed all PopOver to Popover

* Renamed popOver to popover and some minor refactorings

* Renamed files with git mv

* Added ButtonSelect and refactored RefreshPicker

* Refactored TimePicker to use new ButtonSelect

* Removed HeadlessSelect as suggested

* fix: Fix typings and misc errors after rebase

* wip: Enable time picker on dashboard and add tooltip

* Merge branch 'master' into hugoh/new-timepicker-and-unified-component

# Conflicts:
#	packages/grafana-ui/package.json
#	packages/grafana-ui/src/components/Input/Input.test.tsx
#	packages/grafana-ui/src/components/Input/Input.tsx
#	packages/grafana-ui/src/utils/validate.ts
#	public/app/features/dashboard/panel_editor/QueryOptions.tsx
#	yarn.lock

* fix: Snapshot update

* Move TimePicker default options into the TimePicker as statics, pass the tooltipContent down the line when wanted and wrap the button in a tooltip element

* fix: Override internal state prop if we provide one in a prop

* Updated snapshots

* Let dashnav control refreshPicker state

* feat: Add a stringToMs function

* wip: RefreshPicker

* wip: Move RefreshPicker to @grafana/ui

* wip: Move TimePicker to @grafana/ui

* wip: Remove comments

* wip: Add refreshPicker to explore

* wip: Use default intervals if the prop is missing

* wip: Nicer way of setting defaults

* fix: Control the select component

* wip: Add onMoveForward/onMoveBack

* Remove code related to the new time picker and refresh picker from dashnav

* Fix: Typings after merge

* chore: Minor fix after merge

* chore: Remove _.map usage

* chore: Moved refresh-picker logic out of the refresh picker since it will work a little differently in explore and dashboards until we have replaced the TimeSrv

* feat: Add an Interval component to @grafana/ui

* chore: Remove intervalId from redux state and move setInterval logic from ExploreToolbar to its own Interval component

* feat: Add refreshInterval to Explore's URL state

* feat: Pick up refreshInterval from url on page load

* fix: Set default refreshInterval when no value can be retained from URL

* fix: Update test initial state with refreshInterval

* fix: Handle URLs before RefreshPicker

* fix: Move RefreshInterval to url position 3 since the segments can take multiple positions

* fix: A better way of detecting urls without RefreshInterval in Explore

* chore: Some Explore typings

* fix: Attach refresh picker to interval picker

* chore: Sass fix for refresh button border radius

* fix: Remove refreshInterval from URL

* fix: Intervals now start when previous interval is finished

* fix: Use clearTimeout instead of clearInterval

* fix: Make sure there's a delay set before adding a timeout when we have slow explore queries

* wip: Add refresh picker to dashboard

* feat: Add util for removing keys with empty values

* feat: RefreshPicker in dashboards and tmp rem out old RefreshPicker

* fix: Remove the jumpy:ness in the refreshpicker

* Changed placement and made it hide when your in dashboard settings

* chore: Move logic related to refresh picker out of DashNav to its own component

* feat: Add tooltip to refreshpicker

* fix: Fix bug with refreshpicker not updating when setting to 'off'

* fix: Make it possible to override refresh intervals using the dashboard intervals

* chore: Change name of Interval to SetInterval to align with ecmascripts naming since its basically the same but declarative and async

* fix: Use default intervals when auto refresh is empty in dashboard settings

* fix: Hide time/interval picker when hidden is true on the model, such as on the home dashboard

* fix: Interval picker will have to handle location changes since timeSrv wont

* RefreshPicker: Refactoring refresh picker

* RefreshPicker: minor refactoring
2019-04-16 09:15:23 +02:00

864 lines
26 KiB
TypeScript

// Libraries
import _ from 'lodash';
// Services & Utils
import store from 'app/core/store';
import { getDatasourceSrv } from 'app/features/plugins/datasource_srv';
import { Emitter } from 'app/core/core';
import {
LAST_USED_DATASOURCE_KEY,
clearQueryKeys,
ensureQueries,
generateEmptyQuery,
hasNonEmptyQuery,
makeTimeSeriesList,
updateHistory,
buildQueryTransaction,
serializeStateToUrlParam,
parseUrlState,
} from 'app/core/utils/explore';
// Actions
import { updateLocation } from 'app/core/actions';
// Types
import { ResultGetter } from 'app/types/explore';
import { ThunkResult } from 'app/types';
import {
RawTimeRange,
TimeRange,
DataSourceApi,
DataQuery,
DataSourceSelectItem,
QueryHint,
QueryFixAction,
} from '@grafana/ui/src/types';
import {
ExploreId,
ExploreUrlState,
RangeScanner,
ResultType,
QueryOptions,
ExploreUIState,
QueryTransaction,
} from 'app/types/explore';
import {
updateDatasourceInstanceAction,
changeQueryAction,
changeRefreshIntervalAction,
ChangeRefreshIntervalPayload,
changeSizeAction,
ChangeSizePayload,
changeTimeAction,
scanStopAction,
clearQueriesAction,
initializeExploreAction,
loadDatasourceMissingAction,
loadDatasourcePendingAction,
queriesImportedAction,
LoadDatasourceReadyPayload,
loadDatasourceReadyAction,
modifyQueriesAction,
queryTransactionFailureAction,
queryTransactionStartAction,
queryTransactionSuccessAction,
scanRangeAction,
scanStartAction,
setQueriesAction,
splitCloseAction,
splitOpenAction,
addQueryRowAction,
toggleGraphAction,
toggleLogsAction,
toggleTableAction,
ToggleGraphPayload,
ToggleLogsPayload,
ToggleTablePayload,
updateUIStateAction,
runQueriesAction,
testDataSourcePendingAction,
testDataSourceSuccessAction,
testDataSourceFailureAction,
loadExploreDatasources,
} from './actionTypes';
import { ActionOf, ActionCreator } from 'app/core/redux/actionCreatorFactory';
import { LogsDedupStrategy } from 'app/core/logs_model';
import { parseTime } from '../TimePicker';
/**
* Updates UI state and save it to the URL
*/
const updateExploreUIState = (exploreId: ExploreId, uiStateFragment: Partial<ExploreUIState>): ThunkResult<void> => {
return dispatch => {
dispatch(updateUIStateAction({ exploreId, ...uiStateFragment }));
dispatch(stateSave());
};
};
/**
* Adds a query row after the row with the given index.
*/
export function addQueryRow(exploreId: ExploreId, index: number): ThunkResult<void> {
return (dispatch, getState) => {
const query = generateEmptyQuery(getState().explore[exploreId].queries, index);
dispatch(addQueryRowAction({ exploreId, index, query }));
};
}
/**
* Loads a new datasource identified by the given name.
*/
export function changeDatasource(exploreId: ExploreId, datasource: string): ThunkResult<void> {
return async (dispatch, getState) => {
let newDataSourceInstance: DataSourceApi = null;
if (!datasource) {
newDataSourceInstance = await getDatasourceSrv().get();
} else {
newDataSourceInstance = await getDatasourceSrv().get(datasource);
}
const currentDataSourceInstance = getState().explore[exploreId].datasourceInstance;
const queries = getState().explore[exploreId].queries;
await dispatch(importQueries(exploreId, queries, currentDataSourceInstance, newDataSourceInstance));
dispatch(updateDatasourceInstanceAction({ exploreId, datasourceInstance: newDataSourceInstance }));
await dispatch(loadDatasource(exploreId, newDataSourceInstance));
dispatch(runQueries(exploreId));
};
}
/**
* 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,
index: number,
override: boolean
): ThunkResult<void> {
return (dispatch, getState) => {
// Null query means reset
if (query === null) {
query = { ...generateEmptyQuery(getState().explore[exploreId].queries) };
}
dispatch(changeQueryAction({ exploreId, query, index, override }));
if (override) {
dispatch(runQueries(exploreId));
}
};
}
/**
* 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 }
): ActionOf<ChangeSizePayload> {
return changeSizeAction({ exploreId, height, width });
}
/**
* Change the time range of Explore. Usually called from the Time picker or a graph interaction.
*/
export function changeTime(exploreId: ExploreId, range: TimeRange): ThunkResult<void> {
return dispatch => {
dispatch(changeTimeAction({ exploreId, range }));
dispatch(runQueries(exploreId));
};
}
/**
* Change the refresh interval of Explore. Called from the Refresh picker.
*/
export function changeRefreshInterval(
exploreId: ExploreId,
refreshInterval: string
): ActionOf<ChangeRefreshIntervalPayload> {
return changeRefreshIntervalAction({ exploreId, refreshInterval });
}
/**
* Clear all queries and results.
*/
export function clearQueries(exploreId: ExploreId): ThunkResult<void> {
return dispatch => {
dispatch(scanStopAction({ exploreId }));
dispatch(clearQueriesAction({ exploreId }));
dispatch(stateSave());
};
}
/**
* Loads all explore data sources and sets the chosen datasource.
* If there are no datasources a missing datasource action is dispatched.
*/
export function loadExploreDatasourcesAndSetDatasource(
exploreId: ExploreId,
datasourceName: string
): ThunkResult<void> {
return dispatch => {
const exploreDatasources: DataSourceSelectItem[] = getDatasourceSrv()
.getExternal()
.map(
(ds: any) =>
({
value: ds.name,
name: ds.name,
meta: ds.meta,
} as DataSourceSelectItem)
);
dispatch(loadExploreDatasources({ exploreId, exploreDatasources }));
if (exploreDatasources.length >= 1) {
dispatch(changeDatasource(exploreId, datasourceName));
} else {
dispatch(loadDatasourceMissingAction({ exploreId }));
}
};
}
/**
* 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,
datasourceName: string,
queries: DataQuery[],
range: RawTimeRange,
containerWidth: number,
eventBridge: Emitter,
ui: ExploreUIState
): ThunkResult<void> {
return async dispatch => {
dispatch(loadExploreDatasourcesAndSetDatasource(exploreId, datasourceName));
dispatch(
initializeExploreAction({
exploreId,
containerWidth,
eventBridge,
queries,
range,
ui,
})
);
};
}
/**
* Datasource loading was successfully completed.
*/
export const loadDatasourceReady = (
exploreId: ExploreId,
instance: DataSourceApi
): ActionOf<LoadDatasourceReadyPayload> => {
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 loadDatasourceReadyAction({
exploreId,
history,
});
};
export function importQueries(
exploreId: ExploreId,
queries: DataQuery[],
sourceDataSource: DataSourceApi,
targetDataSource: DataSourceApi
): ThunkResult<void> {
return async dispatch => {
if (!sourceDataSource) {
// explore not initialized
dispatch(queriesImportedAction({ exploreId, queries }));
return;
}
let importedQueries = queries;
// Check if queries can be imported from previously selected datasource
if (sourceDataSource.meta.id === targetDataSource.meta.id) {
// Keep same queries if same type of datasource
importedQueries = [...queries];
} else if (targetDataSource.importQueries) {
// Datasource-specific importers
importedQueries = await targetDataSource.importQueries(queries, sourceDataSource.meta);
} else {
// Default is blank queries
importedQueries = ensureQueries();
}
const nextQueries = importedQueries.map((q, i) => ({
...q,
...generateEmptyQuery(queries),
}));
dispatch(queriesImportedAction({ exploreId, queries: nextQueries }));
};
}
/**
* Tests datasource.
*/
export const testDatasource = (exploreId: ExploreId, instance: DataSourceApi): ThunkResult<void> => {
return async dispatch => {
let datasourceError = null;
dispatch(testDataSourcePendingAction({ exploreId }));
try {
const testResult = await instance.testDatasource();
datasourceError = testResult.status === 'success' ? null : testResult.message;
} catch (error) {
datasourceError = (error && error.statusText) || 'Network error';
}
if (datasourceError) {
dispatch(testDataSourceFailureAction({ exploreId, error: datasourceError }));
return;
}
dispatch(testDataSourceSuccessAction({ exploreId }));
};
};
/**
* Reconnects datasource when there is a connection failure.
*/
export const reconnectDatasource = (exploreId: ExploreId): ThunkResult<void> => {
return async (dispatch, getState) => {
const instance = getState().explore[exploreId].datasourceInstance;
dispatch(changeDatasource(exploreId, instance.name));
};
};
/**
* Main action to asynchronously load a datasource. Dispatches lots of smaller actions for feedback.
*/
export function loadDatasource(exploreId: ExploreId, instance: DataSourceApi): ThunkResult<void> {
return async (dispatch, getState) => {
const datasourceName = instance.name;
// Keep ID to track selection
dispatch(loadDatasourcePendingAction({ exploreId, requestedDatasourceName: datasourceName }));
await dispatch(testDatasource(exploreId, instance));
if (datasourceName !== getState().explore[exploreId].requestedDatasourceName) {
// User already changed datasource again, discard results
return;
}
if (instance.init) {
instance.init();
}
if (datasourceName !== getState().explore[exploreId].requestedDatasourceName) {
// User already changed datasource again, discard results
return;
}
dispatch(loadDatasourceReady(exploreId, instance));
};
}
/**
* 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: QueryFixAction,
index: number,
modifier: any
): ThunkResult<void> {
return dispatch => {
dispatch(modifyQueriesAction({ exploreId, modification, index, modifier }));
if (!modification.preventSubmit) {
dispatch(runQueries(exploreId));
}
};
}
/**
* 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,
response: any,
datasourceId: string
): ThunkResult<void> {
return (dispatch, getState) => {
const { datasourceInstance, queryTransactions } = getState().explore[exploreId];
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;
}
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(queryTransactionFailureAction({ exploreId, queryTransactions: nextQueryTransactions }));
};
}
/**
* 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,
result: any,
latency: number,
queries: DataQuery[],
datasourceId: string
): ThunkResult<void> {
return (dispatch, getState) => {
const { datasourceInstance, history, queryTransactions, scanner, scanning } = getState().explore[exploreId];
// 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) {
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(
queryTransactionSuccessAction({
exploreId,
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(scanRangeAction({ exploreId, range }));
}
} else {
// We can stop scanning if we have a result
dispatch(scanStopAction({ exploreId }));
}
}
};
}
/**
* Main action to run queries and dispatches sub-actions based on which result viewers are active
*/
export function runQueries(exploreId: ExploreId, ignoreUIState = false): ThunkResult<Promise<any>> {
return (dispatch, getState) => {
const {
datasourceInstance,
queries,
showingLogs,
showingGraph,
showingTable,
supportsGraph,
supportsLogs,
supportsTable,
datasourceError,
containerWidth,
} = getState().explore[exploreId];
if (datasourceError) {
// let's not run any queries if data source is in a faulty state
return Promise.resolve();
}
if (!hasNonEmptyQuery(queries)) {
dispatch(clearQueriesAction({ exploreId }));
dispatch(stateSave()); // Remember to saves to state and update location
return Promise.resolve();
}
// Some datasource's query builders allow per-query interval limits,
// but we're using the datasource interval limit for now
const interval = datasourceInstance.interval;
dispatch(runQueriesAction({ exploreId }));
// Keep table queries first since they need to return quickly
const tableQueriesPromise =
(ignoreUIState || showingTable) && supportsTable
? dispatch(
runQueriesForType(
exploreId,
'Table',
{
interval,
format: 'table',
instant: true,
valueWithRefId: true,
},
(data: any[]) => data[0]
)
)
: undefined;
const typeQueriesPromise =
(ignoreUIState || showingGraph) && supportsGraph
? dispatch(
runQueriesForType(
exploreId,
'Graph',
{
interval,
format: 'time_series',
instant: false,
maxDataPoints: containerWidth,
},
makeTimeSeriesList
)
)
: undefined;
const logsQueriesPromise =
(ignoreUIState || showingLogs) && supportsLogs
? dispatch(runQueriesForType(exploreId, 'Logs', { interval, format: 'logs' }))
: undefined;
dispatch(stateSave());
return Promise.all([tableQueriesPromise, typeQueriesPromise, logsQueriesPromise]);
};
}
/**
* 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,
queryOptions: QueryOptions,
resultGetter?: ResultGetter
): ThunkResult<void> {
return async (dispatch, getState) => {
const { datasourceInstance, eventBridge, queries, queryIntervals, range, scanning } = getState().explore[exploreId];
const datasourceId = datasourceInstance.meta.id;
// Run all queries concurrently
const queryPromises = queries.map(async (query, rowIndex) => {
const transaction = buildQueryTransaction(
query,
rowIndex,
resultType,
queryOptions,
range,
queryIntervals,
scanning
);
dispatch(queryTransactionStartAction({ exploreId, resultType, rowIndex, transaction }));
try {
const now = Date.now();
const res = await datasourceInstance.query(transaction.options);
eventBridge.emit('data-received', res.data || []);
const latency = Date.now() - now;
const { queryTransactions } = getState().explore[exploreId];
const results = resultGetter ? resultGetter(res.data, transaction, queryTransactions) : res.data;
dispatch(queryTransactionSuccess(exploreId, transaction.id, results, latency, queries, datasourceId));
} catch (response) {
eventBridge.emit('data-error', response);
dispatch(queryTransactionFailure(exploreId, transaction.id, response, datasourceId));
}
});
return Promise.all(queryPromises);
};
}
/**
* 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<void> {
return dispatch => {
// Register the scanner
dispatch(scanStartAction({ exploreId, scanner }));
// Scanning must trigger query run, and return the new range
const range = scanner();
// Set the new range to be displayed
dispatch(scanRangeAction({ exploreId, range }));
};
}
/**
* Reset queries to the given queries. Any modifications will be discarded.
* Use this action for clicks on query examples. Triggers a query run.
*/
export function setQueries(exploreId: ExploreId, rawQueries: DataQuery[]): ThunkResult<void> {
return (dispatch, getState) => {
// Inject react keys into query objects
const queries = rawQueries.map(q => ({ ...q, ...generateEmptyQuery(getState().explore[exploreId].queries) }));
dispatch(setQueriesAction({ exploreId, queries }));
dispatch(runQueries(exploreId));
};
}
/**
* Close the split view and save URL state.
*/
export function splitClose(itemId: ExploreId): ThunkResult<void> {
return dispatch => {
dispatch(splitCloseAction({ itemId }));
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<void> {
return (dispatch, getState) => {
// Clone left state to become the right state
const leftState = getState().explore[ExploreId.left];
const queryState = getState().location.query[ExploreId.left] as string;
const urlState = parseUrlState(queryState);
const queryTransactions: QueryTransaction[] = [];
const itemState = {
...leftState,
queryTransactions,
queries: leftState.queries.slice(),
exploreId: ExploreId.right,
urlState,
};
dispatch(splitOpenAction({ 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(): ThunkResult<void> {
return (dispatch, getState) => {
const { left, right, split } = getState().explore;
const urlStates: { [index: string]: string } = {};
const leftUrlState: ExploreUrlState = {
datasource: left.datasourceInstance.name,
queries: left.queries.map(clearQueryKeys),
range: left.range,
ui: {
showingGraph: left.showingGraph,
showingLogs: left.showingLogs,
showingTable: left.showingTable,
dedupStrategy: left.dedupStrategy,
},
};
urlStates.left = serializeStateToUrlParam(leftUrlState, true);
if (split) {
const rightUrlState: ExploreUrlState = {
datasource: right.datasourceInstance.name,
queries: right.queries.map(clearQueryKeys),
range: right.range,
ui: {
showingGraph: right.showingGraph,
showingLogs: right.showingLogs,
showingTable: right.showingTable,
dedupStrategy: right.dedupStrategy,
},
};
urlStates.right = serializeStateToUrlParam(rightUrlState, true);
}
dispatch(updateLocation({ query: urlStates }));
};
}
/**
* Creates action to collapse graph/logs/table panel. When panel is collapsed,
* queries won't be run
*/
const togglePanelActionCreator = (
actionCreator:
| ActionCreator<ToggleGraphPayload>
| ActionCreator<ToggleLogsPayload>
| ActionCreator<ToggleTablePayload>
) => (exploreId: ExploreId, isPanelVisible: boolean): ThunkResult<void> => {
return dispatch => {
let uiFragmentStateUpdate: Partial<ExploreUIState>;
const shouldRunQueries = !isPanelVisible;
switch (actionCreator.type) {
case toggleGraphAction.type:
uiFragmentStateUpdate = { showingGraph: !isPanelVisible };
break;
case toggleLogsAction.type:
uiFragmentStateUpdate = { showingLogs: !isPanelVisible };
break;
case toggleTableAction.type:
uiFragmentStateUpdate = { showingTable: !isPanelVisible };
break;
}
dispatch(actionCreator({ exploreId }));
dispatch(updateExploreUIState(exploreId, uiFragmentStateUpdate));
if (shouldRunQueries) {
dispatch(runQueries(exploreId));
}
};
};
/**
* Expand/collapse the graph result viewer. When collapsed, graph queries won't be run.
*/
export const toggleGraph = togglePanelActionCreator(toggleGraphAction);
/**
* Expand/collapse the logs result viewer. When collapsed, log queries won't be run.
*/
export const toggleLogs = togglePanelActionCreator(toggleLogsAction);
/**
* Expand/collapse the table result viewer. When collapsed, table queries won't be run.
*/
export const toggleTable = togglePanelActionCreator(toggleTableAction);
/**
* Change logs deduplication strategy and update URL.
*/
export const changeDedupStrategy = (exploreId: ExploreId, dedupStrategy: LogsDedupStrategy): ThunkResult<void> => {
return dispatch => {
dispatch(updateExploreUIState(exploreId, { dedupStrategy }));
};
};
export function refreshExplore(exploreId: ExploreId): ThunkResult<void> {
return (dispatch, getState) => {
const itemState = getState().explore[exploreId];
if (!itemState.initialized) {
return;
}
const { urlState, update, containerWidth, eventBridge } = itemState;
const { datasource, queries, range, ui } = urlState;
const refreshQueries = queries.map(q => ({ ...q, ...generateEmptyQuery(itemState.queries) }));
const refreshRange = { from: parseTime(range.from), to: parseTime(range.to) };
// need to refresh datasource
if (update.datasource) {
const initialQueries = ensureQueries(queries);
const initialRange = { from: parseTime(range.from), to: parseTime(range.to) };
dispatch(initializeExplore(exploreId, datasource, initialQueries, initialRange, containerWidth, eventBridge, ui));
return;
}
if (update.range) {
dispatch(changeTimeAction({ exploreId, range: refreshRange as TimeRange }));
}
// need to refresh ui state
if (update.ui) {
dispatch(updateUIStateAction({ ...ui, exploreId }));
}
// need to refresh queries
if (update.queries) {
dispatch(setQueriesAction({ exploreId, queries: refreshQueries }));
}
// always run queries when refresh is needed
if (update.queries || update.ui || update.range) {
dispatch(runQueries(exploreId));
}
};
}