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] Drilldown on link click #164196

Merged
Show file tree
Hide file tree
Changes from 34 commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
c367b7b
Make dashboard drilldown options a separate component
Heenawter Aug 17, 2023
461feb9
Fix linting
Heenawter Aug 17, 2023
f7e8309
Rename the component
Heenawter Aug 17, 2023
ec4b98e
Add URL options toggles
Heenawter Aug 17, 2023
3953b4f
Add `options` to links
Heenawter Aug 17, 2023
c8d4ab5
First draft of dashboard-to-dashboard links working
Heenawter Aug 21, 2023
4712e57
Fix linting
Heenawter Aug 21, 2023
8e6f266
Another attempt at fixing CI
Heenawter Aug 22, 2023
fd57408
First draft of external URLs working
Heenawter Aug 22, 2023
185f507
Clean up + fix memory leak
Heenawter Aug 23, 2023
5b56e6e
Clean up imports
Heenawter Aug 23, 2023
5dd848b
Rename constants + more clean up
Heenawter Aug 23, 2023
5a1b14f
Final clean up
Heenawter Aug 24, 2023
6e3b3d4
First draft of URL validation
Heenawter Aug 24, 2023
21b2265
Clean up validation
Heenawter Aug 28, 2023
efc4d10
More "final" cleanup :)
Heenawter Aug 28, 2023
938aca2
Merge branch 'navigation-embeddable' of github.com:elastic/kibana int…
Heenawter Aug 28, 2023
426d6e2
`i18n` for missed options string
Heenawter Aug 28, 2023
c01a4d4
Fix Firefox shift+click bug
Heenawter Aug 28, 2023
8fc7bd4
Clean up dashboard links logic
Heenawter Aug 28, 2023
4430c00
Keep options consistent on link type change
Heenawter Aug 29, 2023
895907c
Fix external links not opening in new tab
Heenawter Aug 30, 2023
8f3458a
Fix bug where error state stuck around on link type change
Heenawter Aug 30, 2023
c13723c
Remember destinations on link type change
Heenawter Aug 30, 2023
2f58c31
[CI] Auto-commit changed files from 'node scripts/precommit_hook.js -…
kibanamachine Aug 30, 2023
6edfa42
Switch tooltip to show custom label rather than dashboard title, if p…
Heenawter Aug 31, 2023
b07e180
Update x-pack/plugins/dashboard_enhanced/public/services/drilldowns/a…
Heenawter Aug 31, 2023
a7d719a
Update x-pack/plugins/dashboard_enhanced/public/services/drilldowns/a…
Heenawter Aug 31, 2023
7f28cca
Fix color prop to display correctly in Safari
nickpeihl Aug 31, 2023
eb2e41d
Merge branch 'navigation-embeddable' of github.com:elastic/kibana int…
Heenawter Aug 31, 2023
0d7df01
[CI] Auto-commit changed files from 'node scripts/eslint --no-cache -…
kibanamachine Aug 31, 2023
3ca397b
Fix URL schema + add validation at component level
Heenawter Aug 31, 2023
014d9e4
Merge branch 'nav-link-drilldown_2023-08-16' of github.com:heenawter/…
Heenawter Aug 31, 2023
0ba8b89
[CI] Auto-commit changed files from 'node scripts/eslint --no-cache -…
kibanamachine Aug 31, 2023
6ebc2b1
Update src/plugins/navigation_embeddable/public/components/dashboard_…
nickpeihl Sep 5, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/

import { isQuery, isTimeRange } from '@kbn/data-plugin/common';
import { Filter, isFilterPinned, Query, TimeRange } from '@kbn/es-query';
import { EmbeddableInput, IEmbeddable } from '@kbn/embeddable-plugin/public';
import { DashboardDrilldownOptions } from '@kbn/presentation-util-plugin/public';

import { DashboardAppLocatorParams } from './locator';

interface EmbeddableQueryInput extends EmbeddableInput {
query?: Query;
filters?: Filter[];
timeRange?: TimeRange;
}

export const getEmbeddableParams = (
source: IEmbeddable<EmbeddableQueryInput>,
options: DashboardDrilldownOptions
): Partial<DashboardAppLocatorParams> => {
const params: DashboardAppLocatorParams = {};

const input = source.getInput();
if (isQuery(input.query) && options.useCurrentFilters) {
params.query = input.query;
}

// if useCurrentDashboardDataRange is enabled, then preserve current time range
// if undefined is passed, then destination dashboard will figure out time range itself
// for brush event this time range would be overwritten
if (isTimeRange(input.timeRange) && options.useCurrentDateRange) {
params.timeRange = input.timeRange;
}

// if useCurrentDashboardFilters enabled, then preserve all the filters (pinned, unpinned, and from controls)
// otherwise preserve only pinned
params.filters = options.useCurrentFilters
? input.filters
: input.filters?.filter((f) => isFilterPinned(f));

return params;
};
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ import {
IEmbeddable,
PanelState,
} from '@kbn/embeddable-plugin/public';
import { v4 as uuidv4 } from 'uuid';

import { DashboardPanelState } from '../../../../common';
import { DashboardContainer } from '../dashboard_container';
Expand Down
1 change: 1 addition & 0 deletions src/plugins/dashboard/public/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ export {
type DashboardAppLocatorParams,
cleanEmptyKeys,
} from './dashboard_app/locator/locator';
export { getEmbeddableParams } from './dashboard_app/locator/get_dashboard_locator_params';

