+ }
+ />
);
};
From d26b3736ce4b25063647e7c50675c51ede5a41c3 Mon Sep 17 00:00:00 2001
From: Gloria Hornero
Date: Tue, 31 Oct 2023 12:58:44 +0100
Subject: [PATCH 05/12] [Security Solution] Fixes problematic test (#170222)
## Summary
After the merge of https://github.com/elastic/kibana/pull/169563 seems
that there is a test still using resetKibana (I don't know the reason).
In this PR we are fixing that.
Execution in ESS:
Execution in Serverless:
---
.../cypress/e2e/header/search_bar.cy.ts | 7 -------
1 file changed, 7 deletions(-)
diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/header/search_bar.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/header/search_bar.cy.ts
index 383a51592e91..a92e5aa1d93d 100644
--- a/x-pack/test/security_solution_cypress/cypress/e2e/header/search_bar.cy.ts
+++ b/x-pack/test/security_solution_cypress/cypress/e2e/header/search_bar.cy.ts
@@ -25,18 +25,11 @@ import { waitForAllHostsToBeLoaded } from '../../tasks/hosts/all_hosts';
describe('SearchBar', { tags: ['@ess', '@serverless'] }, () => {
beforeEach(() => {
- cy.task('esArchiverResetKibana');
- cy.task('esArchiverLoad', { archiveName: 'auditbeat' });
-
login();
visitWithTimeRange(hostsUrl('allHosts'));
waitForAllHostsToBeLoaded();
});
- afterEach(() => {
- cy.task('esArchiverUnload', 'auditbeat');
- });
-
it('adds correctly a filter to the global search bar', () => {
openAddFilterPopover();
fillAddFilterForm(getHostIpFilter());
From 8868d08745c4fb760bb82d5a2fc2e3a60da67afb Mon Sep 17 00:00:00 2001
From: Pierre Gayvallet
Date: Tue, 31 Oct 2023 13:55:03 +0100
Subject: [PATCH 06/12] make `isInlineScriptingEnabled` resilient to ES errors
(#170208)
## Summary
Fix https://github.com/elastic/kibana/issues/163787
Change the way `isInlineScriptingEnabled` function to retry retryable
errors from ES (similar to how the valid connection or migration ES
calls do)
---
.../index.ts | 1 +
.../src/is_scripting_enabled.test.mocks.ts | 15 ++++
.../src/is_scripting_enabled.test.ts | 55 ++++++++++++++
.../src/is_scripting_enabled.ts | 47 ++++++++----
.../src/retryable_es_client_errors.test.ts | 73 +++++++++++++++++++
.../src/retryable_es_client_errors.ts | 40 ++++++++++
.../catch_retryable_es_client_errors.ts | 23 +-----
7 files changed, 220 insertions(+), 34 deletions(-)
create mode 100644 packages/core/elasticsearch/core-elasticsearch-server-internal/src/is_scripting_enabled.test.mocks.ts
create mode 100644 packages/core/elasticsearch/core-elasticsearch-server-internal/src/retryable_es_client_errors.test.ts
create mode 100644 packages/core/elasticsearch/core-elasticsearch-server-internal/src/retryable_es_client_errors.ts
diff --git a/packages/core/elasticsearch/core-elasticsearch-server-internal/index.ts b/packages/core/elasticsearch/core-elasticsearch-server-internal/index.ts
index 3d81cebf9dc8..983be835a8db 100644
--- a/packages/core/elasticsearch/core-elasticsearch-server-internal/index.ts
+++ b/packages/core/elasticsearch/core-elasticsearch-server-internal/index.ts
@@ -30,4 +30,5 @@ export { CoreElasticsearchRouteHandlerContext } from './src/elasticsearch_route_
export { retryCallCluster, migrationRetryCallCluster } from './src/retry_call_cluster';
export { isInlineScriptingEnabled } from './src/is_scripting_enabled';
export { getCapabilitiesFromClient } from './src/get_capabilities';
+export { isRetryableEsClientError } from './src/retryable_es_client_errors';
export type { ClusterInfo } from './src/get_cluster_info';
diff --git a/packages/core/elasticsearch/core-elasticsearch-server-internal/src/is_scripting_enabled.test.mocks.ts b/packages/core/elasticsearch/core-elasticsearch-server-internal/src/is_scripting_enabled.test.mocks.ts
new file mode 100644
index 000000000000..99485dca9a58
--- /dev/null
+++ b/packages/core/elasticsearch/core-elasticsearch-server-internal/src/is_scripting_enabled.test.mocks.ts
@@ -0,0 +1,15 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+export const isRetryableEsClientErrorMock = jest.fn();
+
+jest.doMock('./retryable_es_client_errors', () => {
+ return {
+ isRetryableEsClientError: isRetryableEsClientErrorMock,
+ };
+});
diff --git a/packages/core/elasticsearch/core-elasticsearch-server-internal/src/is_scripting_enabled.test.ts b/packages/core/elasticsearch/core-elasticsearch-server-internal/src/is_scripting_enabled.test.ts
index d2922c0161c6..57d40936b824 100644
--- a/packages/core/elasticsearch/core-elasticsearch-server-internal/src/is_scripting_enabled.test.ts
+++ b/packages/core/elasticsearch/core-elasticsearch-server-internal/src/is_scripting_enabled.test.ts
@@ -6,6 +6,7 @@
* Side Public License, v 1.
*/
+import { isRetryableEsClientErrorMock } from './is_scripting_enabled.test.mocks';
import * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
import { elasticsearchClientMock } from '@kbn/core-elasticsearch-client-server-mocks';
import { isInlineScriptingEnabled } from './is_scripting_enabled';
@@ -94,4 +95,58 @@ describe('isInlineScriptingEnabled', () => {
expect(await isInlineScriptingEnabled({ client })).toEqual(false);
});
+
+ describe('resiliency', () => {
+ beforeEach(() => {
+ isRetryableEsClientErrorMock.mockReset();
+ });
+
+ const mockSuccessOnce = () => {
+ client.cluster.getSettings.mockResolvedValueOnce({
+ transient: {},
+ persistent: {},
+ defaults: {},
+ });
+ };
+ const mockErrorOnce = () => {
+ client.cluster.getSettings.mockResponseImplementationOnce(() => {
+ throw Error('ERR CON REFUSED');
+ });
+ };
+
+ it('retries the ES api call in case of retryable error', async () => {
+ isRetryableEsClientErrorMock.mockReturnValue(true);
+
+ mockErrorOnce();
+ mockSuccessOnce();
+
+ await expect(isInlineScriptingEnabled({ client, maxRetryDelay: 1 })).resolves.toEqual(true);
+ expect(client.cluster.getSettings).toHaveBeenCalledTimes(2);
+ });
+
+ it('throws in case of non-retryable error', async () => {
+ isRetryableEsClientErrorMock.mockReturnValue(false);
+
+ mockErrorOnce();
+ mockSuccessOnce();
+
+ await expect(isInlineScriptingEnabled({ client, maxRetryDelay: 0.1 })).rejects.toThrowError(
+ 'ERR CON REFUSED'
+ );
+ });
+
+ it('retries up to `maxRetries` times', async () => {
+ isRetryableEsClientErrorMock.mockReturnValue(true);
+
+ mockErrorOnce();
+ mockErrorOnce();
+ mockErrorOnce();
+ mockSuccessOnce();
+
+ await expect(
+ isInlineScriptingEnabled({ client, maxRetryDelay: 0.1, maxRetries: 2 })
+ ).rejects.toThrowError('ERR CON REFUSED');
+ expect(client.cluster.getSettings).toHaveBeenCalledTimes(3);
+ });
+ });
});
diff --git a/packages/core/elasticsearch/core-elasticsearch-server-internal/src/is_scripting_enabled.ts b/packages/core/elasticsearch/core-elasticsearch-server-internal/src/is_scripting_enabled.ts
index 6a3900229c0d..ca3ca5b5c59a 100644
--- a/packages/core/elasticsearch/core-elasticsearch-server-internal/src/is_scripting_enabled.ts
+++ b/packages/core/elasticsearch/core-elasticsearch-server-internal/src/is_scripting_enabled.ts
@@ -6,27 +6,48 @@
* Side Public License, v 1.
*/
+import { defer, map, retry, timer, firstValueFrom, throwError } from 'rxjs';
import type { ElasticsearchClient } from '@kbn/core-elasticsearch-server';
+import { isRetryableEsClientError } from './retryable_es_client_errors';
const scriptAllowedTypesKey = 'script.allowed_types';
export const isInlineScriptingEnabled = async ({
client,
+ maxRetries = 20,
+ maxRetryDelay = 64,
}: {
client: ElasticsearchClient;
+ maxRetries?: number;
+ maxRetryDelay?: number;
}): Promise => {
- const settings = await client.cluster.getSettings({
- include_defaults: true,
- flat_settings: true,
- });
+ return firstValueFrom(
+ defer(() => {
+ return client.cluster.getSettings({
+ include_defaults: true,
+ flat_settings: true,
+ });
+ }).pipe(
+ retry({
+ count: maxRetries,
+ delay: (error, retryIndex) => {
+ if (isRetryableEsClientError(error)) {
+ const retryDelay = 1000 * Math.min(Math.pow(2, retryIndex), maxRetryDelay); // 2s, 4s, 8s, 16s, 32s, 64s, 64s, 64s ...
+ return timer(retryDelay);
+ } else {
+ return throwError(error);
+ }
+ },
+ }),
+ map((settings) => {
+ const scriptAllowedTypes: string[] =
+ settings.transient[scriptAllowedTypesKey] ??
+ settings.persistent[scriptAllowedTypesKey] ??
+ settings.defaults![scriptAllowedTypesKey] ??
+ [];
- // priority: transient -> persistent -> default
- const scriptAllowedTypes: string[] =
- settings.transient[scriptAllowedTypesKey] ??
- settings.persistent[scriptAllowedTypesKey] ??
- settings.defaults![scriptAllowedTypesKey] ??
- [];
-
- // when unspecified, the setting as a default `[]` value that means that both scriptings are allowed.
- return scriptAllowedTypes.length === 0 || scriptAllowedTypes.includes('inline');
+ return scriptAllowedTypes.length === 0 || scriptAllowedTypes.includes('inline');
+ })
+ )
+ );
};
diff --git a/packages/core/elasticsearch/core-elasticsearch-server-internal/src/retryable_es_client_errors.test.ts b/packages/core/elasticsearch/core-elasticsearch-server-internal/src/retryable_es_client_errors.test.ts
new file mode 100644
index 000000000000..45015cece5b4
--- /dev/null
+++ b/packages/core/elasticsearch/core-elasticsearch-server-internal/src/retryable_es_client_errors.test.ts
@@ -0,0 +1,73 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import { errors as esErrors } from '@elastic/elasticsearch';
+import { elasticsearchClientMock } from '@kbn/core-elasticsearch-client-server-mocks';
+import { isRetryableEsClientError } from './retryable_es_client_errors';
+
+describe('isRetryableEsClientError', () => {
+ describe('returns `false` for', () => {
+ test('non-retryable response errors', async () => {
+ const error = new esErrors.ResponseError(
+ elasticsearchClientMock.createApiResponse({
+ body: { error: { type: 'cluster_block_exception' } },
+ statusCode: 400,
+ })
+ );
+
+ expect(isRetryableEsClientError(error)).toEqual(false);
+ });
+ });
+
+ describe('returns `true` for', () => {
+ it('NoLivingConnectionsError', () => {
+ const error = new esErrors.NoLivingConnectionsError(
+ 'reason',
+ elasticsearchClientMock.createApiResponse()
+ );
+
+ expect(isRetryableEsClientError(error)).toEqual(true);
+ });
+
+ it('ConnectionError', () => {
+ const error = new esErrors.ConnectionError(
+ 'reason',
+ elasticsearchClientMock.createApiResponse()
+ );
+ expect(isRetryableEsClientError(error)).toEqual(true);
+ });
+
+ it('TimeoutError', () => {
+ const error = new esErrors.TimeoutError(
+ 'reason',
+ elasticsearchClientMock.createApiResponse()
+ );
+ expect(isRetryableEsClientError(error)).toEqual(true);
+ });
+
+ it('ResponseError of type snapshot_in_progress_exception', () => {
+ const error = new esErrors.ResponseError(
+ elasticsearchClientMock.createApiResponse({
+ body: { error: { type: 'snapshot_in_progress_exception' } },
+ })
+ );
+ expect(isRetryableEsClientError(error)).toEqual(true);
+ });
+
+ it.each([503, 401, 403, 408, 410, 429])('ResponseError with %p status code', (statusCode) => {
+ const error = new esErrors.ResponseError(
+ elasticsearchClientMock.createApiResponse({
+ statusCode,
+ body: { error: { type: 'reason' } },
+ })
+ );
+
+ expect(isRetryableEsClientError(error)).toEqual(true);
+ });
+ });
+});
diff --git a/packages/core/elasticsearch/core-elasticsearch-server-internal/src/retryable_es_client_errors.ts b/packages/core/elasticsearch/core-elasticsearch-server-internal/src/retryable_es_client_errors.ts
new file mode 100644
index 000000000000..2ba0ff20a273
--- /dev/null
+++ b/packages/core/elasticsearch/core-elasticsearch-server-internal/src/retryable_es_client_errors.ts
@@ -0,0 +1,40 @@
+/*
+ * 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 { errors as EsErrors } from '@elastic/elasticsearch';
+
+const retryResponseStatuses = [
+ 503, // ServiceUnavailable
+ 401, // AuthorizationException
+ 403, // AuthenticationException
+ 408, // RequestTimeout
+ 410, // Gone
+ 429, // TooManyRequests -> ES circuit breaker
+];
+
+/**
+ * Returns true if the given elasticsearch error should be retried
+ * by retry-based resiliency systems such as the SO migration, false otherwise.
+ */
+export const isRetryableEsClientError = (e: EsErrors.ElasticsearchClientError): boolean => {
+ if (
+ e instanceof EsErrors.NoLivingConnectionsError ||
+ e instanceof EsErrors.ConnectionError ||
+ e instanceof EsErrors.TimeoutError ||
+ (e instanceof EsErrors.ResponseError &&
+ (retryResponseStatuses.includes(e?.statusCode!) ||
+ // ES returns a 400 Bad Request when trying to close or delete an
+ // index while snapshots are in progress. This should have been a 503
+ // so once https://github.com/elastic/elasticsearch/issues/65883 is
+ // fixed we can remove this.
+ e?.body?.error?.type === 'snapshot_in_progress_exception'))
+ ) {
+ return true;
+ }
+ return false;
+};
diff --git a/packages/core/saved-objects/core-saved-objects-migration-server-internal/src/actions/catch_retryable_es_client_errors.ts b/packages/core/saved-objects/core-saved-objects-migration-server-internal/src/actions/catch_retryable_es_client_errors.ts
index 74877c938642..965742a2abb8 100644
--- a/packages/core/saved-objects/core-saved-objects-migration-server-internal/src/actions/catch_retryable_es_client_errors.ts
+++ b/packages/core/saved-objects/core-saved-objects-migration-server-internal/src/actions/catch_retryable_es_client_errors.ts
@@ -8,15 +8,7 @@
import * as Either from 'fp-ts/lib/Either';
import { errors as EsErrors } from '@elastic/elasticsearch';
-
-const retryResponseStatuses = [
- 503, // ServiceUnavailable
- 401, // AuthorizationException
- 403, // AuthenticationException
- 408, // RequestTimeout
- 410, // Gone
- 429, // TooManyRequests -> ES circuit breaker
-];
+import { isRetryableEsClientError } from '@kbn/core-elasticsearch-server-internal';
export interface RetryableEsClientError {
type: 'retryable_es_client_error';
@@ -27,18 +19,7 @@ export interface RetryableEsClientError {
export const catchRetryableEsClientErrors = (
e: EsErrors.ElasticsearchClientError
): Either.Either => {
- if (
- e instanceof EsErrors.NoLivingConnectionsError ||
- e instanceof EsErrors.ConnectionError ||
- e instanceof EsErrors.TimeoutError ||
- (e instanceof EsErrors.ResponseError &&
- (retryResponseStatuses.includes(e?.statusCode!) ||
- // ES returns a 400 Bad Request when trying to close or delete an
- // index while snapshots are in progress. This should have been a 503
- // so once https://github.com/elastic/elasticsearch/issues/65883 is
- // fixed we can remove this.
- e?.body?.error?.type === 'snapshot_in_progress_exception'))
- ) {
+ if (isRetryableEsClientError(e)) {
return Either.left({
type: 'retryable_es_client_error' as const,
message: e?.message,
From 508f30926495949e9d9b1f6505071aee1c511f6e Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?S=C3=A9bastien=20Loix?=
Date: Tue, 31 Oct 2023 13:07:51 +0000
Subject: [PATCH 07/12] [Serverless nav] Update docs with panels API (#170221)
---
.../src/project_navigation.ts | 6 +-
.../serverless_projects_documentation.mdx | 150 ++++++++++++++++--
2 files changed, 136 insertions(+), 20 deletions(-)
diff --git a/packages/core/chrome/core-chrome-browser/src/project_navigation.ts b/packages/core/chrome/core-chrome-browser/src/project_navigation.ts
index e7c1689048b0..b879cf6f716d 100644
--- a/packages/core/chrome/core-chrome-browser/src/project_navigation.ts
+++ b/packages/core/chrome/core-chrome-browser/src/project_navigation.ts
@@ -83,11 +83,7 @@ interface NodeDefinitionBase {
*/
breadcrumbStatus?: 'hidden' | 'visible';
/**
- * Optional status to for the side navigation. "hidden" and "visible" are self explanatory.
- * The `renderAsItem` status is _only_ for group nodes (nodes with children declared or with
- * the "nodeType" set to `group`) and allow to render the node as an "item" instead of the head of
- * a group. This is usefull to have sub-pages declared in the tree that will correctly be mapped
- * in the Breadcrumbs, but are not rendered in the side navigation.
+ * Optional status to indicate if the node should be hidden in the side nav (but still present in the navigation tree).
* @default 'visible'
*/
sideNavStatus?: SideNavNodeStatus;
diff --git a/packages/shared-ux/chrome/serverless_projects_documentation.mdx b/packages/shared-ux/chrome/serverless_projects_documentation.mdx
index 498d2a0737f9..25727791e038 100644
--- a/packages/shared-ux/chrome/serverless_projects_documentation.mdx
+++ b/packages/shared-ux/chrome/serverless_projects_documentation.mdx
@@ -48,11 +48,11 @@ In the following sections, we will explore each of these building blocks in deta
> Left Side Navigation is available in shared_ux storybook under the `Chrome/Navigation` section. You can explore the components and their properties there.
> `yarn storybook shared_ux`
-The left side navigation is a primary way for users to navigate through different sections of the project. It consists of a tree of navigation items that can be expanded and collapsed. Apart from the navigation tree it also supports special pre-built blocks like recently accessed items. The main part of the navigation tree is project's navigation: this is fully configured and supported by the project teams (e.g. Observability). We also provide pre-configured platform sections as presets that solutions can use as part of their navigation (e.g. `ml`, `analytics`). Solutions can customize those sections to their needs.
+The left side navigation is a primary way for users to navigate through different sections of the project. It consists of a tree of navigation items that can be expanded/collapsed or opened in a side panel. Apart from the navigation tree it also supports special pre-built blocks like recently accessed items. The main part of the navigation tree is project's navigation: this is fully configured and supported by the project teams (e.g. Observability). We also provide pre-configured platform sections as presets that solutions can use as part of their navigation (e.g. `ml`, `analytics`). Solutions can customize those sections to their needs.
There are two approaches to building the side navigation:
-1. **Navigation tree definition**: Developers provide a navigation tree definition. This approach is recommended if the use case works with the existing building blocks.
+1. **Navigation tree definition**: Developers provide a navigation tree definition. ~~This approach is recommended if the use case works with the existing building blocks.~~ :warning: We are planing in deprecating this approach, use the React components instead.
2. **React components**: Alternatively, we provide a set of pre-built components that can be used to construct the left side navigation. These components include:
@@ -69,6 +69,8 @@ By leveraging these components, you can create a customized left side navigation
### Navigation Tree Definition
+:warning: **Soon to be deprecated** :warning: - Use the React components instead.
+
Use the `NavigationTreeDefinition` interface to create your left side navigation using a tree definition. This interface allows you to define the complete navigation tree, including the **body** and **footer** of the navigation.
#### Example
@@ -109,6 +111,19 @@ const navigationTree: NavigationTreeDefinition = {
},
],
},
+ {
+ id: 'search',
+ title: 'Search',
+ renderAs: 'accordion', // This group will be rendered as an accordion
+ children: [
+ {
+ link: 'foo',
+ },
+ {
+ link: 'bar',
+ },
+ ],
+ },
],
},
{
@@ -126,6 +141,7 @@ const navigationTree: NavigationTreeDefinition = {
children: [
{
id: 'settings',
+ renderAs: 'accordion',
children: [
{
link: 'management',
@@ -190,25 +206,41 @@ The `RootNavigationItemDefinition` is one of:
- `GroupDefinition`
- `RecentlyAccessedDefinition`
+- `PresetDefinition`
+- `ItemDefinition`
+
+All those interfaces extend the `NodeDefinition` which has the following **common properties**:
+
+### `NodeDefinition`
+
+| Property | Type | Description|
+| -------------------- | -------------------------| --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| `id` | `Id` (extends `string`) | Optional ID of the navigation node.|
+| `title` | `string` | Optional title of the navigation node. If not provided and a "link" is provided, the title will be the Deep link title.|
+| `link` | `LinkId` | Optional App ID or deep link ID for the navigation node. [More about deep links](#deep-links)|
+| `icon` | `string` | Optional icon for the navigation node. Note that not all navigation depths will render the icon.|
+| `href` | `string` | Use `href` for absolute links only. Internal links should use "link".|
+| `getIsActive` | `function` | Optional function to control the active state. This function is called whenever the location changes.|
+| `sideNavStatus` | `'hidden'\|'visible'` | Optional status to indicate if the node should be hidden in the side nav (but still present in the navigation tree). |
+| `breadcrumbStatus` | `'hidden'\|'visible'` | An optional flag to indicate if the breadcrumb should be hidden when this node is active. The default value is `'visible'`.|
+| `spaceBefore` | `EuiThemeSize\|null` | Optional vertical space to add before this node. It defaults to `null` except for group node at **tree depth 1** where it defaults to `"m"`. |
+
##### `GroupDefinition`
The `GroupDefinition` interface represents a group of items in the side navigation. It extends the `NodeDefinition` interface and has the following additional properties:
-| Property | Type | Description |
+| Property | Type | Description|
| -------------------- | ----------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
-| `type` | `'navGroup'` | Indicates that this item is a navigation group. |
-| `defaultIsCollapsed` | `boolean \| undefined` | Determines if the group is initially collapsed or expanded. Use `undefined` (recommended) to open the group if any of its children nodes match the current URL, `false` to always open the group, or `true` to always collapse it. |
-| `preset` | `NavigationGroupPreset` | A preset value for the group, such as `'analytics'`, `'devtools'`, `'ml'`, or `'management'`. |
-| `id` | `Id` | Optional ID of the navigation node. |
-| `title` | `string` | Optional title of the navigation node. If not provided and a "link" is provided, the title will be the Deep link title. |
-| `link` | `LinkId` | Optional App ID or deep link ID for the navigation node. [More about deep links](#deep-links) |
-| `cloudLink` | `CloudLinkId` | Optional cloud link ID for the navigation node. [More about cloud links](#cloud-links) |
-| `icon` | `string` | Optional icon for the navigation node. Note that not all navigation depths will render the icon. |
-| `children` | `NodeDefinition[]` | Optional children of the navigation node. |
-| `href` | `string` | Use `href` for absolute links only. Internal links should use "link". |
-| `getIsActive` | `function` | Optional function to control the active state. This function is called whenever the location changes. |
-| `breadcrumbStatus` | `'hidden'\|'visible'` | An optional flag to indicate if the breadcrumb should be hidden when this node is active. The default value is `'visible'`. |
+| `type` | `'navGroup'` | Indicates that this item is a navigation group.|
+| `children` | `NodeDefinition[]` | Children of the group navigation node. This is recursive, groups can contain other groups. |
+| `defaultIsCollapsed` | `boolean \| undefined` | Determines if the group is initially collapsed or expanded. Use `undefined` (recommended) to open the group if any of its children nodes match the current URL, `false` to always open the group, or `true` to always collapse it.|
+| `cloudLink` | `CloudLinkId` | Optional cloud link ID for the navigation node. [More about cloud links](#cloud-links)|
+| `renderAs` | `'block'\|'accordion'\|'panelOpener'\|'item'`| Property to indicate how the group should be rendered. * `'block'`: this is the default, renders the group as a block of items. * `'accordion'`: wraps the items in an `EuiAccordion`. * `'panelOpener'`: renders as an item with a `link` (required) + an icon button to open a panel on the right of the side nav. * `'item'`: renders the group as an item in the side nav. This is useful when you want to declare descendant links of a node that will be displayed in the breadcrumb as such but you don't want to render any of the `children` in the side nav. |
+| `appendHorizontalRule` | `boolean` | Optional flag to indicate if a horizontal rule should be rendered after the node. Note: this property is currently only available for group nodes in the navigation **panel** opening on the right of the side nav. |
+| `isCollapsible` | `boolean` | Optional flag to indicate if the accordion is collapsible (when `renderAs` is set to `'accordion'`.|
+
+
##### `RecentlyAccessedDefinition`
@@ -218,6 +250,94 @@ The `GroupDefinition` interface represents a group of items in the side navigati
| `recentlyAccessed$` | `Observable` | An optional observable for recently accessed items. If not provided, the recently accessed items from the Chrome service will be used. |
| `defaultIsCollapsed` | `boolean` | If set to `true`, the recently accessed list will be collapsed by default. The default value is `false`. |
+##### `PresetDefinition`
+
+The `PresetDefinition` interface represents a group of items which are prepopulated based on a preset. It extends the `GroupDefinition` interface (without `children` as those are pre-defined) and has the following additional properties:
+
+| Property | Type | Description|
+| -------------------- | ----------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| `type` | `'preset'` | Indicates that this item is a navigation group.|
+| `preset` | `NavigationGroupPreset` | A preset value for the group, such as `'analytics'`, `'devtools'`, `'ml'`, or `'management'`.| |
+
+##### `ItemDefinition`
+
+The `GroupDefinition` interface represents a group of items in the side navigation. It extends the `NodeDefinition` interface and has the following additional properties:
+
+
+| Property | Type | Description|
+| -------------------- | ----------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| `type` | `'navItem'` | Indicates that this item is a navigation item.|
+| `cloudLink` | `CloudLinkId` | Optional cloud link ID for the navigation node. [More about cloud links](#cloud-links)|
+| `openInNewTab` | `boolean` | Optional flag to indicate if the target page should be opened in a new Browser tab. Note: this property is currently only used in the navigation **panel** opening on the right of the side nav.|
+| `withBadge` | `boolean` | Optional flag to indicate if a badge should be rendered next to the text. Note: this property is currently only used in the navigation **panel** opening on the right of the side nav.|
+| `badgeOptions` | `{ text?: string; }` | If `withBadge` is true, this object can be used to customize the badge. |
+
+###Â Panels
+
+As seen in the API above, the `renderAs` property can be used to render a group as a panel opener. This is useful when you want to display a group of links in the side navigation and display its content in a panel on the right of the side navigation.
+The content of the panel can be auto-generaged (based on the group's children) or manually provided.
+
+#### Auto-generated panels
+
+When the panel content is auto-generated, the group's children will be rendered in the panel. Those `children` can be items or other groups (that render as `'block' (default) or `'accordion'`).
+The panel will be opened when the user clicks on the group's icon button. The panel will be closed when the user clicks on the group's icon again or when the user clicks outside of the panel.
+
+#### Manually provided panels
+
+When the panel content is manually provided, the group's `children` are used for the navigation tree definition (and the breadcrumbs) but the actual UI content rendered inside the panel is provided through JSX.
+
+```tsx
+// 1. Define the PanelContentProvider
+// -----------------------------------
+const panelContentProvider: PanelContentProvider = (id: string) => {
+ // The full ID of the node icon button that was clicked is provided (e.g. "root.group1.itemA")
+ // You can use this ID to determine which panel content to render
+
+ if (id === 'foo1') {
+ // Return the JSX to render in the panel for this node.
+ return {
+ content: ({
+ /** Handler to close the panel */
+ closePanel,
+ /** ChromeNavigationNode - The node that has been clicked in the main nav */
+ selectedNode,
+ /** ChromeProjectNavigationNode[][] - Active nodes that match the current URL location */
+ activeNodes,
+ }) => {
+ return (
+
+ This is a custom component to render in the panel.
+ closePanel()}>Close panel
+
+ );
+ },
+ };
+ }
+
+ if (id === 'foo2') {
+ // If need be you can only customize the "Title" of the panel and leave the content
+ // to be auto-generated.
+ return {
+ title: