[Dashboard Navigation] Add Links to Visualization library (#170810)

Closes https://github.com/elastic/kibana/issues/162840

## Summary

This PR adds a visualization alias for the new Links embeddable so that
all Links library items can be managed/edited from the Visualization
library, like so:


8541506b-cfdd-4a2f-8bc2-841220def7a3

However, in order to get the above working, it was unfortunately not as
simple as just adding a visualization alias. Because the Links
embeddable does not have a dedicated editing app (all editing/creation
is done through a flyout), the usual `aliasPath` + `aliasApp` redirect
that happens for editing an alias did not work in this case.

To get around this, I've had to make changes to how aliases are
registered, as well as both the Visualization `VisualizeListing`
component and the generic `TableListViewTableComp` content management
component:

- **Summary of visualization alias changes:**

First off, I made changes to the typing of aliases - specifically,
rather than taking two independent `aliasPath` and `aliasApp` props,
I've combined them into a singular `alias` prop which will either be of
type `{ alias: string; path: string; }` or `{ embeddableType: string;
}`. This makes it easier to determine (a) whether a visualization is of
type `BaseVisType` or `VisTypeAlias` and (b) if it **is** of type
`VisTypeAlias`, how the editing of that vis should be handled.
Specifically, if `alias` is of type `{ alias: string; path: string; }`,
then it is a normal visualization and behaviour should be the same as it
was previously; however, if it is of type `{ embeddableType: string; }`,
then this is an **inline** alias - i.e. editing should be done inline
via the embeddable factory's edit method.

- **Summary of `VisualizeListing`  changes**

The primary changes here were made to the `editItem` callback -
specifically, if the fetched saved object has neither an `editApp` nor
an `editUrl`, then it will now try to fetch the embeddable factory for
the given saved object type and, if this factory exists, it will call
the `getExplicitInput` method in order to handle editing.

- **Summary of `TableListViewTableComp` changes**

Previously, an error would be thrown if both a `getDetailViewLink` and
an `onClickTitle` prop were provided - while I understand the original
reasoning for adding this catch, this no longer works if we want to
support inline editing like this. In this case, I needed **only** the
Link embeddable items to have an `onClick` while keeping the behaviour
for other visualizations the same (i.e. all other visualization types
should continue to link off to their specific editor apps) - however,
since this method is not provided **per item**, I had no way of making
an exception for just one specific item type.

Therefore, to get around this, it is now considered to be valid for
**both** the `getDetailViewLink` and `onClickTitle` props to be defined
for the `TableListViewTableComp` component. In order to prevent conflict
between the two props, I have made it so that, if both are provided,
`getDetailViewLink` **always takes precedence** over `onClickTitle` - in
this case, `onClickTitle` will **only** be called if `getDetailViewLink`
returns `undefined` for a given item. I have added a comment to
hopefully make this clear for consumers.

### Checklist

- [x] [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
- [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)
This commit is contained in:
Hannah Mudge 2023-11-22 10:37:27 -07:00 committed by GitHub
parent 74dea1e2c9
commit 8eaebb6d47
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
25 changed files with 228 additions and 105 deletions

View file

@ -57,7 +57,7 @@ export function ItemDetails<T extends UserContentCommonSchema>({
);
const onClickTitleHandler = useMemo(() => {
if (!onClickTitle) {
if (!onClickTitle || getDetailViewLink?.(item)) {
return undefined;
}
@ -65,7 +65,7 @@ export function ItemDetails<T extends UserContentCommonSchema>({
e.preventDefault();
onClickTitle(item);
}) as React.MouseEventHandler<HTMLAnchorElement>;
}, [item, onClickTitle]);
}, [item, onClickTitle, getDetailViewLink]);
const renderTitle = useCallback(() => {
const href = getDetailViewLink ? getDetailViewLink(item) : undefined;
@ -79,7 +79,7 @@ export function ItemDetails<T extends UserContentCommonSchema>({
<RedirectAppLinks coreStart={redirectAppLinksCoreStart}>
{/* eslint-disable-next-line @elastic/eui/href-or-on-click */}
<EuiLink
href={getDetailViewLink ? getDetailViewLink(item) : undefined}
href={getDetailViewLink?.(item)}
onClick={onClickTitleHandler}
data-test-subj={`${id}ListingTitleLink-${item.attributes.title.split(' ').join('-')}`}
>

View file

@ -289,12 +289,6 @@ function TableListViewTableComp<T extends UserContentCommonSchema>({
);
}
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.`

View file

@ -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';

View file

@ -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 } }) =>

View file

@ -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 { 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 { pluginServices } from '../../../services/plugin_services';
import { useDashboardContainer } from '../../embeddable/dashboard_container';
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,

View file

@ -12,9 +12,10 @@
"dashboard",
"embeddable",
"kibanaReact",
"kibanaUtils",
"presentationUtil",
"uiActionsEnhanced",
"kibanaUtils"
"visualizations"
],
"optionalPlugins": ["triggersActionsUi"],
"requiredBundles": ["savedObjects"]

View file

@ -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<typeof CONTENT_ID, Attr> {
return linksClient as unknown as VisualizationClient<typeof CONTENT_ID, Attr>;
}

View file

@ -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);
}
});
}

View file

@ -125,8 +125,6 @@ export class LinksFactoryDefinition
initialInput: LinksInput,
parent?: DashboardContainer
): Promise<LinksEditorFlyoutReturn> {
if (!parent) return { newInput: {} };
const { openEditorFlyout } = await import('../editor/open_editor_flyout');
const { newInput, attributes } = await openEditorFlyout(

View file

@ -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<LinksStartDependencies>, 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,
};
},
},
},
});
});
}

View file

@ -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/**/*"]
}

View file

@ -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,

View file

@ -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[];

View file

@ -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<void> };
}
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;

View file

@ -11,7 +11,7 @@
.visListingTable__experimentalIcon {
width: $euiSizeL;
vertical-align: baseline;
vertical-align: middle;
padding: 0 $euiSizeS;
margin-left: $euiSizeS;
}

View file

@ -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<string, unknown>): VisualizeUserContent => {
return {
@ -67,19 +68,17 @@ const toTableListViewSavedObject = (savedObject: Record<string, unknown>): 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}

View file

@ -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;
}

View file

@ -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[];

View file

@ -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={
<>

View file

@ -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();

View file

@ -119,7 +119,7 @@ class NewVisModal extends React.Component<TypeSelectionProps, TypeSelectionState
};
private onVisTypeSelected = (visType: BaseVisType | VisTypeAlias) => {
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<TypeSelectionProps, TypeSelectionState
}
let params;
if ('aliasPath' in visType) {
params = visType.aliasPath;
this.props.onClose();
this.navigate(visType.aliasApp, visType.aliasPath);
if ('alias' in visType) {
if (visType.alias && 'path' in visType.alias) {
params = visType.alias.path;
this.props.onClose();
this.navigate(visType.alias.app, visType.alias.path);
}
return;
}

View file

@ -70,8 +70,10 @@ const testVisTypes: BaseVisType[] = [
const testVisTypeAliases: VisTypeAlias[] = [
{
title: 'Lens',
aliasApp: 'lens',
aliasPath: 'path/to/lens',
alias: {
app: 'lens',
path: 'path/to/lens',
},
icon: 'lensApp',
name: 'lens',
description: 'Description of Lens app',
@ -79,8 +81,10 @@ const testVisTypeAliases: VisTypeAlias[] = [
},
{
title: 'Maps',
aliasApp: 'maps',
aliasPath: 'path/to/maps',
alias: {
app: 'maps',
path: 'path/to/maps',
},
icon: 'gisApp',
name: 'maps',
description: 'Description of Maps app',

View file

@ -68,12 +68,14 @@ export const EditorMenu: FC<Props> = ({ 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<Props> = ({ 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(

View file

@ -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,

View file

@ -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,