[Serverless] Partially fix lens/maps/visualize breadcrumbs missing title (#163476)

## Summary

Partially address https://github.com/elastic/kibana/issues/163337 for
lens, visualize, maps

### Context:

In serverless navigation, we changed how breadcrumbs work. Instead of
setting the full path manually, we automatically calculate the main
parts of the path from the side nav + current URL. This was done to keep
side nav and breadcrumbs in sync as much as possible and solve
consistency issues with breadcrumbs across apps.

https://docs.elastic.dev/kibana-dev-docs/serverless-project-navigation#breadcrumbs

Apps can append custom deeper context using the
`serverless.setBreadcrumbs` API. Regular `core.chrome.setBreadcrumbs`
has no effect when the serverless nav is rendered.

### Fix

This PR fixes lens, visualize, and maps to add "title" breadcrumb in
serverless. **Unfortunately, it doesn't fully restore the full
breadcrumbs functionality visualize/maps/lens have in the non-serverless
Kibana:**

In the non-serverless Kibana lens/visualize/maps have sophisticated
breadcrumbs where context takes into account `ByValue` and
`originatingApp` and can switch depending on the context. For example,
if the user is coming from "Dashboard" to edit "byValue" Lens
visualization, Lens breadcrumbs display "Dashboard > Create", instead of
"Visualization > Create".

Currently, we can't repeat this behavior with serverless breadcrumbs
because the context is set by the navigation config, e.g.:


9538fab090/x-pack/plugins/serverless_observability/public/components/side_navigation/index.tsx (L136-L141)

In this PR I attempt to do a quick fix for the serverless breadcrumbs by
simply appending the last ("title") part of the breadcrumb. In a follow
up we need to think about how to bring back the original breadcrumbs
functionality with changing `Visualize <-> Dashboard` context. We also
will need to figure out how to sync the changing context with the side
nav, as we don't want to show "Dashboard" in the breadcrumb, but have
"Visualization" highlighted in the side nav. Here is the issue:
https://github.com/elastic/kibana/issues/163488
This commit is contained in:
Anton Dosov 2023-08-11 10:33:50 +02:00 committed by GitHub
parent 5de69cb567
commit e944a19cbd
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
22 changed files with 200 additions and 40 deletions

View file

@ -33,7 +33,8 @@
"home",
"share",
"spaces",
"savedObjectsTaggingOss"
"savedObjectsTaggingOss",
"serverless"
],
"requiredBundles": [
"kibanaUtils",

View file

@ -59,6 +59,7 @@ import type { SpacesPluginStart } from '@kbn/spaces-plugin/public';
import type { DataViewEditorStart } from '@kbn/data-view-editor-plugin/public';
import { SavedObjectsManagementPluginStart } from '@kbn/saved-objects-management-plugin/public';
import type { SavedSearchPublicPluginStart } from '@kbn/saved-search-plugin/public';
import type { ServerlessPluginStart } from '@kbn/serverless/public';
import {
ContentManagementPublicSetup,
ContentManagementPublicStart,
@ -164,6 +165,7 @@ export interface VisualizationsStartDeps {
usageCollection: UsageCollectionStart;
savedObjectsManagement: SavedObjectsManagementPluginStart;
contentManagement: ContentManagementPublicStart;
serverless?: ServerlessPluginStart;
}
/**
@ -327,6 +329,7 @@ export class VisualizationsPlugin
visEditorsRegistry,
listingViewRegistry,
unifiedSearch: pluginsStart.unifiedSearch,
serverless: pluginsStart.serverless,
};
params.element.classList.add('visAppWrapper');

View file

@ -270,6 +270,7 @@ export const VisualizeListing = () => {
uiSettings,
kbnUrlStateStorage,
listingViewRegistry,
serverless,
},
} = useKibana<VisualizeServices>();
const { pathname } = useLocation();
@ -298,13 +299,20 @@ export const VisualizeListing = () => {
useMount(() => {
// Reset editor state for all apps if the visualize listing page is loaded.
stateTransferService.clearEditorState();
chrome.setBreadcrumbs([
{
text: i18n.translate('visualizations.visualizeListingBreadcrumbsTitle', {
defaultMessage: 'Visualize Library',
}),
},
]);
if (serverless?.setBreadcrumbs) {
// reset any deeper context breadcrumbs
// "Visualization" breadcrumb is set automatically by the serverless navigation
serverless.setBreadcrumbs([]);
} else {
chrome.setBreadcrumbs([
{
text: i18n.translate('visualizations.visualizeListingBreadcrumbsTitle', {
defaultMessage: 'Visualize Library',
}),
},
]);
}
chrome.docTitle.change(
i18n.translate('visualizations.listingPageTitle', { defaultMessage: 'Visualize Library' })
);

View file

@ -40,6 +40,7 @@ import type { PresentationUtilPluginStart } from '@kbn/presentation-util-plugin/
import type { SpacesPluginStart } from '@kbn/spaces-plugin/public';
import type { SavedObjectsTaggingApi } from '@kbn/saved-objects-tagging-oss-plugin/public';
import type { SavedSearch, SavedSearchPublicPluginStart } from '@kbn/saved-search-plugin/public';
import type { ServerlessPluginStart } from '@kbn/serverless/public';
import type {
Vis,
VisualizeEmbeddableContract,
@ -115,6 +116,7 @@ export interface VisualizeServices extends CoreStart {
visEditorsRegistry: VisEditorsRegistry;
listingViewRegistry: ListingViewRegistry;
unifiedSearch: UnifiedSearchPublicPluginStart;
serverless?: ServerlessPluginStart;
}
export interface VisInstance {

View file

@ -45,6 +45,28 @@ export function getCreateBreadcrumbs({
];
}
export function getCreateServerlessBreadcrumbs({
byValue,
originatingAppName,
redirectToOrigin,
}: {
byValue?: boolean;
originatingAppName?: string;
redirectToOrigin?: () => void;
}) {
// TODO: https://github.com/elastic/kibana/issues/163488
// for now, serverless breadcrumbs only set the title,
// the rest of the breadcrumbs are handled by the serverless navigation
// the serverless navigation is not yet aware of the byValue/originatingApp context
return [
{
text: i18n.translate('visualizations.editor.createBreadcrumb', {
defaultMessage: 'Create',
}),
},
];
}
export function getEditBreadcrumbs(
{
byValue,
@ -65,3 +87,26 @@ export function getEditBreadcrumbs(
},
];
}
export function getEditServerlessBreadcrumbs(
{
byValue,
originatingAppName,
redirectToOrigin,
}: {
byValue?: boolean;
originatingAppName?: string;
redirectToOrigin?: () => void;
},
title: string = defaultEditText
) {
// TODO: https://github.com/elastic/kibana/issues/163488
// for now, serverless breadcrumbs only set the title,
// the rest of the breadcrumbs are handled by the serverless navigation
// the serverless navigation is not yet aware of the byValue/originatingApp context
return [
{
text: title,
},
];
}

View file

@ -36,7 +36,7 @@ import {
VisualizeEditorVisInstance,
} from '../types';
import { VisualizeConstants } from '../../../common/constants';
import { getEditBreadcrumbs } from './breadcrumbs';
import { getEditBreadcrumbs, getEditServerlessBreadcrumbs } from './breadcrumbs';
import { VISUALIZE_APP_LOCATOR, VisualizeLocatorParams } from '../../../common/locator';
import { getUiActions } from '../../services';
import { VISUALIZE_EDITOR_TRIGGER, AGG_BASED_VISUALIZATION_TRIGGER } from '../../triggers';
@ -117,6 +117,7 @@ export const getTopNavConfig = (
savedObjectsTagging,
presentationUtil,
getKibanaVersion,
serverless,
}: VisualizeServices
) => {
const { vis, embeddableHandler } = visInstance;
@ -202,7 +203,11 @@ export const getTopNavConfig = (
stateTransfer.clearEditorState(VisualizeConstants.APP_ID);
}
chrome.docTitle.change(savedVis.lastSavedTitle);
chrome.setBreadcrumbs(getEditBreadcrumbs({}, savedVis.lastSavedTitle));
if (serverless?.setBreadcrumbs) {
serverless.setBreadcrumbs(getEditServerlessBreadcrumbs({}, savedVis.lastSavedTitle));
} else {
chrome.setBreadcrumbs(getEditBreadcrumbs({}, savedVis.lastSavedTitle));
}
if (id !== visualizationIdFromUrl) {
history.replace({

View file

@ -12,7 +12,12 @@ import { parse } from 'query-string';
import { i18n } from '@kbn/i18n';
import { getVisualizationInstance } from '../get_visualization_instance';
import { getEditBreadcrumbs, getCreateBreadcrumbs } from '../breadcrumbs';
import {
getEditBreadcrumbs,
getCreateBreadcrumbs,
getCreateServerlessBreadcrumbs,
getEditServerlessBreadcrumbs,
} from '../breadcrumbs';
import { SavedVisInstance, VisualizeServices, IEditorController } from '../../types';
import { VisualizeConstants } from '../../../../common/constants';
import { getTypes } from '../../../services';
@ -46,6 +51,7 @@ export const useSavedVisInstance = (
stateTransferService,
visEditorsRegistry,
application: { navigateToApp },
serverless,
} = services;
const getSavedVisInstance = async () => {
try {
@ -104,18 +110,35 @@ export const useSavedVisInstance = (
const redirectToOrigin = originatingApp ? () => navigateToApp(originatingApp) : undefined;
if (savedVis.id) {
chrome.setBreadcrumbs(
getEditBreadcrumbs({ originatingAppName, redirectToOrigin }, savedVis.title)
);
if (serverless?.setBreadcrumbs) {
serverless.setBreadcrumbs(
getEditServerlessBreadcrumbs({ originatingAppName, redirectToOrigin }, savedVis.title)
);
} else {
chrome.setBreadcrumbs(
getEditBreadcrumbs({ originatingAppName, redirectToOrigin }, savedVis.title)
);
}
chrome.docTitle.change(savedVis.title);
} else {
chrome.setBreadcrumbs(
getCreateBreadcrumbs({
byValue: Boolean(originatingApp),
originatingAppName,
redirectToOrigin,
})
);
if (serverless?.setBreadcrumbs) {
serverless.setBreadcrumbs(
getCreateServerlessBreadcrumbs({
byValue: Boolean(originatingApp),
originatingAppName,
redirectToOrigin,
})
);
} else {
chrome.setBreadcrumbs(
getCreateBreadcrumbs({
byValue: Boolean(originatingApp),
originatingAppName,
redirectToOrigin,
})
);
}
}
let visEditorController;

View file

@ -11,7 +11,7 @@ import { useEffect, useRef, useState } from 'react';
import { VisualizeInput } from '../../..';
import { ByValueVisInstance, VisualizeServices, IEditorController } from '../../types';
import { getVisualizationInstanceFromInput } from '../get_visualization_instance';
import { getEditBreadcrumbs } from '../breadcrumbs';
import { getEditBreadcrumbs, getEditServerlessBreadcrumbs } from '../breadcrumbs';
export const useVisByValue = (
services: VisualizeServices,
@ -33,6 +33,7 @@ export const useVisByValue = (
application: { navigateToApp },
stateTransferService,
visEditorsRegistry,
serverless,
} = services;
const getVisInstance = async () => {
if (!valueInput || loaded.current || !visEditorRef.current) {
@ -59,9 +60,16 @@ export const useVisByValue = (
const redirectToOrigin = originatingApp
? () => navigateToApp(originatingApp, { path: originatingPath })
: undefined;
chrome?.setBreadcrumbs(
getEditBreadcrumbs({ byValue: true, originatingAppName, redirectToOrigin })
);
if (serverless?.setBreadcrumbs) {
serverless.setBreadcrumbs(
getEditServerlessBreadcrumbs({ byValue: true, originatingAppName, redirectToOrigin })
);
} else {
chrome?.setBreadcrumbs(
getEditBreadcrumbs({ byValue: true, originatingAppName, redirectToOrigin })
);
}
loaded.current = true;
setState({

View file

@ -61,7 +61,8 @@
"@kbn/content-management-table-list-view-table",
"@kbn/content-management-tabbed-table-list-view",
"@kbn/content-management-table-list-view",
"@kbn/content-management-utils"
"@kbn/content-management-utils",
"@kbn/serverless"
],
"exclude": [
"target/**/*",

View file

@ -45,7 +45,8 @@
"taskManager",
"globalSearch",
"savedObjectsTagging",
"spaces"
"spaces",
"serverless"
],
"requiredBundles": [
"unifiedSearch",

View file

@ -33,6 +33,7 @@ import { TopNavMenuData } from '@kbn/navigation-plugin/public';
import { LensByValueInput } from '../embeddable/embeddable';
import { SavedObjectReference } from '@kbn/core/types';
import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public';
import { serverlessMock } from '@kbn/serverless/public/mocks';
import moment from 'moment';
import { setState, LensAppState } from '../state_management';
@ -365,6 +366,30 @@ describe('Lens App', () => {
{ text: 'Daaaaaaadaumching!' },
]);
});
it('sets serverless breadcrumbs when the document title changes when serverless service is available', async () => {
const serverless = serverlessMock.createStart();
const { instance, services, lensStore } = await mountWith({
services: {
...makeDefaultServices(),
serverless,
},
});
expect(services.chrome.setBreadcrumbs).not.toHaveBeenCalled();
expect(serverless.setBreadcrumbs).toHaveBeenCalledWith({ text: 'Create' });
await act(async () => {
instance.setProps({ initialInput: { savedObjectId: breadcrumbDocSavedObjectId } });
lensStore.dispatch(
setState({
persistedDoc: breadcrumbDoc,
})
);
});
expect(services.chrome.setBreadcrumbs).not.toHaveBeenCalled();
expect(serverless.setBreadcrumbs).toHaveBeenCalledWith({ text: 'Daaaaaaadaumching!' });
});
});
describe('TopNavMenu#showDatePicker', () => {

View file

@ -93,6 +93,7 @@ export function App({
dashboardFeatureFlag,
locator,
share,
serverless,
} = lensAppServices;
const saveAndExit = useRef<() => void>();
@ -288,8 +289,18 @@ export function App({
},
});
}
breadcrumbs.push({ text: currentDocTitle });
chrome.setBreadcrumbs(breadcrumbs);
const currentDocBreadcrumb: EuiBreadcrumb = { text: currentDocTitle };
breadcrumbs.push(currentDocBreadcrumb);
if (serverless?.setBreadcrumbs) {
// TODO: https://github.com/elastic/kibana/issues/163488
// for now, serverless breadcrumbs only set the title,
// the rest of the breadcrumbs are handled by the serverless navigation
// the serverless navigation is not yet aware of the byValue/originatingApp context
serverless.setBreadcrumbs(currentDocBreadcrumb);
} else {
chrome.setBreadcrumbs(breadcrumbs);
}
}, [
dashboardFeatureFlag.allowByValueEmbeddables,
getOriginatingAppName,
@ -300,6 +311,7 @@ export function App({
isLinkedToOriginatingApp,
persistedDoc,
initialContext,
serverless,
]);
const switchDatasource = useCallback(() => {

View file

@ -101,6 +101,7 @@ export async function getLensServices(
spaces,
share,
unifiedSearch,
serverless,
} = startDependencies;
const storage = new Storage(localStorage);
@ -147,6 +148,7 @@ export async function getLensServices(
unifiedSearch,
docLinks: coreStart.docLinks,
locator,
serverless,
};
}

View file

@ -47,6 +47,7 @@ import type { DocLinksStart } from '@kbn/core-doc-links-browser';
import type { SharePluginStart } from '@kbn/share-plugin/public';
import type { EventAnnotationServiceType } from '@kbn/event-annotation-plugin/public';
import type { SettingsStart } from '@kbn/core-ui-settings-browser';
import type { ServerlessPluginStart } from '@kbn/serverless/public';
import type {
DatasourceMap,
EditorFrameInstance,
@ -174,6 +175,7 @@ export interface LensAppServices {
dataViewFieldEditor: IndexPatternFieldEditorStart;
locator?: LensAppLocator;
savedObjectStore: SavedObjectIndexStore;
serverless?: ServerlessPluginStart;
}
interface TopNavAction {

View file

@ -63,6 +63,7 @@ import {
ContentManagementPublicStart,
} from '@kbn/content-management-plugin/public';
import { i18n } from '@kbn/i18n';
import type { ServerlessPluginStart } from '@kbn/serverless/public';
import type { EditorFrameService as EditorFrameServiceType } from './editor_frame_service';
import type {
FormBasedDatasource as FormBasedDatasourceType,
@ -168,6 +169,7 @@ export interface LensPluginStartDependencies {
share?: SharePluginStart;
eventAnnotationService: EventAnnotationServiceType;
contentManagement: ContentManagementPublicStart;
serverless?: ServerlessPluginStart;
}
export interface LensPublicSetup {

View file

@ -84,6 +84,7 @@
"@kbn/core-theme-browser-mocks",
"@kbn/event-annotation-components",
"@kbn/content-management-utils",
"@kbn/serverless",
],
"exclude": [
"target/**/*",

View file

@ -41,7 +41,8 @@
"screenshotMode",
"security",
"spaces",
"usageCollection"
"usageCollection",
"serverless"
],
"requiredBundles": [
"kibanaReact",

View file

@ -76,6 +76,7 @@ export const getContentManagement = () => pluginsStart.contentManagement;
export const isScreenshotMode = () => {
return pluginsStart.screenshotMode ? pluginsStart.screenshotMode.isScreenshotMode() : false;
};
export const getServerless = () => pluginsStart.serverless;
// xpack.maps.* kibana.yml settings from this plugin
let mapAppConfig: MapsConfigType;

View file

@ -45,6 +45,7 @@ import type {
ContentManagementPublicSetup,
ContentManagementPublicStart,
} from '@kbn/content-management-plugin/public';
import type { ServerlessPluginStart } from '@kbn/serverless/public';
import {
createRegionMapFn,
@ -121,6 +122,7 @@ export interface MapsPluginStartDependencies {
contentManagement: ContentManagementPublicStart;
screenshotMode?: ScreenshotModePluginSetup;
usageCollection?: UsageCollectionSetup;
serverless?: ServerlessPluginStart;
}
/**

View file

@ -21,6 +21,7 @@ import {
getNavigateToApp,
getUiSettings,
getUsageCollection,
getServerless,
} from '../../kibana_services';
import { mapsClient } from '../../content_management';
@ -78,7 +79,11 @@ function MapsListViewComp({ history }: Props) {
// wrap chrome updates in useEffect to avoid potentially causing state changes in other component during render phase.
useEffect(() => {
getCoreChrome().docTitle.change(APP_NAME);
getCoreChrome().setBreadcrumbs([{ text: APP_NAME }]);
if (getServerless()) {
getServerless()!.setBreadcrumbs({ text: APP_NAME });
} else {
getCoreChrome().setBreadcrumbs([{ text: APP_NAME }]);
}
}, []);
const findMaps = useCallback(

View file

@ -45,6 +45,7 @@ import {
getSavedObjectsTagging,
getTimeFilter,
getUsageCollection,
getServerless,
} from '../../../kibana_services';
import { LayerDescriptor } from '../../../../common/descriptor_types';
import { copyPersistentState } from '../../../reducers/copy_persistent_state';
@ -331,15 +332,23 @@ export class SavedMap {
throw new Error('Invalid usage, must await whenReady before calling hasUnsavedChanges');
}
const breadcrumbs = getBreadcrumbs({
pageTitle: this._getPageTitle(),
isByValue: this.isByValue(),
getHasUnsavedChanges: this.hasUnsavedChanges,
originatingApp: this._originatingApp,
getAppNameFromId: this._getStateTransfer().getAppNameFromId,
history,
});
getCoreChrome().setBreadcrumbs(breadcrumbs);
if (getServerless()) {
// TODO: https://github.com/elastic/kibana/issues/163488
// for now, serverless breadcrumbs only set the title,
// the rest of the breadcrumbs are handled by the serverless navigation
// the serverless navigation is not yet aware of the byValue/originatingApp context
getServerless()!.setBreadcrumbs({ text: this._getPageTitle() });
} else {
const breadcrumbs = getBreadcrumbs({
pageTitle: this._getPageTitle(),
isByValue: this.isByValue(),
getHasUnsavedChanges: this.hasUnsavedChanges,
originatingApp: this._originatingApp,
getAppNameFromId: this._getStateTransfer().getAppNameFromId,
history,
});
getCoreChrome().setBreadcrumbs(breadcrumbs);
}
}
public getSavedObjectId(): string | undefined {

View file

@ -72,6 +72,7 @@
"@kbn/core-http-common",
"@kbn/content-management-table-list-view-table",
"@kbn/content-management-table-list-view",
"@kbn/serverless",
],
"exclude": [
"target/**/*",