From 577134c2013a2f1caa5ea4b616f36dabd5d22e37 Mon Sep 17 00:00:00 2001
From: Nick Peihl
Date: Thu, 10 Aug 2023 13:42:10 -0400
Subject: [PATCH] [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 c4a3018fdfb3..9347ce4fd7f1 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 9100f489bef4..94780b9abf80 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 cb78e060d6ed..169221d659e5 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 c1cfca07017b..bc6be0a11b2b 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 30888521d651..99b16aec7107 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 14067f0b6aa6..6f93b08a2708 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 7b02001a93c6..a052a8fef103 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 98e541fd08e6..2076c8d6f1e7 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 2f29410bc3e5..df007a93483e 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 5539f854b24d..34e7cc0593e6 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 000000000000..9731275e04f1
--- /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 000000000000..fa050138b35f
--- /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 000000000000..282ba879c17f
--- /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 000000000000..e9c79f0f50f9
--- /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 000000000000..5494a193ba7b
--- /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 000000000000..00f40932638f
--- /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 000000000000..bedc5a6ff2f0
--- /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 000000000000..0d1a87a17d14
--- /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 000000000000..9cb4fc42124a
--- /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 000000000000..e03b4a4dd146
--- /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 961aacc7641a..b74e4bbd6f33 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 db371c426ed4..60b88a740c14 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 90bf4066d4c2..7b940ac02735 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 b17be812accb..a45d6d802867 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 1d5fa9876605..def291c63bca 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 f4c2ce6b5149..97a5a86a9d65 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 be65c130222e..7c6d2b726810 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 5628c3444d2d..20953c41dbe8 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 000000000000..a477c62d3bd9
--- /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 000000000000..3115e110467e
--- /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 000000000000..883a28a34ad2
--- /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 000000000000..f7cb54da2393
--- /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 000000000000..31274817c5cb
--- /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 83cc4cfdc7c4..4248af756f52 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 19edb5fb4f2c..e156180d8dd4 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 794b8812d793..1fee9bdd9b20 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 12c60f3ebd00..eeaae0533480 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 9f3194d0b376..58a665488b82 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 8a9662492909..9711c81b6412 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 0513d50fc8cb..43d12f611df8 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 9cdbcfcc6c66..d1655bd9bc25 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 7a969c0298f7..8863c4393b05 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 000000000000..7a7dbfe2bd13
--- /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 710c6227a356..ddc5daad6495 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 000000000000..2376765bcac8
--- /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 000000000000..07318f62a3e1
--- /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 000000000000..6ececdd95b5d
--- /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 000000000000..05e3ca79f997
--- /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