mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 09:19:04 -04:00
[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:
parent
74dea1e2c9
commit
8eaebb6d47
25 changed files with 228 additions and 105 deletions
|
@ -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('-')}`}
|
||||
>
|
||||
|
|
|
@ -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.`
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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 } }) =>
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -12,9 +12,10 @@
|
|||
"dashboard",
|
||||
"embeddable",
|
||||
"kibanaReact",
|
||||
"kibanaUtils",
|
||||
"presentationUtil",
|
||||
"uiActionsEnhanced",
|
||||
"kibanaUtils"
|
||||
"visualizations"
|
||||
],
|
||||
"optionalPlugins": ["triggersActionsUi"],
|
||||
"requiredBundles": ["savedObjects"]
|
||||
|
|
|
@ -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>;
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -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/**/*"]
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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[];
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -11,7 +11,7 @@
|
|||
|
||||
.visListingTable__experimentalIcon {
|
||||
width: $euiSizeL;
|
||||
vertical-align: baseline;
|
||||
vertical-align: middle;
|
||||
padding: 0 $euiSizeS;
|
||||
margin-left: $euiSizeS;
|
||||
}
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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[];
|
||||
|
|
|
@ -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={
|
||||
<>
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue