From f0ebcb21d7df46c32655e5f39f2a5a945bd5238e Mon Sep 17 00:00:00 2001
From: Hannah Mudge
Date: Wed, 16 Aug 2023 11:19:47 -0600
Subject: [PATCH] [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