From 0c8115ce7df1b1a4a2e65fe5b882863e3efd0a79 Mon Sep 17 00:00:00 2001 From: Yaroslav Kuznietsov Date: Thu, 18 Nov 2021 09:07:51 +0200 Subject: [PATCH] [Canvas] Filters panel. (#116592) * Added new GlobalConfig layout. * Added filter components. * Added filterViews and their transforming. * Refactored getFilterFormatter method. * Added types. * Added hook for connecting to the canvas store. * Added filter types. * Fixed the style of filter view. * Added sidebar reducer and saved groupByOption there. * Added strict type. * Added time formatting and translations. * added invalid date translation. * Added components to the view of filter. * Fixed some bugs and done refactoring. * Added unit tests for filter.ts lib. * Refactored use_canvas_filters and added unit tests for filter_adapters. * Fixed format. * Added test to groupFiltersBy function. * Added default (beta) FiltersGroup story. * Refactored the code. * Storybook and snapshot for FiltersGroup component. * Added utils for WorkpadFilters storybook. * FilterComponent storybook and snapshot added. * WorkpadFiltersComponent storybook and snapshots added. * WorkpadFilters redux storybook added. * Added element without group to the redux WorkpadFilters storybook. * Updated snapshot for filter.component. * Moved filter views to a workpad_filters directory. * Fixed styles of the filter component. * Changed FunctionComponent to FC. * filter_group.tsx to filter_group.component.tsx * Added default to the groupFiltersByField * Added DEFAULT_GROUP_BY_FIELD. * filters_group.stories to filters_group.component.stories * Updated snapshots. Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- x-pack/plugins/canvas/common/lib/constants.ts | 2 + .../components/sidebar/global_config.tsx | 27 - .../sidebar/global_config/filter_config.tsx | 15 + .../sidebar/global_config/general_config.tsx | 29 + .../sidebar/global_config/global_config.tsx | 62 ++ .../sidebar/global_config/index.tsx | 8 + .../public/components/sidebar/sidebar.scss | 9 +- .../workpad_config.component.tsx | 17 +- .../filter.component.stories.storyshot | 435 +++++++++++ .../filters_group.component.stories.storyshot | 127 ++++ ...orkpad_filters.component.stories.storyshot | 706 ++++++++++++++++++ .../workpad_filters/__stories__/elements.ts | 66 ++ .../__stories__/filter.component.stories.tsx | 53 ++ .../filters_group.component.stories.tsx | 47 ++ .../workpad_filters.component.stories.tsx | 95 +++ .../__stories__/workpad_filters.stories.tsx | 23 + .../workpad_filters/filter.component.tsx | 64 ++ .../filter_views/default_filter.ts | 39 + .../workpad_filters/filter_views/index.ts | 15 + .../filter_views/time_filter.ts | 68 ++ .../filters_group.component.tsx | 50 ++ .../components/workpad_filters/hooks/index.ts | 8 + .../hooks/use_canvas_filters.ts | 23 + .../components/workpad_filters/index.tsx | 9 + .../components/workpad_filters/types.ts | 13 + .../components/workpad_filters/utils.ts | 42 ++ .../workpad_filters.component.tsx | 98 +++ .../workpad_filters/workpad_filters.tsx | 44 ++ .../plugins/canvas/public/lib/filter.test.ts | 282 +++++++ x-pack/plugins/canvas/public/lib/filter.ts | 55 ++ .../canvas/public/lib/filter_adapters.test.ts | 59 ++ .../canvas/public/lib/filter_adapters.ts | 48 ++ .../canvas/public/state/actions/sidebar.ts | 16 + .../plugins/canvas/public/state/defaults.js | 6 +- .../canvas/public/state/initial_state.js | 3 +- .../canvas/public/state/reducers/index.js | 3 +- .../canvas/public/state/reducers/sidebar.ts | 22 + .../canvas/public/state/selectors/sidebar.ts | 13 + x-pack/plugins/canvas/types/canvas.ts | 5 + x-pack/plugins/canvas/types/filters.ts | 39 + x-pack/plugins/canvas/types/state.ts | 5 +- .../translations/translations/ja-JP.json | 1 - .../translations/translations/zh-CN.json | 1 - 43 files changed, 2702 insertions(+), 50 deletions(-) delete mode 100644 x-pack/plugins/canvas/public/components/sidebar/global_config.tsx create mode 100644 x-pack/plugins/canvas/public/components/sidebar/global_config/filter_config.tsx create mode 100644 x-pack/plugins/canvas/public/components/sidebar/global_config/general_config.tsx create mode 100644 x-pack/plugins/canvas/public/components/sidebar/global_config/global_config.tsx create mode 100644 x-pack/plugins/canvas/public/components/sidebar/global_config/index.tsx create mode 100644 x-pack/plugins/canvas/public/components/workpad_filters/__stories__/__snapshots__/filter.component.stories.storyshot create mode 100644 x-pack/plugins/canvas/public/components/workpad_filters/__stories__/__snapshots__/filters_group.component.stories.storyshot create mode 100644 x-pack/plugins/canvas/public/components/workpad_filters/__stories__/__snapshots__/workpad_filters.component.stories.storyshot create mode 100644 x-pack/plugins/canvas/public/components/workpad_filters/__stories__/elements.ts create mode 100644 x-pack/plugins/canvas/public/components/workpad_filters/__stories__/filter.component.stories.tsx create mode 100644 x-pack/plugins/canvas/public/components/workpad_filters/__stories__/filters_group.component.stories.tsx create mode 100644 x-pack/plugins/canvas/public/components/workpad_filters/__stories__/workpad_filters.component.stories.tsx create mode 100644 x-pack/plugins/canvas/public/components/workpad_filters/__stories__/workpad_filters.stories.tsx create mode 100644 x-pack/plugins/canvas/public/components/workpad_filters/filter.component.tsx create mode 100644 x-pack/plugins/canvas/public/components/workpad_filters/filter_views/default_filter.ts create mode 100644 x-pack/plugins/canvas/public/components/workpad_filters/filter_views/index.ts create mode 100644 x-pack/plugins/canvas/public/components/workpad_filters/filter_views/time_filter.ts create mode 100644 x-pack/plugins/canvas/public/components/workpad_filters/filters_group.component.tsx create mode 100644 x-pack/plugins/canvas/public/components/workpad_filters/hooks/index.ts create mode 100644 x-pack/plugins/canvas/public/components/workpad_filters/hooks/use_canvas_filters.ts create mode 100644 x-pack/plugins/canvas/public/components/workpad_filters/index.tsx create mode 100644 x-pack/plugins/canvas/public/components/workpad_filters/types.ts create mode 100644 x-pack/plugins/canvas/public/components/workpad_filters/utils.ts create mode 100644 x-pack/plugins/canvas/public/components/workpad_filters/workpad_filters.component.tsx create mode 100644 x-pack/plugins/canvas/public/components/workpad_filters/workpad_filters.tsx create mode 100644 x-pack/plugins/canvas/public/lib/filter.test.ts create mode 100644 x-pack/plugins/canvas/public/lib/filter.ts create mode 100644 x-pack/plugins/canvas/public/lib/filter_adapters.test.ts create mode 100644 x-pack/plugins/canvas/public/lib/filter_adapters.ts create mode 100644 x-pack/plugins/canvas/public/state/actions/sidebar.ts create mode 100644 x-pack/plugins/canvas/public/state/reducers/sidebar.ts create mode 100644 x-pack/plugins/canvas/public/state/selectors/sidebar.ts diff --git a/x-pack/plugins/canvas/common/lib/constants.ts b/x-pack/plugins/canvas/common/lib/constants.ts index 7212baf2414ea..6a61ec595acb7 100644 --- a/x-pack/plugins/canvas/common/lib/constants.ts +++ b/x-pack/plugins/canvas/common/lib/constants.ts @@ -6,6 +6,7 @@ */ import { SHAREABLE_RUNTIME_NAME } from '../../shareable_runtime/constants_static'; +import { FilterField } from '../../types'; export const CANVAS_TYPE = 'canvas-workpad'; export const CUSTOM_ELEMENT_TYPE = 'canvas-element'; @@ -25,6 +26,7 @@ export const SESSIONSTORAGE_LASTPATH = 'lastPath:canvas'; export const FETCH_TIMEOUT = 30000; // 30 seconds export const DEFAULT_WORKPAD_CSS = '.canvasPage {\n\n}'; export const DEFAULT_ELEMENT_CSS = '.canvasRenderEl{\n\n}'; +export const DEFAULT_GROUP_BY_FIELD: FilterField = 'filterGroup'; export const VALID_IMAGE_TYPES = ['gif', 'jpeg', 'png', 'svg+xml']; export const ASSET_MAX_SIZE = 25000; export const ELEMENT_SHIFT_OFFSET = 10; diff --git a/x-pack/plugins/canvas/public/components/sidebar/global_config.tsx b/x-pack/plugins/canvas/public/components/sidebar/global_config.tsx deleted file mode 100644 index 7602a4d3e95ec..0000000000000 --- a/x-pack/plugins/canvas/public/components/sidebar/global_config.tsx +++ /dev/null @@ -1,27 +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; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React, { Fragment, FunctionComponent } from 'react'; -// @ts-expect-error unconverted component -import { ElementConfig } from '../element_config'; -// @ts-expect-error unconverted component -import { PageConfig } from '../page_config'; -import { WorkpadConfig } from '../workpad_config'; -// @ts-expect-error unconverted component -import { SidebarSection } from './sidebar_section'; - -export const GlobalConfig: FunctionComponent = () => ( - - - - - - - - - -); diff --git a/x-pack/plugins/canvas/public/components/sidebar/global_config/filter_config.tsx b/x-pack/plugins/canvas/public/components/sidebar/global_config/filter_config.tsx new file mode 100644 index 0000000000000..305ad9f7931f3 --- /dev/null +++ b/x-pack/plugins/canvas/public/components/sidebar/global_config/filter_config.tsx @@ -0,0 +1,15 @@ +/* + * 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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { FC } from 'react'; +import { WorkpadFilters } from '../../workpad_filters'; +// @ts-expect-error unconverted component +import { SidebarSection } from '../sidebar_section'; + +export const FilterConfig: FC = () => { + return ; +}; diff --git a/x-pack/plugins/canvas/public/components/sidebar/global_config/general_config.tsx b/x-pack/plugins/canvas/public/components/sidebar/global_config/general_config.tsx new file mode 100644 index 0000000000000..13031f306012e --- /dev/null +++ b/x-pack/plugins/canvas/public/components/sidebar/global_config/general_config.tsx @@ -0,0 +1,29 @@ +/* + * 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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { Fragment, FC } from 'react'; +// @ts-expect-error unconverted component +import { ElementConfig } from '../../element_config'; +// @ts-expect-error unconverted component +import { PageConfig } from '../../page_config'; +import { WorkpadConfig } from '../../workpad_config'; +// @ts-expect-error unconverted component +import { SidebarSection } from '../sidebar_section'; + +export const GeneralConfig: FC = () => { + return ( + + + + + + + + + + ); +}; diff --git a/x-pack/plugins/canvas/public/components/sidebar/global_config/global_config.tsx b/x-pack/plugins/canvas/public/components/sidebar/global_config/global_config.tsx new file mode 100644 index 0000000000000..ccdb0d88508eb --- /dev/null +++ b/x-pack/plugins/canvas/public/components/sidebar/global_config/global_config.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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { Fragment, FC } from 'react'; +import { EuiTabbedContent, EuiTitle, EuiSpacer } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { GeneralConfig } from './general_config'; +import { FilterConfig } from './filter_config'; + +const strings = { + getTitle: () => + i18n.translate('xpack.canvas.globalConfig.title', { + defaultMessage: 'Workpad settings', + }), + getGeneralLabel: () => + i18n.translate('xpack.canvas.globalConfig.general', { + defaultMessage: 'General', + }), + getFilterLabel: () => + i18n.translate('xpack.canvas.globalConfig.filter', { + defaultMessage: 'Filter', + }), +}; + +export const GlobalConfig: FC = () => { + const tabs = [ + { + id: 'general', + name: strings.getGeneralLabel(), + content: ( +
+ + +
+ ), + }, + { + id: 'filter', + name: strings.getFilterLabel(), + content: ( +
+ +
+ ), + }, + ]; + + return ( + +
+ +

{strings.getTitle()}

+
+
+ +
+ ); +}; diff --git a/x-pack/plugins/canvas/public/components/sidebar/global_config/index.tsx b/x-pack/plugins/canvas/public/components/sidebar/global_config/index.tsx new file mode 100644 index 0000000000000..f7fb95e5454c3 --- /dev/null +++ b/x-pack/plugins/canvas/public/components/sidebar/global_config/index.tsx @@ -0,0 +1,8 @@ +/* + * 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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export { GlobalConfig } from './global_config'; diff --git a/x-pack/plugins/canvas/public/components/sidebar/sidebar.scss b/x-pack/plugins/canvas/public/components/sidebar/sidebar.scss index 76d758197aa19..94c4ab6c19070 100644 --- a/x-pack/plugins/canvas/public/components/sidebar/sidebar.scss +++ b/x-pack/plugins/canvas/public/components/sidebar/sidebar.scss @@ -51,8 +51,12 @@ min-width: 0; } +.canvasSidebar__expandable { + width: 100%; +} + .canvasSidebar__expandable + .canvasSidebar__expandable { - margin-top: 0; + margin-top: 1px; .canvasSidebar__accordion:before { display: none; @@ -87,6 +91,9 @@ bottom: 0; } } +.canvasSidebar__accordion.filtersSidebar__accordion { + margin: auto; +} .canvasSidebar__accordionContent { padding-top: $euiSize; diff --git a/x-pack/plugins/canvas/public/components/workpad_config/workpad_config.component.tsx b/x-pack/plugins/canvas/public/components/workpad_config/workpad_config.component.tsx index 18e3f2dac9777..b69893c46fb9e 100644 --- a/x-pack/plugins/canvas/public/components/workpad_config/workpad_config.component.tsx +++ b/x-pack/plugins/canvas/public/components/workpad_config/workpad_config.component.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { FunctionComponent, useState } from 'react'; +import React, { FC, useState } from 'react'; import PropTypes from 'prop-types'; import { EuiFieldText, @@ -16,7 +16,6 @@ import { EuiFlexGroup, EuiFlexItem, EuiSpacer, - EuiTitle, EuiToolTip, EuiTextArea, EuiAccordion, @@ -76,10 +75,6 @@ const strings = { i18n.translate('xpack.canvas.workpadConfig.widthLabel', { defaultMessage: 'Width', }), - getTitle: () => - i18n.translate('xpack.canvas.workpadConfig.title', { - defaultMessage: 'Workpad settings', - }), getUSLetterButtonLabel: () => i18n.translate('xpack.canvas.workpadConfig.USLetterButtonLabel', { defaultMessage: 'US Letter', @@ -101,7 +96,7 @@ export interface Props { setWorkpadVariables: (vars: CanvasVariable[]) => void; } -export const WorkpadConfig: FunctionComponent = (props) => { +export const WorkpadConfig: FC = (props) => { const [css, setCSS] = useState(props.css); const { size, name, setSize, setName, setWorkpadCSS, variables, setWorkpadVariables } = props; const rotate = () => setSize({ width: size.height, height: size.width }); @@ -127,14 +122,6 @@ export const WorkpadConfig: FunctionComponent = (props) => { return (
-
- -

{strings.getTitle()}

-
-
- - - setName(e.target.value)} /> diff --git a/x-pack/plugins/canvas/public/components/workpad_filters/__stories__/__snapshots__/filter.component.stories.storyshot b/x-pack/plugins/canvas/public/components/workpad_filters/__stories__/__snapshots__/filter.component.stories.storyshot new file mode 100644 index 0000000000000..7e1deefdf249b --- /dev/null +++ b/x-pack/plugins/canvas/public/components/workpad_filters/__stories__/__snapshots__/filter.component.stories.storyshot @@ -0,0 +1,435 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Storyshots components/WorkpadFilters/FilterComponent default 1`] = ` +
+
+
+
+

+ Type +

+
+
+
+
+ exactly +
+
+
+
+

+ Column +

+
+
+
+
+ project +
+
+
+
+

+ Value +

+
+
+
+
+ kibana +
+
+
+
+

+ Filter group +

+
+
+
+
+ Group 1 +
+
+
+
+`; + +exports[`Storyshots components/WorkpadFilters/FilterComponent with component field 1`] = ` +
+
+
+
+

+ Type +

+
+
+
+
+ exactly +
+
+
+
+

+ Column +

+
+
+
+
+ project +
+
+
+
+

+ Value +

+
+
+
+
+
+ +

+ kibana +

+
+
+
+
+
+
+

+ Filter group +

+
+
+
+
+ Group 1 +
+
+
+
+`; + +exports[`Storyshots components/WorkpadFilters/FilterComponent with custom filter fields 1`] = ` +
+
+
+
+

+ Type +

+
+
+
+
+ exactly +
+
+
+
+

+ Column +

+
+
+
+
+ project +
+
+
+
+

+ Value +

+
+
+
+
+ kibana +
+
+
+
+

+ Filter group +

+
+
+
+
+ Group 1 +
+
+
+
+

+ Custom Field +

+
+
+
+
+ Some unknown field +
+
+
+
+`; diff --git a/x-pack/plugins/canvas/public/components/workpad_filters/__stories__/__snapshots__/filters_group.component.stories.storyshot b/x-pack/plugins/canvas/public/components/workpad_filters/__stories__/__snapshots__/filters_group.component.stories.storyshot new file mode 100644 index 0000000000000..d30fca5dc199c --- /dev/null +++ b/x-pack/plugins/canvas/public/components/workpad_filters/__stories__/__snapshots__/filters_group.component.stories.storyshot @@ -0,0 +1,127 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Storyshots components/WorkpadFilters/FiltersGroupComponent default 1`] = ` +
+
+
+
+ + +
+
+
+
+
+`; + +exports[`Storyshots components/WorkpadFilters/FiltersGroupComponent empty group 1`] = ` +
+
+
+
+ + +
+
+
+
+
+`; diff --git a/x-pack/plugins/canvas/public/components/workpad_filters/__stories__/__snapshots__/workpad_filters.component.stories.storyshot b/x-pack/plugins/canvas/public/components/workpad_filters/__stories__/__snapshots__/workpad_filters.component.stories.storyshot new file mode 100644 index 0000000000000..31e473f527e06 --- /dev/null +++ b/x-pack/plugins/canvas/public/components/workpad_filters/__stories__/__snapshots__/workpad_filters.component.stories.storyshot @@ -0,0 +1,706 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Storyshots components/WorkpadFilters/WorkpadFiltersComponent Empty filters groups 1`] = ` +
+
+
+
+
+
+
+
+ Group by +
+
+
+
+
+
+ +
+ + +
+
+
+
+
+
+
+
+
+`; + +exports[`Storyshots components/WorkpadFilters/WorkpadFiltersComponent Filters groups without group name 1`] = ` +
+
+
+
+
+
+
+
+ Group by +
+
+
+
+
+
+ +
+ + +
+
+
+
+
+
+
+
+
+ + +
+
+
+
+
+
+
+`; + +exports[`Storyshots components/WorkpadFilters/WorkpadFiltersComponent Filters groups without name 1`] = ` +
+
+
+
+
+
+
+
+ Group by +
+
+
+
+
+
+ +
+ + +
+
+
+
+
+
+
+
+
+ + +
+
+
+
+
+
+
+`; + +exports[`Storyshots components/WorkpadFilters/WorkpadFiltersComponent Filters groups without name and filters 1`] = ` +
+
+
+
+
+
+
+
+ Group by +
+
+
+
+
+
+ +
+ + +
+
+
+
+
+
+
+
+
+ + +
+
+
+
+
+
+
+`; + +exports[`Storyshots components/WorkpadFilters/WorkpadFiltersComponent default 1`] = ` +
+
+
+
+
+
+
+
+ Group by +
+
+
+
+
+
+ +
+ + +
+
+
+
+
+
+
+
+
+ + +
+
+
+
+
+
+
+ + +
+
+
+
+
+
+
+`; diff --git a/x-pack/plugins/canvas/public/components/workpad_filters/__stories__/elements.ts b/x-pack/plugins/canvas/public/components/workpad_filters/__stories__/elements.ts new file mode 100644 index 0000000000000..9eacfb54a411f --- /dev/null +++ b/x-pack/plugins/canvas/public/components/workpad_filters/__stories__/elements.ts @@ -0,0 +1,66 @@ +/* + * 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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import moment from 'moment'; +import { CanvasElement } from '../../../../types'; + +const timeFormat = 'MM.dd.YYYY HH:mm'; + +const generatePosition = (n: number): CanvasElement['position'] => ({ + left: n, + top: n, + width: n, + height: n, + angle: n, + parent: null, +}); + +const time1 = { + from: moment('1.01.2021 8:15', timeFormat).format(), + to: moment('2.01.2021 17:22', timeFormat).format(), +}; +const group1 = 'Group 1'; + +const time2 = { + from: moment('1.10.2021 12:20', timeFormat).format(), + to: moment('2.10.2021 12:33', timeFormat).format(), +}; +const group2 = 'Group 2'; + +const element1: CanvasElement = { + id: '1', + position: generatePosition(1), + type: 'element', + expression: '', + filter: `timefilter column="@timestamp" from="${time1.from}" to="${time1.to}" filterGroup="${group1}"`, +}; + +const element2: CanvasElement = { + id: '2', + position: generatePosition(2), + type: 'element', + expression: '', + filter: `exactly value="machine-learning" column="project1" filterGroup="${group1}"`, +}; + +const element3: CanvasElement = { + id: '3', + position: generatePosition(3), + type: 'element', + expression: '', + filter: `timefilter column="@timestamp" from="${time2.from}" to="${time2.to}"`, +}; + +const element4: CanvasElement = { + id: '4', + position: generatePosition(4), + type: 'element', + expression: '', + filter: `exactly value="kibana" column="project2" filterGroup="${group2}"`, +}; + +export const elements = [element1, element2, element3, element4]; diff --git a/x-pack/plugins/canvas/public/components/workpad_filters/__stories__/filter.component.stories.tsx b/x-pack/plugins/canvas/public/components/workpad_filters/__stories__/filter.component.stories.tsx new file mode 100644 index 0000000000000..2f80ffed5abf9 --- /dev/null +++ b/x-pack/plugins/canvas/public/components/workpad_filters/__stories__/filter.component.stories.tsx @@ -0,0 +1,53 @@ +/* + * 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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { EuiText, EuiTextColor } from '@elastic/eui'; +import { storiesOf } from '@storybook/react'; +import React, { FC } from 'react'; +import { FormattedFilterViewInstance } from '../../../../types'; +import { Filter } from '../filter.component'; + +const filter: FormattedFilterViewInstance = { + type: { + label: 'Type', + formattedValue: 'exactly', + }, + column: { + label: 'Column', + formattedValue: 'project', + }, + value: { + label: 'Value', + formattedValue: 'kibana', + }, + filterGroup: { + label: 'Filter group', + formattedValue: 'Group 1', + }, +}; + +const component: FC = ({ value }) => ( + + +

{value}

+
+
+); + +storiesOf('components/WorkpadFilters/FilterComponent', module) + .add('default', () => ) + .add('with component field', () => ( + + )) + .add('with custom filter fields', () => ( + + )); diff --git a/x-pack/plugins/canvas/public/components/workpad_filters/__stories__/filters_group.component.stories.tsx b/x-pack/plugins/canvas/public/components/workpad_filters/__stories__/filters_group.component.stories.tsx new file mode 100644 index 0000000000000..bdeb963dc8832 --- /dev/null +++ b/x-pack/plugins/canvas/public/components/workpad_filters/__stories__/filters_group.component.stories.tsx @@ -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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { storiesOf } from '@storybook/react'; +import React from 'react'; +import moment from 'moment'; +import { FiltersGroup } from '../filters_group.component'; +import { FiltersGroup as FiltersGroupType } from '../types'; + +const timeFormat = 'MM.dd.YYYY HH:mm'; + +const filtersGroup: FiltersGroupType = { + name: 'Group 1', + filters: [ + { type: 'exactly', column: 'project', value: 'kibana', filterGroup: 'Group 1' }, + { + type: 'time', + column: '@timestamp', + value: { + from: moment('1.01.2021 8:15', timeFormat).format(), + to: moment('2.01.2021 17:22', timeFormat).format(), + }, + filterGroup: 'Group 1', + }, + { type: 'exactly', column: 'country', value: 'US', filterGroup: 'Group 1' }, + { + type: 'time', + column: 'time', + value: { + from: moment('05.21.2021 10:50', timeFormat).format(), + to: moment('05.22.2021 4:40', timeFormat).format(), + }, + filterGroup: 'Group 1', + }, + ], +}; + +storiesOf('components/WorkpadFilters/FiltersGroupComponent', module) + .addDecorator((story) =>
{story()}
) + .add('default', () => ) + .add('empty group', () => ( + + )); diff --git a/x-pack/plugins/canvas/public/components/workpad_filters/__stories__/workpad_filters.component.stories.tsx b/x-pack/plugins/canvas/public/components/workpad_filters/__stories__/workpad_filters.component.stories.tsx new file mode 100644 index 0000000000000..8dc062886a12e --- /dev/null +++ b/x-pack/plugins/canvas/public/components/workpad_filters/__stories__/workpad_filters.component.stories.tsx @@ -0,0 +1,95 @@ +/* + * 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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { storiesOf } from '@storybook/react'; +import { action } from '@storybook/addon-actions'; +import React from 'react'; +import moment from 'moment'; +import { WorkpadFilters } from '../workpad_filters.component'; +import { FiltersGroup as FiltersGroupType } from '../types'; +import { Filter } from '../../../../types'; + +const timeFormat = 'MM.dd.YYYY HH:mm'; + +const filters: Filter[] = [ + { type: 'exactly', column: 'project', value: 'kibana', filterGroup: 'Group 1' }, + { + type: 'time', + column: '@timestamp', + value: { + from: moment('1.01.2021 8:15', timeFormat).format(), + to: moment('2.01.2021 17:22', timeFormat).format(), + }, + filterGroup: 'Group 1', + }, + { type: 'exactly', column: 'country', value: 'US', filterGroup: 'Group 2' }, + { + type: 'time', + column: 'time', + value: { + from: moment('05.21.2021 10:50', timeFormat).format(), + to: moment('05.22.2021 4:40', timeFormat).format(), + }, + filterGroup: 'Group 2', + }, +]; + +const filtersGroups: FiltersGroupType[] = [ + { + name: filters[0].filterGroup, + filters: [filters[0], filters[1]], + }, + { + name: filters[2].filterGroup, + filters: [filters[2], filters[3]], + }, +]; + +storiesOf('components/WorkpadFilters/WorkpadFiltersComponent', module) + .addDecorator((story) => ( +
+
+
{story()}
+
+
+ )) + .add('default', () => ( + + )) + .add('Filters groups without name', () => ( + ((acc, group) => [...acc, ...group.filters], []), + }, + ]} + groupFiltersByField={'column'} + onGroupByChange={action('onGroupByChange')} + /> + )) + .add('Filters groups without group name', () => ( + ((acc, group) => [...acc, ...group.filters], []), + }, + ]} + groupFiltersByField={'filterGroup'} + onGroupByChange={action('onGroupByChange')} + /> + )) + .add('Filters groups without name and filters', () => ( + + )) + .add('Empty filters groups', () => ( + + )); diff --git a/x-pack/plugins/canvas/public/components/workpad_filters/__stories__/workpad_filters.stories.tsx b/x-pack/plugins/canvas/public/components/workpad_filters/__stories__/workpad_filters.stories.tsx new file mode 100644 index 0000000000000..b97043bf83304 --- /dev/null +++ b/x-pack/plugins/canvas/public/components/workpad_filters/__stories__/workpad_filters.stories.tsx @@ -0,0 +1,23 @@ +/* + * 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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { storiesOf } from '@storybook/react'; +import React from 'react'; +import { reduxDecorator } from '../../../../storybook'; +import { WorkpadFilters } from '../workpad_filters'; +import { elements } from './elements'; + +storiesOf('components/WorkpadFilters/WorkpadFilters', module) + .addDecorator((story) => ( +
+
+
{story()}
+
+
+ )) + .addDecorator(reduxDecorator({ elements })) + .add('redux: default', () => ); diff --git a/x-pack/plugins/canvas/public/components/workpad_filters/filter.component.tsx b/x-pack/plugins/canvas/public/components/workpad_filters/filter.component.tsx new file mode 100644 index 0000000000000..bec6bec090d62 --- /dev/null +++ b/x-pack/plugins/canvas/public/components/workpad_filters/filter.component.tsx @@ -0,0 +1,64 @@ +/* + * 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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { FC } from 'react'; +import { EuiDescriptionList, EuiPanel, EuiText } from '@elastic/eui'; +import { FormattedFilterViewInstance } from '../../../types'; + +interface Props { + filter: FormattedFilterViewInstance; + updateFilter?: (value: any) => void; +} + +type CustomComponentProps = Omit & { value: string }; + +const titleStyle = { + width: '30%', +}; + +const descriptionStyle = { + width: '70%', +}; + +const renderElement = ( + Component: FC< + Omit & { onChange?: CustomComponentProps['updateFilter'] } + >, + { updateFilter, ...props }: CustomComponentProps +) => { + return ; +}; + +export const Filter: FC = ({ filter, ...restProps }) => { + const filterView = Object.values(filter).map((filterValue) => { + const description = filterValue.component + ? renderElement(filterValue.component, { value: filterValue.formattedValue, ...restProps }) + : filterValue.formattedValue; + + return { + title: ( + +

{filterValue.label}

+
+ ), + description: {description}, + }; + }); + + return ( + + + + ); +}; diff --git a/x-pack/plugins/canvas/public/components/workpad_filters/filter_views/default_filter.ts b/x-pack/plugins/canvas/public/components/workpad_filters/filter_views/default_filter.ts new file mode 100644 index 0000000000000..b2686fb660535 --- /dev/null +++ b/x-pack/plugins/canvas/public/components/workpad_filters/filter_views/default_filter.ts @@ -0,0 +1,39 @@ +/* + * 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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; +import { FilterViewSpec } from '../../../../types'; +import { formatByKey } from '../utils'; + +const strings = { + getTypeLabel: () => + i18n.translate('xpack.canvas.workpadFilters.defaultFilter.type', { + defaultMessage: 'Type', + }), + getColumnLabel: () => + i18n.translate('xpack.canvas.workpadFilters.defaultFilter.column', { + defaultMessage: 'Column', + }), + getFilterGroupLabel: () => + i18n.translate('xpack.canvas.workpadFilters.defaultFilter.filterGroup', { + defaultMessage: 'Filter group', + }), + getValueLabel: () => + i18n.translate('xpack.canvas.workpadFilters.defaultFilter.value', { + defaultMessage: 'Value', + }), +}; + +export const defaultFilter: FilterViewSpec = { + name: 'default', + view: { + column: { label: strings.getColumnLabel() }, + value: { label: strings.getValueLabel() }, + type: { label: strings.getTypeLabel(), formatter: formatByKey('type') }, + filterGroup: { label: strings.getFilterGroupLabel() }, + }, +}; diff --git a/x-pack/plugins/canvas/public/components/workpad_filters/filter_views/index.ts b/x-pack/plugins/canvas/public/components/workpad_filters/filter_views/index.ts new file mode 100644 index 0000000000000..98fad36c0015a --- /dev/null +++ b/x-pack/plugins/canvas/public/components/workpad_filters/filter_views/index.ts @@ -0,0 +1,15 @@ +/* + * 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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FilterViewSpec } from '../../../../types'; +import { defaultFilter } from './default_filter'; +import { timeFilter } from './time_filter'; + +export const filterViews: Record> = { + [defaultFilter.name]: defaultFilter, + [timeFilter.name]: timeFilter, +}; diff --git a/x-pack/plugins/canvas/public/components/workpad_filters/filter_views/time_filter.ts b/x-pack/plugins/canvas/public/components/workpad_filters/filter_views/time_filter.ts new file mode 100644 index 0000000000000..1dc02f61d05f7 --- /dev/null +++ b/x-pack/plugins/canvas/public/components/workpad_filters/filter_views/time_filter.ts @@ -0,0 +1,68 @@ +/* + * 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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import dateMath from '@elastic/datemath'; +import { i18n } from '@kbn/i18n'; +import { FilterType, FilterViewSpec, SimpleFilterViewField } from '../../../../types'; +import { formatByKey } from '../utils'; +import { defaultFilter } from './default_filter'; + +export interface TimeFilterValue { + to: string; + from: string; +} + +const strings = { + getFromLabel: () => + i18n.translate('xpack.canvas.workpadFilters.timeFilter.from', { + defaultMessage: 'From', + }), + getToLabel: () => + i18n.translate('xpack.canvas.workpadFilters.timeFilter.to', { + defaultMessage: 'To', + }), + getInvalidDateLabel: (date: string) => + i18n.translate('xpack.canvas.workpadFilters.timeFilter.invalidDate', { + defaultMessage: 'Invalid date: {date}', + values: { + date, + }, + }), +}; + +const { column, type, filterGroup } = defaultFilter.view; + +const formatTime = (str: string, roundUp: boolean) => { + const moment = dateMath.parse(str, { roundUp }); + if (!moment || !moment.isValid()) { + return strings.getInvalidDateLabel(str); + } + + return moment.format('YYYY-MM-DD HH:mm:ss'); +}; + +export const timeFilter: FilterViewSpec = { + name: FilterType.time, + view: { + column, + value: ({ to, from }) => ({ + from: { + label: strings.getFromLabel(), + formatter: () => formatTime(from, false), + }, + to: { + label: strings.getToLabel(), + formatter: () => formatTime(to, true), + }, + }), + type: { + label: (type as SimpleFilterViewField).label, + formatter: formatByKey('type'), + }, + filterGroup, + }, +}; diff --git a/x-pack/plugins/canvas/public/components/workpad_filters/filters_group.component.tsx b/x-pack/plugins/canvas/public/components/workpad_filters/filters_group.component.tsx new file mode 100644 index 0000000000000..8ceb60fe7866f --- /dev/null +++ b/x-pack/plugins/canvas/public/components/workpad_filters/filters_group.component.tsx @@ -0,0 +1,50 @@ +/* + * 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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { EuiAccordion } from '@elastic/eui'; +import React, { FC } from 'react'; +import { FormattedFilterViewInstance } from '../../../types'; +import { createFilledFilterView } from '../../lib/filter'; +import { Filter } from './filter.component'; +import { filterViews } from './filter_views'; +import { FiltersGroup as FiltersGroupType } from './types'; + +interface Props { + filtersGroup: FiltersGroupType; + id: string | number; +} + +const panelStyle = { + paddingTop: '15px', +}; + +export const FiltersGroup: FC = ({ filtersGroup, id }) => { + const { name, filters: groupFilters } = filtersGroup; + + const filledFilterViews: FormattedFilterViewInstance[] = groupFilters.map((filter) => { + const filterView = filterViews[filter.type] ?? filterViews.default; + return createFilledFilterView(filterView.view, filter); + }); + + const filtersComponents = filledFilterViews.map((filter, index) => ( + + )); + + return ( +
+ +
{filtersComponents}
+
+
+ ); +}; diff --git a/x-pack/plugins/canvas/public/components/workpad_filters/hooks/index.ts b/x-pack/plugins/canvas/public/components/workpad_filters/hooks/index.ts new file mode 100644 index 0000000000000..62f2a28130bfa --- /dev/null +++ b/x-pack/plugins/canvas/public/components/workpad_filters/hooks/index.ts @@ -0,0 +1,8 @@ +/* + * 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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export { useCanvasFilters } from './use_canvas_filters'; diff --git a/x-pack/plugins/canvas/public/components/workpad_filters/hooks/use_canvas_filters.ts b/x-pack/plugins/canvas/public/components/workpad_filters/hooks/use_canvas_filters.ts new file mode 100644 index 0000000000000..ce8e90def5aad --- /dev/null +++ b/x-pack/plugins/canvas/public/components/workpad_filters/hooks/use_canvas_filters.ts @@ -0,0 +1,23 @@ +/* + * 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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { fromExpression } from '@kbn/interpreter/common'; +import { shallowEqual, useSelector } from 'react-redux'; +import { State } from '../../../../types'; +import { adaptCanvasFilter } from '../../../lib/filter_adapters'; +import { getGlobalFilters } from '../../../state/selectors/workpad'; + +const extractExpressionAST = (filtersExpressions: string[]) => + fromExpression(filtersExpressions.join(' | ')); + +export function useCanvasFilters() { + const filterExpressions = useSelector((state: State) => getGlobalFilters(state), shallowEqual); + const expression = extractExpressionAST(filterExpressions); + const filters = expression.chain.map(adaptCanvasFilter); + + return filters; +} diff --git a/x-pack/plugins/canvas/public/components/workpad_filters/index.tsx b/x-pack/plugins/canvas/public/components/workpad_filters/index.tsx new file mode 100644 index 0000000000000..684d5e09c18fc --- /dev/null +++ b/x-pack/plugins/canvas/public/components/workpad_filters/index.tsx @@ -0,0 +1,9 @@ +/* + * 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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export { WorkpadFilters } from './workpad_filters'; +export { WorkpadFilters as WorkpadFiltersComponent } from './workpad_filters.component'; diff --git a/x-pack/plugins/canvas/public/components/workpad_filters/types.ts b/x-pack/plugins/canvas/public/components/workpad_filters/types.ts new file mode 100644 index 0000000000000..4733c18da9be5 --- /dev/null +++ b/x-pack/plugins/canvas/public/components/workpad_filters/types.ts @@ -0,0 +1,13 @@ +/* + * 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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { Filter } from '../../../types'; + +export interface FiltersGroup { + name: string | null; + filters: Filter[]; +} diff --git a/x-pack/plugins/canvas/public/components/workpad_filters/utils.ts b/x-pack/plugins/canvas/public/components/workpad_filters/utils.ts new file mode 100644 index 0000000000000..cc5836112db8a --- /dev/null +++ b/x-pack/plugins/canvas/public/components/workpad_filters/utils.ts @@ -0,0 +1,42 @@ +/* + * 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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; +import { FilterField } from '../../../types'; + +const strings = { + getBlankLabel: () => + i18n.translate('xpack.canvas.workpadFilters.filter.blankTypeLabel', { + defaultMessage: '(Blank)', + }), + getExactlyFilterTypeLabel: () => + i18n.translate('xpack.canvas.workpadFilters.defaultFilter.typeLabel', { + defaultMessage: 'Dropdown', + }), + getTimeFilterTypeLabel: () => + i18n.translate('xpack.canvas.workpadFilters.timeFilter.typeLabel', { + defaultMessage: 'Time', + }), + getWithoutGroupLabel: () => + i18n.translate('xpack.canvas.workpadFilters.filters_group.withoutGroup', { + defaultMessage: 'Without group', + }), +}; + +const formatType = (type: unknown) => { + const types: Record = { + exactly: strings.getExactlyFilterTypeLabel(), + time: strings.getTimeFilterTypeLabel(), + }; + return typeof type === 'string' ? types[type] ?? type : null; +}; + +const formatters: Partial string | null>> = { + type: formatType, +}; + +export const formatByKey = (key: FilterField) => formatters[key]; diff --git a/x-pack/plugins/canvas/public/components/workpad_filters/workpad_filters.component.tsx b/x-pack/plugins/canvas/public/components/workpad_filters/workpad_filters.component.tsx new file mode 100644 index 0000000000000..e3504f906fb3a --- /dev/null +++ b/x-pack/plugins/canvas/public/components/workpad_filters/workpad_filters.component.tsx @@ -0,0 +1,98 @@ +/* + * 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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { FC, Fragment } from 'react'; +import { EuiFlexGroup, EuiFlexItem, EuiSelect, EuiText } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { identity } from 'lodash'; +import { FiltersGroup as FiltersGroupType } from './types'; +import { FiltersGroup } from './filters_group.component'; +import { FilterField } from '../../../types'; +import { formatByKey } from './utils'; + +interface Props { + filtersGroups: FiltersGroupType[]; + groupFiltersByField?: FilterField; + onGroupByChange: (groupBy: FilterField) => void; +} + +const strings = { + getGroupBySelectLabel: () => + i18n.translate('xpack.canvas.workpadFilters.groupBySelect', { + defaultMessage: 'Group by', + }), + getGroupByFilterGroupLabel: () => + i18n.translate('xpack.canvas.workpadFilters.groupByFilterGroup', { + defaultMessage: 'Filter group', + }), + getGroupByFilterTypeLabel: () => + i18n.translate('xpack.canvas.workpadFilters.groupByFilterType', { + defaultMessage: 'Filter type', + }), + getGroupByColumnLabel: () => + i18n.translate('xpack.canvas.workpadFilters.groupByColumn', { + defaultMessage: 'Column', + }), + getWithoutGroupLabel: () => + i18n.translate('xpack.canvas.workpadFilters.filters_group.withoutGroup', { + defaultMessage: 'Without group', + }), + getBlankValueLabel: () => + i18n.translate('xpack.canvas.workpadFilters.filters_group.blankValue', { + defaultMessage: '(Blank)', + }), +}; + +const groupByOptions: Array<{ value: FilterField; text: string }> = [ + { value: 'filterGroup', text: strings.getGroupByFilterGroupLabel() }, + { value: 'type', text: strings.getGroupByFilterTypeLabel() }, + { value: 'column', text: strings.getGroupByColumnLabel() }, +]; + +export const WorkpadFilters: FC = ({ + filtersGroups, + onGroupByChange, + groupFiltersByField, +}) => { + const groupedByFilterGroupField = groupFiltersByField === 'filterGroup'; + const formatter = groupFiltersByField ? formatByKey(groupFiltersByField) ?? identity : identity; + + const preparedFilterGroups = filtersGroups.map((filterGroup) => ({ + ...filterGroup, + name: + formatter(filterGroup.name) ?? + (groupedByFilterGroupField ? strings.getWithoutGroupLabel() : strings.getBlankValueLabel()), + })); + + const filtersGroupsComponents = preparedFilterGroups.map((filtersGroup, index) => { + return ; + }); + + return ( + +
+ + + +
{strings.getGroupBySelectLabel()}
+
+
+ + onGroupByChange(e.target.value as FilterField)} + aria-label="Use aria labels when no actual label is in use" + /> + +
+
+ {filtersGroupsComponents} +
+ ); +}; diff --git a/x-pack/plugins/canvas/public/components/workpad_filters/workpad_filters.tsx b/x-pack/plugins/canvas/public/components/workpad_filters/workpad_filters.tsx new file mode 100644 index 0000000000000..c04fe543804b4 --- /dev/null +++ b/x-pack/plugins/canvas/public/components/workpad_filters/workpad_filters.tsx @@ -0,0 +1,44 @@ +/* + * 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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { FC, useCallback } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import { State, FilterField } from '../../../types'; +import { groupFiltersBy } from '../../lib/filter'; +import { setGroupFiltersByOption } from '../../state/actions/sidebar'; +import { getGroupFiltersByOption } from '../../state/selectors/sidebar'; +import { useCanvasFilters } from './hooks'; +import { WorkpadFilters as Component } from './workpad_filters.component'; + +export const WorkpadFilters: FC = () => { + const groupFiltersByField: FilterField = useSelector((state: State) => + getGroupFiltersByOption(state) + ); + + const dispatch = useDispatch(); + + const onGroupByChange = useCallback( + (groupByOption: FilterField) => { + dispatch(setGroupFiltersByOption(groupByOption)); + }, + [dispatch] + ); + + const canvasFilters = useCanvasFilters(); + + const filtersGroups = groupFiltersByField + ? groupFiltersBy(canvasFilters, groupFiltersByField) + : []; + + return ( + + ); +}; diff --git a/x-pack/plugins/canvas/public/lib/filter.test.ts b/x-pack/plugins/canvas/public/lib/filter.test.ts new file mode 100644 index 0000000000000..497f75b91650f --- /dev/null +++ b/x-pack/plugins/canvas/public/lib/filter.test.ts @@ -0,0 +1,282 @@ +/* + * 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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FC } from 'react'; +import { + Filter as FilterType, + FilterViewInstance, + FlattenFilterViewInstance, + SimpleFilterViewField, +} from '../../types'; +import { + defaultFormatter, + formatFilterView, + flattenFilterView, + createFilledFilterView, + groupFiltersBy, +} from './filter'; + +const formatterFactory = (value: unknown) => () => JSON.stringify(value); +const fc: FC = () => null; + +const simpleFilterValue: FilterType = { + type: 'exactly', + column: 'project', + value: 'kibana', + filterGroup: 'someGroup', +}; + +const filterWithNestedValue: FilterType = { + type: 'exactlyNested' as any, + column: 'project', + value: { nestedField1: 'nestedField1', nestedField2: 'nestedField2' }, + filterGroup: 'someGroup', +}; + +const simpleFilterView: FilterViewInstance = { + type: { label: 'label' }, + column: { label: 'column' }, + value: { label: 'value' }, + filterGroup: { label: 'filterGroup' }, +}; + +const nestedFilterView: FilterViewInstance = { + type: { label: 'label' }, + column: { label: 'column' }, + value: (value: unknown) => ({ + nested: { + label: 'nested', + formatter: formatterFactory(value), + }, + }), + filterGroup: { label: 'filterGroup' }, +}; + +describe('defaultFormatter', () => { + it('returns string when passed not null/undefined/falsy/emtpy value', () => { + expect(defaultFormatter(10)).toBe('10'); + expect(defaultFormatter('10')).toBe('10'); + const objToFormat = { field: 10 }; + expect(defaultFormatter(objToFormat)).toBe(objToFormat.toString()); + const arrToFormat = [10, 20]; + expect(defaultFormatter(arrToFormat)).toBe(arrToFormat.toString()); + }); + + it("returns '-' when passed null/undefined/falsy/emtpy value", () => { + const empty = '-'; + expect(defaultFormatter(null)).toBe(empty); + expect(defaultFormatter(undefined)).toBe(empty); + expect(defaultFormatter('')).toBe(empty); + expect(defaultFormatter(false)).toBe(empty); + }); +}); + +describe('flattenFilterView returns fn which', () => { + it('returns the same filter view if it expects all fiends to be simple values', () => { + const flattenFn = flattenFilterView(simpleFilterValue); + expect(flattenFn(simpleFilterView)).toEqual(simpleFilterView); + }); + + it('returns the same filter view if filterValue is empty object', () => { + const flattenFn = flattenFilterView({} as any); + expect(flattenFn(simpleFilterView)).toEqual(simpleFilterView); + }); + + it('returns empty filter view if filter view is empty object', () => { + const flattenFn = flattenFilterView(simpleFilterValue); + expect(flattenFn({} as any)).toEqual({}); + }); + + it('returns single nesting filter view if it expects some fields to be nested objects', () => { + const flattenFn = flattenFilterView(filterWithNestedValue); + const { value, ...restExpectedFields } = nestedFilterView; + const flattenFilterViewRes = flattenFn(nestedFilterView); + + expect(flattenFilterViewRes).toEqual({ + ...restExpectedFields, + nested: { + label: 'nested', + formatter: expect.any(Function), + }, + }); + expect(flattenFilterViewRes.nested.formatter?.()).toBe( + formatterFactory(filterWithNestedValue.value)() + ); + }); + + it('returns single nesting filter view if filterValue is empty object', () => { + const flattenFn = flattenFilterView({} as any); + const { value, ...rest } = nestedFilterView; + expect(flattenFn(nestedFilterView)).toEqual({ + ...rest, + nested: { + label: 'nested', + formatter: expect.any(Function), + }, + }); + }); +}); + +describe('formatFilterView returns fn which', () => { + const simpleFlattenFilterView: FlattenFilterViewInstance = { + type: { label: 'label' }, + value: { label: 'value' }, + column: { label: 'column' }, + filterGroup: { label: 'filterGroup' }, + nestedField: { label: 'nestedField', formatter: () => 'null' }, + }; + + it('returns formatted filter view with any passed keys', () => { + const formatFn = formatFilterView(simpleFilterValue); + expect(formatFn(simpleFlattenFilterView)).toEqual({ + type: { label: 'label', formattedValue: simpleFilterValue.type }, + value: { label: 'value', formattedValue: simpleFilterValue.value }, + column: { label: 'column', formattedValue: simpleFilterValue.column }, + filterGroup: { label: 'filterGroup', formattedValue: simpleFilterValue.filterGroup }, + nestedField: { label: 'nestedField', formattedValue: 'null' }, + }); + }); + + it("returns formatted filter view with formattedValue = '-' ", () => { + const formatFn = formatFilterView({} as any); + expect(formatFn(simpleFlattenFilterView)).toEqual({ + type: { label: 'label', formattedValue: '-' }, + value: { label: 'value', formattedValue: '-' }, + column: { label: 'column', formattedValue: '-' }, + filterGroup: { label: 'filterGroup', formattedValue: '-' }, + nestedField: { label: 'nestedField', formattedValue: 'null' }, + }); + }); + + it('returns emtpy object when filter view is empty object', () => { + const formatFn = formatFilterView(simpleFilterValue); + expect(formatFn({} as any)).toEqual({}); + }); + + it('returns filter view fields with component property if defined at filter view', () => { + const flattenFilterViewWithComponent: FlattenFilterViewInstance = { + ...simpleFlattenFilterView, + nestedField: { + ...simpleFlattenFilterView.nestedField, + component: fc, + }, + }; + + const formatFn = formatFilterView(simpleFilterValue); + expect(formatFn(flattenFilterViewWithComponent)).toEqual({ + type: { label: 'label', formattedValue: simpleFilterValue.type }, + value: { label: 'value', formattedValue: simpleFilterValue.value }, + column: { label: 'column', formattedValue: simpleFilterValue.column }, + filterGroup: { label: 'filterGroup', formattedValue: simpleFilterValue.filterGroup }, + nestedField: { label: 'nestedField', formattedValue: 'null', component: fc }, + }); + }); +}); + +describe('createFilledFilterView', () => { + it('returns simple filter view with formattedValue and components', () => { + const simpleFilterValueWithComponent = { + ...simpleFilterView, + value: { + ...(simpleFilterView.value as SimpleFilterViewField), + component: fc, + }, + }; + + expect(createFilledFilterView(simpleFilterValueWithComponent, simpleFilterValue)).toEqual({ + type: { label: 'label', formattedValue: simpleFilterValue.type }, + value: { label: 'value', formattedValue: simpleFilterValue.value, component: fc }, + column: { label: 'column', formattedValue: simpleFilterValue.column }, + filterGroup: { label: 'filterGroup', formattedValue: simpleFilterValue.filterGroup }, + }); + }); + + it('returns nested filter view with formattedValue and components', () => { + const nestedFilterViewWithComponent = { + ...nestedFilterView, + value: (value: unknown) => ({ + nested: { + label: 'nested', + formatter: formatterFactory(value), + component: fc, + }, + }), + }; + + expect(createFilledFilterView(nestedFilterViewWithComponent, filterWithNestedValue)).toEqual({ + type: { label: 'label', formattedValue: filterWithNestedValue.type }, + column: { label: 'column', formattedValue: filterWithNestedValue.column }, + filterGroup: { label: 'filterGroup', formattedValue: filterWithNestedValue.filterGroup }, + nested: { + label: 'nested', + formattedValue: formatterFactory(filterWithNestedValue.value)(), + component: fc, + }, + }); + }); +}); + +describe('groupFiltersBy', () => { + const filters: FilterType[] = [ + { type: 'exactly', column: 'project', value: 'kibana', filterGroup: 'someGroup' }, + { + type: 'time', + column: '@timestamp', + value: { from: 'some time', to: 'some time' }, + filterGroup: 'someGroup2', + }, + { type: 'exactly', column: 'country', value: 'US', filterGroup: 'someGroup2' }, + { + type: 'time', + column: 'time', + value: { from: 'some time', to: 'some time' }, + filterGroup: null, + }, + ]; + + it('groups by type', () => { + const grouped = groupFiltersBy(filters, 'type'); + expect(grouped).toEqual([ + { name: 'exactly', filters: [filters[0], filters[2]] }, + { name: 'time', filters: [filters[1], filters[3]] }, + ]); + }); + + it('groups by column', () => { + const grouped = groupFiltersBy(filters, 'column'); + expect(grouped).toEqual([ + { name: 'project', filters: [filters[0]] }, + { name: '@timestamp', filters: [filters[1]] }, + { name: 'country', filters: [filters[2]] }, + { name: 'time', filters: [filters[3]] }, + ]); + }); + + it('groups by filterGroup', () => { + const grouped = groupFiltersBy(filters, 'filterGroup'); + expect(grouped).toEqual([ + { name: 'someGroup', filters: [filters[0]] }, + { name: 'someGroup2', filters: [filters[1], filters[2]] }, + { name: null, filters: [filters[3]] }, + ]); + }); + + it('groups by field on empty array', () => { + const grouped = groupFiltersBy([], 'filterGroup'); + expect(grouped).toEqual([]); + }); + + it('groups by empty field', () => { + const filtersWithoutGroups = filters.map(({ filterGroup, ...rest }) => ({ + ...rest, + filterGroup: null, + })); + + const grouped = groupFiltersBy(filtersWithoutGroups, 'filterGroup'); + expect(grouped).toEqual([{ name: null, filters: filtersWithoutGroups }]); + }); +}); diff --git a/x-pack/plugins/canvas/public/lib/filter.ts b/x-pack/plugins/canvas/public/lib/filter.ts new file mode 100644 index 0000000000000..ae75822e4a7c9 --- /dev/null +++ b/x-pack/plugins/canvas/public/lib/filter.ts @@ -0,0 +1,55 @@ +/* + * 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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { flowRight, groupBy } from 'lodash'; +import { + Filter as FilterType, + FilterField, + FilterViewInstance, + FlattenFilterViewInstance, +} from '../../types/filters'; + +export const defaultFormatter = (value: unknown) => (value || null ? `${value}` : '-'); + +export const formatFilterView = + (filterValue: FilterType) => (filterView: FlattenFilterViewInstance) => { + const filterViewKeys = Object.keys(filterView) as Array; + return filterViewKeys.reduce( + (acc, key) => ({ + ...acc, + [key]: { + label: filterView[key].label, + formattedValue: (filterView[key].formatter ?? defaultFormatter)(filterValue[key]), + component: filterView[key].component, + }, + }), + {} + ); + }; + +export const flattenFilterView = (filterValue: FilterType) => (filterView: FilterViewInstance) => { + const filterViewKeys = Object.keys(filterView) as Array; + return filterViewKeys.reduce((acc, key) => { + const filterField = filterView[key]; + if (typeof filterField === 'function') { + const val = filterField(filterValue[key]); + return { ...acc, ...val }; + } + return { ...acc, [key]: filterField }; + }, {}); +}; + +export const createFilledFilterView = (filterView: FilterViewInstance, filter: FilterType) => + flowRight(formatFilterView(filter), flattenFilterView(filter))(filterView); + +export const groupFiltersBy = (filters: FilterType[], groupByField: FilterField) => { + const groupedFilters = groupBy(filters, (filter) => filter[groupByField]); + return Object.keys(groupedFilters).map((key) => ({ + name: groupedFilters[key]?.[0]?.[groupByField] ? key : null, + filters: groupedFilters[key], + })); +}; diff --git a/x-pack/plugins/canvas/public/lib/filter_adapters.test.ts b/x-pack/plugins/canvas/public/lib/filter_adapters.test.ts new file mode 100644 index 0000000000000..5061e47a44347 --- /dev/null +++ b/x-pack/plugins/canvas/public/lib/filter_adapters.test.ts @@ -0,0 +1,59 @@ +/* + * 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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { ExpressionFunctionAST } from '@kbn/interpreter/common'; +import { adaptCanvasFilter } from './filter_adapters'; + +describe('adaptCanvasFilter', () => { + const filterAST: ExpressionFunctionAST = { + type: 'function', + function: 'exactly', + arguments: { + type: ['exactly'], + column: ['project'], + filterGroup: [], + value: ['kibana'], + }, + }; + + it('returns filter when AST arguments consists of arrays with one element', () => { + const resultFilter = { type: 'exactly', column: 'project', filterGroup: null, value: 'kibana' }; + + const filter = adaptCanvasFilter(filterAST); + expect(filter).toEqual(resultFilter); + }); + + it('returns filter with all additional fields stored on value field', () => { + const { value, ...rest } = filterAST.arguments; + const additionalArguments = { value1: ['value1'], value2: ['value2'] }; + const newFilterAST = { ...filterAST, arguments: { ...rest, ...additionalArguments } }; + + const resultFilter = { + type: 'exactly', + column: 'project', + filterGroup: null, + value: { value1: 'value1', value2: 'value2' }, + }; + + const filter = adaptCanvasFilter(newFilterAST); + expect(filter).toEqual(resultFilter); + }); + + it('returns filter if args are empty', () => { + const { arguments: args, ...rest } = filterAST; + + const resultFilter = { + type: 'exactly', + column: null, + filterGroup: null, + value: null, + }; + + const filter = adaptCanvasFilter({ ...rest, arguments: {} }); + expect(filter).toEqual(resultFilter); + }); +}); diff --git a/x-pack/plugins/canvas/public/lib/filter_adapters.ts b/x-pack/plugins/canvas/public/lib/filter_adapters.ts new file mode 100644 index 0000000000000..478b0a5302631 --- /dev/null +++ b/x-pack/plugins/canvas/public/lib/filter_adapters.ts @@ -0,0 +1,48 @@ +/* + * 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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { ExpressionFunctionAST } from '@kbn/interpreter/common'; +import { identity } from 'lodash'; +import { ExpressionAstArgument, Filter, FilterType } from '../../types'; + +const functionToFilter: Record = { + timefilter: FilterType.time, + exactly: FilterType.exactly, +}; + +const defaultFormatter = (arg: ExpressionAstArgument) => arg.toString(); + +const argToValue = ( + arg: ExpressionAstArgument[], + formatter: (arg: ExpressionAstArgument) => string | null = defaultFormatter +) => (arg?.[0] ? formatter(arg[0]) : null); + +const convertFunctionToFilterType = (func: string) => functionToFilter[func] ?? FilterType.exactly; + +const collectArgs = (args: ExpressionFunctionAST['arguments']) => { + const argsKeys = Object.keys(args); + + if (!argsKeys.length) { + return null; + } + + return argsKeys.reduce>( + (acc, key) => ({ ...acc, [key]: argToValue(args[key], identity) }), + {} + ); +}; + +export function adaptCanvasFilter(filter: ExpressionFunctionAST): Filter { + const { function: type, arguments: args } = filter; + const { column, filterGroup, value: valueArg, type: typeArg, ...rest } = args ?? {}; + return { + type: convertFunctionToFilterType(type), + column: argToValue(column), + filterGroup: argToValue(filterGroup), + value: argToValue(valueArg) ?? collectArgs(rest), + }; +} diff --git a/x-pack/plugins/canvas/public/state/actions/sidebar.ts b/x-pack/plugins/canvas/public/state/actions/sidebar.ts new file mode 100644 index 0000000000000..309cb43fcd936 --- /dev/null +++ b/x-pack/plugins/canvas/public/state/actions/sidebar.ts @@ -0,0 +1,16 @@ +/* + * 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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { createAction } from 'redux-actions'; +import { FilterField } from '../../../types'; + +export type SetGroupFiltersByOptionPayload = FilterField; +export const SetGroupFiltersByOptionType = 'setGroupFiltersByOption'; + +export const setGroupFiltersByOption = createAction( + SetGroupFiltersByOptionType +); diff --git a/x-pack/plugins/canvas/public/state/defaults.js b/x-pack/plugins/canvas/public/state/defaults.js index a245d515a32d9..40e8425c98ff0 100644 --- a/x-pack/plugins/canvas/public/state/defaults.js +++ b/x-pack/plugins/canvas/public/state/defaults.js @@ -6,7 +6,7 @@ */ import { getId } from '../lib/get_id'; -import { DEFAULT_WORKPAD_CSS } from '../../common/lib/constants'; +import { DEFAULT_WORKPAD_CSS, DEFAULT_GROUP_BY_FIELD } from '../../common/lib/constants'; export const getDefaultElement = () => { return { @@ -86,3 +86,7 @@ export const getDefaultWorkpad = () => { isWriteable: true, }; }; + +export const getDefaultSidebar = () => ({ + groupFiltersByOption: DEFAULT_GROUP_BY_FIELD, +}); diff --git a/x-pack/plugins/canvas/public/state/initial_state.js b/x-pack/plugins/canvas/public/state/initial_state.js index c652cc573abe9..d676b27c7a906 100644 --- a/x-pack/plugins/canvas/public/state/initial_state.js +++ b/x-pack/plugins/canvas/public/state/initial_state.js @@ -7,7 +7,7 @@ import { get } from 'lodash'; import { pluginServices } from '../services'; -import { getDefaultWorkpad } from './defaults'; +import { getDefaultWorkpad, getDefaultSidebar } from './defaults'; export const getInitialState = (path) => { const platformService = pluginServices.getServices().platform; @@ -40,6 +40,7 @@ export const getInitialState = (path) => { // In there will live an object with a status (string), value (any), and error (Error) property. // If the state is 'error', the error property will be the error object, the value will not change // See the resolved_args reducer for more information. + sidebar: getDefaultSidebar(), }, persistent: { schemaVersion: 2, diff --git a/x-pack/plugins/canvas/public/state/reducers/index.js b/x-pack/plugins/canvas/public/state/reducers/index.js index 5a901fc3bb294..a07de70d9a005 100644 --- a/x-pack/plugins/canvas/public/state/reducers/index.js +++ b/x-pack/plugins/canvas/public/state/reducers/index.js @@ -18,12 +18,13 @@ import { elementsReducer } from './elements'; import { assetsReducer } from './assets'; import { historyReducer } from './history'; import { embeddableReducer } from './embeddable'; +import { sidebarReducer } from './sidebar'; export function getRootReducer(initialState) { return combineReducers({ assets: assetsReducer, app: appReducer, - transient: reduceReducers(transientReducer, resolvedArgsReducer), + transient: reduceReducers(transientReducer, resolvedArgsReducer, sidebarReducer), persistent: reduceReducers( historyReducer, combineReducers({ diff --git a/x-pack/plugins/canvas/public/state/reducers/sidebar.ts b/x-pack/plugins/canvas/public/state/reducers/sidebar.ts new file mode 100644 index 0000000000000..55697b17c09e0 --- /dev/null +++ b/x-pack/plugins/canvas/public/state/reducers/sidebar.ts @@ -0,0 +1,22 @@ +/* + * 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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { handleActions } from 'redux-actions'; +import { State } from '../../../types'; +import { SetGroupFiltersByOptionType, SetGroupFiltersByOptionPayload } from '../actions/sidebar'; + +export const sidebarReducer = handleActions( + { + [SetGroupFiltersByOptionType]: (transientState, { payload }) => { + return { + ...transientState, + sidebar: { ...transientState.sidebar, groupFiltersByOption: payload }, + }; + }, + }, + {} as State['transient'] +); diff --git a/x-pack/plugins/canvas/public/state/selectors/sidebar.ts b/x-pack/plugins/canvas/public/state/selectors/sidebar.ts new file mode 100644 index 0000000000000..637264a200b9a --- /dev/null +++ b/x-pack/plugins/canvas/public/state/selectors/sidebar.ts @@ -0,0 +1,13 @@ +/* + * 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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { DEFAULT_GROUP_BY_FIELD } from '../../../common/lib'; +import { FilterField, State } from '../../../types'; + +export const getGroupFiltersByOption = (state: State): FilterField => { + return state.transient.sidebar.groupFiltersByOption ?? DEFAULT_GROUP_BY_FIELD; +}; diff --git a/x-pack/plugins/canvas/types/canvas.ts b/x-pack/plugins/canvas/types/canvas.ts index 0868054d0a489..efb121b2948af 100644 --- a/x-pack/plugins/canvas/types/canvas.ts +++ b/x-pack/plugins/canvas/types/canvas.ts @@ -6,6 +6,7 @@ */ import { ElementPosition } from './elements'; +import { FilterField } from './filters'; export interface CanvasAsset { '@created': string; @@ -44,6 +45,10 @@ export interface CanvasVariable { type: 'boolean' | 'number' | 'string'; } +export interface Sidebar { + groupFiltersByOption?: FilterField; +} + export interface CanvasWorkpad { '@created': string; '@timestamp': string; diff --git a/x-pack/plugins/canvas/types/filters.ts b/x-pack/plugins/canvas/types/filters.ts index 942e4259d780e..8529b37e40b1b 100644 --- a/x-pack/plugins/canvas/types/filters.ts +++ b/x-pack/plugins/canvas/types/filters.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { FC } from 'react'; import { ExpressionValueFilter } from '.'; export enum FilterType { @@ -31,3 +32,41 @@ export type CanvasExactlyFilter = ExpressionValueFilter & { }; export type CanvasFilter = CanvasTimeFilter | CanvasExactlyFilter | CanvasLuceneFilter; + +export interface Filter { + type: keyof typeof FilterType; + column: string | null; + value: unknown; + filterGroup: string | null; +} + +export type ComplexFilterViewField = ( + value: FilterValue +) => Record; + +export interface SimpleFilterViewField { + label: string; + formatter?: (value?: unknown) => string | null; + component?: FC; +} + +export interface FormattedFilterViewField { + label: string; + formattedValue: string; + component?: FC; +} + +export type FilterViewInstance = Record< + keyof Filter, + SimpleFilterViewField | ComplexFilterViewField +>; + +export interface FilterViewSpec { + name: string; + view: FilterViewInstance; +} + +export type FlattenFilterViewInstance = Record; +export type FormattedFilterViewInstance = Record; + +export type FilterField = 'column' | 'type' | 'filterGroup'; diff --git a/x-pack/plugins/canvas/types/state.ts b/x-pack/plugins/canvas/types/state.ts index a3c770f12f225..b283de1386f55 100644 --- a/x-pack/plugins/canvas/types/state.ts +++ b/x-pack/plugins/canvas/types/state.ts @@ -18,7 +18,7 @@ import { } from 'src/plugins/expressions'; import { Datasource, Model, Transform, View } from '../public/expression_types'; import { AssetType } from './assets'; -import { CanvasWorkpad } from './canvas'; +import { CanvasWorkpad, Sidebar } from './canvas'; export enum AppStateKeys { FULLSCREEN = '__fullscreen', @@ -75,7 +75,7 @@ export interface ResolvedArgType { expressionContext: ExpressionContext; } -interface TransientState { +export interface TransientState { canUserWrite: boolean; zoomScale: number; elementStats: ElementStatsType; @@ -90,6 +90,7 @@ interface TransientState { interval: number; }; inFlight: boolean; + sidebar: Sidebar; } interface PersistentState { diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 1a5335ef93e72..af76ea0e98276 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -8115,7 +8115,6 @@ "xpack.canvas.workpadConfig.pageSizeBadgeOnClickAriaLabel": "ページサイズを {sizeName} に設定", "xpack.canvas.workpadConfig.swapDimensionsAriaLabel": "ページの幅と高さを入れ替えます", "xpack.canvas.workpadConfig.swapDimensionsTooltip": "ページの幅と高さを入れ替える", - "xpack.canvas.workpadConfig.title": "ワークパッドの設定", "xpack.canvas.workpadConfig.USLetterButtonLabel": "US レター", "xpack.canvas.workpadConfig.widthLabel": "幅", "xpack.canvas.workpadCreate.createButtonLabel": "ワークパッドを作成", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 529b692e3552e..91513909af9c7 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -8178,7 +8178,6 @@ "xpack.canvas.workpadConfig.pageSizeBadgeOnClickAriaLabel": "将页面大小设置为 {sizeName}", "xpack.canvas.workpadConfig.swapDimensionsAriaLabel": "交换页面的宽和高", "xpack.canvas.workpadConfig.swapDimensionsTooltip": "交换宽高", - "xpack.canvas.workpadConfig.title": "Workpad 设置", "xpack.canvas.workpadConfig.USLetterButtonLabel": "美国信函", "xpack.canvas.workpadConfig.widthLabel": "宽", "xpack.canvas.workpadCreate.createButtonLabel": "创建 Workpad",