Add K7 header navigation (#23300)
* Add basic support for new K7 navigation * Make visibility and app title work * Allow nav controls on right side of navbar * Use render callback w/ el * Add support for multiple sides * Remove fake spaces nav control * Breadcrumb support * Hide breadcrumbs in plugins when k7design is enabled: * Fix units * Rename k7 -> header * Add tests * Fix tests * Fix loading indicator * PR comments * Move ts-ignore * Use canvasApp icon type
This commit is contained in:
parent
f74b4bfdac
commit
49798bc8ad
|
@ -2213,7 +2213,8 @@ main {
|
||||||
-ms-flex-align: center;
|
-ms-flex-align: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding-left: 10px;
|
padding-left: 10px;
|
||||||
/* 1 */ }
|
/* 1 */
|
||||||
|
height: 100%; }
|
||||||
|
|
||||||
.kuiLocalBreadcrumb {
|
.kuiLocalBreadcrumb {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
|
@ -2685,6 +2686,8 @@ main {
|
||||||
-ms-flex-pack: justify;
|
-ms-flex-pack: justify;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
min-height: 29px;
|
min-height: 29px;
|
||||||
|
/* 1 */
|
||||||
|
line-height: 29px;
|
||||||
/* 1 */ }
|
/* 1 */ }
|
||||||
|
|
||||||
.kuiLocalNavRow__section {
|
.kuiLocalNavRow__section {
|
||||||
|
|
|
@ -6,6 +6,7 @@
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding-left: $localNavSideSpacing; /* 1 */
|
padding-left: $localNavSideSpacing; /* 1 */
|
||||||
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.kuiLocalBreadcrumb {
|
.kuiLocalBreadcrumb {
|
||||||
|
|
|
@ -28,6 +28,7 @@
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
min-height: 29px; /* 1 */
|
min-height: 29px; /* 1 */
|
||||||
|
line-height: 29px; /* 1 */
|
||||||
}
|
}
|
||||||
|
|
||||||
.kuiLocalNavRow__section {
|
.kuiLocalNavRow__section {
|
||||||
|
|
|
@ -76,6 +76,7 @@ export default function (kibana) {
|
||||||
url: `${kbnBaseUrl}#/discover`,
|
url: `${kbnBaseUrl}#/discover`,
|
||||||
description: 'interactively explore your data',
|
description: 'interactively explore your data',
|
||||||
icon: 'plugins/kibana/assets/discover.svg',
|
icon: 'plugins/kibana/assets/discover.svg',
|
||||||
|
euiIconType: 'discoverApp',
|
||||||
}, {
|
}, {
|
||||||
id: 'kibana:visualize',
|
id: 'kibana:visualize',
|
||||||
title: 'Visualize',
|
title: 'Visualize',
|
||||||
|
@ -83,6 +84,7 @@ export default function (kibana) {
|
||||||
url: `${kbnBaseUrl}#/visualize`,
|
url: `${kbnBaseUrl}#/visualize`,
|
||||||
description: 'design data visualizations',
|
description: 'design data visualizations',
|
||||||
icon: 'plugins/kibana/assets/visualize.svg',
|
icon: 'plugins/kibana/assets/visualize.svg',
|
||||||
|
euiIconType: 'visualizeApp',
|
||||||
}, {
|
}, {
|
||||||
id: 'kibana:dashboard',
|
id: 'kibana:dashboard',
|
||||||
title: 'Dashboard',
|
title: 'Dashboard',
|
||||||
|
@ -96,13 +98,15 @@ export default function (kibana) {
|
||||||
subUrlBase: `${kbnBaseUrl}#/dashboard`,
|
subUrlBase: `${kbnBaseUrl}#/dashboard`,
|
||||||
description: 'compose visualizations for much win',
|
description: 'compose visualizations for much win',
|
||||||
icon: 'plugins/kibana/assets/dashboard.svg',
|
icon: 'plugins/kibana/assets/dashboard.svg',
|
||||||
|
euiIconType: 'dashboardApp',
|
||||||
}, {
|
}, {
|
||||||
id: 'kibana:dev_tools',
|
id: 'kibana:dev_tools',
|
||||||
title: 'Dev Tools',
|
title: 'Dev Tools',
|
||||||
order: 9001,
|
order: 9001,
|
||||||
url: '/app/kibana#/dev_tools',
|
url: '/app/kibana#/dev_tools',
|
||||||
description: 'development tools',
|
description: 'development tools',
|
||||||
icon: 'plugins/kibana/assets/wrench.svg'
|
icon: 'plugins/kibana/assets/wrench.svg',
|
||||||
|
euiIconType: 'devToolsApp',
|
||||||
}, {
|
}, {
|
||||||
id: 'kibana:management',
|
id: 'kibana:management',
|
||||||
title: 'Management',
|
title: 'Management',
|
||||||
|
@ -110,6 +114,7 @@ export default function (kibana) {
|
||||||
url: `${kbnBaseUrl}#/management`,
|
url: `${kbnBaseUrl}#/management`,
|
||||||
description: 'define index patterns, change config, and more',
|
description: 'define index patterns, change config, and more',
|
||||||
icon: 'plugins/kibana/assets/settings.svg',
|
icon: 'plugins/kibana/assets/settings.svg',
|
||||||
|
euiIconType: 'managementApp',
|
||||||
linkToLastSubUrl: false
|
linkToLastSubUrl: false
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|
|
@ -7,20 +7,21 @@
|
||||||
<!-- Transcluded elements. -->
|
<!-- Transcluded elements. -->
|
||||||
<div data-transclude-slots>
|
<div data-transclude-slots>
|
||||||
<!-- Title. -->
|
<!-- Title. -->
|
||||||
<div
|
<div data-transclude-slot="topLeftCorner">
|
||||||
data-transclude-slot="topLeftCorner"
|
<div
|
||||||
class="kuiLocalBreadcrumbs"
|
class="kuiLocalBreadcrumbs"
|
||||||
data-test-subj="breadcrumbs"
|
data-test-subj="breadcrumbs"
|
||||||
role="heading"
|
role="heading"
|
||||||
aria-level="1"
|
aria-level="1"
|
||||||
>
|
ng-if="showPluginBreadcrumbs">
|
||||||
<div class="kuiLocalBreadcrumb">
|
<div class="kuiLocalBreadcrumb">
|
||||||
<a class="kuiLocalBreadcrumb__link" href="{{landingPageUrl()}}">Dashboard</a>
|
<a class="kuiLocalBreadcrumb__link" href="{{landingPageUrl()}}">Dashboard</a>
|
||||||
|
</div>
|
||||||
|
<div class="kuiLocalBreadcrumb">
|
||||||
|
{{ getDashTitle() }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="kuiLocalBreadcrumb">
|
|
||||||
{{ getDashTitle() }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Search. -->
|
<!-- Search. -->
|
||||||
<div ng-show="chrome.getVisible()" class="fullWidth" data-transclude-slot="bottomRow">
|
<div ng-show="chrome.getVisible()" class="fullWidth" data-transclude-slot="bottomRow">
|
||||||
|
|
|
@ -81,7 +81,17 @@ app.directive('dashboardApp', function ($injector) {
|
||||||
return {
|
return {
|
||||||
restrict: 'E',
|
restrict: 'E',
|
||||||
controllerAs: 'dashboardApp',
|
controllerAs: 'dashboardApp',
|
||||||
controller: function ($scope, $rootScope, $route, $routeParams, $location, getAppState, dashboardConfig, localStorage) {
|
controller: function (
|
||||||
|
$scope,
|
||||||
|
$rootScope,
|
||||||
|
$route,
|
||||||
|
$routeParams,
|
||||||
|
$location,
|
||||||
|
getAppState,
|
||||||
|
dashboardConfig,
|
||||||
|
localStorage,
|
||||||
|
breadcrumbState
|
||||||
|
) {
|
||||||
const filterManager = Private(FilterManagerProvider);
|
const filterManager = Private(FilterManagerProvider);
|
||||||
const filterBar = Private(FilterBarQueryFilterProvider);
|
const filterBar = Private(FilterBarQueryFilterProvider);
|
||||||
const docTitle = Private(DocTitleProvider);
|
const docTitle = Private(DocTitleProvider);
|
||||||
|
@ -169,6 +179,18 @@ app.directive('dashboardApp', function ($injector) {
|
||||||
dashboardStateManager.getTitle(),
|
dashboardStateManager.getTitle(),
|
||||||
dashboardStateManager.getViewMode(),
|
dashboardStateManager.getViewMode(),
|
||||||
dashboardStateManager.getIsDirty(timefilter));
|
dashboardStateManager.getIsDirty(timefilter));
|
||||||
|
|
||||||
|
// Push breadcrumbs to new header navigation
|
||||||
|
const updateBreadcrumbs = () => {
|
||||||
|
breadcrumbState.set([
|
||||||
|
{ text: 'Dashboard', href: $scope.landingPageUrl() },
|
||||||
|
{ text: $scope.getDashTitle() }
|
||||||
|
]);
|
||||||
|
};
|
||||||
|
updateBreadcrumbs();
|
||||||
|
dashboardStateManager.registerChangeListener(updateBreadcrumbs);
|
||||||
|
config.watch('k7design', (val) => $scope.showPluginBreadcrumbs = !val);
|
||||||
|
|
||||||
$scope.newDashboard = () => { kbnUrl.change(DashboardConstants.CREATE_NEW_DASHBOARD_URL, {}); };
|
$scope.newDashboard = () => { kbnUrl.change(DashboardConstants.CREATE_NEW_DASHBOARD_URL, {}); };
|
||||||
$scope.saveState = () => dashboardStateManager.saveState();
|
$scope.saveState = () => dashboardStateManager.saveState();
|
||||||
$scope.getShouldShowEditHelp = () => (
|
$scope.getShouldShowEditHelp = () => (
|
||||||
|
|
|
@ -50,7 +50,7 @@ uiRoutes
|
||||||
})
|
})
|
||||||
.when(DashboardConstants.LANDING_PAGE_PATH, {
|
.when(DashboardConstants.LANDING_PAGE_PATH, {
|
||||||
template: dashboardListingTemplate,
|
template: dashboardListingTemplate,
|
||||||
controller($injector, $location, $scope, Private, config) {
|
controller($injector, $location, $scope, Private, config, breadcrumbState) {
|
||||||
const services = Private(SavedObjectRegistryProvider).byLoaderPropertiesName;
|
const services = Private(SavedObjectRegistryProvider).byLoaderPropertiesName;
|
||||||
const dashboardConfig = $injector.get('dashboardConfig');
|
const dashboardConfig = $injector.get('dashboardConfig');
|
||||||
|
|
||||||
|
@ -63,6 +63,7 @@ uiRoutes
|
||||||
};
|
};
|
||||||
$scope.hideWriteControls = dashboardConfig.getHideWriteControls();
|
$scope.hideWriteControls = dashboardConfig.getHideWriteControls();
|
||||||
$scope.initialFilter = ($location.search()).filter || EMPTY_FILTER;
|
$scope.initialFilter = ($location.search()).filter || EMPTY_FILTER;
|
||||||
|
breadcrumbState.set([{ text: 'Dashboards' }]);
|
||||||
},
|
},
|
||||||
resolve: {
|
resolve: {
|
||||||
dash: function ($route, Private, redirectWhenMissing, kbnUrl) {
|
dash: function ($route, Private, redirectWhenMissing, kbnUrl) {
|
||||||
|
|
|
@ -155,8 +155,8 @@ function discoverController(
|
||||||
courier,
|
courier,
|
||||||
kbnUrl,
|
kbnUrl,
|
||||||
localStorage,
|
localStorage,
|
||||||
|
breadcrumbState
|
||||||
) {
|
) {
|
||||||
|
|
||||||
const Vis = Private(VisProvider);
|
const Vis = Private(VisProvider);
|
||||||
const docTitle = Private(DocTitleProvider);
|
const docTitle = Private(DocTitleProvider);
|
||||||
const HitSortFn = Private(PluginsKibanaDiscoverHitSortFnProvider);
|
const HitSortFn = Private(PluginsKibanaDiscoverHitSortFnProvider);
|
||||||
|
@ -289,6 +289,12 @@ function discoverController(
|
||||||
const pageTitleSuffix = savedSearch.id && savedSearch.title ? `: ${savedSearch.title}` : '';
|
const pageTitleSuffix = savedSearch.id && savedSearch.title ? `: ${savedSearch.title}` : '';
|
||||||
docTitle.change(`Discover${pageTitleSuffix}`);
|
docTitle.change(`Discover${pageTitleSuffix}`);
|
||||||
|
|
||||||
|
if (savedSearch.id && savedSearch.title) {
|
||||||
|
breadcrumbState.set([{ text: 'Discover', href: '#/discover' }, { text: savedSearch.title }]);
|
||||||
|
} else {
|
||||||
|
breadcrumbState.set([{ text: 'Discover' }]);
|
||||||
|
}
|
||||||
|
|
||||||
let stateMonitor;
|
let stateMonitor;
|
||||||
|
|
||||||
const $state = $scope.state = new AppState(getStateDefaults());
|
const $state = $scope.state = new AppState(getStateDefaults());
|
||||||
|
|
|
@ -5,11 +5,14 @@
|
||||||
<!-- Title. -->
|
<!-- Title. -->
|
||||||
<div
|
<div
|
||||||
data-transclude-slot="topLeftCorner"
|
data-transclude-slot="topLeftCorner"
|
||||||
class="kuiLocalTitle"
|
|
||||||
role="heading"
|
|
||||||
aria-level="1"
|
|
||||||
>
|
>
|
||||||
Visualize
|
<div
|
||||||
|
class="kuiLocalTitle"
|
||||||
|
role="heading"
|
||||||
|
aria-level="1"
|
||||||
|
ng-if="listingController.showPluginBreadcrumbs">
|
||||||
|
Visualize
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</kbn-top-nav>
|
</kbn-top-nav>
|
||||||
|
|
|
@ -34,6 +34,7 @@ export function VisualizeListingController($injector) {
|
||||||
const Notifier = $injector.get('Notifier');
|
const Notifier = $injector.get('Notifier');
|
||||||
const Private = $injector.get('Private');
|
const Private = $injector.get('Private');
|
||||||
const config = $injector.get('config');
|
const config = $injector.get('config');
|
||||||
|
const breadcrumbState = $injector.get('breadcrumbState');
|
||||||
|
|
||||||
timefilter.disableAutoRefreshSelector();
|
timefilter.disableAutoRefreshSelector();
|
||||||
timefilter.disableTimeRangeSelector();
|
timefilter.disableTimeRangeSelector();
|
||||||
|
@ -58,4 +59,7 @@ export function VisualizeListingController($injector) {
|
||||||
return visualizationService.delete(selectedIds)
|
return visualizationService.delete(selectedIds)
|
||||||
.catch(error => notify.error(error));
|
.catch(error => notify.error(error));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
breadcrumbState.set([{ text: 'Visualize' }]);
|
||||||
|
config.watch('k7design', (val) => this.showPluginBreadcrumbs = !val);
|
||||||
}
|
}
|
||||||
|
|
|
@ -44,6 +44,12 @@ export function getUiSettingDefaults() {
|
||||||
description: `When set, * is allowed as the first character in a query clause. Currently only applies when experimental query
|
description: `When set, * is allowed as the first character in a query clause. Currently only applies when experimental query
|
||||||
features are enabled in the query bar. To disallow leading wildcards in basic lucene queries, use query:queryString:options`,
|
features are enabled in the query bar. To disallow leading wildcards in basic lucene queries, use query:queryString:options`,
|
||||||
},
|
},
|
||||||
|
'k7design': {
|
||||||
|
name: 'Use the new K7 UI design',
|
||||||
|
value: false,
|
||||||
|
description: `When set, Kibana will use the new K7 design targeted for release in 7.0. At this time, not all features are
|
||||||
|
implemented.`,
|
||||||
|
},
|
||||||
'search:queryLanguage': {
|
'search:queryLanguage': {
|
||||||
name: 'Query language',
|
name: 'Query language',
|
||||||
value: 'lucene',
|
value: 'lucene',
|
||||||
|
|
|
@ -26,6 +26,7 @@ export default function (kibana) {
|
||||||
order: -1000,
|
order: -1000,
|
||||||
description: 'Time series expressions for everything',
|
description: 'Time series expressions for everything',
|
||||||
icon: 'plugins/timelion/icon.svg',
|
icon: 'plugins/timelion/icon.svg',
|
||||||
|
euiIconType: 'timelionApp',
|
||||||
main: 'plugins/timelion/app',
|
main: 'plugins/timelion/app',
|
||||||
},
|
},
|
||||||
styleSheetPaths: `${__dirname}/public/index.scss`,
|
styleSheetPaths: `${__dirname}/public/index.scss`,
|
||||||
|
|
|
@ -0,0 +1,46 @@
|
||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`HeaderBreadcrumbs renders updates to the breadcrumbs observable 1`] = `
|
||||||
|
<span
|
||||||
|
aria-current="page"
|
||||||
|
className="euiBreadcrumb euiBreadcrumb--last"
|
||||||
|
title="First"
|
||||||
|
>
|
||||||
|
First
|
||||||
|
</span>
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`HeaderBreadcrumbs renders updates to the breadcrumbs observable 2`] = `
|
||||||
|
Array [
|
||||||
|
<EuiLink
|
||||||
|
className="euiBreadcrumb"
|
||||||
|
color="subdued"
|
||||||
|
title="First"
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
className="euiLink euiLink--subdued euiBreadcrumb"
|
||||||
|
title="First"
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
First
|
||||||
|
</button>
|
||||||
|
</EuiLink>,
|
||||||
|
<button
|
||||||
|
className="euiLink euiLink--subdued euiBreadcrumb"
|
||||||
|
title="First"
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
First
|
||||||
|
</button>,
|
||||||
|
<span
|
||||||
|
aria-current="page"
|
||||||
|
className="euiBreadcrumb euiBreadcrumb--last"
|
||||||
|
title="Second"
|
||||||
|
>
|
||||||
|
Second
|
||||||
|
</span>,
|
||||||
|
]
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`HeaderBreadcrumbs renders updates to the breadcrumbs observable 3`] = `null`;
|
|
@ -0,0 +1,87 @@
|
||||||
|
/*
|
||||||
|
* 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 React, { Component } from 'react';
|
||||||
|
import { Subscribable } from 'rxjs';
|
||||||
|
|
||||||
|
import {
|
||||||
|
// TODO: add type annotations
|
||||||
|
// @ts-ignore
|
||||||
|
EuiHeader,
|
||||||
|
// @ts-ignore
|
||||||
|
EuiHeaderLogo,
|
||||||
|
// @ts-ignore
|
||||||
|
EuiHeaderSection,
|
||||||
|
// @ts-ignore
|
||||||
|
EuiHeaderSectionItem,
|
||||||
|
} from '@elastic/eui';
|
||||||
|
|
||||||
|
import { HeaderAppMenu } from './header_app_menu';
|
||||||
|
import { HeaderBreadcrumbs } from './header_breadcrumbs';
|
||||||
|
import { HeaderNavControls } from './header_nav_controls';
|
||||||
|
|
||||||
|
import { ChromeHeaderNavControlsRegistry } from 'ui/registry/chrome_header_nav_controls';
|
||||||
|
import { Breadcrumb, NavControlSide, NavLink } from '../';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
appTitle?: string;
|
||||||
|
breadcrumbs: Subscribable<Breadcrumb[]>;
|
||||||
|
homeHref: string;
|
||||||
|
isVisible: boolean;
|
||||||
|
navLinks: NavLink[];
|
||||||
|
navControls: ChromeHeaderNavControlsRegistry;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Header extends Component<Props> {
|
||||||
|
public renderLogo() {
|
||||||
|
const { homeHref } = this.props;
|
||||||
|
return <EuiHeaderLogo iconType="logoKibana" href={homeHref} aria-label="Go to home page" />;
|
||||||
|
}
|
||||||
|
|
||||||
|
public render() {
|
||||||
|
const { appTitle, breadcrumbs, isVisible, navControls, navLinks } = this.props;
|
||||||
|
|
||||||
|
if (!isVisible) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const leftNavControls = navControls.bySide[NavControlSide.Left];
|
||||||
|
const rightNavControls = navControls.bySide[NavControlSide.Right];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<EuiHeader>
|
||||||
|
<EuiHeaderSection>
|
||||||
|
<EuiHeaderSectionItem border="right">{this.renderLogo()}</EuiHeaderSectionItem>
|
||||||
|
|
||||||
|
<HeaderNavControls navControls={leftNavControls} />
|
||||||
|
|
||||||
|
<HeaderBreadcrumbs appTitle={appTitle} breadcrumbs={breadcrumbs} />
|
||||||
|
</EuiHeaderSection>
|
||||||
|
|
||||||
|
<EuiHeaderSection side="right">
|
||||||
|
<HeaderNavControls navControls={rightNavControls} />
|
||||||
|
|
||||||
|
<EuiHeaderSectionItem>
|
||||||
|
<HeaderAppMenu navLinks={navLinks} />
|
||||||
|
</EuiHeaderSectionItem>
|
||||||
|
</EuiHeaderSection>
|
||||||
|
</EuiHeader>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,106 @@
|
||||||
|
/*
|
||||||
|
* 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 React, { Component } from 'react';
|
||||||
|
|
||||||
|
import {
|
||||||
|
// TODO: add type annotations
|
||||||
|
// @ts-ignore
|
||||||
|
EuiHeaderSectionItemButton,
|
||||||
|
// @ts-ignore
|
||||||
|
EuiIcon,
|
||||||
|
// @ts-ignore
|
||||||
|
EuiKeyPadMenu,
|
||||||
|
// @ts-ignore
|
||||||
|
EuiKeyPadMenuItem,
|
||||||
|
EuiPopover,
|
||||||
|
} from '@elastic/eui';
|
||||||
|
|
||||||
|
import { NavLink } from '../';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
navLinks: NavLink[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface State {
|
||||||
|
isOpen: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class HeaderAppMenu extends Component<Props, State> {
|
||||||
|
constructor(props: Props) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
isOpen: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public render() {
|
||||||
|
const { navLinks = [] } = this.props;
|
||||||
|
|
||||||
|
const button = (
|
||||||
|
<EuiHeaderSectionItemButton
|
||||||
|
aria-controls="keyPadMenu"
|
||||||
|
aria-expanded={this.state.isOpen}
|
||||||
|
aria-haspopup="true"
|
||||||
|
aria-label="Apps menu"
|
||||||
|
onClick={this.onMenuButtonClick}
|
||||||
|
>
|
||||||
|
<EuiIcon type="apps" size="m" />
|
||||||
|
</EuiHeaderSectionItemButton>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<EuiPopover
|
||||||
|
id="headerAppMenu"
|
||||||
|
button={button}
|
||||||
|
isOpen={this.state.isOpen}
|
||||||
|
anchorPosition="downRight"
|
||||||
|
closePopover={this.closeMenu}
|
||||||
|
>
|
||||||
|
<EuiKeyPadMenu id="keyPadMenu" style={{ width: 288 }}>
|
||||||
|
{navLinks.map(this.renderNavLink)}
|
||||||
|
</EuiKeyPadMenu>
|
||||||
|
</EuiPopover>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private onMenuButtonClick = () => {
|
||||||
|
this.setState({
|
||||||
|
isOpen: !this.state.isOpen,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
private closeMenu = () => {
|
||||||
|
this.setState({
|
||||||
|
isOpen: false,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
private renderNavLink = (navLink: NavLink) => (
|
||||||
|
<EuiKeyPadMenuItem
|
||||||
|
label={navLink.title}
|
||||||
|
href={navLink.url}
|
||||||
|
key={navLink.id}
|
||||||
|
onClick={this.closeMenu}
|
||||||
|
>
|
||||||
|
<EuiIcon type={navLink.euiIconType} size="l" />
|
||||||
|
</EuiKeyPadMenuItem>
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,42 @@
|
||||||
|
/*
|
||||||
|
* 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 { mount } from 'enzyme';
|
||||||
|
import React from 'react';
|
||||||
|
import { breadcrumbs, set } from '../../../services/breadcrumb_state';
|
||||||
|
import { HeaderBreadcrumbs } from './header_breadcrumbs';
|
||||||
|
|
||||||
|
describe('HeaderBreadcrumbs', () => {
|
||||||
|
it('renders updates to the breadcrumbs observable', () => {
|
||||||
|
const wrapper = mount(<HeaderBreadcrumbs breadcrumbs={breadcrumbs} />);
|
||||||
|
|
||||||
|
set([{ text: 'First' }]);
|
||||||
|
// Unfortunately, enzyme won't update the wrapper until we call update.
|
||||||
|
wrapper.update();
|
||||||
|
expect(wrapper.find('.euiBreadcrumb')).toMatchSnapshot();
|
||||||
|
|
||||||
|
set([{ text: 'First' }, { text: 'Second' }]);
|
||||||
|
wrapper.update();
|
||||||
|
expect(wrapper.find('.euiBreadcrumb')).toMatchSnapshot();
|
||||||
|
|
||||||
|
set([]);
|
||||||
|
wrapper.update();
|
||||||
|
expect(wrapper.find('.euiBreadcrumb')).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,87 @@
|
||||||
|
/*
|
||||||
|
* 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 React, { Component } from 'react';
|
||||||
|
import { Subscribable, Unsubscribable } from 'rxjs';
|
||||||
|
|
||||||
|
import {
|
||||||
|
// @ts-ignore
|
||||||
|
EuiHeaderBreadcrumbs,
|
||||||
|
} from '@elastic/eui';
|
||||||
|
|
||||||
|
import { Breadcrumb } from '../';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
appTitle?: string;
|
||||||
|
breadcrumbs: Subscribable<Breadcrumb[]>;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface State {
|
||||||
|
breadcrumbs: Breadcrumb[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export class HeaderBreadcrumbs extends Component<Props, State> {
|
||||||
|
private unsubscribable?: Unsubscribable;
|
||||||
|
|
||||||
|
constructor(props: Props) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this.state = { breadcrumbs: [] };
|
||||||
|
}
|
||||||
|
|
||||||
|
public componentDidMount() {
|
||||||
|
this.subscribe();
|
||||||
|
}
|
||||||
|
|
||||||
|
public componentDidUpdate(prevProps: Props) {
|
||||||
|
if (prevProps.breadcrumbs === this.props.breadcrumbs) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.unsubscribe();
|
||||||
|
this.subscribe();
|
||||||
|
}
|
||||||
|
|
||||||
|
public componentWillUnmount() {
|
||||||
|
this.unsubscribe();
|
||||||
|
}
|
||||||
|
|
||||||
|
public render() {
|
||||||
|
let breadcrumbs = this.state.breadcrumbs;
|
||||||
|
|
||||||
|
if (breadcrumbs.length === 0 && this.props.appTitle) {
|
||||||
|
breadcrumbs = [{ text: this.props.appTitle }];
|
||||||
|
}
|
||||||
|
|
||||||
|
return <EuiHeaderBreadcrumbs breadcrumbs={breadcrumbs} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
private subscribe() {
|
||||||
|
this.unsubscribable = this.props.breadcrumbs.subscribe(breadcrumbs => {
|
||||||
|
this.setState({ breadcrumbs });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private unsubscribe() {
|
||||||
|
if (this.unsubscribable) {
|
||||||
|
this.unsubscribable.unsubscribe();
|
||||||
|
delete this.unsubscribable;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,50 @@
|
||||||
|
/*
|
||||||
|
* 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 { mount } from 'enzyme';
|
||||||
|
import React from 'react';
|
||||||
|
import { NavControl, NavControlSide } from '../';
|
||||||
|
import { HeaderNavControl } from './header_nav_control';
|
||||||
|
|
||||||
|
describe('HeaderNavControl', () => {
|
||||||
|
const defaultNavControl = { name: '', order: 1, side: NavControlSide.Right };
|
||||||
|
|
||||||
|
it('calls navControl.render with div node', () => {
|
||||||
|
const renderSpy = jest.fn();
|
||||||
|
const navControl = { ...defaultNavControl, render: renderSpy } as NavControl;
|
||||||
|
|
||||||
|
mount(<HeaderNavControl navControl={navControl} />);
|
||||||
|
|
||||||
|
expect(renderSpy.mock.calls.length).toEqual(1);
|
||||||
|
|
||||||
|
const [divNode] = renderSpy.mock.calls[0];
|
||||||
|
expect(divNode).toBeInstanceOf(HTMLElement);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('calls unrender callback when unmounted', () => {
|
||||||
|
const unrenderSpy = jest.fn();
|
||||||
|
const render = () => unrenderSpy;
|
||||||
|
const navControl = { ...defaultNavControl, render } as NavControl;
|
||||||
|
|
||||||
|
const wrapper = mount(<HeaderNavControl navControl={navControl} />);
|
||||||
|
|
||||||
|
wrapper.unmount();
|
||||||
|
expect(unrenderSpy.mock.calls.length).toEqual(1);
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,64 @@
|
||||||
|
/*
|
||||||
|
* 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 React from 'react';
|
||||||
|
import { NavControl } from '../';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
navControl: NavControl;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class HeaderNavControl extends React.Component<Props> {
|
||||||
|
private readonly ref = React.createRef<HTMLDivElement>();
|
||||||
|
private unrender?: () => void;
|
||||||
|
|
||||||
|
public componentDidMount() {
|
||||||
|
if (!this.ref.current) {
|
||||||
|
throw new Error('<NavControl /> mounted without ref');
|
||||||
|
}
|
||||||
|
|
||||||
|
this.unrender = this.props.navControl.render(this.ref.current) || undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
public componentDidUpdate(prevProps: Props) {
|
||||||
|
if (this.props.navControl.render === prevProps.navControl.render) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.ref.current) {
|
||||||
|
throw new Error('<NavControl /> updated without ref');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.unrender) {
|
||||||
|
this.unrender();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.unrender = this.props.navControl.render(this.ref.current) || undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
public componentWillUnmount() {
|
||||||
|
if (this.unrender) {
|
||||||
|
this.unrender();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public render() {
|
||||||
|
return <div ref={this.ref} />;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,50 @@
|
||||||
|
/*
|
||||||
|
* 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 React, { Component } from 'react';
|
||||||
|
|
||||||
|
import {
|
||||||
|
// @ts-ignore
|
||||||
|
EuiHeaderSectionItem,
|
||||||
|
} from '@elastic/eui';
|
||||||
|
|
||||||
|
import { NavControl } from '../';
|
||||||
|
import { HeaderNavControl } from './header_nav_control';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
navControls: NavControl[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export class HeaderNavControls extends Component<Props> {
|
||||||
|
public render() {
|
||||||
|
const { navControls } = this.props;
|
||||||
|
|
||||||
|
if (!navControls) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return navControls.map(this.renderNavControl);
|
||||||
|
}
|
||||||
|
|
||||||
|
private renderNavControl = (navControl: NavControl) => (
|
||||||
|
<EuiHeaderSectionItem key={navControl.name}>
|
||||||
|
<HeaderNavControl navControl={navControl} />
|
||||||
|
</EuiHeaderSectionItem>
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,47 @@
|
||||||
|
/*
|
||||||
|
* 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 { uiModules } from '../../../modules';
|
||||||
|
import { Header } from './components/header';
|
||||||
|
import './header_global_nav.less';
|
||||||
|
import { chromeHeaderNavControlsRegistry } from 'ui/registry/chrome_header_nav_controls';
|
||||||
|
import { breadcrumbs } from '../../services/breadcrumb_state';
|
||||||
|
|
||||||
|
const module = uiModules.get('kibana');
|
||||||
|
|
||||||
|
module.directive('headerGlobalNav', (reactDirective, chrome, Private) => {
|
||||||
|
const navControls = Private(chromeHeaderNavControlsRegistry);
|
||||||
|
const navLinks = chrome.getNavLinks();
|
||||||
|
const homeHref = chrome.addBasePath('/app/kibana#/home');
|
||||||
|
|
||||||
|
return reactDirective(Header, [
|
||||||
|
// scope accepted by directive, passed in as React props
|
||||||
|
'appTitle',
|
||||||
|
'isVisible',
|
||||||
|
],
|
||||||
|
{},
|
||||||
|
// angular injected React props
|
||||||
|
{
|
||||||
|
breadcrumbs,
|
||||||
|
navLinks,
|
||||||
|
navControls,
|
||||||
|
homeHref
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,11 @@
|
||||||
|
.header-global-wrapper {
|
||||||
|
width: 100%;
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-global-wrapper + .app-wrapper {
|
||||||
|
top: 65px;
|
||||||
|
left: 0;
|
||||||
|
}
|
45
src/ui/public/chrome/directives/header_global_nav/index.ts
Normal file
45
src/ui/public/chrome/directives/header_global_nav/index.ts
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
/*
|
||||||
|
* 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 { IconType } from '@elastic/eui';
|
||||||
|
import './header_global_nav';
|
||||||
|
|
||||||
|
export enum NavControlSide {
|
||||||
|
Left = 'left',
|
||||||
|
Right = 'right',
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface NavControl {
|
||||||
|
name: string;
|
||||||
|
order: number;
|
||||||
|
side: NavControlSide;
|
||||||
|
render: (targetDomElement: HTMLDivElement) => (() => void) | void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface NavLink {
|
||||||
|
title: string;
|
||||||
|
url: string;
|
||||||
|
id: string;
|
||||||
|
euiIconType: IconType;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Breadcrumb {
|
||||||
|
text: string;
|
||||||
|
href?: string;
|
||||||
|
}
|
|
@ -19,6 +19,7 @@
|
||||||
|
|
||||||
|
|
||||||
import './global_nav';
|
import './global_nav';
|
||||||
|
import './header_global_nav';
|
||||||
|
|
||||||
import { kbnChromeProvider } from './kbn_chrome';
|
import { kbnChromeProvider } from './kbn_chrome';
|
||||||
import { kbnAppendChromeNavControls } from './append_nav_controls';
|
import { kbnAppendChromeNavControls } from './append_nav_controls';
|
||||||
|
|
|
@ -1,5 +1,8 @@
|
||||||
<div class="content" chrome-context data-test-subj="kibanaChrome">
|
<div class="content" chrome-context data-test-subj="kibanaChrome">
|
||||||
|
<kbn-loading-indicator></kbn-loading-indicator>
|
||||||
|
|
||||||
<global-nav
|
<global-nav
|
||||||
|
ng-if="!k7design"
|
||||||
chrome="chrome"
|
chrome="chrome"
|
||||||
data-test-subj="globalNav"
|
data-test-subj="globalNav"
|
||||||
is-visible="chrome.getVisible()"
|
is-visible="chrome.getVisible()"
|
||||||
|
@ -8,6 +11,13 @@
|
||||||
app-title="chrome.getAppTitle()"
|
app-title="chrome.getAppTitle()"
|
||||||
></global-nav>
|
></global-nav>
|
||||||
|
|
||||||
|
<header-global-nav
|
||||||
|
ng-if="k7design"
|
||||||
|
class="header-global-wrapper"
|
||||||
|
is-visible="chrome.getVisible()"
|
||||||
|
app-title="chrome.getAppTitle()"
|
||||||
|
></header-global-nav>
|
||||||
|
|
||||||
<div class="app-wrapper" ng-class="{ 'hidden-chrome': !chrome.getVisible() }">
|
<div class="app-wrapper" ng-class="{ 'hidden-chrome': !chrome.getVisible() }">
|
||||||
<div class="app-wrapper-panel">
|
<div class="app-wrapper-panel">
|
||||||
<kbn-notifications
|
<kbn-notifications
|
||||||
|
@ -16,8 +26,6 @@
|
||||||
|
|
||||||
<div id="globalBannerList"></div>
|
<div id="globalBannerList"></div>
|
||||||
|
|
||||||
<kbn-loading-indicator></kbn-loading-indicator>
|
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="application"
|
class="application"
|
||||||
ng-class="'tab-' + chrome.getFirstPathSegment() + ' ' + chrome.getApplicationClasses()"
|
ng-class="'tab-' + chrome.getFirstPathSegment() + ' ' + chrome.getApplicationClasses()"
|
||||||
|
|
|
@ -57,7 +57,9 @@ export function kbnChromeProvider(chrome, internals) {
|
||||||
},
|
},
|
||||||
|
|
||||||
controllerAs: 'chrome',
|
controllerAs: 'chrome',
|
||||||
controller($scope, $rootScope, $location, $http, Private) {
|
controller($scope, $rootScope, $location, $http, Private, config) {
|
||||||
|
config.watch('k7design', (val) => $scope.k7design = val);
|
||||||
|
|
||||||
const getUnhashableStates = Private(getUnhashableStatesProvider);
|
const getUnhashableStates = Private(getUnhashableStatesProvider);
|
||||||
|
|
||||||
// are we showing the embedded version of the chrome?
|
// are we showing the embedded version of the chrome?
|
||||||
|
|
|
@ -14,7 +14,7 @@
|
||||||
top: 0; // 1
|
top: 0; // 1
|
||||||
left: 0; // 1
|
left: 0; // 1
|
||||||
right: 0; // 1
|
right: 0; // 1
|
||||||
z-index: 1; // 1
|
z-index: 20; // 1
|
||||||
overflow: hidden; // 2
|
overflow: hidden; // 2
|
||||||
height: @loadingIndicatorHeight;
|
height: @loadingIndicatorHeight;
|
||||||
|
|
||||||
|
@ -31,7 +31,7 @@
|
||||||
right: 0;
|
right: 0;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
z-index: 10;
|
z-index: 21;
|
||||||
visibility: visible;
|
visibility: visible;
|
||||||
display: block;
|
display: block;
|
||||||
animation: animate-loading-indicator 2s linear infinite;
|
animation: animate-loading-indicator 2s linear infinite;
|
||||||
|
|
62
src/ui/public/chrome/services/breadcrumb_state.ts
Normal file
62
src/ui/public/chrome/services/breadcrumb_state.ts
Normal file
|
@ -0,0 +1,62 @@
|
||||||
|
/*
|
||||||
|
* 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 { Subject, Subscribable } from 'rxjs';
|
||||||
|
// @ts-ignore
|
||||||
|
import { uiModules } from '../../modules';
|
||||||
|
import { Breadcrumb } from '../directives/header_global_nav';
|
||||||
|
|
||||||
|
// A flag used to keep track of clearing between route changes.
|
||||||
|
let shouldClear = false;
|
||||||
|
|
||||||
|
// Subject used by Header component to subscribe to breadcrumbs changes.
|
||||||
|
// This is not exposed publicly.
|
||||||
|
const breadcrumbsSubject = new Subject();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A rxjs subscribable that can be used to subscribe to breadcrumb updates.
|
||||||
|
*/
|
||||||
|
export const breadcrumbs: Subscribable<Breadcrumb[]> = breadcrumbsSubject;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Should be called by plugins to set breadcrumbs in the header navigation.
|
||||||
|
*
|
||||||
|
* @param breadcrumbs: Array<Breadcrumb> where Breadcrumb has shape
|
||||||
|
* { text: '', href?: '' }
|
||||||
|
*/
|
||||||
|
export const set = (newBreadcrumbs: Breadcrumb[]) => {
|
||||||
|
breadcrumbsSubject.next(newBreadcrumbs);
|
||||||
|
|
||||||
|
// If a plugin called set, don't clear on route change.
|
||||||
|
shouldClear = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
uiModules.get('kibana').service('breadcrumbState', ($rootScope: any) => {
|
||||||
|
// When a route change happens we want to clear the breadcrumbs ONLY if
|
||||||
|
// the new route does not set any breadcrumbs. Deferring the clearing until
|
||||||
|
// the route finishes changing helps avoiding the breadcrumbs from 'flickering'.
|
||||||
|
$rootScope.$on('$routeChangeStart', () => (shouldClear = true));
|
||||||
|
$rootScope.$on('$routeChangeSuccess', () => {
|
||||||
|
if (shouldClear) {
|
||||||
|
set([]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return { set };
|
||||||
|
});
|
|
@ -18,3 +18,4 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import './global_nav_state';
|
import './global_nav_state';
|
||||||
|
import './breadcrumb_state';
|
||||||
|
|
|
@ -99,7 +99,11 @@ describe('IndexedArray', function () {
|
||||||
reg.push(firstUser);
|
reg.push(firstUser);
|
||||||
|
|
||||||
// end up with the same structure that is in the users fixture
|
// end up with the same structure that is in the users fixture
|
||||||
expect(reg.byGroup).to.eql(users.byGroup);
|
expect(Object.keys(reg.byGroup).length).to.be(Object.keys(users.byGroup).length);
|
||||||
|
for (const group of Object.keys(reg.byGroup)) {
|
||||||
|
expect(reg.byGroup[group].toJSON()).to.eql(users.byGroup[group]);
|
||||||
|
}
|
||||||
|
|
||||||
expect(reg.inIdOrder).to.eql(users.inIdOrder);
|
expect(reg.inIdOrder).to.eql(users.inIdOrder);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
41
src/ui/public/indexed_array/index.d.ts
vendored
Normal file
41
src/ui/public/indexed_array/index.d.ts
vendored
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
/*
|
||||||
|
* 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 { ListIterator } from 'lodash';
|
||||||
|
|
||||||
|
interface IndexedArrayConfig<T> {
|
||||||
|
index?: string[];
|
||||||
|
group?: string[];
|
||||||
|
order?: string[];
|
||||||
|
initialSet?: T[];
|
||||||
|
immutable?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare class IndexedArray<T> extends Array<T> {
|
||||||
|
public immutable: boolean;
|
||||||
|
public raw: T[];
|
||||||
|
// May not actually be present, is dynamically defined.
|
||||||
|
public inOrder: T[];
|
||||||
|
|
||||||
|
constructor(config: IndexedArrayConfig<T>);
|
||||||
|
|
||||||
|
public remove(predicate: ListIterator<T, boolean>): T[];
|
||||||
|
|
||||||
|
public toJSON(): T[];
|
||||||
|
}
|
|
@ -51,7 +51,7 @@ export class IndexedArray {
|
||||||
Object.defineProperty(this, 'raw', { value: [] });
|
Object.defineProperty(this, 'raw', { value: [] });
|
||||||
|
|
||||||
this._indexNames = _.union(
|
this._indexNames = _.union(
|
||||||
this._setupIndex(config.group, inflectIndex, organizeBy),
|
this._setupIndex(config.group, inflectIndex, organizeByIndexedArray(config)),
|
||||||
this._setupIndex(config.index, inflectIndex, _.indexBy),
|
this._setupIndex(config.index, inflectIndex, _.indexBy),
|
||||||
this._setupIndex(config.order, inflectOrder, (raw, pluckValue) => {
|
this._setupIndex(config.order, inflectOrder, (raw, pluckValue) => {
|
||||||
return [...raw].sort((itemA, itemB) => {
|
return [...raw].sort((itemA, itemB) => {
|
||||||
|
@ -195,3 +195,20 @@ export class IndexedArray {
|
||||||
// using traditional `extends Array` syntax doesn't work with babel
|
// using traditional `extends Array` syntax doesn't work with babel
|
||||||
// See https://babeljs.io/docs/usage/caveats/
|
// See https://babeljs.io/docs/usage/caveats/
|
||||||
Object.setPrototypeOf(IndexedArray.prototype, Array.prototype);
|
Object.setPrototypeOf(IndexedArray.prototype, Array.prototype);
|
||||||
|
|
||||||
|
|
||||||
|
// Similar to `organizeBy` but returns IndexedArrays instead of normal Arrays.
|
||||||
|
function organizeByIndexedArray(config) {
|
||||||
|
return (...args) => {
|
||||||
|
const grouped = organizeBy(...args);
|
||||||
|
|
||||||
|
return _.reduce(grouped, (acc, value, group) => {
|
||||||
|
acc[group] = new IndexedArray({
|
||||||
|
...config,
|
||||||
|
initialSet: value
|
||||||
|
});
|
||||||
|
|
||||||
|
return acc;
|
||||||
|
}, {});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
data-test-subj="breadcrumbs"
|
data-test-subj="breadcrumbs"
|
||||||
role="heading"
|
role="heading"
|
||||||
aria-level="1"
|
aria-level="1"
|
||||||
|
ng-if="showPluginBreadcrumbs"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="kuiLocalBreadcrumb"
|
class="kuiLocalBreadcrumb"
|
||||||
|
|
|
@ -26,7 +26,6 @@ const module = uiModules.get('kibana');
|
||||||
module.directive('breadCrumbs', function () {
|
module.directive('breadCrumbs', function () {
|
||||||
return {
|
return {
|
||||||
restrict: 'E',
|
restrict: 'E',
|
||||||
replace: true,
|
|
||||||
scope: {
|
scope: {
|
||||||
omitCurrentPage: '=',
|
omitCurrentPage: '=',
|
||||||
/**
|
/**
|
||||||
|
@ -47,7 +46,8 @@ module.directive('breadCrumbs', function () {
|
||||||
useLinks: '='
|
useLinks: '='
|
||||||
},
|
},
|
||||||
template: breadCrumbsTemplate,
|
template: breadCrumbsTemplate,
|
||||||
controller: function ($scope) {
|
controller: function ($scope, config, breadcrumbState) {
|
||||||
|
config.watch('k7design', (val) => $scope.showPluginBreadcrumbs = !val);
|
||||||
|
|
||||||
function omitPagesFilter(crumb) {
|
function omitPagesFilter(crumb) {
|
||||||
return (
|
return (
|
||||||
|
@ -70,6 +70,15 @@ module.directive('breadCrumbs', function () {
|
||||||
.filter(omitPagesFilter)
|
.filter(omitPagesFilter)
|
||||||
.filter(omitCurrentPageFilter)
|
.filter(omitCurrentPageFilter)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const newBreadcrumbs = $scope.breadcrumbs
|
||||||
|
.map(b => ({ text: b.display, href: b.href }));
|
||||||
|
|
||||||
|
if ($scope.pageTitle) {
|
||||||
|
newBreadcrumbs.push({ text: $scope.pageTitle });
|
||||||
|
}
|
||||||
|
|
||||||
|
breadcrumbState.set(newBreadcrumbs);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
31
src/ui/public/registry/_registry.d.ts
vendored
Normal file
31
src/ui/public/registry/_registry.d.ts
vendored
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
/*
|
||||||
|
* 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 { IndexedArray, IndexedArrayConfig } from '../indexed_array';
|
||||||
|
|
||||||
|
interface UIRegistry<T> extends IndexedArray<T> {
|
||||||
|
register<T>(privateModule: T): UIRegistry<T>;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface UIRegistrySpec<T> extends IndexedArrayConfig<T> {
|
||||||
|
name: string;
|
||||||
|
filter?(item: T): boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare function uiRegistry<T>(spec: UIRegistrySpec<T>): UIRegistry<T>;
|
37
src/ui/public/registry/chrome_header_nav_controls.ts
Normal file
37
src/ui/public/registry/chrome_header_nav_controls.ts
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
/*
|
||||||
|
* 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 { NavControl } from '../chrome/directives/header_global_nav';
|
||||||
|
import { IndexedArray } from '../indexed_array';
|
||||||
|
import { uiRegistry, UIRegistry } from './_registry';
|
||||||
|
|
||||||
|
interface BySideDictionary {
|
||||||
|
// this key should be from NavControlSide
|
||||||
|
[side: string]: IndexedArray<NavControl>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ChromeHeaderNavControlsRegistry extends UIRegistry<NavControl> {
|
||||||
|
bySide: BySideDictionary;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const chromeHeaderNavControlsRegistry: ChromeHeaderNavControlsRegistry = uiRegistry({
|
||||||
|
name: 'chromeHeaderNavControls',
|
||||||
|
order: ['order'],
|
||||||
|
group: ['side'],
|
||||||
|
}) as ChromeHeaderNavControlsRegistry;
|
|
@ -23,4 +23,3 @@ export const chromeNavControlsRegistry = uiRegistry({
|
||||||
name: 'chromeNavControls',
|
name: 'chromeNavControls',
|
||||||
order: ['order']
|
order: ['order']
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -29,6 +29,7 @@ export class UiApp {
|
||||||
order = 0,
|
order = 0,
|
||||||
description,
|
description,
|
||||||
icon,
|
icon,
|
||||||
|
euiIconType,
|
||||||
hidden,
|
hidden,
|
||||||
linkToLastSubUrl,
|
linkToLastSubUrl,
|
||||||
listed,
|
listed,
|
||||||
|
@ -45,6 +46,7 @@ export class UiApp {
|
||||||
this._order = order;
|
this._order = order;
|
||||||
this._description = description;
|
this._description = description;
|
||||||
this._icon = icon;
|
this._icon = icon;
|
||||||
|
this._euiIconType = euiIconType;
|
||||||
this._linkToLastSubUrl = linkToLastSubUrl;
|
this._linkToLastSubUrl = linkToLastSubUrl;
|
||||||
this._hidden = hidden;
|
this._hidden = hidden;
|
||||||
this._listed = listed;
|
this._listed = listed;
|
||||||
|
@ -66,6 +68,7 @@ export class UiApp {
|
||||||
order: this._order,
|
order: this._order,
|
||||||
description: this._description,
|
description: this._description,
|
||||||
icon: this._icon,
|
icon: this._icon,
|
||||||
|
euiIconType: this._euiIconType,
|
||||||
url: this._url,
|
url: this._url,
|
||||||
linkToLastSubUrl: this._linkToLastSubUrl
|
linkToLastSubUrl: this._linkToLastSubUrl
|
||||||
});
|
});
|
||||||
|
@ -117,6 +120,7 @@ export class UiApp {
|
||||||
title: this._title,
|
title: this._title,
|
||||||
description: this._description,
|
description: this._description,
|
||||||
icon: this._icon,
|
icon: this._icon,
|
||||||
|
euiIconType: this._euiIconType,
|
||||||
main: this._main,
|
main: this._main,
|
||||||
navLink: this._navLink,
|
navLink: this._navLink,
|
||||||
linkToLastSubUrl: this._linkToLastSubUrl,
|
linkToLastSubUrl: this._linkToLastSubUrl,
|
||||||
|
|
|
@ -29,6 +29,7 @@ function applySpecDefaults(spec, type, pluginSpec) {
|
||||||
order = 0,
|
order = 0,
|
||||||
description = '',
|
description = '',
|
||||||
icon,
|
icon,
|
||||||
|
euiIconType,
|
||||||
hidden = false,
|
hidden = false,
|
||||||
linkToLastSubUrl = true,
|
linkToLastSubUrl = true,
|
||||||
listed = !hidden,
|
listed = !hidden,
|
||||||
|
@ -55,6 +56,7 @@ function applySpecDefaults(spec, type, pluginSpec) {
|
||||||
order,
|
order,
|
||||||
description,
|
description,
|
||||||
icon,
|
icon,
|
||||||
|
euiIconType,
|
||||||
hidden,
|
hidden,
|
||||||
linkToLastSubUrl,
|
linkToLastSubUrl,
|
||||||
listed,
|
listed,
|
||||||
|
|
|
@ -31,6 +31,7 @@ describe('UiNavLink', () => {
|
||||||
url: '/app/kibana#/discover',
|
url: '/app/kibana#/discover',
|
||||||
description: 'interactively explore your data',
|
description: 'interactively explore your data',
|
||||||
icon: 'plugins/kibana/assets/discover.svg',
|
icon: 'plugins/kibana/assets/discover.svg',
|
||||||
|
euiIconType: 'discoverApp',
|
||||||
hidden: true,
|
hidden: true,
|
||||||
disabled: true
|
disabled: true
|
||||||
};
|
};
|
||||||
|
@ -44,6 +45,7 @@ describe('UiNavLink', () => {
|
||||||
subUrlBase: spec.url,
|
subUrlBase: spec.url,
|
||||||
description: spec.description,
|
description: spec.description,
|
||||||
icon: spec.icon,
|
icon: spec.icon,
|
||||||
|
euiIconType: spec.euiIconType,
|
||||||
hidden: spec.hidden,
|
hidden: spec.hidden,
|
||||||
disabled: spec.disabled,
|
disabled: spec.disabled,
|
||||||
|
|
||||||
|
|
|
@ -27,6 +27,7 @@ export class UiNavLink {
|
||||||
subUrlBase,
|
subUrlBase,
|
||||||
description,
|
description,
|
||||||
icon,
|
icon,
|
||||||
|
euiIconType,
|
||||||
linkToLastSubUrl = true,
|
linkToLastSubUrl = true,
|
||||||
hidden = false,
|
hidden = false,
|
||||||
disabled = false,
|
disabled = false,
|
||||||
|
@ -40,6 +41,7 @@ export class UiNavLink {
|
||||||
this._subUrlBase = subUrlBase || url;
|
this._subUrlBase = subUrlBase || url;
|
||||||
this._description = description;
|
this._description = description;
|
||||||
this._icon = icon;
|
this._icon = icon;
|
||||||
|
this._euiIconType = euiIconType;
|
||||||
this._linkToLastSubUrl = linkToLastSubUrl;
|
this._linkToLastSubUrl = linkToLastSubUrl;
|
||||||
this._hidden = hidden;
|
this._hidden = hidden;
|
||||||
this._disabled = disabled;
|
this._disabled = disabled;
|
||||||
|
@ -59,6 +61,7 @@ export class UiNavLink {
|
||||||
subUrlBase: this._subUrlBase,
|
subUrlBase: this._subUrlBase,
|
||||||
description: this._description,
|
description: this._description,
|
||||||
icon: this._icon,
|
icon: this._icon,
|
||||||
|
euiIconType: this._euiIconType,
|
||||||
linkToLastSubUrl: this._linkToLastSubUrl,
|
linkToLastSubUrl: this._linkToLastSubUrl,
|
||||||
hidden: this._hidden,
|
hidden: this._hidden,
|
||||||
disabled: this._disabled,
|
disabled: this._disabled,
|
||||||
|
|
|
@ -22,7 +22,8 @@ export function apm(kibana) {
|
||||||
title: 'APM',
|
title: 'APM',
|
||||||
description: 'APM for the Elastic Stack',
|
description: 'APM for the Elastic Stack',
|
||||||
main: 'plugins/apm/index',
|
main: 'plugins/apm/index',
|
||||||
icon: 'plugins/apm/icon.svg'
|
icon: 'plugins/apm/icon.svg',
|
||||||
|
euiIconType: 'apmApp'
|
||||||
},
|
},
|
||||||
home: ['plugins/apm/register_feature'],
|
home: ['plugins/apm/register_feature'],
|
||||||
injectDefaultVars(server) {
|
injectDefaultVars(server) {
|
||||||
|
|
|
@ -9,12 +9,37 @@ import { withBreadcrumbs } from 'react-router-breadcrumbs-hoc';
|
||||||
import { toQuery } from '../../../utils/url';
|
import { toQuery } from '../../../utils/url';
|
||||||
import { routes } from './routeConfig';
|
import { routes } from './routeConfig';
|
||||||
import { flatten, capitalize } from 'lodash';
|
import { flatten, capitalize } from 'lodash';
|
||||||
|
import { set } from 'ui/chrome/services/breadcrumb_state';
|
||||||
|
|
||||||
class Breadcrumbs extends React.Component {
|
class Breadcrumbs extends React.Component {
|
||||||
|
updateHeaderBreadcrumbs() {
|
||||||
|
const { _g = '', kuery = '' } = toQuery(this.props.location.search);
|
||||||
|
const breadcrumbs = this.props.breadcrumbs.map(({ breadcrumb, match }) => ({
|
||||||
|
text: breadcrumb,
|
||||||
|
href: `#${match.url}?_g=${_g}&kuery=${kuery}`
|
||||||
|
}));
|
||||||
|
|
||||||
|
set(breadcrumbs);
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
this.updateHeaderBreadcrumbs();
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidUpdate() {
|
||||||
|
this.updateHeaderBreadcrumbs();
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { breadcrumbs, location } = this.props;
|
const { breadcrumbs, location, showPluginBreadcrumbs } = this.props;
|
||||||
const { _g = '', kuery = '' } = toQuery(location.search);
|
const { _g = '', kuery = '' } = toQuery(location.search);
|
||||||
|
|
||||||
|
// If we don't display plugin breadcrumbs, render null, but continue
|
||||||
|
// to push updates to header.
|
||||||
|
if (!showPluginBreadcrumbs) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="kuiLocalBreadcrumbs">
|
<div className="kuiLocalBreadcrumbs">
|
||||||
{breadcrumbs.map(({ breadcrumb, path, match }, i) => {
|
{breadcrumbs.map(({ breadcrumb, path, match }, i) => {
|
||||||
|
|
|
@ -36,7 +36,7 @@ jest.mock(
|
||||||
function expectBreadcrumbToMatchSnapshot(route) {
|
function expectBreadcrumbToMatchSnapshot(route) {
|
||||||
const wrapper = mount(
|
const wrapper = mount(
|
||||||
<MemoryRouter initialEntries={[`${route}?_g=myG&kuery=myKuery`]}>
|
<MemoryRouter initialEntries={[`${route}?_g=myG&kuery=myKuery`]}>
|
||||||
<Breadcrumbs />
|
<Breadcrumbs showPluginBreadcrumbs={true} />
|
||||||
</MemoryRouter>
|
</MemoryRouter>
|
||||||
);
|
);
|
||||||
expect(
|
expect(
|
||||||
|
@ -74,4 +74,13 @@ describe('Breadcrumbs', () => {
|
||||||
'/:serviceName/transactions/request/my-transaction-name'
|
'/:serviceName/transactions/request/my-transaction-name'
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('does not render breadcrumbs when showPluginBreadcrumbs = false', () => {
|
||||||
|
const wrapper = mount(
|
||||||
|
<MemoryRouter initialEntries={[`/?_g=myG&kuery=myKuery`]}>
|
||||||
|
<Breadcrumbs showPluginBreadcrumbs={false} />
|
||||||
|
</MemoryRouter>
|
||||||
|
);
|
||||||
|
expect(wrapper.find('.kuiLocalBreadcrumbs').exists()).toEqual(false);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -31,9 +31,13 @@ chrome.setRootTemplate(template);
|
||||||
const store = configureStore();
|
const store = configureStore();
|
||||||
|
|
||||||
initTimepicker(history, store.dispatch).then(() => {
|
initTimepicker(history, store.dispatch).then(() => {
|
||||||
|
const showPluginBreadcrumbs = !chrome
|
||||||
|
.getUiSettingsClient()
|
||||||
|
.get('k7design', false);
|
||||||
|
|
||||||
ReactDOM.render(
|
ReactDOM.render(
|
||||||
<Router history={history}>
|
<Router history={history}>
|
||||||
<Breadcrumbs />
|
<Breadcrumbs showPluginBreadcrumbs={showPluginBreadcrumbs} />
|
||||||
</Router>,
|
</Router>,
|
||||||
document.getElementById('react-apm-breadcrumbs')
|
document.getElementById('react-apm-breadcrumbs')
|
||||||
);
|
);
|
||||||
|
|
|
@ -20,6 +20,7 @@ export function canvas(kibana) {
|
||||||
title: 'Canvas',
|
title: 'Canvas',
|
||||||
description: 'Data driven workpads',
|
description: 'Data driven workpads',
|
||||||
icon: 'plugins/canvas/icon.svg',
|
icon: 'plugins/canvas/icon.svg',
|
||||||
|
euiIconType: 'canvasApp',
|
||||||
main: 'plugins/canvas/app',
|
main: 'plugins/canvas/app',
|
||||||
},
|
},
|
||||||
styleSheetPaths: `${__dirname}/public/style/index.scss`,
|
styleSheetPaths: `${__dirname}/public/style/index.scss`,
|
||||||
|
|
|
@ -62,12 +62,15 @@ export class Popover extends Component {
|
||||||
return button(handleClick);
|
return button(handleClick);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const appWrapper = document.querySelector('.app-wrapper');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<EuiPopover
|
<EuiPopover
|
||||||
{...rest}
|
{...rest}
|
||||||
button={wrappedButton(this.handleClick)}
|
button={wrappedButton(this.handleClick)}
|
||||||
isOpen={this.state.isPopoverOpen}
|
isOpen={this.state.isPopoverOpen}
|
||||||
closePopover={this.closePopover}
|
closePopover={this.closePopover}
|
||||||
|
container={appWrapper}
|
||||||
>
|
>
|
||||||
{children({ closePopover: this.closePopover })}
|
{children({ closePopover: this.closePopover })}
|
||||||
</EuiPopover>
|
</EuiPopover>
|
||||||
|
|
|
@ -21,6 +21,7 @@ export function graph(kibana) {
|
||||||
title: 'Graph',
|
title: 'Graph',
|
||||||
order: 9000,
|
order: 9000,
|
||||||
icon: 'plugins/graph/icon.png',
|
icon: 'plugins/graph/icon.png',
|
||||||
|
euiIconType: 'graphApp',
|
||||||
description: 'Graph exploration',
|
description: 'Graph exploration',
|
||||||
main: 'plugins/graph/app',
|
main: 'plugins/graph/app',
|
||||||
},
|
},
|
||||||
|
|
|
@ -37,6 +37,7 @@ export const ml = (kibana) => {
|
||||||
title: 'Machine Learning',
|
title: 'Machine Learning',
|
||||||
description: 'Machine Learning for the Elastic Stack',
|
description: 'Machine Learning for the Elastic Stack',
|
||||||
icon: 'plugins/ml/ml.svg',
|
icon: 'plugins/ml/ml.svg',
|
||||||
|
euiIconType: 'machineLearningApp',
|
||||||
main: 'plugins/ml/app',
|
main: 'plugins/ml/app',
|
||||||
},
|
},
|
||||||
hacks: ['plugins/ml/hacks/toggle_app_link_in_nav'],
|
hacks: ['plugins/ml/hacks/toggle_app_link_in_nav'],
|
||||||
|
|
|
@ -1,7 +1,10 @@
|
||||||
<kbn-top-nav name="dashboard" config="topNavMenu">
|
<kbn-top-nav name="dashboard" config="topNavMenu">
|
||||||
<div data-transclude-slots>
|
<div data-transclude-slots>
|
||||||
<!-- Breadcrumbs. -->
|
<!-- Breadcrumbs. -->
|
||||||
<div data-transclude-slot="topLeftCorner" class="kuiLocalBreadcrumbs">
|
<div
|
||||||
|
data-transclude-slot="topLeftCorner"
|
||||||
|
ng-if="showPluginBreadcrumbs"
|
||||||
|
class="kuiLocalBreadcrumbs">
|
||||||
<div ng-repeat="crumb in breadcrumbs" class="kuiLocalBreadcrumb">
|
<div ng-repeat="crumb in breadcrumbs" class="kuiLocalBreadcrumb">
|
||||||
<a ng-if="crumb.url" kbn-href="{{ crumb.url }}" class="kuiLocalBreadcrumb__link">{{ crumb.label }}</a>
|
<a ng-if="crumb.url" kbn-href="{{ crumb.url }}" class="kuiLocalBreadcrumb__link">{{ crumb.label }}</a>
|
||||||
<span ng-if="!crumb.url" class="kuiLocalBreadcrumb__link">{{ crumb.label }}</span>
|
<span ng-if="!crumb.url" class="kuiLocalBreadcrumb__link">{{ crumb.label }}</span>
|
||||||
|
|
|
@ -14,7 +14,7 @@ import uiRouter from 'ui/routes';
|
||||||
import { uiModules } from 'ui/modules';
|
import { uiModules } from 'ui/modules';
|
||||||
const module = uiModules.get('apps/ml');
|
const module = uiModules.get('apps/ml');
|
||||||
|
|
||||||
module.directive('mlNavMenu', function () {
|
module.directive('mlNavMenu', function (breadcrumbState, config) {
|
||||||
return {
|
return {
|
||||||
restrict: 'E',
|
restrict: 'E',
|
||||||
transclude: true,
|
transclude: true,
|
||||||
|
@ -64,6 +64,9 @@ module.directive('mlNavMenu', function () {
|
||||||
});
|
});
|
||||||
scope.breadcrumbs = breadcrumbs.filter(Boolean);
|
scope.breadcrumbs = breadcrumbs.filter(Boolean);
|
||||||
|
|
||||||
|
config.watch('k7design', (val) => scope.showPluginBreadcrumbs = !val);
|
||||||
|
breadcrumbState.set(scope.breadcrumbs.map(b => ({ text: b.label, href: b.url })));
|
||||||
|
|
||||||
// when the page loads, focus on the first breadcrumb
|
// when the page loads, focus on the first breadcrumb
|
||||||
el.ready(() => {
|
el.ready(() => {
|
||||||
const $crumbs = $('.kuiLocalBreadcrumbs a');
|
const $crumbs = $('.kuiLocalBreadcrumbs a');
|
||||||
|
|
|
@ -7,20 +7,24 @@
|
||||||
Breadcrumbs can't be automatically derived because the directive doesn't
|
Breadcrumbs can't be automatically derived because the directive doesn't
|
||||||
automatically know to show the Clusters breadcrumb. We recreate the
|
automatically know to show the Clusters breadcrumb. We recreate the
|
||||||
structure and styles manually -->
|
structure and styles manually -->
|
||||||
<div data-transclude-slot="topLeftCorner" class="kuiLocalBreadcrumbs">
|
<div data-transclude-slot="topLeftCorner">
|
||||||
<div ng-repeat="crumb in monitoringMain.breadcrumbs" class="kuiLocalBreadcrumb">
|
<div
|
||||||
<a
|
ng-if="showPluginBreadcrumbs"
|
||||||
ng-if="crumb.url"
|
class="kuiLocalBreadcrumbs">
|
||||||
kbn-href="{{ crumb.url }}"
|
<div ng-repeat="crumb in monitoringMain.breadcrumbs" class="kuiLocalBreadcrumb">
|
||||||
class="kuiLocalBreadcrumb__link"
|
<a
|
||||||
data-test-subj="{{ crumb.testSubj }}"
|
ng-if="crumb.url"
|
||||||
>
|
kbn-href="{{ crumb.url }}"
|
||||||
{{ crumb.label }}
|
class="kuiLocalBreadcrumb__link"
|
||||||
</a>
|
data-test-subj="{{ crumb.testSubj }}"
|
||||||
|
>
|
||||||
|
{{ crumb.label }}
|
||||||
|
</a>
|
||||||
|
|
||||||
<span ng-if="!crumb.url">
|
<span ng-if="!crumb.url">
|
||||||
{{ crumb.label }}
|
{{ crumb.label }}
|
||||||
</span>
|
</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -71,7 +71,7 @@ export class MonitoringMainController {
|
||||||
}
|
}
|
||||||
|
|
||||||
const uiModule = uiModules.get('plugins/monitoring/directives', []);
|
const uiModule = uiModules.get('plugins/monitoring/directives', []);
|
||||||
uiModule.directive('monitoringMain', (breadcrumbs, license, kbnUrl) => {
|
uiModule.directive('monitoringMain', (breadcrumbs, license, kbnUrl, config) => {
|
||||||
return {
|
return {
|
||||||
restrict: 'E',
|
restrict: 'E',
|
||||||
transclude: true,
|
transclude: true,
|
||||||
|
@ -80,6 +80,7 @@ uiModule.directive('monitoringMain', (breadcrumbs, license, kbnUrl) => {
|
||||||
controllerAs: 'monitoringMain',
|
controllerAs: 'monitoringMain',
|
||||||
bindToController: true,
|
bindToController: true,
|
||||||
link(scope, _element, attributes, controller) {
|
link(scope, _element, attributes, controller) {
|
||||||
|
config.watch('k7design', (val) => scope.showPluginBreadcrumbs = !val);
|
||||||
|
|
||||||
controller.setup({
|
controller.setup({
|
||||||
licenseService: license,
|
licenseService: license,
|
||||||
|
|
|
@ -4,6 +4,8 @@
|
||||||
* you may not use this file except in compliance with the Elastic License.
|
* you may not use this file except in compliance with the Elastic License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { set as setBreadcrumbs } from 'ui/chrome/services/breadcrumb_state';
|
||||||
|
|
||||||
// Helper for making objects to use in a link element
|
// Helper for making objects to use in a link element
|
||||||
const createCrumb = (url, label, testSubj) => {
|
const createCrumb = (url, label, testSubj) => {
|
||||||
const crumb = { url, label };
|
const crumb = { url, label };
|
||||||
|
@ -118,6 +120,8 @@ export function breadcrumbsProvider() {
|
||||||
breadcrumbs = breadcrumbs.concat(getApmBreadcrumbs(mainInstance));
|
breadcrumbs = breadcrumbs.concat(getApmBreadcrumbs(mainInstance));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setBreadcrumbs(breadcrumbs.map(b => ({ text: b.label, href: b.url })));
|
||||||
|
|
||||||
return breadcrumbs;
|
return breadcrumbs;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,6 +16,7 @@ export const uiExports = {
|
||||||
order: 9002,
|
order: 9002,
|
||||||
description: 'Monitoring for Elastic Stack',
|
description: 'Monitoring for Elastic Stack',
|
||||||
icon: 'plugins/monitoring/icons/monitoring.svg',
|
icon: 'plugins/monitoring/icons/monitoring.svg',
|
||||||
|
euiIconType: 'monitoringApp',
|
||||||
linkToLastSubUrl: false,
|
linkToLastSubUrl: false,
|
||||||
main: 'plugins/monitoring/monitoring',
|
main: 'plugins/monitoring/monitoring',
|
||||||
},
|
},
|
||||||
|
|
|
@ -4,15 +4,23 @@
|
||||||
* you may not use this file except in compliance with the Elastic License.
|
* you may not use this file except in compliance with the Elastic License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import ReactDOM from 'react-dom';
|
||||||
import { constant } from 'lodash';
|
import { constant } from 'lodash';
|
||||||
import { chromeNavControlsRegistry } from 'ui/registry/chrome_nav_controls';
|
|
||||||
import { uiModules } from 'ui/modules';
|
import { uiModules } from 'ui/modules';
|
||||||
|
|
||||||
|
import { chromeNavControlsRegistry } from 'ui/registry/chrome_nav_controls';
|
||||||
import template from 'plugins/security/views/nav_control/nav_control.html';
|
import template from 'plugins/security/views/nav_control/nav_control.html';
|
||||||
import 'plugins/security/services/shield_user';
|
import 'plugins/security/services/shield_user';
|
||||||
import '../account/account';
|
import '../account/account';
|
||||||
import { PathProvider } from 'plugins/xpack_main/services/path';
|
import { PathProvider } from 'plugins/xpack_main/services/path';
|
||||||
import { XPackInfoProvider } from 'plugins/xpack_main/services/xpack_info';
|
import { XPackInfoProvider } from 'plugins/xpack_main/services/xpack_info';
|
||||||
|
|
||||||
|
import { chromeHeaderNavControlsRegistry } from 'ui/registry/chrome_header_nav_controls';
|
||||||
|
import { SecurityNavControl } from './nav_control_component';
|
||||||
|
import { NavControlSide } from 'ui/chrome/directives/header_global_nav';
|
||||||
|
|
||||||
chromeNavControlsRegistry.register(constant({
|
chromeNavControlsRegistry.register(constant({
|
||||||
name: 'security',
|
name: 'security',
|
||||||
order: 1000,
|
order: 1000,
|
||||||
|
@ -37,3 +45,27 @@ module.controller('securityNavController', ($scope, ShieldUser, globalNavState,
|
||||||
return tooltip;
|
return tooltip;
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
chromeHeaderNavControlsRegistry.register((ShieldUser, kbnBaseUrl, Private) => ({
|
||||||
|
name: 'security',
|
||||||
|
order: 1000,
|
||||||
|
side: NavControlSide.Right,
|
||||||
|
render(el) {
|
||||||
|
const xpackInfo = Private(XPackInfoProvider);
|
||||||
|
const showSecurityLinks = xpackInfo.get('features.security.showLinks');
|
||||||
|
if (Private(PathProvider).isLoginOrLogout() || !showSecurityLinks) return null;
|
||||||
|
|
||||||
|
const props = {
|
||||||
|
user: ShieldUser.getCurrent(),
|
||||||
|
route: `${kbnBaseUrl}#/account`,
|
||||||
|
};
|
||||||
|
|
||||||
|
props.user.$promise.then(() => {
|
||||||
|
// Wait for the user to be propogated before rendering into the DOM.
|
||||||
|
ReactDOM.render(<SecurityNavControl {...props} />, el);
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => ReactDOM.unmountComponentAtNode(el);
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
|
@ -0,0 +1,103 @@
|
||||||
|
/*
|
||||||
|
* 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 React, {
|
||||||
|
Component,
|
||||||
|
} from 'react';
|
||||||
|
|
||||||
|
import {
|
||||||
|
EuiAvatar,
|
||||||
|
EuiFlexGroup,
|
||||||
|
EuiFlexItem,
|
||||||
|
EuiHeaderSectionItemButton,
|
||||||
|
EuiLink,
|
||||||
|
EuiText,
|
||||||
|
EuiSpacer,
|
||||||
|
EuiPopover,
|
||||||
|
} from '@elastic/eui';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Placeholder for now from eui demo. Will need to be populated by Security plugin
|
||||||
|
*/
|
||||||
|
export class SecurityNavControl extends Component {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
isOpen: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
onMenuButtonClick = () => {
|
||||||
|
this.setState({
|
||||||
|
isOpen: !this.state.isOpen,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
closeMenu = () => {
|
||||||
|
this.setState({
|
||||||
|
isOpen: false,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { user, route } = this.props;
|
||||||
|
const name = user.full_name || user.username || '';
|
||||||
|
const button = (
|
||||||
|
<EuiHeaderSectionItemButton
|
||||||
|
aria-controls="headerUserMenu"
|
||||||
|
aria-expanded={this.state.isOpen}
|
||||||
|
aria-haspopup="true"
|
||||||
|
aria-label="Account menu"
|
||||||
|
onClick={this.onMenuButtonClick}
|
||||||
|
>
|
||||||
|
<EuiAvatar name={name} size="s" />
|
||||||
|
</EuiHeaderSectionItemButton>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<EuiPopover
|
||||||
|
id="headerUserMenu"
|
||||||
|
ownFocus
|
||||||
|
button={button}
|
||||||
|
isOpen={this.state.isOpen}
|
||||||
|
anchorPosition="downRight"
|
||||||
|
closePopover={this.closeMenu}
|
||||||
|
panelPaddingSize="none"
|
||||||
|
>
|
||||||
|
<div style={{ width: 320 }}>
|
||||||
|
<EuiFlexGroup gutterSize="m" className="euiHeaderProfile" responsive={false}>
|
||||||
|
<EuiFlexItem grow={false}>
|
||||||
|
<EuiAvatar name={name} size="xl" />
|
||||||
|
</EuiFlexItem>
|
||||||
|
|
||||||
|
<EuiFlexItem>
|
||||||
|
<EuiText>
|
||||||
|
<p>{name}</p>
|
||||||
|
</EuiText>
|
||||||
|
|
||||||
|
<EuiSpacer size="m" />
|
||||||
|
|
||||||
|
<EuiFlexGroup>
|
||||||
|
<EuiFlexItem>
|
||||||
|
<EuiFlexGroup justifyContent="spaceBetween">
|
||||||
|
<EuiFlexItem grow={false}>
|
||||||
|
<EuiLink href={route}>Edit profile</EuiLink>
|
||||||
|
</EuiFlexItem>
|
||||||
|
|
||||||
|
<EuiFlexItem grow={false}>
|
||||||
|
<EuiLink href="/logout">Log out</EuiLink>
|
||||||
|
</EuiFlexItem>
|
||||||
|
</EuiFlexGroup>
|
||||||
|
</EuiFlexItem>
|
||||||
|
</EuiFlexGroup>
|
||||||
|
</EuiFlexItem>
|
||||||
|
</EuiFlexGroup>
|
||||||
|
</div>
|
||||||
|
</EuiPopover>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue