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 ? ( + + ) : ( + { + // 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