diff --git a/packages/content-management/table_list_view_table/src/components/item_details.tsx b/packages/content-management/table_list_view_table/src/components/item_details.tsx index 0b2f52b216905..6c14c9c52021d 100644 --- a/packages/content-management/table_list_view_table/src/components/item_details.tsx +++ b/packages/content-management/table_list_view_table/src/components/item_details.tsx @@ -57,7 +57,7 @@ export function ItemDetails({ ); const onClickTitleHandler = useMemo(() => { - if (!onClickTitle) { + if (!onClickTitle || getDetailViewLink?.(item)) { return undefined; } @@ -65,7 +65,7 @@ export function ItemDetails({ e.preventDefault(); onClickTitle(item); }) as React.MouseEventHandler; - }, [item, onClickTitle]); + }, [item, onClickTitle, getDetailViewLink]); const renderTitle = useCallback(() => { const href = getDetailViewLink ? getDetailViewLink(item) : undefined; @@ -79,7 +79,7 @@ export function ItemDetails({ {/* eslint-disable-next-line @elastic/eui/href-or-on-click */} diff --git a/packages/content-management/table_list_view_table/src/table_list_view_table.tsx b/packages/content-management/table_list_view_table/src/table_list_view_table.tsx index ccf6ef791d8d4..f7de8935ea949 100644 --- a/packages/content-management/table_list_view_table/src/table_list_view_table.tsx +++ b/packages/content-management/table_list_view_table/src/table_list_view_table.tsx @@ -289,12 +289,6 @@ function TableListViewTableComp({ ); } - if (getDetailViewLink && onClickTitle) { - throw new Error( - `[TableListView] Either "getDetailViewLink" or "onClickTitle" can be provided. Not both.` - ); - } - if (contentEditor.isReadonly === false && contentEditor.onSave === undefined) { throw new Error( `[TableListView] A value for [contentEditor.onSave()] must be provided when [contentEditor.isReadonly] is false.` diff --git a/src/plugins/dashboard/public/dashboard_app/top_nav/dashboard_editing_toolbar.tsx b/src/plugins/dashboard/public/dashboard_app/top_nav/dashboard_editing_toolbar.tsx index 0190bbaefa00b..6afdf1429663b 100644 --- a/src/plugins/dashboard/public/dashboard_app/top_nav/dashboard_editing_toolbar.tsx +++ b/src/plugins/dashboard/public/dashboard_app/top_nav/dashboard_editing_toolbar.tsx @@ -54,12 +54,14 @@ export function DashboardEditingToolbar({ isDisabled }: { isDisabled?: boolean } trackUiMetric(METRIC_TYPE.CLICK, `${visType.name}:create`); } - if ('aliasPath' in visType) { - appId = visType.aliasApp; - path = visType.aliasPath; - } else { + if (!('alias' in visType)) { + // this visualization is not an alias appId = 'visualize'; path = `#/create?type=${encodeURIComponent(visType.name)}`; + } else if (visType.alias && 'path' in visType.alias) { + // this visualization **is** an alias, and it has an app to redirect to for creation + appId = visType.alias.app; + path = visType.alias.path; } } else { appId = 'visualize'; diff --git a/src/plugins/dashboard/public/dashboard_app/top_nav/editor_menu.tsx b/src/plugins/dashboard/public/dashboard_app/top_nav/editor_menu.tsx index d2b6470650caa..18f26704221ab 100644 --- a/src/plugins/dashboard/public/dashboard_app/top_nav/editor_menu.tsx +++ b/src/plugins/dashboard/public/dashboard_app/top_nav/editor_menu.tsx @@ -104,10 +104,11 @@ export const EditorMenu = ({ createNewVisType, createNewEmbeddable, isDisabled } const promotedVisTypes = getSortedVisTypesByGroup(VisGroups.PROMOTED); const aggsBasedVisTypes = getSortedVisTypesByGroup(VisGroups.AGGBASED); const toolVisTypes = getSortedVisTypesByGroup(VisGroups.TOOLS); - const visTypeAliases = getVisTypeAliases().sort( - ({ promotion: a = false }: VisTypeAlias, { promotion: b = false }: VisTypeAlias) => + const visTypeAliases = getVisTypeAliases() + .sort(({ promotion: a = false }: VisTypeAlias, { promotion: b = false }: VisTypeAlias) => a === b ? 0 : a ? -1 : 1 - ); + ) + .filter(({ disableCreate }: VisTypeAlias) => !disableCreate); const factories = unwrappedEmbeddableFactories.filter( ({ isEditable, factory: { type, canCreateNew, isContainerType } }) => diff --git a/src/plugins/dashboard/public/dashboard_container/component/empty_screen/dashboard_empty_screen.tsx b/src/plugins/dashboard/public/dashboard_container/component/empty_screen/dashboard_empty_screen.tsx index 8767b5abe3567..de7d79d456b9f 100644 --- a/src/plugins/dashboard/public/dashboard_container/component/empty_screen/dashboard_empty_screen.tsx +++ b/src/plugins/dashboard/public/dashboard_container/component/empty_screen/dashboard_empty_screen.tsx @@ -10,21 +10,21 @@ import React, { useCallback, useMemo } from 'react'; import useObservable from 'react-use/lib/useObservable'; import { - EuiText, - EuiImage, EuiButton, - EuiFlexItem, - EuiFlexGroup, EuiButtonEmpty, + EuiFlexGroup, + EuiFlexItem, + EuiImage, EuiPageTemplate, + EuiText, } from '@elastic/eui'; import { METRIC_TYPE } from '@kbn/analytics'; import { ViewMode } from '@kbn/embeddable-plugin/public'; +import { DASHBOARD_UI_METRIC_ID } from '../../../dashboard_constants'; import { pluginServices } from '../../../services/plugin_services'; -import { emptyScreenStrings } from '../../_dashboard_container_strings'; import { useDashboardContainer } from '../../embeddable/dashboard_container'; -import { DASHBOARD_UI_METRIC_ID } from '../../../dashboard_constants'; +import { emptyScreenStrings } from '../../_dashboard_container_strings'; export function DashboardEmptyScreen() { const { @@ -53,7 +53,7 @@ export function DashboardEmptyScreen() { const originatingApp = embeddableAppContext?.currentAppId; const goToLens = useCallback(() => { - if (!lensAlias || !lensAlias.aliasPath) return; + if (!lensAlias || !lensAlias.alias) return; const trackUiMetric = usageCollection.reportUiCounter?.bind( usageCollection, DASHBOARD_UI_METRIC_ID @@ -62,8 +62,8 @@ export function DashboardEmptyScreen() { if (trackUiMetric) { trackUiMetric(METRIC_TYPE.CLICK, `${lensAlias.name}:create`); } - getStateTransfer().navigateToEditor(lensAlias.aliasApp, { - path: lensAlias.aliasPath, + getStateTransfer().navigateToEditor(lensAlias.alias.app, { + path: lensAlias.alias.path, state: { originatingApp, originatingPath, diff --git a/src/plugins/links/kibana.jsonc b/src/plugins/links/kibana.jsonc index 5f0796d55b43a..a1e59cff9dc3b 100644 --- a/src/plugins/links/kibana.jsonc +++ b/src/plugins/links/kibana.jsonc @@ -12,9 +12,10 @@ "dashboard", "embeddable", "kibanaReact", + "kibanaUtils", "presentationUtil", "uiActionsEnhanced", - "kibanaUtils" + "visualizations" ], "optionalPlugins": ["triggersActionsUi"], "requiredBundles": ["savedObjects"] diff --git a/src/plugins/links/public/content_management/links_content_management_client.ts b/src/plugins/links/public/content_management/links_content_management_client.ts index 777fd8731d691..586b5aff5efb8 100644 --- a/src/plugins/links/public/content_management/links_content_management_client.ts +++ b/src/plugins/links/public/content_management/links_content_management_client.ts @@ -7,8 +7,9 @@ */ import type { SearchQuery } from '@kbn/content-management-plugin/common'; +import { SerializableAttributes, VisualizationClient } from '@kbn/visualizations-plugin/public'; +import { CONTENT_ID as contentTypeId, CONTENT_ID } from '../../common'; import type { LinksCrudTypes } from '../../common/content_management'; -import { CONTENT_ID as contentTypeId } from '../../common'; import { contentManagement } from '../services/kibana_services'; const get = async (id: string) => { @@ -65,3 +66,9 @@ export const linksClient = { delete: deleteLinks, search, }; + +export function getLinksClient< + Attr extends SerializableAttributes = SerializableAttributes +>(): VisualizationClient { + return linksClient as unknown as VisualizationClient; +} diff --git a/src/plugins/links/public/editor/open_editor_flyout.tsx b/src/plugins/links/public/editor/open_editor_flyout.tsx index 1c722a484eb1d..d47b178d5ff97 100644 --- a/src/plugins/links/public/editor/open_editor_flyout.tsx +++ b/src/plugins/links/public/editor/open_editor_flyout.tsx @@ -7,18 +7,20 @@ */ import React from 'react'; +import { skip, take } from 'rxjs/operators'; -import { withSuspense } from '@kbn/shared-ux-utility'; -import { toMountPoint } from '@kbn/react-kibana-mount'; import { EuiLoadingSpinner, EuiPanel } from '@elastic/eui'; -import { tracksOverlays } from '@kbn/embeddable-plugin/public'; import { DashboardContainer } from '@kbn/dashboard-plugin/public/dashboard_container'; +import { tracksOverlays } from '@kbn/embeddable-plugin/public'; +import { toMountPoint } from '@kbn/react-kibana-mount'; +import { withSuspense } from '@kbn/shared-ux-utility'; -import { LinksInput, LinksByReferenceInput, LinksEditorFlyoutReturn } from '../embeddable/types'; -import { coreServices } from '../services/kibana_services'; -import { runSaveToLibrary } from '../content_management/save_to_library'; +import { OverlayRef } from '@kbn/core-mount-utils-browser'; import { Link, LinksLayoutType } from '../../common/content_management'; +import { runSaveToLibrary } from '../content_management/save_to_library'; +import { LinksByReferenceInput, LinksEditorFlyoutReturn, LinksInput } from '../embeddable/types'; import { getLinksAttributeService } from '../services/attribute_service'; +import { coreServices } from '../services/kibana_services'; const LazyLinksEditor = React.lazy(() => import('../components/editor/links_editor')); @@ -40,7 +42,8 @@ export async function openEditorFlyout( const { attributes } = await attributeService.unwrapAttributes(initialInput); const isByReference = attributeService.inputIsRefType(initialInput); const initialLinks = attributes?.links; - const overlayTracker = tracksOverlays(parentDashboard) ? parentDashboard : undefined; + const overlayTracker = + parentDashboard && tracksOverlays(parentDashboard) ? parentDashboard : undefined; if (!initialLinks) { /** @@ -55,6 +58,22 @@ export async function openEditorFlyout( } return new Promise((resolve, reject) => { + const closeEditorFlyout = (editorFlyout: OverlayRef) => { + if (overlayTracker) { + overlayTracker.clearOverlays(); + } else { + editorFlyout.close(); + } + }; + + /** + * Close the flyout whenever the app changes - this handles cases for when the flyout is open outside of the + * Dashboard app (`overlayTracker` is not available) + */ + coreServices.application.currentAppId$.pipe(skip(1), take(1)).subscribe(() => { + if (!overlayTracker) editorFlyout.close(); + }); + const onSaveToLibrary = async (newLinks: Link[], newLayout: LinksLayoutType) => { const newAttributes = { ...attributes, @@ -74,7 +93,7 @@ export async function openEditorFlyout( attributes: newAttributes, }); parentDashboard?.reload(); - if (overlayTracker) overlayTracker.clearOverlays(); + closeEditorFlyout(editorFlyout); }; const onAddToDashboard = (newLinks: Link[], newLayout: LinksLayoutType) => { @@ -94,12 +113,12 @@ export async function openEditorFlyout( attributes: newAttributes, }); parentDashboard?.reload(); - if (overlayTracker) overlayTracker.clearOverlays(); + closeEditorFlyout(editorFlyout); }; const onCancel = () => { reject(); - if (overlayTracker) overlayTracker.clearOverlays(); + closeEditorFlyout(editorFlyout); }; const editorFlyout = coreServices.overlays.openFlyout( @@ -125,6 +144,8 @@ export async function openEditorFlyout( } ); - if (overlayTracker) overlayTracker.openOverlay(editorFlyout); + if (overlayTracker) { + overlayTracker.openOverlay(editorFlyout); + } }); } diff --git a/src/plugins/links/public/embeddable/links_embeddable_factory.ts b/src/plugins/links/public/embeddable/links_embeddable_factory.ts index 55838c6d65229..e1446aff316af 100644 --- a/src/plugins/links/public/embeddable/links_embeddable_factory.ts +++ b/src/plugins/links/public/embeddable/links_embeddable_factory.ts @@ -125,8 +125,6 @@ export class LinksFactoryDefinition initialInput: LinksInput, parent?: DashboardContainer ): Promise { - if (!parent) return { newInput: {} }; - const { openEditorFlyout } = await import('../editor/open_editor_flyout'); const { newInput, attributes } = await openEditorFlyout( diff --git a/src/plugins/links/public/plugin.ts b/src/plugins/links/public/plugin.ts index 7927de88b80e7..6569f6d767808 100644 --- a/src/plugins/links/public/plugin.ts +++ b/src/plugins/links/public/plugin.ts @@ -6,22 +6,28 @@ * Side Public License, v 1. */ -import { CoreSetup, CoreStart, Plugin } from '@kbn/core/public'; import { ContentManagementPublicSetup, ContentManagementPublicStart, } from '@kbn/content-management-plugin/public'; +import { CoreSetup, CoreStart, Plugin } from '@kbn/core/public'; import { DashboardStart } from '@kbn/dashboard-plugin/public'; +import { DashboardContainer } from '@kbn/dashboard-plugin/public/dashboard_container'; import { EmbeddableSetup, EmbeddableStart } from '@kbn/embeddable-plugin/public'; import { PresentationUtilPluginStart } from '@kbn/presentation-util-plugin/public'; +import { VisualizationsSetup } from '@kbn/visualizations-plugin/public'; -import { APP_NAME } from '../common'; +import { APP_ICON, APP_NAME, CONTENT_ID, LATEST_VERSION } from '../common'; +import { LinksCrudTypes } from '../common/content_management'; +import { LinksStrings } from './components/links_strings'; +import { getLinksClient } from './content_management/links_content_management_client'; import { LinksFactoryDefinition } from './embeddable'; -import { CONTENT_ID, LATEST_VERSION } from '../common'; +import { LinksByReferenceInput } from './embeddable/types'; import { setKibanaServices } from './services/kibana_services'; export interface LinksSetupDependencies { embeddable: EmbeddableSetup; + visualizations: VisualizationsSetup; contentManagement: ContentManagementPublicSetup; } @@ -39,7 +45,9 @@ export class LinksPlugin public setup(core: CoreSetup, plugins: LinksSetupDependencies) { core.getStartServices().then(([_, deps]) => { - plugins.embeddable.registerEmbeddableFactory(CONTENT_ID, new LinksFactoryDefinition()); + const linksFactory = new LinksFactoryDefinition(); + + plugins.embeddable.registerEmbeddableFactory(CONTENT_ID, linksFactory); plugins.contentManagement.registry.register({ id: CONTENT_ID, @@ -48,6 +56,53 @@ export class LinksPlugin }, name: APP_NAME, }); + + const getExplicitInput = async ({ + savedObjectId, + parent, + }: { + savedObjectId?: string; + parent?: DashboardContainer; + }) => { + try { + await linksFactory.getExplicitInput({ savedObjectId } as LinksByReferenceInput, parent); + } catch { + // swallow any errors - this just means that the user cancelled editing + } + return; + }; + + plugins.visualizations.registerAlias({ + disableCreate: true, // do not allow creation through visualization listing page + name: CONTENT_ID, + title: APP_NAME, + icon: APP_ICON, + description: LinksStrings.getDescription(), + stage: 'experimental', + appExtensions: { + visualizations: { + docTypes: [CONTENT_ID], + searchFields: ['title^3'], + client: getLinksClient, + toListItem(linkItem: LinksCrudTypes['Item']) { + const { id, type, updatedAt, attributes } = linkItem; + const { title, description } = attributes; + + return { + id, + title, + editor: { onEdit: (savedObjectId: string) => getExplicitInput({ savedObjectId }) }, + description, + updatedAt, + icon: APP_ICON, + typeTitle: APP_NAME, + stage: 'experimental', + savedObjectType: type, + }; + }, + }, + }, + }); }); } diff --git a/src/plugins/links/tsconfig.json b/src/plugins/links/tsconfig.json index e9814f4e107e7..be7692c7fa431 100644 --- a/src/plugins/links/tsconfig.json +++ b/src/plugins/links/tsconfig.json @@ -26,7 +26,9 @@ "@kbn/logging", "@kbn/core-plugins-server", "@kbn/react-kibana-mount", - "@kbn/react-kibana-context-theme" + "@kbn/react-kibana-context-theme", + "@kbn/visualizations-plugin", + "@kbn/core-mount-utils-browser" ], "exclude": ["target/**/*"] } diff --git a/src/plugins/visualizations/public/utils/saved_visualize_utils.test.ts b/src/plugins/visualizations/public/utils/saved_visualize_utils.test.ts index 01475fe483c80..54de776faa624 100644 --- a/src/plugins/visualizations/public/utils/saved_visualize_utils.test.ts +++ b/src/plugins/visualizations/public/utils/saved_visualize_utils.test.ts @@ -504,11 +504,13 @@ describe('saved_visualize_utils', () => { { id: 'wat', image: undefined, + editor: { + editUrl: '/edit/wat', + }, readOnly: false, references: undefined, icon: undefined, savedObjectType: 'visualization', - editUrl: '/edit/wat', type: 'test', typeName: 'test', typeTitle: undefined, diff --git a/src/plugins/visualizations/public/utils/saved_visualize_utils.ts b/src/plugins/visualizations/public/utils/saved_visualize_utils.ts index 9232504e026d3..85932c09729c3 100644 --- a/src/plugins/visualizations/public/utils/saved_visualize_utils.ts +++ b/src/plugins/visualizations/public/utils/saved_visualize_utils.ts @@ -73,7 +73,7 @@ export function mapHitSource( references: SavedObjectReference[]; url: string; savedObjectType?: string; - editUrl?: string; + editor?: { editUrl?: string }; updatedAt?: string; type?: BaseVisType; icon?: BaseVisType['icon']; @@ -108,7 +108,7 @@ export function mapHitSource( newAttributes.icon = newAttributes.type?.icon; newAttributes.image = newAttributes.type?.image; newAttributes.typeTitle = newAttributes.type?.title; - newAttributes.editUrl = `/edit/${id}`; + newAttributes.editor = { editUrl: `/edit/${id}` }; newAttributes.readOnly = Boolean(visTypes.get(typeName as string)?.disableEdit); return newAttributes; @@ -168,7 +168,6 @@ export async function findListItems( return acc; }, acc); }, {} as { [visType: string]: VisualizationsAppExtension }); - const searchOption = (field: string, ...defaults: string[]) => _(extensions).map(field).concat(defaults).compact().flatten().uniq().value() as string[]; diff --git a/src/plugins/visualizations/public/vis_types/vis_type_alias_registry.ts b/src/plugins/visualizations/public/vis_types/vis_type_alias_registry.ts index 2a46b28f06dd9..617f0386f6181 100644 --- a/src/plugins/visualizations/public/vis_types/vis_type_alias_registry.ts +++ b/src/plugins/visualizations/public/vis_types/vis_type_alias_registry.ts @@ -19,8 +19,6 @@ import { BaseVisType } from './base_vis_type'; export type VisualizationStage = 'experimental' | 'beta' | 'production'; export interface VisualizationListItem { - editUrl: string; - editApp?: string; error?: string; icon: string; id: string; @@ -32,6 +30,9 @@ export interface VisualizationListItem { typeTitle: string; image?: string; type?: BaseVisType | string; + editor: + | { editUrl: string; editApp?: string } + | { onEdit: (savedObjectId: string) => Promise }; } export interface SerializableAttributes { @@ -86,8 +87,14 @@ export interface VisualizationsAppExtension { } export interface VisTypeAlias { - aliasPath: string; - aliasApp: string; + /** + * Provide `alias` when your visualization has a dedicated app for creation. + * TODO: Provide a generic callback to create visualizations inline. + */ + alias?: { + app: string; + path: string; + }; name: string; title: string; icon: string; diff --git a/src/plugins/visualizations/public/visualize_app/components/visualize_listing.scss b/src/plugins/visualizations/public/visualize_app/components/visualize_listing.scss index 7fe4fe05ab267..48df3fc673886 100644 --- a/src/plugins/visualizations/public/visualize_app/components/visualize_listing.scss +++ b/src/plugins/visualizations/public/visualize_app/components/visualize_listing.scss @@ -11,7 +11,7 @@ .visListingTable__experimentalIcon { width: $euiSizeL; - vertical-align: baseline; + vertical-align: middle; padding: 0 $euiSizeS; margin-left: $euiSizeS; } diff --git a/src/plugins/visualizations/public/visualize_app/components/visualize_listing.tsx b/src/plugins/visualizations/public/visualize_app/components/visualize_listing.tsx index d4beb45c4e248..d1de28ac73795 100644 --- a/src/plugins/visualizations/public/visualize_app/components/visualize_listing.tsx +++ b/src/plugins/visualizations/public/visualize_app/components/visualize_listing.tsx @@ -36,6 +36,7 @@ import { TableListViewProps, } from '@kbn/content-management-table-list-view'; import { TableListViewTable } from '@kbn/content-management-table-list-view-table'; + import { findListItems } from '../../utils/saved_visualize_utils'; import { updateBasicSoAttributes } from '../../utils/saved_objects_utils/update_basic_attributes'; import { checkForDuplicateTitle } from '../../utils/saved_objects_utils/check_for_duplicate_title'; @@ -49,17 +50,17 @@ import { getNoItemsMessage, getCustomColumn } from '../utils'; import { getVisualizeListItemLink } from '../utils/get_visualize_list_item_link'; import type { VisualizationStage } from '../../vis_types/vis_type_alias_registry'; -interface VisualizeUserContent extends VisualizationListItem, UserContentCommonSchema { - type: string; - attributes: { - title: string; - description?: string; - editApp: string; - editUrl: string; - readOnly: boolean; - error?: string; +type VisualizeUserContent = VisualizationListItem & + UserContentCommonSchema & { + type: string; + attributes: { + id: string; + title: string; + description?: string; + readOnly: boolean; + error?: string; + }; }; -} const toTableListViewSavedObject = (savedObject: Record): VisualizeUserContent => { return { @@ -67,19 +68,17 @@ const toTableListViewSavedObject = (savedObject: Record): Visua updatedAt: savedObject.updatedAt as string, references: savedObject.references as Array<{ id: string; type: string; name: string }>, type: savedObject.savedObjectType as string, - editUrl: savedObject.editUrl as string, - editApp: savedObject.editApp as string, icon: savedObject.icon as string, stage: savedObject.stage as VisualizationStage, savedObjectType: savedObject.savedObjectType as string, typeTitle: savedObject.typeTitle as string, title: (savedObject.title as string) ?? '', error: (savedObject.error as string) ?? '', + editor: savedObject.editor as any, attributes: { + id: savedObject.id as string, title: (savedObject.title as string) ?? '', description: savedObject.description as string, - editApp: savedObject.editApp as string, - editUrl: savedObject.editUrl as string, readOnly: savedObject.readOnly as boolean, error: savedObject.error as string, }, @@ -120,7 +119,13 @@ const useTableListViewProps = ( }, [closeNewVisModal]); const editItem = useCallback( - ({ attributes: { editUrl, editApp } }: VisualizeUserContent) => { + async ({ attributes: { id }, editor }: VisualizeUserContent) => { + if (!('editApp' in editor || 'editUrl' in editor)) { + await editor.onEdit(id); + return; + } + + const { editApp, editUrl } = editor; if (editApp) { application.navigateToApp(editApp, { path: editUrl }); return; @@ -383,10 +388,19 @@ export const VisualizeListing = () => { entityNamePlural={i18n.translate('visualizations.listing.table.entityNamePlural', { defaultMessage: 'visualizations', })} - getDetailViewLink={({ attributes: { editApp, editUrl, error, readOnly } }) => - readOnly + onClickTitle={(item) => { + tableViewProps.editItem?.(item); + }} + getDetailViewLink={({ editor, attributes: { error, readOnly } }) => + readOnly || (editor && 'onEdit' in editor) ? undefined - : getVisualizeListItemLink(application, kbnUrlStateStorage, editApp, editUrl, error) + : getVisualizeListItemLink( + application, + kbnUrlStateStorage, + editor.editApp, + editor.editUrl, + error + ) } tableCaption={visualizeLibraryTitle} {...tableViewProps} diff --git a/src/plugins/visualizations/public/visualize_app/utils/get_visualize_list_item_link.ts b/src/plugins/visualizations/public/visualize_app/utils/get_visualize_list_item_link.ts index 524ccbad44f81..5c1a5b1ee3ebb 100644 --- a/src/plugins/visualizations/public/visualize_app/utils/get_visualize_list_item_link.ts +++ b/src/plugins/visualizations/public/visualize_app/utils/get_visualize_list_item_link.ts @@ -17,10 +17,10 @@ export const getVisualizeListItemLink = ( application: ApplicationStart, kbnUrlStateStorage: IKbnUrlStateStorage, editApp: string | undefined, - editUrl: string, + editUrl: string | undefined, error: string | undefined = undefined ) => { - if (error) { + if (error || (!editApp && !editUrl)) { return undefined; } diff --git a/src/plugins/visualizations/public/wizard/group_selection/group_selection.test.tsx b/src/plugins/visualizations/public/wizard/group_selection/group_selection.test.tsx index e1e9cec25e15a..a9a21446e06a2 100644 --- a/src/plugins/visualizations/public/wizard/group_selection/group_selection.test.tsx +++ b/src/plugins/visualizations/public/wizard/group_selection/group_selection.test.tsx @@ -37,8 +37,10 @@ describe('GroupSelection', () => { { name: 'visWithAliasUrl', title: 'Vis with alias Url', - aliasApp: 'aliasApp', - aliasPath: '#/aliasApp', + alias: { + app: 'aliasApp', + path: '#/aliasApp', + }, description: 'Vis with alias Url', stage: 'production', group: VisGroups.PROMOTED, @@ -49,8 +51,10 @@ describe('GroupSelection', () => { description: 'Vis alias with promotion', stage: 'production', group: VisGroups.PROMOTED, - aliasApp: 'anotherApp', - aliasPath: '#/anotherUrl', + alias: { + app: 'anotherApp', + path: '#/anotherUrl', + }, promotion: true, } as unknown, ] as BaseVisType[]; diff --git a/src/plugins/visualizations/public/wizard/group_selection/group_selection.tsx b/src/plugins/visualizations/public/wizard/group_selection/group_selection.tsx index dc8aaa03161b8..3bcdff18c47a7 100644 --- a/src/plugins/visualizations/public/wizard/group_selection/group_selection.tsx +++ b/src/plugins/visualizations/public/wizard/group_selection/group_selection.tsx @@ -200,7 +200,7 @@ const VisGroup = ({ visType, onVisTypeSelected }: VisCardProps) => { } onClick={onClick} data-test-subj={`visType-${visType.name}`} - data-vis-stage={!('aliasPath' in visType) ? visType.stage : 'alias'} + data-vis-stage={!('alias' in visType) ? visType.stage : 'alias'} aria-label={`visType-${visType.name}`} description={ <> diff --git a/src/plugins/visualizations/public/wizard/new_vis_modal.test.tsx b/src/plugins/visualizations/public/wizard/new_vis_modal.test.tsx index 907c989c7bb43..0db5f66cc5c92 100644 --- a/src/plugins/visualizations/public/wizard/new_vis_modal.test.tsx +++ b/src/plugins/visualizations/public/wizard/new_vis_modal.test.tsx @@ -47,8 +47,10 @@ describe('NewVisModal', () => { title: 'Vis with alias Url', stage: 'production', group: VisGroups.PROMOTED, - aliasApp: 'otherApp', - aliasPath: '#/aliasUrl', + alias: { + app: 'otherApp', + path: '#/aliasUrl', + }, }, { name: 'visWithSearch', @@ -181,7 +183,7 @@ describe('NewVisModal', () => { ); }); - it('closes and redirects properly if visualization with aliasPath and originatingApp in props', () => { + it('closes and redirects properly if visualization with alias.path and originatingApp in props', () => { const onClose = jest.fn(); const navigateToApp = jest.fn(); const stateTransfer = embeddablePluginMock.createStartContract().getStateTransfer(); diff --git a/src/plugins/visualizations/public/wizard/new_vis_modal.tsx b/src/plugins/visualizations/public/wizard/new_vis_modal.tsx index ac1f89e73700f..b1e5de3215260 100644 --- a/src/plugins/visualizations/public/wizard/new_vis_modal.tsx +++ b/src/plugins/visualizations/public/wizard/new_vis_modal.tsx @@ -119,7 +119,7 @@ class NewVisModal extends React.Component { - if (!('aliasPath' in visType) && visType.requiresSearch && visType.options.showIndexSelection) { + if ('visConfig' in visType && visType.requiresSearch && visType.options.showIndexSelection) { this.setState({ showSearchVisModal: true, visType, @@ -143,10 +143,12 @@ class NewVisModal extends React.Component = ({ addElement }) => { trackCanvasUiMetric(METRIC_TYPE.CLICK, `${visType.name}:create`); } - if ('aliasPath' in visType) { - appId = visType.aliasApp; - path = visType.aliasPath; - } else { + if (!('alias' in visType)) { + // this visualization is not an alias appId = 'visualize'; path = `#/create?type=${encodeURIComponent(visType.name)}`; + } else if (visType.alias && 'path' in visType.alias) { + // this visualization **is** an alias, and it has an app to redirect to for creation + appId = visType.alias.app; + path = visType.alias.path; } } else { appId = 'visualize'; @@ -134,7 +136,8 @@ export const EditorMenu: FC = ({ addElement }) => { .getAliases() .sort(({ promotion: a = false }: VisTypeAlias, { promotion: b = false }: VisTypeAlias) => a === b ? 0 : a ? -1 : 1 - ); + ) + .filter(({ disableCreate }: VisTypeAlias) => !disableCreate); const factories = unwrappedEmbeddableFactories .filter( diff --git a/x-pack/plugins/lens/public/vis_type_alias.ts b/x-pack/plugins/lens/public/vis_type_alias.ts index 2fc493df38edc..20822ee76a094 100644 --- a/x-pack/plugins/lens/public/vis_type_alias.ts +++ b/x-pack/plugins/lens/public/vis_type_alias.ts @@ -11,8 +11,10 @@ import { getBasePath, getEditPath } from '../common/constants'; import { getLensClient } from './persistence/lens_client'; export const getLensAliasConfig = (): VisTypeAlias => ({ - aliasPath: getBasePath(), - aliasApp: 'lens', + alias: { + path: getBasePath(), + app: 'lens', + }, name: 'lens', promotion: true, title: i18n.translate('xpack.lens.visTypeAlias.title', { @@ -41,8 +43,7 @@ export const getLensAliasConfig = (): VisTypeAlias => ({ title, description, updatedAt, - editUrl: getEditPath(id), - editApp: 'lens', + editor: { editUrl: getEditPath(id), editApp: 'lens' }, icon: 'lensApp', stage: 'production', savedObjectType: type, diff --git a/x-pack/plugins/maps/public/maps_vis_type_alias.ts b/x-pack/plugins/maps/public/maps_vis_type_alias.ts index bcbf0170afb25..e161689643612 100644 --- a/x-pack/plugins/maps/public/maps_vis_type_alias.ts +++ b/x-pack/plugins/maps/public/maps_vis_type_alias.ts @@ -24,8 +24,10 @@ export function getMapsVisTypeAlias() { }); return { - aliasApp: APP_ID, - aliasPath: `/${MAP_PATH}`, + alias: { + app: APP_ID, + path: `/${MAP_PATH}`, + }, name: APP_ID, title: APP_NAME, description: appDescription, @@ -45,8 +47,10 @@ export function getMapsVisTypeAlias() { title, description, updatedAt, - editUrl: getEditPath(id), - editApp: APP_ID, + editor: { + editUrl: getEditPath(id), + editApp: APP_ID, + }, icon: APP_ICON, stage: 'production' as VisualizationStage, savedObjectType: type,