From 9ddf3f8a4672a9dc956bbd49034f8015ba64fc20 Mon Sep 17 00:00:00 2001 From: Josh Dover Date: Wed, 3 Apr 2019 14:34:08 -0500 Subject: [PATCH] [wip] move nav to core --- src/core/public/chrome/chrome_service.ts | 26 ++--- src/core/public/chrome/nav_links/nav_link.ts | 55 +++++++++ .../chrome/nav_links/nav_links_service.ts | 88 ++++++++++++++ .../injected_metadata_service.ts | 2 +- src/core/public/legacy/legacy_service.ts | 2 + .../kibana/public/context/index.js | 5 +- .../hacks/__tests__/hide_empty_tools.js | 13 ++- .../dev_tools/hacks/hide_empty_tools.js | 9 +- .../components/fetch_error/fetch_error.js | 6 +- .../kibana/public/visualize/editor/editor.js | 5 +- .../tests_bundle/tests_entry_template.js | 3 +- ...nk_in_nav.js => toggle_app_link_in_nav.ts} | 9 +- .../ui/public/chrome/api/__tests__/nav.js | 99 ++++++---------- src/legacy/ui/public/chrome/api/nav.d.ts | 4 - src/legacy/ui/public/chrome/api/nav.js | 108 +++++++++--------- .../header_global_nav/header_global_nav.js | 8 +- .../public/hacks/toggle_app_link_in_nav.js | 12 -- .../public/hacks/toggle_app_link_in_nav.ts | 13 +++ x-pack/plugins/graph/public/app.js | 4 +- .../public/hacks/toggle_app_link_in_nav.js | 30 ++--- .../ml/public/hacks/toggle_app_link_in_nav.js | 15 ++- .../public/hacks/toggle_app_link_in_nav.js | 8 +- .../public/hacks/job_completion_notifier.js | 9 +- 23 files changed, 333 insertions(+), 200 deletions(-) create mode 100644 src/core/public/chrome/nav_links/nav_link.ts create mode 100644 src/core/public/chrome/nav_links/nav_links_service.ts rename src/legacy/core_plugins/timelion/public/hacks/{toggle_app_link_in_nav.js => toggle_app_link_in_nav.ts} (74%) delete mode 100644 x-pack/plugins/apm/public/hacks/toggle_app_link_in_nav.js create mode 100644 x-pack/plugins/apm/public/hacks/toggle_app_link_in_nav.ts diff --git a/src/core/public/chrome/chrome_service.ts b/src/core/public/chrome/chrome_service.ts index b6695ced2861b12..22709fa70629bef 100644 --- a/src/core/public/chrome/chrome_service.ts +++ b/src/core/public/chrome/chrome_service.ts @@ -22,8 +22,10 @@ import * as Url from 'url'; import { i18n } from '@kbn/i18n'; import * as Rx from 'rxjs'; import { map, takeUntil } from 'rxjs/operators'; +import { BasePathSetup } from '../base_path'; import { InjectedMetadataSetup } from '../injected_metadata'; import { NotificationsSetup } from '../notifications'; +import { NavLinksService } from './nav_links/nav_links_service'; const IS_COLLAPSED_KEY = 'core.chrome.isCollapsed'; @@ -50,6 +52,7 @@ interface ConstructorParams { } interface SetupDeps { + basePath: BasePathSetup; injectedMetadata: InjectedMetadataSetup; notifications: NotificationsSetup; } @@ -57,12 +60,13 @@ interface SetupDeps { export class ChromeService { private readonly stop$ = new Rx.ReplaySubject(1); private readonly browserSupportsCsp: boolean; + private readonly navLinks = new NavLinksService(); public constructor({ browserSupportsCsp }: ConstructorParams) { this.browserSupportsCsp = browserSupportsCsp; } - public setup({ injectedMetadata, notifications }: SetupDeps) { + public setup({ basePath, injectedMetadata, notifications }: SetupDeps) { const FORCE_HIDDEN = isEmbedParamInHash(); const brand$ = new Rx.BehaviorSubject({}); @@ -96,14 +100,8 @@ export class ChromeService { * */ setBrand: (brand: ChromeBrand) => { - brand$.next( - Object.freeze({ - logo: brand.logo, - smallLogo: brand.smallLogo, - }) - ); + brand$.next(Object.freeze({ logo: brand.logo, smallLogo: brand.smallLogo })); }, - /** * Get an observable of the current brand information. */ @@ -117,7 +115,6 @@ export class ChromeService { setIsVisible: (visibility: boolean) => { isVisible$.next(visibility); }, - /** * Get an observable of the current visibility state of the chrome. */ @@ -126,7 +123,6 @@ export class ChromeService { map(visibility => (FORCE_HIDDEN ? false : visibility)), takeUntil(this.stop$) ), - /** * Set the collapsed state of the chrome navigation. */ @@ -138,12 +134,10 @@ export class ChromeService { localStorage.removeItem(IS_COLLAPSED_KEY); } }, - /** * Get an observable of the current collapsed state of the chrome. */ getIsCollapsed$: () => isCollapsed$.pipe(takeUntil(this.stop$)), - /** * Add a className that should be set on the application container. */ @@ -152,7 +146,6 @@ export class ChromeService { update.add(className); applicationClasses$.next(update); }, - /** * Remove a className added with `addApplicationClass()`. If className is unknown it is ignored. */ @@ -161,7 +154,6 @@ export class ChromeService { update.delete(className); applicationClasses$.next(update); }, - /** * Get the current set of classNames that will be set on the application container. */ @@ -170,34 +162,32 @@ export class ChromeService { map(set => [...set]), takeUntil(this.stop$) ), - /** * Get an observable of the current list of breadcrumbs */ getBreadcrumbs$: () => breadcrumbs$.pipe(takeUntil(this.stop$)), - /** * Override the current set of breadcrumbs */ setBreadcrumbs: (newBreadcrumbs: ChromeBreadcrumb[]) => { breadcrumbs$.next(newBreadcrumbs); }, - /** * Get an observable of the current custom help conttent */ getHelpExtension$: () => helpExtension$.pipe(takeUntil(this.stop$)), - /** * Override the current set of breadcrumbs */ setHelpExtension: (helpExtension?: ChromeHelpExtension) => { helpExtension$.next(helpExtension); }, + navLinks: this.navLinks.setup(basePath), }; } public stop() { + this.navLinks.stop(); this.stop$.next(); } } diff --git a/src/core/public/chrome/nav_links/nav_link.ts b/src/core/public/chrome/nav_links/nav_link.ts new file mode 100644 index 000000000000000..2eff9fcdfb8da9f --- /dev/null +++ b/src/core/public/chrome/nav_links/nav_link.ts @@ -0,0 +1,55 @@ +/* + * 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 { BasePathSetup } from '../../base_path'; + +export interface NavLinkProperties { + disabled: boolean; + euiIconType?: string; + hidden: boolean; + icon?: string; + id: string; + linkToLastSubUrl: boolean; + order: number; + title: string; + tooltip: string; + url: string; +} + +export class NavLink { + public readonly id: string; + public readonly properties: Readonly; + + constructor(properties: NavLinkProperties, private readonly basePath: BasePathSetup) { + if (!properties || !properties.id) { + throw new Error('`id` is required.'); + } + + this.id = properties.id; + this.properties = Object.freeze(properties); + } + + public getUrl() { + return this.basePath.addToPath(this.properties.url); + } + + public update(newProps: Partial) { + return new NavLink({ ...this.properties, ...newProps }, this.basePath); + } +} diff --git a/src/core/public/chrome/nav_links/nav_links_service.ts b/src/core/public/chrome/nav_links/nav_links_service.ts new file mode 100644 index 000000000000000..48a40eb926e0764 --- /dev/null +++ b/src/core/public/chrome/nav_links/nav_links_service.ts @@ -0,0 +1,88 @@ +/* + * 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 { sortBy } from 'lodash'; +import { BehaviorSubject, ReplaySubject } from 'rxjs'; +import { map, takeUntil } from 'rxjs/operators'; +import { BasePathSetup } from '../../base_path'; +import { NavLink, NavLinkProperties } from './nav_link'; + +export class NavLinksService { + private readonly stop$ = new ReplaySubject(1); + + public setup(basePath: BasePathSetup) { + const navLinks$ = new BehaviorSubject>([]); + + return { + getNavLinks$: () => { + return navLinks$.pipe( + map(sortNavLinks), + takeUntil(this.stop$) + ); + }, + + add(navLink: NavLinkProperties) { + navLinks$.next([...navLinks$.value, new NavLink(navLink, basePath)]); + }, + + clear() { + navLinks$.next([]); + }, + + get(id: string) { + const link = navLinks$.value.find(l => l.id === id); + return link ? link.properties : undefined; + }, + + getAll() { + return sortNavLinks(navLinks$.value); + }, + + exists(id: string) { + return this.get(id) !== undefined; + }, + + showOnly(id: string) { + navLinks$.next(navLinks$.value.filter(link => link.id === id)); + }, + + update(id: string, values: Partial) { + if (!this.exists(id)) { + return; + } + + navLinks$.next( + navLinks$.value.map(link => { + return link.id === id ? link.update(values) : link; + }) + ); + + return this.get(id); + }, + }; + } + + public stop() { + this.stop$.next(); + } +} + +function sortNavLinks(navLinks: ReadonlyArray) { + return sortBy(navLinks.map(link => link.properties), 'order'); +} diff --git a/src/core/public/injected_metadata/injected_metadata_service.ts b/src/core/public/injected_metadata/injected_metadata_service.ts index ea546731f37b35a..e8b3518f6298890 100644 --- a/src/core/public/injected_metadata/injected_metadata_service.ts +++ b/src/core/public/injected_metadata/injected_metadata_service.ts @@ -41,7 +41,7 @@ export interface InjectedMetadataParams { app: unknown; translations: unknown; bundleId: string; - nav: unknown; + nav: any[]; version: string; branch: string; buildNum: number; diff --git a/src/core/public/legacy/legacy_service.ts b/src/core/public/legacy/legacy_service.ts index 2095b7fb60e81b0..ef52616499e9cbf 100644 --- a/src/core/public/legacy/legacy_service.ts +++ b/src/core/public/legacy/legacy_service.ts @@ -64,6 +64,8 @@ export class LegacyPlatformService { require('ui/chrome/api/breadcrumbs').__newPlatformInit__(chrome); require('ui/chrome/services/global_nav_state').__newPlatformInit__(chrome); + injectedMetadata.getLegacyMetadata().nav.forEach(navLink => chrome.navLinks.add(navLink)); + // Load the bootstrap module before loading the legacy platform files so that // the bootstrap module can modify the environment a bit first const bootstrapModule = this.loadBootstrapModule(); diff --git a/src/legacy/core_plugins/kibana/public/context/index.js b/src/legacy/core_plugins/kibana/public/context/index.js index d1908be4bea7d89..1b85664467e783f 100644 --- a/src/legacy/core_plugins/kibana/public/context/index.js +++ b/src/legacy/core_plugins/kibana/public/context/index.js @@ -27,6 +27,9 @@ import { i18n } from '@kbn/i18n'; import './app'; import contextAppRouteTemplate from './index.html'; import { getRootBreadcrumbs } from '../discover/breadcrumbs'; +import { getNewPlatform } from 'ui/new_platform'; + +const core = getNewPlatform().setup.core; uiRoutes .when('/context/:indexPatternId/:type/:id*', { @@ -85,7 +88,7 @@ function ContextAppRouteController( this.anchorType = $routeParams.type; this.anchorId = $routeParams.id; this.indexPattern = indexPattern; - this.discoverUrl = chrome.getNavLinkById('kibana:discover').lastSubUrl; + this.discoverUrl = core.chrome.navLinks.get('kibana:discover').lastSubUrl; this.filters = _.cloneDeep(queryFilter.getFilters()); } diff --git a/src/legacy/core_plugins/kibana/public/dev_tools/hacks/__tests__/hide_empty_tools.js b/src/legacy/core_plugins/kibana/public/dev_tools/hacks/__tests__/hide_empty_tools.js index bf69fa09823f25a..48bff941da18f7a 100644 --- a/src/legacy/core_plugins/kibana/public/dev_tools/hacks/__tests__/hide_empty_tools.js +++ b/src/legacy/core_plugins/kibana/public/dev_tools/hacks/__tests__/hide_empty_tools.js @@ -20,11 +20,13 @@ import expect from '@kbn/expect'; import sinon from 'sinon'; -import chrome from 'ui/chrome'; import { hideEmptyDevTools } from '../hide_empty_tools'; +import { getNewPlatform } from 'ui/new_platform'; + +const coreNavLinks = getNewPlatform().setup.core.chrome.navLinks; describe('hide dev tools', function () { - let navlinks; + let updateNavLink; function PrivateWithoutTools() { return []; @@ -35,12 +37,11 @@ describe('hide dev tools', function () { } function isHidden() { - return !!chrome.getNavLinkById('kibana:dev_tools').hidden; + return updateNavLink.calledWith('kibana:dev_tools', { hidden: true }); } beforeEach(function () { - navlinks = {}; - sinon.stub(chrome, 'getNavLinkById').returns(navlinks); + updateNavLink = sinon.spy(coreNavLinks, 'update'); }); it('should hide the app if there are no dev tools', function () { @@ -54,6 +55,6 @@ describe('hide dev tools', function () { }); afterEach(function () { - chrome.getNavLinkById.restore(); + updateNavLink.restore(); }); }); diff --git a/src/legacy/core_plugins/kibana/public/dev_tools/hacks/hide_empty_tools.js b/src/legacy/core_plugins/kibana/public/dev_tools/hacks/hide_empty_tools.js index 3426932ea5e72b5..32dc842a78a985c 100644 --- a/src/legacy/core_plugins/kibana/public/dev_tools/hacks/hide_empty_tools.js +++ b/src/legacy/core_plugins/kibana/public/dev_tools/hacks/hide_empty_tools.js @@ -18,14 +18,17 @@ */ import { uiModules } from 'ui/modules'; -import chrome from 'ui/chrome'; import { DevToolsRegistryProvider } from 'ui/registry/dev_tools'; +import { getNewPlatform } from 'ui/new_platform'; + +const core = getNewPlatform().setup.core; export function hideEmptyDevTools(Private) { const hasTools = !!Private(DevToolsRegistryProvider).length; if (!hasTools) { - const navLink = chrome.getNavLinkById('kibana:dev_tools'); - navLink.hidden = true; + core.chrome.navLinks.update('kibana:dev_tools', { + hidden: true + }); } } diff --git a/src/legacy/core_plugins/kibana/public/discover/components/fetch_error/fetch_error.js b/src/legacy/core_plugins/kibana/public/discover/components/fetch_error/fetch_error.js index ec531600bd21eb6..223e41d37aca564 100644 --- a/src/legacy/core_plugins/kibana/public/discover/components/fetch_error/fetch_error.js +++ b/src/legacy/core_plugins/kibana/public/discover/components/fetch_error/fetch_error.js @@ -20,9 +20,9 @@ import 'ngreact'; import React, { Fragment } from 'react'; import { uiModules } from 'ui/modules'; -import chrome from 'ui/chrome'; import { wrapInI18nContext } from 'ui/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; +import { getNewPlatform } from 'ui/new_platform'; import { EuiFlexGroup, @@ -32,6 +32,8 @@ import { EuiSpacer, } from '@elastic/eui'; +const core = getNewPlatform().setup.core; + const DiscoverFetchError = ({ fetchError }) => { if (!fetchError) { return null; @@ -40,7 +42,7 @@ const DiscoverFetchError = ({ fetchError }) => { let body; if (fetchError.lang === 'painless') { - const managementUrl = chrome.getNavLinkById('kibana:management').url; + const managementUrl = core.chrome.navLinks.get('kibana:management').url; const url = `${managementUrl}/kibana/index_patterns`; body = ( diff --git a/src/legacy/core_plugins/kibana/public/visualize/editor/editor.js b/src/legacy/core_plugins/kibana/public/visualize/editor/editor.js index a84a805b2f1b947..2ae8c1bb6abbc1d 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/editor/editor.js +++ b/src/legacy/core_plugins/kibana/public/visualize/editor/editor.js @@ -52,6 +52,9 @@ import { getUnhashableStatesProvider } from 'ui/state_management/state_hashing'; import { showSaveModal } from 'ui/saved_objects/show_saved_object_save_modal'; import { SavedObjectSaveModal } from 'ui/saved_objects/components/saved_object_save_modal'; import { getEditBreadcrumbs, getCreateBreadcrumbs } from '../breadcrumbs'; +import { getNewPlatform } from 'ui/new_platform'; + +const core = getNewPlatform().setup.core; uiRoutes .when(VisualizeConstants.CREATE_PATH, { @@ -495,7 +498,7 @@ function VisEditor( // url, not the unsaved one. chrome.trackSubUrlForApp('kibana:visualize', savedVisualizationParsedUrl); - const lastDashboardAbsoluteUrl = chrome.getNavLinkById('kibana:dashboard').lastSubUrl; + const lastDashboardAbsoluteUrl = core.chrome.navLinks.get('kibana:dashboard').lastSubUrl; const dashboardParsedUrl = absoluteToParsedUrl(lastDashboardAbsoluteUrl, chrome.getBasePath()); dashboardParsedUrl.addQueryParameter(DashboardConstants.NEW_VISUALIZATION_ID_PARAM, savedVis.id); kbnUrl.change(dashboardParsedUrl.appPath); diff --git a/src/legacy/core_plugins/tests_bundle/tests_entry_template.js b/src/legacy/core_plugins/tests_bundle/tests_entry_template.js index a6d2d9cbf1f7836..7cbc436ef44e6a4 100644 --- a/src/legacy/core_plugins/tests_bundle/tests_entry_template.js +++ b/src/legacy/core_plugins/tests_bundle/tests_entry_template.js @@ -54,7 +54,8 @@ new CoreSystem({ uiSettings: { defaults: ${JSON.stringify(defaultUiSettings, null, 2).split('\n').join('\n ')}, user: {} - } + }, + nav: [] }, csp: { warnLegacyBrowsers: false, diff --git a/src/legacy/core_plugins/timelion/public/hacks/toggle_app_link_in_nav.js b/src/legacy/core_plugins/timelion/public/hacks/toggle_app_link_in_nav.ts similarity index 74% rename from src/legacy/core_plugins/timelion/public/hacks/toggle_app_link_in_nav.js rename to src/legacy/core_plugins/timelion/public/hacks/toggle_app_link_in_nav.ts index e3b68ddf0228252..feaffe6fe33b0dd 100644 --- a/src/legacy/core_plugins/timelion/public/hacks/toggle_app_link_in_nav.js +++ b/src/legacy/core_plugins/timelion/public/hacks/toggle_app_link_in_nav.ts @@ -17,9 +17,10 @@ * under the License. */ -import chrome from 'ui/chrome'; +import { getNewPlatform } from 'ui/new_platform'; -const timelionUiEnabled = chrome.getInjected('timelionUiEnabled'); -if (timelionUiEnabled === false && chrome.navLinkExists('timelion')) { - chrome.getNavLinkById('timelion').hidden = true; +const core = getNewPlatform().setup.core; +const timelionUiEnabled = core.injectedMetadata.getInjectedVar('timelionUiEnabled'); +if (timelionUiEnabled === false) { + core.chrome.navLinks.update('timelion', { hidden: true }); } diff --git a/src/legacy/ui/public/chrome/api/__tests__/nav.js b/src/legacy/ui/public/chrome/api/__tests__/nav.js index faf43058259e844..22f89c735389689 100644 --- a/src/legacy/ui/public/chrome/api/__tests__/nav.js +++ b/src/legacy/ui/public/chrome/api/__tests__/nav.js @@ -22,6 +22,9 @@ import expect from '@kbn/expect'; import { initChromeNavApi } from '../nav'; import { StubBrowserStorage } from 'test_utils/stub_browser_storage'; import { KibanaParsedUrl } from '../../../url/kibana_parsed_url'; +import { getNewPlatform } from 'ui/new_platform'; + +const core = getNewPlatform().setup.core; const basePath = '/someBasePath'; @@ -39,37 +42,8 @@ function init(customInternals = { basePath }) { } describe('chrome nav apis', function () { - describe('#getNavLinkById', () => { - it('retrieves the correct nav link, given its ID', () => { - const appUrlStore = new StubBrowserStorage(); - const nav = [ - { id: 'kibana:discover', title: 'Discover' } - ]; - const { - chrome - } = init({ appUrlStore, nav }); - - const navLink = chrome.getNavLinkById('kibana:discover'); - expect(navLink).to.eql(nav[0]); - }); - - it('throws an error if the nav link with the given ID is not found', () => { - const appUrlStore = new StubBrowserStorage(); - const nav = [ - { id: 'kibana:discover', title: 'Discover' } - ]; - const { - chrome - } = init({ appUrlStore, nav }); - - let errorThrown = false; - try { - chrome.getNavLinkById('nonexistent'); - } catch (e) { - errorThrown = true; - } - expect(errorThrown).to.be(true); - }); + beforeEach(() => { + core.chrome.navLinks.clear(); }); describe('#untrackNavLinksForDeletedSavedObjects', function () { @@ -79,27 +53,25 @@ describe('chrome nav apis', function () { it('should clear last url when last url contains link to deleted saved object', function () { const appUrlStore = new StubBrowserStorage(); - const nav = [ - { - id: appId, - title: 'Discover', - linkToLastSubUrl: true, - lastSubUrl: `${appUrl}?id=${deletedId}`, - url: appUrl - } - ]; + core.chrome.navLinks.add({ + id: appId, + title: 'Discover', + linkToLastSubUrl: true, + lastSubUrl: `${appUrl}?id=${deletedId}`, + url: appUrl + }); const { chrome - } = init({ appUrlStore, nav }); + } = init({ appUrlStore }); chrome.untrackNavLinksForDeletedSavedObjects([deletedId]); - expect(chrome.getNavLinkById('appId').lastSubUrl).to.be(appUrl); + expect(core.chrome.navLinks.get(appId).lastSubUrl).to.be(appUrl); }); it('should not clear last url when last url does not contains link to deleted saved object', function () { const lastUrl = `${appUrl}?id=anotherSavedObjectId`; const appUrlStore = new StubBrowserStorage(); - const nav = [ + core.chrome.navLinks.add( { id: appId, title: 'Discover', @@ -107,66 +79,71 @@ describe('chrome nav apis', function () { lastSubUrl: lastUrl, url: appUrl } - ]; + ); const { chrome - } = init({ appUrlStore, nav }); + } = init({ appUrlStore }); chrome.untrackNavLinksForDeletedSavedObjects([deletedId]); - expect(chrome.getNavLinkById(appId).lastSubUrl).to.be(lastUrl); + expect(core.chrome.navLinks.get(appId).lastSubUrl).to.be(lastUrl); }); }); describe('internals.trackPossibleSubUrl()', function () { it('injects the globalState of the current url to all links for the same app', function () { const appUrlStore = new StubBrowserStorage(); - const nav = [ + [ { + id: 'kibana:discover', url: 'https://localhost:9200/app/kibana#discover', subUrlBase: 'https://localhost:9200/app/kibana#discover' }, { + id: 'kibana:visualize', url: 'https://localhost:9200/app/kibana#visualize', subUrlBase: 'https://localhost:9200/app/kibana#visualize' }, { + id: 'kibana:dashboard', url: 'https://localhost:9200/app/kibana#dashboards', subUrlBase: 'https://localhost:9200/app/kibana#dashboard' }, - ].map(l => { + ].forEach(l => { l.lastSubUrl = l.url; - return l; + core.chrome.navLinks.add(l); }); const { internals - } = init({ appUrlStore, nav }); + } = init({ appUrlStore }); internals.trackPossibleSubUrl('https://localhost:9200/app/kibana#dashboard?_g=globalstate'); - expect(internals.nav[0].lastSubUrl).to.be('https://localhost:9200/app/kibana#discover?_g=globalstate'); - expect(internals.nav[0].active).to.be(false); + const navLinks = core.chrome.navLinks.getAll(); + + expect(navLinks[0].lastSubUrl).to.be('https://localhost:9200/app/kibana#discover?_g=globalstate'); + expect(navLinks[0].active).to.be(false); - expect(internals.nav[1].lastSubUrl).to.be('https://localhost:9200/app/kibana#visualize?_g=globalstate'); - expect(internals.nav[1].active).to.be(false); + expect(navLinks[1].lastSubUrl).to.be('https://localhost:9200/app/kibana#visualize?_g=globalstate'); + expect(navLinks[1].active).to.be(false); - expect(internals.nav[2].lastSubUrl).to.be('https://localhost:9200/app/kibana#dashboard?_g=globalstate'); - expect(internals.nav[2].active).to.be(true); + expect(navLinks[2].lastSubUrl).to.be('https://localhost:9200/app/kibana#dashboard?_g=globalstate'); + expect(navLinks[2].active).to.be(true); }); }); - describe('internals.trackSubUrlForApp()', function () { + describe('chrome.trackSubUrlForApp()', function () { it('injects a manual app url', function () { const appUrlStore = new StubBrowserStorage(); - const nav = [ + core.chrome.navLinks.add( { id: 'kibana:visualize', url: 'https://localhost:9200/app/kibana#visualize', lastSubUrl: 'https://localhost:9200/app/kibana#visualize', subUrlBase: 'https://localhost:9200/app/kibana#visualize' } - ]; + ); - const { chrome, internals } = init({ appUrlStore, nav }); + const { chrome } = init({ appUrlStore }); const basePath = '/xyz'; const appId = 'kibana'; @@ -177,7 +154,7 @@ describe('chrome nav apis', function () { const kibanaParsedUrl = new KibanaParsedUrl({ basePath, appId, appPath, hostname, port, protocol }); chrome.trackSubUrlForApp('kibana:visualize', kibanaParsedUrl); - expect(internals.nav[0].lastSubUrl).to.be('https://localhost:9200/xyz/app/kibana#visualize/1234?_g=globalstate'); + expect(core.chrome.navLinks.get('kibana:visualize').lastSubUrl).to.be('https://localhost:9200/xyz/app/kibana#visualize/1234?_g=globalstate'); }); }); }); diff --git a/src/legacy/ui/public/chrome/api/nav.d.ts b/src/legacy/ui/public/chrome/api/nav.d.ts index f7c639bc5733c24..b9ecca3f1f7cc69 100644 --- a/src/legacy/ui/public/chrome/api/nav.d.ts +++ b/src/legacy/ui/public/chrome/api/nav.d.ts @@ -36,10 +36,6 @@ export interface NavLink { } export interface ChromeNavLinks { - getNavLinks$(): Rx.Observable; - getNavLinks(): NavLink[]; - navLinkExists(id: string): boolean; - getNavLinkById(id: string): NavLink; showOnlyById(id: string): void; untrackNavLinksForDeletedSavedObjects(deletedIds: string[]): void; trackSubUrlForApp(linkId: string, parsedKibanaUrl: KibanaParsedUrl): void; diff --git a/src/legacy/ui/public/chrome/api/nav.js b/src/legacy/ui/public/chrome/api/nav.js index 1feb0a8f22e0a08..15e7f494e0afcdb 100644 --- a/src/legacy/ui/public/chrome/api/nav.js +++ b/src/legacy/ui/public/chrome/api/nav.js @@ -18,32 +18,23 @@ */ import * as Rx from 'rxjs'; -import { mapTo } from 'rxjs/operators'; -import { remove } from 'lodash'; import { relativeToAbsolute } from '../../url/relative_to_absolute'; import { absoluteToParsedUrl } from '../../url/absolute_to_parsed_url'; +import { getNewPlatform } from '../../new_platform'; export function initChromeNavApi(chrome, internals) { - const navUpdate$ = new Rx.BehaviorSubject(undefined); - - chrome.getNavLinks = function () { - return internals.nav; - }; - - chrome.getNavLinks$ = function () { - return navUpdate$.pipe(mapTo(internals.nav)); - }; + const core = getNewPlatform().setup.core; + // const navUpdate$ = new Rx.BehaviorSubject(undefined); // track navLinks with $rootScope.$watch like the old nav used to, necessary // as long as random parts of the app are directly mutating the navLinks - internals.$initNavLinksDeepWatch = function ($rootScope) { - $rootScope.$watch( - () => internals.nav, - () => navUpdate$.next(), - true - ); - }; - + // internals.$initNavLinksDeepWatch = function ($rootScope) { + // $rootScope.$watch( + // () => internals.nav, + // () => navUpdate$.next(), + // true + // ); + // }; const forceAppSwitcherNavigation$ = new Rx.BehaviorSubject(false); /** @@ -53,46 +44,41 @@ export function initChromeNavApi(chrome, internals) { * links to that app will set the current URL and change the hash, but * the routes for the correct are not loaded so nothing will happen. * https://github.com/elastic/kibana/pull/29770 + * + * Used only by status_page plugin */ chrome.enableForcedAppSwitcherNavigation = () => { forceAppSwitcherNavigation$.next(true); return chrome; }; + /** used only by directive */ chrome.getForceAppSwitcherNavigation$ = () => { return forceAppSwitcherNavigation$.asObservable(); }; - chrome.navLinkExists = (id) => { - return !!internals.nav.find(link => link.id === id); - }; - - chrome.getNavLinkById = (id) => { - const navLink = internals.nav.find(link => link.id === id); - if (!navLink) { - throw new Error(`Nav link for id = ${id} not found`); - } - return navLink; - }; - + /** used by dashboard_mode to hide all other navlinks */ chrome.showOnlyById = (id) => { - remove(internals.nav, app => app.id !== id); + core.chrome.navLinks.showOnly(id); }; function lastSubUrlKey(link) { return `lastSubUrl:${link.url}`; } - function setLastUrl(link, url) { - if (link.linkToLastSubUrl === false) { - return; - } + function getLastUrl(link) { + return internals.appUrlStore.getItem(lastSubUrlKey(link)); + } - link.lastSubUrl = url; + function setLastUrl(link, url) { internals.appUrlStore.setItem(lastSubUrlKey(link), url); + refreshLastUrl(link); } function refreshLastUrl(link) { - link.lastSubUrl = internals.appUrlStore.getItem(lastSubUrlKey(link)) || link.lastSubUrl || link.url; + const lastSubUrl = getLastUrl(link); + return core.chrome.navLinks.update(link.id, { + lastSubUrl: lastSubUrl || link.lastSubUrl || link.url, + }); } function injectNewGlobalState(link, fromAppId, newGlobalState) { @@ -103,11 +89,15 @@ export function initChromeNavApi(chrome, internals) { kibanaParsedUrl.setGlobalState(newGlobalState); - link.lastSubUrl = kibanaParsedUrl.getAbsoluteUrl(); + core.chrome.navLinks.update(link.id, { + lastSubUrl: kibanaParsedUrl.getAbsoluteUrl() + }); } /** * Clear last url for deleted saved objects to avoid loading pages with "Could not locate.." + * + * Really should be part of the subUrl service. */ chrome.untrackNavLinksForDeletedSavedObjects = (deletedIds) => { function urlContainsDeletedId(url) { @@ -120,7 +110,7 @@ export function initChromeNavApi(chrome, internals) { return true; } - internals.nav.forEach(link => { + core.chrome.navLinks.getAll().forEach(link => { if (link.linkToLastSubUrl && urlContainsDeletedId(link.lastSubUrl)) { setLastUrl(link, link.url); } @@ -134,31 +124,29 @@ export function initChromeNavApi(chrome, internals) { * should be the saved instance, but because of the redirect to a different page (e.g. `Save and Add to Dashboard` * on visualize tab), it won't be tracked automatically and will need to be inserted manually. See * https://github.com/elastic/kibana/pull/11932 for more background on why this was added. - * @param linkId {String} - an id that represents the navigation link. + * + * @param id {String} - an id that represents the navigation link. * @param kibanaParsedUrl {KibanaParsedUrl} the url to track */ - chrome.trackSubUrlForApp = (linkId, kibanaParsedUrl) => { - for (const link of internals.nav) { - if (link.id === linkId) { - const absoluteUrl = kibanaParsedUrl.getAbsoluteUrl(); - setLastUrl(link, absoluteUrl); - return; - } + chrome.trackSubUrlForApp = (id, kibanaParsedUrl) => { + const navLink = core.chrome.navLinks.get(id); + if (navLink) { + setLastUrl(navLink, kibanaParsedUrl.getAbsoluteUrl()); } }; - internals.trackPossibleSubUrl = function (url) { - const kibanaParsedUrl = absoluteToParsedUrl(url, chrome.getBasePath()); + internals.trackPossibleSubUrl = async function (url) { + for (let link of core.chrome.navLinks.getAll()) { + link = core.chrome.navLinks.update(link.id, { active: url.startsWith(link.subUrlBase) }); - for (const link of internals.nav) { - link.active = url.startsWith(link.subUrlBase); if (link.active) { setLastUrl(link, url); continue; } - refreshLastUrl(link); + link = refreshLastUrl(link); + const kibanaParsedUrl = absoluteToParsedUrl(url, chrome.getBasePath()); const newGlobalState = kibanaParsedUrl.getGlobalState(); if (newGlobalState) { injectNewGlobalState(link, kibanaParsedUrl.appId, newGlobalState); @@ -166,11 +154,19 @@ export function initChromeNavApi(chrome, internals) { } }; - internals.nav.forEach(link => { - link.url = relativeToAbsolute(chrome.addBasePath(link.url)); - link.subUrlBase = relativeToAbsolute(chrome.addBasePath(link.subUrlBase)); + // TODO: remove or move to core + core.chrome.navLinks.getAll().forEach(link => { + core.chrome.navLinks.update(link.id, { + url: relativeToAbsolute(chrome.addBasePath(link.url)), + subUrlBase: relativeToAbsolute(chrome.addBasePath(link.subUrlBase)), + }); }); + // internals.nav.forEach(link => { + // link.url = relativeToAbsolute(chrome.addBasePath(link.url)); + // link.subUrlBase = relativeToAbsolute(chrome.addBasePath(link.subUrlBase)); + // }); + // simulate a possible change in url to initialize the // link.active and link.lastUrl properties internals.trackPossibleSubUrl(document.location.href); diff --git a/src/legacy/ui/public/chrome/directives/header_global_nav/header_global_nav.js b/src/legacy/ui/public/chrome/directives/header_global_nav/header_global_nav.js index 66b34b38df6c632..77ee683f36c38bb 100644 --- a/src/legacy/ui/public/chrome/directives/header_global_nav/header_global_nav.js +++ b/src/legacy/ui/public/chrome/directives/header_global_nav/header_global_nav.js @@ -22,6 +22,7 @@ import { uiModules } from '../../../modules'; import { Header } from './components/header'; import { wrapInI18nContext } from 'ui/i18n'; import { chromeHeaderNavControlsRegistry } from 'ui/registry/chrome_header_nav_controls'; +import { getNewPlatform } from '../../../new_platform'; const module = uiModules.get('kibana'); @@ -29,6 +30,7 @@ module.directive('headerGlobalNav', (reactDirective, chrome, Private) => { const { recentlyAccessed } = require('ui/persisted_log'); const navControls = Private(chromeHeaderNavControlsRegistry); const homeHref = chrome.addBasePath('/app/kibana#/home'); + const newPlatformCore = getNewPlatform().setup.core; return reactDirective(wrapInI18nContext(Header), [ // scope accepted by directive, passed in as React props @@ -38,9 +40,9 @@ module.directive('headerGlobalNav', (reactDirective, chrome, Private) => { {}, // angular injected React props { - breadcrumbs$: chrome.breadcrumbs.get$(), - helpExtension$: chrome.helpExtension.get$(), - navLinks$: chrome.getNavLinks$(), + breadcrumbs$: newPlatformCore.chrome.getBreadcrumbs$(), + helpExtension$: newPlatformCore.chrome.getHelpExtension$(), + navLinks$: newPlatformCore.chrome.navLinks.getNavLinks$(), recentlyAccessed$: recentlyAccessed.get$(), forceAppSwitcherNavigation$: chrome.getForceAppSwitcherNavigation$(), navControls, diff --git a/x-pack/plugins/apm/public/hacks/toggle_app_link_in_nav.js b/x-pack/plugins/apm/public/hacks/toggle_app_link_in_nav.js deleted file mode 100644 index 90a8f31364f549f..000000000000000 --- a/x-pack/plugins/apm/public/hacks/toggle_app_link_in_nav.js +++ /dev/null @@ -1,12 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import chrome from 'ui/chrome'; - -const apmUiEnabled = chrome.getInjected('apmUiEnabled'); -if (apmUiEnabled === false && chrome.navLinkExists('apm')) { - chrome.getNavLinkById('apm').hidden = true; -} diff --git a/x-pack/plugins/apm/public/hacks/toggle_app_link_in_nav.ts b/x-pack/plugins/apm/public/hacks/toggle_app_link_in_nav.ts new file mode 100644 index 000000000000000..eaa8cdffc883a43 --- /dev/null +++ b/x-pack/plugins/apm/public/hacks/toggle_app_link_in_nav.ts @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { getNewPlatform } from 'ui/new_platform'; + +const core = getNewPlatform().setup.core; +const apmUiEnabled = core.injectedMetadata.getInjectedVar('apmUiEnabled'); +if (apmUiEnabled === false) { + core.chrome.navLinks.update('apm', { hidden: true }); +} diff --git a/x-pack/plugins/graph/public/app.js b/x-pack/plugins/graph/public/app.js index 305a229a80fceb2..dbfd58cca9d29cb 100644 --- a/x-pack/plugins/graph/public/app.js +++ b/x-pack/plugins/graph/public/app.js @@ -26,6 +26,7 @@ import { notify, addAppRedirectMessageToUrl, fatalError, toastNotifications } fr import { IndexPatternsProvider } from 'ui/index_patterns/index_patterns'; import { SavedObjectsClientProvider } from 'ui/saved_objects'; import { KibanaParsedUrl } from 'ui/url/kibana_parsed_url'; +import { getNewPlatform } from 'ui/new_platform'; import { XPackInfoProvider } from 'plugins/xpack_main/services/xpack_info'; @@ -48,6 +49,7 @@ import { getOutlinkEncoders, } from './services/outlink_encoders'; +const core = getNewPlatform().setup.core; const app = uiModules.get('app/graph'); function checkLicense(Private, Promise, kbnBaseUrl) { @@ -751,7 +753,7 @@ app.controller('graphuiPlugin', function ($scope, $route, $http, kbnUrl, Private .on('zoom', redraw)); - const managementUrl = chrome.getNavLinkById('kibana:management').url; + const managementUrl = core.chrome.navLinks.get('kibana:management').url; const url = `${managementUrl}/kibana/index_patterns`; if ($scope.indices.length === 0) { diff --git a/x-pack/plugins/graph/public/hacks/toggle_app_link_in_nav.js b/x-pack/plugins/graph/public/hacks/toggle_app_link_in_nav.js index 70162e321f716e3..301b5f98085b23e 100644 --- a/x-pack/plugins/graph/public/hacks/toggle_app_link_in_nav.js +++ b/x-pack/plugins/graph/public/hacks/toggle_app_link_in_nav.js @@ -6,21 +6,23 @@ import chrome from 'ui/chrome'; import { uiModules } from 'ui/modules'; +import { getNewPlatform } from 'ui/new_platform'; import { XPackInfoProvider } from 'plugins/xpack_main/services/xpack_info'; -uiModules.get('xpack/graph').run((Private) => { - const xpackInfo = Private(XPackInfoProvider); - if (!chrome.navLinkExists('graph')) { - return; - } +const core = getNewPlatform().setup.core; +uiModules.get('xpack/graph') + .run(Private => { + const xpackInfo = Private(XPackInfoProvider); - const navLink = chrome.getNavLinkById('graph'); - navLink.hidden = true; - const showAppLink = xpackInfo.get('features.graph.showAppLink', false); - navLink.hidden = !showAppLink; - if (showAppLink) { - navLink.disabled = !xpackInfo.get('features.graph.enableAppLink', false); - navLink.tooltip = xpackInfo.get('features.graph.message'); - } -}); + const navLinkUpdates = {}; + navLinkUpdates.hidden = true; + const showAppLink = xpackInfo.get('features.graph.showAppLink', false); + navLinkUpdates.hidden = !showAppLink; + if (showAppLink) { + navLinkUpdates.disabled = !xpackInfo.get('features.graph.enableAppLink', false); + navLinkUpdates.tooltip = xpackInfo.get('features.graph.message'); + } + + core.chrome.navLinks.update('graph', navLinkUpdates); + }); diff --git a/x-pack/plugins/ml/public/hacks/toggle_app_link_in_nav.js b/x-pack/plugins/ml/public/hacks/toggle_app_link_in_nav.js index bed0fe46c323533..532fb71c599d7b2 100644 --- a/x-pack/plugins/ml/public/hacks/toggle_app_link_in_nav.js +++ b/x-pack/plugins/ml/public/hacks/toggle_app_link_in_nav.js @@ -7,19 +7,22 @@ import { XPackInfoProvider } from 'plugins/xpack_main/services/xpack_info'; -import chrome from 'ui/chrome'; import { uiModules } from 'ui/modules'; +import { getNewPlatform } from 'ui/new_platform'; + +const core = getNewPlatform().setup.core; uiModules.get('xpack/ml').run((Private) => { const xpackInfo = Private(XPackInfoProvider); - if (!chrome.navLinkExists('ml')) return; - const navLink = chrome.getNavLinkById('ml'); + const navLinkUpdates = {}; // hide by default, only show once the xpackInfo is initialized - navLink.hidden = true; + navLinkUpdates.hidden = true; const showAppLink = xpackInfo.get('features.ml.showLinks', false); - navLink.hidden = !showAppLink; + navLinkUpdates.hidden = !showAppLink; if (showAppLink) { - navLink.disabled = !xpackInfo.get('features.ml.isAvailable', false); + navLinkUpdates.disabled = !xpackInfo.get('features.ml.isAvailable', false); } + + core.chrome.navLinks.update('ml', navLinkUpdates); }); diff --git a/x-pack/plugins/monitoring/public/hacks/toggle_app_link_in_nav.js b/x-pack/plugins/monitoring/public/hacks/toggle_app_link_in_nav.js index 451793b83dd6591..423c72153bc9320 100644 --- a/x-pack/plugins/monitoring/public/hacks/toggle_app_link_in_nav.js +++ b/x-pack/plugins/monitoring/public/hacks/toggle_app_link_in_nav.js @@ -4,13 +4,15 @@ * you may not use this file except in compliance with the Elastic License. */ -import chrome from 'ui/chrome'; import { uiModules } from 'ui/modules'; +import { getNewPlatform } from 'ui/new_platform'; + +const core = getNewPlatform().setup.core; uiModules.get('monitoring/hacks').run((monitoringUiEnabled) => { - if (monitoringUiEnabled || !chrome.navLinkExists('monitoring')) { + if (monitoringUiEnabled) { return; } - chrome.getNavLinkById('monitoring').hidden = true; + core.chrome.navLinks.update('monitoring', { hidden: true }); }); diff --git a/x-pack/plugins/reporting/public/hacks/job_completion_notifier.js b/x-pack/plugins/reporting/public/hacks/job_completion_notifier.js index 505ef204f4d0f66..65732ac9c5a543a 100644 --- a/x-pack/plugins/reporting/public/hacks/job_completion_notifier.js +++ b/x-pack/plugins/reporting/public/hacks/job_completion_notifier.js @@ -7,7 +7,6 @@ import React from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; import { toastNotifications } from 'ui/notify'; -import chrome from 'ui/chrome'; import { uiModules } from 'ui/modules'; import { get } from 'lodash'; import { jobQueueClient } from 'plugins/reporting/lib/job_queue_client'; @@ -20,6 +19,9 @@ import { EuiButton, } from '@elastic/eui'; import { downloadReport } from '../lib/download_report'; +import { getNewPlatform } from 'ui/new_platform'; + +const core = getNewPlatform().setup.core; /** * Poll for changes to reports. Inform the user of changes when the license is active. @@ -61,8 +63,9 @@ uiModules.get('kibana') // In-case the license expired/changed between the time they queued the job and the time that // the job completes, that way we don't give the user a toast to download their report if they can't. - if (chrome.navLinkExists('kibana:management')) { - const managementUrl = chrome.getNavLinkById('kibana:management').url; + // NOTE: this should be looking at configuration rather than the existence of a navLink + if (core.chrome.navLinks.exists('kibana:management')) { + const managementUrl = core.chrome.navLinks.get('kibana:management').url; const reportingSectionUrl = `${managementUrl}/kibana/reporting`; seeReportLink = (