export function plugin(initializerContext: PluginInitializerContext) {
return new DashboardPlugin(initializerContext);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export type { NavigationEmbeddableContentType } from '../types';
export type {
NavigationLinkType,
NavigationLayoutType,
NavigationLinkOptions,
NavigationEmbeddableLink,
NavigationEmbeddableItem,
NavigationEmbeddableCrudTypes,
Expand All @@ -24,7 +25,6 @@ export {
DASHBOARD_LINK_TYPE,
NAV_VERTICAL_LAYOUT,
NAV_HORIZONTAL_LAYOUT,
EXTERNAL_LINK_SUPPORTED_PROTOCOLS,
} from './latest';

export * as NavigationEmbeddableV1 from './v1';
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,7 @@ import {
objectTypeToGetResultSchema,
} from '@kbn/content-management-utils';
import { DASHBOARD_LINK_TYPE, EXTERNAL_LINK_TYPE } from '.';
import {
EXTERNAL_LINK_SUPPORTED_PROTOCOLS,
NAV_HORIZONTAL_LAYOUT,
NAV_VERTICAL_LAYOUT,
} from './constants';
import { NAV_HORIZONTAL_LAYOUT, NAV_VERTICAL_LAYOUT } from './constants';

const baseNavigationEmbeddableLinkSchema = {
id: schema.string(),
Expand All @@ -32,12 +28,31 @@ const dashboardLinkSchema = schema.object({
...baseNavigationEmbeddableLinkSchema,
destinationRefName: schema.string(),
type: schema.literal(DASHBOARD_LINK_TYPE),
options: schema.maybe(
schema.object(
{
openInNewTab: schema.boolean(),
useCurrentFilters: schema.boolean(),
useCurrentDateRange: schema.boolean(),
},
{ unknowns: 'forbid' }
)
),
});

const externalLinkSchema = schema.object({
...baseNavigationEmbeddableLinkSchema,
type: schema.literal(EXTERNAL_LINK_TYPE),
destination: schema.uri({ scheme: EXTERNAL_LINK_SUPPORTED_PROTOCOLS }),
destination: schema.string(),
options: schema.maybe(
schema.object(
{
openInNewTab: schema.boolean(),
encodeUrl: schema.boolean(),
},
{ unknowns: 'forbid' }
)
),
});

const navigationEmbeddableAttributesSchema = schema.object(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,3 @@ export const EXTERNAL_LINK_TYPE = 'externalLink';
*/
export const NAV_HORIZONTAL_LAYOUT = 'horizontal';
export const NAV_VERTICAL_LAYOUT = 'vertical';

export const EXTERNAL_LINK_SUPPORTED_PROTOCOLS = ['http', 'https', 'mailto'];
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export type {
NavigationEmbeddableCrudTypes,
NavigationEmbeddableAttributes,
NavigationEmbeddableLink,
NavigationLinkOptions,
NavigationLayoutType,
NavigationLinkType,
} from './types';
Expand All @@ -20,5 +21,4 @@ export {
DASHBOARD_LINK_TYPE,
NAV_VERTICAL_LAYOUT,
NAV_HORIZONTAL_LAYOUT,
EXTERNAL_LINK_SUPPORTED_PROTOCOLS,
} from './constants';
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ import type {
SavedObjectCreateOptions,
SavedObjectUpdateOptions,
} from '@kbn/content-management-utils';
import { type UrlDrilldownOptions } from '@kbn/ui-actions-enhanced-plugin/public';
import { type DashboardDrilldownOptions } from '@kbn/presentation-util-plugin/public';

import { NavigationEmbeddableContentType } from '../../types';
import {
DASHBOARD_LINK_TYPE,
Expand All @@ -35,10 +38,12 @@ export type NavigationEmbeddableCrudTypes = ContentManagementCrudTypes<
*/
export type NavigationLinkType = typeof DASHBOARD_LINK_TYPE | typeof EXTERNAL_LINK_TYPE;

export type NavigationLinkOptions = DashboardDrilldownOptions | UrlDrilldownOptions;
interface BaseNavigationEmbeddableLink {
id: string;
label?: string;
order: number;
options?: NavigationLinkOptions;
destination?: string;
}

Expand Down
9 changes: 4 additions & 5 deletions src/plugins/navigation_embeddable/kibana.jsonc
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,11 @@
"dashboard",
"embeddable",
"kibanaReact",
"presentationUtil"
"presentationUtil",
"uiActionsEnhanced",
"kibanaUtils"
],
"optionalPlugins": ["triggersActionsUi"],
"requiredBundles": [
"savedObjects"
]
"requiredBundles": ["savedObjects"]
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -9,18 +9,24 @@
import classNames from 'classnames';
import useAsync from 'react-use/lib/useAsync';
import React, { useMemo, useState } from 'react';
import useObservable from 'react-use/lib/useObservable';

import { EuiButtonEmpty, EuiListGroupItem, EuiToolTip } from '@elastic/eui';
import {
DashboardDrilldownOptions,
DEFAULT_DASHBOARD_DRILLDOWN_OPTIONS,
} from '@kbn/presentation-util-plugin/public';
import { EuiButtonEmpty, EuiListGroupItem } from '@elastic/eui';
import { DashboardContainer } from '@kbn/dashboard-plugin/public/dashboard_container';

import {
NavigationEmbeddableLink,
NavigationLayoutType,
NAV_VERTICAL_LAYOUT,
NavigationLayoutType,
NavigationEmbeddableLink,
} from '../../../common/content_management';
import { fetchDashboard } from './dashboard_link_tools';
import { coreServices } from '../../services/kibana_services';
import { DashboardLinkStrings } from './dashboard_link_strings';
import { useNavigationEmbeddable } from '../../embeddable/navigation_embeddable';
import { fetchDashboard, getDashboardHref, getDashboardLocator } from './dashboard_link_tools';

export const DashboardLinkComponent = ({
link,
Expand All @@ -33,13 +39,10 @@ export const DashboardLinkComponent = ({
const [error, setError] = useState<Error | undefined>();

const dashboardContainer = navEmbeddable.parent as DashboardContainer;
const parentDashboardTitle = dashboardContainer.select((state) => state.explicitInput.title);
const parentDashboardDescription = dashboardContainer.select(
(state) => state.explicitInput.description
);

const parentDashboardInput = useObservable(dashboardContainer.getInput$());
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This fixes a React unmount state error where, on navigating to a different dashboard, the nav embeddable would try to update the state of the previous nav embeddable even though it was unmounted. Switching to the input observable prevents this.

const parentDashboardId = dashboardContainer.select((state) => state.componentState.lastSavedId);

/** Fetch the dashboard that the link is pointing to */
const { loading: loadingDestinationDashboard, value: destinationDashboard } =
useAsync(async () => {
if (link.id !== parentDashboardId && link.destination) {
Expand All @@ -57,18 +60,20 @@ export const DashboardLinkComponent = ({
}
}, [link, parentDashboardId]);

/**
* Returns the title and description of the dashboard that the link points to; note that, if the link points to
* the current dashboard, then we need to get the most up-to-date information via the `parentDashboardInput` - this
* will respond to changes so that the link label/tooltip remains in sync with the dashboard title/description.
*/
const [dashboardTitle, dashboardDescription] = useMemo(() => {
return link.destination === parentDashboardId
? [parentDashboardTitle, parentDashboardDescription]
? [parentDashboardInput?.title, parentDashboardInput?.description]
: [destinationDashboard?.attributes.title, destinationDashboard?.attributes.description];
}, [
link.destination,
parentDashboardId,
parentDashboardTitle,
destinationDashboard,
parentDashboardDescription,
]);
}, [link.destination, parentDashboardId, parentDashboardInput, destinationDashboard]);

/**
* Memoized link information
*/
const linkLabel = useMemo(() => {
return link.label || (dashboardTitle ?? DashboardLinkStrings.getDashboardErrorLabel());
}, [link, dashboardTitle]);
Expand All @@ -81,10 +86,56 @@ export const DashboardLinkComponent = ({
};
}
return {
tooltipTitle: Boolean(dashboardDescription) ? dashboardTitle : undefined,
tooltipMessage: dashboardDescription || dashboardTitle,
tooltipTitle: Boolean(dashboardDescription) ? linkLabel : undefined,
tooltipMessage: dashboardDescription || linkLabel,
};
}, [error, linkLabel, dashboardDescription]);

/**
* Dashboard-to-dashboard navigation
*/
const { loading: loadingOnClickProps, value: onClickProps } = useAsync(async () => {
/** If the link points to the current dashboard, then there should be no `onClick` or `href` prop */
if (link.destination === parentDashboardId) return;

const linkOptions = {
...DEFAULT_DASHBOARD_DRILLDOWN_OPTIONS,
...link.options,
} as DashboardDrilldownOptions;

const locator = await getDashboardLocator({
link: { ...link, options: linkOptions },
navEmbeddable,
});
if (!locator) return;

const href = getDashboardHref(locator);
return {
href,
Copy link
Contributor Author

@Heenawter Heenawter Aug 30, 2023

Choose a reason for hiding this comment

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

href is always necessary so that (a) right clicking the link gives the "Open in new <tab/window>" option and (b) modified clicking behaviour (i.e. shift clicking, ctrl clicking, etc.) is consistent across browsers.

Aug-30-2023 13-36-57

onClick: async (event: React.MouseEvent) => {
/**
* If the link is being opened via a modified click, then we should use the default `href` navigation behaviour
* by passing all the dashboard state via the URL - this will keep behaviour consistent across all browsers.
*/
const modifiedClick = event.ctrlKey || event.metaKey || event.shiftKey;
if (modifiedClick) {
return;
}

/** Otherwise, prevent the default behaviour and handle click depending on `openInNewTab` option */
event.preventDefault();
if (linkOptions.openInNewTab) {
window.open(href, '_blank');
} else {
const { app, path, state } = locator;
await coreServices.application.navigateToApp(app, {
path,
state,
});
}
},
};
}, [error, dashboardTitle, dashboardDescription]);
}, [link]);

return loadingDestinationDashboard ? (
<li id={`dashboardLink--${link.id}--loading`}>
Expand All @@ -96,37 +147,25 @@ export const DashboardLinkComponent = ({
<EuiListGroupItem
size="s"
color="text"
isDisabled={Boolean(error)}
{...onClickProps}
id={`dashboardLink--${link.id}`}
showToolTip={Boolean(error)}
toolTipProps={{
title: tooltipTitle,
content: tooltipMessage,
position: layout === NAV_VERTICAL_LAYOUT ? 'right' : 'bottom',
repositionOnScroll: true,
delay: 'long',
}}
iconType={error ? 'warning' : undefined}
iconProps={{ className: 'dashboardLinkIcon' }}
isDisabled={Boolean(error) || loadingOnClickProps}
className={classNames('navigationLink', {
navigationLinkCurrent: link.destination === parentDashboardId,
dashboardLinkError: Boolean(error),
'dashboardLinkError--noLabel': !link.label,
})}
onClick={
link.destination === parentDashboardId
? undefined
: () => {
// TODO: As part of https://github.com/elastic/kibana/issues/154381, connect to drilldown
}
}
label={
<EuiToolTip
delay="long"
display="block"
repositionOnScroll
position={layout === NAV_VERTICAL_LAYOUT ? 'right' : 'bottom'}
title={tooltipTitle}
content={tooltipMessage}
>
{/* Setting `title=""` so that the native browser tooltip is disabled */}
<div className="eui-textTruncate" title="">
{linkLabel}
</div>
</EuiToolTip>
}
label={linkLabel}
/>
);
};
Loading