mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 17:59:23 -04:00
1161 lines
42 KiB
TypeScript
1161 lines
42 KiB
TypeScript
/*
|
||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||
* license agreements. See the NOTICE file distributed with
|
||
* this work for additional information regarding copyright
|
||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||
* the Apache License, Version 2.0 (the "License"); you may
|
||
* not use this file except in compliance with the License.
|
||
* You may obtain a copy of the License at
|
||
*
|
||
* http://www.apache.org/licenses/LICENSE-2.0
|
||
*
|
||
* Unless required by applicable law or agreed to in writing,
|
||
* software distributed under the License is distributed on an
|
||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||
* KIND, either express or implied. See the License for the
|
||
* specific language governing permissions and limitations
|
||
* under the License.
|
||
*/
|
||
|
||
import _, { uniqBy } from 'lodash';
|
||
import { i18n } from '@kbn/i18n';
|
||
import { EUI_MODAL_CANCEL_BUTTON, EuiCheckboxGroup } from '@elastic/eui';
|
||
import { EuiCheckboxGroupIdToSelectedMap } from '@elastic/eui/src/components/form/checkbox/checkbox_group';
|
||
import React, { useState, ReactElement } from 'react';
|
||
import ReactDOM from 'react-dom';
|
||
import angular from 'angular';
|
||
import deepEqual from 'fast-deep-equal';
|
||
|
||
import { Observable, pipe, Subscription, merge } from 'rxjs';
|
||
import {
|
||
filter,
|
||
map,
|
||
debounceTime,
|
||
mapTo,
|
||
startWith,
|
||
switchMap,
|
||
distinctUntilChanged,
|
||
} from 'rxjs/operators';
|
||
import { History } from 'history';
|
||
import { SavedObjectSaveOpts } from 'src/plugins/saved_objects/public';
|
||
import { NavigationPublicPluginStart as NavigationStart } from 'src/plugins/navigation/public';
|
||
import { DashboardEmptyScreen, DashboardEmptyScreenProps } from './dashboard_empty_screen';
|
||
|
||
import {
|
||
connectToQueryState,
|
||
esFilters,
|
||
IndexPattern,
|
||
IndexPatternsContract,
|
||
QueryState,
|
||
SavedQuery,
|
||
syncQueryStateWithUrl,
|
||
} from '../../../data/public';
|
||
import { getSavedObjectFinder, SaveResult, showSaveModal } from '../../../saved_objects/public';
|
||
|
||
import {
|
||
DASHBOARD_CONTAINER_TYPE,
|
||
DashboardContainer,
|
||
DashboardContainerInput,
|
||
DashboardPanelState,
|
||
} from './embeddable';
|
||
import {
|
||
EmbeddableFactoryNotFoundError,
|
||
ErrorEmbeddable,
|
||
isErrorEmbeddable,
|
||
openAddPanelFlyout,
|
||
ViewMode,
|
||
ContainerOutput,
|
||
EmbeddableInput,
|
||
} from '../../../embeddable/public';
|
||
import { NavAction, SavedDashboardPanel } from '../types';
|
||
|
||
import { showOptionsPopover } from './top_nav/show_options_popover';
|
||
import { DashboardSaveModal } from './top_nav/save_modal';
|
||
import { showCloneModal } from './top_nav/show_clone_modal';
|
||
import { saveDashboard } from './lib';
|
||
import { DashboardStateManager } from './dashboard_state_manager';
|
||
import { createDashboardEditUrl, DashboardConstants } from '../dashboard_constants';
|
||
import { getTopNavConfig } from './top_nav/get_top_nav_config';
|
||
import { TopNavIds } from './top_nav/top_nav_ids';
|
||
import { getDashboardTitle } from './dashboard_strings';
|
||
import { DashboardAppScope } from './dashboard_app';
|
||
import { convertSavedDashboardPanelToPanelState } from './lib/embeddable_saved_object_converters';
|
||
import { RenderDeps } from './application';
|
||
import { IKbnUrlStateStorage, setStateToKbnUrl, unhashUrl } from '../../../kibana_utils/public';
|
||
import {
|
||
addFatalError,
|
||
AngularHttpError,
|
||
KibanaLegacyStart,
|
||
subscribeWithScope,
|
||
} from '../../../kibana_legacy/public';
|
||
import { migrateLegacyQuery } from './lib/migrate_legacy_query';
|
||
|
||
export interface DashboardAppControllerDependencies extends RenderDeps {
|
||
$scope: DashboardAppScope;
|
||
$route: any;
|
||
$routeParams: any;
|
||
indexPatterns: IndexPatternsContract;
|
||
dashboardConfig: KibanaLegacyStart['dashboardConfig'];
|
||
history: History;
|
||
kbnUrlStateStorage: IKbnUrlStateStorage;
|
||
navigation: NavigationStart;
|
||
}
|
||
|
||
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',
|
||
}
|
||
|
||
interface UrlParamsSelectedMap {
|
||
[UrlParams.SHOW_TOP_MENU]: boolean;
|
||
[UrlParams.SHOW_QUERY_INPUT]: boolean;
|
||
[UrlParams.SHOW_TIME_FILTER]: boolean;
|
||
[UrlParams.SHOW_FILTER_BAR]: boolean;
|
||
}
|
||
|
||
interface UrlParamValues extends Omit<UrlParamsSelectedMap, UrlParams.SHOW_FILTER_BAR> {
|
||
[UrlParams.HIDE_FILTER_BAR]: boolean;
|
||
}
|
||
|
||
export class DashboardAppController {
|
||
// Part of the exposed plugin API - do not remove without careful consideration.
|
||
appStatus: {
|
||
dirty: boolean;
|
||
};
|
||
|
||
constructor({
|
||
pluginInitializerContext,
|
||
$scope,
|
||
$route,
|
||
$routeParams,
|
||
dashboardConfig,
|
||
indexPatterns,
|
||
savedQueryService,
|
||
embeddable,
|
||
share,
|
||
dashboardCapabilities,
|
||
scopedHistory,
|
||
embeddableCapabilities: { visualizeCapabilities, mapsCapabilities },
|
||
data: { query: queryService, search: searchService },
|
||
core: {
|
||
notifications,
|
||
overlays,
|
||
chrome,
|
||
fatalErrors,
|
||
uiSettings,
|
||
savedObjects,
|
||
http,
|
||
i18n: i18nStart,
|
||
},
|
||
history,
|
||
setHeaderActionMenu,
|
||
kbnUrlStateStorage,
|
||
usageCollection,
|
||
navigation,
|
||
}: DashboardAppControllerDependencies) {
|
||
const filterManager = queryService.filterManager;
|
||
const timefilter = queryService.timefilter.timefilter;
|
||
const queryStringManager = queryService.queryString;
|
||
const isEmbeddedExternally = Boolean($routeParams.embed);
|
||
|
||
// url param rules should only apply when embedded (e.g. url?embed=true)
|
||
const shouldForceDisplay = (param: string): boolean =>
|
||
isEmbeddedExternally && Boolean($routeParams[param]);
|
||
|
||
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);
|
||
|
||
let lastReloadRequestTime = 0;
|
||
const dash = ($scope.dash = $route.current.locals.dash);
|
||
if (dash.id) {
|
||
chrome.docTitle.change(dash.title);
|
||
}
|
||
|
||
let incomingEmbeddable = embeddable
|
||
.getStateTransfer(scopedHistory())
|
||
.getIncomingEmbeddablePackage();
|
||
|
||
const dashboardStateManager = new DashboardStateManager({
|
||
savedDashboard: dash,
|
||
hideWriteControls: dashboardConfig.getHideWriteControls(),
|
||
kibanaVersion: pluginInitializerContext.env.packageInfo.version,
|
||
kbnUrlStateStorage,
|
||
history,
|
||
usageCollection,
|
||
});
|
||
|
||
// sync initial app filters from state to filterManager
|
||
// if there is an existing similar global filter, then leave it as global
|
||
filterManager.setAppFilters(_.cloneDeep(dashboardStateManager.appState.filters));
|
||
queryStringManager.setQuery(migrateLegacyQuery(dashboardStateManager.appState.query));
|
||
|
||
// setup syncing of app filters between appState and filterManager
|
||
const stopSyncingAppFilters = connectToQueryState(
|
||
queryService,
|
||
{
|
||
set: ({ filters, query }) => {
|
||
dashboardStateManager.setFilters(filters || []);
|
||
dashboardStateManager.setQuery(query || queryStringManager.getDefaultQuery());
|
||
},
|
||
get: () => ({
|
||
filters: dashboardStateManager.appState.filters,
|
||
query: dashboardStateManager.getQuery(),
|
||
}),
|
||
state$: dashboardStateManager.appState$.pipe(
|
||
map((state) => ({
|
||
filters: state.filters,
|
||
query: queryStringManager.formatQuery(state.query),
|
||
}))
|
||
),
|
||
},
|
||
{
|
||
filters: esFilters.FilterStateStore.APP_STATE,
|
||
query: true,
|
||
}
|
||
);
|
||
|
||
// The hash check is so we only update the time filter on dashboard open, not during
|
||
// normal cross app navigation.
|
||
if (dashboardStateManager.getIsTimeSavedWithDashboard()) {
|
||
const initialGlobalStateInUrl = kbnUrlStateStorage.get<QueryState>('_g');
|
||
if (!initialGlobalStateInUrl?.time) {
|
||
dashboardStateManager.syncTimefilterWithDashboardTime(timefilter);
|
||
}
|
||
if (!initialGlobalStateInUrl?.refreshInterval) {
|
||
dashboardStateManager.syncTimefilterWithDashboardRefreshInterval(timefilter);
|
||
}
|
||
}
|
||
|
||
// starts syncing `_g` portion of url with query services
|
||
// it is important to start this syncing after `dashboardStateManager.syncTimefilterWithDashboard(timefilter);` above is run,
|
||
// otherwise it will case redundant browser history records
|
||
const { stop: stopSyncingQueryServiceStateWithUrl } = syncQueryStateWithUrl(
|
||
queryService,
|
||
kbnUrlStateStorage
|
||
);
|
||
|
||
// starts syncing `_a` portion of url
|
||
dashboardStateManager.startStateSyncing();
|
||
|
||
$scope.showSaveQuery = dashboardCapabilities.saveQuery as boolean;
|
||
|
||
const getShouldShowEditHelp = () =>
|
||
!dashboardStateManager.getPanels().length &&
|
||
dashboardStateManager.getIsEditMode() &&
|
||
!dashboardConfig.getHideWriteControls();
|
||
|
||
const getShouldShowViewHelp = () =>
|
||
!dashboardStateManager.getPanels().length &&
|
||
dashboardStateManager.getIsViewMode() &&
|
||
!dashboardConfig.getHideWriteControls();
|
||
|
||
const shouldShowUnauthorizedEmptyState = () => {
|
||
const readonlyMode =
|
||
!dashboardStateManager.getPanels().length &&
|
||
!getShouldShowEditHelp() &&
|
||
!getShouldShowViewHelp() &&
|
||
dashboardConfig.getHideWriteControls();
|
||
const userHasNoPermissions =
|
||
!dashboardStateManager.getPanels().length &&
|
||
!visualizeCapabilities.save &&
|
||
!mapsCapabilities.save;
|
||
return readonlyMode || userHasNoPermissions;
|
||
};
|
||
|
||
const addVisualization = () => {
|
||
navActions[TopNavIds.VISUALIZE]();
|
||
};
|
||
|
||
function getDashboardIndexPatterns(container: DashboardContainer): IndexPattern[] {
|
||
let panelIndexPatterns: IndexPattern[] = [];
|
||
Object.values(container.getChildIds()).forEach((id) => {
|
||
const embeddableInstance = container.getChild(id);
|
||
if (isErrorEmbeddable(embeddableInstance)) return;
|
||
const embeddableIndexPatterns = (embeddableInstance.getOutput() as any).indexPatterns;
|
||
if (!embeddableIndexPatterns) return;
|
||
panelIndexPatterns.push(...embeddableIndexPatterns);
|
||
});
|
||
panelIndexPatterns = uniqBy(panelIndexPatterns, 'id');
|
||
return panelIndexPatterns;
|
||
}
|
||
|
||
const updateIndexPatternsOperator = pipe(
|
||
filter((container: DashboardContainer) => !!container && !isErrorEmbeddable(container)),
|
||
map(getDashboardIndexPatterns),
|
||
distinctUntilChanged((a, b) =>
|
||
deepEqual(
|
||
a.map((ip) => ip.id),
|
||
b.map((ip) => ip.id)
|
||
)
|
||
),
|
||
// using switchMap for previous task cancellation
|
||
switchMap((panelIndexPatterns: IndexPattern[]) => {
|
||
return new Observable((observer) => {
|
||
if (panelIndexPatterns && panelIndexPatterns.length > 0) {
|
||
$scope.$evalAsync(() => {
|
||
if (observer.closed) return;
|
||
$scope.indexPatterns = panelIndexPatterns;
|
||
observer.complete();
|
||
});
|
||
} else {
|
||
indexPatterns.getDefault().then((defaultIndexPattern) => {
|
||
if (observer.closed) return;
|
||
$scope.$evalAsync(() => {
|
||
if (observer.closed) return;
|
||
$scope.indexPatterns = [defaultIndexPattern as IndexPattern];
|
||
observer.complete();
|
||
});
|
||
});
|
||
}
|
||
});
|
||
})
|
||
);
|
||
|
||
const getEmptyScreenProps = (
|
||
shouldShowEditHelp: boolean,
|
||
isEmptyInReadOnlyMode: boolean
|
||
): DashboardEmptyScreenProps => {
|
||
const emptyScreenProps: DashboardEmptyScreenProps = {
|
||
onLinkClick: shouldShowEditHelp ? $scope.showAddPanel : $scope.enterEditMode,
|
||
showLinkToVisualize: shouldShowEditHelp,
|
||
uiSettings,
|
||
http,
|
||
};
|
||
if (shouldShowEditHelp) {
|
||
emptyScreenProps.onVisualizeClick = addVisualization;
|
||
}
|
||
if (isEmptyInReadOnlyMode) {
|
||
emptyScreenProps.isReadonlyMode = true;
|
||
}
|
||
return emptyScreenProps;
|
||
};
|
||
|
||
const getDashboardInput = (): DashboardContainerInput => {
|
||
const embeddablesMap: {
|
||
[key: string]: DashboardPanelState;
|
||
} = {};
|
||
dashboardStateManager.getPanels().forEach((panel: SavedDashboardPanel) => {
|
||
embeddablesMap[panel.panelIndex] = convertSavedDashboardPanelToPanelState(panel);
|
||
});
|
||
|
||
// If the incoming embeddable state's id already exists in the embeddables map, replace the input, retaining the existing gridData for that panel.
|
||
if (incomingEmbeddable?.embeddableId && embeddablesMap[incomingEmbeddable.embeddableId]) {
|
||
const originalPanelState = embeddablesMap[incomingEmbeddable.embeddableId];
|
||
embeddablesMap[incomingEmbeddable.embeddableId] = {
|
||
gridData: originalPanelState.gridData,
|
||
type: incomingEmbeddable.type,
|
||
explicitInput: {
|
||
...originalPanelState.explicitInput,
|
||
...incomingEmbeddable.input,
|
||
id: incomingEmbeddable.embeddableId,
|
||
},
|
||
};
|
||
incomingEmbeddable = undefined;
|
||
}
|
||
|
||
const shouldShowEditHelp = getShouldShowEditHelp();
|
||
const shouldShowViewHelp = getShouldShowViewHelp();
|
||
const isEmptyInReadonlyMode = shouldShowUnauthorizedEmptyState();
|
||
return {
|
||
id: dashboardStateManager.savedDashboard.id || '',
|
||
filters: filterManager.getFilters(),
|
||
hidePanelTitles: dashboardStateManager.getHidePanelTitles(),
|
||
query: $scope.model.query,
|
||
timeRange: {
|
||
..._.cloneDeep(timefilter.getTime()),
|
||
},
|
||
refreshConfig: timefilter.getRefreshInterval(),
|
||
viewMode: dashboardStateManager.getViewMode(),
|
||
panels: embeddablesMap,
|
||
isFullScreenMode: dashboardStateManager.getFullScreenMode(),
|
||
isEmbeddedExternally,
|
||
isEmptyState: shouldShowEditHelp || shouldShowViewHelp || isEmptyInReadonlyMode,
|
||
useMargins: dashboardStateManager.getUseMargins(),
|
||
lastReloadRequestTime,
|
||
title: dashboardStateManager.getTitle(),
|
||
description: dashboardStateManager.getDescription(),
|
||
expandedPanelId: dashboardStateManager.getExpandedPanelId(),
|
||
};
|
||
};
|
||
|
||
const updateState = () => {
|
||
// Following the "best practice" of always have a '.' in your ng-models –
|
||
// https://github.com/angular/angular.js/wiki/Understanding-Scopes
|
||
$scope.model = {
|
||
query: dashboardStateManager.getQuery(),
|
||
filters: filterManager.getFilters(),
|
||
timeRestore: dashboardStateManager.getTimeRestore(),
|
||
title: dashboardStateManager.getTitle(),
|
||
description: dashboardStateManager.getDescription(),
|
||
timeRange: timefilter.getTime(),
|
||
refreshInterval: timefilter.getRefreshInterval(),
|
||
};
|
||
$scope.panels = dashboardStateManager.getPanels();
|
||
};
|
||
|
||
updateState();
|
||
|
||
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);
|
||
|
||
if (dashboardFactory) {
|
||
const searchSessionId = searchService.session.start();
|
||
dashboardFactory
|
||
.create({ ...getDashboardInput(), searchSessionId })
|
||
.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(
|
||
mapTo(dashboardContainer),
|
||
startWith(dashboardContainer), // to trigger initial index pattern update
|
||
updateIndexPatternsOperator
|
||
)
|
||
.subscribe();
|
||
|
||
inputSubscription = dashboardContainer.getInput$().subscribe(() => {
|
||
let dirty = false;
|
||
|
||
// This has to be first because handleDashboardContainerChanges causes
|
||
// appState.save which will cause refreshDashboardContainer to be called.
|
||
|
||
if (
|
||
!esFilters.compareFilters(
|
||
container.getInput().filters,
|
||
filterManager.getFilters(),
|
||
esFilters.COMPARE_ALL_OPTIONS
|
||
)
|
||
) {
|
||
// Add filters modifies the object passed to it, hence the clone deep.
|
||
filterManager.addFilters(_.cloneDeep(container.getInput().filters));
|
||
|
||
dashboardStateManager.applyFilters(
|
||
$scope.model.query,
|
||
container.getInput().filters
|
||
);
|
||
dirty = true;
|
||
}
|
||
|
||
dashboardStateManager.handleDashboardContainerChanges(container);
|
||
$scope.$evalAsync(() => {
|
||
if (dirty) {
|
||
updateState();
|
||
}
|
||
});
|
||
});
|
||
|
||
dashboardStateManager.registerChangeListener(() => {
|
||
// we aren't checking dirty state because there are changes the container needs to know about
|
||
// that won't make the dashboard "dirty" - like a view mode change.
|
||
refreshDashboardContainer();
|
||
});
|
||
|
||
// If the incomingEmbeddable does not yet exist in the panels listing, create a new panel using the container's addEmbeddable method.
|
||
if (
|
||
incomingEmbeddable &&
|
||
(!incomingEmbeddable.embeddableId ||
|
||
!container.getInput().panels[incomingEmbeddable.embeddableId])
|
||
) {
|
||
container.addNewEmbeddable<EmbeddableInput>(
|
||
incomingEmbeddable.type,
|
||
incomingEmbeddable.input
|
||
);
|
||
}
|
||
}
|
||
|
||
if (dashboardDom && container) {
|
||
container.render(dashboardDom);
|
||
}
|
||
});
|
||
}
|
||
|
||
// Part of the exposed plugin API - do not remove without careful consideration.
|
||
this.appStatus = {
|
||
dirty: !dash.id,
|
||
};
|
||
|
||
dashboardStateManager.registerChangeListener((status) => {
|
||
this.appStatus.dirty = status.dirty || !dash.id;
|
||
updateState();
|
||
});
|
||
|
||
dashboardStateManager.applyFilters(
|
||
dashboardStateManager.getQuery() || queryStringManager.getDefaultQuery(),
|
||
filterManager.getFilters()
|
||
);
|
||
|
||
const landingPageUrl = () => `#${DashboardConstants.LANDING_PAGE_PATH}`;
|
||
|
||
const getDashTitle = () =>
|
||
getDashboardTitle(
|
||
dashboardStateManager.getTitle(),
|
||
dashboardStateManager.getViewMode(),
|
||
dashboardStateManager.getIsDirty(timefilter),
|
||
dashboardStateManager.isNew()
|
||
);
|
||
|
||
// Push breadcrumbs to new header navigation
|
||
const updateBreadcrumbs = () => {
|
||
chrome.setBreadcrumbs([
|
||
{
|
||
text: i18n.translate('dashboard.dashboardAppBreadcrumbsTitle', {
|
||
defaultMessage: 'Dashboard',
|
||
}),
|
||
href: landingPageUrl(),
|
||
},
|
||
{ text: getDashTitle() },
|
||
]);
|
||
};
|
||
|
||
updateBreadcrumbs();
|
||
dashboardStateManager.registerChangeListener(updateBreadcrumbs);
|
||
|
||
const getChangesFromAppStateForContainerState = () => {
|
||
const appStateDashboardInput = getDashboardInput();
|
||
if (!dashboardContainer || isErrorEmbeddable(dashboardContainer)) {
|
||
return appStateDashboardInput;
|
||
}
|
||
|
||
const containerInput = dashboardContainer.getInput();
|
||
const differences: Partial<DashboardContainerInput> = {};
|
||
|
||
// Filters shouldn't be compared using regular isEqual
|
||
if (
|
||
!esFilters.compareFilters(
|
||
containerInput.filters,
|
||
appStateDashboardInput.filters,
|
||
esFilters.COMPARE_ALL_OPTIONS
|
||
)
|
||
) {
|
||
differences.filters = appStateDashboardInput.filters;
|
||
}
|
||
|
||
Object.keys(_.omit(containerInput, ['filters', 'searchSessionId'])).forEach((key) => {
|
||
const containerValue = (containerInput as { [key: string]: unknown })[key];
|
||
const appStateValue = ((appStateDashboardInput as unknown) as { [key: string]: unknown })[
|
||
key
|
||
];
|
||
if (!_.isEqual(containerValue, appStateValue)) {
|
||
(differences as { [key: string]: unknown })[key] = appStateValue;
|
||
}
|
||
});
|
||
|
||
// cloneDeep hack is needed, as there are multiple place, where container's input mutated,
|
||
// but values from appStateValue are deeply frozen, as they can't be mutated directly
|
||
return Object.values(differences).length === 0 ? undefined : _.cloneDeep(differences);
|
||
};
|
||
|
||
const refreshDashboardContainer = () => {
|
||
const changes = getChangesFromAppStateForContainerState();
|
||
if (changes && dashboardContainer) {
|
||
const searchSessionId = searchService.session.start();
|
||
dashboardContainer.updateInput({ ...changes, searchSessionId });
|
||
}
|
||
};
|
||
|
||
$scope.handleRefresh = function (_payload, isUpdate) {
|
||
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();
|
||
refreshDashboardContainer();
|
||
}
|
||
};
|
||
|
||
const updateStateFromSavedQuery = (savedQuery: SavedQuery) => {
|
||
const allFilters = filterManager.getFilters();
|
||
dashboardStateManager.applyFilters(savedQuery.attributes.query, allFilters);
|
||
if (savedQuery.attributes.timefilter) {
|
||
timefilter.setTime({
|
||
from: savedQuery.attributes.timefilter.from,
|
||
to: savedQuery.attributes.timefilter.to,
|
||
});
|
||
if (savedQuery.attributes.timefilter.refreshInterval) {
|
||
timefilter.setRefreshInterval(savedQuery.attributes.timefilter.refreshInterval);
|
||
}
|
||
}
|
||
// Making this method sync broke the updates.
|
||
// Temporary fix, until we fix the complex state in this file.
|
||
setTimeout(() => {
|
||
filterManager.setFilters(allFilters);
|
||
}, 0);
|
||
};
|
||
|
||
$scope.$watch('savedQuery', (newSavedQuery: SavedQuery) => {
|
||
if (!newSavedQuery) return;
|
||
dashboardStateManager.setSavedQueryId(newSavedQuery.id);
|
||
|
||
updateStateFromSavedQuery(newSavedQuery);
|
||
});
|
||
|
||
$scope.$watch(
|
||
() => {
|
||
return dashboardStateManager.getSavedQueryId();
|
||
},
|
||
(newSavedQueryId) => {
|
||
if (!newSavedQueryId) {
|
||
$scope.savedQuery = undefined;
|
||
return;
|
||
}
|
||
if (!$scope.savedQuery || newSavedQueryId !== $scope.savedQuery.id) {
|
||
savedQueryService.getSavedQuery(newSavedQueryId).then((savedQuery: SavedQuery) => {
|
||
$scope.$evalAsync(() => {
|
||
$scope.savedQuery = savedQuery;
|
||
updateStateFromSavedQuery(savedQuery);
|
||
});
|
||
});
|
||
}
|
||
}
|
||
);
|
||
|
||
$scope.indexPatterns = [];
|
||
|
||
$scope.$watch(
|
||
() => dashboardCapabilities.saveQuery,
|
||
(newCapability) => {
|
||
$scope.showSaveQuery = newCapability as boolean;
|
||
}
|
||
);
|
||
|
||
const onSavedQueryIdChange = (savedQueryId?: string) => {
|
||
dashboardStateManager.setSavedQueryId(savedQueryId);
|
||
};
|
||
|
||
const shouldShowFilterBar = (forceHide: boolean): boolean =>
|
||
!forceHide && ($scope.model.filters.length > 0 || !dashboardStateManager.getFullScreenMode());
|
||
|
||
const shouldShowNavBarComponent = (forceShow: boolean): boolean =>
|
||
(forceShow || $scope.isVisible) && !dashboardStateManager.getFullScreenMode();
|
||
|
||
const getNavBarProps = () => {
|
||
const isFullScreenMode = dashboardStateManager.getFullScreenMode();
|
||
const screenTitle = dashboardStateManager.getTitle();
|
||
const showTopNavMenu = shouldShowNavBarComponent(forceShowTopNavMenu);
|
||
const showQueryInput = shouldShowNavBarComponent(forceShowQueryInput);
|
||
const showDatePicker = shouldShowNavBarComponent(forceShowDatePicker);
|
||
const showQueryBar = showQueryInput || showDatePicker;
|
||
const showFilterBar = shouldShowFilterBar(forceHideFilterBar);
|
||
const showSearchBar = showQueryBar || showFilterBar;
|
||
|
||
return {
|
||
appName: 'dashboard',
|
||
config: showTopNavMenu ? $scope.topNavMenu : undefined,
|
||
className: isFullScreenMode ? 'kbnTopNavMenu-isFullScreen' : undefined,
|
||
screenTitle,
|
||
showTopNavMenu,
|
||
showSearchBar,
|
||
showQueryBar,
|
||
showQueryInput,
|
||
showDatePicker,
|
||
showFilterBar,
|
||
indexPatterns: $scope.indexPatterns,
|
||
showSaveQuery: $scope.showSaveQuery,
|
||
savedQuery: $scope.savedQuery,
|
||
onSavedQueryIdChange,
|
||
savedQueryId: dashboardStateManager.getSavedQueryId(),
|
||
useDefaultBehaviors: true,
|
||
onQuerySubmit: $scope.handleRefresh,
|
||
};
|
||
};
|
||
const dashboardNavBar = document.getElementById('dashboardChrome');
|
||
const updateNavBar = () => {
|
||
ReactDOM.render(
|
||
<navigation.ui.TopNavMenu
|
||
{...getNavBarProps()}
|
||
{...(isEmbeddedExternally ? {} : { setMenuMountPoint: setHeaderActionMenu })}
|
||
/>,
|
||
dashboardNavBar
|
||
);
|
||
};
|
||
|
||
const unmountNavBar = () => {
|
||
if (dashboardNavBar) {
|
||
ReactDOM.unmountComponentAtNode(dashboardNavBar);
|
||
}
|
||
};
|
||
|
||
$scope.timefilterSubscriptions$ = new Subscription();
|
||
const timeChanges$ = merge(timefilter.getRefreshIntervalUpdate$(), timefilter.getTimeUpdate$());
|
||
$scope.timefilterSubscriptions$.add(
|
||
subscribeWithScope(
|
||
$scope,
|
||
timeChanges$,
|
||
{
|
||
next: () => {
|
||
updateState();
|
||
refreshDashboardContainer();
|
||
},
|
||
},
|
||
(error: AngularHttpError | Error | string) => addFatalError(fatalErrors, error)
|
||
)
|
||
);
|
||
|
||
function updateViewMode(newMode: ViewMode) {
|
||
dashboardStateManager.switchViewMode(newMode);
|
||
}
|
||
|
||
const onChangeViewMode = (newMode: ViewMode) => {
|
||
const isPageRefresh = newMode === dashboardStateManager.getViewMode();
|
||
const isLeavingEditMode = !isPageRefresh && newMode === ViewMode.VIEW;
|
||
const willLoseChanges = isLeavingEditMode && dashboardStateManager.getIsDirty(timefilter);
|
||
|
||
if (!willLoseChanges) {
|
||
updateViewMode(newMode);
|
||
return;
|
||
}
|
||
|
||
function revertChangesAndExitEditMode() {
|
||
dashboardStateManager.resetState();
|
||
// This is only necessary for new dashboards, which will default to Edit mode.
|
||
updateViewMode(ViewMode.VIEW);
|
||
|
||
// We need to do a hard reset of the timepicker. appState will not reload like
|
||
// it does on 'open' because it's been saved to the url and the getAppState.previouslyStored() check on
|
||
// reload will cause it not to sync.
|
||
if (dashboardStateManager.getIsTimeSavedWithDashboard()) {
|
||
dashboardStateManager.syncTimefilterWithDashboardTime(timefilter);
|
||
dashboardStateManager.syncTimefilterWithDashboardRefreshInterval(timefilter);
|
||
}
|
||
|
||
// Angular's $location skips this update because of history updates from syncState which happen simultaneously
|
||
// when calling kbnUrl.change() angular schedules url update and when angular finally starts to process it,
|
||
// the update is considered outdated and angular skips it
|
||
// so have to use implementation of dashboardStateManager.changeDashboardUrl, which workarounds those issues
|
||
dashboardStateManager.changeDashboardUrl(
|
||
dash.id ? createDashboardEditUrl(dash.id) : DashboardConstants.CREATE_NEW_DASHBOARD_URL
|
||
);
|
||
}
|
||
|
||
overlays
|
||
.openConfirm(
|
||
i18n.translate('dashboard.changeViewModeConfirmModal.discardChangesDescription', {
|
||
defaultMessage: `Once you discard your changes, there's no getting them back.`,
|
||
}),
|
||
{
|
||
confirmButtonText: i18n.translate(
|
||
'dashboard.changeViewModeConfirmModal.confirmButtonLabel',
|
||
{ defaultMessage: 'Discard changes' }
|
||
),
|
||
cancelButtonText: i18n.translate(
|
||
'dashboard.changeViewModeConfirmModal.cancelButtonLabel',
|
||
{ defaultMessage: 'Continue editing' }
|
||
),
|
||
defaultFocusedButton: EUI_MODAL_CANCEL_BUTTON,
|
||
title: i18n.translate('dashboard.changeViewModeConfirmModal.discardChangesTitle', {
|
||
defaultMessage: 'Discard changes to dashboard?',
|
||
}),
|
||
}
|
||
)
|
||
.then((isConfirmed) => {
|
||
if (isConfirmed) {
|
||
revertChangesAndExitEditMode();
|
||
}
|
||
});
|
||
|
||
updateNavBar();
|
||
};
|
||
|
||
/**
|
||
* Saves the dashboard.
|
||
*
|
||
* @param {object} [saveOptions={}]
|
||
* @property {boolean} [saveOptions.confirmOverwrite=false] - If true, attempts to create the source so it
|
||
* can confirm an overwrite if a document with the id already exists.
|
||
* @property {boolean} [saveOptions.isTitleDuplicateConfirmed=false] - If true, save allowed with duplicate title
|
||
* @property {func} [saveOptions.onTitleDuplicate] - function called if duplicate title exists.
|
||
* When not provided, confirm modal will be displayed asking user to confirm or cancel save.
|
||
* @return {Promise}
|
||
* @resolved {String} - The id of the doc
|
||
*/
|
||
function save(saveOptions: SavedObjectSaveOpts): Promise<SaveResult> {
|
||
return saveDashboard(angular.toJson, timefilter, dashboardStateManager, saveOptions)
|
||
.then(function (id) {
|
||
if (id) {
|
||
notifications.toasts.addSuccess({
|
||
title: i18n.translate('dashboard.dashboardWasSavedSuccessMessage', {
|
||
defaultMessage: `Dashboard '{dashTitle}' was saved`,
|
||
values: { dashTitle: dash.title },
|
||
}),
|
||
'data-test-subj': 'saveDashboardSuccess',
|
||
});
|
||
|
||
if (dash.id !== $routeParams.id) {
|
||
// Angular's $location skips this update because of history updates from syncState which happen simultaneously
|
||
// when calling kbnUrl.change() angular schedules url update and when angular finally starts to process it,
|
||
// the update is considered outdated and angular skips it
|
||
// so have to use implementation of dashboardStateManager.changeDashboardUrl, which workarounds those issues
|
||
dashboardStateManager.changeDashboardUrl(createDashboardEditUrl(dash.id));
|
||
} else {
|
||
chrome.docTitle.change(dash.lastSavedTitle);
|
||
updateViewMode(ViewMode.VIEW);
|
||
}
|
||
}
|
||
return { id };
|
||
})
|
||
.catch((error) => {
|
||
notifications.toasts.addDanger({
|
||
title: i18n.translate('dashboard.dashboardWasNotSavedDangerMessage', {
|
||
defaultMessage: `Dashboard '{dashTitle}' was not saved. Error: {errorMessage}`,
|
||
values: {
|
||
dashTitle: dash.title,
|
||
errorMessage: error.message,
|
||
},
|
||
}),
|
||
'data-test-subj': 'saveDashboardFailure',
|
||
});
|
||
return { error };
|
||
});
|
||
}
|
||
|
||
$scope.showAddPanel = () => {
|
||
dashboardStateManager.setFullScreenMode(false);
|
||
/*
|
||
* Temp solution for triggering menu click.
|
||
* When de-angularizing this code, please call the underlaying action function
|
||
* directly and not via the top nav object.
|
||
**/
|
||
navActions[TopNavIds.ADD_EXISTING]();
|
||
};
|
||
$scope.enterEditMode = () => {
|
||
dashboardStateManager.setFullScreenMode(false);
|
||
/*
|
||
* Temp solution for triggering menu click.
|
||
* When de-angularizing this code, please call the underlaying action function
|
||
* directly and not via the top nav object.
|
||
**/
|
||
navActions[TopNavIds.ENTER_EDIT_MODE]();
|
||
};
|
||
const navActions: {
|
||
[key: string]: NavAction;
|
||
} = {};
|
||
navActions[TopNavIds.FULL_SCREEN] = () => {
|
||
dashboardStateManager.setFullScreenMode(true);
|
||
updateNavBar();
|
||
};
|
||
navActions[TopNavIds.EXIT_EDIT_MODE] = () => onChangeViewMode(ViewMode.VIEW);
|
||
navActions[TopNavIds.ENTER_EDIT_MODE] = () => onChangeViewMode(ViewMode.EDIT);
|
||
navActions[TopNavIds.SAVE] = () => {
|
||
const currentTitle = dashboardStateManager.getTitle();
|
||
const currentDescription = dashboardStateManager.getDescription();
|
||
const currentTimeRestore = dashboardStateManager.getTimeRestore();
|
||
const onSave = ({
|
||
newTitle,
|
||
newDescription,
|
||
newCopyOnSave,
|
||
newTimeRestore,
|
||
isTitleDuplicateConfirmed,
|
||
onTitleDuplicate,
|
||
}: {
|
||
newTitle: string;
|
||
newDescription: string;
|
||
newCopyOnSave: boolean;
|
||
newTimeRestore: boolean;
|
||
isTitleDuplicateConfirmed: boolean;
|
||
onTitleDuplicate: () => void;
|
||
}) => {
|
||
dashboardStateManager.setTitle(newTitle);
|
||
dashboardStateManager.setDescription(newDescription);
|
||
dashboardStateManager.savedDashboard.copyOnSave = newCopyOnSave;
|
||
dashboardStateManager.setTimeRestore(newTimeRestore);
|
||
const saveOptions = {
|
||
confirmOverwrite: false,
|
||
isTitleDuplicateConfirmed,
|
||
onTitleDuplicate,
|
||
};
|
||
return save(saveOptions).then((response: SaveResult) => {
|
||
// If the save wasn't successful, put the original values back.
|
||
if (!(response as { id: string }).id) {
|
||
dashboardStateManager.setTitle(currentTitle);
|
||
dashboardStateManager.setDescription(currentDescription);
|
||
dashboardStateManager.setTimeRestore(currentTimeRestore);
|
||
}
|
||
return response;
|
||
});
|
||
};
|
||
|
||
const dashboardSaveModal = (
|
||
<DashboardSaveModal
|
||
onSave={onSave}
|
||
onClose={() => {}}
|
||
title={currentTitle}
|
||
description={currentDescription}
|
||
timeRestore={currentTimeRestore}
|
||
showCopyOnSave={dash.id ? true : false}
|
||
/>
|
||
);
|
||
showSaveModal(dashboardSaveModal, i18nStart.Context);
|
||
};
|
||
navActions[TopNavIds.CLONE] = () => {
|
||
const currentTitle = dashboardStateManager.getTitle();
|
||
const onClone = (
|
||
newTitle: string,
|
||
isTitleDuplicateConfirmed: boolean,
|
||
onTitleDuplicate: () => void
|
||
) => {
|
||
dashboardStateManager.savedDashboard.copyOnSave = true;
|
||
dashboardStateManager.setTitle(newTitle);
|
||
const saveOptions = {
|
||
confirmOverwrite: false,
|
||
isTitleDuplicateConfirmed,
|
||
onTitleDuplicate,
|
||
};
|
||
return save(saveOptions).then((response: { id?: string } | { error: Error }) => {
|
||
// If the save wasn't successful, put the original title back.
|
||
if ((response as { error: Error }).error) {
|
||
dashboardStateManager.setTitle(currentTitle);
|
||
}
|
||
updateNavBar();
|
||
return response;
|
||
});
|
||
};
|
||
|
||
showCloneModal(onClone, currentTitle);
|
||
};
|
||
|
||
navActions[TopNavIds.ADD_EXISTING] = () => {
|
||
if (dashboardContainer && !isErrorEmbeddable(dashboardContainer)) {
|
||
openAddPanelFlyout({
|
||
embeddable: dashboardContainer,
|
||
getAllFactories: embeddable.getEmbeddableFactories,
|
||
getFactory: embeddable.getEmbeddableFactory,
|
||
notifications,
|
||
overlays,
|
||
SavedObjectFinder: getSavedObjectFinder(savedObjects, uiSettings),
|
||
});
|
||
}
|
||
};
|
||
|
||
navActions[TopNavIds.VISUALIZE] = async () => {
|
||
const type = 'visualization';
|
||
const factory = embeddable.getEmbeddableFactory(type);
|
||
if (!factory) {
|
||
throw new EmbeddableFactoryNotFoundError(type);
|
||
}
|
||
const explicitInput = await factory.getExplicitInput();
|
||
if (dashboardContainer) {
|
||
await dashboardContainer.addNewEmbeddable(type, explicitInput);
|
||
}
|
||
};
|
||
|
||
navActions[TopNavIds.OPTIONS] = (anchorElement) => {
|
||
showOptionsPopover({
|
||
anchorElement,
|
||
useMargins: dashboardStateManager.getUseMargins(),
|
||
onUseMarginsChange: (isChecked: boolean) => {
|
||
dashboardStateManager.setUseMargins(isChecked);
|
||
},
|
||
hidePanelTitles: dashboardStateManager.getHidePanelTitles(),
|
||
onHidePanelTitlesChange: (isChecked: boolean) => {
|
||
dashboardStateManager.setHidePanelTitles(isChecked);
|
||
},
|
||
});
|
||
};
|
||
|
||
if (share) {
|
||
// the share button is only availabale if "share" plugin contract enabled
|
||
navActions[TopNavIds.SHARE] = (anchorElement) => {
|
||
const EmbedUrlParamExtension = ({
|
||
setParamValue,
|
||
}: {
|
||
setParamValue: (paramUpdate: UrlParamValues) => void;
|
||
}): ReactElement => {
|
||
const [urlParamsSelectedMap, setUrlParamsSelectedMap] = useState<UrlParamsSelectedMap>({
|
||
[UrlParams.SHOW_TOP_MENU]: false,
|
||
[UrlParams.SHOW_QUERY_INPUT]: false,
|
||
[UrlParams.SHOW_TIME_FILTER]: false,
|
||
[UrlParams.SHOW_FILTER_BAR]: true,
|
||
});
|
||
|
||
const checkboxes = [
|
||
{
|
||
id: UrlParams.SHOW_TOP_MENU,
|
||
label: i18n.translate('dashboard.embedUrlParamExtension.topMenu', {
|
||
defaultMessage: 'Top menu',
|
||
}),
|
||
},
|
||
{
|
||
id: UrlParams.SHOW_QUERY_INPUT,
|
||
label: i18n.translate('dashboard.embedUrlParamExtension.query', {
|
||
defaultMessage: 'Query',
|
||
}),
|
||
},
|
||
{
|
||
id: UrlParams.SHOW_TIME_FILTER,
|
||
label: i18n.translate('dashboard.embedUrlParamExtension.timeFilter', {
|
||
defaultMessage: 'Time filter',
|
||
}),
|
||
},
|
||
{
|
||
id: UrlParams.SHOW_FILTER_BAR,
|
||
label: i18n.translate('dashboard.embedUrlParamExtension.filterBar', {
|
||
defaultMessage: 'Filter bar',
|
||
}),
|
||
},
|
||
];
|
||
|
||
const handleChange = (param: string): void => {
|
||
const urlParamsSelectedMapUpdate = {
|
||
...urlParamsSelectedMap,
|
||
[param]: !urlParamsSelectedMap[param as keyof UrlParamsSelectedMap],
|
||
};
|
||
setUrlParamsSelectedMap(urlParamsSelectedMapUpdate);
|
||
|
||
const urlParamValues = {
|
||
[UrlParams.SHOW_TOP_MENU]: urlParamsSelectedMap[UrlParams.SHOW_TOP_MENU],
|
||
[UrlParams.SHOW_QUERY_INPUT]: urlParamsSelectedMap[UrlParams.SHOW_QUERY_INPUT],
|
||
[UrlParams.SHOW_TIME_FILTER]: urlParamsSelectedMap[UrlParams.SHOW_TIME_FILTER],
|
||
[UrlParams.HIDE_FILTER_BAR]: !urlParamsSelectedMap[UrlParams.SHOW_FILTER_BAR],
|
||
[param === UrlParams.SHOW_FILTER_BAR ? UrlParams.HIDE_FILTER_BAR : param]:
|
||
param === UrlParams.SHOW_FILTER_BAR
|
||
? urlParamsSelectedMap[UrlParams.SHOW_FILTER_BAR]
|
||
: !urlParamsSelectedMap[param as keyof UrlParamsSelectedMap],
|
||
};
|
||
setParamValue(urlParamValues);
|
||
};
|
||
|
||
return (
|
||
<EuiCheckboxGroup
|
||
options={checkboxes}
|
||
idToSelectedMap={(urlParamsSelectedMap as unknown) as EuiCheckboxGroupIdToSelectedMap}
|
||
onChange={handleChange}
|
||
legend={{
|
||
children: i18n.translate('dashboard.embedUrlParamExtension.include', {
|
||
defaultMessage: 'Include',
|
||
}),
|
||
}}
|
||
data-test-subj="embedUrlParamExtension"
|
||
/>
|
||
);
|
||
};
|
||
|
||
share.toggleShareContextMenu({
|
||
anchorElement,
|
||
allowEmbed: true,
|
||
allowShortUrl:
|
||
!dashboardConfig.getHideWriteControls() || dashboardCapabilities.createShortUrl,
|
||
shareableUrl: setStateToKbnUrl(
|
||
'_a',
|
||
dashboardStateManager.getAppState(),
|
||
{ useHash: false, storeInHashQuery: true },
|
||
unhashUrl(window.location.href)
|
||
),
|
||
objectId: dash.id,
|
||
objectType: 'dashboard',
|
||
sharingData: {
|
||
title: dash.title,
|
||
},
|
||
isDirty: dashboardStateManager.getIsDirty(),
|
||
embedUrlParamExtensions: [
|
||
{
|
||
paramName: 'embed',
|
||
component: EmbedUrlParamExtension,
|
||
},
|
||
],
|
||
});
|
||
};
|
||
}
|
||
|
||
updateViewMode(dashboardStateManager.getViewMode());
|
||
|
||
const filterChanges = merge(filterManager.getUpdates$(), queryStringManager.getUpdates$()).pipe(
|
||
debounceTime(100)
|
||
);
|
||
|
||
// update root source when filters update
|
||
const updateSubscription = filterChanges.subscribe({
|
||
next: () => {
|
||
$scope.model.filters = filterManager.getFilters();
|
||
$scope.model.query = queryStringManager.getQuery();
|
||
dashboardStateManager.applyFilters($scope.model.query, $scope.model.filters);
|
||
},
|
||
});
|
||
|
||
const visibleSubscription = chrome.getIsVisible$().subscribe((isVisible) => {
|
||
$scope.$evalAsync(() => {
|
||
$scope.isVisible = isVisible;
|
||
updateNavBar();
|
||
});
|
||
});
|
||
|
||
dashboardStateManager.registerChangeListener(() => {
|
||
// view mode could have changed, so trigger top nav update
|
||
$scope.topNavMenu = getTopNavConfig(
|
||
dashboardStateManager.getViewMode(),
|
||
navActions,
|
||
dashboardConfig.getHideWriteControls()
|
||
);
|
||
updateNavBar();
|
||
});
|
||
|
||
$scope.$watch('indexPatterns', () => {
|
||
updateNavBar();
|
||
});
|
||
|
||
$scope.$on('$destroy', () => {
|
||
// we have to unmount nav bar manually to make sure all internal subscriptions are unsubscribed
|
||
unmountNavBar();
|
||
|
||
updateSubscription.unsubscribe();
|
||
stopSyncingQueryServiceStateWithUrl();
|
||
stopSyncingAppFilters();
|
||
visibleSubscription.unsubscribe();
|
||
$scope.timefilterSubscriptions$.unsubscribe();
|
||
|
||
dashboardStateManager.destroy();
|
||
if (inputSubscription) {
|
||
inputSubscription.unsubscribe();
|
||
}
|
||
if (outputSubscription) {
|
||
outputSubscription.unsubscribe();
|
||
}
|
||
if (dashboardContainer) {
|
||
dashboardContainer.destroy();
|
||
}
|
||
searchService.session.clear();
|
||
});
|
||
}
|
||
}
|