[New Platform Migration]: Management - Implement NP API (#66781)

* [New Platform Migration]: Management - Implement NP API

Part of #47432

* partial progress on a number of management sections

* fix passing history

* Fixed types

* Fixed routing for Ingest Node Pipelines

* introduce and use react router wrapped eui components

* react router utils

* work in progress => hashRouter to router

* more partial progress

* remove console.log

* use reactRouterNavigate for management_sidebar

* Breadcrumbs will need to make use of the reactRouterNavigate function

* [triggersActions] app. Hash Router -> Router

* Replace /app/kibana#/management urls to /app/management

* remove ui/public/management

* fix some links to management apps

* fix management url for functional tests

* add data-test-subj for EuiSideNavItem

* partial progress

* fix some of ts issues

* Fixed breadcrumbs for data index management

* [kibana/spaces] section

* fix functional test

* [role_management] fix Breadcrumbs

* [api_keys] fix Breadcrumbs and Navigation

* Fixed routing for remote cluster

* [role_mapping] Partial progress

* [users] partial progress

* [watcher] partial progress

* fix eslint issues

* [snapshot_restore] partial progress

* [rollup_jobs] partial progress

* Fixed routing for cross cluster replications (partial progress). Enhanced reactRouterNavigate

* Perf optimization: fix extra re-rendering

* fix TS errors

* x-pack fix config for functional tests

* Fixed routing for index lifecycle management

* fix some broken CI tests

* fix PR comment

* [snapshot_restore] move onClick into reactRouterNavigate

* fix some jest

* fix some functional tests

* fix functiona test: management  scripted fields testing regression for issue

* fix some functional tests

* [licence_management] partial progress

* Fixed x-pack jest tests

* [saved_object_management] partial progress

* Fixed some tests

* fix functional test: should add new role myroleEast

* Reverted part of changes for ml

* [transforms] partial progress

* fix TS errors

* fix functional:  redirects to Kibana home

* add support of Backward compatibility

* fix functional: Saved objects management feature controls saved objects management global visualize all privileges listing redirects to Kibana home

* fix PR comment

* fix TS issues

* Fixed x-pack jest tests

* fix oss JEST

* Fixed functional test

* fix functional test

* fix PR comment

* Fixed i18n

* fix typo

* fix Styles

* Fixed paths for cross_cluster_replication

* fix wrong link

* Fixed jest

* Fixed some comments

* fix sorting

* fix type check

* fixed x-pack jest

* fixed x-pack jest

* reverted using of parentHistory

* Add debugging toasts to CCR.

* Comment out non-CCR functional tests.

* Fix typo.

* Uncomment non-CCR functional tests.

* Enable CCR.

* fix CI

* Add comment to explain why CCR is enabled by default and move config variable back to original location in CCR plugin.

* revert some changes in APM

* add space between index pattern name and tags

* fix function test

* Update x-pack/plugins/security/public/management/management_urls.ts

Co-authored-by: Joe Portner <5295965+jportner@users.noreply.github.com>

* Update x-pack/plugins/security/public/management/api_keys/api_keys_management_app.tsx

Co-authored-by: Joe Portner <5295965+jportner@users.noreply.github.com>

* Update x-pack/plugins/spaces/public/management/spaces_management_app.tsx

Co-authored-by: Joe Portner <5295965+jportner@users.noreply.github.com>

* Update x-pack/plugins/security/public/management/roles/roles_management_app.tsx

Co-authored-by: Joe Portner <5295965+jportner@users.noreply.github.com>

* Update x-pack/plugins/security/public/management/users/users_management_app.tsx

Co-authored-by: Joe Portner <5295965+jportner@users.noreply.github.com>

* Update x-pack/plugins/security/public/management/management_urls.ts

Co-authored-by: Joe Portner <5295965+jportner@users.noreply.github.com>

* Update x-pack/plugins/security/public/management/management_urls.ts

Co-authored-by: Joe Portner <5295965+jportner@users.noreply.github.com>

* [security] getUrlForApp -> navigateToApp

* [mp] fix Uncaught (in promise) undefined

Co-authored-by: Matt Kime <matt@mattki.me>
Co-authored-by: Uladzislau Lasitsa <Uladzislau_Lasitsa@epam.com>
Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
Co-authored-by: CJ Cenizal <cj@cenizal.com>
Co-authored-by: Joe Portner <5295965+jportner@users.noreply.github.com>
This commit is contained in:
Alexey Antonov 2020-06-03 18:55:06 +03:00 committed by GitHub
parent a2f44bc27f
commit d661d66faa
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
343 changed files with 3433 additions and 3308 deletions

View file

@ -38,7 +38,7 @@ export const uiSettingsType: SavedObjectsType = {
importableAndExportable: true,
getInAppUrl() {
return {
path: `/app/kibana#/management/kibana/settings`,
path: `/app/management/kibana/settings`,
uiCapabilitiesPath: 'advancedSettings.show',
};
},

View file

@ -26,8 +26,7 @@ import { exportApi } from './server/routes/api/export';
import { getUiSettingDefaults } from './server/ui_setting_defaults';
import { registerCspCollector } from './server/lib/csp_usage_collector';
import { injectVars } from './inject_vars';
import { i18n } from '@kbn/i18n';
import { DEFAULT_APP_CATEGORIES } from '../../../../src/core/server';
import { kbnBaseUrl } from '../../../plugins/kibana_legacy/server';
const mkdirAsync = promisify(Fs.mkdir);
@ -53,19 +52,7 @@ export default function (kibana) {
main: 'plugins/kibana/kibana',
},
styleSheetPaths: resolve(__dirname, 'public/index.scss'),
links: [
{
id: 'kibana:stack_management',
title: i18n.translate('kbn.managementTitle', {
defaultMessage: 'Stack Management',
}),
order: 9003,
url: `${kbnBaseUrl}#/management`,
euiIconType: 'managementApp',
linkToLastSubUrl: false,
category: DEFAULT_APP_CATEGORIES.management,
},
],
links: [],
injectDefaultVars(server, options) {
const mapConfig = server.config().get('map');

View file

@ -1,83 +0,0 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
const topLevelConfig = require('../../../../../.eslintrc.js');
const path = require('path');
const topLevelRestricedZones = topLevelConfig.overrides.find(
override =>
override.files[0] === '**/*.{js,ts,tsx}' &&
Object.keys(override.rules)[0] === '@kbn/eslint/no-restricted-paths'
).rules['@kbn/eslint/no-restricted-paths'][1].zones;
/**
* Builds custom restricted paths configuration for the shimmed plugins within the kibana plugin.
* These custom rules extend the default checks in the top level `eslintrc.js` by also checking two other things:
* * Making sure nothing within np_ready imports from the `ui` directory
* * Making sure no other code is importing things deep from within the shimmed plugins
* @param shimmedPlugins List of plugin names within the kibana plugin that are partially np ready
* @returns zones configuration for the no-restricted-paths linter
*/
function buildRestrictedPaths(shimmedPlugins) {
return shimmedPlugins
.map(shimmedPlugin => [
{
target: [`src/legacy/core_plugins/kibana/public/${shimmedPlugin}/np_ready/**/*`],
from: [
'ui/**/*',
'src/legacy/ui/**/*',
'src/legacy/core_plugins/kibana/public/**/*',
`!src/legacy/core_plugins/kibana/public/${shimmedPlugin}/**/*`,
],
allowSameFolder: false,
errorMessage: `${shimmedPlugin} is a shimmed plugin that is not allowed to import modules from the legacy platform. If you need legacy modules for the transition period, import them either in the legacy_imports, kibana_services or index module.`,
},
{
target: [
'src/**/*',
`!src/legacy/core_plugins/kibana/public/${shimmedPlugin}/**/*`,
'x-pack/**/*',
],
from: [
`src/legacy/core_plugins/kibana/public/${shimmedPlugin}/**/*`,
`!src/legacy/core_plugins/kibana/public/${shimmedPlugin}/index.ts`,
`!src/legacy/core_plugins/kibana/public/${shimmedPlugin}/legacy.ts`,
],
allowSameFolder: false,
errorMessage: `kibana/public/${shimmedPlugin} is behaving like a NP plugin and does not allow deep imports. If you need something from within ${shimmedPlugin} in another plugin, consider re-exporting it from the top level index module`,
},
])
.reduce((acc, part) => [...acc, ...part], []);
}
module.exports = {
rules: {
'no-console': 2,
'import/no-default-export': 'error',
'@kbn/eslint/no-restricted-paths': [
'error',
{
basePath: path.resolve(__dirname, '../../../../../'),
zones: topLevelRestricedZones.concat(
buildRestrictedPaths(['visualize', 'discover', 'dashboard', 'devTools'])
),
},
],
},
};

View file

@ -38,17 +38,22 @@ import 'uiExports/shareContextMenuExtensions';
import 'uiExports/interpreter';
import 'ui/autoload/all';
import './management';
import { localApplicationService } from './local_application_service';
npSetup.plugins.kibanaLegacy.registerLegacyAppAlias('doc', 'discover', { keepPrefix: true });
npSetup.plugins.kibanaLegacy.registerLegacyAppAlias('context', 'discover', { keepPrefix: true });
npSetup.plugins.kibanaLegacy.forwardApp('management', 'management', (path) => {
return path.replace('/management', '');
});
localApplicationService.attachToAngular(routes);
routes.enable();
const { config } = npSetup.plugins.kibanaLegacy;
routes.otherwise({
redirectTo: `/${config.defaultAppId || 'discover'}`,
});

View file

@ -1,29 +0,0 @@
// SASSTODO: figure out why this is needed
kbn-management-app,
kbn-management-landing,
kbn-management-indices,
kbn-management-indices-edit,
kbn-management-indices-create,
kbn-management-advanced,
kbn-management-objects,
kbn-management-objects-view {
display: block;
}
#management-landing {
display: flex;
}
.kbn-management-tab:first-letter {
text-transform: capitalize;
}
// SASSTODO: Remove when this is replaced with EuiCode
kbn-management-objects-view {
.ace_editor { height: 300px; }
}
// Hack because the management wrapper is flat HTML and needs a class
.mgtPage__body {
max-width: map-get($euiBreakpoints, 'xl');
}

View file

@ -1,69 +0,0 @@
.mgtPanel {
margin-bottom: $euiSize;
background: $euiColorEmptyShade;
}
/**
* 1. Override kuiPanelBody styles to accommodate padding of items within the panel body..
*/
.mgtPanel__body {
padding: 5px 10px; /* 1 */
}
/**
* 1. Create vertical space between items when they wrap.
*/
.mgtPanel__item {
padding: 5px 15px; /* 1 */
}
// SASSTODO: Remove when this is replaced by the side nav
.mgtPanel__link {
@include euiFontSizeL;
line-height: 1.5; // Make sure the space between wrapped lines is than the vertical space between items.
&.mgtPanel__link--disabled {
opacity: $euiColorDarkShade;
cursor: default;
&:hover, &:visited {
color: $euiColorPrimary;
}
}
}
// SASSTODO: Remove when this form is replaced by EUI
kbn-management-objects {
form {
margin-bottom: $euiSize;
}
.list-unstyled {
li {
border-bottom: $euiBorderThin;
padding: $euiSizeS;
}
}
.empty {
color: $euiColorDarkShade;
}
.item {
padding: $euiSizeM;
.item-title {
margin-left: $euiSizeL;
}
.actions {
margin-top: $euiSizeXS;
}
}
.header {
.title, .controls {
padding-right: 1em;
display: inline-block;
}
}
}

View file

@ -1,4 +0,0 @@
<div class="euiPage mgtPage">
<div id="management-sidenav" class="euiPageSideBar" style="position: static;" data-test-subj="managementNav"></div>
<main class="euiPageBody euiPageBody--restrictWidth-default mgtPage__body" ng-transclude></main>
</div>

View file

@ -1,166 +0,0 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import React from 'react';
import { render, unmountComponentAtNode } from 'react-dom';
import { FormattedMessage } from '@kbn/i18n/react';
import uiRoutes from 'ui/routes';
import { I18nContext } from 'ui/i18n';
import { uiModules } from 'ui/modules';
import appTemplate from './app.html';
import landingTemplate from './landing.html';
import { management, MANAGEMENT_BREADCRUMB } from 'ui/management';
import { ManagementSidebarNav } from '../../../../../plugins/management/public';
import { timefilter } from 'ui/timefilter';
import {
EuiPageContent,
EuiTitle,
EuiText,
EuiSpacer,
EuiIcon,
EuiHorizontalRule,
} from '@elastic/eui';
import { npStart } from 'ui/new_platform';
const SIDENAV_ID = 'management-sidenav';
const LANDING_ID = 'management-landing';
uiRoutes.when('/management', {
template: landingTemplate,
k7Breadcrumbs: () => [MANAGEMENT_BREADCRUMB],
});
uiRoutes.when('/management/:section', {
redirectTo: '/management',
});
export function updateLandingPage(version) {
const node = document.getElementById(LANDING_ID);
if (!node) {
return;
}
render(
<EuiPageContent horizontalPosition="center" data-test-subj="managementHome">
<I18nContext>
<div>
<div className="eui-textCenter">
<EuiIcon type="managementApp" size="xxl" />
<EuiSpacer />
<EuiTitle>
<h1>
<FormattedMessage
id="kbn.management.landing.header"
defaultMessage="Welcome to Stack Management {version}"
values={{ version }}
/>
</h1>
</EuiTitle>
<EuiText>
<FormattedMessage
id="kbn.management.landing.subhead"
defaultMessage="Manage your indices, index patterns, saved objects, Kibana settings, and more."
/>
</EuiText>
</div>
<EuiHorizontalRule />
<EuiText color="subdued" size="s" textAlign="center">
<p>
<FormattedMessage
id="kbn.management.landing.text"
defaultMessage="A complete list of apps is in the menu on the left."
/>
</p>
</EuiText>
</div>
</I18nContext>
</EuiPageContent>,
node
);
}
export function updateSidebar(legacySections, id) {
const node = document.getElementById(SIDENAV_ID);
if (!node) {
return;
}
render(
<I18nContext>
<ManagementSidebarNav
getSections={npStart.plugins.management.sections.getSectionsEnabled}
legacySections={legacySections}
selectedId={id}
className="mgtSideNav"
/>
</I18nContext>,
node
);
}
export const destroyReact = (id) => {
const node = document.getElementById(id);
node && unmountComponentAtNode(node);
};
uiModules.get('apps/management').directive('kbnManagementApp', function ($location) {
return {
restrict: 'E',
template: appTemplate,
transclude: true,
scope: {
sectionName: '@section',
omitPages: '@omitBreadcrumbPages',
pageTitle: '=',
},
link: function ($scope) {
timefilter.disableAutoRefreshSelector();
timefilter.disableTimeRangeSelector();
$scope.sections = management.visibleItems;
$scope.section = management.getSection($scope.sectionName) || management;
if ($scope.section) {
$scope.section.items.forEach((item) => {
item.active = `#${$location.path()}`.indexOf(item.url) > -1;
});
}
updateSidebar($scope.sections, $scope.section.id);
$scope.$on('$destroy', () => destroyReact(SIDENAV_ID));
management.addListener(() => updateSidebar(management.visibleItems, $scope.section.id));
updateLandingPage($scope.$root.chrome.getKibanaVersion());
$scope.$on('$destroy', () => destroyReact(LANDING_ID));
},
};
});
uiModules.get('apps/management').directive('kbnManagementLanding', function (kbnVersion) {
return {
restrict: 'E',
link: function ($scope) {
$scope.sections = management.visibleItems;
$scope.kbnVersion = kbnVersion;
},
};
});

View file

@ -7,9 +7,7 @@
// mgtChart__legend--small
// mgtChart__legend-isLoading
@import 'hacks';
// Core
@import 'management_app';
@import '../../../../../plugins/advanced_settings/public/index';
@import 'sections/index_patterns/index';

View file

@ -1,3 +0,0 @@
<kbn-management-app>
<div id="management-landing" data-test-subj="management-landing"></div>
</kbn-management-app>

View file

@ -69,7 +69,7 @@ const savedObjectsManagement = getManagementaMock({
},
getInAppUrl(obj) {
return {
path: `/app/kibana#/management/kibana/indexPatterns/patterns/${encodeURIComponent(obj.id)}`,
path: `/app/management/kibana/indexPatterns/patterns/${encodeURIComponent(obj.id)}`,
uiCapabilitiesPath: 'management.kibana.index_patterns',
};
},
@ -325,7 +325,7 @@ describe('findRelationships', () => {
title: 'My Index Pattern',
editUrl: '/management/kibana/indexPatterns/patterns/1',
inAppUrl: {
path: '/app/kibana#/management/kibana/indexPatterns/patterns/1',
path: '/app/management/kibana/indexPatterns/patterns/1',
uiCapabilitiesPath: 'management.kibana.index_patterns',
},
},
@ -439,7 +439,7 @@ describe('findRelationships', () => {
title: 'My Index Pattern',
editUrl: '/management/kibana/indexPatterns/patterns/1',
inAppUrl: {
path: '/app/kibana#/management/kibana/indexPatterns/patterns/1',
path: '/app/management/kibana/indexPatterns/patterns/1',
uiCapabilitiesPath: 'management.kibana.index_patterns',
},
},

View file

@ -19,7 +19,7 @@
import React from 'react';
import ReactDOM from 'react-dom';
import { HashRouter, Switch, Route } from 'react-router-dom';
import { Router, Switch, Route } from 'react-router-dom';
import { i18n } from '@kbn/i18n';
import { I18nProvider } from '@kbn/i18n/react';
@ -60,7 +60,7 @@ export async function mountManagementSection(
ReactDOM.render(
<I18nProvider>
<HashRouter basename={params.basePath}>
<Router history={params.history}>
<Switch>
<Route path={['/:query', '/']}>
<AdvancedSettings
@ -72,7 +72,7 @@ export async function mountManagementSection(
/>
</Route>
</Switch>
</HashRouter>
</Router>
</I18nProvider>,
params.element
);

View file

@ -91,9 +91,9 @@ export const createEnsureDefaultIndexPattern = (core: CoreStart) => {
if (redirectTarget === '/home') {
core.application.navigateToApp('home');
} else {
window.location.href = core.http.basePath.prepend(
`/app/kibana#/management/kibana/indexPatterns?bannerMessage=${bannerMessage}`
);
core.application.navigateToApp('management', {
path: `/kibana/indexPatterns?bannerMessage=${bannerMessage}`,
});
}
// return never-resolving promise to stop resolving and wait for the url change

View file

@ -44,7 +44,7 @@ export function LongQueryNotification(props: Props) {
<EuiButton
size="s"
onClick={async () => {
await props.application.navigateToApp('kibana#/management/stack/license_management');
await props.application.navigateToApp('management/stack/license_management');
}}
>
<FormattedMessage

View file

@ -36,7 +36,7 @@ export const indexPatternSavedObjectType: SavedObjectsType = {
},
getInAppUrl(obj) {
return {
path: `/app/kibana#/management/kibana/indexPatterns/patterns/${encodeURIComponent(obj.id)}`,
path: `/app/management/kibana/indexPatterns/patterns/${encodeURIComponent(obj.id)}`,
uiCapabilitiesPath: 'management.kibana.index_patterns',
};
},

View file

@ -162,8 +162,8 @@ app.config(($routeProvider) => {
mapping: {
search: '/',
'index-pattern': {
app: 'kibana',
path: `#/management/kibana/objects/savedSearches/${$route.current.params.id}`,
app: 'management',
path: `kibana/objects/savedSearches/${$route.current.params.id}`,
},
},
toastNotifications,

View file

@ -53,7 +53,7 @@ exports[`render 1`] = `
>
<EuiButton
fill={true}
href="#/management/kibana/objects?_a=(tab:search)"
href="/app/management/kibana/objects?_a=(tab:search)"
onClick={[Function]}
>
<FormattedMessage

View file

@ -40,6 +40,7 @@ const SEARCH_OBJECT_TYPE = 'search';
export function OpenSearchPanel(props) {
const {
core: { uiSettings, savedObjects },
addBasePath,
} = getServices();
return (
@ -86,7 +87,9 @@ export function OpenSearchPanel(props) {
<EuiButton
fill
onClick={props.onClose}
href={`#/management/kibana/objects?_a=${rison.encode({ tab: SEARCH_OBJECT_TYPE })}`}
href={addBasePath(
`/app/management/kibana/objects?_a=${rison.encode({ tab: SEARCH_OBJECT_TYPE })}`
)}
>
<FormattedMessage
id="discover.topNav.openSearchPanel.manageSearchesButtonLabel"

View file

@ -24,6 +24,7 @@ jest.mock('../../../kibana_services', () => {
return {
getServices: () => ({
core: { uiSettings: {}, savedObjects: {} },
addBasePath: (path) => path,
}),
};
});

View file

@ -277,7 +277,7 @@ exports[`apmUiEnabled 1`] = `
/>
</strong>
<EuiLink
href="#/management/kibana/indexPatterns"
href="path/app/management/kibana/indexPatterns"
style={
Object {
"display": "block",
@ -543,7 +543,7 @@ exports[`isNewKibanaInstance 1`] = `
/>
</strong>
<EuiLink
href="#/management/kibana/indexPatterns"
href="path/app/management/kibana/indexPatterns"
style={
Object {
"display": "block",
@ -876,7 +876,7 @@ exports[`mlEnabled 1`] = `
/>
</strong>
<EuiLink
href="#/management/kibana/indexPatterns"
href="path/app/management/kibana/indexPatterns"
style={
Object {
"display": "block",
@ -1142,7 +1142,7 @@ exports[`render 1`] = `
/>
</strong>
<EuiLink
href="#/management/kibana/indexPatterns"
href="path/app/management/kibana/indexPatterns"
style={
Object {
"display": "block",

View file

@ -141,7 +141,7 @@ exports[`should render a Welcome screen with the telemetry disclaimer 1`] = `
values={Object {}}
/>
<EuiLink
href="#/management/kibana/settings"
href="rootapp/management/kibana/settings"
>
<FormattedMessage
defaultMessage="disable usage data here."
@ -240,7 +240,7 @@ exports[`should render a Welcome screen with the telemetry disclaimer when optIn
values={Object {}}
/>
<EuiLink
href="#/management/kibana/settings"
href="rootapp/management/kibana/settings"
>
<FormattedMessage
defaultMessage="enable usage data here."
@ -339,7 +339,7 @@ exports[`should render a Welcome screen with the telemetry disclaimer when optIn
values={Object {}}
/>
<EuiLink
href="#/management/kibana/settings"
href="rootapp/management/kibana/settings"
>
<FormattedMessage
defaultMessage="disable usage data here."

View file

@ -40,6 +40,7 @@ import {
const AddDataUi = ({ apmUiEnabled, isNewKibanaInstance, intl, mlEnabled }) => {
const basePath = getServices().getBasePath();
const renderCards = () => {
const apmData = {
title: intl.formatMessage({
@ -296,7 +297,7 @@ const AddDataUi = ({ apmUiEnabled, isNewKibanaInstance, intl, mlEnabled }) => {
</strong>
<EuiLink
style={{ display: 'block', textAlign: 'center' }}
href="#/management/kibana/indexPatterns"
href={`${basePath}/app/management/kibana/indexPatterns`}
>
<FormattedMessage
id="home.addData.yourDataLink"

View file

@ -101,7 +101,7 @@ export class Welcome extends React.Component<Props> {
id="home.dataManagementDisableCollection"
defaultMessage=" To stop collection, "
/>
<EuiLink href="#/management/kibana/settings">
<EuiLink href={this.services.addBasePath('app/management/kibana/settings')}>
<FormattedMessage
id="home.dataManagementDisableCollectionLink"
defaultMessage="disable usage data here."
@ -116,7 +116,7 @@ export class Welcome extends React.Component<Props> {
id="home.dataManagementEnableCollection"
defaultMessage=" To start collection, "
/>
<EuiLink href="#/management/kibana/settings">
<EuiLink href={this.services.addBasePath('app/management/kibana/settings')}>
<FormattedMessage
id="home.dataManagementEnableCollectionLink"
defaultMessage="enable usage data here."

View file

@ -26,7 +26,7 @@ export function getListBreadcrumbs() {
text: i18n.translate('indexPatternManagement.indexPatterns.listBreadcrumb', {
defaultMessage: 'Index patterns',
}),
href: `#/management/kibana/indexPatterns/`,
href: `/`,
},
];
}
@ -38,7 +38,7 @@ export function getCreateBreadcrumbs() {
text: i18n.translate('indexPatternManagement.indexPatterns.createBreadcrumb', {
defaultMessage: 'Create index pattern',
}),
href: `#/management/kibana/indexPatterns/create`,
href: `/create`,
},
];
}
@ -48,7 +48,7 @@ export function getEditBreadcrumbs(indexPattern: IndexPattern) {
...getListBreadcrumbs(),
{
text: indexPattern.title,
href: `#/management/kibana/indexPatterns/patterns/${indexPattern.id}`,
href: `/patterns/${indexPattern.id}`,
},
];
}

View file

@ -79,7 +79,9 @@ export const CreateEditField = withRouter(
chrome.docTitle.change([docFieldName, indexPattern.title]);
const redirectAway = () => {
history.push(`${url}?_a=(tab:${field?.scripted ? TAB_SCRIPTED_FIELDS : TAB_INDEXED_FIELDS})`);
history.push(
`${url}#/?_a=(tab:${field?.scripted ? TAB_SCRIPTED_FIELDS : TAB_INDEXED_FIELDS})`
);
};
if (field) {

View file

@ -20,6 +20,8 @@
import React from 'react';
import { render } from 'enzyme';
import { RouteComponentProps } from 'react-router-dom';
import { ScopedHistory } from 'kibana/public';
import { scopedHistoryMock } from '../../../../../../../../core/public/mocks';
import { Header } from './header';
@ -28,7 +30,7 @@ describe('Header', () => {
const component = render(
<Header.WrappedComponent
indexPatternId="test"
history={({} as unknown) as RouteComponentProps['history']}
history={(scopedHistoryMock.create() as unknown) as ScopedHistory}
location={({} as unknown) as RouteComponentProps['location']}
match={({} as unknown) as RouteComponentProps['match']}
/>

View file

@ -22,9 +22,13 @@ import { withRouter, RouteComponentProps } from 'react-router-dom';
import { EuiButton, EuiFlexGroup, EuiFlexItem, EuiText, EuiTitle } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import { ScopedHistory } from 'kibana/public';
import { reactRouterNavigate } from '../../../../../../../kibana_react/public';
interface HeaderProps extends RouteComponentProps {
indexPatternId: string;
history: ScopedHistory;
}
export const Header = withRouter(({ indexPatternId, history }: HeaderProps) => (
@ -52,9 +56,7 @@ export const Header = withRouter(({ indexPatternId, history }: HeaderProps) => (
<EuiFlexItem grow={false}>
<EuiButton
data-test-subj="addScriptedFieldLink"
onClick={() => {
history.push(`${indexPatternId}/create-field/`);
}}
{...reactRouterNavigate(history, `patterns/${indexPatternId}/create-field/`)}
>
<FormattedMessage
id="indexPatternManagement.editIndexPattern.scripted.addFieldButton"

View file

@ -117,7 +117,7 @@ export function getTabs(
}
export function getPath(field: IndexPatternField) {
return `${field.indexPattern?.id}/field/${field.name}`;
return `/patterns/${field.indexPattern?.id}/field/${field.name}`;
}
const allTypesDropDown = i18n.translate(

View file

@ -27,12 +27,13 @@ import {
EuiPanel,
EuiSpacer,
EuiText,
EuiBadgeGroup,
} from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import { withRouter, RouteComponentProps } from 'react-router-dom';
import React, { useState, useEffect } from 'react';
import { i18n } from '@kbn/i18n';
import { useKibana } from '../../../../../plugins/kibana_react/public';
import { reactRouterNavigate, useKibana } from '../../../../../plugins/kibana_react/public';
import { IndexPatternManagmentContext } from '../../types';
import { CreateButton } from '../create_button';
import { CreateIndexPatternPrompt } from '../create_index_pattern_prompt';
@ -40,35 +41,6 @@ import { IndexPatternTableItem, IndexPatternCreationOption } from '../types';
import { getIndexPatterns } from '../utils';
import { getListBreadcrumbs } from '../breadcrumbs';
const columns = [
{
field: 'title',
name: 'Pattern',
render: (
name: string,
index: {
id: string;
tags?: Array<{
key: string;
name: string;
}>;
}
) => (
<EuiButtonEmpty size="xs" href={`#/management/kibana/indexPatterns/patterns/${index.id}`}>
{name}
{index.tags &&
index.tags.map(({ key: tagKey, name: tagName }) => (
<EuiBadge className="indexPatternList__badge" key={tagKey}>
{tagName}
</EuiBadge>
))}
</EuiButtonEmpty>
),
dataType: 'string' as const,
sortable: ({ sort }: { sort: string }) => sort,
},
];
const pagination = {
initialPageSize: 10,
pageSizeOptions: [5, 10, 25, 50],
@ -140,6 +112,39 @@ export const IndexPatternTable = ({ canSave, history }: Props) => {
chrome.docTitle.change(title);
const columns = [
{
field: 'title',
name: 'Pattern',
render: (
name: string,
index: {
id: string;
tags?: Array<{
key: string;
name: string;
}>;
}
) => (
<>
<EuiButtonEmpty size="xs" {...reactRouterNavigate(history, `patterns/${index.id}`)}>
{name}
</EuiButtonEmpty>
<EuiBadgeGroup gutterSize="s">
{index.tags &&
index.tags.map(({ key: tagKey, name: tagName }) => (
<EuiBadge className="indexPatternList__badge" key={tagKey}>
{tagName}
</EuiBadge>
))}
</EuiBadgeGroup>
</>
),
dataType: 'string' as const,
sortable: ({ sort }: { sort: string }) => sort,
},
];
const createButton = canSave ? (
<CreateButton options={creationOptions}>
<FormattedMessage

View file

@ -19,7 +19,7 @@
import React from 'react';
import ReactDOM from 'react-dom';
import { HashRouter, Switch, Route } from 'react-router-dom';
import { Router, Switch, Route } from 'react-router-dom';
import { i18n } from '@kbn/i18n';
import { I18nProvider } from '@kbn/i18n/react';
@ -78,7 +78,7 @@ export async function mountManagementSection(
ReactDOM.render(
<KibanaContextProvider services={deps}>
<I18nProvider>
<HashRouter basename={params.basePath}>
<Router history={params.history}>
<Switch>
<Route path={['/create']}>
<CreateIndexPatternWizardWithRouter />
@ -93,7 +93,7 @@ export async function mountManagementSection(
<IndexPatternTableWithRouter canSave={canSave} />
</Route>
</Switch>
</HashRouter>
</Router>
</I18nProvider>
</KibanaContextProvider>,
params.element

View file

@ -71,7 +71,7 @@ export class IndexPatternManagementPlugin
throw new Error('`kibana` management section not found.');
}
const newAppPath = `kibana#/management/kibana/${IPM_APP_ID}`;
const newAppPath = `management/kibana/${IPM_APP_ID}`;
const legacyPatternsPath = 'management/kibana/index_patterns';
kibanaLegacy.forwardApp('management/kibana/index_pattern', newAppPath, (path) => '/create');

View file

@ -426,7 +426,11 @@ const $setupUrlOverflowHandling = (newPlatform: CoreStart, isLocalAngular: boole
values={{
storeInSessionStorageParam: <code>state:storeInSessionStorage</code>,
advancedSettingsLink: (
<a href="#/management/kibana/settings">
<a
href={newPlatform.application.getUrlForApp('management', {
path: 'kibana/settings',
})}
>
<FormattedMessage
id="kibana_legacy.bigUrlWarningNotificationMessage.advancedSettingsLinkText"
defaultMessage="advanced settings"

View file

@ -25,6 +25,7 @@ export * from './ui_settings';
export * from './field_icon';
export * from './table_list_view';
export * from './split_panel';
export * from './react_router_navigate';
export { ValidatedDualRange, Value } from './validated_range';
export * from './notifications';
export { Markdown, MarkdownSimple } from './markdown';

View file

@ -17,5 +17,4 @@
* under the License.
*/
export { LegacyManagementAdapter } from './sections_register';
export { LegacyManagementSection } from './section';
export { reactRouterNavigate, reactRouterOnClickHandler } from './react_router_navigate';

View file

@ -0,0 +1,70 @@
/*
* 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 { ScopedHistory } from 'kibana/public';
import { History } from 'history';
interface LocationObject {
pathname?: string;
search?: string;
hash?: string;
}
const isModifiedEvent = (event: any) =>
!!(event.metaKey || event.altKey || event.ctrlKey || event.shiftKey);
const isLeftClickEvent = (event: any) => event.button === 0;
export const toLocationObject = (to: string | LocationObject) =>
typeof to === 'string' ? { pathname: to } : to;
export const reactRouterNavigate = (
history: ScopedHistory | History,
to: string | LocationObject,
onClickCallback?: Function
) => ({
href: history.createHref(toLocationObject(to)),
onClick: reactRouterOnClickHandler(history, toLocationObject(to), onClickCallback),
});
export const reactRouterOnClickHandler = (
history: ScopedHistory | History,
to: string | LocationObject,
onClickCallback?: Function
) => (event: any) => {
if (onClickCallback) {
onClickCallback(event);
}
if (event.defaultPrevented) {
return;
}
if (event.target.getAttribute('target')) {
return;
}
if (isModifiedEvent(event) || !isLeftClickEvent(event)) {
return;
}
// prevents page reload
event.preventDefault();
history.push(toLocationObject(to));
};

View file

@ -35,7 +35,7 @@ import { ScopedHistory } from '../../../../../core/public';
describe('kbn_url_storage', () => {
describe('getStateFromUrl & setStateToUrl', () => {
const url = 'http://localhost:5601/oxf/app/kibana#/management/kibana/indexPatterns/patterns/id';
const url = 'http://localhost:5601/oxf/app/kibana#/yourApp';
const state1 = {
testStr: '123',
testNumber: 0,
@ -50,14 +50,14 @@ describe('kbn_url_storage', () => {
it('should set expanded state to url', () => {
let newUrl = setStateToKbnUrl('_s', state1, { useHash: false }, url);
expect(newUrl).toMatchInlineSnapshot(
`"http://localhost:5601/oxf/app/kibana#/management/kibana/indexPatterns/patterns/id?_s=(testArray:!(1,2,()),testNull:!n,testNumber:0,testObj:(test:'123'),testStr:'123')"`
`"http://localhost:5601/oxf/app/kibana#/yourApp?_s=(testArray:!(1,2,()),testNull:!n,testNumber:0,testObj:(test:'123'),testStr:'123')"`
);
const retrievedState1 = getStateFromKbnUrl('_s', newUrl);
expect(retrievedState1).toEqual(state1);
newUrl = setStateToKbnUrl('_s', state2, { useHash: false }, newUrl);
expect(newUrl).toMatchInlineSnapshot(
`"http://localhost:5601/oxf/app/kibana#/management/kibana/indexPatterns/patterns/id?_s=(test:'123')"`
`"http://localhost:5601/oxf/app/kibana#/yourApp?_s=(test:'123')"`
);
const retrievedState2 = getStateFromKbnUrl('_s', newUrl);
expect(retrievedState2).toEqual(state2);
@ -66,14 +66,14 @@ describe('kbn_url_storage', () => {
it('should set hashed state to url', () => {
let newUrl = setStateToKbnUrl('_s', state1, { useHash: true }, url);
expect(newUrl).toMatchInlineSnapshot(
`"http://localhost:5601/oxf/app/kibana#/management/kibana/indexPatterns/patterns/id?_s=h@a897fac"`
`"http://localhost:5601/oxf/app/kibana#/yourApp?_s=h@a897fac"`
);
const retrievedState1 = getStateFromKbnUrl('_s', newUrl);
expect(retrievedState1).toEqual(state1);
newUrl = setStateToKbnUrl('_s', state2, { useHash: true }, newUrl);
expect(newUrl).toMatchInlineSnapshot(
`"http://localhost:5601/oxf/app/kibana#/management/kibana/indexPatterns/patterns/id?_s=h@40f94d5"`
`"http://localhost:5601/oxf/app/kibana#/yourApp?_s=h@40f94d5"`
);
const retrievedState2 = getStateFromKbnUrl('_s', newUrl);
expect(retrievedState2).toEqual(state2);
@ -244,67 +244,55 @@ describe('kbn_url_storage', () => {
it('should extract path relative to browser history without basename', () => {
const history = createBrowserHistory();
const url =
"http://localhost:5601/oxf/app/kibana#/management/kibana/indexPatterns/patterns/id?_a=(tab:indexedFields)&_b=(f:test,i:'',l:'')";
"http://localhost:5601/oxf/app/kibana#/yourApp?_a=(tab:indexedFields)&_b=(f:test,i:'',l:'')";
const relativePath = getRelativeToHistoryPath(url, history);
expect(relativePath).toEqual(
"/oxf/app/kibana#/management/kibana/indexPatterns/patterns/id?_a=(tab:indexedFields)&_b=(f:test,i:'',l:'')"
"/oxf/app/kibana#/yourApp?_a=(tab:indexedFields)&_b=(f:test,i:'',l:'')"
);
});
it('should extract path relative to browser history with basename', () => {
const url =
"http://localhost:5601/oxf/app/kibana#/management/kibana/indexPatterns/patterns/id?_a=(tab:indexedFields)&_b=(f:test,i:'',l:'')";
"http://localhost:5601/oxf/app/kibana#/yourApp?_a=(tab:indexedFields)&_b=(f:test,i:'',l:'')";
const history1 = createBrowserHistory({ basename: '/oxf/app/' });
const relativePath1 = getRelativeToHistoryPath(url, history1);
expect(relativePath1).toEqual(
"/kibana#/management/kibana/indexPatterns/patterns/id?_a=(tab:indexedFields)&_b=(f:test,i:'',l:'')"
"/kibana#/yourApp?_a=(tab:indexedFields)&_b=(f:test,i:'',l:'')"
);
const history2 = createBrowserHistory({ basename: '/oxf/app/kibana/' });
const relativePath2 = getRelativeToHistoryPath(url, history2);
expect(relativePath2).toEqual(
"#/management/kibana/indexPatterns/patterns/id?_a=(tab:indexedFields)&_b=(f:test,i:'',l:'')"
);
expect(relativePath2).toEqual("#/yourApp?_a=(tab:indexedFields)&_b=(f:test,i:'',l:'')");
});
it('should extract path relative to browser history with basename from relative url', () => {
const history = createBrowserHistory({ basename: '/oxf/app/' });
const url =
"/oxf/app/kibana#/management/kibana/indexPatterns/patterns/id?_a=(tab:indexedFields)&_b=(f:test,i:'',l:'')";
const url = "/oxf/app/kibana#/yourApp?_a=(tab:indexedFields)&_b=(f:test,i:'',l:'')";
const relativePath = getRelativeToHistoryPath(url, history);
expect(relativePath).toEqual(
"/kibana#/management/kibana/indexPatterns/patterns/id?_a=(tab:indexedFields)&_b=(f:test,i:'',l:'')"
);
expect(relativePath).toEqual("/kibana#/yourApp?_a=(tab:indexedFields)&_b=(f:test,i:'',l:'')");
});
it('should extract path relative to hash history without basename', () => {
const history = createHashHistory();
const url =
"http://localhost:5601/oxf/app/kibana#/management/kibana/indexPatterns/patterns/id?_a=(tab:indexedFields)&_b=(f:test,i:'',l:'')";
"http://localhost:5601/oxf/app/kibana#/yourApp?_a=(tab:indexedFields)&_b=(f:test,i:'',l:'')";
const relativePath = getRelativeToHistoryPath(url, history);
expect(relativePath).toEqual(
"/management/kibana/indexPatterns/patterns/id?_a=(tab:indexedFields)&_b=(f:test,i:'',l:'')"
);
expect(relativePath).toEqual("/yourApp?_a=(tab:indexedFields)&_b=(f:test,i:'',l:'')");
});
it('should extract path relative to hash history with basename', () => {
const history = createHashHistory({ basename: 'management' });
const url =
"http://localhost:5601/oxf/app/kibana#/management/kibana/indexPatterns/patterns/id?_a=(tab:indexedFields)&_b=(f:test,i:'',l:'')";
"http://localhost:5601/oxf/app/kibana#/yourApp?_a=(tab:indexedFields)&_b=(f:test,i:'',l:'')";
const relativePath = getRelativeToHistoryPath(url, history);
expect(relativePath).toEqual(
"/kibana/indexPatterns/patterns/id?_a=(tab:indexedFields)&_b=(f:test,i:'',l:'')"
);
expect(relativePath).toEqual("/yourApp?_a=(tab:indexedFields)&_b=(f:test,i:'',l:'')");
});
it('should extract path relative to hash history with basename from relative url', () => {
const history = createHashHistory({ basename: 'management' });
const url =
"/oxf/app/kibana#/management/kibana/indexPatterns/patterns/id?_a=(tab:indexedFields)&_b=(f:test,i:'',l:'')";
const url = "/oxf/app/kibana#/yourApp?_a=(tab:indexedFields)&_b=(f:test,i:'',l:'')";
const relativePath = getRelativeToHistoryPath(url, history);
expect(relativePath).toEqual(
"/kibana/indexPatterns/patterns/id?_a=(tab:indexedFields)&_b=(f:test,i:'',l:'')"
);
expect(relativePath).toEqual("/yourApp?_a=(tab:indexedFields)&_b=(f:test,i:'',l:'')");
});
});
});

View file

@ -31,7 +31,7 @@ import { url as urlUtils } from '../../../common';
* e.g.:
*
* given an url:
* http://localhost:5601/oxf/app/kibana#/management/kibana/indexPatterns/patterns/id?_a=(tab:indexedFields)&_b=(f:test,i:'',l:'')
* http://localhost:5601/oxf/app/kibana#/yourApp?_a=(tab:indexedFields)&_b=(f:test,i:'',l:'')
* will return object:
* {_a: {tab: 'indexedFields'}, _b: {f: 'test', i: '', l: ''}};
*/
@ -57,7 +57,7 @@ export function getStatesFromKbnUrl(
* e.g.:
*
* given an url:
* http://localhost:5601/oxf/app/kibana#/management/kibana/indexPatterns/patterns/id?_a=(tab:indexedFields)&_b=(f:test,i:'',l:'')
* http://localhost:5601/oxf/app/kibana#/yourApp?_a=(tab:indexedFields)&_b=(f:test,i:'',l:'')
* and key '_a'
* will return object:
* {tab: 'indexedFields'}
@ -74,12 +74,12 @@ export function getStateFromKbnUrl<State>(
* Doesn't actually updates history
*
* e.g.:
* given a url: http://localhost:5601/oxf/app/kibana#/management/kibana/indexPatterns/patterns/id?_a=(tab:indexedFields)&_b=(f:test,i:'',l:'')
* given a url: http://localhost:5601/oxf/app/kibana#/yourApp?_a=(tab:indexedFields)&_b=(f:test,i:'',l:'')
* key: '_a'
* and state: {tab: 'other'}
*
* will return url:
* http://localhost:5601/oxf/app/kibana#/management/kibana/indexPatterns/patterns/id?_a=(tab:other)&_b=(f:test,i:'',l:'')
* http://localhost:5601/oxf/app/kibana#/yourApp?_a=(tab:other)&_b=(f:test,i:'',l:'')
*/
export function setStateToKbnUrl<State>(
key: string,

View file

@ -1,11 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Management app can mount and unmount 1`] = `
<div>
<div>
Test App - Hello world!
</div>
</div>
`;
exports[`Management app can mount and unmount 2`] = `<div />`;

View file

@ -17,11 +17,26 @@
* under the License.
*/
declare module 'ui/management' {
export const SidebarNav: React.FC<any>;
export const management: any; // TODO - properly provide types
export const MANAGEMENT_BREADCRUMB: {
text: string;
href: string;
};
}
import React from 'react';
import ReactDOM from 'react-dom';
import { AppMountContext, AppMountParameters } from 'kibana/public';
import { ManagementApp, ManagementAppDependencies } from './components/management_app';
export const renderApp = async (
context: AppMountContext,
{ history, appBasePath, element }: AppMountParameters,
dependencies: ManagementAppDependencies
) => {
ReactDOM.render(
<ManagementApp
context={context}
dependencies={dependencies}
appBasePath={appBasePath}
history={history}
/>,
element
);
return () => ReactDOM.unmountComponentAtNode(element);
};

View file

@ -1 +0,0 @@
@import './management_sidebar_nav/index';

View file

@ -17,5 +17,5 @@
* under the License.
*/
export { ManagementSidebarNav } from './management_sidebar_nav';
export { ManagementChrome } from './management_chrome';
export { ManagementApp } from './management_app';
export { managementSections } from './management_sections';

View file

@ -17,4 +17,4 @@
* under the License.
*/
export { ManagementChrome } from './management_chrome';
export { ManagementLandingPage } from './landing';

View file

@ -0,0 +1,76 @@
/*
* 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 { FormattedMessage } from '@kbn/i18n/react';
import {
EuiHorizontalRule,
EuiIcon,
EuiPageContent,
EuiSpacer,
EuiText,
EuiTitle,
} from '@elastic/eui';
interface ManagementLandingPageProps {
version: string;
setBreadcrumbs: () => void;
}
export const ManagementLandingPage = ({ version, setBreadcrumbs }: ManagementLandingPageProps) => {
setBreadcrumbs();
return (
<EuiPageContent horizontalPosition="center" data-test-subj="managementHome">
<div>
<div className="eui-textCenter">
<EuiIcon type="managementApp" size="xxl" />
<EuiSpacer />
<EuiTitle>
<h1>
<FormattedMessage
id="management.landing.header"
defaultMessage="Welcome to Stack Management {version}"
values={{ version }}
/>
</h1>
</EuiTitle>
<EuiText>
<FormattedMessage
id="management.landing.subhead"
defaultMessage="Manage your indices, index patterns, saved objects, Kibana settings, and more."
/>
</EuiText>
</div>
<EuiHorizontalRule />
<EuiText color="subdued" size="s" textAlign="center">
<p>
<FormattedMessage
id="management.landing.text"
defaultMessage="A complete list of apps is in the menu on the left."
/>
</p>
</EuiText>
</div>
</EuiPageContent>
);
};

View file

@ -0,0 +1,20 @@
/*
* 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.
*/
export { ManagementApp, ManagementAppDependencies } from './management_app';

View file

@ -0,0 +1,6 @@
// Hack because the management wrapper is flat HTML and needs a class
.mgtPage__body {
max-width: map-get($euiBreakpoints, 'xl');
margin: 0 auto;
}

View file

@ -0,0 +1,95 @@
/*
* 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, { useState, useEffect, useCallback } from 'react';
import {
AppMountContext,
AppMountParameters,
ChromeBreadcrumb,
ScopedHistory,
} from 'kibana/public';
import { I18nProvider } from '@kbn/i18n/react';
import { EuiPage } from '@elastic/eui';
import { ManagementStart } from '../../types';
import { ManagementSection, MANAGEMENT_BREADCRUMB } from '../../utils';
import { ManagementRouter } from './management_router';
import { ManagementSidebarNav } from '../management_sidebar_nav';
import { reactRouterNavigate } from '../../../../kibana_react/public';
import './management_app.scss';
interface ManagementAppProps {
appBasePath: string;
context: AppMountContext;
history: AppMountParameters['history'];
dependencies: ManagementAppDependencies;
}
export interface ManagementAppDependencies {
management: ManagementStart;
kibanaVersion: string;
}
export const ManagementApp = ({ context, dependencies, history }: ManagementAppProps) => {
const [selectedId, setSelectedId] = useState<string>('');
const [sections, setSections] = useState<ManagementSection[]>();
const onAppMounted = useCallback((id: string) => {
setSelectedId(id);
window.scrollTo(0, 0);
}, []);
const setBreadcrumbs = useCallback(
(crumbs: ChromeBreadcrumb[] = [], appHistory?: ScopedHistory) => {
const wrapBreadcrumb = (item: ChromeBreadcrumb, scopedHistory: ScopedHistory) => ({
...item,
...(item.href ? reactRouterNavigate(scopedHistory, item.href) : {}),
});
context.core.chrome.setBreadcrumbs([
wrapBreadcrumb(MANAGEMENT_BREADCRUMB, history),
...crumbs.map((item) => wrapBreadcrumb(item, appHistory || history)),
]);
},
[context.core.chrome, history]
);
useEffect(() => {
setSections(dependencies.management.sections.getSectionsEnabled());
}, [dependencies.management.sections]);
if (!sections) {
return null;
}
return (
<I18nProvider>
<EuiPage>
<ManagementSidebarNav selectedId={selectedId} sections={sections} history={history} />
<ManagementRouter
history={history}
setBreadcrumbs={setBreadcrumbs}
onAppMounted={onAppMounted}
sections={sections}
dependencies={dependencies}
/>
</EuiPage>
</I18nProvider>
);
};

View file

@ -0,0 +1,72 @@
/*
* 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, { memo } from 'react';
import { Route, Router, Switch } from 'react-router-dom';
import { EuiPageBody } from '@elastic/eui';
import { AppMountParameters, ChromeBreadcrumb, ScopedHistory } from 'kibana/public';
import { ManagementAppWrapper } from '../management_app_wrapper';
import { ManagementLandingPage } from '../landing';
import { ManagementAppDependencies } from './management_app';
import { ManagementSection } from '../../utils';
interface ManagementRouterProps {
history: AppMountParameters['history'];
dependencies: ManagementAppDependencies;
setBreadcrumbs: (crumbs?: ChromeBreadcrumb[], appHistory?: ScopedHistory) => void;
onAppMounted: (id: string) => void;
sections: ManagementSection[];
}
export const ManagementRouter = memo(
({ dependencies, history, setBreadcrumbs, onAppMounted, sections }: ManagementRouterProps) => (
<Router history={history}>
<EuiPageBody restrictWidth={false} className="mgtPage__body">
<Switch>
{sections.map((section) =>
section
.getAppsEnabled()
.map((app) => (
<Route
path={`${app.basePath}`}
component={() => (
<ManagementAppWrapper
app={app}
setBreadcrumbs={setBreadcrumbs}
onAppMounted={onAppMounted}
history={history}
/>
)}
/>
))
)}
<Route
path={'/'}
component={() => (
<ManagementLandingPage
version={dependencies.kibanaVersion}
setBreadcrumbs={setBreadcrumbs}
/>
)}
/>
</Switch>
</EuiPageBody>
</Router>
)
);

View file

@ -17,8 +17,4 @@
* under the License.
*/
import { npSetup } from 'ui/new_platform';
const registry = npSetup.plugins.savedObjectsManagement?.serviceRegistry;
export const savedObjectManagementRegistry = registry!;
export { ManagementAppWrapper } from './management_app_wrapper';

View file

@ -0,0 +1,69 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import React, { createRef, Component } from 'react';
import { ChromeBreadcrumb, AppMountParameters, ScopedHistory } from 'kibana/public';
import { ManagementApp } from '../../utils';
import { Unmount } from '../../types';
interface ManagementSectionWrapperProps {
app: ManagementApp;
setBreadcrumbs: (crumbs?: ChromeBreadcrumb[], history?: ScopedHistory) => void;
onAppMounted: (id: string) => void;
history: AppMountParameters['history'];
}
export class ManagementAppWrapper extends Component<ManagementSectionWrapperProps> {
private unmount?: Unmount;
private mountElementRef = createRef<HTMLElement>();
componentDidMount() {
const { setBreadcrumbs, app, onAppMounted, history } = this.props;
const { mount, basePath } = app;
const appHistory = history.createSubHistory(app.basePath);
const mountResult = mount({
basePath,
setBreadcrumbs: (crumbs: ChromeBreadcrumb[]) => setBreadcrumbs(crumbs, appHistory),
element: this.mountElementRef.current!,
history: appHistory,
});
onAppMounted(app.id);
if (mountResult instanceof Promise) {
mountResult.then((um) => {
this.unmount = um;
});
} else {
this.unmount = mountResult;
}
}
async componentWillUnmount() {
if (this.unmount) {
await this.unmount();
}
}
render() {
return <main ref={this.mountElementRef} />;
}
}

View file

@ -1,59 +0,0 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import * as React from 'react';
import { EuiPage, EuiPageBody, EuiPageSideBar } from '@elastic/eui';
import { I18nProvider } from '@kbn/i18n/react';
import { ManagementSidebarNav } from '../management_sidebar_nav';
import { LegacySection } from '../../types';
import { ManagementSection } from '../../management_section';
interface Props {
getSections: () => ManagementSection[];
legacySections: LegacySection[];
selectedId: string;
onMounted: (element: HTMLDivElement) => void;
}
export class ManagementChrome extends React.Component<Props> {
private container = React.createRef<HTMLDivElement>();
componentDidMount() {
if (this.container.current) {
this.props.onMounted(this.container.current);
}
}
render() {
return (
<I18nProvider>
<EuiPage>
<EuiPageSideBar>
<ManagementSidebarNav
getSections={this.props.getSections}
legacySections={this.props.legacySections}
selectedId={this.props.selectedId}
/>
</EuiPageSideBar>
<EuiPageBody restrictWidth={true} className="mgtPage__body">
<div ref={this.container} />
</EuiPageBody>
</EuiPage>
</I18nProvider>
);
}
}

View file

@ -20,14 +20,14 @@
import React from 'react';
import { EuiFlexGroup, EuiFlexItem, EuiToolTip, EuiIcon } from '@elastic/eui';
import { ManagementSectionId } from './types';
import { ManagementSectionId } from '../types';
interface Props {
interface ManagementSectionTitleProps {
text: string;
tip: string;
}
const ManagementSectionTitle = ({ text, tip }: Props) => (
const ManagementSectionTitle = ({ text, tip }: ManagementSectionTitleProps) => (
<EuiToolTip content={tip} position="right">
<EuiFlexGroup alignItems="center" gutterSize="s" responsive={false}>
<EuiFlexItem grow={false}>{text}</EuiFlexItem>

View file

@ -1,95 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Management adds legacy apps to existing SidebarNav sections 1`] = `
Array [
Object {
"data-test-subj": "activeSection",
"icon": null,
"id": "activeSection",
"items": Array [
Object {
"data-test-subj": "item",
"href": undefined,
"id": "item",
"isSelected": false,
"name": "item",
"order": undefined,
},
],
"name": "activeSection",
"order": 10,
},
Object {
"data-test-subj": "no-active-items",
"icon": null,
"id": "no-active-items",
"items": Array [
Object {
"data-test-subj": "disabled",
"href": undefined,
"id": "disabled",
"isSelected": false,
"name": "disabled",
"order": undefined,
},
Object {
"data-test-subj": "notVisible",
"href": undefined,
"id": "notVisible",
"isSelected": false,
"name": "notVisible",
"order": undefined,
},
],
"name": "No active items",
"order": 10,
},
]
`;
exports[`Management maps legacy sections and apps into SidebarNav items 1`] = `
Array [
Object {
"data-test-subj": "no-active-items",
"icon": null,
"id": "no-active-items",
"items": Array [
Object {
"data-test-subj": "disabled",
"href": undefined,
"id": "disabled",
"isSelected": false,
"name": "disabled",
"order": undefined,
},
Object {
"data-test-subj": "notVisible",
"href": undefined,
"id": "notVisible",
"isSelected": false,
"name": "notVisible",
"order": undefined,
},
],
"name": "No active items",
"order": 10,
},
Object {
"data-test-subj": "activeSection",
"icon": null,
"id": "activeSection",
"items": Array [
Object {
"data-test-subj": "item",
"href": undefined,
"id": "item",
"isSelected": false,
"name": "item",
"order": undefined,
},
],
"name": "activeSection",
"order": 10,
},
]
`;

View file

@ -1 +0,0 @@
@import './sidebar_nav';

View file

@ -1,5 +1,6 @@
.mgtSideBarNav {
width: 210px;
margin-right: $euiSize;
}
@include euiBreakpoint('xs','s') {

View file

@ -1,98 +0,0 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { IndexedArray } from '../../../../../legacy/ui/public/indexed_array';
import { mergeLegacyItems } from './management_sidebar_nav';
const toIndexedArray = (initialSet: any[]) =>
new IndexedArray({
index: ['id'],
order: ['order'],
initialSet,
});
const activeProps = { visible: true, disabled: false };
const disabledProps = { visible: true, disabled: true };
const notVisibleProps = { visible: false, disabled: false };
const visibleItem = { display: 'item', id: 'item', ...activeProps };
const notVisibleSection = {
display: 'Not visible',
id: 'not-visible',
order: 10,
visibleItems: toIndexedArray([visibleItem]),
...notVisibleProps,
};
const disabledSection = {
display: 'Disabled',
id: 'disabled',
order: 10,
visibleItems: toIndexedArray([visibleItem]),
...disabledProps,
};
const noItemsSection = {
display: 'No items',
id: 'no-items',
order: 10,
visibleItems: toIndexedArray([]),
...activeProps,
};
const noActiveItemsSection = {
display: 'No active items',
id: 'no-active-items',
order: 10,
visibleItems: toIndexedArray([
{ display: 'disabled', id: 'disabled', ...disabledProps },
{ display: 'notVisible', id: 'notVisible', ...notVisibleProps },
]),
...activeProps,
};
const activeSection = {
display: 'activeSection',
id: 'activeSection',
order: 10,
visibleItems: toIndexedArray([visibleItem]),
...activeProps,
};
const managementSections = [
notVisibleSection,
disabledSection,
noItemsSection,
noActiveItemsSection,
activeSection,
];
describe('Management', () => {
it('maps legacy sections and apps into SidebarNav items', () => {
expect(mergeLegacyItems([], managementSections, 'active-item-id')).toMatchSnapshot();
});
it('adds legacy apps to existing SidebarNav sections', () => {
const navSection = {
'data-test-subj': 'activeSection',
icon: null,
id: 'activeSection',
items: [],
name: 'activeSection',
order: 10,
};
expect(mergeLegacyItems([navSection], managementSections, 'active-item-id')).toMatchSnapshot();
});
});

View file

@ -17,184 +17,97 @@
* under the License.
*/
import {
EuiIcon,
// @ts-ignore
EuiSideNav,
EuiScreenReaderOnly,
} from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import React, { useState } from 'react';
import { i18n } from '@kbn/i18n';
import React, { ReactElement } from 'react';
import { LegacySection, LegacyApp } from '../../types';
import { ManagementApp } from '../../management_app';
import { ManagementSection } from '../../management_section';
import { sortBy } from 'lodash';
interface NavApp {
id: string;
name: ReactElement | string;
[key: string]: unknown;
order: number; // only needed while merging platform and legacy
}
import { EuiIcon, EuiSideNav, EuiScreenReaderOnly, EuiSideNavItemType } from '@elastic/eui';
import { AppMountParameters } from 'kibana/public';
import { ManagementApp, ManagementSection } from '../../utils';
interface NavSection extends NavApp {
items: NavApp[];
}
import './management_sidebar_nav.scss';
import { ManagementItem } from '../../utils/management_item';
import { reactRouterNavigate } from '../../../../kibana_react/public';
interface ManagementSidebarNavProps {
getSections: () => ManagementSection[];
legacySections: LegacySection[];
sections: ManagementSection[];
history: AppMountParameters['history'];
selectedId: string;
}
interface ManagementSidebarNavState {
isSideNavOpenOnMobile: boolean;
}
const managementSectionOrAppToNav = (appOrSection: ManagementApp | ManagementSection) => ({
id: appOrSection.id,
name: appOrSection.title,
'data-test-subj': appOrSection.id,
order: appOrSection.order,
const headerLabel = i18n.translate('management.nav.label', {
defaultMessage: 'Management',
});
const managementSectionToNavSection = (section: ManagementSection) => {
const iconType = section.euiIconType
? section.euiIconType
: section.icon
? section.icon
: 'empty';
const navMenuLabel = i18n.translate('management.nav.menu', {
defaultMessage: 'Management menu',
});
return {
icon: <EuiIcon type={iconType} size="m" />,
...managementSectionOrAppToNav(section),
/** @internal **/
export const ManagementSidebarNav = ({
selectedId,
sections,
history,
}: ManagementSidebarNavProps) => {
const HEADER_ID = 'stack-management-nav-header';
const [isSideNavOpenOnMobile, setIsSideNavOpenOnMobile] = useState(false);
const toggleOpenOnMobile = () => setIsSideNavOpenOnMobile(!isSideNavOpenOnMobile);
const sectionsToNavItems = (managementSections: ManagementSection[]) => {
const sortedManagementSections = sortBy(managementSections, 'order');
return sortedManagementSections.reduce<Array<EuiSideNavItemType<any>>>((acc, section) => {
const apps = sortBy(section.getAppsEnabled(), 'order');
if (apps.length) {
acc.push({
...createNavItem(section, {
items: appsToNavItems(apps),
}),
});
}
return acc;
}, []);
};
};
const managementAppToNavItem = (selectedId?: string, parentId?: string) => (
app: ManagementApp
) => ({
isSelected: selectedId === app.id,
href: `#/management/${parentId}/${app.id}`,
...managementSectionOrAppToNav(app),
});
const appsToNavItems = (managementApps: ManagementApp[]) =>
managementApps.map((app) => ({
...createNavItem(app, {
...reactRouterNavigate(history, app.basePath),
}),
}));
const legacySectionToNavSection = (section: LegacySection) => ({
name: section.display,
id: section.id,
icon: section.icon ? <EuiIcon type={section.icon} /> : null,
items: [],
'data-test-subj': section.id,
// @ts-ignore
order: section.order,
});
const createNavItem = <T extends ManagementItem>(
item: T,
customParams: Partial<EuiSideNavItemType<any>> = {}
) => {
const iconType = item.euiIconType || item.icon;
const legacyAppToNavItem = (app: LegacyApp, selectedId: string) => ({
isSelected: selectedId === app.id,
name: app.display,
id: app.id,
href: app.url,
'data-test-subj': app.id,
// @ts-ignore
order: app.order,
});
const sectionVisible = (section: LegacySection | LegacyApp) => !section.disabled && section.visible;
const sideNavItems = (sections: ManagementSection[], selectedId: string) =>
sections.map((section) => ({
items: section.getAppsEnabled().map(managementAppToNavItem(selectedId, section.id)),
...managementSectionToNavSection(section),
}));
const findOrAddSection = (navItems: NavSection[], legacySection: LegacySection): NavSection => {
const foundSection = navItems.find((sec) => sec.id === legacySection.id);
if (foundSection) {
return foundSection;
} else {
const newSection = legacySectionToNavSection(legacySection);
navItems.push(newSection);
navItems.sort((a: NavSection, b: NavSection) => a.order - b.order); // only needed while merging platform and legacy
return newSection;
}
};
export const mergeLegacyItems = (
navItems: NavSection[],
legacySections: LegacySection[],
selectedId: string
) => {
const filteredLegacySections = legacySections
.filter(sectionVisible)
.filter((section) => section.visibleItems.length);
filteredLegacySections.forEach((legacySection) => {
const section = findOrAddSection(navItems, legacySection);
legacySection.visibleItems.forEach((app) => {
section.items.push(legacyAppToNavItem(app, selectedId));
return section.items.sort((a, b) => a.order - b.order);
});
});
return navItems;
};
const sectionsToItems = (
sections: ManagementSection[],
legacySections: LegacySection[],
selectedId: string
) => {
const navItems = sideNavItems(sections, selectedId);
return mergeLegacyItems(navItems, legacySections, selectedId);
};
export class ManagementSidebarNav extends React.Component<
ManagementSidebarNavProps,
ManagementSidebarNavState
> {
constructor(props: ManagementSidebarNavProps) {
super(props);
this.state = {
isSideNavOpenOnMobile: false,
return {
id: item.id,
name: item.title,
isSelected: item.id === selectedId,
icon: iconType ? <EuiIcon type={iconType} size="m" /> : undefined,
'data-test-subj': item.id,
...customParams,
};
}
public render() {
const HEADER_ID = 'stack-management-nav-header';
return (
<>
<EuiScreenReaderOnly>
<h2 id={HEADER_ID}>
{i18n.translate('management.nav.label', {
defaultMessage: 'Management',
})}
</h2>
</EuiScreenReaderOnly>
<EuiSideNav
aria-labelledby={HEADER_ID}
mobileTitle={this.renderMobileTitle()}
isOpenOnMobile={this.state.isSideNavOpenOnMobile}
toggleOpenOnMobile={this.toggleOpenOnMobile}
items={sectionsToItems(
this.props.getSections(),
this.props.legacySections,
this.props.selectedId
)}
className="mgtSideBarNav"
/>
</>
);
}
private renderMobileTitle() {
return <FormattedMessage id="management.nav.menu" defaultMessage="Management menu" />;
}
private toggleOpenOnMobile = () => {
this.setState({
isSideNavOpenOnMobile: !this.state.isSideNavOpenOnMobile,
});
};
}
return (
<>
<EuiScreenReaderOnly>
<h2 id={HEADER_ID}>{headerLabel}</h2>
</EuiScreenReaderOnly>
<EuiSideNav
aria-labelledby={HEADER_ID}
mobileTitle={navMenuLabel}
toggleOpenOnMobile={toggleOpenOnMobile}
isOpenOnMobile={isSideNavOpenOnMobile}
items={sectionsToNavItems(sections)}
className="mgtSideBarNav"
/>
</>
);
};

View file

@ -21,17 +21,14 @@ import { PluginInitializerContext } from 'kibana/public';
import { ManagementPlugin } from './plugin';
export function plugin(initializerContext: PluginInitializerContext) {
return new ManagementPlugin();
return new ManagementPlugin(initializerContext);
}
export { RegisterManagementAppArgs, ManagementSection, ManagementApp } from './utils';
export {
ManagementSectionId,
ManagementAppMountParams,
ManagementSetup,
ManagementStart,
RegisterManagementApp,
ManagementSectionId,
RegisterManagementAppArgs,
ManagementAppMountParams,
} from './types';
export { ManagementApp } from './management_app';
export { ManagementSection } from './management_section';
export { ManagementSidebarNav } from './components'; // for use in legacy management apps

View file

@ -1,67 +0,0 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import React from 'react';
import { EuiCallOut } from '@elastic/eui';
import { NotificationsStart, OverlayStart } from 'kibana/public';
import { parse } from 'query-string';
import { i18n } from '@kbn/i18n';
import { toMountPoint } from '../../../kibana_react/public';
import { MarkdownSimple } from '../../../kibana_react/public';
/**
* Show banners and toasts carried over from other applications. This is only necessary as long as
* management is rendered in the legacy platform (which requires a full page reload to switch to).
*
* Once management is rendered using the core application service, this file and the places setting
* bannerMessage and notFoundMessage URL params can be removed.
* @param notifications Core notifications service
* @param overlays Core overlays service
*/
export function showLegacyRedirectMessages(
notifications: NotificationsStart,
overlays: OverlayStart
) {
const queryPosition = window.location.hash.indexOf('?');
if (queryPosition === -1) {
return;
}
const urlParams = parse(window.location.hash.substr(queryPosition)) as Record<string, string>;
if (urlParams.bannerMessage) {
const bannerId = overlays.banners.add(
toMountPoint(
<EuiCallOut color="warning" iconType="iInCircle" title={urlParams.bannerMessage} />
)
);
setTimeout(() => {
overlays.banners.remove(bannerId);
}, 15000);
}
if (urlParams.notFoundMessage) {
notifications.toasts.addWarning({
title: i18n.translate('management.history.savedObjectIsMissingNotificationMessage', {
defaultMessage: 'Saved object is missing',
}),
text: toMountPoint(<MarkdownSimple>{urlParams.notFoundMessage}</MarkdownSimple>),
});
}
}

View file

@ -1,160 +0,0 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { assign } from 'lodash';
import { IndexedArray } from '../../../../legacy/ui/public/indexed_array';
const listeners = [];
export class LegacyManagementSection {
/**
* @param {string} id
* @param {object} options
* @param {number|null} options.order
* @param {string|null} options.display - defaults to id
* @param {string|null} options.url - defaults to ''
* @param {boolean|null} options.visible - defaults to true
* @param {boolean|null} options.disabled - defaults to false
* @param {string|null} options.tooltip - defaults to ''
* @param {string|null} options.icon - defaults to ''
* @returns {ManagementSection}
*/
constructor(id, options = {}, capabilities) {
this.display = id;
this.id = id;
this.items = new IndexedArray({
index: ['id'],
order: ['order'],
});
this.visible = true;
this.disabled = false;
this.tooltip = '';
this.icon = '';
this.url = '';
this.capabilities = capabilities;
assign(this, options);
}
get visibleItems() {
return this.items.inOrder.filter((item) => {
const capabilityManagementSection = this.capabilities.management[this.id];
const itemCapability = capabilityManagementSection
? capabilityManagementSection[item.id]
: null;
return item.visible && itemCapability !== false;
});
}
/**
* Registers a callback that will be executed when management sections are updated
* Globally bound to solve for sidebar nav needs
*
* @param {function} fn
*/
addListener(fn) {
listeners.push(fn);
}
/**
* Registers a sub-section
*
* @param {string} id
* @param {object} options
* @returns {ManagementSection}
*/
register(id, options = {}) {
const item = new LegacyManagementSection(
id,
assign(options, { parent: this }),
this.capabilities
);
if (this.hasItem(id)) {
throw new Error(`'${id}' is already registered`);
}
this.items.push(item);
listeners.forEach((fn) => fn());
return item;
}
/**
* Deregisters a section
*
* @param {string} id
*/
deregister(id) {
this.items.remove((item) => item.id === id);
listeners.forEach((fn) => fn(this.items));
}
/**
* Determine if an id is already registered
*
* @param {string} id
* @returns {boolean}
*/
hasItem(id) {
return this.items.byId.hasOwnProperty(id);
}
/**
* Fetches a section by id
*
* @param {string} id
* @returns {ManagementSection}
*/
getSection(id) {
if (!id) {
return;
}
const sectionPath = id.split('/');
return sectionPath.reduce((currentSection, nextSection) => {
if (!currentSection) {
return;
}
return currentSection.items.byId[nextSection];
}, this);
}
hide() {
this.visible = false;
}
show() {
this.visible = true;
}
disable() {
this.disabled = true;
}
enable() {
this.disabled = false;
}
}

View file

@ -1,285 +0,0 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { LegacyManagementSection } from './section';
import { IndexedArray } from '../../../../legacy/ui/public/indexed_array';
const capabilitiesMock = {
management: {
kibana: { sampleFeature2: false },
},
};
describe('ManagementSection', () => {
describe('constructor', () => {
it('defaults display to id', () => {
const section = new LegacyManagementSection('kibana', {}, capabilitiesMock);
expect(section.display).toBe('kibana');
});
it('defaults visible to true', () => {
const section = new LegacyManagementSection('kibana', {}, capabilitiesMock);
expect(section.visible).toBe(true);
});
it('defaults disabled to false', () => {
const section = new LegacyManagementSection('kibana', {}, capabilitiesMock);
expect(section.disabled).toBe(false);
});
it('defaults tooltip to empty string', () => {
const section = new LegacyManagementSection('kibana', {}, capabilitiesMock);
expect(section.tooltip).toBe('');
});
it('defaults url to empty string', () => {
const section = new LegacyManagementSection('kibana', {}, capabilitiesMock);
expect(section.url).toBe('');
});
it('exposes items', () => {
const section = new LegacyManagementSection('kibana', {}, capabilitiesMock);
expect(section.items).toHaveLength(0);
});
it('exposes visibleItems', () => {
const section = new LegacyManagementSection('kibana', {}, capabilitiesMock);
expect(section.visibleItems).toHaveLength(0);
});
it('assigns all options', () => {
const section = new LegacyManagementSection(
'kibana',
{ description: 'test', url: 'foobar' },
capabilitiesMock
);
expect(section.description).toBe('test');
expect(section.url).toBe('foobar');
});
});
describe('register', () => {
let section;
beforeEach(() => {
section = new LegacyManagementSection('kibana', {}, capabilitiesMock);
});
it('returns a ManagementSection', () => {
expect(section.register('about')).toBeInstanceOf(LegacyManagementSection);
});
it('provides a reference to the parent', () => {
expect(section.register('about').parent).toBe(section);
});
it('adds item', function () {
section.register('about', { description: 'test' });
expect(section.items).toHaveLength(1);
expect(section.items[0]).toBeInstanceOf(LegacyManagementSection);
expect(section.items[0].id).toBe('about');
});
it('can only register a section once', () => {
let threwException = false;
section.register('about');
try {
section.register('about');
} catch (e) {
threwException = e.message.indexOf('is already registered') > -1;
}
expect(threwException).toBe(true);
});
it('calls listener when item added', () => {
let listerCalled = false;
const listenerFn = () => {
listerCalled = true;
};
section.addListener(listenerFn);
section.register('about');
expect(listerCalled).toBe(true);
});
});
describe('deregister', () => {
let section;
beforeEach(() => {
section = new LegacyManagementSection('kibana', {}, capabilitiesMock);
section.register('about');
});
it('deregisters an existing section', () => {
section.deregister('about');
expect(section.items).toHaveLength(0);
});
it('allows deregistering a section more than once', () => {
section.deregister('about');
section.deregister('about');
expect(section.items).toHaveLength(0);
});
it('calls listener when item added', () => {
let listerCalled = false;
const listenerFn = () => {
listerCalled = true;
};
section.addListener(listenerFn);
section.deregister('about');
expect(listerCalled).toBe(true);
});
});
describe('getSection', () => {
let section;
beforeEach(() => {
section = new LegacyManagementSection('kibana', {}, capabilitiesMock);
section.register('about');
});
it('returns registered section', () => {
expect(section.getSection('about')).toBeInstanceOf(LegacyManagementSection);
});
it('returns undefined if un-registered', () => {
expect(section.getSection('unknown')).not.toBeDefined();
});
it('returns sub-sections specified via a /-separated path', () => {
section.getSection('about').register('time');
expect(section.getSection('about/time')).toBeInstanceOf(LegacyManagementSection);
expect(section.getSection('about/time')).toBe(section.getSection('about').getSection('time'));
});
it('returns undefined if a sub-section along a /-separated path does not exist', () => {
expect(section.getSection('about/damn/time')).toBe(undefined);
});
});
describe('items', () => {
let section;
beforeEach(() => {
section = new LegacyManagementSection('kibana', {}, capabilitiesMock);
section.register('three', { order: 3 });
section.register('one', { order: 1 });
section.register('two', { order: 2 });
});
it('is an indexed array', () => {
expect(section.items).toBeInstanceOf(IndexedArray);
});
it('is indexed on id', () => {
const keys = Object.keys(section.items.byId).sort();
expect(section.items.byId).toBeInstanceOf(Object);
expect(keys).toEqual(['one', 'three', 'two']);
});
it('can be ordered', () => {
const ids = section.items.inOrder.map((i) => {
return i.id;
});
expect(ids).toEqual(['one', 'two', 'three']);
});
});
describe('visible', () => {
let section;
beforeEach(() => {
section = new LegacyManagementSection('kibana', {}, capabilitiesMock);
});
it('hide sets visible to false', () => {
section.hide();
expect(section.visible).toBe(false);
});
it('show sets visible to true', () => {
section.hide();
section.show();
expect(section.visible).toBe(true);
});
});
describe('disabled', () => {
let section;
beforeEach(() => {
section = new LegacyManagementSection('kibana', {}, capabilitiesMock);
});
it('disable sets disabled to true', () => {
section.disable();
expect(section.disabled).toBe(true);
});
it('enable sets disabled to false', () => {
section.enable();
expect(section.disabled).toBe(false);
});
});
describe('visibleItems', () => {
let section;
beforeEach(() => {
section = new LegacyManagementSection('kibana', {}, capabilitiesMock);
section.register('three', { order: 3 });
section.register('one', { order: 1 });
section.register('two', { order: 2 });
});
it('maintains the order', () => {
const ids = section.visibleItems.map((i) => {
return i.id;
});
expect(ids).toEqual(['one', 'two', 'three']);
});
it('does not include hidden items', () => {
section.getSection('two').hide();
const ids = section.visibleItems.map((i) => {
return i.id;
});
expect(ids).toEqual(['one', 'three']);
});
it('does not include visible items hidden via uiCapabilities', () => {
section.register('sampleFeature2', { order: 4, visible: true });
const ids = section.visibleItems.map((i) => {
return i.id;
});
expect(ids).toEqual(['one', 'two', 'three']);
});
});
});

View file

@ -1,47 +0,0 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { i18n } from '@kbn/i18n';
import { LegacyManagementSection } from './section';
import { managementSections } from '../management_sections';
export class LegacyManagementAdapter {
main = undefined;
init = (capabilities) => {
this.main = new LegacyManagementSection(
'management',
{
display: i18n.translate('management.displayName', {
defaultMessage: 'Stack Management',
}),
},
capabilities
);
managementSections.forEach(({ id, title }, idx) => {
this.main.register(id, {
display: title,
order: idx,
});
});
return this.main;
};
getManagement = () => this.main;
}

View file

@ -1,66 +0,0 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import * as React from 'react';
import * as ReactDOM from 'react-dom';
import { coreMock } from '../../../core/public/mocks';
import { ManagementApp } from './management_app';
// @ts-ignore
import { LegacyManagementSection } from './legacy';
function createTestApp() {
const legacySection = new LegacyManagementSection('legacy');
return new ManagementApp(
{
id: 'test-app',
title: 'Test App',
basePath: '',
mount(params) {
params.setBreadcrumbs([{ text: 'Test App' }]);
ReactDOM.render(<div>Test App - Hello world!</div>, params.element);
return () => {
ReactDOM.unmountComponentAtNode(params.element);
};
},
},
() => [],
jest.fn(),
() => legacySection,
coreMock.createSetup().getStartServices
);
}
test('Management app can mount and unmount', async () => {
const testApp = createTestApp();
const container = document.createElement('div');
document.body.appendChild(container);
const unmount = testApp.mount({ element: container, basePath: '', setBreadcrumbs: jest.fn() });
expect(container).toMatchSnapshot();
(await unmount)();
expect(container).toMatchSnapshot();
});
test('Enabled by default, can disable', () => {
const testApp = createTestApp();
expect(testApp.enabled).toBe(true);
testApp.disable();
expect(testApp.enabled).toBe(false);
});

View file

@ -1,107 +0,0 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import * as React from 'react';
import ReactDOM from 'react-dom';
import { i18n } from '@kbn/i18n';
import { CreateManagementApp, ManagementSectionMount, Unmount } from './types';
import { KibanaLegacySetup } from '../../kibana_legacy/public';
// @ts-ignore
import { LegacyManagementSection } from './legacy';
import { ManagementChrome } from './components';
import { ManagementSection } from './management_section';
import { ChromeBreadcrumb, StartServicesAccessor } from '../../../core/public/';
export class ManagementApp {
readonly id: string;
readonly title: string;
readonly basePath: string;
readonly order: number;
readonly mount: ManagementSectionMount;
private enabledStatus = true;
constructor(
{ id, title, basePath, order = 100, mount }: CreateManagementApp,
getSections: () => ManagementSection[],
registerLegacyApp: KibanaLegacySetup['registerLegacyApp'],
getLegacyManagementSections: () => LegacyManagementSection,
getStartServices: StartServicesAccessor
) {
this.id = id;
this.title = title;
this.basePath = basePath;
this.order = order;
this.mount = mount;
registerLegacyApp({
id: basePath.substr(1), // get rid of initial slash
title,
mount: async ({}, params) => {
let appUnmount: Unmount;
if (!this.enabledStatus) {
const [coreStart] = await getStartServices();
coreStart.application.navigateToApp('kibana#/management');
return () => {};
}
async function setBreadcrumbs(crumbs: ChromeBreadcrumb[]) {
const [coreStart] = await getStartServices();
coreStart.chrome.setBreadcrumbs([
{
text: i18n.translate('management.breadcrumb', {
defaultMessage: 'Stack Management',
}),
href: '#/management',
},
...crumbs,
]);
}
ReactDOM.render(
<ManagementChrome
getSections={getSections}
selectedId={id}
legacySections={getLegacyManagementSections().items}
onMounted={async (element) => {
appUnmount = await mount({
basePath,
element,
setBreadcrumbs,
});
}}
/>,
params.element
);
return async () => {
appUnmount();
ReactDOM.unmountComponentAtNode(params.element);
};
},
});
}
public enable() {
this.enabledStatus = true;
}
public disable() {
this.enabledStatus = false;
}
public get enabled() {
return this.enabledStatus;
}
}

View file

@ -1,66 +0,0 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { ManagementSection } from './management_section';
import { ManagementSectionId } from './types';
// @ts-ignore
import { LegacyManagementSection } from './legacy';
import { coreMock } from '../../../core/public/mocks';
function createSection(registerLegacyApp: () => void) {
const legacySection = new LegacyManagementSection('legacy');
const getLegacySection = () => legacySection;
const getManagementSections: () => ManagementSection[] = () => [];
const testSectionConfig = { id: ManagementSectionId.Data, title: 'Test Section' };
return new ManagementSection(
testSectionConfig,
getManagementSections,
registerLegacyApp,
getLegacySection,
coreMock.createSetup().getStartServices
);
}
test('cannot register two apps with the same id', () => {
const registerLegacyApp = jest.fn();
const section = createSection(registerLegacyApp);
const testAppConfig = { id: 'test-app', title: 'Test App', mount: () => () => {} };
section.registerApp(testAppConfig);
expect(registerLegacyApp).toHaveBeenCalled();
expect(section.apps.length).toEqual(1);
expect(() => {
section.registerApp(testAppConfig);
}).toThrow();
});
test('can enable and disable apps', () => {
const registerLegacyApp = jest.fn();
const section = createSection(registerLegacyApp);
const testAppConfig = { id: 'test-app', title: 'Test App', mount: () => () => {} };
const app = section.registerApp(testAppConfig);
expect(section.getAppsEnabled().length).toEqual(1);
app.disable();
expect(section.getAppsEnabled().length).toEqual(0);
});

View file

@ -1,80 +0,0 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { ReactElement } from 'react';
import { CreateSection, RegisterManagementAppArgs, ManagementSectionId } from './types';
import { KibanaLegacySetup } from '../../kibana_legacy/public';
import { StartServicesAccessor } from '../../../core/public';
// @ts-ignore
import { LegacyManagementSection } from './legacy';
import { ManagementApp } from './management_app';
export class ManagementSection {
public readonly id: ManagementSectionId;
public readonly title: string | ReactElement = '';
public readonly apps: ManagementApp[] = [];
public readonly order: number;
public readonly euiIconType?: string;
public readonly icon?: string;
private readonly getSections: () => ManagementSection[];
private readonly registerLegacyApp: KibanaLegacySetup['registerLegacyApp'];
private readonly getLegacyManagementSection: () => LegacyManagementSection;
private readonly getStartServices: StartServicesAccessor;
constructor(
{ id, title, order = 100, euiIconType, icon }: CreateSection,
getSections: () => ManagementSection[],
registerLegacyApp: KibanaLegacySetup['registerLegacyApp'],
getLegacyManagementSection: () => ManagementSection,
getStartServices: StartServicesAccessor
) {
this.id = id;
this.title = title;
this.order = order;
this.euiIconType = euiIconType;
this.icon = icon;
this.getSections = getSections;
this.registerLegacyApp = registerLegacyApp;
this.getLegacyManagementSection = getLegacyManagementSection;
this.getStartServices = getStartServices;
}
registerApp({ id, title, order, mount }: RegisterManagementAppArgs) {
if (this.getApp(id)) {
throw new Error(`Management app already registered - id: ${id}, title: ${title}`);
}
const app = new ManagementApp(
{ id, title, order, mount, basePath: `/management/${this.id}/${id}` },
this.getSections,
this.registerLegacyApp,
this.getLegacyManagementSection,
this.getStartServices
);
this.apps.push(app);
return app;
}
getApp(id: ManagementApp['id']) {
return this.apps.find((app) => app.id === id);
}
getAppsEnabled() {
return this.apps.filter((app) => app.enabled).sort((a, b) => a.order - b.order);
}
}

View file

@ -0,0 +1,59 @@
/*
* 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 { ManagementSectionId } from './index';
import { ManagementSectionsService } from './management_sections_service';
describe('ManagementService', () => {
let managementService: ManagementSectionsService;
beforeEach(() => {
managementService = new ManagementSectionsService();
});
test('Provides default sections', () => {
managementService.setup();
const start = managementService.start();
expect(start.getAllSections().length).toEqual(6);
expect(start.getSection(ManagementSectionId.Ingest)).toBeDefined();
expect(start.getSection(ManagementSectionId.Data)).toBeDefined();
expect(start.getSection(ManagementSectionId.InsightsAndAlerting)).toBeDefined();
expect(start.getSection(ManagementSectionId.Security)).toBeDefined();
expect(start.getSection(ManagementSectionId.Kibana)).toBeDefined();
expect(start.getSection(ManagementSectionId.Stack)).toBeDefined();
});
test('Register section, enable and disable', () => {
// Setup phase:
const setup = managementService.setup();
const testSection = setup.register({ id: 'test-section', title: 'Test Section' });
expect(setup.getSection('test-section')).not.toBeUndefined();
// Start phase:
const start = managementService.start();
expect(start.getSectionsEnabled().length).toEqual(7);
testSection.disable();
expect(start.getSectionsEnabled().length).toEqual(6);
});
});

View file

@ -0,0 +1,65 @@
/*
* 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 { ReactElement } from 'react';
import { ManagementSection, RegisterManagementSectionArgs } from './utils';
import { managementSections } from './components/management_sections';
import { ManagementSectionId, SectionsServiceSetup, SectionsServiceStart } from './types';
export class ManagementSectionsService {
private sections: Map<ManagementSectionId | string, ManagementSection> = new Map();
private getSection = (sectionId: ManagementSectionId | string) =>
this.sections.get(sectionId) as ManagementSection;
private getAllSections = () => [...this.sections.values()];
private registerSection = (section: RegisterManagementSectionArgs) => {
if (this.sections.has(section.id)) {
throw Error(`ManagementSection '${section.id}' already registered`);
}
const newSection = new ManagementSection(section);
this.sections.set(section.id, newSection);
return newSection;
};
setup(): SectionsServiceSetup {
managementSections.forEach(
({ id, title }: { id: ManagementSectionId; title: ReactElement }, idx: number) => {
this.registerSection({ id, title, order: idx });
}
);
return {
register: this.registerSection,
getSection: this.getSection,
};
}
start(): SectionsServiceStart {
return {
getSection: this.getSection,
getAllSections: this.getAllSections,
getSectionsEnabled: () => this.getAllSections().filter((section) => section.enabled),
};
}
}

View file

@ -1,40 +0,0 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { ManagementService } from './management_service';
import { ManagementSectionId } from './types';
import { coreMock } from '../../../core/public/mocks';
import { npSetup } from '../../../legacy/ui/public/new_platform/__mocks__';
jest.mock('ui/new_platform');
test('Provides default sections', () => {
const service = new ManagementService().setup(
npSetup.plugins.kibanaLegacy,
() => {},
coreMock.createSetup().getStartServices
);
expect(service.getAllSections().length).toEqual(6);
expect(service.getSection(ManagementSectionId.Ingest)).toBeDefined();
expect(service.getSection(ManagementSectionId.Data)).toBeDefined();
expect(service.getSection(ManagementSectionId.InsightsAndAlerting)).toBeDefined();
expect(service.getSection(ManagementSectionId.Security)).toBeDefined();
expect(service.getSection(ManagementSectionId.Kibana)).toBeDefined();
expect(service.getSection(ManagementSectionId.Stack)).toBeDefined();
});

View file

@ -1,109 +0,0 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { ReactElement } from 'react';
import { ManagementSection } from './management_section';
import { managementSections } from './management_sections';
import { KibanaLegacySetup } from '../../kibana_legacy/public';
// @ts-ignore
import { LegacyManagementSection, sections } from './legacy';
import { CreateSection, ManagementSectionId } from './types';
import { StartServicesAccessor, CoreStart } from '../../../core/public';
export class ManagementService {
private sections: ManagementSection[] = [];
private register(
registerLegacyApp: KibanaLegacySetup['registerLegacyApp'],
getLegacyManagement: () => LegacyManagementSection,
getStartServices: StartServicesAccessor
) {
return (section: CreateSection) => {
if (this.getSection(section.id)) {
throw Error(`ManagementSection '${section.id}' already registered`);
}
const newSection = new ManagementSection(
section,
this.getSectionsEnabled.bind(this),
registerLegacyApp,
getLegacyManagement,
getStartServices
);
this.sections.push(newSection);
return newSection;
};
}
private getSection(sectionId: ManagementSectionId) {
return this.sections.find((section) => section.id === sectionId);
}
private getAllSections() {
return this.sections;
}
private getSectionsEnabled() {
return this.sections
.filter((section) => section.getAppsEnabled().length > 0)
.sort((a, b) => a.order - b.order);
}
private sharedInterface = {
getSection: (sectionId: ManagementSectionId) => {
const section = this.getSection(sectionId);
if (!section) {
throw new Error(`Management section with id ${sectionId} is undefined`);
}
return section;
},
getSectionsEnabled: this.getSectionsEnabled.bind(this),
getAllSections: this.getAllSections.bind(this),
};
public setup(
kibanaLegacy: KibanaLegacySetup,
getLegacyManagement: () => LegacyManagementSection,
getStartServices: StartServicesAccessor
) {
const register = this.register.bind(this)(
kibanaLegacy.registerLegacyApp,
getLegacyManagement,
getStartServices
);
managementSections.forEach(
({ id, title }: { id: ManagementSectionId; title: ReactElement }, idx: number) => {
register({ id, title, order: idx });
}
);
return {
...this.sharedInterface,
};
}
public start(navigateToApp: CoreStart['application']['navigateToApp']) {
return {
navigateToApp, // apps are currently registered as top level apps but this may change in the future
...this.sharedInterface,
};
}
}

View file

@ -18,29 +18,29 @@
*/
import { ManagementSetup, ManagementStart } from '../types';
import { ManagementSection } from '../management_section';
import { ManagementSection } from '../index';
const createManagementSectionMock = (): jest.Mocked<PublicMethodsOf<ManagementSection>> => {
return {
const createManagementSectionMock = () =>
(({
disable: jest.fn(),
enable: jest.fn(),
registerApp: jest.fn(),
getApp: jest.fn(),
getAppsEnabled: jest.fn().mockReturnValue([]),
};
};
getEnabledItems: jest.fn().mockReturnValue([]),
} as unknown) as ManagementSection);
const createSetupContract = (): DeeplyMockedKeys<ManagementSetup> => ({
sections: {
register: jest.fn(),
getSection: jest.fn().mockReturnValue(createManagementSectionMock()),
getAllSections: jest.fn().mockReturnValue([]),
},
});
const createStartContract = (): DeeplyMockedKeys<ManagementStart> => ({
legacy: {},
sections: {
getSection: jest.fn(),
getAllSections: jest.fn(),
navigateToApp: jest.fn(),
getSectionsEnabled: jest.fn(),
},
});

View file

@ -18,23 +18,30 @@
*/
import { i18n } from '@kbn/i18n';
import { CoreSetup, CoreStart, Plugin } from 'kibana/public';
import { ManagementSetup, ManagementStart } from './types';
import { ManagementService } from './management_service';
import { KibanaLegacySetup } from '../../kibana_legacy/public';
import { FeatureCatalogueCategory, HomePublicPluginSetup } from '../../home/public';
// @ts-ignore
import { LegacyManagementAdapter } from './legacy';
import { showLegacyRedirectMessages } from './legacy/redirect_messages';
import {
CoreSetup,
CoreStart,
Plugin,
DEFAULT_APP_CATEGORIES,
PluginInitializerContext,
} from '../../../core/public';
import { ManagementSectionsService } from './management_sections_service';
interface ManagementSetupDependencies {
home: HomePublicPluginSetup;
}
export class ManagementPlugin implements Plugin<ManagementSetup, ManagementStart> {
private managementSections = new ManagementService();
private legacyManagement = new LegacyManagementAdapter();
private readonly managementSections = new ManagementSectionsService();
constructor(private initializerContext: PluginInitializerContext) {}
public setup(core: CoreSetup, { home }: ManagementSetupDependencies) {
const kibanaVersion = this.initializerContext.env.packageInfo.version;
public setup(
core: CoreSetup,
{ kibanaLegacy, home }: { kibanaLegacy: KibanaLegacySetup; home: HomePublicPluginSetup }
) {
home.featureCatalogue.register({
id: 'stack-management',
title: i18n.translate('management.stackManagement.managementLabel', {
@ -44,25 +51,38 @@ export class ManagementPlugin implements Plugin<ManagementSetup, ManagementStart
defaultMessage: 'Your center console for managing the Elastic Stack.',
}),
icon: 'managementApp',
path: '/app/kibana#/management',
path: '/app/management',
showOnHomePage: false,
category: FeatureCatalogueCategory.ADMIN,
});
core.application.register({
id: 'management',
title: i18n.translate('management.stackManagement.title', {
defaultMessage: 'Stack Management',
}),
order: 9003,
euiIconType: 'managementApp',
category: DEFAULT_APP_CATEGORIES.management,
async mount(context, params) {
const { renderApp } = await import('./application');
const selfStart = (await core.getStartServices())[2] as ManagementStart;
return renderApp(context, params, {
kibanaVersion,
management: selfStart,
});
},
});
return {
sections: this.managementSections.setup(
kibanaLegacy,
this.legacyManagement.getManagement,
core.getStartServices
),
sections: this.managementSections.setup(),
};
}
public start(core: CoreStart) {
showLegacyRedirectMessages(core.notifications, core.overlays);
return {
sections: this.managementSections.start(core.application.navigateToApp),
legacy: this.legacyManagement.init(core.application.capabilities),
sections: this.managementSections.start(),
};
}
}

View file

@ -17,30 +17,10 @@
* under the License.
*/
/*
* 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 { ReactElement } from 'react';
import { IconType } from '@elastic/eui';
import { ManagementApp } from './management_app';
import { ManagementSection } from './management_section';
import { ChromeBreadcrumb, ApplicationStart } from '../../../core/public/';
import { ScopedHistory } from 'kibana/public';
import { ManagementSection, RegisterManagementSectionArgs } from './utils';
import { ChromeBreadcrumb } from '../../../core/public/';
export interface ManagementSetup {
sections: SectionsServiceSetup;
@ -48,7 +28,17 @@ export interface ManagementSetup {
export interface ManagementStart {
sections: SectionsServiceStart;
legacy: any;
}
export interface SectionsServiceSetup {
register: (args: RegisterManagementSectionArgs) => ManagementSection;
getSection: (sectionId: ManagementSectionId | string) => ManagementSection;
}
export interface SectionsServiceStart {
getSection: (sectionId: ManagementSectionId | string) => ManagementSection;
getAllSections: () => ManagementSection[];
getSectionsEnabled: () => ManagementSection[];
}
export enum ManagementSectionId {
@ -60,67 +50,20 @@ export enum ManagementSectionId {
Stack = 'stack',
}
interface SectionsServiceSetup {
getSection: (sectionId: ManagementSectionId) => ManagementSection;
getAllSections: () => ManagementSection[];
}
interface SectionsServiceStart {
getSection: (sectionId: ManagementSectionId) => ManagementSection;
getAllSections: () => ManagementSection[];
navigateToApp: ApplicationStart['navigateToApp'];
}
export interface CreateSection {
id: ManagementSectionId;
title: string | ReactElement;
order?: number;
euiIconType?: string; // takes precedence over `icon` property.
icon?: string; // URL to image file; fallback if no `euiIconType`
}
export type RegisterSection = (section: CreateSection) => ManagementSection;
export interface RegisterManagementAppArgs {
id: string;
title: string;
mount: ManagementSectionMount;
order?: number;
}
export type RegisterManagementApp = (managementApp: RegisterManagementAppArgs) => ManagementApp;
export type Unmount = () => Promise<void> | void;
export type Mount = (params: ManagementAppMountParams) => Unmount | Promise<Unmount>;
export interface ManagementAppMountParams {
basePath: string; // base path for setting up your router
element: HTMLElement; // element the section should render into
setBreadcrumbs: (crumbs: ChromeBreadcrumb[]) => void;
history: ScopedHistory;
}
export type ManagementSectionMount = (
params: ManagementAppMountParams
) => Unmount | Promise<Unmount>;
export interface CreateManagementApp {
export interface CreateManagementItemArgs {
id: string;
title: string;
basePath: string;
title: string | ReactElement;
order?: number;
mount: ManagementSectionMount;
}
export interface LegacySection extends LegacyApp {
visibleItems: LegacyApp[];
}
export interface LegacyApp {
disabled: boolean;
visible: boolean;
id: string;
display: string;
url?: string;
euiIconType?: IconType;
icon?: string;
order: number;
euiIconType?: string; // takes precedence over `icon` property.
icon?: string; // URL to image file; fallback if no `euiIconType`
}

View file

@ -19,9 +19,9 @@
import { i18n } from '@kbn/i18n';
export const MANAGEMENT_BREADCRUMB = Object.freeze({
text: i18n.translate('common.ui.stackManagement.breadcrumb', {
export const MANAGEMENT_BREADCRUMB = {
text: i18n.translate('management.breadcrumb', {
defaultMessage: 'Stack Management',
}),
href: '#/management',
});
href: '/',
};

View file

@ -18,5 +18,5 @@
*/
export { MANAGEMENT_BREADCRUMB } from './breadcrumbs';
import { npStart } from 'ui/new_platform';
export const management = npStart.plugins.management.legacy;
export { ManagementApp, RegisterManagementAppArgs } from './management_app';
export { ManagementSection, RegisterManagementSectionArgs } from './management_section';

View file

@ -17,18 +17,22 @@
* under the License.
*/
export interface IndexPatternCreationOption {
text: string;
description?: string;
onClick: () => void;
import { CreateManagementItemArgs, Mount } from '../types';
import { ManagementItem } from './management_item';
export interface RegisterManagementAppArgs extends CreateManagementItemArgs {
mount: Mount;
basePath: string;
}
export interface IndexPattern {
id: string;
title: string;
url: string;
active: boolean;
default: boolean;
tag?: string[];
sort: string;
export class ManagementApp extends ManagementItem {
public readonly mount: Mount;
public readonly basePath: string;
constructor(args: RegisterManagementAppArgs) {
super(args);
this.mount = args.mount;
this.basePath = args.basePath;
}
}

View file

@ -0,0 +1,46 @@
/*
* 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 { ReactElement } from 'react';
import { CreateManagementItemArgs } from '../types';
export class ManagementItem {
public readonly id: string = '';
public readonly title: string | ReactElement = '';
public readonly order: number;
public readonly euiIconType?: string;
public readonly icon?: string;
public enabled: boolean = true;
constructor({ id, title, order = 100, euiIconType, icon }: CreateManagementItemArgs) {
this.id = id;
this.title = title;
this.order = order;
this.euiIconType = euiIconType;
this.icon = icon;
}
disable() {
this.enabled = false;
}
enable() {
this.enabled = true;
}
}

View file

@ -0,0 +1,55 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { ManagementSection, RegisterManagementSectionArgs } from './management_section';
describe('ManagementSection', () => {
const createSection = (
config: RegisterManagementSectionArgs = {
id: 'test-section',
title: 'Test Section',
} as RegisterManagementSectionArgs
) => new ManagementSection(config);
test('cannot register two apps with the same id', () => {
const section = createSection();
const testAppConfig = { id: 'test-app', title: 'Test App', mount: () => () => {} };
section.registerApp(testAppConfig);
expect(section.apps.length).toEqual(1);
expect(() => {
section.registerApp(testAppConfig);
}).toThrow();
});
test('can enable and disable apps', () => {
const section = createSection();
const testAppConfig = { id: 'test-app', title: 'Test App', mount: () => () => {} };
const app = section.registerApp(testAppConfig);
expect(section.getAppsEnabled().length).toEqual(1);
app.disable();
expect(section.getAppsEnabled().length).toEqual(0);
});
});

View file

@ -0,0 +1,58 @@
/*
* 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 { Assign } from '@kbn/utility-types';
import { CreateManagementItemArgs, ManagementSectionId } from '../types';
import { ManagementItem } from './management_item';
import { ManagementApp, RegisterManagementAppArgs } from './management_app';
export type RegisterManagementSectionArgs = Assign<
CreateManagementItemArgs,
{ id: ManagementSectionId | string }
>;
export class ManagementSection extends ManagementItem {
public readonly apps: ManagementApp[] = [];
constructor(args: RegisterManagementSectionArgs) {
super(args);
}
registerApp(args: Omit<RegisterManagementAppArgs, 'basePath'>) {
if (this.getApp(args.id)) {
throw new Error(`Management app already registered - id: ${args.id}, title: ${args.title}`);
}
const app = new ManagementApp({
...args,
basePath: `/${this.id}/${args.id}`,
});
this.apps.push(app);
return app;
}
getApp(id: ManagementApp['id']) {
return this.apps.find((app) => app.id === id);
}
getAppsEnabled() {
return this.apps.filter((app) => app.enabled);
}
}

View file

@ -19,10 +19,10 @@
import React, { lazy, Suspense } from 'react';
import ReactDOM from 'react-dom';
import { HashRouter, Switch, Route } from 'react-router-dom';
import { Router, Switch, Route } from 'react-router-dom';
import { I18nProvider } from '@kbn/i18n/react';
import { EuiLoadingSpinner } from '@elastic/eui';
import { CoreSetup, Capabilities } from 'src/core/public';
import { CoreSetup } from 'src/core/public';
import { ManagementAppMountParams } from '../../../management/public';
import { StartDependencies, SavedObjectsManagementPluginStart } from '../plugin';
import { ISavedObjectsManagementServiceRegistry } from '../services';
@ -44,30 +44,41 @@ export const mountManagementSection = async ({
serviceRegistry,
}: MountParams) => {
const [coreStart, { data }, pluginStart] = await core.getStartServices();
const { element, basePath, setBreadcrumbs } = mountParams;
const { element, history, setBreadcrumbs } = mountParams;
if (allowedObjectTypes === undefined) {
allowedObjectTypes = await getAllowedTypes(coreStart.http);
}
const capabilities = coreStart.application.capabilities;
const RedirectToHomeIfUnauthorized: React.FunctionComponent = ({ children }) => {
const allowed = capabilities?.management?.kibana?.objects ?? false;
if (!allowed) {
coreStart.application.navigateToApp('home');
return null;
}
return children! as React.ReactElement;
};
ReactDOM.render(
<I18nProvider>
<HashRouter basename={basePath}>
<Router history={history}>
<Switch>
<Route path={'/:service/:id'} exact={true}>
<RedirectToHomeIfUnauthorized capabilities={capabilities}>
<RedirectToHomeIfUnauthorized>
<Suspense fallback={<EuiLoadingSpinner />}>
<SavedObjectsEditionPage
coreStart={coreStart}
serviceRegistry={serviceRegistry}
setBreadcrumbs={setBreadcrumbs}
history={history}
/>
</Suspense>
</RedirectToHomeIfUnauthorized>
</Route>
<Route path={'/'} exact={false}>
<RedirectToHomeIfUnauthorized capabilities={capabilities}>
<RedirectToHomeIfUnauthorized>
<Suspense fallback={<EuiLoadingSpinner />}>
<SavedObjectsTablePage
coreStart={coreStart}
@ -81,7 +92,7 @@ export const mountManagementSection = async ({
</RedirectToHomeIfUnauthorized>
</Route>
</Switch>
</HashRouter>
</Router>
</I18nProvider>,
element
);
@ -90,14 +101,3 @@ export const mountManagementSection = async ({
ReactDOM.unmountComponentAtNode(element);
};
};
const RedirectToHomeIfUnauthorized: React.FunctionComponent<{
capabilities: Capabilities;
}> = ({ children, capabilities }) => {
const allowed = capabilities?.management?.kibana?.objects ?? false;
if (!allowed) {
window.location.hash = '/home';
return null;
}
return children! as React.ReactElement;
};

View file

@ -26,6 +26,7 @@ import {
OverlayStart,
NotificationsStart,
SimpleSavedObject,
ScopedHistory,
} from '../../../../../core/public';
import { ISavedObjectsManagementServiceRegistry } from '../../services';
import { Header, NotFoundErrors, Intro, Form } from './components';
@ -41,6 +42,7 @@ interface SavedObjectEditionProps {
notifications: NotificationsStart;
notFoundType?: string;
savedObjectsClient: SavedObjectsClientContract;
history: ScopedHistory;
}
interface SavedObjectEditionState {
@ -171,6 +173,6 @@ export class SavedObjectEdition extends Component<
};
redirectToListing() {
window.location.hash = '/management/kibana/objects';
this.props.history.push('/');
}
}

View file

@ -314,7 +314,7 @@ exports[`SavedObjectsTable relationships should show the flyout 1`] = `
Object {
"id": "2",
"meta": Object {
"editUrl": "#/management/kibana/objects/savedSearches/2",
"editUrl": "/management/kibana/objects/savedSearches/2",
"icon": "search",
"inAppUrl": Object {
"path": "/discover/2",
@ -404,7 +404,7 @@ exports[`SavedObjectsTable should render normally 1`] = `
Object {
"id": "2",
"meta": Object {
"editUrl": "#/management/kibana/objects/savedSearches/2",
"editUrl": "/management/kibana/objects/savedSearches/2",
"icon": "search",
"inAppUrl": Object {
"path": "/discover/2",
@ -417,7 +417,7 @@ exports[`SavedObjectsTable should render normally 1`] = `
Object {
"id": "3",
"meta": Object {
"editUrl": "#/management/kibana/objects/savedDashboards/3",
"editUrl": "/management/kibana/objects/savedDashboards/3",
"icon": "dashboardApp",
"inAppUrl": Object {
"path": "/dashboard/3",
@ -430,7 +430,7 @@ exports[`SavedObjectsTable should render normally 1`] = `
Object {
"id": "4",
"meta": Object {
"editUrl": "#/management/kibana/objects/savedVisualizations/4",
"editUrl": "/management/kibana/objects/savedVisualizations/4",
"icon": "visualizeApp",
"inAppUrl": Object {
"path": "/edit/4",

View file

@ -455,7 +455,7 @@ exports[`Relationships should render searches normally 1`] = `
"editUrl": "/management/kibana/indexPatterns/patterns/1",
"icon": "indexPatternApp",
"inAppUrl": Object {
"path": "/app/kibana#/management/kibana/indexPatterns/patterns/1",
"path": "/app/management/kibana/indexPatterns/patterns/1",
"uiCapabilitiesPath": "management.kibana.index_patterns",
},
"title": "My Index Pattern",

View file

@ -112,7 +112,7 @@ describe('Relationships', () => {
editUrl: '/management/kibana/indexPatterns/patterns/1',
icon: 'indexPatternApp',
inAppUrl: {
path: '/app/kibana#/management/kibana/indexPatterns/patterns/1',
path: '/app/management/kibana/indexPatterns/patterns/1',
uiCapabilitiesPath: 'management.kibana.index_patterns',
},
title: 'My Index Pattern',
@ -141,7 +141,7 @@ describe('Relationships', () => {
meta: {
title: 'MySearch',
icon: 'search',
editUrl: '#/management/kibana/objects/savedSearches/1',
editUrl: '/management/kibana/objects/savedSearches/1',
inAppUrl: {
path: '/discover/1',
uiCapabilitiesPath: 'discover.show',
@ -208,7 +208,7 @@ describe('Relationships', () => {
meta: {
title: 'MyViz',
icon: 'visualizeApp',
editUrl: '#/management/kibana/objects/savedVisualizations/1',
editUrl: '/management/kibana/objects/savedVisualizations/1',
inAppUrl: {
path: '/edit/1',
uiCapabilitiesPath: 'visualize.show',
@ -275,7 +275,7 @@ describe('Relationships', () => {
meta: {
title: 'MyDashboard',
icon: 'dashboardApp',
editUrl: '#/management/kibana/objects/savedDashboards/1',
editUrl: '/management/kibana/objects/savedDashboards/1',
inAppUrl: {
path: '/dashboard/1',
uiCapabilitiesPath: 'dashboard.show',
@ -315,7 +315,7 @@ describe('Relationships', () => {
meta: {
title: 'MyDashboard',
icon: 'dashboardApp',
editUrl: '#/management/kibana/objects/savedDashboards/1',
editUrl: '/management/kibana/objects/savedDashboards/1',
inAppUrl: {
path: '/dashboard/1',
uiCapabilitiesPath: 'dashboard.show',

View file

@ -168,7 +168,7 @@ describe('SavedObjectsTable', () => {
meta: {
title: `MySearch`,
icon: 'search',
editUrl: '#/management/kibana/objects/savedSearches/2',
editUrl: '/management/kibana/objects/savedSearches/2',
inAppUrl: {
path: '/discover/2',
uiCapabilitiesPath: 'discover.show',
@ -181,7 +181,7 @@ describe('SavedObjectsTable', () => {
meta: {
title: `MyDashboard`,
icon: 'dashboardApp',
editUrl: '#/management/kibana/objects/savedDashboards/3',
editUrl: '/management/kibana/objects/savedDashboards/3',
inAppUrl: {
path: '/dashboard/3',
uiCapabilitiesPath: 'dashboard.show',
@ -194,7 +194,7 @@ describe('SavedObjectsTable', () => {
meta: {
title: `MyViz`,
icon: 'visualizeApp',
editUrl: '#/management/kibana/objects/savedVisualizations/4',
editUrl: '/management/kibana/objects/savedVisualizations/4',
inAppUrl: {
path: '/edit/4',
uiCapabilitiesPath: 'visualize.show',
@ -441,7 +441,7 @@ describe('SavedObjectsTable', () => {
meta: {
title: `MySearch`,
icon: 'search',
editUrl: '#/management/kibana/objects/savedSearches/2',
editUrl: '/management/kibana/objects/savedSearches/2',
inAppUrl: {
path: '/discover/2',
uiCapabilitiesPath: 'discover.show',
@ -456,7 +456,7 @@ describe('SavedObjectsTable', () => {
type: 'search',
meta: {
title: 'MySearch',
editUrl: '#/management/kibana/objects/savedSearches/2',
editUrl: '/management/kibana/objects/savedSearches/2',
icon: 'search',
inAppUrl: {
path: '/discover/2',

View file

@ -457,8 +457,8 @@ export class SavedObjectsTable extends Component<SavedObjectsTableProps, SavedOb
return null;
}
const { applications } = this.props;
const newIndexPatternUrl = applications.getUrlForApp('kibana', {
path: '#/management/kibana/indexPattern',
const newIndexPatternUrl = applications.getUrlForApp('management', {
path: 'kibana/indexPatterns',
});
return (

View file

@ -21,7 +21,7 @@ import React, { useEffect } from 'react';
import { useParams, useLocation } from 'react-router-dom';
import { parse } from 'query-string';
import { i18n } from '@kbn/i18n';
import { CoreStart, ChromeBreadcrumb } from 'src/core/public';
import { CoreStart, ChromeBreadcrumb, ScopedHistory } from 'src/core/public';
import { ISavedObjectsManagementServiceRegistry } from '../services';
import { SavedObjectEdition } from './object_view';
@ -29,10 +29,12 @@ const SavedObjectsEditionPage = ({
coreStart,
serviceRegistry,
setBreadcrumbs,
history,
}: {
coreStart: CoreStart;
serviceRegistry: ISavedObjectsManagementServiceRegistry;
setBreadcrumbs: (crumbs: ChromeBreadcrumb[]) => void;
history: ScopedHistory;
}) => {
const { service: serviceName, id } = useParams<{ service: string; id: string }>();
const capabilities = coreStart.application.capabilities;
@ -47,7 +49,7 @@ const SavedObjectsEditionPage = ({
text: i18n.translate('savedObjectsManagement.breadcrumb.index', {
defaultMessage: 'Saved objects',
}),
href: '#/management/kibana/objects',
href: '/',
},
{
text: i18n.translate('savedObjectsManagement.breadcrumb.edit', {
@ -68,6 +70,7 @@ const SavedObjectsEditionPage = ({
notifications={coreStart.notifications}
capabilities={capabilities}
notFoundType={query.notFound as string}
history={history}
/>
);
};

View file

@ -52,7 +52,7 @@ const SavedObjectsTablePage = ({
text: i18n.translate('savedObjectsManagement.breadcrumb.index', {
defaultMessage: 'Saved objects',
}),
href: '#/management/kibana/objects',
href: '/',
},
]);
}, [setBreadcrumbs]);
@ -73,11 +73,7 @@ const SavedObjectsTablePage = ({
goInspectObject={(savedObject) => {
const { editUrl } = savedObject.meta;
if (editUrl) {
// previously, kbnUrl.change(object.meta.editUrl); was used.
// using direct access to location.hash seems the only option for now,
// as using react-router-dom will prefix the url with the router's basename
// which should be ignored there.
window.location.hash = editUrl;
return coreStart.application.navigateToUrl('/app' + editUrl);
}
}}
canGoInApp={(savedObject) => {

View file

@ -82,7 +82,7 @@ export class SavedObjectsManagementPlugin
'Import, export, and manage your saved searches, visualizations, and dashboards.',
}),
icon: 'savedObjectsApp',
path: '/app/kibana#/management/kibana/objects',
path: '/app/management/kibana/objects',
showOnHomePage: true,
category: FeatureCatalogueCategory.ADMIN,
});

View file

@ -49,7 +49,7 @@ export const LOCALSTORAGE_KEY = 'telemetry.data';
/**
* Link to Advanced Settings.
*/
export const PATH_TO_ADVANCED_SETTINGS = 'kibana#/management/kibana/settings';
export const PATH_TO_ADVANCED_SETTINGS = 'management/kibana/settings';
/**
* Link to the Elastic Telemetry privacy statement.

View file

@ -10,7 +10,7 @@ exports[`OptInDetailsComponent renders as expected 1`] = `
values={
Object {
"disableLink": <ForwardRef
href="kibana#/management/kibana/settings"
href="management/kibana/settings"
onClick={[Function]}
>
<FormattedMessage

View file

@ -211,22 +211,16 @@ export function initVisualizeApp(app, deps) {
mapping: {
visualization: VisualizeConstants.LANDING_PAGE_PATH,
search: {
app: 'kibana',
path:
'#/management/kibana/objects/savedVisualizations/' +
$route.current.params.id,
app: 'management',
path: 'kibana/objects/savedVisualizations/' + $route.current.params.id,
},
'index-pattern': {
app: 'kibana',
path:
'#/management/kibana/objects/savedVisualizations/' +
$route.current.params.id,
app: 'management',
path: 'kibana/objects/savedVisualizations/' + $route.current.params.id,
},
'index-pattern-field': {
app: 'kibana',
path:
'#/management/kibana/objects/savedVisualizations/' +
$route.current.params.id,
app: 'management',
path: 'kibana/objects/savedVisualizations/' + $route.current.params.id,
},
},
toastNotifications,

View file

@ -285,7 +285,7 @@ export default function ({ getService }: FtrProviderContext) {
'/management/kibana/indexPatterns/patterns/8963ca30-3224-11e8-a572-ffca06da1357',
inAppUrl: {
path:
'/app/kibana#/management/kibana/indexPatterns/patterns/8963ca30-3224-11e8-a572-ffca06da1357',
'/app/management/kibana/indexPatterns/patterns/8963ca30-3224-11e8-a572-ffca06da1357',
uiCapabilitiesPath: 'management.kibana.index_patterns',
},
});

View file

@ -86,7 +86,7 @@ export default function ({ getService }: FtrProviderContext) {
'/management/kibana/indexPatterns/patterns/8963ca30-3224-11e8-a572-ffca06da1357',
inAppUrl: {
path:
'/app/kibana#/management/kibana/indexPatterns/patterns/8963ca30-3224-11e8-a572-ffca06da1357',
'/app/management/kibana/indexPatterns/patterns/8963ca30-3224-11e8-a572-ffca06da1357',
uiCapabilitiesPath: 'management.kibana.index_patterns',
},
},
@ -127,7 +127,7 @@ export default function ({ getService }: FtrProviderContext) {
'/management/kibana/indexPatterns/patterns/8963ca30-3224-11e8-a572-ffca06da1357',
inAppUrl: {
path:
'/app/kibana#/management/kibana/indexPatterns/patterns/8963ca30-3224-11e8-a572-ffca06da1357',
'/app/management/kibana/indexPatterns/patterns/8963ca30-3224-11e8-a572-ffca06da1357',
uiCapabilitiesPath: 'management.kibana.index_patterns',
},
},

View file

@ -71,7 +71,7 @@ export async function createTestUserService(
}
}
async setRoles(roles: string[]) {
async setRoles(roles: string[], shouldRefreshBrowser: boolean = true) {
if (isEnabled()) {
log.debug(`set roles = ${roles}`);
await user.create('test_user', {
@ -80,7 +80,7 @@ export async function createTestUserService(
full_name: 'test user',
});
if (browser && testSubjects) {
if (browser && testSubjects && shouldRefreshBrowser) {
if (await testSubjects.exists('kibanaChrome', { allowHidden: true })) {
await browser.refresh();
await testSubjects.find('kibanaChrome', config.get('timeouts.find') * 10);

View file

@ -82,9 +82,12 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
let objects = await PageObjects.settings.getSavedObjectsInTable();
expect(objects.includes('A Dashboard')).to.be(true);
await PageObjects.common.navigateToActualUrl(
'kibana',
'/management/kibana/objects/savedDashboards/i-exist'
await PageObjects.common.navigateToUrl(
'management',
'kibana/objects/savedDashboards/i-exist',
{
shouldUseHashForSubUrl: false,
}
);
await testSubjects.existOrFail('savedObjectEditSave');
@ -100,9 +103,12 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
expect(objects.includes('A Dashboard')).to.be(false);
expect(objects.includes('Edited Dashboard')).to.be(true);
await PageObjects.common.navigateToActualUrl(
'kibana',
'/management/kibana/objects/savedDashboards/i-exist'
await PageObjects.common.navigateToUrl(
'management',
'kibana/objects/savedDashboards/i-exist',
{
shouldUseHashForSubUrl: false,
}
);
expect(await getFieldValue('title')).to.eql('Edited Dashboard');
@ -110,9 +116,12 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
});
it('allows to delete a saved object', async () => {
await PageObjects.common.navigateToActualUrl(
'kibana',
'/management/kibana/objects/savedDashboards/i-exist'
await PageObjects.common.navigateToUrl(
'management',
'kibana/objects/savedDashboards/i-exist',
{
shouldUseHashForSubUrl: false,
}
);
await focusAndClickButton('savedObjectEditDelete');
@ -124,7 +133,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
it('preserves the object references when saving', async () => {
const testVisualizationUrl =
'/management/kibana/objects/savedVisualizations/75c3e060-1e7c-11e9-8488-65449e65d0ed';
'kibana/objects/savedVisualizations/75c3e060-1e7c-11e9-8488-65449e65d0ed';
const visualizationRefs = [
{
name: 'kibanaSavedObjectMeta.searchSourceJSON.index',
@ -139,7 +148,9 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
const objects = await PageObjects.settings.getSavedObjectsInTable();
expect(objects.includes('A Pie')).to.be(true);
await PageObjects.common.navigateToActualUrl('kibana', testVisualizationUrl);
await PageObjects.common.navigateToUrl('management', testVisualizationUrl, {
shouldUseHashForSubUrl: false,
});
await testSubjects.existOrFail('savedObjectEditSave');
@ -151,7 +162,9 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
await PageObjects.settings.getSavedObjectsInTable();
await PageObjects.common.navigateToActualUrl('kibana', testVisualizationUrl);
await PageObjects.common.navigateToUrl('management', testVisualizationUrl, {
shouldUseHashForSubUrl: false,
});
// Parsing to avoid random keys ordering issues in raw string comparison
expect(JSON.parse(await getAceEditorFieldValue('references'))).to.eql(visualizationRefs);
@ -162,7 +175,9 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
await PageObjects.settings.getSavedObjectsInTable();
await PageObjects.common.navigateToActualUrl('kibana', testVisualizationUrl);
await PageObjects.common.navigateToUrl('management', testVisualizationUrl, {
shouldUseHashForSubUrl: false,
});
displayedReferencesValue = await getAceEditorFieldValue('references');

View file

@ -83,9 +83,12 @@ export default async function ({ readConfigFile }) {
pathname: '/app/dashboards',
hash: '/list',
},
management: {
pathname: '/app/management',
},
/** @obsolete "management" should be instead of "settings" **/
settings: {
pathname: '/app/kibana',
hash: '/management',
pathname: '/app/management',
},
timelion: {
pathname: '/app/timelion',

View file

@ -147,13 +147,19 @@ export function CommonPageProvider({ getService, getPageObjects }: FtrProviderCo
shouldLoginIfPrompted = true,
useActualUrl = false,
insertTimestamp = true,
shouldUseHashForSubUrl = true,
} = {}
) {
const appConfig = {
const appConfig: { pathname: string; hash?: string } = {
pathname: `${basePath}${config.get(['apps', appName]).pathname}`,
hash: useActualUrl ? subUrl : `/${appName}/${subUrl}`,
};
if (shouldUseHashForSubUrl) {
appConfig.hash = useActualUrl ? subUrl : `/${appName}/${subUrl}`;
} else {
appConfig.pathname += `/${subUrl}`;
}
await this.navigate({
appConfig,
ensureCurrentUrl,

View file

@ -300,7 +300,9 @@ export function SettingsPageProvider({ getService, getPageObjects }: FtrProvider
async getIndexPatternList() {
await testSubjects.existOrFail('indexPatternTable', { timeout: 5000 });
return await find.allByCssSelector('[data-test-subj="indexPatternTable"] .euiTable a');
return await find.allByCssSelector(
'[data-test-subj="indexPatternTable"] .euiTable .euiTableRow'
);
}
async isIndexPatternListEmpty() {

View file

@ -19,7 +19,7 @@
import * as React from 'react';
import ReactDOM from 'react-dom';
import { HashRouter as Router, Switch, Route, Link } from 'react-router-dom';
import { Router, Switch, Route, Link } from 'react-router-dom';
import { CoreSetup, Plugin } from 'kibana/public';
import { ManagementSetup, ManagementSectionId } from '../../../../../src/plugins/management/public';
@ -34,19 +34,19 @@ export class ManagementTestPlugin
mount(params: any) {
params.setBreadcrumbs([{ text: 'Management Test' }]);
ReactDOM.render(
<Router>
<Router history={params.history}>
<h1 data-test-subj="test-management-header">Hello from management test plugin</h1>
<Switch>
<Route exact path={`${params.basePath}`}>
<Link to={`${params.basePath}/one`} data-test-subj="test-management-link-one">
Link to /one
</Link>
</Route>
<Route path={`${params.basePath}/one`}>
<Route path={'/one'}>
<Link to={`${params.basePath}`} data-test-subj="test-management-link-basepath">
Link to basePath
</Link>
</Route>
<Route path={'/'}>
<Link to={'/one'} data-test-subj="test-management-link-one">
Link to /one
</Link>
</Route>
</Switch>
</Router>,
params.element

Some files were not shown because too many files have changed in this diff Show more