mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 09:19:04 -04:00
[Dashboard] Interactive Managed Dashboard Popover vs badge (#189404)
## Summary Closes #https://github.com/elastic/kibana/issues/179152 This PR makes the managed badge into an interactive popover that allows users to duplicate the managed dashboard. The user is automatically redirected to the new dashboard where they can edit it and make changes. ### Checklist - [x] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md) - [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))
This commit is contained in:
parent
22031dfb05
commit
5bd3f902f8
12 changed files with 175 additions and 44 deletions
|
@ -9,9 +9,10 @@ import { i18n } from '@kbn/i18n';
|
|||
import type { EuiToolTipProps } from '@elastic/eui';
|
||||
import type { TopNavMenuBadgeProps } from '@kbn/navigation-plugin/public';
|
||||
|
||||
export const getManagedContentBadge: (tooltipText: string) => TopNavMenuBadgeProps = (
|
||||
tooltipText
|
||||
) => ({
|
||||
export const getManagedContentBadge: (
|
||||
tooltipText: string,
|
||||
disableTooltipProps?: boolean
|
||||
) => TopNavMenuBadgeProps = (tooltipText, enableTooltipProps = true) => ({
|
||||
'data-test-subj': 'managedContentBadge',
|
||||
badgeText: i18n.translate('managedContentBadge.text', {
|
||||
defaultMessage: 'Managed',
|
||||
|
@ -21,8 +22,10 @@ export const getManagedContentBadge: (tooltipText: string) => TopNavMenuBadgePro
|
|||
}),
|
||||
color: 'primary',
|
||||
iconType: 'glasses',
|
||||
toolTipProps: {
|
||||
content: tooltipText,
|
||||
position: 'bottom',
|
||||
} as EuiToolTipProps,
|
||||
toolTipProps: enableTooltipProps
|
||||
? ({
|
||||
content: tooltipText,
|
||||
position: 'bottom',
|
||||
} as EuiToolTipProps)
|
||||
: undefined,
|
||||
});
|
||||
|
|
|
@ -26,9 +26,13 @@ export const dashboardReadonlyBadge = {
|
|||
};
|
||||
|
||||
export const dashboardManagedBadge = {
|
||||
getTooltip: () =>
|
||||
i18n.translate('dashboard.badge.managed.tooltip', {
|
||||
defaultMessage: 'Elastic manages this dashboard. Clone it to make changes.',
|
||||
getDuplicateButtonAriaLabel: () =>
|
||||
i18n.translate('dashboard.managedContentPopoverFooterText', {
|
||||
defaultMessage: 'Click here to duplicate this dashboard',
|
||||
}),
|
||||
getBadgeAriaLabel: () =>
|
||||
i18n.translate('dashboard.managedContentBadge.ariaLabel', {
|
||||
defaultMessage: 'Elastic manages this dashboard. Duplicate it to make changes.',
|
||||
}),
|
||||
};
|
||||
|
||||
|
|
|
@ -18,19 +18,18 @@ import { topNavStrings } from '../_dashboard_app_strings';
|
|||
import { ShowShareModal } from './share/show_share_modal';
|
||||
import { pluginServices } from '../../services/plugin_services';
|
||||
import { CHANGE_CHECK_DEBOUNCE } from '../../dashboard_constants';
|
||||
import { DashboardRedirect } from '../../dashboard_container/types';
|
||||
import { SaveDashboardReturn } from '../../services/dashboard_content_management/types';
|
||||
import { confirmDiscardUnsavedChanges } from '../../dashboard_listing/confirm_overlays';
|
||||
import { SaveDashboardReturn } from '../../services/dashboard_content_management/types';
|
||||
|
||||
export const useDashboardMenuItems = ({
|
||||
redirectTo,
|
||||
isLabsShown,
|
||||
setIsLabsShown,
|
||||
maybeRedirect,
|
||||
showResetChange,
|
||||
}: {
|
||||
redirectTo: DashboardRedirect;
|
||||
isLabsShown: boolean;
|
||||
setIsLabsShown: Dispatch<SetStateAction<boolean>>;
|
||||
maybeRedirect: (result?: SaveDashboardReturn) => void;
|
||||
showResetChange?: boolean;
|
||||
}) => {
|
||||
const [isSaveInProgress, setIsSaveInProgress] = useState(false);
|
||||
|
@ -78,22 +77,6 @@ export const useDashboardMenuItems = ({
|
|||
[dashboardTitle, hasUnsavedChanges, lastSavedId, dashboard]
|
||||
);
|
||||
|
||||
const maybeRedirect = useCallback(
|
||||
(result?: SaveDashboardReturn) => {
|
||||
if (!result) return;
|
||||
const { redirectRequired, id } = result;
|
||||
if (redirectRequired) {
|
||||
redirectTo({
|
||||
id,
|
||||
editMode: true,
|
||||
useReplace: true,
|
||||
destination: 'dashboard',
|
||||
});
|
||||
}
|
||||
},
|
||||
[redirectTo]
|
||||
);
|
||||
|
||||
/**
|
||||
* Save the dashboard without any UI or popups.
|
||||
*/
|
||||
|
|
|
@ -261,6 +261,7 @@ export async function runInteractiveSave(this: DashboardContainer, interactionMo
|
|||
this.saveNotification$.next();
|
||||
|
||||
resolve(saveResult);
|
||||
|
||||
return saveResult;
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
|
|
|
@ -440,7 +440,6 @@ export class DashboardContainer
|
|||
// ------------------------------------------------------------------------------------------------------
|
||||
// Dashboard API
|
||||
// ------------------------------------------------------------------------------------------------------
|
||||
|
||||
public runInteractiveSave = runInteractiveSave;
|
||||
public runQuickSave = runQuickSave;
|
||||
|
||||
|
|
|
@ -0,0 +1,63 @@
|
|||
/*
|
||||
* 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 React from 'react';
|
||||
import { render } from '@testing-library/react';
|
||||
import { buildMockDashboard } from '../mocks';
|
||||
import { InternalDashboardTopNav } from './internal_dashboard_top_nav';
|
||||
import { setMockedPresentationUtilServices } from '@kbn/presentation-util-plugin/public/mocks';
|
||||
import { pluginServices } from '../services/plugin_services';
|
||||
import { DashboardAPIContext } from '../dashboard_app/dashboard_app';
|
||||
import { TopNavMenuProps } from '@kbn/navigation-plugin/public';
|
||||
|
||||
describe('Internal dashboard top nav', () => {
|
||||
const mockTopNav = (badges: TopNavMenuProps['badges'] | undefined[]) => {
|
||||
if (badges) {
|
||||
return badges?.map((badge, index) => (
|
||||
<div key={index} className="badge">
|
||||
{badge?.badgeText}
|
||||
</div>
|
||||
));
|
||||
} else {
|
||||
return <></>;
|
||||
}
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
setMockedPresentationUtilServices();
|
||||
pluginServices.getServices().data.query.filterManager.getFilters = jest
|
||||
.fn()
|
||||
.mockReturnValue([]);
|
||||
// topNavMenu is mocked as a jest.fn() so we want to mock it with a component
|
||||
// @ts-ignore type issue with the mockTopNav for this test suite
|
||||
pluginServices.getServices().navigation.TopNavMenu = ({ badges }: TopNavMenuProps) =>
|
||||
mockTopNav(badges);
|
||||
});
|
||||
|
||||
it('should not render the managed badge by default', async () => {
|
||||
const component = render(
|
||||
<DashboardAPIContext.Provider value={buildMockDashboard()}>
|
||||
<InternalDashboardTopNav redirectTo={jest.fn()} />
|
||||
</DashboardAPIContext.Provider>
|
||||
);
|
||||
|
||||
expect(component.queryByText('Managed')).toBeNull();
|
||||
});
|
||||
|
||||
it('should render the managed badge when the dashboard is managed', async () => {
|
||||
const container = buildMockDashboard();
|
||||
container.dispatch.setManaged(true);
|
||||
const component = render(
|
||||
<DashboardAPIContext.Provider value={container}>
|
||||
<InternalDashboardTopNav redirectTo={jest.fn()} />
|
||||
</DashboardAPIContext.Provider>
|
||||
);
|
||||
|
||||
expect(component.getByText('Managed')).toBeInTheDocument();
|
||||
});
|
||||
});
|
|
@ -7,20 +7,28 @@
|
|||
*/
|
||||
|
||||
import UseUnmount from 'react-use/lib/useUnmount';
|
||||
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
|
||||
import {
|
||||
withSuspense,
|
||||
LazyLabsFlyout,
|
||||
getContextProvider as getPresentationUtilContextProvider,
|
||||
} from '@kbn/presentation-util-plugin/public';
|
||||
import { getManagedContentBadge } from '@kbn/managed-content-badge';
|
||||
import { ViewMode } from '@kbn/embeddable-plugin/public';
|
||||
import type { DataView } from '@kbn/data-views-plugin/public';
|
||||
import { TopNavMenuProps } from '@kbn/navigation-plugin/public';
|
||||
import { EuiHorizontalRule, EuiIcon, EuiToolTipProps } from '@elastic/eui';
|
||||
import type { EuiBreadcrumb } from '@elastic/eui';
|
||||
import { TopNavMenuBadgeProps, TopNavMenuProps } from '@kbn/navigation-plugin/public';
|
||||
import {
|
||||
EuiBreadcrumb,
|
||||
EuiHorizontalRule,
|
||||
EuiIcon,
|
||||
EuiToolTipProps,
|
||||
EuiPopover,
|
||||
EuiBadge,
|
||||
EuiLink,
|
||||
} from '@elastic/eui';
|
||||
import { MountPoint } from '@kbn/core/public';
|
||||
import { getManagedContentBadge } from '@kbn/managed-content-badge';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import {
|
||||
getDashboardTitle,
|
||||
leaveConfirmStrings,
|
||||
|
@ -38,6 +46,7 @@ import { useDashboardMountContext } from '../dashboard_app/hooks/dashboard_mount
|
|||
import { getFullEditPath, LEGACY_DASHBOARD_APP_ID } from '../dashboard_constants';
|
||||
import './_dashboard_top_nav.scss';
|
||||
import { DashboardRedirect } from '../dashboard_container/types';
|
||||
import { SaveDashboardReturn } from '../services/dashboard_content_management/types';
|
||||
|
||||
export interface InternalDashboardTopNavProps {
|
||||
customLeadingBreadCrumbs?: EuiBreadcrumb[];
|
||||
|
@ -90,7 +99,6 @@ export function InternalDashboardTopNav({
|
|||
|
||||
const dashboard = useDashboardAPI();
|
||||
const PresentationUtilContextProvider = getPresentationUtilContextProvider();
|
||||
|
||||
const hasRunMigrations = dashboard.select(
|
||||
(state) => state.componentState.hasRunClientsideMigrations
|
||||
);
|
||||
|
@ -107,6 +115,7 @@ export function InternalDashboardTopNav({
|
|||
|
||||
// store data views in state & subscribe to dashboard data view changes.
|
||||
const [allDataViews, setAllDataViews] = useState<DataView[]>([]);
|
||||
const [isPopoverOpen, setIsPopoverOpen] = useState(false);
|
||||
useEffect(() => {
|
||||
setAllDataViews(dashboard.getAllDataViews());
|
||||
const subscription = dashboard.onDataViewsUpdate$.subscribe((dataViews) =>
|
||||
|
@ -274,10 +283,26 @@ export function InternalDashboardTopNav({
|
|||
viewMode,
|
||||
]);
|
||||
|
||||
const maybeRedirect = useCallback(
|
||||
(result?: SaveDashboardReturn) => {
|
||||
if (!result) return;
|
||||
const { redirectRequired, id } = result;
|
||||
if (redirectRequired) {
|
||||
redirectTo({
|
||||
id,
|
||||
editMode: true,
|
||||
useReplace: true,
|
||||
destination: 'dashboard',
|
||||
});
|
||||
}
|
||||
},
|
||||
[redirectTo]
|
||||
);
|
||||
|
||||
const { viewModeTopNavConfig, editModeTopNavConfig } = useDashboardMenuItems({
|
||||
redirectTo,
|
||||
isLabsShown,
|
||||
setIsLabsShown,
|
||||
maybeRedirect,
|
||||
showResetChange,
|
||||
});
|
||||
|
||||
|
@ -313,10 +338,63 @@ export function InternalDashboardTopNav({
|
|||
});
|
||||
}
|
||||
if (showWriteControls && managed) {
|
||||
allBadges.push(getManagedContentBadge(dashboardManagedBadge.getTooltip()));
|
||||
const badgeProps = {
|
||||
...getManagedContentBadge(dashboardManagedBadge.getBadgeAriaLabel()),
|
||||
onClick: () => setIsPopoverOpen(!isPopoverOpen),
|
||||
onClickAriaLabel: dashboardManagedBadge.getBadgeAriaLabel(),
|
||||
iconOnClick: () => setIsPopoverOpen(!isPopoverOpen),
|
||||
iconOnClickAriaLabel: dashboardManagedBadge.getBadgeAriaLabel(),
|
||||
} as TopNavMenuBadgeProps;
|
||||
|
||||
allBadges.push({
|
||||
renderCustomBadge: ({ badgeText }) => {
|
||||
const badgeButton = <EuiBadge {...badgeProps}>{badgeText}</EuiBadge>;
|
||||
return (
|
||||
<EuiPopover
|
||||
button={badgeButton}
|
||||
isOpen={isPopoverOpen}
|
||||
closePopover={() => setIsPopoverOpen(false)}
|
||||
panelStyle={{ maxWidth: 250 }}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="dashboard.managedContentPopoverButton"
|
||||
defaultMessage="Elastic manages this dashboard. {Duplicate} it to make changes."
|
||||
values={{
|
||||
Duplicate: (
|
||||
<EuiLink
|
||||
id="dashboardManagedContentPopoverButton"
|
||||
onClick={() => {
|
||||
dashboard
|
||||
.runInteractiveSave(viewMode)
|
||||
.then((result) => maybeRedirect(result));
|
||||
}}
|
||||
aria-label={dashboardManagedBadge.getDuplicateButtonAriaLabel()}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="dashboard.managedContentPopoverButtonText"
|
||||
defaultMessage="Duplicate"
|
||||
/>
|
||||
</EuiLink>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</EuiPopover>
|
||||
);
|
||||
},
|
||||
badgeText: badgeProps.badgeText,
|
||||
});
|
||||
}
|
||||
return allBadges;
|
||||
}, [hasUnsavedChanges, viewMode, hasRunMigrations, showWriteControls, managed]);
|
||||
}, [
|
||||
hasUnsavedChanges,
|
||||
viewMode,
|
||||
hasRunMigrations,
|
||||
showWriteControls,
|
||||
managed,
|
||||
isPopoverOpen,
|
||||
dashboard,
|
||||
maybeRedirect,
|
||||
]);
|
||||
|
||||
return (
|
||||
<div className="dashboardTopNav">
|
||||
|
|
|
@ -73,7 +73,6 @@
|
|||
"@kbn/presentation-panel-plugin",
|
||||
"@kbn/content-management-table-list-view-common",
|
||||
"@kbn/shared-ux-utility",
|
||||
"@kbn/managed-content-badge",
|
||||
"@kbn/core-test-helpers-model-versions",
|
||||
"@kbn/deeplinks-analytics",
|
||||
"@kbn/core-user-profile-browser-mocks",
|
||||
|
@ -84,6 +83,7 @@
|
|||
"@kbn/lens-embeddable-utils",
|
||||
"@kbn/lens-plugin",
|
||||
"@kbn/recently-accessed",
|
||||
"@kbn/managed-content-badge",
|
||||
"@kbn/content-management-favorites-public",
|
||||
"@kbn/core-http-browser-mocks",
|
||||
],
|
||||
|
|
|
@ -38,3 +38,6 @@ export const mockedReduxEmbeddablePackage: ReduxToolsPackage = {
|
|||
};
|
||||
|
||||
export * from './__stories__/fixtures/flights';
|
||||
export const setMockedPresentationUtilServices = () => {
|
||||
pluginServices.setRegistry(stubRegistry.start({}));
|
||||
};
|
||||
|
|
|
@ -1307,7 +1307,6 @@
|
|||
"dashboard.appLeaveConfirmModal.cancelButtonLabel": "Annuler",
|
||||
"dashboard.appLeaveConfirmModal.unsavedChangesSubtitle": "Quitter Dashboard sans enregistrer ?",
|
||||
"dashboard.appLeaveConfirmModal.unsavedChangesTitle": "Modifications non enregistrées",
|
||||
"dashboard.badge.managed.tooltip": "Elastic gère ce tableau de bord. Clonez-le pour effectuer des modifications.",
|
||||
"dashboard.badge.readOnly.text": "Lecture seule",
|
||||
"dashboard.badge.readOnly.tooltip": "Impossible d'enregistrer les tableaux de bord",
|
||||
"dashboard.createConfirmModal.cancelButtonLabel": "Annuler",
|
||||
|
|
|
@ -1307,7 +1307,6 @@
|
|||
"dashboard.appLeaveConfirmModal.cancelButtonLabel": "キャンセル",
|
||||
"dashboard.appLeaveConfirmModal.unsavedChangesSubtitle": "作業を保存せずにダッシュボードから移動しますか?",
|
||||
"dashboard.appLeaveConfirmModal.unsavedChangesTitle": "保存されていない変更",
|
||||
"dashboard.badge.managed.tooltip": "Elasticはこのダッシュボードを管理します。変更するには、複製してください。",
|
||||
"dashboard.badge.readOnly.text": "読み取り専用",
|
||||
"dashboard.badge.readOnly.tooltip": "ダッシュボードを保存できません",
|
||||
"dashboard.createConfirmModal.cancelButtonLabel": "キャンセル",
|
||||
|
|
|
@ -1307,7 +1307,6 @@
|
|||
"dashboard.appLeaveConfirmModal.cancelButtonLabel": "取消",
|
||||
"dashboard.appLeaveConfirmModal.unsavedChangesSubtitle": "离开有未保存工作的仪表板?",
|
||||
"dashboard.appLeaveConfirmModal.unsavedChangesTitle": "未保存的更改",
|
||||
"dashboard.badge.managed.tooltip": "Elastic 将管理此仪表板。进行克隆以做出更改。",
|
||||
"dashboard.badge.readOnly.text": "只读",
|
||||
"dashboard.badge.readOnly.tooltip": "无法保存仪表板",
|
||||
"dashboard.createConfirmModal.cancelButtonLabel": "取消",
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue