diff --git a/ui/package-lock.json b/ui/package-lock.json index accc5474bc..742980e281 100644 --- a/ui/package-lock.json +++ b/ui/package-lock.json @@ -1234,6 +1234,12 @@ "integrity": "sha512-lRnAtKnxMXcYYXqOiotTmJd74uawNWuPnsnPrrO7HiFuE3npE2iQhfABatbYDyxTNqZNuXzcKGhw37R7RjBFLg==", "dev": true }, + "@types/memoize-one": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@types/memoize-one/-/memoize-one-4.1.0.tgz", + "integrity": "sha512-cmSgi6JMX/yBwgpVm4GooNWIH+vEeJoa8FAa6ExOhpJbC0Juq32/uYKiKb3VPSqrEA0aOnjvwZanla3O1WZMbw==", + "dev": true + }, "@types/minimatch": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.3.tgz", @@ -2538,7 +2544,7 @@ }, "bindings": { "version": "1.2.1", - "resolved": "http://registry.npmjs.org/bindings/-/bindings-1.2.1.tgz", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.2.1.tgz", "integrity": "sha1-FK1hE4EtLTfXLme0ystLtyZQXxE=", "dev": true }, @@ -2956,7 +2962,7 @@ }, "callsites": { "version": "2.0.0", - "resolved": "http://registry.npmjs.org/callsites/-/callsites-2.0.0.tgz", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-2.0.0.tgz", "integrity": "sha1-BuuE8A7qQT2oav/vrL/7Ngk7PFA=", "dev": true }, @@ -3750,7 +3756,7 @@ }, "css-color-names": { "version": "0.0.4", - "resolved": "http://registry.npmjs.org/css-color-names/-/css-color-names-0.0.4.tgz", + "resolved": "https://registry.npmjs.org/css-color-names/-/css-color-names-0.0.4.tgz", "integrity": "sha1-gIrcLnnPhHOAabZGyyDsJ762KeA=", "dev": true }, @@ -4632,7 +4638,7 @@ }, "dotenv": { "version": "5.0.1", - "resolved": "http://registry.npmjs.org/dotenv/-/dotenv-5.0.1.tgz", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-5.0.1.tgz", "integrity": "sha512-4As8uPrjfwb7VXC+WnLCbXK7y+Ueb2B3zgNCePYfhxS1PYeaO1YTeplffTEcbfLhvFNGLAz90VvJs9yomG7bow==", "dev": true }, @@ -6100,8 +6106,7 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", - "dev": true, - "optional": true + "dev": true }, "aproba": { "version": "1.2.0", @@ -6125,15 +6130,13 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", - "dev": true, - "optional": true + "dev": true }, "brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", "dev": true, - "optional": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -6150,22 +6153,19 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=", - "dev": true, - "optional": true + "dev": true }, "concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", - "dev": true, - "optional": true + "dev": true }, "console-control-strings": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=", - "dev": true, - "optional": true + "dev": true }, "core-util-is": { "version": "1.0.2", @@ -6296,8 +6296,7 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", - "dev": true, - "optional": true + "dev": true }, "ini": { "version": "1.3.5", @@ -6311,7 +6310,6 @@ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", "dev": true, - "optional": true, "requires": { "number-is-nan": "^1.0.0" } @@ -6328,7 +6326,6 @@ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", "dev": true, - "optional": true, "requires": { "brace-expansion": "^1.1.7" } @@ -6337,15 +6334,13 @@ "version": "0.0.8", "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", - "dev": true, - "optional": true + "dev": true }, "minipass": { "version": "2.2.4", "resolved": "https://registry.npmjs.org/minipass/-/minipass-2.2.4.tgz", "integrity": "sha512-hzXIWWet/BzWhYs2b+u7dRHlruXhwdgvlTMDKC6Cb1U7ps6Ac6yQlR39xsbjWJE377YTCtKwIXIpJ5oP+j5y8g==", "dev": true, - "optional": true, "requires": { "safe-buffer": "^5.1.1", "yallist": "^3.0.0" @@ -6366,7 +6361,6 @@ "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", "dev": true, - "optional": true, "requires": { "minimist": "0.0.8" } @@ -6455,8 +6449,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", - "dev": true, - "optional": true + "dev": true }, "object-assign": { "version": "4.1.1", @@ -6470,7 +6463,6 @@ "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", "dev": true, - "optional": true, "requires": { "wrappy": "1" } @@ -6566,8 +6558,7 @@ "version": "5.1.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz", "integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg==", - "dev": true, - "optional": true + "dev": true }, "safer-buffer": { "version": "2.1.2", @@ -6609,7 +6600,6 @@ "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", "dev": true, - "optional": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -6631,7 +6621,6 @@ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", "dev": true, - "optional": true, "requires": { "ansi-regex": "^2.0.0" } @@ -6680,15 +6669,13 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", - "dev": true, - "optional": true + "dev": true }, "yallist": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.0.2.tgz", "integrity": "sha1-hFK0u36Dx8GI2AQcGoN8dz1ti7k=", - "dev": true, - "optional": true + "dev": true } } }, @@ -8290,7 +8277,7 @@ }, "is-obj": { "version": "1.0.1", - "resolved": "http://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz", + "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz", "integrity": "sha1-PkcprB9f3gJc19g6iW2rn09n2w8=", "dev": true }, @@ -10089,7 +10076,7 @@ }, "magic-string": { "version": "0.22.5", - "resolved": "http://registry.npmjs.org/magic-string/-/magic-string-0.22.5.tgz", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.22.5.tgz", "integrity": "sha512-oreip9rJZkzvA8Qzk9HFs8fZGF/u7H/gtrE8EN6RjKJ9kh2HlC+yQ2QezifqTZfGyiuAV0dRv5a+y/8gBb1m9w==", "dev": true, "requires": { @@ -11471,7 +11458,7 @@ }, "json5": { "version": "1.0.1", - "resolved": "http://registry.npmjs.org/json5/-/json5-1.0.1.tgz", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz", "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", "dev": true, "requires": { diff --git a/ui/package.json b/ui/package.json index c70fa99f94..317bef5a9e 100644 --- a/ui/package.json +++ b/ui/package.json @@ -88,6 +88,7 @@ "@types/jest": "^23.3.2", "@types/levelup": "^3.1.0", "@types/lodash": "^4.14.116", + "@types/memoize-one": "^4.0.2", "@types/node": "^9.4.6", "@types/papaparse": "^4.1.34", "@types/prop-types": "^15.5.2", diff --git a/ui/src/dashboards/actions/v2/index.ts b/ui/src/dashboards/actions/v2/index.ts index 35babc7b1c..ed759feaf0 100644 --- a/ui/src/dashboards/actions/v2/index.ts +++ b/ui/src/dashboards/actions/v2/index.ts @@ -14,6 +14,7 @@ import { deleteCell as deleteCellAJAX, addDashboardLabels as addDashboardLabelsAJAX, removeDashboardLabels as removeDashboardLabelsAJAX, + getView as getViewAJAX, updateView as updateViewAJAX, } from 'src/dashboards/apis/v2' import {client} from 'src/utils/api' @@ -25,13 +26,16 @@ import { updateTimeRangeFromQueryParams, DeleteTimeRangeAction, } from 'src/dashboards/actions/v2/ranges' -import {setView, SetViewAction} from 'src/dashboards/actions/v2/views' import { importDashboardSucceeded, importDashboardFailed, } from 'src/shared/copy/notifications' +import {setView, SetViewAction, setViews} from 'src/dashboards/actions/v2/views' +import {getVariables, refreshVariableValues} from 'src/variables/actions' // Utils +import {filterUnusedVars} from 'src/shared/utils/filterUnusedVars' +import {getVariablesForOrg} from 'src/variables/selectors' import { getNewDashboardCell, getClonedDashboardCell, @@ -44,7 +48,7 @@ import * as copy from 'src/shared/copy/notifications' import {RemoteDataState} from 'src/types' import {PublishNotificationAction} from 'src/types/actions/notifications' import {CreateCell, IDashboardTemplate} from '@influxdata/influx' -import {Dashboard, NewView, Cell} from 'src/types/v2' +import {Dashboard, NewView, Cell, GetState} from 'src/types/v2' import {ILabel} from '@influxdata/influx' export enum ActionTypes { @@ -253,14 +257,37 @@ export const deleteDashboardAsync = (dashboard: Dashboard) => async ( } export const getDashboardAsync = (dashboardID: string) => async ( - dispatch + dispatch, + getState: GetState ): Promise => { try { - const dashboard = await getDashboardAJAX(dashboardID) + // Fetch the dashboard and all variables a user has access to + const [dashboard] = await Promise.all([ + getDashboardAJAX(dashboardID), + dispatch(getVariables()), + ]) + + // Fetch all the views in use on the dashboard + const views = await Promise.all( + dashboard.cells.map(cell => getViewAJAX(dashboard.id, cell.id)) + ) + + dispatch(setViews(RemoteDataState.Done, views)) + + // Find variables in use on dashboard + const variables = getVariablesForOrg(getState(), dashboard.orgID) + const variablesInUse = filterUnusedVars(variables, views) + + // Ensure the values for those variables are loaded + await dispatch( + refreshVariableValues(dashboard.id, dashboard.orgID, variablesInUse) + ) + + // Now that all the necessary state has been loaded, set the dashboard dispatch(loadDashboard(dashboard)) } catch { dispatch(replace(`/dashboards`)) - dispatch(notify(copy.dashboardNotFound(dashboardID))) + dispatch(notify(copy.dashboardGetFailed(dashboardID))) return } diff --git a/ui/src/dashboards/actions/v2/views.ts b/ui/src/dashboards/actions/v2/views.ts index c9070194bb..c2dae7aee4 100644 --- a/ui/src/dashboards/actions/v2/views.ts +++ b/ui/src/dashboards/actions/v2/views.ts @@ -9,7 +9,23 @@ import {RemoteDataState} from 'src/types' import {Dispatch} from 'redux' import {View} from 'src/types/v2' -export type Action = SetViewAction +export type Action = SetViewAction | SetViewsAction + +export interface SetViewsAction { + type: 'SET_VIEWS' + payload: { + views?: View[] + status: RemoteDataState + } +} + +export const setViews = ( + status: RemoteDataState, + views: View[] +): SetViewsAction => ({ + type: 'SET_VIEWS', + payload: {views, status}, +}) export interface SetViewAction { type: 'SET_VIEW' diff --git a/ui/src/dashboards/components/DashboardPage.tsx b/ui/src/dashboards/components/DashboardPage.tsx index 3f971898e6..27dbb82025 100644 --- a/ui/src/dashboards/components/DashboardPage.tsx +++ b/ui/src/dashboards/components/DashboardPage.tsx @@ -340,7 +340,7 @@ const mstp = (state: AppState, {params: {dashboardID}}): StateProps => { }, ranges, dashboards, - views, + views: {views}, } = state const timeRange = diff --git a/ui/src/dashboards/reducers/v2/views.ts b/ui/src/dashboards/reducers/v2/views.ts index 5433c3ed40..d3914b1cd4 100644 --- a/ui/src/dashboards/reducers/v2/views.ts +++ b/ui/src/dashboards/reducers/v2/views.ts @@ -4,20 +4,61 @@ import {RemoteDataState} from 'src/types' import {View} from 'src/types/v2' export interface ViewsState { - [viewID: string]: { - status: RemoteDataState - view: View + status: RemoteDataState + views: { + [viewID: string]: { + status: RemoteDataState + view: View + } } } -const viewsReducer = (state: ViewsState = {}, action: Action) => { +const initialState = () => ({ + status: RemoteDataState.NotStarted, + views: {}, +}) + +const viewsReducer = ( + state: ViewsState = initialState(), + action: Action +): ViewsState => { switch (action.type) { + case 'SET_VIEWS': { + const {status} = action.payload + + if (!action.payload.views) { + return { + ...state, + status, + } + } + + const views = action.payload.views.reduce( + (acc, view) => ({ + ...acc, + [view.id]: { + view, + status: RemoteDataState.Done, + }, + }), + {} + ) + + return { + status, + views, + } + } + case 'SET_VIEW': { const {id, view, status} = action.payload return { ...state, - [id]: {view, status}, + views: { + ...state.views, + [id]: {view, status}, + }, } } } diff --git a/ui/src/dashboards/selectors/index.ts b/ui/src/dashboards/selectors/index.ts new file mode 100644 index 0000000000..69061d309c --- /dev/null +++ b/ui/src/dashboards/selectors/index.ts @@ -0,0 +1,6 @@ +import {get} from 'lodash' + +import {AppState, View} from 'src/types/v2' + +export const getView = (state: AppState, id: string): View => + get(state, `views.views.${id}.view`) diff --git a/ui/src/shared/components/RefreshingView.tsx b/ui/src/shared/components/RefreshingView.tsx index f8ceb1a6fe..f95da72be8 100644 --- a/ui/src/shared/components/RefreshingView.tsx +++ b/ui/src/shared/components/RefreshingView.tsx @@ -1,5 +1,6 @@ // Libraries import React, {PureComponent} from 'react' +import {connect} from 'react-redux' import _ from 'lodash' // Components @@ -10,25 +11,35 @@ import QueryViewSwitcher from 'src/shared/components/QueryViewSwitcher' // Utils import {GlobalAutoRefresher} from 'src/utils/AutoRefresher' import {getTimeRangeVars} from 'src/variables/utils/getTimeRangeVars' +import {getVariableAssignments} from 'src/variables/selectors' // Types import {TimeRange} from 'src/types' +import {VariableAssignment} from 'src/types/ast' +import {AppState} from 'src/types/v2' import {DashboardQuery} from 'src/types/v2/dashboards' import {QueryViewProperties, ViewType} from 'src/types/v2/dashboards' -interface Props { +interface OwnProps { timeRange: TimeRange viewID: string inView: boolean manualRefresh: number onZoom: (range: TimeRange) => void properties: QueryViewProperties + dashboardID: string +} + +interface StateProps { + variableAssignments: VariableAssignment[] } interface State { submitToken: number } +type Props = OwnProps & StateProps + class RefreshingView extends PureComponent { public static defaultProps: Partial = { inView: true, @@ -66,7 +77,7 @@ class RefreshingView extends PureComponent { submitToken={submitToken} queries={this.queries} key={manualRefresh} - variables={getTimeRangeVars(timeRange)} + variables={this.variableAssignments} > {({tables, loading, error, isInitialFetch}) => { return ( @@ -108,6 +119,12 @@ class RefreshingView extends PureComponent { return queries } + private get variableAssignments(): VariableAssignment[] { + const {timeRange, variableAssignments} = this.props + + return [...variableAssignments, ...getTimeRangeVars(timeRange)] + } + private get fallbackNote(): string { const {note, showNoteWhenEmpty} = this.props.properties @@ -119,4 +136,13 @@ class RefreshingView extends PureComponent { } } -export default RefreshingView +const mstp = (state: AppState, ownProps: OwnProps): StateProps => { + const variableAssignments = getVariableAssignments( + state, + ownProps.dashboardID + ) + + return {variableAssignments} +} + +export default connect(mstp)(RefreshingView) diff --git a/ui/src/shared/components/cells/Cell.tsx b/ui/src/shared/components/cells/Cell.tsx index 0aacc4dfd6..d79c5b2b61 100644 --- a/ui/src/shared/components/cells/Cell.tsx +++ b/ui/src/shared/components/cells/Cell.tsx @@ -1,5 +1,5 @@ // Libraries -import React, {Component, ComponentClass} from 'react' +import React, {Component} from 'react' import {connect} from 'react-redux' import {get} from 'lodash' @@ -8,13 +8,12 @@ import CellHeader from 'src/shared/components/cells/CellHeader' import CellContext from 'src/shared/components/cells/CellContext' import ViewComponent from 'src/shared/components/cells/View' import {ErrorHandling} from 'src/shared/decorators/errors' -import Conditional from 'src/shared/components/Conditional' -// Actions -import {getView} from 'src/dashboards/actions/v2/views' +// Utils +import {getView} from 'src/dashboards/selectors' // Types -import {RemoteDataState, TimeRange} from 'src/types' +import {TimeRange} from 'src/types' import {AppState, ViewType, View, Cell} from 'src/types/v2' // Styles @@ -22,14 +21,9 @@ import './Cell.scss' interface StateProps { view: View - viewStatus: RemoteDataState } -interface DispatchProps { - onGetView: typeof getView -} - -interface PassedProps { +interface OwnProps { cell: Cell timeRange: TimeRange autoRefresh: number @@ -40,19 +34,10 @@ interface PassedProps { onZoom: (range: TimeRange) => void } -type Props = StateProps & DispatchProps & PassedProps +type Props = StateProps & OwnProps @ErrorHandling class CellComponent extends Component { - public async componentDidMount() { - const {viewStatus, cell, onGetView} = this.props - - if (viewStatus === RemoteDataState.NotStarted) { - const dashboardID = cell.dashboardID - onGetView(dashboardID, cell.id) - } - } - public render() { const {onEditCell, onDeleteCell, onCloneCell, cell, view} = this.props @@ -110,21 +95,18 @@ class CellComponent extends Component { manualRefresh, onZoom, view, - viewStatus, onEditCell, } = this.props return ( - - - + ) } @@ -133,21 +115,11 @@ class CellComponent extends Component { } } -const mstp = (state: AppState, ownProps: PassedProps): StateProps => { - const entry = state.views[ownProps.cell.id] +const mstp = (state: AppState, ownProps: OwnProps): StateProps => ({ + view: getView(state, ownProps.cell.id), +}) - if (entry) { - return {view: entry.view, viewStatus: entry.status} - } - - return {view: null, viewStatus: RemoteDataState.NotStarted} -} - -const mdtp: DispatchProps = { - onGetView: getView, -} - -export default connect( +export default connect( mstp, - mdtp -)(CellComponent) as ComponentClass + null +)(CellComponent) diff --git a/ui/src/shared/components/cells/View.tsx b/ui/src/shared/components/cells/View.tsx index b2f40fea7e..f482a0877d 100644 --- a/ui/src/shared/components/cells/View.tsx +++ b/ui/src/shared/components/cells/View.tsx @@ -1,5 +1,6 @@ // Libraries import React, {Component} from 'react' +import {withRouter, WithRouterProps} from 'react-router' // Components import Markdown from 'src/shared/components/views/Markdown' @@ -11,7 +12,7 @@ import {ViewType, ViewShape, View} from 'src/types/v2' import {ErrorHandling} from 'src/shared/decorators/errors' -interface Props { +interface OwnProps { view: View timeRange: TimeRange autoRefresh: number @@ -20,6 +21,8 @@ interface Props { onEditCell: () => void } +type Props = OwnProps & WithRouterProps + @ErrorHandling class ViewComponent extends Component { public state = { @@ -28,6 +31,7 @@ class ViewComponent extends Component { public render() { const {view, onZoom, timeRange, manualRefresh} = this.props + const {dashboardID} = this.props.params switch (view.properties.type) { case ViewShape.Empty: @@ -42,6 +46,8 @@ class ViewComponent extends Component { timeRange={timeRange} properties={view.properties} manualRefresh={manualRefresh} + inView={true} + dashboardID={dashboardID} /> ) } @@ -61,4 +67,4 @@ class ViewComponent extends Component { } } -export default ViewComponent +export default withRouter(ViewComponent) diff --git a/ui/src/shared/copy/notifications.ts b/ui/src/shared/copy/notifications.ts index 2305c06500..025e82360e 100644 --- a/ui/src/shared/copy/notifications.ts +++ b/ui/src/shared/copy/notifications.ts @@ -472,10 +472,10 @@ export const tempVarAlreadyExists = (tempVarName: string): Notification => ({ message: `Variable '${tempVarName}' already exists. Please enter a new value.`, }) -export const dashboardNotFound = (dashboardID: string): Notification => ({ +export const dashboardGetFailed = (dashboardID: string): Notification => ({ ...defaultErrorNotification, icon: 'dash-h', - message: `Dashboard ${dashboardID} could not be found`, + message: `Failed to load dashboard with id "${dashboardID}"`, }) export const dashboardUpdateFailed = (): Notification => ({ diff --git a/ui/src/shared/utils/filterUnusedVars.ts b/ui/src/shared/utils/filterUnusedVars.ts new file mode 100644 index 0000000000..cbd4fb7daf --- /dev/null +++ b/ui/src/shared/utils/filterUnusedVars.ts @@ -0,0 +1,30 @@ +// Constants +import {OPTION_NAME} from 'src/variables/constants' + +// Types +import {QueryView} from 'src/types/v2/dashboards' +import {Variable, View} from '@influxdata/influx' + +/* + Given a collection variables and a collection of views, return only the + variables that are used in at least one of the view queries. +*/ +export const filterUnusedVars = (variables: Variable[], views: View[]) => { + const queryViews: QueryView[] = views.filter( + view => !!view.properties.queries + ) + + const queryTexts = queryViews.reduce( + (acc, view) => [ + ...acc, + ...view.properties.queries.map(query => query.text), + ], + [] + ) + + const varsInUse = variables.filter(variable => + queryTexts.some(text => text.includes(`${OPTION_NAME}.${variable.name}`)) + ) + + return varsInUse +} diff --git a/ui/src/variables/actions/index.ts b/ui/src/variables/actions/index.ts index b197a2fe83..2d8c943180 100644 --- a/ui/src/variables/actions/index.ts +++ b/ui/src/variables/actions/index.ts @@ -185,6 +185,7 @@ export const refreshVariableValues = ( dispatch(setValues(contextID, RemoteDataState.Done, values)) } catch (e) { + console.error(e) dispatch(setValues(contextID, RemoteDataState.Error)) } } diff --git a/ui/src/variables/selectors/index.tsx b/ui/src/variables/selectors/index.tsx index d505dac8c5..3f26e5777a 100644 --- a/ui/src/variables/selectors/index.tsx +++ b/ui/src/variables/selectors/index.tsx @@ -2,12 +2,18 @@ import memoizeOne from 'memoize-one' import {get} from 'lodash' +// Utils +import {getVarAssignment} from 'src/variables/utils/getVarAssignment' + // Types import {RemoteDataState} from 'src/types' +import {VariableAssignment} from 'src/types/ast' import {AppState} from 'src/types/v2' import {VariableValues, ValueSelections} from 'src/variables/types' +import {Variable} from '@influxdata/influx' type VariablesState = AppState['variables']['variables'] +type ValuesState = AppState['variables']['values']['contextID'] const getVariablesForOrgMemoized = memoizeOne( (variablesState: VariablesState, orgID: string) => { @@ -19,7 +25,10 @@ const getVariablesForOrgMemoized = memoizeOne( } ) -export const getVariablesForOrg = (state: AppState, orgID: string) => { +export const getVariablesForOrg = ( + state: AppState, + orgID: string +): Variable[] => { return getVariablesForOrgMemoized(state.variables.variables, orgID) } @@ -27,11 +36,8 @@ export const getValueSelections = ( state: AppState, contextID: string ): ValueSelections => { - const contextValues: VariableValues = get( - state, - `variables.values.${contextID}.values`, - {} - ) + const contextValues: VariableValues = + get(state, `variables.values.${contextID}.values`) || {} const selections: ValueSelections = Object.keys(contextValues).reduce( (acc, k) => { @@ -48,3 +54,35 @@ export const getValueSelections = ( return selections } + +const getVariableAssignmentsMemoized = memoizeOne( + ( + valuesState: ValuesState, + variablesState: VariablesState + ): VariableAssignment[] => { + if (!valuesState) { + return [] + } + + const result = [] + + for (const [variableID, values] of Object.entries(valuesState.values)) { + const variableName = get(variablesState, [variableID, 'variable', 'name']) + + if (variableName) { + result.push(getVarAssignment(variableName, values)) + } + } + + return result + } +) + +export const getVariableAssignments = ( + state: AppState, + contextID: string +): VariableAssignment[] => + getVariableAssignmentsMemoized( + state.variables.values[contextID], + state.variables.variables + )