mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[Serverless nav] Update footer + project settings cloud links (#161971)
This commit is contained in:
parent
af4a047006
commit
209d35365f
37 changed files with 1491 additions and 226 deletions
|
@ -10,9 +10,6 @@ xpack.fleet.internal.activeAgentsSoftLimit: 25000
|
|||
|
||||
# Cloud links
|
||||
xpack.cloud.base_url: "https://cloud.elastic.co"
|
||||
xpack.cloud.profile_url: "/user/settings"
|
||||
xpack.cloud.billing_url: "/billing"
|
||||
xpack.cloud.organization_url: "/account"
|
||||
|
||||
# Enable ZDT migration algorithm
|
||||
migrations.algorithm: zdt
|
||||
|
|
|
@ -140,7 +140,8 @@ export class ProjectNavigationService {
|
|||
const activeNodes = findActiveNodes(
|
||||
currentPathname,
|
||||
this.projectNavigationNavTreeFlattened,
|
||||
location
|
||||
location,
|
||||
this.http?.basePath.prepend
|
||||
);
|
||||
|
||||
// Each time we call findActiveNodes() we create a new array of activeNodes. As this array is used
|
||||
|
|
|
@ -254,6 +254,29 @@ describe('findActiveNodes', () => {
|
|||
]);
|
||||
});
|
||||
|
||||
test('should find active node at the root', () => {
|
||||
const flattendNavTree: Record<string, ChromeProjectNavigationNode> = {
|
||||
'[0]': {
|
||||
id: 'root',
|
||||
title: 'Root',
|
||||
deepLink: getDeepLink('root', `root`),
|
||||
path: ['root'],
|
||||
},
|
||||
};
|
||||
|
||||
expect(findActiveNodes(`/foo/root`, flattendNavTree)).toEqual([
|
||||
[
|
||||
{
|
||||
id: 'root',
|
||||
title: 'Root',
|
||||
isActive: true,
|
||||
deepLink: getDeepLink('root', `root`),
|
||||
path: ['root'],
|
||||
},
|
||||
],
|
||||
]);
|
||||
});
|
||||
|
||||
test('should match the longest matching node', () => {
|
||||
const flattendNavTree: Record<string, ChromeProjectNavigationNode> = {
|
||||
'[0]': {
|
||||
|
@ -345,7 +368,7 @@ describe('findActiveNodes', () => {
|
|||
id: 'item1',
|
||||
title: 'Item 1',
|
||||
path: ['root', 'item1'],
|
||||
getIsActive: (loc) => loc.pathname.startsWith('/foo'), // Should match
|
||||
getIsActive: ({ location }) => location.pathname.startsWith('/foo'), // Should match
|
||||
},
|
||||
'[0][2]': {
|
||||
id: 'item2',
|
||||
|
|
|
@ -73,9 +73,12 @@ function serializeDeeplinkUrl(url?: string) {
|
|||
* @param key The key to extract parent paths from
|
||||
* @returns An array of parent paths
|
||||
*/
|
||||
function extractParentPaths(key: string) {
|
||||
function extractParentPaths(key: string, navTree: Record<string, ChromeProjectNavigationNode>) {
|
||||
// Split the string on every '][' to get an array of values without the brackets.
|
||||
const arr = key.split('][');
|
||||
if (arr.length === 1) {
|
||||
return arr;
|
||||
}
|
||||
// Add the brackets back in for the first and last elements, and all elements in between.
|
||||
arr[0] = `${arr[0]}]`;
|
||||
arr[arr.length - 1] = `[${arr[arr.length - 1]}`;
|
||||
|
@ -83,10 +86,12 @@ function extractParentPaths(key: string) {
|
|||
arr[i] = `[${arr[i]}]`;
|
||||
}
|
||||
|
||||
return arr.reduce<string[]>((acc, currentValue, currentIndex) => {
|
||||
acc.push(arr.slice(0, currentIndex + 1).join(''));
|
||||
return acc;
|
||||
}, []);
|
||||
return arr
|
||||
.reduce<string[]>((acc, currentValue, currentIndex) => {
|
||||
acc.push(arr.slice(0, currentIndex + 1).join(''));
|
||||
return acc;
|
||||
}, [])
|
||||
.filter((k) => Boolean(navTree[k]));
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -101,7 +106,8 @@ function extractParentPaths(key: string) {
|
|||
export const findActiveNodes = (
|
||||
currentPathname: string,
|
||||
navTree: Record<string, ChromeProjectNavigationNode>,
|
||||
location?: Location
|
||||
location?: Location,
|
||||
prepend: (path: string) => string = (path) => path
|
||||
): ChromeProjectNavigationNode[][] => {
|
||||
const activeNodes: ChromeProjectNavigationNode[][] = [];
|
||||
const matches: string[][] = [];
|
||||
|
@ -113,9 +119,9 @@ export const findActiveNodes = (
|
|||
|
||||
Object.entries(navTree).forEach(([key, node]) => {
|
||||
if (node.getIsActive && location) {
|
||||
const isActive = node.getIsActive(location);
|
||||
const isActive = node.getIsActive({ pathNameSerialized: currentPathname, location, prepend });
|
||||
if (isActive) {
|
||||
const keysWithParents = extractParentPaths(key);
|
||||
const keysWithParents = extractParentPaths(key, navTree);
|
||||
activeNodes.push(keysWithParents.map(activeNodeFromKey));
|
||||
}
|
||||
return;
|
||||
|
@ -139,7 +145,7 @@ export const findActiveNodes = (
|
|||
if (matches.length > 0) {
|
||||
const longestMatch = matches[matches.length - 1];
|
||||
longestMatch.forEach((key) => {
|
||||
const keysWithParents = extractParentPaths(key);
|
||||
const keysWithParents = extractParentPaths(key, navTree);
|
||||
activeNodes.push(keysWithParents.map(activeNodeFromKey));
|
||||
});
|
||||
}
|
||||
|
|
|
@ -34,6 +34,7 @@ export type {
|
|||
ChromeUserBanner,
|
||||
ChromeProjectNavigation,
|
||||
ChromeProjectNavigationNode,
|
||||
CloudLinkId,
|
||||
SideNavCompProps,
|
||||
SideNavComponent,
|
||||
ChromeProjectBreadcrumb,
|
||||
|
|
|
@ -33,6 +33,7 @@ export type {
|
|||
ChromeProjectNavigationNode,
|
||||
AppDeepLinkId,
|
||||
AppId,
|
||||
CloudLinkId,
|
||||
SideNavCompProps,
|
||||
SideNavComponent,
|
||||
ChromeSetProjectBreadcrumbsParams,
|
||||
|
|
|
@ -44,6 +44,18 @@ export type AppDeepLinkId =
|
|||
| SearchLink
|
||||
| ObservabilityLink;
|
||||
|
||||
/** @public */
|
||||
export type CloudLinkId = 'userAndRoles' | 'performance' | 'billingAndSub';
|
||||
|
||||
export type GetIsActiveFn = (params: {
|
||||
/** The current path name including the basePath + hash value but **without** any query params */
|
||||
pathNameSerialized: string;
|
||||
/** The history Location */
|
||||
location: Location;
|
||||
/** Utiliy function to prepend a path with the basePath */
|
||||
prepend: (path: string) => string;
|
||||
}) => boolean;
|
||||
|
||||
/** @public */
|
||||
export interface ChromeProjectNavigationNode {
|
||||
/** Optional id, if not passed a "link" must be provided. */
|
||||
|
@ -69,7 +81,7 @@ export interface ChromeProjectNavigationNode {
|
|||
/**
|
||||
* Optional function to get the active state. This function is called whenever the location changes.
|
||||
*/
|
||||
getIsActive?: (location: Location) => boolean;
|
||||
getIsActive?: GetIsActiveFn;
|
||||
|
||||
/**
|
||||
* Optional flag to indicate if the breadcrumb should be hidden when this node is active.
|
||||
|
@ -123,6 +135,8 @@ export interface NodeDefinition<
|
|||
title?: string;
|
||||
/** App id or deeplink id */
|
||||
link?: LinkId;
|
||||
/** Cloud link id */
|
||||
cloudLink?: CloudLinkId;
|
||||
/** Optional icon for the navigation node. Note: not all navigation depth will render the icon */
|
||||
icon?: string;
|
||||
/** Optional children of the navigation node */
|
||||
|
@ -134,7 +148,7 @@ export interface NodeDefinition<
|
|||
/**
|
||||
* Optional function to get the active state. This function is called whenever the location changes.
|
||||
*/
|
||||
getIsActive?: (location: Location) => boolean;
|
||||
getIsActive?: GetIsActiveFn;
|
||||
|
||||
/**
|
||||
* Optional flag to indicate if the breadcrumb should be hidden when this node is active.
|
||||
|
|
|
@ -29,5 +29,19 @@ export const getServicesMock = ({
|
|||
navigateToUrl,
|
||||
onProjectNavigationChange: jest.fn(),
|
||||
activeNodes$: of(activeNodes),
|
||||
cloudLinks: {
|
||||
billingAndSub: {
|
||||
title: 'Mock Billing & Subscriptions',
|
||||
href: 'https://cloud.elastic.co/account/billing',
|
||||
},
|
||||
performance: {
|
||||
title: 'Mock Performance',
|
||||
href: 'https://cloud.elastic.co/deployments/123456789/performance',
|
||||
},
|
||||
userAndRoles: {
|
||||
title: 'Mock Users & Roles',
|
||||
href: 'https://cloud.elastic.co/deployments/123456789/security/users',
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
|
|
|
@ -28,6 +28,7 @@ const allNavLinks: AppDeepLinkId[] = [
|
|||
'discover',
|
||||
'fleet',
|
||||
'integrations',
|
||||
'management',
|
||||
'management:api_keys',
|
||||
'management:cases',
|
||||
'management:cross_cluster_replication',
|
||||
|
|
|
@ -44,6 +44,20 @@ export class StorybookMock extends AbstractStorybookMock<{}, NavigationServices>
|
|||
navLinks$: params.navLinks$ ?? new BehaviorSubject([]),
|
||||
onProjectNavigationChange: params.onProjectNavigationChange ?? (() => undefined),
|
||||
activeNodes$: new BehaviorSubject([]),
|
||||
cloudLinks: {
|
||||
billingAndSub: {
|
||||
title: 'Billing & Subscriptions',
|
||||
href: 'https://cloud.elastic.co/account/billing',
|
||||
},
|
||||
performance: {
|
||||
title: 'Performance',
|
||||
href: 'https://cloud.elastic.co/deployments/123456789/performance',
|
||||
},
|
||||
userAndRoles: {
|
||||
title: 'Users & Roles',
|
||||
href: 'https://cloud.elastic.co/deployments/123456789/security/users',
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
|
58
packages/shared-ux/chrome/navigation/src/cloud_links.tsx
Normal file
58
packages/shared-ux/chrome/navigation/src/cloud_links.tsx
Normal file
|
@ -0,0 +1,58 @@
|
|||
/*
|
||||
* 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 { i18n } from '@kbn/i18n';
|
||||
import type { CloudLinkId } from '@kbn/core-chrome-browser';
|
||||
import type { CloudStart } from '@kbn/cloud-plugin/public';
|
||||
|
||||
export type CloudLinks = {
|
||||
[id in CloudLinkId]?: {
|
||||
title: string;
|
||||
href: string;
|
||||
};
|
||||
};
|
||||
|
||||
export const getCloudLinks = (cloud: CloudStart): CloudLinks => {
|
||||
const { billingUrl, performanceUrl, usersAndRolesUrl } = cloud;
|
||||
|
||||
const links: CloudLinks = {};
|
||||
|
||||
if (usersAndRolesUrl) {
|
||||
links.userAndRoles = {
|
||||
title: i18n.translate(
|
||||
'sharedUXPackages.chrome.sideNavigation.cloudLinks.usersAndRolesLinkText',
|
||||
{
|
||||
defaultMessage: 'Users and roles',
|
||||
}
|
||||
),
|
||||
href: usersAndRolesUrl,
|
||||
};
|
||||
}
|
||||
|
||||
if (performanceUrl) {
|
||||
links.performance = {
|
||||
title: i18n.translate(
|
||||
'sharedUXPackages.chrome.sideNavigation.cloudLinks.performanceLinkText',
|
||||
{
|
||||
defaultMessage: 'Performance',
|
||||
}
|
||||
),
|
||||
href: performanceUrl,
|
||||
};
|
||||
}
|
||||
|
||||
if (billingUrl) {
|
||||
links.billingAndSub = {
|
||||
title: i18n.translate('sharedUXPackages.chrome.sideNavigation.cloudLinks.billingLinkText', {
|
||||
defaultMessage: 'Billing and subscription',
|
||||
}),
|
||||
href: billingUrl,
|
||||
};
|
||||
}
|
||||
|
||||
return links;
|
||||
};
|
|
@ -6,8 +6,9 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import React, { FC, useContext } from 'react';
|
||||
import React, { FC, useContext, useMemo } from 'react';
|
||||
import { NavigationKibanaDependencies, NavigationServices } from '../types';
|
||||
import { CloudLinks, getCloudLinks } from './cloud_links';
|
||||
|
||||
const Context = React.createContext<NavigationServices | null>(null);
|
||||
|
||||
|
@ -25,11 +26,13 @@ export const NavigationKibanaProvider: FC<NavigationKibanaDependencies> = ({
|
|||
children,
|
||||
...dependencies
|
||||
}) => {
|
||||
const { core, serverless } = dependencies;
|
||||
const { core, serverless, cloud } = dependencies;
|
||||
const { chrome, http } = core;
|
||||
const { basePath } = http;
|
||||
const { navigateToUrl } = core.application;
|
||||
|
||||
const cloudLinks: CloudLinks = useMemo(() => (cloud ? getCloudLinks(cloud) : {}), [cloud]);
|
||||
|
||||
const value: NavigationServices = {
|
||||
basePath,
|
||||
recentlyAccessed$: chrome.recentlyAccessed.get$(),
|
||||
|
@ -38,6 +41,7 @@ export const NavigationKibanaProvider: FC<NavigationKibanaDependencies> = ({
|
|||
navIsOpen: true,
|
||||
onProjectNavigationChange: serverless.setNavigation,
|
||||
activeNodes$: serverless.getActiveNavigationNodes$(),
|
||||
cloudLinks,
|
||||
};
|
||||
|
||||
return <Context.Provider value={value}>{children}</Context.Provider>;
|
||||
|
|
678
packages/shared-ux/chrome/navigation/src/ui/__snapshots__/default_navigation.test.tsx.snap
generated
Normal file
678
packages/shared-ux/chrome/navigation/src/ui/__snapshots__/default_navigation.test.tsx.snap
generated
Normal file
|
@ -0,0 +1,678 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`<DefaultNavigation /> builds the full navigation tree when only custom project is provided reading the title from config or deeplink 1`] = `
|
||||
Array [
|
||||
Object {
|
||||
"children": Array [
|
||||
Object {
|
||||
"children": undefined,
|
||||
"deepLink": undefined,
|
||||
"href": undefined,
|
||||
"id": "item1",
|
||||
"isActive": false,
|
||||
"path": Array [
|
||||
"group1",
|
||||
"item1",
|
||||
],
|
||||
"renderItem": undefined,
|
||||
"title": "Item 1",
|
||||
},
|
||||
Object {
|
||||
"children": undefined,
|
||||
"deepLink": Object {
|
||||
"baseUrl": "",
|
||||
"href": "",
|
||||
"id": "item2",
|
||||
"title": "Title from deeplink!",
|
||||
"url": "",
|
||||
},
|
||||
"href": undefined,
|
||||
"id": "item2",
|
||||
"isActive": false,
|
||||
"path": Array [
|
||||
"group1",
|
||||
"item2",
|
||||
],
|
||||
"renderItem": undefined,
|
||||
"title": "Title from deeplink!",
|
||||
},
|
||||
Object {
|
||||
"children": undefined,
|
||||
"deepLink": Object {
|
||||
"baseUrl": "",
|
||||
"href": "",
|
||||
"id": "item2",
|
||||
"title": "Title from deeplink!",
|
||||
"url": "",
|
||||
},
|
||||
"href": undefined,
|
||||
"id": "item3",
|
||||
"isActive": false,
|
||||
"path": Array [
|
||||
"group1",
|
||||
"item3",
|
||||
],
|
||||
"renderItem": undefined,
|
||||
"title": "Deeplink title overriden",
|
||||
},
|
||||
],
|
||||
"deepLink": undefined,
|
||||
"href": undefined,
|
||||
"id": "group1",
|
||||
"isActive": false,
|
||||
"path": Array [
|
||||
"group1",
|
||||
],
|
||||
"title": "Group 1",
|
||||
"type": "navGroup",
|
||||
},
|
||||
Object {
|
||||
"children": Array [
|
||||
Object {
|
||||
"children": Array [
|
||||
Object {
|
||||
"children": undefined,
|
||||
"deepLink": Object {
|
||||
"baseUrl": "/mocked",
|
||||
"href": "http://mocked/discover",
|
||||
"id": "discover",
|
||||
"title": "Deeplink discover",
|
||||
"url": "/mocked/discover",
|
||||
},
|
||||
"href": undefined,
|
||||
"id": "discover",
|
||||
"isActive": false,
|
||||
"path": Array [
|
||||
"rootNav:analytics",
|
||||
"root",
|
||||
"discover",
|
||||
],
|
||||
"renderItem": undefined,
|
||||
"title": "Deeplink discover",
|
||||
},
|
||||
Object {
|
||||
"children": undefined,
|
||||
"deepLink": Object {
|
||||
"baseUrl": "/mocked",
|
||||
"href": "http://mocked/dashboards",
|
||||
"id": "dashboards",
|
||||
"title": "Deeplink dashboards",
|
||||
"url": "/mocked/dashboards",
|
||||
},
|
||||
"href": undefined,
|
||||
"id": "dashboards",
|
||||
"isActive": false,
|
||||
"path": Array [
|
||||
"rootNav:analytics",
|
||||
"root",
|
||||
"dashboards",
|
||||
],
|
||||
"renderItem": undefined,
|
||||
"title": "Deeplink dashboards",
|
||||
},
|
||||
Object {
|
||||
"children": undefined,
|
||||
"deepLink": Object {
|
||||
"baseUrl": "/mocked",
|
||||
"href": "http://mocked/visualize",
|
||||
"id": "visualize",
|
||||
"title": "Deeplink visualize",
|
||||
"url": "/mocked/visualize",
|
||||
},
|
||||
"href": undefined,
|
||||
"id": "visualize",
|
||||
"isActive": false,
|
||||
"path": Array [
|
||||
"rootNav:analytics",
|
||||
"root",
|
||||
"visualize",
|
||||
],
|
||||
"renderItem": undefined,
|
||||
"title": "Deeplink visualize",
|
||||
},
|
||||
],
|
||||
"deepLink": undefined,
|
||||
"href": undefined,
|
||||
"id": "root",
|
||||
"isActive": false,
|
||||
"path": Array [
|
||||
"rootNav:analytics",
|
||||
"root",
|
||||
],
|
||||
"title": "",
|
||||
},
|
||||
],
|
||||
"deepLink": undefined,
|
||||
"href": undefined,
|
||||
"icon": "stats",
|
||||
"id": "rootNav:analytics",
|
||||
"isActive": false,
|
||||
"path": Array [
|
||||
"rootNav:analytics",
|
||||
],
|
||||
"title": "Data exploration",
|
||||
"type": "navGroup",
|
||||
},
|
||||
Object {
|
||||
"children": Array [
|
||||
Object {
|
||||
"children": Array [
|
||||
Object {
|
||||
"children": undefined,
|
||||
"deepLink": Object {
|
||||
"baseUrl": "/mocked",
|
||||
"href": "http://mocked/ml:overview",
|
||||
"id": "ml:overview",
|
||||
"title": "Deeplink ml:overview",
|
||||
"url": "/mocked/ml:overview",
|
||||
},
|
||||
"href": undefined,
|
||||
"id": "ml:overview",
|
||||
"isActive": false,
|
||||
"path": Array [
|
||||
"rootNav:ml",
|
||||
"root",
|
||||
"ml:overview",
|
||||
],
|
||||
"renderItem": undefined,
|
||||
"title": "Deeplink ml:overview",
|
||||
},
|
||||
Object {
|
||||
"children": undefined,
|
||||
"deepLink": Object {
|
||||
"baseUrl": "/mocked",
|
||||
"href": "http://mocked/ml:notifications",
|
||||
"id": "ml:notifications",
|
||||
"title": "Deeplink ml:notifications",
|
||||
"url": "/mocked/ml:notifications",
|
||||
},
|
||||
"href": undefined,
|
||||
"id": "ml:notifications",
|
||||
"isActive": false,
|
||||
"path": Array [
|
||||
"rootNav:ml",
|
||||
"root",
|
||||
"ml:notifications",
|
||||
],
|
||||
"renderItem": undefined,
|
||||
"title": "Deeplink ml:notifications",
|
||||
},
|
||||
],
|
||||
"deepLink": undefined,
|
||||
"href": undefined,
|
||||
"id": "root",
|
||||
"isActive": false,
|
||||
"path": Array [
|
||||
"rootNav:ml",
|
||||
"root",
|
||||
],
|
||||
"title": "",
|
||||
},
|
||||
Object {
|
||||
"children": Array [
|
||||
Object {
|
||||
"children": undefined,
|
||||
"deepLink": Object {
|
||||
"baseUrl": "/mocked",
|
||||
"href": "http://mocked/ml:anomalyDetection",
|
||||
"id": "ml:anomalyDetection",
|
||||
"title": "Deeplink ml:anomalyDetection",
|
||||
"url": "/mocked/ml:anomalyDetection",
|
||||
},
|
||||
"href": undefined,
|
||||
"id": "ml:anomalyDetection",
|
||||
"isActive": false,
|
||||
"path": Array [
|
||||
"rootNav:ml",
|
||||
"anomaly_detection",
|
||||
"ml:anomalyDetection",
|
||||
],
|
||||
"renderItem": undefined,
|
||||
"title": "Jobs",
|
||||
},
|
||||
Object {
|
||||
"children": undefined,
|
||||
"deepLink": Object {
|
||||
"baseUrl": "/mocked",
|
||||
"href": "http://mocked/ml:anomalyExplorer",
|
||||
"id": "ml:anomalyExplorer",
|
||||
"title": "Deeplink ml:anomalyExplorer",
|
||||
"url": "/mocked/ml:anomalyExplorer",
|
||||
},
|
||||
"href": undefined,
|
||||
"id": "ml:anomalyExplorer",
|
||||
"isActive": false,
|
||||
"path": Array [
|
||||
"rootNav:ml",
|
||||
"anomaly_detection",
|
||||
"ml:anomalyExplorer",
|
||||
],
|
||||
"renderItem": undefined,
|
||||
"title": "Deeplink ml:anomalyExplorer",
|
||||
},
|
||||
Object {
|
||||
"children": undefined,
|
||||
"deepLink": Object {
|
||||
"baseUrl": "/mocked",
|
||||
"href": "http://mocked/ml:singleMetricViewer",
|
||||
"id": "ml:singleMetricViewer",
|
||||
"title": "Deeplink ml:singleMetricViewer",
|
||||
"url": "/mocked/ml:singleMetricViewer",
|
||||
},
|
||||
"href": undefined,
|
||||
"id": "ml:singleMetricViewer",
|
||||
"isActive": false,
|
||||
"path": Array [
|
||||
"rootNav:ml",
|
||||
"anomaly_detection",
|
||||
"ml:singleMetricViewer",
|
||||
],
|
||||
"renderItem": undefined,
|
||||
"title": "Deeplink ml:singleMetricViewer",
|
||||
},
|
||||
Object {
|
||||
"children": undefined,
|
||||
"deepLink": Object {
|
||||
"baseUrl": "/mocked",
|
||||
"href": "http://mocked/ml:settings",
|
||||
"id": "ml:settings",
|
||||
"title": "Deeplink ml:settings",
|
||||
"url": "/mocked/ml:settings",
|
||||
},
|
||||
"href": undefined,
|
||||
"id": "ml:settings",
|
||||
"isActive": false,
|
||||
"path": Array [
|
||||
"rootNav:ml",
|
||||
"anomaly_detection",
|
||||
"ml:settings",
|
||||
],
|
||||
"renderItem": undefined,
|
||||
"title": "Deeplink ml:settings",
|
||||
},
|
||||
],
|
||||
"deepLink": undefined,
|
||||
"href": undefined,
|
||||
"id": "anomaly_detection",
|
||||
"isActive": false,
|
||||
"path": Array [
|
||||
"rootNav:ml",
|
||||
"anomaly_detection",
|
||||
],
|
||||
"title": "Anomaly Detection",
|
||||
},
|
||||
Object {
|
||||
"children": Array [
|
||||
Object {
|
||||
"children": undefined,
|
||||
"deepLink": Object {
|
||||
"baseUrl": "/mocked",
|
||||
"href": "http://mocked/ml:dataFrameAnalytics",
|
||||
"id": "ml:dataFrameAnalytics",
|
||||
"title": "Deeplink ml:dataFrameAnalytics",
|
||||
"url": "/mocked/ml:dataFrameAnalytics",
|
||||
},
|
||||
"href": undefined,
|
||||
"id": "ml:dataFrameAnalytics",
|
||||
"isActive": false,
|
||||
"path": Array [
|
||||
"rootNav:ml",
|
||||
"data_frame_analytics",
|
||||
"ml:dataFrameAnalytics",
|
||||
],
|
||||
"renderItem": undefined,
|
||||
"title": "Jobs",
|
||||
},
|
||||
Object {
|
||||
"children": undefined,
|
||||
"deepLink": Object {
|
||||
"baseUrl": "/mocked",
|
||||
"href": "http://mocked/ml:resultExplorer",
|
||||
"id": "ml:resultExplorer",
|
||||
"title": "Deeplink ml:resultExplorer",
|
||||
"url": "/mocked/ml:resultExplorer",
|
||||
},
|
||||
"href": undefined,
|
||||
"id": "ml:resultExplorer",
|
||||
"isActive": false,
|
||||
"path": Array [
|
||||
"rootNav:ml",
|
||||
"data_frame_analytics",
|
||||
"ml:resultExplorer",
|
||||
],
|
||||
"renderItem": undefined,
|
||||
"title": "Deeplink ml:resultExplorer",
|
||||
},
|
||||
Object {
|
||||
"children": undefined,
|
||||
"deepLink": Object {
|
||||
"baseUrl": "/mocked",
|
||||
"href": "http://mocked/ml:analyticsMap",
|
||||
"id": "ml:analyticsMap",
|
||||
"title": "Deeplink ml:analyticsMap",
|
||||
"url": "/mocked/ml:analyticsMap",
|
||||
},
|
||||
"href": undefined,
|
||||
"id": "ml:analyticsMap",
|
||||
"isActive": false,
|
||||
"path": Array [
|
||||
"rootNav:ml",
|
||||
"data_frame_analytics",
|
||||
"ml:analyticsMap",
|
||||
],
|
||||
"renderItem": undefined,
|
||||
"title": "Deeplink ml:analyticsMap",
|
||||
},
|
||||
],
|
||||
"deepLink": undefined,
|
||||
"href": undefined,
|
||||
"id": "data_frame_analytics",
|
||||
"isActive": false,
|
||||
"path": Array [
|
||||
"rootNav:ml",
|
||||
"data_frame_analytics",
|
||||
],
|
||||
"title": "Data Frame Analytics",
|
||||
},
|
||||
Object {
|
||||
"children": Array [
|
||||
Object {
|
||||
"children": undefined,
|
||||
"deepLink": Object {
|
||||
"baseUrl": "/mocked",
|
||||
"href": "http://mocked/ml:nodesOverview",
|
||||
"id": "ml:nodesOverview",
|
||||
"title": "Deeplink ml:nodesOverview",
|
||||
"url": "/mocked/ml:nodesOverview",
|
||||
},
|
||||
"href": undefined,
|
||||
"id": "ml:nodesOverview",
|
||||
"isActive": false,
|
||||
"path": Array [
|
||||
"rootNav:ml",
|
||||
"model_management",
|
||||
"ml:nodesOverview",
|
||||
],
|
||||
"renderItem": undefined,
|
||||
"title": "Deeplink ml:nodesOverview",
|
||||
},
|
||||
Object {
|
||||
"children": undefined,
|
||||
"deepLink": Object {
|
||||
"baseUrl": "/mocked",
|
||||
"href": "http://mocked/ml:nodes",
|
||||
"id": "ml:nodes",
|
||||
"title": "Deeplink ml:nodes",
|
||||
"url": "/mocked/ml:nodes",
|
||||
},
|
||||
"href": undefined,
|
||||
"id": "ml:nodes",
|
||||
"isActive": false,
|
||||
"path": Array [
|
||||
"rootNav:ml",
|
||||
"model_management",
|
||||
"ml:nodes",
|
||||
],
|
||||
"renderItem": undefined,
|
||||
"title": "Deeplink ml:nodes",
|
||||
},
|
||||
],
|
||||
"deepLink": undefined,
|
||||
"href": undefined,
|
||||
"id": "model_management",
|
||||
"isActive": false,
|
||||
"path": Array [
|
||||
"rootNav:ml",
|
||||
"model_management",
|
||||
],
|
||||
"title": "Model Management",
|
||||
},
|
||||
Object {
|
||||
"children": Array [
|
||||
Object {
|
||||
"children": undefined,
|
||||
"deepLink": Object {
|
||||
"baseUrl": "/mocked",
|
||||
"href": "http://mocked/ml:fileUpload",
|
||||
"id": "ml:fileUpload",
|
||||
"title": "Deeplink ml:fileUpload",
|
||||
"url": "/mocked/ml:fileUpload",
|
||||
},
|
||||
"href": undefined,
|
||||
"id": "ml:fileUpload",
|
||||
"isActive": false,
|
||||
"path": Array [
|
||||
"rootNav:ml",
|
||||
"data_visualizer",
|
||||
"ml:fileUpload",
|
||||
],
|
||||
"renderItem": undefined,
|
||||
"title": "File",
|
||||
},
|
||||
Object {
|
||||
"children": undefined,
|
||||
"deepLink": Object {
|
||||
"baseUrl": "/mocked",
|
||||
"href": "http://mocked/ml:indexDataVisualizer",
|
||||
"id": "ml:indexDataVisualizer",
|
||||
"title": "Deeplink ml:indexDataVisualizer",
|
||||
"url": "/mocked/ml:indexDataVisualizer",
|
||||
},
|
||||
"href": undefined,
|
||||
"id": "ml:indexDataVisualizer",
|
||||
"isActive": false,
|
||||
"path": Array [
|
||||
"rootNav:ml",
|
||||
"data_visualizer",
|
||||
"ml:indexDataVisualizer",
|
||||
],
|
||||
"renderItem": undefined,
|
||||
"title": "Data view",
|
||||
},
|
||||
],
|
||||
"deepLink": undefined,
|
||||
"href": undefined,
|
||||
"id": "data_visualizer",
|
||||
"isActive": false,
|
||||
"path": Array [
|
||||
"rootNav:ml",
|
||||
"data_visualizer",
|
||||
],
|
||||
"title": "Data Visualizer",
|
||||
},
|
||||
Object {
|
||||
"children": Array [
|
||||
Object {
|
||||
"children": undefined,
|
||||
"deepLink": Object {
|
||||
"baseUrl": "/mocked",
|
||||
"href": "http://mocked/ml:explainLogRateSpikes",
|
||||
"id": "ml:explainLogRateSpikes",
|
||||
"title": "Deeplink ml:explainLogRateSpikes",
|
||||
"url": "/mocked/ml:explainLogRateSpikes",
|
||||
},
|
||||
"href": undefined,
|
||||
"id": "ml:explainLogRateSpikes",
|
||||
"isActive": false,
|
||||
"path": Array [
|
||||
"rootNav:ml",
|
||||
"aiops_labs",
|
||||
"ml:explainLogRateSpikes",
|
||||
],
|
||||
"renderItem": undefined,
|
||||
"title": "Deeplink ml:explainLogRateSpikes",
|
||||
},
|
||||
Object {
|
||||
"children": undefined,
|
||||
"deepLink": Object {
|
||||
"baseUrl": "/mocked",
|
||||
"href": "http://mocked/ml:logPatternAnalysis",
|
||||
"id": "ml:logPatternAnalysis",
|
||||
"title": "Deeplink ml:logPatternAnalysis",
|
||||
"url": "/mocked/ml:logPatternAnalysis",
|
||||
},
|
||||
"href": undefined,
|
||||
"id": "ml:logPatternAnalysis",
|
||||
"isActive": false,
|
||||
"path": Array [
|
||||
"rootNav:ml",
|
||||
"aiops_labs",
|
||||
"ml:logPatternAnalysis",
|
||||
],
|
||||
"renderItem": undefined,
|
||||
"title": "Deeplink ml:logPatternAnalysis",
|
||||
},
|
||||
Object {
|
||||
"children": undefined,
|
||||
"deepLink": Object {
|
||||
"baseUrl": "/mocked",
|
||||
"href": "http://mocked/ml:changePointDetections",
|
||||
"id": "ml:changePointDetections",
|
||||
"title": "Deeplink ml:changePointDetections",
|
||||
"url": "/mocked/ml:changePointDetections",
|
||||
},
|
||||
"href": undefined,
|
||||
"id": "ml:changePointDetections",
|
||||
"isActive": false,
|
||||
"path": Array [
|
||||
"rootNav:ml",
|
||||
"aiops_labs",
|
||||
"ml:changePointDetections",
|
||||
],
|
||||
"renderItem": undefined,
|
||||
"title": "Deeplink ml:changePointDetections",
|
||||
},
|
||||
],
|
||||
"deepLink": undefined,
|
||||
"href": undefined,
|
||||
"id": "aiops_labs",
|
||||
"isActive": false,
|
||||
"path": Array [
|
||||
"rootNav:ml",
|
||||
"aiops_labs",
|
||||
],
|
||||
"title": "AIOps labs",
|
||||
},
|
||||
],
|
||||
"deepLink": undefined,
|
||||
"href": undefined,
|
||||
"icon": "machineLearningApp",
|
||||
"id": "rootNav:ml",
|
||||
"isActive": false,
|
||||
"path": Array [
|
||||
"rootNav:ml",
|
||||
],
|
||||
"title": "Machine Learning",
|
||||
"type": "navGroup",
|
||||
},
|
||||
Object {
|
||||
"children": undefined,
|
||||
"deepLink": Object {
|
||||
"baseUrl": "/mocked",
|
||||
"href": "http://mocked/dev_tools",
|
||||
"id": "dev_tools",
|
||||
"title": "Deeplink dev_tools",
|
||||
"url": "/mocked/dev_tools",
|
||||
},
|
||||
"href": undefined,
|
||||
"icon": "editorCodeBlock",
|
||||
"id": "devTools",
|
||||
"isActive": false,
|
||||
"path": Array [
|
||||
"devTools",
|
||||
],
|
||||
"title": "Developer tools",
|
||||
"type": "navGroup",
|
||||
},
|
||||
Object {
|
||||
"breadcrumbStatus": "hidden",
|
||||
"children": Array [
|
||||
Object {
|
||||
"children": Array [
|
||||
Object {
|
||||
"children": undefined,
|
||||
"deepLink": Object {
|
||||
"baseUrl": "/mocked",
|
||||
"href": "http://mocked/management",
|
||||
"id": "management",
|
||||
"title": "Deeplink management",
|
||||
"url": "/mocked/management",
|
||||
},
|
||||
"href": undefined,
|
||||
"id": "management",
|
||||
"isActive": false,
|
||||
"path": Array [
|
||||
"project_settings_project_nav",
|
||||
"settings",
|
||||
"management",
|
||||
],
|
||||
"renderItem": undefined,
|
||||
"title": "Management",
|
||||
},
|
||||
Object {
|
||||
"children": undefined,
|
||||
"deepLink": undefined,
|
||||
"href": "https://cloud.elastic.co/deployments/123456789/security/users",
|
||||
"id": "cloudLinkUserAndRoles",
|
||||
"isActive": false,
|
||||
"path": Array [
|
||||
"project_settings_project_nav",
|
||||
"settings",
|
||||
"cloudLinkUserAndRoles",
|
||||
],
|
||||
"renderItem": undefined,
|
||||
"title": "Mock Users & Roles",
|
||||
},
|
||||
Object {
|
||||
"children": undefined,
|
||||
"deepLink": undefined,
|
||||
"href": "https://cloud.elastic.co/deployments/123456789/performance",
|
||||
"id": "cloudLinkPerformance",
|
||||
"isActive": false,
|
||||
"path": Array [
|
||||
"project_settings_project_nav",
|
||||
"settings",
|
||||
"cloudLinkPerformance",
|
||||
],
|
||||
"renderItem": undefined,
|
||||
"title": "Mock Performance",
|
||||
},
|
||||
Object {
|
||||
"children": undefined,
|
||||
"deepLink": undefined,
|
||||
"href": "https://cloud.elastic.co/account/billing",
|
||||
"id": "cloudLinkBilling",
|
||||
"isActive": false,
|
||||
"path": Array [
|
||||
"project_settings_project_nav",
|
||||
"settings",
|
||||
"cloudLinkBilling",
|
||||
],
|
||||
"renderItem": undefined,
|
||||
"title": "Mock Billing & Subscriptions",
|
||||
},
|
||||
],
|
||||
"deepLink": undefined,
|
||||
"href": undefined,
|
||||
"id": "settings",
|
||||
"isActive": false,
|
||||
"path": Array [
|
||||
"project_settings_project_nav",
|
||||
"settings",
|
||||
],
|
||||
"title": "",
|
||||
},
|
||||
],
|
||||
"deepLink": undefined,
|
||||
"href": undefined,
|
||||
"icon": "gear",
|
||||
"id": "project_settings_project_nav",
|
||||
"isActive": false,
|
||||
"path": Array [
|
||||
"project_settings_project_nav",
|
||||
],
|
||||
"title": "Project settings",
|
||||
"type": "navGroup",
|
||||
},
|
||||
]
|
||||
`;
|
|
@ -0,0 +1,61 @@
|
|||
/*
|
||||
* 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 {
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiIcon,
|
||||
EuiLink,
|
||||
EuiTitle,
|
||||
useGeneratedHtmlId,
|
||||
} from '@elastic/eui';
|
||||
|
||||
import type { NavigateToUrlFn } from '../../../types/internal';
|
||||
|
||||
interface Props {
|
||||
title: string;
|
||||
href: string;
|
||||
navigateToUrl: NavigateToUrlFn;
|
||||
iconType?: string;
|
||||
}
|
||||
|
||||
export const GroupAsLink = ({ title, href, navigateToUrl, iconType }: Props) => {
|
||||
const groupID = useGeneratedHtmlId();
|
||||
const titleID = `${groupID}__title`;
|
||||
const TitleElement = 'h3';
|
||||
|
||||
return (
|
||||
<EuiFlexGroup gutterSize="m" alignItems="center" responsive={false}>
|
||||
{iconType && (
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiIcon type={iconType} size="m" />
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
|
||||
<EuiFlexItem>
|
||||
{/* eslint-disable-next-line @elastic/eui/href-or-on-click */}
|
||||
<EuiLink
|
||||
color="text"
|
||||
onClick={(e: React.MouseEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
navigateToUrl(href);
|
||||
}}
|
||||
href={href}
|
||||
>
|
||||
<EuiTitle size="xxs">
|
||||
<TitleElement id={titleID} className="euiCollapsibleNavGroup__title">
|
||||
{title}
|
||||
</TitleElement>
|
||||
</EuiTitle>
|
||||
</EuiLink>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
};
|
|
@ -735,4 +735,36 @@ describe('<Navigation />', () => {
|
|||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('cloud links', () => {
|
||||
test('render the cloud link', async () => {
|
||||
const onProjectNavigationChange = jest.fn();
|
||||
|
||||
const { findByTestId } = render(
|
||||
<NavigationProvider {...services} onProjectNavigationChange={onProjectNavigationChange}>
|
||||
<Navigation>
|
||||
<Navigation.Group id="group1">
|
||||
<Navigation.Item id="cloudLink1" cloudLink="userAndRoles" />
|
||||
<Navigation.Item id="cloudLink2" cloudLink="performance" />
|
||||
<Navigation.Item id="cloudLink3" cloudLink="billingAndSub" />
|
||||
</Navigation.Group>
|
||||
</Navigation>
|
||||
</NavigationProvider>
|
||||
);
|
||||
|
||||
expect(await findByTestId('nav-item-group1.cloudLink1')).toBeVisible();
|
||||
expect(await findByTestId('nav-item-group1.cloudLink2')).toBeVisible();
|
||||
expect(await findByTestId('nav-item-group1.cloudLink3')).toBeVisible();
|
||||
|
||||
expect(await (await findByTestId('nav-item-group1.cloudLink1')).textContent).toBe(
|
||||
'Mock Users & RolesExternal link'
|
||||
);
|
||||
expect(await (await findByTestId('nav-item-group1.cloudLink2')).textContent).toBe(
|
||||
'Mock PerformanceExternal link'
|
||||
);
|
||||
expect(await (await findByTestId('nav-item-group1.cloudLink3')).textContent).toBe(
|
||||
'Mock Billing & SubscriptionsExternal link'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -7,8 +7,9 @@
|
|||
*/
|
||||
|
||||
import React, { createContext, useCallback, useMemo, useContext } from 'react';
|
||||
|
||||
import type { AppDeepLinkId } from '@kbn/core-chrome-browser';
|
||||
|
||||
import { useNavigation as useNavigationServices } from '../../services';
|
||||
import { useInitNavNode } from '../hooks';
|
||||
import type { NodeProps, RegisterFunction } from '../types';
|
||||
import { NavigationSectionUI } from './navigation_section_ui';
|
||||
|
@ -45,6 +46,7 @@ function NavigationGroupInternalComp<
|
|||
Id extends string = string,
|
||||
ChildrenId extends string = Id
|
||||
>(props: Props<LinkId, Id, ChildrenId>) {
|
||||
const { cloudLinks } = useNavigationServices();
|
||||
const navigationContext = useNavigation();
|
||||
|
||||
const { children, node } = useMemo(() => {
|
||||
|
@ -58,7 +60,7 @@ function NavigationGroupInternalComp<
|
|||
};
|
||||
}, [props]);
|
||||
|
||||
const { navNode, registerChildNode, path, childrenNodes } = useInitNavNode(node);
|
||||
const { navNode, registerChildNode, path, childrenNodes } = useInitNavNode(node, { cloudLinks });
|
||||
|
||||
const unstyled = props.unstyled ?? navigationContext.unstyled;
|
||||
|
||||
|
|
|
@ -9,6 +9,7 @@
|
|||
import React, { Fragment, ReactElement, ReactNode, useEffect, useMemo } from 'react';
|
||||
|
||||
import type { AppDeepLinkId } from '@kbn/core-chrome-browser';
|
||||
import { useNavigation as useNavigationServices } from '../../services';
|
||||
import type { ChromeProjectNavigationNodeEnhanced, NodeProps } from '../types';
|
||||
import { useInitNavNode } from '../hooks';
|
||||
import { useNavigation } from './navigation';
|
||||
|
@ -31,6 +32,7 @@ function NavigationItemComp<
|
|||
Id extends string = string,
|
||||
ChildrenId extends string = Id
|
||||
>(props: Props<LinkId, Id, ChildrenId>) {
|
||||
const { cloudLinks } = useNavigationServices();
|
||||
const navigationContext = useNavigation();
|
||||
const navNodeRef = React.useRef<ChromeProjectNavigationNodeEnhanced | null>(null);
|
||||
|
||||
|
@ -51,7 +53,7 @@ function NavigationItemComp<
|
|||
typeof children === 'function' ? () => children(navNodeRef.current) : () => children;
|
||||
}
|
||||
|
||||
const { navNode } = useInitNavNode({ ...node, children, renderItem });
|
||||
const { navNode } = useInitNavNode({ ...node, children, renderItem }, { cloudLinks });
|
||||
|
||||
useEffect(() => {
|
||||
navNodeRef.current = navNode;
|
||||
|
|
|
@ -20,6 +20,7 @@ import { navigationStyles as styles } from '../../styles';
|
|||
import { useNavigation as useServices } from '../../services';
|
||||
import { ChromeProjectNavigationNodeEnhanced } from '../types';
|
||||
import { isAbsoluteLink } from '../../utils';
|
||||
import { GroupAsLink } from './group_as_link';
|
||||
|
||||
type RenderItem = EuiSideNavItemType<unknown>['renderItem'];
|
||||
|
||||
|
@ -39,7 +40,7 @@ const navigationNodeToEuiItem = (
|
|||
|
||||
return () => (
|
||||
<div className="euiSideNavItemButton" data-test-subj={dataTestSubj}>
|
||||
<EuiLink href={href} external>
|
||||
<EuiLink href={href} external color="text">
|
||||
{item.title}
|
||||
</EuiLink>
|
||||
</div>
|
||||
|
@ -108,6 +109,10 @@ export const NavigationSectionUI: FC<Props> = ({ navNode, items = [] }) => {
|
|||
});
|
||||
|
||||
const groupHasLink = Boolean(navNode.deepLink) || Boolean(navNode.href);
|
||||
const groupHasChildren = filteredItems.some(itemHasLinkOrChildren);
|
||||
// Group with a link and no children will be rendered as a link and not an EUI accordion
|
||||
const groupIsLink = groupHasLink && !groupHasChildren;
|
||||
const groupHref = navNode.deepLink?.url ?? navNode.href!;
|
||||
|
||||
useEffect(() => {
|
||||
if (doCollapseFromActiveState) {
|
||||
|
@ -115,17 +120,35 @@ export const NavigationSectionUI: FC<Props> = ({ navNode, items = [] }) => {
|
|||
}
|
||||
}, [isActive, doCollapseFromActiveState]);
|
||||
|
||||
if (!groupHasLink && !filteredItems.some(itemHasLinkOrChildren)) {
|
||||
if (!groupHasLink && !groupHasChildren) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const propsForGroupAsLink = groupIsLink
|
||||
? {
|
||||
buttonElement: 'div' as const,
|
||||
// If we don't force the state there is a little UI animation as if the
|
||||
// accordion was openin/closing. We don't want any animation when it is a link.
|
||||
forceState: 'closed' as const,
|
||||
buttonContent: (
|
||||
<GroupAsLink
|
||||
title={title}
|
||||
iconType={icon}
|
||||
href={groupHref}
|
||||
navigateToUrl={navigateToUrl}
|
||||
/>
|
||||
),
|
||||
arrowProps: { style: { display: 'none' } },
|
||||
}
|
||||
: {};
|
||||
|
||||
return (
|
||||
<EuiCollapsibleNavGroup
|
||||
id={id}
|
||||
title={title}
|
||||
iconType={icon}
|
||||
iconSize={'m'}
|
||||
isCollapsible={true}
|
||||
iconSize="m"
|
||||
isCollapsible
|
||||
initialIsOpen={isActive}
|
||||
onToggle={(isOpen) => {
|
||||
setIsCollapsed(!isOpen);
|
||||
|
@ -133,6 +156,7 @@ export const NavigationSectionUI: FC<Props> = ({ navNode, items = [] }) => {
|
|||
}}
|
||||
forceState={isCollapsed ? 'closed' : 'open'}
|
||||
data-test-subj={`nav-bucket-${id}`}
|
||||
{...propsForGroupAsLink}
|
||||
>
|
||||
<EuiText color="default">
|
||||
<EuiSideNav
|
||||
|
|
|
@ -106,60 +106,105 @@ describe('<DefaultNavigation />', () => {
|
|||
onProjectNavigationChange.mock.calls[onProjectNavigationChange.mock.calls.length - 1];
|
||||
const [navTreeGenerated] = lastCall;
|
||||
|
||||
expect(navTreeGenerated.navigationTree).toEqual([
|
||||
{
|
||||
id: 'group1',
|
||||
path: ['group1'],
|
||||
title: '',
|
||||
isActive: false,
|
||||
children: [
|
||||
{
|
||||
id: 'item1',
|
||||
title: 'Item 1',
|
||||
href: 'http://foo',
|
||||
isActive: false,
|
||||
path: ['group1', 'item1'],
|
||||
},
|
||||
{
|
||||
id: 'item2',
|
||||
title: 'Item 2',
|
||||
href: 'http://foo',
|
||||
isActive: false,
|
||||
path: ['group1', 'item2'],
|
||||
},
|
||||
{
|
||||
id: 'group1A',
|
||||
title: 'Group1A',
|
||||
isActive: false,
|
||||
path: ['group1', 'group1A'],
|
||||
children: [
|
||||
{
|
||||
id: 'item1',
|
||||
title: 'Group 1A Item 1',
|
||||
href: 'http://foo',
|
||||
isActive: false,
|
||||
path: ['group1', 'group1A', 'item1'],
|
||||
},
|
||||
{
|
||||
id: 'group1A_1',
|
||||
title: 'Group1A_1',
|
||||
isActive: false,
|
||||
path: ['group1', 'group1A', 'group1A_1'],
|
||||
children: [
|
||||
{
|
||||
id: 'item1',
|
||||
title: 'Group 1A_1 Item 1',
|
||||
href: 'http://foo',
|
||||
isActive: false,
|
||||
path: ['group1', 'group1A', 'group1A_1', 'item1'],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
expect(navTreeGenerated.navigationTree).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"children": Array [
|
||||
Object {
|
||||
"children": undefined,
|
||||
"deepLink": undefined,
|
||||
"href": "http://foo",
|
||||
"id": "item1",
|
||||
"isActive": false,
|
||||
"path": Array [
|
||||
"group1",
|
||||
"item1",
|
||||
],
|
||||
"renderItem": undefined,
|
||||
"title": "Item 1",
|
||||
},
|
||||
Object {
|
||||
"children": undefined,
|
||||
"deepLink": undefined,
|
||||
"href": "http://foo",
|
||||
"id": "item2",
|
||||
"isActive": false,
|
||||
"path": Array [
|
||||
"group1",
|
||||
"item2",
|
||||
],
|
||||
"renderItem": undefined,
|
||||
"title": "Item 2",
|
||||
},
|
||||
Object {
|
||||
"children": Array [
|
||||
Object {
|
||||
"children": undefined,
|
||||
"deepLink": undefined,
|
||||
"href": "http://foo",
|
||||
"id": "item1",
|
||||
"isActive": false,
|
||||
"path": Array [
|
||||
"group1",
|
||||
"group1A",
|
||||
"item1",
|
||||
],
|
||||
"renderItem": undefined,
|
||||
"title": "Group 1A Item 1",
|
||||
},
|
||||
Object {
|
||||
"children": Array [
|
||||
Object {
|
||||
"children": undefined,
|
||||
"deepLink": undefined,
|
||||
"href": "http://foo",
|
||||
"id": "item1",
|
||||
"isActive": false,
|
||||
"path": Array [
|
||||
"group1",
|
||||
"group1A",
|
||||
"group1A_1",
|
||||
"item1",
|
||||
],
|
||||
"renderItem": undefined,
|
||||
"title": "Group 1A_1 Item 1",
|
||||
},
|
||||
],
|
||||
"deepLink": undefined,
|
||||
"href": undefined,
|
||||
"id": "group1A_1",
|
||||
"isActive": false,
|
||||
"path": Array [
|
||||
"group1",
|
||||
"group1A",
|
||||
"group1A_1",
|
||||
],
|
||||
"title": "Group1A_1",
|
||||
},
|
||||
],
|
||||
"deepLink": undefined,
|
||||
"href": undefined,
|
||||
"id": "group1A",
|
||||
"isActive": false,
|
||||
"path": Array [
|
||||
"group1",
|
||||
"group1A",
|
||||
],
|
||||
"title": "Group1A",
|
||||
},
|
||||
],
|
||||
"deepLink": undefined,
|
||||
"href": undefined,
|
||||
"id": "group1",
|
||||
"isActive": false,
|
||||
"path": Array [
|
||||
"group1",
|
||||
],
|
||||
"title": "",
|
||||
"type": "navGroup",
|
||||
},
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
test('should read the title from deeplink', async () => {
|
||||
|
@ -223,50 +268,76 @@ describe('<DefaultNavigation />', () => {
|
|||
onProjectNavigationChange.mock.calls[onProjectNavigationChange.mock.calls.length - 1];
|
||||
const [navTreeGenerated] = lastCall;
|
||||
|
||||
expect(navTreeGenerated.navigationTree).toEqual([
|
||||
{
|
||||
id: 'root',
|
||||
path: ['root'],
|
||||
title: '',
|
||||
isActive: false,
|
||||
children: [
|
||||
{
|
||||
id: 'group1',
|
||||
path: ['root', 'group1'],
|
||||
title: '',
|
||||
isActive: false,
|
||||
children: [
|
||||
{
|
||||
id: 'item1',
|
||||
path: ['root', 'group1', 'item1'],
|
||||
title: 'Title from deeplink',
|
||||
isActive: false,
|
||||
deepLink: {
|
||||
id: 'item1',
|
||||
title: 'Title from deeplink',
|
||||
baseUrl: '',
|
||||
url: '',
|
||||
href: '',
|
||||
expect(navTreeGenerated.navigationTree).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"children": Array [
|
||||
Object {
|
||||
"children": Array [
|
||||
Object {
|
||||
"children": undefined,
|
||||
"deepLink": Object {
|
||||
"baseUrl": "",
|
||||
"href": "",
|
||||
"id": "item1",
|
||||
"title": "Title from deeplink",
|
||||
"url": "",
|
||||
},
|
||||
"href": undefined,
|
||||
"id": "item1",
|
||||
"isActive": false,
|
||||
"path": Array [
|
||||
"root",
|
||||
"group1",
|
||||
"item1",
|
||||
],
|
||||
"renderItem": undefined,
|
||||
"title": "Title from deeplink",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'item2',
|
||||
title: 'Overwrite deeplink title',
|
||||
path: ['root', 'group1', 'item2'],
|
||||
isActive: false,
|
||||
deepLink: {
|
||||
id: 'item1',
|
||||
title: 'Title from deeplink',
|
||||
baseUrl: '',
|
||||
url: '',
|
||||
href: '',
|
||||
Object {
|
||||
"children": undefined,
|
||||
"deepLink": Object {
|
||||
"baseUrl": "",
|
||||
"href": "",
|
||||
"id": "item1",
|
||||
"title": "Title from deeplink",
|
||||
"url": "",
|
||||
},
|
||||
"href": undefined,
|
||||
"id": "item2",
|
||||
"isActive": false,
|
||||
"path": Array [
|
||||
"root",
|
||||
"group1",
|
||||
"item2",
|
||||
],
|
||||
"renderItem": undefined,
|
||||
"title": "Overwrite deeplink title",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
],
|
||||
"deepLink": undefined,
|
||||
"href": undefined,
|
||||
"id": "group1",
|
||||
"isActive": false,
|
||||
"path": Array [
|
||||
"root",
|
||||
"group1",
|
||||
],
|
||||
"title": "",
|
||||
},
|
||||
],
|
||||
"deepLink": undefined,
|
||||
"href": undefined,
|
||||
"id": "root",
|
||||
"isActive": false,
|
||||
"path": Array [
|
||||
"root",
|
||||
],
|
||||
"title": "",
|
||||
"type": "navGroup",
|
||||
},
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
test('should allow href for absolute links', async () => {
|
||||
|
@ -306,31 +377,50 @@ describe('<DefaultNavigation />', () => {
|
|||
onProjectNavigationChange.mock.calls[onProjectNavigationChange.mock.calls.length - 1];
|
||||
const [navTreeGenerated] = lastCall;
|
||||
|
||||
expect(navTreeGenerated.navigationTree).toEqual([
|
||||
{
|
||||
id: 'root',
|
||||
path: ['root'],
|
||||
title: '',
|
||||
isActive: false,
|
||||
children: [
|
||||
{
|
||||
id: 'group1',
|
||||
path: ['root', 'group1'],
|
||||
title: '',
|
||||
isActive: false,
|
||||
children: [
|
||||
{
|
||||
id: 'item1',
|
||||
path: ['root', 'group1', 'item1'],
|
||||
title: 'Absolute link',
|
||||
href: 'https://example.com',
|
||||
isActive: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
expect(navTreeGenerated.navigationTree).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"children": Array [
|
||||
Object {
|
||||
"children": Array [
|
||||
Object {
|
||||
"children": undefined,
|
||||
"deepLink": undefined,
|
||||
"href": "https://example.com",
|
||||
"id": "item1",
|
||||
"isActive": false,
|
||||
"path": Array [
|
||||
"root",
|
||||
"group1",
|
||||
"item1",
|
||||
],
|
||||
"renderItem": undefined,
|
||||
"title": "Absolute link",
|
||||
},
|
||||
],
|
||||
"deepLink": undefined,
|
||||
"href": undefined,
|
||||
"id": "group1",
|
||||
"isActive": false,
|
||||
"path": Array [
|
||||
"root",
|
||||
"group1",
|
||||
],
|
||||
"title": "",
|
||||
},
|
||||
],
|
||||
"deepLink": undefined,
|
||||
"href": undefined,
|
||||
"id": "root",
|
||||
"isActive": false,
|
||||
"path": Array [
|
||||
"root",
|
||||
],
|
||||
"title": "",
|
||||
"type": "navGroup",
|
||||
},
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
test('should throw if href is not an absolute links', async () => {
|
||||
|
@ -601,45 +691,38 @@ describe('<DefaultNavigation />', () => {
|
|||
});
|
||||
|
||||
// The project navigation tree passed
|
||||
expect(navTreeGenerated.navigationTree[0]).toEqual({
|
||||
id: 'group1',
|
||||
title: 'Group 1',
|
||||
path: ['group1'],
|
||||
isActive: false,
|
||||
children: [
|
||||
{
|
||||
id: 'item1',
|
||||
title: 'Item 1',
|
||||
isActive: false,
|
||||
path: ['group1', 'item1'],
|
||||
},
|
||||
{
|
||||
id: 'item2',
|
||||
path: ['group1', 'item2'],
|
||||
title: 'Title from deeplink!',
|
||||
isActive: false,
|
||||
deepLink: {
|
||||
id: 'item2',
|
||||
title: 'Title from deeplink!',
|
||||
baseUrl: '',
|
||||
url: '',
|
||||
href: '',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'item3',
|
||||
title: 'Deeplink title overriden',
|
||||
path: ['group1', 'item3'],
|
||||
isActive: false,
|
||||
deepLink: {
|
||||
id: 'item2',
|
||||
title: 'Title from deeplink!',
|
||||
baseUrl: '',
|
||||
url: '',
|
||||
href: '',
|
||||
},
|
||||
},
|
||||
],
|
||||
expect(navTreeGenerated.navigationTree).toMatchSnapshot();
|
||||
});
|
||||
|
||||
describe('cloud links', () => {
|
||||
test('render the cloud link', async () => {
|
||||
const { findByTestId } = render(
|
||||
<NavigationProvider {...services}>
|
||||
<DefaultNavigation projectNavigationTree={[]} />
|
||||
</NavigationProvider>
|
||||
);
|
||||
|
||||
expect(
|
||||
await (
|
||||
await findByTestId(
|
||||
'nav-item-project_settings_project_nav.settings.cloudLinkUserAndRoles'
|
||||
)
|
||||
).textContent
|
||||
).toBe('Mock Users & RolesExternal link');
|
||||
|
||||
expect(
|
||||
await (
|
||||
await findByTestId(
|
||||
'nav-item-project_settings_project_nav.settings.cloudLinkPerformance'
|
||||
)
|
||||
).textContent
|
||||
).toBe('Mock PerformanceExternal link');
|
||||
|
||||
expect(
|
||||
await (
|
||||
await findByTestId('nav-item-project_settings_project_nav.settings.cloudLinkBilling')
|
||||
).textContent
|
||||
).toBe('Mock Billing & SubscriptionsExternal link');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
*/
|
||||
|
||||
import React, { FC, useCallback } from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import type { AppDeepLinkId, NodeDefinition } from '@kbn/core-chrome-browser';
|
||||
|
||||
import { Navigation } from './components';
|
||||
|
@ -54,11 +55,46 @@ const getDefaultNavigationTree = (
|
|||
footer: [
|
||||
{
|
||||
type: 'navGroup',
|
||||
...getPresets('devtools'),
|
||||
id: 'devTools',
|
||||
title: i18n.translate('sharedUXPackages.chrome.sideNavigation.devTools', {
|
||||
defaultMessage: 'Developer tools',
|
||||
}),
|
||||
link: 'dev_tools',
|
||||
icon: 'editorCodeBlock',
|
||||
},
|
||||
{
|
||||
type: 'navGroup',
|
||||
...getPresets('management'),
|
||||
id: 'project_settings_project_nav',
|
||||
title: i18n.translate('sharedUXPackages.chrome.sideNavigation.projectSettings', {
|
||||
defaultMessage: 'Project settings',
|
||||
}),
|
||||
icon: 'gear',
|
||||
breadcrumbStatus: 'hidden',
|
||||
children: [
|
||||
{
|
||||
id: 'settings',
|
||||
children: [
|
||||
{
|
||||
link: 'management',
|
||||
title: i18n.translate('sharedUXPackages.chrome.sideNavigation.mngt', {
|
||||
defaultMessage: 'Management',
|
||||
}),
|
||||
},
|
||||
{
|
||||
id: 'cloudLinkUserAndRoles',
|
||||
cloudLink: 'userAndRoles',
|
||||
},
|
||||
{
|
||||
id: 'cloudLinkPerformance',
|
||||
cloudLink: 'performance',
|
||||
},
|
||||
{
|
||||
id: 'cloudLinkBilling',
|
||||
cloudLink: 'billingAndSub',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
@ -102,15 +138,12 @@ export const DefaultNavigation: FC<ProjectNavigationDefinition & { dataTestSubj?
|
|||
);
|
||||
}
|
||||
|
||||
const { ...copy } = item as GroupDefinition;
|
||||
delete (copy as any).type;
|
||||
|
||||
return copy.children ? (
|
||||
<Navigation.Group {...copy} key={id}>
|
||||
{renderItems(copy.children, [...path, id])}
|
||||
return item.children || (item as GroupDefinition).type === 'navGroup' ? (
|
||||
<Navigation.Group {...item} key={id}>
|
||||
{renderItems(item.children, [...path, id])}
|
||||
</Navigation.Group>
|
||||
) : (
|
||||
<Navigation.Item {...copy} key={id} />
|
||||
<Navigation.Item {...item} key={id} />
|
||||
);
|
||||
});
|
||||
},
|
||||
|
|
|
@ -10,9 +10,11 @@ import {
|
|||
AppDeepLinkId,
|
||||
ChromeNavLink,
|
||||
ChromeProjectNavigationNode,
|
||||
CloudLinkId,
|
||||
} from '@kbn/core-chrome-browser';
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import useObservable from 'react-use/lib/useObservable';
|
||||
import { CloudLinks } from '../../cloud_links';
|
||||
|
||||
import { useNavigation as useNavigationServices } from '../../services';
|
||||
import { isAbsoluteLink } from '../../utils';
|
||||
|
@ -40,12 +42,40 @@ function getIdFromNavigationNode<
|
|||
return id;
|
||||
}
|
||||
|
||||
function isNodeVisible({ link, deepLink }: { link?: string; deepLink?: ChromeNavLink }) {
|
||||
/**
|
||||
* We don't have currently a way to know if a user has access to a Cloud section.
|
||||
* TODO: This function will have to be revisited once we have an API from Cloud to know the user
|
||||
* permissions.
|
||||
*/
|
||||
function hasUserAccessToCloudLink(): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
function isNodeVisible(
|
||||
{
|
||||
link,
|
||||
deepLink,
|
||||
cloudLink,
|
||||
}: {
|
||||
link?: string;
|
||||
deepLink?: ChromeNavLink;
|
||||
cloudLink?: CloudLinkId;
|
||||
},
|
||||
{ cloudLinks }: { cloudLinks: CloudLinks }
|
||||
) {
|
||||
if (link && !deepLink) {
|
||||
// If a link is provided, but no deepLink is found, don't render anything
|
||||
return false;
|
||||
}
|
||||
|
||||
if (cloudLink) {
|
||||
if (!cloudLinks[cloudLink]) {
|
||||
// Invalid cloudLinkId or link url has not been set in kibana.yml
|
||||
return false;
|
||||
}
|
||||
return hasUserAccessToCloudLink();
|
||||
}
|
||||
|
||||
if (deepLink) {
|
||||
return !deepLink.hidden;
|
||||
}
|
||||
|
@ -53,6 +83,47 @@ function isNodeVisible({ link, deepLink }: { link?: string; deepLink?: ChromeNav
|
|||
return true;
|
||||
}
|
||||
|
||||
function getTitleForNode<
|
||||
LinkId extends AppDeepLinkId = AppDeepLinkId,
|
||||
Id extends string = string,
|
||||
ChildrenId extends string = Id
|
||||
>(
|
||||
navNode: NodePropsEnhanced<LinkId, Id, ChildrenId>,
|
||||
{ deepLink, cloudLinks }: { deepLink?: ChromeNavLink; cloudLinks: CloudLinks }
|
||||
): string {
|
||||
const { children } = navNode;
|
||||
if (navNode.title) {
|
||||
return navNode.title;
|
||||
}
|
||||
|
||||
if (typeof children === 'string') {
|
||||
return children;
|
||||
}
|
||||
|
||||
if (deepLink?.title) {
|
||||
return deepLink.title;
|
||||
}
|
||||
|
||||
if (navNode.cloudLink) {
|
||||
return cloudLinks[navNode.cloudLink]?.title ?? '';
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
function validateNodeProps<
|
||||
LinkId extends AppDeepLinkId = AppDeepLinkId,
|
||||
Id extends string = string,
|
||||
ChildrenId extends string = Id
|
||||
>({ link, href, cloudLink }: NodePropsEnhanced<LinkId, Id, ChildrenId>) {
|
||||
if (link && cloudLink) {
|
||||
throw new Error(`Only one of "link" or "cloudLink" can be provided.`);
|
||||
}
|
||||
if (href && cloudLink) {
|
||||
throw new Error(`Only one of "href" or "cloudLink" can be provided.`);
|
||||
}
|
||||
}
|
||||
|
||||
function createInternalNavNode<
|
||||
LinkId extends AppDeepLinkId = AppDeepLinkId,
|
||||
Id extends string = string,
|
||||
|
@ -62,14 +133,16 @@ function createInternalNavNode<
|
|||
_navNode: NodePropsEnhanced<LinkId, Id, ChildrenId>,
|
||||
deepLinks: Readonly<ChromeNavLink[]>,
|
||||
path: string[] | null,
|
||||
isActive: boolean
|
||||
isActive: boolean,
|
||||
{ cloudLinks }: { cloudLinks: CloudLinks }
|
||||
): ChromeProjectNavigationNodeEnhanced | null {
|
||||
const { children, link, href, ...navNode } = _navNode;
|
||||
const deepLink = deepLinks.find((dl) => dl.id === link);
|
||||
const isVisible = isNodeVisible({ link, deepLink });
|
||||
validateNodeProps(_navNode);
|
||||
|
||||
const titleFromDeepLinkOrChildren = typeof children === 'string' ? children : deepLink?.title;
|
||||
const title = navNode.title ?? titleFromDeepLinkOrChildren;
|
||||
const { children, link, cloudLink, ...navNode } = _navNode;
|
||||
const deepLink = deepLinks.find((dl) => dl.id === link);
|
||||
const isVisible = isNodeVisible({ link, deepLink, cloudLink }, { cloudLinks });
|
||||
const title = getTitleForNode(_navNode, { deepLink, cloudLinks });
|
||||
const href = cloudLink ? cloudLinks[cloudLink]?.href : _navNode.href;
|
||||
|
||||
if (href && !isAbsoluteLink(href)) {
|
||||
throw new Error(`href must be an absolute URL. Node id [${id}].`);
|
||||
|
@ -104,7 +177,8 @@ export const useInitNavNode = <
|
|||
Id extends string = string,
|
||||
ChildrenId extends string = Id
|
||||
>(
|
||||
node: NodePropsEnhanced<LinkId, Id, ChildrenId>
|
||||
node: NodePropsEnhanced<LinkId, Id, ChildrenId>,
|
||||
{ cloudLinks }: { cloudLinks: CloudLinks }
|
||||
) => {
|
||||
const { isActive: isActiveControlled } = node;
|
||||
|
||||
|
@ -150,8 +224,8 @@ export const useInitNavNode = <
|
|||
const id = getIdFromNavigationNode(node);
|
||||
|
||||
const internalNavNode = useMemo(
|
||||
() => createInternalNavNode(id, node, deepLinks, nodePath, isActive),
|
||||
[node, id, deepLinks, nodePath, isActive]
|
||||
() => createInternalNavNode(id, node, deepLinks, nodePath, isActive, { cloudLinks }),
|
||||
[node, id, deepLinks, nodePath, isActive, cloudLinks]
|
||||
);
|
||||
|
||||
// Register the node on the parent whenever its properties change or whenever
|
||||
|
|
|
@ -363,8 +363,20 @@ export const WithUIComponents = (args: NavigationServices) => {
|
|||
<Navigation.Group preset="ml" />
|
||||
|
||||
<Navigation.Footer>
|
||||
<Navigation.Group preset="devtools" />
|
||||
<Navigation.Group preset="management" />
|
||||
<Navigation.Group link="dev_tools" icon="editorCodeBlock" title="Developer tools" />
|
||||
<Navigation.Group
|
||||
id="project_settings_project_nav"
|
||||
title="Project settings"
|
||||
breadcrumbStatus="hidden"
|
||||
icon="gear"
|
||||
>
|
||||
<Navigation.Group id="settings">
|
||||
<Navigation.Item link="management" title="Management" />
|
||||
<Navigation.Item id="cloudLinkUserAndRoles" cloudLink="userAndRoles" />
|
||||
<Navigation.Item id="cloudLinkPerformance" cloudLink="performance" />
|
||||
<Navigation.Item id="cloudLinkBilling" cloudLink="billingAndSub" />
|
||||
</Navigation.Group>
|
||||
</Navigation.Group>
|
||||
</Navigation.Footer>
|
||||
</Navigation>
|
||||
</NavigationProvider>
|
||||
|
|
|
@ -94,11 +94,11 @@ export interface GroupDefinition<
|
|||
/**
|
||||
* Flag to indicate if the group is initially collapsed or not.
|
||||
*
|
||||
* `undefined`: (Recommended) the group will be opened if any of its children nodes matches the current URL.
|
||||
*
|
||||
* `false`: the group will be opened event if none of its children nodes matches the current URL.
|
||||
*
|
||||
* `true`: the group will be collapsed event if any of its children nodes matches the current URL.
|
||||
*
|
||||
* `undefined`: the group will be opened if any of its children nodes matches the current URL.
|
||||
*/
|
||||
defaultIsCollapsed?: boolean;
|
||||
preset?: NavigationGroupPreset;
|
||||
|
|
|
@ -26,6 +26,7 @@
|
|||
"@kbn/default-nav-devtools",
|
||||
"@kbn/shared-ux-storybook-mock",
|
||||
"@kbn/core-http-browser",
|
||||
"@kbn/cloud-plugin",
|
||||
],
|
||||
"exclude": [
|
||||
"target/**/*"
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
*/
|
||||
|
||||
import type { Observable } from 'rxjs';
|
||||
import type { CloudStart } from '@kbn/cloud-plugin/public';
|
||||
|
||||
import type {
|
||||
ChromeNavLink,
|
||||
|
@ -14,6 +15,7 @@ import type {
|
|||
ChromeProjectNavigationNode,
|
||||
} from '@kbn/core-chrome-browser';
|
||||
import type { BasePathService, NavigateToUrlFn, RecentItem } from './internal';
|
||||
import type { CloudLinks } from '../src/cloud_links';
|
||||
|
||||
/**
|
||||
* A list of services that are consumed by this component.
|
||||
|
@ -27,6 +29,7 @@ export interface NavigationServices {
|
|||
navigateToUrl: NavigateToUrlFn;
|
||||
onProjectNavigationChange: (chromeProjectNavigation: ChromeProjectNavigation) => void;
|
||||
activeNodes$: Observable<ChromeProjectNavigationNode[][]>;
|
||||
cloudLinks: CloudLinks;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -55,4 +58,5 @@ export interface NavigationKibanaDependencies {
|
|||
) => void;
|
||||
getActiveNavigationNodes$: () => Observable<ChromeProjectNavigationNode[][]>;
|
||||
};
|
||||
cloud: CloudStart;
|
||||
}
|
||||
|
|
|
@ -221,6 +221,8 @@ export default function ({ getService }: PluginFunctionalProviderContext) {
|
|||
'xpack.cloud.organization_url (string)',
|
||||
'xpack.cloud.billing_url (string)',
|
||||
'xpack.cloud.profile_url (string)',
|
||||
'xpack.cloud.performance_url (string)',
|
||||
'xpack.cloud.users_and_roles_url (string)',
|
||||
// can't be used to infer urls or customer id from the outside
|
||||
'xpack.cloud.serverless.project_id (string)',
|
||||
'xpack.discoverEnhanced.actions.exploreDataInChart.enabled (boolean)',
|
||||
|
|
|
@ -24,6 +24,8 @@ export interface CloudConfigType {
|
|||
deployment_url?: string;
|
||||
billing_url?: string;
|
||||
organization_url?: string;
|
||||
users_and_roles_url?: string;
|
||||
performance_url?: string;
|
||||
trial_end_date?: string;
|
||||
is_elastic_staff_owned?: boolean;
|
||||
serverless?: {
|
||||
|
@ -37,6 +39,8 @@ interface CloudUrls {
|
|||
billingUrl?: string;
|
||||
organizationUrl?: string;
|
||||
snapshotsUrl?: string;
|
||||
performanceUrl?: string;
|
||||
usersAndRolesUrl?: string;
|
||||
}
|
||||
|
||||
export class CloudPlugin implements Plugin<CloudSetup> {
|
||||
|
@ -110,7 +114,14 @@ export class CloudPlugin implements Plugin<CloudSetup> {
|
|||
);
|
||||
};
|
||||
|
||||
const { deploymentUrl, profileUrl, billingUrl, organizationUrl } = this.getCloudUrls();
|
||||
const {
|
||||
deploymentUrl,
|
||||
profileUrl,
|
||||
billingUrl,
|
||||
organizationUrl,
|
||||
performanceUrl,
|
||||
usersAndRolesUrl,
|
||||
} = this.getCloudUrls();
|
||||
|
||||
let decodedId: DecodedCloudId | undefined;
|
||||
if (this.config.id) {
|
||||
|
@ -131,6 +142,8 @@ export class CloudPlugin implements Plugin<CloudSetup> {
|
|||
serverless: {
|
||||
projectId: this.config.serverless?.project_id,
|
||||
},
|
||||
performanceUrl,
|
||||
usersAndRolesUrl,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -143,12 +156,16 @@ export class CloudPlugin implements Plugin<CloudSetup> {
|
|||
organization_url: organizationUrl,
|
||||
deployment_url: deploymentUrl,
|
||||
base_url: baseUrl,
|
||||
performance_url: performanceUrl,
|
||||
users_and_roles_url: usersAndRolesUrl,
|
||||
} = this.config;
|
||||
|
||||
const fullCloudDeploymentUrl = getFullCloudUrl(baseUrl, deploymentUrl);
|
||||
const fullCloudProfileUrl = getFullCloudUrl(baseUrl, profileUrl);
|
||||
const fullCloudBillingUrl = getFullCloudUrl(baseUrl, billingUrl);
|
||||
const fullCloudOrganizationUrl = getFullCloudUrl(baseUrl, organizationUrl);
|
||||
const fullCloudPerformanceUrl = getFullCloudUrl(baseUrl, performanceUrl);
|
||||
const fullCloudUsersAndRolesUrl = getFullCloudUrl(baseUrl, usersAndRolesUrl);
|
||||
const fullCloudSnapshotsUrl = `${fullCloudDeploymentUrl}/${CLOUD_SNAPSHOTS_PATH}`;
|
||||
|
||||
return {
|
||||
|
@ -157,6 +174,8 @@ export class CloudPlugin implements Plugin<CloudSetup> {
|
|||
billingUrl: fullCloudBillingUrl,
|
||||
organizationUrl: fullCloudOrganizationUrl,
|
||||
snapshotsUrl: fullCloudSnapshotsUrl,
|
||||
performanceUrl: fullCloudPerformanceUrl,
|
||||
usersAndRolesUrl: fullCloudUsersAndRolesUrl,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
@ -36,6 +36,14 @@ export interface CloudStart {
|
|||
* The full URL to the organization management page on Elastic Cloud. Undefined if not running on Cloud.
|
||||
*/
|
||||
organizationUrl?: string;
|
||||
/**
|
||||
* The full URL to the performance page on Elastic Cloud. Undefined if not running on Cloud.
|
||||
*/
|
||||
performanceUrl?: string;
|
||||
/**
|
||||
* The full URL to the users and roles page on Elastic Cloud. Undefined if not running on Cloud.
|
||||
*/
|
||||
usersAndRolesUrl?: string;
|
||||
/**
|
||||
* The full URL to the elasticsearch cluster.
|
||||
*/
|
||||
|
|
|
@ -25,6 +25,8 @@ const configSchema = schema.object({
|
|||
deployment_url: schema.maybe(schema.string()),
|
||||
id: schema.maybe(schema.string()),
|
||||
billing_url: schema.maybe(schema.string()),
|
||||
performance_url: schema.maybe(schema.string()),
|
||||
users_and_roles_url: schema.maybe(schema.string()),
|
||||
organization_url: schema.maybe(schema.string()),
|
||||
profile_url: schema.maybe(schema.string()),
|
||||
trial_end_date: schema.maybe(schema.string()),
|
||||
|
@ -45,6 +47,8 @@ export const config: PluginConfigDescriptor<CloudConfigType> = {
|
|||
deployment_url: true,
|
||||
id: true,
|
||||
billing_url: true,
|
||||
users_and_roles_url: true,
|
||||
performance_url: true,
|
||||
organization_url: true,
|
||||
profile_url: true,
|
||||
trial_end_date: true,
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
"server": true,
|
||||
"browser": true,
|
||||
"configPath": ["xpack", "serverless", "observability"],
|
||||
"requiredPlugins": ["serverless", "observabilityShared", "kibanaReact", "management", "ml"],
|
||||
"requiredPlugins": ["serverless", "observabilityShared", "kibanaReact", "management", "ml", "cloud"],
|
||||
"optionalPlugins": [],
|
||||
"requiredBundles": []
|
||||
}
|
||||
|
|
|
@ -5,7 +5,8 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { CoreStart } from '@kbn/core/public';
|
||||
import type { CoreStart } from '@kbn/core/public';
|
||||
import type { CloudStart } from '@kbn/cloud-plugin/public';
|
||||
import { ServerlessPluginStart } from '@kbn/serverless/public';
|
||||
import {
|
||||
DefaultNavigation,
|
||||
|
@ -37,6 +38,9 @@ const navigationTree: NavigationTreeDefinition = {
|
|||
defaultMessage: 'Dashboards',
|
||||
}),
|
||||
link: 'dashboards',
|
||||
getIsActive: ({ pathNameSerialized, prepend }) => {
|
||||
return pathNameSerialized.startsWith(prepend('/app/dashboards'));
|
||||
},
|
||||
},
|
||||
{
|
||||
link: 'observability-overview:alerts',
|
||||
|
@ -75,13 +79,14 @@ const navigationTree: NavigationTreeDefinition = {
|
|||
},
|
||||
],
|
||||
},
|
||||
|
||||
{
|
||||
id: 'applications',
|
||||
children: [
|
||||
{
|
||||
id: 'apm',
|
||||
title: 'Applications',
|
||||
title: i18n.translate('xpack.serverlessObservability.nav.applications', {
|
||||
defaultMessage: 'Applications',
|
||||
}),
|
||||
children: [
|
||||
{
|
||||
link: 'apm:services',
|
||||
|
@ -107,6 +112,13 @@ const navigationTree: NavigationTreeDefinition = {
|
|||
defaultMessage: 'Visualizations',
|
||||
}),
|
||||
link: 'visualize',
|
||||
getIsActive: ({ pathNameSerialized, prepend }) => {
|
||||
return (
|
||||
pathNameSerialized.startsWith(prepend('/app/visualize')) ||
|
||||
pathNameSerialized.startsWith(prepend('/app/lens')) ||
|
||||
pathNameSerialized.startsWith(prepend('/app/maps'))
|
||||
);
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
|
@ -127,10 +139,20 @@ const navigationTree: NavigationTreeDefinition = {
|
|||
footer: [
|
||||
{
|
||||
type: 'navGroup',
|
||||
id: 'projest_settings_project_nav',
|
||||
title: 'Project settings',
|
||||
id: 'devTools',
|
||||
title: i18n.translate('xpack.serverlessObservability.nav.devTools', {
|
||||
defaultMessage: 'Developer tools',
|
||||
}),
|
||||
link: 'dev_tools',
|
||||
icon: 'editorCodeBlock',
|
||||
},
|
||||
{
|
||||
type: 'navGroup',
|
||||
id: 'project_settings_project_nav',
|
||||
title: i18n.translate('xpack.serverlessObservability.nav.projectSettings', {
|
||||
defaultMessage: 'Project settings',
|
||||
}),
|
||||
icon: 'gear',
|
||||
defaultIsCollapsed: true,
|
||||
breadcrumbStatus: 'hidden',
|
||||
children: [
|
||||
{
|
||||
|
@ -148,6 +170,18 @@ const navigationTree: NavigationTreeDefinition = {
|
|||
{
|
||||
link: 'fleet',
|
||||
},
|
||||
{
|
||||
id: 'cloudLinkUserAndRoles',
|
||||
cloudLink: 'userAndRoles',
|
||||
},
|
||||
{
|
||||
id: 'cloudLinkPerformance',
|
||||
cloudLink: 'performance',
|
||||
},
|
||||
{
|
||||
id: 'cloudLinkBilling',
|
||||
cloudLink: 'billingAndSub',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
|
@ -156,10 +190,13 @@ const navigationTree: NavigationTreeDefinition = {
|
|||
};
|
||||
|
||||
export const getObservabilitySideNavComponent =
|
||||
(core: CoreStart, { serverless }: { serverless: ServerlessPluginStart }) =>
|
||||
(
|
||||
core: CoreStart,
|
||||
{ serverless, cloud }: { serverless: ServerlessPluginStart; cloud: CloudStart }
|
||||
) =>
|
||||
() => {
|
||||
return (
|
||||
<NavigationKibanaProvider core={core} serverless={serverless}>
|
||||
<NavigationKibanaProvider core={core} serverless={serverless} cloud={cloud}>
|
||||
<DefaultNavigation navigationTree={navigationTree} dataTestSubj="svlObservabilitySideNav" />
|
||||
</NavigationKibanaProvider>
|
||||
);
|
||||
|
|
|
@ -29,10 +29,10 @@ export class ServerlessObservabilityPlugin
|
|||
core: CoreStart,
|
||||
setupDeps: ServerlessObservabilityPluginStartDependencies
|
||||
): ServerlessObservabilityPluginStart {
|
||||
const { observabilityShared, serverless, management } = setupDeps;
|
||||
const { observabilityShared, serverless, management, cloud } = setupDeps;
|
||||
observabilityShared.setIsSidebarEnabled(false);
|
||||
serverless.setProjectHome('/app/observability/landing');
|
||||
serverless.setSideNavComponent(getObservabilitySideNavComponent(core, { serverless }));
|
||||
serverless.setSideNavComponent(getObservabilitySideNavComponent(core, { serverless, cloud }));
|
||||
management.setupCardsNavigation({
|
||||
enabled: true,
|
||||
hideLinksTo: [appIds.RULES],
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
*/
|
||||
|
||||
import { CoreStart } from '@kbn/core/public';
|
||||
import type { CloudStart } from '@kbn/cloud-plugin/public';
|
||||
import React from 'react';
|
||||
import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public';
|
||||
import type { ServerlessObservabilityPluginStartDependencies } from './types';
|
||||
|
@ -15,7 +16,8 @@ type Services = CoreStart & ServerlessObservabilityPluginStartDependencies;
|
|||
export const KibanaServicesProvider: React.FC<{
|
||||
core: CoreStart;
|
||||
pluginsStart: ServerlessObservabilityPluginStartDependencies;
|
||||
}> = ({ core, pluginsStart, children }) => {
|
||||
const services: Services = { ...core, ...pluginsStart };
|
||||
cloud: CloudStart;
|
||||
}> = ({ core, pluginsStart, cloud, children }) => {
|
||||
const services: Services = { ...core, ...pluginsStart, cloud };
|
||||
return <KibanaContextProvider services={services}>{children}</KibanaContextProvider>;
|
||||
};
|
||||
|
|
|
@ -11,6 +11,7 @@ import {
|
|||
ObservabilitySharedPluginStart,
|
||||
} from '@kbn/observability-shared-plugin/public';
|
||||
import type { ManagementSetup, ManagementStart } from '@kbn/management-plugin/public';
|
||||
import type { CloudStart } from '@kbn/cloud-plugin/public';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-interface
|
||||
export interface ServerlessObservabilityPluginSetup {}
|
||||
|
@ -28,4 +29,5 @@ export interface ServerlessObservabilityPluginStartDependencies {
|
|||
observabilityShared: ObservabilitySharedPluginStart;
|
||||
serverless: ServerlessPluginStart;
|
||||
management: ManagementStart;
|
||||
cloud: CloudStart;
|
||||
}
|
||||
|
|
|
@ -25,5 +25,6 @@
|
|||
"@kbn/ml-plugin",
|
||||
"@kbn/i18n",
|
||||
"@kbn/management-cards-navigation",
|
||||
"@kbn/cloud-plugin",
|
||||
]
|
||||
}
|
||||
|
|
|
@ -15,6 +15,7 @@ import {
|
|||
import React from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import type { ServerlessPluginStart } from '@kbn/serverless/public';
|
||||
import type { CloudStart } from '@kbn/cloud-plugin/public';
|
||||
|
||||
const navigationTree: NavigationTreeDefinition = {
|
||||
body: [
|
||||
|
@ -52,9 +53,19 @@ const navigationTree: NavigationTreeDefinition = {
|
|||
},
|
||||
{
|
||||
link: 'dashboards',
|
||||
getIsActive: ({ pathNameSerialized, prepend }) => {
|
||||
return pathNameSerialized.startsWith(prepend('/app/dashboards'));
|
||||
},
|
||||
},
|
||||
{
|
||||
link: 'visualize',
|
||||
getIsActive: ({ pathNameSerialized, prepend }) => {
|
||||
return (
|
||||
pathNameSerialized.startsWith(prepend('/app/visualize')) ||
|
||||
pathNameSerialized.startsWith(prepend('/app/lens')) ||
|
||||
pathNameSerialized.startsWith(prepend('/app/maps'))
|
||||
);
|
||||
},
|
||||
},
|
||||
{ link: 'observability-overview:alerts' },
|
||||
],
|
||||
|
@ -106,13 +117,52 @@ const navigationTree: NavigationTreeDefinition = {
|
|||
...getPresets('ml'),
|
||||
},
|
||||
],
|
||||
footer: [
|
||||
{
|
||||
type: 'navGroup',
|
||||
id: 'project_settings_project_nav',
|
||||
title: i18n.translate('xpack.serverlessSearch.nav.projectSettings', {
|
||||
defaultMessage: 'Project settings',
|
||||
}),
|
||||
icon: 'gear',
|
||||
breadcrumbStatus: 'hidden',
|
||||
children: [
|
||||
{
|
||||
id: 'settings',
|
||||
children: [
|
||||
{
|
||||
link: 'management',
|
||||
title: i18n.translate('xpack.serverlessSearch.nav.mngt', {
|
||||
defaultMessage: 'Management',
|
||||
}),
|
||||
},
|
||||
{
|
||||
id: 'cloudLinkUserAndRoles',
|
||||
cloudLink: 'userAndRoles',
|
||||
},
|
||||
{
|
||||
id: 'cloudLinkPerformance',
|
||||
cloudLink: 'performance',
|
||||
},
|
||||
{
|
||||
id: 'cloudLinkBilling',
|
||||
cloudLink: 'billingAndSub',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export const createServerlessSearchSideNavComponent =
|
||||
(core: CoreStart, { serverless }: { serverless: ServerlessPluginStart }) =>
|
||||
(
|
||||
core: CoreStart,
|
||||
{ serverless, cloud }: { serverless: ServerlessPluginStart; cloud: CloudStart }
|
||||
) =>
|
||||
() => {
|
||||
return (
|
||||
<NavigationKibanaProvider core={core} serverless={serverless}>
|
||||
<NavigationKibanaProvider core={core} serverless={serverless} cloud={cloud}>
|
||||
<DefaultNavigation navigationTree={navigationTree} dataTestSubj="svlSearchSideNav" />
|
||||
</NavigationKibanaProvider>
|
||||
);
|
||||
|
|
|
@ -69,10 +69,10 @@ export class ServerlessSearchPlugin
|
|||
|
||||
public start(
|
||||
core: CoreStart,
|
||||
{ serverless, management, observabilityShared }: ServerlessSearchPluginStartDependencies
|
||||
{ serverless, management, observabilityShared, cloud }: ServerlessSearchPluginStartDependencies
|
||||
): ServerlessSearchPluginStart {
|
||||
serverless.setProjectHome('/app/elasticsearch');
|
||||
serverless.setSideNavComponent(createComponent(core, { serverless }));
|
||||
serverless.setSideNavComponent(createComponent(core, { serverless, cloud }));
|
||||
observabilityShared.setIsSidebarEnabled(false);
|
||||
management.setupCardsNavigation({
|
||||
enabled: true,
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue