mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
* 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
ad8dc820c2
commit
cf18cc6bcc
57 changed files with 1162 additions and 53 deletions
|
@ -2213,7 +2213,8 @@ main {
|
|||
-ms-flex-align: center;
|
||||
align-items: center;
|
||||
padding-left: 10px;
|
||||
/* 1 */ }
|
||||
/* 1 */
|
||||
height: 100%; }
|
||||
|
||||
.kuiLocalBreadcrumb {
|
||||
font-size: 14px;
|
||||
|
@ -2685,6 +2686,8 @@ main {
|
|||
-ms-flex-pack: justify;
|
||||
justify-content: space-between;
|
||||
min-height: 29px;
|
||||
/* 1 */
|
||||
line-height: 29px;
|
||||
/* 1 */ }
|
||||
|
||||
.kuiLocalNavRow__section {
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
display: flex;
|
||||
align-items: center;
|
||||
padding-left: $localNavSideSpacing; /* 1 */
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.kuiLocalBreadcrumb {
|
||||
|
|
|
@ -28,6 +28,7 @@
|
|||
align-items: stretch;
|
||||
justify-content: space-between;
|
||||
min-height: 29px; /* 1 */
|
||||
line-height: 29px; /* 1 */
|
||||
}
|
||||
|
||||
.kuiLocalNavRow__section {
|
||||
|
|
|
@ -76,6 +76,7 @@ export default function (kibana) {
|
|||
url: `${kbnBaseUrl}#/discover`,
|
||||
description: 'interactively explore your data',
|
||||
icon: 'plugins/kibana/assets/discover.svg',
|
||||
euiIconType: 'discoverApp',
|
||||
}, {
|
||||
id: 'kibana:visualize',
|
||||
title: 'Visualize',
|
||||
|
@ -83,6 +84,7 @@ export default function (kibana) {
|
|||
url: `${kbnBaseUrl}#/visualize`,
|
||||
description: 'design data visualizations',
|
||||
icon: 'plugins/kibana/assets/visualize.svg',
|
||||
euiIconType: 'visualizeApp',
|
||||
}, {
|
||||
id: 'kibana:dashboard',
|
||||
title: 'Dashboard',
|
||||
|
@ -96,13 +98,15 @@ export default function (kibana) {
|
|||
subUrlBase: `${kbnBaseUrl}#/dashboard`,
|
||||
description: 'compose visualizations for much win',
|
||||
icon: 'plugins/kibana/assets/dashboard.svg',
|
||||
euiIconType: 'dashboardApp',
|
||||
}, {
|
||||
id: 'kibana:dev_tools',
|
||||
title: 'Dev Tools',
|
||||
order: 9001,
|
||||
url: '/app/kibana#/dev_tools',
|
||||
description: 'development tools',
|
||||
icon: 'plugins/kibana/assets/wrench.svg'
|
||||
icon: 'plugins/kibana/assets/wrench.svg',
|
||||
euiIconType: 'devToolsApp',
|
||||
}, {
|
||||
id: 'kibana:management',
|
||||
title: 'Management',
|
||||
|
@ -110,6 +114,7 @@ export default function (kibana) {
|
|||
url: `${kbnBaseUrl}#/management`,
|
||||
description: 'define index patterns, change config, and more',
|
||||
icon: 'plugins/kibana/assets/settings.svg',
|
||||
euiIconType: 'managementApp',
|
||||
linkToLastSubUrl: false
|
||||
},
|
||||
],
|
||||
|
|
|
@ -7,20 +7,21 @@
|
|||
<!-- Transcluded elements. -->
|
||||
<div data-transclude-slots>
|
||||
<!-- Title. -->
|
||||
<div
|
||||
data-transclude-slot="topLeftCorner"
|
||||
class="kuiLocalBreadcrumbs"
|
||||
data-test-subj="breadcrumbs"
|
||||
role="heading"
|
||||
aria-level="1"
|
||||
>
|
||||
<div class="kuiLocalBreadcrumb">
|
||||
<a class="kuiLocalBreadcrumb__link" href="{{landingPageUrl()}}">Dashboard</a>
|
||||
<div data-transclude-slot="topLeftCorner">
|
||||
<div
|
||||
class="kuiLocalBreadcrumbs"
|
||||
data-test-subj="breadcrumbs"
|
||||
role="heading"
|
||||
aria-level="1"
|
||||
ng-if="showPluginBreadcrumbs">
|
||||
<div class="kuiLocalBreadcrumb">
|
||||
<a class="kuiLocalBreadcrumb__link" href="{{landingPageUrl()}}">Dashboard</a>
|
||||
</div>
|
||||
<div class="kuiLocalBreadcrumb">
|
||||
{{ getDashTitle() }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="kuiLocalBreadcrumb">
|
||||
{{ getDashTitle() }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Search. -->
|
||||
<div ng-show="chrome.getVisible()" class="fullWidth" data-transclude-slot="bottomRow">
|
||||
|
|
|
@ -81,7 +81,17 @@ app.directive('dashboardApp', function ($injector) {
|
|||
return {
|
||||
restrict: 'E',
|
||||
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 filterBar = Private(FilterBarQueryFilterProvider);
|
||||
const docTitle = Private(DocTitleProvider);
|
||||
|
@ -169,6 +179,18 @@ app.directive('dashboardApp', function ($injector) {
|
|||
dashboardStateManager.getTitle(),
|
||||
dashboardStateManager.getViewMode(),
|
||||
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.saveState = () => dashboardStateManager.saveState();
|
||||
$scope.getShouldShowEditHelp = () => (
|
||||
|
|
|
@ -50,7 +50,7 @@ uiRoutes
|
|||
})
|
||||
.when(DashboardConstants.LANDING_PAGE_PATH, {
|
||||
template: dashboardListingTemplate,
|
||||
controller($injector, $location, $scope, Private, config) {
|
||||
controller($injector, $location, $scope, Private, config, breadcrumbState) {
|
||||
const services = Private(SavedObjectRegistryProvider).byLoaderPropertiesName;
|
||||
const dashboardConfig = $injector.get('dashboardConfig');
|
||||
|
||||
|
@ -63,6 +63,7 @@ uiRoutes
|
|||
};
|
||||
$scope.hideWriteControls = dashboardConfig.getHideWriteControls();
|
||||
$scope.initialFilter = ($location.search()).filter || EMPTY_FILTER;
|
||||
breadcrumbState.set([{ text: 'Dashboards' }]);
|
||||
},
|
||||
resolve: {
|
||||
dash: function ($route, Private, redirectWhenMissing, kbnUrl) {
|
||||
|
|
|
@ -155,8 +155,8 @@ function discoverController(
|
|||
courier,
|
||||
kbnUrl,
|
||||
localStorage,
|
||||
breadcrumbState
|
||||
) {
|
||||
|
||||
const Vis = Private(VisProvider);
|
||||
const docTitle = Private(DocTitleProvider);
|
||||
const HitSortFn = Private(PluginsKibanaDiscoverHitSortFnProvider);
|
||||
|
@ -289,6 +289,12 @@ function discoverController(
|
|||
const pageTitleSuffix = savedSearch.id && savedSearch.title ? `: ${savedSearch.title}` : '';
|
||||
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;
|
||||
|
||||
const $state = $scope.state = new AppState(getStateDefaults());
|
||||
|
|
|
@ -5,11 +5,14 @@
|
|||
<!-- Title. -->
|
||||
<div
|
||||
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>
|
||||
</kbn-top-nav>
|
||||
|
|
|
@ -34,6 +34,7 @@ export function VisualizeListingController($injector) {
|
|||
const Notifier = $injector.get('Notifier');
|
||||
const Private = $injector.get('Private');
|
||||
const config = $injector.get('config');
|
||||
const breadcrumbState = $injector.get('breadcrumbState');
|
||||
|
||||
timefilter.disableAutoRefreshSelector();
|
||||
timefilter.disableTimeRangeSelector();
|
||||
|
@ -58,4 +59,7 @@ export function VisualizeListingController($injector) {
|
|||
return visualizationService.delete(selectedIds)
|
||||
.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
|
||||
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': {
|
||||
name: 'Query language',
|
||||
value: 'lucene',
|
||||
|
|
|
@ -26,6 +26,7 @@ export default function (kibana) {
|
|||
order: -1000,
|
||||
description: 'Time series expressions for everything',
|
||||
icon: 'plugins/timelion/icon.svg',
|
||||
euiIconType: 'timelionApp',
|
||||
main: 'plugins/timelion/app',
|
||||
},
|
||||
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 './header_global_nav';
|
||||
|
||||
import { kbnChromeProvider } from './kbn_chrome';
|
||||
import { kbnAppendChromeNavControls } from './append_nav_controls';
|
||||
|
|
|
@ -1,5 +1,8 @@
|
|||
<div class="content" chrome-context data-test-subj="kibanaChrome">
|
||||
<kbn-loading-indicator></kbn-loading-indicator>
|
||||
|
||||
<global-nav
|
||||
ng-if="!k7design"
|
||||
chrome="chrome"
|
||||
data-test-subj="globalNav"
|
||||
is-visible="chrome.getVisible()"
|
||||
|
@ -8,6 +11,13 @@
|
|||
app-title="chrome.getAppTitle()"
|
||||
></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-panel">
|
||||
<kbn-notifications
|
||||
|
@ -16,8 +26,6 @@
|
|||
|
||||
<div id="globalBannerList"></div>
|
||||
|
||||
<kbn-loading-indicator></kbn-loading-indicator>
|
||||
|
||||
<div
|
||||
class="application"
|
||||
ng-class="'tab-' + chrome.getFirstPathSegment() + ' ' + chrome.getApplicationClasses()"
|
||||
|
|
|
@ -57,7 +57,9 @@ export function kbnChromeProvider(chrome, internals) {
|
|||
},
|
||||
|
||||
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);
|
||||
|
||||
// are we showing the embedded version of the chrome?
|
||||
|
|
|
@ -14,7 +14,7 @@
|
|||
top: 0; // 1
|
||||
left: 0; // 1
|
||||
right: 0; // 1
|
||||
z-index: 1; // 1
|
||||
z-index: 20; // 1
|
||||
overflow: hidden; // 2
|
||||
height: @loadingIndicatorHeight;
|
||||
|
||||
|
@ -31,7 +31,7 @@
|
|||
right: 0;
|
||||
bottom: 0;
|
||||
position: absolute;
|
||||
z-index: 10;
|
||||
z-index: 21;
|
||||
visibility: visible;
|
||||
display: block;
|
||||
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 './breadcrumb_state';
|
||||
|
|
|
@ -99,7 +99,11 @@ describe('IndexedArray', function () {
|
|||
reg.push(firstUser);
|
||||
|
||||
// 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);
|
||||
});
|
||||
|
||||
|
|
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: [] });
|
||||
|
||||
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.order, inflectOrder, (raw, pluckValue) => {
|
||||
return [...raw].sort((itemA, itemB) => {
|
||||
|
@ -195,3 +195,20 @@ export class IndexedArray {
|
|||
// using traditional `extends Array` syntax doesn't work with babel
|
||||
// See https://babeljs.io/docs/usage/caveats/
|
||||
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"
|
||||
role="heading"
|
||||
aria-level="1"
|
||||
ng-if="showPluginBreadcrumbs"
|
||||
>
|
||||
<div
|
||||
class="kuiLocalBreadcrumb"
|
||||
|
|
|
@ -26,7 +26,6 @@ const module = uiModules.get('kibana');
|
|||
module.directive('breadCrumbs', function () {
|
||||
return {
|
||||
restrict: 'E',
|
||||
replace: true,
|
||||
scope: {
|
||||
omitCurrentPage: '=',
|
||||
/**
|
||||
|
@ -47,7 +46,8 @@ module.directive('breadCrumbs', function () {
|
|||
useLinks: '='
|
||||
},
|
||||
template: breadCrumbsTemplate,
|
||||
controller: function ($scope) {
|
||||
controller: function ($scope, config, breadcrumbState) {
|
||||
config.watch('k7design', (val) => $scope.showPluginBreadcrumbs = !val);
|
||||
|
||||
function omitPagesFilter(crumb) {
|
||||
return (
|
||||
|
@ -70,6 +70,15 @@ module.directive('breadCrumbs', function () {
|
|||
.filter(omitPagesFilter)
|
||||
.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',
|
||||
order: ['order']
|
||||
});
|
||||
|
||||
|
|
|
@ -29,6 +29,7 @@ export class UiApp {
|
|||
order = 0,
|
||||
description,
|
||||
icon,
|
||||
euiIconType,
|
||||
hidden,
|
||||
linkToLastSubUrl,
|
||||
listed,
|
||||
|
@ -45,6 +46,7 @@ export class UiApp {
|
|||
this._order = order;
|
||||
this._description = description;
|
||||
this._icon = icon;
|
||||
this._euiIconType = euiIconType;
|
||||
this._linkToLastSubUrl = linkToLastSubUrl;
|
||||
this._hidden = hidden;
|
||||
this._listed = listed;
|
||||
|
@ -66,6 +68,7 @@ export class UiApp {
|
|||
order: this._order,
|
||||
description: this._description,
|
||||
icon: this._icon,
|
||||
euiIconType: this._euiIconType,
|
||||
url: this._url,
|
||||
linkToLastSubUrl: this._linkToLastSubUrl
|
||||
});
|
||||
|
@ -117,6 +120,7 @@ export class UiApp {
|
|||
title: this._title,
|
||||
description: this._description,
|
||||
icon: this._icon,
|
||||
euiIconType: this._euiIconType,
|
||||
main: this._main,
|
||||
navLink: this._navLink,
|
||||
linkToLastSubUrl: this._linkToLastSubUrl,
|
||||
|
|
|
@ -29,6 +29,7 @@ function applySpecDefaults(spec, type, pluginSpec) {
|
|||
order = 0,
|
||||
description = '',
|
||||
icon,
|
||||
euiIconType,
|
||||
hidden = false,
|
||||
linkToLastSubUrl = true,
|
||||
listed = !hidden,
|
||||
|
@ -55,6 +56,7 @@ function applySpecDefaults(spec, type, pluginSpec) {
|
|||
order,
|
||||
description,
|
||||
icon,
|
||||
euiIconType,
|
||||
hidden,
|
||||
linkToLastSubUrl,
|
||||
listed,
|
||||
|
|
|
@ -31,6 +31,7 @@ describe('UiNavLink', () => {
|
|||
url: '/app/kibana#/discover',
|
||||
description: 'interactively explore your data',
|
||||
icon: 'plugins/kibana/assets/discover.svg',
|
||||
euiIconType: 'discoverApp',
|
||||
hidden: true,
|
||||
disabled: true
|
||||
};
|
||||
|
@ -44,6 +45,7 @@ describe('UiNavLink', () => {
|
|||
subUrlBase: spec.url,
|
||||
description: spec.description,
|
||||
icon: spec.icon,
|
||||
euiIconType: spec.euiIconType,
|
||||
hidden: spec.hidden,
|
||||
disabled: spec.disabled,
|
||||
|
||||
|
|
|
@ -27,6 +27,7 @@ export class UiNavLink {
|
|||
subUrlBase,
|
||||
description,
|
||||
icon,
|
||||
euiIconType,
|
||||
linkToLastSubUrl = true,
|
||||
hidden = false,
|
||||
disabled = false,
|
||||
|
@ -40,6 +41,7 @@ export class UiNavLink {
|
|||
this._subUrlBase = subUrlBase || url;
|
||||
this._description = description;
|
||||
this._icon = icon;
|
||||
this._euiIconType = euiIconType;
|
||||
this._linkToLastSubUrl = linkToLastSubUrl;
|
||||
this._hidden = hidden;
|
||||
this._disabled = disabled;
|
||||
|
@ -59,6 +61,7 @@ export class UiNavLink {
|
|||
subUrlBase: this._subUrlBase,
|
||||
description: this._description,
|
||||
icon: this._icon,
|
||||
euiIconType: this._euiIconType,
|
||||
linkToLastSubUrl: this._linkToLastSubUrl,
|
||||
hidden: this._hidden,
|
||||
disabled: this._disabled,
|
||||
|
|
|
@ -22,7 +22,8 @@ export function apm(kibana) {
|
|||
title: 'APM',
|
||||
description: 'APM for the Elastic Stack',
|
||||
main: 'plugins/apm/index',
|
||||
icon: 'plugins/apm/icon.svg'
|
||||
icon: 'plugins/apm/icon.svg',
|
||||
euiIconType: 'apmApp'
|
||||
},
|
||||
home: ['plugins/apm/register_feature'],
|
||||
injectDefaultVars(server) {
|
||||
|
|
|
@ -9,12 +9,37 @@ import { withBreadcrumbs } from 'react-router-breadcrumbs-hoc';
|
|||
import { toQuery } from '../../../utils/url';
|
||||
import { routes } from './routeConfig';
|
||||
import { flatten, capitalize } from 'lodash';
|
||||
import { set } from 'ui/chrome/services/breadcrumb_state';
|
||||
|
||||
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() {
|
||||
const { breadcrumbs, location } = this.props;
|
||||
const { breadcrumbs, location, showPluginBreadcrumbs } = this.props;
|
||||
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 (
|
||||
<div className="kuiLocalBreadcrumbs">
|
||||
{breadcrumbs.map(({ breadcrumb, path, match }, i) => {
|
||||
|
|
|
@ -36,7 +36,7 @@ jest.mock(
|
|||
function expectBreadcrumbToMatchSnapshot(route) {
|
||||
const wrapper = mount(
|
||||
<MemoryRouter initialEntries={[`${route}?_g=myG&kuery=myKuery`]}>
|
||||
<Breadcrumbs />
|
||||
<Breadcrumbs showPluginBreadcrumbs={true} />
|
||||
</MemoryRouter>
|
||||
);
|
||||
expect(
|
||||
|
@ -74,4 +74,13 @@ describe('Breadcrumbs', () => {
|
|||
'/: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();
|
||||
|
||||
initTimepicker(history, store.dispatch).then(() => {
|
||||
const showPluginBreadcrumbs = !chrome
|
||||
.getUiSettingsClient()
|
||||
.get('k7design', false);
|
||||
|
||||
ReactDOM.render(
|
||||
<Router history={history}>
|
||||
<Breadcrumbs />
|
||||
<Breadcrumbs showPluginBreadcrumbs={showPluginBreadcrumbs} />
|
||||
</Router>,
|
||||
document.getElementById('react-apm-breadcrumbs')
|
||||
);
|
||||
|
|
|
@ -20,6 +20,7 @@ export function canvas(kibana) {
|
|||
title: 'Canvas',
|
||||
description: 'Data driven workpads',
|
||||
icon: 'plugins/canvas/icon.svg',
|
||||
euiIconType: 'canvasApp',
|
||||
main: 'plugins/canvas/app',
|
||||
},
|
||||
styleSheetPaths: `${__dirname}/public/style/index.scss`,
|
||||
|
|
|
@ -62,12 +62,15 @@ export class Popover extends Component {
|
|||
return button(handleClick);
|
||||
};
|
||||
|
||||
const appWrapper = document.querySelector('.app-wrapper');
|
||||
|
||||
return (
|
||||
<EuiPopover
|
||||
{...rest}
|
||||
button={wrappedButton(this.handleClick)}
|
||||
isOpen={this.state.isPopoverOpen}
|
||||
closePopover={this.closePopover}
|
||||
container={appWrapper}
|
||||
>
|
||||
{children({ closePopover: this.closePopover })}
|
||||
</EuiPopover>
|
||||
|
|
|
@ -21,6 +21,7 @@ export function graph(kibana) {
|
|||
title: 'Graph',
|
||||
order: 9000,
|
||||
icon: 'plugins/graph/icon.png',
|
||||
euiIconType: 'graphApp',
|
||||
description: 'Graph exploration',
|
||||
main: 'plugins/graph/app',
|
||||
},
|
||||
|
|
|
@ -37,6 +37,7 @@ export const ml = (kibana) => {
|
|||
title: 'Machine Learning',
|
||||
description: 'Machine Learning for the Elastic Stack',
|
||||
icon: 'plugins/ml/ml.svg',
|
||||
euiIconType: 'machineLearningApp',
|
||||
main: 'plugins/ml/app',
|
||||
},
|
||||
hacks: ['plugins/ml/hacks/toggle_app_link_in_nav'],
|
||||
|
|
|
@ -1,7 +1,10 @@
|
|||
<kbn-top-nav name="dashboard" config="topNavMenu">
|
||||
<div data-transclude-slots>
|
||||
<!-- 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">
|
||||
<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>
|
||||
|
|
|
@ -14,7 +14,7 @@ import uiRouter from 'ui/routes';
|
|||
import { uiModules } from 'ui/modules';
|
||||
const module = uiModules.get('apps/ml');
|
||||
|
||||
module.directive('mlNavMenu', function () {
|
||||
module.directive('mlNavMenu', function (breadcrumbState, config) {
|
||||
return {
|
||||
restrict: 'E',
|
||||
transclude: true,
|
||||
|
@ -64,6 +64,9 @@ module.directive('mlNavMenu', function () {
|
|||
});
|
||||
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
|
||||
el.ready(() => {
|
||||
const $crumbs = $('.kuiLocalBreadcrumbs a');
|
||||
|
|
|
@ -7,20 +7,24 @@
|
|||
Breadcrumbs can't be automatically derived because the directive doesn't
|
||||
automatically know to show the Clusters breadcrumb. We recreate the
|
||||
structure and styles manually -->
|
||||
<div data-transclude-slot="topLeftCorner" class="kuiLocalBreadcrumbs">
|
||||
<div ng-repeat="crumb in monitoringMain.breadcrumbs" class="kuiLocalBreadcrumb">
|
||||
<a
|
||||
ng-if="crumb.url"
|
||||
kbn-href="{{ crumb.url }}"
|
||||
class="kuiLocalBreadcrumb__link"
|
||||
data-test-subj="{{ crumb.testSubj }}"
|
||||
>
|
||||
{{ crumb.label }}
|
||||
</a>
|
||||
<div data-transclude-slot="topLeftCorner">
|
||||
<div
|
||||
ng-if="showPluginBreadcrumbs"
|
||||
class="kuiLocalBreadcrumbs">
|
||||
<div ng-repeat="crumb in monitoringMain.breadcrumbs" class="kuiLocalBreadcrumb">
|
||||
<a
|
||||
ng-if="crumb.url"
|
||||
kbn-href="{{ crumb.url }}"
|
||||
class="kuiLocalBreadcrumb__link"
|
||||
data-test-subj="{{ crumb.testSubj }}"
|
||||
>
|
||||
{{ crumb.label }}
|
||||
</a>
|
||||
|
||||
<span ng-if="!crumb.url">
|
||||
{{ crumb.label }}
|
||||
</span>
|
||||
<span ng-if="!crumb.url">
|
||||
{{ crumb.label }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -71,7 +71,7 @@ export class MonitoringMainController {
|
|||
}
|
||||
|
||||
const uiModule = uiModules.get('plugins/monitoring/directives', []);
|
||||
uiModule.directive('monitoringMain', (breadcrumbs, license, kbnUrl) => {
|
||||
uiModule.directive('monitoringMain', (breadcrumbs, license, kbnUrl, config) => {
|
||||
return {
|
||||
restrict: 'E',
|
||||
transclude: true,
|
||||
|
@ -80,6 +80,7 @@ uiModule.directive('monitoringMain', (breadcrumbs, license, kbnUrl) => {
|
|||
controllerAs: 'monitoringMain',
|
||||
bindToController: true,
|
||||
link(scope, _element, attributes, controller) {
|
||||
config.watch('k7design', (val) => scope.showPluginBreadcrumbs = !val);
|
||||
|
||||
controller.setup({
|
||||
licenseService: license,
|
||||
|
|
|
@ -4,6 +4,8 @@
|
|||
* 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
|
||||
const createCrumb = (url, label, testSubj) => {
|
||||
const crumb = { url, label };
|
||||
|
@ -118,6 +120,8 @@ export function breadcrumbsProvider() {
|
|||
breadcrumbs = breadcrumbs.concat(getApmBreadcrumbs(mainInstance));
|
||||
}
|
||||
|
||||
setBreadcrumbs(breadcrumbs.map(b => ({ text: b.label, href: b.url })));
|
||||
|
||||
return breadcrumbs;
|
||||
};
|
||||
}
|
||||
|
|
|
@ -16,6 +16,7 @@ export const uiExports = {
|
|||
order: 9002,
|
||||
description: 'Monitoring for Elastic Stack',
|
||||
icon: 'plugins/monitoring/icons/monitoring.svg',
|
||||
euiIconType: 'monitoringApp',
|
||||
linkToLastSubUrl: false,
|
||||
main: 'plugins/monitoring/monitoring',
|
||||
},
|
||||
|
|
|
@ -4,15 +4,23 @@
|
|||
* 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 { chromeNavControlsRegistry } from 'ui/registry/chrome_nav_controls';
|
||||
|
||||
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 'plugins/security/services/shield_user';
|
||||
import '../account/account';
|
||||
import { PathProvider } from 'plugins/xpack_main/services/path';
|
||||
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({
|
||||
name: 'security',
|
||||
order: 1000,
|
||||
|
@ -53,3 +61,27 @@ module.controller('securityNavController', ($scope, ShieldUser, globalNavState,
|
|||
};
|
||||
|
||||
});
|
||||
|
||||
|
||||
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…
Add table
Add a link
Reference in a new issue