Skip to content

Commit

Permalink
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Basic top nav bar for dashboard (#4108)
Browse files Browse the repository at this point in the history
Basic top nav bar for dashboard

This PR will add basic structure to render top nav bar, including a basic implementation for dashboard app state. This is not functionality complete, but to help implement a basic working dashboard app as the first step.

Signed-off-by: abbyhu2000 <abigailhu2000@gmail.com>
abbyhu2000 committed Jul 5, 2023
1 parent c1644c6 commit 65d57f7
Showing 8 changed files with 709 additions and 6 deletions.
Original file line number Diff line number Diff line change
@@ -3,8 +3,212 @@
* SPDX-License-Identifier: Apache-2.0
*/

import React from 'react';
import React, { useEffect, useState } from 'react';
import { useParams } from 'react-router-dom';
import { EventEmitter } from 'events';
import { EMPTY, Subscription, merge } from 'rxjs';
import { catchError, distinctUntilChanged, map, mapTo, startWith, switchMap } from 'rxjs/operators';
import deepEqual from 'fast-deep-equal';
import { DashboardTopNav } from '../components/dashboard_top_nav';
import { useChromeVisibility } from '../utils/use/use_chrome_visibility';
import { useOpenSearchDashboards } from '../../../../opensearch_dashboards_react/public';
import { useSavedDashboardInstance } from '../utils/use/use_saved_dashboard_instance';

import { DashboardServices, SavedDashboardPanel } from '../../types';
import {
DASHBOARD_CONTAINER_TYPE,
DashboardContainer,
DashboardContainerInput,
DashboardPanelState,
} from '../embeddable';
import {
ContainerOutput,
ErrorEmbeddable,
ViewMode,
isErrorEmbeddable,
} from '../../embeddable_plugin';
import { DashboardEmptyScreen, DashboardEmptyScreenProps } from '../dashboard_empty_screen';
import { convertSavedDashboardPanelToPanelState } from '../lib/embeddable_saved_object_converters';
import { useDashboardAppState } from '../utils/use/use_dashboard_app_state';

export const DashboardEditor = () => {
return <div>Dashboard Editor</div>;
const { id: dashboardIdFromUrl } = useParams<{ id: string }>();
const { services } = useOpenSearchDashboards<DashboardServices>();
const { embeddable, data, dashboardConfig, embeddableCapabilities, uiSettings, http } = services;
const { query: queryService } = data;
const { visualizeCapabilities, mapsCapabilities } = embeddableCapabilities;
const timefilter = queryService.timefilter.timefilter;
const isChromeVisible = useChromeVisibility(services.chrome);
const [eventEmitter] = useState(new EventEmitter());

const { savedDashboardInstance } = useSavedDashboardInstance(
services,
eventEmitter,
isChromeVisible,
dashboardIdFromUrl
);

const { appState } = useDashboardAppState(services, eventEmitter, savedDashboardInstance);

const appStateData = appState?.get();
if (!appStateData) {
return null;
}
let dashboardContainer: DashboardContainer | undefined;
let inputSubscription: Subscription | undefined;
let outputSubscription: Subscription | undefined;

const dashboardDom = document.getElementById('dashboardViewport');
const dashboardFactory = embeddable.getEmbeddableFactory<
DashboardContainerInput,
ContainerOutput,
DashboardContainer
>(DASHBOARD_CONTAINER_TYPE);

const getShouldShowEditHelp = () => {
return (
!appStateData.panels.length &&
appStateData.viewMode === ViewMode.EDIT &&
!dashboardConfig.getHideWriteControls()
);
};

const getShouldShowViewHelp = () => {
return (
!appStateData.panels.length &&
appStateData.viewMode === ViewMode.VIEW &&
!dashboardConfig.getHideWriteControls()
);
};

const shouldShowUnauthorizedEmptyState = () => {
const readonlyMode =
!appStateData.panels.length &&
!getShouldShowEditHelp() &&
!getShouldShowViewHelp() &&
dashboardConfig.getHideWriteControls();
const userHasNoPermissions =
!appStateData.panels.length && !visualizeCapabilities.save && !mapsCapabilities.save;
return readonlyMode || userHasNoPermissions;
};

const getEmptyScreenProps = (
shouldShowEditHelp: boolean,
isEmptyInReadOnlyMode: boolean
): DashboardEmptyScreenProps => {
const emptyScreenProps: DashboardEmptyScreenProps = {
onLinkClick: () => {}, // TODO
showLinkToVisualize: shouldShowEditHelp,
uiSettings,
http,
};
if (shouldShowEditHelp) {
emptyScreenProps.onVisualizeClick = () => {
alert('click'); // TODO
};
}
if (isEmptyInReadOnlyMode) {
emptyScreenProps.isReadonlyMode = true;
}
return emptyScreenProps;
};

const getDashboardInput = () => {
const embeddablesMap: {
[key: string]: DashboardPanelState;
} = {};
appStateData.panels.forEach((panel: SavedDashboardPanel) => {
embeddablesMap[panel.panelIndex] = convertSavedDashboardPanelToPanelState(panel);
});

const lastReloadRequestTime = 0;
return {
id: savedDashboardInstance.id || '',
filters: appStateData.filters,
hidePanelTitles: appStateData?.options.hidePanelTitles,
query: appStateData.query,
timeRange: {
..._.cloneDeep(timefilter.getTime()),
},
refreshConfig: timefilter.getRefreshInterval(),
viewMode: appStateData.viewMode,
panels: embeddablesMap,
isFullScreenMode: appStateData?.fullScreenMode,
isEmbeddedExternally: false, // TODO
// isEmptyState: shouldShowEditHelp || shouldShowViewHelp || isEmptyInReadonlyMode,
isEmptyState: false, // TODO
useMargins: appStateData.options.useMargins,
lastReloadRequestTime, // TODO
title: appStateData.title,
description: appStateData.description,
expandedPanelId: appStateData.expandedPanelId,
};
};

if (dashboardFactory) {
dashboardFactory
.create(getDashboardInput())
.then((container: DashboardContainer | ErrorEmbeddable | undefined) => {
if (container && !isErrorEmbeddable(container)) {
dashboardContainer = container;

dashboardContainer.renderEmpty = () => {
const shouldShowEditHelp = getShouldShowEditHelp();
const shouldShowViewHelp = getShouldShowViewHelp();
const isEmptyInReadOnlyMode = shouldShowUnauthorizedEmptyState();
const isEmptyState = shouldShowEditHelp || shouldShowViewHelp || isEmptyInReadOnlyMode;
return isEmptyState ? (
<DashboardEmptyScreen
{...getEmptyScreenProps(shouldShowEditHelp, isEmptyInReadOnlyMode)}
/>
) : null;
};

outputSubscription = merge(
// output of dashboard container itself
dashboardContainer.getOutput$(),
// plus output of dashboard container children,
// children may change, so make sure we subscribe/unsubscribe with switchMap
dashboardContainer.getOutput$().pipe(
map(() => dashboardContainer!.getChildIds()),
distinctUntilChanged(deepEqual),
switchMap((newChildIds: string[]) =>
merge(
...newChildIds.map((childId) =>
dashboardContainer!
.getChild(childId)
.getOutput$()
.pipe(catchError(() => EMPTY))
)
)
)
)
)
.pipe(
mapTo(dashboardContainer),
startWith(dashboardContainer) // to trigger initial index pattern update
// updateIndexPatternsOperator //TODO
)
.subscribe();

inputSubscription = dashboardContainer.getInput$().subscribe(() => {});

if (dashboardDom && container) {
container.render(dashboardDom);
}
}
});
}

return (
<div>
{savedDashboardInstance && appState && (
<DashboardTopNav
isChromeVisible={isChromeVisible}
savedDashboardInstance={savedDashboardInstance}
appState={appState}
/>
)}
</div>
);
};
Original file line number Diff line number Diff line change
@@ -3,8 +3,115 @@
* SPDX-License-Identifier: Apache-2.0
*/

import React from 'react';
import React, { memo, useState, useEffect } from 'react';
import { Filter } from 'src/plugins/data/public';
import { useCallback } from 'react';
import { useOpenSearchDashboards } from '../../../../opensearch_dashboards_react/public';
import { getTopNavConfig } from '../top_nav/get_top_nav_config';
import { DashboardAppState, DashboardServices, NavAction } from '../../types';

export const DashboardTopNav = () => {
return <div>Dashboard Top Nav</div>;
interface DashboardTopNavProps {
isChromeVisible: boolean;
savedDashboardInstance: any;
appState: DashboardAppState;
}

enum UrlParams {
SHOW_TOP_MENU = 'show-top-menu',
SHOW_QUERY_INPUT = 'show-query-input',
SHOW_TIME_FILTER = 'show-time-filter',
SHOW_FILTER_BAR = 'show-filter-bar',
HIDE_FILTER_BAR = 'hide-filter-bar',
}

const TopNav = ({ isChromeVisible, savedDashboardInstance, appState }: DashboardTopNavProps) => {
const [filters, setFilters] = useState<Filter[]>([]);
const [topNavMenu, setTopNavMenu] = useState<any>();
const [isFullScreenMode, setIsFullScreenMode] = useState<any>();

const { services } = useOpenSearchDashboards<DashboardServices>();
const { TopNavMenu } = services.navigation.ui;
const { data, dashboardConfig, setHeaderActionMenu } = services;
const { query: queryService } = data;

// TODO: this should base on URL
const isEmbeddedExternally = false;

// TODO: should use URL params
const shouldForceDisplay = (param: string): boolean => {
// const [searchParams] = useSearchParams();
return false;
};

const shouldShowNavBarComponent = (forceShow: boolean): boolean =>
(forceShow || isChromeVisible) && !appState?.fullScreenMode;

useEffect(() => {
setFilters(queryService.filterManager.getFilters());
}, [services, queryService]);

useEffect(() => {
const navActions: {
[key: string]: NavAction;
} = {}; // TODO: need to implement nav actions
setTopNavMenu(
getTopNavConfig(appState?.viewMode, navActions, dashboardConfig.getHideWriteControls())
);
}, [appState, services, dashboardConfig]);

useEffect(() => {
setIsFullScreenMode(appState?.fullScreenMode);
}, [appState, services]);

const shouldShowFilterBar = (forceHide: boolean): boolean =>
!forceHide && (filters!.length > 0 || !appState?.fullScreenMode);

const forceShowTopNavMenu = shouldForceDisplay(UrlParams.SHOW_TOP_MENU);
const forceShowQueryInput = shouldForceDisplay(UrlParams.SHOW_QUERY_INPUT);
const forceShowDatePicker = shouldForceDisplay(UrlParams.SHOW_TIME_FILTER);
const forceHideFilterBar = shouldForceDisplay(UrlParams.HIDE_FILTER_BAR);
const showTopNavMenu = shouldShowNavBarComponent(forceShowTopNavMenu);
const showQueryInput = shouldShowNavBarComponent(forceShowQueryInput);
const showDatePicker = shouldShowNavBarComponent(forceShowDatePicker);
const showQueryBar = showQueryInput || showDatePicker;
const showFilterBar = shouldShowFilterBar(forceHideFilterBar);
const showSearchBar = showQueryBar || showFilterBar;

// TODO: implement handleRefresh
const handleRefresh = useCallback((_payload: any, isUpdate?: boolean) => {
/* if (isUpdate === false) {
// The user can still request a reload in the query bar, even if the
// query is the same, and in that case, we have to explicitly ask for
// a reload, since no state changes will cause it.
lastReloadRequestTime = new Date().getTime();
const changes = getChangesFromAppStateForContainerState();
if (changes && dashboardContainer) {
dashboardContainer.updateInput(changes);
}*/
}, []);

return isChromeVisible ? (
<TopNavMenu
appName={'dashboard'}
savedQueryId={appState?.savedQuery}
config={showTopNavMenu ? topNavMenu : undefined}
className={isFullScreenMode ? 'osdTopNavMenu-isFullScreen' : undefined}
screenTitle={appState.title}
// showTopNavMenu={showTopNavMenu}
showSearchBar={showSearchBar}
showQueryBar={showQueryBar}
showQueryInput={showQueryInput}
showDatePicker={showDatePicker}
showFilterBar={showFilterBar}
useDefaultBehaviors={true}
indexPatterns={[]}
showSaveQuery={services.dashboardCapabilities.saveQuery as boolean}
savedQuery={undefined}
onSavedQueryIdChange={() => {}}
onQuerySubmit={handleRefresh}
setMenuMountPoint={isEmbeddedExternally ? undefined : setHeaderActionMenu}
/>
) : null;
};

export const DashboardTopNav = memo(TopNav);
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

import { migrateAppState } from '../lib/migrate_app_state';
import {
IOsdUrlStateStorage,
createStateContainer,
syncState,
} from '../../../../opensearch_dashboards_utils/public';
import {
DashboardAppState,
DashboardAppStateTransitions,
DashboardAppStateInUrl,
DashboardServices,
} from '../../types';
import { ViewMode } from '../../embeddable_plugin';
import { getDashboardIdFromUrl } from '../lib';

const STATE_STORAGE_KEY = '_a';

interface Arguments {
osdUrlStateStorage: IOsdUrlStateStorage;
stateDefaults: DashboardAppState;
services: DashboardServices;
instance: any;
}

export const createDashboardAppState = ({
stateDefaults,
osdUrlStateStorage,
services,
instance,
}: Arguments) => {
const urlState = osdUrlStateStorage.get<DashboardAppState>(STATE_STORAGE_KEY);
const { opensearchDashboardsVersion, usageCollection, history } = services;
const initialState = migrateAppState(
{
...stateDefaults,
...urlState,
},
opensearchDashboardsVersion,
usageCollection
);

const pureTransitions = {
set: (state) => (prop, value) => ({ ...state, [prop]: value }),
setOption: (state) => (option, value) => ({
...state,
options: {
...state.options,
[option]: value,
},
}),
} as DashboardAppStateTransitions;
/*
make sure url ('_a') matches initial state
Initializing appState does two things - first it translates the defaults into AppState,
second it updates appState based on the url (the url trumps the defaults). This means if
we update the state format at all and want to handle BWC, we must not only migrate the
data stored with saved vis, but also any old state in the url.
*/
osdUrlStateStorage.set(STATE_STORAGE_KEY, initialState, { replace: true });

const stateContainer = createStateContainer<DashboardAppState, DashboardAppStateTransitions>(
initialState,
pureTransitions
);

const toUrlState = (state: DashboardAppState): DashboardAppStateInUrl => {
if (state.viewMode === ViewMode.VIEW) {
const { panels, ...stateWithoutPanels } = state;
return stateWithoutPanels;
}
return state;
};

const { start: startStateSync, stop: stopStateSync } = syncState({
storageKey: STATE_STORAGE_KEY,
stateContainer: {
...stateContainer,
get: () => toUrlState(stateContainer.get()),
set: (state: DashboardAppStateInUrl | null) => {
// sync state required state container to be able to handle null
// overriding set() so it could handle null coming from url
if (state) {
// Skip this update if current dashboardId in the url is different from what we have in the current instance of state manager
// As dashboard is driven by angular at the moment, the destroy cycle happens async,
// If the dashboardId has changed it means this instance
// is going to be destroyed soon and we shouldn't sync state anymore,
// as it could potentially trigger further url updates
const currentDashboardIdInUrl = getDashboardIdFromUrl(history.location.pathname);
if (currentDashboardIdInUrl !== instance.id) return;

stateContainer.set({
...stateDefaults,
...state,
});
} else {
// Do nothing in case when state from url is empty,
// this fixes: https://github.com/elastic/kibana/issues/57789
// There are not much cases when state in url could become empty:
// 1. User manually removed `_a` from the url
// 2. Browser is navigating away from the page and most likely there is no `_a` in the url.
// In this case we don't want to do any state updates
// and just allow $scope.$on('destroy') fire later and clean up everything
}
},
},
stateStorage: osdUrlStateStorage,
});

// start syncing the appState with the ('_a') url
startStateSync();
return { stateContainer, stopStateSync };
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/*
* SPDX-License-Identifier: Apache-2.0
*
* The OpenSearch Contributors require contributions made to
* this file be licensed under the Apache-2.0 license or a
* compatible open source license.
*
* Any modifications Copyright OpenSearch Contributors. See
* GitHub history for details.
*/

import { useState, useEffect } from 'react';
import { ChromeStart } from 'opensearch-dashboards/public';

export const useChromeVisibility = (chrome: ChromeStart) => {
const [isVisible, setIsVisible] = useState<boolean>(true);

useEffect(() => {
const subscription = chrome.getIsVisible$().subscribe((value: boolean) => {
setIsVisible(value);
});

return () => subscription.unsubscribe();
}, [chrome]);

return isVisible;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

import EventEmitter from 'events';
import { useEffect, useState } from 'react';
import { cloneDeep } from 'lodash';
import { map } from 'rxjs/operators';
import { connectToQueryState, opensearchFilters } from '../../../../../data/public';
import { migrateLegacyQuery } from '../../lib/migrate_legacy_query';
import { DashboardServices } from '../../../types';

import { DashboardAppStateContainer } from '../../../types';
import { migrateAppState, getAppStateDefaults } from '../../lib';
import { createDashboardAppState } from '../create_dashboard_app_state';

/**
* This effect is responsible for instantiating the dashboard app state container,
* which is in sync with "_a" url param
*/
export const useDashboardAppState = (
services: DashboardServices,
eventEmitter: EventEmitter,
instance: any
) => {
const [appState, setAppState] = useState<DashboardAppStateContainer | null>(null);

useEffect(() => {
if (!instance) {
return;
}
const { dashboardConfig, usageCollection, opensearchDashboardsVersion } = services;
const hideWriteControls = dashboardConfig.getHideWriteControls();
const stateDefaults = migrateAppState(
getAppStateDefaults(instance, hideWriteControls),
opensearchDashboardsVersion,
usageCollection
);

const { stateContainer, stopStateSync } = createDashboardAppState({
stateDefaults,
osdUrlStateStorage: services.osdUrlStateStorage,
services,
instance,
});

const { filterManager, queryString } = services.data.query;

// sync initial app state from state container to managers
filterManager.setAppFilters(cloneDeep(stateContainer.getState().filters));
queryString.setQuery(migrateLegacyQuery(stateContainer.getState().query));

// setup syncing of app filters between app state and query services
const stopSyncingAppFilters = connectToQueryState(
services.data.query,
{
set: ({ filters, query }) => {
stateContainer.transitions.set('filters', filters || []);
stateContainer.transitions.set('query', query || queryString.getDefaultQuery());
},
get: () => ({
filters: stateContainer.getState().filters,
query: migrateLegacyQuery(stateContainer.getState().query),
}),
state$: stateContainer.state$.pipe(
map((state) => ({
filters: state.filters,
query: queryString.formatQuery(state.query),
}))
),
},
{
filters: opensearchFilters.FilterStateStore.APP_STATE,
query: true,
}
);

setAppState(stateContainer);

return () => {
stopStateSync();
stopSyncingAppFilters();
};
}, [eventEmitter, instance, services]);

return { appState };
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

import { i18n } from '@osd/i18n';
import { EventEmitter } from 'events';
import { useEffect, useRef, useState } from 'react';
import {
redirectWhenMissing,
SavedObjectNotFound,
} from '../../../../../opensearch_dashboards_utils/public';
import { DashboardConstants } from '../../../dashboard_constants';
import { DashboardServices } from '../../../types';

/**
* This effect is responsible for instantiating a saved dashboard or creating a new one
* using url parameters, embedding and destroying it in DOM
*/
export const useSavedDashboardInstance = (
services: DashboardServices,
eventEmitter: EventEmitter,
isChromeVisible: boolean | undefined,
dashboardIdFromUrl: string | undefined
) => {
const [state, setState] = useState<{
savedDashboardInstance?: any;
}>({});

const dashboardId = useRef('');

useEffect(() => {
const getSavedDashboardInstance = async () => {
const {
application: { navigateToApp },
chrome,
history,
http: { basePath },
notifications,
savedDashboards,
} = services;
try {
console.log('trying to get saved dashboard');
let savedDashboardInstance: any;
if (history.location.pathname === '/create') {
try {
savedDashboardInstance = await savedDashboards.get();
} catch {
redirectWhenMissing({
history,
basePath,
navigateToApp,
mapping: {
dashboard: DashboardConstants.LANDING_PAGE_PATH,
},
toastNotifications: notifications.toasts,
});
}
} else if (dashboardIdFromUrl) {
try {
savedDashboardInstance = await savedDashboards.get(dashboardIdFromUrl);
chrome.recentlyAccessed.add(
savedDashboardInstance.getFullPath(),
savedDashboardInstance.title,
dashboardIdFromUrl
);
console.log('saved dashboard', savedDashboardInstance);
} catch (error) {
// Preserve BWC of v5.3.0 links for new, unsaved dashboards.
// See https://github.com/elastic/kibana/issues/10951 for more context.
if (error instanceof SavedObjectNotFound && dashboardIdFromUrl === 'create') {
// Note preserve querystring part is necessary so the state is preserved through the redirect.
history.replace({
...history.location, // preserve query,
pathname: DashboardConstants.CREATE_NEW_DASHBOARD_URL,
});

notifications.toasts.addWarning(
i18n.translate('dashboard.urlWasRemovedInSixZeroWarningMessage', {
defaultMessage:
'The url "dashboard/create" was removed in 6.0. Please update your bookmarks.',
})
);
return new Promise(() => {});
} else {
// E.g. a corrupt or deleted dashboard
notifications.toasts.addDanger(error.message);
history.push(DashboardConstants.LANDING_PAGE_PATH);
return new Promise(() => {});
}
}
}

setState({ savedDashboardInstance });
} catch (error) {}
};

if (isChromeVisible === undefined) {
// waiting for specifying chrome
return;
}

if (!dashboardId.current) {
dashboardId.current = dashboardIdFromUrl || 'new';
getSavedDashboardInstance();
} else if (
dashboardIdFromUrl &&
dashboardId.current !== dashboardIdFromUrl &&
state.savedDashboardInstance?.id !== dashboardIdFromUrl
) {
dashboardId.current = dashboardIdFromUrl;
setState({});
getSavedDashboardInstance();
}
}, [eventEmitter, isChromeVisible, services, state.savedDashboardInstance, dashboardIdFromUrl]);

return {
...state,
};
};
29 changes: 29 additions & 0 deletions src/plugins/dashboard/public/application/utils/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

import { Filter } from 'src/plugins/data/public';
import { DashboardServices } from '../../types';

export const getDefaultQuery = ({ data }: DashboardServices) => {
return data.query.queryString.getDefaultQuery();
};

export const dashboardStateToEditorState = (
dashboardInstance: any,
services: DashboardServices
) => {
const savedDashboardState = {
id: dashboardInstance.id,
title: dashboardInstance.title,
description: dashboardInstance.description,
searchSource: dashboardInstance.searchSource,
savedSearchId: dashboardInstance.savedSearchId,
};
return {
query: dashboardInstance.searchSource?.getOwnField('query') || getDefaultQuery(services),
filters: (dashboardInstance.searchSource?.getOwnField('filter') as Filter[]) || [],
savedDashboardState,
};
};
13 changes: 12 additions & 1 deletion src/plugins/dashboard/public/types.ts
Original file line number Diff line number Diff line change
@@ -40,7 +40,11 @@ import {
ScopedHistory,
AppMountParameters,
} from 'src/core/public';
import { IOsdUrlStateStorage, Storage } from 'src/plugins/opensearch_dashboards_utils/public';
import {
IOsdUrlStateStorage,
ReduxLikeStateContainer,
Storage,
} from 'src/plugins/opensearch_dashboards_utils/public';
import { SavedObjectLoader, SavedObjectsStart } from 'src/plugins/saved_objects/public';
import { OpenSearchDashboardsLegacyStart } from 'src/plugins/opensearch_dashboards_legacy/public';
import { SharePluginStart } from 'src/plugins/share/public';
@@ -54,6 +58,8 @@ import { SavedDashboardPanel730ToLatest } from '../common';
export interface DashboardCapabilities {
showWriteControls: boolean;
createNew: boolean;
showSavedQuery: boolean;
saveQuery: boolean;
}

// TODO: Replace Saved object interfaces by the ones Core will provide when it is ready.
@@ -149,6 +155,11 @@ export interface DashboardAppStateTransitions {
) => DashboardAppState;
}

export type DashboardAppStateContainer = ReduxLikeStateContainer<
DashboardAppState,
DashboardAppStateTransitions
>;

export interface SavedDashboardPanelMap {
[key: string]: SavedDashboardPanel;
}

0 comments on commit 65d57f7

Please sign in to comment.