mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
parent
c9ede92f4a
commit
eebf010924
31 changed files with 681 additions and 121 deletions
|
@ -97,13 +97,8 @@ export default function(kibana) {
|
|||
}),
|
||||
order: -1001,
|
||||
url: `${kbnBaseUrl}#/dashboards`,
|
||||
// The subUrlBase is the common substring of all urls for this app. If not given, it defaults to the url
|
||||
// above. This app has to use a different subUrlBase, in addition to the url above, because "#/dashboard"
|
||||
// routes to a page that creates a new dashboard. When we introduced a landing page, we needed to change
|
||||
// the url above in order to preserve the original url for BWC. The subUrlBase helps the Chrome api nav
|
||||
// to determine what url to use for the app link.
|
||||
subUrlBase: `${kbnBaseUrl}#/dashboard`,
|
||||
euiIconType: 'dashboardApp',
|
||||
disableSubUrlTracking: true,
|
||||
category: DEFAULT_APP_CATEGORIES.analyze,
|
||||
},
|
||||
{
|
||||
|
|
|
@ -17,8 +17,15 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
const topLevelConfig = require('../../../../../.eslintrc.js');
|
||||
const path = require('path');
|
||||
|
||||
const topLevelRestricedZones = topLevelConfig.overrides.find(
|
||||
override =>
|
||||
override.files[0] === '**/*.{js,ts,tsx}' &&
|
||||
Object.keys(override.rules)[0] === '@kbn/eslint/no-restricted-paths'
|
||||
).rules['@kbn/eslint/no-restricted-paths'][1].zones;
|
||||
|
||||
/**
|
||||
* Builds custom restricted paths configuration for the shimmed plugins within the kibana plugin.
|
||||
* These custom rules extend the default checks in the top level `eslintrc.js` by also checking two other things:
|
||||
|
@ -28,34 +35,37 @@ const path = require('path');
|
|||
* @returns zones configuration for the no-restricted-paths linter
|
||||
*/
|
||||
function buildRestrictedPaths(shimmedPlugins) {
|
||||
return shimmedPlugins.map(shimmedPlugin => ([{
|
||||
target: [
|
||||
`src/legacy/core_plugins/kibana/public/${shimmedPlugin}/np_ready/**/*`,
|
||||
],
|
||||
from: [
|
||||
'ui/**/*',
|
||||
'src/legacy/ui/**/*',
|
||||
'src/legacy/core_plugins/kibana/public/**/*',
|
||||
'src/legacy/core_plugins/data/public/**/*',
|
||||
'!src/legacy/core_plugins/data/public/index.ts',
|
||||
`!src/legacy/core_plugins/kibana/public/${shimmedPlugin}/**/*`,
|
||||
],
|
||||
allowSameFolder: false,
|
||||
errorMessage: `${shimmedPlugin} is a shimmed plugin that is not allowed to import modules from the legacy platform. If you need legacy modules for the transition period, import them either in the legacy_imports, kibana_services or index module.`,
|
||||
}, {
|
||||
target: [
|
||||
'src/**/*',
|
||||
`!src/legacy/core_plugins/kibana/public/${shimmedPlugin}/**/*`,
|
||||
'x-pack/**/*',
|
||||
],
|
||||
from: [
|
||||
`src/legacy/core_plugins/kibana/public/${shimmedPlugin}/**/*`,
|
||||
`!src/legacy/core_plugins/kibana/public/${shimmedPlugin}/index.ts`,
|
||||
`!src/legacy/core_plugins/kibana/public/${shimmedPlugin}/legacy.ts`,
|
||||
],
|
||||
allowSameFolder: false,
|
||||
errorMessage: `kibana/public/${shimmedPlugin} is behaving like a NP plugin and does not allow deep imports. If you need something from within ${shimmedPlugin} in another plugin, consider re-exporting it from the top level index module`,
|
||||
}])).reduce((acc, part) => [...acc, ...part], []);
|
||||
return shimmedPlugins
|
||||
.map(shimmedPlugin => [
|
||||
{
|
||||
target: [`src/legacy/core_plugins/kibana/public/${shimmedPlugin}/np_ready/**/*`],
|
||||
from: [
|
||||
'ui/**/*',
|
||||
'src/legacy/ui/**/*',
|
||||
'src/legacy/core_plugins/kibana/public/**/*',
|
||||
'src/legacy/core_plugins/data/public/**/*',
|
||||
'!src/legacy/core_plugins/data/public/index.ts',
|
||||
`!src/legacy/core_plugins/kibana/public/${shimmedPlugin}/**/*`,
|
||||
],
|
||||
allowSameFolder: false,
|
||||
errorMessage: `${shimmedPlugin} is a shimmed plugin that is not allowed to import modules from the legacy platform. If you need legacy modules for the transition period, import them either in the legacy_imports, kibana_services or index module.`,
|
||||
},
|
||||
{
|
||||
target: [
|
||||
'src/**/*',
|
||||
`!src/legacy/core_plugins/kibana/public/${shimmedPlugin}/**/*`,
|
||||
'x-pack/**/*',
|
||||
],
|
||||
from: [
|
||||
`src/legacy/core_plugins/kibana/public/${shimmedPlugin}/**/*`,
|
||||
`!src/legacy/core_plugins/kibana/public/${shimmedPlugin}/index.ts`,
|
||||
`!src/legacy/core_plugins/kibana/public/${shimmedPlugin}/legacy.ts`,
|
||||
],
|
||||
allowSameFolder: false,
|
||||
errorMessage: `kibana/public/${shimmedPlugin} is behaving like a NP plugin and does not allow deep imports. If you need something from within ${shimmedPlugin} in another plugin, consider re-exporting it from the top level index module`,
|
||||
},
|
||||
])
|
||||
.reduce((acc, part) => [...acc, ...part], []);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
|
@ -66,7 +76,9 @@ module.exports = {
|
|||
'error',
|
||||
{
|
||||
basePath: path.resolve(__dirname, '../../../../../'),
|
||||
zones: buildRestrictedPaths(['visualize', 'discover', 'dashboard', 'devTools', 'home']),
|
||||
zones: topLevelRestricedZones.concat(
|
||||
buildRestrictedPaths(['visualize', 'discover', 'dashboard', 'devTools', 'home'])
|
||||
),
|
||||
},
|
||||
],
|
||||
},
|
||||
|
|
|
@ -41,6 +41,7 @@ async function getAngularDependencies(): Promise<LegacyAngularInjectedDependenci
|
|||
const instance = plugin({} as PluginInitializerContext);
|
||||
instance.setup(npSetup.core, {
|
||||
...npSetup.plugins,
|
||||
npData: npSetup.plugins.data,
|
||||
__LEGACY: {
|
||||
getAngularDependencies,
|
||||
},
|
||||
|
|
|
@ -73,7 +73,6 @@ export const renderApp = (element: HTMLElement, appBasePath: string, deps: Rende
|
|||
angularModuleInstance = createLocalAngularModule(deps.core, deps.navigation);
|
||||
// global routing stuff
|
||||
configureAppAngularModule(angularModuleInstance, deps.core as LegacyCoreStart, true);
|
||||
// custom routing stuff
|
||||
initDashboardApp(angularModuleInstance, deps);
|
||||
}
|
||||
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
import { searchSourceMock } from '../../../../../../../plugins/data/public/search/search_source/mocks';
|
||||
import { searchSourceMock } from '../../../../../../../plugins/data/public/mocks';
|
||||
import { SavedObjectDashboard } from '../../saved_dashboard/saved_dashboard';
|
||||
|
||||
export function getSavedDashboardMock(
|
||||
|
|
|
@ -17,6 +17,7 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
import {
|
||||
App,
|
||||
CoreSetup,
|
||||
|
@ -28,7 +29,10 @@ import {
|
|||
import { i18n } from '@kbn/i18n';
|
||||
import { RenderDeps } from './np_ready/application';
|
||||
import { DataStart } from '../../../data/public';
|
||||
import { DataPublicPluginStart as NpDataStart } from '../../../../../plugins/data/public';
|
||||
import {
|
||||
DataPublicPluginStart as NpDataStart,
|
||||
DataPublicPluginSetup as NpDataSetup,
|
||||
} from '../../../../../plugins/data/public';
|
||||
import { IEmbeddableStart } from '../../../../../plugins/embeddable/public';
|
||||
import { Storage } from '../../../../../plugins/kibana_utils/public';
|
||||
import { NavigationPublicPluginStart as NavigationStart } from '../../../../../plugins/navigation/public';
|
||||
|
@ -38,8 +42,13 @@ import {
|
|||
HomePublicPluginSetup,
|
||||
FeatureCatalogueCategory,
|
||||
} from '../../../../../plugins/home/public';
|
||||
import { KibanaLegacySetup } from '../../../../../plugins/kibana_legacy/public';
|
||||
import {
|
||||
AngularRenderedAppUpdater,
|
||||
KibanaLegacySetup,
|
||||
} from '../../../../../plugins/kibana_legacy/public';
|
||||
import { createSavedDashboardLoader } from './saved_dashboard/saved_dashboards';
|
||||
import { createKbnUrlTracker } from '../../../../../plugins/kibana_utils/public';
|
||||
import { getQueryStateContainer } from '../../../../../plugins/data/public';
|
||||
|
||||
export interface LegacyAngularInjectedDependencies {
|
||||
dashboardConfig: any;
|
||||
|
@ -59,6 +68,7 @@ export interface DashboardPluginSetupDependencies {
|
|||
};
|
||||
home: HomePublicPluginSetup;
|
||||
kibana_legacy: KibanaLegacySetup;
|
||||
npData: NpDataSetup;
|
||||
}
|
||||
|
||||
export class DashboardPlugin implements Plugin {
|
||||
|
@ -70,10 +80,38 @@ export class DashboardPlugin implements Plugin {
|
|||
share: SharePluginStart;
|
||||
} | null = null;
|
||||
|
||||
private appStateUpdater = new BehaviorSubject<AngularRenderedAppUpdater>(() => ({}));
|
||||
private stopUrlTracking: (() => void) | undefined = undefined;
|
||||
|
||||
public setup(
|
||||
core: CoreSetup,
|
||||
{ __LEGACY: { getAngularDependencies }, home, kibana_legacy }: DashboardPluginSetupDependencies
|
||||
{
|
||||
__LEGACY: { getAngularDependencies },
|
||||
home,
|
||||
kibana_legacy,
|
||||
npData,
|
||||
}: DashboardPluginSetupDependencies
|
||||
) {
|
||||
const { querySyncStateContainer, stop: stopQuerySyncStateContainer } = getQueryStateContainer(
|
||||
npData.query
|
||||
);
|
||||
const { appMounted, appUnMounted, stop: stopUrlTracker } = createKbnUrlTracker({
|
||||
baseUrl: core.http.basePath.prepend('/app/kibana'),
|
||||
defaultSubUrl: '#/dashboards',
|
||||
storageKey: 'lastUrl:dashboard',
|
||||
navLinkUpdater$: this.appStateUpdater,
|
||||
toastNotifications: core.notifications.toasts,
|
||||
stateParams: [
|
||||
{
|
||||
kbnUrlKey: '_g',
|
||||
stateUpdate$: querySyncStateContainer.state$,
|
||||
},
|
||||
],
|
||||
});
|
||||
this.stopUrlTracking = () => {
|
||||
stopQuerySyncStateContainer();
|
||||
stopUrlTracker();
|
||||
};
|
||||
const app: App = {
|
||||
id: '',
|
||||
title: 'Dashboards',
|
||||
|
@ -81,6 +119,7 @@ export class DashboardPlugin implements Plugin {
|
|||
if (this.startDependencies === null) {
|
||||
throw new Error('not started yet');
|
||||
}
|
||||
appMounted();
|
||||
const {
|
||||
savedObjectsClient,
|
||||
embeddables,
|
||||
|
@ -114,10 +153,20 @@ export class DashboardPlugin implements Plugin {
|
|||
localStorage: new Storage(localStorage),
|
||||
};
|
||||
const { renderApp } = await import('./np_ready/application');
|
||||
return renderApp(params.element, params.appBasePath, deps);
|
||||
const unmount = renderApp(params.element, params.appBasePath, deps);
|
||||
return () => {
|
||||
unmount();
|
||||
appUnMounted();
|
||||
};
|
||||
},
|
||||
};
|
||||
kibana_legacy.registerLegacyApp({ ...app, id: 'dashboard' });
|
||||
kibana_legacy.registerLegacyApp({
|
||||
...app,
|
||||
id: 'dashboard',
|
||||
// only register the updater in once app, otherwise all updates would happen twice
|
||||
updater$: this.appStateUpdater.asObservable(),
|
||||
navLinkId: 'kibana:dashboard',
|
||||
});
|
||||
kibana_legacy.registerLegacyApp({ ...app, id: 'dashboards' });
|
||||
|
||||
home.featureCatalogue.register({
|
||||
|
@ -147,4 +196,10 @@ export class DashboardPlugin implements Plugin {
|
|||
share,
|
||||
};
|
||||
}
|
||||
|
||||
stop() {
|
||||
if (this.stopUrlTracking) {
|
||||
this.stopUrlTracking();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -22,7 +22,7 @@ import PropTypes from 'prop-types';
|
|||
import { Instruction } from './instruction';
|
||||
import { ParameterForm } from './parameter_form';
|
||||
import { Content } from './content';
|
||||
import { getDisplayText } from '../../../../../../../../plugins/home/server/tutorials/instructions/instruction_variant';
|
||||
import { getDisplayText } from '../../../../../../../../plugins/home/public';
|
||||
import {
|
||||
EuiTabs,
|
||||
EuiTab,
|
||||
|
|
|
@ -79,6 +79,17 @@ export class LocalApplicationService {
|
|||
})();
|
||||
},
|
||||
});
|
||||
|
||||
if (app.updater$) {
|
||||
app.updater$.subscribe(updater => {
|
||||
const updatedFields = updater(app);
|
||||
if (updatedFields && updatedFields.activeUrl) {
|
||||
npStart.core.chrome.navLinks.update(app.navLinkId || app.id, {
|
||||
url: updatedFields.activeUrl,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
npStart.plugins.kibana_legacy.getForwards().forEach(({ legacyAppId, newAppId, keepPrefix }) => {
|
||||
|
|
|
@ -146,7 +146,7 @@ export function initChromeNavApi(chrome: any, internals: NavInternals) {
|
|||
// link.active and link.lastUrl properties
|
||||
coreNavLinks
|
||||
.getAll()
|
||||
.filter(link => link.subUrlBase)
|
||||
.filter(link => link.subUrlBase && !link.disableSubUrlTracking)
|
||||
.forEach(link => {
|
||||
coreNavLinks.update(link.id, {
|
||||
subUrlBase: relativeToAbsolute(chrome.addBasePath(link.subUrlBase)),
|
||||
|
|
|
@ -101,6 +101,8 @@ const createStartContract = (): Start => {
|
|||
return startContract;
|
||||
};
|
||||
|
||||
export { searchSourceMock } from './search/mocks';
|
||||
|
||||
export const dataPluginMock = {
|
||||
createSetupContract,
|
||||
createStartContract,
|
||||
|
|
|
@ -17,5 +17,5 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
export { syncQuery } from './sync_query';
|
||||
export { syncQuery, getQueryStateContainer } from './sync_query';
|
||||
export { syncAppFilters } from './sync_app_filters';
|
||||
|
|
|
@ -31,7 +31,7 @@ import {
|
|||
import { QueryService, QueryStart } from '../query_service';
|
||||
import { StubBrowserStorage } from 'test_utils/stub_browser_storage';
|
||||
import { TimefilterContract } from '../timefilter';
|
||||
import { QuerySyncState, syncQuery } from './sync_query';
|
||||
import { getQueryStateContainer, QuerySyncState, syncQuery } from './sync_query';
|
||||
|
||||
const setupMock = coreMock.createSetup();
|
||||
const startMock = coreMock.createStart();
|
||||
|
@ -163,4 +163,69 @@ describe('sync_query', () => {
|
|||
expect(spy).not.toBeCalled();
|
||||
stop();
|
||||
});
|
||||
|
||||
describe('getQueryStateContainer', () => {
|
||||
test('state is initialized with state from query service', () => {
|
||||
const { stop, querySyncStateContainer, initialState } = getQueryStateContainer(
|
||||
queryServiceStart
|
||||
);
|
||||
expect(querySyncStateContainer.getState()).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"filters": Array [],
|
||||
"refreshInterval": Object {
|
||||
"pause": true,
|
||||
"value": 0,
|
||||
},
|
||||
"time": Object {
|
||||
"from": "now-15m",
|
||||
"to": "now",
|
||||
},
|
||||
}
|
||||
`);
|
||||
expect(initialState).toEqual(querySyncStateContainer.getState());
|
||||
stop();
|
||||
});
|
||||
|
||||
test('state takes initial overrides into account', () => {
|
||||
const { stop, querySyncStateContainer, initialState } = getQueryStateContainer(
|
||||
queryServiceStart,
|
||||
{
|
||||
time: { from: 'now-99d', to: 'now' },
|
||||
}
|
||||
);
|
||||
expect(querySyncStateContainer.getState().time).toEqual({
|
||||
from: 'now-99d',
|
||||
to: 'now',
|
||||
});
|
||||
expect(initialState).toEqual(querySyncStateContainer.getState());
|
||||
stop();
|
||||
});
|
||||
|
||||
test('when filters change, state container contains updated global filters', () => {
|
||||
const { stop, querySyncStateContainer } = getQueryStateContainer(queryServiceStart);
|
||||
filterManager.setFilters([gF, aF]);
|
||||
expect(querySyncStateContainer.getState().filters).toHaveLength(1);
|
||||
stop();
|
||||
});
|
||||
|
||||
test('when time range changes, state container contains updated time range', () => {
|
||||
const { stop, querySyncStateContainer } = getQueryStateContainer(queryServiceStart);
|
||||
timefilter.setTime({ from: 'now-30m', to: 'now' });
|
||||
expect(querySyncStateContainer.getState().time).toEqual({
|
||||
from: 'now-30m',
|
||||
to: 'now',
|
||||
});
|
||||
stop();
|
||||
});
|
||||
|
||||
test('when refresh interval changes, state container contains updated refresh interval', () => {
|
||||
const { stop, querySyncStateContainer } = getQueryStateContainer(queryServiceStart);
|
||||
timefilter.setRefreshInterval({ pause: true, value: 100 });
|
||||
expect(querySyncStateContainer.getState().refreshInterval).toEqual({
|
||||
pause: true,
|
||||
value: 100,
|
||||
});
|
||||
stop();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -27,7 +27,7 @@ import {
|
|||
} from '../../../../kibana_utils/public';
|
||||
import { COMPARE_ALL_OPTIONS, compareFilters } from '../filter_manager/lib/compare_filters';
|
||||
import { esFilters, RefreshInterval, TimeRange } from '../../../common';
|
||||
import { QueryStart } from '../query_service';
|
||||
import { QuerySetup, QueryStart } from '../query_service';
|
||||
|
||||
const GLOBAL_STATE_STORAGE_KEY = '_g';
|
||||
|
||||
|
@ -40,16 +40,11 @@ export interface QuerySyncState {
|
|||
/**
|
||||
* Helper utility to set up syncing between query services and url's '_g' query param
|
||||
*/
|
||||
export const syncQuery = (
|
||||
{ timefilter: { timefilter }, filterManager }: QueryStart,
|
||||
urlStateStorage: IKbnUrlStateStorage
|
||||
) => {
|
||||
const defaultState: QuerySyncState = {
|
||||
time: timefilter.getTime(),
|
||||
refreshInterval: timefilter.getRefreshInterval(),
|
||||
filters: filterManager.getGlobalFilters(),
|
||||
};
|
||||
|
||||
export const syncQuery = (queryStart: QueryStart, urlStateStorage: IKbnUrlStateStorage) => {
|
||||
const {
|
||||
timefilter: { timefilter },
|
||||
filterManager,
|
||||
} = queryStart;
|
||||
// retrieve current state from `_g` url
|
||||
const initialStateFromUrl = urlStateStorage.get<QuerySyncState>(GLOBAL_STATE_STORAGE_KEY);
|
||||
|
||||
|
@ -58,10 +53,82 @@ export const syncQuery = (
|
|||
initialStateFromUrl && Object.keys(initialStateFromUrl).length
|
||||
);
|
||||
|
||||
// prepare initial state, whatever was in URL takes precedences over current state in services
|
||||
const {
|
||||
querySyncStateContainer,
|
||||
stop: stopPullQueryState,
|
||||
initialState,
|
||||
} = getQueryStateContainer(queryStart, initialStateFromUrl || {});
|
||||
|
||||
const pushQueryStateSubscription = querySyncStateContainer.state$.subscribe(
|
||||
({ time, filters: globalFilters, refreshInterval }) => {
|
||||
// cloneDeep is required because services are mutating passed objects
|
||||
// and state in state container is frozen
|
||||
if (time && !_.isEqual(time, timefilter.getTime())) {
|
||||
timefilter.setTime(_.cloneDeep(time));
|
||||
}
|
||||
|
||||
if (refreshInterval && !_.isEqual(refreshInterval, timefilter.getRefreshInterval())) {
|
||||
timefilter.setRefreshInterval(_.cloneDeep(refreshInterval));
|
||||
}
|
||||
|
||||
if (
|
||||
globalFilters &&
|
||||
!compareFilters(globalFilters, filterManager.getGlobalFilters(), COMPARE_ALL_OPTIONS)
|
||||
) {
|
||||
filterManager.setGlobalFilters(_.cloneDeep(globalFilters));
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// if there weren't any initial state in url,
|
||||
// then put _g key into url
|
||||
if (!initialStateFromUrl) {
|
||||
urlStateStorage.set<QuerySyncState>(GLOBAL_STATE_STORAGE_KEY, initialState, {
|
||||
replace: true,
|
||||
});
|
||||
}
|
||||
|
||||
// trigger initial syncing from state container to services if needed
|
||||
querySyncStateContainer.set(initialState);
|
||||
|
||||
const { start, stop: stopSyncState } = syncState({
|
||||
stateStorage: urlStateStorage,
|
||||
stateContainer: {
|
||||
...querySyncStateContainer,
|
||||
set: state => {
|
||||
if (state) {
|
||||
// syncState utils requires to handle incoming "null" value
|
||||
querySyncStateContainer.set(state);
|
||||
}
|
||||
},
|
||||
},
|
||||
storageKey: GLOBAL_STATE_STORAGE_KEY,
|
||||
});
|
||||
|
||||
start();
|
||||
return {
|
||||
stop: () => {
|
||||
stopSyncState();
|
||||
pushQueryStateSubscription.unsubscribe();
|
||||
stopPullQueryState();
|
||||
},
|
||||
hasInheritedQueryFromUrl,
|
||||
};
|
||||
};
|
||||
|
||||
export const getQueryStateContainer = (
|
||||
{ timefilter: { timefilter }, filterManager }: QuerySetup,
|
||||
initialStateOverrides: Partial<QuerySyncState> = {}
|
||||
) => {
|
||||
const defaultState: QuerySyncState = {
|
||||
time: timefilter.getTime(),
|
||||
refreshInterval: timefilter.getRefreshInterval(),
|
||||
filters: filterManager.getGlobalFilters(),
|
||||
};
|
||||
|
||||
const initialState: QuerySyncState = {
|
||||
...defaultState,
|
||||
...initialStateFromUrl,
|
||||
...initialStateOverrides,
|
||||
};
|
||||
|
||||
// create state container, which will be used for syncing with syncState() util
|
||||
|
@ -109,59 +176,13 @@ export const syncQuery = (
|
|||
.subscribe(newGlobalFilters => {
|
||||
querySyncStateContainer.transitions.setFilters(newGlobalFilters);
|
||||
}),
|
||||
querySyncStateContainer.state$.subscribe(
|
||||
({ time, filters: globalFilters, refreshInterval }) => {
|
||||
// cloneDeep is required because services are mutating passed objects
|
||||
// and state in state container is frozen
|
||||
if (time && !_.isEqual(time, timefilter.getTime())) {
|
||||
timefilter.setTime(_.cloneDeep(time));
|
||||
}
|
||||
|
||||
if (refreshInterval && !_.isEqual(refreshInterval, timefilter.getRefreshInterval())) {
|
||||
timefilter.setRefreshInterval(_.cloneDeep(refreshInterval));
|
||||
}
|
||||
|
||||
if (
|
||||
globalFilters &&
|
||||
!compareFilters(globalFilters, filterManager.getGlobalFilters(), COMPARE_ALL_OPTIONS)
|
||||
) {
|
||||
filterManager.setGlobalFilters(_.cloneDeep(globalFilters));
|
||||
}
|
||||
}
|
||||
),
|
||||
];
|
||||
|
||||
// if there weren't any initial state in url,
|
||||
// then put _g key into url
|
||||
if (!initialStateFromUrl) {
|
||||
urlStateStorage.set<QuerySyncState>(GLOBAL_STATE_STORAGE_KEY, initialState, {
|
||||
replace: true,
|
||||
});
|
||||
}
|
||||
|
||||
// trigger initial syncing from state container to services if needed
|
||||
querySyncStateContainer.set(initialState);
|
||||
|
||||
const { start, stop } = syncState({
|
||||
stateStorage: urlStateStorage,
|
||||
stateContainer: {
|
||||
...querySyncStateContainer,
|
||||
set: state => {
|
||||
if (state) {
|
||||
// syncState utils requires to handle incoming "null" value
|
||||
querySyncStateContainer.set(state);
|
||||
}
|
||||
},
|
||||
},
|
||||
storageKey: GLOBAL_STATE_STORAGE_KEY,
|
||||
});
|
||||
|
||||
start();
|
||||
return {
|
||||
querySyncStateContainer,
|
||||
stop: () => {
|
||||
subs.forEach(s => s.unsubscribe());
|
||||
stop();
|
||||
},
|
||||
hasInheritedQueryFromUrl,
|
||||
initialState,
|
||||
};
|
||||
};
|
||||
|
|
|
@ -17,6 +17,8 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
export * from './search_source/mocks';
|
||||
|
||||
export const searchSetupMock = {
|
||||
registerSearchStrategyContext: jest.fn(),
|
||||
registerSearchStrategyProvider: jest.fn(),
|
||||
|
|
|
@ -26,6 +26,7 @@ export {
|
|||
HomePublicPluginStart,
|
||||
} from './plugin';
|
||||
export { FeatureCatalogueEntry, FeatureCatalogueCategory, Environment } from './services';
|
||||
export * from '../common/instruction_variant';
|
||||
import { HomePublicPlugin } from './plugin';
|
||||
|
||||
export const plugin = (initializerContext: PluginInitializerContext) =>
|
||||
|
|
|
@ -36,5 +36,5 @@ export const config: PluginConfigDescriptor<ConfigSchema> = {
|
|||
|
||||
export const plugin = (initContext: PluginInitializerContext) => new HomeServerPlugin(initContext);
|
||||
|
||||
export { INSTRUCTION_VARIANT } from './tutorials/instructions/instruction_variant';
|
||||
export { INSTRUCTION_VARIANT } from '../common/instruction_variant';
|
||||
export { ArtifactsSchema, TutorialsCategory } from './services/tutorials';
|
||||
|
|
|
@ -18,7 +18,7 @@
|
|||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { INSTRUCTION_VARIANT } from './instruction_variant';
|
||||
import { INSTRUCTION_VARIANT } from '../../../common/instruction_variant';
|
||||
import { createTrycloudOption1, createTrycloudOption2 } from './onprem_cloud_instructions';
|
||||
import { getSpaceIdForBeatsTutorial } from './get_space_id_for_beats_tutorial';
|
||||
import { Platform, TutorialContext } from '../../services/tutorials/lib/tutorials_registry_types';
|
||||
|
|
|
@ -18,7 +18,7 @@
|
|||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { INSTRUCTION_VARIANT } from './instruction_variant';
|
||||
import { INSTRUCTION_VARIANT } from '../../../common/instruction_variant';
|
||||
import { createTrycloudOption1, createTrycloudOption2 } from './onprem_cloud_instructions';
|
||||
import { getSpaceIdForBeatsTutorial } from './get_space_id_for_beats_tutorial';
|
||||
import { Platform, TutorialContext } from '../../services/tutorials/lib/tutorials_registry_types';
|
||||
|
|
|
@ -18,7 +18,7 @@
|
|||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { INSTRUCTION_VARIANT } from './instruction_variant';
|
||||
import { INSTRUCTION_VARIANT } from '../../../common/instruction_variant';
|
||||
import { createTrycloudOption1, createTrycloudOption2 } from './onprem_cloud_instructions';
|
||||
import { getSpaceIdForBeatsTutorial } from './get_space_id_for_beats_tutorial';
|
||||
import { Platform, TutorialContext } from '../../services/tutorials/lib/tutorials_registry_types';
|
||||
|
|
|
@ -18,7 +18,7 @@
|
|||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { INSTRUCTION_VARIANT } from './instruction_variant';
|
||||
import { INSTRUCTION_VARIANT } from '../../../common/instruction_variant';
|
||||
import { createTrycloudOption1, createTrycloudOption2 } from './onprem_cloud_instructions';
|
||||
import { getSpaceIdForBeatsTutorial } from './get_space_id_for_beats_tutorial';
|
||||
import { Platform, TutorialContext } from '../../services/tutorials/lib/tutorials_registry_types';
|
||||
|
|
|
@ -18,7 +18,7 @@
|
|||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { INSTRUCTION_VARIANT } from './instruction_variant';
|
||||
import { INSTRUCTION_VARIANT } from '../../../common/instruction_variant';
|
||||
import { createTrycloudOption1, createTrycloudOption2 } from './onprem_cloud_instructions';
|
||||
import { getSpaceIdForBeatsTutorial } from './get_space_id_for_beats_tutorial';
|
||||
import { TutorialContext } from '../../services/tutorials/lib/tutorials_registry_types';
|
||||
|
|
|
@ -18,7 +18,7 @@
|
|||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { INSTRUCTION_VARIANT } from './instruction_variant';
|
||||
import { INSTRUCTION_VARIANT } from '../../../common/instruction_variant';
|
||||
import { createTrycloudOption1, createTrycloudOption2 } from './onprem_cloud_instructions';
|
||||
import { getSpaceIdForBeatsTutorial } from './get_space_id_for_beats_tutorial';
|
||||
import { TutorialContext } from '../../services/tutorials/lib/tutorials_registry_types';
|
||||
|
|
|
@ -19,7 +19,7 @@
|
|||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
import { INSTRUCTION_VARIANT } from '../instructions/instruction_variant';
|
||||
import { INSTRUCTION_VARIANT } from '../../../common/instruction_variant';
|
||||
import { createLogstashInstructions } from '../instructions/logstash_instructions';
|
||||
import { createCommonNetflowInstructions } from './common_instructions';
|
||||
|
||||
|
|
|
@ -19,7 +19,7 @@
|
|||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
import { INSTRUCTION_VARIANT } from '../instructions/instruction_variant';
|
||||
import { INSTRUCTION_VARIANT } from '../../../common/instruction_variant';
|
||||
import { createLogstashInstructions } from '../instructions/logstash_instructions';
|
||||
import { createCommonNetflowInstructions } from './common_instructions';
|
||||
|
||||
|
|
|
@ -19,7 +19,7 @@
|
|||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
import { INSTRUCTION_VARIANT } from '../instructions/instruction_variant';
|
||||
import { INSTRUCTION_VARIANT } from '../../../common/instruction_variant';
|
||||
import { createLogstashInstructions } from '../instructions/logstash_instructions';
|
||||
import {
|
||||
createTrycloudOption1,
|
||||
|
|
|
@ -17,8 +17,8 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
import { App, PluginInitializerContext } from 'kibana/public';
|
||||
|
||||
import { App, AppBase, PluginInitializerContext, AppUpdatableFields } from 'kibana/public';
|
||||
import { Observable } from 'rxjs';
|
||||
import { ConfigSchema } from '../config';
|
||||
|
||||
interface ForwardDefinition {
|
||||
|
@ -27,8 +27,26 @@ interface ForwardDefinition {
|
|||
keepPrefix: boolean;
|
||||
}
|
||||
|
||||
export type AngularRenderedAppUpdater = (
|
||||
app: AppBase
|
||||
) => Partial<AppUpdatableFields & { activeUrl: string }> | undefined;
|
||||
|
||||
export interface AngularRenderedApp extends App {
|
||||
/**
|
||||
* Angular rendered apps are able to update the active url in the nav link (which is currently not
|
||||
* possible for actual NP apps). When regular applications have the same functionality, this type
|
||||
* override can be removed.
|
||||
*/
|
||||
updater$?: Observable<AngularRenderedAppUpdater>;
|
||||
/**
|
||||
* If the active url is updated via the updater$ subject, the app id is assumed to be identical with
|
||||
* the nav link id. If this is not the case, it is possible to provide another nav link id here.
|
||||
*/
|
||||
navLinkId?: string;
|
||||
}
|
||||
|
||||
export class KibanaLegacyPlugin {
|
||||
private apps: App[] = [];
|
||||
private apps: AngularRenderedApp[] = [];
|
||||
private forwards: ForwardDefinition[] = [];
|
||||
|
||||
constructor(private readonly initializerContext: PluginInitializerContext<ConfigSchema>) {}
|
||||
|
@ -52,7 +70,7 @@ export class KibanaLegacyPlugin {
|
|||
*
|
||||
* @param app The app descriptor
|
||||
*/
|
||||
registerLegacyApp: (app: App) => {
|
||||
registerLegacyApp: (app: AngularRenderedApp) => {
|
||||
this.apps.push(app);
|
||||
},
|
||||
|
||||
|
|
|
@ -40,6 +40,7 @@ export {
|
|||
unhashUrl,
|
||||
unhashQuery,
|
||||
createUrlTracker,
|
||||
createKbnUrlTracker,
|
||||
createKbnUrlControls,
|
||||
getStateFromKbnUrl,
|
||||
getStatesFromKbnUrl,
|
||||
|
|
|
@ -25,4 +25,5 @@ export {
|
|||
getStatesFromKbnUrl,
|
||||
IKbnUrlControls,
|
||||
} from './kbn_url_storage';
|
||||
export { createKbnUrlTracker } from './kbn_url_tracker';
|
||||
export { createUrlTracker } from './url_tracker';
|
||||
|
|
|
@ -0,0 +1,184 @@
|
|||
/*
|
||||
* 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 { StubBrowserStorage } from 'test_utils/stub_browser_storage';
|
||||
import { createMemoryHistory, History } from 'history';
|
||||
import { createKbnUrlTracker, KbnUrlTracker } from './kbn_url_tracker';
|
||||
import { BehaviorSubject, Subject } from 'rxjs';
|
||||
import { AppBase, ToastsSetup } from 'kibana/public';
|
||||
import { coreMock } from '../../../../../core/public/mocks';
|
||||
import { unhashUrl } from './hash_unhash_url';
|
||||
|
||||
jest.mock('./hash_unhash_url', () => ({
|
||||
unhashUrl: jest.fn(x => x),
|
||||
}));
|
||||
|
||||
describe('kbnUrlTracker', () => {
|
||||
let storage: StubBrowserStorage;
|
||||
let history: History;
|
||||
let urlTracker: KbnUrlTracker;
|
||||
let state1Subject: Subject<{ key1: string }>;
|
||||
let state2Subject: Subject<{ key2: string }>;
|
||||
let navLinkUpdaterSubject: BehaviorSubject<(app: AppBase) => { activeUrl?: string } | undefined>;
|
||||
let toastService: jest.Mocked<ToastsSetup>;
|
||||
|
||||
function createTracker() {
|
||||
urlTracker = createKbnUrlTracker({
|
||||
baseUrl: '/app/test',
|
||||
defaultSubUrl: '#/start',
|
||||
storageKey: 'storageKey',
|
||||
history,
|
||||
storage,
|
||||
stateParams: [
|
||||
{
|
||||
kbnUrlKey: 'state1',
|
||||
stateUpdate$: state1Subject.asObservable(),
|
||||
},
|
||||
{
|
||||
kbnUrlKey: 'state2',
|
||||
stateUpdate$: state2Subject.asObservable(),
|
||||
},
|
||||
],
|
||||
navLinkUpdater$: navLinkUpdaterSubject,
|
||||
toastNotifications: toastService,
|
||||
});
|
||||
}
|
||||
|
||||
function getActiveNavLinkUrl() {
|
||||
return navLinkUpdaterSubject.getValue()({} as AppBase)?.activeUrl;
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
toastService = coreMock.createSetup().notifications.toasts;
|
||||
storage = new StubBrowserStorage();
|
||||
history = createMemoryHistory();
|
||||
state1Subject = new Subject<{ key1: string }>();
|
||||
state2Subject = new Subject<{ key2: string }>();
|
||||
navLinkUpdaterSubject = new BehaviorSubject<
|
||||
(app: AppBase) => { activeUrl?: string } | undefined
|
||||
>(() => undefined);
|
||||
});
|
||||
|
||||
test('do not touch nav link to default if nothing else is set', () => {
|
||||
createTracker();
|
||||
expect(getActiveNavLinkUrl()).toEqual(undefined);
|
||||
});
|
||||
|
||||
test('set nav link to session storage value if defined', () => {
|
||||
storage.setItem('storageKey', '#/deep/path');
|
||||
createTracker();
|
||||
expect(getActiveNavLinkUrl()).toEqual('/app/test#/deep/path');
|
||||
});
|
||||
|
||||
test('set nav link to default if app gets mounted', () => {
|
||||
storage.setItem('storageKey', '#/deep/path');
|
||||
createTracker();
|
||||
urlTracker.appMounted();
|
||||
expect(getActiveNavLinkUrl()).toEqual('/app/test#/start');
|
||||
});
|
||||
|
||||
test('keep nav link to default if path gets changed while app mounted', () => {
|
||||
storage.setItem('storageKey', '#/deep/path');
|
||||
createTracker();
|
||||
urlTracker.appMounted();
|
||||
history.push('/deep/path/2');
|
||||
expect(getActiveNavLinkUrl()).toEqual('/app/test#/start');
|
||||
});
|
||||
|
||||
test('change nav link to last visited url within app after unmount', () => {
|
||||
createTracker();
|
||||
urlTracker.appMounted();
|
||||
history.push('/deep/path/2');
|
||||
history.push('/deep/path/3');
|
||||
urlTracker.appUnMounted();
|
||||
expect(getActiveNavLinkUrl()).toEqual('/app/test#/deep/path/3');
|
||||
});
|
||||
|
||||
test('unhash all urls that are recorded while app is mounted', () => {
|
||||
(unhashUrl as jest.Mock).mockImplementation(x => x + '?unhashed');
|
||||
createTracker();
|
||||
urlTracker.appMounted();
|
||||
history.push('/deep/path/2');
|
||||
history.push('/deep/path/3');
|
||||
urlTracker.appUnMounted();
|
||||
expect(unhashUrl).toHaveBeenCalledTimes(2);
|
||||
expect(getActiveNavLinkUrl()).toEqual('/app/test#/deep/path/3?unhashed');
|
||||
});
|
||||
|
||||
test('show warning and use hashed url if unhashing does not work', () => {
|
||||
(unhashUrl as jest.Mock).mockImplementation(() => {
|
||||
throw new Error('unhash broke');
|
||||
});
|
||||
createTracker();
|
||||
urlTracker.appMounted();
|
||||
history.push('/deep/path/2');
|
||||
urlTracker.appUnMounted();
|
||||
expect(getActiveNavLinkUrl()).toEqual('/app/test#/deep/path/2');
|
||||
expect(toastService.addDanger).toHaveBeenCalledWith('unhash broke');
|
||||
});
|
||||
|
||||
test('change nav link back to default if app gets mounted again', () => {
|
||||
createTracker();
|
||||
urlTracker.appMounted();
|
||||
history.push('/deep/path/2');
|
||||
history.push('/deep/path/3');
|
||||
urlTracker.appUnMounted();
|
||||
urlTracker.appMounted();
|
||||
expect(getActiveNavLinkUrl()).toEqual('/app/test#/start');
|
||||
});
|
||||
|
||||
test('update state param when app is not mounted', () => {
|
||||
createTracker();
|
||||
state1Subject.next({ key1: 'abc' });
|
||||
expect(getActiveNavLinkUrl()).toMatchInlineSnapshot(`"/app/test#/start?state1=(key1:abc)"`);
|
||||
});
|
||||
|
||||
test('update state param without overwriting rest of the url when app is not mounted', () => {
|
||||
storage.setItem('storageKey', '#/deep/path?extrastate=1');
|
||||
createTracker();
|
||||
state1Subject.next({ key1: 'abc' });
|
||||
expect(getActiveNavLinkUrl()).toMatchInlineSnapshot(
|
||||
`"/app/test#/deep/path?extrastate=1&state1=(key1:abc)"`
|
||||
);
|
||||
});
|
||||
|
||||
test('not update state param when app is mounted', () => {
|
||||
createTracker();
|
||||
urlTracker.appMounted();
|
||||
state1Subject.next({ key1: 'abc' });
|
||||
expect(getActiveNavLinkUrl()).toEqual('/app/test#/start');
|
||||
});
|
||||
|
||||
test('update state param multiple times when app is not mounted', () => {
|
||||
createTracker();
|
||||
state1Subject.next({ key1: 'abc' });
|
||||
state1Subject.next({ key1: 'def' });
|
||||
expect(getActiveNavLinkUrl()).toMatchInlineSnapshot(`"/app/test#/start?state1=(key1:def)"`);
|
||||
});
|
||||
|
||||
test('update multiple state params when app is not mounted', () => {
|
||||
createTracker();
|
||||
state1Subject.next({ key1: 'abc' });
|
||||
state2Subject.next({ key2: 'def' });
|
||||
expect(getActiveNavLinkUrl()).toMatchInlineSnapshot(
|
||||
`"/app/test#/start?state1=(key1:abc)&state2=(key2:def)"`
|
||||
);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,192 @@
|
|||
/*
|
||||
* 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 { createHashHistory, History, UnregisterCallback } from 'history';
|
||||
import { BehaviorSubject, Observable, Subscription } from 'rxjs';
|
||||
import { AppBase, ToastsSetup } from 'kibana/public';
|
||||
import { setStateToKbnUrl } from './kbn_url_storage';
|
||||
import { unhashUrl } from './hash_unhash_url';
|
||||
|
||||
export interface KbnUrlTracker {
|
||||
/**
|
||||
* Callback to invoke when the app is mounted
|
||||
*/
|
||||
appMounted: () => void;
|
||||
/**
|
||||
* Callback to invoke when the app is unmounted
|
||||
*/
|
||||
appUnMounted: () => void;
|
||||
/**
|
||||
* Unregistering the url tracker. This won't reset the current state of the nav link
|
||||
*/
|
||||
stop: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Listens to history changes and optionally to global state changes and updates the nav link url of
|
||||
* a given app to point to the last visited page within the app.
|
||||
*
|
||||
* This includes the following parts:
|
||||
* * When the app is currently active, the nav link points to the configurable default url of the app.
|
||||
* * When the app is not active the last visited url is set to the nav link.
|
||||
* * When a provided observable emits a new value, the state parameter in the url of the nav link is updated
|
||||
* as long as the app is not active.
|
||||
*/
|
||||
export function createKbnUrlTracker({
|
||||
baseUrl,
|
||||
defaultSubUrl,
|
||||
storageKey,
|
||||
stateParams,
|
||||
navLinkUpdater$,
|
||||
toastNotifications,
|
||||
history,
|
||||
storage,
|
||||
}: {
|
||||
/**
|
||||
* Base url of the current app. This will be used as a prefix for the
|
||||
* nav link in the side bar
|
||||
*/
|
||||
baseUrl: string;
|
||||
/**
|
||||
* Default sub url for this app. If the app is currently active or no sub url is already stored in session storage and the app hasn't been visited yet, the nav link will be set to this url.
|
||||
*/
|
||||
defaultSubUrl: string;
|
||||
/**
|
||||
* List of URL mapped states that should get updated even when the app is not currently active
|
||||
*/
|
||||
stateParams: Array<{
|
||||
/**
|
||||
* Key of the query parameter containing the state
|
||||
*/
|
||||
kbnUrlKey: string;
|
||||
/**
|
||||
* Observable providing updates to the state
|
||||
*/
|
||||
stateUpdate$: Observable<unknown>;
|
||||
}>;
|
||||
/**
|
||||
* Key used to store the current sub url in session storage. This key should only be used for one active url tracker at any given ntime.
|
||||
*/
|
||||
storageKey: string;
|
||||
/**
|
||||
* App updater subject passed into the application definition to change nav link url.
|
||||
*/
|
||||
navLinkUpdater$: BehaviorSubject<(app: AppBase) => { activeUrl?: string } | undefined>;
|
||||
/**
|
||||
* Toast notifications service to show toasts in error cases.
|
||||
*/
|
||||
toastNotifications: ToastsSetup;
|
||||
/**
|
||||
* History object to use to track url changes. If this isn't provided, a local history instance will be created.
|
||||
*/
|
||||
history?: History;
|
||||
/**
|
||||
* Storage object to use to persist currently active url. If this isn't provided, the browser wide session storage instance will be used.
|
||||
*/
|
||||
storage?: Storage;
|
||||
}): KbnUrlTracker {
|
||||
const historyInstance = history || createHashHistory();
|
||||
const storageInstance = storage || sessionStorage;
|
||||
|
||||
// local state storing current listeners and active url
|
||||
let activeUrl: string = '';
|
||||
let unsubscribeURLHistory: UnregisterCallback | undefined;
|
||||
let unsubscribeGlobalState: Subscription[] | undefined;
|
||||
|
||||
function setNavLink(hash: string) {
|
||||
navLinkUpdater$.next(() => ({ activeUrl: baseUrl + hash }));
|
||||
}
|
||||
|
||||
function getActiveSubUrl(url: string) {
|
||||
// remove baseUrl prefix (just storing the sub url part)
|
||||
return url.substr(baseUrl.length);
|
||||
}
|
||||
|
||||
function unsubscribe() {
|
||||
if (unsubscribeURLHistory) {
|
||||
unsubscribeURLHistory();
|
||||
unsubscribeURLHistory = undefined;
|
||||
}
|
||||
|
||||
if (unsubscribeGlobalState) {
|
||||
unsubscribeGlobalState.forEach(sub => sub.unsubscribe());
|
||||
unsubscribeGlobalState = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
function onMountApp() {
|
||||
unsubscribe();
|
||||
// track current hash when within app
|
||||
unsubscribeURLHistory = historyInstance.listen(location => {
|
||||
const urlWithHashes = baseUrl + '#' + location.pathname + location.search;
|
||||
let urlWithStates = '';
|
||||
try {
|
||||
urlWithStates = unhashUrl(urlWithHashes);
|
||||
} catch (e) {
|
||||
toastNotifications.addDanger(e.message);
|
||||
}
|
||||
|
||||
activeUrl = getActiveSubUrl(urlWithStates || urlWithHashes);
|
||||
storageInstance.setItem(storageKey, activeUrl);
|
||||
});
|
||||
}
|
||||
|
||||
function onUnmountApp() {
|
||||
unsubscribe();
|
||||
// propagate state updates when in other apps
|
||||
unsubscribeGlobalState = stateParams.map(({ stateUpdate$, kbnUrlKey }) =>
|
||||
stateUpdate$.subscribe(state => {
|
||||
const updatedUrl = setStateToKbnUrl(
|
||||
kbnUrlKey,
|
||||
state,
|
||||
{ useHash: false },
|
||||
baseUrl + (activeUrl || defaultSubUrl)
|
||||
);
|
||||
// remove baseUrl prefix (just storing the sub url part)
|
||||
activeUrl = getActiveSubUrl(updatedUrl);
|
||||
storageInstance.setItem(storageKey, activeUrl);
|
||||
setNavLink(activeUrl);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
// register listeners for unmounted app initially
|
||||
onUnmountApp();
|
||||
|
||||
// initialize nav link and internal state
|
||||
const storedUrl = storageInstance.getItem(storageKey);
|
||||
if (storedUrl) {
|
||||
activeUrl = storedUrl;
|
||||
setNavLink(storedUrl);
|
||||
}
|
||||
|
||||
return {
|
||||
appMounted() {
|
||||
onMountApp();
|
||||
setNavLink(defaultSubUrl);
|
||||
},
|
||||
appUnMounted() {
|
||||
onUnmountApp();
|
||||
setNavLink(activeUrl);
|
||||
},
|
||||
stop() {
|
||||
unsubscribe();
|
||||
},
|
||||
};
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue