[Deployment Management] Add cards navigation in management landing page for serverless (#160096)

This commit is contained in:
Ignacio Rivas 2023-06-30 11:27:56 +02:00 committed by GitHub
parent 3382061db4
commit ec620e7fb3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
47 changed files with 1097 additions and 50 deletions

View file

@ -35,6 +35,7 @@ const STORYBOOKS = [
'expression_reveal_image',
'expression_shape',
'expression_tagcloud',
'management',
'fleet',
'grouping',
'home',

2
.github/CODEOWNERS vendored
View file

@ -471,7 +471,9 @@ packages/kbn-logging-mocks @elastic/kibana-core
x-pack/plugins/logstash @elastic/logstash
packages/kbn-managed-vscode-config @elastic/kibana-operations
packages/kbn-managed-vscode-config-cli @elastic/kibana-operations
packages/kbn-management/cards_navigation @elastic/platform-deployment-management
src/plugins/management @elastic/platform-deployment-management
packages/kbn-management/storybook/config @elastic/platform-deployment-management
test/plugin_functional/plugins/management_test_plugin @elastic/kibana-app-services
packages/kbn-mapbox-gl @elastic/kibana-gis
x-pack/examples/third_party_maps_source_example @elastic/kibana-gis

View file

@ -71,7 +71,7 @@
"kibanaOverview": "src/plugins/kibana_overview",
"lists": "packages/kbn-securitysolution-list-utils/src",
"exceptionList-components": "packages/kbn-securitysolution-exception-list-components/src",
"management": ["src/legacy/core_plugins/management", "src/plugins/management"],
"management": ["src/legacy/core_plugins/management", "src/plugins/management", "packages/kbn-management"],
"monaco": "packages/kbn-monaco/src",
"navigation": "src/plugins/navigation",
"newsfeed": "src/plugins/newsfeed",

View file

@ -236,7 +236,7 @@ in Kibana, e.g. visualizations. It has the form of a flyout panel.
|{kib-repo}blob/{branch}/src/plugins/management/README.md[management]
|This plugins contains the "Stack Management" page framework. It offers navigation and an API
to link individual managment section into it. This plugin does not contain any individual
to link individual management section into it. This plugin does not contain any individual
management section itself.

View file

@ -488,6 +488,7 @@
"@kbn/logging": "link:packages/kbn-logging",
"@kbn/logging-mocks": "link:packages/kbn-logging-mocks",
"@kbn/logstash-plugin": "link:x-pack/plugins/logstash",
"@kbn/management-cards-navigation": "link:packages/kbn-management/cards_navigation",
"@kbn/management-plugin": "link:src/plugins/management",
"@kbn/management-test-plugin": "link:test/plugin_functional/plugins/management_test_plugin",
"@kbn/mapbox-gl": "link:packages/kbn-mapbox-gl",
@ -1143,6 +1144,7 @@
"@kbn/lint-ts-projects-cli": "link:packages/kbn-lint-ts-projects-cli",
"@kbn/managed-vscode-config": "link:packages/kbn-managed-vscode-config",
"@kbn/managed-vscode-config-cli": "link:packages/kbn-managed-vscode-config-cli",
"@kbn/management-storybook-config": "link:packages/kbn-management/storybook/config",
"@kbn/optimizer": "link:packages/kbn-optimizer",
"@kbn/optimizer-webpack-helpers": "link:packages/kbn-optimizer-webpack-helpers",
"@kbn/peggy": "link:packages/kbn-peggy",

View file

@ -0,0 +1,41 @@
---
id: kbn-management/components/CardsNavigation
slug: /kbn-management/components/cards_navigation
title: Cards Navigation
description: A component that allows the users to navigate to other management apps
tags: ['management', 'component']
date: 2023-04-23
---
This component is simply in charge of rendering a list of links to other management apps. It also
makes sure that the apps are enabled before doing so and it also aggregates them into predefined
categories.
### Adding new items to the navigation
For adding a new item to the navigation all you have to do is edit the `cards_navigation/src/consts.tsx`
file and add two things:
* Add the app id into the `appIds` enum (make sure that the app_id value matches the one from the plugin)
* Add a new entry to the `appDefinitions` object. In here you can specify the category where you want it to be, icon and description.
### Removing an item from the navigation
If an item needs to be hidden from the navigation you can specify that by using the `hideLinksTo` prop:
```typescript
<CardsNavigation
sections={sections}
appBasePath={appBasePath}
hideLinksTo={[ appIds.RULES, appIds.TAGS ]}
/>
```
In case an app needs to be removed all together from all the solutions you can remove its
definition from the `consts.tsx` file. The app might still be visible in the side nav, so if you
want to remove all links to it from management but without disabling the plugin you will have
to remove it from the side nav too.
Bare in mind that if the app is disabled then it will be hidden anyway from the cards navigation
and from the sidenav.

View file

@ -0,0 +1,12 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
export type { AppId, CardsNavigationComponentProps } from './src';
export { appIds } from './src';
export { CardsNavigation } from './src';

View file

@ -0,0 +1,13 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
module.exports = {
preset: '@kbn/test',
rootDir: '../../..',
roots: ['<rootDir>/packages/kbn-management/cards_navigation'],
};

View file

@ -0,0 +1,5 @@
{
"type": "shared-common",
"id": "@kbn/management-cards-navigation",
"owner": "@elastic/platform-deployment-management"
}

View file

@ -0,0 +1,90 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
export const APP_BASE_PATH = 'http://localhost:9001';
export const sectionsMock = [
{
id: 'data',
title: 'Data',
apps: [
{
id: 'ingest_pipelines',
title: 'Ingest pipelines',
enabled: true,
basePath: '/app/management/ingest/pipelines',
},
{
id: 'pipelines',
title: 'Pipelines',
enabled: true,
basePath: '/app/management/ingest/pipelines_logstash',
},
{
id: 'index_management',
title: 'Index Management',
enabled: true,
basePath: '/app/management/ingest/pipelines_logstash',
},
{
id: 'transform',
title: 'Transforms',
enabled: true,
basePath: '/app/management/ingest/pipelines_logstash',
},
{
id: 'jobsListLink',
title: 'Machine Learning',
enabled: true,
basePath: '/app/management/ingest/pipelines_logstash',
},
{
id: 'data_view',
title: 'Data View',
enabled: true,
basePath: '/app/management/ingest/pipelines_logstash',
},
],
},
{
id: 'content',
title: 'Content',
apps: [
{
id: 'objects',
title: 'Saved Objects',
enabled: true,
basePath: '/app/management/ingest/pipelines_logstash',
},
{
id: 'tags',
title: 'Tags',
enabled: true,
basePath: '/app/management/ingest/pipelines_logstash',
},
{
id: 'filesManagement',
title: 'Files Management',
enabled: true,
basePath: '/app/management/ingest/pipelines_logstash',
},
],
},
{
id: 'other',
title: 'Other',
apps: [
{
id: 'api_keys',
title: 'API Keys',
enabled: true,
basePath: '/app/management/ingest/pipelines_logstash',
},
],
},
];

View file

@ -0,0 +1,19 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { action } from '@storybook/addon-actions';
import { APP_BASE_PATH, sectionsMock } from './mocks';
export const mockProps = {
appBasePath: APP_BASE_PATH,
sections: sectionsMock,
onCardClick: (e: any) => {
e.preventDefault();
action('Navigate to: ', e.target.href);
},
};

View file

@ -0,0 +1,6 @@
{
"name": "@kbn/management-cards-navigation",
"private": true,
"version": "1.0.0",
"license": "SSPL-1.0 OR Elastic License 2.0"
}

View file

@ -0,0 +1,36 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import React from 'react';
import { CardsNavigation as Component } from './cards_navigation';
import { mockProps } from '../mocks/storybook.mock';
import mdx from '../README.mdx';
export default {
title: 'Developer/Cards Navigation',
description: '',
parameters: {
docs: {
page: mdx,
},
},
};
export const CardsNavigationWillAllLinks = () => {
return <Component {...mockProps} />;
};
export const CardsNavigationWithSomeLinks = () => {
return <Component {...mockProps} sections={[{ apps: mockProps.sections[1].apps }]} />;
};
export const CardsNavigationWithHiddenLinks = () => {
return <Component {...mockProps} hideLinksTo={['api_keys']} />;
};

View file

@ -0,0 +1,73 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import React from 'react';
import { render, screen, cleanup } from '@testing-library/react';
import { APP_BASE_PATH, sectionsMock } from '../mocks/mocks';
import { CardsNavigation } from './cards_navigation';
import { CardsNavigationComponentProps } from './types';
const renderCardsNavigationComponent = (props: CardsNavigationComponentProps) => {
return [render(<CardsNavigation {...props} />)];
};
describe('Cards Navigation', () => {
describe('Component', () => {
test('is rendered', () => {
expect(() =>
render(<CardsNavigation appBasePath={APP_BASE_PATH} sections={sectionsMock} />)
).not.toThrowError();
});
});
describe('States', () => {
beforeEach(() => {
cleanup();
});
test('it renders categories and cards', () => {
renderCardsNavigationComponent({ sections: sectionsMock, appBasePath: APP_BASE_PATH });
const dataCategory = screen.queryByTestId('category-data');
const dataPipelinesApp = screen.queryByTestId('app-card-pipelines');
expect(dataCategory).not.toBeNull();
expect(dataPipelinesApp).not.toBeNull();
});
test('it doesnt show empty categories', () => {
renderCardsNavigationComponent({
sections: [
{
id: 'data',
title: 'Data',
apps: [],
},
],
appBasePath: APP_BASE_PATH,
});
const dataCategory = screen.queryByTestId('category-data');
expect(dataCategory).toBeNull();
});
test('it allows to disable certain apps', () => {
renderCardsNavigationComponent({
sections: sectionsMock,
appBasePath: APP_BASE_PATH,
hideLinksTo: ['pipelines'],
});
const dataPipelinesApp = screen.queryByTestId('app-card-pipelines');
expect(dataPipelinesApp).toBeNull();
});
});
});

View file

@ -0,0 +1,146 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import React from 'react';
import { i18n } from '@kbn/i18n';
import { flatMap } from 'lodash';
import {
EuiPageSection,
EuiPageHeader,
EuiSpacer,
EuiFlexGrid,
EuiFlexItem,
EuiCard,
EuiText,
EuiHorizontalRule,
} from '@elastic/eui';
import { CardsNavigationComponentProps, AppRegistrySections, Application, AppProps } from './types';
import { appCategories, appDefinitions, getAppIdsByCategory } from './consts';
import type { AppId } from './consts';
// Retrieve the data we need from a given app from the management app registry
const getDataFromManagementApp = (app: Application) => {
return {
id: app.id,
title: app.title,
href: app.basePath,
};
};
// Given a category and a list of apps, build an array of apps that belong to that category
const getAppsForCategory = (category: string, filteredApps: { [key: string]: Application }) => {
return getAppIdsByCategory(category)
.map((appId: AppId) => {
if (!filteredApps[appId]) {
return null;
}
return {
...getDataFromManagementApp(filteredApps[appId]),
...appDefinitions[appId],
};
})
.filter(Boolean) as AppProps[];
};
const getEnabledAppsByCategory = (sections: AppRegistrySections[], hideLinksTo: string[]) => {
// Flatten all apps into a single array
const flattenApps = flatMap(sections, (section) => section.apps)
// Remove all apps that the consumer wants to disable.
.filter((app) => !hideLinksTo.includes(app.id));
// Filter out apps that are not enabled and create an object with the
// app id as the key so we can easily do app look up by id.
const filteredApps: { [key: string]: Application } = flattenApps.reduce(
(obj, item: Application) => {
return item.enabled ? { ...obj, [item.id]: item } : obj;
},
{}
);
// Build list of categories with apps that are enabled
return [
{
id: appCategories.DATA,
title: i18n.translate('management.landing.withCardNavigation.dataTitle', {
defaultMessage: 'Data',
}),
apps: getAppsForCategory(appCategories.DATA, filteredApps),
},
{
id: appCategories.CONTENT,
title: i18n.translate('management.landing.withCardNavigation.contentTitle', {
defaultMessage: 'Content',
}),
apps: getAppsForCategory(appCategories.CONTENT, filteredApps),
},
{
id: appCategories.OTHER,
title: i18n.translate('management.landing.withCardNavigation.otherTitle', {
defaultMessage: 'Other',
}),
apps: getAppsForCategory(appCategories.OTHER, filteredApps),
},
// Filter out categories that don't have any apps since they dont need to be rendered
].filter((category) => category.apps.length > 0);
};
export const CardsNavigation = ({
sections,
appBasePath,
onCardClick,
hideLinksTo = [],
}: CardsNavigationComponentProps) => {
const appsByCategory = getEnabledAppsByCategory(sections, hideLinksTo);
return (
<EuiPageSection color="transparent" paddingSize="none">
<EuiPageHeader
bottomBorder
pageTitle={i18n.translate('management.landing.withCardNavigation.pageTitle', {
defaultMessage: 'Management',
})}
description={i18n.translate('management.landing.withCardNavigation.pageDescription', {
defaultMessage: 'Manage your indices, data views, saved objects, settings, and more.',
})}
/>
{appsByCategory.map((category, index) => (
<div key={category.id}>
{index === 0 ? (
<EuiSpacer size="l" />
) : (
<>
<EuiSpacer size="s" />
<EuiHorizontalRule />
</>
)}
<EuiText data-test-subj={`category-${category.id}`}>
<h3>{category.title}</h3>
</EuiText>
<EuiSpacer size="l" />
<EuiFlexGrid columns={3}>
{category.apps.map((app: AppProps) => (
<EuiFlexItem key={app.id}>
<EuiCard
data-test-subj={`app-card-${app.id}`}
layout="horizontal"
icon={app.icon}
titleSize="xs"
title={app.title}
description={app.description}
href={appBasePath + app.href}
onClick={onCardClick}
/>
</EuiFlexItem>
))}
</EuiFlexGrid>
</div>
))}
</EuiPageSection>
);
};

View file

@ -0,0 +1,171 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import React from 'react';
import { i18n } from '@kbn/i18n';
import { EuiIcon } from '@elastic/eui';
import { AppDefinition } from './types';
export enum appIds {
INGEST_PIPELINES = 'ingest_pipelines',
PIPELINES = 'pipelines',
INDEX_MANAGEMENT = 'index_management',
TRANSFORM = 'transform',
ML = 'jobsListLink',
DATA_VIEW = 'data_view',
SAVED_OBJECTS = 'objects',
TAGS = 'tags',
FILES_MANAGEMENT = 'filesManagement',
API_KEYS = 'api_keys',
DATA_VIEWS = 'dataViews',
REPORTING = 'reporting',
CONNECTORS = 'triggersActionsConnectors',
RULES = 'triggersActions',
MAINTENANCE_WINDOWS = 'maintenanceWindows',
}
// Create new type that is a union of all the appId values
export type AppId = `${appIds}`;
export const appCategories = {
DATA: 'data',
CONTENT: 'content',
OTHER: 'other',
};
export const appDefinitions: Record<AppId, AppDefinition> = {
[appIds.INGEST_PIPELINES]: {
category: appCategories.DATA,
description: i18n.translate(
'management.landing.withCardNavigation.ingestPipelinesDescription',
{
defaultMessage:
'Use pipelines to remove or transform fields, extract values from text, and enrich your data before indexing.',
}
),
icon: <EuiIcon size="l" type="logstashInput" />,
},
[appIds.PIPELINES]: {
category: appCategories.DATA,
description: i18n.translate('management.landing.withCardNavigation.ingestDescription', {
defaultMessage: 'Manage Logstash event processing and see the result visually.',
}),
icon: <EuiIcon size="l" type="logstashQueue" />,
},
[appIds.INDEX_MANAGEMENT]: {
category: appCategories.DATA,
description: i18n.translate(
'management.landing.withCardNavigation.indexmanagementDescription',
{
defaultMessage: 'Update your Elasticsearch indices individually or in bulk.',
}
),
icon: <EuiIcon size="l" type="indexSettings" />,
},
[appIds.TRANSFORM]: {
category: appCategories.DATA,
description: i18n.translate('management.landing.withCardNavigation.transformDescription', {
defaultMessage:
'Transforms pivot indices into summarized, entity-centric indices, or create an indexed view of the latest documents.',
}),
icon: <EuiIcon size="l" type="indexFlush" />,
},
[appIds.ML]: {
category: appCategories.DATA,
description: i18n.translate('management.landing.withCardNavigation.mlDescription', {
defaultMessage:
'View, export, and import machine learning analytics and anomaly detection items.',
}),
icon: <EuiIcon size="l" type="indexMapping" />,
},
[appIds.DATA_VIEW]: {
category: appCategories.DATA,
description: i18n.translate('management.landing.withCardNavigation.dataViewsDescription', {
defaultMessage:
'Create and manage the data views that help you retrieve your data from Elasticsearch.',
}),
icon: <EuiIcon size="l" type="indexEdit" />,
},
[appIds.SAVED_OBJECTS]: {
category: appCategories.CONTENT,
description: i18n.translate('management.landing.withCardNavigation.objectsDescription', {
defaultMessage:
'Manage and share your saved objects. To edit the underlying data of an object, go to its associated application.',
}),
icon: <EuiIcon size="l" type="save" />,
},
[appIds.TAGS]: {
category: appCategories.CONTENT,
description: i18n.translate('management.landing.withCardNavigation.tagsDescription', {
defaultMessage: 'Use tags to categorize and easily find your objects.',
}),
icon: <EuiIcon size="l" type="tag" />,
},
[appIds.FILES_MANAGEMENT]: {
category: appCategories.CONTENT,
description: i18n.translate('management.landing.withCardNavigation.fileManagementDescription', {
defaultMessage: 'Any files created will be listed here.',
}),
icon: <EuiIcon size="l" type="documents" />,
},
[appIds.API_KEYS]: {
category: appCategories.OTHER,
description: i18n.translate('management.landing.withCardNavigation.apiKeysDescription', {
defaultMessage: 'Allow applications to access Elastic on your behalf.',
}),
icon: <EuiIcon size="l" type="lockOpen" />,
},
[appIds.DATA_VIEWS]: {
category: appCategories.DATA,
description: i18n.translate('management.landing.withCardNavigation.dataViewsDescription', {
defaultMessage:
'Create and manage the data views that help you retrieve your data from Elasticsearch.',
}),
icon: <EuiIcon size="l" type="indexEdit" />,
},
[appIds.CONNECTORS]: {
category: appCategories.OTHER,
description: i18n.translate('management.landing.withCardNavigation.connectorsDescription', {
defaultMessage: 'Connect third-party software with your alerting data.',
}),
icon: <EuiIcon size="l" type="desktop" />,
},
[appIds.RULES]: {
category: appCategories.OTHER,
description: i18n.translate('management.landing.withCardNavigation.rulesDescription', {
defaultMessage: 'Detect conditions using rules.',
}),
icon: <EuiIcon size="l" type="editorChecklist" />,
},
[appIds.MAINTENANCE_WINDOWS]: {
category: appCategories.OTHER,
description: i18n.translate(
'management.landing.withCardNavigation.maintenanceWindowsDescription',
{
defaultMessage: 'Suppress rule notifications for scheduled periods of time.',
}
),
icon: <EuiIcon size="l" type="wrench" />,
},
[appIds.REPORTING]: {
category: appCategories.CONTENT,
description: i18n.translate('management.landing.withCardNavigation.reportingDescription', {
defaultMessage: 'Get reports generated in applications.',
}),
icon: <EuiIcon size="l" type="visPie" />,
},
};
// Compose a list of app ids that belong to a given category
export const getAppIdsByCategory = (category: string) => {
const appKeys = Object.keys(appDefinitions) as AppId[];
return appKeys.filter((appId: AppId) => {
return appDefinitions[appId].category === category;
});
};

View file

@ -0,0 +1,13 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
export type { CardsNavigationComponentProps } from './types';
export type { AppId } from './consts';
export { appIds } from './consts';
export { CardsNavigation } from './cards_navigation';

View file

@ -0,0 +1,42 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import type { AppId } from './consts';
export interface Application {
id: string;
title: string;
basePath: string;
enabled: boolean;
}
export interface AppRegistrySections {
id?: string;
title?: string;
apps: Application[];
}
export interface CardsNavigationComponentProps {
sections: AppRegistrySections[];
appBasePath: string;
onCardClick?: (e: React.MouseEvent<HTMLAnchorElement>) => void;
hideLinksTo?: AppId[];
}
export interface ManagementAppProps {
id: string;
title: string;
href: string;
}
export interface AppDefinition {
category: string;
description: string;
icon: React.ReactElement;
}
export type AppProps = ManagementAppProps & AppDefinition;

View file

@ -0,0 +1,22 @@
{
"extends": "../../../tsconfig.base.json",
"compilerOptions": {
"outDir": "target/types",
"types": [
"jest",
"node",
"react",
"@kbn/ambient-ui-types"
]
},
"include": [
"**/*.ts",
"**/*.tsx"
],
"exclude": [
"target/**/*"
],
"kbn_references": [
"@kbn/i18n",
]
}

View file

@ -0,0 +1,5 @@
# kbn-management storybook config
This directory contains the configuration for the Storybook deployment for all kbn-management component packages.
For more information, refer to the [Storybook documentation](https://storybook.js.org/docs/react/configure/overview) and the `@kbn/storybook` package.

View file

@ -0,0 +1,13 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
/** The title of the Storybook. */
export const TITLE = 'kbn-management storybook';
/** The remote URL of the root from which Storybook loads stories for kbn-management. */
export const URL = 'https://github.com/elastic/kibana/tree/main/packages/kbn-management';

View file

@ -0,0 +1,9 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
export { TITLE, URL } from './constants';

View file

@ -0,0 +1,6 @@
{
"type": "shared-common",
"id": "@kbn/management-storybook-config",
"owner": "@elastic/platform-deployment-management",
"devOnly": true
}

View file

@ -0,0 +1,17 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { defaultConfig } from '@kbn/storybook';
module.exports = {
...defaultConfig,
stories: ['../../**/*.stories.+(tsx|mdx)'],
reactOptions: {
strictMode: true,
},
};

View file

@ -0,0 +1,23 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { addons } from '@storybook/addons';
import { create } from '@storybook/theming';
import { PANEL_ID as selectedPanel } from '@storybook/addon-actions';
import { TITLE as brandTitle, URL as brandUrl } from './constants';
addons.setConfig({
theme: create({
base: 'light',
brandTitle,
brandUrl,
}),
selectedPanel,
showPanel: true.valueOf,
});

View file

@ -0,0 +1,6 @@
{
"name": "@kbn/management-storybook-config",
"private": true,
"version": "1.0.0",
"license": "SSPL-1.0 OR Elastic License 2.0"
}

View file

@ -0,0 +1,22 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
/* eslint-disable @typescript-eslint/no-namespace,@typescript-eslint/no-empty-interface */
declare global {
namespace NodeJS {
interface Global {}
interface InspectOptions {}
type ConsoleConstructor = console.ConsoleConstructor;
}
}
/* eslint-enable */
import jest from 'jest-mock';
/* @ts-expect-error TS doesn't see jest as a property of window, and I don't want to edit our global config. */
window.jest = jest;

View file

@ -0,0 +1,19 @@
{
"extends": "../../../../tsconfig.base.json",
"compilerOptions": {
"outDir": "target/types",
"types": [
"jest",
"node"
]
},
"include": [
"**/*.ts"
],
"exclude": [
"target/**/*"
],
"kbn_references": [
"@kbn/storybook"
]
}

View file

@ -46,6 +46,7 @@ export const storybookAliases = {
infra: 'x-pack/plugins/infra/.storybook',
kibana_react: 'src/plugins/kibana_react/.storybook',
lists: 'x-pack/plugins/lists/.storybook',
management: 'packages/kbn-management/storybook/config',
observability: 'x-pack/plugins/observability/.storybook',
presentation: 'src/plugins/presentation_util/storybook',
random_sampling: 'x-pack/packages/kbn-random-sampling/.storybook',

View file

@ -1,5 +1,41 @@
# Management Plugin
This plugins contains the "Stack Management" page framework. It offers navigation and an API
to link individual managment section into it. This plugin does not contain any individual
management section itself.
to link individual management section into it. This plugin does not contain any individual
management section itself.
## Cards navigation
This plugin offers a special version of its landing page with a special feature called "cards navigation".
This feature can be enabled by calling the `setupCardsNavigation` method from the `management` plugin from
your plugin's `setup` method:
```
management.setupCardsNavigation({ enabled: true });
```
The cards that will be shown are defined in the `packages/kbn-management/cards_navigation/src/consts.tsx` file
and they are grouped into categories. These cards are computed based on the `SectionsService` that is provided
in the `management` plugin.
### Adding a new card to the navigation
For adding a new item to the navigation all you have to do is edit the `packages/kbn-management/cards_navigation/src/consts.tsx`
file and add two things:
* Add the app id into the `appIds` enum (make sure that the app_id value matches the one from the plugin)
* Add a new entry to the `appDefinitions` object. In here you can specify the category where you want it to be, icon and description.
### Removing an item from the navigation
If card needs to be hidden from the navigation you can specify that by using the `hideLinksTo` prop:
```
management.setupCardsNavigation({
enabled: true,
hideLinksTo: [appIds.MAINTENANCE_WINDOWS],
});
```
More specifics about the `setupCardsNavigation` can be found in `packages/kbn-management/cards_navigation/readme.mdx`.

View file

@ -0,0 +1,90 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import React from 'react';
import { merge } from 'lodash';
import { registerTestBed, AsyncTestBedConfig, TestBed } from '@kbn/test-jest-helpers';
import { AppContextProvider } from '../management_app/management_context';
import { ManagementLandingPage } from './landing';
const sectionsMock = [
{
id: 'data',
title: 'Data',
apps: [
{
id: 'ingest_pipelines',
title: 'Ingest pipelines',
enabled: true,
basePath: '/app/management/ingest/pipelines',
},
],
},
];
const testBedConfig: AsyncTestBedConfig = {
memoryRouter: {
initialEntries: [`/management_landing`],
componentRoutePath: '/management_landing',
},
doMountAsync: true,
};
export const WithAppDependencies =
(Comp: any, overrides: Record<string, unknown> = {}) =>
(props: Record<string, unknown>) => {
const contextDependencies = {
appBasePath: 'http://localhost:9001',
kibanaVersion: '8.10.0',
cardsNavigationConfig: { enabled: true },
sections: sectionsMock,
};
return (
// @ts-ignore
<AppContextProvider value={merge(contextDependencies, overrides)}>
<Comp {...props} setBreadcrumbs={jest.fn()} onAppMounted={jest.fn()} />
</AppContextProvider>
);
};
export const setupLandingPage = async (overrides?: Record<string, unknown>): Promise<TestBed> => {
const initTestBed = registerTestBed(
WithAppDependencies(ManagementLandingPage, overrides),
testBedConfig
);
const testBed = await initTestBed();
return {
...testBed,
};
};
describe('Landing Page', () => {
let testBed: TestBed;
describe('Can be configured through cardsNavigationConfig', () => {
beforeEach(async () => {
testBed = await setupLandingPage();
});
test('Shows cards navigation when feature is enabled', async () => {
const { exists } = testBed;
expect(exists('cards-navigation-page')).toBe(true);
});
test('Hide cards navigation when feature is disabled', async () => {
testBed = await setupLandingPage({ cardsNavigationConfig: { enabled: false } });
const { exists } = testBed;
expect(exists('cards-navigation-page')).toBe(false);
expect(exists('managementHome')).toBe(true);
});
});
});

View file

@ -10,24 +10,38 @@ import React, { useEffect } from 'react';
import { FormattedMessage } from '@kbn/i18n-react';
import { EuiHorizontalRule } from '@elastic/eui';
import { KibanaPageTemplate } from '@kbn/shared-ux-page-kibana-template';
import { EuiPageBody } from '@elastic/eui';
import { CardsNavigation } from '@kbn/management-cards-navigation';
import { useAppContext } from '../management_app/management_context';
interface ManagementLandingPageProps {
version: string;
onAppMounted: (id: string) => void;
setBreadcrumbs: () => void;
}
export const ManagementLandingPage = ({
version,
setBreadcrumbs,
onAppMounted,
}: ManagementLandingPageProps) => {
const { appBasePath, sections, kibanaVersion, cardsNavigationConfig } = useAppContext();
setBreadcrumbs();
useEffect(() => {
onAppMounted('');
}, [onAppMounted]);
if (cardsNavigationConfig?.enabled) {
return (
<EuiPageBody restrictWidth={true} data-test-subj="cards-navigation-page">
<CardsNavigation
sections={sections}
appBasePath={appBasePath}
hideLinksTo={cardsNavigationConfig?.hideLinksTo}
/>
</EuiPageBody>
);
}
return (
<KibanaPageTemplate.EmptyPrompt
data-test-subj="managementHome"
@ -37,7 +51,7 @@ export const ManagementLandingPage = ({
<FormattedMessage
id="management.landing.header"
defaultMessage="Welcome to Stack Management {version}"
values={{ version }}
values={{ version: kibanaVersion }}
/>
</h1>
}

View file

@ -13,10 +13,13 @@ import { BehaviorSubject } from 'rxjs';
import { I18nProvider } from '@kbn/i18n-react';
import { i18n } from '@kbn/i18n';
import { AppMountParameters, ChromeBreadcrumb, ScopedHistory } from '@kbn/core/public';
import { CoreStart } from '@kbn/core/public';
import { RedirectAppLinks } from '@kbn/shared-ux-link-redirect-app';
import { reactRouterNavigate, KibanaThemeProvider } from '@kbn/kibana-react-plugin/public';
import { KibanaPageTemplate, KibanaPageTemplateProps } from '@kbn/shared-ux-page-kibana-template';
import useObservable from 'react-use/lib/useObservable';
import { AppContextProvider } from './management_context';
import {
ManagementSection,
MANAGEMENT_BREADCRUMB,
@ -24,7 +27,7 @@ import {
} from '../../utils';
import { ManagementRouter } from './management_router';
import { managementSidebarNav } from '../management_sidebar_nav/management_sidebar_nav';
import { SectionsServiceStart } from '../../types';
import { SectionsServiceStart, NavigationCardsSubject } from '../../types';
interface ManagementAppProps {
appBasePath: string;
@ -36,15 +39,23 @@ interface ManagementAppProps {
export interface ManagementAppDependencies {
sections: SectionsServiceStart;
kibanaVersion: string;
coreStart: CoreStart;
setBreadcrumbs: (newBreadcrumbs: ChromeBreadcrumb[]) => void;
isSidebarEnabled$: BehaviorSubject<boolean>;
cardsNavigationConfig$: BehaviorSubject<NavigationCardsSubject>;
}
export const ManagementApp = ({ dependencies, history, theme$ }: ManagementAppProps) => {
const { setBreadcrumbs, isSidebarEnabled$ } = dependencies;
export const ManagementApp = ({
dependencies,
history,
theme$,
appBasePath,
}: ManagementAppProps) => {
const { setBreadcrumbs, isSidebarEnabled$, cardsNavigationConfig$ } = dependencies;
const [selectedId, setSelectedId] = useState<string>('');
const [sections, setSections] = useState<ManagementSection[]>();
const isSidebarEnabled = useObservable(isSidebarEnabled$);
const cardsNavigationConfig = useObservable(cardsNavigationConfig$);
const onAppMounted = useCallback((id: string) => {
setSelectedId(id);
@ -95,26 +106,36 @@ export const ManagementApp = ({ dependencies, history, theme$ }: ManagementAppPr
}
: undefined;
const contextDependencies = {
appBasePath,
sections,
cardsNavigationConfig,
kibanaVersion: dependencies.kibanaVersion,
};
return (
<I18nProvider>
<KibanaThemeProvider theme$={theme$}>
<KibanaPageTemplate
restrictWidth={false}
solutionNav={solution}
// @ts-expect-error Techincally `paddingSize` isn't supported but it is passed through,
// this is a stop-gap for Stack managmement specifically until page components can be converted to template components
mainProps={{ paddingSize: 'l' }}
>
<ManagementRouter
history={history}
theme$={theme$}
setBreadcrumbs={setBreadcrumbsScoped}
onAppMounted={onAppMounted}
sections={sections}
dependencies={dependencies}
/>
</KibanaPageTemplate>
</KibanaThemeProvider>
</I18nProvider>
<RedirectAppLinks coreStart={dependencies.coreStart}>
<I18nProvider>
<AppContextProvider value={contextDependencies}>
<KibanaThemeProvider theme$={theme$}>
<KibanaPageTemplate
restrictWidth={false}
solutionNav={solution}
// @ts-expect-error Techincally `paddingSize` isn't supported but it is passed through,
// this is a stop-gap for Stack managmement specifically until page components can be converted to template components
mainProps={{ paddingSize: 'l' }}
>
<ManagementRouter
history={history}
theme$={theme$}
setBreadcrumbs={setBreadcrumbsScoped}
onAppMounted={onAppMounted}
sections={sections}
/>
</KibanaPageTemplate>
</KibanaThemeProvider>
</AppContextProvider>
</I18nProvider>
</RedirectAppLinks>
);
};

View file

@ -0,0 +1,30 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import React, { createContext, useContext } from 'react';
import { AppDependencies } from '../../types';
export const AppContext = createContext<AppDependencies | undefined>(undefined);
export const AppContextProvider = ({
children,
value,
}: {
children: React.ReactNode;
value: AppDependencies;
}) => {
return <AppContext.Provider value={value}>{children}</AppContext.Provider>;
};
export const useAppContext = () => {
const ctx = useContext(AppContext);
if (!ctx) {
throw new Error('useAppContext must be called from inside AppContext');
}
return ctx;
};

View file

@ -12,27 +12,18 @@ import { Router, Routes, Route } from '@kbn/shared-ux-router';
import { AppMountParameters, ChromeBreadcrumb, ScopedHistory } from '@kbn/core/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'];
theme$: AppMountParameters['theme$'];
dependencies: ManagementAppDependencies;
setBreadcrumbs: (crumbs?: ChromeBreadcrumb[], appHistory?: ScopedHistory) => void;
onAppMounted: (id: string) => void;
sections: ManagementSection[];
}
export const ManagementRouter = memo(
({
dependencies,
history,
setBreadcrumbs,
onAppMounted,
sections,
theme$,
}: ManagementRouterProps) => (
({ history, setBreadcrumbs, onAppMounted, sections, theme$ }: ManagementRouterProps) => (
<Router history={history}>
<Routes>
{sections.map((section) =>
@ -62,11 +53,7 @@ export const ManagementRouter = memo(
<Route
path={'/'}
component={() => (
<ManagementLandingPage
version={dependencies.kibanaVersion}
setBreadcrumbs={setBreadcrumbs}
onAppMounted={onAppMounted}
/>
<ManagementLandingPage setBreadcrumbs={setBreadcrumbs} onAppMounted={onAppMounted} />
)}
/>
</Routes>

View file

@ -43,6 +43,7 @@ const createSetupContract = (): ManagementSetup => ({
const createStartContract = (): ManagementStart => ({
setIsSidebarEnabled: jest.fn(),
setupCardsNavigation: jest.fn(),
});
export const managementPluginMock = {

View file

@ -22,7 +22,7 @@ import {
AppNavLinkStatus,
AppDeepLink,
} from '@kbn/core/public';
import { ManagementSetup, ManagementStart } from './types';
import { ManagementSetup, ManagementStart, NavigationCardsSubject } from './types';
import { MANAGEMENT_APP_ID } from '../common/contants';
import { ManagementAppLocatorDefinition } from '../common/locator';
@ -72,6 +72,10 @@ export class ManagementPlugin
private hasAnyEnabledApps = true;
private isSidebarEnabled$ = new BehaviorSubject<boolean>(true);
private cardsNavigationConfig$ = new BehaviorSubject<NavigationCardsSubject>({
enabled: false,
hideLinksTo: [],
});
constructor(private initializerContext: PluginInitializerContext) {}
@ -116,8 +120,10 @@ export class ManagementPlugin
return renderApp(params, {
sections: getSectionsServiceStartPrivate(),
kibanaVersion,
coreStart,
setBreadcrumbs: coreStart.chrome.setBreadcrumbs,
isSidebarEnabled$: managementPlugin.isSidebarEnabled$,
cardsNavigationConfig$: managementPlugin.cardsNavigationConfig$,
});
},
});
@ -146,6 +152,8 @@ export class ManagementPlugin
return {
setIsSidebarEnabled: (isSidebarEnabled: boolean) =>
this.isSidebarEnabled$.next(isSidebarEnabled),
setupCardsNavigation: ({ enabled, hideLinksTo }) =>
this.cardsNavigationConfig$.next({ enabled, hideLinksTo }),
};
}
}

View file

@ -10,6 +10,7 @@ import { Observable } from 'rxjs';
import { ScopedHistory, Capabilities } from '@kbn/core/public';
import type { LocatorPublic } from '@kbn/share-plugin/common';
import { ChromeBreadcrumb, CoreTheme } from '@kbn/core/public';
import type { AppId } from '@kbn/management-cards-navigation';
import { ManagementSection, RegisterManagementSectionArgs } from './utils';
import type { ManagementAppLocatorParams } from '../common/locator';
@ -29,6 +30,7 @@ export interface DefinedSections {
export interface ManagementStart {
setIsSidebarEnabled: (enabled: boolean) => void;
setupCardsNavigation: ({ enabled, hideLinksTo }: NavigationCardsSubject) => void;
}
export interface ManagementSectionsStartPrivate {
@ -78,3 +80,15 @@ export interface CreateManagementItemArgs {
capabilitiesId?: string; // overrides app id
redirectFrom?: string; // redirects from an old app id to the current app id
}
export interface NavigationCardsSubject {
enabled: boolean;
hideLinksTo?: AppId[];
}
export interface AppDependencies {
appBasePath: string;
kibanaVersion: string;
sections: ManagementSection[];
cardsNavigationConfig?: NavigationCardsSubject;
}

View file

@ -19,7 +19,10 @@
"@kbn/i18n",
"@kbn/i18n-react",
"@kbn/shared-ux-page-kibana-template",
"@kbn/shared-ux-router"
"@kbn/shared-ux-router",
"@kbn/management-cards-navigation",
"@kbn/shared-ux-link-redirect-app",
"@kbn/test-jest-helpers"
],
"exclude": [
"target/**/*",

View file

@ -936,8 +936,12 @@
"@kbn/managed-vscode-config/*": ["packages/kbn-managed-vscode-config/*"],
"@kbn/managed-vscode-config-cli": ["packages/kbn-managed-vscode-config-cli"],
"@kbn/managed-vscode-config-cli/*": ["packages/kbn-managed-vscode-config-cli/*"],
"@kbn/management-cards-navigation": ["packages/kbn-management/cards_navigation"],
"@kbn/management-cards-navigation/*": ["packages/kbn-management/cards_navigation/*"],
"@kbn/management-plugin": ["src/plugins/management"],
"@kbn/management-plugin/*": ["src/plugins/management/*"],
"@kbn/management-storybook-config": ["packages/kbn-management/storybook/config"],
"@kbn/management-storybook-config/*": ["packages/kbn-management/storybook/config/*"],
"@kbn/management-test-plugin": ["test/plugin_functional/plugins/management_test_plugin"],
"@kbn/management-test-plugin/*": ["test/plugin_functional/plugins/management_test_plugin/*"],
"@kbn/mapbox-gl": ["packages/kbn-mapbox-gl"],

View file

@ -8,7 +8,7 @@
"server": true,
"browser": true,
"configPath": ["xpack", "serverless", "observability"],
"requiredPlugins": ["serverless", "observabilityShared", "kibanaReact"],
"requiredPlugins": ["serverless", "observabilityShared", "kibanaReact", "management"],
"optionalPlugins": [],
"requiredBundles": []
}

View file

@ -6,6 +6,7 @@
*/
import { CoreSetup, CoreStart, Plugin } from '@kbn/core/public';
import { appIds } from '@kbn/management-cards-navigation';
import { getObservabilitySideNavComponent } from './components/side_navigation';
import {
ServerlessObservabilityPluginSetup,
@ -28,10 +29,14 @@ export class ServerlessObservabilityPlugin
core: CoreStart,
setupDeps: ServerlessObservabilityPluginStartDependencies
): ServerlessObservabilityPluginStart {
const { observabilityShared, serverless } = setupDeps;
const { observabilityShared, serverless, management } = setupDeps;
observabilityShared.setIsSidebarEnabled(false);
serverless.setProjectHome('/app/observability/landing');
serverless.setSideNavComponent(getObservabilitySideNavComponent(core, { serverless }));
management.setupCardsNavigation({
enabled: true,
hideLinksTo: [appIds.RULES],
});
return {};
}

View file

@ -10,6 +10,7 @@ import {
ObservabilitySharedPluginSetup,
ObservabilitySharedPluginStart,
} from '@kbn/observability-shared-plugin/public';
import type { ManagementSetup, ManagementStart } from '@kbn/management-plugin/public';
// eslint-disable-next-line @typescript-eslint/no-empty-interface
export interface ServerlessObservabilityPluginSetup {}
@ -20,9 +21,11 @@ export interface ServerlessObservabilityPluginStart {}
export interface ServerlessObservabilityPluginSetupDependencies {
observabilityShared: ObservabilitySharedPluginSetup;
serverless: ServerlessPluginSetup;
management: ManagementSetup;
}
export interface ServerlessObservabilityPluginStartDependencies {
observabilityShared: ObservabilitySharedPluginStart;
serverless: ServerlessPluginStart;
management: ManagementStart;
}

View file

@ -17,10 +17,12 @@
"kbn_references": [
"@kbn/core",
"@kbn/config-schema",
"@kbn/management-plugin",
"@kbn/serverless",
"@kbn/observability-shared-plugin",
"@kbn/kibana-react-plugin",
"@kbn/shared-ux-chrome-navigation",
"@kbn/i18n",
"@kbn/management-cards-navigation",
]
}

View file

@ -7,6 +7,7 @@
import { AppMountParameters, CoreSetup, CoreStart, Plugin } from '@kbn/core/public';
import { i18n } from '@kbn/i18n';
import { appIds } from '@kbn/management-cards-navigation';
import { createServerlessSearchSideNavComponent as createComponent } from './layout/nav';
import { docLinks } from '../common/doc_links';
import {
@ -68,10 +69,14 @@ export class ServerlessSearchPlugin
public start(
core: CoreStart,
{ serverless }: ServerlessSearchPluginStartDependencies
{ serverless, management }: ServerlessSearchPluginStartDependencies
): ServerlessSearchPluginStart {
serverless.setProjectHome('/app/elasticsearch');
serverless.setSideNavComponent(createComponent(core, { serverless }));
management.setupCardsNavigation({
enabled: true,
hideLinksTo: [appIds.MAINTENANCE_WINDOWS],
});
return {};
}

View file

@ -27,6 +27,7 @@
"@kbn/security-plugin",
"@kbn/cloud-plugin",
"@kbn/share-plugin",
"@kbn/management-cards-navigation",
"@kbn/core-elasticsearch-server",
]
}

View file

@ -4630,10 +4630,18 @@
version "0.0.0"
uid ""
"@kbn/management-cards-navigation@link:packages/kbn-management/cards_navigation":
version "0.0.0"
uid ""
"@kbn/management-plugin@link:src/plugins/management":
version "0.0.0"
uid ""
"@kbn/management-storybook-config@link:packages/kbn-management/storybook/config":
version "0.0.0"
uid ""
"@kbn/management-test-plugin@link:test/plugin_functional/plugins/management_test_plugin":
version "0.0.0"
uid ""