diff --git a/CHANGELOG.md b/CHANGELOG.md index 54f82cf519b..b6bac3afa80 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,7 @@ 1. [18623](https://github.com/influxdata/influxdb/pull/18623): Drop support for --local flag within influx CLI 1. [18632](https://github.com/influxdata/influxdb/pull/18632): Prevents undefined queries in cells from erroring out in dashboards 1. [18658](https://github.com/influxdata/influxdb/pull/18658): Add support for 'd' day time identifier in the CLI for bucket and setup commands +1. [18581](https://github.com/influxdata/influxdb/pull/18581): Cache dashboard cell query results to use as a reference for cell configurations ## v2.0.0-beta.12 [2020-06-12] diff --git a/ui/src/dashboards/components/DashboardPage.tsx b/ui/src/dashboards/components/DashboardPage.tsx index e9ead55f7f3..103a2f3a0e7 100644 --- a/ui/src/dashboards/components/DashboardPage.tsx +++ b/ui/src/dashboards/components/DashboardPage.tsx @@ -16,13 +16,18 @@ import RateLimitAlert from 'src/cloud/components/RateLimitAlert' // Utils import {pageTitleSuffixer} from 'src/shared/utils/pageTitles' -// Selectors +// Selectors & Actions +import {resetCachedQueryResults} from 'src/queryCache/actions' import {getByID} from 'src/resources/selectors' // Types import {AppState, AutoRefresh, ResourceType, Dashboard} from 'src/types' import {ManualRefreshProps} from 'src/shared/components/ManualRefresh' +interface DispatchProps { + resetCachedQueryResults: typeof resetCachedQueryResults +} + interface StateProps { dashboard: Dashboard } @@ -31,10 +36,14 @@ interface OwnProps { autoRefresh: AutoRefresh } -type Props = OwnProps & StateProps & ManualRefreshProps +type Props = OwnProps & StateProps & ManualRefreshProps & DispatchProps @ErrorHandling class DashboardPage extends Component { + public componentWillUnmount() { + this.props.resetCachedQueryResults() + } + public render() { const {autoRefresh, manualRefresh, onManualRefresh, children} = this.props @@ -76,7 +85,11 @@ const mstp = (state: AppState): StateProps => { } } -export default connect( +const mdtp = { + resetCachedQueryResults: resetCachedQueryResults, +} + +export default connect( mstp, - null + mdtp )(ManualRefresh(DashboardPage)) diff --git a/ui/src/dashboards/components/EditVEO.tsx b/ui/src/dashboards/components/EditVEO.tsx index 21f07dfcd88..c39c366b42d 100644 --- a/ui/src/dashboards/components/EditVEO.tsx +++ b/ui/src/dashboards/components/EditVEO.tsx @@ -12,7 +12,7 @@ import VEOHeader from 'src/dashboards/components/VEOHeader' // Actions import {setName} from 'src/timeMachine/actions' import {saveVEOView} from 'src/dashboards/actions/thunks' -import {getViewForTimeMachine} from 'src/views/actions/thunks' +import {getViewAndResultsForVEO} from 'src/views/actions/thunks' // Utils import {getActiveTimeMachine} from 'src/timeMachine/selectors' @@ -21,21 +21,21 @@ import {getActiveTimeMachine} from 'src/timeMachine/selectors' import {AppState, RemoteDataState, QueryView, TimeMachineID} from 'src/types' interface DispatchProps { + getViewAndResultsForVEO: typeof getViewAndResultsForVEO onSetName: typeof setName onSaveView: typeof saveVEOView - getViewForTimeMachine: typeof getViewForTimeMachine } interface StateProps { - view: QueryView | null activeTimeMachineID: TimeMachineID + view: QueryView | null } type Props = DispatchProps & StateProps & WithRouterProps const EditViewVEO: FunctionComponent = ({ - getViewForTimeMachine, activeTimeMachineID, + getViewAndResultsForVEO, onSaveView, onSetName, params: {orgID, cellID, dashboardID}, @@ -46,7 +46,7 @@ const EditViewVEO: FunctionComponent = ({ // TODO split this up into "loadView" "setActiveTimeMachine" // and something to tell the component to pull from the context // of the dashboardID - getViewForTimeMachine(dashboardID, cellID, 'veo') + getViewAndResultsForVEO(dashboardID, cellID, 'veo') }, []) const handleClose = () => { @@ -92,16 +92,15 @@ const EditViewVEO: FunctionComponent = ({ const mstp = (state: AppState): StateProps => { const {activeTimeMachineID} = state.timeMachines - const {view} = getActiveTimeMachine(state) return {view, activeTimeMachineID} } const mdtp: DispatchProps = { + getViewAndResultsForVEO: getViewAndResultsForVEO, onSetName: setName, onSaveView: saveVEOView, - getViewForTimeMachine: getViewForTimeMachine, } export default connect( diff --git a/ui/src/queryCache/actions/index.ts b/ui/src/queryCache/actions/index.ts new file mode 100644 index 00000000000..b7ee06e815f --- /dev/null +++ b/ui/src/queryCache/actions/index.ts @@ -0,0 +1,33 @@ +export type Action = + | ReturnType + | ReturnType + +// Hashing function found here: +// https://jsperf.com/hashcodelordvlad +// Through this thread: +// https://stackoverflow.com/questions/7616461/generate-a-hash-from-string-in-javascript +export const hashCode = (queryText: string): string => { + let hash = 0, + char + if (!queryText) { + return `${hash}` + } + for (let i = 0; i < queryText.length; i++) { + char = queryText.charCodeAt(i) + hash = (hash << 5) - hash + char + hash |= 0 // Convert to 32bit integer + } + return `${hash}` +} + +export const setQueryResultsByQueryID = (queryID: string, files: string[]) => + ({ + type: 'SET_QUERY_RESULTS_BY_QUERY', + queryID, + files, + } as const) + +export const resetCachedQueryResults = () => + ({ + type: 'RESET_CACHED_QUERY_RESULTS', + } as const) diff --git a/ui/src/queryCache/reducers/index.ts b/ui/src/queryCache/reducers/index.ts new file mode 100644 index 00000000000..ea9b78d3e4e --- /dev/null +++ b/ui/src/queryCache/reducers/index.ts @@ -0,0 +1,33 @@ +// Libraries +import {produce} from 'immer' + +// Actions +import {Action} from 'src/queryCache/actions' + +export interface QueryCacheState { + queryResultsByQueryID: {[queryID: string]: string[]} +} + +export const initialState: QueryCacheState = { + queryResultsByQueryID: {}, +} + +export const queryCacheReducer = ( + state: QueryCacheState = initialState, + action: Action +): QueryCacheState => { + switch (action.type) { + case 'SET_QUERY_RESULTS_BY_QUERY': { + return produce(state, draftState => { + const {queryID, files} = action + draftState.queryResultsByQueryID[queryID] = files + }) + } + + case 'RESET_CACHED_QUERY_RESULTS': { + return {queryResultsByQueryID: {}} + } + } + + return state +} diff --git a/ui/src/shared/components/TimeSeries.tsx b/ui/src/shared/components/TimeSeries.tsx index eb191c72bc2..aad5edba436 100644 --- a/ui/src/shared/components/TimeSeries.tsx +++ b/ui/src/shared/components/TimeSeries.tsx @@ -32,6 +32,7 @@ import { isDemoDataAvailabilityError, demoDataError, } from 'src/cloud/utils/demoDataErrors' +import {hashCode} from 'src/queryCache/actions' // Constants import { @@ -43,6 +44,7 @@ import {TIME_RANGE_START, TIME_RANGE_STOP} from 'src/variables/constants' // Actions import {notify as notifyAction} from 'src/shared/actions/notifications' +import {setQueryResultsByQueryID} from 'src/queryCache/actions' // Types import { @@ -87,6 +89,7 @@ interface OwnProps { interface DispatchProps { notify: typeof notifyAction + onSetQueryResultsByQueryID: typeof setQueryResultsByQueryID } type Props = StateProps & OwnProps & DispatchProps @@ -181,7 +184,13 @@ class TimeSeries extends Component { } private reload = async () => { - const {variables, notify, check, buckets} = this.props + const { + buckets, + check, + notify, + onSetQueryResultsByQueryID, + variables, + } = this.props const queries = this.props.queries.filter(({text}) => !!text.trim()) if (!queries.length) { @@ -270,6 +279,11 @@ class TimeSeries extends Component { } this.pendingReload = false + const queryText = queries.map(({text}) => text).join('') + const queryID = hashCode(queryText) + if (queryID && files.length) { + onSetQueryResultsByQueryID(queryID, files) + } this.setState({ giraffeResult, @@ -346,6 +360,7 @@ const mstp = (state: AppState, props: OwnProps): StateProps => { const mdtp: DispatchProps = { notify: notifyAction, + onSetQueryResultsByQueryID: setQueryResultsByQueryID, } export default connect( diff --git a/ui/src/store/configureStore.ts b/ui/src/store/configureStore.ts index 01e4dd55d70..a3f4ce3c0a4 100644 --- a/ui/src/store/configureStore.ts +++ b/ui/src/store/configureStore.ts @@ -55,6 +55,7 @@ import alertBuilderReducer from 'src/alerting/reducers/alertBuilder' // Types import {AppState, LocalStorage} from 'src/types' +import {queryCacheReducer} from 'src/queryCache/reducers' type ReducerState = Pick> @@ -81,6 +82,7 @@ export const rootReducer = combineReducers({ overlays: overlaysReducer, plugins: pluginsResourceReducer, predicates: predicatesReducer, + queryCache: queryCacheReducer, ranges: rangesReducer, resources: combineReducers({ buckets: bucketsReducer, diff --git a/ui/src/timeMachine/actions/queries.ts b/ui/src/timeMachine/actions/queries.ts index 08f193fc731..2d98369d6cc 100644 --- a/ui/src/timeMachine/actions/queries.ts +++ b/ui/src/timeMachine/actions/queries.ts @@ -67,7 +67,7 @@ interface SetQueryResults { } } -const setQueryResults = ( +export const setQueryResults = ( status: RemoteDataState, files?: string[], fetchDuration?: number, @@ -125,9 +125,6 @@ export const executeQueries = (abortController?: AbortController) => async ( const queries = activeTimeMachine.view.properties.queries.filter( ({text}) => !!text.trim() ) - const { - alertBuilder: {id: checkID}, - } = state if (!queries.length) { dispatch(setQueryResults(RemoteDataState.Done, [], null)) @@ -177,6 +174,10 @@ export const executeQueries = (abortController?: AbortController) => async ( ) let statuses = [[]] as StatusRow[][] + const { + alertBuilder: {id: checkID}, + } = state + if (checkID) { const extern = buildVarsOption(variableAssignments) pendingCheckStatuses = runStatusesQuery(getOrg(state).id, checkID, extern) diff --git a/ui/src/types/stores.ts b/ui/src/types/stores.ts index 2e225ed0338..5d2773b7447 100644 --- a/ui/src/types/stores.ts +++ b/ui/src/types/stores.ts @@ -1,6 +1,3 @@ -import {Links} from 'src/types/links' -import {Notification} from 'src/types' -import {TimeRange} from 'src/types/queries' import {TimeMachinesState} from 'src/timeMachine/reducers' import {AppState as AppPresentationState} from 'src/shared/reducers/app' import {RouterState} from 'react-router-redux' @@ -10,7 +7,14 @@ import {CurrentDashboardState} from 'src/shared/reducers/currentDashboard' import {NoteEditorState} from 'src/dashboards/reducers/notes' import {DataLoadingState} from 'src/dataLoaders/reducers' import {OnboardingState} from 'src/onboarding/reducers' -import {PredicatesState, VariableEditorState} from 'src/types' +import { + Links, + Notification, + PredicatesState, + ResourceState, + TimeRange, + VariableEditorState, +} from 'src/types' import { TelegrafEditorPluginState, PluginResourceState, @@ -26,8 +30,7 @@ import {AlertBuilderState} from 'src/alerting/reducers/alertBuilder' import {CurrentPage} from 'src/shared/reducers/currentPage' import {DemoDataState} from 'src/cloud/reducers/demodata' import {OrgSettingsState} from 'src/cloud/reducers/orgsettings' - -import {ResourceState} from 'src/types' +import {QueryCacheState} from 'src/queryCache/reducers' export interface AppState { alertBuilder: AlertBuilderState @@ -40,6 +43,7 @@ export interface AppState { } currentPage: CurrentPage currentDashboard: CurrentDashboardState + queryCache: QueryCacheState dataLoading: DataLoadingState links: Links me: MeState diff --git a/ui/src/views/actions/thunks.ts b/ui/src/views/actions/thunks.ts index e4a5f96fd55..44cffdd966d 100644 --- a/ui/src/views/actions/thunks.ts +++ b/ui/src/views/actions/thunks.ts @@ -1,6 +1,6 @@ // Libraries import {normalize} from 'normalizr' - +import {get} from 'lodash' // APIs import { getView as getViewAJAX, @@ -16,6 +16,8 @@ import {notify} from 'src/shared/actions/notifications' import {setActiveTimeMachine} from 'src/timeMachine/actions' import {executeQueries} from 'src/timeMachine/actions/queries' import {setView, Action} from 'src/views/actions/creators' +import {hashCode} from 'src/queryCache/actions' +import {setQueryResults} from 'src/timeMachine/actions/queries' // Selectors import {getViewsForDashboard} from 'src/views/selectors' @@ -96,7 +98,7 @@ export const updateViewAndVariables = ( } } -export const getViewForTimeMachine = ( +export const getViewAndResultsForVEO = ( dashboardID: string, cellID: string, timeMachineID: TimeMachineID @@ -116,8 +118,24 @@ export const getViewForTimeMachine = ( view, }) ) + const queries = view.properties.queries.filter(({text}) => !!text.trim()) + if (!queries.length) { + dispatch(setQueryResults(RemoteDataState.Done, [], null)) + } + const queryText = queries.map(({text}) => text).join('') + const queryID = hashCode(queryText) + const files = get( + state, + ['queryCache', 'queryResultsByQueryID', queryID], + undefined + ) + if (files) { + dispatch(setQueryResults(RemoteDataState.Done, files, null, null)) + return + } dispatch(executeQueries()) } catch (error) { + console.error(error) dispatch(notify(copy.getViewFailed(error.message))) dispatch(setView(cellID, RemoteDataState.Error)) }