From b28c6bbe90774fbd0c6739af4ef190a4b4952dbd Mon Sep 17 00:00:00 2001 From: Catherine Liu Date: Tue, 13 Apr 2021 09:24:17 -0700 Subject: [PATCH] [Presentation Util] Shared toolbar component (#94139) --- packages/kbn-optimizer/limits.yml | 2 +- .../dashboard/.storybook/storyshots.test.tsx | 76 ------- src/plugins/dashboard/kibana.json | 3 +- .../application/top_nav/dashboard_top_nav.tsx | 66 +++++- .../panel_toolbar.stories.storyshot | 71 ------ .../top_nav/panel_toolbar/panel_toolbar.tsx | 51 ----- src/plugins/dashboard/tsconfig.json | 3 + .../components/solution_toolbar}/index.ts | 3 +- .../items/add_from_library.tsx | 24 ++ .../solution_toolbar/items/button.scss} | 6 +- .../solution_toolbar/items/button.tsx | 30 +++ .../solution_toolbar/items/index.ts | 14 ++ .../solution_toolbar/items/popover.tsx | 36 +++ .../items/primary_button.tsx} | 14 +- .../items/primary_popover.tsx | 17 ++ .../solution_toolbar/items/quick_group.scss | 5 + .../solution_toolbar/items/quick_group.tsx | 58 +++++ .../solution_toolbar/solution_toolbar.scss | 4 + .../solution_toolbar.stories.tsx | 205 ++++++++++++++++++ .../solution_toolbar/solution_toolbar.tsx | 62 ++++++ .../public/i18n/components.ts | 35 +++ src/plugins/presentation_util/public/index.ts | 10 + src/plugins/presentation_util/tsconfig.json | 9 +- .../visualize_embeddable_factory.tsx | 2 +- src/plugins/visualizations/public/mocks.ts | 2 - src/plugins/visualizations/public/plugin.ts | 4 +- src/plugins/visualizations/tsconfig.json | 1 - .../dashboard/create_and_add_embeddables.ts | 30 +++ .../apps/dashboard/edit_visualizations.js | 5 +- .../functional/page_objects/dashboard_page.ts | 10 + .../translations/translations/ja-JP.json | 3 +- .../translations/translations/zh-CN.json | 3 +- 32 files changed, 626 insertions(+), 238 deletions(-) delete mode 100644 src/plugins/dashboard/.storybook/storyshots.test.tsx delete mode 100644 src/plugins/dashboard/public/application/top_nav/panel_toolbar/__snapshots__/panel_toolbar.stories.storyshot delete mode 100644 src/plugins/dashboard/public/application/top_nav/panel_toolbar/panel_toolbar.tsx rename src/plugins/{dashboard/public/application/top_nav/panel_toolbar => presentation_util/public/components/solution_toolbar}/index.ts (81%) create mode 100644 src/plugins/presentation_util/public/components/solution_toolbar/items/add_from_library.tsx rename src/plugins/{dashboard/public/application/top_nav/panel_toolbar/panel_toolbar.scss => presentation_util/public/components/solution_toolbar/items/button.scss} (73%) create mode 100644 src/plugins/presentation_util/public/components/solution_toolbar/items/button.tsx create mode 100644 src/plugins/presentation_util/public/components/solution_toolbar/items/index.ts create mode 100644 src/plugins/presentation_util/public/components/solution_toolbar/items/popover.tsx rename src/plugins/{dashboard/public/application/top_nav/panel_toolbar/panel_toolbar.stories.tsx => presentation_util/public/components/solution_toolbar/items/primary_button.tsx} (53%) create mode 100644 src/plugins/presentation_util/public/components/solution_toolbar/items/primary_popover.tsx create mode 100644 src/plugins/presentation_util/public/components/solution_toolbar/items/quick_group.scss create mode 100644 src/plugins/presentation_util/public/components/solution_toolbar/items/quick_group.tsx create mode 100644 src/plugins/presentation_util/public/components/solution_toolbar/solution_toolbar.scss create mode 100644 src/plugins/presentation_util/public/components/solution_toolbar/solution_toolbar.stories.tsx create mode 100644 src/plugins/presentation_util/public/components/solution_toolbar/solution_toolbar.tsx create mode 100644 src/plugins/presentation_util/public/i18n/components.ts diff --git a/packages/kbn-optimizer/limits.yml b/packages/kbn-optimizer/limits.yml index 848e0ac9c9086..ac10509aec3ee 100644 --- a/packages/kbn-optimizer/limits.yml +++ b/packages/kbn-optimizer/limits.yml @@ -100,7 +100,7 @@ pageLoadAssetSize: watcher: 43742 runtimeFields: 41752 stackAlerts: 29684 - presentationUtil: 28545 + presentationUtil: 49767 spacesOss: 18817 indexPatternFieldEditor: 90489 osquery: 107090 diff --git a/src/plugins/dashboard/.storybook/storyshots.test.tsx b/src/plugins/dashboard/.storybook/storyshots.test.tsx deleted file mode 100644 index 80e8aa795ed40..0000000000000 --- a/src/plugins/dashboard/.storybook/storyshots.test.tsx +++ /dev/null @@ -1,76 +0,0 @@ -/* - * 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 fs from 'fs'; -import { ReactChildren } from 'react'; -import path from 'path'; -import moment from 'moment'; -import 'moment-timezone'; -import ReactDOM from 'react-dom'; - -import initStoryshots, { multiSnapshotWithOptions } from '@storybook/addon-storyshots'; -// @ts-ignore -import styleSheetSerializer from 'jest-styled-components/src/styleSheetSerializer'; -import { addSerializer } from 'jest-specific-snapshot'; - -// Set our default timezone to UTC for tests so we can generate predictable snapshots -moment.tz.setDefault('UTC'); - -// Freeze time for the tests for predictable snapshots -const testTime = new Date(Date.UTC(2019, 5, 1)); // June 1 2019 -Date.now = jest.fn(() => testTime.getTime()); - -// Mock React Portal for components that use modals, tooltips, etc -// @ts-expect-error Portal mocks are notoriously difficult to type -ReactDOM.createPortal = jest.fn((element) => element); - -// Mock EUI generated ids to be consistently predictable for snapshots. -jest.mock(`@elastic/eui/lib/components/form/form_row/make_id`, () => () => `generated-id`); - -// Mock react-datepicker dep used by eui to avoid rendering the entire large component -jest.mock('@elastic/eui/packages/react-datepicker', () => { - return { - __esModule: true, - default: 'ReactDatePicker', - }; -}); - -// Mock the EUI HTML ID Generator so elements have a predictable ID in snapshots -jest.mock('@elastic/eui/lib/services/accessibility/html_id_generator', () => { - return { - htmlIdGenerator: () => () => `generated-id`, - }; -}); - -// To be resolved by EUI team. -// https://github.com/elastic/eui/issues/3712 -jest.mock('@elastic/eui/lib/components/overlay_mask/overlay_mask', () => { - return { - EuiOverlayMask: ({ children }: { children: ReactChildren }) => children, - }; -}); - -// @ts-ignore -import { EuiObserver } from '@elastic/eui/test-env/components/observer/observer'; -jest.mock('@elastic/eui/test-env/components/observer/observer'); -EuiObserver.mockImplementation(() => 'EuiObserver'); - -// Some of the code requires that this directory exists, but the tests don't actually require any css to be present -const cssDir = path.resolve(__dirname, '../../../../built_assets/css'); -if (!fs.existsSync(cssDir)) { - fs.mkdirSync(cssDir, { recursive: true }); -} - -addSerializer(styleSheetSerializer); - -// Initialize Storyshots and build the Jest Snapshots -initStoryshots({ - configPath: path.resolve(__dirname, './../.storybook'), - framework: 'react', - test: multiSnapshotWithOptions({}), -}); diff --git a/src/plugins/dashboard/kibana.json b/src/plugins/dashboard/kibana.json index 32507cbc5e5f4..41335069461fa 100644 --- a/src/plugins/dashboard/kibana.json +++ b/src/plugins/dashboard/kibana.json @@ -23,6 +23,7 @@ "requiredBundles": [ "home", "kibanaReact", - "kibanaUtils" + "kibanaUtils", + "presentationUtil" ] } diff --git a/src/plugins/dashboard/public/application/top_nav/dashboard_top_nav.tsx b/src/plugins/dashboard/public/application/top_nav/dashboard_top_nav.tsx index a82aa78b815ec..ef0cd376df98b 100644 --- a/src/plugins/dashboard/public/application/top_nav/dashboard_top_nav.tsx +++ b/src/plugins/dashboard/public/application/top_nav/dashboard_top_nav.tsx @@ -11,6 +11,12 @@ import angular from 'angular'; import React, { useCallback, useEffect, useMemo, useState } from 'react'; import UseUnmount from 'react-use/lib/useUnmount'; +import { + AddFromLibraryButton, + PrimaryActionButton, + QuickButtonGroup, + SolutionToolbar, +} from '../../../../presentation_util/public'; import { useKibana } from '../../services/kibana_react'; import { IndexPattern, SavedQuery, TimefilterContract } from '../../services/data'; import { @@ -43,9 +49,9 @@ import { showCloneModal } from './show_clone_modal'; import { showOptionsPopover } from './show_options_popover'; import { TopNavIds } from './top_nav_ids'; import { ShowShareModal } from './show_share_modal'; -import { PanelToolbar } from './panel_toolbar'; import { confirmDiscardOrKeepUnsavedChanges } from '../listing/confirm_overlays'; import { OverlayRef } from '../../../../../core/public'; +import { DashboardConstants } from '../../dashboard_constants'; import { getNewDashboardTitle, unsavedChangesBadge } from '../../dashboard_strings'; import { DASHBOARD_PANELS_UNSAVED_ID } from '../lib/dashboard_panel_storage'; import { DashboardContainer } from '..'; @@ -103,6 +109,8 @@ export function DashboardTopNav({ const [state, setState] = useState({ chromeIsVisible: false }); const [isSaveInProgress, setIsSaveInProgress] = useState(false); + const stateTransferService = embeddable.getStateTransfer(); + useEffect(() => { const visibleSubscription = chrome.getIsVisible$().subscribe((chromeIsVisible) => { setState((s) => ({ ...s, chromeIsVisible })); @@ -147,12 +155,26 @@ export function DashboardTopNav({ const createNew = useCallback(async () => { const type = 'visualization'; const factory = embeddable.getEmbeddableFactory(type); + if (!factory) { throw new EmbeddableFactoryNotFoundError(type); } + await factory.create({} as EmbeddableInput, dashboardContainer); }, [dashboardContainer, embeddable]); + const createNewVisType = useCallback( + (newVisType: string) => async () => { + stateTransferService.navigateToEditor('visualize', { + path: `#/create?type=${encodeURIComponent(newVisType)}`, + state: { + originatingApp: DashboardConstants.DASHBOARDS_ID, + }, + }); + }, + [stateTransferService] + ); + const clearAddPanel = useCallback(() => { if (state.addPanelOverlay) { state.addPanelOverlay.close(); @@ -540,11 +562,51 @@ export function DashboardTopNav({ }; const { TopNavMenu } = navigation.ui; + + const quickButtons = [ + { + iconType: 'visText', + createType: i18n.translate('dashboard.solutionToolbar.markdownQuickButtonLabel', { + defaultMessage: 'Markdown', + }), + onClick: createNewVisType('markdown'), + 'data-test-subj': 'dashboardMarkdownQuickButton', + }, + { + iconType: 'controlsHorizontal', + createType: i18n.translate('dashboard.solutionToolbar.inputControlsQuickButtonLabel', { + defaultMessage: 'Input control', + }), + onClick: createNewVisType('input_control_vis'), + 'data-test-subj': 'dashboardInputControlsQuickButton', + }, + ]; + return ( <> {viewMode !== ViewMode.VIEW ? ( - + + {{ + primaryActionButton: ( + + ), + quickButtonGroup: , + addFromLibraryButton: ( + + ), + }} + ) : null} ); diff --git a/src/plugins/dashboard/public/application/top_nav/panel_toolbar/__snapshots__/panel_toolbar.stories.storyshot b/src/plugins/dashboard/public/application/top_nav/panel_toolbar/__snapshots__/panel_toolbar.stories.storyshot deleted file mode 100644 index afbbecb3935e0..0000000000000 --- a/src/plugins/dashboard/public/application/top_nav/panel_toolbar/__snapshots__/panel_toolbar.stories.storyshot +++ /dev/null @@ -1,71 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Storyshots components/PanelToolbar default 1`] = ` -
-
- -
-
- -
-
-`; diff --git a/src/plugins/dashboard/public/application/top_nav/panel_toolbar/panel_toolbar.tsx b/src/plugins/dashboard/public/application/top_nav/panel_toolbar/panel_toolbar.tsx deleted file mode 100644 index 0449fae80186d..0000000000000 --- a/src/plugins/dashboard/public/application/top_nav/panel_toolbar/panel_toolbar.tsx +++ /dev/null @@ -1,51 +0,0 @@ -/* - * 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 './panel_toolbar.scss'; -import React, { FC } from 'react'; -import { i18n } from '@kbn/i18n'; -import { EuiButton, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; - -interface Props { - /** The click handler for the Add Panel button for creating new panels */ - onAddPanelClick: () => void; - /** The click handler for the Library button for adding existing visualizations/embeddables */ - onLibraryClick: () => void; -} - -export const PanelToolbar: FC = ({ onAddPanelClick, onLibraryClick }) => ( - - - - {i18n.translate('dashboard.panelToolbar.addPanelButtonLabel', { - defaultMessage: 'Create panel', - })} - - - - - {i18n.translate('dashboard.panelToolbar.libraryButtonLabel', { - defaultMessage: 'Add from library', - })} - - - -); diff --git a/src/plugins/dashboard/tsconfig.json b/src/plugins/dashboard/tsconfig.json index dd99119cfb457..12820fc08310d 100644 --- a/src/plugins/dashboard/tsconfig.json +++ b/src/plugins/dashboard/tsconfig.json @@ -32,5 +32,8 @@ { "path": "../saved_objects/tsconfig.json" }, { "path": "../ui_actions/tsconfig.json" }, { "path": "../spaces_oss/tsconfig.json" }, + { "path": "../charts/tsconfig.json" }, + { "path": "../discover/tsconfig.json" }, + { "path": "../visualizations/tsconfig.json" }, ] } diff --git a/src/plugins/dashboard/public/application/top_nav/panel_toolbar/index.ts b/src/plugins/presentation_util/public/components/solution_toolbar/index.ts similarity index 81% rename from src/plugins/dashboard/public/application/top_nav/panel_toolbar/index.ts rename to src/plugins/presentation_util/public/components/solution_toolbar/index.ts index fd0ce66beb97c..332d60787b8cb 100644 --- a/src/plugins/dashboard/public/application/top_nav/panel_toolbar/index.ts +++ b/src/plugins/presentation_util/public/components/solution_toolbar/index.ts @@ -6,4 +6,5 @@ * Side Public License, v 1. */ -export { PanelToolbar } from './panel_toolbar'; +export { SolutionToolbar } from './solution_toolbar'; +export * from './items'; diff --git a/src/plugins/presentation_util/public/components/solution_toolbar/items/add_from_library.tsx b/src/plugins/presentation_util/public/components/solution_toolbar/items/add_from_library.tsx new file mode 100644 index 0000000000000..0550de1d069fa --- /dev/null +++ b/src/plugins/presentation_util/public/components/solution_toolbar/items/add_from_library.tsx @@ -0,0 +1,24 @@ +/* + * 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 { ComponentStrings } from '../../../i18n/components'; +import { SolutionToolbarButton, Props as SolutionToolbarButtonProps } from './button'; + +const { SolutionToolbar: strings } = ComponentStrings; + +export type Props = Omit; + +export const AddFromLibraryButton = ({ onClick, ...rest }: Props) => ( + +); diff --git a/src/plugins/dashboard/public/application/top_nav/panel_toolbar/panel_toolbar.scss b/src/plugins/presentation_util/public/components/solution_toolbar/items/button.scss similarity index 73% rename from src/plugins/dashboard/public/application/top_nav/panel_toolbar/panel_toolbar.scss rename to src/plugins/presentation_util/public/components/solution_toolbar/items/button.scss index 9ad6a5257df3e..79c3d4cca7ace 100644 --- a/src/plugins/dashboard/public/application/top_nav/panel_toolbar/panel_toolbar.scss +++ b/src/plugins/presentation_util/public/components/solution_toolbar/items/button.scss @@ -1,9 +1,5 @@ -.panelToolbar { - padding: 0 $euiSizeS $euiSizeS; - flex-grow: 0; -} -.panelToolbarButton { +.solutionToolbarButton { line-height: $euiButtonHeight; // Keeps alignment of text and chart icon background-color: $euiColorEmptyShade; diff --git a/src/plugins/presentation_util/public/components/solution_toolbar/items/button.tsx b/src/plugins/presentation_util/public/components/solution_toolbar/items/button.tsx new file mode 100644 index 0000000000000..5de8e24ef5f0d --- /dev/null +++ b/src/plugins/presentation_util/public/components/solution_toolbar/items/button.tsx @@ -0,0 +1,30 @@ +/* + * 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 { EuiButton } from '@elastic/eui'; +import { EuiButtonPropsForButton } from '@elastic/eui/src/components/button/button'; + +import './button.scss'; + +export interface Props extends Pick { + label: string; + primary?: boolean; +} + +export const SolutionToolbarButton = ({ label, primary, ...rest }: Props) => ( + + {label} + +); diff --git a/src/plugins/presentation_util/public/components/solution_toolbar/items/index.ts b/src/plugins/presentation_util/public/components/solution_toolbar/items/index.ts new file mode 100644 index 0000000000000..654831e86d3f6 --- /dev/null +++ b/src/plugins/presentation_util/public/components/solution_toolbar/items/index.ts @@ -0,0 +1,14 @@ +/* + * 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. + */ + +export { SolutionToolbarButton } from './button'; +export { SolutionToolbarPopover } from './popover'; +export { AddFromLibraryButton } from './add_from_library'; +export { QuickButtonProps, QuickButtonGroup } from './quick_group'; +export { PrimaryActionButton } from './primary_button'; +export { PrimaryActionPopover } from './primary_popover'; diff --git a/src/plugins/presentation_util/public/components/solution_toolbar/items/popover.tsx b/src/plugins/presentation_util/public/components/solution_toolbar/items/popover.tsx new file mode 100644 index 0000000000000..fbb34e165190d --- /dev/null +++ b/src/plugins/presentation_util/public/components/solution_toolbar/items/popover.tsx @@ -0,0 +1,36 @@ +/* + * 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, { useState } from 'react'; +import { EuiPopover } from '@elastic/eui'; +import { Props as EuiPopoverProps } from '@elastic/eui/src/components/popover/popover'; + +import { SolutionToolbarButton, Props as ButtonProps } from './button'; + +type AllowedButtonProps = Omit; +type AllowedPopoverProps = Omit< + EuiPopoverProps, + 'button' | 'isOpen' | 'closePopover' | 'anchorPosition' +>; + +export type Props = AllowedButtonProps & AllowedPopoverProps; + +export const SolutionToolbarPopover = ({ label, iconType, primary, ...popover }: Props) => { + const [isOpen, setIsOpen] = useState(false); + + const onButtonClick = () => setIsOpen((status) => !status); + const closePopover = () => setIsOpen(false); + + const button = ( + + ); + + return ( + + ); +}; diff --git a/src/plugins/dashboard/public/application/top_nav/panel_toolbar/panel_toolbar.stories.tsx b/src/plugins/presentation_util/public/components/solution_toolbar/items/primary_button.tsx similarity index 53% rename from src/plugins/dashboard/public/application/top_nav/panel_toolbar/panel_toolbar.stories.tsx rename to src/plugins/presentation_util/public/components/solution_toolbar/items/primary_button.tsx index 5525b1cd069ad..e2ef75e45a404 100644 --- a/src/plugins/dashboard/public/application/top_nav/panel_toolbar/panel_toolbar.stories.tsx +++ b/src/plugins/presentation_util/public/components/solution_toolbar/items/primary_button.tsx @@ -6,14 +6,10 @@ * Side Public License, v 1. */ -import { storiesOf } from '@storybook/react'; -import { action } from '@storybook/addon-actions'; import React from 'react'; -import { PanelToolbar } from './panel_toolbar'; -storiesOf('components/PanelToolbar', module).add('default', () => ( - -)); +import { SolutionToolbarButton, Props as SolutionToolbarButtonProps } from './button'; + +export const PrimaryActionButton = (props: Omit) => ( + +); diff --git a/src/plugins/presentation_util/public/components/solution_toolbar/items/primary_popover.tsx b/src/plugins/presentation_util/public/components/solution_toolbar/items/primary_popover.tsx new file mode 100644 index 0000000000000..164d4c9b4a1a6 --- /dev/null +++ b/src/plugins/presentation_util/public/components/solution_toolbar/items/primary_popover.tsx @@ -0,0 +1,17 @@ +/* + * 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 { SolutionToolbarPopover, Props as SolutionToolbarPopoverProps } from './popover'; + +export type Props = Omit; + +export const PrimaryActionPopover = (props: Omit) => ( + +); diff --git a/src/plugins/presentation_util/public/components/solution_toolbar/items/quick_group.scss b/src/plugins/presentation_util/public/components/solution_toolbar/items/quick_group.scss new file mode 100644 index 0000000000000..639ff5bf2a117 --- /dev/null +++ b/src/plugins/presentation_util/public/components/solution_toolbar/items/quick_group.scss @@ -0,0 +1,5 @@ +.quickButtonGroup { + .quickButtonGroup__button { + background-color: $euiColorEmptyShade; + } +} diff --git a/src/plugins/presentation_util/public/components/solution_toolbar/items/quick_group.tsx b/src/plugins/presentation_util/public/components/solution_toolbar/items/quick_group.tsx new file mode 100644 index 0000000000000..58f8bd803b636 --- /dev/null +++ b/src/plugins/presentation_util/public/components/solution_toolbar/items/quick_group.tsx @@ -0,0 +1,58 @@ +/* + * 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 { EuiButtonGroup, htmlIdGenerator, EuiButtonGroupOptionProps } from '@elastic/eui'; +import { ComponentStrings } from '../../../i18n/components'; + +const { QuickButtonGroup: strings } = ComponentStrings; + +import './quick_group.scss'; + +export interface QuickButtonProps extends Pick { + createType: string; + onClick: () => void; +} + +export interface Props { + buttons: QuickButtonProps[]; +} + +type Option = EuiButtonGroupOptionProps & Omit; + +export const QuickButtonGroup = ({ buttons }: Props) => { + const buttonGroupOptions: Option[] = buttons.map((button: QuickButtonProps, index) => { + const { createType: label, ...rest } = button; + const title = strings.getAriaButtonLabel(label); + + return { + ...rest, + 'aria-label': title, + className: 'quickButtonGroup__button', + id: `${htmlIdGenerator()()}${index}`, + label, + title, + }; + }); + + const onChangeIconsMulti = (optionId: string) => { + buttonGroupOptions.find((x) => x.id === optionId)?.onClick(); + }; + + return ( + + ); +}; diff --git a/src/plugins/presentation_util/public/components/solution_toolbar/solution_toolbar.scss b/src/plugins/presentation_util/public/components/solution_toolbar/solution_toolbar.scss new file mode 100644 index 0000000000000..18160acf191e4 --- /dev/null +++ b/src/plugins/presentation_util/public/components/solution_toolbar/solution_toolbar.scss @@ -0,0 +1,4 @@ +.solutionToolbar { + padding: 0 $euiSizeS $euiSizeS; + flex-grow: 0; +} diff --git a/src/plugins/presentation_util/public/components/solution_toolbar/solution_toolbar.stories.tsx b/src/plugins/presentation_util/public/components/solution_toolbar/solution_toolbar.stories.tsx new file mode 100644 index 0000000000000..fa33f53f9ae4f --- /dev/null +++ b/src/plugins/presentation_util/public/components/solution_toolbar/solution_toolbar.stories.tsx @@ -0,0 +1,205 @@ +/* + * 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 { Story } from '@storybook/react'; +import { action } from '@storybook/addon-actions'; +import { EuiContextMenu } from '@elastic/eui'; + +import { SolutionToolbar } from './solution_toolbar'; +import { SolutionToolbarPopover } from './items'; +import { AddFromLibraryButton, PrimaryActionButton, QuickButtonGroup } from './items'; + +const quickButtons = [ + { + createType: 'Text', + onClick: action('onTextClick'), + iconType: 'visText', + }, + { + createType: 'Control', + onClick: action('onControlClick'), + iconType: 'controlsHorizontal', + }, + { + createType: 'Link', + onClick: action('onLinkClick'), + iconType: 'link', + }, + { + createType: 'Image', + onClick: action('onImageClick'), + iconType: 'image', + }, + { + createType: 'Markup', + onClick: action('onMarkupClick'), + iconType: 'visVega', + }, +]; + +const primaryButtonConfigs = { + Generic: ( + + ), + Canvas: ( + + + + ), + Dashboard: ( + + ), +}; + +const extraButtonConfigs = { + Generic: undefined, + Canvas: undefined, + Dashboard: [ + + + , + ], +}; + +export default { + title: 'Solution Toolbar', + description: 'A universal toolbar for solutions maintained by the Presentation Team.', + component: SolutionToolbar, + argTypes: { + quickButtonCount: { + defaultValue: 2, + control: { + type: 'number', + min: 0, + max: 5, + step: 1, + }, + }, + showAddFromLibraryButton: { + defaultValue: true, + control: { + type: 'boolean', + }, + }, + solution: { + table: { + disable: true, + }, + }, + }, + // https://github.com/storybookjs/storybook/issues/11543#issuecomment-684130442 + parameters: { + docs: { + source: { + type: 'code', + }, + }, + }, +}; + +const Template: Story<{ + solution: 'Generic' | 'Canvas' | 'Dashboard'; + quickButtonCount: number; + showAddFromLibraryButton: boolean; +}> = ({ quickButtonCount, solution, showAddFromLibraryButton }) => { + const primaryActionButton = primaryButtonConfigs[solution]; + const extraButtons = extraButtonConfigs[solution]; + let quickButtonGroup; + let addFromLibraryButton; + + if (quickButtonCount > 0) { + quickButtonGroup = ; + } + + if (showAddFromLibraryButton) { + addFromLibraryButton = ; + } + + return ( + + {{ + primaryActionButton, + quickButtonGroup, + extraButtons, + addFromLibraryButton, + }} + + ); +}; + +export const Generic = Template.bind({}); +Generic.args = { + ...Template.args, + solution: 'Generic', +}; + +export const Canvas = Template.bind({}); +Canvas.args = { + ...Template.args, + solution: 'Canvas', +}; + +export const Dashboard = Template.bind({}); +Dashboard.args = { + ...Template.args, + solution: 'Dashboard', +}; diff --git a/src/plugins/presentation_util/public/components/solution_toolbar/solution_toolbar.tsx b/src/plugins/presentation_util/public/components/solution_toolbar/solution_toolbar.tsx new file mode 100644 index 0000000000000..bb8b04e8b8f09 --- /dev/null +++ b/src/plugins/presentation_util/public/components/solution_toolbar/solution_toolbar.tsx @@ -0,0 +1,62 @@ +/* + * 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, { ReactElement } from 'react'; +import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; + +import { + AddFromLibraryButton, + QuickButtonGroup, + PrimaryActionButton, + SolutionToolbarButton, + PrimaryActionPopover, + SolutionToolbarPopover, +} from './items'; + +import './solution_toolbar.scss'; + +interface NamedSlots { + primaryActionButton: ReactElement; + quickButtonGroup?: ReactElement; + addFromLibraryButton?: ReactElement; + extraButtons?: Array>; +} + +export interface Props { + children: NamedSlots; +} + +export const SolutionToolbar = ({ children }: Props) => { + const { + primaryActionButton, + quickButtonGroup, + addFromLibraryButton, + extraButtons = [], + } = children; + + const extra = extraButtons.map((button, index) => + button ? ( + + {button} + + ) : null + ); + + return ( + + {primaryActionButton} + {quickButtonGroup ? {quickButtonGroup} : null} + {extra} + {addFromLibraryButton ? {addFromLibraryButton} : null} + + ); +}; diff --git a/src/plugins/presentation_util/public/i18n/components.ts b/src/plugins/presentation_util/public/i18n/components.ts new file mode 100644 index 0000000000000..ab0e6d1bdbda0 --- /dev/null +++ b/src/plugins/presentation_util/public/i18n/components.ts @@ -0,0 +1,35 @@ +/* + * 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 { i18n } from '@kbn/i18n'; + +export const ComponentStrings = { + SolutionToolbar: { + getEditorMenuButtonLabel: () => + i18n.translate('presentationUtil.solutionToolbar.editorMenuButtonLabel', { + defaultMessage: 'All editors', + }), + getLibraryButtonLabel: () => + i18n.translate('presentationUtil.solutionToolbar.libraryButtonLabel', { + defaultMessage: 'Add from library', + }), + }, + QuickButtonGroup: { + getAriaButtonLabel: (createType: string) => + i18n.translate('presentationUtil.solutionToolbar.quickButton.ariaButtonLabel', { + defaultMessage: `Create new {createType}`, + values: { + createType, + }, + }), + getLegend: () => + i18n.translate('presentationUtil.solutionToolbar.quickButton.legendLabel', { + defaultMessage: 'Quick create', + }), + }, +}; diff --git a/src/plugins/presentation_util/public/index.ts b/src/plugins/presentation_util/public/index.ts index 1cbf4b5a4f334..9c5f65de40955 100644 --- a/src/plugins/presentation_util/public/index.ts +++ b/src/plugins/presentation_util/public/index.ts @@ -19,6 +19,16 @@ export { LazySavedObjectSaveModalDashboard, withSuspense, } from './components'; +export { + AddFromLibraryButton, + PrimaryActionButton, + PrimaryActionPopover, + QuickButtonGroup, + QuickButtonProps, + SolutionToolbar, + SolutionToolbarButton, + SolutionToolbarPopover, +} from './components/solution_toolbar'; export function plugin() { return new PresentationUtilPlugin(); diff --git a/src/plugins/presentation_util/tsconfig.json b/src/plugins/presentation_util/tsconfig.json index 63d136cf9445a..c0fafe8c3aaba 100644 --- a/src/plugins/presentation_util/tsconfig.json +++ b/src/plugins/presentation_util/tsconfig.json @@ -15,11 +15,8 @@ "../../../typings/**/*" ], "references": [ - { - "path": "../../core/tsconfig.json" - }, - { - "path": "../saved_objects/tsconfig.json" - }, + { "path": "../../core/tsconfig.json" }, + { "path": "../saved_objects/tsconfig.json" }, + { "path": "../kibana_react/tsconfig.json" }, ] } diff --git a/src/plugins/visualizations/public/embeddable/visualize_embeddable_factory.tsx b/src/plugins/visualizations/public/embeddable/visualize_embeddable_factory.tsx index c2b9fcd77757a..2b5a611cd946e 100644 --- a/src/plugins/visualizations/public/embeddable/visualize_embeddable_factory.tsx +++ b/src/plugins/visualizations/public/embeddable/visualize_embeddable_factory.tsx @@ -58,7 +58,7 @@ interface VisualizationAttributes extends SavedObjectAttributes { export interface VisualizeEmbeddableFactoryDeps { start: StartServicesGetter< - Pick + Pick >; } diff --git a/src/plugins/visualizations/public/mocks.ts b/src/plugins/visualizations/public/mocks.ts index 8f1ebe25b5059..901593626a945 100644 --- a/src/plugins/visualizations/public/mocks.ts +++ b/src/plugins/visualizations/public/mocks.ts @@ -17,7 +17,6 @@ import { dataPluginMock } from '../../../plugins/data/public/mocks'; import { usageCollectionPluginMock } from '../../../plugins/usage_collection/public/mocks'; import { uiActionsPluginMock } from '../../../plugins/ui_actions/public/mocks'; import { inspectorPluginMock } from '../../../plugins/inspector/public/mocks'; -import { dashboardPluginMock } from '../../../plugins/dashboard/public/mocks'; import { savedObjectsPluginMock } from '../../../plugins/saved_objects/public/mocks'; const createSetupContract = (): VisualizationsSetup => ({ @@ -62,7 +61,6 @@ const createInstance = async () => { uiActions: uiActionsPluginMock.createStartContract(), application: applicationServiceMock.createStartContract(), embeddable: embeddablePluginMock.createStartContract(), - dashboard: dashboardPluginMock.createStartContract(), getAttributeService: jest.fn(), savedObjectsClient: coreMock.createStart().savedObjects.client, savedObjects: savedObjectsPluginMock.createStartContract(), diff --git a/src/plugins/visualizations/public/plugin.ts b/src/plugins/visualizations/public/plugin.ts index d4e7132a1a21e..081f5d65103c2 100644 --- a/src/plugins/visualizations/public/plugin.ts +++ b/src/plugins/visualizations/public/plugin.ts @@ -62,7 +62,6 @@ import { convertToSerializedVis, } from './saved_visualizations/_saved_vis'; import { createSavedSearchesLoader } from '../../discover/public'; -import { DashboardStart } from '../../dashboard/public'; import { SavedObjectsStart } from '../../saved_objects/public'; /** @@ -97,7 +96,6 @@ export interface VisualizationsStartDeps { inspector: InspectorStart; uiActions: UiActionsStart; application: ApplicationStart; - dashboard: DashboardStart; getAttributeService: EmbeddableStart['getAttributeService']; savedObjects: SavedObjectsStart; savedObjectsClient: SavedObjectsClientContract; @@ -145,7 +143,7 @@ export class VisualizationsPlugin public start( core: CoreStart, - { data, expressions, uiActions, embeddable, dashboard, savedObjects }: VisualizationsStartDeps + { data, expressions, uiActions, embeddable, savedObjects }: VisualizationsStartDeps ): VisualizationsStart { const types = this.types.start(); setTypes(types); diff --git a/src/plugins/visualizations/tsconfig.json b/src/plugins/visualizations/tsconfig.json index d7c5e6a4b4366..356448aa59771 100644 --- a/src/plugins/visualizations/tsconfig.json +++ b/src/plugins/visualizations/tsconfig.json @@ -15,7 +15,6 @@ "references": [ { "path": "../../core/tsconfig.json" }, { "path": "../data/tsconfig.json" }, - { "path": "../dashboard/tsconfig.json" }, { "path": "../expressions/tsconfig.json" }, { "path": "../ui_actions/tsconfig.json" }, { "path": "../embeddable/tsconfig.json" }, diff --git a/test/functional/apps/dashboard/create_and_add_embeddables.ts b/test/functional/apps/dashboard/create_and_add_embeddables.ts index f4ee4e9904768..9b8fc4785a671 100644 --- a/test/functional/apps/dashboard/create_and_add_embeddables.ts +++ b/test/functional/apps/dashboard/create_and_add_embeddables.ts @@ -69,6 +69,36 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.dashboard.waitForRenderComplete(); }); + it('adds a markdown visualization via the quick button', async () => { + const originalPanelCount = await PageObjects.dashboard.getPanelCount(); + await PageObjects.dashboard.clickMarkdownQuickButton(); + await PageObjects.visualize.saveVisualizationExpectSuccess( + 'visualization from markdown quick button', + { redirectToOrigin: true } + ); + + await retry.try(async () => { + const panelCount = await PageObjects.dashboard.getPanelCount(); + expect(panelCount).to.eql(originalPanelCount + 1); + }); + await PageObjects.dashboard.waitForRenderComplete(); + }); + + it('adds an input control visualization via the quick button', async () => { + const originalPanelCount = await PageObjects.dashboard.getPanelCount(); + await PageObjects.dashboard.clickInputControlsQuickButton(); + await PageObjects.visualize.saveVisualizationExpectSuccess( + 'visualization from input control quick button', + { redirectToOrigin: true } + ); + + await retry.try(async () => { + const panelCount = await PageObjects.dashboard.getPanelCount(); + expect(panelCount).to.eql(originalPanelCount + 1); + }); + await PageObjects.dashboard.waitForRenderComplete(); + }); + it('saves the listing page instead of the visualization to the app link', async () => { await PageObjects.header.clickVisualize(true); const currentUrl = await browser.getCurrentUrl(); diff --git a/test/functional/apps/dashboard/edit_visualizations.js b/test/functional/apps/dashboard/edit_visualizations.js index d5df97881a1d3..ce32f53587e74 100644 --- a/test/functional/apps/dashboard/edit_visualizations.js +++ b/test/functional/apps/dashboard/edit_visualizations.js @@ -15,15 +15,12 @@ export default function ({ getService, getPageObjects }) { const appsMenu = getService('appsMenu'); const kibanaServer = getService('kibanaServer'); const dashboardPanelActions = getService('dashboardPanelActions'); - const dashboardVisualizations = getService('dashboardVisualizations'); const originalMarkdownText = 'Original markdown text'; const modifiedMarkdownText = 'Modified markdown text'; const createMarkdownVis = async (title) => { - await testSubjects.click('dashboardAddNewPanelButton'); - await dashboardVisualizations.ensureNewVisualizationDialogIsShowing(); - await PageObjects.visualize.clickMarkdownWidget(); + await PageObjects.dashboard.clickMarkdownQuickButton(); await PageObjects.visEditor.setMarkdownTxt(originalMarkdownText); await PageObjects.visEditor.clickGo(); if (title) { diff --git a/test/functional/page_objects/dashboard_page.ts b/test/functional/page_objects/dashboard_page.ts index 9c12296db138c..34559afdf6ae1 100644 --- a/test/functional/page_objects/dashboard_page.ts +++ b/test/functional/page_objects/dashboard_page.ts @@ -413,6 +413,16 @@ export function DashboardPageProvider({ getService, getPageObjects }: FtrProvide await testSubjects.click('confirmSaveSavedObjectButton'); } + public async clickMarkdownQuickButton() { + log.debug('Click markdown quick button'); + await testSubjects.click('dashboardMarkdownQuickButton'); + } + + public async clickInputControlsQuickButton() { + log.debug('Click input controls quick button'); + await testSubjects.click('dashboardInputControlsQuickButton'); + } + /** * * @param dashboardTitle {String} diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index e6f71de691143..52947c3368a7c 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -670,8 +670,7 @@ "dashboard.panelStorageError.clearError": "保存されていない変更の消去中にエラーが発生しました。{message}", "dashboard.panelStorageError.getError": "保存されていない変更の取得中にエラーが発生しました。{message}", "dashboard.panelStorageError.setError": "保存されていない変更の設定中にエラーが発生しました。{message}", - "dashboard.panelToolbar.addPanelButtonLabel": "パネルの作成", - "dashboard.panelToolbar.libraryButtonLabel": "ライブラリから追加", + "dashboard.solutionToolbar.addPanelButtonLabel": "パネルの作成", "dashboard.placeholder.factory.displayName": "プレースホルダー", "dashboard.savedDashboard.newDashboardTitle": "新規ダッシュボード", "dashboard.stateManager.timeNotSavedWithDashboardErrorMessage": "このダッシュボードに時刻が保存されていないため、同期できません。", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 057b11561588f..f94a7198f4f81 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -673,8 +673,7 @@ "dashboard.panelStorageError.clearError": "清除未保存更改时遇到错误:{message}", "dashboard.panelStorageError.getError": "获取未保存更改时遇到错误:{message}", "dashboard.panelStorageError.setError": "设置未保存更改时遇到错误:{message}", - "dashboard.panelToolbar.addPanelButtonLabel": "创建面板", - "dashboard.panelToolbar.libraryButtonLabel": "从库中添加", + "dashboard.solutionToolbar.addPanelButtonLabel": "创建面板", "dashboard.placeholder.factory.displayName": "占位符", "dashboard.savedDashboard.newDashboardTitle": "新建仪表板", "dashboard.stateManager.timeNotSavedWithDashboardErrorMessage": "时间未随此仪表板保存,因此无法同步。",