Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Dashboard Navigation] Add Links to Visualization library #170810

Merged
merged 17 commits into from Nov 22, 2023
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Expand Up @@ -57,15 +57,15 @@ export function ItemDetails<T extends UserContentCommonSchema>({
);

const onClickTitleHandler = useMemo(() => {
if (!onClickTitle) {
if (!onClickTitle || getDetailViewLink?.(item)) {
return undefined;
}

return ((e) => {
e.preventDefault();
onClickTitle(item);
}) as React.MouseEventHandler<HTMLAnchorElement>;
}, [item, onClickTitle]);
}, [item, onClickTitle, getDetailViewLink]);

const renderTitle = useCallback(() => {
const href = getDetailViewLink ? getDetailViewLink(item) : undefined;
Expand All @@ -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('-')}`}
>
Expand Down
Expand Up @@ -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.`
Expand Down
Expand Up @@ -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 ('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';
Expand Down
Expand Up @@ -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);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since we are relying on the embeddable factory create method for the link embeddable, we need to hide the visualization alias item from the dropdown.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good workaround. I'm thinking that as part of the Embeddable refactor, we should introduce a separate registry for creation buttons in the Dashboard top nav. That would make this more explicit and unified.


const factories = unwrappedEmbeddableFactories.filter(
({ isEditable, factory: { type, canCreateNew, isContainerType } }) =>
Expand Down
Expand Up @@ -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 {
Expand Down Expand Up @@ -53,7 +53,7 @@ export function DashboardEmptyScreen() {
const originatingApp = embeddableAppContext?.currentAppId;

const goToLens = useCallback(() => {
if (!lensAlias || !lensAlias.aliasPath) return;
if (!lensAlias || !('path' in lensAlias.alias)) return;
const trackUiMetric = usageCollection.reportUiCounter?.bind(
usageCollection,
DASHBOARD_UI_METRIC_ID
Expand All @@ -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,
Expand Down
3 changes: 2 additions & 1 deletion src/plugins/links/kibana.jsonc
Expand Up @@ -12,9 +12,10 @@
"dashboard",
"embeddable",
"kibanaReact",
"kibanaUtils",
"presentationUtil",
"uiActionsEnhanced",
"kibanaUtils"
"visualizations"
],
"optionalPlugins": ["triggersActionsUi"],
"requiredBundles": ["savedObjects"]
Expand Down
Expand Up @@ -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) => {
Expand Down Expand Up @@ -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>;
}
43 changes: 32 additions & 11 deletions src/plugins/links/public/editor/open_editor_flyout.tsx
Expand Up @@ -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'));

Expand All @@ -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) {
/**
Expand All @@ -55,6 +58,22 @@ export async function openEditorFlyout(
}

return new Promise((resolve, reject) => {
const closeEditorFlyout = (editorFlyout: OverlayRef) => {
if (overlayTracker) {
overlayTracker.clearOverlays();
} else {
editorFlyout.close();
}
};
Comment on lines +61 to +67
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can no longer assume that the overlayTracker (from the Dashboard app) exists, since this flyout can now be opened outside of the Dashboard app - therefore, if the overlayTracker does not exist, we need to rely on the normal flyout close method.


/**
* 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,
Expand All @@ -74,7 +93,7 @@ export async function openEditorFlyout(
attributes: newAttributes,
});
parentDashboard?.reload();
if (overlayTracker) overlayTracker.clearOverlays();
closeEditorFlyout(editorFlyout);
};

const onAddToDashboard = (newLinks: Link[], newLayout: LinksLayoutType) => {
Expand All @@ -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(
Expand All @@ -125,6 +144,8 @@ export async function openEditorFlyout(
}
);

if (overlayTracker) overlayTracker.openOverlay(editorFlyout);
if (overlayTracker) {
overlayTracker.openOverlay(editorFlyout);
}
});
}
Expand Up @@ -128,8 +128,6 @@ export class LinksFactoryDefinition
initialInput: LinksInput,
parent?: DashboardContainer
): Promise<LinksEditorFlyoutReturn> {
if (!parent) return { newInput: {} };
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similar to https://github.com/elastic/kibana/pull/170810/files#r1393019545, since the Link embeddable can now be edited outside of the Dashboard app, it is no longer considered invalid for a Link embeddable to not have a parent.

Note that this early return was actually unnecessary in the first place, because we already treat the parent as potentially undefined in the rest of the code - so, it's safe to remove.


const { openEditorFlyout } = await import('../editor/open_editor_flyout');

const { newInput, attributes } = await openEditorFlyout(
Expand Down
42 changes: 39 additions & 3 deletions src/plugins/links/public/plugin.ts
Expand Up @@ -6,22 +6,26 @@
* 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 { 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 { setKibanaServices } from './services/kibana_services';

export interface LinksSetupDependencies {
embeddable: EmbeddableSetup;
visualizations: VisualizationsSetup;
contentManagement: ContentManagementPublicSetup;
}

Expand All @@ -48,6 +52,38 @@ export class LinksPlugin
},
name: APP_NAME,
});

plugins.visualizations.registerAlias({
alias: { embeddableType: CONTENT_ID },
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,
description,
updatedAt,
icon: APP_ICON,
typeTitle: APP_NAME,
stage: 'experimental',
savedObjectType: type,
};
},
},
},
});
});
}

Expand Down
4 changes: 3 additions & 1 deletion src/plugins/links/tsconfig.json
Expand Up @@ -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/**/*"]
}
Expand Up @@ -19,7 +19,7 @@ import { BaseVisType } from './base_vis_type';
export type VisualizationStage = 'experimental' | 'beta' | 'production';

export interface VisualizationListItem {
editUrl: string;
editUrl?: string;
editApp?: string;
error?: string;
icon: string;
Expand Down Expand Up @@ -86,8 +86,17 @@ export interface VisualizationsAppExtension {
}

export interface VisTypeAlias {
aliasPath: string;
aliasApp: string;
alias:
| {
app: string;
path: string;
}
/**
* Use this when your visualization uses inline editing and does not have a specific app
* to redirect to. This will default to the embeddable factory's editing method (`getExplicitInput`),
* which should handle the inline editing.
*/
| { embeddableType: string };
Heenawter marked this conversation as resolved.
Show resolved Hide resolved
name: string;
title: string;
icon: string;
Expand Down
Expand Up @@ -11,7 +11,7 @@

.visListingTable__experimentalIcon {
width: $euiSizeL;
vertical-align: baseline;
vertical-align: middle;
padding: 0 $euiSizeS;
margin-left: $euiSizeS;
}
Expand Down