From 2cb977fa5012c5d451e3adffb518d2d4de837e62 Mon Sep 17 00:00:00 2001
From: Nick Peihl
Date: Thu, 4 May 2023 17:06:45 -0400
Subject: [PATCH 01/53] [Dashboard Navigation] Create and register navigation
embeddable plugin (#156627)
---
.github/CODEOWNERS | 1 +
.i18nrc.json | 1 +
docs/developer/plugin-list.asciidoc | 4 ++
package.json | 1 +
packages/kbn-optimizer/limits.yml | 1 +
src/plugins/navigation_embeddable/README.md | 3 ++
.../navigation_embeddable/kibana.jsonc | 17 ++++++++
.../navigation_embeddable/public/index.ts | 20 ++++++++++
.../public/navigation_embeddable/index.ts | 11 ++++++
.../navigation_embeddable.tsx | 32 +++++++++++++++
.../navigation_embeddable_factory.ts | 36 +++++++++++++++++
.../navigation_embeddable/public/plugin.ts | 39 +++++++++++++++++++
.../navigation_embeddable/tsconfig.json | 15 +++++++
tsconfig.base.json | 2 +
yarn.lock | 4 ++
15 files changed, 187 insertions(+)
create mode 100644 src/plugins/navigation_embeddable/README.md
create mode 100644 src/plugins/navigation_embeddable/kibana.jsonc
create mode 100644 src/plugins/navigation_embeddable/public/index.ts
create mode 100644 src/plugins/navigation_embeddable/public/navigation_embeddable/index.ts
create mode 100644 src/plugins/navigation_embeddable/public/navigation_embeddable/navigation_embeddable.tsx
create mode 100644 src/plugins/navigation_embeddable/public/navigation_embeddable/navigation_embeddable_factory.ts
create mode 100644 src/plugins/navigation_embeddable/public/plugin.ts
create mode 100644 src/plugins/navigation_embeddable/tsconfig.json
diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS
index 4e23e6d1f00cc..2b4250d27d6a8 100644
--- a/.github/CODEOWNERS
+++ b/.github/CODEOWNERS
@@ -499,6 +499,7 @@ x-pack/packages/ml/url_state @elastic/ml-ui
packages/kbn-monaco @elastic/appex-sharedux
x-pack/plugins/monitoring_collection @elastic/infra-monitoring-ui
x-pack/plugins/monitoring @elastic/infra-monitoring-ui
+src/plugins/navigation_embeddable @elastic/kibana-presentation
src/plugins/navigation @elastic/appex-sharedux
src/plugins/newsfeed @elastic/kibana-core
test/common/plugins/newsfeed @elastic/kibana-core
diff --git a/.i18nrc.json b/.i18nrc.json
index 3d457a16a3fbc..7c200c947b28f 100644
--- a/.i18nrc.json
+++ b/.i18nrc.json
@@ -74,6 +74,7 @@
"management": ["src/legacy/core_plugins/management", "src/plugins/management"],
"monaco": "packages/kbn-monaco/src",
"navigation": "src/plugins/navigation",
+ "navigationEmbeddable": "src/plugins/navigation_embeddable",
"newsfeed": "src/plugins/newsfeed",
"presentationUtil": "src/plugins/presentation_util",
"randomSampling": "x-pack/packages/kbn-random-sampling",
diff --git a/docs/developer/plugin-list.asciidoc b/docs/developer/plugin-list.asciidoc
index 31d7fe690cfe3..3294092957744 100644
--- a/docs/developer/plugin-list.asciidoc
+++ b/docs/developer/plugin-list.asciidoc
@@ -249,6 +249,10 @@ management section itself.
It also provides a stateful version of it on the start contract.
+|{kib-repo}blob/{branch}/src/plugins/navigation_embeddable/README.md[navigationEmbeddable]
+|This plugin adds the Navigation Embeddable which allows authors to create hard links to navigate on click and bring all context from the source dashboard to the destination dashboard.
+
+
|{kib-repo}blob/{branch}/src/plugins/newsfeed/README.md[newsfeed]
|The newsfeed plugin adds a NewsfeedNavButton to the top navigation bar and renders the content in the flyout.
Content is fetched from the remote (https://feeds.elastic.co) once a day, with periodic checks if the content needs to be refreshed. All newsfeed content is hosted remotely.
diff --git a/package.json b/package.json
index a21bd7f6aa01c..a6e05507e119e 100644
--- a/package.json
+++ b/package.json
@@ -516,6 +516,7 @@
"@kbn/monaco": "link:packages/kbn-monaco",
"@kbn/monitoring-collection-plugin": "link:x-pack/plugins/monitoring_collection",
"@kbn/monitoring-plugin": "link:x-pack/plugins/monitoring",
+ "@kbn/navigation-embeddable-plugin": "link:src/plugins/navigation_embeddable",
"@kbn/navigation-plugin": "link:src/plugins/navigation",
"@kbn/newsfeed-plugin": "link:src/plugins/newsfeed",
"@kbn/newsfeed-test-plugin": "link:test/common/plugins/newsfeed",
diff --git a/packages/kbn-optimizer/limits.yml b/packages/kbn-optimizer/limits.yml
index fe708fe8c95c5..5b53136ab79f0 100644
--- a/packages/kbn-optimizer/limits.yml
+++ b/packages/kbn-optimizer/limits.yml
@@ -94,6 +94,7 @@ pageLoadAssetSize:
ml: 82187
monitoring: 80000
navigation: 37269
+ navigationEmbeddable: 17892
newsfeed: 42228
observability: 115443
observabilityOnboarding: 19573
diff --git a/src/plugins/navigation_embeddable/README.md b/src/plugins/navigation_embeddable/README.md
new file mode 100644
index 0000000000000..598a29be2e037
--- /dev/null
+++ b/src/plugins/navigation_embeddable/README.md
@@ -0,0 +1,3 @@
+# Navigation Embeddable
+
+This plugin adds the Navigation Embeddable which allows authors to create hard links to navigate on click and bring all context from the source dashboard to the destination dashboard.
diff --git a/src/plugins/navigation_embeddable/kibana.jsonc b/src/plugins/navigation_embeddable/kibana.jsonc
new file mode 100644
index 0000000000000..76d185c560084
--- /dev/null
+++ b/src/plugins/navigation_embeddable/kibana.jsonc
@@ -0,0 +1,17 @@
+{
+ "type": "plugin",
+ "id": "@kbn/navigation-embeddable-plugin",
+ "owner": "@elastic/kibana-presentation",
+ "description": "An embeddable for quickly navigating between dashboards.",
+ "plugin": {
+ "id": "navigationEmbeddable",
+ "server": false,
+ "browser": true,
+ "requiredPlugins": [
+ "embeddable"
+ ],
+ "optionalPlugins": [],
+ "requiredBundles": [
+ ]
+ }
+}
diff --git a/src/plugins/navigation_embeddable/public/index.ts b/src/plugins/navigation_embeddable/public/index.ts
new file mode 100644
index 0000000000000..5e8be17c8958b
--- /dev/null
+++ b/src/plugins/navigation_embeddable/public/index.ts
@@ -0,0 +1,20 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+export type { NavigationEmbeddableFactory } from './navigation_embeddable';
+export {
+ NAVIGATION_EMBEDDABLE_TYPE,
+ NavigationEmbeddableFactoryDefinition,
+ NavigationEmbeddable,
+} from './navigation_embeddable';
+
+import { NavigationEmbeddablePlugin } from './plugin';
+
+export function plugin() {
+ return new NavigationEmbeddablePlugin();
+}
diff --git a/src/plugins/navigation_embeddable/public/navigation_embeddable/index.ts b/src/plugins/navigation_embeddable/public/navigation_embeddable/index.ts
new file mode 100644
index 0000000000000..1676f521ceb84
--- /dev/null
+++ b/src/plugins/navigation_embeddable/public/navigation_embeddable/index.ts
@@ -0,0 +1,11 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 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 { NAVIGATION_EMBEDDABLE_TYPE, NavigationEmbeddable } from './navigation_embeddable';
+export type { NavigationEmbeddableFactory } from './navigation_embeddable_factory';
+export { NavigationEmbeddableFactoryDefinition } from './navigation_embeddable_factory';
diff --git a/src/plugins/navigation_embeddable/public/navigation_embeddable/navigation_embeddable.tsx b/src/plugins/navigation_embeddable/public/navigation_embeddable/navigation_embeddable.tsx
new file mode 100644
index 0000000000000..2c66f4b655fac
--- /dev/null
+++ b/src/plugins/navigation_embeddable/public/navigation_embeddable/navigation_embeddable.tsx
@@ -0,0 +1,32 @@
+/*
+ * 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 { EuiTitle } from '@elastic/eui';
+import { Embeddable } from '@kbn/embeddable-plugin/public';
+import type { EmbeddableInput, IContainer } from '@kbn/embeddable-plugin/public';
+
+export const NAVIGATION_EMBEDDABLE_TYPE = 'navigation';
+
+export class NavigationEmbeddable extends Embeddable {
+ public readonly type = NAVIGATION_EMBEDDABLE_TYPE;
+
+ constructor(initialInput: EmbeddableInput, parent?: IContainer) {
+ super(initialInput, {}, parent);
+ }
+
+ public render(el: HTMLElement) {
+ return (
+
+ Call me Magellan, cuz I'm a navigator!
+
+ );
+ }
+
+ public reload() {}
+}
diff --git a/src/plugins/navigation_embeddable/public/navigation_embeddable/navigation_embeddable_factory.ts b/src/plugins/navigation_embeddable/public/navigation_embeddable/navigation_embeddable_factory.ts
new file mode 100644
index 0000000000000..54f8ecbb9f892
--- /dev/null
+++ b/src/plugins/navigation_embeddable/public/navigation_embeddable/navigation_embeddable_factory.ts
@@ -0,0 +1,36 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import { i18n } from '@kbn/i18n';
+import type { EmbeddableInput, IContainer } from '@kbn/embeddable-plugin/public';
+import { EmbeddableFactory, EmbeddableFactoryDefinition } from '@kbn/embeddable-plugin/public';
+import { NavigationEmbeddable, NAVIGATION_EMBEDDABLE_TYPE } from './navigation_embeddable';
+
+export type NavigationEmbeddableFactory = EmbeddableFactory;
+
+export class NavigationEmbeddableFactoryDefinition implements EmbeddableFactoryDefinition {
+ public readonly type = NAVIGATION_EMBEDDABLE_TYPE;
+
+ public async isEditable() {
+ return true;
+ }
+
+ public async create(initialInput: EmbeddableInput, parent?: IContainer) {
+ return new NavigationEmbeddable(initialInput, parent);
+ }
+
+ public getDisplayName() {
+ return i18n.translate('navigationEmbeddable.navigationEmbeddableFactory.displayName', {
+ defaultMessage: 'Navigation',
+ });
+ }
+
+ public getIconType() {
+ return 'link';
+ }
+}
diff --git a/src/plugins/navigation_embeddable/public/plugin.ts b/src/plugins/navigation_embeddable/public/plugin.ts
new file mode 100644
index 0000000000000..23dff1f5d1eb8
--- /dev/null
+++ b/src/plugins/navigation_embeddable/public/plugin.ts
@@ -0,0 +1,39 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 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 { CoreSetup, CoreStart, Plugin } from '@kbn/core/public';
+import { EmbeddableSetup, EmbeddableStart } from '@kbn/embeddable-plugin/public';
+import { NAVIGATION_EMBEDDABLE_TYPE } from './navigation_embeddable';
+import { NavigationEmbeddableFactoryDefinition } from './navigation_embeddable';
+
+export interface SetupDependencies {
+ embeddable: EmbeddableSetup;
+}
+
+export interface StartDependencies {
+ embeddable: EmbeddableStart;
+}
+
+export class NavigationEmbeddablePlugin
+ implements Plugin
+{
+ constructor() {}
+
+ public setup(core: CoreSetup, plugins: SetupDependencies) {
+ plugins.embeddable.registerEmbeddableFactory(
+ NAVIGATION_EMBEDDABLE_TYPE,
+ new NavigationEmbeddableFactoryDefinition()
+ );
+ }
+
+ public start(core: CoreStart, plugins: StartDependencies) {
+ return {};
+ }
+
+ public stop() {}
+}
diff --git a/src/plugins/navigation_embeddable/tsconfig.json b/src/plugins/navigation_embeddable/tsconfig.json
new file mode 100644
index 0000000000000..05ce2326f9768
--- /dev/null
+++ b/src/plugins/navigation_embeddable/tsconfig.json
@@ -0,0 +1,15 @@
+{
+ "extends": "../../../tsconfig.base.json",
+ "compilerOptions": {
+ "outDir": "target/types",
+ },
+ "include": ["public/**/*", "common/**/*", "server/**/*"],
+ "kbn_references": [
+ "@kbn/core",
+ "@kbn/embeddable-plugin",
+ "@kbn/i18n",
+ ],
+ "exclude": [
+ "target/**/*",
+ ]
+}
diff --git a/tsconfig.base.json b/tsconfig.base.json
index eb7ad0b4b8f55..20bbcb50377d5 100644
--- a/tsconfig.base.json
+++ b/tsconfig.base.json
@@ -992,6 +992,8 @@
"@kbn/monitoring-collection-plugin/*": ["x-pack/plugins/monitoring_collection/*"],
"@kbn/monitoring-plugin": ["x-pack/plugins/monitoring"],
"@kbn/monitoring-plugin/*": ["x-pack/plugins/monitoring/*"],
+ "@kbn/navigation-embeddable-plugin": ["src/plugins/navigation_embeddable"],
+ "@kbn/navigation-embeddable-plugin/*": ["src/plugins/navigation_embeddable/*"],
"@kbn/navigation-plugin": ["src/plugins/navigation"],
"@kbn/navigation-plugin/*": ["src/plugins/navigation/*"],
"@kbn/newsfeed-plugin": ["src/plugins/newsfeed"],
diff --git a/yarn.lock b/yarn.lock
index 3bd506adc6293..c266b33f3d400 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -4824,6 +4824,10 @@
version "0.0.0"
uid ""
+"@kbn/navigation-embeddable-plugin@link:src/plugins/navigation_embeddable":
+ version "0.0.0"
+ uid ""
+
"@kbn/navigation-plugin@link:src/plugins/navigation":
version "0.0.0"
uid ""
From dcc37c65a87d5ff255e8cd3850deda296f73ba9f Mon Sep 17 00:00:00 2001
From: Nick Peihl
Date: Thu, 29 Jun 2023 10:05:10 -0400
Subject: [PATCH 02/53] Initialize content management
---
.../navigation_embeddable/common/constants.ts | 17 +++
.../common/content_management/cm_services.ts | 21 ++++
.../common/content_management/index.ts | 19 ++++
.../common/content_management/latest.ts | 9 ++
.../content_management/v1/cm_services.ts | 100 ++++++++++++++++++
.../common/content_management/v1/index.ts | 11 ++
.../common/content_management/v1/types.ts | 32 ++++++
.../navigation_embeddable/common/index.ts | 9 ++
.../navigation_embeddable/common/types.ts | 9 ++
.../navigation_embeddable/kibana.jsonc | 8 +-
.../public/content_management/index.ts | 9 ++
...embeddable_content_management_client.ts.ts | 80 ++++++++++++++
.../index.ts | 0
.../navigation_embeddable.tsx | 0
.../navigation_embeddable_factory.tsx} | 4 +-
.../navigation_embeddable/public/index.ts | 4 +-
.../public/navigation_embeddable_services.ts | 44 ++++++++
.../navigation_embeddable/public/plugin.ts | 29 ++++-
.../server/content_management/index.ts | 9 ++
.../navigation_embeddable_storage.ts | 23 ++++
.../navigation_embeddable/server/index.ts | 11 ++
.../navigation_embeddable/server/plugin.ts | 52 +++++++++
22 files changed, 492 insertions(+), 8 deletions(-)
create mode 100644 src/plugins/navigation_embeddable/common/constants.ts
create mode 100644 src/plugins/navigation_embeddable/common/content_management/cm_services.ts
create mode 100644 src/plugins/navigation_embeddable/common/content_management/index.ts
create mode 100644 src/plugins/navigation_embeddable/common/content_management/latest.ts
create mode 100644 src/plugins/navigation_embeddable/common/content_management/v1/cm_services.ts
create mode 100644 src/plugins/navigation_embeddable/common/content_management/v1/index.ts
create mode 100644 src/plugins/navigation_embeddable/common/content_management/v1/types.ts
create mode 100644 src/plugins/navigation_embeddable/common/index.ts
create mode 100644 src/plugins/navigation_embeddable/common/types.ts
create mode 100644 src/plugins/navigation_embeddable/public/content_management/index.ts
create mode 100644 src/plugins/navigation_embeddable/public/content_management/navigation_embeddable_content_management_client.ts.ts
rename src/plugins/navigation_embeddable/public/{navigation_embeddable => embeddable}/index.ts (100%)
rename src/plugins/navigation_embeddable/public/{navigation_embeddable => embeddable}/navigation_embeddable.tsx (100%)
rename src/plugins/navigation_embeddable/public/{navigation_embeddable/navigation_embeddable_factory.ts => embeddable/navigation_embeddable_factory.tsx} (92%)
create mode 100644 src/plugins/navigation_embeddable/public/navigation_embeddable_services.ts
create mode 100644 src/plugins/navigation_embeddable/server/content_management/index.ts
create mode 100644 src/plugins/navigation_embeddable/server/content_management/navigation_embeddable_storage.ts
create mode 100644 src/plugins/navigation_embeddable/server/index.ts
create mode 100644 src/plugins/navigation_embeddable/server/plugin.ts
diff --git a/src/plugins/navigation_embeddable/common/constants.ts b/src/plugins/navigation_embeddable/common/constants.ts
new file mode 100644
index 0000000000000..e71ee4836638b
--- /dev/null
+++ b/src/plugins/navigation_embeddable/common/constants.ts
@@ -0,0 +1,17 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import { i18n } from '@kbn/i18n';
+
+export const LATEST_VERSION = 1;
+
+export const CONTENT_ID = 'navigationEmbeddable';
+
+export const APP_NAME = i18n.translate('xpack.maps.visTypeAlias.title', {
+ defaultMessage: 'Links',
+});
diff --git a/src/plugins/navigation_embeddable/common/content_management/cm_services.ts b/src/plugins/navigation_embeddable/common/content_management/cm_services.ts
new file mode 100644
index 0000000000000..af32d57e0437f
--- /dev/null
+++ b/src/plugins/navigation_embeddable/common/content_management/cm_services.ts
@@ -0,0 +1,21 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import type {
+ ContentManagementServicesDefinition as ServicesDefinition,
+ Version,
+} from '@kbn/object-versioning';
+
+// We export the versionned service definition from this file and not the barrel to avoid adding
+// the schemas in the "public" js bundle
+
+import { serviceDefinition as v1 } from './v1/cm_services';
+
+export const cmServicesDefinition: { [version: Version]: ServicesDefinition } = {
+ 1: v1,
+};
diff --git a/src/plugins/navigation_embeddable/common/content_management/index.ts b/src/plugins/navigation_embeddable/common/content_management/index.ts
new file mode 100644
index 0000000000000..3c5ca47d812c4
--- /dev/null
+++ b/src/plugins/navigation_embeddable/common/content_management/index.ts
@@ -0,0 +1,19 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+export { LATEST_VERSION, CONTENT_ID } from '../constants';
+
+export type { NavigationEmbeddableContentType } from '../types';
+
+export type {
+ NavigationEmbeddableCrudTypes,
+ NavigationEmbeddableAttributes,
+ NavigationEmbeddableItem,
+} from './latest';
+
+export * as NavigationEmbeddableV1 from './v1';
diff --git a/src/plugins/navigation_embeddable/common/content_management/latest.ts b/src/plugins/navigation_embeddable/common/content_management/latest.ts
new file mode 100644
index 0000000000000..e9c79f0f50f93
--- /dev/null
+++ b/src/plugins/navigation_embeddable/common/content_management/latest.ts
@@ -0,0 +1,9 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+export * from './v1';
diff --git a/src/plugins/navigation_embeddable/common/content_management/v1/cm_services.ts b/src/plugins/navigation_embeddable/common/content_management/v1/cm_services.ts
new file mode 100644
index 0000000000000..66d1df1454892
--- /dev/null
+++ b/src/plugins/navigation_embeddable/common/content_management/v1/cm_services.ts
@@ -0,0 +1,100 @@
+/*
+ * 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 { schema } from '@kbn/config-schema';
+import type { ContentManagementServicesDefinition as ServicesDefinition } from '@kbn/object-versioning';
+import {
+ savedObjectSchema,
+ objectTypeToGetResultSchema,
+ createOptionsSchemas,
+ updateOptionsSchema,
+ createResultSchema,
+} from '@kbn/content-management-utils';
+
+const navigationEmbeddableAttributesSchema = schema.object(
+ {
+ title: schema.string(),
+ description: schema.maybe(schema.string()),
+ linkListJSON: schema.maybe(schema.string()),
+ },
+ { unknowns: 'forbid' }
+);
+
+const navigationEmbeddableSavedObjectSchema = savedObjectSchema(
+ navigationEmbeddableAttributesSchema
+);
+
+const searchOptionsSchema = schema.maybe(
+ schema.object(
+ {
+ onlyTitle: schema.maybe(schema.boolean()),
+ },
+ { unknowns: 'forbid' }
+ )
+);
+
+const navigationEmbeddableCreateOptionsSchema = schema.object({
+ id: createOptionsSchemas.id,
+ references: schema.maybe(createOptionsSchemas.references),
+ overwrite: createOptionsSchemas.overwrite,
+});
+
+const navigationEmbeddableUpdateOptionsSchema = schema.object({
+ references: updateOptionsSchema.references,
+});
+
+// Content management service definition.
+// We need it for BWC support between different versions of the content
+export const serviceDefinition: ServicesDefinition = {
+ get: {
+ out: {
+ result: {
+ schema: objectTypeToGetResultSchema(navigationEmbeddableSavedObjectSchema),
+ },
+ },
+ },
+ create: {
+ in: {
+ options: {
+ schema: navigationEmbeddableCreateOptionsSchema,
+ },
+ data: {
+ schema: navigationEmbeddableSavedObjectSchema,
+ },
+ },
+ out: {
+ result: {
+ schema: createResultSchema(navigationEmbeddableSavedObjectSchema),
+ },
+ },
+ },
+ update: {
+ in: {
+ options: {
+ schema: navigationEmbeddableUpdateOptionsSchema, // same schema as "create"
+ },
+ data: {
+ schema: navigationEmbeddableSavedObjectSchema,
+ },
+ },
+ },
+ search: {
+ in: {
+ options: {
+ schema: searchOptionsSchema,
+ },
+ },
+ },
+ mSearch: {
+ out: {
+ result: {
+ schema: navigationEmbeddableSavedObjectSchema,
+ },
+ },
+ },
+};
diff --git a/src/plugins/navigation_embeddable/common/content_management/v1/index.ts b/src/plugins/navigation_embeddable/common/content_management/v1/index.ts
new file mode 100644
index 0000000000000..8ef0422178d54
--- /dev/null
+++ b/src/plugins/navigation_embeddable/common/content_management/v1/index.ts
@@ -0,0 +1,11 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 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 { NavigationEmbeddableCrudTypes } from './types';
+export type { NavigationEmbeddableCrudTypes, NavigationEmbeddableAttributes } from './types';
+export type NavigationEmbeddableItem = NavigationEmbeddableCrudTypes['Item'];
diff --git a/src/plugins/navigation_embeddable/common/content_management/v1/types.ts b/src/plugins/navigation_embeddable/common/content_management/v1/types.ts
new file mode 100644
index 0000000000000..00a989292f933
--- /dev/null
+++ b/src/plugins/navigation_embeddable/common/content_management/v1/types.ts
@@ -0,0 +1,32 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import type {
+ ContentManagementCrudTypes,
+ SavedObjectCreateOptions,
+ SavedObjectUpdateOptions,
+} from '@kbn/content-management-utils';
+import { NavigationEmbeddableContentType } from '../../types';
+
+export type NavigationEmbeddableCrudTypes = ContentManagementCrudTypes<
+ NavigationEmbeddableContentType,
+ NavigationEmbeddableAttributes,
+ Pick,
+ Pick,
+ {
+ /** Flag to indicate to only search the text on the "title" field */
+ onlyTitle?: boolean;
+ }
+>;
+
+// eslint-disable-next-line @typescript-eslint/consistent-type-definitions
+export type NavigationEmbeddableAttributes = {
+ title: string;
+ description?: string;
+ linkListJSON?: string;
+};
diff --git a/src/plugins/navigation_embeddable/common/index.ts b/src/plugins/navigation_embeddable/common/index.ts
new file mode 100644
index 0000000000000..1c5c974297430
--- /dev/null
+++ b/src/plugins/navigation_embeddable/common/index.ts
@@ -0,0 +1,9 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+export { CONTENT_ID, LATEST_VERSION } from './constants';
diff --git a/src/plugins/navigation_embeddable/common/types.ts b/src/plugins/navigation_embeddable/common/types.ts
new file mode 100644
index 0000000000000..2038a5c0f04c1
--- /dev/null
+++ b/src/plugins/navigation_embeddable/common/types.ts
@@ -0,0 +1,9 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+export type NavigationEmbeddableContentType = 'navigationEmbeddable';
diff --git a/src/plugins/navigation_embeddable/kibana.jsonc b/src/plugins/navigation_embeddable/kibana.jsonc
index 76d185c560084..d0b8f4d43c9eb 100644
--- a/src/plugins/navigation_embeddable/kibana.jsonc
+++ b/src/plugins/navigation_embeddable/kibana.jsonc
@@ -5,10 +5,14 @@
"description": "An embeddable for quickly navigating between dashboards.",
"plugin": {
"id": "navigationEmbeddable",
- "server": false,
+ "server": true,
"browser": true,
"requiredPlugins": [
- "embeddable"
+ "contentManagement",
+ "dashboard",
+ "embeddable",
+ "kibanaReact",
+ "presentationUtil"
],
"optionalPlugins": [],
"requiredBundles": [
diff --git a/src/plugins/navigation_embeddable/public/content_management/index.ts b/src/plugins/navigation_embeddable/public/content_management/index.ts
new file mode 100644
index 0000000000000..abdf2fd0159eb
--- /dev/null
+++ b/src/plugins/navigation_embeddable/public/content_management/index.ts
@@ -0,0 +1,9 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+export { navigationEmbeddableClient } from './navigation_embeddable_content_management_client.ts';
diff --git a/src/plugins/navigation_embeddable/public/content_management/navigation_embeddable_content_management_client.ts.ts b/src/plugins/navigation_embeddable/public/content_management/navigation_embeddable_content_management_client.ts.ts
new file mode 100644
index 0000000000000..800c7ff5eb59f
--- /dev/null
+++ b/src/plugins/navigation_embeddable/public/content_management/navigation_embeddable_content_management_client.ts.ts
@@ -0,0 +1,80 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import type { SearchQuery } from '@kbn/content-management-plugin/common';
+import type { NavigationEmbeddableCrudTypes } from '../../common/content_management';
+import { CONTENT_ID as contentTypeId } from '../../common';
+import { contentManagement } from '../navigation_embeddable_services';
+
+const get = async (id: string) => {
+ return contentManagement.client.get({ contentTypeId, id });
+};
+
+const create = async ({
+ data,
+ options,
+}: Omit) => {
+ const res = await contentManagement.client.create<
+ NavigationEmbeddableCrudTypes['CreateIn'],
+ NavigationEmbeddableCrudTypes['CreateOut']
+ >({
+ contentTypeId,
+ data,
+ options,
+ });
+ return res;
+};
+
+const update = async ({
+ id,
+ data,
+ options,
+}: Omit) => {
+ const res = await contentManagement.client.update<
+ NavigationEmbeddableCrudTypes['UpdateIn'],
+ NavigationEmbeddableCrudTypes['UpdateOut']
+ >({
+ contentTypeId,
+ id,
+ data,
+ options,
+ });
+ return res;
+};
+
+const deleteNavigationEmbeddable = async (id: string) => {
+ await contentManagement.client.delete<
+ NavigationEmbeddableCrudTypes['DeleteIn'],
+ NavigationEmbeddableCrudTypes['DeleteOut']
+ >({
+ contentTypeId,
+ id,
+ });
+};
+
+const search = async (
+ query: SearchQuery = {},
+ options?: NavigationEmbeddableCrudTypes['SearchOptions']
+) => {
+ return contentManagement.client.search<
+ NavigationEmbeddableCrudTypes['SearchIn'],
+ NavigationEmbeddableCrudTypes['SearchOut']
+ >({
+ contentTypeId,
+ query,
+ options,
+ });
+};
+
+export const navigationEmbeddableClient = {
+ get,
+ create,
+ update,
+ delete: deleteNavigationEmbeddable,
+ search,
+};
diff --git a/src/plugins/navigation_embeddable/public/navigation_embeddable/index.ts b/src/plugins/navigation_embeddable/public/embeddable/index.ts
similarity index 100%
rename from src/plugins/navigation_embeddable/public/navigation_embeddable/index.ts
rename to src/plugins/navigation_embeddable/public/embeddable/index.ts
diff --git a/src/plugins/navigation_embeddable/public/navigation_embeddable/navigation_embeddable.tsx b/src/plugins/navigation_embeddable/public/embeddable/navigation_embeddable.tsx
similarity index 100%
rename from src/plugins/navigation_embeddable/public/navigation_embeddable/navigation_embeddable.tsx
rename to src/plugins/navigation_embeddable/public/embeddable/navigation_embeddable.tsx
diff --git a/src/plugins/navigation_embeddable/public/navigation_embeddable/navigation_embeddable_factory.ts b/src/plugins/navigation_embeddable/public/embeddable/navigation_embeddable_factory.tsx
similarity index 92%
rename from src/plugins/navigation_embeddable/public/navigation_embeddable/navigation_embeddable_factory.ts
rename to src/plugins/navigation_embeddable/public/embeddable/navigation_embeddable_factory.tsx
index 54f8ecbb9f892..3bdb38511838e 100644
--- a/src/plugins/navigation_embeddable/public/navigation_embeddable/navigation_embeddable_factory.ts
+++ b/src/plugins/navigation_embeddable/public/embeddable/navigation_embeddable_factory.tsx
@@ -9,7 +9,7 @@
import { i18n } from '@kbn/i18n';
import type { EmbeddableInput, IContainer } from '@kbn/embeddable-plugin/public';
import { EmbeddableFactory, EmbeddableFactoryDefinition } from '@kbn/embeddable-plugin/public';
-import { NavigationEmbeddable, NAVIGATION_EMBEDDABLE_TYPE } from './navigation_embeddable';
+import { NAVIGATION_EMBEDDABLE_TYPE, NavigationEmbeddable } from './navigation_embeddable';
export type NavigationEmbeddableFactory = EmbeddableFactory;
@@ -26,7 +26,7 @@ export class NavigationEmbeddableFactoryDefinition implements EmbeddableFactoryD
public getDisplayName() {
return i18n.translate('navigationEmbeddable.navigationEmbeddableFactory.displayName', {
- defaultMessage: 'Navigation',
+ defaultMessage: 'Links',
});
}
diff --git a/src/plugins/navigation_embeddable/public/index.ts b/src/plugins/navigation_embeddable/public/index.ts
index 5e8be17c8958b..9cdbcfcc6c667 100644
--- a/src/plugins/navigation_embeddable/public/index.ts
+++ b/src/plugins/navigation_embeddable/public/index.ts
@@ -6,12 +6,12 @@
* Side Public License, v 1.
*/
-export type { NavigationEmbeddableFactory } from './navigation_embeddable';
+export type { NavigationEmbeddableFactory } from './embeddable';
export {
NAVIGATION_EMBEDDABLE_TYPE,
NavigationEmbeddableFactoryDefinition,
NavigationEmbeddable,
-} from './navigation_embeddable';
+} from './embeddable';
import { NavigationEmbeddablePlugin } from './plugin';
diff --git a/src/plugins/navigation_embeddable/public/navigation_embeddable_services.ts b/src/plugins/navigation_embeddable/public/navigation_embeddable_services.ts
new file mode 100644
index 0000000000000..7a2311e412006
--- /dev/null
+++ b/src/plugins/navigation_embeddable/public/navigation_embeddable_services.ts
@@ -0,0 +1,44 @@
+/*
+ * 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 { BehaviorSubject } from 'rxjs';
+
+import { CoreStart } from '@kbn/core/public';
+import { DashboardStart } from '@kbn/dashboard-plugin/public';
+
+import { ContentManagementPublicStart } from '@kbn/content-management-plugin/public';
+import { StartDependencies as NavigationEmbeddableStartDependencies } from './plugin';
+
+export let coreServices: CoreStart;
+export let dashboardServices: DashboardStart;
+export let contentManagement: ContentManagementPublicStart;
+
+const servicesReady$ = new BehaviorSubject(false);
+
+export const untilPluginStartServicesReady = () => {
+ if (servicesReady$.value) return Promise.resolve();
+ return new Promise((resolve) => {
+ const subscription = servicesReady$.subscribe((isInitialized) => {
+ if (isInitialized) {
+ subscription.unsubscribe();
+ resolve();
+ }
+ });
+ });
+};
+
+export const setKibanaServices = (
+ kibanaCore: CoreStart,
+ deps: NavigationEmbeddableStartDependencies
+) => {
+ coreServices = kibanaCore;
+ dashboardServices = deps.dashboard;
+ contentManagement = deps.contentManagement;
+
+ servicesReady$.next(true);
+};
diff --git a/src/plugins/navigation_embeddable/public/plugin.ts b/src/plugins/navigation_embeddable/public/plugin.ts
index 23dff1f5d1eb8..940c4b037e90a 100644
--- a/src/plugins/navigation_embeddable/public/plugin.ts
+++ b/src/plugins/navigation_embeddable/public/plugin.ts
@@ -7,16 +7,32 @@
*/
import { CoreSetup, CoreStart, Plugin } from '@kbn/core/public';
+import {
+ ContentManagementPublicSetup,
+ ContentManagementPublicStart,
+} from '@kbn/content-management-plugin/public';
+import { DashboardStart } from '@kbn/dashboard-plugin/public';
import { EmbeddableSetup, EmbeddableStart } from '@kbn/embeddable-plugin/public';
-import { NAVIGATION_EMBEDDABLE_TYPE } from './navigation_embeddable';
-import { NavigationEmbeddableFactoryDefinition } from './navigation_embeddable';
+import { NAVIGATION_EMBEDDABLE_TYPE } from './embeddable';
+import { NavigationEmbeddableFactoryDefinition } from './embeddable';
+import { CONTENT_ID, LATEST_VERSION } from '../common/content_management';
+import { APP_NAME } from '../common/constants';
+import { setKibanaServices } from './navigation_embeddable_services';
+// eslint-disable-next-line @typescript-eslint/no-empty-interface
+export interface PluginSetup {}
+
+// eslint-disable-next-line @typescript-eslint/no-empty-interface
+export interface PluginStart {}
export interface SetupDependencies {
embeddable: EmbeddableSetup;
+ contentManagement: ContentManagementPublicSetup;
}
export interface StartDependencies {
embeddable: EmbeddableStart;
+ contentManagement: ContentManagementPublicStart;
+ dashboard: DashboardStart;
}
export class NavigationEmbeddablePlugin
@@ -29,9 +45,18 @@ export class NavigationEmbeddablePlugin
NAVIGATION_EMBEDDABLE_TYPE,
new NavigationEmbeddableFactoryDefinition()
);
+
+ plugins.contentManagement.registry.register({
+ id: CONTENT_ID,
+ version: {
+ latest: LATEST_VERSION,
+ },
+ name: APP_NAME,
+ });
}
public start(core: CoreStart, plugins: StartDependencies) {
+ setKibanaServices(core, plugins);
return {};
}
diff --git a/src/plugins/navigation_embeddable/server/content_management/index.ts b/src/plugins/navigation_embeddable/server/content_management/index.ts
new file mode 100644
index 0000000000000..2376765bcac83
--- /dev/null
+++ b/src/plugins/navigation_embeddable/server/content_management/index.ts
@@ -0,0 +1,9 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+export { NavigationEmbeddableStorage } from './navigation_embeddable_storage';
diff --git a/src/plugins/navigation_embeddable/server/content_management/navigation_embeddable_storage.ts b/src/plugins/navigation_embeddable/server/content_management/navigation_embeddable_storage.ts
new file mode 100644
index 0000000000000..c6e1974dc24c3
--- /dev/null
+++ b/src/plugins/navigation_embeddable/server/content_management/navigation_embeddable_storage.ts
@@ -0,0 +1,23 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import { SOContentStorage } from '@kbn/content-management-utils';
+import type { NavigationEmbeddableCrudTypes } from '../../common/content_management';
+import { CONTENT_ID } from '../../common';
+import { cmServicesDefinition } from '../../common/content_management/cm_services';
+
+export class NavigationEmbeddableStorage extends SOContentStorage {
+ constructor() {
+ super({
+ savedObjectType: CONTENT_ID,
+ cmServicesDefinition,
+ enableMSearch: true,
+ allowedSavedObjectAttributes: ['title', 'description', 'linkListJSON'],
+ });
+ }
+}
diff --git a/src/plugins/navigation_embeddable/server/index.ts b/src/plugins/navigation_embeddable/server/index.ts
new file mode 100644
index 0000000000000..6ececdd95b5d0
--- /dev/null
+++ b/src/plugins/navigation_embeddable/server/index.ts
@@ -0,0 +1,11 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 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 { NavigationEmbeddableServerPlugin } from './plugin';
+
+export const plugin = () => new NavigationEmbeddableServerPlugin();
diff --git a/src/plugins/navigation_embeddable/server/plugin.ts b/src/plugins/navigation_embeddable/server/plugin.ts
new file mode 100644
index 0000000000000..9e2d973310eba
--- /dev/null
+++ b/src/plugins/navigation_embeddable/server/plugin.ts
@@ -0,0 +1,52 @@
+/*
+ * 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 { CoreSetup, CoreStart, Plugin } from '@kbn/core/server';
+import type { ContentManagementServerSetup } from '@kbn/content-management-plugin/server';
+import { CONTENT_ID, LATEST_VERSION } from '../common';
+import { NavigationEmbeddableStorage } from './content_management';
+
+export class NavigationEmbeddableServerPlugin implements Plugin {
+ public setup(
+ core: CoreSetup,
+ plugins: {
+ contentManagement: ContentManagementServerSetup;
+ }
+ ) {
+ plugins.contentManagement.register({
+ id: CONTENT_ID,
+ storage: new NavigationEmbeddableStorage(),
+ version: {
+ latest: LATEST_VERSION,
+ },
+ });
+
+ core.savedObjects.registerType({
+ name: CONTENT_ID,
+ hidden: false,
+ hiddenFromHttpApis: true,
+ namespaceType: 'multiple',
+ mappings: {
+ dynamic: false,
+ properties: {
+ title: { type: 'text' },
+ description: { type: 'text' },
+ linkListJSON: { type: 'text' },
+ },
+ },
+ });
+
+ return {};
+ }
+
+ public start(core: CoreStart) {
+ return {};
+ }
+
+ public stop() {}
+}
From e36e4e6501d749b4d98ca2eec3fc0592b320a164 Mon Sep 17 00:00:00 2001
From: Nick Peihl
Date: Thu, 29 Jun 2023 10:07:29 -0400
Subject: [PATCH 03/53] Revert "Initialize content management"
This reverts commit dcc37c65a87d5ff255e8cd3850deda296f73ba9f.
---
.../navigation_embeddable/common/constants.ts | 17 ---
.../common/content_management/cm_services.ts | 21 ----
.../common/content_management/index.ts | 19 ----
.../common/content_management/latest.ts | 9 --
.../content_management/v1/cm_services.ts | 100 ------------------
.../common/content_management/v1/index.ts | 11 --
.../common/content_management/v1/types.ts | 32 ------
.../navigation_embeddable/common/index.ts | 9 --
.../navigation_embeddable/common/types.ts | 9 --
.../navigation_embeddable/kibana.jsonc | 8 +-
.../public/content_management/index.ts | 9 --
...embeddable_content_management_client.ts.ts | 80 --------------
.../navigation_embeddable/public/index.ts | 4 +-
.../index.ts | 0
.../navigation_embeddable.tsx | 0
.../navigation_embeddable_factory.ts} | 4 +-
.../public/navigation_embeddable_services.ts | 44 --------
.../navigation_embeddable/public/plugin.ts | 29 +----
.../server/content_management/index.ts | 9 --
.../navigation_embeddable_storage.ts | 23 ----
.../navigation_embeddable/server/index.ts | 11 --
.../navigation_embeddable/server/plugin.ts | 52 ---------
22 files changed, 8 insertions(+), 492 deletions(-)
delete mode 100644 src/plugins/navigation_embeddable/common/constants.ts
delete mode 100644 src/plugins/navigation_embeddable/common/content_management/cm_services.ts
delete mode 100644 src/plugins/navigation_embeddable/common/content_management/index.ts
delete mode 100644 src/plugins/navigation_embeddable/common/content_management/latest.ts
delete mode 100644 src/plugins/navigation_embeddable/common/content_management/v1/cm_services.ts
delete mode 100644 src/plugins/navigation_embeddable/common/content_management/v1/index.ts
delete mode 100644 src/plugins/navigation_embeddable/common/content_management/v1/types.ts
delete mode 100644 src/plugins/navigation_embeddable/common/index.ts
delete mode 100644 src/plugins/navigation_embeddable/common/types.ts
delete mode 100644 src/plugins/navigation_embeddable/public/content_management/index.ts
delete mode 100644 src/plugins/navigation_embeddable/public/content_management/navigation_embeddable_content_management_client.ts.ts
rename src/plugins/navigation_embeddable/public/{embeddable => navigation_embeddable}/index.ts (100%)
rename src/plugins/navigation_embeddable/public/{embeddable => navigation_embeddable}/navigation_embeddable.tsx (100%)
rename src/plugins/navigation_embeddable/public/{embeddable/navigation_embeddable_factory.tsx => navigation_embeddable/navigation_embeddable_factory.ts} (92%)
delete mode 100644 src/plugins/navigation_embeddable/public/navigation_embeddable_services.ts
delete mode 100644 src/plugins/navigation_embeddable/server/content_management/index.ts
delete mode 100644 src/plugins/navigation_embeddable/server/content_management/navigation_embeddable_storage.ts
delete mode 100644 src/plugins/navigation_embeddable/server/index.ts
delete mode 100644 src/plugins/navigation_embeddable/server/plugin.ts
diff --git a/src/plugins/navigation_embeddable/common/constants.ts b/src/plugins/navigation_embeddable/common/constants.ts
deleted file mode 100644
index e71ee4836638b..0000000000000
--- a/src/plugins/navigation_embeddable/common/constants.ts
+++ /dev/null
@@ -1,17 +0,0 @@
-/*
- * 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';
-
-export const LATEST_VERSION = 1;
-
-export const CONTENT_ID = 'navigationEmbeddable';
-
-export const APP_NAME = i18n.translate('xpack.maps.visTypeAlias.title', {
- defaultMessage: 'Links',
-});
diff --git a/src/plugins/navigation_embeddable/common/content_management/cm_services.ts b/src/plugins/navigation_embeddable/common/content_management/cm_services.ts
deleted file mode 100644
index af32d57e0437f..0000000000000
--- a/src/plugins/navigation_embeddable/common/content_management/cm_services.ts
+++ /dev/null
@@ -1,21 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License
- * 2.0 and the Server Side Public License, v 1; you may not use this file except
- * in compliance with, at your election, the Elastic License 2.0 or the Server
- * Side Public License, v 1.
- */
-
-import type {
- ContentManagementServicesDefinition as ServicesDefinition,
- Version,
-} from '@kbn/object-versioning';
-
-// We export the versionned service definition from this file and not the barrel to avoid adding
-// the schemas in the "public" js bundle
-
-import { serviceDefinition as v1 } from './v1/cm_services';
-
-export const cmServicesDefinition: { [version: Version]: ServicesDefinition } = {
- 1: v1,
-};
diff --git a/src/plugins/navigation_embeddable/common/content_management/index.ts b/src/plugins/navigation_embeddable/common/content_management/index.ts
deleted file mode 100644
index 3c5ca47d812c4..0000000000000
--- a/src/plugins/navigation_embeddable/common/content_management/index.ts
+++ /dev/null
@@ -1,19 +0,0 @@
-/*
- * 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 { LATEST_VERSION, CONTENT_ID } from '../constants';
-
-export type { NavigationEmbeddableContentType } from '../types';
-
-export type {
- NavigationEmbeddableCrudTypes,
- NavigationEmbeddableAttributes,
- NavigationEmbeddableItem,
-} from './latest';
-
-export * as NavigationEmbeddableV1 from './v1';
diff --git a/src/plugins/navigation_embeddable/common/content_management/latest.ts b/src/plugins/navigation_embeddable/common/content_management/latest.ts
deleted file mode 100644
index e9c79f0f50f93..0000000000000
--- a/src/plugins/navigation_embeddable/common/content_management/latest.ts
+++ /dev/null
@@ -1,9 +0,0 @@
-/*
- * 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 * from './v1';
diff --git a/src/plugins/navigation_embeddable/common/content_management/v1/cm_services.ts b/src/plugins/navigation_embeddable/common/content_management/v1/cm_services.ts
deleted file mode 100644
index 66d1df1454892..0000000000000
--- a/src/plugins/navigation_embeddable/common/content_management/v1/cm_services.ts
+++ /dev/null
@@ -1,100 +0,0 @@
-/*
- * 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 { schema } from '@kbn/config-schema';
-import type { ContentManagementServicesDefinition as ServicesDefinition } from '@kbn/object-versioning';
-import {
- savedObjectSchema,
- objectTypeToGetResultSchema,
- createOptionsSchemas,
- updateOptionsSchema,
- createResultSchema,
-} from '@kbn/content-management-utils';
-
-const navigationEmbeddableAttributesSchema = schema.object(
- {
- title: schema.string(),
- description: schema.maybe(schema.string()),
- linkListJSON: schema.maybe(schema.string()),
- },
- { unknowns: 'forbid' }
-);
-
-const navigationEmbeddableSavedObjectSchema = savedObjectSchema(
- navigationEmbeddableAttributesSchema
-);
-
-const searchOptionsSchema = schema.maybe(
- schema.object(
- {
- onlyTitle: schema.maybe(schema.boolean()),
- },
- { unknowns: 'forbid' }
- )
-);
-
-const navigationEmbeddableCreateOptionsSchema = schema.object({
- id: createOptionsSchemas.id,
- references: schema.maybe(createOptionsSchemas.references),
- overwrite: createOptionsSchemas.overwrite,
-});
-
-const navigationEmbeddableUpdateOptionsSchema = schema.object({
- references: updateOptionsSchema.references,
-});
-
-// Content management service definition.
-// We need it for BWC support between different versions of the content
-export const serviceDefinition: ServicesDefinition = {
- get: {
- out: {
- result: {
- schema: objectTypeToGetResultSchema(navigationEmbeddableSavedObjectSchema),
- },
- },
- },
- create: {
- in: {
- options: {
- schema: navigationEmbeddableCreateOptionsSchema,
- },
- data: {
- schema: navigationEmbeddableSavedObjectSchema,
- },
- },
- out: {
- result: {
- schema: createResultSchema(navigationEmbeddableSavedObjectSchema),
- },
- },
- },
- update: {
- in: {
- options: {
- schema: navigationEmbeddableUpdateOptionsSchema, // same schema as "create"
- },
- data: {
- schema: navigationEmbeddableSavedObjectSchema,
- },
- },
- },
- search: {
- in: {
- options: {
- schema: searchOptionsSchema,
- },
- },
- },
- mSearch: {
- out: {
- result: {
- schema: navigationEmbeddableSavedObjectSchema,
- },
- },
- },
-};
diff --git a/src/plugins/navigation_embeddable/common/content_management/v1/index.ts b/src/plugins/navigation_embeddable/common/content_management/v1/index.ts
deleted file mode 100644
index 8ef0422178d54..0000000000000
--- a/src/plugins/navigation_embeddable/common/content_management/v1/index.ts
+++ /dev/null
@@ -1,11 +0,0 @@
-/*
- * 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 { NavigationEmbeddableCrudTypes } from './types';
-export type { NavigationEmbeddableCrudTypes, NavigationEmbeddableAttributes } from './types';
-export type NavigationEmbeddableItem = NavigationEmbeddableCrudTypes['Item'];
diff --git a/src/plugins/navigation_embeddable/common/content_management/v1/types.ts b/src/plugins/navigation_embeddable/common/content_management/v1/types.ts
deleted file mode 100644
index 00a989292f933..0000000000000
--- a/src/plugins/navigation_embeddable/common/content_management/v1/types.ts
+++ /dev/null
@@ -1,32 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License
- * 2.0 and the Server Side Public License, v 1; you may not use this file except
- * in compliance with, at your election, the Elastic License 2.0 or the Server
- * Side Public License, v 1.
- */
-
-import type {
- ContentManagementCrudTypes,
- SavedObjectCreateOptions,
- SavedObjectUpdateOptions,
-} from '@kbn/content-management-utils';
-import { NavigationEmbeddableContentType } from '../../types';
-
-export type NavigationEmbeddableCrudTypes = ContentManagementCrudTypes<
- NavigationEmbeddableContentType,
- NavigationEmbeddableAttributes,
- Pick,
- Pick,
- {
- /** Flag to indicate to only search the text on the "title" field */
- onlyTitle?: boolean;
- }
->;
-
-// eslint-disable-next-line @typescript-eslint/consistent-type-definitions
-export type NavigationEmbeddableAttributes = {
- title: string;
- description?: string;
- linkListJSON?: string;
-};
diff --git a/src/plugins/navigation_embeddable/common/index.ts b/src/plugins/navigation_embeddable/common/index.ts
deleted file mode 100644
index 1c5c974297430..0000000000000
--- a/src/plugins/navigation_embeddable/common/index.ts
+++ /dev/null
@@ -1,9 +0,0 @@
-/*
- * 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 { CONTENT_ID, LATEST_VERSION } from './constants';
diff --git a/src/plugins/navigation_embeddable/common/types.ts b/src/plugins/navigation_embeddable/common/types.ts
deleted file mode 100644
index 2038a5c0f04c1..0000000000000
--- a/src/plugins/navigation_embeddable/common/types.ts
+++ /dev/null
@@ -1,9 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License
- * 2.0 and the Server Side Public License, v 1; you may not use this file except
- * in compliance with, at your election, the Elastic License 2.0 or the Server
- * Side Public License, v 1.
- */
-
-export type NavigationEmbeddableContentType = 'navigationEmbeddable';
diff --git a/src/plugins/navigation_embeddable/kibana.jsonc b/src/plugins/navigation_embeddable/kibana.jsonc
index d0b8f4d43c9eb..76d185c560084 100644
--- a/src/plugins/navigation_embeddable/kibana.jsonc
+++ b/src/plugins/navigation_embeddable/kibana.jsonc
@@ -5,14 +5,10 @@
"description": "An embeddable for quickly navigating between dashboards.",
"plugin": {
"id": "navigationEmbeddable",
- "server": true,
+ "server": false,
"browser": true,
"requiredPlugins": [
- "contentManagement",
- "dashboard",
- "embeddable",
- "kibanaReact",
- "presentationUtil"
+ "embeddable"
],
"optionalPlugins": [],
"requiredBundles": [
diff --git a/src/plugins/navigation_embeddable/public/content_management/index.ts b/src/plugins/navigation_embeddable/public/content_management/index.ts
deleted file mode 100644
index abdf2fd0159eb..0000000000000
--- a/src/plugins/navigation_embeddable/public/content_management/index.ts
+++ /dev/null
@@ -1,9 +0,0 @@
-/*
- * 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 { navigationEmbeddableClient } from './navigation_embeddable_content_management_client.ts';
diff --git a/src/plugins/navigation_embeddable/public/content_management/navigation_embeddable_content_management_client.ts.ts b/src/plugins/navigation_embeddable/public/content_management/navigation_embeddable_content_management_client.ts.ts
deleted file mode 100644
index 800c7ff5eb59f..0000000000000
--- a/src/plugins/navigation_embeddable/public/content_management/navigation_embeddable_content_management_client.ts.ts
+++ /dev/null
@@ -1,80 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License
- * 2.0 and the Server Side Public License, v 1; you may not use this file except
- * in compliance with, at your election, the Elastic License 2.0 or the Server
- * Side Public License, v 1.
- */
-
-import type { SearchQuery } from '@kbn/content-management-plugin/common';
-import type { NavigationEmbeddableCrudTypes } from '../../common/content_management';
-import { CONTENT_ID as contentTypeId } from '../../common';
-import { contentManagement } from '../navigation_embeddable_services';
-
-const get = async (id: string) => {
- return contentManagement.client.get({ contentTypeId, id });
-};
-
-const create = async ({
- data,
- options,
-}: Omit) => {
- const res = await contentManagement.client.create<
- NavigationEmbeddableCrudTypes['CreateIn'],
- NavigationEmbeddableCrudTypes['CreateOut']
- >({
- contentTypeId,
- data,
- options,
- });
- return res;
-};
-
-const update = async ({
- id,
- data,
- options,
-}: Omit) => {
- const res = await contentManagement.client.update<
- NavigationEmbeddableCrudTypes['UpdateIn'],
- NavigationEmbeddableCrudTypes['UpdateOut']
- >({
- contentTypeId,
- id,
- data,
- options,
- });
- return res;
-};
-
-const deleteNavigationEmbeddable = async (id: string) => {
- await contentManagement.client.delete<
- NavigationEmbeddableCrudTypes['DeleteIn'],
- NavigationEmbeddableCrudTypes['DeleteOut']
- >({
- contentTypeId,
- id,
- });
-};
-
-const search = async (
- query: SearchQuery = {},
- options?: NavigationEmbeddableCrudTypes['SearchOptions']
-) => {
- return contentManagement.client.search<
- NavigationEmbeddableCrudTypes['SearchIn'],
- NavigationEmbeddableCrudTypes['SearchOut']
- >({
- contentTypeId,
- query,
- options,
- });
-};
-
-export const navigationEmbeddableClient = {
- get,
- create,
- update,
- delete: deleteNavigationEmbeddable,
- search,
-};
diff --git a/src/plugins/navigation_embeddable/public/index.ts b/src/plugins/navigation_embeddable/public/index.ts
index 9cdbcfcc6c667..5e8be17c8958b 100644
--- a/src/plugins/navigation_embeddable/public/index.ts
+++ b/src/plugins/navigation_embeddable/public/index.ts
@@ -6,12 +6,12 @@
* Side Public License, v 1.
*/
-export type { NavigationEmbeddableFactory } from './embeddable';
+export type { NavigationEmbeddableFactory } from './navigation_embeddable';
export {
NAVIGATION_EMBEDDABLE_TYPE,
NavigationEmbeddableFactoryDefinition,
NavigationEmbeddable,
-} from './embeddable';
+} from './navigation_embeddable';
import { NavigationEmbeddablePlugin } from './plugin';
diff --git a/src/plugins/navigation_embeddable/public/embeddable/index.ts b/src/plugins/navigation_embeddable/public/navigation_embeddable/index.ts
similarity index 100%
rename from src/plugins/navigation_embeddable/public/embeddable/index.ts
rename to src/plugins/navigation_embeddable/public/navigation_embeddable/index.ts
diff --git a/src/plugins/navigation_embeddable/public/embeddable/navigation_embeddable.tsx b/src/plugins/navigation_embeddable/public/navigation_embeddable/navigation_embeddable.tsx
similarity index 100%
rename from src/plugins/navigation_embeddable/public/embeddable/navigation_embeddable.tsx
rename to src/plugins/navigation_embeddable/public/navigation_embeddable/navigation_embeddable.tsx
diff --git a/src/plugins/navigation_embeddable/public/embeddable/navigation_embeddable_factory.tsx b/src/plugins/navigation_embeddable/public/navigation_embeddable/navigation_embeddable_factory.ts
similarity index 92%
rename from src/plugins/navigation_embeddable/public/embeddable/navigation_embeddable_factory.tsx
rename to src/plugins/navigation_embeddable/public/navigation_embeddable/navigation_embeddable_factory.ts
index 3bdb38511838e..54f8ecbb9f892 100644
--- a/src/plugins/navigation_embeddable/public/embeddable/navigation_embeddable_factory.tsx
+++ b/src/plugins/navigation_embeddable/public/navigation_embeddable/navigation_embeddable_factory.ts
@@ -9,7 +9,7 @@
import { i18n } from '@kbn/i18n';
import type { EmbeddableInput, IContainer } from '@kbn/embeddable-plugin/public';
import { EmbeddableFactory, EmbeddableFactoryDefinition } from '@kbn/embeddable-plugin/public';
-import { NAVIGATION_EMBEDDABLE_TYPE, NavigationEmbeddable } from './navigation_embeddable';
+import { NavigationEmbeddable, NAVIGATION_EMBEDDABLE_TYPE } from './navigation_embeddable';
export type NavigationEmbeddableFactory = EmbeddableFactory;
@@ -26,7 +26,7 @@ export class NavigationEmbeddableFactoryDefinition implements EmbeddableFactoryD
public getDisplayName() {
return i18n.translate('navigationEmbeddable.navigationEmbeddableFactory.displayName', {
- defaultMessage: 'Links',
+ defaultMessage: 'Navigation',
});
}
diff --git a/src/plugins/navigation_embeddable/public/navigation_embeddable_services.ts b/src/plugins/navigation_embeddable/public/navigation_embeddable_services.ts
deleted file mode 100644
index 7a2311e412006..0000000000000
--- a/src/plugins/navigation_embeddable/public/navigation_embeddable_services.ts
+++ /dev/null
@@ -1,44 +0,0 @@
-/*
- * 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 { BehaviorSubject } from 'rxjs';
-
-import { CoreStart } from '@kbn/core/public';
-import { DashboardStart } from '@kbn/dashboard-plugin/public';
-
-import { ContentManagementPublicStart } from '@kbn/content-management-plugin/public';
-import { StartDependencies as NavigationEmbeddableStartDependencies } from './plugin';
-
-export let coreServices: CoreStart;
-export let dashboardServices: DashboardStart;
-export let contentManagement: ContentManagementPublicStart;
-
-const servicesReady$ = new BehaviorSubject(false);
-
-export const untilPluginStartServicesReady = () => {
- if (servicesReady$.value) return Promise.resolve();
- return new Promise((resolve) => {
- const subscription = servicesReady$.subscribe((isInitialized) => {
- if (isInitialized) {
- subscription.unsubscribe();
- resolve();
- }
- });
- });
-};
-
-export const setKibanaServices = (
- kibanaCore: CoreStart,
- deps: NavigationEmbeddableStartDependencies
-) => {
- coreServices = kibanaCore;
- dashboardServices = deps.dashboard;
- contentManagement = deps.contentManagement;
-
- servicesReady$.next(true);
-};
diff --git a/src/plugins/navigation_embeddable/public/plugin.ts b/src/plugins/navigation_embeddable/public/plugin.ts
index 940c4b037e90a..23dff1f5d1eb8 100644
--- a/src/plugins/navigation_embeddable/public/plugin.ts
+++ b/src/plugins/navigation_embeddable/public/plugin.ts
@@ -7,32 +7,16 @@
*/
import { CoreSetup, CoreStart, Plugin } from '@kbn/core/public';
-import {
- ContentManagementPublicSetup,
- ContentManagementPublicStart,
-} from '@kbn/content-management-plugin/public';
-import { DashboardStart } from '@kbn/dashboard-plugin/public';
import { EmbeddableSetup, EmbeddableStart } from '@kbn/embeddable-plugin/public';
-import { NAVIGATION_EMBEDDABLE_TYPE } from './embeddable';
-import { NavigationEmbeddableFactoryDefinition } from './embeddable';
-import { CONTENT_ID, LATEST_VERSION } from '../common/content_management';
-import { APP_NAME } from '../common/constants';
-import { setKibanaServices } from './navigation_embeddable_services';
+import { NAVIGATION_EMBEDDABLE_TYPE } from './navigation_embeddable';
+import { NavigationEmbeddableFactoryDefinition } from './navigation_embeddable';
-// eslint-disable-next-line @typescript-eslint/no-empty-interface
-export interface PluginSetup {}
-
-// eslint-disable-next-line @typescript-eslint/no-empty-interface
-export interface PluginStart {}
export interface SetupDependencies {
embeddable: EmbeddableSetup;
- contentManagement: ContentManagementPublicSetup;
}
export interface StartDependencies {
embeddable: EmbeddableStart;
- contentManagement: ContentManagementPublicStart;
- dashboard: DashboardStart;
}
export class NavigationEmbeddablePlugin
@@ -45,18 +29,9 @@ export class NavigationEmbeddablePlugin
NAVIGATION_EMBEDDABLE_TYPE,
new NavigationEmbeddableFactoryDefinition()
);
-
- plugins.contentManagement.registry.register({
- id: CONTENT_ID,
- version: {
- latest: LATEST_VERSION,
- },
- name: APP_NAME,
- });
}
public start(core: CoreStart, plugins: StartDependencies) {
- setKibanaServices(core, plugins);
return {};
}
diff --git a/src/plugins/navigation_embeddable/server/content_management/index.ts b/src/plugins/navigation_embeddable/server/content_management/index.ts
deleted file mode 100644
index 2376765bcac83..0000000000000
--- a/src/plugins/navigation_embeddable/server/content_management/index.ts
+++ /dev/null
@@ -1,9 +0,0 @@
-/*
- * 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 { NavigationEmbeddableStorage } from './navigation_embeddable_storage';
diff --git a/src/plugins/navigation_embeddable/server/content_management/navigation_embeddable_storage.ts b/src/plugins/navigation_embeddable/server/content_management/navigation_embeddable_storage.ts
deleted file mode 100644
index c6e1974dc24c3..0000000000000
--- a/src/plugins/navigation_embeddable/server/content_management/navigation_embeddable_storage.ts
+++ /dev/null
@@ -1,23 +0,0 @@
-/*
- * 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 { SOContentStorage } from '@kbn/content-management-utils';
-import type { NavigationEmbeddableCrudTypes } from '../../common/content_management';
-import { CONTENT_ID } from '../../common';
-import { cmServicesDefinition } from '../../common/content_management/cm_services';
-
-export class NavigationEmbeddableStorage extends SOContentStorage {
- constructor() {
- super({
- savedObjectType: CONTENT_ID,
- cmServicesDefinition,
- enableMSearch: true,
- allowedSavedObjectAttributes: ['title', 'description', 'linkListJSON'],
- });
- }
-}
diff --git a/src/plugins/navigation_embeddable/server/index.ts b/src/plugins/navigation_embeddable/server/index.ts
deleted file mode 100644
index 6ececdd95b5d0..0000000000000
--- a/src/plugins/navigation_embeddable/server/index.ts
+++ /dev/null
@@ -1,11 +0,0 @@
-/*
- * 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 { NavigationEmbeddableServerPlugin } from './plugin';
-
-export const plugin = () => new NavigationEmbeddableServerPlugin();
diff --git a/src/plugins/navigation_embeddable/server/plugin.ts b/src/plugins/navigation_embeddable/server/plugin.ts
deleted file mode 100644
index 9e2d973310eba..0000000000000
--- a/src/plugins/navigation_embeddable/server/plugin.ts
+++ /dev/null
@@ -1,52 +0,0 @@
-/*
- * 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 { CoreSetup, CoreStart, Plugin } from '@kbn/core/server';
-import type { ContentManagementServerSetup } from '@kbn/content-management-plugin/server';
-import { CONTENT_ID, LATEST_VERSION } from '../common';
-import { NavigationEmbeddableStorage } from './content_management';
-
-export class NavigationEmbeddableServerPlugin implements Plugin {
- public setup(
- core: CoreSetup,
- plugins: {
- contentManagement: ContentManagementServerSetup;
- }
- ) {
- plugins.contentManagement.register({
- id: CONTENT_ID,
- storage: new NavigationEmbeddableStorage(),
- version: {
- latest: LATEST_VERSION,
- },
- });
-
- core.savedObjects.registerType({
- name: CONTENT_ID,
- hidden: false,
- hiddenFromHttpApis: true,
- namespaceType: 'multiple',
- mappings: {
- dynamic: false,
- properties: {
- title: { type: 'text' },
- description: { type: 'text' },
- linkListJSON: { type: 'text' },
- },
- },
- });
-
- return {};
- }
-
- public start(core: CoreStart) {
- return {};
- }
-
- public stop() {}
-}
From 3c2e8f64425aa9c603dfbda95402f6cfd1fb448f Mon Sep 17 00:00:00 2001
From: Hannah Mudge
Date: Mon, 10 Jul 2023 09:10:05 -0600
Subject: [PATCH 04/53] [Dashboard Navigation] Add creation UI (#160179)
Closes https://github.com/elastic/kibana/issues/154360
## Summary
This PR adds the first draft of the UI for creating the navigation
embeddable and adding links to it. Note that this PR **only** addresses
adding links - you cannot currently remove links from the panel or edit
existing links. Also, because this PR contains critical code, some
cleanup tasks were left as `TODO` in order to get it merged ASAP - these
will be addressed in follow up PRs.
https://github.com/elastic/kibana/assets/8698078/ac011207-1c4b-40cd-a151-78addb99d32d
### Checklist
- [x] Any text added follows [EUI's writing
guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses
sentence case text and includes [i18n
support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md)
- [ ] ~[Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios~ Will be
addressed in https://github.com/elastic/kibana/issues/161287
- [x] Any UI touched in this PR is usable by keyboard only (learn more
about [keyboard accessibility](https://webaim.org/techniques/keyboard/))
- [x] Any UI touched in this PR does not create any new axe failures
(run axe in browser:
[FF](https://addons.mozilla.org/en-US/firefox/addon/axe-devtools/),
[Chrome](https://chrome.google.com/webstore/detail/axe-web-accessibility-tes/lhdoppojpmngadmnindnejefpokejbdd?hl=en-US))
- [x] This renders correctly on smaller devices using a responsive
layout. (You can test this [in your
browser](https://www.browserstack.com/guide/responsive-testing-on-local-server))
- [x] This was checked for [cross-browser
compatibility](https://www.elastic.co/support/matrix#matrix_browsers)
### For maintainers
- [ ] This was checked for breaking API changes and was [labeled
appropriately](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process)
---------
Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
---
.../top_nav/dashboard_editing_toolbar.tsx | 2 +-
.../dashboard_content_management_service.ts | 3 +-
.../lib/find_dashboards.ts | 3 +
.../dashboard_content_management/types.ts | 5 +-
.../public/lib/actions/edit_panel_action.ts | 2 +-
.../public/lib/containers/container.ts | 3 +-
.../lib/embeddables/embeddable_factory.ts | 5 +-
.../public/lib/panel/embeddable_panel.tsx | 19 +-
.../navigation_embeddable/kibana.jsonc | 11 +-
.../navigation_embeddable/public/_mixins.scss | 25 +++
.../dashboard_link_component.tsx | 54 +++++
.../dashboard_link_destination_picker.tsx | 118 +++++++++++
.../dashboard_link/dashboard_link_strings.ts | 32 +++
.../dashboard_link/dashboard_link_tools.tsx | 89 ++++++++
.../external_link/external_link_component.tsx | 24 +++
.../external_link_destination_picker.tsx | 45 ++++
.../external_link/external_link_strings.ts | 24 +++
.../components/navigation_embeddable.scss | 23 +++
.../navigation_embeddable_component.tsx | 44 ++++
.../navigation_embeddable_link_editor.tsx | 175 ++++++++++++++++
.../navigation_embeddable_panel_editor.tsx | 192 ++++++++++++++++++
.../navigation_embeddable_strings.ts | 58 ++++++
.../public/editor/open_editor_flyout.tsx | 70 +++++++
.../index.ts | 5 +-
.../embeddable/navigation_embeddable.tsx | 97 +++++++++
.../navigation_embeddable_factory.ts | 102 ++++++++++
.../navigation_embeddable_reducers.ts | 27 +++
.../public/embeddable/types.ts | 70 +++++++
.../navigation_embeddable/public/index.ts | 4 +-
.../navigation_embeddable.tsx | 32 ---
.../navigation_embeddable_factory.ts | 36 ----
.../navigation_embeddable/public/plugin.ts | 39 ++--
.../public/services/kibana_services.ts | 41 ++++
.../navigation_embeddable/tsconfig.json | 13 +-
34 files changed, 1380 insertions(+), 112 deletions(-)
create mode 100644 src/plugins/navigation_embeddable/public/_mixins.scss
create mode 100644 src/plugins/navigation_embeddable/public/components/dashboard_link/dashboard_link_component.tsx
create mode 100644 src/plugins/navigation_embeddable/public/components/dashboard_link/dashboard_link_destination_picker.tsx
create mode 100644 src/plugins/navigation_embeddable/public/components/dashboard_link/dashboard_link_strings.ts
create mode 100644 src/plugins/navigation_embeddable/public/components/dashboard_link/dashboard_link_tools.tsx
create mode 100644 src/plugins/navigation_embeddable/public/components/external_link/external_link_component.tsx
create mode 100644 src/plugins/navigation_embeddable/public/components/external_link/external_link_destination_picker.tsx
create mode 100644 src/plugins/navigation_embeddable/public/components/external_link/external_link_strings.ts
create mode 100644 src/plugins/navigation_embeddable/public/components/navigation_embeddable.scss
create mode 100644 src/plugins/navigation_embeddable/public/components/navigation_embeddable_component.tsx
create mode 100644 src/plugins/navigation_embeddable/public/components/navigation_embeddable_link_editor.tsx
create mode 100644 src/plugins/navigation_embeddable/public/components/navigation_embeddable_panel_editor.tsx
create mode 100644 src/plugins/navigation_embeddable/public/components/navigation_embeddable_strings.ts
create mode 100644 src/plugins/navigation_embeddable/public/editor/open_editor_flyout.tsx
rename src/plugins/navigation_embeddable/public/{navigation_embeddable => embeddable}/index.ts (81%)
create mode 100644 src/plugins/navigation_embeddable/public/embeddable/navigation_embeddable.tsx
create mode 100644 src/plugins/navigation_embeddable/public/embeddable/navigation_embeddable_factory.ts
create mode 100644 src/plugins/navigation_embeddable/public/embeddable/navigation_embeddable_reducers.ts
create mode 100644 src/plugins/navigation_embeddable/public/embeddable/types.ts
delete mode 100644 src/plugins/navigation_embeddable/public/navigation_embeddable/navigation_embeddable.tsx
delete mode 100644 src/plugins/navigation_embeddable/public/navigation_embeddable/navigation_embeddable_factory.ts
create mode 100644 src/plugins/navigation_embeddable/public/services/kibana_services.ts
diff --git a/src/plugins/dashboard/public/dashboard_app/top_nav/dashboard_editing_toolbar.tsx b/src/plugins/dashboard/public/dashboard_app/top_nav/dashboard_editing_toolbar.tsx
index 708af176d785d..05962d387e476 100644
--- a/src/plugins/dashboard/public/dashboard_app/top_nav/dashboard_editing_toolbar.tsx
+++ b/src/plugins/dashboard/public/dashboard_app/top_nav/dashboard_editing_toolbar.tsx
@@ -101,7 +101,7 @@ export function DashboardEditingToolbar() {
let explicitInput: Awaited>;
try {
- explicitInput = await embeddableFactory.getExplicitInput();
+ explicitInput = await embeddableFactory.getExplicitInput(undefined, dashboard);
} catch (e) {
// error likely means user canceled embeddable creation
return;
diff --git a/src/plugins/dashboard/public/services/dashboard_content_management/dashboard_content_management_service.ts b/src/plugins/dashboard/public/services/dashboard_content_management/dashboard_content_management_service.ts
index 5a5c67c798606..f16bd4442f2c1 100644
--- a/src/plugins/dashboard/public/services/dashboard_content_management/dashboard_content_management_service.ts
+++ b/src/plugins/dashboard/public/services/dashboard_content_management/dashboard_content_management_service.ts
@@ -65,11 +65,12 @@ export const dashboardContentManagementServiceFactory: DashboardContentManagemen
dashboardSessionStorage,
}),
findDashboards: {
- search: ({ hasReference, hasNoReference, search, size }) =>
+ search: ({ hasReference, hasNoReference, search, size, options }) =>
searchDashboards({
contentManagement,
hasNoReference,
hasReference,
+ options,
search,
size,
}),
diff --git a/src/plugins/dashboard/public/services/dashboard_content_management/lib/find_dashboards.ts b/src/plugins/dashboard/public/services/dashboard_content_management/lib/find_dashboards.ts
index 41bdd60d6c1d7..49ffee54d536b 100644
--- a/src/plugins/dashboard/public/services/dashboard_content_management/lib/find_dashboards.ts
+++ b/src/plugins/dashboard/public/services/dashboard_content_management/lib/find_dashboards.ts
@@ -18,6 +18,7 @@ import { DASHBOARD_CONTENT_ID } from '../../../dashboard_constants';
export interface SearchDashboardsArgs {
contentManagement: DashboardStartDependencies['contentManagement'];
+ options?: DashboardCrudTypes['SearchIn']['options'];
hasNoReference?: SavedObjectsFindOptionsReference[];
hasReference?: SavedObjectsFindOptionsReference[];
search: string;
@@ -33,6 +34,7 @@ export async function searchDashboards({
contentManagement,
hasNoReference,
hasReference,
+ options,
search,
size,
}: SearchDashboardsArgs): Promise {
@@ -52,6 +54,7 @@ export async function searchDashboards({
excluded: (hasNoReference ?? []).map(({ id }) => id),
},
},
+ options,
});
return {
total,
diff --git a/src/plugins/dashboard/public/services/dashboard_content_management/types.ts b/src/plugins/dashboard/public/services/dashboard_content_management/types.ts
index 92fbe3005e9e4..858d5800961b5 100644
--- a/src/plugins/dashboard/public/services/dashboard_content_management/types.ts
+++ b/src/plugins/dashboard/public/services/dashboard_content_management/types.ts
@@ -87,7 +87,10 @@ export interface SaveDashboardReturn {
*/
export interface FindDashboardsService {
search: (
- props: Pick
+ props: Pick<
+ SearchDashboardsArgs,
+ 'hasReference' | 'hasNoReference' | 'search' | 'size' | 'options'
+ >
) => Promise;
findByIds: (ids: string[]) => Promise;
findByTitle: (title: string) => Promise<{ id: string } | undefined>;
diff --git a/src/plugins/embeddable/public/lib/actions/edit_panel_action.ts b/src/plugins/embeddable/public/lib/actions/edit_panel_action.ts
index ba59d92cbef60..98e541fd08e6f 100644
--- a/src/plugins/embeddable/public/lib/actions/edit_panel_action.ts
+++ b/src/plugins/embeddable/public/lib/actions/edit_panel_action.ts
@@ -92,7 +92,7 @@ export class EditPanelAction implements Action {
}
const oldExplicitInput = embeddable.getExplicitInput();
- const newExplicitInput = await factory.getExplicitInput(oldExplicitInput);
+ const newExplicitInput = await factory.getExplicitInput(oldExplicitInput, embeddable.parent);
embeddable.parent?.replaceEmbeddable(embeddable.id, newExplicitInput);
return;
}
diff --git a/src/plugins/embeddable/public/lib/containers/container.ts b/src/plugins/embeddable/public/lib/containers/container.ts
index a53cfb8725fac..2f29410bc3e59 100644
--- a/src/plugins/embeddable/public/lib/containers/container.ts
+++ b/src/plugins/embeddable/public/lib/containers/container.ts
@@ -301,7 +301,7 @@ export abstract class Container<
public async getExplicitInputIsEqual(lastInput: TContainerInput) {
const { panels: lastPanels, ...restOfLastInput } = lastInput;
- const { panels: currentPanels, ...restOfCurrentInput } = this.getInput();
+ const { panels: currentPanels, ...restOfCurrentInput } = this.getExplicitInput();
const otherInputIsEqual = isEqual(restOfLastInput, restOfCurrentInput);
if (!otherInputIsEqual) return false;
@@ -371,7 +371,6 @@ export abstract class Container<
initializeSettings?: EmbeddableContainerSettings
) {
let initializeOrder = Object.keys(initialInput.panels);
-
if (initializeSettings?.childIdInitializeOrder) {
const initializeOrderSet = new Set();
diff --git a/src/plugins/embeddable/public/lib/embeddables/embeddable_factory.ts b/src/plugins/embeddable/public/lib/embeddables/embeddable_factory.ts
index 0d4aa5f150abc..f785ea9195930 100644
--- a/src/plugins/embeddable/public/lib/embeddables/embeddable_factory.ts
+++ b/src/plugins/embeddable/public/lib/embeddables/embeddable_factory.ts
@@ -99,7 +99,10 @@ export interface EmbeddableFactory<
*
* Can be used to edit an embeddable by re-requesting explicit input. Initial input can be provided to allow the editor to show the current state.
*/
- getExplicitInput(initialInput?: Partial): Promise>;
+ getExplicitInput(
+ initialInput?: Partial,
+ parent?: IContainer
+ ): Promise>;
/**
* Creates a new embeddable instance based off the saved object id.
diff --git a/src/plugins/embeddable/public/lib/panel/embeddable_panel.tsx b/src/plugins/embeddable/public/lib/panel/embeddable_panel.tsx
index c6bef2db73ac9..d964dd8c4b0b4 100644
--- a/src/plugins/embeddable/public/lib/panel/embeddable_panel.tsx
+++ b/src/plugins/embeddable/public/lib/panel/embeddable_panel.tsx
@@ -430,27 +430,28 @@ export class EmbeddablePanel extends React.Component {
};
private getActionContextMenuPanel = async () => {
- let regularActions =
+ const regularActions =
(await this.props.getActions?.(CONTEXT_MENU_TRIGGER, {
embeddable: this.props.embeddable,
})) ?? [];
const { disabledActions } = this.props.embeddable.getInput();
+
+ let allActions = regularActions.concat(
+ Object.values(this.state.universalActions ?? {}) as Array>
+ );
if (disabledActions) {
const removeDisabledActions = removeById(disabledActions);
- regularActions = regularActions.filter(removeDisabledActions);
+ allActions = allActions.filter(removeDisabledActions);
}
- let sortedActions = regularActions
- .concat(Object.values(this.state.universalActions || {}) as Array>)
- .sort(sortByOrderField);
-
if (this.props.actionPredicate) {
- sortedActions = sortedActions.filter(({ id }) => this.props.actionPredicate!(id));
+ allActions = allActions.filter(({ id }) => this.props.actionPredicate!(id));
}
+ allActions.sort(sortByOrderField);
const panels = await buildContextMenuForActions({
- actions: sortedActions.map((action) => ({
+ actions: allActions.map((action) => ({
action,
context: { embeddable: this.props.embeddable },
trigger: contextMenuTrigger,
@@ -460,7 +461,7 @@ export class EmbeddablePanel extends React.Component {
return {
panels,
- actions: sortedActions,
+ actions: allActions,
};
};
}
diff --git a/src/plugins/navigation_embeddable/kibana.jsonc b/src/plugins/navigation_embeddable/kibana.jsonc
index 76d185c560084..961aacc7641aa 100644
--- a/src/plugins/navigation_embeddable/kibana.jsonc
+++ b/src/plugins/navigation_embeddable/kibana.jsonc
@@ -1,17 +1,14 @@
{
"type": "plugin",
- "id": "@kbn/navigation-embeddable-plugin",
"owner": "@elastic/kibana-presentation",
+ "id": "@kbn/navigation-embeddable-plugin",
"description": "An embeddable for quickly navigating between dashboards.",
"plugin": {
"id": "navigationEmbeddable",
"server": false,
"browser": true,
- "requiredPlugins": [
- "embeddable"
- ],
- "optionalPlugins": [],
- "requiredBundles": [
- ]
+ "requiredPlugins": ["dashboard", "embeddable", "kibanaReact", "presentationUtil"],
+ "optionalPlugins": ["triggersActionsUi"],
+ "requiredBundles": []
}
}
diff --git a/src/plugins/navigation_embeddable/public/_mixins.scss b/src/plugins/navigation_embeddable/public/_mixins.scss
new file mode 100644
index 0000000000000..47249b0aa42d4
--- /dev/null
+++ b/src/plugins/navigation_embeddable/public/_mixins.scss
@@ -0,0 +1,25 @@
+@import '../../../core/public/mixins';
+
+@keyframes euiFlyoutAnimation {
+ 0% {
+ opacity: 0;
+ transform: translateX(100%);
+ }
+
+ 100% {
+ opacity: 1;
+ transform: translateX(0%);
+ }
+}
+
+@mixin euiFlyout {
+ @include kibanaFullBodyHeight();
+ border-left: $euiBorderThin;
+ position: fixed;
+ width: 50%;
+ z-index: $euiZFlyout;
+ background: $euiColorEmptyShade;
+ display: flex;
+ flex-direction: column;
+ align-items: stretch;
+}
\ No newline at end of file
diff --git a/src/plugins/navigation_embeddable/public/components/dashboard_link/dashboard_link_component.tsx b/src/plugins/navigation_embeddable/public/components/dashboard_link/dashboard_link_component.tsx
new file mode 100644
index 0000000000000..7c8170674d9ea
--- /dev/null
+++ b/src/plugins/navigation_embeddable/public/components/dashboard_link/dashboard_link_component.tsx
@@ -0,0 +1,54 @@
+/*
+ * 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 useAsync from 'react-use/lib/useAsync';
+
+import { EuiButtonEmpty } from '@elastic/eui';
+import { DashboardContainer } from '@kbn/dashboard-plugin/public/dashboard_container';
+
+import {
+ DASHBOARD_LINK_TYPE,
+ NavigationEmbeddableLink,
+ NavigationLinkInfo,
+} from '../../embeddable/types';
+import { fetchDashboard } from './dashboard_link_tools';
+import { useNavigationEmbeddable } from '../../embeddable/navigation_embeddable';
+
+export const DashboardLinkComponent = ({ link }: { link: NavigationEmbeddableLink }) => {
+ const navEmbeddable = useNavigationEmbeddable();
+
+ const dashboardContainer = navEmbeddable.parent as DashboardContainer;
+ const parentDashboardTitle = dashboardContainer.select((state) => state.explicitInput.title);
+ const parentDashboardId = dashboardContainer.select((state) => state.componentState.lastSavedId);
+
+ const { loading: loadingDestinationDashboard, value: destinationDashboard } =
+ useAsync(async () => {
+ return await fetchDashboard(link.destination);
+ }, [link, parentDashboardId]);
+
+ return (
+ {}, // TODO: As part of https://github.com/elastic/kibana/issues/154381, connect to drilldown
+ })}
+ >
+ {link.label ||
+ (link.destination === parentDashboardId
+ ? parentDashboardTitle
+ : destinationDashboard?.attributes.title)}
+
+ );
+};
diff --git a/src/plugins/navigation_embeddable/public/components/dashboard_link/dashboard_link_destination_picker.tsx b/src/plugins/navigation_embeddable/public/components/dashboard_link/dashboard_link_destination_picker.tsx
new file mode 100644
index 0000000000000..715434dc2c80f
--- /dev/null
+++ b/src/plugins/navigation_embeddable/public/components/dashboard_link/dashboard_link_destination_picker.tsx
@@ -0,0 +1,118 @@
+/*
+ * 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 { debounce } from 'lodash';
+import useAsync from 'react-use/lib/useAsync';
+import React, { useEffect, useMemo, useState } from 'react';
+
+import {
+ EuiBadge,
+ EuiSpacer,
+ EuiHighlight,
+ EuiSelectable,
+ EuiFieldSearch,
+ EuiSelectableOption,
+} from '@elastic/eui';
+import { DashboardContainer } from '@kbn/dashboard-plugin/public/dashboard_container';
+import { DashboardItem } from '../../embeddable/types';
+import { memoizedFetchDashboards } from './dashboard_link_tools';
+import { DashboardLinkEmbeddableStrings } from './dashboard_link_strings';
+
+export const DashboardLinkDestinationPicker = ({
+ setDestination,
+ setPlaceholder,
+ currentDestination,
+ parentDashboard,
+ ...other
+}: {
+ setDestination: (destination?: string) => void;
+ setPlaceholder: (placeholder?: string) => void;
+ currentDestination?: string;
+ parentDashboard?: DashboardContainer;
+}) => {
+ const [searchString, setSearchString] = useState('');
+ const [selectedDashboard, setSelectedDashboard] = useState();
+ const [dashboardListOptions, setDashboardListOptions] = useState([]);
+
+ const parentDashboardId = parentDashboard?.select((state) => state.componentState.lastSavedId);
+
+ const { loading: loadingDashboardList, value: dashboardList } = useAsync(async () => {
+ return await memoizedFetchDashboards(searchString, undefined, parentDashboardId);
+ }, [searchString, parentDashboardId]);
+
+ useEffect(() => {
+ const dashboardOptions =
+ (dashboardList ?? []).map((dashboard: DashboardItem) => {
+ return {
+ data: dashboard,
+ label: dashboard.attributes.title,
+ ...(dashboard.id === parentDashboardId
+ ? {
+ prepend: (
+ {DashboardLinkEmbeddableStrings.getCurrentDashboardLabel()}
+ ),
+ }
+ : {}),
+ } as EuiSelectableOption;
+ }) ?? [];
+
+ setDashboardListOptions(dashboardOptions);
+ }, [dashboardList, parentDashboardId, searchString]);
+
+ const debouncedSetSearch = useMemo(
+ () =>
+ debounce((newSearch: string) => {
+ setSearchString(newSearch);
+ }, 250),
+ [setSearchString]
+ );
+
+ useEffect(() => {
+ if (selectedDashboard) {
+ setDestination(selectedDashboard.id);
+ setPlaceholder(selectedDashboard.attributes.title);
+ } else {
+ setDestination(undefined);
+ setPlaceholder(undefined);
+ }
+ }, [selectedDashboard, setDestination, setPlaceholder]);
+
+ /* {...other} is needed so all inner elements are treated as part of the form */
+ return (
+
+ {
+ debouncedSetSearch(e.target.value);
+ }}
+ />
+
+ {
+ if (selected.checked) {
+ setSelectedDashboard(selected.data as DashboardItem);
+ } else {
+ setSelectedDashboard(undefined);
+ }
+ setDashboardListOptions(newOptions);
+ }}
+ renderOption={(option) => {
+ return {option.label} ;
+ }}
+ >
+ {(list) => list}
+
+
+ );
+};
diff --git a/src/plugins/navigation_embeddable/public/components/dashboard_link/dashboard_link_strings.ts b/src/plugins/navigation_embeddable/public/components/dashboard_link/dashboard_link_strings.ts
new file mode 100644
index 0000000000000..9bc2e2d40f0b0
--- /dev/null
+++ b/src/plugins/navigation_embeddable/public/components/dashboard_link/dashboard_link_strings.ts
@@ -0,0 +1,32 @@
+/*
+ * 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';
+
+export const DashboardLinkEmbeddableStrings = {
+ getDisplayName: () =>
+ i18n.translate('navigationEmbeddable.dashboardLink.displayName', {
+ defaultMessage: 'Dashboard',
+ }),
+ getDescription: () =>
+ i18n.translate('navigationEmbeddable.dsahboardLink.description', {
+ defaultMessage: 'Go to dashboard',
+ }),
+ getSearchPlaceholder: () =>
+ i18n.translate('navigationEmbeddable.dashboardLink.editor.searchPlaceholder', {
+ defaultMessage: 'Search for a dashboard',
+ }),
+ getDashboardPickerAriaLabel: () =>
+ i18n.translate('navigationEmbeddable.dashboardLink.editor.dashboardPickerAriaLabel', {
+ defaultMessage: 'Pick a destination dashboard',
+ }),
+ getCurrentDashboardLabel: () =>
+ i18n.translate('navigationEmbeddable.dashboardLink.editor.currentDashboardLabel', {
+ defaultMessage: 'Current',
+ }),
+};
diff --git a/src/plugins/navigation_embeddable/public/components/dashboard_link/dashboard_link_tools.tsx b/src/plugins/navigation_embeddable/public/components/dashboard_link/dashboard_link_tools.tsx
new file mode 100644
index 0000000000000..3735e5a044ffa
--- /dev/null
+++ b/src/plugins/navigation_embeddable/public/components/dashboard_link/dashboard_link_tools.tsx
@@ -0,0 +1,89 @@
+/*
+ * 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 { isEmpty, memoize } from 'lodash';
+import { DashboardItem } from '../../embeddable/types';
+
+import { dashboardServices } from '../../services/kibana_services';
+
+/**
+ * Memoized fetch dashboard will only refetch the dashboard information if the given `dashboardId` changed between
+ * calls; otherwise, it will use the cached dashboard, which may not take into account changes to the dashboard's title
+ * description, etc. Be mindful when choosing the memoized version.
+ */
+export const memoizedFetchDashboard = memoize(
+ async (dashboardId: string) => {
+ return await fetchDashboard(dashboardId);
+ },
+ (dashboardId) => {
+ return dashboardId;
+ }
+);
+
+export const fetchDashboard = async (dashboardId: string): Promise => {
+ const findDashboardsService = await dashboardServices.findDashboardsService();
+ const response = (await findDashboardsService.findByIds([dashboardId]))[0];
+ if (response.status === 'error') {
+ throw new Error('failure'); // TODO: better error handling
+ }
+ return response;
+};
+
+export const memoizedFetchDashboards = memoize(
+ async (search: string = '', size: number = 10, currentDashboardId?: string) => {
+ return await fetchDashboards(search, size, currentDashboardId);
+ },
+ (search, size, currentDashboardId) => {
+ return [search, size, currentDashboardId].join('|');
+ }
+);
+
+const fetchDashboards = async (
+ search: string = '',
+ size: number = 10,
+ currentDashboardId?: string
+): Promise => {
+ const findDashboardsService = await dashboardServices.findDashboardsService();
+ const responses = await findDashboardsService.search({
+ search,
+ size,
+ options: { onlyTitle: true },
+ });
+
+ let currentDashboard: DashboardItem | undefined;
+ let dashboardList: DashboardItem[] = responses.hits;
+
+ /** When the parent dashboard has been saved (i.e. it has an ID) and there is no search string ... */
+ if (currentDashboardId && isEmpty(search)) {
+ /** ...force the current dashboard (if it is present in the original search results) to the top of the list */
+ dashboardList = dashboardList.sort((dashboard) => {
+ const isCurrentDashboard = dashboard.id === currentDashboardId;
+ if (isCurrentDashboard) {
+ currentDashboard = dashboard;
+ }
+ return isCurrentDashboard ? -1 : 1;
+ });
+
+ /**
+ * If the current dashboard wasn't returned in the original search, perform another search to find it and
+ * force it to the front of the list
+ */
+ if (!currentDashboard) {
+ currentDashboard = await fetchDashboard(currentDashboardId);
+ dashboardList.pop(); // the result should still be of `size,` so remove the dashboard at the end of the list
+ dashboardList.unshift(currentDashboard); // in order to force the current dashboard to the start of the list
+ }
+ }
+
+ /** Then, only return the parts of the dashboard object that we need */
+ const simplifiedDashboardList = dashboardList.map((hit) => {
+ return { id: hit.id, attributes: hit.attributes };
+ });
+
+ return simplifiedDashboardList;
+};
diff --git a/src/plugins/navigation_embeddable/public/components/external_link/external_link_component.tsx b/src/plugins/navigation_embeddable/public/components/external_link/external_link_component.tsx
new file mode 100644
index 0000000000000..90bf4066d4c20
--- /dev/null
+++ b/src/plugins/navigation_embeddable/public/components/external_link/external_link_component.tsx
@@ -0,0 +1,24 @@
+/*
+ * 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 { EuiButtonEmpty } from '@elastic/eui';
+import {
+ EXTERNAL_LINK_TYPE,
+ NavigationLinkInfo,
+ NavigationEmbeddableLink,
+} from '../../embeddable/types';
+
+export const ExternalLinkComponent = ({ link }: { link: NavigationEmbeddableLink }) => {
+ return (
+
+ {link.label || link.destination}
+
+ );
+};
diff --git a/src/plugins/navigation_embeddable/public/components/external_link/external_link_destination_picker.tsx b/src/plugins/navigation_embeddable/public/components/external_link/external_link_destination_picker.tsx
new file mode 100644
index 0000000000000..596ce183d696b
--- /dev/null
+++ b/src/plugins/navigation_embeddable/public/components/external_link/external_link_destination_picker.tsx
@@ -0,0 +1,45 @@
+/*
+ * 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, { useState } from 'react';
+import { EuiFieldText } from '@elastic/eui';
+import { ExternalLinkEmbeddableStrings } from './external_link_strings';
+
+// TODO: As part of https://github.com/elastic/kibana/issues/154381, replace this regex URL check with more robust url validation
+const isValidUrl =
+ /^https?:\/\/(?:www.)?[-a-zA-Z0-9@:%._+~#=]{1,256}.[a-zA-Z0-9()]{1,6}\b(?:[-a-zA-Z0-9()@:%_+.~#?&/=]*)$/;
+
+export const ExternalLinkDestinationPicker = ({
+ setDestination,
+ setPlaceholder,
+ currentDestination,
+ ...other
+}: {
+ setDestination: (destination?: string) => void;
+ setPlaceholder: (placeholder?: string) => void;
+ currentDestination?: string;
+}) => {
+ const [validUrl, setValidUrl] = useState(true);
+
+ /* {...other} is needed so all inner elements are treated as part of the form */
+ return (
+
+ {
+ const url = e.target.value;
+ const isValid = isValidUrl.test(url);
+ setValidUrl(isValid);
+ setDestination(isValid ? url : undefined);
+ setPlaceholder(isValid ? url : undefined);
+ }}
+ />
+
+ );
+};
diff --git a/src/plugins/navigation_embeddable/public/components/external_link/external_link_strings.ts b/src/plugins/navigation_embeddable/public/components/external_link/external_link_strings.ts
new file mode 100644
index 0000000000000..77d7b479706b6
--- /dev/null
+++ b/src/plugins/navigation_embeddable/public/components/external_link/external_link_strings.ts
@@ -0,0 +1,24 @@
+/*
+ * 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';
+
+export const ExternalLinkEmbeddableStrings = {
+ getDisplayName: () =>
+ i18n.translate('navigationEmbeddable.externalLink.displayName', {
+ defaultMessage: 'URL',
+ }),
+ getDescription: () =>
+ i18n.translate('navigationEmbeddable.externalLink.description', {
+ defaultMessage: 'Go to URL',
+ }),
+ getPlaceholder: () =>
+ i18n.translate('navigationEmbeddable.externalLink.editor.placeholder', {
+ defaultMessage: 'Enter external URL',
+ }),
+};
diff --git a/src/plugins/navigation_embeddable/public/components/navigation_embeddable.scss b/src/plugins/navigation_embeddable/public/components/navigation_embeddable.scss
new file mode 100644
index 0000000000000..214cbbc7a8760
--- /dev/null
+++ b/src/plugins/navigation_embeddable/public/components/navigation_embeddable.scss
@@ -0,0 +1,23 @@
+@import '../mixins';
+
+.navEmbeddableLinkEditor {
+ @include euiFlyout;
+ animation: euiFlyoutAnimation $euiAnimSpeedNormal $euiAnimSlightResistance;
+
+ .linkEditorBackButton {
+ height: auto;
+ }
+}
+
+.navEmbeddablePanelEditor {
+ .linkText {
+ flex: 1;
+ min-width: 0;
+
+ .wrapText {
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/plugins/navigation_embeddable/public/components/navigation_embeddable_component.tsx b/src/plugins/navigation_embeddable/public/components/navigation_embeddable_component.tsx
new file mode 100644
index 0000000000000..abcbe098f3896
--- /dev/null
+++ b/src/plugins/navigation_embeddable/public/components/navigation_embeddable_component.tsx
@@ -0,0 +1,44 @@
+/*
+ * 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 { EuiPanel } from '@elastic/eui';
+
+import { DASHBOARD_LINK_TYPE } from '../embeddable/types';
+import { useNavigationEmbeddable } from '../embeddable/navigation_embeddable';
+import { DashboardLinkComponent } from './dashboard_link/dashboard_link_component';
+import { ExternalLinkComponent } from './external_link/external_link_component';
+
+export const NavigationEmbeddableComponent = () => {
+ const navEmbeddable = useNavigationEmbeddable();
+
+ const links = navEmbeddable.select((state) => state.explicitInput.links);
+
+ /** TODO: Render this as a list **or** "tabs" as part of https://github.com/elastic/kibana/issues/154357 */
+ return (
+
+ {Object.keys(links).map((linkId) => {
+ return (
+
+ {links[linkId].type === DASHBOARD_LINK_TYPE ? (
+
+ ) : (
+
+ )}
+
+ );
+ })}
+
+ );
+};
diff --git a/src/plugins/navigation_embeddable/public/components/navigation_embeddable_link_editor.tsx b/src/plugins/navigation_embeddable/public/components/navigation_embeddable_link_editor.tsx
new file mode 100644
index 0000000000000..a0753130bcc83
--- /dev/null
+++ b/src/plugins/navigation_embeddable/public/components/navigation_embeddable_link_editor.tsx
@@ -0,0 +1,175 @@
+/*
+ * 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, { useMemo, useState } from 'react';
+
+import {
+ EuiForm,
+ EuiIcon,
+ EuiTitle,
+ EuiButton,
+ EuiFormRow,
+ EuiFlexItem,
+ EuiFieldText,
+ EuiFocusTrap,
+ EuiFlexGroup,
+ EuiRadioGroup,
+ EuiFlyoutBody,
+ EuiButtonEmpty,
+ EuiFlyoutFooter,
+ EuiFlyoutHeader,
+ EuiRadioGroupOption,
+} from '@elastic/eui';
+import { DashboardContainer } from '@kbn/dashboard-plugin/public/dashboard_container';
+
+import {
+ NavigationLinkInfo,
+ NavigationLinkType,
+ EXTERNAL_LINK_TYPE,
+ DASHBOARD_LINK_TYPE,
+ NavigationEmbeddableLink,
+} from '../embeddable/types';
+import { NavEmbeddableStrings } from './navigation_embeddable_strings';
+import { ExternalLinkDestinationPicker } from './external_link/external_link_destination_picker';
+import { DashboardLinkDestinationPicker } from './dashboard_link/dashboard_link_destination_picker';
+
+export const NavigationEmbeddableLinkEditor = ({
+ onSave,
+ onClose,
+ parentDashboard,
+}: {
+ onClose: () => void;
+ parentDashboard?: DashboardContainer;
+ onSave: (newLink: NavigationEmbeddableLink) => void;
+}) => {
+ const [selectedLinkType, setSelectedLinkType] = useState(DASHBOARD_LINK_TYPE);
+ const [linkLabel, setLinkLabel] = useState('');
+ const [linkDestination, setLinkDestination] = useState();
+ const [linkLabelPlaceholder, setLinkLabelPlaceholder] = useState();
+
+ const linkTypes: EuiRadioGroupOption[] = useMemo(() => {
+ return ([DASHBOARD_LINK_TYPE, EXTERNAL_LINK_TYPE] as NavigationLinkType[]).map((type) => {
+ return {
+ id: type,
+ label: (
+
+
+
+
+ {NavigationLinkInfo[type].displayName}
+
+ ),
+ };
+ });
+ }, []);
+
+ return (
+
+
+ onClose()}
+ >
+
+ {NavEmbeddableStrings.editor.getAddButtonLabel()}
+
+
+
+
+
+
+ {
+ setLinkDestination(undefined);
+ setLinkLabelPlaceholder(undefined);
+ setSelectedLinkType(id as NavigationLinkType);
+ }}
+ />
+
+
+
+ {selectedLinkType === DASHBOARD_LINK_TYPE ? (
+
+ ) : (
+
+ )}
+
+
+
+ {
+ setLinkLabel(e.target.value);
+ }}
+ />
+
+
+
+ {/* TODO: As part of https://github.com/elastic/kibana/issues/154381, we should pull in the custom settings for each link type.
+ Refer to `x-pack/examples/ui_actions_enhanced_examples/public/drilldowns/dashboard_to_discover_drilldown/collect_config_container.tsx`
+ for the dashboard drilldown settings, for example.
+
+ Open question: It probably makes sense to re-use these components so any changes made to the drilldown architecture
+ trickle down to the navigation embeddable - this would require some refactoring, though. Is this a goal for MVP?
+ */}
+
+
+
+
+ onClose()} iconType="cross">
+ {NavEmbeddableStrings.editor.getCancelButtonLabel()}
+
+
+
+ {
+ // this check should always be true, since the button is disabled otherwise - this is just for type safety
+ if (linkDestination) {
+ onSave({
+ destination: linkDestination,
+ label: linkLabel,
+ type: selectedLinkType,
+ });
+ onClose();
+ }
+ }}
+ >
+ {NavEmbeddableStrings.editor.getAddButtonLabel()}
+
+
+
+
+
+ );
+};
diff --git a/src/plugins/navigation_embeddable/public/components/navigation_embeddable_panel_editor.tsx b/src/plugins/navigation_embeddable/public/components/navigation_embeddable_panel_editor.tsx
new file mode 100644
index 0000000000000..ab24c998feab0
--- /dev/null
+++ b/src/plugins/navigation_embeddable/public/components/navigation_embeddable_panel_editor.tsx
@@ -0,0 +1,192 @@
+/*
+ * 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 { isEmpty } from 'lodash';
+import { v4 as uuidv4 } from 'uuid';
+import React, { useState } from 'react';
+import useAsync from 'react-use/lib/useAsync';
+
+import {
+ EuiText,
+ EuiIcon,
+ EuiForm,
+ EuiTitle,
+ EuiPanel,
+ IconType,
+ EuiSpacer,
+ EuiButton,
+ EuiFormRow,
+ EuiFlexItem,
+ EuiFlexGroup,
+ EuiFlyoutBody,
+ EuiButtonEmpty,
+ EuiFlyoutFooter,
+ EuiFlyoutHeader,
+} from '@elastic/eui';
+import { DashboardContainer } from '@kbn/dashboard-plugin/public/dashboard_container';
+
+import {
+ DASHBOARD_LINK_TYPE,
+ EXTERNAL_LINK_TYPE,
+ NavigationEmbeddableInput,
+ NavigationEmbeddableLink,
+ NavigationLinkInfo,
+} from '../embeddable/types';
+import { NavEmbeddableStrings } from './navigation_embeddable_strings';
+import { memoizedFetchDashboard } from './dashboard_link/dashboard_link_tools';
+import { NavigationEmbeddableLinkEditor } from './navigation_embeddable_link_editor';
+
+import './navigation_embeddable.scss';
+
+export const NavigationEmbeddablePanelEditor = ({
+ onSave,
+ onClose,
+ initialInput,
+ parentDashboard,
+}: {
+ onClose: () => void;
+ initialInput: Partial;
+ onSave: (input: Partial) => void;
+ parentDashboard?: DashboardContainer;
+}) => {
+ const [showLinkEditorFlyout, setShowLinkEditorFlyout] = useState(false);
+ const [links, setLinks] = useState(initialInput.links);
+
+ /**
+ * TODO: There is probably a more efficient way of storing the dashboard information "temporarily" for any new
+ * panels and only fetching the dashboard saved objects when first loading this flyout.
+ *
+ * Will need to think this through and fix as part of the editing process - not worth holding this PR, since it's
+ * blocking so much other work :)
+ */
+ const { value: linkList } = useAsync(async () => {
+ if (!links || isEmpty(links)) return [];
+
+ const newLinks: Array<{ id: string; icon: IconType; label: string }> = await Promise.all(
+ Object.keys(links).map(async (panelId) => {
+ let label = links[panelId].label;
+ let icon = NavigationLinkInfo[EXTERNAL_LINK_TYPE].icon;
+
+ if (links[panelId].type === DASHBOARD_LINK_TYPE) {
+ icon = NavigationLinkInfo[DASHBOARD_LINK_TYPE].icon;
+ if (!label) {
+ const dashboard = await memoizedFetchDashboard(links[panelId].destination);
+ label = dashboard.attributes.title;
+ }
+ }
+
+ return { id: panelId, label: label || links[panelId].destination, icon };
+ })
+ );
+ return newLinks;
+ }, [links]);
+
+ return (
+ <>
+
+
+ {NavEmbeddableStrings.editor.panelEditor.getCreateFlyoutTitle()}
+
+
+
+
+
+ <>
+ {!links || Object.keys(links).length === 0 ? (
+
+
+
+
+ {NavEmbeddableStrings.editor.panelEditor.getEmptyLinksMessage()}
+
+
+
+
+
+
+ setShowLinkEditorFlyout(true)}
+ iconType="plusInCircle"
+ >
+ {NavEmbeddableStrings.editor.getAddButtonLabel()}
+
+
+
+
+ ) : (
+ <>
+ {linkList?.map((link) => {
+ return (
+
+
+
+
+
+
+
+ {link.label}
+
+
+
+
+
+ );
+ })}
+ setShowLinkEditorFlyout(true)}
+ >
+ {NavEmbeddableStrings.editor.getAddButtonLabel()}
+
+ >
+ )}
+ >
+
+
+
+
+
+
+
+ {NavEmbeddableStrings.editor.getCancelButtonLabel()}
+
+
+
+ {
+ onSave({ ...initialInput, links });
+ onClose();
+ }}
+ >
+ {NavEmbeddableStrings.editor.panelEditor.getSaveButtonLabel()}
+
+
+
+
+
+ {showLinkEditorFlyout && (
+ {
+ setShowLinkEditorFlyout(false);
+ }}
+ onSave={(newLink: NavigationEmbeddableLink) => {
+ setLinks({ ...links, [uuidv4()]: newLink });
+ }}
+ parentDashboard={parentDashboard}
+ />
+ )}
+ >
+ );
+};
diff --git a/src/plugins/navigation_embeddable/public/components/navigation_embeddable_strings.ts b/src/plugins/navigation_embeddable/public/components/navigation_embeddable_strings.ts
new file mode 100644
index 0000000000000..28faa11a00d23
--- /dev/null
+++ b/src/plugins/navigation_embeddable/public/components/navigation_embeddable_strings.ts
@@ -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';
+
+export const NavEmbeddableStrings = {
+ editor: {
+ getAddButtonLabel: () =>
+ i18n.translate('navigationEmbeddable.editor.addButtonLabel', {
+ defaultMessage: 'Add link',
+ }),
+ getCancelButtonLabel: () =>
+ i18n.translate('navigationEmbeddable.editor.cancelButtonLabel', {
+ defaultMessage: 'Close',
+ }),
+ panelEditor: {
+ getEmptyLinksMessage: () =>
+ i18n.translate('navigationEmbeddable.panelEditor.emptyLinksMessage', {
+ defaultMessage: "You haven't added any links yet.",
+ }),
+ getCreateFlyoutTitle: () =>
+ i18n.translate('navigationEmbeddable.panelEditor.createFlyoutTitle', {
+ defaultMessage: 'Create links panel',
+ }),
+ getSaveButtonLabel: () =>
+ i18n.translate('navigationEmbeddable.panelEditor.saveButtonLabel', {
+ defaultMessage: 'Save',
+ }),
+ },
+ linkEditor: {
+ getGoBackAriaLabel: () =>
+ i18n.translate('navigationEmbeddable.linkEditor.goBackAriaLabel', {
+ defaultMessage: 'Go back to panel editor.',
+ }),
+ getLinkTypePickerLabel: () =>
+ i18n.translate('navigationEmbeddable.linkEditor.linkTypeFormLabel', {
+ defaultMessage: 'Go to',
+ }),
+ getLinkDestinationLabel: () =>
+ i18n.translate('navigationEmbeddable.linkEditor.linkDestinationLabel', {
+ defaultMessage: 'Choose destination',
+ }),
+ getLinkTextLabel: () =>
+ i18n.translate('navigationEmbeddable.linkEditor.linkTextLabel', {
+ defaultMessage: 'Text (optional)',
+ }),
+ getLinkTextPlaceholder: () =>
+ i18n.translate('navigationEmbeddable.linkEditor.linkTextPlaceholder', {
+ defaultMessage: 'Enter text for link',
+ }),
+ },
+ },
+};
diff --git a/src/plugins/navigation_embeddable/public/editor/open_editor_flyout.tsx b/src/plugins/navigation_embeddable/public/editor/open_editor_flyout.tsx
new file mode 100644
index 0000000000000..e47ed639f501d
--- /dev/null
+++ b/src/plugins/navigation_embeddable/public/editor/open_editor_flyout.tsx
@@ -0,0 +1,70 @@
+/*
+ * 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 { Subject } from 'rxjs';
+import { skip, take, takeUntil } from 'rxjs/operators';
+
+import { toMountPoint } from '@kbn/kibana-react-plugin/public';
+import { DashboardContainer } from '@kbn/dashboard-plugin/public/dashboard_container';
+
+import { coreServices } from '../services/kibana_services';
+import { NavigationEmbeddableInput } from '../embeddable/types';
+import { NavigationEmbeddablePanelEditor } from '../components/navigation_embeddable_panel_editor';
+
+/**
+ * @throws in case user cancels
+ */
+export async function openEditorFlyout(
+ initialInput?: Omit,
+ parentDashboard?: DashboardContainer
+): Promise> {
+ return new Promise((resolve, reject) => {
+ const closed$ = new Subject();
+
+ const onSave = (partialInput: Partial) => {
+ resolve(partialInput);
+ editorFlyout.close();
+ };
+
+ const onCancel = () => {
+ reject();
+ editorFlyout.close();
+ };
+
+ // Close the flyout whenever the breadcrumbs change - i.e. when the dashboard's title changes, or when
+ // the user navigates away from the given dashboard (to the listing page **or** to another app), etc.
+ coreServices.chrome
+ .getBreadcrumbs$()
+ .pipe(takeUntil(closed$), skip(1), take(1))
+ .subscribe(() => {
+ editorFlyout.close();
+ });
+
+ const editorFlyout = coreServices.overlays.openFlyout(
+ toMountPoint(
+ ,
+ { theme$: coreServices.theme.theme$ }
+ ),
+ {
+ ownFocus: true,
+ outsideClickCloses: false,
+ onClose: onCancel,
+ }
+ );
+
+ editorFlyout.onClose.then(() => {
+ closed$.next(true);
+ });
+ });
+}
diff --git a/src/plugins/navigation_embeddable/public/navigation_embeddable/index.ts b/src/plugins/navigation_embeddable/public/embeddable/index.ts
similarity index 81%
rename from src/plugins/navigation_embeddable/public/navigation_embeddable/index.ts
rename to src/plugins/navigation_embeddable/public/embeddable/index.ts
index 1676f521ceb84..12c60f3ebd004 100644
--- a/src/plugins/navigation_embeddable/public/navigation_embeddable/index.ts
+++ b/src/plugins/navigation_embeddable/public/embeddable/index.ts
@@ -6,6 +6,9 @@
* Side Public License, v 1.
*/
-export { NAVIGATION_EMBEDDABLE_TYPE, NavigationEmbeddable } from './navigation_embeddable';
+export {
+ NAVIGATION_EMBEDDABLE_TYPE,
+ NavigationEmbeddable as NavigationEmbeddable,
+} from './navigation_embeddable';
export type { NavigationEmbeddableFactory } from './navigation_embeddable_factory';
export { NavigationEmbeddableFactoryDefinition } from './navigation_embeddable_factory';
diff --git a/src/plugins/navigation_embeddable/public/embeddable/navigation_embeddable.tsx b/src/plugins/navigation_embeddable/public/embeddable/navigation_embeddable.tsx
new file mode 100644
index 0000000000000..9f3194d0b376c
--- /dev/null
+++ b/src/plugins/navigation_embeddable/public/embeddable/navigation_embeddable.tsx
@@ -0,0 +1,97 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import React, { createContext, useContext } from 'react';
+
+import { Embeddable, EmbeddableOutput } from '@kbn/embeddable-plugin/public';
+import { DashboardContainer } from '@kbn/dashboard-plugin/public/dashboard_container';
+import { ReduxEmbeddableTools, ReduxToolsPackage } from '@kbn/presentation-util-plugin/public';
+
+import { navigationEmbeddableReducers } from './navigation_embeddable_reducers';
+import { NavigationEmbeddableInput, NavigationEmbeddableReduxState } from './types';
+import { NavigationEmbeddableComponent } from '../components/navigation_embeddable_component';
+
+export const NAVIGATION_EMBEDDABLE_TYPE = 'navigation';
+
+export const NavigationEmbeddableContext = createContext(null);
+export const useNavigationEmbeddable = (): NavigationEmbeddable => {
+ const navigation = useContext(NavigationEmbeddableContext);
+ if (navigation == null) {
+ throw new Error('useNavigation must be used inside NavigationEmbeddableContext.');
+ }
+ return navigation!;
+};
+
+type NavigationReduxEmbeddableTools = ReduxEmbeddableTools<
+ NavigationEmbeddableReduxState,
+ typeof navigationEmbeddableReducers
+>;
+
+export interface NavigationEmbeddableConfig {
+ editable: boolean;
+}
+
+export class NavigationEmbeddable extends Embeddable {
+ public readonly type = NAVIGATION_EMBEDDABLE_TYPE;
+
+ // state management
+ public select: NavigationReduxEmbeddableTools['select'];
+ public getState: NavigationReduxEmbeddableTools['getState'];
+ public dispatch: NavigationReduxEmbeddableTools['dispatch'];
+ public onStateChange: NavigationReduxEmbeddableTools['onStateChange'];
+
+ private cleanupStateTools: () => void;
+
+ constructor(
+ reduxToolsPackage: ReduxToolsPackage,
+ config: NavigationEmbeddableConfig,
+ initialInput: NavigationEmbeddableInput,
+ parent?: DashboardContainer
+ ) {
+ super(
+ initialInput,
+ {
+ editable: config.editable,
+ editableWithExplicitInput: true,
+ },
+ parent
+ );
+
+ /** Build redux embeddable tools */
+ const reduxEmbeddableTools = reduxToolsPackage.createReduxEmbeddableTools<
+ NavigationEmbeddableReduxState,
+ typeof navigationEmbeddableReducers
+ >({
+ embeddable: this,
+ reducers: navigationEmbeddableReducers,
+ initialComponentState: {},
+ });
+
+ this.select = reduxEmbeddableTools.select;
+ this.getState = reduxEmbeddableTools.getState;
+ this.dispatch = reduxEmbeddableTools.dispatch;
+ this.cleanupStateTools = reduxEmbeddableTools.cleanup;
+ this.onStateChange = reduxEmbeddableTools.onStateChange;
+ this.setInitializationFinished();
+ }
+
+ public async reload() {}
+
+ public destroy() {
+ super.destroy();
+ this.cleanupStateTools();
+ }
+
+ public render() {
+ return (
+
+
+
+ );
+ }
+}
diff --git a/src/plugins/navigation_embeddable/public/embeddable/navigation_embeddable_factory.ts b/src/plugins/navigation_embeddable/public/embeddable/navigation_embeddable_factory.ts
new file mode 100644
index 0000000000000..8f9985b687665
--- /dev/null
+++ b/src/plugins/navigation_embeddable/public/embeddable/navigation_embeddable_factory.ts
@@ -0,0 +1,102 @@
+/*
+ * 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 { isEmpty } from 'lodash';
+
+import { i18n } from '@kbn/i18n';
+import {
+ ACTION_ADD_PANEL,
+ EmbeddableFactory,
+ EmbeddableFactoryDefinition,
+} from '@kbn/embeddable-plugin/public';
+import { lazyLoadReduxToolsPackage } from '@kbn/presentation-util-plugin/public';
+import { DashboardContainer } from '@kbn/dashboard-plugin/public/dashboard_container';
+
+import { NavigationEmbeddableInput } from './types';
+import { NAVIGATION_EMBEDDABLE_TYPE } from './navigation_embeddable';
+import { coreServices, untilPluginStartServicesReady } from '../services/kibana_services';
+
+export type NavigationEmbeddableFactory = EmbeddableFactory;
+
+// TODO: Replace string 'OPEN_FLYOUT_ADD_DRILLDOWN' with constant as part of https://github.com/elastic/kibana/issues/154381
+const getDefaultNavigationEmbeddableInput = (): Omit => ({
+ links: {},
+ disabledActions: [ACTION_ADD_PANEL, 'OPEN_FLYOUT_ADD_DRILLDOWN'],
+});
+
+export class NavigationEmbeddableFactoryDefinition
+ implements EmbeddableFactoryDefinition
+{
+ public readonly type = NAVIGATION_EMBEDDABLE_TYPE;
+
+ public isContainerType = false;
+
+ public async isEditable() {
+ await untilPluginStartServicesReady();
+ return Boolean(coreServices.application.capabilities.dashboard?.showWriteControls);
+ }
+
+ public canCreateNew() {
+ return true;
+ }
+
+ public getDefaultInput(): Partial {
+ return getDefaultNavigationEmbeddableInput();
+ }
+
+ public async create(initialInput: NavigationEmbeddableInput, parent: DashboardContainer) {
+ if (!initialInput.links || isEmpty(initialInput.links)) {
+ // don't create an empty navigation embeddable - it should always have at least one link
+ return;
+ }
+
+ await untilPluginStartServicesReady();
+
+ const reduxEmbeddablePackage = await lazyLoadReduxToolsPackage();
+ const { NavigationEmbeddable } = await import('./navigation_embeddable');
+ const editable = await this.isEditable();
+
+ return new NavigationEmbeddable(
+ reduxEmbeddablePackage,
+ { editable },
+ { ...getDefaultNavigationEmbeddableInput(), ...initialInput },
+ parent
+ );
+ }
+
+ public async getExplicitInput(
+ initialInput?: NavigationEmbeddableInput,
+ parent?: DashboardContainer
+ ) {
+ if (!parent) return {};
+
+ const { openEditorFlyout: createNavigationEmbeddable } = await import(
+ '../editor/open_editor_flyout'
+ );
+
+ const input = await createNavigationEmbeddable(
+ { ...getDefaultNavigationEmbeddableInput(), ...initialInput },
+ parent
+ ).catch(() => {
+ // swallow the promise rejection that happens when the flyout is closed
+ return {};
+ });
+
+ return input;
+ }
+
+ public getDisplayName() {
+ return i18n.translate('navigationEmbeddable.navigationEmbeddableFactory.displayName', {
+ defaultMessage: 'Links',
+ });
+ }
+
+ public getIconType() {
+ return 'link';
+ }
+}
diff --git a/src/plugins/navigation_embeddable/public/embeddable/navigation_embeddable_reducers.ts b/src/plugins/navigation_embeddable/public/embeddable/navigation_embeddable_reducers.ts
new file mode 100644
index 0000000000000..29a79c4f6154f
--- /dev/null
+++ b/src/plugins/navigation_embeddable/public/embeddable/navigation_embeddable_reducers.ts
@@ -0,0 +1,27 @@
+/*
+ * 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 { WritableDraft } from 'immer/dist/types/types-external';
+
+import { PayloadAction } from '@reduxjs/toolkit';
+
+import { NavigationEmbeddableReduxState } from './types';
+
+export const navigationEmbeddableReducers = {
+ /**
+ * TODO: Right now, we aren't using any reducers - but, I'm keeping this here as a draft
+ * just in case we need them later on. As a final cleanup, we could remove this if we never
+ * end up using reducers
+ */
+ setLoading: (
+ state: WritableDraft,
+ action: PayloadAction
+ ) => {
+ state.output.loading = action.payload;
+ },
+};
diff --git a/src/plugins/navigation_embeddable/public/embeddable/types.ts b/src/plugins/navigation_embeddable/public/embeddable/types.ts
new file mode 100644
index 0000000000000..40dd5901db948
--- /dev/null
+++ b/src/plugins/navigation_embeddable/public/embeddable/types.ts
@@ -0,0 +1,70 @@
+/*
+ * 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 { DashboardAttributes } from '@kbn/dashboard-plugin/common';
+import { ReduxEmbeddableState } from '@kbn/presentation-util-plugin/public';
+import { EmbeddableInput, EmbeddableOutput } from '@kbn/embeddable-plugin/public';
+
+import { ExternalLinkEmbeddableStrings } from '../components/external_link/external_link_strings';
+import { DashboardLinkEmbeddableStrings } from '../components/dashboard_link/dashboard_link_strings';
+
+/**
+ * Dashboard to dashboard links
+ */
+export const DASHBOARD_LINK_TYPE = 'dashboardLink';
+export interface DashboardItem {
+ id: string;
+ attributes: DashboardAttributes;
+}
+
+/**
+ * External URL links
+ */
+export const EXTERNAL_LINK_TYPE = 'externalLink';
+
+/**
+ * Navigation embeddable explicit input
+ */
+export type NavigationLinkType = typeof DASHBOARD_LINK_TYPE | typeof EXTERNAL_LINK_TYPE;
+
+export interface NavigationEmbeddableLink {
+ type: NavigationLinkType;
+ destination: string;
+ // order: number; TODO: Use this as part of https://github.com/elastic/kibana/issues/154361
+ label?: string;
+}
+
+export interface NavigationEmbeddableInput extends EmbeddableInput {
+ links: { [id: string]: NavigationEmbeddableLink };
+}
+
+export const NavigationLinkInfo: {
+ [id in NavigationLinkType]: { icon: string; displayName: string; description: string };
+} = {
+ [DASHBOARD_LINK_TYPE]: {
+ icon: 'dashboardApp',
+ displayName: DashboardLinkEmbeddableStrings.getDisplayName(),
+ description: DashboardLinkEmbeddableStrings.getDescription(),
+ },
+ [EXTERNAL_LINK_TYPE]: {
+ icon: 'link',
+ displayName: ExternalLinkEmbeddableStrings.getDisplayName(),
+ description: ExternalLinkEmbeddableStrings.getDescription(),
+ },
+};
+
+/**
+ * Navigation embeddable redux state
+ */
+// export interface NavigationEmbeddableComponentState {} // TODO: Uncomment this if we end up needing component state
+
+export type NavigationEmbeddableReduxState = ReduxEmbeddableState<
+ NavigationEmbeddableInput,
+ EmbeddableOutput,
+ {} // We currently don't have any component state - TODO: Replace with `NavigationEmbeddableComponentState` if necessary
+>;
diff --git a/src/plugins/navigation_embeddable/public/index.ts b/src/plugins/navigation_embeddable/public/index.ts
index 5e8be17c8958b..9cdbcfcc6c667 100644
--- a/src/plugins/navigation_embeddable/public/index.ts
+++ b/src/plugins/navigation_embeddable/public/index.ts
@@ -6,12 +6,12 @@
* Side Public License, v 1.
*/
-export type { NavigationEmbeddableFactory } from './navigation_embeddable';
+export type { NavigationEmbeddableFactory } from './embeddable';
export {
NAVIGATION_EMBEDDABLE_TYPE,
NavigationEmbeddableFactoryDefinition,
NavigationEmbeddable,
-} from './navigation_embeddable';
+} from './embeddable';
import { NavigationEmbeddablePlugin } from './plugin';
diff --git a/src/plugins/navigation_embeddable/public/navigation_embeddable/navigation_embeddable.tsx b/src/plugins/navigation_embeddable/public/navigation_embeddable/navigation_embeddable.tsx
deleted file mode 100644
index 2c66f4b655fac..0000000000000
--- a/src/plugins/navigation_embeddable/public/navigation_embeddable/navigation_embeddable.tsx
+++ /dev/null
@@ -1,32 +0,0 @@
-/*
- * 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 { EuiTitle } from '@elastic/eui';
-import { Embeddable } from '@kbn/embeddable-plugin/public';
-import type { EmbeddableInput, IContainer } from '@kbn/embeddable-plugin/public';
-
-export const NAVIGATION_EMBEDDABLE_TYPE = 'navigation';
-
-export class NavigationEmbeddable extends Embeddable {
- public readonly type = NAVIGATION_EMBEDDABLE_TYPE;
-
- constructor(initialInput: EmbeddableInput, parent?: IContainer) {
- super(initialInput, {}, parent);
- }
-
- public render(el: HTMLElement) {
- return (
-
- Call me Magellan, cuz I'm a navigator!
-
- );
- }
-
- public reload() {}
-}
diff --git a/src/plugins/navigation_embeddable/public/navigation_embeddable/navigation_embeddable_factory.ts b/src/plugins/navigation_embeddable/public/navigation_embeddable/navigation_embeddable_factory.ts
deleted file mode 100644
index 54f8ecbb9f892..0000000000000
--- a/src/plugins/navigation_embeddable/public/navigation_embeddable/navigation_embeddable_factory.ts
+++ /dev/null
@@ -1,36 +0,0 @@
-/*
- * 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 { EmbeddableInput, IContainer } from '@kbn/embeddable-plugin/public';
-import { EmbeddableFactory, EmbeddableFactoryDefinition } from '@kbn/embeddable-plugin/public';
-import { NavigationEmbeddable, NAVIGATION_EMBEDDABLE_TYPE } from './navigation_embeddable';
-
-export type NavigationEmbeddableFactory = EmbeddableFactory;
-
-export class NavigationEmbeddableFactoryDefinition implements EmbeddableFactoryDefinition {
- public readonly type = NAVIGATION_EMBEDDABLE_TYPE;
-
- public async isEditable() {
- return true;
- }
-
- public async create(initialInput: EmbeddableInput, parent?: IContainer) {
- return new NavigationEmbeddable(initialInput, parent);
- }
-
- public getDisplayName() {
- return i18n.translate('navigationEmbeddable.navigationEmbeddableFactory.displayName', {
- defaultMessage: 'Navigation',
- });
- }
-
- public getIconType() {
- return 'link';
- }
-}
diff --git a/src/plugins/navigation_embeddable/public/plugin.ts b/src/plugins/navigation_embeddable/public/plugin.ts
index 23dff1f5d1eb8..7a969c0298f7a 100644
--- a/src/plugins/navigation_embeddable/public/plugin.ts
+++ b/src/plugins/navigation_embeddable/public/plugin.ts
@@ -6,33 +6,48 @@
* Side Public License, v 1.
*/
+import { DashboardStart } from '@kbn/dashboard-plugin/public';
import { CoreSetup, CoreStart, Plugin } from '@kbn/core/public';
import { EmbeddableSetup, EmbeddableStart } from '@kbn/embeddable-plugin/public';
-import { NAVIGATION_EMBEDDABLE_TYPE } from './navigation_embeddable';
-import { NavigationEmbeddableFactoryDefinition } from './navigation_embeddable';
-export interface SetupDependencies {
+import { NAVIGATION_EMBEDDABLE_TYPE } from './embeddable';
+import { setKibanaServices } from './services/kibana_services';
+import { NavigationEmbeddableFactoryDefinition } from './embeddable';
+
+export interface NavigationEmbeddableSetupDependencies {
embeddable: EmbeddableSetup;
}
-export interface StartDependencies {
+export interface NavigationEmbeddableStartDependencies {
embeddable: EmbeddableStart;
+ dashboard: DashboardStart;
}
export class NavigationEmbeddablePlugin
- implements Plugin
+ implements
+ Plugin<
+ void,
+ void,
+ NavigationEmbeddableSetupDependencies,
+ NavigationEmbeddableStartDependencies
+ >
{
constructor() {}
- public setup(core: CoreSetup, plugins: SetupDependencies) {
- plugins.embeddable.registerEmbeddableFactory(
- NAVIGATION_EMBEDDABLE_TYPE,
- new NavigationEmbeddableFactoryDefinition()
- );
+ public setup(
+ core: CoreSetup,
+ plugins: NavigationEmbeddableSetupDependencies
+ ) {
+ core.getStartServices().then(([_, deps]) => {
+ plugins.embeddable.registerEmbeddableFactory(
+ NAVIGATION_EMBEDDABLE_TYPE,
+ new NavigationEmbeddableFactoryDefinition()
+ );
+ });
}
- public start(core: CoreStart, plugins: StartDependencies) {
- return {};
+ public start(core: CoreStart, plugins: NavigationEmbeddableStartDependencies) {
+ setKibanaServices(core, plugins);
}
public stop() {}
diff --git a/src/plugins/navigation_embeddable/public/services/kibana_services.ts b/src/plugins/navigation_embeddable/public/services/kibana_services.ts
new file mode 100644
index 0000000000000..710c6227a3568
--- /dev/null
+++ b/src/plugins/navigation_embeddable/public/services/kibana_services.ts
@@ -0,0 +1,41 @@
+/*
+ * 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 { BehaviorSubject } from 'rxjs';
+
+import { CoreStart } from '@kbn/core/public';
+import { DashboardStart } from '@kbn/dashboard-plugin/public';
+
+import { NavigationEmbeddableStartDependencies } from '../plugin';
+
+export let coreServices: CoreStart;
+export let dashboardServices: DashboardStart;
+
+const servicesReady$ = new BehaviorSubject(false);
+
+export const untilPluginStartServicesReady = () => {
+ if (servicesReady$.value) return Promise.resolve();
+ return new Promise((resolve) => {
+ const subscription = servicesReady$.subscribe((isInitialized) => {
+ if (isInitialized) {
+ subscription.unsubscribe();
+ resolve();
+ }
+ });
+ });
+};
+
+export const setKibanaServices = (
+ kibanaCore: CoreStart,
+ deps: NavigationEmbeddableStartDependencies
+) => {
+ coreServices = kibanaCore;
+ dashboardServices = deps.dashboard;
+
+ servicesReady$.next(true);
+};
diff --git a/src/plugins/navigation_embeddable/tsconfig.json b/src/plugins/navigation_embeddable/tsconfig.json
index 05ce2326f9768..a7ea3f209f7ad 100644
--- a/src/plugins/navigation_embeddable/tsconfig.json
+++ b/src/plugins/navigation_embeddable/tsconfig.json
@@ -1,15 +1,16 @@
{
"extends": "../../../tsconfig.base.json",
"compilerOptions": {
- "outDir": "target/types",
+ "outDir": "target/types"
},
- "include": ["public/**/*", "common/**/*", "server/**/*"],
+ "include": ["public/**/*", "common/**/*", "server/**/*", "public/**/*.json"],
"kbn_references": [
"@kbn/core",
- "@kbn/embeddable-plugin",
"@kbn/i18n",
+ "@kbn/dashboard-plugin",
+ "@kbn/embeddable-plugin",
+ "@kbn/kibana-react-plugin",
+ "@kbn/presentation-util-plugin",
],
- "exclude": [
- "target/**/*",
- ]
+ "exclude": ["target/**/*"]
}
From 92809805e8eef672ce79e463859dfc6f9b60d1e5 Mon Sep 17 00:00:00 2001
From: Andrea Del Rio
Date: Fri, 14 Jul 2023 10:54:54 -0700
Subject: [PATCH 05/53] [Dashboard Navigation] Improve empty state (#161605)
Closes https://github.com/elastic/kibana/issues/161552
## Summary
- Adds an illustration to the empty state in both light and dark mode.
- Updates copy as suggested by Gail.
### Updated design
### Checklist
Delete any items that are not applicable to this PR.
- [ ] Any text added follows [EUI's writing
guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses
sentence case text and includes [i18n
support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md)
- [ ]
[Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html)
was added for features that require explanation or tutorials
- [ ] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios
- [ ] Any UI touched in this PR is usable by keyboard only (learn more
about [keyboard accessibility](https://webaim.org/techniques/keyboard/))
- [ ] Any UI touched in this PR does not create any new axe failures
(run axe in browser:
[FF](https://addons.mozilla.org/en-US/firefox/addon/axe-devtools/),
[Chrome](https://chrome.google.com/webstore/detail/axe-web-accessibility-tes/lhdoppojpmngadmnindnejefpokejbdd?hl=en-US))
- [ ] If a plugin configuration key changed, check if it needs to be
allowlisted in the cloud and added to the [docker
list](https://github.com/elastic/kibana/blob/main/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker)
- [ ] This renders correctly on smaller devices using a responsive
layout. (You can test this [in your
browser](https://www.browserstack.com/guide/responsive-testing-on-local-server))
- [ ] This was checked for [cross-browser
compatibility](https://www.elastic.co/support/matrix#matrix_browsers)
### Risk Matrix
Delete this section if it is not applicable to this PR.
Before closing this PR, invite QA, stakeholders, and other developers to
identify risks that should be tested prior to the change/feature
release.
When forming the risk matrix, consider some of the following examples
and how they may potentially impact the change:
| Risk | Probability | Severity | Mitigation/Notes |
|---------------------------|-------------|----------|-------------------------|
| Multiple Spaces—unexpected behavior in non-default Kibana Space.
| Low | High | Integration tests will verify that all features are still
supported in non-default Kibana Space and when user switches between
spaces. |
| Multiple nodes—Elasticsearch polling might have race conditions
when multiple Kibana nodes are polling for the same tasks. | High | Low
| Tasks are idempotent, so executing them multiple times will not result
in logical error, but will degrade performance. To test for this case we
add plenty of unit tests around this logic and document manual testing
procedure. |
| Code should gracefully handle cases when feature X or plugin Y are
disabled. | Medium | High | Unit tests will verify that any feature flag
or plugin combination still results in our service operational. |
| [See more potential risk
examples](https://github.com/elastic/kibana/blob/main/RISK_MATRIX.mdx) |
### For maintainers
- [ ] This was checked for breaking API changes and was [labeled
appropriately](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process)
---
.../public/assets/empty_links_dark.svg | 1 +
.../public/assets/empty_links_light.svg | 1 +
.../navigation_embeddable_panel_editor.tsx | 56 ++++++++++++-------
.../navigation_embeddable_strings.ts | 2 +-
4 files changed, 38 insertions(+), 22 deletions(-)
create mode 100644 src/plugins/navigation_embeddable/public/assets/empty_links_dark.svg
create mode 100644 src/plugins/navigation_embeddable/public/assets/empty_links_light.svg
diff --git a/src/plugins/navigation_embeddable/public/assets/empty_links_dark.svg b/src/plugins/navigation_embeddable/public/assets/empty_links_dark.svg
new file mode 100644
index 0000000000000..6540d66a39419
--- /dev/null
+++ b/src/plugins/navigation_embeddable/public/assets/empty_links_dark.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/plugins/navigation_embeddable/public/assets/empty_links_light.svg b/src/plugins/navigation_embeddable/public/assets/empty_links_light.svg
new file mode 100644
index 0000000000000..5bd8a7f8d5878
--- /dev/null
+++ b/src/plugins/navigation_embeddable/public/assets/empty_links_light.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/plugins/navigation_embeddable/public/components/navigation_embeddable_panel_editor.tsx b/src/plugins/navigation_embeddable/public/components/navigation_embeddable_panel_editor.tsx
index ab24c998feab0..1c117048fcb23 100644
--- a/src/plugins/navigation_embeddable/public/components/navigation_embeddable_panel_editor.tsx
+++ b/src/plugins/navigation_embeddable/public/components/navigation_embeddable_panel_editor.tsx
@@ -10,7 +10,7 @@ import { isEmpty } from 'lodash';
import { v4 as uuidv4 } from 'uuid';
import React, { useState } from 'react';
import useAsync from 'react-use/lib/useAsync';
-
+import useObservable from 'react-use/lib/useObservable';
import {
EuiText,
EuiIcon,
@@ -27,9 +27,11 @@ import {
EuiButtonEmpty,
EuiFlyoutFooter,
EuiFlyoutHeader,
+ EuiImage,
+ EuiEmptyPrompt,
} from '@elastic/eui';
import { DashboardContainer } from '@kbn/dashboard-plugin/public/dashboard_container';
-
+import { coreServices } from '../services/kibana_services';
import {
DASHBOARD_LINK_TYPE,
EXTERNAL_LINK_TYPE,
@@ -40,6 +42,8 @@ import {
import { NavEmbeddableStrings } from './navigation_embeddable_strings';
import { memoizedFetchDashboard } from './dashboard_link/dashboard_link_tools';
import { NavigationEmbeddableLinkEditor } from './navigation_embeddable_link_editor';
+import noLinksIllustrationDark from '../assets/empty_links_dark.svg';
+import noLinksIllustrationLight from '../assets/empty_links_light.svg';
import './navigation_embeddable.scss';
@@ -56,6 +60,7 @@ export const NavigationEmbeddablePanelEditor = ({
}) => {
const [showLinkEditorFlyout, setShowLinkEditorFlyout] = useState(false);
const [links, setLinks] = useState(initialInput.links);
+ const isDarkTheme = useObservable(coreServices.theme.theme$)?.darkMode;
/**
* TODO: There is probably a more efficient way of storing the dashboard information "temporarily" for any new
@@ -98,25 +103,34 @@ export const NavigationEmbeddablePanelEditor = ({
<>
{!links || Object.keys(links).length === 0 ? (
-
-
-
-
- {NavEmbeddableStrings.editor.panelEditor.getEmptyLinksMessage()}
-
-
-
-
-
-
- setShowLinkEditorFlyout(true)}
- iconType="plusInCircle"
- >
- {NavEmbeddableStrings.editor.getAddButtonLabel()}
-
-
-
+
+
+ }
+ body={
+ <>
+
+ {NavEmbeddableStrings.editor.panelEditor.getEmptyLinksMessage()}
+
+
+ setShowLinkEditorFlyout(true)}
+ iconType="plusInCircle"
+ >
+ {NavEmbeddableStrings.editor.getAddButtonLabel()}
+
+ >
+ }
+ />
) : (
<>
diff --git a/src/plugins/navigation_embeddable/public/components/navigation_embeddable_strings.ts b/src/plugins/navigation_embeddable/public/components/navigation_embeddable_strings.ts
index 28faa11a00d23..7499eda011509 100644
--- a/src/plugins/navigation_embeddable/public/components/navigation_embeddable_strings.ts
+++ b/src/plugins/navigation_embeddable/public/components/navigation_embeddable_strings.ts
@@ -21,7 +21,7 @@ export const NavEmbeddableStrings = {
panelEditor: {
getEmptyLinksMessage: () =>
i18n.translate('navigationEmbeddable.panelEditor.emptyLinksMessage', {
- defaultMessage: "You haven't added any links yet.",
+ defaultMessage: 'Use links to navigate to commonly used dashboards and websites.',
}),
getCreateFlyoutTitle: () =>
i18n.translate('navigationEmbeddable.panelEditor.createFlyoutTitle', {
From c7485e683866151bcdeb4b3e9ad65625f2a66a98 Mon Sep 17 00:00:00 2001
From: Hannah Mudge
Date: Mon, 17 Jul 2023 16:42:56 -0600
Subject: [PATCH 06/53] [Dashboard Navigation] Add link editing + reordering
(#161568)
Closes https://github.com/elastic/kibana/issues/154361
Closes https://github.com/elastic/kibana/issues/161274
Closes https://github.com/elastic/kibana/issues/161693
## Summary
This PR adds editing capabilities to the navigation embeddable,
including deleting/editing existing links and reordering the list of
links. It also fixes the delay in opening the editing flyout from the
async import in `getExplicitInput` (from the navigation embeddable
factory) by moving it to the constructor of the factory.
https://github.com/elastic/kibana/assets/8698078/ace9dcd4-0607-40de-959e-94348a5fa4fa
### Checklist
- [x] Any text added follows [EUI's writing
guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses
sentence case text and includes [i18n
support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md)
- [ ] ~[Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios~ Will be
addressed in https://github.com/elastic/kibana/issues/161287
- [x] Any UI touched in this PR is usable by keyboard only (learn more
about [keyboard accessibility](https://webaim.org/techniques/keyboard/))
- [x] Any UI touched in this PR does not create any new axe failures
(run axe in browser:
[FF](https://addons.mozilla.org/en-US/firefox/addon/axe-devtools/),
[Chrome](https://chrome.google.com/webstore/detail/axe-web-accessibility-tes/lhdoppojpmngadmnindnejefpokejbdd?hl=en-US))
- [x] This renders correctly on smaller devices using a responsive
layout. (You can test this [in your
browser](https://www.browserstack.com/guide/responsive-testing-on-local-server))
- [x] This was checked for [cross-browser
compatibility](https://www.elastic.co/support/matrix#matrix_browsers)
### For maintainers
- [ ] This was checked for breaking API changes and was [labeled
appropriately](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process)
---------
Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
---
.../customize_panel_action.tsx | 8 +-
.../navigation_embeddable/public/_mixins.scss | 21 +-
.../dashboard_link_component.tsx | 8 +-
.../dashboard_link_destination_picker.tsx | 161 ++++++------
.../dashboard_link/dashboard_link_strings.ts | 4 +-
.../dashboard_link/dashboard_link_tools.tsx | 99 +++++---
.../external_link_destination_picker.tsx | 27 +-
.../components/navigation_embeddable.scss | 73 ++++--
.../navigation_embeddable_component.tsx | 20 +-
.../navigation_embeddable_link_editor.tsx | 92 +++++--
.../navigation_embeddable_panel_editor.tsx | 235 ++++++++++--------
...avigation_embeddable_panel_editor_link.tsx | 114 +++++++++
.../navigation_embeddable_strings.ts | 30 ++-
.../navigation_embeddable_editor_tools.tsx | 34 +++
.../public/editor/open_editor_flyout.tsx | 19 +-
.../public/editor/open_link_editor_flyout.tsx | 77 ++++++
.../navigation_embeddable_factory.ts | 7 +-
.../public/embeddable/types.ts | 9 +-
.../navigation_embeddable/tsconfig.json | 1 +
19 files changed, 757 insertions(+), 282 deletions(-)
create mode 100644 src/plugins/navigation_embeddable/public/components/navigation_embeddable_panel_editor_link.tsx
create mode 100644 src/plugins/navigation_embeddable/public/editor/navigation_embeddable_editor_tools.tsx
create mode 100644 src/plugins/navigation_embeddable/public/editor/open_link_editor_flyout.tsx
diff --git a/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/customize_panel/customize_panel_action.tsx b/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/customize_panel/customize_panel_action.tsx
index 6146199eee24b..5c3a3a8586354 100644
--- a/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/customize_panel/customize_panel_action.tsx
+++ b/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/customize_panel/customize_panel_action.tsx
@@ -79,9 +79,15 @@ export class CustomizePanelAction implements Action
(embeddable as VisualizeEmbeddable).getOutput().visTypeName === 'markdown';
const isImage = embeddable.type === 'image';
+ const isNavigation = embeddable.type === 'navigation';
return Boolean(
- embeddable && hasTimeRange(embeddable) && !isInputControl && !isMarkdown && !isImage
+ embeddable &&
+ hasTimeRange(embeddable) &&
+ !isInputControl &&
+ !isMarkdown &&
+ !isImage &&
+ !isNavigation
);
}
diff --git a/src/plugins/navigation_embeddable/public/_mixins.scss b/src/plugins/navigation_embeddable/public/_mixins.scss
index 47249b0aa42d4..f327bc1fe73d7 100644
--- a/src/plugins/navigation_embeddable/public/_mixins.scss
+++ b/src/plugins/navigation_embeddable/public/_mixins.scss
@@ -1,6 +1,6 @@
@import '../../../core/public/mixins';
-@keyframes euiFlyoutAnimation {
+@keyframes euiFlyoutOpenAnimation {
0% {
opacity: 0;
transform: translateX(100%);
@@ -12,14 +12,31 @@
}
}
+@keyframes euiFlyoutCloseAnimation {
+ 0% {
+ opacity: 1;
+ transform: translateX(0%);
+ }
+
+ 100% {
+ opacity: 0;
+ transform: translateX(100%);
+ }
+}
+
@mixin euiFlyout {
@include kibanaFullBodyHeight();
border-left: $euiBorderThin;
position: fixed;
- width: 50%;
z-index: $euiZFlyout;
background: $euiColorEmptyShade;
display: flex;
flex-direction: column;
align-items: stretch;
+ inline-size: 50vw;
+
+ @media only screen and (max-width: 767px) {
+ inline-size: $euiSizeXL * 13; // 424px
+ max-inline-size: 90vw;
+ }
}
\ No newline at end of file
diff --git a/src/plugins/navigation_embeddable/public/components/dashboard_link/dashboard_link_component.tsx b/src/plugins/navigation_embeddable/public/components/dashboard_link/dashboard_link_component.tsx
index 7c8170674d9ea..db371c426ed4d 100644
--- a/src/plugins/navigation_embeddable/public/components/dashboard_link/dashboard_link_component.tsx
+++ b/src/plugins/navigation_embeddable/public/components/dashboard_link/dashboard_link_component.tsx
@@ -29,7 +29,13 @@ export const DashboardLinkComponent = ({ link }: { link: NavigationEmbeddableLin
const { loading: loadingDestinationDashboard, value: destinationDashboard } =
useAsync(async () => {
- return await fetchDashboard(link.destination);
+ if (!link.label && link.id !== parentDashboardId) {
+ /**
+ * only fetch the dashboard if **absolutely** necessary; i.e. only if the dashboard link doesn't have
+ * some custom label, and if it's not the current dashboard (if it is, use `dashboardContainer` instead)
+ */
+ return await fetchDashboard(link.destination);
+ }
}, [link, parentDashboardId]);
return (
diff --git a/src/plugins/navigation_embeddable/public/components/dashboard_link/dashboard_link_destination_picker.tsx b/src/plugins/navigation_embeddable/public/components/dashboard_link/dashboard_link_destination_picker.tsx
index 715434dc2c80f..7156449be366d 100644
--- a/src/plugins/navigation_embeddable/public/components/dashboard_link/dashboard_link_destination_picker.tsx
+++ b/src/plugins/navigation_embeddable/public/components/dashboard_link/dashboard_link_destination_picker.tsx
@@ -8,61 +8,68 @@
import { debounce } from 'lodash';
import useAsync from 'react-use/lib/useAsync';
-import React, { useEffect, useMemo, useState } from 'react';
+import useMount from 'react-use/lib/useMount';
+import React, { useCallback, useMemo, useState } from 'react';
import {
EuiBadge,
- EuiSpacer,
+ EuiComboBox,
+ EuiFlexItem,
EuiHighlight,
- EuiSelectable,
- EuiFieldSearch,
- EuiSelectableOption,
+ EuiFlexGroup,
+ EuiComboBoxOptionOption,
} from '@elastic/eui';
import { DashboardContainer } from '@kbn/dashboard-plugin/public/dashboard_container';
+
import { DashboardItem } from '../../embeddable/types';
-import { memoizedFetchDashboards } from './dashboard_link_tools';
+import { memoizedFetchDashboard, memoizedFetchDashboards } from './dashboard_link_tools';
import { DashboardLinkEmbeddableStrings } from './dashboard_link_strings';
+type DashboardComboBoxOption = EuiComboBoxOptionOption;
+
export const DashboardLinkDestinationPicker = ({
- setDestination,
- setPlaceholder,
- currentDestination,
+ onDestinationPicked,
+ initialSelection,
parentDashboard,
...other
}: {
- setDestination: (destination?: string) => void;
- setPlaceholder: (placeholder?: string) => void;
- currentDestination?: string;
+ onDestinationPicked: (selectedDashboard?: DashboardItem) => void;
parentDashboard?: DashboardContainer;
+ initialSelection?: string;
}) => {
const [searchString, setSearchString] = useState('');
- const [selectedDashboard, setSelectedDashboard] = useState();
- const [dashboardListOptions, setDashboardListOptions] = useState([]);
+ const [selectedOption, setSelectedOption] = useState([]);
const parentDashboardId = parentDashboard?.select((state) => state.componentState.lastSavedId);
- const { loading: loadingDashboardList, value: dashboardList } = useAsync(async () => {
- return await memoizedFetchDashboards(searchString, undefined, parentDashboardId);
- }, [searchString, parentDashboardId]);
+ const getDashboardItem = useCallback((dashboard: DashboardItem) => {
+ return {
+ key: dashboard.id,
+ value: dashboard,
+ label: dashboard.attributes.title,
+ className: 'navEmbeddableDashboardItem',
+ };
+ }, []);
- useEffect(() => {
- const dashboardOptions =
- (dashboardList ?? []).map((dashboard: DashboardItem) => {
- return {
- data: dashboard,
- label: dashboard.attributes.title,
- ...(dashboard.id === parentDashboardId
- ? {
- prepend: (
- {DashboardLinkEmbeddableStrings.getCurrentDashboardLabel()}
- ),
- }
- : {}),
- } as EuiSelectableOption;
- }) ?? [];
+ useMount(async () => {
+ if (initialSelection) {
+ const dashboard = await memoizedFetchDashboard(initialSelection);
+ onDestinationPicked(dashboard);
+ setSelectedOption([getDashboardItem(dashboard)]);
+ }
+ });
- setDashboardListOptions(dashboardOptions);
- }, [dashboardList, parentDashboardId, searchString]);
+ const { loading: loadingDashboardList, value: dashboardList } = useAsync(async () => {
+ const dashboards = await memoizedFetchDashboards({
+ search: searchString,
+ parentDashboardId,
+ selectedDashboardId: initialSelection,
+ });
+ const dashboardOptions = (dashboards ?? []).map((dashboard: DashboardItem) => {
+ return getDashboardItem(dashboard);
+ });
+ return dashboardOptions;
+ }, [searchString, parentDashboardId, getDashboardItem]);
const debouncedSetSearch = useMemo(
() =>
@@ -72,47 +79,53 @@ export const DashboardLinkDestinationPicker = ({
[setSearchString]
);
- useEffect(() => {
- if (selectedDashboard) {
- setDestination(selectedDashboard.id);
- setPlaceholder(selectedDashboard.attributes.title);
- } else {
- setDestination(undefined);
- setPlaceholder(undefined);
- }
- }, [selectedDashboard, setDestination, setPlaceholder]);
+ const renderOption = useCallback(
+ (option, searchValue, contentClassName) => {
+ const { label, key: dashboardId } = option;
+ return (
+
+ {dashboardId === parentDashboardId && (
+
+ {DashboardLinkEmbeddableStrings.getCurrentDashboardLabel()}
+
+ )}
+
+
+ {label}
+
+
+
+ );
+ },
+ [parentDashboardId]
+ );
- /* {...other} is needed so all inner elements are treated as part of the form */
+ /* {...other} is needed so the EuiComboBox is treated as part of the form */
return (
-
- {
- debouncedSetSearch(e.target.value);
- }}
- />
-
- {
- if (selected.checked) {
- setSelectedDashboard(selected.data as DashboardItem);
- } else {
- setSelectedDashboard(undefined);
- }
- setDashboardListOptions(newOptions);
- }}
- renderOption={(option) => {
- return {option.label} ;
- }}
- >
- {(list) => list}
-
-
+ {
+ debouncedSetSearch(searchValue);
+ }}
+ renderOption={renderOption}
+ selectedOptions={selectedOption}
+ onChange={(option) => {
+ setSelectedOption(option);
+ if (option.length > 0) {
+ // single select is `true`, so there is only ever one item in the array
+ onDestinationPicked(option[0].value);
+ } else {
+ onDestinationPicked(undefined);
+ }
+ }}
+ />
);
};
diff --git a/src/plugins/navigation_embeddable/public/components/dashboard_link/dashboard_link_strings.ts b/src/plugins/navigation_embeddable/public/components/dashboard_link/dashboard_link_strings.ts
index 9bc2e2d40f0b0..c763b0bd88e4e 100644
--- a/src/plugins/navigation_embeddable/public/components/dashboard_link/dashboard_link_strings.ts
+++ b/src/plugins/navigation_embeddable/public/components/dashboard_link/dashboard_link_strings.ts
@@ -17,8 +17,8 @@ export const DashboardLinkEmbeddableStrings = {
i18n.translate('navigationEmbeddable.dsahboardLink.description', {
defaultMessage: 'Go to dashboard',
}),
- getSearchPlaceholder: () =>
- i18n.translate('navigationEmbeddable.dashboardLink.editor.searchPlaceholder', {
+ getDashboardPickerPlaceholder: () =>
+ i18n.translate('navigationEmbeddable.dashboardLink.editor.dashboardComboBoxPlaceholder', {
defaultMessage: 'Search for a dashboard',
}),
getDashboardPickerAriaLabel: () =>
diff --git a/src/plugins/navigation_embeddable/public/components/dashboard_link/dashboard_link_tools.tsx b/src/plugins/navigation_embeddable/public/components/dashboard_link/dashboard_link_tools.tsx
index 3735e5a044ffa..9590df2bd6c0d 100644
--- a/src/plugins/navigation_embeddable/public/components/dashboard_link/dashboard_link_tools.tsx
+++ b/src/plugins/navigation_embeddable/public/components/dashboard_link/dashboard_link_tools.tsx
@@ -6,24 +6,16 @@
* Side Public License, v 1.
*/
-import { isEmpty, memoize } from 'lodash';
+import { isEmpty, memoize, filter } from 'lodash';
import { DashboardItem } from '../../embeddable/types';
import { dashboardServices } from '../../services/kibana_services';
/**
- * Memoized fetch dashboard will only refetch the dashboard information if the given `dashboardId` changed between
- * calls; otherwise, it will use the cached dashboard, which may not take into account changes to the dashboard's title
- * description, etc. Be mindful when choosing the memoized version.
+ * ----------------------------------
+ * Fetch a single dashboard
+ * ----------------------------------
*/
-export const memoizedFetchDashboard = memoize(
- async (dashboardId: string) => {
- return await fetchDashboard(dashboardId);
- },
- (dashboardId) => {
- return dashboardId;
- }
-);
export const fetchDashboard = async (dashboardId: string): Promise => {
const findDashboardsService = await dashboardServices.findDashboardsService();
@@ -34,20 +26,39 @@ export const fetchDashboard = async (dashboardId: string): Promise {
- return await fetchDashboards(search, size, currentDashboardId);
+/**
+ * Memoized fetch dashboard will only refetch the dashboard information if the given `dashboardId` changed between
+ * calls; otherwise, it will use the cached dashboard, which may not take into account changes to the dashboard's title
+ * description, etc. Be mindful when choosing the memoized version.
+ */
+export const memoizedFetchDashboard = memoize(
+ async (dashboardId: string) => {
+ return await fetchDashboard(dashboardId);
},
- (search, size, currentDashboardId) => {
- return [search, size, currentDashboardId].join('|');
+ (dashboardId) => {
+ return dashboardId;
}
);
-const fetchDashboards = async (
- search: string = '',
- size: number = 10,
- currentDashboardId?: string
-): Promise => {
+/**
+ * ----------------------------------
+ * Fetch lists of dashboards
+ * ----------------------------------
+ */
+
+interface FetchDashboardsProps {
+ size?: number;
+ search?: string;
+ parentDashboardId?: string;
+ selectedDashboardId?: string;
+}
+
+const fetchDashboards = async ({
+ search = '',
+ size = 10,
+ parentDashboardId,
+ selectedDashboardId,
+}: FetchDashboardsProps): Promise => {
const findDashboardsService = await dashboardServices.findDashboardsService();
const responses = await findDashboardsService.search({
search,
@@ -55,28 +66,22 @@ const fetchDashboards = async (
options: { onlyTitle: true },
});
- let currentDashboard: DashboardItem | undefined;
let dashboardList: DashboardItem[] = responses.hits;
- /** When the parent dashboard has been saved (i.e. it has an ID) and there is no search string ... */
- if (currentDashboardId && isEmpty(search)) {
- /** ...force the current dashboard (if it is present in the original search results) to the top of the list */
- dashboardList = dashboardList.sort((dashboard) => {
- const isCurrentDashboard = dashboard.id === currentDashboardId;
- if (isCurrentDashboard) {
- currentDashboard = dashboard;
- }
- return isCurrentDashboard ? -1 : 1;
+ /** If there is no search string... */
+ if (isEmpty(search)) {
+ /** ... filter out both the parent and selected dashboard from the list ... */
+ dashboardList = filter(dashboardList, (dash) => {
+ return dash.id !== parentDashboardId && dash.id !== selectedDashboardId;
});
- /**
- * If the current dashboard wasn't returned in the original search, perform another search to find it and
- * force it to the front of the list
- */
- if (!currentDashboard) {
- currentDashboard = await fetchDashboard(currentDashboardId);
- dashboardList.pop(); // the result should still be of `size,` so remove the dashboard at the end of the list
- dashboardList.unshift(currentDashboard); // in order to force the current dashboard to the start of the list
+ /** ... so that we can force them to the top of the list as necessary. */
+ if (parentDashboardId) {
+ dashboardList.unshift(await fetchDashboard(parentDashboardId));
+ }
+
+ if (selectedDashboardId && selectedDashboardId !== parentDashboardId) {
+ dashboardList.unshift(await fetchDashboard(selectedDashboardId));
}
}
@@ -87,3 +92,17 @@ const fetchDashboards = async (
return simplifiedDashboardList;
};
+
+export const memoizedFetchDashboards = memoize(
+ async ({ search, size, parentDashboardId, selectedDashboardId }: FetchDashboardsProps) => {
+ return await fetchDashboards({
+ search,
+ size,
+ parentDashboardId,
+ selectedDashboardId,
+ });
+ },
+ ({ search, size, parentDashboardId, selectedDashboardId }) => {
+ return [search, size, parentDashboardId, selectedDashboardId].join('|');
+ }
+);
diff --git a/src/plugins/navigation_embeddable/public/components/external_link/external_link_destination_picker.tsx b/src/plugins/navigation_embeddable/public/components/external_link/external_link_destination_picker.tsx
index 596ce183d696b..4119cc32f32aa 100644
--- a/src/plugins/navigation_embeddable/public/components/external_link/external_link_destination_picker.tsx
+++ b/src/plugins/navigation_embeddable/public/components/external_link/external_link_destination_picker.tsx
@@ -6,7 +6,9 @@
* Side Public License, v 1.
*/
+import useMount from 'react-use/lib/useMount';
import React, { useState } from 'react';
+
import { EuiFieldText } from '@elastic/eui';
import { ExternalLinkEmbeddableStrings } from './external_link_strings';
@@ -15,29 +17,38 @@ const isValidUrl =
/^https?:\/\/(?:www.)?[-a-zA-Z0-9@:%._+~#=]{1,256}.[a-zA-Z0-9()]{1,6}\b(?:[-a-zA-Z0-9()@:%_+.~#?&/=]*)$/;
export const ExternalLinkDestinationPicker = ({
- setDestination,
- setPlaceholder,
- currentDestination,
+ onDestinationPicked,
+ initialSelection,
...other
}: {
- setDestination: (destination?: string) => void;
- setPlaceholder: (placeholder?: string) => void;
- currentDestination?: string;
+ onDestinationPicked: (destination?: string) => void;
+ initialSelection?: string;
}) => {
const [validUrl, setValidUrl] = useState(true);
+ const [currentUrl, setCurrentUrl] = useState(initialSelection ?? '');
+
+ useMount(() => {
+ if (initialSelection) {
+ onDestinationPicked(initialSelection);
+ setValidUrl(isValidUrl.test(initialSelection));
+ }
+ });
/* {...other} is needed so all inner elements are treated as part of the form */
return (
{
const url = e.target.value;
const isValid = isValidUrl.test(url);
setValidUrl(isValid);
- setDestination(isValid ? url : undefined);
- setPlaceholder(isValid ? url : undefined);
+ setCurrentUrl(url);
+ if (isValid) {
+ onDestinationPicked(url);
+ }
}}
/>
diff --git a/src/plugins/navigation_embeddable/public/components/navigation_embeddable.scss b/src/plugins/navigation_embeddable/public/components/navigation_embeddable.scss
index 214cbbc7a8760..e7a6e5a1890a0 100644
--- a/src/plugins/navigation_embeddable/public/components/navigation_embeddable.scss
+++ b/src/plugins/navigation_embeddable/public/components/navigation_embeddable.scss
@@ -1,23 +1,68 @@
@import '../mixins';
-.navEmbeddableLinkEditor {
- @include euiFlyout;
- animation: euiFlyoutAnimation $euiAnimSpeedNormal $euiAnimSlightResistance;
+.navEmbeddablePanelEditor {
+ max-inline-size: $euiSizeXXL * 18; // 40px * 18 = 720px
+
+ .navEmbeddableLinkEditor {
+ @include euiFlyout;
+ max-inline-size: $euiSizeXXL * 18;
+
+ &.in {
+ animation: euiFlyoutOpenAnimation $euiAnimSpeedNormal $euiAnimSlightResistance;
+ }
+
+ &.out {
+ animation: euiFlyoutCloseAnimation $euiAnimSpeedNormal $euiAnimSlightResistance;
+ }
- .linkEditorBackButton {
- height: auto;
+ .linkEditorBackButton {
+ height: auto;
+ }
}
}
-.navEmbeddablePanelEditor {
- .linkText {
- flex: 1;
- min-width: 0;
-
- .wrapText {
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
+.navEmbeddableDashboardItem {
+ .euiBadge {
+ cursor: pointer !important;
+ }
+
+ // in order to ensure that the "Current" badge doesn't recieve an underline on hover, we have to set the
+ // text-decoration to `none` for the entire list item and manually set the underline **only** on the text
+ &:hover {
+ text-decoration: none;
+ }
+
+ .navEmbeddableLinkText {
+ &:hover {
+ text-decoration: underline !important;
+ }
+ }
+}
+
+.navEmbeddableLinkText {
+ flex: 1;
+ min-width: 0;
+
+ .wrapText {
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ }
+}
+
+.navEmbeddableLinkPanel {
+ padding: $euiSizeXS $euiSizeS;
+
+ .navEmbeddable_hoverActions {
+ opacity: 0;
+ visibility: hidden;
+ transition: visibility $euiAnimSpeedNormal, opacity $euiAnimSpeedNormal;
+ }
+
+ &:hover, &:focus-within {
+ .navEmbeddable_hoverActions {
+ opacity: 1;
+ visibility: visible;
}
}
}
\ No newline at end of file
diff --git a/src/plugins/navigation_embeddable/public/components/navigation_embeddable_component.tsx b/src/plugins/navigation_embeddable/public/components/navigation_embeddable_component.tsx
index abcbe098f3896..b17be812accbc 100644
--- a/src/plugins/navigation_embeddable/public/components/navigation_embeddable_component.tsx
+++ b/src/plugins/navigation_embeddable/public/components/navigation_embeddable_component.tsx
@@ -6,35 +6,39 @@
* Side Public License, v 1.
*/
-import React from 'react';
+import React, { useMemo } from 'react';
import { EuiPanel } from '@elastic/eui';
import { DASHBOARD_LINK_TYPE } from '../embeddable/types';
import { useNavigationEmbeddable } from '../embeddable/navigation_embeddable';
-import { DashboardLinkComponent } from './dashboard_link/dashboard_link_component';
import { ExternalLinkComponent } from './external_link/external_link_component';
+import { DashboardLinkComponent } from './dashboard_link/dashboard_link_component';
+import { memoizedGetOrderedLinkList } from '../editor/navigation_embeddable_editor_tools';
export const NavigationEmbeddableComponent = () => {
const navEmbeddable = useNavigationEmbeddable();
const links = navEmbeddable.select((state) => state.explicitInput.links);
+ const orderedLinks = useMemo(() => {
+ return memoizedGetOrderedLinkList(links);
+ }, [links]);
/** TODO: Render this as a list **or** "tabs" as part of https://github.com/elastic/kibana/issues/154357 */
return (
- {Object.keys(links).map((linkId) => {
+ {orderedLinks.map((link) => {
return (
- {links[linkId].type === DASHBOARD_LINK_TYPE ? (
-
+ {link.type === DASHBOARD_LINK_TYPE ? (
+
) : (
-
+
)}
);
diff --git a/src/plugins/navigation_embeddable/public/components/navigation_embeddable_link_editor.tsx b/src/plugins/navigation_embeddable/public/components/navigation_embeddable_link_editor.tsx
index a0753130bcc83..1d5fa98766051 100644
--- a/src/plugins/navigation_embeddable/public/components/navigation_embeddable_link_editor.tsx
+++ b/src/plugins/navigation_embeddable/public/components/navigation_embeddable_link_editor.tsx
@@ -6,7 +6,8 @@
* Side Public License, v 1.
*/
-import React, { useMemo, useState } from 'react';
+import { v4 as uuidv4 } from 'uuid';
+import React, { useCallback, useMemo, useState } from 'react';
import {
EuiForm,
@@ -33,24 +34,30 @@ import {
EXTERNAL_LINK_TYPE,
DASHBOARD_LINK_TYPE,
NavigationEmbeddableLink,
+ DashboardItem,
} from '../embeddable/types';
import { NavEmbeddableStrings } from './navigation_embeddable_strings';
+import { NavigationEmbeddableUnorderedLink } from '../editor/open_link_editor_flyout';
import { ExternalLinkDestinationPicker } from './external_link/external_link_destination_picker';
import { DashboardLinkDestinationPicker } from './dashboard_link/dashboard_link_destination_picker';
export const NavigationEmbeddableLinkEditor = ({
+ link,
onSave,
onClose,
parentDashboard,
}: {
onClose: () => void;
parentDashboard?: DashboardContainer;
- onSave: (newLink: NavigationEmbeddableLink) => void;
+ link?: NavigationEmbeddableUnorderedLink; // will only be defined if **editing** a link; otherwise, creating a new link
+ onSave: (newLink: Omit) => void;
}) => {
- const [selectedLinkType, setSelectedLinkType] = useState(DASHBOARD_LINK_TYPE);
- const [linkLabel, setLinkLabel] = useState('');
- const [linkDestination, setLinkDestination] = useState();
- const [linkLabelPlaceholder, setLinkLabelPlaceholder] = useState();
+ const [selectedLinkType, setSelectedLinkType] = useState(
+ link?.type ?? DASHBOARD_LINK_TYPE
+ );
+ const [defaultLinkLabel, setDefaultLinkLabel] = useState();
+ const [currentLinkLabel, setCurrentLinkLabel] = useState(link?.label ?? '');
+ const [linkDestination, setLinkDestination] = useState(link?.destination);
const linkTypes: EuiRadioGroupOption[] = useMemo(() => {
return ([DASHBOARD_LINK_TYPE, EXTERNAL_LINK_TYPE] as NavigationLinkType[]).map((type) => {
@@ -72,8 +79,35 @@ export const NavigationEmbeddableLinkEditor = ({
});
}, []);
+ const onDashboardSelected = useCallback(
+ (selectedDashboard?: DashboardItem) => {
+ setLinkDestination(selectedDashboard?.id);
+ if (selectedDashboard) {
+ const dashboardTitle = selectedDashboard.attributes.title;
+ setDefaultLinkLabel(dashboardTitle);
+ if (!currentLinkLabel || currentLinkLabel === defaultLinkLabel) {
+ setCurrentLinkLabel(dashboardTitle);
+ }
+ }
+ },
+ [currentLinkLabel, defaultLinkLabel]
+ );
+
+ const onUrlSelected = useCallback(
+ (url?: string) => {
+ setLinkDestination(url);
+ if (url) {
+ setDefaultLinkLabel(url);
+ if (!currentLinkLabel || currentLinkLabel === defaultLinkLabel) {
+ setCurrentLinkLabel(url);
+ }
+ }
+ },
+ [currentLinkLabel, defaultLinkLabel]
+ );
+
return (
-
+
- {NavEmbeddableStrings.editor.getAddButtonLabel()}
+
+ {link
+ ? NavEmbeddableStrings.editor.getEditLinkTitle()
+ : NavEmbeddableStrings.editor.getAddButtonLabel()}
+
@@ -97,8 +135,14 @@ export const NavigationEmbeddableLinkEditor = ({
options={linkTypes}
idSelected={selectedLinkType}
onChange={(id) => {
- setLinkDestination(undefined);
- setLinkLabelPlaceholder(undefined);
+ if (link?.type === id) {
+ setLinkDestination(link.destination);
+ setCurrentLinkLabel(link.label ?? '');
+ } else {
+ setLinkDestination(undefined);
+ setCurrentLinkLabel('');
+ }
+ setDefaultLinkLabel(undefined);
setSelectedLinkType(id as NavigationLinkType);
}}
/>
@@ -108,15 +152,13 @@ export const NavigationEmbeddableLinkEditor = ({
{selectedLinkType === DASHBOARD_LINK_TYPE ? (
) : (
)}
@@ -124,13 +166,11 @@ export const NavigationEmbeddableLinkEditor = ({
{
- setLinkLabel(e.target.value);
- }}
+ value={linkDestination ? currentLinkLabel : ''}
+ onChange={(e) => setCurrentLinkLabel(e.target.value)}
/>
@@ -157,15 +197,19 @@ export const NavigationEmbeddableLinkEditor = ({
// this check should always be true, since the button is disabled otherwise - this is just for type safety
if (linkDestination) {
onSave({
- destination: linkDestination,
- label: linkLabel,
+ label: currentLinkLabel === defaultLinkLabel ? undefined : currentLinkLabel,
type: selectedLinkType,
+ id: link?.id ?? uuidv4(),
+ destination: linkDestination,
});
+
onClose();
}
}}
>
- {NavEmbeddableStrings.editor.getAddButtonLabel()}
+ {link
+ ? NavEmbeddableStrings.editor.getUpdateButtonLabel()
+ : NavEmbeddableStrings.editor.getAddButtonLabel()}
diff --git a/src/plugins/navigation_embeddable/public/components/navigation_embeddable_panel_editor.tsx b/src/plugins/navigation_embeddable/public/components/navigation_embeddable_panel_editor.tsx
index 1c117048fcb23..f4c2ce6b51492 100644
--- a/src/plugins/navigation_embeddable/public/components/navigation_embeddable_panel_editor.tsx
+++ b/src/plugins/navigation_embeddable/public/components/navigation_embeddable_panel_editor.tsx
@@ -6,103 +6,164 @@
* Side Public License, v 1.
*/
-import { isEmpty } from 'lodash';
-import { v4 as uuidv4 } from 'uuid';
-import React, { useState } from 'react';
-import useAsync from 'react-use/lib/useAsync';
import useObservable from 'react-use/lib/useObservable';
+import React, { useCallback, useEffect, useMemo, useState } from 'react';
+
import {
EuiText,
- EuiIcon,
EuiForm,
+ EuiImage,
EuiTitle,
EuiPanel,
- IconType,
EuiSpacer,
EuiButton,
+ EuiToolTip,
EuiFormRow,
EuiFlexItem,
EuiFlexGroup,
+ EuiDroppable,
+ EuiDraggable,
EuiFlyoutBody,
+ EuiEmptyPrompt,
EuiButtonEmpty,
EuiFlyoutFooter,
EuiFlyoutHeader,
- EuiImage,
- EuiEmptyPrompt,
+ EuiDragDropContext,
+ euiDragDropReorder,
} from '@elastic/eui';
import { DashboardContainer } from '@kbn/dashboard-plugin/public/dashboard_container';
+
import { coreServices } from '../services/kibana_services';
import {
- DASHBOARD_LINK_TYPE,
- EXTERNAL_LINK_TYPE,
- NavigationEmbeddableInput,
NavigationEmbeddableLink,
- NavigationLinkInfo,
+ NavigationEmbeddableInput,
+ NavigationEmbeddableLinkList,
} from '../embeddable/types';
import { NavEmbeddableStrings } from './navigation_embeddable_strings';
-import { memoizedFetchDashboard } from './dashboard_link/dashboard_link_tools';
-import { NavigationEmbeddableLinkEditor } from './navigation_embeddable_link_editor';
+
+import { openLinkEditorFlyout } from '../editor/open_link_editor_flyout';
+import { memoizedGetOrderedLinkList } from '../editor/navigation_embeddable_editor_tools';
+import { NavigationEmbeddablePanelEditorLink } from './navigation_embeddable_panel_editor_link';
+
import noLinksIllustrationDark from '../assets/empty_links_dark.svg';
import noLinksIllustrationLight from '../assets/empty_links_light.svg';
import './navigation_embeddable.scss';
-export const NavigationEmbeddablePanelEditor = ({
+const NavigationEmbeddablePanelEditor = ({
onSave,
onClose,
initialInput,
parentDashboard,
}: {
onClose: () => void;
+ parentDashboard?: DashboardContainer;
initialInput: Partial;
onSave: (input: Partial) => void;
- parentDashboard?: DashboardContainer;
}) => {
- const [showLinkEditorFlyout, setShowLinkEditorFlyout] = useState(false);
- const [links, setLinks] = useState(initialInput.links);
const isDarkTheme = useObservable(coreServices.theme.theme$)?.darkMode;
+ const editLinkFlyoutRef: React.RefObject = useMemo(() => React.createRef(), []);
+
+ const [orderedLinks, setOrderedLinks] = useState([]);
+
+ useEffect(() => {
+ const { links: initialLinks } = initialInput;
+ if (!initialLinks) {
+ setOrderedLinks([]);
+ return;
+ }
+ setOrderedLinks(memoizedGetOrderedLinkList(initialLinks));
+ }, [initialInput]);
- /**
- * TODO: There is probably a more efficient way of storing the dashboard information "temporarily" for any new
- * panels and only fetching the dashboard saved objects when first loading this flyout.
- *
- * Will need to think this through and fix as part of the editing process - not worth holding this PR, since it's
- * blocking so much other work :)
- */
- const { value: linkList } = useAsync(async () => {
- if (!links || isEmpty(links)) return [];
-
- const newLinks: Array<{ id: string; icon: IconType; label: string }> = await Promise.all(
- Object.keys(links).map(async (panelId) => {
- let label = links[panelId].label;
- let icon = NavigationLinkInfo[EXTERNAL_LINK_TYPE].icon;
-
- if (links[panelId].type === DASHBOARD_LINK_TYPE) {
- icon = NavigationLinkInfo[DASHBOARD_LINK_TYPE].icon;
- if (!label) {
- const dashboard = await memoizedFetchDashboard(links[panelId].destination);
- label = dashboard.attributes.title;
- }
+ const onDragEnd = useCallback(
+ ({ source, destination }) => {
+ if (source && destination) {
+ const newList = euiDragDropReorder(orderedLinks, source.index, destination.index);
+ setOrderedLinks(newList);
+ }
+ },
+ [orderedLinks]
+ );
+
+ const addOrEditLink = useCallback(
+ async (linkToEdit?: NavigationEmbeddableLink) => {
+ const newLink = await openLinkEditorFlyout({
+ parentDashboard,
+ link: linkToEdit,
+ ref: editLinkFlyoutRef,
+ });
+ if (newLink) {
+ if (linkToEdit) {
+ setOrderedLinks(
+ orderedLinks.map((link) => {
+ if (link.id === linkToEdit.id) {
+ return { ...newLink, order: linkToEdit.order };
+ }
+ return link;
+ })
+ );
+ } else {
+ setOrderedLinks([...orderedLinks, { ...newLink, order: orderedLinks.length }]);
}
+ }
+ },
+ [editLinkFlyoutRef, orderedLinks, parentDashboard]
+ );
- return { id: panelId, label: label || links[panelId].destination, icon };
- })
+ const deleteLink = useCallback(
+ (linkId: string) => {
+ setOrderedLinks(
+ orderedLinks.filter((link) => {
+ return link.id !== linkId;
+ })
+ );
+ },
+ [orderedLinks]
+ );
+
+ const saveButtonComponent = useMemo(() => {
+ const canSave = orderedLinks.length !== 0;
+
+ const button = (
+ {
+ const newLinks = orderedLinks.reduce((prev, link, i) => {
+ return { ...prev, [link.id]: { ...link, order: i } };
+ }, {} as NavigationEmbeddableLinkList);
+ onSave({ links: newLinks });
+ }}
+ >
+ {NavEmbeddableStrings.editor.panelEditor.getSaveButtonLabel()}
+
);
- return newLinks;
- }, [links]);
+
+ return canSave ? (
+ button
+ ) : (
+
+ {button}
+
+ );
+ }, [onSave, orderedLinks]);
return (
<>
+
- {NavEmbeddableStrings.editor.panelEditor.getCreateFlyoutTitle()}
+
+ {initialInput.links && Object.keys(initialInput.links).length > 0
+ ? NavEmbeddableStrings.editor.panelEditor.getEditFlyoutTitle()
+ : NavEmbeddableStrings.editor.panelEditor.getCreateFlyoutTitle()}
+
<>
- {!links || Object.keys(links).length === 0 ? (
+ {orderedLinks.length === 0 ? (
- setShowLinkEditorFlyout(true)}
- iconType="plusInCircle"
- >
+ addOrEditLink()} iconType="plusInCircle">
{NavEmbeddableStrings.editor.getAddButtonLabel()}
>
@@ -134,33 +191,31 @@ export const NavigationEmbeddablePanelEditor = ({
) : (
<>
- {linkList?.map((link) => {
- return (
-
-
+
+ {orderedLinks.map((link, idx) => (
+
-
-
-
-
-
- {link.label}
-
-
-
-
-
- );
- })}
- setShowLinkEditorFlyout(true)}
- >
+ {(provided) => (
+ addOrEditLink(link)}
+ deleteLink={() => deleteLink(link.id)}
+ dragHandleProps={provided.dragHandleProps}
+ />
+ )}
+
+ ))}
+
+
+ addOrEditLink()}>
{NavEmbeddableStrings.editor.getAddButtonLabel()}
>
@@ -176,31 +231,13 @@ export const NavigationEmbeddablePanelEditor = ({
{NavEmbeddableStrings.editor.getCancelButtonLabel()}
-
- {
- onSave({ ...initialInput, links });
- onClose();
- }}
- >
- {NavEmbeddableStrings.editor.panelEditor.getSaveButtonLabel()}
-
-
+ {saveButtonComponent}
-
- {showLinkEditorFlyout && (
- {
- setShowLinkEditorFlyout(false);
- }}
- onSave={(newLink: NavigationEmbeddableLink) => {
- setLinks({ ...links, [uuidv4()]: newLink });
- }}
- parentDashboard={parentDashboard}
- />
- )}
>
);
};
+
+// required for dynamic import using React.lazy()
+// eslint-disable-next-line import/no-default-export
+export default NavigationEmbeddablePanelEditor;
diff --git a/src/plugins/navigation_embeddable/public/components/navigation_embeddable_panel_editor_link.tsx b/src/plugins/navigation_embeddable/public/components/navigation_embeddable_panel_editor_link.tsx
new file mode 100644
index 0000000000000..be65c130222eb
--- /dev/null
+++ b/src/plugins/navigation_embeddable/public/components/navigation_embeddable_panel_editor_link.tsx
@@ -0,0 +1,114 @@
+/*
+ * 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 useAsync from 'react-use/lib/useAsync';
+
+import {
+ EuiIcon,
+ EuiPanel,
+ EuiFlexItem,
+ EuiFlexGroup,
+ EuiButtonIcon,
+ EuiSkeletonTitle,
+ DraggableProvidedDragHandleProps,
+ EuiToolTip,
+} from '@elastic/eui';
+import { DashboardContainer } from '@kbn/dashboard-plugin/public/dashboard_container';
+
+import {
+ NavigationLinkInfo,
+ DASHBOARD_LINK_TYPE,
+ NavigationEmbeddableLink,
+} from '../embeddable/types';
+import { fetchDashboard } from './dashboard_link/dashboard_link_tools';
+import { NavEmbeddableStrings } from './navigation_embeddable_strings';
+
+export const NavigationEmbeddablePanelEditorLink = ({
+ link,
+ editLink,
+ deleteLink,
+ parentDashboard,
+ dragHandleProps,
+}: {
+ editLink: () => void;
+ deleteLink: () => void;
+ link: NavigationEmbeddableLink;
+ parentDashboard?: DashboardContainer;
+ dragHandleProps?: DraggableProvidedDragHandleProps;
+}) => {
+ const parentDashboardTitle = parentDashboard?.select((state) => state.explicitInput.title);
+ const parentDashboardId = parentDashboard?.select((state) => state.componentState.lastSavedId);
+
+ const { value: linkLabel, loading: linkLabelLoading } = useAsync(async () => {
+ let label = link.label;
+ if (link.type === DASHBOARD_LINK_TYPE && !label) {
+ if (parentDashboardId === link.destination) {
+ label = parentDashboardTitle;
+ } else {
+ const dashboard = await fetchDashboard(link.destination);
+ label = dashboard.attributes.title;
+ }
+ }
+ return label || link.destination;
+ }, [link]);
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+ {linkLabel}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
diff --git a/src/plugins/navigation_embeddable/public/components/navigation_embeddable_strings.ts b/src/plugins/navigation_embeddable/public/components/navigation_embeddable_strings.ts
index 7499eda011509..5628c3444d2d4 100644
--- a/src/plugins/navigation_embeddable/public/components/navigation_embeddable_strings.ts
+++ b/src/plugins/navigation_embeddable/public/components/navigation_embeddable_strings.ts
@@ -14,6 +14,18 @@ export const NavEmbeddableStrings = {
i18n.translate('navigationEmbeddable.editor.addButtonLabel', {
defaultMessage: 'Add link',
}),
+ getUpdateButtonLabel: () =>
+ i18n.translate('navigationEmbeddable.editor.updateButtonLabel', {
+ defaultMessage: 'Update link',
+ }),
+ getEditLinkTitle: () =>
+ i18n.translate('navigationEmbeddable.editor.editLinkTitle', {
+ defaultMessage: 'Edit link',
+ }),
+ getDeleteLinkTitle: () =>
+ i18n.translate('navigationEmbeddable.editor.deleteLinkTitle', {
+ defaultMessage: 'Delete link',
+ }),
getCancelButtonLabel: () =>
i18n.translate('navigationEmbeddable.editor.cancelButtonLabel', {
defaultMessage: 'Close',
@@ -23,14 +35,30 @@ export const NavEmbeddableStrings = {
i18n.translate('navigationEmbeddable.panelEditor.emptyLinksMessage', {
defaultMessage: 'Use links to navigate to commonly used dashboards and websites.',
}),
+ getEmptyLinksTooltip: () =>
+ i18n.translate('navigationEmbeddable.panelEditor.emptyLinksTooltip', {
+ defaultMessage: 'Add one or more links.',
+ }),
getCreateFlyoutTitle: () =>
i18n.translate('navigationEmbeddable.panelEditor.createFlyoutTitle', {
defaultMessage: 'Create links panel',
}),
+ getEditFlyoutTitle: () =>
+ i18n.translate('navigationEmbeddable.panelEditor.editFlyoutTitle', {
+ defaultMessage: 'Edit links panel',
+ }),
getSaveButtonLabel: () =>
i18n.translate('navigationEmbeddable.panelEditor.saveButtonLabel', {
defaultMessage: 'Save',
}),
+ getLinkLoadingAriaLabel: () =>
+ i18n.translate('navigationEmbeddable.linkEditor.linkLoadingAriaLabel', {
+ defaultMessage: 'Loading link',
+ }),
+ getDragHandleAriaLabel: () =>
+ i18n.translate('navigationEmbeddable.editor.dragHandleAriaLabel', {
+ defaultMessage: 'Link drag handle',
+ }),
},
linkEditor: {
getGoBackAriaLabel: () =>
@@ -47,7 +75,7 @@ export const NavEmbeddableStrings = {
}),
getLinkTextLabel: () =>
i18n.translate('navigationEmbeddable.linkEditor.linkTextLabel', {
- defaultMessage: 'Text (optional)',
+ defaultMessage: 'Text',
}),
getLinkTextPlaceholder: () =>
i18n.translate('navigationEmbeddable.linkEditor.linkTextPlaceholder', {
diff --git a/src/plugins/navigation_embeddable/public/editor/navigation_embeddable_editor_tools.tsx b/src/plugins/navigation_embeddable/public/editor/navigation_embeddable_editor_tools.tsx
new file mode 100644
index 0000000000000..83cc4cfdc7c40
--- /dev/null
+++ b/src/plugins/navigation_embeddable/public/editor/navigation_embeddable_editor_tools.tsx
@@ -0,0 +1,34 @@
+/*
+ * 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 { memoize } from 'lodash';
+import { NavigationEmbeddableLink, NavigationEmbeddableLinkList } from '../embeddable/types';
+
+const getOrderedLinkList = (links: NavigationEmbeddableLinkList): NavigationEmbeddableLink[] => {
+ return Object.keys(links)
+ .map((linkId) => {
+ return links[linkId];
+ })
+ .sort((linkA, linkB) => {
+ return linkA.order - linkB.order;
+ });
+};
+
+/**
+ * Memoizing this prevents the navigation embeddable panel editor from having to unnecessarily calculate this
+ * a second time once the embeddable exists - after all, the navigation embeddable component should have already
+ * calculated this so, we can get away with using the cached version in the editor
+ */
+export const memoizedGetOrderedLinkList = memoize(
+ (links: NavigationEmbeddableLinkList) => {
+ return getOrderedLinkList(links);
+ },
+ (links) => {
+ return links;
+ }
+);
diff --git a/src/plugins/navigation_embeddable/public/editor/open_editor_flyout.tsx b/src/plugins/navigation_embeddable/public/editor/open_editor_flyout.tsx
index e47ed639f501d..19edb5fb4f2c8 100644
--- a/src/plugins/navigation_embeddable/public/editor/open_editor_flyout.tsx
+++ b/src/plugins/navigation_embeddable/public/editor/open_editor_flyout.tsx
@@ -8,14 +8,28 @@
import React from 'react';
import { Subject } from 'rxjs';
+import { memoize } from 'lodash';
import { skip, take, takeUntil } from 'rxjs/operators';
+import { withSuspense } from '@kbn/shared-ux-utility';
+import { EuiLoadingSpinner, EuiPanel } from '@elastic/eui';
import { toMountPoint } from '@kbn/kibana-react-plugin/public';
import { DashboardContainer } from '@kbn/dashboard-plugin/public/dashboard_container';
import { coreServices } from '../services/kibana_services';
import { NavigationEmbeddableInput } from '../embeddable/types';
-import { NavigationEmbeddablePanelEditor } from '../components/navigation_embeddable_panel_editor';
+import { memoizedFetchDashboards } from '../components/dashboard_link/dashboard_link_tools';
+
+const LazyNavigationEmbeddablePanelEditor = React.lazy(
+ () => import('../components/navigation_embeddable_panel_editor')
+);
+
+const NavigationEmbeddablePanelEditor = withSuspense(
+ LazyNavigationEmbeddablePanelEditor,
+
+
+
+);
/**
* @throws in case user cancels
@@ -60,10 +74,13 @@ export async function openEditorFlyout(
ownFocus: true,
outsideClickCloses: false,
onClose: onCancel,
+ className: 'navEmbeddablePanelEditor',
}
);
editorFlyout.onClose.then(() => {
+ // we should always re-fetch the dashboards when the editor is opened; so, clear the cache on close
+ memoizedFetchDashboards.cache = new memoize.Cache();
closed$.next(true);
});
});
diff --git a/src/plugins/navigation_embeddable/public/editor/open_link_editor_flyout.tsx b/src/plugins/navigation_embeddable/public/editor/open_link_editor_flyout.tsx
new file mode 100644
index 0000000000000..794b8812d793b
--- /dev/null
+++ b/src/plugins/navigation_embeddable/public/editor/open_link_editor_flyout.tsx
@@ -0,0 +1,77 @@
+/*
+ * 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 ReactDOM from 'react-dom';
+
+import { KibanaThemeProvider } from '@kbn/kibana-react-plugin/public';
+import { DashboardContainer } from '@kbn/dashboard-plugin/public/dashboard_container';
+
+import { coreServices } from '../services/kibana_services';
+import { NavigationEmbeddableLink } from '../embeddable/types';
+import { NavigationEmbeddableLinkEditor } from '../components/navigation_embeddable_link_editor';
+
+export interface LinkEditorProps {
+ link?: NavigationEmbeddableLink;
+ parentDashboard?: DashboardContainer;
+ ref: React.RefObject;
+}
+
+/**
+ * This editor has no context about other links, so it cannot determine order; order will be determined
+ * by the **caller** (i.e. the panel editor, which contains the context about **all links**)
+ */
+export type NavigationEmbeddableUnorderedLink = Omit;
+
+/**
+ * @throws in case user cancels
+ */
+export async function openLinkEditorFlyout({
+ ref,
+ link,
+ parentDashboard,
+}: LinkEditorProps): Promise {
+ const unmountFlyout = async () => {
+ if (ref.current) {
+ ref.current.children[1].className = 'navEmbeddableLinkEditor out';
+ }
+ await new Promise(() => {
+ // wait for close animation before unmounting
+ setTimeout(() => {
+ if (ref.current) ReactDOM.unmountComponentAtNode(ref.current);
+ }, 180);
+ });
+ };
+
+ return new Promise((resolve, reject) => {
+ const onSave = async (newLink: NavigationEmbeddableUnorderedLink) => {
+ resolve(newLink);
+ await unmountFlyout();
+ };
+
+ const onCancel = async () => {
+ reject();
+ await unmountFlyout();
+ };
+
+ ReactDOM.render(
+
+
+ ,
+ ref.current
+ );
+ }).catch(() => {
+ // on reject (i.e. on cancel), just return the original list of links
+ return undefined;
+ });
+}
diff --git a/src/plugins/navigation_embeddable/public/embeddable/navigation_embeddable_factory.ts b/src/plugins/navigation_embeddable/public/embeddable/navigation_embeddable_factory.ts
index 8f9985b687665..8a9662492909a 100644
--- a/src/plugins/navigation_embeddable/public/embeddable/navigation_embeddable_factory.ts
+++ b/src/plugins/navigation_embeddable/public/embeddable/navigation_embeddable_factory.ts
@@ -33,7 +33,6 @@ export class NavigationEmbeddableFactoryDefinition
implements EmbeddableFactoryDefinition
{
public readonly type = NAVIGATION_EMBEDDABLE_TYPE;
-
public isContainerType = false;
public async isEditable() {
@@ -75,11 +74,9 @@ export class NavigationEmbeddableFactoryDefinition
) {
if (!parent) return {};
- const { openEditorFlyout: createNavigationEmbeddable } = await import(
- '../editor/open_editor_flyout'
- );
+ const { openEditorFlyout } = await import('../editor/open_editor_flyout');
- const input = await createNavigationEmbeddable(
+ const input = await openEditorFlyout(
{ ...getDefaultNavigationEmbeddableInput(), ...initialInput },
parent
).catch(() => {
diff --git a/src/plugins/navigation_embeddable/public/embeddable/types.ts b/src/plugins/navigation_embeddable/public/embeddable/types.ts
index 40dd5901db948..0513d50fc8cb8 100644
--- a/src/plugins/navigation_embeddable/public/embeddable/types.ts
+++ b/src/plugins/navigation_embeddable/public/embeddable/types.ts
@@ -33,14 +33,19 @@ export const EXTERNAL_LINK_TYPE = 'externalLink';
export type NavigationLinkType = typeof DASHBOARD_LINK_TYPE | typeof EXTERNAL_LINK_TYPE;
export interface NavigationEmbeddableLink {
+ id: string;
type: NavigationLinkType;
destination: string;
- // order: number; TODO: Use this as part of https://github.com/elastic/kibana/issues/154361
label?: string;
+ order: number;
+}
+
+export interface NavigationEmbeddableLinkList {
+ [id: string]: NavigationEmbeddableLink;
}
export interface NavigationEmbeddableInput extends EmbeddableInput {
- links: { [id: string]: NavigationEmbeddableLink };
+ links: NavigationEmbeddableLinkList;
}
export const NavigationLinkInfo: {
diff --git a/src/plugins/navigation_embeddable/tsconfig.json b/src/plugins/navigation_embeddable/tsconfig.json
index a7ea3f209f7ad..3c1cee2edb3d7 100644
--- a/src/plugins/navigation_embeddable/tsconfig.json
+++ b/src/plugins/navigation_embeddable/tsconfig.json
@@ -11,6 +11,7 @@
"@kbn/embeddable-plugin",
"@kbn/kibana-react-plugin",
"@kbn/presentation-util-plugin",
+ "@kbn/shared-ux-utility",
],
"exclude": ["target/**/*"]
}
From 577134c2013a2f1caa5ea4b616f36dabd5d22e37 Mon Sep 17 00:00:00 2001
From: Nick Peihl
Date: Thu, 10 Aug 2023 13:42:10 -0400
Subject: [PATCH 07/53] [Navigation embeddable] Add content management
(#160896)
Fixes https://github.com/elastic/kibana/issues/154362
## Summary
Adds content management to navigation embeddable feature branch.
Allows Links panels to be by-value or by-reference on a Dashboard. The
UX for users to choose to save by-value or by-reference remains to be
finalized and is out of scope for this PR.
---------
Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
Co-authored-by: Hannah Mudge
---
.../src/constants.ts | 1 +
.../src/kibana_migrator_utils.fixtures.ts | 17 +++
.../current_mappings.json | 17 +++
.../group2/check_registered_types.test.ts | 1 +
.../group5/dot_kibana_split.test.ts | 1 +
.../replace_panel_flyout.tsx | 30 +---
.../embeddable/api/panel_management.ts | 49 ++-----
.../public/lib/actions/edit_panel_action.ts | 8 +-
.../public/lib/containers/container.ts | 38 ++++--
.../public/lib/containers/i_container.ts | 5 +-
.../navigation_embeddable/common/constants.ts | 19 +++
.../common/content_management/cm_services.ts | 21 +++
.../common/content_management/index.ts | 23 ++++
.../common/content_management/latest.ts | 9 ++
.../content_management/v1/cm_services.ts | 108 +++++++++++++++
.../common/content_management/v1/constants.ts | 17 +++
.../common/content_management/v1/index.ts | 17 +++
.../common/content_management/v1/types.ts | 46 +++++++
.../navigation_embeddable/common/index.ts | 9 ++
.../navigation_embeddable/common/types.ts | 19 +++
.../navigation_embeddable/kibana.jsonc | 17 ++-
.../dashboard_link_component.tsx | 7 +-
.../external_link/external_link_component.tsx | 7 +-
.../navigation_embeddable_component.tsx | 9 +-
.../navigation_embeddable_link_editor.tsx | 8 +-
.../navigation_embeddable_panel_editor.tsx | 129 ++++++++++++------
...avigation_embeddable_panel_editor_link.tsx | 7 +-
.../navigation_embeddable_strings.ts | 39 +++++-
.../public/components/tooltip_wrapper.tsx | 35 +++++
.../duplicate_title_check.ts | 57 ++++++++
.../public/content_management/index.ts | 10 ++
...embeddable_content_management_client.ts.ts | 83 +++++++++++
.../content_management/save_to_library.tsx | 82 +++++++++++
.../navigation_embeddable_editor_tools.tsx | 18 +--
.../public/editor/open_editor_flyout.tsx | 48 ++++++-
.../public/editor/open_link_editor_flyout.tsx | 2 +-
.../public/embeddable/index.ts | 5 +-
.../embeddable/navigation_embeddable.tsx | 90 ++++++++++--
.../navigation_embeddable_factory.ts | 80 +++++++----
.../public/embeddable/types.ts | 60 ++++----
.../navigation_embeddable/public/index.ts | 6 +-
.../navigation_embeddable/public/plugin.ts | 27 +++-
.../public/services/attribute_service.ts | 93 +++++++++++++
.../public/services/kibana_services.ts | 6 +
.../server/content_management/index.ts | 9 ++
.../navigation_embeddable_storage.ts | 23 ++++
.../navigation_embeddable/server/index.ts | 11 ++
.../navigation_embeddable/server/plugin.ts | 43 ++++++
.../server/saved_objects/index.ts | 9 ++
.../saved_objects/navigation_embeddable.ts | 40 ++++++
.../navigation_embeddable/tsconfig.json | 8 ++
51 files changed, 1275 insertions(+), 248 deletions(-)
create mode 100644 src/plugins/navigation_embeddable/common/constants.ts
create mode 100644 src/plugins/navigation_embeddable/common/content_management/cm_services.ts
create mode 100644 src/plugins/navigation_embeddable/common/content_management/index.ts
create mode 100644 src/plugins/navigation_embeddable/common/content_management/latest.ts
create mode 100644 src/plugins/navigation_embeddable/common/content_management/v1/cm_services.ts
create mode 100644 src/plugins/navigation_embeddable/common/content_management/v1/constants.ts
create mode 100644 src/plugins/navigation_embeddable/common/content_management/v1/index.ts
create mode 100644 src/plugins/navigation_embeddable/common/content_management/v1/types.ts
create mode 100644 src/plugins/navigation_embeddable/common/index.ts
create mode 100644 src/plugins/navigation_embeddable/common/types.ts
create mode 100644 src/plugins/navigation_embeddable/public/components/tooltip_wrapper.tsx
create mode 100644 src/plugins/navigation_embeddable/public/content_management/duplicate_title_check.ts
create mode 100644 src/plugins/navigation_embeddable/public/content_management/index.ts
create mode 100644 src/plugins/navigation_embeddable/public/content_management/navigation_embeddable_content_management_client.ts.ts
create mode 100644 src/plugins/navigation_embeddable/public/content_management/save_to_library.tsx
create mode 100644 src/plugins/navigation_embeddable/public/services/attribute_service.ts
create mode 100644 src/plugins/navigation_embeddable/server/content_management/index.ts
create mode 100644 src/plugins/navigation_embeddable/server/content_management/navigation_embeddable_storage.ts
create mode 100644 src/plugins/navigation_embeddable/server/index.ts
create mode 100644 src/plugins/navigation_embeddable/server/plugin.ts
create mode 100644 src/plugins/navigation_embeddable/server/saved_objects/index.ts
create mode 100644 src/plugins/navigation_embeddable/server/saved_objects/navigation_embeddable.ts
diff --git a/packages/core/saved-objects/core-saved-objects-base-server-internal/src/constants.ts b/packages/core/saved-objects/core-saved-objects-base-server-internal/src/constants.ts
index c4a3018fdfb37..9347ce4fd7f1b 100644
--- a/packages/core/saved-objects/core-saved-objects-base-server-internal/src/constants.ts
+++ b/packages/core/saved-objects/core-saved-objects-base-server-internal/src/constants.ts
@@ -77,6 +77,7 @@ export const DEFAULT_INDEX_TYPES_MAP: IndexTypesMap = {
'ml-module',
'ml-trained-model',
'monitoring-telemetry',
+ 'navigation_embeddable',
'osquery-manager-usage-metric',
'osquery-pack',
'osquery-pack-asset',
diff --git a/packages/core/saved-objects/core-saved-objects-migration-server-internal/src/kibana_migrator_utils.fixtures.ts b/packages/core/saved-objects/core-saved-objects-migration-server-internal/src/kibana_migrator_utils.fixtures.ts
index 9100f489bef42..94780b9abf808 100644
--- a/packages/core/saved-objects/core-saved-objects-migration-server-internal/src/kibana_migrator_utils.fixtures.ts
+++ b/packages/core/saved-objects/core-saved-objects-migration-server-internal/src/kibana_migrator_utils.fixtures.ts
@@ -1504,6 +1504,23 @@ export const INDEX_MAP_BEFORE_SPLIT: IndexMap = {
},
},
},
+ navigation_embeddable: {
+ properties: {
+ id: {
+ type: 'text',
+ },
+ title: {
+ type: 'text',
+ },
+ description: {
+ type: 'text',
+ },
+ links: {
+ dynamic: false,
+ properties: {},
+ },
+ },
+ },
'cases-comments': {
dynamic: false,
properties: {
diff --git a/packages/kbn-check-mappings-update-cli/current_mappings.json b/packages/kbn-check-mappings-update-cli/current_mappings.json
index cb78e060d6edc..169221d659e53 100644
--- a/packages/kbn-check-mappings-update-cli/current_mappings.json
+++ b/packages/kbn-check-mappings-update-cli/current_mappings.json
@@ -1054,6 +1054,23 @@
}
}
},
+ "navigation_embeddable": {
+ "properties": {
+ "id": {
+ "type": "text"
+ },
+ "title": {
+ "type": "text"
+ },
+ "description": {
+ "type": "text"
+ },
+ "links": {
+ "dynamic": false,
+ "properties": {}
+ }
+ }
+ },
"lens": {
"properties": {
"title": {
diff --git a/src/core/server/integration_tests/saved_objects/migrations/group2/check_registered_types.test.ts b/src/core/server/integration_tests/saved_objects/migrations/group2/check_registered_types.test.ts
index c1cfca07017bc..bc6be0a11b2b5 100644
--- a/src/core/server/integration_tests/saved_objects/migrations/group2/check_registered_types.test.ts
+++ b/src/core/server/integration_tests/saved_objects/migrations/group2/check_registered_types.test.ts
@@ -120,6 +120,7 @@ describe('checking migration metadata changes on all registered SO types', () =>
"ml-module": "2225cbb4bd508ea5f69db4b848be9d8a74b60198",
"ml-trained-model": "482195cefd6b04920e539d34d7356d22cb68e4f3",
"monitoring-telemetry": "5d91bf75787d9d4dd2fae954d0b3f76d33d2e559",
+ "navigation_embeddable": "de71a127ed325261ca6bc926d93c4cd676d17a05",
"observability-onboarding-state": "55b112d6a33fedb7c1e4fec4da768d2bcc5fadc2",
"osquery-manager-usage-metric": "983bcbc3b7dda0aad29b20907db233abba709bcc",
"osquery-pack": "6ab4358ca4304a12dcfc1777c8135b75cffb4397",
diff --git a/src/core/server/integration_tests/saved_objects/migrations/group5/dot_kibana_split.test.ts b/src/core/server/integration_tests/saved_objects/migrations/group5/dot_kibana_split.test.ts
index 30888521d651e..99b16aec71079 100644
--- a/src/core/server/integration_tests/saved_objects/migrations/group5/dot_kibana_split.test.ts
+++ b/src/core/server/integration_tests/saved_objects/migrations/group5/dot_kibana_split.test.ts
@@ -240,6 +240,7 @@ describe('split .kibana index into multiple system indices', () => {
"ml-module",
"ml-trained-model",
"monitoring-telemetry",
+ "navigation_embeddable",
"observability-onboarding-state",
"osquery-manager-usage-metric",
"osquery-pack",
diff --git a/src/plugins/dashboard/public/dashboard_actions/replace_panel_flyout.tsx b/src/plugins/dashboard/public/dashboard_actions/replace_panel_flyout.tsx
index 14067f0b6aa68..6f93b08a2708f 100644
--- a/src/plugins/dashboard/public/dashboard_actions/replace_panel_flyout.tsx
+++ b/src/plugins/dashboard/public/dashboard_actions/replace_panel_flyout.tsx
@@ -18,7 +18,6 @@ import {
} from '@kbn/embeddable-plugin/public';
import { Toast } from '@kbn/core/public';
-import { DashboardPanelState } from '../../common';
import { pluginServices } from '../services/plugin_services';
import { dashboardReplacePanelActionStrings } from './_dashboard_actions_strings';
import { DashboardContainer } from '../dashboard_container';
@@ -58,30 +57,15 @@ export class ReplacePanelFlyout extends React.Component {
public onReplacePanel = async (savedObjectId: string, type: string, name: string) => {
const { panelToRemove, container } = this.props;
- const { w, h, x, y } = (container.getInput().panels[panelToRemove.id] as DashboardPanelState)
- .gridData;
- const { id } = await container.addNewEmbeddable(type, {
- savedObjectId,
- });
-
- const { [panelToRemove.id]: omit, ...panels } = container.getInput().panels;
-
- container.updateInput({
- panels: {
- ...panels,
- [id]: {
- ...panels[id],
- gridData: {
- ...(panels[id] as DashboardPanelState).gridData,
- w,
- h,
- x,
- y,
- },
- } as DashboardPanelState,
+ const id = await container.replaceEmbeddable(
+ panelToRemove.id,
+ {
+ savedObjectId,
},
- });
+ type,
+ true
+ );
(container as DashboardContainer).setHighlightPanelId(id);
this.showToast(name);
diff --git a/src/plugins/dashboard/public/dashboard_container/embeddable/api/panel_management.ts b/src/plugins/dashboard/public/dashboard_container/embeddable/api/panel_management.ts
index 7b02001a93c6c..a052a8fef1031 100644
--- a/src/plugins/dashboard/public/dashboard_container/embeddable/api/panel_management.ts
+++ b/src/plugins/dashboard/public/dashboard_container/embeddable/api/panel_management.ts
@@ -47,46 +47,15 @@ export async function replacePanel(
newPanelState: Partial,
generateNewId?: boolean
): Promise {
- let panels;
- let panelId;
-
- if (generateNewId) {
- // replace panel can be called with generateNewId in order to totally destroy and recreate the embeddable
- panelId = uuidv4();
- panels = { ...this.input.panels };
- delete panels[previousPanelState.explicitInput.id];
- panels[panelId] = {
- ...previousPanelState,
- ...newPanelState,
- gridData: {
- ...previousPanelState.gridData,
- i: panelId,
- },
- explicitInput: {
- ...newPanelState.explicitInput,
- id: panelId,
- },
- };
- } else {
- // Because the embeddable type can change, we have to operate at the container level here
- panelId = previousPanelState.explicitInput.id;
- panels = {
- ...this.input.panels,
- [panelId]: {
- ...previousPanelState,
- ...newPanelState,
- gridData: {
- ...previousPanelState.gridData,
- },
- explicitInput: {
- ...newPanelState.explicitInput,
- id: panelId,
- },
- },
- };
- }
-
- await this.updateInput({ panels });
+ const panelId = await this.replaceEmbeddable(
+ previousPanelState.explicitInput.id,
+ {
+ ...newPanelState.explicitInput,
+ id: previousPanelState.explicitInput.id,
+ },
+ newPanelState.type,
+ generateNewId
+ );
return panelId;
}
diff --git a/src/plugins/embeddable/public/lib/actions/edit_panel_action.ts b/src/plugins/embeddable/public/lib/actions/edit_panel_action.ts
index 98e541fd08e6f..2076c8d6f1e7f 100644
--- a/src/plugins/embeddable/public/lib/actions/edit_panel_action.ts
+++ b/src/plugins/embeddable/public/lib/actions/edit_panel_action.ts
@@ -92,7 +92,13 @@ export class EditPanelAction implements Action {
}
const oldExplicitInput = embeddable.getExplicitInput();
- const newExplicitInput = await factory.getExplicitInput(oldExplicitInput, embeddable.parent);
+ let newExplicitInput: Awaited>;
+ try {
+ newExplicitInput = await factory.getExplicitInput(oldExplicitInput, embeddable.parent);
+ } catch (e) {
+ // error likely means user canceled editing
+ return;
+ }
embeddable.parent?.replaceEmbeddable(embeddable.id, newExplicitInput);
return;
}
diff --git a/src/plugins/embeddable/public/lib/containers/container.ts b/src/plugins/embeddable/public/lib/containers/container.ts
index 2f29410bc3e59..df007a93483e4 100644
--- a/src/plugins/embeddable/public/lib/containers/container.ts
+++ b/src/plugins/embeddable/public/lib/containers/container.ts
@@ -176,7 +176,12 @@ export abstract class Container<
EEI extends EmbeddableInput = EmbeddableInput,
EEO extends EmbeddableOutput = EmbeddableOutput,
E extends IEmbeddable = IEmbeddable
- >(id: string, newExplicitInput: Partial, newType?: string) {
+ >(
+ id: string,
+ newExplicitInput: Partial,
+ newType?: string,
+ generateNewId?: boolean
+ ): Promise {
if (!this.input.panels[id]) {
throw new PanelNotFoundError();
}
@@ -186,21 +191,28 @@ export abstract class Container<
if (!factory) {
throw new EmbeddableFactoryNotFoundError(newType);
}
- this.updateInput({
- panels: {
- ...this.input.panels,
- [id]: {
- ...this.input.panels[id],
- explicitInput: { ...newExplicitInput, id },
- type: newType,
- },
- },
- } as Partial);
- } else {
- this.updateInputForChild(id, newExplicitInput);
}
+ const panels = { ...this.input.panels };
+ const oldPanel = panels[id];
+
+ if (generateNewId) {
+ delete panels[id];
+ id = uuidv4();
+ }
+ this.updateInput({
+ panels: {
+ ...panels,
+ [id]: {
+ ...oldPanel,
+ explicitInput: { ...newExplicitInput, id },
+ type: newType ?? oldPanel.type,
+ },
+ },
+ } as Partial);
+
await this.untilEmbeddableLoaded(id);
+ return id;
}
public removeEmbeddable(embeddableId: string) {
diff --git a/src/plugins/embeddable/public/lib/containers/i_container.ts b/src/plugins/embeddable/public/lib/containers/i_container.ts
index 5539f854b24d9..34e7cc0593e64 100644
--- a/src/plugins/embeddable/public/lib/containers/i_container.ts
+++ b/src/plugins/embeddable/public/lib/containers/i_container.ts
@@ -106,6 +106,7 @@ export interface IContainer<
>(
id: string,
newExplicitInput: Partial,
- newType?: string
- ): void;
+ newType?: string,
+ generateNewId?: boolean
+ ): Promise;
}
diff --git a/src/plugins/navigation_embeddable/common/constants.ts b/src/plugins/navigation_embeddable/common/constants.ts
new file mode 100644
index 0000000000000..9731275e04f14
--- /dev/null
+++ b/src/plugins/navigation_embeddable/common/constants.ts
@@ -0,0 +1,19 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import { i18n } from '@kbn/i18n';
+
+export const LATEST_VERSION = 1;
+
+export const CONTENT_ID = 'navigation_embeddable';
+
+export const APP_ICON = 'link';
+
+export const APP_NAME = i18n.translate('navigationEmbeddable.visTypeAlias.title', {
+ defaultMessage: 'Links',
+});
diff --git a/src/plugins/navigation_embeddable/common/content_management/cm_services.ts b/src/plugins/navigation_embeddable/common/content_management/cm_services.ts
new file mode 100644
index 0000000000000..fa050138b35ff
--- /dev/null
+++ b/src/plugins/navigation_embeddable/common/content_management/cm_services.ts
@@ -0,0 +1,21 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import type {
+ ContentManagementServicesDefinition as ServicesDefinition,
+ Version,
+} from '@kbn/object-versioning';
+
+// We export the versioned service definition from this file and not the barrel to avoid adding
+// the schemas in the "public" js bundle
+
+import { serviceDefinition as v1 } from './v1/cm_services';
+
+export const cmServicesDefinition: { [version: Version]: ServicesDefinition } = {
+ 1: v1,
+};
diff --git a/src/plugins/navigation_embeddable/common/content_management/index.ts b/src/plugins/navigation_embeddable/common/content_management/index.ts
new file mode 100644
index 0000000000000..282ba879c17fc
--- /dev/null
+++ b/src/plugins/navigation_embeddable/common/content_management/index.ts
@@ -0,0 +1,23 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+export { LATEST_VERSION, CONTENT_ID } from '../constants';
+
+export type { NavigationEmbeddableContentType } from '../types';
+
+export type {
+ NavigationEmbeddableCrudTypes,
+ NavigationEmbeddableAttributes,
+ NavigationEmbeddableItem,
+ NavigationLinkType,
+ NavigationEmbeddableLink,
+} from './latest';
+
+export { DASHBOARD_LINK_TYPE, EXTERNAL_LINK_TYPE } from './latest';
+
+export * as NavigationEmbeddableV1 from './v1';
diff --git a/src/plugins/navigation_embeddable/common/content_management/latest.ts b/src/plugins/navigation_embeddable/common/content_management/latest.ts
new file mode 100644
index 0000000000000..e9c79f0f50f93
--- /dev/null
+++ b/src/plugins/navigation_embeddable/common/content_management/latest.ts
@@ -0,0 +1,9 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+export * from './v1';
diff --git a/src/plugins/navigation_embeddable/common/content_management/v1/cm_services.ts b/src/plugins/navigation_embeddable/common/content_management/v1/cm_services.ts
new file mode 100644
index 0000000000000..5494a193ba7b5
--- /dev/null
+++ b/src/plugins/navigation_embeddable/common/content_management/v1/cm_services.ts
@@ -0,0 +1,108 @@
+/*
+ * 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 { schema } from '@kbn/config-schema';
+import type { ContentManagementServicesDefinition as ServicesDefinition } from '@kbn/object-versioning';
+import {
+ savedObjectSchema,
+ objectTypeToGetResultSchema,
+ createOptionsSchemas,
+ updateOptionsSchema,
+ createResultSchema,
+} from '@kbn/content-management-utils';
+import { DASHBOARD_LINK_TYPE, EXTERNAL_LINK_TYPE } from '.';
+
+const navigationEmbeddableLinkSchema = schema.object({
+ id: schema.string(),
+ type: schema.oneOf([schema.literal(DASHBOARD_LINK_TYPE), schema.literal(EXTERNAL_LINK_TYPE)]),
+ destination: schema.string(),
+ label: schema.maybe(schema.string()),
+ order: schema.number(),
+});
+
+const navigationEmbeddableAttributesSchema = schema.object(
+ {
+ title: schema.string(),
+ description: schema.maybe(schema.string()),
+ links: schema.maybe(schema.arrayOf(navigationEmbeddableLinkSchema)),
+ },
+ { unknowns: 'forbid' }
+);
+
+const navigationEmbeddableSavedObjectSchema = savedObjectSchema(
+ navigationEmbeddableAttributesSchema
+);
+
+const searchOptionsSchema = schema.maybe(
+ schema.object(
+ {
+ onlyTitle: schema.maybe(schema.boolean()),
+ },
+ { unknowns: 'forbid' }
+ )
+);
+
+const navigationEmbeddableCreateOptionsSchema = schema.object({
+ references: schema.maybe(createOptionsSchemas.references),
+ overwrite: createOptionsSchemas.overwrite,
+});
+
+const navigationEmbeddableUpdateOptionsSchema = schema.object({
+ references: updateOptionsSchema.references,
+});
+
+// Content management service definition.
+// We need it for BWC support between different versions of the content
+export const serviceDefinition: ServicesDefinition = {
+ get: {
+ out: {
+ result: {
+ schema: objectTypeToGetResultSchema(navigationEmbeddableSavedObjectSchema),
+ },
+ },
+ },
+ create: {
+ in: {
+ options: {
+ schema: navigationEmbeddableCreateOptionsSchema,
+ },
+ data: {
+ schema: navigationEmbeddableAttributesSchema,
+ },
+ },
+ out: {
+ result: {
+ schema: createResultSchema(navigationEmbeddableSavedObjectSchema),
+ },
+ },
+ },
+ update: {
+ in: {
+ options: {
+ schema: navigationEmbeddableUpdateOptionsSchema, // same schema as "create"
+ },
+ data: {
+ schema: navigationEmbeddableAttributesSchema,
+ },
+ },
+ },
+ search: {
+ in: {
+ options: {
+ schema: searchOptionsSchema,
+ },
+ },
+ },
+ mSearch: {
+ out: {
+ result: {
+ schema: navigationEmbeddableSavedObjectSchema,
+ },
+ },
+ },
+};
diff --git a/src/plugins/navigation_embeddable/common/content_management/v1/constants.ts b/src/plugins/navigation_embeddable/common/content_management/v1/constants.ts
new file mode 100644
index 0000000000000..00f40932638fe
--- /dev/null
+++ b/src/plugins/navigation_embeddable/common/content_management/v1/constants.ts
@@ -0,0 +1,17 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+/**
+ * Dashboard to dashboard links
+ */
+export const DASHBOARD_LINK_TYPE = 'dashboardLink';
+
+/**
+ * External URL links
+ */
+export const EXTERNAL_LINK_TYPE = 'externalLink';
diff --git a/src/plugins/navigation_embeddable/common/content_management/v1/index.ts b/src/plugins/navigation_embeddable/common/content_management/v1/index.ts
new file mode 100644
index 0000000000000..bedc5a6ff2f08
--- /dev/null
+++ b/src/plugins/navigation_embeddable/common/content_management/v1/index.ts
@@ -0,0 +1,17 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import { NavigationEmbeddableCrudTypes } from './types';
+export type {
+ NavigationEmbeddableCrudTypes,
+ NavigationEmbeddableAttributes,
+ NavigationEmbeddableLink,
+ NavigationLinkType,
+} from './types';
+export type NavigationEmbeddableItem = NavigationEmbeddableCrudTypes['Item'];
+export { DASHBOARD_LINK_TYPE, EXTERNAL_LINK_TYPE } from './constants';
diff --git a/src/plugins/navigation_embeddable/common/content_management/v1/types.ts b/src/plugins/navigation_embeddable/common/content_management/v1/types.ts
new file mode 100644
index 0000000000000..0d1a87a17d148
--- /dev/null
+++ b/src/plugins/navigation_embeddable/common/content_management/v1/types.ts
@@ -0,0 +1,46 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import type {
+ ContentManagementCrudTypes,
+ SavedObjectCreateOptions,
+ SavedObjectUpdateOptions,
+} from '@kbn/content-management-utils';
+import { NavigationEmbeddableContentType } from '../../types';
+import { DASHBOARD_LINK_TYPE, EXTERNAL_LINK_TYPE } from './constants';
+
+export type NavigationEmbeddableCrudTypes = ContentManagementCrudTypes<
+ NavigationEmbeddableContentType,
+ NavigationEmbeddableAttributes,
+ Pick,
+ Pick,
+ {
+ /** Flag to indicate to only search the text on the "title" field */
+ onlyTitle?: boolean;
+ }
+>;
+
+/**
+ * Navigation embeddable explicit input
+ */
+export type NavigationLinkType = typeof DASHBOARD_LINK_TYPE | typeof EXTERNAL_LINK_TYPE;
+
+export interface NavigationEmbeddableLink {
+ id: string;
+ type: NavigationLinkType;
+ destination: string;
+ label?: string;
+ order: number;
+}
+
+// eslint-disable-next-line @typescript-eslint/consistent-type-definitions
+export type NavigationEmbeddableAttributes = {
+ title: string;
+ description?: string;
+ links?: NavigationEmbeddableLink[];
+};
diff --git a/src/plugins/navigation_embeddable/common/index.ts b/src/plugins/navigation_embeddable/common/index.ts
new file mode 100644
index 0000000000000..9cb4fc42124aa
--- /dev/null
+++ b/src/plugins/navigation_embeddable/common/index.ts
@@ -0,0 +1,9 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+export { APP_ICON, APP_NAME, CONTENT_ID, LATEST_VERSION } from './constants';
diff --git a/src/plugins/navigation_embeddable/common/types.ts b/src/plugins/navigation_embeddable/common/types.ts
new file mode 100644
index 0000000000000..e03b4a4dd1469
--- /dev/null
+++ b/src/plugins/navigation_embeddable/common/types.ts
@@ -0,0 +1,19 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import type { SavedObjectsResolveResponse } from '@kbn/core-saved-objects-api-server';
+
+export type NavigationEmbeddableContentType = 'navigation_embeddable';
+
+// TODO does this type need to be versioned?
+export interface SharingSavedObjectProps {
+ outcome: SavedObjectsResolveResponse['outcome'];
+ aliasTargetId?: SavedObjectsResolveResponse['alias_target_id'];
+ aliasPurpose?: SavedObjectsResolveResponse['alias_purpose'];
+ sourceId?: string;
+}
diff --git a/src/plugins/navigation_embeddable/kibana.jsonc b/src/plugins/navigation_embeddable/kibana.jsonc
index 961aacc7641aa..b74e4bbd6f330 100644
--- a/src/plugins/navigation_embeddable/kibana.jsonc
+++ b/src/plugins/navigation_embeddable/kibana.jsonc
@@ -1,14 +1,23 @@
{
"type": "plugin",
- "owner": "@elastic/kibana-presentation",
"id": "@kbn/navigation-embeddable-plugin",
+ "owner": "@elastic/kibana-presentation",
"description": "An embeddable for quickly navigating between dashboards.",
"plugin": {
"id": "navigationEmbeddable",
- "server": false,
+ "server": true,
"browser": true,
- "requiredPlugins": ["dashboard", "embeddable", "kibanaReact", "presentationUtil"],
+ "requiredPlugins": [
+ "contentManagement",
+ "dashboard",
+ "embeddable",
+ "kibanaReact",
+ "presentationUtil"
+ ],
"optionalPlugins": ["triggersActionsUi"],
- "requiredBundles": []
+ "requiredBundles": [
+ "savedObjects"
+ ]
}
}
+
diff --git a/src/plugins/navigation_embeddable/public/components/dashboard_link/dashboard_link_component.tsx b/src/plugins/navigation_embeddable/public/components/dashboard_link/dashboard_link_component.tsx
index db371c426ed4d..60b88a740c14d 100644
--- a/src/plugins/navigation_embeddable/public/components/dashboard_link/dashboard_link_component.tsx
+++ b/src/plugins/navigation_embeddable/public/components/dashboard_link/dashboard_link_component.tsx
@@ -12,13 +12,10 @@ import useAsync from 'react-use/lib/useAsync';
import { EuiButtonEmpty } from '@elastic/eui';
import { DashboardContainer } from '@kbn/dashboard-plugin/public/dashboard_container';
-import {
- DASHBOARD_LINK_TYPE,
- NavigationEmbeddableLink,
- NavigationLinkInfo,
-} from '../../embeddable/types';
+import { NavigationLinkInfo } from '../../embeddable/types';
import { fetchDashboard } from './dashboard_link_tools';
import { useNavigationEmbeddable } from '../../embeddable/navigation_embeddable';
+import { DASHBOARD_LINK_TYPE, NavigationEmbeddableLink } from '../../../common/content_management';
export const DashboardLinkComponent = ({ link }: { link: NavigationEmbeddableLink }) => {
const navEmbeddable = useNavigationEmbeddable();
diff --git a/src/plugins/navigation_embeddable/public/components/external_link/external_link_component.tsx b/src/plugins/navigation_embeddable/public/components/external_link/external_link_component.tsx
index 90bf4066d4c20..7b940ac027357 100644
--- a/src/plugins/navigation_embeddable/public/components/external_link/external_link_component.tsx
+++ b/src/plugins/navigation_embeddable/public/components/external_link/external_link_component.tsx
@@ -9,11 +9,8 @@
import React from 'react';
import { EuiButtonEmpty } from '@elastic/eui';
-import {
- EXTERNAL_LINK_TYPE,
- NavigationLinkInfo,
- NavigationEmbeddableLink,
-} from '../../embeddable/types';
+import { NavigationLinkInfo } from '../../embeddable/types';
+import { EXTERNAL_LINK_TYPE, NavigationEmbeddableLink } from '../../../common/content_management';
export const ExternalLinkComponent = ({ link }: { link: NavigationEmbeddableLink }) => {
return (
diff --git a/src/plugins/navigation_embeddable/public/components/navigation_embeddable_component.tsx b/src/plugins/navigation_embeddable/public/components/navigation_embeddable_component.tsx
index b17be812accbc..a45d6d8028676 100644
--- a/src/plugins/navigation_embeddable/public/components/navigation_embeddable_component.tsx
+++ b/src/plugins/navigation_embeddable/public/components/navigation_embeddable_component.tsx
@@ -10,17 +10,22 @@ import React, { useMemo } from 'react';
import { EuiPanel } from '@elastic/eui';
-import { DASHBOARD_LINK_TYPE } from '../embeddable/types';
+import { DASHBOARD_LINK_TYPE } from '../../common/content_management';
import { useNavigationEmbeddable } from '../embeddable/navigation_embeddable';
import { ExternalLinkComponent } from './external_link/external_link_component';
import { DashboardLinkComponent } from './dashboard_link/dashboard_link_component';
import { memoizedGetOrderedLinkList } from '../editor/navigation_embeddable_editor_tools';
+import { NavigationEmbeddableByValueInput } from '../embeddable/types';
export const NavigationEmbeddableComponent = () => {
const navEmbeddable = useNavigationEmbeddable();
- const links = navEmbeddable.select((state) => state.explicitInput.links);
+ const links = navEmbeddable.select(
+ (state) => (state.explicitInput as NavigationEmbeddableByValueInput).attributes?.links
+ );
+
const orderedLinks = useMemo(() => {
+ if (!links) return [];
return memoizedGetOrderedLinkList(links);
}, [links]);
diff --git a/src/plugins/navigation_embeddable/public/components/navigation_embeddable_link_editor.tsx b/src/plugins/navigation_embeddable/public/components/navigation_embeddable_link_editor.tsx
index 1d5fa98766051..def291c63bcac 100644
--- a/src/plugins/navigation_embeddable/public/components/navigation_embeddable_link_editor.tsx
+++ b/src/plugins/navigation_embeddable/public/components/navigation_embeddable_link_editor.tsx
@@ -28,14 +28,14 @@ import {
} from '@elastic/eui';
import { DashboardContainer } from '@kbn/dashboard-plugin/public/dashboard_container';
+import { NavigationLinkInfo } from '../embeddable/types';
import {
- NavigationLinkInfo,
NavigationLinkType,
EXTERNAL_LINK_TYPE,
DASHBOARD_LINK_TYPE,
NavigationEmbeddableLink,
- DashboardItem,
-} from '../embeddable/types';
+} from '../../common/content_management';
+import { DashboardItem } from '../embeddable/types';
import { NavEmbeddableStrings } from './navigation_embeddable_strings';
import { NavigationEmbeddableUnorderedLink } from '../editor/open_link_editor_flyout';
import { ExternalLinkDestinationPicker } from './external_link/external_link_destination_picker';
@@ -177,7 +177,7 @@ export const NavigationEmbeddableLinkEditor = ({
{/* TODO: As part of https://github.com/elastic/kibana/issues/154381, we should pull in the custom settings for each link type.
Refer to `x-pack/examples/ui_actions_enhanced_examples/public/drilldowns/dashboard_to_discover_drilldown/collect_config_container.tsx`
- for the dashboard drilldown settings, for example.
+ for the dashboard drilldown settings, for example.
Open question: It probably makes sense to re-use these components so any changes made to the drilldown architecture
trickle down to the navigation embeddable - this would require some refactoring, though. Is this a goal for MVP?
diff --git a/src/plugins/navigation_embeddable/public/components/navigation_embeddable_panel_editor.tsx b/src/plugins/navigation_embeddable/public/components/navigation_embeddable_panel_editor.tsx
index f4c2ce6b51492..97a5a86a9d653 100644
--- a/src/plugins/navigation_embeddable/public/components/navigation_embeddable_panel_editor.tsx
+++ b/src/plugins/navigation_embeddable/public/components/navigation_embeddable_panel_editor.tsx
@@ -17,7 +17,6 @@ import {
EuiPanel,
EuiSpacer,
EuiButton,
- EuiToolTip,
EuiFormRow,
EuiFlexItem,
EuiFlexGroup,
@@ -30,55 +29,63 @@ import {
EuiFlyoutHeader,
EuiDragDropContext,
euiDragDropReorder,
+ EuiToolTip,
} from '@elastic/eui';
import { DashboardContainer } from '@kbn/dashboard-plugin/public/dashboard_container';
import { coreServices } from '../services/kibana_services';
-import {
- NavigationEmbeddableLink,
- NavigationEmbeddableInput,
- NavigationEmbeddableLinkList,
-} from '../embeddable/types';
+import { NavigationEmbeddableLink } from '../../common/content_management';
import { NavEmbeddableStrings } from './navigation_embeddable_strings';
import { openLinkEditorFlyout } from '../editor/open_link_editor_flyout';
import { memoizedGetOrderedLinkList } from '../editor/navigation_embeddable_editor_tools';
import { NavigationEmbeddablePanelEditorLink } from './navigation_embeddable_panel_editor_link';
+import { TooltipWrapper } from './tooltip_wrapper';
import noLinksIllustrationDark from '../assets/empty_links_dark.svg';
import noLinksIllustrationLight from '../assets/empty_links_light.svg';
-
import './navigation_embeddable.scss';
const NavigationEmbeddablePanelEditor = ({
- onSave,
+ onSaveToLibrary,
+ onAddToDashboard,
onClose,
- initialInput,
+ initialLinks,
parentDashboard,
+ isByReference,
}: {
+ onSaveToLibrary: (newLinks: NavigationEmbeddableLink[]) => Promise;
+ onAddToDashboard: (newLinks: NavigationEmbeddableLink[]) => void;
onClose: () => void;
+ initialLinks?: NavigationEmbeddableLink[];
parentDashboard?: DashboardContainer;
- initialInput: Partial;
- onSave: (input: Partial) => void;
+ isByReference: boolean;
}) => {
const isDarkTheme = useObservable(coreServices.theme.theme$)?.darkMode;
+ const toasts = coreServices.notifications.toasts;
const editLinkFlyoutRef: React.RefObject = useMemo(() => React.createRef(), []);
const [orderedLinks, setOrderedLinks] = useState([]);
+ const [isSaving, setIsSaving] = useState(false);
+
+ const isEditingExisting = initialLinks || isByReference;
useEffect(() => {
- const { links: initialLinks } = initialInput;
if (!initialLinks) {
setOrderedLinks([]);
return;
}
setOrderedLinks(memoizedGetOrderedLinkList(initialLinks));
- }, [initialInput]);
+ }, [initialLinks]);
const onDragEnd = useCallback(
({ source, destination }) => {
if (source && destination) {
- const newList = euiDragDropReorder(orderedLinks, source.index, destination.index);
+ const newList = euiDragDropReorder(orderedLinks, source.index, destination.index).map(
+ (link, i) => {
+ return { ...link, order: i };
+ }
+ );
setOrderedLinks(newList);
}
},
@@ -121,39 +128,13 @@ const NavigationEmbeddablePanelEditor = ({
[orderedLinks]
);
- const saveButtonComponent = useMemo(() => {
- const canSave = orderedLinks.length !== 0;
-
- const button = (
- {
- const newLinks = orderedLinks.reduce((prev, link, i) => {
- return { ...prev, [link.id]: { ...link, order: i } };
- }, {} as NavigationEmbeddableLinkList);
- onSave({ links: newLinks });
- }}
- >
- {NavEmbeddableStrings.editor.panelEditor.getSaveButtonLabel()}
-
- );
-
- return canSave ? (
- button
- ) : (
-
- {button}
-
- );
- }, [onSave, orderedLinks]);
-
return (
<>
- {initialInput.links && Object.keys(initialInput.links).length > 0
+ {isEditingExisting
? NavEmbeddableStrings.editor.panelEditor.getEditFlyoutTitle()
: NavEmbeddableStrings.editor.panelEditor.getCreateFlyoutTitle()}
@@ -227,11 +208,73 @@ const NavigationEmbeddablePanelEditor = ({
-
+
{NavEmbeddableStrings.editor.getCancelButtonLabel()}
- {saveButtonComponent}
+
+
+ {!isByReference ? (
+
+
+ {
+ onAddToDashboard(orderedLinks);
+ }}
+ >
+ {initialLinks
+ ? NavEmbeddableStrings.editor.panelEditor.getApplyButtonLabel()
+ : NavEmbeddableStrings.editor.panelEditor.getAddToDashboardButtonLabel()}
+
+
+
+ ) : null}
+ {!initialLinks || isByReference ? (
+
+
+ {initialLinks
+ ? NavEmbeddableStrings.editor.panelEditor.getUpdateLibraryItemButtonTooltip()
+ : NavEmbeddableStrings.editor.panelEditor.getSaveToLibraryButtonTooltip()}
+
+ }
+ >
+ {
+ setIsSaving(true);
+ onSaveToLibrary(orderedLinks)
+ .catch((e) => {
+ toasts.addError(e, {
+ title:
+ NavEmbeddableStrings.editor.panelEditor.getErrorDuringSaveToastTitle(),
+ });
+ })
+ .finally(() => {
+ setIsSaving(false);
+ });
+ }}
+ >
+ {initialLinks
+ ? NavEmbeddableStrings.editor.panelEditor.getUpdateLibraryItemButtonLabel()
+ : NavEmbeddableStrings.editor.panelEditor.getSaveToLibraryButtonLabel()}
+
+
+
+ ) : null}
+
+
>
diff --git a/src/plugins/navigation_embeddable/public/components/navigation_embeddable_panel_editor_link.tsx b/src/plugins/navigation_embeddable/public/components/navigation_embeddable_panel_editor_link.tsx
index be65c130222eb..7c6d2b7268102 100644
--- a/src/plugins/navigation_embeddable/public/components/navigation_embeddable_panel_editor_link.tsx
+++ b/src/plugins/navigation_embeddable/public/components/navigation_embeddable_panel_editor_link.tsx
@@ -21,11 +21,8 @@ import {
} from '@elastic/eui';
import { DashboardContainer } from '@kbn/dashboard-plugin/public/dashboard_container';
-import {
- NavigationLinkInfo,
- DASHBOARD_LINK_TYPE,
- NavigationEmbeddableLink,
-} from '../embeddable/types';
+import { NavigationLinkInfo } from '../embeddable/types';
+import { DASHBOARD_LINK_TYPE, NavigationEmbeddableLink } from '../../common/content_management';
import { fetchDashboard } from './dashboard_link/dashboard_link_tools';
import { NavEmbeddableStrings } from './navigation_embeddable_strings';
diff --git a/src/plugins/navigation_embeddable/public/components/navigation_embeddable_strings.ts b/src/plugins/navigation_embeddable/public/components/navigation_embeddable_strings.ts
index 5628c3444d2d4..20953c41dbe8c 100644
--- a/src/plugins/navigation_embeddable/public/components/navigation_embeddable_strings.ts
+++ b/src/plugins/navigation_embeddable/public/components/navigation_embeddable_strings.ts
@@ -47,9 +47,38 @@ export const NavEmbeddableStrings = {
i18n.translate('navigationEmbeddable.panelEditor.editFlyoutTitle', {
defaultMessage: 'Edit links panel',
}),
- getSaveButtonLabel: () =>
- i18n.translate('navigationEmbeddable.panelEditor.saveButtonLabel', {
- defaultMessage: 'Save',
+ getApplyButtonLabel: () =>
+ i18n.translate('navigationEmbeddable.panelEditor.applyButtonLabel', {
+ defaultMessage: 'Apply',
+ }),
+ getAddToDashboardButtonLabel: () =>
+ i18n.translate('navigationEmbeddable.panelEditor.addToDashboardButtonLabel', {
+ defaultMessage: 'Add to dashboard',
+ }),
+ getAddToDashboardButtonTooltip: () =>
+ i18n.translate('navigationEmbeddable.panelEditor.addToDashboardButtonTooltip', {
+ defaultMessage: 'Add this links panel directly to this dashboard.',
+ }),
+ getSaveToLibraryButtonLabel: () =>
+ i18n.translate('navigationEmbeddable.panelEditor.saveToLibraryButtonLabel', {
+ defaultMessage: 'Save to library',
+ }),
+ getSaveToLibraryButtonTooltip: () =>
+ i18n.translate('navigationEmbeddable.panelEditor.saveToLibraryButtonTooltip', {
+ defaultMessage:
+ 'Save this links panel to the library so you can easily add it to other dashboards.',
+ }),
+ getUpdateLibraryItemButtonLabel: () =>
+ i18n.translate('navigationEmbeddable.panelEditor.updateLibraryItemButtonLabel', {
+ defaultMessage: 'Update library item',
+ }),
+ getUpdateLibraryItemButtonTooltip: () =>
+ i18n.translate('navigationEmbeddable.panelEditor.updateLibraryItemButtonTooltip', {
+ defaultMessage: 'Editing this panel might affect other dashboards.',
+ }),
+ getTitleInputLabel: () =>
+ i18n.translate('navigationEmbeddable.panelEditor.titleInputLabel', {
+ defaultMessage: 'Title',
}),
getLinkLoadingAriaLabel: () =>
i18n.translate('navigationEmbeddable.linkEditor.linkLoadingAriaLabel', {
@@ -59,6 +88,10 @@ export const NavEmbeddableStrings = {
i18n.translate('navigationEmbeddable.editor.dragHandleAriaLabel', {
defaultMessage: 'Link drag handle',
}),
+ getErrorDuringSaveToastTitle: () =>
+ i18n.translate('navigationEmbeddable.editor.unableToSaveToastTitle', {
+ defaultMessage: 'Error saving Link panel',
+ }),
},
linkEditor: {
getGoBackAriaLabel: () =>
diff --git a/src/plugins/navigation_embeddable/public/components/tooltip_wrapper.tsx b/src/plugins/navigation_embeddable/public/components/tooltip_wrapper.tsx
new file mode 100644
index 0000000000000..a477c62d3bd9c
--- /dev/null
+++ b/src/plugins/navigation_embeddable/public/components/tooltip_wrapper.tsx
@@ -0,0 +1,35 @@
+/*
+ * 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 { EuiToolTip, EuiToolTipProps } from '@elastic/eui';
+
+type TooltipWrapperProps = Partial> & {
+ tooltipContent: string;
+ /** When the condition is truthy, the tooltip will be shown */
+ condition: boolean;
+};
+
+export const TooltipWrapper: React.FunctionComponent = ({
+ children,
+ condition,
+ tooltipContent,
+ ...tooltipProps
+}) => {
+ return (
+ <>
+ {condition ? (
+
+ <>{children}>
+
+ ) : (
+ children
+ )}
+ >
+ );
+};
diff --git a/src/plugins/navigation_embeddable/public/content_management/duplicate_title_check.ts b/src/plugins/navigation_embeddable/public/content_management/duplicate_title_check.ts
new file mode 100644
index 0000000000000..3115e110467e8
--- /dev/null
+++ b/src/plugins/navigation_embeddable/public/content_management/duplicate_title_check.ts
@@ -0,0 +1,57 @@
+/*
+ * 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 { navigationEmbeddableClient } from './navigation_embeddable_content_management_client.ts';
+
+const rejectErrorMessage = i18n.translate('navigationEmbeddable.saveDuplicateRejectedDescription', {
+ defaultMessage: 'Save with duplicate title confirmation was rejected',
+});
+
+interface Props {
+ title: string;
+ id?: string;
+ onTitleDuplicate: () => void;
+ lastSavedTitle: string;
+ copyOnSave: boolean;
+ isTitleDuplicateConfirmed: boolean;
+}
+
+export const checkForDuplicateTitle = async ({
+ id,
+ title,
+ lastSavedTitle,
+ copyOnSave,
+ isTitleDuplicateConfirmed,
+ onTitleDuplicate,
+}: Props) => {
+ if (isTitleDuplicateConfirmed) {
+ return true;
+ }
+
+ if (title === lastSavedTitle && !copyOnSave) {
+ return true;
+ }
+
+ const { hits } = await navigationEmbeddableClient.search(
+ {
+ text: `"${title}"`,
+ limit: 10,
+ },
+ { onlyTitle: true }
+ );
+
+ const existing = hits.find((obj) => obj.attributes.title.toLowerCase() === title.toLowerCase());
+
+ if (!existing || existing.id === id) {
+ return true;
+ }
+
+ onTitleDuplicate();
+ return Promise.reject(new Error(rejectErrorMessage));
+};
diff --git a/src/plugins/navigation_embeddable/public/content_management/index.ts b/src/plugins/navigation_embeddable/public/content_management/index.ts
new file mode 100644
index 0000000000000..883a28a34ad24
--- /dev/null
+++ b/src/plugins/navigation_embeddable/public/content_management/index.ts
@@ -0,0 +1,10 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 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 { navigationEmbeddableClient } from './navigation_embeddable_content_management_client.ts';
+export { checkForDuplicateTitle } from './duplicate_title_check';
diff --git a/src/plugins/navigation_embeddable/public/content_management/navigation_embeddable_content_management_client.ts.ts b/src/plugins/navigation_embeddable/public/content_management/navigation_embeddable_content_management_client.ts.ts
new file mode 100644
index 0000000000000..f7cb54da23937
--- /dev/null
+++ b/src/plugins/navigation_embeddable/public/content_management/navigation_embeddable_content_management_client.ts.ts
@@ -0,0 +1,83 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import type { SearchQuery } from '@kbn/content-management-plugin/common';
+import type { NavigationEmbeddableCrudTypes } from '../../common/content_management';
+import { CONTENT_ID as contentTypeId } from '../../common';
+import { contentManagement } from '../services/kibana_services';
+
+const get = async (id: string) => {
+ return contentManagement.client.get<
+ NavigationEmbeddableCrudTypes['GetIn'],
+ NavigationEmbeddableCrudTypes['GetOut']
+ >({ contentTypeId, id });
+};
+
+const create = async ({
+ data,
+ options,
+}: Omit) => {
+ const res = await contentManagement.client.create<
+ NavigationEmbeddableCrudTypes['CreateIn'],
+ NavigationEmbeddableCrudTypes['CreateOut']
+ >({
+ contentTypeId,
+ data,
+ options,
+ });
+ return res;
+};
+
+const update = async ({
+ id,
+ data,
+ options,
+}: Omit) => {
+ const res = await contentManagement.client.update<
+ NavigationEmbeddableCrudTypes['UpdateIn'],
+ NavigationEmbeddableCrudTypes['UpdateOut']
+ >({
+ contentTypeId,
+ id,
+ data,
+ options,
+ });
+ return res;
+};
+
+const deleteNavigationEmbeddable = async (id: string) => {
+ await contentManagement.client.delete<
+ NavigationEmbeddableCrudTypes['DeleteIn'],
+ NavigationEmbeddableCrudTypes['DeleteOut']
+ >({
+ contentTypeId,
+ id,
+ });
+};
+
+const search = async (
+ query: SearchQuery = {},
+ options?: NavigationEmbeddableCrudTypes['SearchOptions']
+) => {
+ return contentManagement.client.search<
+ NavigationEmbeddableCrudTypes['SearchIn'],
+ NavigationEmbeddableCrudTypes['SearchOut']
+ >({
+ contentTypeId,
+ query,
+ options,
+ });
+};
+
+export const navigationEmbeddableClient = {
+ get,
+ create,
+ update,
+ delete: deleteNavigationEmbeddable,
+ search,
+};
diff --git a/src/plugins/navigation_embeddable/public/content_management/save_to_library.tsx b/src/plugins/navigation_embeddable/public/content_management/save_to_library.tsx
new file mode 100644
index 0000000000000..31274817c5cb0
--- /dev/null
+++ b/src/plugins/navigation_embeddable/public/content_management/save_to_library.tsx
@@ -0,0 +1,82 @@
+/*
+ * 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 {
+ showSaveModal,
+ OnSaveProps,
+ SavedObjectSaveModal,
+ SaveResult,
+} from '@kbn/saved-objects-plugin/public';
+
+import { APP_NAME } from '../../common';
+import { NavigationEmbeddableAttributes } from '../../common/content_management';
+import {
+ NavigationEmbeddableByReferenceInput,
+ NavigationEmbeddableInput,
+} from '../embeddable/types';
+import { checkForDuplicateTitle } from './duplicate_title_check';
+import { getNavigationEmbeddableAttributeService } from '../services/attribute_service';
+
+export const runSaveToLibrary = async (
+ newAttributes: NavigationEmbeddableAttributes,
+ initialInput: NavigationEmbeddableInput
+): Promise => {
+ return new Promise((resolve) => {
+ const onSave = async ({
+ newTitle,
+ newDescription,
+ onTitleDuplicate,
+ isTitleDuplicateConfirmed,
+ }: OnSaveProps): Promise => {
+ const stateFromSaveModal = {
+ title: newTitle,
+ description: newDescription,
+ };
+
+ if (
+ !(await checkForDuplicateTitle({
+ title: newTitle,
+ lastSavedTitle: newAttributes.title,
+ copyOnSave: false,
+ onTitleDuplicate,
+ isTitleDuplicateConfirmed,
+ }))
+ ) {
+ return {};
+ }
+
+ const stateToSave = {
+ ...newAttributes,
+ ...stateFromSaveModal,
+ };
+
+ const updatedInput = (await getNavigationEmbeddableAttributeService().wrapAttributes(
+ stateToSave,
+ true,
+ initialInput
+ )) as unknown as NavigationEmbeddableByReferenceInput;
+
+ resolve(updatedInput);
+ return { id: updatedInput.savedObjectId };
+ };
+
+ const saveModal = (
+ resolve(undefined)}
+ title={newAttributes.title}
+ description={newAttributes.description}
+ showDescription
+ showCopyOnSave={false}
+ objectType={APP_NAME}
+ />
+ );
+ showSaveModal(saveModal);
+ });
+};
diff --git a/src/plugins/navigation_embeddable/public/editor/navigation_embeddable_editor_tools.tsx b/src/plugins/navigation_embeddable/public/editor/navigation_embeddable_editor_tools.tsx
index 83cc4cfdc7c40..4248af756f525 100644
--- a/src/plugins/navigation_embeddable/public/editor/navigation_embeddable_editor_tools.tsx
+++ b/src/plugins/navigation_embeddable/public/editor/navigation_embeddable_editor_tools.tsx
@@ -7,16 +7,12 @@
*/
import { memoize } from 'lodash';
-import { NavigationEmbeddableLink, NavigationEmbeddableLinkList } from '../embeddable/types';
+import { NavigationEmbeddableLink } from '../../common/content_management';
-const getOrderedLinkList = (links: NavigationEmbeddableLinkList): NavigationEmbeddableLink[] => {
- return Object.keys(links)
- .map((linkId) => {
- return links[linkId];
- })
- .sort((linkA, linkB) => {
- return linkA.order - linkB.order;
- });
+const getOrderedLinkList = (links: NavigationEmbeddableLink[]): NavigationEmbeddableLink[] => {
+ return [...links].sort((linkA, linkB) => {
+ return linkA.order - linkB.order;
+ });
};
/**
@@ -25,10 +21,10 @@ const getOrderedLinkList = (links: NavigationEmbeddableLinkList): NavigationEmbe
* calculated this so, we can get away with using the cached version in the editor
*/
export const memoizedGetOrderedLinkList = memoize(
- (links: NavigationEmbeddableLinkList) => {
+ (links: NavigationEmbeddableLink[]) => {
return getOrderedLinkList(links);
},
- (links) => {
+ (links: NavigationEmbeddableLink[]) => {
return links;
}
);
diff --git a/src/plugins/navigation_embeddable/public/editor/open_editor_flyout.tsx b/src/plugins/navigation_embeddable/public/editor/open_editor_flyout.tsx
index 19edb5fb4f2c8..e156180d8dd4d 100644
--- a/src/plugins/navigation_embeddable/public/editor/open_editor_flyout.tsx
+++ b/src/plugins/navigation_embeddable/public/editor/open_editor_flyout.tsx
@@ -17,8 +17,14 @@ import { toMountPoint } from '@kbn/kibana-react-plugin/public';
import { DashboardContainer } from '@kbn/dashboard-plugin/public/dashboard_container';
import { coreServices } from '../services/kibana_services';
-import { NavigationEmbeddableInput } from '../embeddable/types';
+import {
+ NavigationEmbeddableByReferenceInput,
+ NavigationEmbeddableInput,
+} from '../embeddable/types';
import { memoizedFetchDashboards } from '../components/dashboard_link/dashboard_link_tools';
+import { getNavigationEmbeddableAttributeService } from '../services/attribute_service';
+import { NavigationEmbeddableLink } from '../../common/content_management';
+import { runSaveToLibrary } from '../content_management/save_to_library';
const LazyNavigationEmbeddablePanelEditor = React.lazy(
() => import('../components/navigation_embeddable_panel_editor')
@@ -35,14 +41,42 @@ const NavigationEmbeddablePanelEditor = withSuspense(
* @throws in case user cancels
*/
export async function openEditorFlyout(
- initialInput?: Omit,
+ initialInput: NavigationEmbeddableInput,
parentDashboard?: DashboardContainer
): Promise> {
+ const attributeService = getNavigationEmbeddableAttributeService();
+ const { attributes } = await attributeService.unwrapAttributes(initialInput);
+ const isByReference = attributeService.inputIsRefType(initialInput);
+
return new Promise((resolve, reject) => {
const closed$ = new Subject();
- const onSave = (partialInput: Partial) => {
- resolve(partialInput);
+ const onSaveToLibrary = async (newLinks: NavigationEmbeddableLink[]) => {
+ const newAttributes = {
+ ...attributes,
+ links: newLinks,
+ };
+ const updatedInput = (initialInput as NavigationEmbeddableByReferenceInput).savedObjectId
+ ? await attributeService.wrapAttributes(newAttributes, true, initialInput)
+ : await runSaveToLibrary(newAttributes, initialInput);
+ if (!updatedInput) {
+ return;
+ }
+ resolve(updatedInput);
+ parentDashboard?.reload();
+ editorFlyout.close();
+ };
+
+ const onAddToDashboard = (newLinks: NavigationEmbeddableLink[]) => {
+ const newInput: NavigationEmbeddableInput = {
+ ...initialInput,
+ attributes: {
+ ...attributes,
+ links: newLinks,
+ },
+ };
+ resolve(newInput);
+ parentDashboard?.reload();
editorFlyout.close();
};
@@ -63,10 +97,12 @@ export async function openEditorFlyout(
const editorFlyout = coreServices.overlays.openFlyout(
toMountPoint(
,
{ theme$: coreServices.theme.theme$ }
),
diff --git a/src/plugins/navigation_embeddable/public/editor/open_link_editor_flyout.tsx b/src/plugins/navigation_embeddable/public/editor/open_link_editor_flyout.tsx
index 794b8812d793b..1fee9bdd9b206 100644
--- a/src/plugins/navigation_embeddable/public/editor/open_link_editor_flyout.tsx
+++ b/src/plugins/navigation_embeddable/public/editor/open_link_editor_flyout.tsx
@@ -13,7 +13,7 @@ import { KibanaThemeProvider } from '@kbn/kibana-react-plugin/public';
import { DashboardContainer } from '@kbn/dashboard-plugin/public/dashboard_container';
import { coreServices } from '../services/kibana_services';
-import { NavigationEmbeddableLink } from '../embeddable/types';
+import { NavigationEmbeddableLink } from '../../common/content_management';
import { NavigationEmbeddableLinkEditor } from '../components/navigation_embeddable_link_editor';
export interface LinkEditorProps {
diff --git a/src/plugins/navigation_embeddable/public/embeddable/index.ts b/src/plugins/navigation_embeddable/public/embeddable/index.ts
index 12c60f3ebd004..eeaae05334801 100644
--- a/src/plugins/navigation_embeddable/public/embeddable/index.ts
+++ b/src/plugins/navigation_embeddable/public/embeddable/index.ts
@@ -6,9 +6,6 @@
* Side Public License, v 1.
*/
-export {
- NAVIGATION_EMBEDDABLE_TYPE,
- NavigationEmbeddable as NavigationEmbeddable,
-} from './navigation_embeddable';
+export { NavigationEmbeddable } from './navigation_embeddable';
export type { NavigationEmbeddableFactory } from './navigation_embeddable_factory';
export { NavigationEmbeddableFactoryDefinition } from './navigation_embeddable_factory';
diff --git a/src/plugins/navigation_embeddable/public/embeddable/navigation_embeddable.tsx b/src/plugins/navigation_embeddable/public/embeddable/navigation_embeddable.tsx
index 9f3194d0b376c..58a665488b825 100644
--- a/src/plugins/navigation_embeddable/public/embeddable/navigation_embeddable.tsx
+++ b/src/plugins/navigation_embeddable/public/embeddable/navigation_embeddable.tsx
@@ -7,16 +7,27 @@
*/
import React, { createContext, useContext } from 'react';
-
-import { Embeddable, EmbeddableOutput } from '@kbn/embeddable-plugin/public';
+import { Subscription } from 'rxjs';
+
+import {
+ AttributeService,
+ Embeddable,
+ ReferenceOrValueEmbeddable,
+ SavedObjectEmbeddableInput,
+} from '@kbn/embeddable-plugin/public';
import { DashboardContainer } from '@kbn/dashboard-plugin/public/dashboard_container';
import { ReduxEmbeddableTools, ReduxToolsPackage } from '@kbn/presentation-util-plugin/public';
import { navigationEmbeddableReducers } from './navigation_embeddable_reducers';
-import { NavigationEmbeddableInput, NavigationEmbeddableReduxState } from './types';
+import {
+ NavigationEmbeddableByReferenceInput,
+ NavigationEmbeddableByValueInput,
+ NavigationEmbeddableReduxState,
+} from './types';
import { NavigationEmbeddableComponent } from '../components/navigation_embeddable_component';
-
-export const NAVIGATION_EMBEDDABLE_TYPE = 'navigation';
+import { NavigationEmbeddableInput, NavigationEmbeddableOutput } from './types';
+import { NavigationEmbeddableAttributes } from '../../common/content_management';
+import { CONTENT_ID } from '../../common';
export const NavigationEmbeddableContext = createContext(null);
export const useNavigationEmbeddable = (): NavigationEmbeddable => {
@@ -36,8 +47,19 @@ export interface NavigationEmbeddableConfig {
editable: boolean;
}
-export class NavigationEmbeddable extends Embeddable {
- public readonly type = NAVIGATION_EMBEDDABLE_TYPE;
+export class NavigationEmbeddable
+ extends Embeddable
+ implements
+ ReferenceOrValueEmbeddable<
+ NavigationEmbeddableByValueInput,
+ NavigationEmbeddableByReferenceInput
+ >
+{
+ public readonly type = CONTENT_ID;
+ deferEmbeddableLoad = true;
+
+ private isDestroyed?: boolean;
+ private subscriptions: Subscription = new Subscription();
// state management
public select: NavigationReduxEmbeddableTools['select'];
@@ -51,6 +73,7 @@ export class NavigationEmbeddable extends Embeddable,
parent?: DashboardContainer
) {
super(
@@ -77,17 +100,66 @@ export class NavigationEmbeddable extends Embeddable this.setInitializationFinished())
+ .catch((e: Error) => this.onFatalError(e));
}
- public async reload() {}
+ private async initializeSavedLinks(input: NavigationEmbeddableInput) {
+ const { attributes } = await this.attributeService.unwrapAttributes(input);
+ if (this.isDestroyed) return;
+
+ // TODO handle metaInfo
+
+ this.updateInput({ attributes });
+
+ await this.initializeOutput();
+ }
+
+ private async initializeOutput() {
+ const { attributes } = this.getInput() as NavigationEmbeddableByValueInput;
+ const { title, description } = this.getInput();
+ this.updateOutput({
+ defaultTitle: attributes.title,
+ defaultDescription: attributes.description,
+ title: title ?? attributes.title,
+ description: description ?? attributes.description,
+ });
+ }
+
+ public inputIsRefType(
+ input: NavigationEmbeddableByValueInput | NavigationEmbeddableByReferenceInput
+ ): input is NavigationEmbeddableByReferenceInput {
+ return this.attributeService.inputIsRefType(input);
+ }
+
+ public async getInputAsRefType(): Promise {
+ return this.attributeService.getInputAsRefType(this.getExplicitInput(), {
+ showSaveModal: true,
+ saveModalTitle: this.getTitle(),
+ });
+ }
+
+ public async getInputAsValueType(): Promise {
+ return this.attributeService.getInputAsValueType(this.getExplicitInput());
+ }
+
+ public async reload() {
+ if (this.isDestroyed) return;
+ await this.initializeSavedLinks(this.getInput());
+ this.render();
+ }
public destroy() {
+ this.isDestroyed = true;
super.destroy();
+ this.subscriptions.unsubscribe();
this.cleanupStateTools();
}
public render() {
+ if (this.isDestroyed) return;
return (
diff --git a/src/plugins/navigation_embeddable/public/embeddable/navigation_embeddable_factory.ts b/src/plugins/navigation_embeddable/public/embeddable/navigation_embeddable_factory.ts
index 8a9662492909a..9711c81b64125 100644
--- a/src/plugins/navigation_embeddable/public/embeddable/navigation_embeddable_factory.ts
+++ b/src/plugins/navigation_embeddable/public/embeddable/navigation_embeddable_factory.ts
@@ -6,34 +6,63 @@
* Side Public License, v 1.
*/
-import { isEmpty } from 'lodash';
-
-import { i18n } from '@kbn/i18n';
import {
ACTION_ADD_PANEL,
EmbeddableFactory,
EmbeddableFactoryDefinition,
+ EmbeddablePackageState,
+ ErrorEmbeddable,
} from '@kbn/embeddable-plugin/public';
import { lazyLoadReduxToolsPackage } from '@kbn/presentation-util-plugin/public';
import { DashboardContainer } from '@kbn/dashboard-plugin/public/dashboard_container';
-import { NavigationEmbeddableInput } from './types';
-import { NAVIGATION_EMBEDDABLE_TYPE } from './navigation_embeddable';
+import { EmbeddablePersistableStateService } from '@kbn/embeddable-plugin/common';
+import {
+ NavigationEmbeddableByReferenceInput,
+ NavigationEmbeddableByValueInput,
+ NavigationEmbeddableInput,
+} from './types';
+import type { NavigationEmbeddable } from './navigation_embeddable';
import { coreServices, untilPluginStartServicesReady } from '../services/kibana_services';
+import { getNavigationEmbeddableAttributeService } from '../services/attribute_service';
+import { APP_ICON, APP_NAME, CONTENT_ID } from '../../common';
export type NavigationEmbeddableFactory = EmbeddableFactory;
+export interface NavigationEmbeddableCreationOptions {
+ getInitialInput?: () => Partial;
+ getIncomingEmbeddable?: () => EmbeddablePackageState | undefined;
+}
+
// TODO: Replace string 'OPEN_FLYOUT_ADD_DRILLDOWN' with constant as part of https://github.com/elastic/kibana/issues/154381
-const getDefaultNavigationEmbeddableInput = (): Omit => ({
- links: {},
+const getDefaultNavigationEmbeddableInput = (): Omit => ({
+ attributes: {
+ title: '',
+ },
disabledActions: [ACTION_ADD_PANEL, 'OPEN_FLYOUT_ADD_DRILLDOWN'],
});
export class NavigationEmbeddableFactoryDefinition
implements EmbeddableFactoryDefinition
{
- public readonly type = NAVIGATION_EMBEDDABLE_TYPE;
- public isContainerType = false;
+ public readonly type = CONTENT_ID;
+
+ public readonly isContainerType = false;
+
+ public readonly savedObjectMetaData = {
+ name: APP_NAME,
+ type: CONTENT_ID,
+ getIconForSavedObject: () => APP_ICON,
+ };
+
+ // TODO create functions
+ // public inject: EmbeddablePersistableStateService['inject'];
+ // public extract: EmbeddablePersistableStateService['extract'];
+
+ constructor(persistableStateService: EmbeddablePersistableStateService) {
+ // this.inject = createInject(this.persistableStateService);
+ // this.extract = createExtract(this.persistableStateService);
+ }
public async isEditable() {
await untilPluginStartServicesReady();
@@ -48,12 +77,18 @@ export class NavigationEmbeddableFactoryDefinition
return getDefaultNavigationEmbeddableInput();
}
- public async create(initialInput: NavigationEmbeddableInput, parent: DashboardContainer) {
- if (!initialInput.links || isEmpty(initialInput.links)) {
- // don't create an empty navigation embeddable - it should always have at least one link
- return;
+ public async createFromSavedObject(
+ savedObjectId: string,
+ input: NavigationEmbeddableInput,
+ parent: DashboardContainer
+ ): Promise {
+ if (!(input as NavigationEmbeddableByReferenceInput).savedObjectId) {
+ (input as NavigationEmbeddableByReferenceInput).savedObjectId = savedObjectId;
}
+ return this.create(input, parent);
+ }
+ public async create(initialInput: NavigationEmbeddableInput, parent: DashboardContainer) {
await untilPluginStartServicesReady();
const reduxEmbeddablePackage = await lazyLoadReduxToolsPackage();
@@ -64,33 +99,32 @@ export class NavigationEmbeddableFactoryDefinition
reduxEmbeddablePackage,
{ editable },
{ ...getDefaultNavigationEmbeddableInput(), ...initialInput },
+ getNavigationEmbeddableAttributeService(),
parent
);
}
public async getExplicitInput(
- initialInput?: NavigationEmbeddableInput,
+ initialInput: NavigationEmbeddableInput,
parent?: DashboardContainer
- ) {
+ ): Promise> {
if (!parent) return {};
const { openEditorFlyout } = await import('../editor/open_editor_flyout');
const input = await openEditorFlyout(
- { ...getDefaultNavigationEmbeddableInput(), ...initialInput },
+ {
+ ...getDefaultNavigationEmbeddableInput(),
+ ...initialInput,
+ },
parent
- ).catch(() => {
- // swallow the promise rejection that happens when the flyout is closed
- return {};
- });
+ );
return input;
}
public getDisplayName() {
- return i18n.translate('navigationEmbeddable.navigationEmbeddableFactory.displayName', {
- defaultMessage: 'Links',
- });
+ return APP_NAME;
}
public getIconType() {
diff --git a/src/plugins/navigation_embeddable/public/embeddable/types.ts b/src/plugins/navigation_embeddable/public/embeddable/types.ts
index 0513d50fc8cb8..43d12f611df8b 100644
--- a/src/plugins/navigation_embeddable/public/embeddable/types.ts
+++ b/src/plugins/navigation_embeddable/public/embeddable/types.ts
@@ -6,48 +6,28 @@
* Side Public License, v 1.
*/
-import { DashboardAttributes } from '@kbn/dashboard-plugin/common';
import { ReduxEmbeddableState } from '@kbn/presentation-util-plugin/public';
-import { EmbeddableInput, EmbeddableOutput } from '@kbn/embeddable-plugin/public';
+import {
+ EmbeddableInput,
+ EmbeddableOutput,
+ SavedObjectEmbeddableInput,
+} from '@kbn/embeddable-plugin/public';
+import { DashboardAttributes } from '@kbn/dashboard-plugin/common';
import { ExternalLinkEmbeddableStrings } from '../components/external_link/external_link_strings';
import { DashboardLinkEmbeddableStrings } from '../components/dashboard_link/dashboard_link_strings';
+import {
+ DASHBOARD_LINK_TYPE,
+ EXTERNAL_LINK_TYPE,
+ NavigationLinkType,
+ NavigationEmbeddableAttributes,
+} from '../../common/content_management';
-/**
- * Dashboard to dashboard links
- */
-export const DASHBOARD_LINK_TYPE = 'dashboardLink';
export interface DashboardItem {
id: string;
attributes: DashboardAttributes;
}
-/**
- * External URL links
- */
-export const EXTERNAL_LINK_TYPE = 'externalLink';
-
-/**
- * Navigation embeddable explicit input
- */
-export type NavigationLinkType = typeof DASHBOARD_LINK_TYPE | typeof EXTERNAL_LINK_TYPE;
-
-export interface NavigationEmbeddableLink {
- id: string;
- type: NavigationLinkType;
- destination: string;
- label?: string;
- order: number;
-}
-
-export interface NavigationEmbeddableLinkList {
- [id: string]: NavigationEmbeddableLink;
-}
-
-export interface NavigationEmbeddableInput extends EmbeddableInput {
- links: NavigationEmbeddableLinkList;
-}
-
export const NavigationLinkInfo: {
[id in NavigationLinkType]: { icon: string; displayName: string; description: string };
} = {
@@ -63,6 +43,20 @@ export const NavigationLinkInfo: {
},
};
+export type NavigationEmbeddableByValueInput = {
+ attributes: NavigationEmbeddableAttributes;
+} & EmbeddableInput;
+
+export type NavigationEmbeddableByReferenceInput = SavedObjectEmbeddableInput;
+
+export type NavigationEmbeddableInput =
+ | NavigationEmbeddableByValueInput
+ | NavigationEmbeddableByReferenceInput;
+
+export type NavigationEmbeddableOutput = EmbeddableOutput & {
+ attributes?: NavigationEmbeddableAttributes;
+};
+
/**
* Navigation embeddable redux state
*/
@@ -70,6 +64,6 @@ export const NavigationLinkInfo: {
export type NavigationEmbeddableReduxState = ReduxEmbeddableState<
NavigationEmbeddableInput,
- EmbeddableOutput,
+ NavigationEmbeddableOutput,
{} // We currently don't have any component state - TODO: Replace with `NavigationEmbeddableComponentState` if necessary
>;
diff --git a/src/plugins/navigation_embeddable/public/index.ts b/src/plugins/navigation_embeddable/public/index.ts
index 9cdbcfcc6c667..d1655bd9bc25d 100644
--- a/src/plugins/navigation_embeddable/public/index.ts
+++ b/src/plugins/navigation_embeddable/public/index.ts
@@ -7,11 +7,7 @@
*/
export type { NavigationEmbeddableFactory } from './embeddable';
-export {
- NAVIGATION_EMBEDDABLE_TYPE,
- NavigationEmbeddableFactoryDefinition,
- NavigationEmbeddable,
-} from './embeddable';
+export { NavigationEmbeddableFactoryDefinition, NavigationEmbeddable } from './embeddable';
import { NavigationEmbeddablePlugin } from './plugin';
diff --git a/src/plugins/navigation_embeddable/public/plugin.ts b/src/plugins/navigation_embeddable/public/plugin.ts
index 7a969c0298f7a..8863c4393b058 100644
--- a/src/plugins/navigation_embeddable/public/plugin.ts
+++ b/src/plugins/navigation_embeddable/public/plugin.ts
@@ -6,20 +6,26 @@
* Side Public License, v 1.
*/
-import { DashboardStart } from '@kbn/dashboard-plugin/public';
import { CoreSetup, CoreStart, Plugin } from '@kbn/core/public';
+import {
+ ContentManagementPublicSetup,
+ ContentManagementPublicStart,
+} from '@kbn/content-management-plugin/public';
+import { DashboardStart } from '@kbn/dashboard-plugin/public';
import { EmbeddableSetup, EmbeddableStart } from '@kbn/embeddable-plugin/public';
-
-import { NAVIGATION_EMBEDDABLE_TYPE } from './embeddable';
-import { setKibanaServices } from './services/kibana_services';
import { NavigationEmbeddableFactoryDefinition } from './embeddable';
+import { CONTENT_ID, LATEST_VERSION } from '../common';
+import { APP_NAME } from '../common';
+import { setKibanaServices } from './services/kibana_services';
export interface NavigationEmbeddableSetupDependencies {
embeddable: EmbeddableSetup;
+ contentManagement: ContentManagementPublicSetup;
}
export interface NavigationEmbeddableStartDependencies {
embeddable: EmbeddableStart;
+ contentManagement: ContentManagementPublicStart;
dashboard: DashboardStart;
}
@@ -40,14 +46,23 @@ export class NavigationEmbeddablePlugin
) {
core.getStartServices().then(([_, deps]) => {
plugins.embeddable.registerEmbeddableFactory(
- NAVIGATION_EMBEDDABLE_TYPE,
- new NavigationEmbeddableFactoryDefinition()
+ CONTENT_ID,
+ new NavigationEmbeddableFactoryDefinition(deps.embeddable)
);
+
+ plugins.contentManagement.registry.register({
+ id: CONTENT_ID,
+ version: {
+ latest: LATEST_VERSION,
+ },
+ name: APP_NAME,
+ });
});
}
public start(core: CoreStart, plugins: NavigationEmbeddableStartDependencies) {
setKibanaServices(core, plugins);
+ return {};
}
public stop() {}
diff --git a/src/plugins/navigation_embeddable/public/services/attribute_service.ts b/src/plugins/navigation_embeddable/public/services/attribute_service.ts
new file mode 100644
index 0000000000000..7a7dbfe2bd139
--- /dev/null
+++ b/src/plugins/navigation_embeddable/public/services/attribute_service.ts
@@ -0,0 +1,93 @@
+/*
+ * 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 { Reference } from '@kbn/content-management-utils';
+import { AttributeService } from '@kbn/embeddable-plugin/public';
+import type { OnSaveProps } from '@kbn/saved-objects-plugin/public';
+import { SharingSavedObjectProps } from '../../common/types';
+import { NavigationEmbeddableAttributes } from '../../common/content_management';
+import {
+ NavigationEmbeddableByReferenceInput,
+ NavigationEmbeddableByValueInput,
+} from '../embeddable/types';
+import { embeddableService } from './kibana_services';
+import { checkForDuplicateTitle, navigationEmbeddableClient } from '../content_management';
+import { CONTENT_ID } from '../../common';
+
+export type NavigationEmbeddableDocument = NavigationEmbeddableAttributes & {
+ references?: Reference[];
+};
+
+export interface NavigationEmbeddableUnwrapMetaInfo {
+ sharingSavedObjectProps?: SharingSavedObjectProps;
+}
+
+export type NavigationEmbeddableAttributeService = AttributeService<
+ NavigationEmbeddableDocument,
+ NavigationEmbeddableByValueInput,
+ NavigationEmbeddableByReferenceInput,
+ NavigationEmbeddableUnwrapMetaInfo
+>;
+
+let navigationEmbeddableAttributeService: NavigationEmbeddableAttributeService | null = null;
+export function getNavigationEmbeddableAttributeService(): NavigationEmbeddableAttributeService {
+ if (navigationEmbeddableAttributeService) return navigationEmbeddableAttributeService;
+
+ navigationEmbeddableAttributeService = embeddableService.getAttributeService<
+ NavigationEmbeddableDocument,
+ NavigationEmbeddableByValueInput,
+ NavigationEmbeddableByReferenceInput,
+ NavigationEmbeddableUnwrapMetaInfo
+ >(CONTENT_ID, {
+ saveMethod: async (attributes: NavigationEmbeddableDocument, savedObjectId?: string) => {
+ // TODO extract references
+ const {
+ item: { id },
+ } = await (savedObjectId
+ ? navigationEmbeddableClient.update({ id: savedObjectId, data: attributes })
+ : navigationEmbeddableClient.create({ data: attributes, options: { references: [] } }));
+ return { id };
+ },
+ unwrapMethod: async (
+ savedObjectId: string
+ ): Promise<{
+ attributes: NavigationEmbeddableDocument;
+ metaInfo: NavigationEmbeddableUnwrapMetaInfo;
+ }> => {
+ const {
+ item: savedObject,
+ meta: { outcome, aliasPurpose, aliasTargetId },
+ } = await navigationEmbeddableClient.get(savedObjectId);
+ if (savedObject.error) throw savedObject.error;
+
+ // TODO inject references
+ const attributes = savedObject.attributes;
+ return {
+ attributes,
+ metaInfo: {
+ sharingSavedObjectProps: {
+ aliasTargetId,
+ outcome,
+ aliasPurpose,
+ sourceId: savedObjectId,
+ },
+ },
+ };
+ },
+ checkForDuplicateTitle: (props: OnSaveProps) => {
+ return checkForDuplicateTitle({
+ title: props.newTitle,
+ copyOnSave: false,
+ lastSavedTitle: '',
+ isTitleDuplicateConfirmed: props.isTitleDuplicateConfirmed,
+ onTitleDuplicate: props.onTitleDuplicate,
+ });
+ },
+ });
+ return navigationEmbeddableAttributeService;
+}
diff --git a/src/plugins/navigation_embeddable/public/services/kibana_services.ts b/src/plugins/navigation_embeddable/public/services/kibana_services.ts
index 710c6227a3568..ddc5daad6495a 100644
--- a/src/plugins/navigation_embeddable/public/services/kibana_services.ts
+++ b/src/plugins/navigation_embeddable/public/services/kibana_services.ts
@@ -10,11 +10,15 @@ import { BehaviorSubject } from 'rxjs';
import { CoreStart } from '@kbn/core/public';
import { DashboardStart } from '@kbn/dashboard-plugin/public';
+import { ContentManagementPublicStart } from '@kbn/content-management-plugin/public';
+import { EmbeddableStart } from '@kbn/embeddable-plugin/public';
import { NavigationEmbeddableStartDependencies } from '../plugin';
export let coreServices: CoreStart;
export let dashboardServices: DashboardStart;
+export let embeddableService: EmbeddableStart;
+export let contentManagement: ContentManagementPublicStart;
const servicesReady$ = new BehaviorSubject(false);
@@ -36,6 +40,8 @@ export const setKibanaServices = (
) => {
coreServices = kibanaCore;
dashboardServices = deps.dashboard;
+ embeddableService = deps.embeddable;
+ contentManagement = deps.contentManagement;
servicesReady$.next(true);
};
diff --git a/src/plugins/navigation_embeddable/server/content_management/index.ts b/src/plugins/navigation_embeddable/server/content_management/index.ts
new file mode 100644
index 0000000000000..2376765bcac83
--- /dev/null
+++ b/src/plugins/navigation_embeddable/server/content_management/index.ts
@@ -0,0 +1,9 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+export { NavigationEmbeddableStorage } from './navigation_embeddable_storage';
diff --git a/src/plugins/navigation_embeddable/server/content_management/navigation_embeddable_storage.ts b/src/plugins/navigation_embeddable/server/content_management/navigation_embeddable_storage.ts
new file mode 100644
index 0000000000000..07318f62a3e10
--- /dev/null
+++ b/src/plugins/navigation_embeddable/server/content_management/navigation_embeddable_storage.ts
@@ -0,0 +1,23 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import { SOContentStorage } from '@kbn/content-management-utils';
+import { CONTENT_ID } from '../../common';
+import type { NavigationEmbeddableCrudTypes } from '../../common/content_management';
+import { cmServicesDefinition } from '../../common/content_management/cm_services';
+
+export class NavigationEmbeddableStorage extends SOContentStorage {
+ constructor() {
+ super({
+ savedObjectType: CONTENT_ID,
+ cmServicesDefinition,
+ enableMSearch: true,
+ allowedSavedObjectAttributes: ['id', 'title', 'description', 'links'],
+ });
+ }
+}
diff --git a/src/plugins/navigation_embeddable/server/index.ts b/src/plugins/navigation_embeddable/server/index.ts
new file mode 100644
index 0000000000000..6ececdd95b5d0
--- /dev/null
+++ b/src/plugins/navigation_embeddable/server/index.ts
@@ -0,0 +1,11 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 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 { NavigationEmbeddableServerPlugin } from './plugin';
+
+export const plugin = () => new NavigationEmbeddableServerPlugin();
diff --git a/src/plugins/navigation_embeddable/server/plugin.ts b/src/plugins/navigation_embeddable/server/plugin.ts
new file mode 100644
index 0000000000000..05e3ca79f9971
--- /dev/null
+++ b/src/plugins/navigation_embeddable/server/plugin.ts
@@ -0,0 +1,43 @@
+/*
+ * 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 { CoreSetup, CoreStart, Plugin } from '@kbn/core/server';
+import type { ContentManagementServerSetup } from '@kbn/content-management-plugin/server';
+import { CONTENT_ID, LATEST_VERSION } from '../common';
+import { NavigationEmbeddableAttributes } from '../common/content_management';
+import { NavigationEmbeddableStorage } from './content_management';
+import { navigationEmbeddableSavedObjectType } from './saved_objects';
+
+export class NavigationEmbeddableServerPlugin implements Plugin {
+ public setup(
+ core: CoreSetup,
+ plugins: {
+ contentManagement: ContentManagementServerSetup;
+ }
+ ) {
+ plugins.contentManagement.register({
+ id: CONTENT_ID,
+ storage: new NavigationEmbeddableStorage(),
+ version: {
+ latest: LATEST_VERSION,
+ },
+ });
+
+ core.savedObjects.registerType(
+ navigationEmbeddableSavedObjectType
+ );
+
+ return {};
+ }
+
+ public start(core: CoreStart) {
+ return {};
+ }
+
+ public stop() {}
+}
diff --git a/src/plugins/navigation_embeddable/server/saved_objects/index.ts b/src/plugins/navigation_embeddable/server/saved_objects/index.ts
new file mode 100644
index 0000000000000..1c33d59959426
--- /dev/null
+++ b/src/plugins/navigation_embeddable/server/saved_objects/index.ts
@@ -0,0 +1,9 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+export { navigationEmbeddableSavedObjectType } from './navigation_embeddable';
diff --git a/src/plugins/navigation_embeddable/server/saved_objects/navigation_embeddable.ts b/src/plugins/navigation_embeddable/server/saved_objects/navigation_embeddable.ts
new file mode 100644
index 0000000000000..0a2bfbd97897a
--- /dev/null
+++ b/src/plugins/navigation_embeddable/server/saved_objects/navigation_embeddable.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 type { SavedObjectsType } from '@kbn/core/server';
+import { ANALYTICS_SAVED_OBJECT_INDEX } from '@kbn/core-saved-objects-server';
+import { APP_ICON, CONTENT_ID } from '../../common';
+
+export const navigationEmbeddableSavedObjectType: SavedObjectsType = {
+ name: CONTENT_ID,
+ indexPattern: ANALYTICS_SAVED_OBJECT_INDEX,
+ hidden: false,
+ namespaceType: 'multiple',
+ management: {
+ icon: APP_ICON,
+ defaultSearchField: 'title',
+ importableAndExportable: true,
+ getTitle(obj) {
+ return obj.attributes.title;
+ },
+ },
+ mappings: {
+ properties: {
+ id: { type: 'text' },
+ title: { type: 'text' },
+ description: { type: 'text' },
+ links: {
+ dynamic: false,
+ properties: {},
+ },
+ },
+ },
+ migrations: () => {
+ return {};
+ },
+};
diff --git a/src/plugins/navigation_embeddable/tsconfig.json b/src/plugins/navigation_embeddable/tsconfig.json
index 3c1cee2edb3d7..ae66fa7cc3144 100644
--- a/src/plugins/navigation_embeddable/tsconfig.json
+++ b/src/plugins/navigation_embeddable/tsconfig.json
@@ -11,7 +11,15 @@
"@kbn/embeddable-plugin",
"@kbn/kibana-react-plugin",
"@kbn/presentation-util-plugin",
+ "@kbn/object-versioning",
+ "@kbn/config-schema",
+ "@kbn/content-management-utils",
+ "@kbn/content-management-plugin",
"@kbn/shared-ux-utility",
+ "@kbn/core-saved-objects-api-server",
+ "@kbn/saved-objects-plugin",
+ "@kbn/core-saved-objects-server",
+ "@kbn/saved-objects-plugin",
],
"exclude": ["target/**/*"]
}
From f0ebcb21d7df46c32655e5f39f2a5a945bd5238e Mon Sep 17 00:00:00 2001
From: Hannah Mudge
Date: Wed, 16 Aug 2023 11:19:47 -0600
Subject: [PATCH 08/53] [Dashboard Navigation] Add horizontal/vertical
embeddable rendering + error handling (#162285)
Closes https://github.com/elastic/kibana/issues/154357
Closes https://github.com/elastic/kibana/issues/161563
## Summary
> **Warning**
> I will be waiting to merge this PR until **after**
https://github.com/elastic/kibana/pull/160896 is merged - I am simply
opening it early so that we can start the design review process :+1:
### Layout
This PR improves the rendering of the navigation embeddable to include
both a horizontal and vertical layout option, as well as changing the
style of how the links are rendered:
https://github.com/elastic/kibana/assets/8698078/37d27683-a6c4-4e7a-9589-0eb0fb899e98
A known issue with the horizontal layout is that, as demonstrated in the
above video, a "compact" horizontal navigation panel does not render as
nicely in edit mode versus view mode - this is an **overall panel
problem** and not specifically a problem with the navigation embeddable
(although the navigation embeddable definitely makes it more obvious).
This will be resolved for **all panels** by [removing the panel header
altogether](https://github.com/elastic/kibana/issues/162182).
### Error handling
This PR adds proper error handling to the navigation embeddable so that,
if a dashboard link is "broken" (i.e. the destination dashboard has been
deleted or cannot be fetched), an appropriate error message shows up in
both the component and the editor flyout:
https://github.com/elastic/kibana/assets/8698078/33a3e573-36a2-47ca-b367-3e04f9541ca3
> **Note**
> When possible, we want to provide the user with as much context as
possible for broken dashboard links - that is why, if a dashboard link
was given a custom label, we still show this custom label even when the
destination dashboard has been deleted/is unreachable.
>
> However, once a dashboard has been deleted, we no longer know what the
title of that dashboard was because the saved object no longer exists -
so, if a dashboard link is **not** given a custom label and the
destination dashboard is deleted, we default to the "Error fetching
dashboard" error message instead. In order to create a distinction
between these two scenarios (a broken dashboard link with a custom label
versus without), we italicize the generic "Error fetching dashboard"
error text.
### Improved efficiency
Previously, the navigation embeddable was handling its **own** dashboard
cache, which meant that (a) every single embeddable had its own cache
and (b) the navigation embeddable code had to be mindful when choosing
to use the memoized/cached version of the dashboard versus fetching it
fresh.
After discussing with @ThomThomson about how to better handle this, we
opted to move this logic to the dashboard content management service -
not only does this clean up the navigation embeddable code, it also
improves all the loading of dashboards in general. For example, consider
the following video where I was testing re-loading a previously loaded
dashboard on a throttled `Slow 3G` network:
https://github.com/elastic/kibana/assets/8698078/41d68ac7-557c-4586-a59b-7268086991dd
Notice in the above video how much faster the secondary load of the
dashboard is in comparison to the first initial load - this is because,
in the second load, we can hit the cache instead of re-fetching the
dashboard from the content management client, which allows us to skip an
entire loading state.
### Checklist
- [x] Any text added follows [EUI's writing
guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses
sentence case text and includes [i18n
support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md)
- [ ] ~[Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios~ Will be
addressed in https://github.com/elastic/kibana/issues/161287
- [x] Any UI touched in this PR is usable by keyboard only (learn more
about [keyboard accessibility](https://webaim.org/techniques/keyboard/))
- [x] Any UI touched in this PR does not create any new axe failures
(run axe in browser:
[FF](https://addons.mozilla.org/en-US/firefox/addon/axe-devtools/),
[Chrome](https://chrome.google.com/webstore/detail/axe-web-accessibility-tes/lhdoppojpmngadmnindnejefpokejbdd?hl=en-US))
- [x] This renders correctly on smaller devices using a responsive
layout. (You can test this [in your
browser](https://www.browserstack.com/guide/responsive-testing-on-local-server))
- [x] This was checked for [cross-browser
compatibility](https://www.elastic.co/support/matrix#matrix_browsers)
### For maintainers
- [ ] This was checked for breaking API changes and was [labeled
appropriately](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process)
---------
Co-authored-by: Andrea Del Rio
Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
---
.../current_mappings.json | 1 +
.../dashboard/public/dashboard_constants.ts | 6 +
.../dashboard_content_management.stub.ts | 1 +
.../dashboard_content_management_cache.ts | 40 ++++
.../dashboard_content_management_service.ts | 5 +
.../lib/delete_dashboards.ts | 10 +-
.../lib/find_dashboards.ts | 43 +++-
.../lib/load_dashboard_state.ts | 31 ++-
.../lib/save_dashboard_state.ts | 3 +
.../dashboard_content_management/types.ts | 1 +
.../navigation_embeddable/common/constants.ts | 7 +
.../common/content_management/index.ts | 14 +-
.../content_management/v1/cm_services.ts | 10 +-
.../common/content_management/v1/constants.ts | 8 +-
.../common/content_management/v1/index.ts | 8 +-
.../common/content_management/v1/types.ts | 10 +-
.../navigation_embeddable/public/_mixins.scss | 16 +-
.../dashboard_link_component.tsx | 129 ++++++++---
.../dashboard_link_destination_picker.tsx | 27 ++-
.../dashboard_link/dashboard_link_strings.ts | 14 +-
.../dashboard_link/dashboard_link_tools.tsx | 44 +---
.../navigation_embeddable_editor.scss} | 33 ++-
.../navigation_embeddable_link_editor.tsx | 15 +-
.../navigation_embeddable_panel_editor.tsx | 210 +++++++++---------
...n_embeddable_panel_editor_empty_prompt.tsx | 66 ++++++
...avigation_embeddable_panel_editor_link.tsx | 102 +++++++--
.../external_link/external_link_component.tsx | 18 +-
.../external_link_destination_picker.tsx | 4 +-
.../external_link/external_link_strings.ts | 6 +-
.../navigation_embeddable_component.scss | 58 +++++
.../navigation_embeddable_component.tsx | 64 ++++--
.../navigation_embeddable_strings.ts | 58 ++---
.../public/editor/open_editor_flyout.tsx | 27 ++-
.../public/editor/open_link_editor_flyout.tsx | 2 +-
.../navigation_embeddable_factory.ts | 12 +-
.../public/embeddable/types.ts | 40 +++-
.../navigation_embeddable_storage.ts | 2 +-
.../saved_objects/navigation_embeddable.ts | 1 +
38 files changed, 802 insertions(+), 344 deletions(-)
create mode 100644 src/plugins/dashboard/public/services/dashboard_content_management/dashboard_content_management_cache.ts
rename src/plugins/navigation_embeddable/public/components/{navigation_embeddable.scss => editor/navigation_embeddable_editor.scss} (75%)
rename src/plugins/navigation_embeddable/public/components/{ => editor}/navigation_embeddable_link_editor.tsx (92%)
rename src/plugins/navigation_embeddable/public/components/{ => editor}/navigation_embeddable_panel_editor.tsx (57%)
create mode 100644 src/plugins/navigation_embeddable/public/components/editor/navigation_embeddable_panel_editor_empty_prompt.tsx
rename src/plugins/navigation_embeddable/public/components/{ => editor}/navigation_embeddable_panel_editor_link.tsx (56%)
create mode 100644 src/plugins/navigation_embeddable/public/components/navigation_embeddable_component.scss
diff --git a/packages/kbn-check-mappings-update-cli/current_mappings.json b/packages/kbn-check-mappings-update-cli/current_mappings.json
index 169221d659e53..101c986a30a56 100644
--- a/packages/kbn-check-mappings-update-cli/current_mappings.json
+++ b/packages/kbn-check-mappings-update-cli/current_mappings.json
@@ -1055,6 +1055,7 @@
}
},
"navigation_embeddable": {
+ "dynamic": false,
"properties": {
"id": {
"type": "text"
diff --git a/src/plugins/dashboard/public/dashboard_constants.ts b/src/plugins/dashboard/public/dashboard_constants.ts
index 572d1b9d0f11a..1764f55a176e7 100644
--- a/src/plugins/dashboard/public/dashboard_constants.ts
+++ b/src/plugins/dashboard/public/dashboard_constants.ts
@@ -65,8 +65,14 @@ export const DEFAULT_PANEL_WIDTH = DASHBOARD_GRID_COLUMN_COUNT / 2;
export const CHANGE_CHECK_DEBOUNCE = 100;
+// ------------------------------------------------------------------
+// Content Management
+// ------------------------------------------------------------------
export { CONTENT_ID as DASHBOARD_CONTENT_ID } from '../common/content_management/constants';
+export const DASHBOARD_CACHE_SIZE = 20; // only store a max of 20 dashboards
+export const DASHBOARD_CACHE_TTL = 1000 * 60 * 5; // time to live = 5 minutes
+
// ------------------------------------------------------------------
// Default State
// ------------------------------------------------------------------
diff --git a/src/plugins/dashboard/public/services/dashboard_content_management/dashboard_content_management.stub.ts b/src/plugins/dashboard/public/services/dashboard_content_management/dashboard_content_management.stub.ts
index 9bb54da53653d..b4915ff67d0ba 100644
--- a/src/plugins/dashboard/public/services/dashboard_content_management/dashboard_content_management.stub.ts
+++ b/src/plugins/dashboard/public/services/dashboard_content_management/dashboard_content_management.stub.ts
@@ -44,6 +44,7 @@ export const dashboardContentManagementServiceFactory: DashboardContentManagemen
hits,
});
}),
+ findById: jest.fn(),
findByIds: jest.fn().mockImplementation(() =>
Promise.resolve([
{
diff --git a/src/plugins/dashboard/public/services/dashboard_content_management/dashboard_content_management_cache.ts b/src/plugins/dashboard/public/services/dashboard_content_management/dashboard_content_management_cache.ts
new file mode 100644
index 0000000000000..20b9e5a9cb2a7
--- /dev/null
+++ b/src/plugins/dashboard/public/services/dashboard_content_management/dashboard_content_management_cache.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 LRUCache from 'lru-cache';
+import { DashboardCrudTypes } from '../../../common/content_management';
+import { DASHBOARD_CACHE_SIZE, DASHBOARD_CACHE_TTL } from '../../dashboard_constants';
+
+export class DashboardContentManagementCache {
+ private cache: LRUCache;
+
+ constructor() {
+ this.cache = new LRUCache({
+ max: DASHBOARD_CACHE_SIZE,
+ maxAge: DASHBOARD_CACHE_TTL,
+ });
+ }
+
+ /** Fetch the dashboard with `id` from the cache */
+ public fetchDashboard(id: string) {
+ return this.cache.get(id);
+ }
+
+ /** Add the fetched dashboard to the cache */
+ public addDashboard({ item: dashboard, meta }: DashboardCrudTypes['GetOut']) {
+ this.cache.set(dashboard.id, {
+ meta,
+ item: dashboard,
+ });
+ }
+
+ /** Delete the dashboard with `id` from the cache */
+ public deleteDashboard(id: string) {
+ this.cache.del(id);
+ }
+}
diff --git a/src/plugins/dashboard/public/services/dashboard_content_management/dashboard_content_management_service.ts b/src/plugins/dashboard/public/services/dashboard_content_management/dashboard_content_management_service.ts
index f16bd4442f2c1..69ac2488ff47b 100644
--- a/src/plugins/dashboard/public/services/dashboard_content_management/dashboard_content_management_service.ts
+++ b/src/plugins/dashboard/public/services/dashboard_content_management/dashboard_content_management_service.ts
@@ -13,6 +13,7 @@ import { checkForDuplicateDashboardTitle } from './lib/check_for_duplicate_dashb
import {
searchDashboards,
+ findDashboardById,
findDashboardsByIds,
findDashboardIdByTitle,
} from './lib/find_dashboards';
@@ -23,6 +24,7 @@ import type {
} from './types';
import { loadDashboardState } from './lib/load_dashboard_state';
import { deleteDashboards } from './lib/delete_dashboards';
+import { DashboardContentManagementCache } from './dashboard_content_management_cache';
export type DashboardContentManagementServiceFactory = KibanaPluginServiceFactory<
DashboardContentManagementService,
@@ -30,6 +32,8 @@ export type DashboardContentManagementServiceFactory = KibanaPluginServiceFactor
DashboardContentManagementRequiredServices
>;
+export const dashboardContentManagementCache = new DashboardContentManagementCache();
+
export const dashboardContentManagementServiceFactory: DashboardContentManagementServiceFactory = (
{ startPlugins: { contentManagement } },
requiredServices
@@ -74,6 +78,7 @@ export const dashboardContentManagementServiceFactory: DashboardContentManagemen
search,
size,
}),
+ findById: (id) => findDashboardById(contentManagement, id),
findByIds: (ids) => findDashboardsByIds(contentManagement, ids),
findByTitle: (title) => findDashboardIdByTitle(contentManagement, title),
},
diff --git a/src/plugins/dashboard/public/services/dashboard_content_management/lib/delete_dashboards.ts b/src/plugins/dashboard/public/services/dashboard_content_management/lib/delete_dashboards.ts
index e18841eacfcfd..cf861a02b45e9 100644
--- a/src/plugins/dashboard/public/services/dashboard_content_management/lib/delete_dashboards.ts
+++ b/src/plugins/dashboard/public/services/dashboard_content_management/lib/delete_dashboards.ts
@@ -9,20 +9,22 @@
import { DashboardStartDependencies } from '../../../plugin';
import { DASHBOARD_CONTENT_ID } from '../../../dashboard_constants';
import { DashboardCrudTypes } from '../../../../common/content_management';
+import { dashboardContentManagementCache } from '../dashboard_content_management_service';
export const deleteDashboards = async (
ids: string[],
contentManagement: DashboardStartDependencies['contentManagement']
) => {
- const deletePromises = ids.map((id) =>
- contentManagement.client.delete<
+ const deletePromises = ids.map((id) => {
+ dashboardContentManagementCache.deleteDashboard(id);
+ return contentManagement.client.delete<
DashboardCrudTypes['DeleteIn'],
DashboardCrudTypes['DeleteOut']
>({
contentTypeId: DASHBOARD_CONTENT_ID,
id,
- })
- );
+ });
+ });
await Promise.all(deletePromises);
};
diff --git a/src/plugins/dashboard/public/services/dashboard_content_management/lib/find_dashboards.ts b/src/plugins/dashboard/public/services/dashboard_content_management/lib/find_dashboards.ts
index 49ffee54d536b..85f0cf4f53394 100644
--- a/src/plugins/dashboard/public/services/dashboard_content_management/lib/find_dashboards.ts
+++ b/src/plugins/dashboard/public/services/dashboard_content_management/lib/find_dashboards.ts
@@ -15,6 +15,7 @@ import {
} from '../../../../common/content_management';
import { DashboardStartDependencies } from '../../../plugin';
import { DASHBOARD_CONTENT_ID } from '../../../dashboard_constants';
+import { dashboardContentManagementCache } from '../dashboard_content_management_service';
export interface SearchDashboardsArgs {
contentManagement: DashboardStartDependencies['contentManagement'];
@@ -67,23 +68,41 @@ export type FindDashboardsByIdResponse = { id: string } & (
| { status: 'error'; error: SavedObjectError }
);
-export async function findDashboardsByIds(
+export async function findDashboardById(
contentManagement: DashboardStartDependencies['contentManagement'],
- ids: string[]
-): Promise {
- const findPromises = ids.map((id) =>
- contentManagement.client.get({
+ id: string
+): Promise {
+ /** If the dashboard exists in the cache, then return the result from that */
+ const cachedDashboard = dashboardContentManagementCache.fetchDashboard(id);
+ if (cachedDashboard) {
+ return {
+ id,
+ status: 'success',
+ attributes: cachedDashboard.item.attributes,
+ };
+ }
+ /** Otherwise, fetch the dashboard from the content management client, add it to the cache, and return the result */
+ const response = await contentManagement.client
+ .get({
contentTypeId: DASHBOARD_CONTENT_ID,
id,
})
- );
- const results = await Promise.all(findPromises);
+ .then((result) => {
+ dashboardContentManagementCache.addDashboard(result);
+ return { id, status: 'success', attributes: result.item.attributes };
+ })
+ .catch((e) => ({ status: 'error', error: e.body, id }));
- return results.map((result) => {
- if (result.item.error) return { status: 'error', error: result.item.error, id: result.item.id };
- const { attributes, id } = result.item;
- return { id, status: 'success', attributes };
- });
+ return response as FindDashboardsByIdResponse;
+}
+
+export async function findDashboardsByIds(
+ contentManagement: DashboardStartDependencies['contentManagement'],
+ ids: string[]
+): Promise {
+ const findPromises = ids.map((id) => findDashboardById(contentManagement, id));
+ const results = await Promise.all(findPromises);
+ return results as FindDashboardsByIdResponse[];
}
export async function findDashboardIdByTitle(
diff --git a/src/plugins/dashboard/public/services/dashboard_content_management/lib/load_dashboard_state.ts b/src/plugins/dashboard/public/services/dashboard_content_management/lib/load_dashboard_state.ts
index 538162a8eacbc..835dab964779d 100644
--- a/src/plugins/dashboard/public/services/dashboard_content_management/lib/load_dashboard_state.ts
+++ b/src/plugins/dashboard/public/services/dashboard_content_management/lib/load_dashboard_state.ts
@@ -23,6 +23,7 @@ import {
import { DashboardCrudTypes } from '../../../../common/content_management';
import type { LoadDashboardFromSavedObjectProps, LoadDashboardReturn } from '../types';
import { DASHBOARD_CONTENT_ID, DEFAULT_DASHBOARD_INPUT } from '../../../dashboard_constants';
+import { dashboardContentManagementCache } from '../dashboard_content_management_service';
export function migrateLegacyQuery(query: Query | { [key: string]: any } | string): Query {
// Lucene was the only option before, so language-less queries are all lucene
@@ -58,14 +59,28 @@ export const loadDashboardState = async ({
/**
* Load the saved object from Content Management
*/
- const { item: rawDashboardContent, meta: resolveMeta } = await contentManagement.client
- .get({
- contentTypeId: DASHBOARD_CONTENT_ID,
- id,
- })
- .catch((e) => {
- throw new SavedObjectNotFound(DASHBOARD_CONTENT_ID, id);
- });
+ let rawDashboardContent;
+ let resolveMeta;
+
+ const cachedDashboard = dashboardContentManagementCache.fetchDashboard(id);
+ if (cachedDashboard) {
+ /** If the dashboard exists in the cache, use the cached version to load the dashboard */
+ ({ item: rawDashboardContent, meta: resolveMeta } = cachedDashboard);
+ } else {
+ /** Otherwise, fetch and load the dashboard from the content management client, and add it to the cache */
+ const result = await contentManagement.client
+ .get({
+ contentTypeId: DASHBOARD_CONTENT_ID,
+ id,
+ })
+ .catch((e) => {
+ throw new SavedObjectNotFound(DASHBOARD_CONTENT_ID, id);
+ });
+
+ dashboardContentManagementCache.addDashboard(result);
+ ({ item: rawDashboardContent, meta: resolveMeta } = result);
+ }
+
if (!rawDashboardContent || !rawDashboardContent.version) {
return {
dashboardInput: newDashboardState,
diff --git a/src/plugins/dashboard/public/services/dashboard_content_management/lib/save_dashboard_state.ts b/src/plugins/dashboard/public/services/dashboard_content_management/lib/save_dashboard_state.ts
index 60d1a0f8972e0..aef2d01f44e52 100644
--- a/src/plugins/dashboard/public/services/dashboard_content_management/lib/save_dashboard_state.ts
+++ b/src/plugins/dashboard/public/services/dashboard_content_management/lib/save_dashboard_state.ts
@@ -31,6 +31,7 @@ import { DashboardStartDependencies } from '../../../plugin';
import { DASHBOARD_CONTENT_ID } from '../../../dashboard_constants';
import { DashboardCrudTypes, DashboardAttributes } from '../../../../common/content_management';
import { dashboardSaveToastStrings } from '../../../dashboard_container/_dashboard_container_strings';
+import { dashboardContentManagementCache } from '../dashboard_content_management_service';
export const serializeControlGroupInput = (
controlGroupInput: DashboardContainerInput['controlGroupInput']
@@ -200,6 +201,8 @@ export const saveDashboardState = async ({
if (newId !== lastSavedId) {
dashboardSessionStorage.clearState(lastSavedId);
return { redirectRequired: true, id: newId };
+ } else {
+ dashboardContentManagementCache.deleteDashboard(newId); // something changed in an existing dashboard, so delete it from the cache so that it can be re-fetched
}
}
return { id: newId };
diff --git a/src/plugins/dashboard/public/services/dashboard_content_management/types.ts b/src/plugins/dashboard/public/services/dashboard_content_management/types.ts
index 858d5800961b5..35874d3df1fd1 100644
--- a/src/plugins/dashboard/public/services/dashboard_content_management/types.ts
+++ b/src/plugins/dashboard/public/services/dashboard_content_management/types.ts
@@ -92,6 +92,7 @@ export interface FindDashboardsService {
'hasReference' | 'hasNoReference' | 'search' | 'size' | 'options'
>
) => Promise;
+ findById: (id: string) => Promise;
findByIds: (ids: string[]) => Promise;
findByTitle: (title: string) => Promise<{ id: string } | undefined>;
}
diff --git a/src/plugins/navigation_embeddable/common/constants.ts b/src/plugins/navigation_embeddable/common/constants.ts
index 9731275e04f14..d3b25a888a084 100644
--- a/src/plugins/navigation_embeddable/common/constants.ts
+++ b/src/plugins/navigation_embeddable/common/constants.ts
@@ -17,3 +17,10 @@ export const APP_ICON = 'link';
export const APP_NAME = i18n.translate('navigationEmbeddable.visTypeAlias.title', {
defaultMessage: 'Links',
});
+
+export const EMBEDDABLE_DISPLAY_NAME = i18n.translate(
+ 'navigationEmbeddable.embeddableDisplayName',
+ {
+ defaultMessage: 'links',
+ }
+);
diff --git a/src/plugins/navigation_embeddable/common/content_management/index.ts b/src/plugins/navigation_embeddable/common/content_management/index.ts
index 282ba879c17fc..7b26870c7ce53 100644
--- a/src/plugins/navigation_embeddable/common/content_management/index.ts
+++ b/src/plugins/navigation_embeddable/common/content_management/index.ts
@@ -11,13 +11,19 @@ export { LATEST_VERSION, CONTENT_ID } from '../constants';
export type { NavigationEmbeddableContentType } from '../types';
export type {
- NavigationEmbeddableCrudTypes,
- NavigationEmbeddableAttributes,
- NavigationEmbeddableItem,
NavigationLinkType,
+ NavigationLayoutType,
NavigationEmbeddableLink,
+ NavigationEmbeddableItem,
+ NavigationEmbeddableCrudTypes,
+ NavigationEmbeddableAttributes,
} from './latest';
-export { DASHBOARD_LINK_TYPE, EXTERNAL_LINK_TYPE } from './latest';
+export {
+ EXTERNAL_LINK_TYPE,
+ DASHBOARD_LINK_TYPE,
+ NAV_VERTICAL_LAYOUT,
+ NAV_HORIZONTAL_LAYOUT,
+} from './latest';
export * as NavigationEmbeddableV1 from './v1';
diff --git a/src/plugins/navigation_embeddable/common/content_management/v1/cm_services.ts b/src/plugins/navigation_embeddable/common/content_management/v1/cm_services.ts
index 5494a193ba7b5..3c9c7a1bb759c 100644
--- a/src/plugins/navigation_embeddable/common/content_management/v1/cm_services.ts
+++ b/src/plugins/navigation_embeddable/common/content_management/v1/cm_services.ts
@@ -10,12 +10,13 @@ import { schema } from '@kbn/config-schema';
import type { ContentManagementServicesDefinition as ServicesDefinition } from '@kbn/object-versioning';
import {
savedObjectSchema,
- objectTypeToGetResultSchema,
- createOptionsSchemas,
- updateOptionsSchema,
createResultSchema,
+ updateOptionsSchema,
+ createOptionsSchemas,
+ objectTypeToGetResultSchema,
} from '@kbn/content-management-utils';
import { DASHBOARD_LINK_TYPE, EXTERNAL_LINK_TYPE } from '.';
+import { NAV_HORIZONTAL_LAYOUT, NAV_VERTICAL_LAYOUT } from './constants';
const navigationEmbeddableLinkSchema = schema.object({
id: schema.string(),
@@ -30,6 +31,9 @@ const navigationEmbeddableAttributesSchema = schema.object(
title: schema.string(),
description: schema.maybe(schema.string()),
links: schema.maybe(schema.arrayOf(navigationEmbeddableLinkSchema)),
+ layout: schema.maybe(
+ schema.oneOf([schema.literal(NAV_HORIZONTAL_LAYOUT), schema.literal(NAV_VERTICAL_LAYOUT)])
+ ),
},
{ unknowns: 'forbid' }
);
diff --git a/src/plugins/navigation_embeddable/common/content_management/v1/constants.ts b/src/plugins/navigation_embeddable/common/content_management/v1/constants.ts
index 00f40932638fe..70f1af5c0f69d 100644
--- a/src/plugins/navigation_embeddable/common/content_management/v1/constants.ts
+++ b/src/plugins/navigation_embeddable/common/content_management/v1/constants.ts
@@ -7,11 +7,13 @@
*/
/**
- * Dashboard to dashboard links
+ * Link types
*/
export const DASHBOARD_LINK_TYPE = 'dashboardLink';
+export const EXTERNAL_LINK_TYPE = 'externalLink';
/**
- * External URL links
+ * Layout options
*/
-export const EXTERNAL_LINK_TYPE = 'externalLink';
+export const NAV_HORIZONTAL_LAYOUT = 'horizontal';
+export const NAV_VERTICAL_LAYOUT = 'vertical';
diff --git a/src/plugins/navigation_embeddable/common/content_management/v1/index.ts b/src/plugins/navigation_embeddable/common/content_management/v1/index.ts
index bedc5a6ff2f08..efda7e1cf696c 100644
--- a/src/plugins/navigation_embeddable/common/content_management/v1/index.ts
+++ b/src/plugins/navigation_embeddable/common/content_management/v1/index.ts
@@ -11,7 +11,13 @@ export type {
NavigationEmbeddableCrudTypes,
NavigationEmbeddableAttributes,
NavigationEmbeddableLink,
+ NavigationLayoutType,
NavigationLinkType,
} from './types';
export type NavigationEmbeddableItem = NavigationEmbeddableCrudTypes['Item'];
-export { DASHBOARD_LINK_TYPE, EXTERNAL_LINK_TYPE } from './constants';
+export {
+ EXTERNAL_LINK_TYPE,
+ DASHBOARD_LINK_TYPE,
+ NAV_VERTICAL_LAYOUT,
+ NAV_HORIZONTAL_LAYOUT,
+} from './constants';
diff --git a/src/plugins/navigation_embeddable/common/content_management/v1/types.ts b/src/plugins/navigation_embeddable/common/content_management/v1/types.ts
index 0d1a87a17d148..bb5c6c10c584b 100644
--- a/src/plugins/navigation_embeddable/common/content_management/v1/types.ts
+++ b/src/plugins/navigation_embeddable/common/content_management/v1/types.ts
@@ -12,7 +12,12 @@ import type {
SavedObjectUpdateOptions,
} from '@kbn/content-management-utils';
import { NavigationEmbeddableContentType } from '../../types';
-import { DASHBOARD_LINK_TYPE, EXTERNAL_LINK_TYPE } from './constants';
+import {
+ DASHBOARD_LINK_TYPE,
+ EXTERNAL_LINK_TYPE,
+ NAV_HORIZONTAL_LAYOUT,
+ NAV_VERTICAL_LAYOUT,
+} from './constants';
export type NavigationEmbeddableCrudTypes = ContentManagementCrudTypes<
NavigationEmbeddableContentType,
@@ -38,9 +43,12 @@ export interface NavigationEmbeddableLink {
order: number;
}
+export type NavigationLayoutType = typeof NAV_HORIZONTAL_LAYOUT | typeof NAV_VERTICAL_LAYOUT;
+
// eslint-disable-next-line @typescript-eslint/consistent-type-definitions
export type NavigationEmbeddableAttributes = {
title: string;
description?: string;
links?: NavigationEmbeddableLink[];
+ layout?: NavigationLayoutType;
};
diff --git a/src/plugins/navigation_embeddable/public/_mixins.scss b/src/plugins/navigation_embeddable/public/_mixins.scss
index f327bc1fe73d7..cc9b7a5168d80 100644
--- a/src/plugins/navigation_embeddable/public/_mixins.scss
+++ b/src/plugins/navigation_embeddable/public/_mixins.scss
@@ -26,17 +26,13 @@
@mixin euiFlyout {
@include kibanaFullBodyHeight();
- border-left: $euiBorderThin;
position: fixed;
- z-index: $euiZFlyout;
- background: $euiColorEmptyShade;
display: flex;
- flex-direction: column;
- align-items: stretch;
inline-size: 50vw;
-
- @media only screen and (max-width: 767px) {
- inline-size: $euiSizeXL * 13; // 424px
- max-inline-size: 90vw;
- }
+ z-index: $euiZFlyout;
+ align-items: stretch;
+ flex-direction: column;
+ border-left: $euiBorderThin;
+ background: $euiColorEmptyShade;
+ min-width: ($euiSizeXL * 13) + $euiSizeS; // 424px
}
\ No newline at end of file
diff --git a/src/plugins/navigation_embeddable/public/components/dashboard_link/dashboard_link_component.tsx b/src/plugins/navigation_embeddable/public/components/dashboard_link/dashboard_link_component.tsx
index 60b88a740c14d..8259d98cce4b6 100644
--- a/src/plugins/navigation_embeddable/public/components/dashboard_link/dashboard_link_component.tsx
+++ b/src/plugins/navigation_embeddable/public/components/dashboard_link/dashboard_link_component.tsx
@@ -6,52 +6,127 @@
* Side Public License, v 1.
*/
-import React from 'react';
+import classNames from 'classnames';
import useAsync from 'react-use/lib/useAsync';
+import React, { useMemo, useState } from 'react';
-import { EuiButtonEmpty } from '@elastic/eui';
+import { EuiButtonEmpty, EuiListGroupItem, EuiToolTip } from '@elastic/eui';
import { DashboardContainer } from '@kbn/dashboard-plugin/public/dashboard_container';
-import { NavigationLinkInfo } from '../../embeddable/types';
+import {
+ NavigationEmbeddableLink,
+ NavigationLayoutType,
+ NAV_VERTICAL_LAYOUT,
+} from '../../../common/content_management';
import { fetchDashboard } from './dashboard_link_tools';
+import { DashboardLinkStrings } from './dashboard_link_strings';
import { useNavigationEmbeddable } from '../../embeddable/navigation_embeddable';
-import { DASHBOARD_LINK_TYPE, NavigationEmbeddableLink } from '../../../common/content_management';
-export const DashboardLinkComponent = ({ link }: { link: NavigationEmbeddableLink }) => {
+export const DashboardLinkComponent = ({
+ link,
+ layout,
+}: {
+ link: NavigationEmbeddableLink;
+ layout: NavigationLayoutType;
+}) => {
const navEmbeddable = useNavigationEmbeddable();
+ const [error, setError] = useState();
const dashboardContainer = navEmbeddable.parent as DashboardContainer;
const parentDashboardTitle = dashboardContainer.select((state) => state.explicitInput.title);
+ const parentDashboardDescription = dashboardContainer.select(
+ (state) => state.explicitInput.description
+ );
+
const parentDashboardId = dashboardContainer.select((state) => state.componentState.lastSavedId);
const { loading: loadingDestinationDashboard, value: destinationDashboard } =
useAsync(async () => {
- if (!link.label && link.id !== parentDashboardId) {
+ if (link.id !== parentDashboardId) {
/**
- * only fetch the dashboard if **absolutely** necessary; i.e. only if the dashboard link doesn't have
- * some custom label, and if it's not the current dashboard (if it is, use `dashboardContainer` instead)
+ * only fetch the dashboard if it's not the current dashboard - if it is the current dashboard,
+ * use `dashboardContainer` and its corresponding state (title, description, etc.) instead.
*/
- return await fetchDashboard(link.destination);
+ const dashboard = await fetchDashboard(link.destination)
+ .then((result) => {
+ setError(undefined);
+ return result;
+ })
+ .catch((e) => setError(e));
+ return dashboard;
}
}, [link, parentDashboardId]);
- return (
- {}, // TODO: As part of https://github.com/elastic/kibana/issues/154381, connect to drilldown
- })}
- >
- {link.label ||
- (link.destination === parentDashboardId
- ? parentDashboardTitle
- : destinationDashboard?.attributes.title)}
-
+ const [dashboardTitle, dashboardDescription] = useMemo(() => {
+ return link.destination === parentDashboardId
+ ? [parentDashboardTitle, parentDashboardDescription]
+ : [destinationDashboard?.attributes.title, destinationDashboard?.attributes.description];
+ }, [
+ link.destination,
+ parentDashboardId,
+ parentDashboardTitle,
+ destinationDashboard,
+ parentDashboardDescription,
+ ]);
+
+ const linkLabel = useMemo(() => {
+ return link.label || (dashboardTitle ?? DashboardLinkStrings.getDashboardErrorLabel());
+ }, [link, dashboardTitle]);
+
+ const { tooltipTitle, tooltipMessage } = useMemo(() => {
+ if (error) {
+ return {
+ tooltipTitle: DashboardLinkStrings.getDashboardErrorLabel(),
+ tooltipMessage: error.message,
+ };
+ }
+ return {
+ tooltipTitle: Boolean(dashboardDescription) ? dashboardTitle : undefined,
+ tooltipMessage: dashboardDescription || dashboardTitle,
+ };
+ }, [error, dashboardTitle, dashboardDescription]);
+
+ return loadingDestinationDashboard ? (
+
+
+ {DashboardLinkStrings.getLoadingDashboardLabel()}
+
+
+ ) : (
+ {
+ // TODO: As part of https://github.com/elastic/kibana/issues/154381, connect to drilldown
+ }
+ }
+ label={
+
+ {/* Setting `title=""` so that the native browser tooltip is disabled */}
+
+ {linkLabel}
+
+
+ }
+ />
);
};
diff --git a/src/plugins/navigation_embeddable/public/components/dashboard_link/dashboard_link_destination_picker.tsx b/src/plugins/navigation_embeddable/public/components/dashboard_link/dashboard_link_destination_picker.tsx
index 7156449be366d..6bc62ef0912f4 100644
--- a/src/plugins/navigation_embeddable/public/components/dashboard_link/dashboard_link_destination_picker.tsx
+++ b/src/plugins/navigation_embeddable/public/components/dashboard_link/dashboard_link_destination_picker.tsx
@@ -22,8 +22,8 @@ import {
import { DashboardContainer } from '@kbn/dashboard-plugin/public/dashboard_container';
import { DashboardItem } from '../../embeddable/types';
-import { memoizedFetchDashboard, memoizedFetchDashboards } from './dashboard_link_tools';
-import { DashboardLinkEmbeddableStrings } from './dashboard_link_strings';
+import { DashboardLinkStrings } from './dashboard_link_strings';
+import { fetchDashboard, fetchDashboards } from './dashboard_link_tools';
type DashboardComboBoxOption = EuiComboBoxOptionOption;
@@ -53,14 +53,23 @@ export const DashboardLinkDestinationPicker = ({
useMount(async () => {
if (initialSelection) {
- const dashboard = await memoizedFetchDashboard(initialSelection);
- onDestinationPicked(dashboard);
- setSelectedOption([getDashboardItem(dashboard)]);
+ const dashboard = await fetchDashboard(initialSelection).catch(() => {
+ /**
+ * Swallow the error that is thrown, since this just means the selected dashboard was deleted and
+ * so we should treat this the same as "no previous selection."
+ */
+ });
+ if (dashboard) {
+ onDestinationPicked(dashboard);
+ setSelectedOption([getDashboardItem(dashboard)]);
+ } else {
+ onDestinationPicked(undefined);
+ }
}
});
const { loading: loadingDashboardList, value: dashboardList } = useAsync(async () => {
- const dashboards = await memoizedFetchDashboards({
+ const dashboards = await fetchDashboards({
search: searchString,
parentDashboardId,
selectedDashboardId: initialSelection,
@@ -86,7 +95,7 @@ export const DashboardLinkDestinationPicker = ({
{dashboardId === parentDashboardId && (
- {DashboardLinkEmbeddableStrings.getCurrentDashboardLabel()}
+ {DashboardLinkStrings.getCurrentDashboardLabel()}
)}
@@ -108,8 +117,8 @@ export const DashboardLinkDestinationPicker = ({
fullWidth
className={'navEmbeddableDashboardPicker'}
isLoading={loadingDashboardList}
- aria-label={DashboardLinkEmbeddableStrings.getDashboardPickerAriaLabel()}
- placeholder={DashboardLinkEmbeddableStrings.getDashboardPickerPlaceholder()}
+ aria-label={DashboardLinkStrings.getDashboardPickerAriaLabel()}
+ placeholder={DashboardLinkStrings.getDashboardPickerPlaceholder()}
singleSelection={{ asPlainText: true }}
options={dashboardList}
onSearchChange={(searchValue) => {
diff --git a/src/plugins/navigation_embeddable/public/components/dashboard_link/dashboard_link_strings.ts b/src/plugins/navigation_embeddable/public/components/dashboard_link/dashboard_link_strings.ts
index c763b0bd88e4e..ebda3bfa3763b 100644
--- a/src/plugins/navigation_embeddable/public/components/dashboard_link/dashboard_link_strings.ts
+++ b/src/plugins/navigation_embeddable/public/components/dashboard_link/dashboard_link_strings.ts
@@ -8,7 +8,11 @@
import { i18n } from '@kbn/i18n';
-export const DashboardLinkEmbeddableStrings = {
+export const DashboardLinkStrings = {
+ getType: () =>
+ i18n.translate('navigationEmbeddable.dashboardLink.type', {
+ defaultMessage: 'Dashboard link',
+ }),
getDisplayName: () =>
i18n.translate('navigationEmbeddable.dashboardLink.displayName', {
defaultMessage: 'Dashboard',
@@ -29,4 +33,12 @@ export const DashboardLinkEmbeddableStrings = {
i18n.translate('navigationEmbeddable.dashboardLink.editor.currentDashboardLabel', {
defaultMessage: 'Current',
}),
+ getLoadingDashboardLabel: () =>
+ i18n.translate('navigationEmbeddable.dashboardLink.editor.loadingDashboardLabel', {
+ defaultMessage: 'Loading...',
+ }),
+ getDashboardErrorLabel: () =>
+ i18n.translate('navigationEmbeddable.dashboardLink.editor.dashboardErrorLabel', {
+ defaultMessage: 'Error fetching dashboard',
+ }),
};
diff --git a/src/plugins/navigation_embeddable/public/components/dashboard_link/dashboard_link_tools.tsx b/src/plugins/navigation_embeddable/public/components/dashboard_link/dashboard_link_tools.tsx
index 9590df2bd6c0d..d6f3e502d9c54 100644
--- a/src/plugins/navigation_embeddable/public/components/dashboard_link/dashboard_link_tools.tsx
+++ b/src/plugins/navigation_embeddable/public/components/dashboard_link/dashboard_link_tools.tsx
@@ -6,7 +6,7 @@
* Side Public License, v 1.
*/
-import { isEmpty, memoize, filter } from 'lodash';
+import { isEmpty, filter } from 'lodash';
import { DashboardItem } from '../../embeddable/types';
import { dashboardServices } from '../../services/kibana_services';
@@ -19,27 +19,13 @@ import { dashboardServices } from '../../services/kibana_services';
export const fetchDashboard = async (dashboardId: string): Promise => {
const findDashboardsService = await dashboardServices.findDashboardsService();
- const response = (await findDashboardsService.findByIds([dashboardId]))[0];
+ const response = await findDashboardsService.findById(dashboardId);
if (response.status === 'error') {
- throw new Error('failure'); // TODO: better error handling
+ throw new Error(response.error.message);
}
return response;
};
-/**
- * Memoized fetch dashboard will only refetch the dashboard information if the given `dashboardId` changed between
- * calls; otherwise, it will use the cached dashboard, which may not take into account changes to the dashboard's title
- * description, etc. Be mindful when choosing the memoized version.
- */
-export const memoizedFetchDashboard = memoize(
- async (dashboardId: string) => {
- return await fetchDashboard(dashboardId);
- },
- (dashboardId) => {
- return dashboardId;
- }
-);
-
/**
* ----------------------------------
* Fetch lists of dashboards
@@ -53,7 +39,7 @@ interface FetchDashboardsProps {
selectedDashboardId?: string;
}
-const fetchDashboards = async ({
+export const fetchDashboards = async ({
search = '',
size = 10,
parentDashboardId,
@@ -81,7 +67,13 @@ const fetchDashboards = async ({
}
if (selectedDashboardId && selectedDashboardId !== parentDashboardId) {
- dashboardList.unshift(await fetchDashboard(selectedDashboardId));
+ const selectedDashboard = await fetchDashboard(selectedDashboardId).catch(() => {
+ /**
+ * Swallow the error thrown, since this just means the selected dashboard was deleted and therefore
+ * it should not be added to the top of the dashboard list
+ */
+ });
+ if (selectedDashboard) dashboardList.unshift(await fetchDashboard(selectedDashboardId));
}
}
@@ -92,17 +84,3 @@ const fetchDashboards = async ({
return simplifiedDashboardList;
};
-
-export const memoizedFetchDashboards = memoize(
- async ({ search, size, parentDashboardId, selectedDashboardId }: FetchDashboardsProps) => {
- return await fetchDashboards({
- search,
- size,
- parentDashboardId,
- selectedDashboardId,
- });
- },
- ({ search, size, parentDashboardId, selectedDashboardId }) => {
- return [search, size, parentDashboardId, selectedDashboardId].join('|');
- }
-);
diff --git a/src/plugins/navigation_embeddable/public/components/navigation_embeddable.scss b/src/plugins/navigation_embeddable/public/components/editor/navigation_embeddable_editor.scss
similarity index 75%
rename from src/plugins/navigation_embeddable/public/components/navigation_embeddable.scss
rename to src/plugins/navigation_embeddable/public/components/editor/navigation_embeddable_editor.scss
index e7a6e5a1890a0..5a84104ea7c42 100644
--- a/src/plugins/navigation_embeddable/public/components/navigation_embeddable.scss
+++ b/src/plugins/navigation_embeddable/public/components/editor/navigation_embeddable_editor.scss
@@ -1,4 +1,4 @@
-@import '../mixins';
+@import '../../mixins';
.navEmbeddablePanelEditor {
max-inline-size: $euiSizeXXL * 18; // 40px * 18 = 720px
@@ -39,19 +39,26 @@
}
}
-.navEmbeddableLinkText {
- flex: 1;
- min-width: 0;
+.navEmbeddableLinkPanel {
+ padding: $euiSizeXS $euiSizeS;
+ color: $euiTextColor;
- .wrapText {
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
+ .navEmbeddableLinkText {
+ flex: 1;
+ min-width: 0;
}
-}
-.navEmbeddableLinkPanel {
- padding: $euiSizeXS $euiSizeS;
+ &.linkError {
+ border: 1px solid transparentize($euiColorWarningText, .7);
+
+ .navEmbeddableLinkText {
+ color: $euiColorWarningText;
+ }
+
+ .navEmbeddableLinkText--noLabel {
+ font-style: italic;
+ }
+ }
.navEmbeddable_hoverActions {
opacity: 0;
@@ -65,4 +72,8 @@
visibility: visible;
}
}
+}
+
+.navEmbeddableDroppableLinksArea {
+ margin: 0 (-$euiSizeXS);
}
\ No newline at end of file
diff --git a/src/plugins/navigation_embeddable/public/components/navigation_embeddable_link_editor.tsx b/src/plugins/navigation_embeddable/public/components/editor/navigation_embeddable_link_editor.tsx
similarity index 92%
rename from src/plugins/navigation_embeddable/public/components/navigation_embeddable_link_editor.tsx
rename to src/plugins/navigation_embeddable/public/components/editor/navigation_embeddable_link_editor.tsx
index def291c63bcac..f02e37806e396 100644
--- a/src/plugins/navigation_embeddable/public/components/navigation_embeddable_link_editor.tsx
+++ b/src/plugins/navigation_embeddable/public/components/editor/navigation_embeddable_link_editor.tsx
@@ -28,18 +28,17 @@ import {
} from '@elastic/eui';
import { DashboardContainer } from '@kbn/dashboard-plugin/public/dashboard_container';
-import { NavigationLinkInfo } from '../embeddable/types';
import {
NavigationLinkType,
EXTERNAL_LINK_TYPE,
DASHBOARD_LINK_TYPE,
NavigationEmbeddableLink,
-} from '../../common/content_management';
-import { DashboardItem } from '../embeddable/types';
-import { NavEmbeddableStrings } from './navigation_embeddable_strings';
-import { NavigationEmbeddableUnorderedLink } from '../editor/open_link_editor_flyout';
-import { ExternalLinkDestinationPicker } from './external_link/external_link_destination_picker';
-import { DashboardLinkDestinationPicker } from './dashboard_link/dashboard_link_destination_picker';
+} from '../../../common/content_management';
+import { NavEmbeddableStrings } from '../navigation_embeddable_strings';
+import { DashboardItem, NavigationLinkInfo } from '../../embeddable/types';
+import { NavigationEmbeddableUnorderedLink } from '../../editor/open_link_editor_flyout';
+import { ExternalLinkDestinationPicker } from '../external_link/external_link_destination_picker';
+import { DashboardLinkDestinationPicker } from '../dashboard_link/dashboard_link_destination_picker';
export const NavigationEmbeddableLinkEditor = ({
link,
@@ -169,7 +168,7 @@ export const NavigationEmbeddableLinkEditor = ({
(linkDestination ? defaultLinkLabel : '') ||
NavEmbeddableStrings.editor.linkEditor.getLinkTextPlaceholder()
}
- value={linkDestination ? currentLinkLabel : ''}
+ value={currentLinkLabel}
onChange={(e) => setCurrentLinkLabel(e.target.value)}
/>
diff --git a/src/plugins/navigation_embeddable/public/components/navigation_embeddable_panel_editor.tsx b/src/plugins/navigation_embeddable/public/components/editor/navigation_embeddable_panel_editor.tsx
similarity index 57%
rename from src/plugins/navigation_embeddable/public/components/navigation_embeddable_panel_editor.tsx
rename to src/plugins/navigation_embeddable/public/components/editor/navigation_embeddable_panel_editor.tsx
index 97a5a86a9d653..892b2f777d3c5 100644
--- a/src/plugins/navigation_embeddable/public/components/navigation_embeddable_panel_editor.tsx
+++ b/src/plugins/navigation_embeddable/public/components/editor/navigation_embeddable_panel_editor.tsx
@@ -6,16 +6,11 @@
* Side Public License, v 1.
*/
-import useObservable from 'react-use/lib/useObservable';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import {
- EuiText,
EuiForm,
- EuiImage,
EuiTitle,
- EuiPanel,
- EuiSpacer,
EuiButton,
EuiFormRow,
EuiFlexItem,
@@ -23,50 +18,75 @@ import {
EuiDroppable,
EuiDraggable,
EuiFlyoutBody,
- EuiEmptyPrompt,
EuiButtonEmpty,
+ EuiButtonGroup,
EuiFlyoutFooter,
EuiFlyoutHeader,
EuiDragDropContext,
euiDragDropReorder,
- EuiToolTip,
+ EuiButtonGroupOptionProps,
+ EuiSwitch,
} from '@elastic/eui';
import { DashboardContainer } from '@kbn/dashboard-plugin/public/dashboard_container';
-import { coreServices } from '../services/kibana_services';
-import { NavigationEmbeddableLink } from '../../common/content_management';
-import { NavEmbeddableStrings } from './navigation_embeddable_strings';
-
-import { openLinkEditorFlyout } from '../editor/open_link_editor_flyout';
-import { memoizedGetOrderedLinkList } from '../editor/navigation_embeddable_editor_tools';
+import { NavigationLayoutInfo } from '../../embeddable/types';
+import {
+ NavigationEmbeddableLink,
+ NavigationLayoutType,
+ NAV_HORIZONTAL_LAYOUT,
+ NAV_VERTICAL_LAYOUT,
+} from '../../../common/content_management';
+import { coreServices } from '../../services/kibana_services';
+import { NavEmbeddableStrings } from '../navigation_embeddable_strings';
+import { openLinkEditorFlyout } from '../../editor/open_link_editor_flyout';
+import { memoizedGetOrderedLinkList } from '../../editor/navigation_embeddable_editor_tools';
import { NavigationEmbeddablePanelEditorLink } from './navigation_embeddable_panel_editor_link';
-import { TooltipWrapper } from './tooltip_wrapper';
+import { NavigationEmbeddablePanelEditorEmptyPrompt } from './navigation_embeddable_panel_editor_empty_prompt';
+
+import { TooltipWrapper } from '../tooltip_wrapper';
-import noLinksIllustrationDark from '../assets/empty_links_dark.svg';
-import noLinksIllustrationLight from '../assets/empty_links_light.svg';
-import './navigation_embeddable.scss';
+import './navigation_embeddable_editor.scss';
+
+const layoutOptions: EuiButtonGroupOptionProps[] = [
+ {
+ id: NAV_VERTICAL_LAYOUT,
+ label: NavigationLayoutInfo[NAV_VERTICAL_LAYOUT].displayName,
+ },
+ {
+ id: NAV_HORIZONTAL_LAYOUT,
+ label: NavigationLayoutInfo[NAV_HORIZONTAL_LAYOUT].displayName,
+ },
+];
const NavigationEmbeddablePanelEditor = ({
onSaveToLibrary,
onAddToDashboard,
onClose,
initialLinks,
+ initialLayout,
parentDashboard,
isByReference,
}: {
- onSaveToLibrary: (newLinks: NavigationEmbeddableLink[]) => Promise;
- onAddToDashboard: (newLinks: NavigationEmbeddableLink[]) => void;
+ onSaveToLibrary: (
+ newLinks: NavigationEmbeddableLink[],
+ newLayout: NavigationLayoutType
+ ) => Promise;
+ onAddToDashboard: (newLinks: NavigationEmbeddableLink[], newLayout: NavigationLayoutType) => void;
onClose: () => void;
initialLinks?: NavigationEmbeddableLink[];
+ initialLayout?: NavigationLayoutType;
parentDashboard?: DashboardContainer;
isByReference: boolean;
}) => {
- const isDarkTheme = useObservable(coreServices.theme.theme$)?.darkMode;
const toasts = coreServices.notifications.toasts;
const editLinkFlyoutRef: React.RefObject = useMemo(() => React.createRef(), []);
- const [orderedLinks, setOrderedLinks] = useState([]);
+ const [currentLayout, setCurrentLayout] = useState(
+ initialLayout ?? NAV_VERTICAL_LAYOUT
+ );
const [isSaving, setIsSaving] = useState(false);
+ const [orderedLinks, setOrderedLinks] = useState([]);
+ const [saveByReference, setSaveByReference] = useState(!initialLinks ? true : isByReference);
const isEditingExisting = initialLinks || isByReference;
@@ -117,6 +137,10 @@ const NavigationEmbeddablePanelEditor = ({
[editLinkFlyoutRef, orderedLinks, parentDashboard]
);
+ const hasZeroLinks = useMemo(() => {
+ return orderedLinks.length === 0;
+ }, [orderedLinks]);
+
const deleteLink = useCallback(
(linkId: string) => {
setOrderedLinks(
@@ -142,43 +166,35 @@ const NavigationEmbeddablePanelEditor = ({
-
+ {hasZeroLinks ? (
+ addOrEditLink()} />
+ ) : (
<>
- {orderedLinks.length === 0 ? (
-
-
- }
- body={
- <>
-
- {NavEmbeddableStrings.editor.panelEditor.getEmptyLinksMessage()}
-
-
- addOrEditLink()} iconType="plusInCircle">
- {NavEmbeddableStrings.editor.getAddButtonLabel()}
-
- >
- }
- />
-
- ) : (
- <>
+
+ {
+ setCurrentLayout(id as NavigationLayoutType);
+ }}
+ legend={NavEmbeddableStrings.editor.panelEditor.getLayoutSettingsLegend()}
+ />
+
+
+ {/* Needs to be surrounded by a div rather than a fragment so the EuiFormRow can respond
+ to the focus of the inner elements */}
+
-
+
{orderedLinks.map((link, idx) => (
- addOrEditLink()}>
+ addOrEditLink()}
+ >
{NavEmbeddableStrings.editor.getAddButtonLabel()}
- >
- )}
+
+
>
-
+ )}
@@ -213,48 +234,35 @@ const NavigationEmbeddablePanelEditor = ({
-
- {!isByReference ? (
-
+
+ {!initialLinks || !isByReference ? (
+
- {
- onAddToDashboard(orderedLinks);
- }}
- >
- {initialLinks
- ? NavEmbeddableStrings.editor.panelEditor.getApplyButtonLabel()
- : NavEmbeddableStrings.editor.panelEditor.getAddToDashboardButtonLabel()}
-
+ setSaveByReference(!saveByReference)}
+ />
) : null}
- {!initialLinks || isByReference ? (
-
-
- {initialLinks
- ? NavEmbeddableStrings.editor.panelEditor.getUpdateLibraryItemButtonTooltip()
- : NavEmbeddableStrings.editor.panelEditor.getSaveToLibraryButtonTooltip()}
-
- }
- >
- {
+
+
+ {
+ if (saveByReference) {
setIsSaving(true);
- onSaveToLibrary(orderedLinks)
+ onSaveToLibrary(orderedLinks, currentLayout)
.catch((e) => {
toasts.addError(e, {
title:
@@ -264,15 +272,15 @@ const NavigationEmbeddablePanelEditor = ({
.finally(() => {
setIsSaving(false);
});
- }}
- >
- {initialLinks
- ? NavEmbeddableStrings.editor.panelEditor.getUpdateLibraryItemButtonLabel()
- : NavEmbeddableStrings.editor.panelEditor.getSaveToLibraryButtonLabel()}
-
-
-
- ) : null}
+ } else {
+ onAddToDashboard(orderedLinks, currentLayout);
+ }
+ }}
+ >
+ {NavEmbeddableStrings.editor.panelEditor.getSaveButtonLabel()}
+
+
+
diff --git a/src/plugins/navigation_embeddable/public/components/editor/navigation_embeddable_panel_editor_empty_prompt.tsx b/src/plugins/navigation_embeddable/public/components/editor/navigation_embeddable_panel_editor_empty_prompt.tsx
new file mode 100644
index 0000000000000..71083dbf87449
--- /dev/null
+++ b/src/plugins/navigation_embeddable/public/components/editor/navigation_embeddable_panel_editor_empty_prompt.tsx
@@ -0,0 +1,66 @@
+/*
+ * 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 useObservable from 'react-use/lib/useObservable';
+
+import {
+ EuiText,
+ EuiImage,
+ EuiPanel,
+ EuiSpacer,
+ EuiButton,
+ EuiEmptyPrompt,
+ EuiFormRow,
+} from '@elastic/eui';
+
+import { coreServices } from '../../services/kibana_services';
+import { NavEmbeddableStrings } from '../navigation_embeddable_strings';
+
+import noLinksIllustrationDark from '../../assets/empty_links_dark.svg';
+import noLinksIllustrationLight from '../../assets/empty_links_light.svg';
+
+import './navigation_embeddable_editor.scss';
+
+export const NavigationEmbeddablePanelEditorEmptyPrompt = ({
+ addLink,
+}: {
+ addLink: () => Promise;
+}) => {
+ const isDarkTheme = useObservable(coreServices.theme.theme$)?.darkMode;
+
+ return (
+
+
+
+ }
+ body={
+ <>
+
+ {NavEmbeddableStrings.editor.panelEditor.getEmptyLinksMessage()}
+
+
+
+ {NavEmbeddableStrings.editor.getAddButtonLabel()}
+
+ >
+ }
+ />
+
+
+ );
+};
diff --git a/src/plugins/navigation_embeddable/public/components/navigation_embeddable_panel_editor_link.tsx b/src/plugins/navigation_embeddable/public/components/editor/navigation_embeddable_panel_editor_link.tsx
similarity index 56%
rename from src/plugins/navigation_embeddable/public/components/navigation_embeddable_panel_editor_link.tsx
rename to src/plugins/navigation_embeddable/public/components/editor/navigation_embeddable_panel_editor_link.tsx
index 7c6d2b7268102..a886ac6f9eb8f 100644
--- a/src/plugins/navigation_embeddable/public/components/navigation_embeddable_panel_editor_link.tsx
+++ b/src/plugins/navigation_embeddable/public/components/editor/navigation_embeddable_panel_editor_link.tsx
@@ -6,25 +6,28 @@
* Side Public License, v 1.
*/
-import React from 'react';
+import classNames from 'classnames';
+import React, { useMemo, useState } from 'react';
import useAsync from 'react-use/lib/useAsync';
import {
+ EuiText,
EuiIcon,
EuiPanel,
+ EuiToolTip,
EuiFlexItem,
EuiFlexGroup,
EuiButtonIcon,
EuiSkeletonTitle,
DraggableProvidedDragHandleProps,
- EuiToolTip,
} from '@elastic/eui';
import { DashboardContainer } from '@kbn/dashboard-plugin/public/dashboard_container';
-import { NavigationLinkInfo } from '../embeddable/types';
-import { DASHBOARD_LINK_TYPE, NavigationEmbeddableLink } from '../../common/content_management';
-import { fetchDashboard } from './dashboard_link/dashboard_link_tools';
-import { NavEmbeddableStrings } from './navigation_embeddable_strings';
+import { NavigationLinkInfo } from '../../embeddable/types';
+import { fetchDashboard } from '../dashboard_link/dashboard_link_tools';
+import { NavEmbeddableStrings } from '../navigation_embeddable_strings';
+import { DashboardLinkStrings } from '../dashboard_link/dashboard_link_strings';
+import { DASHBOARD_LINK_TYPE, NavigationEmbeddableLink } from '../../../common/content_management';
export const NavigationEmbeddablePanelEditorLink = ({
link,
@@ -39,24 +42,85 @@ export const NavigationEmbeddablePanelEditorLink = ({
parentDashboard?: DashboardContainer;
dragHandleProps?: DraggableProvidedDragHandleProps;
}) => {
+ const [dashboardError, setDashboardError] = useState();
const parentDashboardTitle = parentDashboard?.select((state) => state.explicitInput.title);
const parentDashboardId = parentDashboard?.select((state) => state.componentState.lastSavedId);
const { value: linkLabel, loading: linkLabelLoading } = useAsync(async () => {
- let label = link.label;
- if (link.type === DASHBOARD_LINK_TYPE && !label) {
+ if (link.type === DASHBOARD_LINK_TYPE) {
if (parentDashboardId === link.destination) {
- label = parentDashboardTitle;
+ return link.label || parentDashboardTitle;
} else {
- const dashboard = await fetchDashboard(link.destination);
- label = dashboard.attributes.title;
+ const dashboard = await fetchDashboard(link.destination)
+ .then((result) => {
+ setDashboardError(undefined);
+ return result;
+ })
+ .catch((error) => setDashboardError(error));
+ return (
+ link.label ||
+ (dashboard ? dashboard.attributes.title : DashboardLinkStrings.getDashboardErrorLabel())
+ );
}
+ } else {
+ return link.label || link.destination;
}
- return label || link.destination;
}, [link]);
+ const LinkLabel = useMemo(() => {
+ const labelText = (
+
+
+
+
+
+
+
+
+ {linkLabel}
+
+
+
+
+ );
+
+ return () =>
+ dashboardError ? (
+
+ {labelText}
+
+ ) : (
+ labelText
+ );
+ }, [linkLabel, linkLabelLoading, dashboardError, link.label, link.type]);
+
return (
-
+
-
-
-
-
- {linkLabel}
-
+
+
diff --git a/src/plugins/navigation_embeddable/public/components/external_link/external_link_component.tsx b/src/plugins/navigation_embeddable/public/components/external_link/external_link_component.tsx
index 7b940ac027357..52442cd307b0b 100644
--- a/src/plugins/navigation_embeddable/public/components/external_link/external_link_component.tsx
+++ b/src/plugins/navigation_embeddable/public/components/external_link/external_link_component.tsx
@@ -8,14 +8,20 @@
import React from 'react';
-import { EuiButtonEmpty } from '@elastic/eui';
-import { NavigationLinkInfo } from '../../embeddable/types';
-import { EXTERNAL_LINK_TYPE, NavigationEmbeddableLink } from '../../../common/content_management';
+import { EuiListGroupItem } from '@elastic/eui';
+import { NavigationEmbeddableLink } from '../../../common/content_management';
export const ExternalLinkComponent = ({ link }: { link: NavigationEmbeddableLink }) => {
return (
-
- {link.label || link.destination}
-
+ {
+ // TODO: As part of https://github.com/elastic/kibana/issues/154381, connect to drilldown
+ }}
+ />
);
};
diff --git a/src/plugins/navigation_embeddable/public/components/external_link/external_link_destination_picker.tsx b/src/plugins/navigation_embeddable/public/components/external_link/external_link_destination_picker.tsx
index 4119cc32f32aa..4019e4c843faf 100644
--- a/src/plugins/navigation_embeddable/public/components/external_link/external_link_destination_picker.tsx
+++ b/src/plugins/navigation_embeddable/public/components/external_link/external_link_destination_picker.tsx
@@ -10,7 +10,7 @@ import useMount from 'react-use/lib/useMount';
import React, { useState } from 'react';
import { EuiFieldText } from '@elastic/eui';
-import { ExternalLinkEmbeddableStrings } from './external_link_strings';
+import { ExternalLinkStrings } from './external_link_strings';
// TODO: As part of https://github.com/elastic/kibana/issues/154381, replace this regex URL check with more robust url validation
const isValidUrl =
@@ -39,7 +39,7 @@ export const ExternalLinkDestinationPicker = ({
{
const url = e.target.value;
diff --git a/src/plugins/navigation_embeddable/public/components/external_link/external_link_strings.ts b/src/plugins/navigation_embeddable/public/components/external_link/external_link_strings.ts
index 77d7b479706b6..e286019d4bc05 100644
--- a/src/plugins/navigation_embeddable/public/components/external_link/external_link_strings.ts
+++ b/src/plugins/navigation_embeddable/public/components/external_link/external_link_strings.ts
@@ -8,7 +8,11 @@
import { i18n } from '@kbn/i18n';
-export const ExternalLinkEmbeddableStrings = {
+export const ExternalLinkStrings = {
+ getType: () =>
+ i18n.translate('navigationEmbeddable.externalLink.type', {
+ defaultMessage: 'External URL',
+ }),
getDisplayName: () =>
i18n.translate('navigationEmbeddable.externalLink.displayName', {
defaultMessage: 'URL',
diff --git a/src/plugins/navigation_embeddable/public/components/navigation_embeddable_component.scss b/src/plugins/navigation_embeddable/public/components/navigation_embeddable_component.scss
new file mode 100644
index 0000000000000..bbc51f041efec
--- /dev/null
+++ b/src/plugins/navigation_embeddable/public/components/navigation_embeddable_component.scss
@@ -0,0 +1,58 @@
+.navEmbeddableComponent {
+
+ .navigationLink {
+ max-width: fit-content; // added this so that the error tooltip shows up **right beside** the link label
+
+ &.dashboardLinkError {
+ &.dashboardLinkError--noLabel .euiListGroupItem__button {
+ font-style: italic;
+ }
+
+ .dashboardLinkIcon {
+ margin-right: $euiSizeS;
+ }
+ }
+
+ &.navigationLinkCurrent {
+ border-radius: 0;
+ .euiListGroupItem__text {
+ cursor: default;
+ color: $euiColorPrimary;
+ }
+ }
+ }
+
+ .verticalLayoutWrapper {
+ gap: $euiSizeXS;
+ .navigationLink {
+ &.navigationLinkCurrent {
+ &::before {
+ content: '';
+ position: absolute;
+ width: .5 * $euiSizeXS;
+ height: 75%;
+ background-color: $euiColorPrimary;
+ }
+ }
+ }
+ }
+
+ .horizontalLayoutWrapper {
+ height: 100%;
+ display: flex;
+ flex-wrap: nowrap;
+ align-items: center;
+ flex-direction: row;
+
+ .navigationLink {
+ &.navigationLinkCurrent {
+ padding: 0 $euiSizeS;
+
+ .euiListGroupItem__text {
+ box-shadow: $euiColorPrimary 0 (-.5 * $euiSizeXS) inset;
+ padding-inline: 0;
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/plugins/navigation_embeddable/public/components/navigation_embeddable_component.tsx b/src/plugins/navigation_embeddable/public/components/navigation_embeddable_component.tsx
index a45d6d8028676..544f5593541df 100644
--- a/src/plugins/navigation_embeddable/public/components/navigation_embeddable_component.tsx
+++ b/src/plugins/navigation_embeddable/public/components/navigation_embeddable_component.tsx
@@ -8,46 +8,66 @@
import React, { useMemo } from 'react';
-import { EuiPanel } from '@elastic/eui';
+import { EuiListGroup, EuiPanel } from '@elastic/eui';
-import { DASHBOARD_LINK_TYPE } from '../../common/content_management';
+import { NavigationEmbeddableByValueInput } from '../embeddable/types';
import { useNavigationEmbeddable } from '../embeddable/navigation_embeddable';
import { ExternalLinkComponent } from './external_link/external_link_component';
import { DashboardLinkComponent } from './dashboard_link/dashboard_link_component';
import { memoizedGetOrderedLinkList } from '../editor/navigation_embeddable_editor_tools';
-import { NavigationEmbeddableByValueInput } from '../embeddable/types';
+import {
+ DASHBOARD_LINK_TYPE,
+ NAV_HORIZONTAL_LAYOUT,
+ NAV_VERTICAL_LAYOUT,
+} from '../../common/content_management';
+
+import './navigation_embeddable_component.scss';
export const NavigationEmbeddableComponent = () => {
const navEmbeddable = useNavigationEmbeddable();
-
const links = navEmbeddable.select(
(state) => (state.explicitInput as NavigationEmbeddableByValueInput).attributes?.links
);
+ const layout = navEmbeddable.select(
+ (state) => (state.explicitInput as NavigationEmbeddableByValueInput).attributes?.layout
+ );
const orderedLinks = useMemo(() => {
if (!links) return [];
return memoizedGetOrderedLinkList(links);
}, [links]);
- /** TODO: Render this as a list **or** "tabs" as part of https://github.com/elastic/kibana/issues/154357 */
- return (
-
- {orderedLinks.map((link) => {
- return (
-
- {link.type === DASHBOARD_LINK_TYPE ? (
-
+ const linkItems: { [id: string]: { id: string; content: JSX.Element } } = useMemo(() => {
+ return (links ?? []).reduce((prev, currentLink) => {
+ return {
+ ...prev,
+ [currentLink.id]: {
+ id: currentLink.id,
+ content:
+ currentLink.type === DASHBOARD_LINK_TYPE ? (
+
) : (
-
- )}
-
- );
- })}
+
+ ),
+ },
+ };
+ }, {});
+ }, [links, layout]);
+
+ return (
+
+
+ {orderedLinks.map((link) => linkItems[link.id].content)}
+
);
};
diff --git a/src/plugins/navigation_embeddable/public/components/navigation_embeddable_strings.ts b/src/plugins/navigation_embeddable/public/components/navigation_embeddable_strings.ts
index 20953c41dbe8c..d229058e95f60 100644
--- a/src/plugins/navigation_embeddable/public/components/navigation_embeddable_strings.ts
+++ b/src/plugins/navigation_embeddable/public/components/navigation_embeddable_strings.ts
@@ -31,6 +31,10 @@ export const NavEmbeddableStrings = {
defaultMessage: 'Close',
}),
panelEditor: {
+ getLinksTitle: () =>
+ i18n.translate('navigationEmbeddable.panelEditor.linksTitle', {
+ defaultMessage: 'Links',
+ }),
getEmptyLinksMessage: () =>
i18n.translate('navigationEmbeddable.panelEditor.emptyLinksMessage', {
defaultMessage: 'Use links to navigate to commonly used dashboards and websites.',
@@ -47,47 +51,47 @@ export const NavEmbeddableStrings = {
i18n.translate('navigationEmbeddable.panelEditor.editFlyoutTitle', {
defaultMessage: 'Edit links panel',
}),
- getApplyButtonLabel: () =>
- i18n.translate('navigationEmbeddable.panelEditor.applyButtonLabel', {
- defaultMessage: 'Apply',
- }),
- getAddToDashboardButtonLabel: () =>
- i18n.translate('navigationEmbeddable.panelEditor.addToDashboardButtonLabel', {
- defaultMessage: 'Add to dashboard',
- }),
- getAddToDashboardButtonTooltip: () =>
- i18n.translate('navigationEmbeddable.panelEditor.addToDashboardButtonTooltip', {
- defaultMessage: 'Add this links panel directly to this dashboard.',
+ getSaveButtonLabel: () =>
+ i18n.translate('navigationEmbeddable.panelEditor.saveButtonLabel', {
+ defaultMessage: 'Save',
}),
- getSaveToLibraryButtonLabel: () =>
- i18n.translate('navigationEmbeddable.panelEditor.saveToLibraryButtonLabel', {
+ getSaveToLibrarySwitchLabel: () =>
+ i18n.translate('navigationEmbeddable.panelEditor.saveToLibrarySwitchLabel', {
defaultMessage: 'Save to library',
}),
- getSaveToLibraryButtonTooltip: () =>
- i18n.translate('navigationEmbeddable.panelEditor.saveToLibraryButtonTooltip', {
+ getSaveToLibrarySwitchTooltip: () =>
+ i18n.translate('navigationEmbeddable.panelEditor.saveToLibrarySwitchTooltip', {
defaultMessage:
'Save this links panel to the library so you can easily add it to other dashboards.',
}),
- getUpdateLibraryItemButtonLabel: () =>
- i18n.translate('navigationEmbeddable.panelEditor.updateLibraryItemButtonLabel', {
- defaultMessage: 'Update library item',
- }),
- getUpdateLibraryItemButtonTooltip: () =>
- i18n.translate('navigationEmbeddable.panelEditor.updateLibraryItemButtonTooltip', {
- defaultMessage: 'Editing this panel might affect other dashboards.',
- }),
getTitleInputLabel: () =>
i18n.translate('navigationEmbeddable.panelEditor.titleInputLabel', {
defaultMessage: 'Title',
}),
- getLinkLoadingAriaLabel: () =>
- i18n.translate('navigationEmbeddable.linkEditor.linkLoadingAriaLabel', {
- defaultMessage: 'Loading link',
+ getBrokenDashboardLinkAriaLabel: () =>
+ i18n.translate('navigationEmbeddable.panelEditor.brokenDashboardLinkAriaLabel', {
+ defaultMessage: 'Broken dashboard link',
}),
getDragHandleAriaLabel: () =>
- i18n.translate('navigationEmbeddable.editor.dragHandleAriaLabel', {
+ i18n.translate('navigationEmbeddable.panelEditor.dragHandleAriaLabel', {
defaultMessage: 'Link drag handle',
}),
+ getLayoutSettingsTitle: () =>
+ i18n.translate('navigationEmbeddable.panelEditor.layoutSettingsTitle', {
+ defaultMessage: 'Layout',
+ }),
+ getLayoutSettingsLegend: () =>
+ i18n.translate('navigationEmbeddable.panelEditor.layoutSettingsLegend', {
+ defaultMessage: 'Choose how to display your links.',
+ }),
+ getHorizontalLayoutLabel: () =>
+ i18n.translate('navigationEmbeddable.editor.horizontalLayout', {
+ defaultMessage: 'Horizontal',
+ }),
+ getVerticalLayoutLabel: () =>
+ i18n.translate('navigationEmbeddable.editor.verticalLayout', {
+ defaultMessage: 'Vertical',
+ }),
getErrorDuringSaveToastTitle: () =>
i18n.translate('navigationEmbeddable.editor.unableToSaveToastTitle', {
defaultMessage: 'Error saving Link panel',
diff --git a/src/plugins/navigation_embeddable/public/editor/open_editor_flyout.tsx b/src/plugins/navigation_embeddable/public/editor/open_editor_flyout.tsx
index e156180d8dd4d..14e3974473d94 100644
--- a/src/plugins/navigation_embeddable/public/editor/open_editor_flyout.tsx
+++ b/src/plugins/navigation_embeddable/public/editor/open_editor_flyout.tsx
@@ -8,7 +8,6 @@
import React from 'react';
import { Subject } from 'rxjs';
-import { memoize } from 'lodash';
import { skip, take, takeUntil } from 'rxjs/operators';
import { withSuspense } from '@kbn/shared-ux-utility';
@@ -16,18 +15,17 @@ import { EuiLoadingSpinner, EuiPanel } from '@elastic/eui';
import { toMountPoint } from '@kbn/kibana-react-plugin/public';
import { DashboardContainer } from '@kbn/dashboard-plugin/public/dashboard_container';
-import { coreServices } from '../services/kibana_services';
import {
- NavigationEmbeddableByReferenceInput,
NavigationEmbeddableInput,
+ NavigationEmbeddableByReferenceInput,
} from '../embeddable/types';
-import { memoizedFetchDashboards } from '../components/dashboard_link/dashboard_link_tools';
-import { getNavigationEmbeddableAttributeService } from '../services/attribute_service';
-import { NavigationEmbeddableLink } from '../../common/content_management';
+import { coreServices } from '../services/kibana_services';
import { runSaveToLibrary } from '../content_management/save_to_library';
+import { NavigationEmbeddableLink, NavigationLayoutType } from '../../common/content_management';
+import { getNavigationEmbeddableAttributeService } from '../services/attribute_service';
const LazyNavigationEmbeddablePanelEditor = React.lazy(
- () => import('../components/navigation_embeddable_panel_editor')
+ () => import('../components/editor/navigation_embeddable_panel_editor')
);
const NavigationEmbeddablePanelEditor = withSuspense(
@@ -51,10 +49,14 @@ export async function openEditorFlyout(
return new Promise((resolve, reject) => {
const closed$ = new Subject();
- const onSaveToLibrary = async (newLinks: NavigationEmbeddableLink[]) => {
+ const onSaveToLibrary = async (
+ newLinks: NavigationEmbeddableLink[],
+ newLayout: NavigationLayoutType
+ ) => {
const newAttributes = {
...attributes,
links: newLinks,
+ layout: newLayout,
};
const updatedInput = (initialInput as NavigationEmbeddableByReferenceInput).savedObjectId
? await attributeService.wrapAttributes(newAttributes, true, initialInput)
@@ -67,12 +69,16 @@ export async function openEditorFlyout(
editorFlyout.close();
};
- const onAddToDashboard = (newLinks: NavigationEmbeddableLink[]) => {
+ const onAddToDashboard = (
+ newLinks: NavigationEmbeddableLink[],
+ newLayout: NavigationLayoutType
+ ) => {
const newInput: NavigationEmbeddableInput = {
...initialInput,
attributes: {
...attributes,
links: newLinks,
+ layout: newLayout,
},
};
resolve(newInput);
@@ -98,6 +104,7 @@ export async function openEditorFlyout(
toMountPoint(
{
- // we should always re-fetch the dashboards when the editor is opened; so, clear the cache on close
- memoizedFetchDashboards.cache = new memoize.Cache();
closed$.next(true);
});
});
diff --git a/src/plugins/navigation_embeddable/public/editor/open_link_editor_flyout.tsx b/src/plugins/navigation_embeddable/public/editor/open_link_editor_flyout.tsx
index 1fee9bdd9b206..896c1717f554e 100644
--- a/src/plugins/navigation_embeddable/public/editor/open_link_editor_flyout.tsx
+++ b/src/plugins/navigation_embeddable/public/editor/open_link_editor_flyout.tsx
@@ -14,7 +14,7 @@ import { DashboardContainer } from '@kbn/dashboard-plugin/public/dashboard_conta
import { coreServices } from '../services/kibana_services';
import { NavigationEmbeddableLink } from '../../common/content_management';
-import { NavigationEmbeddableLinkEditor } from '../components/navigation_embeddable_link_editor';
+import { NavigationEmbeddableLinkEditor } from '../components/editor/navigation_embeddable_link_editor';
export interface LinkEditorProps {
link?: NavigationEmbeddableLink;
diff --git a/src/plugins/navigation_embeddable/public/embeddable/navigation_embeddable_factory.ts b/src/plugins/navigation_embeddable/public/embeddable/navigation_embeddable_factory.ts
index 9711c81b64125..ecbaaf8ba6ee2 100644
--- a/src/plugins/navigation_embeddable/public/embeddable/navigation_embeddable_factory.ts
+++ b/src/plugins/navigation_embeddable/public/embeddable/navigation_embeddable_factory.ts
@@ -14,18 +14,19 @@ import {
ErrorEmbeddable,
} from '@kbn/embeddable-plugin/public';
import { lazyLoadReduxToolsPackage } from '@kbn/presentation-util-plugin/public';
+import { EmbeddablePersistableStateService } from '@kbn/embeddable-plugin/common';
import { DashboardContainer } from '@kbn/dashboard-plugin/public/dashboard_container';
-import { EmbeddablePersistableStateService } from '@kbn/embeddable-plugin/common';
import {
- NavigationEmbeddableByReferenceInput,
NavigationEmbeddableByValueInput,
+ NavigationEmbeddableByReferenceInput,
NavigationEmbeddableInput,
} from './types';
import type { NavigationEmbeddable } from './navigation_embeddable';
-import { coreServices, untilPluginStartServicesReady } from '../services/kibana_services';
+import { NAV_VERTICAL_LAYOUT } from '../../common/content_management';
+import { APP_ICON, APP_NAME, CONTENT_ID, EMBEDDABLE_DISPLAY_NAME } from '../../common';
import { getNavigationEmbeddableAttributeService } from '../services/attribute_service';
-import { APP_ICON, APP_NAME, CONTENT_ID } from '../../common';
+import { coreServices, untilPluginStartServicesReady } from '../services/kibana_services';
export type NavigationEmbeddableFactory = EmbeddableFactory;
@@ -38,6 +39,7 @@ export interface NavigationEmbeddableCreationOptions {
const getDefaultNavigationEmbeddableInput = (): Omit => ({
attributes: {
title: '',
+ layout: NAV_VERTICAL_LAYOUT,
},
disabledActions: [ACTION_ADD_PANEL, 'OPEN_FLYOUT_ADD_DRILLDOWN'],
});
@@ -124,7 +126,7 @@ export class NavigationEmbeddableFactoryDefinition
}
public getDisplayName() {
- return APP_NAME;
+ return EMBEDDABLE_DISPLAY_NAME;
}
public getIconType() {
diff --git a/src/plugins/navigation_embeddable/public/embeddable/types.ts b/src/plugins/navigation_embeddable/public/embeddable/types.ts
index 43d12f611df8b..04e79731b6a77 100644
--- a/src/plugins/navigation_embeddable/public/embeddable/types.ts
+++ b/src/plugins/navigation_embeddable/public/embeddable/types.ts
@@ -14,14 +14,29 @@ import {
} from '@kbn/embeddable-plugin/public';
import { DashboardAttributes } from '@kbn/dashboard-plugin/common';
-import { ExternalLinkEmbeddableStrings } from '../components/external_link/external_link_strings';
-import { DashboardLinkEmbeddableStrings } from '../components/dashboard_link/dashboard_link_strings';
import {
- DASHBOARD_LINK_TYPE,
- EXTERNAL_LINK_TYPE,
NavigationLinkType,
+ EXTERNAL_LINK_TYPE,
+ DASHBOARD_LINK_TYPE,
+ NAV_VERTICAL_LAYOUT,
+ NavigationLayoutType,
+ NAV_HORIZONTAL_LAYOUT,
NavigationEmbeddableAttributes,
} from '../../common/content_management';
+import { DashboardLinkStrings } from '../components/dashboard_link/dashboard_link_strings';
+import { ExternalLinkStrings } from '../components/external_link/external_link_strings';
+import { NavEmbeddableStrings } from '../components/navigation_embeddable_strings';
+
+export const NavigationLayoutInfo: {
+ [id in NavigationLayoutType]: { displayName: string };
+} = {
+ [NAV_HORIZONTAL_LAYOUT]: {
+ displayName: NavEmbeddableStrings.editor.panelEditor.getHorizontalLayoutLabel(),
+ },
+ [NAV_VERTICAL_LAYOUT]: {
+ displayName: NavEmbeddableStrings.editor.panelEditor.getVerticalLayoutLabel(),
+ },
+};
export interface DashboardItem {
id: string;
@@ -29,17 +44,24 @@ export interface DashboardItem {
}
export const NavigationLinkInfo: {
- [id in NavigationLinkType]: { icon: string; displayName: string; description: string };
+ [id in NavigationLinkType]: {
+ icon: string;
+ type: string;
+ displayName: string;
+ description: string;
+ };
} = {
[DASHBOARD_LINK_TYPE]: {
icon: 'dashboardApp',
- displayName: DashboardLinkEmbeddableStrings.getDisplayName(),
- description: DashboardLinkEmbeddableStrings.getDescription(),
+ type: DashboardLinkStrings.getType(),
+ displayName: DashboardLinkStrings.getDisplayName(),
+ description: DashboardLinkStrings.getDescription(),
},
[EXTERNAL_LINK_TYPE]: {
icon: 'link',
- displayName: ExternalLinkEmbeddableStrings.getDisplayName(),
- description: ExternalLinkEmbeddableStrings.getDescription(),
+ type: ExternalLinkStrings.getType(),
+ displayName: ExternalLinkStrings.getDisplayName(),
+ description: ExternalLinkStrings.getDescription(),
},
};
diff --git a/src/plugins/navigation_embeddable/server/content_management/navigation_embeddable_storage.ts b/src/plugins/navigation_embeddable/server/content_management/navigation_embeddable_storage.ts
index 07318f62a3e10..db830dfad512d 100644
--- a/src/plugins/navigation_embeddable/server/content_management/navigation_embeddable_storage.ts
+++ b/src/plugins/navigation_embeddable/server/content_management/navigation_embeddable_storage.ts
@@ -17,7 +17,7 @@ export class NavigationEmbeddableStorage extends SOContentStorage
Date: Wed, 16 Aug 2023 12:16:21 -0600
Subject: [PATCH 09/53] Add `EMBEDDABLE_DISPLAY_NAME` to constants
---
src/plugins/navigation_embeddable/common/index.ts | 8 +++++++-
1 file changed, 7 insertions(+), 1 deletion(-)
diff --git a/src/plugins/navigation_embeddable/common/index.ts b/src/plugins/navigation_embeddable/common/index.ts
index 9cb4fc42124aa..0251a4d26782c 100644
--- a/src/plugins/navigation_embeddable/common/index.ts
+++ b/src/plugins/navigation_embeddable/common/index.ts
@@ -6,4 +6,10 @@
* Side Public License, v 1.
*/
-export { APP_ICON, APP_NAME, CONTENT_ID, LATEST_VERSION } from './constants';
+export {
+ APP_ICON,
+ APP_NAME,
+ CONTENT_ID,
+ LATEST_VERSION,
+ EMBEDDABLE_DISPLAY_NAME,
+} from './constants';
From 6fa536cd71244f9bbf3345b91e11c118de3f3962 Mon Sep 17 00:00:00 2001
From: Hannah Mudge
Date: Wed, 16 Aug 2023 13:03:35 -0600
Subject: [PATCH 10/53] Switch back to `APP_NAME` for now
---
src/plugins/navigation_embeddable/common/constants.ts | 7 -------
src/plugins/navigation_embeddable/common/index.ts | 8 +-------
.../public/embeddable/navigation_embeddable_factory.ts | 4 ++--
3 files changed, 3 insertions(+), 16 deletions(-)
diff --git a/src/plugins/navigation_embeddable/common/constants.ts b/src/plugins/navigation_embeddable/common/constants.ts
index d3b25a888a084..9731275e04f14 100644
--- a/src/plugins/navigation_embeddable/common/constants.ts
+++ b/src/plugins/navigation_embeddable/common/constants.ts
@@ -17,10 +17,3 @@ export const APP_ICON = 'link';
export const APP_NAME = i18n.translate('navigationEmbeddable.visTypeAlias.title', {
defaultMessage: 'Links',
});
-
-export const EMBEDDABLE_DISPLAY_NAME = i18n.translate(
- 'navigationEmbeddable.embeddableDisplayName',
- {
- defaultMessage: 'links',
- }
-);
diff --git a/src/plugins/navigation_embeddable/common/index.ts b/src/plugins/navigation_embeddable/common/index.ts
index 0251a4d26782c..9cb4fc42124aa 100644
--- a/src/plugins/navigation_embeddable/common/index.ts
+++ b/src/plugins/navigation_embeddable/common/index.ts
@@ -6,10 +6,4 @@
* Side Public License, v 1.
*/
-export {
- APP_ICON,
- APP_NAME,
- CONTENT_ID,
- LATEST_VERSION,
- EMBEDDABLE_DISPLAY_NAME,
-} from './constants';
+export { APP_ICON, APP_NAME, CONTENT_ID, LATEST_VERSION } from './constants';
diff --git a/src/plugins/navigation_embeddable/public/embeddable/navigation_embeddable_factory.ts b/src/plugins/navigation_embeddable/public/embeddable/navigation_embeddable_factory.ts
index ecbaaf8ba6ee2..d05ca560e350a 100644
--- a/src/plugins/navigation_embeddable/public/embeddable/navigation_embeddable_factory.ts
+++ b/src/plugins/navigation_embeddable/public/embeddable/navigation_embeddable_factory.ts
@@ -22,9 +22,9 @@ import {
NavigationEmbeddableByReferenceInput,
NavigationEmbeddableInput,
} from './types';
+import { APP_ICON, APP_NAME, CONTENT_ID } from '../../common';
import type { NavigationEmbeddable } from './navigation_embeddable';
import { NAV_VERTICAL_LAYOUT } from '../../common/content_management';
-import { APP_ICON, APP_NAME, CONTENT_ID, EMBEDDABLE_DISPLAY_NAME } from '../../common';
import { getNavigationEmbeddableAttributeService } from '../services/attribute_service';
import { coreServices, untilPluginStartServicesReady } from '../services/kibana_services';
@@ -126,7 +126,7 @@ export class NavigationEmbeddableFactoryDefinition
}
public getDisplayName() {
- return EMBEDDABLE_DISPLAY_NAME;
+ return APP_NAME;
}
public getIconType() {
From d51bcd88e50e746faadb3288c0233e4ad9b6dcd7 Mon Sep 17 00:00:00 2001
From: Nick Peihl
Date: Mon, 28 Aug 2023 11:23:33 -0400
Subject: [PATCH 11/53] [Dashboard Navigation] Store unwrapped attributes in
componentState (#164887)
Fixes https://github.com/elastic/kibana/issues/164322
Store the unwrapped attributes from by-reference links panels in
`componentState`.
Since the `explictInput` for a Links panel can be either by-reference or
by-value, we need to be sure to share the unwrapped attributes (as in
from a by-reference panel) to our component.
---
.../navigation_embeddable_component.tsx | 11 ++------
.../embeddable/navigation_embeddable.tsx | 27 +++++++++++++------
.../navigation_embeddable_factory.ts | 22 +++------------
.../navigation_embeddable_reducers.ts | 8 ++++++
.../public/embeddable/types.ts | 4 +--
5 files changed, 34 insertions(+), 38 deletions(-)
diff --git a/src/plugins/navigation_embeddable/public/components/navigation_embeddable_component.tsx b/src/plugins/navigation_embeddable/public/components/navigation_embeddable_component.tsx
index 544f5593541df..c6423f0c529d2 100644
--- a/src/plugins/navigation_embeddable/public/components/navigation_embeddable_component.tsx
+++ b/src/plugins/navigation_embeddable/public/components/navigation_embeddable_component.tsx
@@ -7,10 +7,7 @@
*/
import React, { useMemo } from 'react';
-
import { EuiListGroup, EuiPanel } from '@elastic/eui';
-
-import { NavigationEmbeddableByValueInput } from '../embeddable/types';
import { useNavigationEmbeddable } from '../embeddable/navigation_embeddable';
import { ExternalLinkComponent } from './external_link/external_link_component';
import { DashboardLinkComponent } from './dashboard_link/dashboard_link_component';
@@ -25,12 +22,8 @@ import './navigation_embeddable_component.scss';
export const NavigationEmbeddableComponent = () => {
const navEmbeddable = useNavigationEmbeddable();
- const links = navEmbeddable.select(
- (state) => (state.explicitInput as NavigationEmbeddableByValueInput).attributes?.links
- );
- const layout = navEmbeddable.select(
- (state) => (state.explicitInput as NavigationEmbeddableByValueInput).attributes?.layout
- );
+ const links = navEmbeddable.select((state) => state.componentState.links);
+ const layout = navEmbeddable.select((state) => state.componentState.layout);
const orderedLinks = useMemo(() => {
if (!links) return [];
diff --git a/src/plugins/navigation_embeddable/public/embeddable/navigation_embeddable.tsx b/src/plugins/navigation_embeddable/public/embeddable/navigation_embeddable.tsx
index 58a665488b825..a275ee26fe1fc 100644
--- a/src/plugins/navigation_embeddable/public/embeddable/navigation_embeddable.tsx
+++ b/src/plugins/navigation_embeddable/public/embeddable/navigation_embeddable.tsx
@@ -7,7 +7,8 @@
*/
import React, { createContext, useContext } from 'react';
-import { Subscription } from 'rxjs';
+import { Subscription, distinctUntilChanged, skip } from 'rxjs';
+import deepEqual from 'fast-deep-equal';
import {
AttributeService,
@@ -92,7 +93,9 @@ export class NavigationEmbeddable
>({
embeddable: this,
reducers: navigationEmbeddableReducers,
- initialComponentState: {},
+ initialComponentState: {
+ title: '',
+ },
});
this.select = reduxEmbeddableTools.select;
@@ -101,24 +104,31 @@ export class NavigationEmbeddable
this.cleanupStateTools = reduxEmbeddableTools.cleanup;
this.onStateChange = reduxEmbeddableTools.onStateChange;
- this.initializeSavedLinks(initialInput)
+ this.initializeSavedLinks()
.then(() => this.setInitializationFinished())
.catch((e: Error) => this.onFatalError(e));
+
+ // By-value panels should update the componentState when input changes
+ this.subscriptions.add(
+ this.getInput$()
+ .pipe(distinctUntilChanged(deepEqual), skip(1))
+ .subscribe(async () => await this.initializeSavedLinks())
+ );
}
- private async initializeSavedLinks(input: NavigationEmbeddableInput) {
- const { attributes } = await this.attributeService.unwrapAttributes(input);
+ private async initializeSavedLinks() {
+ const { attributes } = await this.attributeService.unwrapAttributes(this.getInput());
if (this.isDestroyed) return;
// TODO handle metaInfo
- this.updateInput({ attributes });
+ this.dispatch.setAttributes(attributes);
await this.initializeOutput();
}
private async initializeOutput() {
- const { attributes } = this.getInput() as NavigationEmbeddableByValueInput;
+ const attributes = this.getState().componentState;
const { title, description } = this.getInput();
this.updateOutput({
defaultTitle: attributes.title,
@@ -147,7 +157,8 @@ export class NavigationEmbeddable
public async reload() {
if (this.isDestroyed) return;
- await this.initializeSavedLinks(this.getInput());
+ // By-reference embeddable panels are reloaded when changed, so update the componentState
+ this.initializeSavedLinks();
this.render();
}
diff --git a/src/plugins/navigation_embeddable/public/embeddable/navigation_embeddable_factory.ts b/src/plugins/navigation_embeddable/public/embeddable/navigation_embeddable_factory.ts
index d05ca560e350a..3226833f1f9c8 100644
--- a/src/plugins/navigation_embeddable/public/embeddable/navigation_embeddable_factory.ts
+++ b/src/plugins/navigation_embeddable/public/embeddable/navigation_embeddable_factory.ts
@@ -7,41 +7,25 @@
*/
import {
- ACTION_ADD_PANEL,
EmbeddableFactory,
EmbeddableFactoryDefinition,
- EmbeddablePackageState,
ErrorEmbeddable,
} from '@kbn/embeddable-plugin/public';
import { lazyLoadReduxToolsPackage } from '@kbn/presentation-util-plugin/public';
import { EmbeddablePersistableStateService } from '@kbn/embeddable-plugin/common';
import { DashboardContainer } from '@kbn/dashboard-plugin/public/dashboard_container';
-import {
- NavigationEmbeddableByValueInput,
- NavigationEmbeddableByReferenceInput,
- NavigationEmbeddableInput,
-} from './types';
+import { NavigationEmbeddableByReferenceInput, NavigationEmbeddableInput } from './types';
import { APP_ICON, APP_NAME, CONTENT_ID } from '../../common';
import type { NavigationEmbeddable } from './navigation_embeddable';
-import { NAV_VERTICAL_LAYOUT } from '../../common/content_management';
import { getNavigationEmbeddableAttributeService } from '../services/attribute_service';
import { coreServices, untilPluginStartServicesReady } from '../services/kibana_services';
export type NavigationEmbeddableFactory = EmbeddableFactory;
-export interface NavigationEmbeddableCreationOptions {
- getInitialInput?: () => Partial;
- getIncomingEmbeddable?: () => EmbeddablePackageState | undefined;
-}
-
// TODO: Replace string 'OPEN_FLYOUT_ADD_DRILLDOWN' with constant as part of https://github.com/elastic/kibana/issues/154381
-const getDefaultNavigationEmbeddableInput = (): Omit => ({
- attributes: {
- title: '',
- layout: NAV_VERTICAL_LAYOUT,
- },
- disabledActions: [ACTION_ADD_PANEL, 'OPEN_FLYOUT_ADD_DRILLDOWN'],
+const getDefaultNavigationEmbeddableInput = (): Partial => ({
+ disabledActions: ['OPEN_FLYOUT_ADD_DRILLDOWN'],
});
export class NavigationEmbeddableFactoryDefinition
diff --git a/src/plugins/navigation_embeddable/public/embeddable/navigation_embeddable_reducers.ts b/src/plugins/navigation_embeddable/public/embeddable/navigation_embeddable_reducers.ts
index 29a79c4f6154f..b671c395e061b 100644
--- a/src/plugins/navigation_embeddable/public/embeddable/navigation_embeddable_reducers.ts
+++ b/src/plugins/navigation_embeddable/public/embeddable/navigation_embeddable_reducers.ts
@@ -11,6 +11,7 @@ import { WritableDraft } from 'immer/dist/types/types-external';
import { PayloadAction } from '@reduxjs/toolkit';
import { NavigationEmbeddableReduxState } from './types';
+import { NavigationEmbeddableAttributes } from '../../common/content_management';
export const navigationEmbeddableReducers = {
/**
@@ -24,4 +25,11 @@ export const navigationEmbeddableReducers = {
) => {
state.output.loading = action.payload;
},
+
+ setAttributes: (
+ state: WritableDraft,
+ action: PayloadAction
+ ) => {
+ state.componentState = { ...action.payload };
+ },
};
diff --git a/src/plugins/navigation_embeddable/public/embeddable/types.ts b/src/plugins/navigation_embeddable/public/embeddable/types.ts
index 04e79731b6a77..8744dd613c4b5 100644
--- a/src/plugins/navigation_embeddable/public/embeddable/types.ts
+++ b/src/plugins/navigation_embeddable/public/embeddable/types.ts
@@ -82,10 +82,10 @@ export type NavigationEmbeddableOutput = EmbeddableOutput & {
/**
* Navigation embeddable redux state
*/
-// export interface NavigationEmbeddableComponentState {} // TODO: Uncomment this if we end up needing component state
+export type NavigationEmbeddableComponentState = NavigationEmbeddableAttributes;
export type NavigationEmbeddableReduxState = ReduxEmbeddableState<
NavigationEmbeddableInput,
NavigationEmbeddableOutput,
- {} // We currently don't have any component state - TODO: Replace with `NavigationEmbeddableComponentState` if necessary
+ NavigationEmbeddableComponentState
>;
From 409a8f946dc40d1c31d8d3c923ccdcf0e0e37c08 Mon Sep 17 00:00:00 2001
From: Nick Peihl
Date: Thu, 31 Aug 2023 16:07:30 -0400
Subject: [PATCH 12/53] [Dashboard navigation] Inject/extract references
(#164330)
Fixes https://github.com/elastic/kibana/issues/154363
## Summary
Extracts and injects references to Dashboards in Link panels to allow
sharing / importing to other Spaces.
To test this:
1. Create an empty dashboard.
1. Create a Links panel on the dashboard and save to library.
1. Save the dashboard as one or more new dashboards.
1. Update the Links panel with links to the new dashboards.
1. Create two new Spaces, "space1" and "space2" with the default
settings.
1. In Saved Objects, click the Actions button for your Links and choose
"Copy to Spaces".
1. Copy the Links to "space1".
1. Switch to the "space1" space and verify all the dashboards using that
Links panel were included.
1. In Saved Objects, select the Links saved object and export it
including all related objects
1. Switch to "space2" and import the Links saved object from the file
downloaded in the previous step.
1. Verify all the dashboards were also imported.
1. Inspect the saved object for the Links. The `references` array should
contain objects for each dashboard.
1. The `attributes.links[].destination` property should be a string in
the format of `links__dashboard`. This string should match one
of the `references[].id`.
---------
Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
---
.../lib/find_dashboards.ts | 1 +
.../common/content_management/index.ts | 1 +
.../content_management/v1/cm_services.ts | 24 ++-
.../common/content_management/v1/constants.ts | 2 +
.../common/content_management/v1/index.ts | 1 +
.../common/content_management/v1/types.ts | 17 +-
.../common/embeddable/extract.test.ts | 64 +++++++
.../common/embeddable/extract.ts | 35 ++++
.../common/embeddable/index.ts | 10 ++
.../common/embeddable/inject.test.ts | 69 ++++++++
.../common/embeddable/inject.ts | 40 +++++
.../common/embeddable/types.ts | 14 ++
.../common/persistable_state/index.ts | 9 +
.../persistable_state/references.test.ts | 163 ++++++++++++++++++
.../common/persistable_state/references.ts | 81 +++++++++
.../navigation_embeddable/jest.config.js | 18 ++
.../dashboard_link_component.tsx | 2 +-
.../navigation_embeddable_factory.ts | 15 +-
.../public/services/attribute_service.ts | 17 +-
.../navigation_embeddable/tsconfig.json | 1 +
20 files changed, 560 insertions(+), 24 deletions(-)
create mode 100644 src/plugins/navigation_embeddable/common/embeddable/extract.test.ts
create mode 100644 src/plugins/navigation_embeddable/common/embeddable/extract.ts
create mode 100644 src/plugins/navigation_embeddable/common/embeddable/index.ts
create mode 100644 src/plugins/navigation_embeddable/common/embeddable/inject.test.ts
create mode 100644 src/plugins/navigation_embeddable/common/embeddable/inject.ts
create mode 100644 src/plugins/navigation_embeddable/common/embeddable/types.ts
create mode 100644 src/plugins/navigation_embeddable/common/persistable_state/index.ts
create mode 100644 src/plugins/navigation_embeddable/common/persistable_state/references.test.ts
create mode 100644 src/plugins/navigation_embeddable/common/persistable_state/references.ts
create mode 100644 src/plugins/navigation_embeddable/jest.config.js
diff --git a/src/plugins/dashboard/public/services/dashboard_content_management/lib/find_dashboards.ts b/src/plugins/dashboard/public/services/dashboard_content_management/lib/find_dashboards.ts
index 0989c46c6d975..efeaa76297f9e 100644
--- a/src/plugins/dashboard/public/services/dashboard_content_management/lib/find_dashboards.ts
+++ b/src/plugins/dashboard/public/services/dashboard_content_management/lib/find_dashboards.ts
@@ -80,6 +80,7 @@ export async function findDashboardById(
id,
status: 'success',
attributes: cachedDashboard.item.attributes,
+ references: cachedDashboard.item.references,
};
}
/** Otherwise, fetch the dashboard from the content management client, add it to the cache, and return the result */
diff --git a/src/plugins/navigation_embeddable/common/content_management/index.ts b/src/plugins/navigation_embeddable/common/content_management/index.ts
index 7b26870c7ce53..1dbb901c8cf8f 100644
--- a/src/plugins/navigation_embeddable/common/content_management/index.ts
+++ b/src/plugins/navigation_embeddable/common/content_management/index.ts
@@ -24,6 +24,7 @@ export {
DASHBOARD_LINK_TYPE,
NAV_VERTICAL_LAYOUT,
NAV_HORIZONTAL_LAYOUT,
+ EXTERNAL_LINK_SUPPORTED_PROTOCOLS,
} from './latest';
export * as NavigationEmbeddableV1 from './v1';
diff --git a/src/plugins/navigation_embeddable/common/content_management/v1/cm_services.ts b/src/plugins/navigation_embeddable/common/content_management/v1/cm_services.ts
index 3c9c7a1bb759c..9e4452cb09c0e 100644
--- a/src/plugins/navigation_embeddable/common/content_management/v1/cm_services.ts
+++ b/src/plugins/navigation_embeddable/common/content_management/v1/cm_services.ts
@@ -16,21 +16,35 @@ import {
objectTypeToGetResultSchema,
} from '@kbn/content-management-utils';
import { DASHBOARD_LINK_TYPE, EXTERNAL_LINK_TYPE } from '.';
-import { NAV_HORIZONTAL_LAYOUT, NAV_VERTICAL_LAYOUT } from './constants';
+import {
+ EXTERNAL_LINK_SUPPORTED_PROTOCOLS,
+ NAV_HORIZONTAL_LAYOUT,
+ NAV_VERTICAL_LAYOUT,
+} from './constants';
-const navigationEmbeddableLinkSchema = schema.object({
+const baseNavigationEmbeddableLinkSchema = {
id: schema.string(),
- type: schema.oneOf([schema.literal(DASHBOARD_LINK_TYPE), schema.literal(EXTERNAL_LINK_TYPE)]),
- destination: schema.string(),
label: schema.maybe(schema.string()),
order: schema.number(),
+};
+
+const dashboardLinkSchema = schema.object({
+ ...baseNavigationEmbeddableLinkSchema,
+ destinationRefName: schema.string(),
+ type: schema.literal(DASHBOARD_LINK_TYPE),
+});
+
+const externalLinkSchema = schema.object({
+ ...baseNavigationEmbeddableLinkSchema,
+ type: schema.literal(EXTERNAL_LINK_TYPE),
+ destination: schema.uri({ scheme: EXTERNAL_LINK_SUPPORTED_PROTOCOLS }),
});
const navigationEmbeddableAttributesSchema = schema.object(
{
title: schema.string(),
description: schema.maybe(schema.string()),
- links: schema.maybe(schema.arrayOf(navigationEmbeddableLinkSchema)),
+ links: schema.arrayOf(schema.oneOf([dashboardLinkSchema, externalLinkSchema])),
layout: schema.maybe(
schema.oneOf([schema.literal(NAV_HORIZONTAL_LAYOUT), schema.literal(NAV_VERTICAL_LAYOUT)])
),
diff --git a/src/plugins/navigation_embeddable/common/content_management/v1/constants.ts b/src/plugins/navigation_embeddable/common/content_management/v1/constants.ts
index 70f1af5c0f69d..a03894ba6b715 100644
--- a/src/plugins/navigation_embeddable/common/content_management/v1/constants.ts
+++ b/src/plugins/navigation_embeddable/common/content_management/v1/constants.ts
@@ -17,3 +17,5 @@ export const EXTERNAL_LINK_TYPE = 'externalLink';
*/
export const NAV_HORIZONTAL_LAYOUT = 'horizontal';
export const NAV_VERTICAL_LAYOUT = 'vertical';
+
+export const EXTERNAL_LINK_SUPPORTED_PROTOCOLS = ['http', 'https', 'mailto'];
diff --git a/src/plugins/navigation_embeddable/common/content_management/v1/index.ts b/src/plugins/navigation_embeddable/common/content_management/v1/index.ts
index efda7e1cf696c..b0af2ee1a936a 100644
--- a/src/plugins/navigation_embeddable/common/content_management/v1/index.ts
+++ b/src/plugins/navigation_embeddable/common/content_management/v1/index.ts
@@ -20,4 +20,5 @@ export {
DASHBOARD_LINK_TYPE,
NAV_VERTICAL_LAYOUT,
NAV_HORIZONTAL_LAYOUT,
+ EXTERNAL_LINK_SUPPORTED_PROTOCOLS,
} from './constants';
diff --git a/src/plugins/navigation_embeddable/common/content_management/v1/types.ts b/src/plugins/navigation_embeddable/common/content_management/v1/types.ts
index bb5c6c10c584b..ff93f6462915f 100644
--- a/src/plugins/navigation_embeddable/common/content_management/v1/types.ts
+++ b/src/plugins/navigation_embeddable/common/content_management/v1/types.ts
@@ -35,14 +35,25 @@ export type NavigationEmbeddableCrudTypes = ContentManagementCrudTypes<
*/
export type NavigationLinkType = typeof DASHBOARD_LINK_TYPE | typeof EXTERNAL_LINK_TYPE;
-export interface NavigationEmbeddableLink {
+interface BaseNavigationEmbeddableLink {
id: string;
- type: NavigationLinkType;
- destination: string;
label?: string;
order: number;
+ destination?: string;
+}
+
+interface DashboardLink extends BaseNavigationEmbeddableLink {
+ type: typeof DASHBOARD_LINK_TYPE;
+ destinationRefName?: string;
}
+interface ExternalLink extends BaseNavigationEmbeddableLink {
+ type: typeof EXTERNAL_LINK_TYPE;
+ destination: string;
+}
+
+export type NavigationEmbeddableLink = DashboardLink | ExternalLink;
+
export type NavigationLayoutType = typeof NAV_HORIZONTAL_LAYOUT | typeof NAV_VERTICAL_LAYOUT;
// eslint-disable-next-line @typescript-eslint/consistent-type-definitions
diff --git a/src/plugins/navigation_embeddable/common/embeddable/extract.test.ts b/src/plugins/navigation_embeddable/common/embeddable/extract.test.ts
new file mode 100644
index 0000000000000..1fe746b722f8a
--- /dev/null
+++ b/src/plugins/navigation_embeddable/common/embeddable/extract.test.ts
@@ -0,0 +1,64 @@
+/*
+ * 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 { extract } from './extract';
+
+test('Should return original state and empty references with by-reference embeddable state', () => {
+ const navigationEmbeddableByReferenceInput = {
+ id: '2192e502-0ec7-4316-82fb-c9bbf78525c4',
+ type: 'navigation_embeddable',
+ };
+
+ expect(extract!(navigationEmbeddableByReferenceInput)).toEqual({
+ state: navigationEmbeddableByReferenceInput,
+ references: [],
+ });
+});
+
+test('Should update state with refNames with by-value embeddable state', () => {
+ const navigationEmbeddableByValueInput = {
+ id: '8d62c3f0-c61f-4c09-ac24-9b8ee4320e20',
+ attributes: {
+ links: [
+ {
+ type: 'dashboardLink',
+ id: 'fc7b8c70-2eb9-40b2-936d-457d1721a438',
+ destination: 'elastic_agent-1a4e7280-6b5e-11ed-98de-67bdecd21824',
+ order: 0,
+ },
+ ],
+ layout: 'horizontal',
+ },
+ type: 'navigation_embeddable',
+ };
+
+ expect(extract!(navigationEmbeddableByValueInput)).toEqual({
+ references: [
+ {
+ name: 'link_fc7b8c70-2eb9-40b2-936d-457d1721a438_dashboard',
+ type: 'dashboard',
+ id: 'elastic_agent-1a4e7280-6b5e-11ed-98de-67bdecd21824',
+ },
+ ],
+ state: {
+ id: '8d62c3f0-c61f-4c09-ac24-9b8ee4320e20',
+ attributes: {
+ links: [
+ {
+ type: 'dashboardLink',
+ id: 'fc7b8c70-2eb9-40b2-936d-457d1721a438',
+ destinationRefName: 'link_fc7b8c70-2eb9-40b2-936d-457d1721a438_dashboard',
+ order: 0,
+ },
+ ],
+ layout: 'horizontal',
+ },
+ type: 'navigation_embeddable',
+ },
+ });
+});
diff --git a/src/plugins/navigation_embeddable/common/embeddable/extract.ts b/src/plugins/navigation_embeddable/common/embeddable/extract.ts
new file mode 100644
index 0000000000000..9d0e9c0c61b13
--- /dev/null
+++ b/src/plugins/navigation_embeddable/common/embeddable/extract.ts
@@ -0,0 +1,35 @@
+/*
+ * 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 { EmbeddableRegistryDefinition } from '@kbn/embeddable-plugin/common';
+import type { NavigationEmbeddableAttributes } from '../content_management';
+import { extractReferences } from '../persistable_state';
+import { NavigationEmbeddablePersistableState } from './types';
+
+export const extract: EmbeddableRegistryDefinition['extract'] = (state) => {
+ const typedState = state as NavigationEmbeddablePersistableState;
+
+ // by-reference embeddable
+ if (!('attributes' in typedState) || typedState.attributes === undefined) {
+ // No references to extract for by-reference embeddable since all references are stored with by-reference saved object
+ return { state, references: [] };
+ }
+
+ // by-value embeddable
+ const { attributes, references } = extractReferences({
+ attributes: typedState.attributes as unknown as NavigationEmbeddableAttributes,
+ });
+
+ return {
+ state: {
+ ...state,
+ attributes,
+ },
+ references,
+ };
+};
diff --git a/src/plugins/navigation_embeddable/common/embeddable/index.ts b/src/plugins/navigation_embeddable/common/embeddable/index.ts
new file mode 100644
index 0000000000000..c526b0bf9bff8
--- /dev/null
+++ b/src/plugins/navigation_embeddable/common/embeddable/index.ts
@@ -0,0 +1,10 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 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 { inject } from './inject';
+export { extract } from './extract';
diff --git a/src/plugins/navigation_embeddable/common/embeddable/inject.test.ts b/src/plugins/navigation_embeddable/common/embeddable/inject.test.ts
new file mode 100644
index 0000000000000..bf0d9439cb811
--- /dev/null
+++ b/src/plugins/navigation_embeddable/common/embeddable/inject.test.ts
@@ -0,0 +1,69 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 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 { inject } from './inject';
+
+test('Should return original state with by-reference embeddable state', () => {
+ const navigationEmbeddableByReferenceInput = {
+ id: 'ea40fd4e-216c-49a7-917f-f733c8a2c817',
+ type: 'navigation_embeddable',
+ };
+
+ const references = [
+ {
+ name: 'panel_ea40fd4e-216c-49a7-917f-f733c8a2c817',
+ type: 'navigation_embeddable',
+ id: '7f92d7d0-8e5f-11ec-9477-312c8a6de896',
+ },
+ ];
+
+ expect(inject!(navigationEmbeddableByReferenceInput, references)).toEqual(
+ navigationEmbeddableByReferenceInput
+ );
+});
+
+test('Should inject refNames with by-value embeddable state', () => {
+ const navigationEmbeddableByValueInput = {
+ id: 'c3937cf9-29be-43df-a4af-a4df742d7d35',
+ attributes: {
+ links: [
+ {
+ type: 'dashboardLink',
+ id: 'fc7b8c70-2eb9-40b2-936d-457d1721a438',
+ destinationRefName: 'link_fc7b8c70-2eb9-40b2-936d-457d1721a438_dashboard',
+ order: 0,
+ },
+ ],
+ layout: 'horizontal',
+ },
+ type: 'navigation_embeddable',
+ };
+ const references = [
+ {
+ name: 'link_fc7b8c70-2eb9-40b2-936d-457d1721a438_dashboard',
+ type: 'dashboard',
+ id: 'elastic_agent-1a4e7280-6b5e-11ed-98de-67bdecd21824',
+ },
+ ];
+
+ expect(inject!(navigationEmbeddableByValueInput, references)).toEqual({
+ id: 'c3937cf9-29be-43df-a4af-a4df742d7d35',
+ attributes: {
+ links: [
+ {
+ type: 'dashboardLink',
+ id: 'fc7b8c70-2eb9-40b2-936d-457d1721a438',
+ destination: 'elastic_agent-1a4e7280-6b5e-11ed-98de-67bdecd21824',
+ order: 0,
+ },
+ ],
+ layout: 'horizontal',
+ },
+ type: 'navigation_embeddable',
+ });
+});
diff --git a/src/plugins/navigation_embeddable/common/embeddable/inject.ts b/src/plugins/navigation_embeddable/common/embeddable/inject.ts
new file mode 100644
index 0000000000000..ccf06491407ea
--- /dev/null
+++ b/src/plugins/navigation_embeddable/common/embeddable/inject.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 { EmbeddableRegistryDefinition } from '@kbn/embeddable-plugin/common';
+import { NavigationEmbeddableAttributes } from '../content_management';
+import { injectReferences } from '../persistable_state';
+import { NavigationEmbeddablePersistableState } from './types';
+
+export const inject: EmbeddableRegistryDefinition['inject'] = (state, references) => {
+ const typedState = state as NavigationEmbeddablePersistableState;
+
+ // by-reference embeddable
+ if (!('attributes' in typedState) || typedState.attributes === undefined) {
+ return typedState;
+ }
+
+ // by-value embeddable
+ try {
+ const { attributes: attributesWithInjectedIds } = injectReferences({
+ attributes: typedState.attributes as unknown as NavigationEmbeddableAttributes,
+ references,
+ });
+
+ return {
+ ...typedState,
+ attributes: attributesWithInjectedIds,
+ };
+ } catch (error) {
+ // inject exception prevents entire dashboard from display
+ // Instead of throwing, swallow error and let dashboard display
+ // Errors will surface in navigation embeddable panel.
+ // Users can then manually edit links to resolve any problems.
+ return typedState;
+ }
+};
diff --git a/src/plugins/navigation_embeddable/common/embeddable/types.ts b/src/plugins/navigation_embeddable/common/embeddable/types.ts
new file mode 100644
index 0000000000000..80386fbfc1b2b
--- /dev/null
+++ b/src/plugins/navigation_embeddable/common/embeddable/types.ts
@@ -0,0 +1,14 @@
+/*
+ * 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 { EmbeddableStateWithType } from '@kbn/embeddable-plugin/common';
+import { SerializableRecord } from '@kbn/utility-types';
+
+export type NavigationEmbeddablePersistableState = EmbeddableStateWithType & {
+ attributes: SerializableRecord;
+};
diff --git a/src/plugins/navigation_embeddable/common/persistable_state/index.ts b/src/plugins/navigation_embeddable/common/persistable_state/index.ts
new file mode 100644
index 0000000000000..c3e09839f0f2f
--- /dev/null
+++ b/src/plugins/navigation_embeddable/common/persistable_state/index.ts
@@ -0,0 +1,9 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+export { extractReferences, injectReferences } from './references';
diff --git a/src/plugins/navigation_embeddable/common/persistable_state/references.test.ts b/src/plugins/navigation_embeddable/common/persistable_state/references.test.ts
new file mode 100644
index 0000000000000..cf74ba929b1aa
--- /dev/null
+++ b/src/plugins/navigation_embeddable/common/persistable_state/references.test.ts
@@ -0,0 +1,163 @@
+/*
+ * 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 { DASHBOARD_LINK_TYPE, EXTERNAL_LINK_TYPE } from '../content_management';
+import { extractReferences, injectReferences } from './references';
+
+describe('extractReferences', () => {
+ test('should handle missing links attribute', () => {
+ const attributes = {
+ title: 'my links',
+ };
+ expect(extractReferences({ attributes })).toEqual({
+ attributes: {
+ title: 'my links',
+ },
+ references: [],
+ });
+ });
+
+ test('should extract dashboard references from dashboard links', () => {
+ const attributes = {
+ title: 'my links',
+ links: [
+ {
+ id: 'fb1b3fc7-6e12-4542-bcf5-c61ad77241c5',
+ type: DASHBOARD_LINK_TYPE as typeof DASHBOARD_LINK_TYPE,
+ destination: '19e149f0-e95e-404b-b6f8-fc751317c6be',
+ order: 0,
+ },
+ {
+ id: '4d5cd000-5632-4d3a-ad41-11d7800ff2aa',
+ type: EXTERNAL_LINK_TYPE as typeof EXTERNAL_LINK_TYPE,
+ destination: 'https://example.com',
+ order: 1,
+ },
+ {
+ id: '1409fabb-1d2b-49c2-a2dc-705bd8fabd0c',
+ type: DASHBOARD_LINK_TYPE as typeof DASHBOARD_LINK_TYPE,
+ destination: '39555f99-a3b8-4210-b1ef-fa0fa86fa3da',
+ order: 2,
+ },
+ ],
+ };
+ expect(extractReferences({ attributes })).toEqual({
+ attributes: {
+ title: 'my links',
+ links: [
+ {
+ id: 'fb1b3fc7-6e12-4542-bcf5-c61ad77241c5',
+ type: 'dashboardLink',
+ destinationRefName: 'link_fb1b3fc7-6e12-4542-bcf5-c61ad77241c5_dashboard',
+ order: 0,
+ },
+ {
+ id: '4d5cd000-5632-4d3a-ad41-11d7800ff2aa',
+ type: 'externalLink',
+ destination: 'https://example.com',
+ order: 1,
+ },
+ {
+ id: '1409fabb-1d2b-49c2-a2dc-705bd8fabd0c',
+ type: 'dashboardLink',
+ destinationRefName: 'link_1409fabb-1d2b-49c2-a2dc-705bd8fabd0c_dashboard',
+ order: 2,
+ },
+ ],
+ },
+ references: [
+ {
+ id: '19e149f0-e95e-404b-b6f8-fc751317c6be',
+ name: 'link_fb1b3fc7-6e12-4542-bcf5-c61ad77241c5_dashboard',
+ type: 'dashboard',
+ },
+ {
+ id: '39555f99-a3b8-4210-b1ef-fa0fa86fa3da',
+ name: 'link_1409fabb-1d2b-49c2-a2dc-705bd8fabd0c_dashboard',
+ type: 'dashboard',
+ },
+ ],
+ });
+ });
+});
+
+describe('injectReferences', () => {
+ test('should handle missing links attribute', () => {
+ const attributes = {
+ title: 'my links',
+ };
+ expect(injectReferences({ attributes, references: [] })).toEqual({
+ attributes: {
+ title: 'my links',
+ },
+ });
+ });
+
+ test('should inject dashboard references into dashboard links', () => {
+ const attributes = {
+ title: 'my links',
+ links: [
+ {
+ id: 'fb1b3fc7-6e12-4542-bcf5-c61ad77241c5',
+ type: DASHBOARD_LINK_TYPE as typeof DASHBOARD_LINK_TYPE,
+ destinationRefName: 'link_fb1b3fc7-6e12-4542-bcf5-c61ad77241c5_dashboard',
+ order: 0,
+ },
+ {
+ id: '4d5cd000-5632-4d3a-ad41-11d7800ff2aa',
+ type: EXTERNAL_LINK_TYPE as typeof EXTERNAL_LINK_TYPE,
+ destination: 'https://example.com',
+ order: 1,
+ },
+ {
+ id: '1409fabb-1d2b-49c2-a2dc-705bd8fabd0c',
+ type: DASHBOARD_LINK_TYPE as typeof DASHBOARD_LINK_TYPE,
+ destinationRefName: 'link_1409fabb-1d2b-49c2-a2dc-705bd8fabd0c_dashboard',
+ order: 2,
+ },
+ ],
+ };
+ const references = [
+ {
+ id: '19e149f0-e95e-404b-b6f8-fc751317c6be',
+ name: 'link_fb1b3fc7-6e12-4542-bcf5-c61ad77241c5_dashboard',
+ type: 'dashboard',
+ },
+ {
+ id: '39555f99-a3b8-4210-b1ef-fa0fa86fa3da',
+ name: 'link_1409fabb-1d2b-49c2-a2dc-705bd8fabd0c_dashboard',
+ type: 'dashboard',
+ },
+ ];
+ expect(injectReferences({ attributes, references })).toEqual({
+ attributes: {
+ title: 'my links',
+ links: [
+ {
+ id: 'fb1b3fc7-6e12-4542-bcf5-c61ad77241c5',
+ type: 'dashboardLink',
+ destination: '19e149f0-e95e-404b-b6f8-fc751317c6be',
+ order: 0,
+ },
+ {
+ id: '4d5cd000-5632-4d3a-ad41-11d7800ff2aa',
+ type: 'externalLink',
+ destination: 'https://example.com',
+ order: 1,
+ },
+ {
+ id: '1409fabb-1d2b-49c2-a2dc-705bd8fabd0c',
+ type: 'dashboardLink',
+ destination: '39555f99-a3b8-4210-b1ef-fa0fa86fa3da',
+ order: 2,
+ },
+ ],
+ },
+ });
+ });
+});
diff --git a/src/plugins/navigation_embeddable/common/persistable_state/references.ts b/src/plugins/navigation_embeddable/common/persistable_state/references.ts
new file mode 100644
index 0000000000000..8cb0047534279
--- /dev/null
+++ b/src/plugins/navigation_embeddable/common/persistable_state/references.ts
@@ -0,0 +1,81 @@
+/*
+ * 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 { Reference } from '@kbn/content-management-utils';
+import { DASHBOARD_LINK_TYPE, NavigationEmbeddableAttributes } from '../content_management';
+
+export function extractReferences({
+ attributes,
+ references = [],
+}: {
+ attributes: NavigationEmbeddableAttributes;
+ references?: Reference[];
+}) {
+ if (!attributes.links) {
+ return { attributes, references };
+ }
+
+ const { links } = attributes;
+ const extractedReferences: Reference[] = [];
+ links.forEach((link) => {
+ if (link.type === DASHBOARD_LINK_TYPE && link.destination) {
+ const refName = `link_${link.id}_dashboard`;
+ link.destinationRefName = refName;
+ extractedReferences.push({
+ name: refName,
+ type: 'dashboard',
+ id: link.destination,
+ });
+ delete link.destination;
+ }
+ });
+
+ return {
+ attributes: {
+ ...attributes,
+ links,
+ },
+ references: references.concat(extractedReferences),
+ };
+}
+
+function findReference(targetName: string, references: Reference[]) {
+ const reference = references.find(({ name }) => name === targetName);
+ if (!reference) {
+ throw new Error(`Could not find reference "${targetName}"`);
+ }
+ return reference;
+}
+
+export function injectReferences({
+ attributes,
+ references,
+}: {
+ attributes: NavigationEmbeddableAttributes;
+ references: Reference[];
+}) {
+ if (!attributes.links) {
+ return { attributes };
+ }
+
+ const { links } = attributes;
+ links.forEach((link) => {
+ if (link.type === DASHBOARD_LINK_TYPE && link.destinationRefName) {
+ const reference = findReference(link.destinationRefName, references);
+ link.destination = reference.id;
+ delete link.destinationRefName;
+ }
+ });
+
+ return {
+ attributes: {
+ ...attributes,
+ links,
+ },
+ };
+}
diff --git a/src/plugins/navigation_embeddable/jest.config.js b/src/plugins/navigation_embeddable/jest.config.js
new file mode 100644
index 0000000000000..d864b4a234669
--- /dev/null
+++ b/src/plugins/navigation_embeddable/jest.config.js
@@ -0,0 +1,18 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+module.exports = {
+ preset: '@kbn/test',
+ rootDir: '../../..',
+ roots: ['/src/plugins/navigation_embeddable'],
+ coverageDirectory: '/target/kibana-coverage/jest/src/plugins/navigation_embeddable',
+ coverageReporters: ['text', 'html'],
+ collectCoverageFrom: [
+ '/src/plugins/navigation_embeddable/{common,public,server}/**/*.{ts,tsx}',
+ ],
+};
diff --git a/src/plugins/navigation_embeddable/public/components/dashboard_link/dashboard_link_component.tsx b/src/plugins/navigation_embeddable/public/components/dashboard_link/dashboard_link_component.tsx
index 8259d98cce4b6..00f12d65b36c1 100644
--- a/src/plugins/navigation_embeddable/public/components/dashboard_link/dashboard_link_component.tsx
+++ b/src/plugins/navigation_embeddable/public/components/dashboard_link/dashboard_link_component.tsx
@@ -42,7 +42,7 @@ export const DashboardLinkComponent = ({
const { loading: loadingDestinationDashboard, value: destinationDashboard } =
useAsync(async () => {
- if (link.id !== parentDashboardId) {
+ if (link.id !== parentDashboardId && link.destination) {
/**
* only fetch the dashboard if it's not the current dashboard - if it is the current dashboard,
* use `dashboardContainer` and its corresponding state (title, description, etc.) instead.
diff --git a/src/plugins/navigation_embeddable/public/embeddable/navigation_embeddable_factory.ts b/src/plugins/navigation_embeddable/public/embeddable/navigation_embeddable_factory.ts
index 3226833f1f9c8..32aab131ddb1b 100644
--- a/src/plugins/navigation_embeddable/public/embeddable/navigation_embeddable_factory.ts
+++ b/src/plugins/navigation_embeddable/public/embeddable/navigation_embeddable_factory.ts
@@ -12,7 +12,6 @@ import {
ErrorEmbeddable,
} from '@kbn/embeddable-plugin/public';
import { lazyLoadReduxToolsPackage } from '@kbn/presentation-util-plugin/public';
-import { EmbeddablePersistableStateService } from '@kbn/embeddable-plugin/common';
import { DashboardContainer } from '@kbn/dashboard-plugin/public/dashboard_container';
import { NavigationEmbeddableByReferenceInput, NavigationEmbeddableInput } from './types';
@@ -20,6 +19,7 @@ import { APP_ICON, APP_NAME, CONTENT_ID } from '../../common';
import type { NavigationEmbeddable } from './navigation_embeddable';
import { getNavigationEmbeddableAttributeService } from '../services/attribute_service';
import { coreServices, untilPluginStartServicesReady } from '../services/kibana_services';
+import { extract, inject } from '../../common/embeddable';
export type NavigationEmbeddableFactory = EmbeddableFactory;
@@ -41,15 +41,6 @@ export class NavigationEmbeddableFactoryDefinition
getIconForSavedObject: () => APP_ICON,
};
- // TODO create functions
- // public inject: EmbeddablePersistableStateService['inject'];
- // public extract: EmbeddablePersistableStateService['extract'];
-
- constructor(persistableStateService: EmbeddablePersistableStateService) {
- // this.inject = createInject(this.persistableStateService);
- // this.extract = createExtract(this.persistableStateService);
- }
-
public async isEditable() {
await untilPluginStartServicesReady();
return Boolean(coreServices.application.capabilities.dashboard?.showWriteControls);
@@ -116,4 +107,8 @@ export class NavigationEmbeddableFactoryDefinition
public getIconType() {
return 'link';
}
+
+ inject = inject;
+
+ extract = extract;
}
diff --git a/src/plugins/navigation_embeddable/public/services/attribute_service.ts b/src/plugins/navigation_embeddable/public/services/attribute_service.ts
index 7a7dbfe2bd139..74530871fc469 100644
--- a/src/plugins/navigation_embeddable/public/services/attribute_service.ts
+++ b/src/plugins/navigation_embeddable/public/services/attribute_service.ts
@@ -11,6 +11,7 @@ import { AttributeService } from '@kbn/embeddable-plugin/public';
import type { OnSaveProps } from '@kbn/saved-objects-plugin/public';
import { SharingSavedObjectProps } from '../../common/types';
import { NavigationEmbeddableAttributes } from '../../common/content_management';
+import { extractReferences, injectReferences } from '../../common/persistable_state';
import {
NavigationEmbeddableByReferenceInput,
NavigationEmbeddableByValueInput,
@@ -45,12 +46,19 @@ export function getNavigationEmbeddableAttributeService(): NavigationEmbeddableA
NavigationEmbeddableUnwrapMetaInfo
>(CONTENT_ID, {
saveMethod: async (attributes: NavigationEmbeddableDocument, savedObjectId?: string) => {
- // TODO extract references
+ const { attributes: updatedAttributes, references } = extractReferences({
+ attributes,
+ references: attributes.references,
+ });
const {
item: { id },
} = await (savedObjectId
- ? navigationEmbeddableClient.update({ id: savedObjectId, data: attributes })
- : navigationEmbeddableClient.create({ data: attributes, options: { references: [] } }));
+ ? navigationEmbeddableClient.update({
+ id: savedObjectId,
+ data: updatedAttributes,
+ options: { references },
+ })
+ : navigationEmbeddableClient.create({ data: updatedAttributes, options: { references } }));
return { id };
},
unwrapMethod: async (
@@ -65,8 +73,7 @@ export function getNavigationEmbeddableAttributeService(): NavigationEmbeddableA
} = await navigationEmbeddableClient.get(savedObjectId);
if (savedObject.error) throw savedObject.error;
- // TODO inject references
- const attributes = savedObject.attributes;
+ const { attributes } = injectReferences(savedObject);
return {
attributes,
metaInfo: {
diff --git a/src/plugins/navigation_embeddable/tsconfig.json b/src/plugins/navigation_embeddable/tsconfig.json
index ae66fa7cc3144..afcc5945c2082 100644
--- a/src/plugins/navigation_embeddable/tsconfig.json
+++ b/src/plugins/navigation_embeddable/tsconfig.json
@@ -20,6 +20,7 @@
"@kbn/saved-objects-plugin",
"@kbn/core-saved-objects-server",
"@kbn/saved-objects-plugin",
+ "@kbn/utility-types",
],
"exclude": ["target/**/*"]
}
From 84b7b175c1525feb31d79d002d99677c14ebb547 Mon Sep 17 00:00:00 2001
From: Hannah Mudge
Date: Tue, 5 Sep 2023 12:22:17 -0600
Subject: [PATCH 13/53] [Dashboard Navigation] Drilldown on link click
(#164196)
Closes https://github.com/elastic/kibana/issues/154381
## Summary
This PR adds functionality to the link embeddable so that
1) dashboard to dashboard links will now navigate between dashboards,
taking the context (filters, time range, queries, etc.) as the user
navigates (assuming the appropriate settings are applied) and
2) URL links will now navigate to internal and/or external URLs as
expected
### Dashboard Links
https://github.com/elastic/kibana/assets/8698078/1034b454-3add-48c2-8505-44d89e2d87d0
> **Note**
> In the above video, all links were configured to include queries,
filter pills, and the selected time range in the passed context. It is
possible to disable these settings, if necessary.
#### Link settings
We want dashboard drilldowns and dashboard links to work more-or-less
the same way, which means that they should have identical settings -
therefore, in order to share the same settings between dashboard
drilldowns + dashboard links, I created the
`DashboardDrilldownOptionsComponent`:
![image](https://github.com/elastic/kibana/assets/8698078/5c809c7a-c9ef-43d7-b2ea-1d706858228a)
I put this new component into `presentation_util` so that it could
easily be shared and used in the respective editing experiences.
### Testing
- When testing, special attention should be paid to the different link
settings - ensure that all settings are respected.
- Ensure that `ctrl + click`, `shift + click`, and `meta + click` work
as expected, regardless of whether the `Open in new tab` setting is true
or false - specifically, we should use the **URL** to pass the
filters/queries/time range whenever a dashboard is being opened in a new
tab.
### URL Links
https://github.com/elastic/kibana/assets/8698078/5abf9772-17b3-4ab4-a704-4a3ff432fa23
#### Link settings
Similar to the dashboard links, we want the
![image](https://github.com/elastic/kibana/assets/8698078/e9de2c81-fb2e-4d53-a610-7327466e4246)
### Testing
When testing, it is important to ensure that the new URL validation
works as expected. For this, there are technically two types of invalid
links:
1) links that do not fit the expected format; for example, we do not
accept any links that start with something other than `http`, `https`,
or `mailto`.
2) links that are disallowed due to the `externalUrl.policy`
Testing number one should be simple; however, in order to test the
second type of invalid links, you need to modify your `kibana.dev.yml`
to include something like the following:
```
externalUrl.policy:
- allow: false
host: danger.example.com
- allow: true
host: example.com
protocol: https
```
Refer to
http://www.elastic.co/guide/en/kibana/current/url-drilldown-settings-kb.html
for more information.
Also, much like with the dashboard links, it is important to ensure that
`ctrl + click`, `shift + click`, and `meta + click` work as expected,
regardless of whether the `Open in new tab` setting is true or false.
### Checklist
- [x] Any text added follows [EUI's writing
guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses
sentence case text and includes [i18n
support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md)
- [ ] ~[Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios~ Will be
addressed in https://github.com/elastic/kibana/issues/161287
- [x] Any UI touched in this PR is usable by keyboard only (learn more
about [keyboard accessibility](https://webaim.org/techniques/keyboard/))
- [x] Any UI touched in this PR does not create any new axe failures
(run axe in browser:
[FF](https://addons.mozilla.org/en-US/firefox/addon/axe-devtools/),
[Chrome](https://chrome.google.com/webstore/detail/axe-web-accessibility-tes/lhdoppojpmngadmnindnejefpokejbdd?hl=en-US))
- [x] This renders correctly on smaller devices using a responsive
layout. (You can test this [in your
browser](https://www.browserstack.com/guide/responsive-testing-on-local-server))
- [x] This was checked for [cross-browser
compatibility](https://www.elastic.co/support/matrix#matrix_browsers)
> **Note**
> How Chrome and Firefox handle modified `onClick` events varies
significantly and, through this testing, I found a bug where `shift +
click`ing was previously not opening the links in a new tab in Firefox
specifically - this is why I had to use the default to `href` behaviour
whenever possible. Something to keep in mind when testing.
### For maintainers
- [ ] This was checked for breaking API changes and was [labeled
appropriately](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process)
---------
Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
Co-authored-by: Nick Peihl
Co-authored-by: Nick Peihl
---
.../locator/get_dashboard_locator_params.ts | 47 +++++++
.../embeddable/api/panel_management.ts | 1 -
src/plugins/dashboard/public/index.ts | 1 +
.../common/content_management/index.ts | 2 +-
.../content_management/v1/cm_services.ts | 27 +++-
.../common/content_management/v1/constants.ts | 2 -
.../common/content_management/v1/index.ts | 2 +-
.../common/content_management/v1/types.ts | 5 +
.../navigation_embeddable/kibana.jsonc | 9 +-
.../dashboard_link_component.tsx | 125 ++++++++++++------
.../dashboard_link_destination_picker.tsx | 12 +-
...link_tools.tsx => dashboard_link_tools.ts} | 69 +++++++++-
...navigation_embeddable_link_destination.tsx | 80 +++++++++++
.../navigation_embeddable_link_editor.tsx | 84 ++++--------
.../navigation_embeddable_link_options.tsx | 71 ++++++++++
.../navigation_embeddable_panel_editor.tsx | 4 +-
...avigation_embeddable_panel_editor_link.tsx | 40 ++++--
.../external_link/external_link_component.tsx | 70 +++++++++-
.../external_link_destination_picker.tsx | 52 ++++++--
.../external_link/external_link_strings.ts | 12 ++
.../external_link/external_link_tools.ts | 35 +++++
.../navigation_embeddable_component.tsx | 6 +-
.../navigation_embeddable_strings.ts | 4 +
.../navigation_embeddable_factory.ts | 3 +-
.../navigation_embeddable/tsconfig.json | 6 +-
.../dashboard_drilldown_options.tsx | 52 ++++++++
.../dashboard_drilldown_options/types.ts | 20 +++
.../public/components/index.tsx | 18 +++
.../i18n/dashboard_drilldown_config.tsx | 29 ++++
src/plugins/presentation_util/public/index.ts | 3 +
.../url_drilldown/components/index.ts | 5 +-
.../url_drilldown_collect_config/i18n.ts | 2 +-
.../url_drilldown_collect_config/index.ts | 2 +-
.../url_drilldown_collect_config/lazy.tsx | 15 +++
.../test_samples/demo.tsx | 1 +
.../url_drilldown_collect_config.tsx | 46 ++-----
.../url_drilldown_options.tsx | 57 ++++++++
.../drilldowns/url_drilldown/constants.ts | 14 ++
.../public/drilldowns/url_drilldown/index.ts | 10 +-
.../public/drilldowns/url_drilldown/types.ts | 8 +-
.../ui_actions_enhanced/public/index.ts | 3 +
.../dashboard_drilldown_persistable_state.ts | 6 +-
.../drilldowns/dashboard_drilldown/index.ts | 2 +-
.../drilldowns/dashboard_drilldown/types.ts | 10 +-
.../plugins/dashboard_enhanced/kibana.jsonc | 8 +-
.../abstract_dashboard_drilldown.tsx | 12 +-
.../components/collect_config_container.tsx | 26 +---
.../dashboard_drilldown_config.tsx | 68 +++-------
.../dashboard_drilldown_config/i18n.ts | 21 ---
.../abstract_dashboard_drilldown/types.ts | 4 +-
.../embeddable_to_dashboard_drilldown.tsx | 44 ++----
.../plugins/dashboard_enhanced/tsconfig.json | 15 +--
.../public/lib/url_drilldown.test.ts | 8 ++
.../translations/translations/fr-FR.json | 3 -
.../translations/translations/ja-JP.json | 3 -
.../translations/translations/zh-CN.json | 3 -
56 files changed, 920 insertions(+), 367 deletions(-)
create mode 100644 src/plugins/dashboard/public/dashboard_app/locator/get_dashboard_locator_params.ts
rename src/plugins/navigation_embeddable/public/components/dashboard_link/{dashboard_link_tools.tsx => dashboard_link_tools.ts} (55%)
create mode 100644 src/plugins/navigation_embeddable/public/components/editor/navigation_embeddable_link_destination.tsx
create mode 100644 src/plugins/navigation_embeddable/public/components/editor/navigation_embeddable_link_options.tsx
create mode 100644 src/plugins/navigation_embeddable/public/components/external_link/external_link_tools.ts
create mode 100644 src/plugins/presentation_util/public/components/dashboard_drilldown_options/dashboard_drilldown_options.tsx
create mode 100644 src/plugins/presentation_util/public/components/dashboard_drilldown_options/types.ts
create mode 100644 src/plugins/presentation_util/public/i18n/dashboard_drilldown_config.tsx
create mode 100644 src/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/components/url_drilldown_collect_config/url_drilldown_options.tsx
create mode 100644 src/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/constants.ts
diff --git a/src/plugins/dashboard/public/dashboard_app/locator/get_dashboard_locator_params.ts b/src/plugins/dashboard/public/dashboard_app/locator/get_dashboard_locator_params.ts
new file mode 100644
index 0000000000000..f2805d10c29da
--- /dev/null
+++ b/src/plugins/dashboard/public/dashboard_app/locator/get_dashboard_locator_params.ts
@@ -0,0 +1,47 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 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 { isQuery, isTimeRange } from '@kbn/data-plugin/common';
+import { Filter, isFilterPinned, Query, TimeRange } from '@kbn/es-query';
+import { EmbeddableInput, IEmbeddable } from '@kbn/embeddable-plugin/public';
+import { DashboardDrilldownOptions } from '@kbn/presentation-util-plugin/public';
+
+import { DashboardAppLocatorParams } from './locator';
+
+interface EmbeddableQueryInput extends EmbeddableInput {
+ query?: Query;
+ filters?: Filter[];
+ timeRange?: TimeRange;
+}
+
+export const getEmbeddableParams = (
+ source: IEmbeddable,
+ options: DashboardDrilldownOptions
+): Partial => {
+ const params: DashboardAppLocatorParams = {};
+
+ const input = source.getInput();
+ if (isQuery(input.query) && options.useCurrentFilters) {
+ params.query = input.query;
+ }
+
+ // if useCurrentDashboardDataRange is enabled, then preserve current time range
+ // if undefined is passed, then destination dashboard will figure out time range itself
+ // for brush event this time range would be overwritten
+ if (isTimeRange(input.timeRange) && options.useCurrentDateRange) {
+ params.timeRange = input.timeRange;
+ }
+
+ // if useCurrentDashboardFilters enabled, then preserve all the filters (pinned, unpinned, and from controls)
+ // otherwise preserve only pinned
+ params.filters = options.useCurrentFilters
+ ? input.filters
+ : input.filters?.filter((f) => isFilterPinned(f));
+
+ return params;
+};
diff --git a/src/plugins/dashboard/public/dashboard_container/embeddable/api/panel_management.ts b/src/plugins/dashboard/public/dashboard_container/embeddable/api/panel_management.ts
index a72bb88a56123..2a5662387c477 100644
--- a/src/plugins/dashboard/public/dashboard_container/embeddable/api/panel_management.ts
+++ b/src/plugins/dashboard/public/dashboard_container/embeddable/api/panel_management.ts
@@ -12,7 +12,6 @@ import {
IEmbeddable,
PanelState,
} from '@kbn/embeddable-plugin/public';
-import { v4 as uuidv4 } from 'uuid';
import { DashboardPanelState } from '../../../../common';
import { DashboardContainer } from '../dashboard_container';
diff --git a/src/plugins/dashboard/public/index.ts b/src/plugins/dashboard/public/index.ts
index 0bfe5dfae8b4c..290f4b7c10f28 100644
--- a/src/plugins/dashboard/public/index.ts
+++ b/src/plugins/dashboard/public/index.ts
@@ -30,6 +30,7 @@ export {
type DashboardAppLocatorParams,
cleanEmptyKeys,
} from './dashboard_app/locator/locator';
+export { getEmbeddableParams } from './dashboard_app/locator/get_dashboard_locator_params';
export function plugin(initializerContext: PluginInitializerContext) {
return new DashboardPlugin(initializerContext);
diff --git a/src/plugins/navigation_embeddable/common/content_management/index.ts b/src/plugins/navigation_embeddable/common/content_management/index.ts
index 1dbb901c8cf8f..118b02d9ea1e2 100644
--- a/src/plugins/navigation_embeddable/common/content_management/index.ts
+++ b/src/plugins/navigation_embeddable/common/content_management/index.ts
@@ -13,6 +13,7 @@ export type { NavigationEmbeddableContentType } from '../types';
export type {
NavigationLinkType,
NavigationLayoutType,
+ NavigationLinkOptions,
NavigationEmbeddableLink,
NavigationEmbeddableItem,
NavigationEmbeddableCrudTypes,
@@ -24,7 +25,6 @@ export {
DASHBOARD_LINK_TYPE,
NAV_VERTICAL_LAYOUT,
NAV_HORIZONTAL_LAYOUT,
- EXTERNAL_LINK_SUPPORTED_PROTOCOLS,
} from './latest';
export * as NavigationEmbeddableV1 from './v1';
diff --git a/src/plugins/navigation_embeddable/common/content_management/v1/cm_services.ts b/src/plugins/navigation_embeddable/common/content_management/v1/cm_services.ts
index 9e4452cb09c0e..4145f0678f16e 100644
--- a/src/plugins/navigation_embeddable/common/content_management/v1/cm_services.ts
+++ b/src/plugins/navigation_embeddable/common/content_management/v1/cm_services.ts
@@ -16,11 +16,7 @@ import {
objectTypeToGetResultSchema,
} from '@kbn/content-management-utils';
import { DASHBOARD_LINK_TYPE, EXTERNAL_LINK_TYPE } from '.';
-import {
- EXTERNAL_LINK_SUPPORTED_PROTOCOLS,
- NAV_HORIZONTAL_LAYOUT,
- NAV_VERTICAL_LAYOUT,
-} from './constants';
+import { NAV_HORIZONTAL_LAYOUT, NAV_VERTICAL_LAYOUT } from './constants';
const baseNavigationEmbeddableLinkSchema = {
id: schema.string(),
@@ -32,12 +28,31 @@ const dashboardLinkSchema = schema.object({
...baseNavigationEmbeddableLinkSchema,
destinationRefName: schema.string(),
type: schema.literal(DASHBOARD_LINK_TYPE),
+ options: schema.maybe(
+ schema.object(
+ {
+ openInNewTab: schema.boolean(),
+ useCurrentFilters: schema.boolean(),
+ useCurrentDateRange: schema.boolean(),
+ },
+ { unknowns: 'forbid' }
+ )
+ ),
});
const externalLinkSchema = schema.object({
...baseNavigationEmbeddableLinkSchema,
type: schema.literal(EXTERNAL_LINK_TYPE),
- destination: schema.uri({ scheme: EXTERNAL_LINK_SUPPORTED_PROTOCOLS }),
+ destination: schema.string(),
+ options: schema.maybe(
+ schema.object(
+ {
+ openInNewTab: schema.boolean(),
+ encodeUrl: schema.boolean(),
+ },
+ { unknowns: 'forbid' }
+ )
+ ),
});
const navigationEmbeddableAttributesSchema = schema.object(
diff --git a/src/plugins/navigation_embeddable/common/content_management/v1/constants.ts b/src/plugins/navigation_embeddable/common/content_management/v1/constants.ts
index a03894ba6b715..70f1af5c0f69d 100644
--- a/src/plugins/navigation_embeddable/common/content_management/v1/constants.ts
+++ b/src/plugins/navigation_embeddable/common/content_management/v1/constants.ts
@@ -17,5 +17,3 @@ export const EXTERNAL_LINK_TYPE = 'externalLink';
*/
export const NAV_HORIZONTAL_LAYOUT = 'horizontal';
export const NAV_VERTICAL_LAYOUT = 'vertical';
-
-export const EXTERNAL_LINK_SUPPORTED_PROTOCOLS = ['http', 'https', 'mailto'];
diff --git a/src/plugins/navigation_embeddable/common/content_management/v1/index.ts b/src/plugins/navigation_embeddable/common/content_management/v1/index.ts
index b0af2ee1a936a..317db65b4ebbb 100644
--- a/src/plugins/navigation_embeddable/common/content_management/v1/index.ts
+++ b/src/plugins/navigation_embeddable/common/content_management/v1/index.ts
@@ -11,6 +11,7 @@ export type {
NavigationEmbeddableCrudTypes,
NavigationEmbeddableAttributes,
NavigationEmbeddableLink,
+ NavigationLinkOptions,
NavigationLayoutType,
NavigationLinkType,
} from './types';
@@ -20,5 +21,4 @@ export {
DASHBOARD_LINK_TYPE,
NAV_VERTICAL_LAYOUT,
NAV_HORIZONTAL_LAYOUT,
- EXTERNAL_LINK_SUPPORTED_PROTOCOLS,
} from './constants';
diff --git a/src/plugins/navigation_embeddable/common/content_management/v1/types.ts b/src/plugins/navigation_embeddable/common/content_management/v1/types.ts
index ff93f6462915f..0e3d48d45a6d2 100644
--- a/src/plugins/navigation_embeddable/common/content_management/v1/types.ts
+++ b/src/plugins/navigation_embeddable/common/content_management/v1/types.ts
@@ -11,6 +11,9 @@ import type {
SavedObjectCreateOptions,
SavedObjectUpdateOptions,
} from '@kbn/content-management-utils';
+import { type UrlDrilldownOptions } from '@kbn/ui-actions-enhanced-plugin/public';
+import { type DashboardDrilldownOptions } from '@kbn/presentation-util-plugin/public';
+
import { NavigationEmbeddableContentType } from '../../types';
import {
DASHBOARD_LINK_TYPE,
@@ -35,10 +38,12 @@ export type NavigationEmbeddableCrudTypes = ContentManagementCrudTypes<
*/
export type NavigationLinkType = typeof DASHBOARD_LINK_TYPE | typeof EXTERNAL_LINK_TYPE;
+export type NavigationLinkOptions = DashboardDrilldownOptions | UrlDrilldownOptions;
interface BaseNavigationEmbeddableLink {
id: string;
label?: string;
order: number;
+ options?: NavigationLinkOptions;
destination?: string;
}
diff --git a/src/plugins/navigation_embeddable/kibana.jsonc b/src/plugins/navigation_embeddable/kibana.jsonc
index b74e4bbd6f330..8d90360be090e 100644
--- a/src/plugins/navigation_embeddable/kibana.jsonc
+++ b/src/plugins/navigation_embeddable/kibana.jsonc
@@ -12,12 +12,11 @@
"dashboard",
"embeddable",
"kibanaReact",
- "presentationUtil"
+ "presentationUtil",
+ "uiActionsEnhanced",
+ "kibanaUtils"
],
"optionalPlugins": ["triggersActionsUi"],
- "requiredBundles": [
- "savedObjects"
- ]
+ "requiredBundles": ["savedObjects"]
}
}
-
diff --git a/src/plugins/navigation_embeddable/public/components/dashboard_link/dashboard_link_component.tsx b/src/plugins/navigation_embeddable/public/components/dashboard_link/dashboard_link_component.tsx
index 00f12d65b36c1..71627054ea430 100644
--- a/src/plugins/navigation_embeddable/public/components/dashboard_link/dashboard_link_component.tsx
+++ b/src/plugins/navigation_embeddable/public/components/dashboard_link/dashboard_link_component.tsx
@@ -9,18 +9,24 @@
import classNames from 'classnames';
import useAsync from 'react-use/lib/useAsync';
import React, { useMemo, useState } from 'react';
+import useObservable from 'react-use/lib/useObservable';
-import { EuiButtonEmpty, EuiListGroupItem, EuiToolTip } from '@elastic/eui';
+import {
+ DashboardDrilldownOptions,
+ DEFAULT_DASHBOARD_DRILLDOWN_OPTIONS,
+} from '@kbn/presentation-util-plugin/public';
+import { EuiButtonEmpty, EuiListGroupItem } from '@elastic/eui';
import { DashboardContainer } from '@kbn/dashboard-plugin/public/dashboard_container';
import {
- NavigationEmbeddableLink,
- NavigationLayoutType,
NAV_VERTICAL_LAYOUT,
+ NavigationLayoutType,
+ NavigationEmbeddableLink,
} from '../../../common/content_management';
-import { fetchDashboard } from './dashboard_link_tools';
+import { coreServices } from '../../services/kibana_services';
import { DashboardLinkStrings } from './dashboard_link_strings';
import { useNavigationEmbeddable } from '../../embeddable/navigation_embeddable';
+import { fetchDashboard, getDashboardHref, getDashboardLocator } from './dashboard_link_tools';
export const DashboardLinkComponent = ({
link,
@@ -33,13 +39,10 @@ export const DashboardLinkComponent = ({
const [error, setError] = useState();
const dashboardContainer = navEmbeddable.parent as DashboardContainer;
- const parentDashboardTitle = dashboardContainer.select((state) => state.explicitInput.title);
- const parentDashboardDescription = dashboardContainer.select(
- (state) => state.explicitInput.description
- );
-
+ const parentDashboardInput = useObservable(dashboardContainer.getInput$());
const parentDashboardId = dashboardContainer.select((state) => state.componentState.lastSavedId);
+ /** Fetch the dashboard that the link is pointing to */
const { loading: loadingDestinationDashboard, value: destinationDashboard } =
useAsync(async () => {
if (link.id !== parentDashboardId && link.destination) {
@@ -57,18 +60,20 @@ export const DashboardLinkComponent = ({
}
}, [link, parentDashboardId]);
+ /**
+ * Returns the title and description of the dashboard that the link points to; note that, if the link points to
+ * the current dashboard, then we need to get the most up-to-date information via the `parentDashboardInput` - this
+ * will respond to changes so that the link label/tooltip remains in sync with the dashboard title/description.
+ */
const [dashboardTitle, dashboardDescription] = useMemo(() => {
return link.destination === parentDashboardId
- ? [parentDashboardTitle, parentDashboardDescription]
+ ? [parentDashboardInput?.title, parentDashboardInput?.description]
: [destinationDashboard?.attributes.title, destinationDashboard?.attributes.description];
- }, [
- link.destination,
- parentDashboardId,
- parentDashboardTitle,
- destinationDashboard,
- parentDashboardDescription,
- ]);
+ }, [link.destination, parentDashboardId, parentDashboardInput, destinationDashboard]);
+ /**
+ * Memoized link information
+ */
const linkLabel = useMemo(() => {
return link.label || (dashboardTitle ?? DashboardLinkStrings.getDashboardErrorLabel());
}, [link, dashboardTitle]);
@@ -81,10 +86,56 @@ export const DashboardLinkComponent = ({
};
}
return {
- tooltipTitle: Boolean(dashboardDescription) ? dashboardTitle : undefined,
- tooltipMessage: dashboardDescription || dashboardTitle,
+ tooltipTitle: Boolean(dashboardDescription) ? linkLabel : undefined,
+ tooltipMessage: dashboardDescription || linkLabel,
+ };
+ }, [error, linkLabel, dashboardDescription]);
+
+ /**
+ * Dashboard-to-dashboard navigation
+ */
+ const { loading: loadingOnClickProps, value: onClickProps } = useAsync(async () => {
+ /** If the link points to the current dashboard, then there should be no `onClick` or `href` prop */
+ if (link.destination === parentDashboardId) return;
+
+ const linkOptions = {
+ ...DEFAULT_DASHBOARD_DRILLDOWN_OPTIONS,
+ ...link.options,
+ } as DashboardDrilldownOptions;
+
+ const locator = await getDashboardLocator({
+ link: { ...link, options: linkOptions },
+ navEmbeddable,
+ });
+ if (!locator) return;
+
+ const href = getDashboardHref(locator);
+ return {
+ href,
+ onClick: async (event: React.MouseEvent) => {
+ /**
+ * If the link is being opened via a modified click, then we should use the default `href` navigation behaviour
+ * by passing all the dashboard state via the URL - this will keep behaviour consistent across all browsers.
+ */
+ const modifiedClick = event.ctrlKey || event.metaKey || event.shiftKey;
+ if (modifiedClick) {
+ return;
+ }
+
+ /** Otherwise, prevent the default behaviour and handle click depending on `openInNewTab` option */
+ event.preventDefault();
+ if (linkOptions.openInNewTab) {
+ window.open(href, '_blank');
+ } else {
+ const { app, path, state } = locator;
+ await coreServices.application.navigateToApp(app, {
+ path,
+ state,
+ });
+ }
+ },
};
- }, [error, dashboardTitle, dashboardDescription]);
+ }, [link]);
return loadingDestinationDashboard ? (
@@ -96,37 +147,25 @@ export const DashboardLinkComponent = ({
{
- // TODO: As part of https://github.com/elastic/kibana/issues/154381, connect to drilldown
- }
- }
- label={
-
- {/* Setting `title=""` so that the native browser tooltip is disabled */}
-
- {linkLabel}
-
-
- }
+ label={linkLabel}
/>
);
};
diff --git a/src/plugins/navigation_embeddable/public/components/dashboard_link/dashboard_link_destination_picker.tsx b/src/plugins/navigation_embeddable/public/components/dashboard_link/dashboard_link_destination_picker.tsx
index 6bc62ef0912f4..60467de11ed5c 100644
--- a/src/plugins/navigation_embeddable/public/components/dashboard_link/dashboard_link_destination_picker.tsx
+++ b/src/plugins/navigation_embeddable/public/components/dashboard_link/dashboard_link_destination_picker.tsx
@@ -9,6 +9,7 @@
import { debounce } from 'lodash';
import useAsync from 'react-use/lib/useAsync';
import useMount from 'react-use/lib/useMount';
+import useUnmount from 'react-use/lib/useUnmount';
import React, { useCallback, useMemo, useState } from 'react';
import {
@@ -31,11 +32,13 @@ export const DashboardLinkDestinationPicker = ({
onDestinationPicked,
initialSelection,
parentDashboard,
+ onUnmount,
...other
}: {
- onDestinationPicked: (selectedDashboard?: DashboardItem) => void;
- parentDashboard?: DashboardContainer;
initialSelection?: string;
+ parentDashboard?: DashboardContainer;
+ onUnmount: (dashboardId?: string) => void;
+ onDestinationPicked: (selectedDashboard?: DashboardItem) => void;
}) => {
const [searchString, setSearchString] = useState('');
const [selectedOption, setSelectedOption] = useState([]);
@@ -68,6 +71,11 @@ export const DashboardLinkDestinationPicker = ({
}
});
+ useUnmount(() => {
+ /** Save the current selection so we can re-populate it if we switch back to this link editor */
+ onUnmount(selectedOption[0]?.key);
+ });
+
const { loading: loadingDashboardList, value: dashboardList } = useAsync(async () => {
const dashboards = await fetchDashboards({
search: searchString,
diff --git a/src/plugins/navigation_embeddable/public/components/dashboard_link/dashboard_link_tools.tsx b/src/plugins/navigation_embeddable/public/components/dashboard_link/dashboard_link_tools.ts
similarity index 55%
rename from src/plugins/navigation_embeddable/public/components/dashboard_link/dashboard_link_tools.tsx
rename to src/plugins/navigation_embeddable/public/components/dashboard_link/dashboard_link_tools.ts
index d6f3e502d9c54..a8a5eaa04b28d 100644
--- a/src/plugins/navigation_embeddable/public/components/dashboard_link/dashboard_link_tools.tsx
+++ b/src/plugins/navigation_embeddable/public/components/dashboard_link/dashboard_link_tools.ts
@@ -7,9 +7,21 @@
*/
import { isEmpty, filter } from 'lodash';
-import { DashboardItem } from '../../embeddable/types';
-import { dashboardServices } from '../../services/kibana_services';
+import {
+ cleanEmptyKeys,
+ getEmbeddableParams,
+ DashboardAppLocatorParams,
+} from '@kbn/dashboard-plugin/public';
+import { isFilterPinned } from '@kbn/es-query';
+import { KibanaLocation } from '@kbn/share-plugin/public';
+import { setStateToKbnUrl } from '@kbn/kibana-utils-plugin/public';
+import { DashboardDrilldownOptions } from '@kbn/presentation-util-plugin/public';
+
+import { DashboardItem } from '../../embeddable/types';
+import { NavigationEmbeddable } from '../../embeddable';
+import { NavigationEmbeddableLink } from '../../../common/content_management';
+import { coreServices, dashboardServices } from '../../services/kibana_services';
/**
* ----------------------------------
@@ -84,3 +96,56 @@ export const fetchDashboards = async ({
return simplifiedDashboardList;
};
+
+/**
+ * ----------------------------------
+ * Navigate from one dashboard to another
+ * ----------------------------------
+ */
+
+interface GetDashboardLocatorProps {
+ link: NavigationEmbeddableLink & { options: DashboardDrilldownOptions };
+ navEmbeddable: NavigationEmbeddable;
+}
+
+/**
+ * Fetch the locator to use for dashboard navigation
+ * @param props `GetDashboardLocatorProps`
+ * @returns The locator to use for dashboard navigation
+ */
+export const getDashboardLocator = async ({ link, navEmbeddable }: GetDashboardLocatorProps) => {
+ const params: DashboardAppLocatorParams = {
+ dashboardId: link.destination,
+ ...getEmbeddableParams(navEmbeddable, link.options),
+ };
+
+ const locator = dashboardServices.locator; // TODO: Make this generic as part of https://github.com/elastic/kibana/issues/164748
+ if (locator) {
+ const location: KibanaLocation = await locator.getLocation(params);
+ return location;
+ }
+};
+
+/**
+ * Get URL for dashboard app - should only be used when relying on native `href` functionality
+ * @param locator Locator that should be used to get the URL
+ * @returns A full URL to the dashboard, with all state included
+ */
+export const getDashboardHref = ({
+ app,
+ path,
+ state,
+}: KibanaLocation): string => {
+ return coreServices.application.getUrlForApp(app, {
+ path: setStateToKbnUrl(
+ '_a',
+ cleanEmptyKeys({
+ query: state.query,
+ filters: state.filters?.filter((f) => !isFilterPinned(f)),
+ }),
+ { useHash: false, storeInHashQuery: true },
+ path
+ ),
+ absolute: true,
+ });
+};
diff --git a/src/plugins/navigation_embeddable/public/components/editor/navigation_embeddable_link_destination.tsx b/src/plugins/navigation_embeddable/public/components/editor/navigation_embeddable_link_destination.tsx
new file mode 100644
index 0000000000000..dd75accac4b8e
--- /dev/null
+++ b/src/plugins/navigation_embeddable/public/components/editor/navigation_embeddable_link_destination.tsx
@@ -0,0 +1,80 @@
+/*
+ * 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, { useState } from 'react';
+
+import { DashboardContainer } from '@kbn/dashboard-plugin/public/dashboard_container';
+
+import { EuiFormRow } from '@elastic/eui';
+import {
+ NavigationLinkType,
+ EXTERNAL_LINK_TYPE,
+ DASHBOARD_LINK_TYPE,
+} from '../../../common/content_management';
+import { NavigationEmbeddableUnorderedLink } from '../../editor/open_link_editor_flyout';
+import { ExternalLinkDestinationPicker } from '../external_link/external_link_destination_picker';
+import { DashboardLinkDestinationPicker } from '../dashboard_link/dashboard_link_destination_picker';
+import { NavEmbeddableStrings } from '../navigation_embeddable_strings';
+
+export const NavigationEmbeddableLinkDestination = ({
+ link,
+ setDestination,
+ parentDashboard,
+ selectedLinkType,
+}: {
+ selectedLinkType: NavigationLinkType;
+ parentDashboard?: DashboardContainer;
+ link?: NavigationEmbeddableUnorderedLink;
+ setDestination: (destination?: string, defaultLabel?: string) => void;
+}) => {
+ const [destinationError, setDestinationError] = useState();
+
+ /**
+ * Store the dashboard / external destinations separately so that we can remember the selections
+ * made in each component even when the selected link type changes
+ */
+ const [dashboardLinkDestination, setDashboardLinkDestination] = useState(
+ link && link.type === DASHBOARD_LINK_TYPE ? link.destination : undefined
+ );
+ const [externalLinkDestination, setExternalLinkDestination] = useState(
+ link && link.type === EXTERNAL_LINK_TYPE ? link.destination : undefined
+ );
+
+ return (
+
+ {selectedLinkType === DASHBOARD_LINK_TYPE ? (
+ {
+ setDestination(undefined, undefined);
+ if (selectedDashboardId) setDashboardLinkDestination(selectedDashboardId);
+ }}
+ parentDashboard={parentDashboard}
+ initialSelection={dashboardLinkDestination}
+ onDestinationPicked={(dashboard) =>
+ setDestination(dashboard?.id, dashboard?.attributes.title)
+ }
+ />
+ ) : (
+ {
+ setDestinationError(undefined);
+ setDestination(undefined, undefined);
+ if (selectedUrl) setExternalLinkDestination(selectedUrl);
+ }}
+ initialSelection={externalLinkDestination}
+ onDestinationPicked={(url) => setDestination(url, url)}
+ setDestinationError={setDestinationError}
+ />
+ )}
+
+ );
+};
diff --git a/src/plugins/navigation_embeddable/public/components/editor/navigation_embeddable_link_editor.tsx b/src/plugins/navigation_embeddable/public/components/editor/navigation_embeddable_link_editor.tsx
index f02e37806e396..502e7091fa866 100644
--- a/src/plugins/navigation_embeddable/public/components/editor/navigation_embeddable_link_editor.tsx
+++ b/src/plugins/navigation_embeddable/public/components/editor/navigation_embeddable_link_editor.tsx
@@ -32,13 +32,14 @@ import {
NavigationLinkType,
EXTERNAL_LINK_TYPE,
DASHBOARD_LINK_TYPE,
+ NavigationLinkOptions,
NavigationEmbeddableLink,
} from '../../../common/content_management';
+import { NavigationLinkInfo } from '../../embeddable/types';
import { NavEmbeddableStrings } from '../navigation_embeddable_strings';
-import { DashboardItem, NavigationLinkInfo } from '../../embeddable/types';
import { NavigationEmbeddableUnorderedLink } from '../../editor/open_link_editor_flyout';
-import { ExternalLinkDestinationPicker } from '../external_link/external_link_destination_picker';
-import { DashboardLinkDestinationPicker } from '../dashboard_link/dashboard_link_destination_picker';
+import { NavigationEmbeddableLinkOptions } from './navigation_embeddable_link_options';
+import { NavigationEmbeddableLinkDestination } from './navigation_embeddable_link_destination';
export const NavigationEmbeddableLinkEditor = ({
link,
@@ -56,6 +57,7 @@ export const NavigationEmbeddableLinkEditor = ({
);
const [defaultLinkLabel, setDefaultLinkLabel] = useState();
const [currentLinkLabel, setCurrentLinkLabel] = useState(link?.label ?? '');
+ const [linkOptions, setLinkOptions] = useState();
const [linkDestination, setLinkDestination] = useState(link?.destination);
const linkTypes: EuiRadioGroupOption[] = useMemo(() => {
@@ -78,31 +80,16 @@ export const NavigationEmbeddableLinkEditor = ({
});
}, []);
- const onDashboardSelected = useCallback(
- (selectedDashboard?: DashboardItem) => {
- setLinkDestination(selectedDashboard?.id);
- if (selectedDashboard) {
- const dashboardTitle = selectedDashboard.attributes.title;
- setDefaultLinkLabel(dashboardTitle);
- if (!currentLinkLabel || currentLinkLabel === defaultLinkLabel) {
- setCurrentLinkLabel(dashboardTitle);
- }
+ /** When a new destination is picked, handle the logic for what to display as the current + default labels */
+ const handleDestinationPicked = useCallback(
+ (destination?: string, label?: string) => {
+ setLinkDestination(destination);
+ if (!currentLinkLabel || defaultLinkLabel === currentLinkLabel) {
+ setCurrentLinkLabel(label ?? '');
}
+ setDefaultLinkLabel(label);
},
- [currentLinkLabel, defaultLinkLabel]
- );
-
- const onUrlSelected = useCallback(
- (url?: string) => {
- setLinkDestination(url);
- if (url) {
- setDefaultLinkLabel(url);
- if (!currentLinkLabel || currentLinkLabel === defaultLinkLabel) {
- setCurrentLinkLabel(url);
- }
- }
- },
- [currentLinkLabel, defaultLinkLabel]
+ [defaultLinkLabel, currentLinkLabel]
);
return (
@@ -134,34 +121,19 @@ export const NavigationEmbeddableLinkEditor = ({
options={linkTypes}
idSelected={selectedLinkType}
onChange={(id) => {
- if (link?.type === id) {
- setLinkDestination(link.destination);
- setCurrentLinkLabel(link.label ?? '');
- } else {
- setLinkDestination(undefined);
- setCurrentLinkLabel('');
+ if (currentLinkLabel === defaultLinkLabel) {
+ setCurrentLinkLabel(link?.type === id ? link.label ?? '' : '');
}
- setDefaultLinkLabel(undefined);
setSelectedLinkType(id as NavigationLinkType);
}}
/>
-
-
- {selectedLinkType === DASHBOARD_LINK_TYPE ? (
-
- ) : (
-
- )}
-
-
+
setCurrentLinkLabel(e.target.value)}
/>
+
-
- {/* TODO: As part of https://github.com/elastic/kibana/issues/154381, we should pull in the custom settings for each link type.
- Refer to `x-pack/examples/ui_actions_enhanced_examples/public/drilldowns/dashboard_to_discover_drilldown/collect_config_container.tsx`
- for the dashboard drilldown settings, for example.
-
- Open question: It probably makes sense to re-use these components so any changes made to the drilldown architecture
- trickle down to the navigation embeddable - this would require some refactoring, though. Is this a goal for MVP?
- */}
@@ -200,6 +169,7 @@ export const NavigationEmbeddableLinkEditor = ({
type: selectedLinkType,
id: link?.id ?? uuidv4(),
destination: linkDestination,
+ options: linkOptions,
});
onClose();
diff --git a/src/plugins/navigation_embeddable/public/components/editor/navigation_embeddable_link_options.tsx b/src/plugins/navigation_embeddable/public/components/editor/navigation_embeddable_link_options.tsx
new file mode 100644
index 0000000000000..bbb53342a85b3
--- /dev/null
+++ b/src/plugins/navigation_embeddable/public/components/editor/navigation_embeddable_link_options.tsx
@@ -0,0 +1,71 @@
+/*
+ * 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, { useState } from 'react';
+
+import { EuiFormRow } from '@elastic/eui';
+import {
+ DashboardDrilldownOptions,
+ DashboardDrilldownOptionsComponent,
+ DEFAULT_DASHBOARD_DRILLDOWN_OPTIONS,
+} from '@kbn/presentation-util-plugin/public';
+import {
+ UrlDrilldownOptions,
+ UrlDrilldownOptionsComponent,
+ DEFAULT_URL_DRILLDOWN_OPTIONS,
+} from '@kbn/ui-actions-enhanced-plugin/public';
+
+import {
+ NavigationLinkType,
+ EXTERNAL_LINK_TYPE,
+ DASHBOARD_LINK_TYPE,
+ NavigationLinkOptions,
+} from '../../../common/content_management';
+import { NavEmbeddableStrings } from '../navigation_embeddable_strings';
+import { NavigationEmbeddableUnorderedLink } from '../../editor/open_link_editor_flyout';
+
+export const NavigationEmbeddableLinkOptions = ({
+ link,
+ setLinkOptions,
+ selectedLinkType,
+}: {
+ selectedLinkType: NavigationLinkType;
+ link?: NavigationEmbeddableUnorderedLink;
+ setLinkOptions: (options: NavigationLinkOptions) => void;
+}) => {
+ const [dashboardLinkOptions, setDashboardLinkOptions] = useState({
+ ...DEFAULT_DASHBOARD_DRILLDOWN_OPTIONS,
+ ...(link && link.type === DASHBOARD_LINK_TYPE ? link.options : {}),
+ });
+ const [externalLinkOptions, setExternalLinkOptions] = useState({
+ ...DEFAULT_URL_DRILLDOWN_OPTIONS,
+ ...(link && link.type === EXTERNAL_LINK_TYPE ? link.options : {}),
+ });
+
+ return (
+
+ {selectedLinkType === DASHBOARD_LINK_TYPE ? (
+ {
+ setDashboardLinkOptions({ ...dashboardLinkOptions, ...change });
+ setLinkOptions({ ...dashboardLinkOptions, ...change });
+ }}
+ />
+ ) : (
+ {
+ setExternalLinkOptions({ ...externalLinkOptions, ...change });
+ setLinkOptions({ ...externalLinkOptions, ...change });
+ }}
+ />
+ )}
+
+ );
+};
diff --git a/src/plugins/navigation_embeddable/public/components/editor/navigation_embeddable_panel_editor.tsx b/src/plugins/navigation_embeddable/public/components/editor/navigation_embeddable_panel_editor.tsx
index 892b2f777d3c5..da59d720145bd 100644
--- a/src/plugins/navigation_embeddable/public/components/editor/navigation_embeddable_panel_editor.tsx
+++ b/src/plugins/navigation_embeddable/public/components/editor/navigation_embeddable_panel_editor.tsx
@@ -12,6 +12,7 @@ import {
EuiForm,
EuiTitle,
EuiButton,
+ EuiSwitch,
EuiFormRow,
EuiFlexItem,
EuiFlexGroup,
@@ -25,7 +26,6 @@ import {
EuiDragDropContext,
euiDragDropReorder,
EuiButtonGroupOptionProps,
- EuiSwitch,
} from '@elastic/eui';
import { DashboardContainer } from '@kbn/dashboard-plugin/public/dashboard_container';
@@ -205,7 +205,7 @@ const NavigationEmbeddablePanelEditor = ({
parentDashboard={parentDashboard}
editLink={() => addOrEditLink(link)}
deleteLink={() => deleteLink(link.id)}
- dragHandleProps={provided.dragHandleProps}
+ dragHandleProps={provided.dragHandleProps ?? undefined} // casting `null` to `undefined`
/>
)}
diff --git a/src/plugins/navigation_embeddable/public/components/editor/navigation_embeddable_panel_editor_link.tsx b/src/plugins/navigation_embeddable/public/components/editor/navigation_embeddable_panel_editor_link.tsx
index a886ac6f9eb8f..f73aec1104653 100644
--- a/src/plugins/navigation_embeddable/public/components/editor/navigation_embeddable_panel_editor_link.tsx
+++ b/src/plugins/navigation_embeddable/public/components/editor/navigation_embeddable_panel_editor_link.tsx
@@ -24,6 +24,7 @@ import {
import { DashboardContainer } from '@kbn/dashboard-plugin/public/dashboard_container';
import { NavigationLinkInfo } from '../../embeddable/types';
+import { validateUrl } from '../external_link/external_link_tools';
import { fetchDashboard } from '../dashboard_link/dashboard_link_tools';
import { NavEmbeddableStrings } from '../navigation_embeddable_strings';
import { DashboardLinkStrings } from '../dashboard_link/dashboard_link_strings';
@@ -42,27 +43,36 @@ export const NavigationEmbeddablePanelEditorLink = ({
parentDashboard?: DashboardContainer;
dragHandleProps?: DraggableProvidedDragHandleProps;
}) => {
- const [dashboardError, setDashboardError] = useState();
+ const [destinationError, setDestinationError] = useState();
const parentDashboardTitle = parentDashboard?.select((state) => state.explicitInput.title);
const parentDashboardId = parentDashboard?.select((state) => state.componentState.lastSavedId);
const { value: linkLabel, loading: linkLabelLoading } = useAsync(async () => {
+ if (!link.destination) {
+ setDestinationError(DashboardLinkStrings.getDashboardErrorLabel());
+ return;
+ }
+
if (link.type === DASHBOARD_LINK_TYPE) {
if (parentDashboardId === link.destination) {
return link.label || parentDashboardTitle;
} else {
const dashboard = await fetchDashboard(link.destination)
.then((result) => {
- setDashboardError(undefined);
+ setDestinationError(undefined);
return result;
})
- .catch((error) => setDashboardError(error));
+ .catch((error) => setDestinationError(error));
return (
link.label ||
(dashboard ? dashboard.attributes.title : DashboardLinkStrings.getDashboardErrorLabel())
);
}
} else {
+ const { valid, message } = validateUrl(link.destination);
+ if (!valid && message) {
+ setDestinationError(new Error(message));
+ }
return link.label || link.destination;
}
}, [link]);
@@ -72,10 +82,10 @@ export const NavigationEmbeddablePanelEditorLink = ({
-
+
{linkLabel}
@@ -102,24 +112,28 @@ export const NavigationEmbeddablePanelEditorLink = ({
);
return () =>
- dashboardError ? (
+ destinationError ? (
{labelText}
) : (
labelText
);
- }, [linkLabel, linkLabelLoading, dashboardError, link.label, link.type]);
+ }, [linkLabel, linkLabelLoading, destinationError, link.label, link.type]);
return (
diff --git a/src/plugins/navigation_embeddable/public/components/external_link/external_link_component.tsx b/src/plugins/navigation_embeddable/public/components/external_link/external_link_component.tsx
index 52442cd307b0b..cd637f6764247 100644
--- a/src/plugins/navigation_embeddable/public/components/external_link/external_link_component.tsx
+++ b/src/plugins/navigation_embeddable/public/components/external_link/external_link_component.tsx
@@ -6,21 +6,81 @@
* Side Public License, v 1.
*/
-import React from 'react';
+import React, { useMemo, useState } from 'react';
+import {
+ UrlDrilldownOptions,
+ DEFAULT_URL_DRILLDOWN_OPTIONS,
+} from '@kbn/ui-actions-enhanced-plugin/public';
import { EuiListGroupItem } from '@elastic/eui';
-import { NavigationEmbeddableLink } from '../../../common/content_management';
-export const ExternalLinkComponent = ({ link }: { link: NavigationEmbeddableLink }) => {
+import { validateUrl } from './external_link_tools';
+import { coreServices } from '../../services/kibana_services';
+import {
+ NavigationEmbeddableLink,
+ NavigationLayoutType,
+ NAV_VERTICAL_LAYOUT,
+} from '../../../common/content_management';
+
+export const ExternalLinkComponent = ({
+ link,
+ layout,
+}: {
+ link: NavigationEmbeddableLink;
+ layout: NavigationLayoutType;
+}) => {
+ const [error, setError] = useState();
+
+ const linkOptions = useMemo(() => {
+ return {
+ ...DEFAULT_URL_DRILLDOWN_OPTIONS,
+ ...link.options,
+ } as UrlDrilldownOptions;
+ }, [link.options]);
+
+ const isValidUrl = useMemo(() => {
+ if (!link.destination) return false;
+ const { valid, message } = validateUrl(link.destination);
+ if (!valid) setError(message);
+ return valid;
+ }, [link.destination]);
+
+ const destination = useMemo(() => {
+ return link.destination && linkOptions.encodeUrl
+ ? encodeURI(link.destination)
+ : link.destination;
+ }, [linkOptions, link.destination]);
+
return (
{
- // TODO: As part of https://github.com/elastic/kibana/issues/154381, connect to drilldown
+ href={destination}
+ onClick={async (event) => {
+ if (!destination) return;
+
+ /** Only use `navigateToUrl` if we **aren't** opening in a new window/tab; otherwise, just use default href handling */
+ const modifiedClick = event.ctrlKey || event.metaKey || event.shiftKey;
+ if (!modifiedClick) {
+ event.preventDefault();
+ if (linkOptions.openInNewTab) {
+ window.open(destination, '_blank');
+ } else {
+ await coreServices.application.navigateToUrl(destination);
+ }
+ }
}}
/>
);
diff --git a/src/plugins/navigation_embeddable/public/components/external_link/external_link_destination_picker.tsx b/src/plugins/navigation_embeddable/public/components/external_link/external_link_destination_picker.tsx
index 4019e4c843faf..a509787d3f390 100644
--- a/src/plugins/navigation_embeddable/public/components/external_link/external_link_destination_picker.tsx
+++ b/src/plugins/navigation_embeddable/public/components/external_link/external_link_destination_picker.tsx
@@ -7,33 +7,48 @@
*/
import useMount from 'react-use/lib/useMount';
+import useUnmount from 'react-use/lib/useUnmount';
import React, { useState } from 'react';
import { EuiFieldText } from '@elastic/eui';
-import { ExternalLinkStrings } from './external_link_strings';
-// TODO: As part of https://github.com/elastic/kibana/issues/154381, replace this regex URL check with more robust url validation
-const isValidUrl =
- /^https?:\/\/(?:www.)?[-a-zA-Z0-9@:%._+~#=]{1,256}.[a-zA-Z0-9()]{1,6}\b(?:[-a-zA-Z0-9()@:%_+.~#?&/=]*)$/;
+import { ExternalLinkStrings } from './external_link_strings';
+import { validateUrl } from './external_link_tools';
export const ExternalLinkDestinationPicker = ({
onDestinationPicked,
+ setDestinationError,
initialSelection,
+ onUnmount,
...other
}: {
- onDestinationPicked: (destination?: string) => void;
initialSelection?: string;
+ onUnmount: (destination: string) => void;
+ onDestinationPicked: (destination?: string) => void;
+ setDestinationError: (error: string | undefined) => void;
}) => {
const [validUrl, setValidUrl] = useState(true);
const [currentUrl, setCurrentUrl] = useState(initialSelection ?? '');
useMount(() => {
if (initialSelection) {
- onDestinationPicked(initialSelection);
- setValidUrl(isValidUrl.test(initialSelection));
+ const { valid, message } = validateUrl(initialSelection);
+
+ if (!valid) {
+ setValidUrl(false);
+ setDestinationError(message);
+ onDestinationPicked(undefined); // prevent re-saving an invalid link
+ } else {
+ onDestinationPicked(initialSelection);
+ }
}
});
+ useUnmount(() => {
+ /** Save the current selection so we can re-populate it if we switch back to this link editor */
+ onUnmount(currentUrl);
+ });
+
/* {...other} is needed so all inner elements are treated as part of the form */
return (
@@ -41,13 +56,26 @@ export const ExternalLinkDestinationPicker = ({
value={currentUrl}
placeholder={ExternalLinkStrings.getPlaceholder()}
isInvalid={!validUrl}
- onChange={(e) => {
- const url = e.target.value;
- const isValid = isValidUrl.test(url);
- setValidUrl(isValid);
+ onChange={(event) => {
+ const url = event.target.value;
setCurrentUrl(url);
- if (isValid) {
+
+ if (url === '') {
+ /* no need to validate the empty string - not an error, but also not a valid destination */
+ setValidUrl(true);
+ onDestinationPicked(undefined);
+ setDestinationError(undefined);
+ return;
+ }
+
+ const { valid, message } = validateUrl(url);
+ setValidUrl(valid);
+ if (valid) {
onDestinationPicked(url);
+ setDestinationError(undefined);
+ } else {
+ onDestinationPicked(undefined);
+ setDestinationError(message);
}
}}
/>
diff --git a/src/plugins/navigation_embeddable/public/components/external_link/external_link_strings.ts b/src/plugins/navigation_embeddable/public/components/external_link/external_link_strings.ts
index e286019d4bc05..7d6667a51b0f3 100644
--- a/src/plugins/navigation_embeddable/public/components/external_link/external_link_strings.ts
+++ b/src/plugins/navigation_embeddable/public/components/external_link/external_link_strings.ts
@@ -25,4 +25,16 @@ export const ExternalLinkStrings = {
i18n.translate('navigationEmbeddable.externalLink.editor.placeholder', {
defaultMessage: 'Enter external URL',
}),
+ getUrlFormatError: () =>
+ i18n.translate('navigationEmbeddable.externalLink.editor.urlFormatError', {
+ defaultMessage: 'Invalid format. Example: {exampleUrl}',
+ values: {
+ exampleUrl: 'https://elastic.co/',
+ },
+ }),
+ getDisallowedUrlError: () =>
+ i18n.translate('navigationEmbeddable.externalLink.editor.disallowedUrlError', {
+ defaultMessage:
+ 'This URL is not allowed by your administrator. Refer to "externalUrl.policy" configuration.',
+ }),
};
diff --git a/src/plugins/navigation_embeddable/public/components/external_link/external_link_tools.ts b/src/plugins/navigation_embeddable/public/components/external_link/external_link_tools.ts
new file mode 100644
index 0000000000000..4048f1c0f5ccd
--- /dev/null
+++ b/src/plugins/navigation_embeddable/public/components/external_link/external_link_tools.ts
@@ -0,0 +1,35 @@
+/*
+ * 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 { urlDrilldownValidateUrl } from '@kbn/ui-actions-enhanced-plugin/public';
+import { coreServices } from '../../services/kibana_services';
+import { ExternalLinkStrings } from './external_link_strings';
+
+/**
+ *
+ * @param url The URl to validate
+ * @returns Whether or not the URL is valid; if it is not, it will also return the reason it is invalid via the `message`
+ */
+export const validateUrl = (url: string): { valid: boolean; message?: string } => {
+ try {
+ /** This check will throw an error on invalid format, so catch it below */
+ const allowedUrl = coreServices.http.externalUrl.validateUrl(url);
+
+ if (allowedUrl === null) {
+ return { valid: false, message: ExternalLinkStrings.getDisallowedUrlError() };
+ }
+ const validatedUrl = urlDrilldownValidateUrl(url);
+ if (!validatedUrl.isValid) {
+ throw new Error(); // will be caught below
+ }
+ } catch {
+ return { valid: false, message: ExternalLinkStrings.getUrlFormatError() };
+ }
+
+ return { valid: true };
+};
diff --git a/src/plugins/navigation_embeddable/public/components/navigation_embeddable_component.tsx b/src/plugins/navigation_embeddable/public/components/navigation_embeddable_component.tsx
index c6423f0c529d2..7a337426dba71 100644
--- a/src/plugins/navigation_embeddable/public/components/navigation_embeddable_component.tsx
+++ b/src/plugins/navigation_embeddable/public/components/navigation_embeddable_component.tsx
@@ -44,7 +44,11 @@ export const NavigationEmbeddableComponent = () => {
layout={layout ?? NAV_VERTICAL_LAYOUT}
/>
) : (
-
+
),
},
};
diff --git a/src/plugins/navigation_embeddable/public/components/navigation_embeddable_strings.ts b/src/plugins/navigation_embeddable/public/components/navigation_embeddable_strings.ts
index d229058e95f60..efeee7235a89e 100644
--- a/src/plugins/navigation_embeddable/public/components/navigation_embeddable_strings.ts
+++ b/src/plugins/navigation_embeddable/public/components/navigation_embeddable_strings.ts
@@ -118,6 +118,10 @@ export const NavEmbeddableStrings = {
i18n.translate('navigationEmbeddable.linkEditor.linkTextPlaceholder', {
defaultMessage: 'Enter text for link',
}),
+ getLinkOptionsLabel: () =>
+ i18n.translate('navigationEmbeddable.linkEditor.linkOptionsLabel', {
+ defaultMessage: 'Options',
+ }),
},
},
};
diff --git a/src/plugins/navigation_embeddable/public/embeddable/navigation_embeddable_factory.ts b/src/plugins/navigation_embeddable/public/embeddable/navigation_embeddable_factory.ts
index 32aab131ddb1b..21d5b1d93cb31 100644
--- a/src/plugins/navigation_embeddable/public/embeddable/navigation_embeddable_factory.ts
+++ b/src/plugins/navigation_embeddable/public/embeddable/navigation_embeddable_factory.ts
@@ -23,7 +23,8 @@ import { extract, inject } from '../../common/embeddable';
export type NavigationEmbeddableFactory = EmbeddableFactory;
-// TODO: Replace string 'OPEN_FLYOUT_ADD_DRILLDOWN' with constant as part of https://github.com/elastic/kibana/issues/154381
+// TODO: Replace string 'OPEN_FLYOUT_ADD_DRILLDOWN' with constant once the dashboardEnhanced plugin is removed
+// and it is no longer locked behind `x-pack`
const getDefaultNavigationEmbeddableInput = (): Partial
=> ({
disabledActions: ['OPEN_FLYOUT_ADD_DRILLDOWN'],
});
diff --git a/src/plugins/navigation_embeddable/tsconfig.json b/src/plugins/navigation_embeddable/tsconfig.json
index afcc5945c2082..72001ee53bef8 100644
--- a/src/plugins/navigation_embeddable/tsconfig.json
+++ b/src/plugins/navigation_embeddable/tsconfig.json
@@ -20,7 +20,11 @@
"@kbn/saved-objects-plugin",
"@kbn/core-saved-objects-server",
"@kbn/saved-objects-plugin",
- "@kbn/utility-types",
+ "@kbn/ui-actions-enhanced-plugin",
+ "@kbn/es-query",
+ "@kbn/share-plugin",
+ "@kbn/kibana-utils-plugin",
+ "@kbn/utility-types"
],
"exclude": ["target/**/*"]
}
diff --git a/src/plugins/presentation_util/public/components/dashboard_drilldown_options/dashboard_drilldown_options.tsx b/src/plugins/presentation_util/public/components/dashboard_drilldown_options/dashboard_drilldown_options.tsx
new file mode 100644
index 0000000000000..d6df3ac4af240
--- /dev/null
+++ b/src/plugins/presentation_util/public/components/dashboard_drilldown_options/dashboard_drilldown_options.tsx
@@ -0,0 +1,52 @@
+/*
+ * 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 { EuiFormRow, EuiSwitch } from '@elastic/eui';
+
+import { DashboardDrilldownOptions } from './types';
+import { dashboardDrilldownConfigStrings } from '../../i18n/dashboard_drilldown_config';
+
+export interface DashboardDrilldownOptionsProps {
+ options: DashboardDrilldownOptions;
+ onOptionChange: (newOptions: Partial) => void;
+}
+
+export const DashboardDrilldownOptionsComponent = ({
+ options,
+ onOptionChange,
+}: DashboardDrilldownOptionsProps) => {
+ return (
+ <>
+
+ onOptionChange({ useCurrentFilters: !options.useCurrentFilters })}
+ />
+
+
+ onOptionChange({ useCurrentDateRange: !options.useCurrentDateRange })}
+ />
+
+
+ onOptionChange({ openInNewTab: !options.openInNewTab })}
+ />
+
+ >
+ );
+};
diff --git a/src/plugins/presentation_util/public/components/dashboard_drilldown_options/types.ts b/src/plugins/presentation_util/public/components/dashboard_drilldown_options/types.ts
new file mode 100644
index 0000000000000..60d8646b67532
--- /dev/null
+++ b/src/plugins/presentation_util/public/components/dashboard_drilldown_options/types.ts
@@ -0,0 +1,20 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+// eslint-disable-next-line @typescript-eslint/consistent-type-definitions
+export type DashboardDrilldownOptions = {
+ useCurrentFilters: boolean;
+ useCurrentDateRange: boolean;
+ openInNewTab: boolean;
+};
+
+export const DEFAULT_DASHBOARD_DRILLDOWN_OPTIONS: DashboardDrilldownOptions = {
+ openInNewTab: false,
+ useCurrentDateRange: true,
+ useCurrentFilters: true,
+};
diff --git a/src/plugins/presentation_util/public/components/index.tsx b/src/plugins/presentation_util/public/components/index.tsx
index fb66c9c5b2be6..46634a156d953 100644
--- a/src/plugins/presentation_util/public/components/index.tsx
+++ b/src/plugins/presentation_util/public/components/index.tsx
@@ -42,6 +42,24 @@ export const LazyDataViewPicker = React.lazy(() => import('./data_view_picker/da
export const LazyFieldPicker = React.lazy(() => import('./field_picker/field_picker'));
+const LazyDashboardDrilldownOptionsComponent = React.lazy(() =>
+ import('./dashboard_drilldown_options/dashboard_drilldown_options').then(
+ ({ DashboardDrilldownOptionsComponent }) => ({
+ default: DashboardDrilldownOptionsComponent,
+ })
+ )
+);
+
+export const DashboardDrilldownOptionsComponent = withSuspense(
+ LazyDashboardDrilldownOptionsComponent,
+ null
+);
+
+export {
+ type DashboardDrilldownOptions,
+ DEFAULT_DASHBOARD_DRILLDOWN_OPTIONS,
+} from './dashboard_drilldown_options/types';
+
export { FloatingActions } from './floating_actions/floating_actions';
/**
diff --git a/src/plugins/presentation_util/public/i18n/dashboard_drilldown_config.tsx b/src/plugins/presentation_util/public/i18n/dashboard_drilldown_config.tsx
new file mode 100644
index 0000000000000..2bb326cb6f841
--- /dev/null
+++ b/src/plugins/presentation_util/public/i18n/dashboard_drilldown_config.tsx
@@ -0,0 +1,29 @@
+/*
+ * 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';
+
+export const dashboardDrilldownConfigStrings = {
+ component: {
+ getUseCurrentFiltersLabel: () =>
+ i18n.translate(
+ 'presentationUtil.dashboardDrilldownConfig.components.useCurrentFiltersLabel',
+ {
+ defaultMessage: 'Use filters and query from origin dashboard',
+ }
+ ),
+ getUseCurrentDateRange: () =>
+ i18n.translate('presentationUtil.dashboardDrilldownConfig.components.useCurrentDateRange', {
+ defaultMessage: 'Use date range from origin dashboard',
+ }),
+ getOpenInNewTab: () =>
+ i18n.translate('presentationUtil.dashboardDrilldownConfig.components.openInNewTab', {
+ defaultMessage: 'Open dashboard in new tab',
+ }),
+ },
+};
diff --git a/src/plugins/presentation_util/public/index.ts b/src/plugins/presentation_util/public/index.ts
index f5994b3da82e2..8a93bbf8c4f86 100644
--- a/src/plugins/presentation_util/public/index.ts
+++ b/src/plugins/presentation_util/public/index.ts
@@ -37,6 +37,9 @@ export {
LazyDataViewPicker,
LazyFieldPicker,
FloatingActions,
+ type DashboardDrilldownOptions,
+ DashboardDrilldownOptionsComponent,
+ DEFAULT_DASHBOARD_DRILLDOWN_OPTIONS,
} from './components';
export {
diff --git a/src/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/components/index.ts b/src/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/components/index.ts
index 4e73e9a43a9e2..b50b1fe96d46a 100644
--- a/src/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/components/index.ts
+++ b/src/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/components/index.ts
@@ -6,4 +6,7 @@
* Side Public License, v 1.
*/
-export { UrlDrilldownCollectConfig } from './url_drilldown_collect_config';
+export {
+ UrlDrilldownCollectConfig,
+ UrlDrilldownOptionsComponent,
+} from './url_drilldown_collect_config';
diff --git a/src/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/components/url_drilldown_collect_config/i18n.ts b/src/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/components/url_drilldown_collect_config/i18n.ts
index dcb36cbab257a..48f5fe1fdc8b6 100644
--- a/src/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/components/url_drilldown_collect_config/i18n.ts
+++ b/src/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/components/url_drilldown_collect_config/i18n.ts
@@ -57,7 +57,7 @@ export const txtUrlTemplatePreviewLinkText = i18n.translate(
export const txtUrlTemplateOpenInNewTab = i18n.translate(
'uiActionsEnhanced.drilldowns.urlDrilldownCollectConfig.openInNewTabLabel',
{
- defaultMessage: 'Open in new window',
+ defaultMessage: 'Open URL in new tab',
}
);
diff --git a/src/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/components/url_drilldown_collect_config/index.ts b/src/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/components/url_drilldown_collect_config/index.ts
index fd96f908fda23..02a3097eb9d9e 100644
--- a/src/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/components/url_drilldown_collect_config/index.ts
+++ b/src/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/components/url_drilldown_collect_config/index.ts
@@ -7,4 +7,4 @@
*/
export type { UrlDrilldownCollectConfigProps } from './lazy';
-export { UrlDrilldownCollectConfig } from './lazy';
+export { UrlDrilldownCollectConfig, UrlDrilldownOptionsComponent } from './lazy';
diff --git a/src/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/components/url_drilldown_collect_config/lazy.tsx b/src/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/components/url_drilldown_collect_config/lazy.tsx
index eb666d6151ece..cc2b6500de042 100644
--- a/src/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/components/url_drilldown_collect_config/lazy.tsx
+++ b/src/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/components/url_drilldown_collect_config/lazy.tsx
@@ -7,6 +7,7 @@
*/
import * as React from 'react';
+import { UrlDrilldownOptionsProps } from './url_drilldown_options';
import type { UrlDrilldownCollectConfigProps } from './url_drilldown_collect_config';
const UrlDrilldownCollectConfigLazy = React.lazy(() =>
@@ -24,3 +25,17 @@ export const UrlDrilldownCollectConfig: React.FC
);
};
+
+const UrlDrilldownOptionsComponentLazy = React.lazy(() =>
+ import('./url_drilldown_options').then(({ UrlDrilldownOptionsComponent }) => ({
+ default: UrlDrilldownOptionsComponent,
+ }))
+);
+
+export const UrlDrilldownOptionsComponent: React.FC = (props) => {
+ return (
+
+
+
+ );
+};
diff --git a/src/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/components/url_drilldown_collect_config/test_samples/demo.tsx b/src/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/components/url_drilldown_collect_config/test_samples/demo.tsx
index 8fc2fe3c68c2e..28afcea46e6ca 100644
--- a/src/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/components/url_drilldown_collect_config/test_samples/demo.tsx
+++ b/src/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/components/url_drilldown_collect_config/test_samples/demo.tsx
@@ -13,6 +13,7 @@ import { UrlDrilldownCollectConfig } from '../url_drilldown_collect_config';
export const Demo = () => {
const [config, onConfig] = React.useState({
openInNewTab: false,
+ encodeUrl: true,
url: { template: '' },
});
diff --git a/src/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/components/url_drilldown_collect_config/url_drilldown_collect_config.tsx b/src/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/components/url_drilldown_collect_config/url_drilldown_collect_config.tsx
index 0e4825dd58e50..0495f2d61063c 100644
--- a/src/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/components/url_drilldown_collect_config/url_drilldown_collect_config.tsx
+++ b/src/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/components/url_drilldown_collect_config/url_drilldown_collect_config.tsx
@@ -7,15 +7,7 @@
*/
import React, { useRef } from 'react';
-import {
- EuiFormRow,
- EuiLink,
- EuiSwitch,
- EuiAccordion,
- EuiSpacer,
- EuiPanel,
- EuiTextColor,
-} from '@elastic/eui';
+import { EuiFormRow, EuiLink, EuiAccordion, EuiSpacer, EuiPanel } from '@elastic/eui';
import { monaco } from '@kbn/monaco';
import { UrlTemplateEditor, UrlTemplateEditorVariable } from '@kbn/kibana-react-plugin/public';
import { UrlDrilldownConfig } from '../../types';
@@ -23,12 +15,11 @@ import './index.scss';
import {
txtUrlTemplateSyntaxHelpLinkText,
txtUrlTemplateLabel,
- txtUrlTemplateOpenInNewTab,
txtUrlTemplateAdditionalOptions,
- txtUrlTemplateEncodeUrl,
- txtUrlTemplateEncodeDescription,
} from './i18n';
import { VariablePopover } from '../variable_popover';
+import { UrlDrilldownOptionsComponent } from './lazy';
+import { DEFAULT_URL_DRILLDOWN_OPTIONS } from '../../constants';
export interface UrlDrilldownCollectConfigProps {
config: UrlDrilldownConfig;
@@ -114,31 +105,12 @@ export const UrlDrilldownCollectConfig: React.FC
>
-
- onConfig({ ...config, openInNewTab: !config.openInNewTab })}
- data-test-subj="urlDrilldownOpenInNewTab"
- />
-
-
-
- {txtUrlTemplateEncodeUrl}
-
- {txtUrlTemplateEncodeDescription}
- >
- }
- checked={config.encodeUrl ?? true}
- onChange={() => onConfig({ ...config, encodeUrl: !(config.encodeUrl ?? true) })}
- />
-
+ {
+ onConfig({ ...config, ...change });
+ }}
+ />
>
diff --git a/src/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/components/url_drilldown_collect_config/url_drilldown_options.tsx b/src/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/components/url_drilldown_collect_config/url_drilldown_options.tsx
new file mode 100644
index 0000000000000..d8846556e94c4
--- /dev/null
+++ b/src/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/components/url_drilldown_collect_config/url_drilldown_options.tsx
@@ -0,0 +1,57 @@
+/*
+ * 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 { EuiFormRow, EuiSpacer, EuiSwitch, EuiTextColor } from '@elastic/eui';
+
+import {
+ txtUrlTemplateEncodeDescription,
+ txtUrlTemplateEncodeUrl,
+ txtUrlTemplateOpenInNewTab,
+} from './i18n';
+import { UrlDrilldownOptions } from '../../types';
+
+export interface UrlDrilldownOptionsProps {
+ options: UrlDrilldownOptions;
+ onOptionChange: (newOptions: Partial) => void;
+}
+
+export const UrlDrilldownOptionsComponent = ({
+ options,
+ onOptionChange,
+}: UrlDrilldownOptionsProps) => {
+ return (
+ <>
+
+ onOptionChange({ openInNewTab: !options.openInNewTab })}
+ data-test-subj="urlDrilldownOpenInNewTab"
+ />
+
+
+
+ {txtUrlTemplateEncodeUrl}
+
+ {txtUrlTemplateEncodeDescription}
+ >
+ }
+ checked={options.encodeUrl}
+ onChange={() => onOptionChange({ encodeUrl: !options.encodeUrl })}
+ />
+
+ >
+ );
+};
diff --git a/src/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/constants.ts b/src/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/constants.ts
new file mode 100644
index 0000000000000..67e48c74274eb
--- /dev/null
+++ b/src/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/constants.ts
@@ -0,0 +1,14 @@
+/*
+ * 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 { UrlDrilldownOptions } from './types';
+
+export const DEFAULT_URL_DRILLDOWN_OPTIONS: UrlDrilldownOptions = {
+ encodeUrl: true,
+ openInNewTab: true,
+};
diff --git a/src/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/index.ts b/src/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/index.ts
index 543546132a3a2..4f6ef220a0ba1 100644
--- a/src/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/index.ts
+++ b/src/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/index.ts
@@ -6,8 +6,14 @@
* Side Public License, v 1.
*/
-export type { UrlDrilldownConfig, UrlDrilldownGlobalScope, UrlDrilldownScope } from './types';
-export { UrlDrilldownCollectConfig } from './components';
+export type {
+ UrlDrilldownScope,
+ UrlDrilldownConfig,
+ UrlDrilldownOptions,
+ UrlDrilldownGlobalScope,
+} from './types';
+export { DEFAULT_URL_DRILLDOWN_OPTIONS } from './constants';
+export { UrlDrilldownCollectConfig, UrlDrilldownOptionsComponent } from './components';
export {
validateUrlTemplate as urlDrilldownValidateUrlTemplate,
validateUrl as urlDrilldownValidateUrl,
diff --git a/src/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/types.ts b/src/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/types.ts
index 3566b6712c78d..6dc9ee66de9f0 100644
--- a/src/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/types.ts
+++ b/src/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/types.ts
@@ -8,8 +8,14 @@
export type UrlDrilldownConfig = {
url: { format?: 'handlebars_v1'; template: string };
+} & UrlDrilldownOptions;
+
+/**
+ * User-configurable options for URL drilldowns
+ */
+export type UrlDrilldownOptions = {
openInNewTab: boolean;
- encodeUrl?: boolean;
+ encodeUrl: boolean;
};
/**
diff --git a/src/plugins/ui_actions_enhanced/public/index.ts b/src/plugins/ui_actions_enhanced/public/index.ts
index b609c5807a562..c419e6c682734 100644
--- a/src/plugins/ui_actions_enhanced/public/index.ts
+++ b/src/plugins/ui_actions_enhanced/public/index.ts
@@ -45,11 +45,14 @@ export type {
UrlDrilldownConfig,
UrlDrilldownGlobalScope,
UrlDrilldownScope,
+ UrlDrilldownOptions,
} from './drilldowns/url_drilldown';
export {
urlDrilldownCompileUrl,
UrlDrilldownCollectConfig,
+ UrlDrilldownOptionsComponent,
urlDrilldownGlobalScopeProvider,
urlDrilldownValidateUrl,
urlDrilldownValidateUrlTemplate,
+ DEFAULT_URL_DRILLDOWN_OPTIONS,
} from './drilldowns/url_drilldown';
diff --git a/x-pack/plugins/dashboard_enhanced/common/drilldowns/dashboard_drilldown/dashboard_drilldown_persistable_state.ts b/x-pack/plugins/dashboard_enhanced/common/drilldowns/dashboard_drilldown/dashboard_drilldown_persistable_state.ts
index fe71ebd65ea8f..aff6c1cabedca 100644
--- a/x-pack/plugins/dashboard_enhanced/common/drilldowns/dashboard_drilldown/dashboard_drilldown_persistable_state.ts
+++ b/x-pack/plugins/dashboard_enhanced/common/drilldowns/dashboard_drilldown/dashboard_drilldown_persistable_state.ts
@@ -8,7 +8,7 @@
import { SavedObjectReference } from '@kbn/core/types';
import { PersistableStateService } from '@kbn/kibana-utils-plugin/common';
import { SerializedAction, SerializedEvent } from '@kbn/ui-actions-enhanced-plugin/common';
-import { DrilldownConfig } from './types';
+import { DashboardDrilldownConfig } from './types';
type DashboardDrilldownPersistableState = PersistableStateService;
@@ -34,7 +34,7 @@ export const createInject = ({
drilldownId: string;
}): DashboardDrilldownPersistableState['inject'] => {
return (state: SerializedEvent, references: SavedObjectReference[]) => {
- const action = state.action as SerializedAction;
+ const action = state.action as SerializedAction;
const refName = generateRefName(state, drilldownId);
const ref = references.find((r) => r.name === refName);
if (!ref) return state;
@@ -49,7 +49,7 @@ export const createExtract = ({
drilldownId: string;
}): DashboardDrilldownPersistableState['extract'] => {
return (state: SerializedEvent) => {
- const action = state.action as SerializedAction;
+ const action = state.action as SerializedAction;
const references: SavedObjectReference[] = action.config.dashboardId
? [
{
diff --git a/x-pack/plugins/dashboard_enhanced/common/drilldowns/dashboard_drilldown/index.ts b/x-pack/plugins/dashboard_enhanced/common/drilldowns/dashboard_drilldown/index.ts
index fde3b5b06de2d..ab8d69d88475e 100644
--- a/x-pack/plugins/dashboard_enhanced/common/drilldowns/dashboard_drilldown/index.ts
+++ b/x-pack/plugins/dashboard_enhanced/common/drilldowns/dashboard_drilldown/index.ts
@@ -7,4 +7,4 @@
export { createExtract, createInject } from './dashboard_drilldown_persistable_state';
export { EMBEDDABLE_TO_DASHBOARD_DRILLDOWN } from './constants';
-export type { DrilldownConfig } from './types';
+export type { DashboardDrilldownConfig } from './types';
diff --git a/x-pack/plugins/dashboard_enhanced/common/drilldowns/dashboard_drilldown/types.ts b/x-pack/plugins/dashboard_enhanced/common/drilldowns/dashboard_drilldown/types.ts
index ee68681ccc8f5..cf479aedcf8e5 100644
--- a/x-pack/plugins/dashboard_enhanced/common/drilldowns/dashboard_drilldown/types.ts
+++ b/x-pack/plugins/dashboard_enhanced/common/drilldowns/dashboard_drilldown/types.ts
@@ -5,10 +5,8 @@
* 2.0.
*/
-// eslint-disable-next-line @typescript-eslint/consistent-type-definitions
-export type DrilldownConfig = {
+import { type DashboardDrilldownOptions } from '@kbn/presentation-util-plugin/public';
+
+export type DashboardDrilldownConfig = {
dashboardId?: string;
- useCurrentFilters: boolean;
- useCurrentDateRange: boolean;
- openInNewTab: boolean;
-};
+} & DashboardDrilldownOptions;
diff --git a/x-pack/plugins/dashboard_enhanced/kibana.jsonc b/x-pack/plugins/dashboard_enhanced/kibana.jsonc
index 30b70b7c9067f..88bb64bb00503 100644
--- a/x-pack/plugins/dashboard_enhanced/kibana.jsonc
+++ b/x-pack/plugins/dashboard_enhanced/kibana.jsonc
@@ -6,10 +6,7 @@
"id": "dashboardEnhanced",
"server": true,
"browser": true,
- "configPath": [
- "xpack",
- "dashboardEnhanced"
- ],
+ "configPath": ["xpack", "dashboardEnhanced"],
"requiredPlugins": [
"dashboard",
"data",
@@ -23,7 +20,8 @@
"embeddableEnhanced",
"kibanaReact",
"kibanaUtils",
- "imageEmbeddable"
+ "imageEmbeddable",
+ "presentationUtil"
]
}
}
diff --git a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/abstract_dashboard_drilldown/abstract_dashboard_drilldown.tsx b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/abstract_dashboard_drilldown/abstract_dashboard_drilldown.tsx
index 9305d515bdcef..a7d887e690f3a 100644
--- a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/abstract_dashboard_drilldown/abstract_dashboard_drilldown.tsx
+++ b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/abstract_dashboard_drilldown/abstract_dashboard_drilldown.tsx
@@ -15,11 +15,11 @@ import {
UiActionsEnhancedDrilldownDefinition as Drilldown,
} from '@kbn/ui-actions-enhanced-plugin/public';
import { CollectConfigProps, StartServicesGetter } from '@kbn/kibana-utils-plugin/public';
-import { DrilldownConfig } from '../../../../common/drilldowns/dashboard_drilldown/types';
+import { DEFAULT_DASHBOARD_DRILLDOWN_OPTIONS } from '@kbn/presentation-util-plugin/public';
+
import { CollectConfigContainer } from './components';
import { txtGoToDashboard } from './i18n';
import { Config } from './types';
-
export interface Params {
start: StartServicesGetter<{
uiActionsEnhanced: AdvancedUiActionsStart;
@@ -56,15 +56,11 @@ export abstract class AbstractDashboardDrilldown
>;
- public readonly CollectConfig: React.FC<
- CollectConfigProps
- >;
+ public readonly CollectConfig: React.FC>;
public readonly createConfig = () => ({
dashboardId: '',
- useCurrentFilters: true,
- useCurrentDateRange: true,
- openInNewTab: false,
+ ...DEFAULT_DASHBOARD_DRILLDOWN_OPTIONS,
});
public readonly isConfigValid = (config: Config): config is Config => {
diff --git a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/abstract_dashboard_drilldown/components/collect_config_container.tsx b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/abstract_dashboard_drilldown/components/collect_config_container.tsx
index 4a9818b3f23f5..0829bf1710719 100644
--- a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/abstract_dashboard_drilldown/components/collect_config_container.tsx
+++ b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/abstract_dashboard_drilldown/components/collect_config_container.tsx
@@ -80,11 +80,7 @@ export class CollectConfigContainer extends React.Component<
return (
{
@@ -94,24 +90,10 @@ export class CollectConfigContainer extends React.Component<
}
}}
onSearchChange={this.debouncedLoadDashboards}
- onCurrentFiltersToggle={() =>
- onConfig({
- ...config,
- useCurrentFilters: !config.useCurrentFilters,
- })
- }
- onKeepRangeToggle={() =>
- onConfig({
- ...config,
- useCurrentDateRange: !config.useCurrentDateRange,
- })
- }
- onOpenInNewTab={() =>
- onConfig({
- ...config,
- openInNewTab: !config.openInNewTab,
- })
- }
+ config={config}
+ onConfigChange={(changes: Partial) => {
+ onConfig({ ...config, ...changes });
+ }}
/>
);
}
diff --git a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/abstract_dashboard_drilldown/components/dashboard_drilldown_config/dashboard_drilldown_config.tsx b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/abstract_dashboard_drilldown/components/dashboard_drilldown_config/dashboard_drilldown_config.tsx
index d6e00f7de0edb..78ab1db7212b2 100644
--- a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/abstract_dashboard_drilldown/components/dashboard_drilldown_config/dashboard_drilldown_config.tsx
+++ b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/abstract_dashboard_drilldown/components/dashboard_drilldown_config/dashboard_drilldown_config.tsx
@@ -6,44 +6,37 @@
*/
import React from 'react';
-import { EuiFormRow, EuiSwitch, EuiComboBox, EuiComboBoxOptionOption } from '@elastic/eui';
+import { EuiFormRow, EuiComboBox, EuiComboBoxOptionOption } from '@elastic/eui';
import {
- txtChooseDestinationDashboard,
- txtUseCurrentFilters,
- txtUseCurrentDateRange,
- txtOpenInNewTab,
-} from './i18n';
+ withSuspense,
+ DashboardDrilldownOptionsComponent,
+} from '@kbn/presentation-util-plugin/public';
+
+import { txtChooseDestinationDashboard } from './i18n';
+import { Config as DrilldownConfig } from '../../types';
+
+const DashboardDrilldownOptions = withSuspense(DashboardDrilldownOptionsComponent, null);
export interface DashboardDrilldownConfigProps {
- activeDashboardId?: string;
dashboards: Array>;
- currentFilters?: boolean;
- keepRange?: boolean;
- openInNewTab?: boolean;
onDashboardSelect: (dashboardId: string) => void;
- onCurrentFiltersToggle?: () => void;
- onKeepRangeToggle?: () => void;
- onOpenInNewTab?: () => void;
onSearchChange: (searchString: string) => void;
isLoading: boolean;
error?: string;
+ config: DrilldownConfig;
+ onConfigChange: (changes: Partial) => void;
}
export const DashboardDrilldownConfig: React.FC = ({
- activeDashboardId,
dashboards,
- currentFilters,
- keepRange,
- openInNewTab,
onDashboardSelect,
- onCurrentFiltersToggle,
- onKeepRangeToggle,
- onOpenInNewTab,
onSearchChange,
isLoading,
error,
+ config,
+ onConfigChange,
}: DashboardDrilldownConfigProps) => {
- const selectedTitle = dashboards.find((item) => item.value === activeDashboardId)?.label || '';
+ const selectedTitle = dashboards.find((item) => item.value === config.dashboardId)?.label || '';
return (
<>
@@ -51,7 +44,7 @@ export const DashboardDrilldownConfig: React.FC =
async
selectedOptions={
- activeDashboardId ? [{ label: selectedTitle, value: activeDashboardId }] : []
+ config.dashboardId ? [{ label: selectedTitle, value: config.dashboardId }] : []
}
options={dashboards}
onChange={([{ value = '' } = { value: '' }]) => onDashboardSelect(value)}
@@ -63,36 +56,7 @@ export const DashboardDrilldownConfig: React.FC =
isInvalid={!!error}
/>
- {!!onCurrentFiltersToggle && (
-
-
-
- )}
- {!!onKeepRangeToggle && (
-
-
-
- )}
- {!!onOpenInNewTab && (
-
-
-
- )}
+
>
);
};
diff --git a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/abstract_dashboard_drilldown/components/dashboard_drilldown_config/i18n.ts b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/abstract_dashboard_drilldown/components/dashboard_drilldown_config/i18n.ts
index cf822a6a72d7a..5ee0794076348 100644
--- a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/abstract_dashboard_drilldown/components/dashboard_drilldown_config/i18n.ts
+++ b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/abstract_dashboard_drilldown/components/dashboard_drilldown_config/i18n.ts
@@ -13,24 +13,3 @@ export const txtChooseDestinationDashboard = i18n.translate(
defaultMessage: 'Choose destination dashboard',
}
);
-
-export const txtUseCurrentFilters = i18n.translate(
- 'xpack.dashboard.components.DashboardDrilldownConfig.useCurrentFilters',
- {
- defaultMessage: 'Use filters and query from origin dashboard',
- }
-);
-
-export const txtUseCurrentDateRange = i18n.translate(
- 'xpack.dashboard.components.DashboardDrilldownConfig.useCurrentDateRange',
- {
- defaultMessage: 'Use date range from origin dashboard',
- }
-);
-
-export const txtOpenInNewTab = i18n.translate(
- 'xpack.dashboard.components.DashboardDrilldownConfig.openInNewTab',
- {
- defaultMessage: 'Open dashboard in new tab',
- }
-);
diff --git a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/abstract_dashboard_drilldown/types.ts b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/abstract_dashboard_drilldown/types.ts
index 599ecce6217e2..46181deabb8b5 100644
--- a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/abstract_dashboard_drilldown/types.ts
+++ b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/abstract_dashboard_drilldown/types.ts
@@ -6,8 +6,8 @@
*/
import { UiActionsEnhancedBaseActionFactoryContext } from '@kbn/ui-actions-enhanced-plugin/public';
-import { DrilldownConfig } from '../../../../common';
+import { DashboardDrilldownConfig } from '../../../../common';
-export type Config = DrilldownConfig;
+export type Config = DashboardDrilldownConfig;
export type FactoryContext = UiActionsEnhancedBaseActionFactoryContext;
diff --git a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/embeddable_to_dashboard_drilldown/embeddable_to_dashboard_drilldown.tsx b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/embeddable_to_dashboard_drilldown/embeddable_to_dashboard_drilldown.tsx
index 468cbaf9940a9..9a984af52d21c 100644
--- a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/embeddable_to_dashboard_drilldown/embeddable_to_dashboard_drilldown.tsx
+++ b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/embeddable_to_dashboard_drilldown/embeddable_to_dashboard_drilldown.tsx
@@ -4,29 +4,25 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
-import { type Filter, isFilterPinned, Query, TimeRange } from '@kbn/es-query';
+import { extractTimeRange, isFilterPinned } from '@kbn/es-query';
import type { KibanaLocation } from '@kbn/share-plugin/public';
-import { DashboardAppLocatorParams, cleanEmptyKeys } from '@kbn/dashboard-plugin/public';
+import {
+ cleanEmptyKeys,
+ DashboardAppLocatorParams,
+ getEmbeddableParams,
+} from '@kbn/dashboard-plugin/public';
import { setStateToKbnUrl } from '@kbn/kibana-utils-plugin/public';
-import { APPLY_FILTER_TRIGGER, isQuery, isTimeRange } from '@kbn/data-plugin/public';
-import { extractTimeRange } from '@kbn/es-query';
+import { APPLY_FILTER_TRIGGER } from '@kbn/data-plugin/public';
import { ApplyGlobalFilterActionContext } from '@kbn/unified-search-plugin/public';
-import { IEmbeddable, EmbeddableInput } from '@kbn/embeddable-plugin/public';
import { EnhancedEmbeddableContext } from '@kbn/embeddable-enhanced-plugin/public';
import { IMAGE_CLICK_TRIGGER } from '@kbn/image-embeddable-plugin/public';
import {
AbstractDashboardDrilldown,
AbstractDashboardDrilldownParams,
- AbstractDashboardDrilldownConfig as Config,
} from '../abstract_dashboard_drilldown';
import { EMBEDDABLE_TO_DASHBOARD_DRILLDOWN } from './constants';
import { createExtract, createInject } from '../../../../common';
-
-interface EmbeddableQueryInput extends EmbeddableInput {
- query?: Query;
- filters?: Filter[];
- timeRange?: TimeRange;
-}
+import { AbstractDashboardDrilldownConfig as Config } from '../abstract_dashboard_drilldown';
type Context = EnhancedEmbeddableContext & ApplyGlobalFilterActionContext;
export type Params = AbstractDashboardDrilldownParams;
@@ -48,28 +44,16 @@ export class EmbeddableToDashboardDrilldown extends AbstractDashboardDrilldown {
- const params: DashboardAppLocatorParams = {
- dashboardId: config.dashboardId,
- };
+ let params: DashboardAppLocatorParams = { dashboardId: config.dashboardId };
if (context.embeddable) {
- const embeddable = context.embeddable as IEmbeddable;
- const input = embeddable.getInput();
- if (isQuery(input.query) && config.useCurrentFilters) params.query = input.query;
-
- // if useCurrentDashboardDataRange is enabled, then preserve current time range
- // if undefined is passed, then destination dashboard will figure out time range itself
- // for brush event this time range would be overwritten
- if (isTimeRange(input.timeRange) && config.useCurrentDateRange)
- params.timeRange = input.timeRange;
-
- // if useCurrentDashboardFilters enabled, then preserve all the filters (pinned, unpinned, and from controls)
- // otherwise preserve only pinned
- params.filters = config.useCurrentFilters
- ? input.filters
- : input.filters?.filter((f) => isFilterPinned(f));
+ params = {
+ ...params,
+ ...getEmbeddableParams(context.embeddable, config),
+ };
}
+ /** Get event params */
const { restOfFilters: filtersFromEvent, timeRange: timeRangeFromEvent } = extractTimeRange(
context.filters,
context.timeFieldName
diff --git a/x-pack/plugins/dashboard_enhanced/tsconfig.json b/x-pack/plugins/dashboard_enhanced/tsconfig.json
index dd5a78b87dc4f..4c08a46b6e2d6 100644
--- a/x-pack/plugins/dashboard_enhanced/tsconfig.json
+++ b/x-pack/plugins/dashboard_enhanced/tsconfig.json
@@ -1,13 +1,9 @@
{
"extends": "../../../tsconfig.base.json",
"compilerOptions": {
- "outDir": "target/types",
+ "outDir": "target/types"
},
- "include": [
- "common/**/*",
- "public/**/*",
- "server/**/*",
- ],
+ "include": ["common/**/*", "public/**/*", "server/**/*"],
"kbn_references": [
"@kbn/kibana-react-plugin",
"@kbn/kibana-utils-plugin",
@@ -22,9 +18,8 @@
"@kbn/es-query",
"@kbn/unified-search-plugin",
"@kbn/ui-actions-plugin",
- "@kbn/image-embeddable-plugin"
+ "@kbn/image-embeddable-plugin",
+ "@kbn/presentation-util-plugin"
],
- "exclude": [
- "target/**/*",
- ]
+ "exclude": ["target/**/*"]
}
diff --git a/x-pack/plugins/drilldowns/url_drilldown/public/lib/url_drilldown.test.ts b/x-pack/plugins/drilldowns/url_drilldown/public/lib/url_drilldown.test.ts
index 9cfeea206300e..500723a6786b9 100644
--- a/x-pack/plugins/drilldowns/url_drilldown/public/lib/url_drilldown.test.ts
+++ b/x-pack/plugins/drilldowns/url_drilldown/public/lib/url_drilldown.test.ts
@@ -107,6 +107,7 @@ describe('UrlDrilldown', () => {
template: `https://elasti.co/?{{event.value}}`,
},
openInNewTab: false,
+ encodeUrl: true,
};
const context: ActionContext = {
@@ -124,6 +125,7 @@ describe('UrlDrilldown', () => {
template: `https://elasti.co/?{{event.value}}&{{rison context.panel.query}}`,
},
openInNewTab: false,
+ encodeUrl: true,
};
const context: ActionContext = {
@@ -143,6 +145,7 @@ describe('UrlDrilldown', () => {
template: `https://elasti.co/?{{event.value}}&{{rison context.panel.somethingFake}}`,
},
openInNewTab: false,
+ encodeUrl: true,
};
const context: ActionContext = {
@@ -163,6 +166,7 @@ describe('UrlDrilldown', () => {
template: `https://elasti.co/?{{event.value}}&{{rison context.panel.query}}`,
},
openInNewTab: false,
+ encodeUrl: true,
};
const context: ActionContext = {
@@ -191,6 +195,7 @@ describe('UrlDrilldown', () => {
template: `https://elasti.co/?{{event.value}}&{{rison context.panel.query}}`,
},
openInNewTab: false,
+ encodeUrl: true,
};
const context: ActionContext = {
@@ -213,6 +218,7 @@ describe('UrlDrilldown', () => {
template: `https://elasti.co/?{{event.value}}&{{rison context.panel.invalid}}`,
},
openInNewTab: false,
+ encodeUrl: true,
};
const context: ActionContext = {
@@ -235,6 +241,7 @@ describe('UrlDrilldown', () => {
template: `https://elasti.co/?{{event.value}}&{{rison context.panel.query}}`,
},
openInNewTab: false,
+ encodeUrl: true,
};
const context: ActionContext = {
@@ -503,6 +510,7 @@ describe('encoding', () => {
template: 'https://elastic.co?foo=head%26shoulders',
},
openInNewTab: false,
+ encodeUrl: true,
};
const url = await urlDrilldown.getHref(config, context);
diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json
index f0306e4e9da3a..374cea2026289 100644
--- a/x-pack/plugins/translations/translations/fr-FR.json
+++ b/x-pack/plugins/translations/translations/fr-FR.json
@@ -40168,9 +40168,6 @@
"xpack.cloudLinks.setupGuide": "Guides de configuration",
"xpack.cloudLinks.userMenuLinks.profileLinkText": "Modifier le profil",
"xpack.dashboard.components.DashboardDrilldownConfig.chooseDestinationDashboard": "Choisir le tableau de bord de destination",
- "xpack.dashboard.components.DashboardDrilldownConfig.openInNewTab": "Ouvrir le tableau de bord dans un nouvel onglet",
- "xpack.dashboard.components.DashboardDrilldownConfig.useCurrentDateRange": "Utiliser la plage de dates du tableau de bord d'origine",
- "xpack.dashboard.components.DashboardDrilldownConfig.useCurrentFilters": "Utiliser les filtres et la requête du tableau de bord d'origine",
"xpack.dashboard.drilldown.errorDestinationDashboardIsMissing": "Le tableau de bord de destination (\"{dashboardId}\") n'existe plus. Choisissez un autre tableau de bord.",
"xpack.dashboard.drilldown.goToDashboard": "Accéder au tableau de bord",
"xpack.dashboard.FlyoutCreateDrilldownAction.displayName": "Créer une recherche",
diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json
index ad9e79d2eaf6d..29500521e970e 100644
--- a/x-pack/plugins/translations/translations/ja-JP.json
+++ b/x-pack/plugins/translations/translations/ja-JP.json
@@ -40159,9 +40159,6 @@
"xpack.cloudLinks.setupGuide": "セットアップガイド",
"xpack.cloudLinks.userMenuLinks.profileLinkText": "プロフィールを編集",
"xpack.dashboard.components.DashboardDrilldownConfig.chooseDestinationDashboard": "対象ダッシュボードを選択",
- "xpack.dashboard.components.DashboardDrilldownConfig.openInNewTab": "新しいタブでダッシュボードを開く",
- "xpack.dashboard.components.DashboardDrilldownConfig.useCurrentDateRange": "元のダッシュボードから日付範囲を使用",
- "xpack.dashboard.components.DashboardDrilldownConfig.useCurrentFilters": "元のダッシュボードからフィルターとクエリを使用",
"xpack.dashboard.drilldown.errorDestinationDashboardIsMissing": "対象ダッシュボード('{dashboardId}')は存在しません。別のダッシュボードを選択してください。",
"xpack.dashboard.drilldown.goToDashboard": "ダッシュボードに移動",
"xpack.dashboard.FlyoutCreateDrilldownAction.displayName": "ドリルダウンを作成",
diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json
index 4d0de6185e717..e562115194f19 100644
--- a/x-pack/plugins/translations/translations/zh-CN.json
+++ b/x-pack/plugins/translations/translations/zh-CN.json
@@ -40153,9 +40153,6 @@
"xpack.cloudLinks.setupGuide": "设置指南",
"xpack.cloudLinks.userMenuLinks.profileLinkText": "编辑配置文件",
"xpack.dashboard.components.DashboardDrilldownConfig.chooseDestinationDashboard": "选择目标仪表板",
- "xpack.dashboard.components.DashboardDrilldownConfig.openInNewTab": "在新选项卡中打开仪表板",
- "xpack.dashboard.components.DashboardDrilldownConfig.useCurrentDateRange": "使用源仪表板的日期范围",
- "xpack.dashboard.components.DashboardDrilldownConfig.useCurrentFilters": "使用源仪表板的筛选和查询",
"xpack.dashboard.drilldown.errorDestinationDashboardIsMissing": "目标仪表板(“{dashboardId}”)已不存在。选择其他仪表板。",
"xpack.dashboard.drilldown.goToDashboard": "前往仪表板",
"xpack.dashboard.FlyoutCreateDrilldownAction.displayName": "创建向下钻取",
From e585f12bf33d9c5aa51310c4f997a00b9b7a646c Mon Sep 17 00:00:00 2001
From: Nick Peihl
Date: Tue, 5 Sep 2023 15:57:02 -0400
Subject: [PATCH 14/53] [Dashboard Navigation] Disable Links panel in Canvas
(#165770)
Fixes https://github.com/elastic/kibana/issues/164114
## Summary
Disables the new Links panel in Canvas.
Links panel is a dashboard only feature. Canvas does not have any use
for the Links panels. Canvas supports adding multiple pages to a workpad
which offers a similar functionality.
---
.../components/embeddable_flyout/flyout.component.tsx | 6 +++++-
.../components/workpad_header/editor_menu/editor_menu.tsx | 2 +-
2 files changed, 6 insertions(+), 2 deletions(-)
diff --git a/x-pack/plugins/canvas/public/components/embeddable_flyout/flyout.component.tsx b/x-pack/plugins/canvas/public/components/embeddable_flyout/flyout.component.tsx
index d93a9fb49d359..5e68a34928ddc 100644
--- a/x-pack/plugins/canvas/public/components/embeddable_flyout/flyout.component.tsx
+++ b/x-pack/plugins/canvas/public/components/embeddable_flyout/flyout.component.tsx
@@ -61,7 +61,11 @@ export const AddEmbeddableFlyout: FC = ({
const embeddableFactories = getEmbeddableFactories();
const availableSavedObjects = Array.from(embeddableFactories)
- .filter((factory) => isByValueEnabled || availableEmbeddables.includes(factory.type))
+ .filter(
+ (factory) =>
+ factory.type !== 'navigation_embeddable' && // Links panels only exist on Dashboards
+ (isByValueEnabled || availableEmbeddables.includes(factory.type))
+ )
.map((factory) => factory.savedObjectMetaData)
.filter>(function (
maybeSavedObjectMetaData
diff --git a/x-pack/plugins/canvas/public/components/workpad_header/editor_menu/editor_menu.tsx b/x-pack/plugins/canvas/public/components/workpad_header/editor_menu/editor_menu.tsx
index 2258b63a49377..fa656645e5f4e 100644
--- a/x-pack/plugins/canvas/public/components/workpad_header/editor_menu/editor_menu.tsx
+++ b/x-pack/plugins/canvas/public/components/workpad_header/editor_menu/editor_menu.tsx
@@ -142,7 +142,7 @@ export const EditorMenu: FC = ({ addElement }) => {
isEditable &&
!isContainerType &&
canCreateNew() &&
- !['visualization', 'ml'].some((factoryType) => {
+ !['visualization', 'ml', 'navigation_embeddable'].some((factoryType) => {
return type.includes(factoryType);
})
)
From a2a2cf26f1cddee758a7fb4b81c649aa8299e443 Mon Sep 17 00:00:00 2001
From: Devon Thomson
Date: Mon, 11 Sep 2023 16:48:03 -0400
Subject: [PATCH 15/53] [Links] Panel Placement (#165919)
Adds the ability for Links Panels to choose their own size and placement strategy on creation.
---
packages/kbn-optimizer/limits.yml | 2 +-
.../embeddable/control_group_container.tsx | 28 +--
.../dashboard_actions/clone_panel_action.tsx | 30 ++--
.../top_nav/dashboard_editing_toolbar.tsx | 20 ++-
.../_dashboard_container.scss | 1 -
.../{panel => grid}/_dashboard_panel.scss | 0
.../component/grid/_index.scss | 1 +
.../component/grid/dashboard_grid.tsx | 3 +-
.../component/panel/_index.scss | 1 -
.../panel/create_panel_state.test.ts | 83 ---------
.../component/panel/create_panel_state.ts | 60 -------
.../{panel => panel_placement}/index.ts | 4 +-
.../place_clone_panel_strategy.ts} | 103 +----------
.../place_new_panel_strategies.ts | 105 +++++++++++
.../panel_placement/place_panel.test.ts | 167 ++++++++++++++++++
.../component/panel_placement/place_panel.ts | 60 +++++++
.../component/panel_placement/types.ts | 41 +++++
.../embeddable/dashboard_container.tsx | 15 +-
.../state/diffing/dashboard_diffing_utils.ts | 4 +-
src/plugins/dashboard/public/index.ts | 1 +
.../add_panel_flyout/add_panel_flyout.tsx | 3 +-
.../edit_panel_action/edit_panel_action.ts | 11 +-
src/plugins/embeddable/public/index.ts | 1 +
.../public/lib/containers/container.ts | 34 ++--
.../public/lib/containers/i_container.ts | 3 +-
.../default_embeddable_factory_provider.ts | 1 +
.../lib/embeddables/embeddable_factory.ts | 16 +-
.../public/editor/open_editor_flyout.tsx | 28 ++-
.../navigation_embeddable_factory.test.ts | 52 ++++++
.../navigation_embeddable_factory.ts | 61 ++++++-
.../public/embeddable/types.ts | 5 +
.../navigation_embeddable/tsconfig.json | 3 +-
32 files changed, 632 insertions(+), 315 deletions(-)
rename src/plugins/dashboard/public/dashboard_container/component/{panel => grid}/_dashboard_panel.scss (100%)
delete mode 100644 src/plugins/dashboard/public/dashboard_container/component/panel/_index.scss
delete mode 100644 src/plugins/dashboard/public/dashboard_container/component/panel/create_panel_state.test.ts
delete mode 100644 src/plugins/dashboard/public/dashboard_container/component/panel/create_panel_state.ts
rename src/plugins/dashboard/public/dashboard_container/component/{panel => panel_placement}/index.ts (76%)
rename src/plugins/dashboard/public/dashboard_container/component/{panel/dashboard_panel_placement.ts => panel_placement/place_clone_panel_strategy.ts} (53%)
create mode 100644 src/plugins/dashboard/public/dashboard_container/component/panel_placement/place_new_panel_strategies.ts
create mode 100644 src/plugins/dashboard/public/dashboard_container/component/panel_placement/place_panel.test.ts
create mode 100644 src/plugins/dashboard/public/dashboard_container/component/panel_placement/place_panel.ts
create mode 100644 src/plugins/dashboard/public/dashboard_container/component/panel_placement/types.ts
create mode 100644 src/plugins/navigation_embeddable/public/embeddable/navigation_embeddable_factory.test.ts
diff --git a/packages/kbn-optimizer/limits.yml b/packages/kbn-optimizer/limits.yml
index 67d6be54cf004..744058cd49821 100644
--- a/packages/kbn-optimizer/limits.yml
+++ b/packages/kbn-optimizer/limits.yml
@@ -95,7 +95,7 @@ pageLoadAssetSize:
ml: 82187
monitoring: 80000
navigation: 37269
- navigationEmbeddable: 17892
+ navigationEmbeddable: 44490
newsfeed: 42228
noDataPage: 5000
observability: 115443
diff --git a/src/plugins/controls/public/control_group/embeddable/control_group_container.tsx b/src/plugins/controls/public/control_group/embeddable/control_group_container.tsx
index 6344c768eae63..2bf18a70a97e7 100644
--- a/src/plugins/controls/public/control_group/embeddable/control_group_container.tsx
+++ b/src/plugins/controls/public/control_group/embeddable/control_group_container.tsx
@@ -222,22 +222,22 @@ export class ControlGroupContainer extends Container<
public async addDataControlFromField(controlProps: AddDataControlProps) {
const panelState = await getDataControlPanelState(this.getInput(), controlProps);
- return this.createAndSaveEmbeddable(panelState.type, panelState);
+ return this.createAndSaveEmbeddable(panelState.type, panelState, this.getInput().panels);
}
public addOptionsListControl(controlProps: AddOptionsListControlProps) {
const panelState = getOptionsListPanelState(this.getInput(), controlProps);
- return this.createAndSaveEmbeddable(panelState.type, panelState);
+ return this.createAndSaveEmbeddable(panelState.type, panelState, this.getInput().panels);
}
public addRangeSliderControl(controlProps: AddRangeSliderControlProps) {
const panelState = getRangeSliderPanelState(this.getInput(), controlProps);
- return this.createAndSaveEmbeddable(panelState.type, panelState);
+ return this.createAndSaveEmbeddable(panelState.type, panelState, this.getInput().panels);
}
public addTimeSliderControl() {
const panelState = getTimeSliderPanelState(this.getInput());
- return this.createAndSaveEmbeddable(panelState.type, panelState);
+ return this.createAndSaveEmbeddable(panelState.type, panelState, this.getInput().panels);
}
public openAddDataControlFlyout = openAddDataControlFlyout;
@@ -283,15 +283,19 @@ export class ControlGroupContainer extends Container<
protected createNewPanelState(
factory: EmbeddableFactory,
- partial: Partial = {}
- ): ControlPanelState {
- const panelState = super.createNewPanelState(factory, partial);
+ partial: Partial = {},
+ otherPanels: ControlGroupInput['panels']
+ ) {
+ const { newPanel } = super.createNewPanelState(factory, partial);
return {
- order: getNextPanelOrder(this.getInput().panels),
- width: this.getInput().defaultControlWidth,
- grow: this.getInput().defaultControlGrow,
- ...panelState,
- } as ControlPanelState;
+ newPanel: {
+ order: getNextPanelOrder(this.getInput().panels),
+ width: this.getInput().defaultControlWidth,
+ grow: this.getInput().defaultControlGrow,
+ ...newPanel,
+ } as ControlPanelState,
+ otherPanels,
+ };
}
protected onRemoveEmbeddable(idToRemove: string) {
diff --git a/src/plugins/dashboard/public/dashboard_actions/clone_panel_action.tsx b/src/plugins/dashboard/public/dashboard_actions/clone_panel_action.tsx
index e028d8f387312..938beecbdfc1f 100644
--- a/src/plugins/dashboard/public/dashboard_actions/clone_panel_action.tsx
+++ b/src/plugins/dashboard/public/dashboard_actions/clone_panel_action.tsx
@@ -22,10 +22,9 @@ import { Action, IncompatibleActionError } from '@kbn/ui-actions-plugin/public';
import { type DashboardPanelState } from '../../common';
import { pluginServices } from '../services/plugin_services';
-import { createPanelState } from '../dashboard_container/component/panel';
import { dashboardClonePanelActionStrings } from './_dashboard_actions_strings';
+import { placeClonePanel } from '../dashboard_container/component/panel_placement';
import { DASHBOARD_CONTAINER_TYPE, type DashboardContainer } from '../dashboard_container';
-import { placePanelBeside } from '../dashboard_container/component/panel/dashboard_panel_placement';
export const ACTION_CLONE_PANEL = 'clonePanel';
@@ -82,6 +81,7 @@ export class ClonePanelAction implements Action {
throw new PanelNotFoundError();
}
+ // Clone panel input
const clonedPanelState: PanelState = await (async () => {
const newTitle = await this.getCloneTitle(embeddable, embeddable.getTitle() || '');
const id = uuidv4();
@@ -110,18 +110,20 @@ export class ClonePanelAction implements Action {
'data-test-subj': 'addObjectToContainerSuccess',
});
- const { otherPanels, newPanel } = createPanelState(
- clonedPanelState,
- dashboard.getInput().panels,
- placePanelBeside,
- {
- width: panelToClone.gridData.w,
- height: panelToClone.gridData.h,
- currentPanels: dashboard.getInput().panels,
- placeBesideId: panelToClone.explicitInput.id,
- scrollToPanel: true,
- }
- );
+ const { newPanelPlacement, otherPanels } = placeClonePanel({
+ width: panelToClone.gridData.w,
+ height: panelToClone.gridData.h,
+ currentPanels: dashboard.getInput().panels,
+ placeBesideId: panelToClone.explicitInput.id,
+ });
+
+ const newPanel = {
+ ...clonedPanelState,
+ gridData: {
+ ...newPanelPlacement,
+ i: clonedPanelState.explicitInput.id,
+ },
+ };
dashboard.updateInput({
panels: {
diff --git a/src/plugins/dashboard/public/dashboard_app/top_nav/dashboard_editing_toolbar.tsx b/src/plugins/dashboard/public/dashboard_app/top_nav/dashboard_editing_toolbar.tsx
index 11347cf57cc40..b92091bb1376b 100644
--- a/src/plugins/dashboard/public/dashboard_app/top_nav/dashboard_editing_toolbar.tsx
+++ b/src/plugins/dashboard/public/dashboard_app/top_nav/dashboard_editing_toolbar.tsx
@@ -12,8 +12,9 @@ import { METRIC_TYPE } from '@kbn/analytics';
import { useEuiTheme } from '@elastic/eui';
import { AddFromLibraryButton, Toolbar, ToolbarButton } from '@kbn/shared-ux-button-toolbar';
-import { EmbeddableFactory } from '@kbn/embeddable-plugin/public';
+import { EmbeddableFactory, EmbeddableInput } from '@kbn/embeddable-plugin/public';
import { BaseVisType, VisTypeAlias } from '@kbn/visualizations-plugin/public';
+import { isExplicitInputWithAttributes } from '@kbn/embeddable-plugin/public';
import { getCreateVisualizationButtonTitle } from '../_dashboard_app_strings';
import { EditorMenu } from './editor_menu';
@@ -83,15 +84,26 @@ export function DashboardEditingToolbar() {
trackUiMetric(METRIC_TYPE.CLICK, embeddableFactory.type);
}
- let explicitInput: Awaited>;
+ let explicitInput: Partial;
+ let attributes: unknown;
try {
- explicitInput = await embeddableFactory.getExplicitInput(undefined, dashboard);
+ const explicitInputReturn = await embeddableFactory.getExplicitInput(undefined, dashboard);
+ if (isExplicitInputWithAttributes(explicitInputReturn)) {
+ explicitInput = explicitInputReturn.newInput;
+ attributes = explicitInputReturn.attributes;
+ } else {
+ explicitInput = explicitInputReturn;
+ }
} catch (e) {
// error likely means user canceled embeddable creation
return;
}
- const newEmbeddable = await dashboard.addNewEmbeddable(embeddableFactory.type, explicitInput);
+ const newEmbeddable = await dashboard.addNewEmbeddable(
+ embeddableFactory.type,
+ explicitInput,
+ attributes
+ );
if (newEmbeddable) {
dashboard.setScrollToPanelId(newEmbeddable.id);
diff --git a/src/plugins/dashboard/public/dashboard_container/_dashboard_container.scss b/src/plugins/dashboard/public/dashboard_container/_dashboard_container.scss
index aa5b5950a0d59..12c11f778d616 100644
--- a/src/plugins/dashboard/public/dashboard_container/_dashboard_container.scss
+++ b/src/plugins/dashboard/public/dashboard_container/_dashboard_container.scss
@@ -1,7 +1,6 @@
@import '../../../embeddable/public/variables';
@import './component/grid/index';
-@import './component/panel/index';
@import './component/viewport/index';
.dashboardContainer, .dashboardViewport {
diff --git a/src/plugins/dashboard/public/dashboard_container/component/panel/_dashboard_panel.scss b/src/plugins/dashboard/public/dashboard_container/component/grid/_dashboard_panel.scss
similarity index 100%
rename from src/plugins/dashboard/public/dashboard_container/component/panel/_dashboard_panel.scss
rename to src/plugins/dashboard/public/dashboard_container/component/grid/_dashboard_panel.scss
diff --git a/src/plugins/dashboard/public/dashboard_container/component/grid/_index.scss b/src/plugins/dashboard/public/dashboard_container/component/grid/_index.scss
index eb393d7603b8a..cb324e984f7ef 100644
--- a/src/plugins/dashboard/public/dashboard_container/component/grid/_index.scss
+++ b/src/plugins/dashboard/public/dashboard_container/component/grid/_index.scss
@@ -1 +1,2 @@
@import './dashboard_grid';
+@import './dashboard_panel';
\ No newline at end of file
diff --git a/src/plugins/dashboard/public/dashboard_container/component/grid/dashboard_grid.tsx b/src/plugins/dashboard/public/dashboard_container/component/grid/dashboard_grid.tsx
index 12cd26df28f18..28a4ccbb1c8a8 100644
--- a/src/plugins/dashboard/public/dashboard_container/component/grid/dashboard_grid.tsx
+++ b/src/plugins/dashboard/public/dashboard_container/component/grid/dashboard_grid.tsx
@@ -125,8 +125,7 @@ export const DashboardGrid = ({ viewportWidth }: { viewportWidth: number }) => {
className={classes}
width={viewportWidth}
breakpoints={breakpoints}
- onDragStop={onLayoutChange}
- onResizeStop={onLayoutChange}
+ onLayoutChange={onLayoutChange}
isResizable={!expandedPanelId}
isDraggable={!expandedPanelId}
rowHeight={DASHBOARD_GRID_HEIGHT}
diff --git a/src/plugins/dashboard/public/dashboard_container/component/panel/_index.scss b/src/plugins/dashboard/public/dashboard_container/component/panel/_index.scss
deleted file mode 100644
index 8212aad12abf1..0000000000000
--- a/src/plugins/dashboard/public/dashboard_container/component/panel/_index.scss
+++ /dev/null
@@ -1 +0,0 @@
-@import './dashboard_panel';
diff --git a/src/plugins/dashboard/public/dashboard_container/component/panel/create_panel_state.test.ts b/src/plugins/dashboard/public/dashboard_container/component/panel/create_panel_state.test.ts
deleted file mode 100644
index acfec6de31d08..0000000000000
--- a/src/plugins/dashboard/public/dashboard_container/component/panel/create_panel_state.test.ts
+++ /dev/null
@@ -1,83 +0,0 @@
-/*
- * 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 { DashboardPanelState } from '../../../../common';
-import { EmbeddableInput } from '@kbn/embeddable-plugin/public';
-import { CONTACT_CARD_EMBEDDABLE } from '@kbn/embeddable-plugin/public/lib/test_samples';
-import { DEFAULT_PANEL_HEIGHT, DEFAULT_PANEL_WIDTH } from '../../../dashboard_constants';
-
-import { createPanelState } from './create_panel_state';
-
-interface TestInput extends EmbeddableInput {
- test: string;
-}
-const panels: { [key: string]: DashboardPanelState } = {};
-
-test('createPanelState adds a new panel state in 0,0 position', () => {
- const { newPanel: panelState } = createPanelState(
- {
- type: CONTACT_CARD_EMBEDDABLE,
- explicitInput: { test: 'hi', id: '123' },
- },
- panels
- );
- expect(panelState.explicitInput.test).toBe('hi');
- expect(panelState.type).toBe(CONTACT_CARD_EMBEDDABLE);
- expect(panelState.explicitInput.id).toBeDefined();
- expect(panelState.gridData.x).toBe(0);
- expect(panelState.gridData.y).toBe(0);
- expect(panelState.gridData.h).toBe(DEFAULT_PANEL_HEIGHT);
- expect(panelState.gridData.w).toBe(DEFAULT_PANEL_WIDTH);
-
- panels[panelState.explicitInput.id] = panelState;
-});
-
-test('createPanelState adds a second new panel state', () => {
- const { newPanel: panelState } = createPanelState(
- { type: CONTACT_CARD_EMBEDDABLE, explicitInput: { test: 'bye', id: '456' } },
- panels
- );
-
- expect(panelState.gridData.x).toBe(DEFAULT_PANEL_WIDTH);
- expect(panelState.gridData.y).toBe(0);
- expect(panelState.gridData.h).toBe(DEFAULT_PANEL_HEIGHT);
- expect(panelState.gridData.w).toBe(DEFAULT_PANEL_WIDTH);
-
- panels[panelState.explicitInput.id] = panelState;
-});
-
-test('createPanelState adds a third new panel state', () => {
- const { newPanel: panelState } = createPanelState(
- {
- type: CONTACT_CARD_EMBEDDABLE,
- explicitInput: { test: 'bye', id: '789' },
- },
- panels
- );
- expect(panelState.gridData.x).toBe(0);
- expect(panelState.gridData.y).toBe(DEFAULT_PANEL_HEIGHT);
- expect(panelState.gridData.h).toBe(DEFAULT_PANEL_HEIGHT);
- expect(panelState.gridData.w).toBe(DEFAULT_PANEL_WIDTH);
-
- panels[panelState.explicitInput.id] = panelState;
-});
-
-test('createPanelState adds a new panel state in the top most position', () => {
- delete panels['456'];
- const { newPanel: panelState } = createPanelState(
- {
- type: CONTACT_CARD_EMBEDDABLE,
- explicitInput: { test: 'bye', id: '987' },
- },
- panels
- );
- expect(panelState.gridData.x).toBe(DEFAULT_PANEL_WIDTH);
- expect(panelState.gridData.y).toBe(0);
- expect(panelState.gridData.h).toBe(DEFAULT_PANEL_HEIGHT);
- expect(panelState.gridData.w).toBe(DEFAULT_PANEL_WIDTH);
-});
diff --git a/src/plugins/dashboard/public/dashboard_container/component/panel/create_panel_state.ts b/src/plugins/dashboard/public/dashboard_container/component/panel/create_panel_state.ts
deleted file mode 100644
index 8f060f26cfe51..0000000000000
--- a/src/plugins/dashboard/public/dashboard_container/component/panel/create_panel_state.ts
+++ /dev/null
@@ -1,60 +0,0 @@
-/*
- * 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 { PanelState, EmbeddableInput } from '@kbn/embeddable-plugin/public';
-
-import {
- IPanelPlacementArgs,
- findTopLeftMostOpenSpace,
- PanelPlacementMethod,
-} from './dashboard_panel_placement';
-import { DashboardPanelState } from '../../../../common';
-import { DEFAULT_PANEL_HEIGHT, DEFAULT_PANEL_WIDTH } from '../../../dashboard_constants';
-
-/**
- * Creates and initializes a basic panel state.
- */
-export function createPanelState<
- TEmbeddableInput extends EmbeddableInput,
- TPlacementMethodArgs extends IPanelPlacementArgs = IPanelPlacementArgs
->(
- panelState: PanelState,
- currentPanels: { [key: string]: DashboardPanelState },
- placementMethod?: PanelPlacementMethod,
- placementArgs?: TPlacementMethodArgs
-): {
- newPanel: DashboardPanelState;
- otherPanels: { [key: string]: DashboardPanelState };
-} {
- const defaultPlacementArgs = {
- width: DEFAULT_PANEL_WIDTH,
- height: DEFAULT_PANEL_HEIGHT,
- currentPanels,
- };
- const finalPlacementArgs = placementArgs
- ? {
- ...defaultPlacementArgs,
- ...placementArgs,
- }
- : defaultPlacementArgs;
-
- const { newPanelPlacement, otherPanels } = placementMethod
- ? placementMethod(finalPlacementArgs as TPlacementMethodArgs)
- : findTopLeftMostOpenSpace(defaultPlacementArgs);
-
- return {
- newPanel: {
- gridData: {
- ...newPanelPlacement,
- i: panelState.explicitInput.id,
- },
- ...panelState,
- },
- otherPanels,
- };
-}
diff --git a/src/plugins/dashboard/public/dashboard_container/component/panel/index.ts b/src/plugins/dashboard/public/dashboard_container/component/panel_placement/index.ts
similarity index 76%
rename from src/plugins/dashboard/public/dashboard_container/component/panel/index.ts
rename to src/plugins/dashboard/public/dashboard_container/component/panel_placement/index.ts
index 015b31ed725d9..8e7444712c281 100644
--- a/src/plugins/dashboard/public/dashboard_container/component/panel/index.ts
+++ b/src/plugins/dashboard/public/dashboard_container/component/panel_placement/index.ts
@@ -6,4 +6,6 @@
* Side Public License, v 1.
*/
-export { createPanelState } from './create_panel_state';
+export { placePanel } from './place_panel';
+
+export { placeClonePanel } from './place_clone_panel_strategy';
diff --git a/src/plugins/dashboard/public/dashboard_container/component/panel/dashboard_panel_placement.ts b/src/plugins/dashboard/public/dashboard_container/component/panel_placement/place_clone_panel_strategy.ts
similarity index 53%
rename from src/plugins/dashboard/public/dashboard_container/component/panel/dashboard_panel_placement.ts
rename to src/plugins/dashboard/public/dashboard_container/component/panel_placement/place_clone_panel_strategy.ts
index 829b26072f0d9..affe85dff5d26 100644
--- a/src/plugins/dashboard/public/dashboard_container/component/panel/dashboard_panel_placement.ts
+++ b/src/plugins/dashboard/public/dashboard_container/component/panel_placement/place_clone_panel_strategy.ts
@@ -6,103 +6,14 @@
* Side Public License, v 1.
*/
-import _ from 'lodash';
+import { cloneDeep, forOwn } from 'lodash';
import { PanelNotFoundError } from '@kbn/embeddable-plugin/public';
+
import { DashboardPanelState } from '../../../../common';
import { GridData } from '../../../../common/content_management';
+import { PanelPlacementProps, PanelPlacementReturn } from './types';
import { DASHBOARD_GRID_COLUMN_COUNT } from '../../../dashboard_constants';
-export type PanelPlacementMethod = (
- args: PlacementArgs
-) => PanelPlacementMethodReturn;
-
-interface PanelPlacementMethodReturn {
- newPanelPlacement: Omit;
- otherPanels: { [key: string]: DashboardPanelState };
-}
-
-export interface IPanelPlacementArgs {
- width: number;
- height: number;
- currentPanels: { [key: string]: DashboardPanelState };
- scrollToPanel?: boolean;
-}
-
-export interface IPanelPlacementBesideArgs extends IPanelPlacementArgs {
- placeBesideId: string;
-}
-
-// Look for the smallest y and x value where the default panel will fit.
-export function findTopLeftMostOpenSpace({
- width,
- height,
- currentPanels,
-}: IPanelPlacementArgs): PanelPlacementMethodReturn {
- let maxY = -1;
-
- const currentPanelsArray = Object.values(currentPanels);
- currentPanelsArray.forEach((panel) => {
- maxY = Math.max(panel.gridData.y + panel.gridData.h, maxY);
- });
-
- // Handle case of empty grid.
- if (maxY < 0) {
- return { newPanelPlacement: { x: 0, y: 0, w: width, h: height }, otherPanels: currentPanels };
- }
-
- const grid = new Array(maxY);
- for (let y = 0; y < maxY; y++) {
- grid[y] = new Array(DASHBOARD_GRID_COLUMN_COUNT).fill(0);
- }
-
- currentPanelsArray.forEach((panel) => {
- for (let x = panel.gridData.x; x < panel.gridData.x + panel.gridData.w; x++) {
- for (let y = panel.gridData.y; y < panel.gridData.y + panel.gridData.h; y++) {
- const row = grid[y];
- if (row === undefined) {
- throw new Error(
- `Attempted to access a row that doesn't exist at ${y} for panel ${JSON.stringify(
- panel
- )}`
- );
- }
- grid[y][x] = 1;
- }
- }
- });
-
- for (let y = 0; y < maxY; y++) {
- for (let x = 0; x < DASHBOARD_GRID_COLUMN_COUNT; x++) {
- if (grid[y][x] === 1) {
- // Space is filled
- continue;
- } else {
- for (let h = y; h < Math.min(y + height, maxY); h++) {
- for (let w = x; w < Math.min(x + width, DASHBOARD_GRID_COLUMN_COUNT); w++) {
- const spaceIsEmpty = grid[h][w] === 0;
- const fitsPanelWidth = w === x + width - 1;
- // If the panel is taller than any other panel in the current grid, it can still fit in the space, hence
- // we check the minimum of maxY and the panel height.
- const fitsPanelHeight = h === Math.min(y + height - 1, maxY - 1);
-
- if (spaceIsEmpty && fitsPanelWidth && fitsPanelHeight) {
- // Found space
- return {
- newPanelPlacement: { x, y, w: width, h: height },
- otherPanels: currentPanels,
- };
- } else if (grid[h][w] === 1) {
- // x, y spot doesn't work, break.
- break;
- }
- }
- }
- }
- }
- }
- return { newPanelPlacement: { x: 0, y: maxY, w: width, h: height }, otherPanels: currentPanels };
-}
-
interface IplacementDirection {
grid: Omit;
fits: boolean;
@@ -128,19 +39,19 @@ function comparePanels(a: GridData, b: GridData): number {
return 1;
}
-export function placePanelBeside({
+export function placeClonePanel({
width,
height,
currentPanels,
placeBesideId,
-}: IPanelPlacementBesideArgs): PanelPlacementMethodReturn {
+}: PanelPlacementProps & { placeBesideId: string }): PanelPlacementReturn {
const panelToPlaceBeside = currentPanels[placeBesideId];
if (!panelToPlaceBeside) {
throw new PanelNotFoundError();
}
const beside = panelToPlaceBeside.gridData;
const otherPanelGridData: GridData[] = [];
- _.forOwn(currentPanels, (panel: DashboardPanelState, key: string | undefined) => {
+ forOwn(currentPanels, (panel: DashboardPanelState, key: string | undefined) => {
otherPanelGridData.push(panel.gridData);
});
@@ -197,7 +108,7 @@ export function placePanelBeside({
for (let j = position + 1; j < grid.length; j++) {
originalPositionInTheGrid = grid[j].i;
- const movedPanel = _.cloneDeep(otherPanels[originalPositionInTheGrid]);
+ const movedPanel = cloneDeep(otherPanels[originalPositionInTheGrid]);
movedPanel.gridData.y = movedPanel.gridData.y + diff;
otherPanels[originalPositionInTheGrid] = movedPanel;
}
diff --git a/src/plugins/dashboard/public/dashboard_container/component/panel_placement/place_new_panel_strategies.ts b/src/plugins/dashboard/public/dashboard_container/component/panel_placement/place_new_panel_strategies.ts
new file mode 100644
index 0000000000000..626a68b433a6a
--- /dev/null
+++ b/src/plugins/dashboard/public/dashboard_container/component/panel_placement/place_new_panel_strategies.ts
@@ -0,0 +1,105 @@
+/*
+ * 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 { cloneDeep } from 'lodash';
+import { DASHBOARD_GRID_COLUMN_COUNT } from '../../../dashboard_constants';
+import { PanelPlacementProps, PanelPlacementReturn } from './types';
+
+export const panelPlacementStrategies = {
+ // Place on the very top of the Dashboard, add the height of this panel to all other panels.
+ placeAtTop: ({ width, height, currentPanels }: PanelPlacementProps): PanelPlacementReturn => {
+ const otherPanels = { ...currentPanels };
+ for (const [id, panel] of Object.entries(currentPanels)) {
+ const currentPanel = cloneDeep(panel);
+ console.log('MOVING PANEL', currentPanel.explicitInput.title);
+ currentPanel.gridData.y = currentPanel.gridData.y + height;
+ otherPanels[id] = currentPanel;
+ }
+ return {
+ newPanelPlacement: { x: 0, y: 0, w: width, h: height },
+ otherPanels,
+ };
+ },
+
+ // Look for the smallest y and x value where the default panel will fit.
+ findTopLeftMostOpenSpace: ({
+ width,
+ height,
+ currentPanels,
+ }: PanelPlacementProps): PanelPlacementReturn => {
+ let maxY = -1;
+
+ const currentPanelsArray = Object.values(currentPanels);
+ currentPanelsArray.forEach((panel) => {
+ maxY = Math.max(panel.gridData.y + panel.gridData.h, maxY);
+ });
+
+ // Handle case of empty grid.
+ if (maxY < 0) {
+ return {
+ newPanelPlacement: { x: 0, y: 0, w: width, h: height },
+ otherPanels: currentPanels,
+ };
+ }
+
+ const grid = new Array(maxY);
+ for (let y = 0; y < maxY; y++) {
+ grid[y] = new Array(DASHBOARD_GRID_COLUMN_COUNT).fill(0);
+ }
+
+ currentPanelsArray.forEach((panel) => {
+ for (let x = panel.gridData.x; x < panel.gridData.x + panel.gridData.w; x++) {
+ for (let y = panel.gridData.y; y < panel.gridData.y + panel.gridData.h; y++) {
+ const row = grid[y];
+ if (row === undefined) {
+ throw new Error(
+ `Attempted to access a row that doesn't exist at ${y} for panel ${JSON.stringify(
+ panel
+ )}`
+ );
+ }
+ grid[y][x] = 1;
+ }
+ }
+ });
+
+ for (let y = 0; y < maxY; y++) {
+ for (let x = 0; x < DASHBOARD_GRID_COLUMN_COUNT; x++) {
+ if (grid[y][x] === 1) {
+ // Space is filled
+ continue;
+ } else {
+ for (let h = y; h < Math.min(y + height, maxY); h++) {
+ for (let w = x; w < Math.min(x + width, DASHBOARD_GRID_COLUMN_COUNT); w++) {
+ const spaceIsEmpty = grid[h][w] === 0;
+ const fitsPanelWidth = w === x + width - 1;
+ // If the panel is taller than any other panel in the current grid, it can still fit in the space, hence
+ // we check the minimum of maxY and the panel height.
+ const fitsPanelHeight = h === Math.min(y + height - 1, maxY - 1);
+
+ if (spaceIsEmpty && fitsPanelWidth && fitsPanelHeight) {
+ // Found space
+ return {
+ newPanelPlacement: { x, y, w: width, h: height },
+ otherPanels: currentPanels,
+ };
+ } else if (grid[h][w] === 1) {
+ // x, y spot doesn't work, break.
+ break;
+ }
+ }
+ }
+ }
+ }
+ }
+ return {
+ newPanelPlacement: { x: 0, y: maxY, w: width, h: height },
+ otherPanels: currentPanels,
+ };
+ },
+} as const;
diff --git a/src/plugins/dashboard/public/dashboard_container/component/panel_placement/place_panel.test.ts b/src/plugins/dashboard/public/dashboard_container/component/panel_placement/place_panel.test.ts
new file mode 100644
index 0000000000000..24023ba92dbce
--- /dev/null
+++ b/src/plugins/dashboard/public/dashboard_container/component/panel_placement/place_panel.test.ts
@@ -0,0 +1,167 @@
+/*
+ * 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 { DashboardPanelState } from '../../../../common';
+import { EmbeddableFactory, EmbeddableInput } from '@kbn/embeddable-plugin/public';
+import { CONTACT_CARD_EMBEDDABLE } from '@kbn/embeddable-plugin/public/lib/test_samples';
+import { DEFAULT_PANEL_HEIGHT, DEFAULT_PANEL_WIDTH } from '../../../dashboard_constants';
+
+import { placePanel } from './place_panel';
+import { IProvidesPanelPlacementSettings } from './types';
+
+interface TestInput extends EmbeddableInput {
+ test: string;
+}
+const panels: { [key: string]: DashboardPanelState } = {};
+
+test('adds a new panel state in 0,0 position', () => {
+ const { newPanel: panelState } = placePanel(
+ {} as unknown as EmbeddableFactory,
+ {
+ type: CONTACT_CARD_EMBEDDABLE,
+ explicitInput: { test: 'hi', id: '123' },
+ },
+ panels
+ );
+ expect(panelState.explicitInput.test).toBe('hi');
+ expect(panelState.type).toBe(CONTACT_CARD_EMBEDDABLE);
+ expect(panelState.explicitInput.id).toBeDefined();
+ expect(panelState.gridData.x).toBe(0);
+ expect(panelState.gridData.y).toBe(0);
+ expect(panelState.gridData.h).toBe(DEFAULT_PANEL_HEIGHT);
+ expect(panelState.gridData.w).toBe(DEFAULT_PANEL_WIDTH);
+
+ panels[panelState.explicitInput.id] = panelState;
+});
+
+test('adds a second new panel state', () => {
+ const { newPanel: panelState } = placePanel(
+ {} as unknown as EmbeddableFactory,
+ { type: CONTACT_CARD_EMBEDDABLE, explicitInput: { test: 'bye', id: '456' } },
+ panels
+ );
+
+ expect(panelState.gridData.x).toBe(DEFAULT_PANEL_WIDTH);
+ expect(panelState.gridData.y).toBe(0);
+ expect(panelState.gridData.h).toBe(DEFAULT_PANEL_HEIGHT);
+ expect(panelState.gridData.w).toBe(DEFAULT_PANEL_WIDTH);
+
+ panels[panelState.explicitInput.id] = panelState;
+});
+
+test('adds a third new panel state', () => {
+ const { newPanel: panelState } = placePanel(
+ {} as unknown as EmbeddableFactory,
+ {
+ type: CONTACT_CARD_EMBEDDABLE,
+ explicitInput: { test: 'bye', id: '789' },
+ },
+ panels
+ );
+ expect(panelState.gridData.x).toBe(0);
+ expect(panelState.gridData.y).toBe(DEFAULT_PANEL_HEIGHT);
+ expect(panelState.gridData.h).toBe(DEFAULT_PANEL_HEIGHT);
+ expect(panelState.gridData.w).toBe(DEFAULT_PANEL_WIDTH);
+
+ panels[panelState.explicitInput.id] = panelState;
+});
+
+test('adds a new panel state in the top most position when it is open', () => {
+ // deleting panel 456 means that the top leftmost open position will be at the top of the Dashboard.
+ delete panels['456'];
+ const { newPanel: panelState } = placePanel(
+ {} as unknown as EmbeddableFactory,
+ {
+ type: CONTACT_CARD_EMBEDDABLE,
+ explicitInput: { test: 'bye', id: '987' },
+ },
+ panels
+ );
+ expect(panelState.gridData.x).toBe(DEFAULT_PANEL_WIDTH);
+ expect(panelState.gridData.y).toBe(0);
+ expect(panelState.gridData.h).toBe(DEFAULT_PANEL_HEIGHT);
+ expect(panelState.gridData.w).toBe(DEFAULT_PANEL_WIDTH);
+
+ // replace the topmost panel.
+ panels[panelState.explicitInput.id] = panelState;
+});
+
+test('adds a new panel state at the very top of the Dashboard with default sizing', () => {
+ const embeddableFactoryStub: IProvidesPanelPlacementSettings = {
+ getPanelPlacementSettings: jest.fn().mockImplementation(() => {
+ return { strategy: 'placeAtTop' };
+ }),
+ };
+
+ const { newPanel: panelState } = placePanel(
+ embeddableFactoryStub as unknown as EmbeddableFactory,
+ {
+ type: CONTACT_CARD_EMBEDDABLE,
+ explicitInput: { test: 'wowee', id: '9001' },
+ },
+ panels
+ );
+ expect(panelState.gridData.x).toBe(0);
+ expect(panelState.gridData.y).toBe(0);
+ expect(panelState.gridData.h).toBe(DEFAULT_PANEL_HEIGHT);
+ expect(panelState.gridData.w).toBe(DEFAULT_PANEL_WIDTH);
+
+ expect(embeddableFactoryStub.getPanelPlacementSettings).toHaveBeenCalledWith(
+ { id: '9001', test: 'wowee' },
+ undefined
+ );
+});
+
+test('adds a new panel state at the very top of the Dashboard with custom sizing', () => {
+ const embeddableFactoryStub: IProvidesPanelPlacementSettings = {
+ getPanelPlacementSettings: jest.fn().mockImplementation(() => {
+ return { strategy: 'placeAtTop', width: 10, height: 5 };
+ }),
+ };
+
+ const { newPanel: panelState } = placePanel(
+ embeddableFactoryStub as unknown as EmbeddableFactory,
+ {
+ type: CONTACT_CARD_EMBEDDABLE,
+ explicitInput: { test: 'woweee', id: '9002' },
+ },
+ panels
+ );
+ expect(panelState.gridData.x).toBe(0);
+ expect(panelState.gridData.y).toBe(0);
+ expect(panelState.gridData.h).toBe(5);
+ expect(panelState.gridData.w).toBe(10);
+
+ expect(embeddableFactoryStub.getPanelPlacementSettings).toHaveBeenCalledWith(
+ { id: '9002', test: 'woweee' },
+ undefined
+ );
+});
+
+test('passes through given attributes', () => {
+ const embeddableFactoryStub: IProvidesPanelPlacementSettings = {
+ getPanelPlacementSettings: jest.fn().mockImplementation(() => {
+ return { strategy: 'placeAtTop', width: 10, height: 5 };
+ }),
+ };
+
+ placePanel(
+ embeddableFactoryStub as unknown as EmbeddableFactory,
+ {
+ type: CONTACT_CARD_EMBEDDABLE,
+ explicitInput: { test: 'wow', id: '9004' },
+ },
+ panels,
+ { testAttr: 'hello' }
+ );
+
+ expect(embeddableFactoryStub.getPanelPlacementSettings).toHaveBeenCalledWith(
+ { id: '9004', test: 'wow' },
+ { testAttr: 'hello' }
+ );
+});
diff --git a/src/plugins/dashboard/public/dashboard_container/component/panel_placement/place_panel.ts b/src/plugins/dashboard/public/dashboard_container/component/panel_placement/place_panel.ts
new file mode 100644
index 0000000000000..a65c4fca9c115
--- /dev/null
+++ b/src/plugins/dashboard/public/dashboard_container/component/panel_placement/place_panel.ts
@@ -0,0 +1,60 @@
+/*
+ * 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 { PanelState, EmbeddableInput, EmbeddableFactory } from '@kbn/embeddable-plugin/public';
+
+import { DashboardPanelState } from '../../../../common';
+import { panelPlacementStrategies } from './place_new_panel_strategies';
+import { IProvidesPanelPlacementSettings, PanelPlacementSettings } from './types';
+import { DEFAULT_PANEL_HEIGHT, DEFAULT_PANEL_WIDTH } from '../../../dashboard_constants';
+
+export const providesPanelPlacementSettings = (
+ value: unknown
+): value is IProvidesPanelPlacementSettings => {
+ return Boolean((value as IProvidesPanelPlacementSettings).getPanelPlacementSettings);
+};
+
+export function placePanel(
+ factory: EmbeddableFactory,
+ newPanel: PanelState,
+ currentPanels: { [key: string]: DashboardPanelState },
+ attributes?: unknown
+): {
+ newPanel: DashboardPanelState;
+ otherPanels: { [key: string]: DashboardPanelState };
+} {
+ let placementSettings: PanelPlacementSettings = {
+ width: DEFAULT_PANEL_WIDTH,
+ height: DEFAULT_PANEL_HEIGHT,
+ strategy: 'findTopLeftMostOpenSpace',
+ };
+ if (providesPanelPlacementSettings(factory)) {
+ placementSettings = {
+ ...placementSettings,
+ ...factory.getPanelPlacementSettings(newPanel.explicitInput, attributes),
+ };
+ }
+ const { width, height, strategy } = placementSettings;
+
+ const { newPanelPlacement, otherPanels } = panelPlacementStrategies[strategy]({
+ currentPanels,
+ height,
+ width,
+ });
+
+ return {
+ newPanel: {
+ gridData: {
+ ...newPanelPlacement,
+ i: newPanel.explicitInput.id,
+ },
+ ...newPanel,
+ },
+ otherPanels,
+ };
+}
diff --git a/src/plugins/dashboard/public/dashboard_container/component/panel_placement/types.ts b/src/plugins/dashboard/public/dashboard_container/component/panel_placement/types.ts
new file mode 100644
index 0000000000000..7fb20b469c1a9
--- /dev/null
+++ b/src/plugins/dashboard/public/dashboard_container/component/panel_placement/types.ts
@@ -0,0 +1,41 @@
+/*
+ * 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 { EmbeddableInput } from '@kbn/embeddable-plugin/public';
+import { DashboardPanelState } from '../../../../common';
+import { GridData } from '../../../../common/content_management';
+import { panelPlacementStrategies } from './place_new_panel_strategies';
+
+export type PanelPlacementStrategy = keyof typeof panelPlacementStrategies;
+
+export interface PanelPlacementSettings {
+ strategy: PanelPlacementStrategy;
+ height: number;
+ width: number;
+}
+
+export interface PanelPlacementReturn {
+ newPanelPlacement: Omit;
+ otherPanels: { [key: string]: DashboardPanelState };
+}
+
+export interface PanelPlacementProps {
+ width: number;
+ height: number;
+ currentPanels: { [key: string]: DashboardPanelState };
+}
+
+export interface IProvidesPanelPlacementSettings<
+ InputType extends EmbeddableInput = EmbeddableInput,
+ AttributesType = unknown
+> {
+ getPanelPlacementSettings: (
+ input: InputType,
+ attributes?: AttributesType
+ ) => Partial;
+}
diff --git a/src/plugins/dashboard/public/dashboard_container/embeddable/dashboard_container.tsx b/src/plugins/dashboard/public/dashboard_container/embeddable/dashboard_container.tsx
index 3027cddd167dd..39611f244568c 100644
--- a/src/plugins/dashboard/public/dashboard_container/embeddable/dashboard_container.tsx
+++ b/src/plugins/dashboard/public/dashboard_container/embeddable/dashboard_container.tsx
@@ -45,7 +45,7 @@ import {
} from './api';
import { DASHBOARD_CONTAINER_TYPE } from '../..';
-import { createPanelState } from '../component/panel';
+import { placePanel } from '../component/panel_placement';
import { pluginServices } from '../../services/plugin_services';
import { initializeDashboard } from './create/create_dashboard';
import { DashboardCreationOptions } from './dashboard_container_factory';
@@ -218,11 +218,14 @@ export class DashboardContainer extends Container
>(
factory: EmbeddableFactory,
- partial: Partial = {}
- ): DashboardPanelState {
- const panelState = super.createNewPanelState(factory, partial);
- const { newPanel } = createPanelState(panelState, this.input.panels);
- return newPanel;
+ partial: Partial = {},
+ attributes?: unknown
+ ): {
+ newPanel: DashboardPanelState;
+ otherPanels: DashboardContainerInput['panels'];
+ } {
+ const { newPanel } = super.createNewPanelState(factory, partial, attributes);
+ return placePanel(factory, newPanel, this.input.panels, attributes);
}
public render(dom: HTMLElement) {
diff --git a/src/plugins/dashboard/public/dashboard_container/state/diffing/dashboard_diffing_utils.ts b/src/plugins/dashboard/public/dashboard_container/state/diffing/dashboard_diffing_utils.ts
index 0b6d2db559b5a..bb0c157017a12 100644
--- a/src/plugins/dashboard/public/dashboard_container/state/diffing/dashboard_diffing_utils.ts
+++ b/src/plugins/dashboard/public/dashboard_container/state/diffing/dashboard_diffing_utils.ts
@@ -56,7 +56,9 @@ export const getPanelLayoutsAreEqual = (
];
for (const key of keys) {
if (key === undefined) continue;
- if (!defaultDiffFunction(originalObj[key], newObj[key])) differences[key] = newObj[key];
+ if (!defaultDiffFunction(originalObj[key], newObj[key])) {
+ differences[key] = newObj[key];
+ }
}
return differences;
};
diff --git a/src/plugins/dashboard/public/index.ts b/src/plugins/dashboard/public/index.ts
index 290f4b7c10f28..6882090df441a 100644
--- a/src/plugins/dashboard/public/index.ts
+++ b/src/plugins/dashboard/public/index.ts
@@ -13,6 +13,7 @@ export {
createDashboardEditUrl,
DASHBOARD_APP_ID,
LEGACY_DASHBOARD_APP_ID,
+ DASHBOARD_GRID_COLUMN_COUNT,
} from './dashboard_constants';
export {
type DashboardAPI,
diff --git a/src/plugins/embeddable/public/add_panel_flyout/add_panel_flyout.tsx b/src/plugins/embeddable/public/add_panel_flyout/add_panel_flyout.tsx
index 07765836057ff..f0554fed61782 100644
--- a/src/plugins/embeddable/public/add_panel_flyout/add_panel_flyout.tsx
+++ b/src/plugins/embeddable/public/add_panel_flyout/add_panel_flyout.tsx
@@ -103,7 +103,8 @@ export const AddPanelFlyout = ({
const embeddable = await container.addNewEmbeddable(
factoryForSavedObjectType.type,
- { savedObjectId: id }
+ { savedObjectId: id },
+ savedObject.attributes
);
onAddPanel?.(embeddable.id);
diff --git a/src/plugins/embeddable/public/embeddable_panel/panel_actions/edit_panel_action/edit_panel_action.ts b/src/plugins/embeddable/public/embeddable_panel/panel_actions/edit_panel_action/edit_panel_action.ts
index cbd2e5208e458..ddd9082d6b685 100644
--- a/src/plugins/embeddable/public/embeddable_panel/panel_actions/edit_panel_action/edit_panel_action.ts
+++ b/src/plugins/embeddable/public/embeddable_panel/panel_actions/edit_panel_action/edit_panel_action.ts
@@ -18,6 +18,7 @@ import {
EmbeddableInput,
EmbeddableEditorState,
EmbeddableStateTransfer,
+ isExplicitInputWithAttributes,
} from '../../../lib';
import { ViewMode } from '../../../lib/types';
import { EmbeddableStart } from '../../../plugin';
@@ -94,9 +95,15 @@ export class EditPanelAction implements Action {
}
const oldExplicitInput = embeddable.getExplicitInput();
- let newExplicitInput: Awaited>;
+ let newExplicitInput: Partial;
try {
- newExplicitInput = await factory.getExplicitInput(oldExplicitInput, embeddable.parent);
+ const explicitInputReturn = await factory.getExplicitInput(
+ oldExplicitInput,
+ embeddable.parent
+ );
+ newExplicitInput = isExplicitInputWithAttributes(explicitInputReturn)
+ ? explicitInputReturn.newInput
+ : explicitInputReturn;
} catch (e) {
// error likely means user canceled editing
return;
diff --git a/src/plugins/embeddable/public/index.ts b/src/plugins/embeddable/public/index.ts
index 91e6efcdc41c8..0e3650ea8a8a4 100644
--- a/src/plugins/embeddable/public/index.ts
+++ b/src/plugins/embeddable/public/index.ts
@@ -76,6 +76,7 @@ export {
EmbeddableRenderer,
useEmbeddableFactory,
isFilterableEmbeddable,
+ isExplicitInputWithAttributes,
shouldFetch$,
shouldRefreshFilterCompareOptions,
PANEL_HOVER_TRIGGER,
diff --git a/src/plugins/embeddable/public/lib/containers/container.ts b/src/plugins/embeddable/public/lib/containers/container.ts
index eedf083561996..546c9a9a9bf7f 100644
--- a/src/plugins/embeddable/public/lib/containers/container.ts
+++ b/src/plugins/embeddable/public/lib/containers/container.ts
@@ -160,16 +160,20 @@ export abstract class Container<
EEI extends EmbeddableInput = EmbeddableInput,
EEO extends EmbeddableOutput = EmbeddableOutput,
E extends IEmbeddable = IEmbeddable
- >(type: string, explicitInput: Partial): Promise {
+ >(type: string, explicitInput: Partial, attributes?: unknown): Promise {
const factory = this.getFactory(type) as EmbeddableFactory | undefined;
if (!factory) {
throw new EmbeddableFactoryNotFoundError(type);
}
- const panelState = this.createNewPanelState(factory, explicitInput);
+ const { newPanel, otherPanels } = this.createNewPanelState(
+ factory,
+ explicitInput,
+ attributes
+ );
- return this.createAndSaveEmbeddable(type, panelState);
+ return this.createAndSaveEmbeddable(type, newPanel, otherPanels);
}
public async replaceEmbeddable<
@@ -342,8 +346,9 @@ export abstract class Container<
TEmbeddable extends IEmbeddable
>(
factory: EmbeddableFactory,
- partial: Partial = {}
- ): PanelState {
+ partial: Partial = {},
+ attributes?: unknown
+ ): { newPanel: PanelState; otherPanels: TContainerInput['panels'] } {
const embeddableId = partial.id || uuidv4();
const explicitInput = this.createNewExplicitEmbeddableInput(
@@ -353,12 +358,15 @@ export abstract class Container<
);
return {
- type: factory.type,
- explicitInput: {
- ...explicitInput,
- id: embeddableId,
- version: factory.latestVersion,
- } as TEmbeddableInput,
+ newPanel: {
+ type: factory.type,
+ explicitInput: {
+ ...explicitInput,
+ id: embeddableId,
+ version: factory.latestVersion,
+ } as TEmbeddableInput,
+ },
+ otherPanels: this.getInput().panels,
};
}
@@ -412,10 +420,10 @@ export abstract class Container<
protected async createAndSaveEmbeddable<
TEmbeddableInput extends EmbeddableInput = EmbeddableInput,
TEmbeddable extends IEmbeddable = IEmbeddable
- >(type: string, panelState: PanelState) {
+ >(type: string, panelState: PanelState, otherPanels: TContainerInput['panels']) {
this.updateInput({
panels: {
- ...this.input.panels,
+ ...otherPanels,
[panelState.explicitInput.id]: panelState,
},
} as Partial);
diff --git a/src/plugins/embeddable/public/lib/containers/i_container.ts b/src/plugins/embeddable/public/lib/containers/i_container.ts
index 34e7cc0593e64..53226e7d15146 100644
--- a/src/plugins/embeddable/public/lib/containers/i_container.ts
+++ b/src/plugins/embeddable/public/lib/containers/i_container.ts
@@ -96,7 +96,8 @@ export interface IContainer<
E extends Embeddable = Embeddable
>(
type: string,
- explicitInput: Partial
+ explicitInput: Partial,
+ attributes?: unknown
): Promise;
replaceEmbeddable<
diff --git a/src/plugins/embeddable/public/lib/embeddables/default_embeddable_factory_provider.ts b/src/plugins/embeddable/public/lib/embeddables/default_embeddable_factory_provider.ts
index 472840208e139..50555601d4bca 100644
--- a/src/plugins/embeddable/public/lib/embeddables/default_embeddable_factory_provider.ts
+++ b/src/plugins/embeddable/public/lib/embeddables/default_embeddable_factory_provider.ts
@@ -30,6 +30,7 @@ export const defaultEmbeddableFactoryProvider = <
}
const factory: EmbeddableFactory = {
+ ...def,
latestVersion: def.latestVersion,
isContainerType: def.isContainerType ?? false,
canCreateNew: def.canCreateNew ? def.canCreateNew.bind(def) : () => true,
diff --git a/src/plugins/embeddable/public/lib/embeddables/embeddable_factory.ts b/src/plugins/embeddable/public/lib/embeddables/embeddable_factory.ts
index 5285786056468..a96287a61d0f3 100644
--- a/src/plugins/embeddable/public/lib/embeddables/embeddable_factory.ts
+++ b/src/plugins/embeddable/public/lib/embeddables/embeddable_factory.ts
@@ -24,6 +24,17 @@ export interface OutputSpec {
[key: string]: PropertySpec;
}
+export interface ExplicitInputWithAttributes {
+ newInput: Partial;
+ attributes?: unknown;
+}
+
+export const isExplicitInputWithAttributes = (
+ value: ExplicitInputWithAttributes | Partial
+): value is ExplicitInputWithAttributes => {
+ return Boolean((value as ExplicitInputWithAttributes).newInput);
+};
+
/**
* EmbeddableFactories create and initialize an embeddable instance
*/
@@ -106,11 +117,14 @@ export interface EmbeddableFactory<
* input passed down from the parent container.
*
* Can be used to edit an embeddable by re-requesting explicit input. Initial input can be provided to allow the editor to show the current state.
+ *
+ * If saved object information is needed for creation use-cases, getExplicitInput can also return an unknown typed attributes object which will be passed
+ * into the container's addNewEmbeddable function.
*/
getExplicitInput(
initialInput?: Partial,
parent?: IContainer
- ): Promise>;
+ ): Promise | ExplicitInputWithAttributes>;
/**
* Creates a new embeddable instance based off the saved object id.
diff --git a/src/plugins/navigation_embeddable/public/editor/open_editor_flyout.tsx b/src/plugins/navigation_embeddable/public/editor/open_editor_flyout.tsx
index 14e3974473d94..e51606927a912 100644
--- a/src/plugins/navigation_embeddable/public/editor/open_editor_flyout.tsx
+++ b/src/plugins/navigation_embeddable/public/editor/open_editor_flyout.tsx
@@ -18,6 +18,7 @@ import { DashboardContainer } from '@kbn/dashboard-plugin/public/dashboard_conta
import {
NavigationEmbeddableInput,
NavigationEmbeddableByReferenceInput,
+ NavigationEmbeddableEditorFlyoutReturn,
} from '../embeddable/types';
import { coreServices } from '../services/kibana_services';
import { runSaveToLibrary } from '../content_management/save_to_library';
@@ -41,7 +42,7 @@ const NavigationEmbeddablePanelEditor = withSuspense(
export async function openEditorFlyout(
initialInput: NavigationEmbeddableInput,
parentDashboard?: DashboardContainer
-): Promise> {
+): Promise {
const attributeService = getNavigationEmbeddableAttributeService();
const { attributes } = await attributeService.unwrapAttributes(initialInput);
const isByReference = attributeService.inputIsRefType(initialInput);
@@ -64,7 +65,12 @@ export async function openEditorFlyout(
if (!updatedInput) {
return;
}
- resolve(updatedInput);
+ resolve({
+ newInput: updatedInput,
+
+ // pass attributes via attributes so that the Dashboard can choose the right panel size.
+ attributes: newAttributes,
+ });
parentDashboard?.reload();
editorFlyout.close();
};
@@ -73,15 +79,21 @@ export async function openEditorFlyout(
newLinks: NavigationEmbeddableLink[],
newLayout: NavigationLayoutType
) => {
+ const newAttributes = {
+ ...attributes,
+ links: newLinks,
+ layout: newLayout,
+ };
const newInput: NavigationEmbeddableInput = {
...initialInput,
- attributes: {
- ...attributes,
- links: newLinks,
- layout: newLayout,
- },
+ attributes: newAttributes,
};
- resolve(newInput);
+ resolve({
+ newInput,
+
+ // pass attributes so that the Dashboard can choose the right panel size.
+ attributes: newAttributes,
+ });
parentDashboard?.reload();
editorFlyout.close();
};
diff --git a/src/plugins/navigation_embeddable/public/embeddable/navigation_embeddable_factory.test.ts b/src/plugins/navigation_embeddable/public/embeddable/navigation_embeddable_factory.test.ts
new file mode 100644
index 0000000000000..c1dc4deee581f
--- /dev/null
+++ b/src/plugins/navigation_embeddable/public/embeddable/navigation_embeddable_factory.test.ts
@@ -0,0 +1,52 @@
+/*
+ * 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 { NavigationEmbeddableFactoryDefinition } from './navigation_embeddable_factory';
+import { NavigationEmbeddableInput } from './types';
+
+describe('navigationEmbeddableFactory', () => {
+ test('returns an empty object when not given proper meta information', () => {
+ const navigationEmbeddableFactory = new NavigationEmbeddableFactoryDefinition();
+ const settings = navigationEmbeddableFactory.getPanelPlacementSettings(
+ {} as unknown as NavigationEmbeddableInput,
+ {}
+ );
+ expect(settings.height).toBeUndefined();
+ expect(settings.width).toBeUndefined();
+ expect(settings.strategy).toBeUndefined();
+ });
+
+ test('returns a horizontal layout', () => {
+ const navigationEmbeddableFactory = new NavigationEmbeddableFactoryDefinition();
+ const settings = navigationEmbeddableFactory.getPanelPlacementSettings(
+ {} as unknown as NavigationEmbeddableInput,
+ { layout: 'horizontal', links: [] }
+ );
+ expect(settings.height).toBe(4);
+ expect(settings.width).toBe(48);
+ expect(settings.strategy).toBe('placeAtTop');
+ });
+
+ test('returns a vertical layout with the appropriate height', () => {
+ const navigationEmbeddableFactory = new NavigationEmbeddableFactoryDefinition();
+ const settings = navigationEmbeddableFactory.getPanelPlacementSettings(
+ {} as unknown as NavigationEmbeddableInput,
+ {
+ layout: 'vertical',
+ links: [
+ { type: 'dashboardLink', destination: 'superDashboard1' },
+ { type: 'dashboardLink', destination: 'superDashboard2' },
+ { type: 'dashboardLink', destination: 'superDashboard3' },
+ ],
+ }
+ );
+ expect(settings.height).toBe(7); // 4 base plus 3 for each link.
+ expect(settings.width).toBe(8);
+ expect(settings.strategy).toBe('placeAtTop');
+ });
+});
diff --git a/src/plugins/navigation_embeddable/public/embeddable/navigation_embeddable_factory.ts b/src/plugins/navigation_embeddable/public/embeddable/navigation_embeddable_factory.ts
index 21d5b1d93cb31..8f71d1fcbfaa6 100644
--- a/src/plugins/navigation_embeddable/public/embeddable/navigation_embeddable_factory.ts
+++ b/src/plugins/navigation_embeddable/public/embeddable/navigation_embeddable_factory.ts
@@ -14,13 +14,27 @@ import {
import { lazyLoadReduxToolsPackage } from '@kbn/presentation-util-plugin/public';
import { DashboardContainer } from '@kbn/dashboard-plugin/public/dashboard_container';
-import { NavigationEmbeddableByReferenceInput, NavigationEmbeddableInput } from './types';
+import { IProvidesPanelPlacementSettings } from '@kbn/dashboard-plugin/public/dashboard_container/component/panel_placement/types';
+import { EmbeddableStateWithType } from '@kbn/embeddable-plugin/common';
+import {
+ MigrateFunctionsObject,
+ GetMigrationFunctionObjectFn,
+} from '@kbn/kibana-utils-plugin/common';
+import { UiActionsPresentableGrouping } from '@kbn/ui-actions-plugin/public';
+import { DASHBOARD_GRID_COLUMN_COUNT } from '@kbn/dashboard-plugin/public';
+import {
+ NavigationEmbeddableByReferenceInput,
+ NavigationEmbeddableEditorFlyoutReturn,
+ NavigationEmbeddableInput,
+} from './types';
import { APP_ICON, APP_NAME, CONTENT_ID } from '../../common';
import type { NavigationEmbeddable } from './navigation_embeddable';
import { getNavigationEmbeddableAttributeService } from '../services/attribute_service';
import { coreServices, untilPluginStartServicesReady } from '../services/kibana_services';
import { extract, inject } from '../../common/embeddable';
+import { NavigationEmbeddableAttributes } from '../../common/content_management';
+
export type NavigationEmbeddableFactory = EmbeddableFactory;
// TODO: Replace string 'OPEN_FLYOUT_ADD_DRILLDOWN' with constant once the dashboardEnhanced plugin is removed
@@ -29,9 +43,29 @@ const getDefaultNavigationEmbeddableInput = (): Partial {
+ return (
+ attributes !== undefined &&
+ Boolean(
+ (attributes as NavigationEmbeddableAttributes).layout ||
+ (attributes as NavigationEmbeddableAttributes).links
+ )
+ );
+};
+
export class NavigationEmbeddableFactoryDefinition
- implements EmbeddableFactoryDefinition
+ implements
+ EmbeddableFactoryDefinition,
+ IProvidesPanelPlacementSettings
{
+ latestVersion?: string | undefined;
+ telemetry?:
+ | ((state: EmbeddableStateWithType, stats: Record) => Record)
+ | undefined;
+ migrations?: MigrateFunctionsObject | GetMigrationFunctionObjectFn | undefined;
+ grouping?: UiActionsPresentableGrouping | undefined;
public readonly type = CONTENT_ID;
public readonly isContainerType = false;
@@ -42,6 +76,21 @@ export class NavigationEmbeddableFactoryDefinition
getIconForSavedObject: () => APP_ICON,
};
+ public getPanelPlacementSettings: IProvidesPanelPlacementSettings<
+ NavigationEmbeddableInput,
+ NavigationEmbeddableAttributes | unknown
+ >['getPanelPlacementSettings'] = (input, attributes) => {
+ if (!isNavigationEmbeddableAttributes(attributes) || !attributes.layout) {
+ // if we have no information about the layout of this nav embeddable defer to default panel size and placement.
+ return {};
+ }
+
+ const isHorizontal = attributes.layout === 'horizontal';
+ const width = isHorizontal ? DASHBOARD_GRID_COLUMN_COUNT : 8;
+ const height = isHorizontal ? 4 : (attributes.links?.length ?? 1 * 3) + 4;
+ return { width, height, strategy: 'placeAtTop' };
+ };
+
public async isEditable() {
await untilPluginStartServicesReady();
return Boolean(coreServices.application.capabilities.dashboard?.showWriteControls);
@@ -85,12 +134,12 @@ export class NavigationEmbeddableFactoryDefinition
public async getExplicitInput(
initialInput: NavigationEmbeddableInput,
parent?: DashboardContainer
- ): Promise> {
- if (!parent) return {};
+ ): Promise {
+ if (!parent) return { newInput: {} };
const { openEditorFlyout } = await import('../editor/open_editor_flyout');
- const input = await openEditorFlyout(
+ const { newInput, attributes } = await openEditorFlyout(
{
...getDefaultNavigationEmbeddableInput(),
...initialInput,
@@ -98,7 +147,7 @@ export class NavigationEmbeddableFactoryDefinition
parent
);
- return input;
+ return { newInput, attributes };
}
public getDisplayName() {
diff --git a/src/plugins/navigation_embeddable/public/embeddable/types.ts b/src/plugins/navigation_embeddable/public/embeddable/types.ts
index 8744dd613c4b5..2804712da504d 100644
--- a/src/plugins/navigation_embeddable/public/embeddable/types.ts
+++ b/src/plugins/navigation_embeddable/public/embeddable/types.ts
@@ -65,6 +65,11 @@ export const NavigationLinkInfo: {
},
};
+export interface NavigationEmbeddableEditorFlyoutReturn {
+ attributes?: unknown;
+ newInput: Partial;
+}
+
export type NavigationEmbeddableByValueInput = {
attributes: NavigationEmbeddableAttributes;
} & EmbeddableInput;
diff --git a/src/plugins/navigation_embeddable/tsconfig.json b/src/plugins/navigation_embeddable/tsconfig.json
index 72001ee53bef8..e5c172f210048 100644
--- a/src/plugins/navigation_embeddable/tsconfig.json
+++ b/src/plugins/navigation_embeddable/tsconfig.json
@@ -24,7 +24,8 @@
"@kbn/es-query",
"@kbn/share-plugin",
"@kbn/kibana-utils-plugin",
- "@kbn/utility-types"
+ "@kbn/utility-types",
+ "@kbn/ui-actions-plugin"
],
"exclude": ["target/**/*"]
}
From 657d4249797e5417a509e855976e0962b49ddffc Mon Sep 17 00:00:00 2001
From: Hannah Mudge
Date: Mon, 18 Sep 2023 10:18:52 -0600
Subject: [PATCH 16/53] Improve empty state
---
.../navigation_embeddable_panel_editor.tsx | 51 ++++++++++---------
...n_embeddable_panel_editor_empty_prompt.tsx | 30 ++---------
...avigation_embeddable_panel_editor_link.tsx | 2 +-
.../navigation_embeddable_strings.ts | 6 ++-
.../navigation_embeddable_factory.ts | 12 +++--
5 files changed, 44 insertions(+), 57 deletions(-)
diff --git a/src/plugins/navigation_embeddable/public/components/editor/navigation_embeddable_panel_editor.tsx b/src/plugins/navigation_embeddable/public/components/editor/navigation_embeddable_panel_editor.tsx
index da59d720145bd..7c8299310c023 100644
--- a/src/plugins/navigation_embeddable/public/components/editor/navigation_embeddable_panel_editor.tsx
+++ b/src/plugins/navigation_embeddable/public/components/editor/navigation_embeddable_panel_editor.tsx
@@ -124,13 +124,16 @@ const NavigationEmbeddablePanelEditor = ({
setOrderedLinks(
orderedLinks.map((link) => {
if (link.id === linkToEdit.id) {
- return { ...newLink, order: linkToEdit.order };
+ return { ...newLink, order: linkToEdit.order } as NavigationEmbeddableLink;
}
return link;
})
);
} else {
- setOrderedLinks([...orderedLinks, { ...newLink, order: orderedLinks.length }]);
+ setOrderedLinks([
+ ...orderedLinks,
+ { ...newLink, order: orderedLinks.length } as NavigationEmbeddableLink,
+ ]);
}
}
},
@@ -166,25 +169,25 @@ const NavigationEmbeddablePanelEditor = ({
- {hasZeroLinks ? (
- addOrEditLink()} />
- ) : (
- <>
-
- {
- setCurrentLayout(id as NavigationLayoutType);
- }}
- legend={NavEmbeddableStrings.editor.panelEditor.getLayoutSettingsLegend()}
- />
-
-
- {/* Needs to be surrounded by a div rather than a fragment so the EuiFormRow can respond
+
+ {
+ setCurrentLayout(id as NavigationLayoutType);
+ }}
+ legend={NavEmbeddableStrings.editor.panelEditor.getLayoutSettingsLegend()}
+ />
+
+
+ {/* Needs to be surrounded by a div rather than a fragment so the EuiFormRow can respond
to the focus of the inner elements */}
-
+
+ {hasZeroLinks ? (
+ addOrEditLink()} />
+ ) : (
+ <>
{NavEmbeddableStrings.editor.getAddButtonLabel()}
-
-
- >
- )}
+ >
+ )}
+
+
diff --git a/src/plugins/navigation_embeddable/public/components/editor/navigation_embeddable_panel_editor_empty_prompt.tsx b/src/plugins/navigation_embeddable/public/components/editor/navigation_embeddable_panel_editor_empty_prompt.tsx
index 71083dbf87449..1e4d1a30589f8 100644
--- a/src/plugins/navigation_embeddable/public/components/editor/navigation_embeddable_panel_editor_empty_prompt.tsx
+++ b/src/plugins/navigation_embeddable/public/components/editor/navigation_embeddable_panel_editor_empty_prompt.tsx
@@ -7,47 +7,23 @@
*/
import React from 'react';
-import useObservable from 'react-use/lib/useObservable';
-import {
- EuiText,
- EuiImage,
- EuiPanel,
- EuiSpacer,
- EuiButton,
- EuiEmptyPrompt,
- EuiFormRow,
-} from '@elastic/eui';
+import { EuiText, EuiPanel, EuiSpacer, EuiButton, EuiEmptyPrompt, EuiFormRow } from '@elastic/eui';
-import { coreServices } from '../../services/kibana_services';
import { NavEmbeddableStrings } from '../navigation_embeddable_strings';
-import noLinksIllustrationDark from '../../assets/empty_links_dark.svg';
-import noLinksIllustrationLight from '../../assets/empty_links_light.svg';
-
-import './navigation_embeddable_editor.scss';
-
export const NavigationEmbeddablePanelEditorEmptyPrompt = ({
addLink,
}: {
addLink: () => Promise;
}) => {
- const isDarkTheme = useObservable(coreServices.theme.theme$)?.darkMode;
-
return (
- }
+ hasShadow={false}
+ paddingSize="none"
body={
<>
diff --git a/src/plugins/navigation_embeddable/public/components/editor/navigation_embeddable_panel_editor_link.tsx b/src/plugins/navigation_embeddable/public/components/editor/navigation_embeddable_panel_editor_link.tsx
index f73aec1104653..39f985a23e20c 100644
--- a/src/plugins/navigation_embeddable/public/components/editor/navigation_embeddable_panel_editor_link.tsx
+++ b/src/plugins/navigation_embeddable/public/components/editor/navigation_embeddable_panel_editor_link.tsx
@@ -49,7 +49,7 @@ export const NavigationEmbeddablePanelEditorLink = ({
const { value: linkLabel, loading: linkLabelLoading } = useAsync(async () => {
if (!link.destination) {
- setDestinationError(DashboardLinkStrings.getDashboardErrorLabel());
+ setDestinationError(new Error(DashboardLinkStrings.getDashboardErrorLabel()));
return;
}
diff --git a/src/plugins/navigation_embeddable/public/components/navigation_embeddable_strings.ts b/src/plugins/navigation_embeddable/public/components/navigation_embeddable_strings.ts
index efeee7235a89e..7723eb1e5d8c0 100644
--- a/src/plugins/navigation_embeddable/public/components/navigation_embeddable_strings.ts
+++ b/src/plugins/navigation_embeddable/public/components/navigation_embeddable_strings.ts
@@ -9,6 +9,10 @@
import { i18n } from '@kbn/i18n';
export const NavEmbeddableStrings = {
+ getDescription: () =>
+ i18n.translate('navigationEmbeddable.description', {
+ defaultMessage: 'Use links to navigate to commonly used dashboards and websites.',
+ }),
editor: {
getAddButtonLabel: () =>
i18n.translate('navigationEmbeddable.editor.addButtonLabel', {
@@ -37,7 +41,7 @@ export const NavEmbeddableStrings = {
}),
getEmptyLinksMessage: () =>
i18n.translate('navigationEmbeddable.panelEditor.emptyLinksMessage', {
- defaultMessage: 'Use links to navigate to commonly used dashboards and websites.',
+ defaultMessage: "You haven't added any links yet.",
}),
getEmptyLinksTooltip: () =>
i18n.translate('navigationEmbeddable.panelEditor.emptyLinksTooltip', {
diff --git a/src/plugins/navigation_embeddable/public/embeddable/navigation_embeddable_factory.ts b/src/plugins/navigation_embeddable/public/embeddable/navigation_embeddable_factory.ts
index 8f71d1fcbfaa6..dcd8e51382b06 100644
--- a/src/plugins/navigation_embeddable/public/embeddable/navigation_embeddable_factory.ts
+++ b/src/plugins/navigation_embeddable/public/embeddable/navigation_embeddable_factory.ts
@@ -23,17 +23,17 @@ import {
import { UiActionsPresentableGrouping } from '@kbn/ui-actions-plugin/public';
import { DASHBOARD_GRID_COLUMN_COUNT } from '@kbn/dashboard-plugin/public';
import {
+ NavigationEmbeddableInput,
NavigationEmbeddableByReferenceInput,
NavigationEmbeddableEditorFlyoutReturn,
- NavigationEmbeddableInput,
} from './types';
+import { extract, inject } from '../../common/embeddable';
import { APP_ICON, APP_NAME, CONTENT_ID } from '../../common';
import type { NavigationEmbeddable } from './navigation_embeddable';
+import { NavigationEmbeddableAttributes } from '../../common/content_management';
+import { NavEmbeddableStrings } from '../components/navigation_embeddable_strings';
import { getNavigationEmbeddableAttributeService } from '../services/attribute_service';
import { coreServices, untilPluginStartServicesReady } from '../services/kibana_services';
-import { extract, inject } from '../../common/embeddable';
-
-import { NavigationEmbeddableAttributes } from '../../common/content_management';
export type NavigationEmbeddableFactory = EmbeddableFactory;
@@ -158,6 +158,10 @@ export class NavigationEmbeddableFactoryDefinition
return 'link';
}
+ public getDescription() {
+ return NavEmbeddableStrings.getDescription();
+ }
+
inject = inject;
extract = extract;
From b599d13f181bd3bcdd122ab627de3ce28e388bf8 Mon Sep 17 00:00:00 2001
From: Hannah Mudge
Date: Mon, 18 Sep 2023 11:44:19 -0600
Subject: [PATCH 17/53] Fix spacing on comment
---
.../components/editor/navigation_embeddable_panel_editor.tsx | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/plugins/navigation_embeddable/public/components/editor/navigation_embeddable_panel_editor.tsx b/src/plugins/navigation_embeddable/public/components/editor/navigation_embeddable_panel_editor.tsx
index 7c8299310c023..5cac5674daf52 100644
--- a/src/plugins/navigation_embeddable/public/components/editor/navigation_embeddable_panel_editor.tsx
+++ b/src/plugins/navigation_embeddable/public/components/editor/navigation_embeddable_panel_editor.tsx
@@ -182,7 +182,7 @@ const NavigationEmbeddablePanelEditor = ({
{/* Needs to be surrounded by a div rather than a fragment so the EuiFormRow can respond
- to the focus of the inner elements */}
+ to the focus of the inner elements */}
{hasZeroLinks ? (
addOrEditLink()} />
From 9b268f3bc9b6ad2b6c18916cec25baf9fb027867 Mon Sep 17 00:00:00 2001
From: Nick Peihl
Date: Wed, 20 Sep 2023 18:06:58 -0400
Subject: [PATCH 18/53] [Dashboard Navigation] Unit tests (#166297)
Part of #161287
These unit tests were designed to test complex cases for the components.
Please review carefully and suggest any test cases that I may have
overlooked.
---
src/plugins/dashboard/jest_setup.ts | 5 +-
.../clone_panel_action.test.tsx | 12 +-
.../expand_panel_action.test.tsx | 12 +-
.../export_csv_action.test.tsx | 12 +-
.../replace_panel_action.test.tsx | 12 +-
.../dashboard_empty_screen.test.tsx | 2 +-
.../component/grid/dashboard_grid.test.tsx | 22 +-
.../embeddable/dashboard_container.test.tsx | 50 ++--
src/plugins/dashboard/public/mocks.tsx | 26 +-
.../navigation_embeddable/common/mocks.tsx | 62 +++++
.../navigation_embeddable/jest.config.js | 1 +
.../navigation_embeddable/jest_setup.ts | 13 +
.../dashboard_link_component.test.tsx | 227 ++++++++++++++++++
.../dashboard_link_component.tsx | 8 +-
...avigation_embeddable_panel_editor.test.tsx | 129 ++++++++++
.../navigation_embeddable_panel_editor.tsx | 14 +-
...n_embeddable_panel_editor_empty_prompt.tsx | 2 +-
...avigation_embeddable_panel_editor_link.tsx | 1 +
.../external_link_component.test.tsx | 106 ++++++++
.../external_link/external_link_component.tsx | 2 +
.../navigation_embeddable/public/mocks.tsx | 23 ++
.../navigation_embeddable/tsconfig.json | 2 +-
22 files changed, 682 insertions(+), 61 deletions(-)
create mode 100644 src/plugins/navigation_embeddable/common/mocks.tsx
create mode 100644 src/plugins/navigation_embeddable/jest_setup.ts
create mode 100644 src/plugins/navigation_embeddable/public/components/dashboard_link/dashboard_link_component.test.tsx
create mode 100644 src/plugins/navigation_embeddable/public/components/editor/navigation_embeddable_panel_editor.test.tsx
create mode 100644 src/plugins/navigation_embeddable/public/components/external_link/external_link_component.test.tsx
create mode 100644 src/plugins/navigation_embeddable/public/mocks.tsx
diff --git a/src/plugins/dashboard/jest_setup.ts b/src/plugins/dashboard/jest_setup.ts
index 5683ecd4e288b..c6318bc3c4df6 100644
--- a/src/plugins/dashboard/jest_setup.ts
+++ b/src/plugins/dashboard/jest_setup.ts
@@ -6,7 +6,6 @@
* Side Public License, v 1.
*/
-import { pluginServices } from './public/services/plugin_services';
-import { registry } from './public/services/plugin_services.stub';
+import { setStubDashboardServices } from './public/mocks';
-pluginServices.setRegistry(registry.start({}));
+setStubDashboardServices();
diff --git a/src/plugins/dashboard/public/dashboard_actions/clone_panel_action.test.tsx b/src/plugins/dashboard/public/dashboard_actions/clone_panel_action.test.tsx
index 5ec0ac57c574b..76b62f28993ad 100644
--- a/src/plugins/dashboard/public/dashboard_actions/clone_panel_action.test.tsx
+++ b/src/plugins/dashboard/public/dashboard_actions/clone_panel_action.test.tsx
@@ -47,11 +47,13 @@ beforeEach(async () => {
.fn()
.mockReturnValue(mockEmbeddableFactory);
container = buildMockDashboard({
- panels: {
- '123': getSampleDashboardPanel({
- explicitInput: { firstName: 'Kibanana', id: '123' },
- type: CONTACT_CARD_EMBEDDABLE,
- }),
+ overrides: {
+ panels: {
+ '123': getSampleDashboardPanel({
+ explicitInput: { firstName: 'Kibanana', id: '123' },
+ type: CONTACT_CARD_EMBEDDABLE,
+ }),
+ },
},
});
diff --git a/src/plugins/dashboard/public/dashboard_actions/expand_panel_action.test.tsx b/src/plugins/dashboard/public/dashboard_actions/expand_panel_action.test.tsx
index 877488c6d8041..194edc675b108 100644
--- a/src/plugins/dashboard/public/dashboard_actions/expand_panel_action.test.tsx
+++ b/src/plugins/dashboard/public/dashboard_actions/expand_panel_action.test.tsx
@@ -31,11 +31,13 @@ pluginServices.getServices().embeddable.getEmbeddableFactory = jest
beforeEach(async () => {
container = buildMockDashboard({
- panels: {
- '123': getSampleDashboardPanel({
- explicitInput: { firstName: 'Sam', id: '123' },
- type: CONTACT_CARD_EMBEDDABLE,
- }),
+ overrides: {
+ panels: {
+ '123': getSampleDashboardPanel({
+ explicitInput: { firstName: 'Sam', id: '123' },
+ type: CONTACT_CARD_EMBEDDABLE,
+ }),
+ },
},
});
diff --git a/src/plugins/dashboard/public/dashboard_actions/export_csv_action.test.tsx b/src/plugins/dashboard/public/dashboard_actions/export_csv_action.test.tsx
index 350db8fad40b1..0fbbe9c76b2cf 100644
--- a/src/plugins/dashboard/public/dashboard_actions/export_csv_action.test.tsx
+++ b/src/plugins/dashboard/public/dashboard_actions/export_csv_action.test.tsx
@@ -46,11 +46,13 @@ describe('Export CSV action', () => {
};
container = buildMockDashboard({
- panels: {
- '123': getSampleDashboardPanel({
- explicitInput: { firstName: 'Kibanana', id: '123' },
- type: CONTACT_CARD_EXPORTABLE_EMBEDDABLE,
- }),
+ overrides: {
+ panels: {
+ '123': getSampleDashboardPanel({
+ explicitInput: { firstName: 'Kibanana', id: '123' },
+ type: CONTACT_CARD_EXPORTABLE_EMBEDDABLE,
+ }),
+ },
},
});
diff --git a/src/plugins/dashboard/public/dashboard_actions/replace_panel_action.test.tsx b/src/plugins/dashboard/public/dashboard_actions/replace_panel_action.test.tsx
index 0829f89424ede..5873253e105d4 100644
--- a/src/plugins/dashboard/public/dashboard_actions/replace_panel_action.test.tsx
+++ b/src/plugins/dashboard/public/dashboard_actions/replace_panel_action.test.tsx
@@ -29,11 +29,13 @@ let container: DashboardContainer;
let embeddable: ContactCardEmbeddable;
beforeEach(async () => {
container = buildMockDashboard({
- panels: {
- '123': getSampleDashboardPanel({
- explicitInput: { firstName: 'Sam', id: '123' },
- type: CONTACT_CARD_EMBEDDABLE,
- }),
+ overrides: {
+ panels: {
+ '123': getSampleDashboardPanel({
+ explicitInput: { firstName: 'Sam', id: '123' },
+ type: CONTACT_CARD_EMBEDDABLE,
+ }),
+ },
},
});
diff --git a/src/plugins/dashboard/public/dashboard_container/component/empty_screen/dashboard_empty_screen.test.tsx b/src/plugins/dashboard/public/dashboard_container/component/empty_screen/dashboard_empty_screen.test.tsx
index 439fc43ce8eb0..fb2f6e2f16b28 100644
--- a/src/plugins/dashboard/public/dashboard_container/component/empty_screen/dashboard_empty_screen.test.tsx
+++ b/src/plugins/dashboard/public/dashboard_container/component/empty_screen/dashboard_empty_screen.test.tsx
@@ -22,7 +22,7 @@ pluginServices.getServices().visualizations.getAliases = jest
describe('DashboardEmptyScreen', () => {
function mountComponent(viewMode: ViewMode) {
- const dashboardContainer = buildMockDashboard({ viewMode });
+ const dashboardContainer = buildMockDashboard({ overrides: { viewMode } });
return mountWithIntl(
diff --git a/src/plugins/dashboard/public/dashboard_container/component/grid/dashboard_grid.test.tsx b/src/plugins/dashboard/public/dashboard_container/component/grid/dashboard_grid.test.tsx
index be21a9ba6645e..8c587bf175bc7 100644
--- a/src/plugins/dashboard/public/dashboard_container/component/grid/dashboard_grid.test.tsx
+++ b/src/plugins/dashboard/public/dashboard_container/component/grid/dashboard_grid.test.tsx
@@ -39,16 +39,18 @@ jest.mock('./dashboard_grid_item', () => {
const createAndMountDashboardGrid = () => {
const dashboardContainer = buildMockDashboard({
- panels: {
- '1': {
- gridData: { x: 0, y: 0, w: 6, h: 6, i: '1' },
- type: CONTACT_CARD_EMBEDDABLE,
- explicitInput: { id: '1' },
- },
- '2': {
- gridData: { x: 6, y: 6, w: 6, h: 6, i: '2' },
- type: CONTACT_CARD_EMBEDDABLE,
- explicitInput: { id: '2' },
+ overrides: {
+ panels: {
+ '1': {
+ gridData: { x: 0, y: 0, w: 6, h: 6, i: '1' },
+ type: CONTACT_CARD_EMBEDDABLE,
+ explicitInput: { id: '1' },
+ },
+ '2': {
+ gridData: { x: 6, y: 6, w: 6, h: 6, i: '2' },
+ type: CONTACT_CARD_EMBEDDABLE,
+ explicitInput: { id: '2' },
+ },
},
},
});
diff --git a/src/plugins/dashboard/public/dashboard_container/embeddable/dashboard_container.test.tsx b/src/plugins/dashboard/public/dashboard_container/embeddable/dashboard_container.test.tsx
index 67bb482b45676..b80c7d11dcfe8 100644
--- a/src/plugins/dashboard/public/dashboard_container/embeddable/dashboard_container.test.tsx
+++ b/src/plugins/dashboard/public/dashboard_container/embeddable/dashboard_container.test.tsx
@@ -43,11 +43,13 @@ pluginServices.getServices().embeddable.getEmbeddableFactory = jest
test('DashboardContainer initializes embeddables', (done) => {
const container = buildMockDashboard({
- panels: {
- '123': getSampleDashboardPanel({
- explicitInput: { firstName: 'Sam', id: '123' },
- type: CONTACT_CARD_EMBEDDABLE,
- }),
+ overrides: {
+ panels: {
+ '123': getSampleDashboardPanel({
+ explicitInput: { firstName: 'Sam', id: '123' },
+ type: CONTACT_CARD_EMBEDDABLE,
+ }),
+ },
},
});
@@ -94,11 +96,13 @@ test('DashboardContainer.replacePanel', (done) => {
const ID = '123';
const container = buildMockDashboard({
- panels: {
- [ID]: getSampleDashboardPanel({
- explicitInput: { firstName: 'Sam', id: ID },
- type: CONTACT_CARD_EMBEDDABLE,
- }),
+ overrides: {
+ panels: {
+ [ID]: getSampleDashboardPanel({
+ explicitInput: { firstName: 'Sam', id: ID },
+ type: CONTACT_CARD_EMBEDDABLE,
+ }),
+ },
},
});
let counter = 0;
@@ -134,11 +138,13 @@ test('DashboardContainer.replacePanel', (done) => {
test('Container view mode change propagates to existing children', async () => {
const container = buildMockDashboard({
- panels: {
- '123': getSampleDashboardPanel({
- explicitInput: { firstName: 'Sam', id: '123' },
- type: CONTACT_CARD_EMBEDDABLE,
- }),
+ overrides: {
+ panels: {
+ '123': getSampleDashboardPanel({
+ explicitInput: { firstName: 'Sam', id: '123' },
+ type: CONTACT_CARD_EMBEDDABLE,
+ }),
+ },
},
});
@@ -192,7 +198,7 @@ test('DashboardContainer in edit mode shows edit mode actions', async () => {
uiActionsSetup.registerAction(editModeAction);
uiActionsSetup.addTriggerAction(CONTEXT_MENU_TRIGGER, editModeAction);
- const container = buildMockDashboard({ viewMode: ViewMode.VIEW });
+ const container = buildMockDashboard({ overrides: { viewMode: ViewMode.VIEW } });
const embeddable = await container.addNewEmbeddable<
ContactCardEmbeddableInput,
@@ -268,8 +274,10 @@ describe('getInheritedInput', () => {
test('Should pass dashboard timeRange and timeslice to panel when panel does not have custom time range', async () => {
const container = buildMockDashboard({
- timeRange: dashboardTimeRange,
- timeslice: dashboardTimeslice,
+ overrides: {
+ timeRange: dashboardTimeRange,
+ timeslice: dashboardTimeslice,
+ },
});
const embeddable = await container.addNewEmbeddable(
CONTACT_CARD_EMBEDDABLE,
@@ -291,8 +299,10 @@ describe('getInheritedInput', () => {
test('Should not pass dashboard timeRange and timeslice to panel when panel has custom time range', async () => {
const container = buildMockDashboard({
- timeRange: dashboardTimeRange,
- timeslice: dashboardTimeslice,
+ overrides: {
+ timeRange: dashboardTimeRange,
+ timeslice: dashboardTimeslice,
+ },
});
const embeddableTimeRange = {
to: 'now',
diff --git a/src/plugins/dashboard/public/mocks.tsx b/src/plugins/dashboard/public/mocks.tsx
index 69ec7efadf1ad..778f0471b191b 100644
--- a/src/plugins/dashboard/public/mocks.tsx
+++ b/src/plugins/dashboard/public/mocks.tsx
@@ -12,6 +12,8 @@ import { mockedReduxEmbeddablePackage } from '@kbn/presentation-util-plugin/publ
import { DashboardStart } from './plugin';
import { DashboardContainerInput, DashboardPanelState } from '../common';
import { DashboardContainer } from './dashboard_container/embeddable/dashboard_container';
+import { pluginServices } from './services/plugin_services';
+import { registry } from './services/plugin_services.stub';
export type Start = jest.Mocked;
@@ -66,9 +68,25 @@ export function setupIntersectionObserverMock({
});
}
-export function buildMockDashboard(overrides?: Partial) {
+export function buildMockDashboard({
+ overrides,
+ savedObjectId,
+}: {
+ overrides?: Partial;
+ savedObjectId?: string;
+} = {}) {
const initialInput = getSampleDashboardInput(overrides);
- const dashboardContainer = new DashboardContainer(initialInput, mockedReduxEmbeddablePackage);
+ const dashboardContainer = new DashboardContainer(
+ initialInput,
+ mockedReduxEmbeddablePackage,
+ undefined,
+ undefined,
+ undefined,
+ undefined,
+ undefined,
+ undefined,
+ savedObjectId
+ );
return dashboardContainer;
}
@@ -120,3 +138,7 @@ export function getSampleDashboardPanel