mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
[Observability] Add Observability Shared app (#154716)
Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
962e91556c
commit
26f65b3262
36 changed files with 1660 additions and 6 deletions
1
.github/CODEOWNERS
vendored
1
.github/CODEOWNERS
vendored
|
@ -475,6 +475,7 @@ packages/kbn-object-versioning @elastic/appex-sharedux
|
|||
x-pack/packages/observability/alert_details @elastic/actionable-observability
|
||||
x-pack/test/cases_api_integration/common/plugins/observability @elastic/response-ops
|
||||
x-pack/plugins/observability @elastic/actionable-observability
|
||||
x-pack/plugins/observability_shared @elastic/actionable-observability
|
||||
x-pack/test/security_api_integration/plugins/oidc_provider @elastic/kibana-security
|
||||
test/common/plugins/otel_metrics @elastic/infra-monitoring-ui
|
||||
packages/kbn-optimizer @elastic/kibana-operations
|
||||
|
|
|
@ -641,6 +641,10 @@ Elastic.
|
|||
|This plugin provides shared components and services for use across observability solutions, as well as the observability landing page UI.
|
||||
|
||||
|
||||
|{kib-repo}blob/{branch}/x-pack/plugins/observability_shared/README.md[observabilityShared]
|
||||
|A plugin that contains components and utilities shared by all Observability plugins.
|
||||
|
||||
|
||||
|{kib-repo}blob/{branch}/x-pack/plugins/osquery/README.md[osquery]
|
||||
|This plugin adds extended support to Security Solution Fleet Osquery integration
|
||||
|
||||
|
|
|
@ -490,6 +490,7 @@
|
|||
"@kbn/observability-alert-details": "link:x-pack/packages/observability/alert_details",
|
||||
"@kbn/observability-fixtures-plugin": "link:x-pack/test/cases_api_integration/common/plugins/observability",
|
||||
"@kbn/observability-plugin": "link:x-pack/plugins/observability",
|
||||
"@kbn/observability-shared-plugin": "link:x-pack/plugins/observability_shared",
|
||||
"@kbn/oidc-provider-plugin": "link:x-pack/test/security_api_integration/plugins/oidc_provider",
|
||||
"@kbn/open-telemetry-instrumented-plugin": "link:test/common/plugins/otel_metrics",
|
||||
"@kbn/osquery-io-ts-types": "link:packages/kbn-osquery-io-ts-types",
|
||||
|
|
|
@ -14,7 +14,7 @@ module.exports = {
|
|||
USES_STYLED_COMPONENTS: [
|
||||
/packages[\/\\](kbn-ui-shared-deps-(npm|src)|kbn-ecs-data-quality-dashboard)[\/\\]/,
|
||||
/src[\/\\]plugins[\/\\](kibana_react)[\/\\]/,
|
||||
/x-pack[\/\\]plugins[\/\\](apm|beats_management|cases|fleet|infra|lists|observability|exploratory_view|osquery|security_solution|timelines|synthetics|ux)[\/\\]/,
|
||||
/x-pack[\/\\]plugins[\/\\](apm|beats_management|cases|fleet|infra|lists|observability|observability_shared|exploratory_view|osquery|security_solution|timelines|synthetics|ux)[\/\\]/,
|
||||
/x-pack[\/\\]test[\/\\]plugin_functional[\/\\]plugins[\/\\]resolver_test[\/\\]/,
|
||||
],
|
||||
};
|
||||
|
|
|
@ -94,6 +94,7 @@ pageLoadAssetSize:
|
|||
navigation: 37269
|
||||
newsfeed: 42228
|
||||
observability: 95000
|
||||
observabilityShared: 21266
|
||||
osquery: 107090
|
||||
painlessLab: 179748
|
||||
presentationUtil: 58834
|
||||
|
|
|
@ -944,6 +944,8 @@
|
|||
"@kbn/observability-fixtures-plugin/*": ["x-pack/test/cases_api_integration/common/plugins/observability/*"],
|
||||
"@kbn/observability-plugin": ["x-pack/plugins/observability"],
|
||||
"@kbn/observability-plugin/*": ["x-pack/plugins/observability/*"],
|
||||
"@kbn/observability-shared-plugin": ["x-pack/plugins/observability_shared"],
|
||||
"@kbn/observability-shared-plugin/*": ["x-pack/plugins/observability_shared/*"],
|
||||
"@kbn/oidc-provider-plugin": ["x-pack/test/security_api_integration/plugins/oidc_provider"],
|
||||
"@kbn/oidc-provider-plugin/*": ["x-pack/test/security_api_integration/plugins/oidc_provider/*"],
|
||||
"@kbn/open-telemetry-instrumented-plugin": ["test/common/plugins/otel_metrics"],
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
"xpack.stackAlerts": "plugins/stack_alerts",
|
||||
"xpack.stackConnectors": "plugins/stack_connectors",
|
||||
"xpack.apm": "plugins/apm",
|
||||
"xpack.banners": "plugins/banners",
|
||||
"xpack.canvas": "plugins/canvas",
|
||||
"xpack.cases": "plugins/cases",
|
||||
"xpack.cloud": "plugins/cloud",
|
||||
|
@ -47,9 +48,11 @@
|
|||
"xpack.aiops": ["packages/ml/aiops_components", "plugins/aiops"],
|
||||
"xpack.ml": ["packages/ml/date_picker", "packages/ml/trained_models_utils", "plugins/ml"],
|
||||
"xpack.monitoring": ["plugins/monitoring"],
|
||||
"xpack.observability": "plugins/observability",
|
||||
"xpack.observabilityShared": "plugins/observability_shared",
|
||||
"xpack.osquery": ["plugins/osquery"],
|
||||
"xpack.painlessLab": "plugins/painless_lab",
|
||||
"xpack.profiling": [ "plugins/profiling" ],
|
||||
"xpack.profiling": ["plugins/profiling"],
|
||||
"xpack.remoteClusters": "plugins/remote_clusters",
|
||||
"xpack.reporting": ["plugins/reporting"],
|
||||
"xpack.rollupJobs": ["plugins/rollup"],
|
||||
|
@ -64,6 +67,7 @@
|
|||
"xpack.spaces": "plugins/spaces",
|
||||
"xpack.savedObjectsTagging": ["plugins/saved_objects_tagging"],
|
||||
"xpack.taskManager": "legacy/plugins/task_manager",
|
||||
"xpack.threatIntelligence": "plugins/threat_intelligence",
|
||||
"xpack.timelines": "plugins/timelines",
|
||||
"xpack.transform": "plugins/transform",
|
||||
"xpack.triggersActionsUI": "plugins/triggers_actions_ui",
|
||||
|
@ -71,10 +75,7 @@
|
|||
"xpack.synthetics": ["plugins/synthetics"],
|
||||
"xpack.ux": ["plugins/ux"],
|
||||
"xpack.urlDrilldown": "plugins/drilldowns/url_drilldown",
|
||||
"xpack.watcher": "plugins/watcher",
|
||||
"xpack.observability": "plugins/observability",
|
||||
"xpack.banners": "plugins/banners",
|
||||
"xpack.threatIntelligence": "plugins/threat_intelligence"
|
||||
"xpack.watcher": "plugins/watcher"
|
||||
},
|
||||
"exclude": ["examples"],
|
||||
"translations": [
|
||||
|
|
11
x-pack/plugins/observability_shared/.storybook/jest_setup.js
Normal file
11
x-pack/plugins/observability_shared/.storybook/jest_setup.js
Normal file
|
@ -0,0 +1,11 @@
|
|||
/*
|
||||
* 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; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { setGlobalConfig } from '@storybook/testing-react';
|
||||
import * as globalStorybookConfig from './preview';
|
||||
|
||||
setGlobalConfig(globalStorybookConfig);
|
8
x-pack/plugins/observability_shared/.storybook/main.js
Normal file
8
x-pack/plugins/observability_shared/.storybook/main.js
Normal file
|
@ -0,0 +1,8 @@
|
|||
/*
|
||||
* 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; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
module.exports = require('@kbn/storybook').defaultConfig;
|
10
x-pack/plugins/observability_shared/.storybook/preview.js
Normal file
10
x-pack/plugins/observability_shared/.storybook/preview.js
Normal file
|
@ -0,0 +1,10 @@
|
|||
/*
|
||||
* 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; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { EuiThemeProviderDecorator } from '@kbn/kibana-react-plugin/common';
|
||||
|
||||
export const decorators = [EuiThemeProviderDecorator];
|
11
x-pack/plugins/observability_shared/README.md
Normal file
11
x-pack/plugins/observability_shared/README.md
Normal file
|
@ -0,0 +1,11 @@
|
|||
# Observability Shared
|
||||
|
||||
A plugin that contains components and utilities shared by all Observability plugins.
|
||||
|
||||
## Shared navigation
|
||||
|
||||
The Observability plugin maintains a navigation registry for Observability solutions, and exposes a shared page template component. Please refer to the docs in [the component directory](public/components/shared/page_template) for more information on registering your solution's navigation structure, and rendering the navigation via the shared component.
|
||||
|
||||
## A note on cyclical dependencies
|
||||
|
||||
Do not import any Observability plugins into this plugin. Only export shared stuff to other plugins.
|
11
x-pack/plugins/observability_shared/common/index.ts
Normal file
11
x-pack/plugins/observability_shared/common/index.ts
Normal file
|
@ -0,0 +1,11 @@
|
|||
/*
|
||||
* 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; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
export const observabilityFeatureId = 'observability';
|
||||
export const observabilityAppId = 'observability-overview';
|
||||
export const casesFeatureId = 'observabilityCases';
|
||||
export const sloFeatureId = 'slo';
|
18
x-pack/plugins/observability_shared/jest.config.js
Normal file
18
x-pack/plugins/observability_shared/jest.config.js
Normal file
|
@ -0,0 +1,18 @@
|
|||
/*
|
||||
* 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; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
module.exports = {
|
||||
preset: '@kbn/test',
|
||||
rootDir: '../../..',
|
||||
roots: ['<rootDir>/x-pack/plugins/observability_shared'],
|
||||
setupFiles: ['<rootDir>/x-pack/plugins/observability_shared/.storybook/jest_setup.js'],
|
||||
coverageDirectory: '<rootDir>/target/kibana-coverage/jest/x-pack/plugins/observability_shared',
|
||||
coverageReporters: ['text', 'html'],
|
||||
collectCoverageFrom: [
|
||||
'<rootDir>/x-pack/plugins/observability_shared/{common,public,server}/**/*.{js,ts,tsx}',
|
||||
],
|
||||
};
|
15
x-pack/plugins/observability_shared/kibana.jsonc
Normal file
15
x-pack/plugins/observability_shared/kibana.jsonc
Normal file
|
@ -0,0 +1,15 @@
|
|||
{
|
||||
"type": "plugin",
|
||||
"id": "@kbn/observability-shared-plugin",
|
||||
"owner": "@elastic/actionable-observability",
|
||||
"plugin": {
|
||||
"id": "observabilityShared",
|
||||
"server": false,
|
||||
"browser": true,
|
||||
"configPath": ["xpack", "observability_shared"],
|
||||
"requiredPlugins": ["cases", "guidedOnboarding"],
|
||||
"optionalPlugins": [],
|
||||
"requiredBundles": ["kibanaReact"],
|
||||
"extraPublicDirs": ["common"]
|
||||
}
|
||||
}
|
|
@ -0,0 +1,169 @@
|
|||
## Overview
|
||||
|
||||
Observability solutions can register their navigation structures via the Observability plugin, this ensures that these navigation options display in the Observability page template component. This is a two part process, A) register your navigation structure and B) consume and render the shared page template component. These two elements are documented below.
|
||||
|
||||
## Navigation registration
|
||||
|
||||
To register a solution's navigation structure you'll first need to ensure your solution has the Shared Observability plugin specified as a dependency in your `kibana.json` file, e.g.
|
||||
|
||||
```json
|
||||
"requiredPlugins": [
|
||||
"sharedObservability"
|
||||
],
|
||||
```
|
||||
|
||||
Now within your solution's **public** plugin `setup` lifecycle method you can
|
||||
call the `registerSections` method, this will register your solution's specific
|
||||
navigation structure with the overall Observability navigation registry.
|
||||
|
||||
The `registerSections` function takes an `Observable` of an array of
|
||||
`NavigationSection`s. Each section can be defined as
|
||||
|
||||
```typescript
|
||||
export interface NavigationSection {
|
||||
// the label of the section, should be translated
|
||||
label: string | undefined;
|
||||
// the key to sort by in ascending order relative to other entries
|
||||
sortKey: number;
|
||||
// the entries to render inside the section
|
||||
entries: NavigationEntry[];
|
||||
}
|
||||
```
|
||||
|
||||
Each entry inside of a navigation section is defined as
|
||||
|
||||
```typescript
|
||||
export interface NavigationEntry {
|
||||
// the label of the menu entry, should be translated
|
||||
label: string;
|
||||
// the kibana app id
|
||||
app: string;
|
||||
// the path after the application prefix corresponding to this entry
|
||||
path: string;
|
||||
// whether to only match when the full path matches, defaults to `false`
|
||||
matchFullPath?: boolean;
|
||||
// whether to ignore trailing slashes, defaults to `true`
|
||||
ignoreTrailingSlash?: boolean;
|
||||
// shows NEW badge besides the navigation label, which will automatically disappear when menu item is clicked.
|
||||
isNewFeature?: boolean;
|
||||
// shows beta badge lab icon if the feature is still beta besides the navigation label
|
||||
isBeta?: boolean;
|
||||
}
|
||||
```
|
||||
|
||||
A registration might therefore look like the following:
|
||||
|
||||
```typescript
|
||||
// x-pack/plugins/example_plugin/public/plugin.ts
|
||||
|
||||
import { of } from 'rxjs';
|
||||
|
||||
export class Plugin implements PluginClass {
|
||||
constructor(_context: PluginInitializerContext) {}
|
||||
|
||||
setup(core: CoreSetup, plugins: PluginsSetup) {
|
||||
plugins.observabilityShared.navigation.registerSections(
|
||||
of([
|
||||
{
|
||||
label: 'A solution section',
|
||||
sortKey: 200,
|
||||
entries: [
|
||||
{ label: 'Home Page', app: 'exampleA', path: '/', matchFullPath: true },
|
||||
{ label: 'Example Page', app: 'exampleA', path: '/example' },
|
||||
{ label: 'Another Example Page', app: 'exampleA', path: '/another-example' },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Another solution section',
|
||||
sortKey: 300,
|
||||
entries: [{ label: 'Example page', app: 'exampleB', path: '/example' }],
|
||||
},
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
start() {}
|
||||
|
||||
stop() {}
|
||||
}
|
||||
```
|
||||
|
||||
Here `app` would match your solution - e.g. logs, metrics, APM, uptime etc. The registry is fully typed so please refer to the types for specific options.
|
||||
|
||||
Observables are used to facilitate changes over time, for example within the lifetime of your application a license type or set of user permissions may change and as such you may wish to change the navigation structure. If your navigation needs are simple you can pass a value and forget about it. **Solutions are expected to handle their own permissions, and what should or should not be displayed at any time**, the Observability plugin will not add and remove items for you.
|
||||
|
||||
The Observability navigation registry is now aware of your solution's navigation needs ✅
|
||||
|
||||
## Page template component
|
||||
|
||||
The shared page template component can be used to actually display and render all of the registered navigation structures within your solution.
|
||||
|
||||
The `start` contract of the public Observability plugin exposes a React component, under `navigation.PageTemplate`.
|
||||
|
||||
This can be accessed like so:
|
||||
|
||||
```
|
||||
const [coreStart, pluginsStart] = await core.getStartServices();
|
||||
const ObservabilityPageTemplate = pluginsStart.observabilityShared.navigation.PageTemplate;
|
||||
```
|
||||
|
||||
Now that you have access to the component you can render your solution's content using it.
|
||||
|
||||
```jsx
|
||||
<ObservabilityPageTemplate
|
||||
pageHeader={{
|
||||
pageTitle: SolutionPageTitle,
|
||||
rightSideItems: [
|
||||
// Just an example
|
||||
<DatePicker
|
||||
rangeFrom={relativeTime.start}
|
||||
rangeTo={relativeTime.end}
|
||||
refreshInterval={refreshInterval}
|
||||
refreshPaused={refreshPaused}
|
||||
/>,
|
||||
],
|
||||
}}
|
||||
>
|
||||
// Render anything you like here, this is just an example.
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem>// Content</EuiFlexItem>
|
||||
|
||||
<EuiFlexItem>// Content</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</ObservabilityPageTemplate>
|
||||
```
|
||||
|
||||
The `<ObservabilityPageTemplate />` component is a wrapper around the `<KibanaPageTemplate />` component (which in turn is a wrapper around the `<EuiPageTemplate>` component). As such the props mostly reflect those available on the wrapped components, again everything is fully typed so please refer to the types for specific options. The `pageSideBar` prop is handled by the component, and will take care of rendering out and managing the items from the registry.
|
||||
|
||||
After these two steps we should see something like the following (note the navigation on the left):
|
||||
|
||||

|
||||
|
||||
## Adding NEW badge
|
||||
|
||||
You can add a NEW badge beside the label by using the property `isNewFeature?: boolean;`.
|
||||
|
||||
```js
|
||||
setup(core: CoreSetup, plugins: PluginsSetup) {
|
||||
plugins.observabilityShared.navigation.registerSections(
|
||||
of([
|
||||
{
|
||||
label: 'A solution section',
|
||||
sortKey: 200,
|
||||
entries: [
|
||||
{ label: 'Backends', app: 'exampleA', path: '/example', isNewFeature: true },
|
||||
],
|
||||
}
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
```
|
||||
|
||||

|
||||
|
||||
The badge is going to be shown until user clicks on the menu item for the first time. Then we'll save an information at local storage, following this pattern `observability.nav_item_badge_visible_${app}${path}`, the above example would save `observability.nav_item_badge_visible_exampleA/example`. And the badge is removed. It'll only show again if the item saved at local storage is removed or set to `false`.
|
||||
|
||||
It's recommended to remove the badge (e.g. a new feature promotion) in the subsequent release.
|
||||
|
||||
To avoid the navigation flooding with badges, we also want to propose keeping it to maximum 2 active badges for every iteration
|
Binary file not shown.
After Width: | Height: | Size: 9.9 KiB |
|
@ -0,0 +1,39 @@
|
|||
/*
|
||||
* 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; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { combineLatest, map, Observable, ReplaySubject, scan, shareReplay, switchMap } from 'rxjs';
|
||||
import { NavigationSection } from '../page_template';
|
||||
|
||||
export interface NavigationRegistry {
|
||||
registerSections: (sections$: Observable<NavigationSection[]>) => void;
|
||||
sections$: Observable<NavigationSection[]>;
|
||||
}
|
||||
|
||||
export const createNavigationRegistry = (): NavigationRegistry => {
|
||||
const registeredSections$ = new ReplaySubject<Observable<NavigationSection[]>>();
|
||||
|
||||
const registerSections = (sections$: Observable<NavigationSection[]>) => {
|
||||
registeredSections$.next(sections$);
|
||||
};
|
||||
|
||||
const sections$: Observable<NavigationSection[]> = registeredSections$.pipe(
|
||||
scan(
|
||||
(accumulatedSections$, newSections) => accumulatedSections$.add(newSections),
|
||||
new Set<Observable<NavigationSection[]>>()
|
||||
),
|
||||
switchMap((registeredSections) => combineLatest([...registeredSections])),
|
||||
map((registeredSections) =>
|
||||
registeredSections.flat().sort((first, second) => first.sortKey - second.sortKey)
|
||||
),
|
||||
shareReplay(1)
|
||||
);
|
||||
|
||||
return {
|
||||
registerSections,
|
||||
sections$,
|
||||
};
|
||||
};
|
|
@ -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; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
export { createLazyObservabilityPageTemplate } from './lazy_page_template';
|
||||
export type { LazyObservabilityPageTemplateProps } from './lazy_page_template';
|
|
@ -0,0 +1,26 @@
|
|||
/*
|
||||
* 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; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import type {
|
||||
ObservabilityPageTemplateDependencies,
|
||||
WrappedPageTemplateProps,
|
||||
} from './page_template';
|
||||
|
||||
export const LazyObservabilityPageTemplate = React.lazy(() => import('./page_template'));
|
||||
|
||||
export type LazyObservabilityPageTemplateProps = WrappedPageTemplateProps;
|
||||
|
||||
export function createLazyObservabilityPageTemplate(
|
||||
injectedDeps: ObservabilityPageTemplateDependencies
|
||||
) {
|
||||
return (pageTemplateProps: LazyObservabilityPageTemplateProps) => (
|
||||
<React.Suspense fallback={null}>
|
||||
<LazyObservabilityPageTemplate {...pageTemplateProps} {...injectedDeps} />
|
||||
</React.Suspense>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,68 @@
|
|||
/*
|
||||
* 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; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import { EuiBadge } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
interface Props {
|
||||
label: string;
|
||||
localStorageId: string;
|
||||
}
|
||||
|
||||
const LabelContainer = styled.span`
|
||||
max-width: 72%;
|
||||
float: left;
|
||||
&:hover,
|
||||
&:focus {
|
||||
text-decoration: underline;
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledBadge = styled(EuiBadge)`
|
||||
margin-left: 8px;
|
||||
`;
|
||||
|
||||
/**
|
||||
* Gets current state from local storage to show or hide the badge.
|
||||
* Default value: true
|
||||
* @param localStorageId
|
||||
*/
|
||||
function getBadgeVisibility(localStorageId: string) {
|
||||
const storedItem = window.localStorage.getItem(localStorageId);
|
||||
if (storedItem) {
|
||||
return JSON.parse(storedItem) as boolean;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves on local storage that this item should no longer be visible
|
||||
* @param localStorageId
|
||||
*/
|
||||
export function hideBadge(localStorageId: string) {
|
||||
window.localStorage.setItem(localStorageId, JSON.stringify(false));
|
||||
}
|
||||
|
||||
export function NavNameWithBadge({ label, localStorageId }: Props) {
|
||||
const isBadgeVisible = getBadgeVisibility(localStorageId);
|
||||
return (
|
||||
<>
|
||||
<LabelContainer className="eui-textTruncate">
|
||||
<span>{label}</span>
|
||||
</LabelContainer>
|
||||
{isBadgeVisible && (
|
||||
<StyledBadge color="accent">
|
||||
{i18n.translate('xpack.observabilityShared.navigation.newBadge', {
|
||||
defaultMessage: 'NEW',
|
||||
})}
|
||||
</StyledBadge>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,47 @@
|
|||
/*
|
||||
* 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; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import React from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { EuiBetaBadge, EuiFlexGroup, EuiFlexItem, IconType } from '@elastic/eui';
|
||||
|
||||
interface Props {
|
||||
label?: string;
|
||||
isTechnicalPreview?: boolean;
|
||||
iconType?: IconType;
|
||||
}
|
||||
|
||||
export function NavNameWithBetaBadge({ label, iconType, isTechnicalPreview }: Props) {
|
||||
return (
|
||||
<EuiFlexGroup alignItems="center" gutterSize="s">
|
||||
<EuiFlexItem grow={false}>
|
||||
<span className="eui-textTruncate">
|
||||
<span>{label}</span>
|
||||
</span>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false} style={{ height: 20 }}>
|
||||
{isTechnicalPreview ? (
|
||||
<EuiBetaBadge
|
||||
color="hollow"
|
||||
size="s"
|
||||
label={i18n.translate('xpack.observabilityShared.navigation.experimentalBadgeLabel', {
|
||||
defaultMessage: 'Technical preview',
|
||||
})}
|
||||
iconType={iconType}
|
||||
/>
|
||||
) : (
|
||||
<EuiBetaBadge
|
||||
color="hollow"
|
||||
size="s"
|
||||
label={i18n.translate('xpack.observabilityShared.navigation.betaBadge', {
|
||||
defaultMessage: 'Beta',
|
||||
})}
|
||||
/>
|
||||
)}
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
}
|
Binary file not shown.
After Width: | Height: | Size: 304 KiB |
|
@ -0,0 +1,121 @@
|
|||
/*
|
||||
* 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; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { I18nProvider } from '@kbn/i18n-react';
|
||||
import { render } from '@testing-library/react';
|
||||
import { shallow } from 'enzyme';
|
||||
import React from 'react';
|
||||
import { of } from 'rxjs';
|
||||
import { getKibanaPageTemplateKibanaDependenciesMock as getPageTemplateServices } from '@kbn/shared-ux-page-kibana-template-mocks';
|
||||
import { guidedOnboardingMock } from '@kbn/guided-onboarding-plugin/public/mocks';
|
||||
|
||||
import { createLazyObservabilityPageTemplate } from './lazy_page_template';
|
||||
import { ObservabilityPageTemplate } from './page_template';
|
||||
import { createNavigationRegistry } from './helpers/navigation_registry';
|
||||
|
||||
jest.mock('react-router-dom', () => ({
|
||||
...jest.requireActual('react-router-dom'),
|
||||
useLocation: () => ({
|
||||
pathname: '/test-path',
|
||||
}),
|
||||
}));
|
||||
|
||||
const navigationRegistry = createNavigationRegistry();
|
||||
|
||||
navigationRegistry.registerSections(
|
||||
of([
|
||||
{
|
||||
label: 'Test A',
|
||||
sortKey: 100,
|
||||
entries: [
|
||||
{ label: 'Section A Url A', app: 'TestA', path: '/url-a' },
|
||||
{ label: 'Section A Url B', app: 'TestA', path: '/url-b' },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Test B',
|
||||
sortKey: 200,
|
||||
entries: [
|
||||
{ label: 'Section B Url A', app: 'TestB', path: '/url-a' },
|
||||
{ label: 'Section B Url B', app: 'TestB', path: '/url-b' },
|
||||
],
|
||||
},
|
||||
])
|
||||
);
|
||||
|
||||
describe('Page template', () => {
|
||||
it('Provides a working lazy wrapper', () => {
|
||||
const LazyObservabilityPageTemplate = createLazyObservabilityPageTemplate({
|
||||
currentAppId$: of('Test app ID'),
|
||||
getUrlForApp: () => '/test-url',
|
||||
navigateToApp: async () => {},
|
||||
navigationSections$: navigationRegistry.sections$,
|
||||
getPageTemplateServices,
|
||||
guidedOnboardingApi: guidedOnboardingMock.createStart().guidedOnboardingApi,
|
||||
});
|
||||
|
||||
const component = shallow(
|
||||
<LazyObservabilityPageTemplate
|
||||
pageHeader={{
|
||||
pageTitle: 'Test title',
|
||||
rightSideItems: [<span>Test side item</span>],
|
||||
}}
|
||||
>
|
||||
<div>Test structure</div>
|
||||
</LazyObservabilityPageTemplate>
|
||||
);
|
||||
|
||||
expect(component.exists('lazy')).toBe(true);
|
||||
});
|
||||
|
||||
it('Utilises the KibanaPageTemplate for rendering', () => {
|
||||
const component = shallow(
|
||||
<ObservabilityPageTemplate
|
||||
currentAppId$={of('Test app ID')}
|
||||
getUrlForApp={() => '/test-url'}
|
||||
navigateToApp={async () => {}}
|
||||
navigationSections$={navigationRegistry.sections$}
|
||||
pageHeader={{
|
||||
pageTitle: 'Test title',
|
||||
rightSideItems: [<span>Test side item</span>],
|
||||
}}
|
||||
getPageTemplateServices={getPageTemplateServices}
|
||||
guidedOnboardingApi={guidedOnboardingMock.createStart().guidedOnboardingApi}
|
||||
>
|
||||
<div>Test structure</div>
|
||||
</ObservabilityPageTemplate>
|
||||
);
|
||||
|
||||
expect(component.is('KibanaPageTemplate'));
|
||||
});
|
||||
|
||||
it('Handles outputting the registered navigation structures within a side nav', () => {
|
||||
const { container } = render(
|
||||
<I18nProvider>
|
||||
<ObservabilityPageTemplate
|
||||
currentAppId$={of('Test app ID')}
|
||||
getUrlForApp={() => '/test-url'}
|
||||
navigateToApp={async () => {}}
|
||||
navigationSections$={navigationRegistry.sections$}
|
||||
pageHeader={{
|
||||
pageTitle: 'Test title',
|
||||
rightSideItems: [<span>Test side item</span>],
|
||||
}}
|
||||
getPageTemplateServices={getPageTemplateServices}
|
||||
guidedOnboardingApi={guidedOnboardingMock.createStart().guidedOnboardingApi}
|
||||
>
|
||||
<div>Test structure</div>
|
||||
</ObservabilityPageTemplate>
|
||||
</I18nProvider>
|
||||
);
|
||||
|
||||
expect(container).toHaveTextContent('Section A Url A');
|
||||
expect(container).toHaveTextContent('Section A Url B');
|
||||
expect(container).toHaveTextContent('Section B Url A');
|
||||
expect(container).toHaveTextContent('Section B Url B');
|
||||
});
|
||||
});
|
|
@ -0,0 +1,255 @@
|
|||
/*
|
||||
* 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; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { EuiSideNavItemType, EuiPageSectionProps, EuiErrorBoundary } from '@elastic/eui';
|
||||
import { _EuiPageBottomBarProps } from '@elastic/eui/src/components/page_template/bottom_bar/page_bottom_bar';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import React, { useMemo } from 'react';
|
||||
import { matchPath, useLocation } from 'react-router-dom';
|
||||
import useObservable from 'react-use/lib/useObservable';
|
||||
import type { Observable } from 'rxjs';
|
||||
import type { ApplicationStart } from '@kbn/core/public';
|
||||
import { useKibana } from '@kbn/kibana-react-plugin/public';
|
||||
import {
|
||||
KibanaPageTemplate,
|
||||
KibanaPageTemplateKibanaProvider,
|
||||
} from '@kbn/shared-ux-page-kibana-template';
|
||||
import type {
|
||||
KibanaPageTemplateProps,
|
||||
KibanaPageTemplateKibanaDependencies,
|
||||
} from '@kbn/shared-ux-page-kibana-template';
|
||||
import { GuidedOnboardingPluginStart } from '@kbn/guided-onboarding-plugin/public';
|
||||
import { ObservabilityTour } from '../tour';
|
||||
import { NavNameWithBadge, hideBadge } from './nav_name_with_badge';
|
||||
import { NavNameWithBetaBadge } from './nav_name_with_beta_badge';
|
||||
|
||||
export type WrappedPageTemplateProps = Pick<
|
||||
KibanaPageTemplateProps,
|
||||
| 'children'
|
||||
| 'data-test-subj'
|
||||
| 'paddingSize'
|
||||
| 'pageHeader'
|
||||
| 'restrictWidth'
|
||||
| 'isEmptyState'
|
||||
| 'noDataConfig'
|
||||
> & {
|
||||
showSolutionNav?: boolean;
|
||||
isPageDataLoaded?: boolean;
|
||||
pageSectionProps?: EuiPageSectionProps;
|
||||
bottomBar?: React.ReactNode;
|
||||
bottomBarProps?: _EuiPageBottomBarProps;
|
||||
};
|
||||
|
||||
export interface NavigationEntry {
|
||||
// the label of the menu entry, should be translated
|
||||
label: string;
|
||||
// the kibana app id
|
||||
app: string;
|
||||
// the path after the application prefix corresponding to this entry
|
||||
path: string;
|
||||
// whether to only match when the full path matches, defaults to `false`
|
||||
matchFullPath?: boolean;
|
||||
// whether to ignore trailing slashes, defaults to `true`
|
||||
ignoreTrailingSlash?: boolean;
|
||||
// handler to be called when the item is clicked
|
||||
onClick?: (event: React.MouseEvent<HTMLElement | HTMLButtonElement, MouseEvent>) => void;
|
||||
// shows NEW badge besides the navigation label, which will automatically disappear when menu item is clicked.
|
||||
isNewFeature?: boolean;
|
||||
// shows technical preview lab icon if the feature is still in technical preview besides the navigation label
|
||||
isTechnicalPreview?: boolean;
|
||||
// shows beta badge besides the navigation label
|
||||
isBetaFeature?: boolean;
|
||||
// override default path matching logic to determine if nav entry is selected
|
||||
matchPath?: (path: string) => boolean;
|
||||
}
|
||||
|
||||
export interface NavigationSection {
|
||||
// the label of the section, should be translated
|
||||
label: string | undefined;
|
||||
// the key to sort by in ascending order relative to other entries
|
||||
sortKey: number;
|
||||
// the entries to render inside the section
|
||||
entries: NavigationEntry[];
|
||||
// shows beta badge besides the navigation label
|
||||
isBetaFeature?: boolean;
|
||||
}
|
||||
|
||||
export interface ObservabilityPageTemplateDependencies {
|
||||
currentAppId$: Observable<string | undefined>;
|
||||
getUrlForApp: ApplicationStart['getUrlForApp'];
|
||||
navigateToApp: ApplicationStart['navigateToApp'];
|
||||
navigationSections$: Observable<NavigationSection[]>;
|
||||
getPageTemplateServices: () => KibanaPageTemplateKibanaDependencies;
|
||||
guidedOnboardingApi: GuidedOnboardingPluginStart['guidedOnboardingApi'];
|
||||
}
|
||||
|
||||
export type ObservabilityPageTemplateProps = ObservabilityPageTemplateDependencies &
|
||||
WrappedPageTemplateProps;
|
||||
|
||||
export function ObservabilityPageTemplate({
|
||||
children,
|
||||
currentAppId$,
|
||||
getUrlForApp,
|
||||
navigateToApp,
|
||||
navigationSections$,
|
||||
showSolutionNav = true,
|
||||
isPageDataLoaded = true,
|
||||
getPageTemplateServices,
|
||||
bottomBar,
|
||||
bottomBarProps,
|
||||
pageSectionProps,
|
||||
guidedOnboardingApi,
|
||||
...pageTemplateProps
|
||||
}: ObservabilityPageTemplateProps): React.ReactElement | null {
|
||||
const sections = useObservable(navigationSections$, []);
|
||||
const currentAppId = useObservable(currentAppId$, undefined);
|
||||
const { pathname: currentPath } = useLocation();
|
||||
|
||||
const { services } = useKibana();
|
||||
|
||||
const sideNavItems = useMemo<Array<EuiSideNavItemType<unknown>>>(
|
||||
() =>
|
||||
sections.map(({ label, entries, isBetaFeature }, sectionIndex) => ({
|
||||
id: `${sectionIndex}`,
|
||||
name: isBetaFeature ? <NavNameWithBetaBadge label={label} /> : label,
|
||||
items: entries.map((entry, entryIndex) => {
|
||||
const href = getUrlForApp(entry.app, {
|
||||
path: entry.path,
|
||||
});
|
||||
|
||||
const isSelected =
|
||||
entry.app === currentAppId &&
|
||||
(entry.matchPath
|
||||
? entry.matchPath(currentPath)
|
||||
: matchPath(currentPath, {
|
||||
path: entry.path,
|
||||
exact: !!entry.matchFullPath,
|
||||
strict: !entry.ignoreTrailingSlash,
|
||||
}) != null);
|
||||
const badgeLocalStorageId = `observability.nav_item_badge_visible_${entry.app}${entry.path}`;
|
||||
const navId = entry.label.toLowerCase().split(' ').join('_');
|
||||
return {
|
||||
id: `${sectionIndex}.${entryIndex}`,
|
||||
name: entry.isBetaFeature ? (
|
||||
<NavNameWithBetaBadge label={entry.label} />
|
||||
) : entry.isNewFeature ? (
|
||||
<NavNameWithBadge label={entry.label} localStorageId={badgeLocalStorageId} />
|
||||
) : entry.isTechnicalPreview ? (
|
||||
<NavNameWithBetaBadge
|
||||
label={entry.label}
|
||||
iconType="beaker"
|
||||
isTechnicalPreview={true}
|
||||
/>
|
||||
) : (
|
||||
entry.label
|
||||
),
|
||||
href,
|
||||
isSelected,
|
||||
'data-nav-id': navId,
|
||||
'data-test-subj': `observability-nav-${entry.app}-${navId}`,
|
||||
onClick: (event) => {
|
||||
if (entry.onClick) {
|
||||
entry.onClick(event);
|
||||
}
|
||||
|
||||
// Hides NEW badge when the item is clicked
|
||||
if (entry.isNewFeature) {
|
||||
hideBadge(badgeLocalStorageId);
|
||||
}
|
||||
|
||||
if (
|
||||
event.button !== 0 ||
|
||||
event.defaultPrevented ||
|
||||
event.metaKey ||
|
||||
event.altKey ||
|
||||
event.ctrlKey ||
|
||||
event.shiftKey
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
navigateToApp(entry.app, {
|
||||
path: entry.path,
|
||||
});
|
||||
},
|
||||
};
|
||||
}),
|
||||
})),
|
||||
[currentAppId, currentPath, getUrlForApp, navigateToApp, sections]
|
||||
);
|
||||
|
||||
return (
|
||||
<KibanaPageTemplateKibanaProvider {...getPageTemplateServices()}>
|
||||
<ObservabilityTour
|
||||
navigateToApp={navigateToApp}
|
||||
prependBasePath={services?.http?.basePath.prepend}
|
||||
guidedOnboardingApi={guidedOnboardingApi}
|
||||
isPageDataLoaded={isPageDataLoaded}
|
||||
// The tour is dependent on the solution nav, and should not render if it is not visible
|
||||
showTour={showSolutionNav}
|
||||
>
|
||||
{({ isTourVisible }) => {
|
||||
return (
|
||||
<KibanaPageTemplate
|
||||
restrictWidth={false}
|
||||
{...pageTemplateProps}
|
||||
solutionNav={
|
||||
showSolutionNav
|
||||
? {
|
||||
icon: 'logoObservability',
|
||||
items: sideNavItems,
|
||||
name: sideNavTitle,
|
||||
// Only false if tour is active
|
||||
canBeCollapsed: isTourVisible === false,
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
<EuiErrorBoundary>
|
||||
<KibanaPageTemplate.Section
|
||||
component="div"
|
||||
alignment={pageTemplateProps.isEmptyState ? 'center' : 'top'}
|
||||
{...pageSectionProps}
|
||||
>
|
||||
{children}
|
||||
</KibanaPageTemplate.Section>
|
||||
</EuiErrorBoundary>
|
||||
{bottomBar && (
|
||||
<KibanaPageTemplate.BottomBar {...bottomBarProps}>
|
||||
{bottomBar}
|
||||
</KibanaPageTemplate.BottomBar>
|
||||
)}
|
||||
</KibanaPageTemplate>
|
||||
);
|
||||
}}
|
||||
</ObservabilityTour>
|
||||
</KibanaPageTemplateKibanaProvider>
|
||||
);
|
||||
}
|
||||
|
||||
// for lazy import
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default ObservabilityPageTemplate;
|
||||
|
||||
const sideNavTitle = i18n.translate('xpack.observabilityShared.pageLayout.sideNavTitle', {
|
||||
defaultMessage: 'Observability',
|
||||
});
|
||||
|
||||
export const LazyObservabilityPageTemplate = React.lazy(() => import('./page_template'));
|
||||
|
||||
export type LazyObservabilityPageTemplateProps = WrappedPageTemplateProps;
|
||||
|
||||
export function createLazyObservabilityPageTemplate(
|
||||
injectedDeps: ObservabilityPageTemplateDependencies
|
||||
) {
|
||||
return (pageTemplateProps: LazyObservabilityPageTemplateProps) => (
|
||||
<React.Suspense fallback={null}>
|
||||
<LazyObservabilityPageTemplate {...pageTemplateProps} {...injectedDeps} />
|
||||
</React.Suspense>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
/*
|
||||
* 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; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
export { ObservabilityTour, observTourStepStorageKey, useObservabilityTourContext } from './tour';
|
|
@ -0,0 +1,125 @@
|
|||
/*
|
||||
* 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; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { EuiTourStepProps, ElementTarget } from '@elastic/eui';
|
||||
|
||||
interface TourStep {
|
||||
content: string;
|
||||
anchor: ElementTarget;
|
||||
anchorPosition: EuiTourStepProps['anchorPosition'];
|
||||
title: EuiTourStepProps['title'];
|
||||
dataTestSubj: string;
|
||||
offset?: number;
|
||||
imageConfig?: {
|
||||
name: string;
|
||||
altText: string;
|
||||
};
|
||||
}
|
||||
|
||||
export const tourStepsConfig: TourStep[] = [
|
||||
{
|
||||
title: i18n.translate('xpack.observabilityShared.tour.observabilityOverviewStep.tourTitle', {
|
||||
defaultMessage: 'Welcome to Elastic Observability',
|
||||
}),
|
||||
content: i18n.translate(
|
||||
'xpack.observabilityShared.tour.observabilityOverviewStep.tourContent',
|
||||
{
|
||||
defaultMessage:
|
||||
'Take a quick tour to learn the benefits of having all of your observability data in one stack.',
|
||||
}
|
||||
),
|
||||
anchor: `[id^="SolutionNav"]`,
|
||||
anchorPosition: 'rightUp',
|
||||
dataTestSubj: 'overviewStep',
|
||||
},
|
||||
{
|
||||
title: i18n.translate('xpack.observabilityShared.tour.streamStep.tourTitle', {
|
||||
defaultMessage: 'Tail your logs in real time',
|
||||
}),
|
||||
content: i18n.translate('xpack.observabilityShared.tour.streamStep.tourContent', {
|
||||
defaultMessage:
|
||||
'Monitor, filter, and inspect log events flowing in from your applications, servers, virtual machines, and containers.',
|
||||
}),
|
||||
anchor: `[data-nav-id="stream"]`,
|
||||
anchorPosition: 'rightUp',
|
||||
dataTestSubj: 'streamStep',
|
||||
imageConfig: {
|
||||
name: 'onboarding_tour_step_logs.gif',
|
||||
altText: i18n.translate('xpack.observabilityShared.tour.streamStep.imageAltText', {
|
||||
defaultMessage: 'Logs stream demonstration',
|
||||
}),
|
||||
},
|
||||
},
|
||||
{
|
||||
title: i18n.translate('xpack.observabilityShared.tour.metricsExplorerStep.tourTitle', {
|
||||
defaultMessage: 'Monitor your infrastructure health',
|
||||
}),
|
||||
content: i18n.translate('xpack.observabilityShared.tour.metricsExplorerStep.tourContent', {
|
||||
defaultMessage:
|
||||
'Stream, group, and visualize metrics from your systems, cloud, network, and other infrastructure sources.',
|
||||
}),
|
||||
anchor: `[data-nav-id="metrics_explorer"]`,
|
||||
anchorPosition: 'rightUp',
|
||||
dataTestSubj: 'metricsExplorerStep',
|
||||
imageConfig: {
|
||||
name: 'onboarding_tour_step_metrics.gif',
|
||||
altText: i18n.translate('xpack.observabilityShared.tour.metricsExplorerStep.imageAltText', {
|
||||
defaultMessage: 'Metrics explorer demonstration',
|
||||
}),
|
||||
},
|
||||
},
|
||||
{
|
||||
title: i18n.translate('xpack.observabilityShared.tour.servicesStep.tourTitle', {
|
||||
defaultMessage: 'Identify and resolve application issues',
|
||||
}),
|
||||
content: i18n.translate('xpack.observabilityShared.tour.servicesStep.tourContent', {
|
||||
defaultMessage:
|
||||
'Find and fix performance problems quickly by collecting detailed information about your services.',
|
||||
}),
|
||||
anchor: `[data-nav-id="services"]`,
|
||||
anchorPosition: 'rightUp',
|
||||
dataTestSubj: 'servicesStep',
|
||||
imageConfig: {
|
||||
name: 'onboarding_tour_step_services.gif',
|
||||
altText: i18n.translate('xpack.observabilityShared.tour.servicesStep.imageAltText', {
|
||||
defaultMessage: 'Services demonstration',
|
||||
}),
|
||||
},
|
||||
},
|
||||
{
|
||||
title: i18n.translate('xpack.observabilityShared.tour.alertsStep.tourTitle', {
|
||||
defaultMessage: 'Get notified when something changes',
|
||||
}),
|
||||
content: i18n.translate('xpack.observabilityShared.tour.alertsStep.tourContent', {
|
||||
defaultMessage:
|
||||
'Define and detect conditions that trigger alerts with third-party platform integrations like email, PagerDuty, and Slack.',
|
||||
}),
|
||||
anchor: `[data-nav-id="alerts"]`,
|
||||
anchorPosition: 'rightUp',
|
||||
dataTestSubj: 'alertStep',
|
||||
imageConfig: {
|
||||
name: 'onboarding_tour_step_alerts.gif',
|
||||
altText: i18n.translate('xpack.observabilityShared.tour.alertsStep.imageAltText', {
|
||||
defaultMessage: 'Alerts demonstration',
|
||||
}),
|
||||
},
|
||||
},
|
||||
{
|
||||
title: i18n.translate('xpack.observabilityShared.tour.guidedSetupStep.tourTitle', {
|
||||
defaultMessage: 'Do more with Elastic Observability',
|
||||
}),
|
||||
content: i18n.translate('xpack.observabilityShared.tour.guidedSetupStep.tourContent', {
|
||||
defaultMessage:
|
||||
'The easiest way to continue with Elastic Observability is to follow recommended next steps in the data assistant.',
|
||||
}),
|
||||
anchor: '#guidedSetupButton',
|
||||
anchorPosition: 'rightUp',
|
||||
dataTestSubj: 'guidedSetupStep',
|
||||
offset: 10,
|
||||
},
|
||||
];
|
|
@ -0,0 +1,246 @@
|
|||
/*
|
||||
* 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; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, {
|
||||
ReactNode,
|
||||
useState,
|
||||
useCallback,
|
||||
useEffect,
|
||||
createContext,
|
||||
useContext,
|
||||
} from 'react';
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import {
|
||||
EuiButton,
|
||||
EuiButtonEmpty,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiTourStep,
|
||||
EuiTourStepProps,
|
||||
EuiImage,
|
||||
EuiSpacer,
|
||||
EuiText,
|
||||
useIsWithinBreakpoints,
|
||||
} from '@elastic/eui';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { ApplicationStart } from '@kbn/core/public';
|
||||
import useObservable from 'react-use/lib/useObservable';
|
||||
import { of } from 'rxjs';
|
||||
import type { GuidedOnboardingApi } from '@kbn/guided-onboarding-plugin/public/types';
|
||||
import { observabilityAppId } from '../../../common';
|
||||
import { tourStepsConfig } from './steps_config';
|
||||
|
||||
const minWidth: EuiTourStepProps['minWidth'] = 360;
|
||||
const maxWidth: EuiTourStepProps['maxWidth'] = 360;
|
||||
const offset: EuiTourStepProps['offset'] = 30;
|
||||
const repositionOnScroll: EuiTourStepProps['repositionOnScroll'] = true;
|
||||
|
||||
const overviewPath = '/overview';
|
||||
const dataAssistantStep = 6;
|
||||
|
||||
export const observTourStepStorageKey = 'guidedOnboarding.observability.tourStep';
|
||||
|
||||
const getSteps = ({
|
||||
activeStep,
|
||||
incrementStep,
|
||||
endTour,
|
||||
prependBasePath,
|
||||
}: {
|
||||
activeStep: number;
|
||||
incrementStep: () => void;
|
||||
endTour: () => void;
|
||||
prependBasePath?: (imageName: string) => string;
|
||||
}) => {
|
||||
const footerAction = (
|
||||
<EuiFlexGroup gutterSize="s" alignItems="baseline">
|
||||
<EuiFlexItem>
|
||||
<EuiButtonEmpty
|
||||
onClick={() => endTour()}
|
||||
size="xs"
|
||||
color="text"
|
||||
// Used for testing and to track FS usage
|
||||
data-test-subj="onboarding--observTourSkipButton"
|
||||
>
|
||||
{i18n.translate('xpack.observabilityShared.tour.skipButtonLabel', {
|
||||
defaultMessage: 'Skip tour',
|
||||
})}
|
||||
</EuiButtonEmpty>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiButton
|
||||
onClick={() => incrementStep()}
|
||||
size="s"
|
||||
color="success"
|
||||
// Used for testing and to track FS usage
|
||||
data-test-subj="onboarding--observTourNextStepButton"
|
||||
>
|
||||
{i18n.translate('xpack.observabilityShared.tour.nextButtonLabel', {
|
||||
defaultMessage: 'Next',
|
||||
})}
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
|
||||
const lastStepFooterAction = (
|
||||
// data-test-subj is used for testing and to track FS usage
|
||||
<EuiButtonEmpty
|
||||
size="xs"
|
||||
color="text"
|
||||
onClick={() => endTour()}
|
||||
data-test-subj="onboarding--observTourEndButton"
|
||||
>
|
||||
{i18n.translate('xpack.observabilityShared.tour.endButtonLabel', {
|
||||
defaultMessage: 'End tour',
|
||||
})}
|
||||
</EuiButtonEmpty>
|
||||
);
|
||||
|
||||
return tourStepsConfig.map((stepConfig, index) => {
|
||||
const step = index + 1;
|
||||
const { dataTestSubj, content, offset: stepOffset, imageConfig, ...tourStepProps } = stepConfig;
|
||||
return (
|
||||
<EuiTourStep
|
||||
{...tourStepProps}
|
||||
key={step}
|
||||
step={step}
|
||||
minWidth={minWidth}
|
||||
maxWidth={maxWidth}
|
||||
offset={stepOffset ?? offset}
|
||||
repositionOnScroll={repositionOnScroll}
|
||||
stepsTotal={tourStepsConfig.length}
|
||||
isStepOpen={step === activeStep}
|
||||
onFinish={() => endTour()}
|
||||
footerAction={activeStep === tourStepsConfig.length ? lastStepFooterAction : footerAction}
|
||||
panelProps={{
|
||||
'data-test-subj': dataTestSubj,
|
||||
}}
|
||||
content={
|
||||
<>
|
||||
<EuiText size="s">
|
||||
<p>{content}</p>
|
||||
</EuiText>
|
||||
{imageConfig && prependBasePath && (
|
||||
<>
|
||||
<EuiSpacer size="m" />
|
||||
<EuiImage
|
||||
alt={imageConfig.altText}
|
||||
src={prependBasePath(`/plugins/observability/assets/${imageConfig.name}`)}
|
||||
size="fullWidth"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
}
|
||||
/>
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
export interface ObservabilityTourContextValue {
|
||||
endTour: () => void;
|
||||
isTourVisible: boolean;
|
||||
}
|
||||
|
||||
const ObservabilityTourContext = createContext<ObservabilityTourContextValue>({
|
||||
endTour: () => {},
|
||||
isTourVisible: false,
|
||||
} as ObservabilityTourContextValue);
|
||||
|
||||
export function ObservabilityTour({
|
||||
children,
|
||||
navigateToApp,
|
||||
isPageDataLoaded,
|
||||
showTour,
|
||||
prependBasePath,
|
||||
guidedOnboardingApi,
|
||||
}: {
|
||||
children: ({ isTourVisible }: { isTourVisible: boolean }) => ReactNode;
|
||||
navigateToApp: ApplicationStart['navigateToApp'];
|
||||
isPageDataLoaded: boolean;
|
||||
showTour: boolean;
|
||||
prependBasePath?: (imageName: string) => string;
|
||||
guidedOnboardingApi?: GuidedOnboardingApi;
|
||||
}) {
|
||||
const prevActiveStep = localStorage.getItem(observTourStepStorageKey);
|
||||
const initialActiveStep = prevActiveStep === null ? 1 : Number(prevActiveStep);
|
||||
|
||||
const isGuidedOnboardingActive = useObservable(
|
||||
// if guided onboarding is not available, return false
|
||||
guidedOnboardingApi
|
||||
? guidedOnboardingApi.isGuideStepActive$('kubernetes', 'tour_observability')
|
||||
: of(false)
|
||||
);
|
||||
|
||||
const [isTourActive, setIsTourActive] = useState(false);
|
||||
const [activeStep, setActiveStep] = useState(initialActiveStep);
|
||||
|
||||
const { pathname: currentPath } = useLocation();
|
||||
|
||||
const isSmallBreakpoint = useIsWithinBreakpoints(['s']);
|
||||
|
||||
const isOverviewPage = currentPath === overviewPath;
|
||||
|
||||
const incrementStep = useCallback(() => {
|
||||
setActiveStep((prevState) => prevState + 1);
|
||||
}, []);
|
||||
|
||||
const endTour = useCallback(async () => {
|
||||
// Mark the onboarding guide step as complete
|
||||
if (guidedOnboardingApi) {
|
||||
await guidedOnboardingApi.completeGuideStep('kubernetes', 'tour_observability');
|
||||
}
|
||||
// Reset EuiTour step state
|
||||
setActiveStep(1);
|
||||
}, [guidedOnboardingApi]);
|
||||
|
||||
/**
|
||||
* The tour should only be visible if the following conditions are met:
|
||||
* - Only pages with the side nav should show the tour (showTour === true)
|
||||
* - Tour is set to active per the guided onboarding service (isTourActive === true)
|
||||
* - Any page data must be loaded in order for the tour to render correctly
|
||||
* - The tour should only render on medium-large screens
|
||||
*/
|
||||
const isTourVisible = showTour && isTourActive && isPageDataLoaded && isSmallBreakpoint === false;
|
||||
|
||||
const context: ObservabilityTourContextValue = { endTour, isTourVisible };
|
||||
|
||||
useEffect(() => {
|
||||
localStorage.setItem(observTourStepStorageKey, String(activeStep));
|
||||
}, [activeStep]);
|
||||
|
||||
useEffect(() => {
|
||||
setIsTourActive(Boolean(isGuidedOnboardingActive));
|
||||
}, [isGuidedOnboardingActive]);
|
||||
|
||||
useEffect(() => {
|
||||
// The user must be on the overview page to view the data assistant step in the tour
|
||||
if (isTourActive && isOverviewPage === false && activeStep === dataAssistantStep) {
|
||||
navigateToApp(observabilityAppId, {
|
||||
path: overviewPath,
|
||||
});
|
||||
}
|
||||
}, [activeStep, isOverviewPage, isTourActive, navigateToApp]);
|
||||
|
||||
return (
|
||||
<ObservabilityTourContext.Provider value={context}>
|
||||
<>
|
||||
{children({ isTourVisible })}
|
||||
{isTourVisible && getSteps({ activeStep, incrementStep, endTour, prependBasePath })}
|
||||
</>
|
||||
</ObservabilityTourContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export const useObservabilityTourContext = (): ObservabilityTourContextValue => {
|
||||
const ctx = useContext(ObservabilityTourContext);
|
||||
if (!ctx) {
|
||||
throw new Error('useObservabilityTourContext can only be called inside of TourContext');
|
||||
}
|
||||
return ctx;
|
||||
};
|
26
x-pack/plugins/observability_shared/public/index.ts
Normal file
26
x-pack/plugins/observability_shared/public/index.ts
Normal file
|
@ -0,0 +1,26 @@
|
|||
/*
|
||||
* 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; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { ObservabilitySharedPlugin } from './plugin';
|
||||
export type {
|
||||
ObservabilitySharedPlugin,
|
||||
ObservabilitySharedPluginSetup,
|
||||
ObservabilitySharedPluginStart,
|
||||
} from './plugin';
|
||||
|
||||
export type {
|
||||
ObservabilityPageTemplateProps,
|
||||
LazyObservabilityPageTemplateProps,
|
||||
} from './components/page_template/page_template';
|
||||
|
||||
export type { NavigationEntry } from './components/page_template/page_template';
|
||||
|
||||
export const plugin = () => {
|
||||
return new ObservabilitySharedPlugin();
|
||||
};
|
||||
|
||||
export { observabilityFeatureId, casesFeatureId, sloFeatureId } from '../common';
|
53
x-pack/plugins/observability_shared/public/plugin.ts
Normal file
53
x-pack/plugins/observability_shared/public/plugin.ts
Normal file
|
@ -0,0 +1,53 @@
|
|||
/*
|
||||
* 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; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import type { CoreStart, Plugin } from '@kbn/core/public';
|
||||
import type { GuidedOnboardingPluginStart } from '@kbn/guided-onboarding-plugin/public';
|
||||
import { createNavigationRegistry } from './components/page_template/helpers/navigation_registry';
|
||||
import { createLazyObservabilityPageTemplate } from './components/page_template';
|
||||
import { updateGlobalNavigation } from './services/update_global_navigation';
|
||||
|
||||
export interface ObservabilitySharedStart {
|
||||
guidedOnboarding: GuidedOnboardingPluginStart;
|
||||
}
|
||||
|
||||
export type ObservabilitySharedPluginSetup = ReturnType<ObservabilitySharedPlugin['setup']>;
|
||||
export type ObservabilitySharedPluginStart = ReturnType<ObservabilitySharedPlugin['start']>;
|
||||
|
||||
export class ObservabilitySharedPlugin implements Plugin {
|
||||
private readonly navigationRegistry = createNavigationRegistry();
|
||||
|
||||
public setup() {
|
||||
return {
|
||||
navigation: {
|
||||
registerSections: this.navigationRegistry.registerSections,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
public start(core: CoreStart, plugins: ObservabilitySharedStart) {
|
||||
const { application } = core;
|
||||
|
||||
const PageTemplate = createLazyObservabilityPageTemplate({
|
||||
currentAppId$: application.currentAppId$,
|
||||
getUrlForApp: application.getUrlForApp,
|
||||
navigateToApp: application.navigateToApp,
|
||||
navigationSections$: this.navigationRegistry.sections$,
|
||||
guidedOnboardingApi: plugins.guidedOnboarding.guidedOnboardingApi,
|
||||
getPageTemplateServices: () => ({ coreStart: core }),
|
||||
});
|
||||
|
||||
return {
|
||||
navigation: {
|
||||
PageTemplate,
|
||||
},
|
||||
updateGlobalNavigation,
|
||||
};
|
||||
}
|
||||
|
||||
public stop() {}
|
||||
}
|
|
@ -0,0 +1,236 @@
|
|||
/*
|
||||
* 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; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { Subject } from 'rxjs';
|
||||
import { App, AppDeepLink, ApplicationStart, AppNavLinkStatus, AppUpdater } from '@kbn/core/public';
|
||||
import { casesFeatureId, sloFeatureId } from '../../common';
|
||||
import { updateGlobalNavigation } from './update_global_navigation';
|
||||
|
||||
// Used in updater callback
|
||||
const app = {} as unknown as App;
|
||||
|
||||
describe('updateGlobalNavigation', () => {
|
||||
describe('when no observability apps are enabled', () => {
|
||||
it('hides the overview link', () => {
|
||||
const capabilities = {
|
||||
navLinks: { apm: false, logs: false, metrics: false, uptime: false },
|
||||
} as unknown as ApplicationStart['capabilities'];
|
||||
const deepLinks: AppDeepLink[] = [];
|
||||
const callback = jest.fn();
|
||||
const updater$ = {
|
||||
next: (cb: AppUpdater) => callback(cb(app)),
|
||||
} as unknown as Subject<AppUpdater>;
|
||||
|
||||
updateGlobalNavigation({ capabilities, deepLinks, updater$ });
|
||||
|
||||
expect(callback).toHaveBeenCalledWith({
|
||||
deepLinks,
|
||||
navLinkStatus: AppNavLinkStatus.hidden,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when one observability app is enabled', () => {
|
||||
it('shows the overview link', () => {
|
||||
const capabilities = {
|
||||
navLinks: { apm: true, logs: false, metrics: false, uptime: false },
|
||||
} as unknown as ApplicationStart['capabilities'];
|
||||
const deepLinks: AppDeepLink[] = [];
|
||||
const callback = jest.fn();
|
||||
const updater$ = {
|
||||
next: (cb: AppUpdater) => callback(cb(app)),
|
||||
} as unknown as Subject<AppUpdater>;
|
||||
|
||||
updateGlobalNavigation({ capabilities, deepLinks, updater$ });
|
||||
|
||||
expect(callback).toHaveBeenCalledWith({
|
||||
deepLinks,
|
||||
navLinkStatus: AppNavLinkStatus.visible,
|
||||
});
|
||||
});
|
||||
|
||||
describe('when cases are enabled', () => {
|
||||
it('shows the cases deep link', () => {
|
||||
const capabilities = {
|
||||
[casesFeatureId]: { read_cases: true },
|
||||
navLinks: { apm: true, logs: false, metrics: false, uptime: false },
|
||||
} as unknown as ApplicationStart['capabilities'];
|
||||
|
||||
const caseRoute = {
|
||||
id: 'cases',
|
||||
title: 'Cases',
|
||||
order: 8003,
|
||||
path: '/cases',
|
||||
navLinkStatus: AppNavLinkStatus.hidden,
|
||||
};
|
||||
|
||||
const deepLinks = [caseRoute];
|
||||
|
||||
const callback = jest.fn();
|
||||
const updater$ = {
|
||||
next: (cb: AppUpdater) => callback(cb(app)),
|
||||
} as unknown as Subject<AppUpdater>;
|
||||
|
||||
updateGlobalNavigation({ capabilities, deepLinks, updater$ });
|
||||
|
||||
expect(callback).toHaveBeenCalledWith({
|
||||
deepLinks: [
|
||||
{
|
||||
...caseRoute,
|
||||
navLinkStatus: AppNavLinkStatus.visible,
|
||||
},
|
||||
],
|
||||
navLinkStatus: AppNavLinkStatus.visible,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('with no case read capabilities', () => {
|
||||
it('hides the cases deep link', () => {
|
||||
const capabilities = {
|
||||
[casesFeatureId]: { read_cases: false },
|
||||
navLinks: { apm: true, logs: false, metrics: false, uptime: false },
|
||||
} as unknown as ApplicationStart['capabilities'];
|
||||
|
||||
const caseRoute = {
|
||||
id: 'cases',
|
||||
title: 'Cases',
|
||||
order: 8003,
|
||||
path: '/cases',
|
||||
navLinkStatus: AppNavLinkStatus.hidden,
|
||||
};
|
||||
|
||||
const deepLinks = [caseRoute];
|
||||
|
||||
const callback = jest.fn();
|
||||
const updater$ = {
|
||||
next: (cb: AppUpdater) => callback(cb(app)),
|
||||
} as unknown as Subject<AppUpdater>;
|
||||
|
||||
updateGlobalNavigation({ capabilities, deepLinks, updater$ });
|
||||
|
||||
expect(callback).toHaveBeenCalledWith({
|
||||
deepLinks: [
|
||||
{
|
||||
...caseRoute,
|
||||
navLinkStatus: AppNavLinkStatus.hidden,
|
||||
},
|
||||
],
|
||||
navLinkStatus: AppNavLinkStatus.visible,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when alerts are enabled', () => {
|
||||
it('shows the alerts deep link', () => {
|
||||
const capabilities = {
|
||||
[casesFeatureId]: { read_cases: true },
|
||||
navLinks: { apm: true, logs: false, metrics: false, uptime: false },
|
||||
} as unknown as ApplicationStart['capabilities'];
|
||||
|
||||
const deepLinks = [
|
||||
{
|
||||
id: 'alerts',
|
||||
title: 'Alerts',
|
||||
order: 8001,
|
||||
path: '/alerts',
|
||||
navLinkStatus: AppNavLinkStatus.hidden,
|
||||
},
|
||||
];
|
||||
const callback = jest.fn();
|
||||
const updater$ = {
|
||||
next: (cb: AppUpdater) => callback(cb(app)),
|
||||
} as unknown as Subject<AppUpdater>;
|
||||
|
||||
updateGlobalNavigation({ capabilities, deepLinks, updater$ });
|
||||
|
||||
expect(callback).toHaveBeenCalledWith({
|
||||
deepLinks: [
|
||||
{
|
||||
id: 'alerts',
|
||||
title: 'Alerts',
|
||||
order: 8001,
|
||||
path: '/alerts',
|
||||
navLinkStatus: AppNavLinkStatus.visible,
|
||||
},
|
||||
],
|
||||
navLinkStatus: AppNavLinkStatus.visible,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("hides the slo link when the capabilities don't include it", () => {
|
||||
const capabilities = {
|
||||
navLinks: { apm: true, logs: false, metrics: false, uptime: false },
|
||||
} as unknown as ApplicationStart['capabilities'];
|
||||
|
||||
const sloRoute = {
|
||||
id: 'slos',
|
||||
title: 'SLOs',
|
||||
order: 8002,
|
||||
path: '/slos',
|
||||
navLinkStatus: AppNavLinkStatus.hidden,
|
||||
};
|
||||
|
||||
const deepLinks = [sloRoute];
|
||||
|
||||
const callback = jest.fn();
|
||||
const updater$ = {
|
||||
next: (cb: AppUpdater) => callback(cb(app)),
|
||||
} as unknown as Subject<AppUpdater>;
|
||||
|
||||
updateGlobalNavigation({ capabilities, deepLinks, updater$ });
|
||||
|
||||
expect(callback).toHaveBeenCalledWith({
|
||||
deepLinks: [
|
||||
{
|
||||
...sloRoute,
|
||||
navLinkStatus: AppNavLinkStatus.hidden,
|
||||
},
|
||||
],
|
||||
navLinkStatus: AppNavLinkStatus.visible,
|
||||
});
|
||||
});
|
||||
|
||||
describe('when slos are enabled', () => {
|
||||
it('shows the slos deep link', () => {
|
||||
const capabilities = {
|
||||
[casesFeatureId]: { read_cases: true },
|
||||
[sloFeatureId]: { read: true },
|
||||
navLinks: { apm: false, logs: false, metrics: false, uptime: false },
|
||||
} as unknown as ApplicationStart['capabilities'];
|
||||
|
||||
const sloRoute = {
|
||||
id: 'slos',
|
||||
title: 'SLOs',
|
||||
order: 8002,
|
||||
path: '/slos',
|
||||
navLinkStatus: AppNavLinkStatus.hidden,
|
||||
};
|
||||
|
||||
const deepLinks = [sloRoute];
|
||||
|
||||
const callback = jest.fn();
|
||||
const updater$ = {
|
||||
next: (cb: AppUpdater) => callback(cb(app)),
|
||||
} as unknown as Subject<AppUpdater>;
|
||||
|
||||
updateGlobalNavigation({ capabilities, deepLinks, updater$ });
|
||||
|
||||
expect(callback).toHaveBeenCalledWith({
|
||||
deepLinks: [
|
||||
{
|
||||
...sloRoute,
|
||||
navLinkStatus: AppNavLinkStatus.visible,
|
||||
},
|
||||
],
|
||||
navLinkStatus: AppNavLinkStatus.visible,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,69 @@
|
|||
/*
|
||||
* 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; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { Subject } from 'rxjs';
|
||||
import { AppNavLinkStatus, AppUpdater, ApplicationStart, AppDeepLink } from '@kbn/core/public';
|
||||
import { CasesDeepLinkId } from '@kbn/cases-plugin/public';
|
||||
import { casesFeatureId, sloFeatureId } from '../../common';
|
||||
|
||||
export function updateGlobalNavigation({
|
||||
capabilities,
|
||||
deepLinks,
|
||||
updater$,
|
||||
}: {
|
||||
capabilities: ApplicationStart['capabilities'];
|
||||
deepLinks: AppDeepLink[];
|
||||
updater$: Subject<AppUpdater>;
|
||||
}) {
|
||||
const { apm, logs, metrics, uptime } = capabilities.navLinks;
|
||||
const someVisible = Object.values({
|
||||
apm,
|
||||
logs,
|
||||
metrics,
|
||||
uptime,
|
||||
}).some((visible) => visible);
|
||||
|
||||
const updatedDeepLinks = deepLinks.map((link) => {
|
||||
switch (link.id) {
|
||||
case CasesDeepLinkId.cases:
|
||||
return {
|
||||
...link,
|
||||
navLinkStatus:
|
||||
capabilities[casesFeatureId].read_cases && someVisible
|
||||
? AppNavLinkStatus.visible
|
||||
: AppNavLinkStatus.hidden,
|
||||
};
|
||||
case 'alerts':
|
||||
return {
|
||||
...link,
|
||||
navLinkStatus: someVisible ? AppNavLinkStatus.visible : AppNavLinkStatus.hidden,
|
||||
};
|
||||
case 'rules':
|
||||
return {
|
||||
...link,
|
||||
navLinkStatus: someVisible ? AppNavLinkStatus.visible : AppNavLinkStatus.hidden,
|
||||
};
|
||||
case 'slos':
|
||||
return {
|
||||
...link,
|
||||
navLinkStatus: !!capabilities[sloFeatureId]?.read
|
||||
? AppNavLinkStatus.visible
|
||||
: AppNavLinkStatus.hidden,
|
||||
};
|
||||
default:
|
||||
return link;
|
||||
}
|
||||
});
|
||||
|
||||
updater$.next(() => ({
|
||||
deepLinks: updatedDeepLinks,
|
||||
navLinkStatus:
|
||||
someVisible || !!capabilities[sloFeatureId]?.read
|
||||
? AppNavLinkStatus.visible
|
||||
: AppNavLinkStatus.hidden,
|
||||
}));
|
||||
}
|
0
x-pack/plugins/observability_shared/public/types
Normal file
0
x-pack/plugins/observability_shared/public/types
Normal file
16
x-pack/plugins/observability_shared/scripts/storybook.js
Normal file
16
x-pack/plugins/observability_shared/scripts/storybook.js
Normal file
|
@ -0,0 +1,16 @@
|
|||
/*
|
||||
* 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; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { join } from 'path';
|
||||
|
||||
require('@kbn/storybook').runStorybookCli({
|
||||
name: 'observabilityShared',
|
||||
storyGlobs: [
|
||||
join(__dirname, '..', 'public', 'components', '**', '*.stories.tsx'),
|
||||
join(__dirname, '..', 'public', 'pages', '**', '*.stories.tsx'),
|
||||
],
|
||||
});
|
25
x-pack/plugins/observability_shared/tsconfig.json
Normal file
25
x-pack/plugins/observability_shared/tsconfig.json
Normal file
|
@ -0,0 +1,25 @@
|
|||
{
|
||||
"extends": "../../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "target/types"
|
||||
},
|
||||
"include": [
|
||||
"common/**/*",
|
||||
"public/**/*",
|
||||
"public/**/*.json",
|
||||
"server/**/*",
|
||||
"typings/**/*",
|
||||
"../../../typings/**/*"
|
||||
],
|
||||
"kbn_references": [
|
||||
"@kbn/core",
|
||||
"@kbn/kibana-react-plugin",
|
||||
"@kbn/cases-plugin",
|
||||
"@kbn/guided-onboarding-plugin",
|
||||
"@kbn/i18n",
|
||||
"@kbn/shared-ux-page-kibana-template",
|
||||
"@kbn/i18n-react",
|
||||
"@kbn/shared-ux-page-kibana-template-mocks",
|
||||
],
|
||||
"exclude": ["target/**/*"]
|
||||
}
|
18
x-pack/plugins/observability_shared/typings/common.ts
Normal file
18
x-pack/plugins/observability_shared/typings/common.ts
Normal file
|
@ -0,0 +1,18 @@
|
|||
/*
|
||||
* 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; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
export type ObservabilityApp =
|
||||
| 'infra_metrics'
|
||||
| 'infra_logs'
|
||||
| 'apm'
|
||||
// we will remove uptime in future to replace to be replace by synthetics
|
||||
| 'uptime'
|
||||
| 'synthetics'
|
||||
| 'observability-overview'
|
||||
| 'stack_monitoring'
|
||||
| 'ux'
|
||||
| 'fleet';
|
|
@ -4625,6 +4625,10 @@
|
|||
version "0.0.0"
|
||||
uid ""
|
||||
|
||||
"@kbn/observability-shared-plugin@link:x-pack/plugins/observability_shared":
|
||||
version "0.0.0"
|
||||
uid ""
|
||||
|
||||
"@kbn/oidc-provider-plugin@link:x-pack/test/security_api_integration/plugins/oidc_provider":
|
||||
version "0.0.0"
|
||||
uid ""
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue