[7.x] Kibana app migration: Shim dashboard (#48913) (#51204)

* Kibana app migration: Shim dashboard (#48913)

* fix location of feature catalogue
This commit is contained in:
Joe Reuter 2019-11-20 22:12:17 +01:00 committed by Christiane (Tina) Heiligers
parent b9e2fbf71b
commit 43d8a53cdb
56 changed files with 1906 additions and 1258 deletions

View file

@ -17,7 +17,7 @@
* under the License.
*/
import { AppStateClass } from 'ui/state_management/app_state';
import { AppStateClass } from '../legacy_imports';
/**
* A poor excuse for a mock just to get some basic tests to run in jest without requiring the injector.

View file

@ -1,7 +1,7 @@
.dshAppContainer {
flex: 1;
display: flex;
flex-direction: column;
height: 100%;
}
.dshStartScreen {

View file

@ -0,0 +1,228 @@
/*
* 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 { EuiConfirmModal, EuiIcon } from '@elastic/eui';
import angular, { IModule } from 'angular';
import { IPrivate } from 'ui/private';
import { i18nDirective, i18nFilter, I18nProvider } from '@kbn/i18n/angular';
import {
AppMountContext,
ChromeStart,
LegacyCoreStart,
SavedObjectsClientContract,
UiSettingsClientContract,
} from 'kibana/public';
import { Storage } from '../../../../../plugins/kibana_utils/public';
import {
GlobalStateProvider,
StateManagementConfigProvider,
AppStateProvider,
PrivateProvider,
EventsProvider,
PersistedState,
createTopNavDirective,
createTopNavHelper,
PromiseServiceCreator,
KbnUrlProvider,
RedirectWhenMissingProvider,
confirmModalFactory,
configureAppAngularModule,
} from './legacy_imports';
// @ts-ignore
import { initDashboardApp } from './legacy_app';
import { DataStart } from '../../../data/public';
import { EmbeddablePublicPlugin } from '../../../../../plugins/embeddable/public';
import { NavigationStart } from '../../../navigation/public';
import { DataPublicPluginStart as NpDataStart } from '../../../../../plugins/data/public';
import { SharePluginStart } from '../../../../../plugins/share/public';
export interface RenderDeps {
core: LegacyCoreStart;
indexPatterns: DataStart['indexPatterns']['indexPatterns'];
dataStart: DataStart;
npDataStart: NpDataStart;
navigation: NavigationStart;
savedObjectsClient: SavedObjectsClientContract;
savedObjectRegistry: any;
dashboardConfig: any;
savedDashboards: any;
dashboardCapabilities: any;
uiSettings: UiSettingsClientContract;
chrome: ChromeStart;
addBasePath: (path: string) => string;
savedQueryService: DataStart['search']['services']['savedQueryService'];
embeddables: ReturnType<EmbeddablePublicPlugin['start']>;
localStorage: Storage;
share: SharePluginStart;
}
let angularModuleInstance: IModule | null = null;
export const renderApp = (element: HTMLElement, appBasePath: string, deps: RenderDeps) => {
if (!angularModuleInstance) {
angularModuleInstance = createLocalAngularModule(deps.core, deps.navigation);
// global routing stuff
configureAppAngularModule(angularModuleInstance, deps.core as LegacyCoreStart, true);
// custom routing stuff
initDashboardApp(angularModuleInstance, deps);
}
const $injector = mountDashboardApp(appBasePath, element);
return () => {
$injector.get('$rootScope').$destroy();
};
};
const mainTemplate = (basePath: string) => `<div style="height: 100%">
<base href="${basePath}" />
<div ng-view style="height: 100%;"></div>
</div>
`;
const moduleName = 'app/dashboard';
const thirdPartyAngularDependencies = ['ngSanitize', 'ngRoute', 'react'];
function mountDashboardApp(appBasePath: string, element: HTMLElement) {
const mountpoint = document.createElement('div');
mountpoint.setAttribute('style', 'height: 100%');
// eslint-disable-next-line
mountpoint.innerHTML = mainTemplate(appBasePath);
// bootstrap angular into detached element and attach it later to
// make angular-within-angular possible
const $injector = angular.bootstrap(mountpoint, [moduleName]);
// initialize global state handler
element.appendChild(mountpoint);
return $injector;
}
function createLocalAngularModule(core: AppMountContext['core'], navigation: NavigationStart) {
createLocalI18nModule();
createLocalPrivateModule();
createLocalPromiseModule();
createLocalConfigModule(core);
createLocalKbnUrlModule();
createLocalStateModule();
createLocalPersistedStateModule();
createLocalTopNavModule(navigation);
createLocalConfirmModalModule();
createLocalIconModule();
const dashboardAngularModule = angular.module(moduleName, [
...thirdPartyAngularDependencies,
'app/dashboard/Config',
'app/dashboard/I18n',
'app/dashboard/Private',
'app/dashboard/PersistedState',
'app/dashboard/TopNav',
'app/dashboard/State',
'app/dashboard/ConfirmModal',
'app/dashboard/icon',
]);
return dashboardAngularModule;
}
function createLocalIconModule() {
angular
.module('app/dashboard/icon', ['react'])
.directive('icon', reactDirective => reactDirective(EuiIcon));
}
function createLocalConfirmModalModule() {
angular
.module('app/dashboard/ConfirmModal', ['react'])
.factory('confirmModal', confirmModalFactory)
.directive('confirmModal', reactDirective => reactDirective(EuiConfirmModal));
}
function createLocalStateModule() {
angular
.module('app/dashboard/State', [
'app/dashboard/Private',
'app/dashboard/Config',
'app/dashboard/KbnUrl',
'app/dashboard/Promise',
'app/dashboard/PersistedState',
])
.factory('AppState', function(Private: any) {
return Private(AppStateProvider);
})
.service('getAppState', function(Private: any) {
return Private(AppStateProvider).getAppState;
})
.service('globalState', function(Private: any) {
return Private(GlobalStateProvider);
});
}
function createLocalPersistedStateModule() {
angular
.module('app/dashboard/PersistedState', ['app/dashboard/Private', 'app/dashboard/Promise'])
.factory('PersistedState', (Private: IPrivate) => {
const Events = Private(EventsProvider);
return class AngularPersistedState extends PersistedState {
constructor(value: any, path: any) {
super(value, path, Events);
}
};
});
}
function createLocalKbnUrlModule() {
angular
.module('app/dashboard/KbnUrl', ['app/dashboard/Private', 'ngRoute'])
.service('kbnUrl', (Private: IPrivate) => Private(KbnUrlProvider))
.service('redirectWhenMissing', (Private: IPrivate) => Private(RedirectWhenMissingProvider));
}
function createLocalConfigModule(core: AppMountContext['core']) {
angular
.module('app/dashboard/Config', ['app/dashboard/Private'])
.provider('stateManagementConfig', StateManagementConfigProvider)
.provider('config', () => {
return {
$get: () => ({
get: core.uiSettings.get.bind(core.uiSettings),
}),
};
});
}
function createLocalPromiseModule() {
angular.module('app/dashboard/Promise', []).service('Promise', PromiseServiceCreator);
}
function createLocalPrivateModule() {
angular.module('app/dashboard/Private', []).provider('Private', PrivateProvider);
}
function createLocalTopNavModule(navigation: NavigationStart) {
angular
.module('app/dashboard/TopNav', ['react'])
.directive('kbnTopNav', createTopNavDirective)
.directive('kbnTopNavHelper', createTopNavHelper(navigation.ui));
}
function createLocalI18nModule() {
angular
.module('app/dashboard/I18n', [])
.provider('i18n', I18nProvider)
.filter('i18n', i18nFilter)
.directive('i18nId', i18nDirective);
}

View file

@ -4,11 +4,11 @@
>
<!-- Local nav. -->
<kbn-top-nav
ng-show="chrome.getVisible()"
ng-show="isVisible"
app-name="'dashboard'"
config="topNavMenu"
show-search-bar="chrome.getVisible()"
show-search-bar="isVisible"
show-filter-bar="showFilterBar()"
show-save-query="showSaveQuery"
@ -34,13 +34,21 @@
The top nav is hidden in embed mode but the filter bar must still be present so
we show the filter bar on its own here if the chrome is not visible.
-->
<filter-bar
ng-if="showFilterBar() && !chrome.getVisible()"
<kbn-top-nav
ng-if="showFilterBar() && !isVisible"
class-name="'globalFilterGroup__filterBar'"
app-name="'dashboard'"
show-search-bar="true"
show-filter-bar="true"
show-save-query="false"
show-date-picker="false"
filters="model.filters"
on-filters-updated="onFiltersUpdated"
index-patterns="indexPatterns"
></filter-bar>
on-filters-updated="onFiltersUpdated"
>
</kbn-top-nav>
<div ng-show="getShouldShowEditHelp() || getShouldShowViewHelp()" class="dshStartScreen">
<div class="euiPanel euiPanel--paddingLarge euiPageContent euiPageContent--horizontalCenter">

View file

@ -17,26 +17,16 @@
* under the License.
*/
import _ from 'lodash';
// @ts-ignore
import { uiModules } from 'ui/modules';
import { IInjector } from 'ui/chrome';
// @ts-ignore
import * as filterActions from 'plugins/kibana/discover/doc_table/actions/filter';
import { StaticIndexPattern, SavedQuery } from 'plugins/data';
import moment from 'moment';
import { Subscription } from 'rxjs';
import {
AppStateClass as TAppStateClass,
AppState as TAppState,
} from 'ui/state_management/app_state';
import { KbnUrl } from 'ui/url/kbn_url';
import { IndexPattern } from 'ui/index_patterns';
import { IPrivate } from 'ui/private';
import { StaticIndexPattern, SavedQuery } from 'plugins/data';
import moment from 'moment';
import { Subscription } from 'rxjs';
IInjector,
KbnUrl,
} from './legacy_imports';
import { ViewMode } from '../../../embeddable_api/public/np_ready/public';
import { SavedObjectDashboard } from './saved_dashboard/saved_dashboard';
@ -44,6 +34,7 @@ import { DashboardAppState, SavedDashboardPanel, ConfirmModalFn } from './types'
import { TimeRange, Query, esFilters } from '../../../../../../src/plugins/data/public';
import { DashboardAppController } from './dashboard_app_controller';
import { RenderDeps } from './application';
export interface DashboardAppScope extends ng.IScope {
dash: SavedObjectDashboard;
@ -90,54 +81,40 @@ export interface DashboardAppScope extends ng.IScope {
kbnTopNav: any;
enterEditMode: () => void;
timefilterSubscriptions$: Subscription;
isVisible: boolean;
}
const app = uiModules.get('app/dashboard', ['elasticsearch', 'ngRoute', 'react', 'kibana/config']);
export function initDashboardAppDirective(app: any, deps: RenderDeps) {
app.directive('dashboardApp', function($injector: IInjector) {
const AppState = $injector.get<TAppStateClass<DashboardAppState>>('AppState');
const kbnUrl = $injector.get<KbnUrl>('kbnUrl');
const confirmModal = $injector.get<ConfirmModalFn>('confirmModal');
const config = deps.uiSettings;
app.directive('dashboardApp', function($injector: IInjector) {
const AppState = $injector.get<TAppStateClass<DashboardAppState>>('AppState');
const kbnUrl = $injector.get<KbnUrl>('kbnUrl');
const confirmModal = $injector.get<ConfirmModalFn>('confirmModal');
const config = $injector.get('config');
const Private = $injector.get<IPrivate>('Private');
const indexPatterns = $injector.get<{
getDefault: () => Promise<IndexPattern>;
}>('indexPatterns');
return {
restrict: 'E',
controllerAs: 'dashboardApp',
controller: (
$scope: DashboardAppScope,
$route: any,
$routeParams: {
id?: string;
},
getAppState: {
previouslyStored: () => TAppState | undefined;
},
dashboardConfig: {
getHideWriteControls: () => boolean;
},
localStorage: {
get: (prop: string) => unknown;
}
) =>
new DashboardAppController({
$route,
$scope,
$routeParams,
getAppState,
dashboardConfig,
localStorage,
Private,
kbnUrl,
AppStateClass: AppState,
indexPatterns,
config,
confirmModal,
}),
};
});
return {
restrict: 'E',
controllerAs: 'dashboardApp',
controller: (
$scope: DashboardAppScope,
$route: any,
$routeParams: {
id?: string;
},
getAppState: any,
globalState: any
) =>
new DashboardAppController({
$route,
$scope,
$routeParams,
getAppState,
globalState,
kbnUrl,
AppStateClass: AppState,
config,
confirmModal,
...deps,
}),
};
});
}

View file

@ -23,41 +23,23 @@ import React from 'react';
import angular from 'angular';
import { uniq } from 'lodash';
import chrome from 'ui/chrome';
import { subscribeWithScope } from 'ui/utils/subscribe_with_scope';
import { toastNotifications } from 'ui/notify';
// @ts-ignore
import { ConfirmationButtonTypes } from 'ui/modals/confirm_modal';
import { FilterBarQueryFilterProvider } from 'ui/filter_manager/query_filter';
import { docTitle } from 'ui/doc_title/doc_title';
import { showSaveModal, SaveResult } from 'ui/saved_objects/show_saved_object_save_modal';
import { migrateLegacyQuery } from 'ui/utils/migrate_legacy_query';
import { timefilter } from 'ui/timefilter';
import { getUnhashableStatesProvider } from 'ui/state_management/state_hashing/get_unhashable_states_provider';
import { Subscription } from 'rxjs';
import {
subscribeWithScope,
ConfirmationButtonTypes,
showSaveModal,
SaveResult,
migrateLegacyQuery,
State,
AppStateClass as TAppStateClass,
AppState as TAppState,
} from 'ui/state_management/app_state';
import { KbnUrl } from 'ui/url/kbn_url';
import { IndexPattern } from 'ui/index_patterns';
import { IPrivate } from 'ui/private';
import { SavedQuery } from 'src/legacy/core_plugins/data/public';
import { SaveOptions } from 'ui/saved_objects/saved_object';
import { capabilities } from 'ui/capabilities';
import { Subscription } from 'rxjs';
import { npStart } from 'ui/new_platform';
import { unhashUrl } from 'ui/state_management/state_hashing';
import { SavedObjectFinder } from 'ui/saved_objects/components/saved_object_finder';
KbnUrl,
SaveOptions,
SavedObjectFinder,
unhashUrl,
} from './legacy_imports';
import { FilterStateManager, IndexPattern, SavedQuery } from '../../../data/public';
import { Query } from '../../../../../plugins/data/public';
import { start as data } from '../../../data/public/legacy';
import {
DashboardContainer,
@ -72,7 +54,6 @@ import {
ViewMode,
openAddPanelFlyout,
} from '../../../embeddable_api/public/np_ready/public';
import { start } from '../../../embeddable_api/public/np_ready/public/legacy';
import { DashboardAppState, NavAction, ConfirmModalFn, SavedDashboardPanel } from './types';
import { showOptionsPopover } from './top_nav/show_options_popover';
@ -87,8 +68,23 @@ import { getDashboardTitle } from './dashboard_strings';
import { DashboardAppScope } from './dashboard_app';
import { VISUALIZE_EMBEDDABLE_TYPE } from '../visualize/embeddable';
import { convertSavedDashboardPanelToPanelState } from './lib/embeddable_saved_object_converters';
import { RenderDeps } from './application';
const { savedQueryService } = data.search.services;
export interface DashboardAppControllerDependencies extends RenderDeps {
$scope: DashboardAppScope;
$route: any;
$routeParams: any;
getAppState: any;
globalState: State;
indexPatterns: {
getDefault: () => Promise<IndexPattern>;
};
dashboardConfig: any;
kbnUrl: KbnUrl;
AppStateClass: TAppStateClass<DashboardAppState>;
config: any;
confirmModal: ConfirmModalFn;
}
export class DashboardAppController {
// Part of the exposed plugin API - do not remove without careful consideration.
@ -101,58 +97,55 @@ export class DashboardAppController {
$route,
$routeParams,
getAppState,
globalState,
dashboardConfig,
localStorage,
Private,
kbnUrl,
AppStateClass,
indexPatterns,
config,
confirmModal,
}: {
$scope: DashboardAppScope;
$route: any;
$routeParams: any;
getAppState: {
previouslyStored: () => TAppState | undefined;
};
indexPatterns: {
getDefault: () => Promise<IndexPattern>;
};
dashboardConfig: any;
localStorage: {
get: (prop: string) => unknown;
};
Private: IPrivate;
kbnUrl: KbnUrl;
AppStateClass: TAppStateClass<DashboardAppState>;
config: any;
confirmModal: ConfirmModalFn;
}) {
const queryFilter = Private(FilterBarQueryFilterProvider);
const getUnhashableStates = Private(getUnhashableStatesProvider);
savedQueryService,
embeddables,
share,
dashboardCapabilities,
npDataStart: {
query: {
filterManager,
timefilter: { timefilter },
},
},
core: { notifications, overlays, chrome, injectedMetadata },
}: DashboardAppControllerDependencies) {
new FilterStateManager(globalState, getAppState, filterManager);
const queryFilter = filterManager;
function getUnhashableStates(): State[] {
return [getAppState(), globalState].filter(Boolean);
}
let lastReloadRequestTime = 0;
const dash = ($scope.dash = $route.current.locals.dash);
if (dash.id) {
docTitle.change(dash.title);
chrome.docTitle.change(dash.title);
}
const dashboardStateManager = new DashboardStateManager({
savedDashboard: dash,
AppStateClass,
hideWriteControls: dashboardConfig.getHideWriteControls(),
kibanaVersion: injectedMetadata.getKibanaVersion(),
});
$scope.appState = dashboardStateManager.getAppState();
// The 'previouslyStored' check is so we only update the time filter on dashboard open, not during
// The hash check is so we only update the time filter on dashboard open, not during
// normal cross app navigation.
if (dashboardStateManager.getIsTimeSavedWithDashboard() && !getAppState.previouslyStored()) {
if (dashboardStateManager.getIsTimeSavedWithDashboard() && !globalState.$inheritedGlobalState) {
dashboardStateManager.syncTimefilterWithDashboard(timefilter);
}
$scope.showSaveQuery = capabilities.get().dashboard.saveQuery as boolean;
$scope.showSaveQuery = dashboardCapabilities.saveQuery as boolean;
const updateIndexPatterns = (container?: DashboardContainer) => {
if (!container || isErrorEmbeddable(container)) {
@ -187,10 +180,7 @@ export class DashboardAppController {
[key: string]: DashboardPanelState;
} = {};
dashboardStateManager.getPanels().forEach((panel: SavedDashboardPanel) => {
embeddablesMap[panel.panelIndex] = convertSavedDashboardPanelToPanelState(
panel,
dashboardStateManager.getUseMargins()
);
embeddablesMap[panel.panelIndex] = convertSavedDashboardPanelToPanelState(panel);
});
let expandedPanelId;
if (dashboardContainer && !isErrorEmbeddable(dashboardContainer)) {
@ -239,7 +229,7 @@ export class DashboardAppController {
let outputSubscription: Subscription | undefined;
const dashboardDom = document.getElementById('dashboardViewport');
const dashboardFactory = start.getEmbeddableFactory(
const dashboardFactory = embeddables.getEmbeddableFactory(
DASHBOARD_CONTAINER_TYPE
) as DashboardContainerFactory;
dashboardFactory
@ -334,7 +324,7 @@ export class DashboardAppController {
// Push breadcrumbs to new header navigation
const updateBreadcrumbs = () => {
chrome.breadcrumbs.set([
chrome.setBreadcrumbs([
{
text: i18n.translate('kbn.dashboard.dashboardAppBreadcrumbsTitle', {
defaultMessage: 'Dashboard',
@ -495,7 +485,7 @@ export class DashboardAppController {
});
$scope.$watch(
() => capabilities.get().dashboard.saveQuery,
() => dashboardCapabilities.saveQuery,
newCapability => {
$scope.showSaveQuery = newCapability as boolean;
}
@ -595,7 +585,7 @@ export class DashboardAppController {
return saveDashboard(angular.toJson, timefilter, dashboardStateManager, saveOptions)
.then(function(id) {
if (id) {
toastNotifications.addSuccess({
notifications.toasts.addSuccess({
title: i18n.translate('kbn.dashboard.dashboardWasSavedSuccessMessage', {
defaultMessage: `Dashboard '{dashTitle}' was saved`,
values: { dashTitle: dash.title },
@ -606,14 +596,14 @@ export class DashboardAppController {
if (dash.id !== $routeParams.id) {
kbnUrl.change(createDashboardEditUrl(dash.id));
} else {
docTitle.change(dash.lastSavedTitle);
chrome.docTitle.change(dash.lastSavedTitle);
updateViewMode(ViewMode.VIEW);
}
}
return { id };
})
.catch(error => {
toastNotifications.addDanger({
notifications.toasts.addDanger({
title: i18n.translate('kbn.dashboard.dashboardWasNotSavedDangerMessage', {
defaultMessage: `Dashboard '{dashTitle}' was not saved. Error: {errorMessage}`,
values: {
@ -734,10 +724,10 @@ export class DashboardAppController {
if (dashboardContainer && !isErrorEmbeddable(dashboardContainer)) {
openAddPanelFlyout({
embeddable: dashboardContainer,
getAllFactories: start.getEmbeddableFactories,
getFactory: start.getEmbeddableFactory,
notifications: npStart.core.notifications,
overlays: npStart.core.overlays,
getAllFactories: embeddables.getEmbeddableFactories,
getFactory: embeddables.getEmbeddableFactory,
notifications,
overlays,
SavedObjectFinder,
});
}
@ -757,7 +747,7 @@ export class DashboardAppController {
});
};
navActions[TopNavIds.SHARE] = anchorElement => {
npStart.plugins.share.toggleShareContextMenu({
share.toggleShareContextMenu({
anchorElement,
allowEmbed: true,
allowShortUrl: !dashboardConfig.getHideWriteControls(),
@ -784,8 +774,15 @@ export class DashboardAppController {
},
});
const visibleSubscription = chrome.getIsVisible$().subscribe(isVisible => {
$scope.$evalAsync(() => {
$scope.isVisible = isVisible;
});
});
$scope.$on('$destroy', () => {
updateSubscription.unsubscribe();
visibleSubscription.unsubscribe();
$scope.timefilterSubscriptions$.unsubscribe();
dashboardStateManager.destroy();

View file

@ -21,11 +21,14 @@ import './np_core.test.mocks';
import { DashboardStateManager } from './dashboard_state_manager';
import { getAppStateMock, getSavedDashboardMock } from './__tests__';
import { AppStateClass } from 'ui/state_management/app_state';
import { AppStateClass } from './legacy_imports';
import { DashboardAppState } from './types';
import { TimeRange, TimefilterContract } from 'src/plugins/data/public';
import { TimeRange, TimefilterContract, InputTimeRange } from 'src/plugins/data/public';
import { ViewMode } from 'src/plugins/embeddable/public';
import { InputTimeRange } from 'ui/timefilter';
jest.mock('ui/state_management/state', () => ({
State: {},
}));
jest.mock('ui/state_management/state', () => ({
State: {},
@ -50,6 +53,7 @@ describe('DashboardState', function() {
savedDashboard,
AppStateClass: getAppStateMock() as AppStateClass<DashboardAppState>,
hideWriteControls: false,
kibanaVersion: '7.0.0',
});
}

View file

@ -20,15 +20,21 @@
import { i18n } from '@kbn/i18n';
import _ from 'lodash';
import { stateMonitorFactory, StateMonitor } from 'ui/state_management/state_monitor_factory';
import { Timefilter } from 'ui/timefilter';
import { AppStateClass as TAppStateClass } from 'ui/state_management/app_state';
import { migrateLegacyQuery } from 'ui/utils/migrate_legacy_query';
import { Moment } from 'moment';
import { DashboardContainer } from 'src/legacy/core_plugins/dashboard_embeddable_container/public/np_ready/public';
import { ViewMode } from '../../../../../../src/plugins/embeddable/public';
import { Query, esFilters } from '../../../../../../src/plugins/data/public';
import {
stateMonitorFactory,
StateMonitor,
AppStateClass as TAppStateClass,
migrateLegacyQuery,
} from './legacy_imports';
import {
Query,
esFilters,
TimefilterContract as Timefilter,
} from '../../../../../../src/plugins/data/public';
import { getAppStateDefaults, migrateAppState } from './lib';
import { convertPanelStateToSavedDashboardPanel } from './lib/embeddable_saved_object_converters';
@ -54,6 +60,7 @@ export class DashboardStateManager {
};
private stateDefaults: DashboardAppStateDefaults;
private hideWriteControls: boolean;
private kibanaVersion: string;
public isDirty: boolean;
private changeListeners: Array<(status: { dirty: boolean }) => void>;
private stateMonitor: StateMonitor<DashboardAppStateDefaults>;
@ -68,11 +75,14 @@ export class DashboardStateManager {
savedDashboard,
AppStateClass,
hideWriteControls,
kibanaVersion,
}: {
savedDashboard: SavedObjectDashboard;
AppStateClass: TAppStateClass<DashboardAppState>;
hideWriteControls: boolean;
kibanaVersion: string;
}) {
this.kibanaVersion = kibanaVersion;
this.savedDashboard = savedDashboard;
this.hideWriteControls = hideWriteControls;
@ -84,7 +94,7 @@ export class DashboardStateManager {
// 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 Dashboard, but also any old state in the
// url.
migrateAppState(this.appState);
migrateAppState(this.appState, kibanaVersion);
this.isDirty = false;
@ -146,7 +156,8 @@ export class DashboardStateManager {
}
convertedPanelStateMap[panelState.explicitInput.id] = convertPanelStateToSavedDashboardPanel(
panelState
panelState,
this.kibanaVersion
);
if (

View file

@ -0,0 +1,67 @@
/*
* 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 { State } from './legacy_imports';
import { DataPublicPluginStart as NpDataStart } from '../../../../../plugins/data/public';
/**
* Helper function to sync the global state with the various state providers
* when a local angular application mounts. There are three different ways
* global state can be passed into the application:
* * parameter in the URL hash - e.g. shared link
* * in-memory state in the data plugin exports (timefilter and filterManager) - e.g. default values
*
* This function looks up the three sources (earlier in the list means it takes precedence),
* puts it into the globalState object and syncs it with the url.
*
* Currently the legacy chrome takes care of restoring the global state when navigating from
* one app to another - to migrate away from that it will become necessary to also write the current
* state to local storage
*/
export function syncOnMount(
globalState: State,
{
query: {
filterManager,
timefilter: { timefilter },
},
}: NpDataStart
) {
// pull in global state information from the URL
globalState.fetch();
// remember whether there were info in the URL
const hasGlobalURLState = Boolean(Object.keys(globalState.toObject()).length);
// sync kibana platform state with the angular global state
if (!globalState.time) {
globalState.time = timefilter.getTime();
}
if (!globalState.refreshInterval) {
globalState.refreshInterval = timefilter.getRefreshInterval();
}
if (!globalState.filters && filterManager.getGlobalFilters().length > 0) {
globalState.filters = filterManager.getGlobalFilters();
}
// only inject cross app global state if there is none in the url itself (that takes precedence)
if (hasGlobalURLState) {
// set flag the global state is set from the URL
globalState.$inheritedGlobalState = true;
}
globalState.save();
}

View file

@ -17,26 +17,30 @@
* under the License.
*/
import React, { Fragment, PureComponent } from 'react';
import React, { PureComponent } from 'react';
import { EuiButton, EuiHorizontalRule, EuiSpacer } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import { ELASTIC_WEBSITE_URL, DOC_LINK_VERSION } from 'ui/documentation_links';
import { FormattedMessage, I18nProvider } from '@kbn/i18n/react';
export class HelpMenu extends PureComponent {
render() {
return (
<Fragment>
<EuiHorizontalRule margin="none" />
<EuiSpacer />
<EuiButton
fill
iconType="popout"
href={`${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/dashboard.html`}
target="_blank"
>
<FormattedMessage id="kbn.dashboard.helpMenu.docLabel" defaultMessage="Dashboard documentation" />
</EuiButton>
</Fragment>
<I18nProvider>
<>
<EuiHorizontalRule margin="none" />
<EuiSpacer />
<EuiButton
fill
iconType="popout"
href={`${this.props.docLinks.ELASTIC_WEBSITE_URL}guide/en/kibana/${this.props.docLinks.DOC_LINK_VERSION}/dashboard.html`}
target="_blank"
>
<FormattedMessage
id="kbn.dashboard.helpMenu.docLabel"
defaultMessage="Dashboard documentation"
/>
</EuiButton>
</>
</I18nProvider>
);
}
}

View file

@ -21,9 +21,9 @@ import React from 'react';
import { render, unmountComponentAtNode } from 'react-dom';
import { HelpMenu } from './help_menu';
export function addHelpMenuToAppChrome(chrome) {
chrome.helpExtension.set(domElement => {
render(<HelpMenu/>, domElement);
export function addHelpMenuToAppChrome(chrome, docLinks) {
chrome.setHelpExtension(domElement => {
render(<HelpMenu docLinks={docLinks} />, domElement);
return () => {
unmountComponentAtNode(domElement);
};

View file

@ -1,207 +0,0 @@
/*
* 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 './dashboard_app';
import { i18n } from '@kbn/i18n';
import './saved_dashboard/saved_dashboards';
import './dashboard_config';
import uiRoutes from 'ui/routes';
import chrome from 'ui/chrome';
import { wrapInI18nContext } from 'ui/i18n';
import { toastNotifications } from 'ui/notify';
import dashboardTemplate from './dashboard_app.html';
import dashboardListingTemplate from './listing/dashboard_listing_ng_wrapper.html';
import { DashboardConstants, createDashboardEditUrl } from './dashboard_constants';
import { InvalidJSONProperty, SavedObjectNotFound } from '../../../../../plugins/kibana_utils/public';
import { FeatureCatalogueRegistryProvider, FeatureCatalogueCategory } from 'ui/registry/feature_catalogue';
import { SavedObjectsClientProvider } from 'ui/saved_objects';
import { SavedObjectRegistryProvider } from 'ui/saved_objects/saved_object_registry';
import { DashboardListing, EMPTY_FILTER } from './listing/dashboard_listing';
import { uiModules } from 'ui/modules';
import 'ui/capabilities/route_setup';
import { addHelpMenuToAppChrome } from './help_menu/help_menu_util';
import { npStart } from 'ui/new_platform';
// load directives
import '../../../data/public';
const app = uiModules.get('app/dashboard', [
'ngRoute',
'react',
]);
app.directive('dashboardListing', function (reactDirective) {
return reactDirective(wrapInI18nContext(DashboardListing));
});
function createNewDashboardCtrl($scope) {
$scope.visitVisualizeAppLinkText = i18n.translate('kbn.dashboard.visitVisualizeAppLinkText', {
defaultMessage: 'visit the Visualize app',
});
addHelpMenuToAppChrome(chrome);
}
uiRoutes
.defaults(/dashboard/, {
requireDefaultIndex: true,
requireUICapability: 'dashboard.show',
badge: uiCapabilities => {
if (uiCapabilities.dashboard.showWriteControls) {
return undefined;
}
return {
text: i18n.translate('kbn.dashboard.badge.readOnly.text', {
defaultMessage: 'Read only',
}),
tooltip: i18n.translate('kbn.dashboard.badge.readOnly.tooltip', {
defaultMessage: 'Unable to save dashboards',
}),
iconType: 'glasses'
};
}
})
.when(DashboardConstants.LANDING_PAGE_PATH, {
template: dashboardListingTemplate,
controller($injector, $location, $scope, Private, config) {
const services = Private(SavedObjectRegistryProvider).byLoaderPropertiesName;
const kbnUrl = $injector.get('kbnUrl');
const dashboardConfig = $injector.get('dashboardConfig');
$scope.listingLimit = config.get('savedObjects:listingLimit');
$scope.create = () => {
kbnUrl.redirect(DashboardConstants.CREATE_NEW_DASHBOARD_URL);
};
$scope.find = (search) => {
return services.dashboards.find(search, $scope.listingLimit);
};
$scope.editItem = ({ id }) => {
kbnUrl.redirect(`${createDashboardEditUrl(id)}?_a=(viewMode:edit)`);
};
$scope.getViewUrl = ({ id }) => {
return chrome.addBasePath(`#${createDashboardEditUrl(id)}`);
};
$scope.delete = (dashboards) => {
return services.dashboards.delete(dashboards.map(d => d.id));
};
$scope.hideWriteControls = dashboardConfig.getHideWriteControls();
$scope.initialFilter = ($location.search()).filter || EMPTY_FILTER;
chrome.breadcrumbs.set([{
text: i18n.translate('kbn.dashboard.dashboardBreadcrumbsTitle', {
defaultMessage: 'Dashboards',
}),
}]);
addHelpMenuToAppChrome(chrome);
},
resolve: {
dash: function ($route, Private, redirectWhenMissing, kbnUrl) {
const savedObjectsClient = Private(SavedObjectsClientProvider);
const title = $route.current.params.title;
if (title) {
return savedObjectsClient.find({
search: `"${title}"`,
search_fields: 'title',
type: 'dashboard',
}).then(results => {
// The search isn't an exact match, lets see if we can find a single exact match to use
const matchingDashboards = results.savedObjects.filter(
dashboard => dashboard.attributes.title.toLowerCase() === title.toLowerCase());
if (matchingDashboards.length === 1) {
kbnUrl.redirect(createDashboardEditUrl(matchingDashboards[0].id));
} else {
kbnUrl.redirect(`${DashboardConstants.LANDING_PAGE_PATH}?filter="${title}"`);
}
throw uiRoutes.WAIT_FOR_URL_CHANGE_TOKEN;
}).catch(redirectWhenMissing({
'dashboard': DashboardConstants.LANDING_PAGE_PATH
}));
}
}
}
})
.when(DashboardConstants.CREATE_NEW_DASHBOARD_URL, {
template: dashboardTemplate,
controller: createNewDashboardCtrl,
requireUICapability: 'dashboard.createNew',
resolve: {
dash: function (savedDashboards, redirectWhenMissing) {
return savedDashboards.get()
.catch(redirectWhenMissing({
'dashboard': DashboardConstants.LANDING_PAGE_PATH
}));
}
}
})
.when(createDashboardEditUrl(':id'), {
template: dashboardTemplate,
controller: createNewDashboardCtrl,
resolve: {
dash: function (savedDashboards, $route, redirectWhenMissing, kbnUrl, AppState) {
const id = $route.current.params.id;
return savedDashboards.get(id)
.then((savedDashboard) => {
npStart.core.chrome.recentlyAccessed.add(savedDashboard.getFullPath(), savedDashboard.title, id);
return savedDashboard;
})
.catch((error) => {
// A corrupt dashboard was detected (e.g. with invalid JSON properties)
if (error instanceof InvalidJSONProperty) {
toastNotifications.addDanger(error.message);
kbnUrl.redirect(DashboardConstants.LANDING_PAGE_PATH);
return;
}
// 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 && id === 'create') {
// Note "new AppState" is necessary so the state in the url is preserved through the redirect.
kbnUrl.redirect(DashboardConstants.CREATE_NEW_DASHBOARD_URL, {}, new AppState());
toastNotifications.addWarning(i18n.translate('kbn.dashboard.urlWasRemovedInSixZeroWarningMessage',
{ defaultMessage: 'The url "dashboard/create" was removed in 6.0. Please update your bookmarks.' }
));
} else {
throw error;
}
})
.catch(redirectWhenMissing({
'dashboard': DashboardConstants.LANDING_PAGE_PATH
}));
}
}
});
FeatureCatalogueRegistryProvider.register(() => {
return {
id: 'dashboard',
title: i18n.translate('kbn.dashboard.featureCatalogue.dashboardTitle', {
defaultMessage: 'Dashboard',
}),
description: i18n.translate('kbn.dashboard.featureCatalogue.dashboardDescription', {
defaultMessage: 'Display and share a collection of visualizations and saved searches.',
}),
icon: 'dashboardApp',
path: `/app/kibana#${DashboardConstants.LANDING_PAGE_PATH}`,
showOnHomePage: true,
category: FeatureCatalogueCategory.DATA
};
});

View file

@ -0,0 +1,69 @@
/*
* 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 {
npSetup,
npStart,
SavedObjectRegistryProvider,
legacyChrome,
IPrivate,
} from './legacy_imports';
import { DashboardPlugin, LegacyAngularInjectedDependencies } from './plugin';
import { start as data } from '../../../data/public/legacy';
import { localApplicationService } from '../local_application_service';
import { start as embeddables } from '../../../embeddable_api/public/np_ready/public/legacy';
import { start as navigation } from '../../../navigation/public/legacy';
import './saved_dashboard/saved_dashboards';
import './dashboard_config';
/**
* Get dependencies relying on the global angular context.
* They also have to get resolved together with the legacy imports above
*/
async function getAngularDependencies(): Promise<LegacyAngularInjectedDependencies> {
const injector = await legacyChrome.dangerouslyGetActiveInjector();
const Private = injector.get<IPrivate>('Private');
const savedObjectRegistry = Private(SavedObjectRegistryProvider);
return {
dashboardConfig: injector.get('dashboardConfig'),
savedObjectRegistry,
savedDashboards: injector.get('savedDashboards'),
};
}
(async () => {
const instance = new DashboardPlugin();
instance.setup(npSetup.core, {
...npSetup.plugins,
__LEGACY: {
localApplicationService,
getAngularDependencies,
},
});
instance.start(npStart.core, {
...npStart.plugins,
data,
npData: npStart.plugins.data,
embeddables,
navigation,
});
})();

View file

@ -0,0 +1,224 @@
/*
* 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 { i18n } from '@kbn/i18n';
import dashboardTemplate from './dashboard_app.html';
import dashboardListingTemplate from './listing/dashboard_listing_ng_wrapper.html';
import { ensureDefaultIndexPattern } from './legacy_imports';
import { initDashboardAppDirective } from './dashboard_app';
import { DashboardConstants, createDashboardEditUrl } from './dashboard_constants';
import {
InvalidJSONProperty,
SavedObjectNotFound,
} from '../../../../../plugins/kibana_utils/public';
import { DashboardListing, EMPTY_FILTER } from './listing/dashboard_listing';
import { addHelpMenuToAppChrome } from './help_menu/help_menu_util';
import { registerTimefilterWithGlobalStateFactory } from '../../../../ui/public/timefilter/setup_router';
import { syncOnMount } from './global_state_sync';
export function initDashboardApp(app, deps) {
initDashboardAppDirective(app, deps);
app.directive('dashboardListing', function (reactDirective) {
return reactDirective(DashboardListing);
});
function createNewDashboardCtrl($scope) {
$scope.visitVisualizeAppLinkText = i18n.translate('kbn.dashboard.visitVisualizeAppLinkText', {
defaultMessage: 'visit the Visualize app',
});
addHelpMenuToAppChrome(deps.chrome, deps.core.docLinks);
}
app.run(globalState => {
syncOnMount(globalState, deps.npDataStart);
});
app.run((globalState, $rootScope) => {
registerTimefilterWithGlobalStateFactory(
deps.npDataStart.query.timefilter.timefilter,
globalState,
$rootScope
);
});
app.config(function ($routeProvider) {
const defaults = {
reloadOnSearch: false,
requireUICapability: 'dashboard.show',
badge: () => {
if (deps.dashboardCapabilities.showWriteControls) {
return undefined;
}
return {
text: i18n.translate('kbn.dashboard.badge.readOnly.text', {
defaultMessage: 'Read only',
}),
tooltip: i18n.translate('kbn.dashboard.badge.readOnly.tooltip', {
defaultMessage: 'Unable to save dashboards',
}),
iconType: 'glasses',
};
},
};
$routeProvider
.when(DashboardConstants.LANDING_PAGE_PATH, {
...defaults,
template: dashboardListingTemplate,
controller($injector, $location, $scope) {
const services = deps.savedObjectRegistry.byLoaderPropertiesName;
const kbnUrl = $injector.get('kbnUrl');
const dashboardConfig = deps.dashboardConfig;
$scope.listingLimit = deps.uiSettings.get('savedObjects:listingLimit');
$scope.create = () => {
kbnUrl.redirect(DashboardConstants.CREATE_NEW_DASHBOARD_URL);
};
$scope.find = search => {
return services.dashboards.find(search, $scope.listingLimit);
};
$scope.editItem = ({ id }) => {
kbnUrl.redirect(`${createDashboardEditUrl(id)}?_a=(viewMode:edit)`);
};
$scope.getViewUrl = ({ id }) => {
return deps.addBasePath(`#${createDashboardEditUrl(id)}`);
};
$scope.delete = dashboards => {
return services.dashboards.delete(dashboards.map(d => d.id));
};
$scope.hideWriteControls = dashboardConfig.getHideWriteControls();
$scope.initialFilter = $location.search().filter || EMPTY_FILTER;
deps.chrome.setBreadcrumbs([
{
text: i18n.translate('kbn.dashboard.dashboardBreadcrumbsTitle', {
defaultMessage: 'Dashboards',
}),
},
]);
addHelpMenuToAppChrome(deps.chrome, deps.core.docLinks);
},
resolve: {
dash: function ($rootScope, $route, redirectWhenMissing, kbnUrl) {
return ensureDefaultIndexPattern(deps.core, deps.dataStart, $rootScope, kbnUrl).then(() => {
const savedObjectsClient = deps.savedObjectsClient;
const title = $route.current.params.title;
if (title) {
return savedObjectsClient
.find({
search: `"${title}"`,
search_fields: 'title',
type: 'dashboard',
})
.then(results => {
// The search isn't an exact match, lets see if we can find a single exact match to use
const matchingDashboards = results.savedObjects.filter(
dashboard => dashboard.attributes.title.toLowerCase() === title.toLowerCase()
);
if (matchingDashboards.length === 1) {
kbnUrl.redirect(createDashboardEditUrl(matchingDashboards[0].id));
} else {
kbnUrl.redirect(`${DashboardConstants.LANDING_PAGE_PATH}?filter="${title}"`);
}
$rootScope.$digest();
return new Promise(() => {});
});
}
});
},
},
})
.when(DashboardConstants.CREATE_NEW_DASHBOARD_URL, {
...defaults,
template: dashboardTemplate,
controller: createNewDashboardCtrl,
requireUICapability: 'dashboard.createNew',
resolve: {
dash: function (redirectWhenMissing, $rootScope, kbnUrl) {
return ensureDefaultIndexPattern(deps.core, deps.dataStart, $rootScope, kbnUrl)
.then(() => {
return deps.savedDashboards.get();
})
.catch(
redirectWhenMissing({
dashboard: DashboardConstants.LANDING_PAGE_PATH,
})
);
},
},
})
.when(createDashboardEditUrl(':id'), {
...defaults,
template: dashboardTemplate,
controller: createNewDashboardCtrl,
resolve: {
dash: function ($rootScope, $route, redirectWhenMissing, kbnUrl, AppState) {
const id = $route.current.params.id;
return ensureDefaultIndexPattern(deps.core, deps.dataStart, $rootScope, kbnUrl)
.then(() => {
return deps.savedDashboards.get(id);
})
.then(savedDashboard => {
deps.chrome.recentlyAccessed.add(
savedDashboard.getFullPath(),
savedDashboard.title,
id
);
return savedDashboard;
})
.catch(error => {
// A corrupt dashboard was detected (e.g. with invalid JSON properties)
if (error instanceof InvalidJSONProperty) {
deps.toastNotifications.addDanger(error.message);
kbnUrl.redirect(DashboardConstants.LANDING_PAGE_PATH);
return;
}
// 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 && id === 'create') {
// Note "new AppState" is necessary so the state in the url is preserved through the redirect.
kbnUrl.redirect(DashboardConstants.CREATE_NEW_DASHBOARD_URL, {}, new AppState());
deps.toastNotifications.addWarning(
i18n.translate('kbn.dashboard.urlWasRemovedInSixZeroWarningMessage', {
defaultMessage:
'The url "dashboard/create" was removed in 6.0. Please update your bookmarks.',
})
);
return new Promise(() => {});
} else {
throw error;
}
})
.catch(
redirectWhenMissing({
dashboard: DashboardConstants.LANDING_PAGE_PATH,
})
);
},
},
})
.when(`dashboard/:tail*?`, { redirectTo: `/${deps.core.injectedMetadata.getInjectedVar('kbnDefaultAppId')}` })
.when(`dashboards/:tail*?`, { redirectTo: `/${deps.core.injectedMetadata.getInjectedVar('kbnDefaultAppId')}` });
});
}

View file

@ -0,0 +1,68 @@
/*
* 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.
*/
/**
* The imports in this file are static functions and types which still live in legacy folders and are used
* within dashboard. To consolidate them all in one place, they are re-exported from this file. Eventually
* this list should become empty. Imports from the top level of shimmed or moved plugins can be imported
* directly where they are needed.
*/
import chrome from 'ui/chrome';
export const legacyChrome = chrome;
export { State } from 'ui/state_management/state';
export { AppState } from 'ui/state_management/app_state';
export { AppStateClass } from 'ui/state_management/app_state';
export { SaveOptions } from 'ui/saved_objects/saved_object';
export { npSetup, npStart } from 'ui/new_platform';
export { SavedObjectRegistryProvider } from 'ui/saved_objects';
export { IPrivate } from 'ui/private';
export { SavedObjectSaveModal } from 'ui/saved_objects/components/saved_object_save_modal';
export { subscribeWithScope } from 'ui/utils/subscribe_with_scope';
// @ts-ignore
export { ConfirmationButtonTypes } from 'ui/modals/confirm_modal';
export { showSaveModal, SaveResult } from 'ui/saved_objects/show_saved_object_save_modal';
export { migrateLegacyQuery } from 'ui/utils/migrate_legacy_query';
export { KbnUrl } from 'ui/url/kbn_url';
// @ts-ignore
export { GlobalStateProvider } from 'ui/state_management/global_state';
// @ts-ignore
export { StateManagementConfigProvider } from 'ui/state_management/config_provider';
// @ts-ignore
export { AppStateProvider } from 'ui/state_management/app_state';
// @ts-ignore
export { PrivateProvider } from 'ui/private/private';
// @ts-ignore
export { EventsProvider } from 'ui/events';
export { PersistedState } from 'ui/persisted_state';
// @ts-ignore
export { createTopNavDirective, createTopNavHelper } from 'ui/kbn_top_nav/kbn_top_nav';
// @ts-ignore
export { PromiseServiceCreator } from 'ui/promises/promises';
// @ts-ignore
export { KbnUrlProvider, RedirectWhenMissingProvider } from 'ui/url';
// @ts-ignore
export { confirmModalFactory } from 'ui/modals/confirm_modal';
export { configureAppAngularModule } from 'ui/legacy_compat';
export { stateMonitorFactory, StateMonitor } from 'ui/state_management/state_monitor_factory';
export { ensureDefaultIndexPattern } from 'ui/legacy_compat';
export { unhashUrl } from 'ui/state_management/state_hashing';
export { IInjector } from 'ui/chrome';
export { SavedObjectFinder } from 'ui/saved_objects/components/saved_object_finder';

View file

@ -48,7 +48,7 @@ test('convertSavedDashboardPanelToPanelState', () => {
version: '7.0.0',
};
expect(convertSavedDashboardPanelToPanelState(savedDashboardPanel, true)).toEqual({
expect(convertSavedDashboardPanelToPanelState(savedDashboardPanel)).toEqual({
gridData: {
x: 0,
y: 0,
@ -82,7 +82,7 @@ test('convertSavedDashboardPanelToPanelState does not include undefined id', ()
version: '7.0.0',
};
const converted = convertSavedDashboardPanelToPanelState(savedDashboardPanel, false);
const converted = convertSavedDashboardPanelToPanelState(savedDashboardPanel);
expect(converted.hasOwnProperty('savedObjectId')).toBe(false);
});
@ -103,7 +103,7 @@ test('convertPanelStateToSavedDashboardPanel', () => {
type: 'search',
};
expect(convertPanelStateToSavedDashboardPanel(dashboardPanel)).toEqual({
expect(convertPanelStateToSavedDashboardPanel(dashboardPanel, '6.3.0')).toEqual({
type: 'search',
embeddableConfig: {
something: 'hi!',
@ -137,6 +137,6 @@ test('convertPanelStateToSavedDashboardPanel will not add an undefined id when n
type: 'search',
};
const converted = convertPanelStateToSavedDashboardPanel(dashboardPanel);
const converted = convertPanelStateToSavedDashboardPanel(dashboardPanel, '8.0.0');
expect(converted.hasOwnProperty('id')).toBe(false);
});

View file

@ -18,12 +18,10 @@
*/
import { omit } from 'lodash';
import { DashboardPanelState } from 'src/legacy/core_plugins/dashboard_embeddable_container/public/np_ready/public';
import chrome from 'ui/chrome';
import { SavedDashboardPanel } from '../types';
export function convertSavedDashboardPanelToPanelState(
savedDashboardPanel: SavedDashboardPanel,
useMargins: boolean
savedDashboardPanel: SavedDashboardPanel
): DashboardPanelState {
return {
type: savedDashboardPanel.type,
@ -38,13 +36,14 @@ export function convertSavedDashboardPanelToPanelState(
}
export function convertPanelStateToSavedDashboardPanel(
panelState: DashboardPanelState
panelState: DashboardPanelState,
version: string
): SavedDashboardPanel {
const customTitle: string | undefined = panelState.explicitInput.title
? (panelState.explicitInput.title as string)
: undefined;
return {
version: chrome.getKibanaVersion(),
version,
type: panelState.type,
gridData: panelState.gridData,
panelIndex: panelState.explicitInput.id,

View file

@ -43,7 +43,7 @@ test('migrate app state from 6.0', async () => {
getQueryParamName: () => 'a',
save: mockSave,
};
migrateAppState(appState);
migrateAppState(appState, '8.0');
expect(appState.uiState).toBeUndefined();
const newPanel = (appState.panels[0] as unknown) as SavedDashboardPanel;
@ -58,6 +58,7 @@ test('migrate app state from 6.0', async () => {
});
test('migrate sort from 6.1', async () => {
const TARGET_VERSION = '8.0';
const mockSave = jest.fn();
const appState = {
uiState: {
@ -80,7 +81,7 @@ test('migrate sort from 6.1', async () => {
save: mockSave,
useMargins: false,
};
migrateAppState(appState);
migrateAppState(appState, TARGET_VERSION);
expect(appState.uiState).toBeUndefined();
const newPanel = (appState.panels[0] as unknown) as SavedDashboardPanel;
@ -112,7 +113,7 @@ test('migrates 6.0 even when uiState does not exist', async () => {
getQueryParamName: () => 'a',
save: mockSave,
};
migrateAppState(appState);
migrateAppState(appState, '8.0');
expect((appState as any).uiState).toBeUndefined();
const newPanel = (appState.panels[0] as unknown) as SavedDashboardPanel;
@ -147,7 +148,7 @@ test('6.2 migration adjusts w & h without margins', async () => {
save: mockSave,
useMargins: false,
};
migrateAppState(appState);
migrateAppState(appState, '8.0');
expect((appState as any).uiState).toBeUndefined();
const newPanel = (appState.panels[0] as unknown) as SavedDashboardPanel;
@ -184,7 +185,7 @@ test('6.2 migration adjusts w & h with margins', async () => {
save: mockSave,
useMargins: true,
};
migrateAppState(appState);
migrateAppState(appState, '8.0');
expect((appState as any).uiState).toBeUndefined();
const newPanel = (appState.panels[0] as unknown) as SavedDashboardPanel;

View file

@ -18,7 +18,6 @@
*/
import semver from 'semver';
import chrome from 'ui/chrome';
import { i18n } from '@kbn/i18n';
import { createUiStatsReporter, METRIC_TYPE } from '../../../../ui_metric/public';
import {
@ -37,7 +36,10 @@ import { migratePanelsTo730 } from '../migrations/migrate_to_730_panels';
*
* Once we hit a major version, we can remove support for older style URLs and get rid of this logic.
*/
export function migrateAppState(appState: { [key: string]: unknown } | DashboardAppState) {
export function migrateAppState(
appState: { [key: string]: unknown } | DashboardAppState,
kibanaVersion: string
) {
if (!appState.panels) {
throw new Error(
i18n.translate('kbn.dashboard.panel.invalidData', {
@ -73,7 +75,7 @@ export function migrateAppState(appState: { [key: string]: unknown } | Dashboard
| SavedDashboardPanel630
| SavedDashboardPanel640To720
>,
chrome.getKibanaVersion(),
kibanaVersion,
appState.useMargins,
appState.uiState
);

View file

@ -17,8 +17,8 @@
* under the License.
*/
import { SaveOptions } from 'ui/saved_objects/saved_object';
import { Timefilter } from 'ui/timefilter';
import { TimefilterContract } from 'src/plugins/data/public';
import { SaveOptions } from '../legacy_imports';
import { updateSavedDashboard } from './update_saved_dashboard';
import { DashboardStateManager } from '../dashboard_state_manager';
@ -32,7 +32,7 @@ import { DashboardStateManager } from '../dashboard_state_manager';
*/
export function saveDashboard(
toJson: (obj: any) => string,
timeFilter: Timefilter,
timeFilter: TimefilterContract,
dashboardStateManager: DashboardStateManager,
saveOptions: SaveOptions
): Promise<string> {

View file

@ -18,16 +18,15 @@
*/
import _ from 'lodash';
import { AppState } from 'ui/state_management/app_state';
import { Timefilter } from 'ui/timefilter';
import { RefreshInterval } from 'src/plugins/data/public';
import { RefreshInterval, TimefilterContract } from 'src/plugins/data/public';
import { AppState } from '../legacy_imports';
import { FilterUtils } from './filter_utils';
import { SavedObjectDashboard } from '../saved_dashboard/saved_dashboard';
export function updateSavedDashboard(
savedDashboard: SavedObjectDashboard,
appState: AppState,
timeFilter: Timefilter,
timeFilter: TimefilterContract,
toJson: <T>(object: T) => string
) {
savedDashboard.title = appState.title;

View file

@ -19,7 +19,7 @@
import React, { Fragment } from 'react';
import PropTypes from 'prop-types';
import { FormattedMessage } from '@kbn/i18n/react';
import { FormattedMessage, I18nProvider } from '@kbn/i18n/react';
import { i18n } from '@kbn/i18n';
import { EuiLink, EuiButton, EuiEmptyPrompt } from '@elastic/eui';
@ -41,27 +41,29 @@ export class DashboardListing extends React.Component {
render() {
return (
<TableListView
createItem={this.props.hideWriteControls ? null : this.props.createItem}
findItems={this.props.findItems}
deleteItems={this.props.hideWriteControls ? null : this.props.deleteItems}
editItem={this.props.hideWriteControls ? null : this.props.editItem}
tableColumns={this.getTableColumns()}
listingLimit={this.props.listingLimit}
initialFilter={this.props.initialFilter}
noItemsFragment={this.getNoItemsMessage()}
entityName={i18n.translate('kbn.dashboard.listing.table.entityName', {
defaultMessage: 'dashboard',
})}
entityNamePlural={i18n.translate('kbn.dashboard.listing.table.entityNamePlural', {
defaultMessage: 'dashboards',
})}
tableListTitle={i18n.translate('kbn.dashboard.listing.dashboardsTitle', {
defaultMessage: 'Dashboards',
})}
toastNotifications={npStart.core.notifications.toasts}
uiSettings={npStart.core.uiSettings}
/>
<I18nProvider>
<TableListView
createItem={this.props.hideWriteControls ? null : this.props.createItem}
findItems={this.props.findItems}
deleteItems={this.props.hideWriteControls ? null : this.props.deleteItems}
editItem={this.props.hideWriteControls ? null : this.props.editItem}
tableColumns={this.getTableColumns()}
listingLimit={this.props.listingLimit}
initialFilter={this.props.initialFilter}
noItemsFragment={this.getNoItemsMessage()}
entityName={i18n.translate('kbn.dashboard.listing.table.entityName', {
defaultMessage: 'dashboard',
})}
entityNamePlural={i18n.translate('kbn.dashboard.listing.table.entityNamePlural', {
defaultMessage: 'dashboards',
})}
tableListTitle={i18n.translate('kbn.dashboard.listing.dashboardsTitle', {
defaultMessage: 'Dashboards',
})}
toastNotifications={npStart.core.notifications.toasts}
uiSettings={npStart.core.uiSettings}
/>
</I18nProvider>
);
}

View file

@ -0,0 +1,151 @@
/*
* 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 {
App,
CoreSetup,
CoreStart,
LegacyCoreStart,
Plugin,
SavedObjectsClientContract,
} from 'kibana/public';
import { i18n } from '@kbn/i18n';
import { RenderDeps } from './application';
import { LocalApplicationService } from '../local_application_service';
import { DataStart } from '../../../data/public';
import { DataPublicPluginStart as NpDataStart } from '../../../../../plugins/data/public';
import { EmbeddablePublicPlugin } from '../../../../../plugins/embeddable/public';
import { Storage } from '../../../../../plugins/kibana_utils/public';
import { NavigationStart } from '../../../navigation/public';
import { DashboardConstants } from './dashboard_constants';
import { SharePluginStart } from '../../../../../plugins/share/public';
import {
HomePublicPluginSetup,
FeatureCatalogueCategory,
} from '../../../../../plugins/home/public';
export interface LegacyAngularInjectedDependencies {
dashboardConfig: any;
savedObjectRegistry: any;
savedDashboards: any;
}
export interface DashboardPluginStartDependencies {
data: DataStart;
npData: NpDataStart;
embeddables: ReturnType<EmbeddablePublicPlugin['start']>;
navigation: NavigationStart;
share: SharePluginStart;
}
export interface DashboardPluginSetupDependencies {
__LEGACY: {
getAngularDependencies: () => Promise<LegacyAngularInjectedDependencies>;
localApplicationService: LocalApplicationService;
};
home: HomePublicPluginSetup;
}
export class DashboardPlugin implements Plugin {
private startDependencies: {
dataStart: DataStart;
npDataStart: NpDataStart;
savedObjectsClient: SavedObjectsClientContract;
embeddables: ReturnType<EmbeddablePublicPlugin['start']>;
navigation: NavigationStart;
share: SharePluginStart;
} | null = null;
public setup(
core: CoreSetup,
{
__LEGACY: { localApplicationService, getAngularDependencies, ...legacyServices },
home,
}: DashboardPluginSetupDependencies
) {
const app: App = {
id: '',
title: 'Dashboards',
mount: async ({ core: contextCore }, params) => {
if (this.startDependencies === null) {
throw new Error('not started yet');
}
const {
dataStart,
savedObjectsClient,
embeddables,
navigation,
share,
npDataStart,
} = this.startDependencies;
const angularDependencies = await getAngularDependencies();
const deps: RenderDeps = {
core: contextCore as LegacyCoreStart,
...legacyServices,
...angularDependencies,
navigation,
dataStart,
share,
npDataStart,
indexPatterns: dataStart.indexPatterns.indexPatterns,
savedObjectsClient,
chrome: contextCore.chrome,
addBasePath: contextCore.http.basePath.prepend,
uiSettings: contextCore.uiSettings,
savedQueryService: dataStart.search.services.savedQueryService,
embeddables,
dashboardCapabilities: contextCore.application.capabilities.dashboard,
localStorage: new Storage(localStorage),
};
const { renderApp } = await import('./application');
return renderApp(params.element, params.appBasePath, deps);
},
};
localApplicationService.register({ ...app, id: 'dashboard' });
localApplicationService.register({ ...app, id: 'dashboards' });
home.featureCatalogue.register({
id: 'dashboard',
title: i18n.translate('kbn.dashboard.featureCatalogue.dashboardTitle', {
defaultMessage: 'Dashboard',
}),
description: i18n.translate('kbn.dashboard.featureCatalogue.dashboardDescription', {
defaultMessage: 'Display and share a collection of visualizations and saved searches.',
}),
icon: 'dashboardApp',
path: `/app/kibana#${DashboardConstants.LANDING_PAGE_PATH}`,
showOnHomePage: true,
category: FeatureCatalogueCategory.DATA,
});
}
start(
{ savedObjects: { client: savedObjectsClient } }: CoreStart,
{ data: dataStart, embeddables, navigation, npData, share }: DashboardPluginStartDependencies
) {
this.startDependencies = {
dataStart,
npDataStart: npData,
savedObjectsClient,
embeddables,
navigation,
share,
};
}
}

View file

@ -17,9 +17,16 @@
* under the License.
*/
import React from 'react';
import { shallowWithI18nProvider } from 'test_utils/enzyme_helpers';
jest.mock('../legacy_imports', () => ({
SavedObjectSaveModal: () => null
}));
jest.mock('ui/new_platform');
import { DashboardSaveModal } from './save_modal';
test('renders DashboardSaveModal', () => {

View file

@ -19,10 +19,10 @@
import React, { Fragment } from 'react';
import { FormattedMessage } from '@kbn/i18n/react';
import { SavedObjectSaveModal } from 'ui/saved_objects/components/saved_object_save_modal';
import { EuiFormRow, EuiTextArea, EuiSwitch } from '@elastic/eui';
import { SavedObjectSaveModal } from '../legacy_imports';
interface SaveOptions {
newTitle: string;
newDescription: string;

View file

@ -17,10 +17,10 @@
* under the License.
*/
import { I18nContext } from 'ui/i18n';
import React from 'react';
import ReactDOM from 'react-dom';
import { i18n } from '@kbn/i18n';
import { I18nProvider } from '@kbn/i18n/react';
import { DashboardCloneModal } from './clone_modal';
export function showCloneModal(
@ -54,7 +54,7 @@ export function showCloneModal(
};
document.body.appendChild(container);
const element = (
<I18nContext>
<I18nProvider>
<DashboardCloneModal
onClone={onCloneConfirmed}
onClose={closeModal}
@ -63,7 +63,7 @@ export function showCloneModal(
values: { title },
})}
/>
</I18nContext>
</I18nProvider>
);
ReactDOM.render(element, container);
}

View file

@ -19,9 +19,9 @@
import React from 'react';
import ReactDOM from 'react-dom';
import { I18nContext } from 'ui/i18n';
import { I18nProvider } from '@kbn/i18n/react';
import { EuiWrappingPopover } from '@elastic/eui';
import { OptionsMenu } from './options';
let isOpen = false;
@ -55,7 +55,7 @@ export function showOptionsPopover({
document.body.appendChild(container);
const element = (
<I18nContext>
<I18nProvider>
<EuiWrappingPopover id="popover" button={anchorElement} isOpen={true} closePopover={onClose}>
<OptionsMenu
useMargins={useMargins}
@ -64,7 +64,7 @@ export function showOptionsPopover({
onHidePanelTitlesChange={onHidePanelTitlesChange}
/>
</EuiWrappingPopover>
</I18nContext>
</I18nProvider>
);
ReactDOM.render(element, container);
}

View file

@ -17,9 +17,8 @@
* under the License.
*/
import { AppState } from 'ui/state_management/app_state';
import { AppState as TAppState } from 'ui/state_management/app_state';
import { ViewMode } from 'src/plugins/embeddable/public';
import { AppState } from './legacy_imports';
import {
RawSavedDashboardPanelTo60,
RawSavedDashboardPanel610,
@ -153,5 +152,5 @@ export type AddFilterFn = (
operator: string;
index: string;
},
appState: TAppState
appState: AppState
) => void;

View file

@ -57,9 +57,11 @@ import {
vislibSeriesResponseHandlerProvider,
Vis,
SavedObjectSaveModal,
ensureDefaultIndexPattern,
} from '../kibana_services';
const {
core,
chrome,
docTitle,
FilterBarQueryFilterProvider,
@ -72,7 +74,6 @@ const {
} = getServices();
import { getRootBreadcrumbs, getSavedSearchBreadcrumbs } from '../breadcrumbs';
import { extractTimeFilter, changeTimeFilter } from '../../../../data/public';
import { start as data } from '../../../../data/public/legacy';
import { generateFilters } from '../../../../../../plugins/data/public';
@ -91,7 +92,6 @@ const app = uiModules.get('apps/discover', [
uiRoutes
.defaults(/^\/discover(\/|$)/, {
requireDefaultIndex: true,
requireUICapability: 'discover.show',
k7Breadcrumbs: ($route, $injector) =>
$injector.invoke(
@ -119,50 +119,53 @@ uiRoutes
template: indexTemplate,
reloadOnSearch: false,
resolve: {
ip: function (Promise, indexPatterns, config, Private) {
savedObjects: function (Promise, indexPatterns, config, Private, $rootScope, kbnUrl, redirectWhenMissing, savedSearches, $route) {
const State = Private(StateProvider);
return indexPatterns.getCache().then((savedObjects)=> {
/**
* In making the indexPattern modifiable it was placed in appState. Unfortunately,
* the load order of AppState conflicts with the load order of many other things
* so in order to get the name of the index we should use, and to switch to the
* default if necessary, we parse the appState with a temporary State object and
* then destroy it immediatly after we're done
*
* @type {State}
*/
const state = new State('_a', {});
const specified = !!state.index;
const exists = _.findIndex(savedObjects, o => o.id === state.index) > -1;
const id = exists ? state.index : config.get('defaultIndex');
state.destroy();
const savedSearchId = $route.current.params.id;
return ensureDefaultIndexPattern(core, data, $rootScope, kbnUrl).then(() => {
return Promise.props({
list: savedObjects,
loaded: indexPatterns.get(id),
stateVal: state.index,
stateValFound: specified && exists
ip: indexPatterns.getCache().then((savedObjects) => {
/**
* In making the indexPattern modifiable it was placed in appState. Unfortunately,
* the load order of AppState conflicts with the load order of many other things
* so in order to get the name of the index we should use, and to switch to the
* default if necessary, we parse the appState with a temporary State object and
* then destroy it immediatly after we're done
*
* @type {State}
*/
const state = new State('_a', {});
const specified = !!state.index;
const exists = _.findIndex(savedObjects, o => o.id === state.index) > -1;
const id = exists ? state.index : config.get('defaultIndex');
state.destroy();
return Promise.props({
list: savedObjects,
loaded: indexPatterns.get(id),
stateVal: state.index,
stateValFound: specified && exists
});
}),
savedSearch: savedSearches.get(savedSearchId)
.then((savedSearch) => {
if (savedSearchId) {
chrome.recentlyAccessed.add(
savedSearch.getFullPath(),
savedSearch.title,
savedSearchId);
}
return savedSearch;
})
.catch(redirectWhenMissing({
'search': '/discover',
'index-pattern': '/management/kibana/objects/savedSearches/' + $route.current.params.id
}))
});
});
},
savedSearch: function (redirectWhenMissing, savedSearches, $route) {
const savedSearchId = $route.current.params.id;
return savedSearches.get(savedSearchId)
.then((savedSearch) => {
if (savedSearchId) {
chrome.recentlyAccessed.add(
savedSearch.getFullPath(),
savedSearch.title,
savedSearchId);
}
return savedSearch;
})
.catch(redirectWhenMissing({
'search': '/discover',
'index-pattern': '/management/kibana/objects/savedSearches/' + $route.current.params.id
}));
}
}
});
@ -224,7 +227,7 @@ function discoverController(
};
// the saved savedSearch
const savedSearch = $route.current.locals.savedSearch;
const savedSearch = $route.current.locals.savedObjects.savedSearch;
let abortController;
$scope.$on('$destroy', () => {
@ -417,20 +420,6 @@ function discoverController(
queryFilter.setFilters(filters);
};
$scope.applyFilters = filters => {
const { timeRangeFilter, restOfFilters } = extractTimeFilter($scope.indexPattern.timeFieldName, filters);
queryFilter.addFilters(restOfFilters);
if (timeRangeFilter) changeTimeFilter(timefilter, timeRangeFilter);
$scope.state.$newFilters = [];
};
$scope.$watch('state.$newFilters', (filters = []) => {
if (filters.length === 1) {
$scope.applyFilters(filters);
}
});
const getFieldCounts = async () => {
// the field counts aren't set until we have the data back,
// so we wait for the fetch to be done before proceeding
@ -539,7 +528,7 @@ function discoverController(
sampleSize: config.get('discover:sampleSize'),
timefield: isDefaultTypeIndexPattern($scope.indexPattern) && $scope.indexPattern.timeFieldName,
savedSearch: savedSearch,
indexPatternList: $route.current.locals.ip.list,
indexPatternList: $route.current.locals.savedObjects.ip.list,
};
const shouldSearchOnPageLoad = () => {
@ -1055,7 +1044,7 @@ function discoverController(
loaded: loadedIndexPattern,
stateVal,
stateValFound,
} = $route.current.locals.ip;
} = $route.current.locals.savedObjects.ip;
const ownIndexPattern = $scope.searchSource.getOwnField('index');
@ -1103,12 +1092,12 @@ function discoverController(
// Block the UI from loading if the user has loaded a rollup index pattern but it isn't
// supported.
$scope.isUnsupportedIndexPattern = (
!isDefaultTypeIndexPattern($route.current.locals.ip.loaded)
&& !hasSearchStategyForIndexPattern($route.current.locals.ip.loaded)
!isDefaultTypeIndexPattern($route.current.locals.savedObjects.ip.loaded)
&& !hasSearchStategyForIndexPattern($route.current.locals.savedObjects.ip.loaded)
);
if ($scope.isUnsupportedIndexPattern) {
$scope.unsupportedIndexPatternType = $route.current.locals.ip.loaded.type;
$scope.unsupportedIndexPatternType = $route.current.locals.savedObjects.ip.loaded.type;
return;
}

View file

@ -34,7 +34,7 @@ export function getSavedSearchBreadcrumbs($route: any) {
return [
...getRootBreadcrumbs(),
{
text: $route.current.locals.savedSearch.id,
text: $route.current.locals.savedObjects.savedSearch.id,
},
];
}

View file

@ -46,6 +46,7 @@ import * as docViewsRegistry from 'ui/registry/doc_views';
const services = {
// new plattform
core: npStart.core,
addBasePath: npStart.core.http.basePath.prepend,
capabilities: npStart.core.application.capabilities,
chrome: npStart.core.chrome,
@ -108,6 +109,7 @@ export { getUnhashableStatesProvider } from 'ui/state_management/state_hashing';
export { tabifyAggResponse } from 'ui/agg_response/tabify';
// @ts-ignore
export { vislibSeriesResponseHandlerProvider } from 'ui/vis/response_handlers/vislib';
export { ensureDefaultIndexPattern } from 'ui/legacy_compat';
export { unhashUrl } from 'ui/state_management/state_hashing';
// EXPORT types

View file

@ -28,7 +28,6 @@ import { I18nContext } from 'ui/i18n';
import { uiModules } from 'ui/modules';
import appTemplate from './app.html';
import landingTemplate from './landing.html';
import { capabilities } from 'ui/capabilities';
import { management, SidebarNav, MANAGEMENT_BREADCRUMB } from 'ui/management';
import { FeatureCatalogueRegistryProvider, FeatureCatalogueCategory } from 'ui/registry/feature_catalogue';
import { timefilter } from 'ui/timefilter';
@ -50,13 +49,6 @@ uiRoutes
redirectTo: '/management'
});
require('./route_setup/load_default')({
whenMissingRedirectTo: () => {
const canManageIndexPatterns = capabilities.get().management.kibana.index_patterns;
return canManageIndexPatterns ? '/management/kibana/index_pattern' : '/home';
}
});
export function updateLandingPage(version) {
const node = document.getElementById(LANDING_ID);
if (!node) {

View file

@ -1,110 +0,0 @@
/*
* 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 _ from 'lodash';
import React from 'react';
import { banners } from 'ui/notify';
import { NoDefaultIndexPattern } from 'ui/index_patterns';
import uiRoutes from 'ui/routes';
import {
EuiCallOut,
} from '@elastic/eui';
import { clearTimeout } from 'timers';
import { i18n } from '@kbn/i18n';
let bannerId;
let timeoutId;
function displayBanner() {
clearTimeout(timeoutId);
// Avoid being hostile to new users who don't have an index pattern setup yet
// give them a friendly info message instead of a terse error message
bannerId = banners.set({
id: bannerId, // initially undefined, but reused after first set
component: (
<EuiCallOut
color="warning"
iconType="iInCircle"
title={
i18n.translate('kbn.management.indexPattern.bannerLabel',
//eslint-disable-next-line max-len
{ defaultMessage: 'In order to visualize and explore data in Kibana, you\'ll need to create an index pattern to retrieve data from Elasticsearch.' })
}
/>
)
});
// hide the message after the user has had a chance to acknowledge it -- so it doesn't permanently stick around
timeoutId = setTimeout(() => {
banners.remove(bannerId);
timeoutId = undefined;
}, 15000);
}
// eslint-disable-next-line import/no-default-export
export default function (opts) {
opts = opts || {};
const whenMissingRedirectTo = opts.whenMissingRedirectTo || null;
uiRoutes
.addSetupWork(function loadDefaultIndexPattern(Promise, $route, config, indexPatterns) {
const route = _.get($route, 'current.$$route');
if (!route.requireDefaultIndex) {
return;
}
return indexPatterns.getIds()
.then(function (patterns) {
let defaultId = config.get('defaultIndex');
let defined = !!defaultId;
const exists = _.contains(patterns, defaultId);
if (defined && !exists) {
config.remove('defaultIndex');
defaultId = defined = false;
}
if (!defined) {
// If there is any index pattern created, set the first as default
if (patterns.length >= 1) {
defaultId = patterns[0];
config.set('defaultIndex', defaultId);
} else {
throw new NoDefaultIndexPattern();
}
}
});
})
.afterWork(
// success
null,
// failure
function (err, kbnUrl) {
const hasDefault = !(err instanceof NoDefaultIndexPattern);
if (hasDefault || !whenMissingRedirectTo) throw err; // rethrow
kbnUrl.change(whenMissingRedirectTo());
displayBanner();
}
);
}

View file

@ -24,6 +24,7 @@ import '../saved_visualizations/saved_visualizations';
import './visualization_editor';
import './visualization';
import { ensureDefaultIndexPattern } from 'ui/legacy_compat';
import React from 'react';
import { FormattedMessage } from '@kbn/i18n/react';
import { migrateAppState } from './lib';
@ -49,6 +50,7 @@ import {
} from '../kibana_services';
const {
core,
capabilities,
chrome,
chromeLegacy,
@ -71,7 +73,7 @@ uiRoutes
template: editorTemplate,
k7Breadcrumbs: getCreateBreadcrumbs,
resolve: {
savedVis: function (savedVisualizations, redirectWhenMissing, $route) {
savedVis: function (savedVisualizations, redirectWhenMissing, $route, $rootScope, kbnUrl) {
const visTypes = visualizations.types.all();
const visType = _.find(visTypes, { name: $route.current.params.type });
const shouldHaveIndex = visType.requiresSearch && visType.options.showIndexSelection;
@ -84,7 +86,7 @@ uiRoutes
);
}
return savedVisualizations.get($route.current.params)
return ensureDefaultIndexPattern(core, data, $rootScope, kbnUrl).then(() => savedVisualizations.get($route.current.params))
.then(savedVis => {
if (savedVis.vis.type.setup) {
return savedVis.vis.type.setup(savedVis)
@ -102,28 +104,33 @@ uiRoutes
template: editorTemplate,
k7Breadcrumbs: getEditBreadcrumbs,
resolve: {
savedVis: function (savedVisualizations, redirectWhenMissing, $route) {
return savedVisualizations.get($route.current.params.id)
savedVis: function (savedVisualizations, redirectWhenMissing, $route, $rootScope, kbnUrl) {
return ensureDefaultIndexPattern(core, data, $rootScope, kbnUrl)
.then(() => savedVisualizations.get($route.current.params.id))
.then((savedVis) => {
chrome.recentlyAccessed.add(
savedVis.getFullPath(),
savedVis.title,
savedVis.id);
savedVis.id
);
return savedVis;
})
.then(savedVis => {
if (savedVis.vis.type.setup) {
return savedVis.vis.type.setup(savedVis)
.catch(() => savedVis);
return savedVis.vis.type.setup(savedVis).catch(() => savedVis);
}
return savedVis;
})
.catch(redirectWhenMissing({
'visualization': '/visualize',
'search': '/management/kibana/objects/savedVisualizations/' + $route.current.params.id,
'index-pattern': '/management/kibana/objects/savedVisualizations/' + $route.current.params.id,
'index-pattern-field': '/management/kibana/objects/savedVisualizations/' + $route.current.params.id
}));
.catch(
redirectWhenMissing({
visualization: '/visualize',
search: '/management/kibana/objects/savedVisualizations/' + $route.current.params.id,
'index-pattern':
'/management/kibana/objects/savedVisualizations/' + $route.current.params.id,
'index-pattern-field':
'/management/kibana/objects/savedVisualizations/' + $route.current.params.id,
})
);
}
}
});

View file

@ -17,6 +17,7 @@
* under the License.
*/
import { ensureDefaultIndexPattern } from 'ui/legacy_compat';
import './editor/editor';
import { i18n } from '@kbn/i18n';
import './saved_visualizations/_saved_vis';
@ -32,7 +33,6 @@ const { FeatureCatalogueRegistryProvider, uiRoutes } = getServices();
uiRoutes
.defaults(/visualize/, {
requireDefaultIndex: true,
requireUICapability: 'visualize.show',
badge: uiCapabilities => {
if (uiCapabilities.visualize.save) {
@ -57,6 +57,7 @@ uiRoutes
controllerAs: 'listingController',
resolve: {
createNewVis: () => false,
hasDefaultIndex: ($rootScope, kbnUrl) => ensureDefaultIndexPattern(getServices().core, getServices().data, $rootScope, kbnUrl)
},
})
.when(VisualizeConstants.WIZARD_STEP_1_PAGE_PATH, {
@ -66,6 +67,7 @@ uiRoutes
controllerAs: 'listingController',
resolve: {
createNewVis: () => true,
hasDefaultIndex: ($rootScope, kbnUrl) => ensureDefaultIndexPattern(getServices().core, getServices().data, $rootScope, kbnUrl)
},
});

View file

@ -60,6 +60,7 @@ const services = {
savedObjectsClient: npStart.core.savedObjects.client,
toastNotifications: npStart.core.notifications.toasts,
uiSettings: npStart.core.uiSettings,
core: npStart.core,
share: npStart.plugins.share,
data,

View file

@ -1,38 +0,0 @@
/*
* 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 { get } from 'lodash';
import chrome from 'ui/chrome';
import uiRoutes from 'ui/routes';
import { UICapabilities } from '.';
uiRoutes.addSetupWork(
(uiCapabilities: UICapabilities, kbnBaseUrl: string, $route: any, kbnUrl: any) => {
const route = get($route, 'current.$$route') as any;
if (!route.requireUICapability) {
return;
}
if (!get(uiCapabilities, route.requireUICapability)) {
const url = chrome.addBasePath(`${kbnBaseUrl}#/home`);
kbnUrl.redirect(url);
throw uiRoutes.WAIT_FOR_URL_CHANGE_TOKEN;
}
}
);

View file

@ -21,13 +21,15 @@ import { uiModules } from '../../modules';
import { directivesProvider } from '../directives';
import { registerSubUrlHooks } from './sub_url_hooks';
import { start as data } from '../../../../core_plugins/data/public/legacy';
import { configureAppAngularModule } from 'ui/legacy_compat';
import { npStart } from '../../new_platform/new_platform';
export function initAngularApi(chrome, internals) {
chrome.setupAngular = function () {
const kibana = uiModules.get('kibana');
configureAppAngularModule(kibana);
configureAppAngularModule(kibana, npStart.core, data, false);
kibana.value('chrome', chrome);

View file

@ -75,8 +75,7 @@ export function createTopNavDirective() {
module.directive('kbnTopNav', createTopNavDirective);
export function createTopNavHelper(reactDirective) {
const { TopNavMenu } = navigation.ui;
export const createTopNavHelper = ({ TopNavMenu }) => (reactDirective) => {
return reactDirective(
wrapInI18nContext(TopNavMenu),
[
@ -116,6 +115,6 @@ export function createTopNavHelper(reactDirective) {
'showAutoRefreshOnly',
],
);
}
};
module.directive('kbnTopNavHelper', createTopNavHelper);
module.directive('kbnTopNavHelper', createTopNavHelper(navigation.ui));

View file

@ -28,7 +28,7 @@ import {
IRootScopeService,
} from 'angular';
import $ from 'jquery';
import { cloneDeep, forOwn, set } from 'lodash';
import _, { cloneDeep, forOwn, get, set } from 'lodash';
import React, { Fragment } from 'react';
import * as Rx from 'rxjs';
@ -37,27 +37,43 @@ import { FormattedMessage } from '@kbn/i18n/react';
import { CoreStart, LegacyCoreStart } from 'kibana/public';
import { fatalError } from 'ui/notify';
import { capabilities } from 'ui/capabilities';
import { RouteConfiguration } from 'ui/routes/route_manager';
// @ts-ignore
import { modifyUrl } from 'ui/url';
import { toMountPoint } from '../../../../plugins/kibana_react/public';
// @ts-ignore
import { UrlOverflowService } from '../error_url_overflow';
import { npStart } from '../new_platform';
import { toastNotifications } from '../notify';
// @ts-ignore
import { isSystemApiRequest } from '../system_api';
const URL_LIMIT_WARN_WITHIN = 1000;
function isDummyWrapperRoute($route: any) {
/**
* Detects whether a given angular route is a dummy route that doesn't
* require any action. There are two ways this can happen:
* If `outerAngularWrapperRoute` is set on the route config object,
* it means the local application service set up this route on the outer angular
* and the internal routes will handle the hooks.
*
* If angular did not detect a route and it is the local angular, we are currently
* navigating away from a URL controlled by a local angular router and the
* application will get unmounted. In this case the outer router will handle
* the hooks.
* @param $route Injected $route dependency
* @param isLocalAngular Flag whether this is the local angular router
*/
function isDummyRoute($route: any, isLocalAngular: boolean) {
return (
$route.current && $route.current.$$route && $route.current.$$route.outerAngularWrapperRoute
($route.current && $route.current.$$route && $route.current.$$route.outerAngularWrapperRoute) ||
(!$route.current && isLocalAngular)
);
}
export const configureAppAngularModule = (angularModule: IModule) => {
const newPlatform = npStart.core;
export const configureAppAngularModule = (
angularModule: IModule,
newPlatform: LegacyCoreStart,
isLocalAngular: boolean
) => {
const legacyMetadata = newPlatform.injectedMetadata.getLegacyMetadata();
forOwn(newPlatform.injectedMetadata.getInjectedVars(), (val, name) => {
@ -73,15 +89,16 @@ export const configureAppAngularModule = (angularModule: IModule) => {
.value('buildSha', legacyMetadata.buildSha)
.value('serverName', legacyMetadata.serverName)
.value('esUrl', getEsUrl(newPlatform))
.value('uiCapabilities', capabilities.get())
.value('uiCapabilities', newPlatform.application.capabilities)
.config(setupCompileProvider(newPlatform))
.config(setupLocationProvider(newPlatform))
.config($setupXsrfRequestInterceptor(newPlatform))
.run(capture$httpLoadingCount(newPlatform))
.run($setupBreadcrumbsAutoClear(newPlatform))
.run($setupBadgeAutoClear(newPlatform))
.run($setupHelpExtensionAutoClear(newPlatform))
.run($setupUrlOverflowHandling(newPlatform));
.run($setupBreadcrumbsAutoClear(newPlatform, isLocalAngular))
.run($setupBadgeAutoClear(newPlatform, isLocalAngular))
.run($setupHelpExtensionAutoClear(newPlatform, isLocalAngular))
.run($setupUrlOverflowHandling(newPlatform, isLocalAngular))
.run($setupUICapabilityRedirect(newPlatform));
};
const getEsUrl = (newPlatform: CoreStart) => {
@ -168,12 +185,42 @@ const capture$httpLoadingCount = (newPlatform: CoreStart) => (
);
};
/**
* integrates with angular to automatically redirect to home if required
* capability is not met
*/
const $setupUICapabilityRedirect = (newPlatform: CoreStart) => (
$rootScope: IRootScopeService,
$injector: any
) => {
const isKibanaAppRoute = window.location.pathname.endsWith('/app/kibana');
// this feature only works within kibana app for now after everything is
// switched to the application service, this can be changed to handle all
// apps.
if (!isKibanaAppRoute) {
return;
}
$rootScope.$on(
'$routeChangeStart',
(event, { $$route: route }: { $$route?: RouteConfiguration } = {}) => {
if (!route || !route.requireUICapability) {
return;
}
if (!get(newPlatform.application.capabilities, route.requireUICapability)) {
$injector.get('kbnUrl').change('/home');
event.preventDefault();
}
}
);
};
/**
* internal angular run function that will be called when angular bootstraps and
* lets us integrate with the angular router so that we can automatically clear
* the breadcrumbs if we switch to a Kibana app that does not use breadcrumbs correctly
*/
const $setupBreadcrumbsAutoClear = (newPlatform: CoreStart) => (
const $setupBreadcrumbsAutoClear = (newPlatform: CoreStart, isLocalAngular: boolean) => (
$rootScope: IRootScopeService,
$injector: any
) => {
@ -195,7 +242,7 @@ const $setupBreadcrumbsAutoClear = (newPlatform: CoreStart) => (
});
$rootScope.$on('$routeChangeSuccess', () => {
if (isDummyWrapperRoute($route)) {
if (isDummyRoute($route, isLocalAngular)) {
return;
}
const current = $route.current || {};
@ -223,7 +270,7 @@ const $setupBreadcrumbsAutoClear = (newPlatform: CoreStart) => (
* lets us integrate with the angular router so that we can automatically clear
* the badge if we switch to a Kibana app that does not use the badge correctly
*/
const $setupBadgeAutoClear = (newPlatform: CoreStart) => (
const $setupBadgeAutoClear = (newPlatform: CoreStart, isLocalAngular: boolean) => (
$rootScope: IRootScopeService,
$injector: any
) => {
@ -237,7 +284,7 @@ const $setupBadgeAutoClear = (newPlatform: CoreStart) => (
});
$rootScope.$on('$routeChangeSuccess', () => {
if (isDummyWrapperRoute($route)) {
if (isDummyRoute($route, isLocalAngular)) {
return;
}
const current = $route.current || {};
@ -266,7 +313,7 @@ const $setupBadgeAutoClear = (newPlatform: CoreStart) => (
* the helpExtension if we switch to a Kibana app that does not set its own
* helpExtension
*/
const $setupHelpExtensionAutoClear = (newPlatform: CoreStart) => (
const $setupHelpExtensionAutoClear = (newPlatform: CoreStart, isLocalAngular: boolean) => (
$rootScope: IRootScopeService,
$injector: any
) => {
@ -284,14 +331,14 @@ const $setupHelpExtensionAutoClear = (newPlatform: CoreStart) => (
const $route = $injector.has('$route') ? $injector.get('$route') : {};
$rootScope.$on('$routeChangeStart', () => {
if (isDummyWrapperRoute($route)) {
if (isDummyRoute($route, isLocalAngular)) {
return;
}
helpExtensionSetSinceRouteChange = false;
});
$rootScope.$on('$routeChangeSuccess', () => {
if (isDummyWrapperRoute($route)) {
if (isDummyRoute($route, isLocalAngular)) {
return;
}
const current = $route.current || {};
@ -304,7 +351,7 @@ const $setupHelpExtensionAutoClear = (newPlatform: CoreStart) => (
});
};
const $setupUrlOverflowHandling = (newPlatform: CoreStart) => (
const $setupUrlOverflowHandling = (newPlatform: CoreStart, isLocalAngular: boolean) => (
$location: ILocationService,
$rootScope: IRootScopeService,
$injector: auto.IInjectorService
@ -312,7 +359,7 @@ const $setupUrlOverflowHandling = (newPlatform: CoreStart) => (
const $route = $injector.has('$route') ? $injector.get('$route') : {};
const urlOverflow = new UrlOverflowService();
const check = () => {
if (isDummyWrapperRoute($route)) {
if (isDummyRoute($route, isLocalAngular)) {
return;
}
// disable long url checks when storing state in session storage
@ -326,7 +373,7 @@ const $setupUrlOverflowHandling = (newPlatform: CoreStart) => (
try {
if (urlOverflow.check($location.absUrl()) <= URL_LIMIT_WARN_WITHIN) {
toastNotifications.addWarning({
newPlatform.notifications.toasts.addWarning({
title: i18n.translate('common.ui.chrome.bigUrlWarningNotificationTitle', {
defaultMessage: 'The URL is big and Kibana might stop working',
}),

View file

@ -0,0 +1,105 @@
/*
* 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 { contains } from 'lodash';
import { IRootScopeService } from 'angular';
import React from 'react';
import ReactDOM from 'react-dom';
import { i18n } from '@kbn/i18n';
import { I18nProvider } from '@kbn/i18n/react';
import { EuiCallOut } from '@elastic/eui';
import { CoreStart } from 'kibana/public';
import { DataStart } from '../../../core_plugins/data/public';
let bannerId: string;
let timeoutId: NodeJS.Timeout | undefined;
/**
* Checks whether a default index pattern is set and exists and defines
* one otherwise.
*
* If there are no index patterns, redirect to management page and show
* banner. In this case the promise returned from this function will never
* resolve to wait for the URL change to happen.
*/
export async function ensureDefaultIndexPattern(
newPlatform: CoreStart,
data: DataStart,
$rootScope: IRootScopeService,
kbnUrl: any
) {
const patterns = await data.indexPatterns.indexPatterns.getIds();
let defaultId = newPlatform.uiSettings.get('defaultIndex');
let defined = !!defaultId;
const exists = contains(patterns, defaultId);
if (defined && !exists) {
newPlatform.uiSettings.remove('defaultIndex');
defaultId = defined = false;
}
if (defined) {
return;
}
// If there is any index pattern created, set the first as default
if (patterns.length >= 1) {
defaultId = patterns[0];
newPlatform.uiSettings.set('defaultIndex', defaultId);
} else {
const canManageIndexPatterns =
newPlatform.application.capabilities.management.kibana.index_patterns;
const redirectTarget = canManageIndexPatterns ? '/management/kibana/index_pattern' : '/home';
if (timeoutId) {
clearTimeout(timeoutId);
}
// Avoid being hostile to new users who don't have an index pattern setup yet
// give them a friendly info message instead of a terse error message
bannerId = newPlatform.overlays.banners.replace(bannerId, (element: HTMLElement) => {
ReactDOM.render(
<I18nProvider>
<EuiCallOut
color="warning"
iconType="iInCircle"
title={i18n.translate('common.ui.indexPattern.bannerLabel', {
defaultMessage:
"In order to visualize and explore data in Kibana, you'll need to create an index pattern to retrieve data from Elasticsearch.",
})}
/>
</I18nProvider>,
element
);
return () => ReactDOM.unmountComponentAtNode(element);
});
// hide the message after the user has had a chance to acknowledge it -- so it doesn't permanently stick around
timeoutId = setTimeout(() => {
newPlatform.overlays.banners.remove(bannerId);
timeoutId = undefined;
}, 15000);
kbnUrl.change(redirectTarget);
$rootScope.$digest();
// return never-resolving promise to stop resolving and wait for the url change
return new Promise(() => {});
}
}

View file

@ -18,3 +18,4 @@
*/
export { configureAppAngularModule } from './angular_config';
export { ensureDefaultIndexPattern } from './ensure_default_index_pattern';

View file

@ -119,18 +119,6 @@ describe('routes/route_manager', function () {
expect($rp.when.secondCall.args[1]).to.have.property('reloadOnSearch', false);
expect($rp.when.lastCall.args[1]).to.have.property('reloadOnSearch', true);
});
it('sets route.requireDefaultIndex to false by default', function () {
routes.when('/nothing-set');
routes.when('/no-index-required', { requireDefaultIndex: false });
routes.when('/index-required', { requireDefaultIndex: true });
routes.config($rp);
expect($rp.when.callCount).to.be(3);
expect($rp.when.firstCall.args[1]).to.have.property('requireDefaultIndex', false);
expect($rp.when.secondCall.args[1]).to.have.property('requireDefaultIndex', false);
expect($rp.when.lastCall.args[1]).to.have.property('requireDefaultIndex', true);
});
});
describe('#defaults()', () => {

View file

@ -23,7 +23,7 @@
import { ChromeBreadcrumb } from '../../../../core/public';
interface RouteConfiguration {
export interface RouteConfiguration {
controller?: string | ((...args: any[]) => void);
redirectTo?: string;
resolveRedirectTo?: (...args: any[]) => void;

View file

@ -46,10 +46,6 @@ export default function RouteManager() {
route.reloadOnSearch = false;
}
if (route.requireDefaultIndex == null) {
route.requireDefaultIndex = false;
}
wrapRouteWithPrep(route, setup);
$routeProvider.when(path, route);
});

View file

@ -42,9 +42,14 @@ import {
isStateHash,
} from './state_storage';
export function StateProvider(Private, $rootScope, $location, stateManagementConfig, config, kbnUrl) {
export function StateProvider(Private, $rootScope, $location, stateManagementConfig, config, kbnUrl, $injector) {
const Events = Private(EventsProvider);
const isDummyRoute = () =>
$injector.has('$route') &&
$injector.get('$route').current &&
$injector.get('$route').current.outerAngularWrapperRoute;
createLegacyClass(State).inherits(Events);
function State(
urlParam,
@ -137,7 +142,7 @@ export function StateProvider(Private, $rootScope, $location, stateManagementCon
let stash = this._readFromURL();
// nothing to read from the url? save if ordered to persist
// nothing to read from the url? save if ordered to persist, but only if it's not on a wrapper route
if (stash === null) {
if (this._persistAcrossApps) {
return this.save();
@ -150,7 +155,7 @@ export function StateProvider(Private, $rootScope, $location, stateManagementCon
// apply diff to state from stash, will change state in place via side effect
const diffResults = applyDiff(this, stash);
if (diffResults.keys.length) {
if (!isDummyRoute() && diffResults.keys.length) {
this.emit('fetch_with_changes', diffResults.keys);
}
};
@ -164,6 +169,10 @@ export function StateProvider(Private, $rootScope, $location, stateManagementCon
return;
}
if (isDummyRoute()) {
return;
}
let stash = this._readFromURL();
const state = this.toObject();
replace = replace || false;

View file

@ -42,9 +42,14 @@ describe('registerTimefilterWithGlobalState()', () => {
}
};
const rootScope = {
$on: jest.fn()
};
registerTimefilterWithGlobalState(
timefilter,
globalState
globalState,
rootScope,
);
expect(setTime.mock.calls.length).toBe(2);

View file

@ -23,6 +23,7 @@ import moment from 'moment';
import { subscribeWithScope } from 'ui/utils/subscribe_with_scope';
import chrome from 'ui/chrome';
import { RefreshInterval, TimeRange, TimefilterContract } from 'src/plugins/data/public';
import { Subscription } from 'rxjs';
// TODO
// remove everything underneath once globalState is no longer an angular service
@ -40,49 +41,62 @@ export function getTimefilterConfig() {
};
}
export const registerTimefilterWithGlobalStateFactory = (
timefilter: TimefilterContract,
globalState: any,
$rootScope: IScope
) => {
// settings have to be re-fetched here, to make sure that settings changed by overrideLocalDefault are taken into account.
const config = getTimefilterConfig();
timefilter.setTime(_.defaults(globalState.time || {}, config.timeDefaults));
timefilter.setRefreshInterval(
_.defaults(globalState.refreshInterval || {}, config.refreshIntervalDefaults)
);
globalState.on('fetch_with_changes', () => {
// clone and default to {} in one
const newTime: TimeRange = _.defaults({}, globalState.time, config.timeDefaults);
const newRefreshInterval: RefreshInterval = _.defaults(
{},
globalState.refreshInterval,
config.refreshIntervalDefaults
);
if (newTime) {
if (newTime.to) newTime.to = convertISO8601(newTime.to);
if (newTime.from) newTime.from = convertISO8601(newTime.from);
}
timefilter.setTime(newTime);
timefilter.setRefreshInterval(newRefreshInterval);
});
const updateGlobalStateWithTime = () => {
globalState.time = timefilter.getTime();
globalState.refreshInterval = timefilter.getRefreshInterval();
globalState.save();
};
const subscriptions = new Subscription();
subscriptions.add(
subscribeWithScope($rootScope, timefilter.getRefreshIntervalUpdate$(), {
next: updateGlobalStateWithTime,
})
);
subscriptions.add(
subscribeWithScope($rootScope, timefilter.getTimeUpdate$(), {
next: updateGlobalStateWithTime,
})
);
$rootScope.$on('$destroy', () => {
subscriptions.unsubscribe();
});
};
// Currently some parts of Kibana (index patterns, timefilter) rely on addSetupWork in the uiRouter
// and require it to be executed to properly function.
// This function is exposed for applications that do not use uiRoutes like APM
// Kibana issue https://github.com/elastic/kibana/issues/19110 tracks the removal of this dependency on uiRouter
export const registerTimefilterWithGlobalState = _.once(
(timefilter: TimefilterContract, globalState: any, $rootScope: IScope) => {
// settings have to be re-fetched here, to make sure that settings changed by overrideLocalDefault are taken into account.
const config = getTimefilterConfig();
timefilter.setTime(_.defaults(globalState.time || {}, config.timeDefaults));
timefilter.setRefreshInterval(
_.defaults(globalState.refreshInterval || {}, config.refreshIntervalDefaults)
);
globalState.on('fetch_with_changes', () => {
// clone and default to {} in one
const newTime: TimeRange = _.defaults({}, globalState.time, config.timeDefaults);
const newRefreshInterval: RefreshInterval = _.defaults(
{},
globalState.refreshInterval,
config.refreshIntervalDefaults
);
if (newTime) {
if (newTime.to) newTime.to = convertISO8601(newTime.to);
if (newTime.from) newTime.from = convertISO8601(newTime.from);
}
timefilter.setTime(newTime);
timefilter.setRefreshInterval(newRefreshInterval);
});
const updateGlobalStateWithTime = () => {
globalState.time = timefilter.getTime();
globalState.refreshInterval = timefilter.getRefreshInterval();
globalState.save();
};
subscribeWithScope($rootScope, timefilter.getRefreshIntervalUpdate$(), {
next: updateGlobalStateWithTime,
});
subscribeWithScope($rootScope, timefilter.getTimeUpdate$(), {
next: updateGlobalStateWithTime,
});
}
);
export const registerTimefilterWithGlobalState = _.once(registerTimefilterWithGlobalStateFactory);

View file

@ -115,7 +115,6 @@ const VisFiltersProvider = (getAppState, $timeout) => {
}
};
return {
pushFilters,
};

View file

@ -1,8 +1,10 @@
.dshDashboardViewport {
height: 100%;
width: 100%;
background-color: $euiColorEmptyShade;
}
.dshDashboardViewport-withMargins {
width: 100%;
height: 100%;
}

View file

@ -25,6 +25,7 @@ export default function ({ getService, getPageObjects }) {
const kibanaServer = getService('kibanaServer');
const PageObjects = getPageObjects(['dashboard', 'common']);
const browser = getService('browser');
const globalNav = getService('globalNav');
describe('embed mode', () => {
before(async () => {
@ -38,8 +39,8 @@ export default function ({ getService, getPageObjects }) {
});
it('hides the chrome', async () => {
const isChromeVisible = await PageObjects.common.isChromeVisible();
expect(isChromeVisible).to.be(true);
const globalNavShown = await globalNav.exists();
expect(globalNavShown).to.be(true);
const currentUrl = await browser.getCurrentUrl();
const newUrl = currentUrl + '&embed=true';
@ -48,8 +49,8 @@ export default function ({ getService, getPageObjects }) {
await browser.get(newUrl.toString(), useTimeStamp);
await retry.try(async () => {
const isChromeHidden = await PageObjects.common.isChromeHidden();
expect(isChromeHidden).to.be(true);
const globalNavHidden = !(await globalNav.exists());
expect(globalNavHidden).to.be(true);
});
});

View file

@ -37,6 +37,8 @@ import 'ui/agg_response';
import 'ui/agg_types';
import 'leaflet';
import { npStart } from 'ui/new_platform';
import { localApplicationService } from 'plugins/kibana/local_application_service';
import { showAppRedirectNotification } from 'ui/notify';
import { DashboardConstants, createDashboardEditUrl } from 'plugins/kibana/dashboard/dashboard_constants';
@ -44,6 +46,8 @@ import { DashboardConstants, createDashboardEditUrl } from 'plugins/kibana/dashb
uiModules.get('kibana')
.config(dashboardConfigProvider => dashboardConfigProvider.turnHideWriteControlsOn());
localApplicationService.attachToAngular(routes);
routes.enable();
routes.otherwise({ redirectTo: defaultUrl() });

View file

@ -19,6 +19,7 @@ import { SavedObjectRegistryProvider } from 'ui/saved_objects/saved_object_regis
import { npSetup, npStart } from 'ui/new_platform';
import { Storage } from '../../../../../src/plugins/kibana_utils/public';
import { start as data } from '../../../../../src/legacy/core_plugins/data/public/legacy';
import { start as navigation } from '../../../../../src/legacy/core_plugins/navigation/public/legacy';
import { GraphPlugin } from './plugin';
// @ts-ignore
@ -52,6 +53,7 @@ async function getAngularInjectedDependencies(): Promise<LegacyAngularInjectedDe
instance.start(npStart.core, {
data,
npData: npStart.plugins.data,
navigation,
__LEGACY: {
angularDependencies: await getAngularInjectedDependencies(),
},

View file

@ -9,10 +9,12 @@ import { CoreSetup, CoreStart, Plugin, SavedObjectsClientContract } from 'src/co
import { DataStart } from 'src/legacy/core_plugins/data/public';
import { Plugin as DataPlugin } from 'src/plugins/data/public';
import { LegacyAngularInjectedDependencies } from './render_app';
import { NavigationStart } from '../../../../../src/legacy/core_plugins/navigation/public';
export interface GraphPluginStartDependencies {
data: DataStart;
npData: ReturnType<DataPlugin['start']>;
navigation: NavigationStart;
}
export interface GraphPluginSetupDependencies {
@ -30,6 +32,7 @@ export interface GraphPluginStartDependencies {
export class GraphPlugin implements Plugin {
private dataStart: DataStart | null = null;
private navigationStart: NavigationStart | null = null;
private npDataStart: ReturnType<DataPlugin['start']> | null = null;
private savedObjectsClient: SavedObjectsClientContract | null = null;
private angularDependencies: LegacyAngularInjectedDependencies | null = null;
@ -42,6 +45,7 @@ export class GraphPlugin implements Plugin {
const { renderApp } = await import('./render_app');
return renderApp({
...params,
navigation: this.navigationStart!,
npData: this.npDataStart!,
savedObjectsClient: this.savedObjectsClient!,
xpackInfo,
@ -66,9 +70,9 @@ export class GraphPlugin implements Plugin {
start(
core: CoreStart,
{ data, npData, __LEGACY: { angularDependencies } }: GraphPluginStartDependencies
{ data, npData, navigation, __LEGACY: { angularDependencies } }: GraphPluginStartDependencies
) {
// TODO is this really the right way? I though the app context would give us those
this.navigationStart = navigation;
this.dataStart = data;
this.npDataStart = npData;
this.angularDependencies = angularDependencies;

View file

@ -25,6 +25,7 @@ import { DataStart } from 'src/legacy/core_plugins/data/public';
import {
AppMountContext,
ChromeStart,
LegacyCoreStart,
SavedObjectsClientContract,
ToastsStart,
UiSettingsClientContract,
@ -32,6 +33,7 @@ import {
// @ts-ignore
import { initGraphApp } from './app';
import { Plugin as DataPlugin } from '../../../../../src/plugins/data/public';
import { NavigationStart } from '../../../../../src/legacy/core_plugins/navigation/public';
/**
* These are dependencies of the Graph app besides the base dependencies
@ -44,6 +46,7 @@ export interface GraphDependencies extends LegacyAngularInjectedDependencies {
appBasePath: string;
capabilities: Record<string, boolean | Record<string, boolean>>;
coreStart: AppMountContext['core'];
navigation: NavigationStart;
chrome: ChromeStart;
config: UiSettingsClientContract;
toastNotifications: ToastsStart;
@ -75,8 +78,8 @@ export interface LegacyAngularInjectedDependencies {
}
export const renderApp = ({ appBasePath, element, ...deps }: GraphDependencies) => {
const graphAngularModule = createLocalAngularModule(deps.coreStart);
configureAppAngularModule(graphAngularModule);
const graphAngularModule = createLocalAngularModule(deps.navigation);
configureAppAngularModule(graphAngularModule, deps.coreStart as LegacyCoreStart, true);
initGraphApp(graphAngularModule, deps);
const $injector = mountGraphApp(appBasePath, element);
return () => $injector.get('$rootScope').$destroy();
@ -104,9 +107,9 @@ function mountGraphApp(appBasePath: string, element: HTMLElement) {
return $injector;
}
function createLocalAngularModule(core: AppMountContext['core']) {
function createLocalAngularModule(navigation: NavigationStart) {
createLocalI18nModule();
createLocalTopNavModule();
createLocalTopNavModule(navigation);
createLocalConfirmModalModule();
const graphAngularModule = angular.module(moduleName, [
@ -125,11 +128,11 @@ function createLocalConfirmModalModule() {
.directive('confirmModal', reactDirective => reactDirective(EuiConfirmModal));
}
function createLocalTopNavModule() {
function createLocalTopNavModule(navigation: NavigationStart) {
angular
.module('graphTopNav', ['react'])
.directive('kbnTopNav', createTopNavDirective)
.directive('kbnTopNavHelper', createTopNavHelper);
.directive('kbnTopNavHelper', createTopNavHelper(navigation.ui));
}
function createLocalI18nModule() {